Fix options localStorage
This commit is contained in:
parent
e50be0b874
commit
d6c3db8e45
@ -8,7 +8,8 @@ Uses `npm` for `eslint`.
|
|||||||
Screenshots
|
Screenshots
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
----
|
----
|
||||||
@ -30,10 +31,10 @@ TODO
|
|||||||
- [ ] Undo "Clear Traces" Action
|
- [ ] Undo "Clear Traces" Action
|
||||||
- [ ] Undo "Reset
|
- [ ] Undo "Reset
|
||||||
- [ ] Time Control: Reverse Time
|
- [ ] Time Control: Reverse Time
|
||||||
- [ ] Save to LocalStorage
|
- [x] Save to LocalStorage
|
||||||
- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
||||||
- [ ] Track farthest reaches, min/max in each dimension (x, y)
|
- [ ] Track farthest reaches, min/max in each dimension (x, y)
|
||||||
- [ ] Enabling Zoom to Fit Traces
|
|
||||||
- [x] Compute Net Angular Momentum
|
- [x] Compute Net Angular Momentum
|
||||||
- [ ] Display Net Angular Momentum
|
- [ ] Display Net Angular Momentum
|
||||||
|
- [ ] Calculate Work as FxD as measure of energy flux
|
||||||
|
- [ ] Option to automatically slow time when energy flux is greater
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const PATH_TRACES_DASHED_OPACITY = 1.0;
|
|||||||
|
|
||||||
// SIZES
|
// SIZES
|
||||||
export const POINTER_HISTORY_SIZE = 20;
|
export const POINTER_HISTORY_SIZE = 20;
|
||||||
|
export const FRAMERATE_SAMPLE_DURATION = 2.0; // Seconds
|
||||||
export const POINTER_DOWN_HISTORY_SIZE = 5;
|
export const POINTER_DOWN_HISTORY_SIZE = 5;
|
||||||
export const ARROWHEAD_LENGTH = 7;
|
export const ARROWHEAD_LENGTH = 7;
|
||||||
export const ARROWHEAD_WIDTH = 5;
|
export const ARROWHEAD_WIDTH = 5;
|
||||||
@ -36,6 +37,7 @@ export const TOOL_INFO_CLASSNAME = 'lhg-tool-info';
|
|||||||
export const TOOLBAR_CLASSNAME = 'lhg-toolbar';
|
export const TOOLBAR_CLASSNAME = 'lhg-toolbar';
|
||||||
export const TOOLBAR_HEADER_CLASSNAME = 'lhg-toolbar-header';
|
export const TOOLBAR_HEADER_CLASSNAME = 'lhg-toolbar-header';
|
||||||
export const WIDE_CLASSNAME = 'lhg-wide';
|
export const WIDE_CLASSNAME = 'lhg-wide';
|
||||||
|
export const TALL_CLASSNAME = 'lhg-tall';
|
||||||
export const OVERLAY_INFO_BOX_CLASSNAME = 'lhg-overlay-info-box';
|
export const OVERLAY_INFO_BOX_CLASSNAME = 'lhg-overlay-info-box';
|
||||||
|
|
||||||
// EVENT NAMES
|
// EVENT NAMES
|
||||||
@ -48,3 +50,6 @@ export const EVENT_OPTION_SET = 'lhg-option-set';
|
|||||||
export const MODE_MASS_GENERATION = 'mass-gen';
|
export const MODE_MASS_GENERATION = 'mass-gen';
|
||||||
export const MODE_PAN_VIEW = 'pan-view';
|
export const MODE_PAN_VIEW = 'pan-view';
|
||||||
export const MODE_OBJECT_SELECT = 'select';
|
export const MODE_OBJECT_SELECT = 'select';
|
||||||
|
|
||||||
|
// LOCAL STORAGE PREFIXES/SUFFIXES
|
||||||
|
export const TOOLBAR_EXPANDED_SUFFIX = 'lhg-toolbar-expanded';
|
||||||
|
|||||||
@ -59,9 +59,6 @@ export class Display {
|
|||||||
fullscreen() {
|
fullscreen() {
|
||||||
this.canvas.width = document.documentElement.clientWidth;
|
this.canvas.width = document.documentElement.clientWidth;
|
||||||
this.canvas.height = document.documentElement.clientHeight;
|
this.canvas.height = document.documentElement.clientHeight;
|
||||||
if (this.sim.getOption('debug.canvasSize')) {
|
|
||||||
this.sim.info['Canvas'] = [`${this.canvas.width}`, `${this.canvas.height}`];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fillCanvas() {
|
fillCanvas() {
|
||||||
@ -160,8 +157,9 @@ export class Display {
|
|||||||
} else if (this.sim.panning && !this.sim.panning.paused) {
|
} else if (this.sim.panning && !this.sim.panning.paused) {
|
||||||
// Apply update to viewOrigin based on panning
|
// Apply update to viewOrigin based on panning
|
||||||
const { velocity } = this.sim.panning;
|
const { velocity } = this.sim.panning;
|
||||||
this.viewOrigin.x -= velocity.x * elapsedTime;
|
// TODO: something with time scale? Panning is too fast.
|
||||||
this.viewOrigin.y -= velocity.y * elapsedTime;
|
this.viewOrigin.x -= velocity.x * elapsedTime / 1000; // millisecond conversion?
|
||||||
|
this.viewOrigin.y -= velocity.y * elapsedTime / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
gravity-simulator-3.png
Normal file
BIN
gravity-simulator-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
14
objects.js
14
objects.js
@ -200,13 +200,20 @@ export class Objects {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// elapsedTime is given in milliseconds
|
||||||
computeFrame(elapsedTime) {
|
computeFrame(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
|
||||||
const massCreationRate = this.sim.getOption('param.massCreationRate');
|
|
||||||
|
// Scaling this parameter because of millisecond conversion
|
||||||
|
const massCreationRate = this.sim.getOption('param.massCreationRate') / 1000;
|
||||||
|
|
||||||
if (this.creatingObject !== undefined) {
|
if (this.creatingObject !== undefined) {
|
||||||
const obj = this.objects[this.creatingObject];
|
const obj = this.objects[this.creatingObject];
|
||||||
const rate = massCreationRate * obj.age;
|
const rate = massCreationRate * obj.age;
|
||||||
|
console.log('obj.age', obj.age, 'mass creation rate', rate, 'elapsedTime', elapsedTime);
|
||||||
// TODO: After objects merge during creation, mass creation rate can accelerate
|
// TODO: After objects merge during creation, mass creation rate can accelerate
|
||||||
obj.mass += rate * elapsedTime;
|
obj.mass += rate * elapsedTime;
|
||||||
}
|
}
|
||||||
@ -215,6 +222,8 @@ 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};
|
||||||
@ -306,8 +315,9 @@ export class Objects {
|
|||||||
`${obj.position.y.toPrecision(6)}, `,
|
`${obj.position.y.toPrecision(6)}, `,
|
||||||
`${obj.mass.toPrecision(6)} kg, `,
|
`${obj.mass.toPrecision(6)} kg, `,
|
||||||
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
||||||
|
`Alive: ${obj.alive}`,
|
||||||
];
|
];
|
||||||
});
|
}, { alive: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
options.js
63
options.js
@ -2,54 +2,51 @@ import {
|
|||||||
EVENT_OPTION_SET,
|
EVENT_OPTION_SET,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export const optionsLayout = {
|
|
||||||
pauseDuring: {
|
|
||||||
creation: ['Pause While Creating', 'boolean', true],
|
|
||||||
selection: ['Pause While Selecting', 'boolean', true],
|
|
||||||
},
|
|
||||||
display: {
|
|
||||||
velocity: ['Velocity Vectors', 'boolean', true],
|
|
||||||
acceleration: ['Accel. Vectors', 'boolean', true],
|
|
||||||
traces: ['Path Traces', 'boolean', true],
|
|
||||||
dashedTraces: ['Dashed Traces', 'boolean', false],
|
|
||||||
},
|
|
||||||
collision: {
|
|
||||||
merge: ['Merge Masses<br>on Collision', 'boolean', true, {wide: true}],
|
|
||||||
},
|
|
||||||
param: {
|
|
||||||
gravity: ['Gravity', 'number', 4E4],
|
|
||||||
timeScale: ['Time Scale', 'number', 0.2],
|
|
||||||
massCreationRate: ['Mass Creation Rate', 'number', 10],
|
|
||||||
},
|
|
||||||
debug: {
|
|
||||||
objectsInfo: ['Objects Info', 'boolean', false],
|
|
||||||
cursorInfo: ['Cursor Info', 'boolean', false],
|
|
||||||
canvasSize: ['Canvas Size', 'boolean', false],
|
|
||||||
currentMode: ['Current Mode', 'boolean', false],
|
|
||||||
panningInfo: ['Panning Info', 'boolean', false],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Options {
|
export class Options {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
|
options = undefined;
|
||||||
values = {};
|
values = {};
|
||||||
|
|
||||||
constructor(sim) {
|
getStorageKey(path) {
|
||||||
|
return `${path}:options`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sim, options) {
|
||||||
this.sim = sim;
|
this.sim = sim;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
// Global methods to get/set current option values
|
// Global methods to get/set current option values
|
||||||
this.sim.getOption = (path) => this.getOption(path);
|
this.sim.getOption = (path) => this.getOption(path);
|
||||||
this.sim.setOption = (path, value) => this.setOption(path, value);
|
this.sim.setOption = (path, value) => this.setOption(path, value);
|
||||||
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
|
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
|
||||||
|
|
||||||
|
// Initialize values from localStorage
|
||||||
|
for (const groupName of Object.keys(options)) {
|
||||||
|
for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
|
||||||
|
const path = [groupName, name].join('.');
|
||||||
|
let value = this.getOption(path)
|
||||||
|
if (value === undefined) {
|
||||||
|
value = defaultValue;
|
||||||
|
this.setOption(path, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOption(path) {
|
getOption(path) {
|
||||||
const val = this.values[path];
|
let value = this.values[path];
|
||||||
return val;
|
if (value === undefined) {
|
||||||
|
value = localStorage.getItem(this.getStorageKey(path));
|
||||||
|
if (value === 'false') value = false;
|
||||||
|
else if (value === 'true') value = true;
|
||||||
|
this.values[path] = value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOption(path, value) {
|
setOption(path, value) {
|
||||||
this.values[path] = value;
|
this.values[path] = value;
|
||||||
|
window.localStorage.setItem(this.getStorageKey(path), value);
|
||||||
const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}});
|
const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}});
|
||||||
this.sim.div.dispatchEvent(e);
|
this.sim.div.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
@ -63,8 +60,8 @@ export class Options {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSection(layout, sectionName) {
|
getSection(sectionName) {
|
||||||
const section = layout[sectionName];
|
const section = this.options[sectionName];
|
||||||
const group = {
|
const group = {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
name: sectionName,
|
name: sectionName,
|
||||||
|
|||||||
13
pointer.js
13
pointer.js
@ -66,7 +66,7 @@ export class Pointer {
|
|||||||
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];
|
||||||
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
||||||
const dt = (end.t - start.t) / 1000;
|
const dt = (end.t - start.t);
|
||||||
return {
|
return {
|
||||||
x: (end.x - start.x) / dt,
|
x: (end.x - start.x) / dt,
|
||||||
y: (end.y - start.y) / dt,
|
y: (end.y - start.y) / dt,
|
||||||
@ -145,14 +145,13 @@ export class Pointer {
|
|||||||
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
||||||
handlePointerMove({x: clientX, y: clientY}) {
|
handlePointerMove({x: clientX, y: clientY}) {
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
const v = this.latestPointerVelocity;
|
|
||||||
// Convert pointer velocity to simulation scale
|
|
||||||
v.x /= this.sim.display.scale;
|
|
||||||
v.y /= this.sim.display.scale;
|
|
||||||
|
|
||||||
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: v.x, vy: v.y});
|
this.sim.objects.handlePointerMove({x, y, vx, vy});
|
||||||
|
|
||||||
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
} else if (this.sim.isCurrentMode(MODE_PAN_VIEW)) {
|
||||||
if (this.panTouchStart) {
|
if (this.panTouchStart) {
|
||||||
@ -160,7 +159,7 @@ export class Pointer {
|
|||||||
this.panTouchLatest = {
|
this.panTouchLatest = {
|
||||||
x: clientX,
|
x: clientX,
|
||||||
y: clientY,
|
y: clientY,
|
||||||
t: document.timeline.currentTime,
|
t: this.sim.rawTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
simulator.js
64
simulator.js
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
EVENT_ZOOM,
|
EVENT_ZOOM,
|
||||||
|
FRAMERATE_SAMPLE_DURATION,
|
||||||
SCALE_POWER_MAX,
|
SCALE_POWER_MAX,
|
||||||
SCALE_POWER_MIN,
|
SCALE_POWER_MIN,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
@ -16,12 +17,42 @@ import { UtilityTool } from './tool/utility.js';
|
|||||||
import { Toolbar } from './toolbar.js';
|
import { Toolbar } from './toolbar.js';
|
||||||
import { ToolbarGroup } from './toolbar-group.js';
|
import { ToolbarGroup } from './toolbar-group.js';
|
||||||
|
|
||||||
|
const simOptions = {
|
||||||
|
pauseDuring: {
|
||||||
|
creation: ['Pause While Creating', 'boolean', true],
|
||||||
|
selection: ['Pause While Selecting', 'boolean', true],
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
velocity: ['Velocity Vectors', 'boolean', true],
|
||||||
|
acceleration: ['Accel. Vectors', 'boolean', true],
|
||||||
|
traces: ['Path Traces', 'boolean', true],
|
||||||
|
dashedTraces: ['Dashed Traces', 'boolean', false],
|
||||||
|
},
|
||||||
|
collision: {
|
||||||
|
merge: ['Merge Masses<br>on Collision', 'boolean', true, {wide: true}],
|
||||||
|
},
|
||||||
|
param: {
|
||||||
|
gravity: ['Gravity', 'number', 4E4],
|
||||||
|
timeScale: ['Time Scale', 'number', 0.2],
|
||||||
|
massCreationRate: ['Mass Creation Rate', 'number', 10],
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
objectsInfo: ['Objects Info', 'boolean', false],
|
||||||
|
cursorInfo: ['Cursor Info', 'boolean', false],
|
||||||
|
frameRate: ['Frame Rate', 'boolean', false, {wide: true}],
|
||||||
|
currentMode: ['Current Mode', 'boolean', false],
|
||||||
|
panningInfo: ['Panning Info', 'boolean', false],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class Sim {
|
export class Sim {
|
||||||
info = {};
|
info = {};
|
||||||
rawTime = undefined;
|
rawTime = undefined;
|
||||||
time = undefined;
|
time = undefined;
|
||||||
nextZoom = undefined;
|
nextZoom = undefined;
|
||||||
playing = true;
|
playing = true;
|
||||||
|
recentFrames = [];
|
||||||
|
frameRate = 0;
|
||||||
|
|
||||||
objects = undefined;
|
objects = undefined;
|
||||||
display = undefined;
|
display = undefined;
|
||||||
@ -38,12 +69,26 @@ export class Sim {
|
|||||||
onModeEnter = () => undefined;
|
onModeEnter = () => undefined;
|
||||||
onModeLeave = () => undefined;
|
onModeLeave = () => undefined;
|
||||||
|
|
||||||
|
markFrame(t) {
|
||||||
|
const { recentFrames: rfs } = this;
|
||||||
|
rfs.push(t);
|
||||||
|
if (rfs.length < 2) return;
|
||||||
|
const oldest = rfs[0];
|
||||||
|
const newest = rfs[rfs.length - 1];
|
||||||
|
const count = rfs.length;
|
||||||
|
const duration = (newest - oldest) / 1000; // ms to s
|
||||||
|
this.frameRate = count / duration;
|
||||||
|
if (duration >= FRAMERATE_SAMPLE_DURATION) {
|
||||||
|
rfs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(divId) {
|
constructor(divId) {
|
||||||
this.divId = divId;
|
this.divId = divId;
|
||||||
const div = document.getElementById(this.divId);
|
const div = document.getElementById(this.divId);
|
||||||
this.div = div;
|
this.div = div;
|
||||||
|
|
||||||
this.options = new Options(this);
|
this.options = new Options(this, simOptions);
|
||||||
this.display = new Display(this);
|
this.display = new Display(this);
|
||||||
this.objects = new Objects(this);
|
this.objects = new Objects(this);
|
||||||
this.toolbars = {
|
this.toolbars = {
|
||||||
@ -95,7 +140,7 @@ export class Sim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initiate main loop
|
// Initiate main loop
|
||||||
this.rawTime = document.timeline.currentTime / 1000;
|
this.rawTime = document.timeline.currentTime;
|
||||||
this.time = 0;
|
this.time = 0;
|
||||||
requestAnimationFrame(t => this.loop(t));
|
requestAnimationFrame(t => this.loop(t));
|
||||||
}
|
}
|
||||||
@ -163,10 +208,14 @@ export class Sim {
|
|||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop(currentTime) {
|
loop(currentTime) {
|
||||||
currentTime /= 1000;
|
this.markFrame(currentTime);
|
||||||
const timeScale = this.getOption('param.timeScale');
|
const timeScale = this.getOption('param.timeScale');
|
||||||
const elapsedTime = (currentTime - this.rawTime) * timeScale;
|
|
||||||
|
// elapsedTime in milliseconds
|
||||||
|
// rawTime in milliseconds
|
||||||
|
const elapsedTime = (currentTime - this.rawTime) / timeScale;
|
||||||
this.rawTime = currentTime;
|
this.rawTime = currentTime;
|
||||||
|
|
||||||
if (this.playing) {
|
if (this.playing) {
|
||||||
this.time += elapsedTime;
|
this.time += elapsedTime;
|
||||||
}
|
}
|
||||||
@ -175,6 +224,10 @@ export class Sim {
|
|||||||
this.info['Mode'] = this.getCurrentMode();
|
this.info['Mode'] = this.getCurrentMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.getOption('debug.frameRate')) {
|
||||||
|
this.info['Frame Rate'] = this.frameRate?.toPrecision(3);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.nextZoom) {
|
if (this.nextZoom) {
|
||||||
this.zoom(this.nextZoom);
|
this.zoom(this.nextZoom);
|
||||||
this.nextZoom = undefined;
|
this.nextZoom = undefined;
|
||||||
@ -188,15 +241,14 @@ export class Sim {
|
|||||||
this.info['Net Angular Momentum'] = this.objects.computeSystemAngularMomentum().toPrecision(6);
|
this.info['Net Angular Momentum'] = this.objects.computeSystemAngularMomentum().toPrecision(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.display.computePanning(elapsedTime);
|
|
||||||
this.objects.computeFrame(elapsedTime);
|
this.objects.computeFrame(elapsedTime);
|
||||||
this.overlay.renderInfo();
|
this.overlay.renderInfo();
|
||||||
|
// this.display.computePanning(elapsedTime);
|
||||||
this.display.fillCanvas();
|
this.display.fillCanvas();
|
||||||
this.display.drawObjects();
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
style.css
11
style.css
@ -33,8 +33,8 @@ div.lhg-toolbar {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
border-width: 1px;
|
border-width: 2px;
|
||||||
border-color: #282;
|
border-color: #151;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ div.lhg-tool {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 12em;
|
width: 12em;
|
||||||
/* padding: 0.5em; */
|
/* padding: 0.5em; */
|
||||||
margin: 0.5em;
|
/* margin: 0.5em; */
|
||||||
text-align: middle;
|
text-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +107,11 @@ div.lhg-tool .lhg-wide {
|
|||||||
width: 12em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.lhg-tool .lhg-tall {
|
||||||
|
padding-top: 1em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
div.lhg-overlay-info-box {
|
div.lhg-overlay-info-box {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
|
|||||||
12
tool.js
12
tool.js
@ -1,18 +1,18 @@
|
|||||||
// Idea here is, tool can declare its parameters;
|
// Idea here is, tool can declare its parameters;
|
||||||
// can call back to toolbar for whatever...
|
// can call back to container for whatever...
|
||||||
// through toolbar can access sim
|
// through container can access sim
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TOOL_CLASSNAME,
|
TOOL_CLASSNAME,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export class Tool {
|
export class Tool {
|
||||||
toolbar = undefined;
|
container = undefined;
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
|
|
||||||
constructor(toolbar) {
|
constructor(container) {
|
||||||
this.toolbar = toolbar;
|
this.container = container;
|
||||||
this.sim = this.toolbar.sim;
|
this.sim = this.container.sim;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
this.div = div;
|
this.div = div;
|
||||||
div.classList.add(TOOL_CLASSNAME)
|
div.classList.add(TOOL_CLASSNAME)
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import {TOOLBAR_HEADER_CLASSNAME} from '../config.js';
|
|||||||
import { Tool } from '../tool.js';
|
import { Tool } from '../tool.js';
|
||||||
|
|
||||||
export class Header extends Tool {
|
export class Header extends Tool {
|
||||||
constructor(toolbar, title = 'Tools') {
|
|
||||||
super(toolbar);
|
constructor(container, title = 'Tools') {
|
||||||
|
super(container);
|
||||||
this.title = document.createElement('h1');
|
this.title = document.createElement('h1');
|
||||||
this.title.innerHTML = title;
|
this.title.innerHTML = title;
|
||||||
|
|
||||||
@ -22,12 +23,12 @@ export class Header extends Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateButton() {
|
updateButton() {
|
||||||
this.toggleButton.innerHTML = this.toolbar.expanded ? '˄' : '˅';
|
this.toggleButton.innerHTML = this.container.expanded ? '˅' : '˄';
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.toolbar.expanded = !this.toolbar.expanded;
|
this.container.expanded = !this.container.expanded;
|
||||||
this.toolbar.applyExpanded();
|
this.container.applyExpanded();
|
||||||
this.updateButton();
|
this.updateButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ export class ModeSwitch extends Tool {
|
|||||||
];
|
];
|
||||||
buttons = [];
|
buttons = [];
|
||||||
|
|
||||||
constructor(toolbar) {
|
constructor(container) {
|
||||||
super(toolbar);
|
super(container);
|
||||||
|
|
||||||
const modesDiv = document.createElement('div');
|
const modesDiv = document.createElement('div');
|
||||||
const heading = document.createElement('h2');
|
const heading = document.createElement('h2');
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
WIDE_CLASSNAME,
|
WIDE_CLASSNAME,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import { Tool } from '../tool.js';
|
import { Tool } from '../tool.js';
|
||||||
import { Options, optionsLayout } from '../options.js';
|
|
||||||
|
|
||||||
export class OptionsTool extends Tool {
|
export class OptionsTool extends Tool {
|
||||||
visitItem(item, path) {
|
visitItem(item, path) {
|
||||||
@ -29,14 +28,20 @@ export class OptionsTool extends Tool {
|
|||||||
if (item.wide === true) {
|
if (item.wide === true) {
|
||||||
button.classList.add(WIDE_CLASSNAME);
|
button.classList.add(WIDE_CLASSNAME);
|
||||||
}
|
}
|
||||||
this.sim.setOption(path, item.default);
|
const value = this.sim.getOption(path);
|
||||||
button.style.opacity = this.sim.getOption(path) ? '100%' : '50%';
|
if (value === undefined) {
|
||||||
button.addEventListener('click', () => {
|
this.sim.setOption(path, item.default);
|
||||||
this.sim.setOption(path, !this.sim.getOption(path));
|
}
|
||||||
button.style.opacity = this.sim.getOption(path) ? '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);
|
||||||
});
|
});
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
@ -77,12 +82,12 @@ export class OptionsTool extends Tool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(toolbar, sections) {
|
constructor(container, sections) {
|
||||||
super(toolbar);
|
super(container);
|
||||||
|
|
||||||
for (const sectionName of sections) {
|
for (const sectionName of sections) {
|
||||||
const item = Options.getSection(optionsLayout, sectionName);
|
const option = this.sim.options.getSection(sectionName);
|
||||||
const child = this.visitItem(item);
|
const child = this.visitItem(option);
|
||||||
this.div.appendChild(child);
|
this.div.appendChild(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,36 @@
|
|||||||
import {TOOL_INFO_CLASSNAME, WIDE_CLASSNAME} from '../config.js';
|
import {
|
||||||
|
TALL_CLASSNAME
|
||||||
|
} from '../config.js';
|
||||||
import {Tool} from '../tool.js';
|
import {Tool} from '../tool.js';
|
||||||
|
|
||||||
export class PlayPause extends Tool {
|
export class PlayPause extends Tool {
|
||||||
playHTML = 'Play';
|
playHTML = 'Play';
|
||||||
pauseHTML = 'Pause';
|
pauseHTML = 'Pause';
|
||||||
currentTimeEl = undefined;
|
|
||||||
pauseButton = undefined;
|
pauseButton = undefined;
|
||||||
playButton = undefined;
|
playButton = undefined;
|
||||||
|
|
||||||
get timeText() {
|
|
||||||
let time = this.sim.time;
|
|
||||||
// Time in seconds
|
|
||||||
const s = time % 60;
|
|
||||||
time = (time - s) / 60;
|
|
||||||
const m = time % 60;
|
|
||||||
time = (time - m) / 60;
|
|
||||||
const h = time % 24;
|
|
||||||
time = (time - h) / 24;
|
|
||||||
const d = time;
|
|
||||||
time -= m * 60;
|
|
||||||
|
|
||||||
const ms = (s - Math.floor(s)) * 1000;
|
|
||||||
return [
|
|
||||||
d || undefined,
|
|
||||||
h.toString().padStart(2, '0'),
|
|
||||||
m.toString().padStart(2, '0'),
|
|
||||||
[
|
|
||||||
s.toFixed(0).padStart(2, '0'),
|
|
||||||
ms.toFixed(0).padStart(3, '0'),
|
|
||||||
].join('.')
|
|
||||||
].filter(x => x !== undefined).join(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
frame() {
|
|
||||||
if (this.currentTimeEl) {
|
|
||||||
this.currentTimeEl.innerHTML = this.timeText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateButtons() {
|
updateButtons() {
|
||||||
this.pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
|
this.pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
|
||||||
this.playButton.style.opacity = this.sim.playing ? '50%' : '100%';
|
this.playButton.style.opacity = this.sim.playing ? '50%' : '100%';
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(toolbar) {
|
constructor(container) {
|
||||||
super(toolbar);
|
super(container);
|
||||||
|
|
||||||
const currentTime = document.createElement('button');
|
|
||||||
const pauseButton = document.createElement('button');
|
const pauseButton = document.createElement('button');
|
||||||
const playButton = document.createElement('button');
|
const playButton = document.createElement('button');
|
||||||
|
|
||||||
this.pauseButton = pauseButton;
|
this.pauseButton = pauseButton;
|
||||||
this.playButton = playButton;
|
this.playButton = playButton;
|
||||||
this.currentTimeEl = currentTime;
|
|
||||||
|
|
||||||
this.div.appendChild(currentTime);
|
|
||||||
this.div.appendChild(pauseButton);
|
this.div.appendChild(pauseButton);
|
||||||
this.div.appendChild(playButton);
|
this.div.appendChild(playButton);
|
||||||
|
|
||||||
currentTime.classList.add(TOOL_INFO_CLASSNAME);
|
|
||||||
currentTime.classList.add(WIDE_CLASSNAME);
|
|
||||||
|
|
||||||
pauseButton.innerHTML = this.pauseHTML;
|
pauseButton.innerHTML = this.pauseHTML;
|
||||||
playButton.innerHTML = this.playHTML;
|
playButton.innerHTML = this.playHTML;
|
||||||
currentTime.innerHTML = this.timeText;
|
|
||||||
|
playButton.classList.add(TALL_CLASSNAME);
|
||||||
|
pauseButton.classList.add(TALL_CLASSNAME);
|
||||||
|
|
||||||
this.updateButtons();
|
this.updateButtons();
|
||||||
|
|
||||||
|
|||||||
100
tool/utility.js
100
tool/utility.js
@ -1,23 +1,83 @@
|
|||||||
import {Tool} from '../tool.js';
|
import {Tool} from '../tool.js';
|
||||||
import {
|
import {
|
||||||
|
TOOL_INFO_CLASSNAME,
|
||||||
WIDE_CLASSNAME,
|
WIDE_CLASSNAME,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
|
|
||||||
export class UtilityTool extends Tool {
|
export class UtilityTool extends Tool {
|
||||||
constructor(toolbar) {
|
currentTimeEl = undefined;
|
||||||
super(toolbar);
|
|
||||||
|
get displayScaleText() {
|
||||||
|
return `Scale: ${this.sim.getScaleDisplay()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(container) {
|
||||||
|
super(container);
|
||||||
|
|
||||||
const zeroVelocity = document.createElement('button');
|
const zeroVelocity = document.createElement('button');
|
||||||
const clearTraces = document.createElement('button');
|
const clearTraces = document.createElement('button');
|
||||||
|
const zoomAll = document.createElement('button');
|
||||||
|
const currentScale = document.createElement('button')
|
||||||
|
const currentTime = document.createElement('button');
|
||||||
|
const clearDebug = document.createElement('button');
|
||||||
|
|
||||||
this.div.appendChild(clearTraces);
|
this.currentTimeEl = currentTime;
|
||||||
|
|
||||||
|
this.div.appendChild(currentTime);
|
||||||
|
this.div.appendChild(currentScale);
|
||||||
|
this.div.appendChild(zoomAll);
|
||||||
this.div.appendChild(zeroVelocity);
|
this.div.appendChild(zeroVelocity);
|
||||||
|
this.div.appendChild(clearTraces);
|
||||||
|
this.div.appendChild(clearDebug);
|
||||||
|
|
||||||
zeroVelocity.classList.add(WIDE_CLASSNAME);
|
zeroVelocity.classList.add(WIDE_CLASSNAME);
|
||||||
clearTraces.classList.add(WIDE_CLASSNAME);
|
clearTraces.classList.add(WIDE_CLASSNAME);
|
||||||
|
zoomAll.classList.add(WIDE_CLASSNAME);
|
||||||
|
currentScale.classList.add(WIDE_CLASSNAME);
|
||||||
|
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
||||||
|
currentTime.classList.add(TOOL_INFO_CLASSNAME);
|
||||||
|
currentTime.classList.add(WIDE_CLASSNAME);
|
||||||
|
clearDebug.classList.add(WIDE_CLASSNAME);
|
||||||
|
|
||||||
zeroVelocity.innerHTML = 'Zero Momentum';
|
zeroVelocity.innerHTML = 'Zero Momentum';
|
||||||
clearTraces.innerHTML = 'Clear Traces';
|
clearTraces.innerHTML = 'Clear Traces';
|
||||||
|
zoomAll.innerHTML = 'Zoom to Fit';
|
||||||
|
currentScale.innerHTML = this.displayScaleText;
|
||||||
|
currentTime.innerHTML = this.timeText;
|
||||||
|
clearDebug.innerHTML = 'Clear Debug';
|
||||||
|
|
||||||
|
this.sim.onZoom(() => {
|
||||||
|
currentScale.innerHTML = this.displayScaleText;
|
||||||
|
});
|
||||||
|
|
||||||
zeroVelocity.addEventListener('click', () => {
|
zeroVelocity.addEventListener('click', () => {
|
||||||
// Determine center of mass and average momentum
|
// Determine center of mass and average momentum
|
||||||
@ -43,5 +103,39 @@ export class UtilityTool extends Tool {
|
|||||||
obj.history = [];
|
obj.history = [];
|
||||||
}, {alive: null});
|
}, {alive: null});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
zoomAll.addEventListener('click', () => {
|
||||||
|
// Determine bounding box
|
||||||
|
const box = this.sim.objects.boundingBox;
|
||||||
|
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;
|
||||||
|
const heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height;
|
||||||
|
const biggerRatio = Math.max(widthRatio, heightRatio);
|
||||||
|
const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 0.5;
|
||||||
|
const factor = Math.floor(base2factor);
|
||||||
|
// Determine average momentum and set panning velocity to match
|
||||||
|
const netMomentum = {x: 0, y: 0};
|
||||||
|
let totalMass = 0;
|
||||||
|
let count = 0;
|
||||||
|
this.sim.objects.forEachObject(obj => {
|
||||||
|
count++;
|
||||||
|
netMomentum.x += obj.mass * obj.velocity.x;
|
||||||
|
netMomentum.y += obj.mass * obj.velocity.y;
|
||||||
|
totalMass += obj.mass;
|
||||||
|
});
|
||||||
|
if (!count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const netVelocity = {
|
||||||
|
x: netMomentum.x / totalMass,
|
||||||
|
y: netMomentum.y / totalMass,
|
||||||
|
};
|
||||||
|
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
||||||
|
});
|
||||||
|
|
||||||
|
clearDebug.addEventListener('click', () => {
|
||||||
|
this.sim.info = {};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
tool/zoom.js
58
tool/zoom.js
@ -1,45 +1,21 @@
|
|||||||
import { Tool } from '../tool.js';
|
import { Tool } from '../tool.js';
|
||||||
import {
|
import {
|
||||||
TOOL_INFO_CLASSNAME,
|
|
||||||
WIDE_CLASSNAME,
|
|
||||||
ZOOM_IN_FACTOR,
|
ZOOM_IN_FACTOR,
|
||||||
ZOOM_OUT_FACTOR,
|
ZOOM_OUT_FACTOR,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
|
|
||||||
export class Zoom extends Tool {
|
export class Zoom extends Tool {
|
||||||
get displayScale() {
|
constructor(container) {
|
||||||
return this.sim.getScaleDisplay();
|
super(container);
|
||||||
}
|
|
||||||
|
|
||||||
get displayScaleText() {
|
|
||||||
return `Scale: ${this.displayScale}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(toolbar) {
|
|
||||||
super(toolbar);
|
|
||||||
|
|
||||||
const currentScale = document.createElement('button')
|
|
||||||
const zoomOut = document.createElement('button');
|
const zoomOut = document.createElement('button');
|
||||||
const zoomIn = document.createElement('button');
|
const zoomIn = document.createElement('button');
|
||||||
const zoomAll = document.createElement('button');
|
|
||||||
|
|
||||||
this.div.appendChild(currentScale);
|
|
||||||
this.div.appendChild(zoomOut);
|
this.div.appendChild(zoomOut);
|
||||||
this.div.appendChild(zoomIn);
|
this.div.appendChild(zoomIn);
|
||||||
this.div.appendChild(zoomAll);
|
|
||||||
|
|
||||||
zoomAll.classList.add(WIDE_CLASSNAME);
|
|
||||||
currentScale.classList.add(WIDE_CLASSNAME);
|
|
||||||
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
|
||||||
|
|
||||||
zoomOut.innerHTML = 'Zoom<br>Out';
|
zoomOut.innerHTML = 'Zoom<br>Out';
|
||||||
zoomIn.innerHTML = 'Zoom<br>In';
|
zoomIn.innerHTML = 'Zoom<br>In';
|
||||||
zoomAll.innerHTML = 'Zoom to Fit';
|
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
|
||||||
|
|
||||||
this.sim.onZoom(() => {
|
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
|
||||||
});
|
|
||||||
|
|
||||||
zoomOut.addEventListener('click', () => {
|
zoomOut.addEventListener('click', () => {
|
||||||
// Aim at center of view
|
// Aim at center of view
|
||||||
@ -54,35 +30,5 @@ export class Zoom extends Tool {
|
|||||||
const y = this.sim.display.height * this.sim.display.scale / 2;
|
const y = this.sim.display.height * this.sim.display.scale / 2;
|
||||||
this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_IN_FACTOR);
|
this.sim.scheduleZoom(this.sim.screenToSim(x, y), ZOOM_IN_FACTOR);
|
||||||
});
|
});
|
||||||
|
|
||||||
zoomAll.addEventListener('click', () => {
|
|
||||||
// Determine bounding box
|
|
||||||
const box = this.sim.objects.boundingBox;
|
|
||||||
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;
|
|
||||||
const heightRatio = Math.abs(box.start.y - box.end.y) / this.sim.display.height;
|
|
||||||
const biggerRatio = Math.max(widthRatio, heightRatio);
|
|
||||||
const base2factor = Math.log(1/biggerRatio) / Math.log(2) - 1;
|
|
||||||
const factor = Math.floor(base2factor);
|
|
||||||
// Determine average momentum and set panning velocity to match
|
|
||||||
const netMomentum = {x: 0, y: 0};
|
|
||||||
let totalMass = 0;
|
|
||||||
let count = 0;
|
|
||||||
this.sim.objects.forEachObject(obj => {
|
|
||||||
count++;
|
|
||||||
netMomentum.x += obj.mass * obj.velocity.x;
|
|
||||||
netMomentum.y += obj.mass * obj.velocity.y;
|
|
||||||
totalMass += obj.mass;
|
|
||||||
});
|
|
||||||
if (!count) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const netVelocity = {
|
|
||||||
x: netMomentum.x / totalMass,
|
|
||||||
y: netMomentum.y / totalMass,
|
|
||||||
};
|
|
||||||
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
toolbar.js
17
toolbar.js
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
TOOLBAR_CLASSNAME,
|
TOOLBAR_CLASSNAME,
|
||||||
|
TOOLBAR_EXPANDED_SUFFIX,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import {Header} from './tool/header.js';
|
import {Header} from './tool/header.js';
|
||||||
|
|
||||||
@ -8,10 +9,17 @@ export class Toolbar {
|
|||||||
tools = [];
|
tools = [];
|
||||||
expanded = undefined;
|
expanded = undefined;
|
||||||
header = undefined;
|
header = undefined;
|
||||||
|
title = undefined;
|
||||||
|
|
||||||
constructor(sim, title, { expanded } = {}) {
|
// TODO: Index on something more durable than title
|
||||||
|
getExpandedKey() {
|
||||||
|
return [this.title, TOOLBAR_EXPANDED_SUFFIX].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sim, title) {
|
||||||
this.sim = sim;
|
this.sim = sim;
|
||||||
this.expanded = expanded ?? true;
|
this.title = title;
|
||||||
|
this.expanded = window.localStorage.getItem(this.getExpandedKey()) === 'true';
|
||||||
|
|
||||||
// Create ourselves a div, as child of sim's div
|
// Create ourselves a div, as child of sim's div
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@ -22,6 +30,9 @@ export class Toolbar {
|
|||||||
const header = new Header(this, title);
|
const header = new Header(this, title);
|
||||||
this.header = header;
|
this.header = header;
|
||||||
this.addTool(header);
|
this.addTool(header);
|
||||||
|
|
||||||
|
header.updateButton();
|
||||||
|
this.applyExpanded();
|
||||||
}
|
}
|
||||||
|
|
||||||
// tool: instance of Tool
|
// tool: instance of Tool
|
||||||
@ -38,8 +49,10 @@ export class Toolbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyExpanded() {
|
applyExpanded() {
|
||||||
|
window.localStorage.setItem(this.getExpandedKey(), this.expanded);
|
||||||
for (const tool of this.tools) {
|
for (const tool of this.tools) {
|
||||||
if (tool === this.header) continue;
|
if (tool === this.header) continue;
|
||||||
|
|
||||||
if (this.expanded) {
|
if (this.expanded) {
|
||||||
if (!this.div.contains(tool.div)) {
|
if (!this.div.contains(tool.div)) {
|
||||||
this.div.appendChild(tool.div);
|
this.div.appendChild(tool.div);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user