diff --git a/config.js b/config.js index b44f303..dcfb7df 100644 --- a/config.js +++ b/config.js @@ -39,6 +39,7 @@ export const TOOLBAR_HEADER_CLASSNAME = 'lhg-toolbar-header'; export const WIDE_CLASSNAME = 'lhg-wide'; export const TALL_CLASSNAME = 'lhg-tall'; export const OVERLAY_INFO_BOX_CLASSNAME = 'lhg-overlay-info-box'; +export const OPTION_GROUP_CLASSNAME = 'lhg-option-group'; // EVENT NAMES export const EVENT_MODE_LEAVE = 'lhg-mode-leave'; diff --git a/objects.js b/objects.js index 56233eb..42ec672 100644 --- a/objects.js +++ b/objects.js @@ -208,14 +208,16 @@ export class Objects { // with the mass creation rate accelerating over time // Scaling this parameter because of millisecond conversion - const massCreationRate = this.sim.getOption('param.massCreationRate') / 1000; if (this.creatingObject !== undefined) { const obj = this.objects[this.creatingObject]; - 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 - obj.mass += rate * elapsedTime; + // Putting in a somewhat arbitrary scaling factor here + let massCreationRate = this.sim.getOption('param.massCreationRate') / 1000; + // Mass creation rate acceleration + if (this.sim.getOption('param.massAcceleration')) { + massCreationRate *= obj.age; + } + obj.mass += massCreationRate * elapsedTime; } // Calculate forces due to gravity. diff --git a/options.js b/options.js index 5381ad1..f21117d 100644 --- a/options.js +++ b/options.js @@ -24,29 +24,38 @@ export class Options { 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) + let value = this.getFromLocalStorage(path); if (value === undefined) { value = defaultValue; - this.setOption(path, value); } + this.values[path] = value; } } } - getOption(path) { - let value = this.values[path]; - if (value === undefined) { - value = localStorage.getItem(this.getStorageKey(path)); - if (value === 'false') value = false; - else if (value === 'true') value = true; - this.values[path] = value; - } + toStored(value) { + return JSON.stringify(value); + } + + fromStored(value) { + return JSON.parse(value); + } + + getFromLocalStorage(path) { + const storageKey = this.getStorageKey(path); + const value = this.fromStored(window.localStorage.getItem(storageKey)); + this.values[path] = value; return value; } + getOption(path) { + return this.values[path]; + } + setOption(path, value) { this.values[path] = value; - window.localStorage.setItem(this.getStorageKey(path), value); + const storageKey = this.getStorageKey(path); + window.localStorage.setItem(storageKey, this.toStored(value)); const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}}); this.sim.div.dispatchEvent(e); } diff --git a/sim-options.js b/sim-options.js new file mode 100644 index 0000000..64b3c49 --- /dev/null +++ b/sim-options.js @@ -0,0 +1,29 @@ +export const simOptions = { + pauseDuring: { + creation: ['Pause While Creating', 'boolean', true], + selection: ['Pause While Selecting', 'boolean', true], + }, + display: { + velocity: ['Velocity Vector', 'boolean', true], + acceleration: ['Accel Vector', 'boolean', true], + traces: ['Path Trace', 'boolean', true], + dashedTraces: ['Dashed', 'boolean', false, {tall: true}], + }, + collision: { + merge: ['Merge Masses
on Collision', 'boolean', true, {wide: true}], + }, + param: { + gravity: ['Gravity', 'number', 4E4], + timeScale: ['Time Scale', 'number', 0.2], + massCreationRate: ['Mass Creation Rate', 'number', 10], + massAcceleration: ['Mass Rate Accel', 'boolean', true, {wide: true}], + }, + 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], + }, +}; + diff --git a/sim-tools.js b/sim-tools.js new file mode 100644 index 0000000..4325e88 --- /dev/null +++ b/sim-tools.js @@ -0,0 +1,67 @@ +import { Overlay } from './overlay.js'; +import { Pointer } from './pointer.js'; +import { ModeSwitch } from './tool/modes.js'; +import { OptionsTool } from './tool/options.js'; +import { PlayPause } from './tool/play-pause.js'; +import { Zoom } from './tool/zoom.js'; +import { UtilityTool } from './tool/utility.js'; +import { Toolbar } from './toolbar.js'; +import { ToolbarGroup } from './toolbar-group.js'; + +export function initializeTools(sim) { + sim.toolbars = { + tools: new Toolbar(sim, 'Tools'), + modes: new Toolbar(sim, 'Modes'), + utils: new Toolbar(sim, 'Utility', { expanded: false }), + options: new Toolbar(sim, 'Options'), + params: new Toolbar(sim, 'Parameters'), + debug: new Toolbar(sim, 'Debug', { expanded: false }), + }; + const { tools, modes, options, params, debug, utils } = sim.toolbars; + sim.toolbarGroups = { + left: new ToolbarGroup(sim) + .addToolbar(tools) + .addToolbar(modes) + .addToolbar(utils), + right: new ToolbarGroup(sim).topRight() + .addToolbar(options) + .addToolbar(params) + .addToolbar(debug), + }; + sim.overlay = new Overlay(sim); + sim.pointer = new Pointer(sim); + + // Configure toolbars + + // Primary + tools.addTool(new Zoom(tools)); + tools.addTool(new PlayPause(tools)); + + // Secondary + modes.addTool(new ModeSwitch(modes)); + + // Utility + utils.addTool(new UtilityTool(utils)); + + // Options + options.addTool(new OptionsTool(options, [ + 'pauseDuring', + 'display', + 'collision' + ])); + + // Parameters + params.addTool(new OptionsTool(params, [ + 'param' + ])); + + // Debug + debug.addTool(new OptionsTool(debug, [ + 'debug' + ])); + + for (const id in sim.toolbars) { + const toolbar = sim.toolbars[id]; + toolbar.applyExpanded(); + } +} diff --git a/simulator.js b/simulator.js index 111cae2..02f8d29 100644 --- a/simulator.js +++ b/simulator.js @@ -6,16 +6,9 @@ import { } from './config.js'; import { Display } from './display.js'; import { Objects } from './objects.js'; -import { Overlay } from './overlay.js'; -import { Pointer } from './pointer.js'; import { Options } from './options.js'; -import { ModeSwitch } from './tool/modes.js'; -import { OptionsTool } from './tool/options.js'; -import { PlayPause } from './tool/play-pause.js'; -import { Zoom } from './tool/zoom.js'; -import { UtilityTool } from './tool/utility.js'; -import { Toolbar } from './toolbar.js'; -import { ToolbarGroup } from './toolbar-group.js'; +import { simOptions } from './sim-options.js'; +import { initializeTools } from './sim-tools.js'; const simOptions = { pauseDuring: { @@ -91,53 +84,8 @@ export class Sim { this.options = new Options(this, simOptions); this.display = new Display(this); this.objects = new Objects(this); - this.toolbars = { - tools: new Toolbar(this, 'Tools'), - modes: new Toolbar(this, 'Modes'), - utils: new Toolbar(this, 'Utility', { expanded: false }), - options: new Toolbar(this, 'Options'), - params: new Toolbar(this, 'Parameters'), - debug: new Toolbar(this, 'Debug', { expanded: false }), - }; - const { tools, modes, options, params, debug, utils } = this.toolbars; - this.toolbarGroups = { - left: new ToolbarGroup(this) - .addToolbar(tools) - .addToolbar(modes) - .addToolbar(utils), - right: new ToolbarGroup(this).topRight() - .addToolbar(options) - .addToolbar(params) - .addToolbar(debug), - }; - this.overlay = new Overlay(this); - this.pointer = new Pointer(this); - // Configure toolbars - - // Primary - tools.addTool(new Zoom(tools)); - tools.addTool(new PlayPause(tools)); - - // Secondary - modes.addTool(new ModeSwitch(modes)); - - // Utility - utils.addTool(new UtilityTool(utils)); - - // Options - options.addTool(new OptionsTool(options, ['pauseDuring', 'display', 'collision'])); - - // Parameters - params.addTool(new OptionsTool(params, ['param'])); - - // Debug - debug.addTool(new OptionsTool(debug, ['debug'])); - - for (const id in this.toolbars) { - const toolbar = this.toolbars[id]; - toolbar.applyExpanded(); - } + initializeTools(this); // Initiate main loop this.rawTime = document.timeline.currentTime; diff --git a/style.css b/style.css index 8b713cc..0de439f 100644 --- a/style.css +++ b/style.css @@ -108,8 +108,7 @@ div.lhg-tool .lhg-wide { } div.lhg-tool .lhg-tall { - padding-top: 1em; - padding-bottom: 1em; + height: 3.666em; } div.lhg-overlay-info-box { @@ -119,3 +118,8 @@ div.lhg-overlay-info-box { width: fit-content; z-index: 1; } + +div.lhg-option-group > * { + display: inline-block; + vertical-align: top; +} diff --git a/tool/options.js b/tool/options.js index 206b7a8..7a18e7a 100644 --- a/tool/options.js +++ b/tool/options.js @@ -1,16 +1,29 @@ // Options picker import { TOOL_INFO_CLASSNAME, + OPTION_GROUP_CLASSNAME, WIDE_CLASSNAME, + TALL_CLASSNAME, } from '../config.js'; import { Tool } from '../tool.js'; export class OptionsTool extends Tool { + constructor(container, sections) { + super(container); + + for (const sectionName of sections) { + const option = this.sim.options.getSection(sectionName); + const child = this.visitItem(option); + this.div.appendChild(child); + } + } + visitItem(item, path) { path = [path, item.name].filter(x => !!x).join('.'); switch (item.type) { case 'group': { const group = document.createElement('div'); + group.classList.add(OPTION_GROUP_CLASSNAME); if (item.title) { const heading = document.createElement('h3'); heading.innerHTML = item.title; @@ -25,13 +38,9 @@ export class OptionsTool extends Tool { case 'boolean': { const button = document.createElement('button'); button.innerHTML = item.title; - if (item.wide === true) { - button.classList.add(WIDE_CLASSNAME); - } + if (item.wide === true) button.classList.add(WIDE_CLASSNAME); + if (item.tall === true) button.classList.add(TALL_CLASSNAME); const value = this.sim.getOption(path); - if (value === undefined) { - this.sim.setOption(path, item.default); - } button.style.opacity = value ? '100%' : '50%'; this.sim.onOptionSet(path, value => { console.log('option set cb', path, value); @@ -43,6 +52,10 @@ export class OptionsTool extends Tool { console.log('click, option value', value); this.sim.setOption(path, !value); }); + button.addEventListener('click', () => { + const value = this.sim.getOption(path); + this.setOption(path, !value); + }); return button; } case 'number': { @@ -59,8 +72,7 @@ export class OptionsTool extends Tool { input.classList.add(WIDE_CLASSNAME); } title.innerHTML = item.title; - input.value = item.default; - this.sim.setOption(path, item.default); + input.value = this.sim.getOption(path); input.addEventListener('input', () => { input.value = input.value.slice(0, maxLength); @@ -77,18 +89,7 @@ export class OptionsTool extends Tool { return div; } default: - console.error('Unknown option type', item); throw new Error('Unknown option type'); } } - - constructor(container, sections) { - super(container); - - for (const sectionName of sections) { - const option = this.sim.options.getSection(sectionName); - const child = this.visitItem(option); - this.div.appendChild(child); - } - } }