improve load from url

This commit is contained in:
Ladd 2026-01-05 00:45:04 -06:00
parent a0e45f00b6
commit 0a0c9da3af
8 changed files with 162 additions and 35 deletions

95
; Normal file
View File

@ -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};
}
}

View File

@ -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 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 TODO
---- ----
@ -24,7 +27,7 @@ TODO
- [x] Enhancement: Create Vector class and refactor to use - [x] Enhancement: Create Vector class and refactor to use
- [x] Enhancement: Create Panning 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: 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 - [ ] Feature: Automatically slow time when energy flux is greater
- [ ] Enhancement: Add z-axis (Initially nothing changes, z = 0) - [ ] Enhancement: Add z-axis (Initially nothing changes, z = 0)
- [ ] Feature: Isometric 3d View - [ ] Feature: Isometric 3d View
@ -36,7 +39,7 @@ TODO
- [x] Enhancement: World State Snapshots - [x] Enhancement: World State Snapshots
- [x] Feature: List / Save / Load World States - [x] Feature: List / Save / Load World States
- [ ] Enhancement: Save / Load Snapshots from Local Storage - [ ] Enhancement: Save / Load Snapshots from Local Storage
- [ ] Feature: Import / Export / Share Snapshots - [x] Feature: Import / Export / Share Snapshots
- [ ] Feature: Left Button Panning - [ ] Feature: Left Button Panning
- [ ] Feature: Middle Button Pause - [ ] Feature: Middle Button Pause
- [ ] Feature: Parameter Slider (Invisible, mouse/touch drag) - [ ] Feature: Parameter Slider (Invisible, mouse/touch drag)

BIN
gravity-simulator-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -134,7 +134,6 @@ export class Sim {
system: this.system.toJSON(), system: this.system.toJSON(),
panning: this.panning.toJSON(), panning: this.panning.toJSON(),
display: this.display.toJSON(), display: this.display.toJSON(),
playing: this.playing,
time: this.time, time: this.time,
timeScale: this.timeScale, timeScale: this.timeScale,
currentMode: this.getCurrentMode(), currentMode: this.getCurrentMode(),
@ -145,7 +144,6 @@ export class Sim {
this.system.fromJSON(state.system); this.system.fromJSON(state.system);
this.panning.fromJSON(state.panning); this.panning.fromJSON(state.panning);
this.display.fromJSON(state.display); this.display.fromJSON(state.display);
this.playing = state.playing;
this.time = state.time; this.time = state.time;
this.timeScale = state.timeScale; this.timeScale = state.timeScale;
this.setCurrentMode(state.currentMode); this.setCurrentMode(state.currentMode);

View File

@ -87,7 +87,7 @@ div.lhg-tool div.lhg-wide {
flex-direction: row; 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-family: monospace;
font-size: 10pt; font-size: 10pt;
background-color: #333; background-color: #333;
@ -106,6 +106,11 @@ div.lhg-tool button, div.lhg-tool input, div.lhg-tool a {
box-sizing: border-box; box-sizing: border-box;
} }
div.lhg-tool button a {
color: #5f5;
text-decoration: none;
}
div.lhg-tool button:hover, div.lhg-tool input:hover { div.lhg-tool button:hover, div.lhg-tool input:hover {
background-color: #444; background-color: #444;
} }

View File

@ -1,9 +1,8 @@
import {EVENT_OBJECT_CREATE, EVENT_OBJECT_MERGE, 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 {MassObject} from './object.js';
import { import {
add, copy, cross, degrees, add, copy, cross, degrees, direction, div,
direction, div, magnitude, mult, dot, magnitude, mult, square, sub, weightedAvg, zero
square, sub, weightedAvg, zero
} from './vector.js'; } from './vector.js';
export class System { export class System {
@ -93,12 +92,11 @@ export class System {
if (this.sim.playing) { if (this.sim.playing) {
// Predict positions (Velocity verlet method) // Predict positions (Velocity verlet method)
this.forEachObject(obj => { this.forEachObject(obj => {
obj.currentAcceleration = {...obj.acceleration}; obj.currentAcceleration = copy(obj.acceleration);
// If this object is being created/selected, clamp its position // If this object is being created/selected, we're not going to let it move...
if (obj.id === this.getSelectedOrCreating()?.id) { // but we can calculate the work being done by holding it in place.
return; obj.currentPosition = copy(obj.position);
}
obj.position = add(obj.position, mult( obj.position = add(obj.position, mult(
elapsedTime, elapsedTime,
@ -162,10 +160,21 @@ export class System {
// Predict velocities // Predict velocities
this.forEachObject(obj => { this.forEachObject(obj => {
const acceleration = {...obj.acceleration}; const acceleration = copy(obj.acceleration);
obj.acceleration = div(add(obj.currentAcceleration, acceleration), 2); obj.acceleration = div(add(obj.currentAcceleration, acceleration), 2);
obj.velocity = add(obj.velocity, mult(obj.acceleration, elapsedTime)); 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 // Append to object history
obj.history.push({position: {...obj.position}}); obj.history.push({position: {...obj.position}});

View File

@ -5,7 +5,7 @@ import {Tool} from '../tool.js';
export class StateTool extends Tool { export class StateTool extends Tool {
stored = []; stored = [];
setContainer(container) { async setContainer(container) {
super.setContainer(container); super.setContainer(container);
const buttons = document.createElement('div'); const buttons = document.createElement('div');
@ -31,20 +31,45 @@ export class StateTool extends Tool {
}); });
// Check url query parameter, and load specified state if found // 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 paramsString = window.location.search;
const searchParams = new URLSearchParams(paramsString); const searchParams = new URLSearchParams(paramsString);
const stateEnc = searchParams.get("state"); // a const stateEnc = searchParams.get("state");
const rxDigest = searchParams.get("digest");
if (stateEnc) { if (stateEnc) {
const stateText = decodeURI(stateEnc); const stateText = decodeURI(stateEnc);
console.log('decoded state text', stateText);
const state = JSON.parse(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 // Tools in this system can be very powerful
this.sim.pause();
this.sim.fromJSON(state); this.sim.fromJSON(state);
} }
} }
getStateDescription(state) { getStateDescription(state) {
const d = new Date(state.dateSaved); const date = new Date(state.dateSaved);
return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`; 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) { async createItem(state) {
@ -60,34 +85,24 @@ export class StateTool extends Tool {
const load = document.createElement('button'); const load = document.createElement('button');
load.style.flex = '1'; load.style.flex = '1';
load.innerHTML = 'Load';
const link = document.createElement('a'); const link = document.createElement('a');
const {url, digest} = await this.toUrl(state); const {url, digest} = await this.toUrl(state);
link.classList.add(TOOL_INFO_CLASSNAME);
link.classList.add(WIDE_CLASSNAME);
link.href = url; link.href = url;
link.innerHTML = digest.slice(0, 6); link.innerHTML = digest.slice(0, 5);
load.appendChild(link);
item.appendChild(description); item.appendChild(description);
item.appendChild(load); item.appendChild(load);
item.appendChild(link);
load.addEventListener('click', () => { load.addEventListener('click', (e) => {
e.preventDefault();
// Tools in this system can wield great power // Tools in this system can wield great power
this.sim.pause();
this.sim.fromJSON(state); this.sim.fromJSON(state);
}); });
return item; 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};
}
} }

View File

@ -85,7 +85,9 @@ export class Zoom 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.system.computeSystemCenter(); const objects = this.sim.select.selectedGroup;
const {netMomentum} = this.sim.system.computeSystemCenter(objects);
const {totalMass} = this.sim.system.computeSystemCenter();
const netVelocity = { const netVelocity = {
x: netMomentum.x / totalMass, x: netMomentum.x / totalMass,
y: netMomentum.y / totalMass, y: netMomentum.y / totalMass,