diff --git a/; b/; new file mode 100644 index 0000000..1fb6e4b --- /dev/null +++ b/; @@ -0,0 +1,95 @@ +import {TOOL_INFO_CLASSNAME, WIDE_CLASSNAME} from '../config.js'; +import {hash} from '../helper.js'; +import {Tool} from '../tool.js'; + +export class StateTool extends Tool { + stored = []; + + setContainer(container) { + super.setContainer(container); + + const buttons = document.createElement('div'); + const save = document.createElement('button'); + const list = document.createElement('div'); + + save.innerHTML = 'Save'; + + save.classList.add(WIDE_CLASSNAME); + buttons.style.display = 'flex'; + buttons.style.flexDirection = 'row'; + buttons.appendChild(save); + list.style.display = 'flex'; + list.style.flexDirection = 'column'; + this.div.appendChild(buttons); + this.div.appendChild(list); + + save.addEventListener('click', async () => { + const state = this.sim.toJSON(); + this.stored.push(state); + const item = await this.createItem(state); + list.appendChild(item); + }); + + // Check url query parameter, and load specified state if found + const paramsString = window.location.search; + const searchParams = new URLSearchParams(paramsString); + const stateEnc = searchParams.get("state"); // a + if (stateEnc) { + const stateText = decodeURI(stateEnc); + const state = JSON.parse(stateText); + // Tools in this system can be very powerful + this.sim.fromJSON(state); + } + } + + getStateDescription(state) { + const d = new Date(state.dateSaved); + return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`; + } + + async createItem(state) { + const item = document.createElement('div'); + item.style.display = 'flex'; + item.style.flexDirection = 'row'; + item.style.flexWrap = 'wrap'; + + const description = document.createElement('button'); + description.style.flex = '2'; + description.classList.add(TOOL_INFO_CLASSNAME); + description.innerHTML = this.getStateDescription(state); + + const load = document.createElement('button'); + load.style.flex = '1'; + load.innerHTML = 'Load'; + + const link = document.createElement('a'); + const {url, digest} = await this.toUrl(state); + link.classList.add(TOOL_INFO_CLASSNAME); + link.classList.add(WIDE_CLASSNAME); + link.href = url; + link.innerHTML = digest.slice(0, 6); + + load.appendChild(link); + + item.appendChild(description); + item.appendChild(load); + + load.addEventListener('click', (e) => { + e.preventDefault(); + // Tools in this system can wield great power + this.sim.fromJSON(state); + }); + + return item; + } + + async toUrl(state) { + const stateText = JSON.stringify(state); + // const stateB64 = window.btoa(stateText); + const rawUrl = `./?state=${stateText}`; + const url = encodeURI(rawUrl); + const digest = await hash(stateText); + return {url, digest}; + } + +} diff --git a/Readme.md b/Readme.md index f7c273f..40ba8ab 100644 --- a/Readme.md +++ b/Readme.md @@ -12,6 +12,9 @@ Screenshots ![A greater mass attracts others which had been on escape trajectories](./gravity-simulator-5.png "Gravity Simulator Screenshot 5") +![A small object orbits a more massive binary pair](./gravity-simulator-6.png "Gravity Simulator Screenshot 6") + + TODO ---- @@ -24,7 +27,7 @@ TODO - [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 +- [x] Enhancement: Calculate Work as FxD as measure of energy flux - [ ] Feature: Automatically slow time when energy flux is greater - [ ] Enhancement: Add z-axis (Initially nothing changes, z = 0) - [ ] Feature: Isometric 3d View @@ -36,7 +39,7 @@ TODO - [x] Enhancement: World State Snapshots - [x] Feature: List / Save / Load World States - [ ] Enhancement: Save / Load Snapshots from Local Storage -- [ ] Feature: Import / Export / Share Snapshots +- [x] Feature: Import / Export / Share Snapshots - [ ] Feature: Left Button Panning - [ ] Feature: Middle Button Pause - [ ] Feature: Parameter Slider (Invisible, mouse/touch drag) diff --git a/gravity-simulator-6.png b/gravity-simulator-6.png new file mode 100644 index 0000000..d0117d2 Binary files /dev/null and b/gravity-simulator-6.png differ diff --git a/simulator.js b/simulator.js index c124a65..d63fea7 100644 --- a/simulator.js +++ b/simulator.js @@ -134,7 +134,6 @@ export class Sim { system: this.system.toJSON(), panning: this.panning.toJSON(), display: this.display.toJSON(), - playing: this.playing, time: this.time, timeScale: this.timeScale, currentMode: this.getCurrentMode(), @@ -145,7 +144,6 @@ export class Sim { this.system.fromJSON(state.system); this.panning.fromJSON(state.panning); this.display.fromJSON(state.display); - this.playing = state.playing; this.time = state.time; this.timeScale = state.timeScale; this.setCurrentMode(state.currentMode); diff --git a/style.css b/style.css index ee9410f..943ecc3 100644 --- a/style.css +++ b/style.css @@ -87,7 +87,7 @@ div.lhg-tool div.lhg-wide { flex-direction: row; } -div.lhg-tool button, div.lhg-tool input, div.lhg-tool a { +div.lhg-tool button, div.lhg-tool input { font-family: monospace; font-size: 10pt; background-color: #333; @@ -106,6 +106,11 @@ div.lhg-tool button, div.lhg-tool input, div.lhg-tool a { box-sizing: border-box; } +div.lhg-tool button a { + color: #5f5; + text-decoration: none; +} + div.lhg-tool button:hover, div.lhg-tool input:hover { background-color: #444; } diff --git a/system.js b/system.js index 34d4c38..90f7317 100644 --- a/system.js +++ b/system.js @@ -1,9 +1,8 @@ import {EVENT_OBJECT_CREATE, EVENT_OBJECT_MERGE, OBJECT_HISTORY_SIZE} from './config.js'; import {MassObject} from './object.js'; import { - add, copy, cross, degrees, - direction, div, magnitude, mult, - square, sub, weightedAvg, zero + add, copy, cross, degrees, direction, div, + dot, magnitude, mult, square, sub, weightedAvg, zero } from './vector.js'; export class System { @@ -93,12 +92,11 @@ export class System { if (this.sim.playing) { // Predict positions (Velocity verlet method) this.forEachObject(obj => { - obj.currentAcceleration = {...obj.acceleration}; + obj.currentAcceleration = copy(obj.acceleration); - // If this object is being created/selected, clamp its position - if (obj.id === this.getSelectedOrCreating()?.id) { - return; - } + // If this object is being created/selected, we're not going to let it move... + // but we can calculate the work being done by holding it in place. + obj.currentPosition = copy(obj.position); obj.position = add(obj.position, mult( elapsedTime, @@ -162,10 +160,21 @@ export class System { // Predict velocities this.forEachObject(obj => { - const acceleration = {...obj.acceleration}; + const acceleration = copy(obj.acceleration); obj.acceleration = div(add(obj.currentAcceleration, acceleration), 2); obj.velocity = add(obj.velocity, mult(obj.acceleration, elapsedTime)); + + // If the user is positioning this object, we'll leave its position unchanged; + // but let's compute how much work we're doing to accomplish it! + if (obj.id === this.getSelectedOrCreating()?.id) { + const delta = sub(obj.currentPosition, obj.position); + const netForce = mult(obj.acceleration, obj.mass); + const work = dot(netForce, delta); + console.log('work', work); + obj.position = obj.currentPosition; + } + // Append to object history obj.history.push({position: {...obj.position}}); diff --git a/tool/state.js b/tool/state.js index a055d2f..e7ab3ba 100644 --- a/tool/state.js +++ b/tool/state.js @@ -5,7 +5,7 @@ import {Tool} from '../tool.js'; export class StateTool extends Tool { stored = []; - setContainer(container) { + async setContainer(container) { super.setContainer(container); const buttons = document.createElement('div'); @@ -31,20 +31,45 @@ export class StateTool extends Tool { }); // Check url query parameter, and load specified state if found + await this.fromUrl(); + } + + async toUrl(state) { + const stateText = JSON.stringify(state); + const digest = await hash(stateText); + const rawUrl = `./?state=${stateText}&digest=${digest}`; + const url = encodeURI(rawUrl); + return {url, digest}; + } + + async fromUrl() { const paramsString = window.location.search; const searchParams = new URLSearchParams(paramsString); - const stateEnc = searchParams.get("state"); // a + const stateEnc = searchParams.get("state"); + const rxDigest = searchParams.get("digest"); if (stateEnc) { const stateText = decodeURI(stateEnc); + console.log('decoded state text', stateText); const state = JSON.parse(stateText); + const digest = await hash(stateText); + if (digest !== rxDigest) { + throw new Error('state query parameter does not match digest query parameter'); + } // Tools in this system can be very powerful + this.sim.pause(); this.sim.fromJSON(state); } } getStateDescription(state) { - const d = new Date(state.dateSaved); - return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`; + const date = new Date(state.dateSaved); + const Y = date.getFullYear().toString(); + const M = (date.getMonth() + 1).toString().padStart(2, '0'); + const D = date.getDate().toString().padStart(2, '0'); + const h = date.getHours().toString().padStart(2, '0'); + const m = date.getMinutes().toString().padStart(2, '0'); + const s = date.getSeconds().toString().padStart(2, '0'); + return `${Y}-${M}-${D} ${h}:${m}:${s}`; } async createItem(state) { @@ -60,34 +85,24 @@ export class StateTool extends Tool { const load = document.createElement('button'); load.style.flex = '1'; - load.innerHTML = 'Load'; const link = document.createElement('a'); const {url, digest} = await this.toUrl(state); - link.classList.add(TOOL_INFO_CLASSNAME); - link.classList.add(WIDE_CLASSNAME); link.href = url; - link.innerHTML = digest.slice(0, 6); + link.innerHTML = digest.slice(0, 5); + + load.appendChild(link); item.appendChild(description); item.appendChild(load); - item.appendChild(link); - load.addEventListener('click', () => { + load.addEventListener('click', (e) => { + e.preventDefault(); // Tools in this system can wield great power + this.sim.pause(); this.sim.fromJSON(state); }); return item; } - - async toUrl(state) { - const stateText = JSON.stringify(state); - // const stateB64 = window.btoa(stateText); - const rawUrl = `./?state=${stateText}`; - const url = encodeURI(rawUrl); - const digest = await hash(stateText); - return {url, digest}; - } - } diff --git a/tool/zoom.js b/tool/zoom.js index 0c718e8..545b9fd 100644 --- a/tool/zoom.js +++ b/tool/zoom.js @@ -85,7 +85,9 @@ export class Zoom extends Tool { zeroVelocity.addEventListener('click', () => { // Determine center of mass and average momentum - const {totalMass, netMomentum} = this.sim.system.computeSystemCenter(); + const objects = this.sim.select.selectedGroup; + const {netMomentum} = this.sim.system.computeSystemCenter(objects); + const {totalMass} = this.sim.system.computeSystemCenter(); const netVelocity = { x: netMomentum.x / totalMass, y: netMomentum.y / totalMass,