diff --git a/Readme.md b/Readme.md index 17ed70f..21efbdf 100644 --- a/Readme.md +++ b/Readme.md @@ -8,8 +8,6 @@ Uses `npm` for `eslint`. TODO ---- -- [x] Numeric Option Type -- [x] Time Indicator - [ ] Selection Box - [ ] Object List - [ ] Object Detail @@ -19,9 +17,11 @@ TODO - [ ] Zoom Easing - [ ] 2-touch Pan & Zoom - [ ] Multi-touch Mass Create -- [x] Tool to Clear Traces - [ ] Undo feature: - [ ] Undo "Clear Traces" Action - [ ] Undo "Reset - [ ] Save to LocalStorage - [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow) +- [ ] Track farthest reaches, min/max in each dimension (x, y) + - [ ] Enabling Zoom to Fit Traces +- [ ] Tool: Zero Angular Momentum diff --git a/commit b/commit index f253847..5020aa7 100755 --- a/commit +++ b/commit @@ -1,15 +1,11 @@ #!/bin/env bash - -if [[ $(hostname) != "ladd76" ]]; then - echo >&2 "host $(hostname) != 'ladd76'" - exit 1 -fi - set -eo pipefail git checkout dev -./sync +if [[ $(hostname) == "ladd76" ]]; then + ./sync +fi if git diff; then git add . diff --git a/config.js b/config.js index 117f13c..79636ff 100644 --- a/config.js +++ b/config.js @@ -56,6 +56,7 @@ export const WIDE_CLASSNAME = 'lhg-wide'; export const EVENT_MODE_LEAVE = 'lhg-mode-leave'; export const EVENT_MODE_ENTER = 'lhg-mode-enter'; export const EVENT_ZOOM = 'lhg-zoom-event'; +export const EVENT_OPTION_SET = 'lhg-option-set'; // MODES export const MODE_MASS_GENERATION = 'mass-gen'; diff --git a/options.js b/options.js new file mode 100644 index 0000000..24625dd --- /dev/null +++ b/options.js @@ -0,0 +1,90 @@ +import { + DISPLAY_ACCELERATION_VECTORS, + DISPLAY_VELOCITY_VECTORS, + GRAVITATIONAL_CONSTANT, + MASS_CREATION_RATE, + MERGE_ON_COLLIDE, + MOTION_TIME_SCALE, + PATH_TRACES_DASHED, + PAUSE_DURING_CREATION, + PAUSE_DURING_SELECTION, + EVENT_OPTION_SET, +} from './config.js'; + +export const optionsLayout = { + pauseDuring: { + creation: ['Pause While Creating', 'boolean', PAUSE_DURING_CREATION], + creation2: ['Pause While Creating', 'boolean', PAUSE_DURING_CREATION], + selection: ['Pause While Selecting', 'boolean', PAUSE_DURING_SELECTION], + }, + display: { + velocity: ['Velocity Vectors', 'boolean', DISPLAY_VELOCITY_VECTORS], + acceleration: ['Accel. Vectors', 'boolean', DISPLAY_ACCELERATION_VECTORS], + traces: ['Path Traces', 'boolean', DISPLAY_ACCELERATION_VECTORS], + dashedTraces: ['Dashed Traces', 'boolean', PATH_TRACES_DASHED], + }, + collision: { + merge: ['Merge Masses
on Collision', 'boolean', MERGE_ON_COLLIDE, {wide: true}], + }, + param: { + gravity: ['Gravity', 'number', GRAVITATIONAL_CONSTANT], + timeScale: ['Time Scale', 'number', MOTION_TIME_SCALE], + massCreationRate: ['Mass Creation Rate', 'number', MASS_CREATION_RATE], + } +}; + +export class Options { + sim = undefined; + values = {}; + + constructor(sim) { + this.sim = sim; + + // Global methods to get/set current option values + this.sim.getOption = (path) => this.getOption(path); + this.sim.setOption = (path, value) => this.setOption(path, value); + this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb); + } + + getOption(path) { + const val = this.values[path]; + return val; + } + + setOption(path, value) { + this.values[path] = value; + const e = new CustomEvent(EVENT_OPTION_SET, {detail: {path, value}}); + this.sim.div.dispatchEvent(e); + } + + // cb: (value) => undefined + onOptionSet(path, cb) { + this.sim.div.addEventListener(EVENT_OPTION_SET, (e) => { + if (path === e.detail.path) { + cb(e.detail.value); + } + }); + } + + static getSection(layout, sectionName) { + const section = layout[sectionName]; + const group = { + type: 'group', + name: sectionName, + title: section._title, + items: [], + }; + for (const name in section) { + if (name.startsWith('_')) continue; + const [title, type, defaultValue, opts] = section[name]; + group.items.push({ + name, + type, + title, + default: defaultValue, + ...opts + }) + } + return group; + } +} diff --git a/pointer.js b/pointer.js index 7b0337d..98e17dd 100644 --- a/pointer.js +++ b/pointer.js @@ -4,7 +4,7 @@ import { MODE_OBJECT_SELECT, MODE_PAN_VIEW, POINTER_HISTORY_SIZE, - TOOL_CLASSNAME, + TOOLBAR_CLASSNAME, ZOOM_IN_FACTOR, ZOOM_OUT_FACTOR, } from './config.js'; @@ -33,7 +33,7 @@ export class Pointer { el.addEventListener('pointerdown', e => { let target = e.target; - while (target && !target.classList?.contains(TOOL_CLASSNAME)) { + while (target && !target.classList?.contains(TOOLBAR_CLASSNAME)) { target = target.parentNode; } if (target) { diff --git a/simulator.js b/simulator.js index f26c4b8..da112a3 100644 --- a/simulator.js +++ b/simulator.js @@ -5,15 +5,17 @@ import { SCALE_POWER_MAX, SCALE_POWER_MIN, } from './config.js'; -import {Display} from './display.js'; -import {Objects} from './objects.js'; -import {Overlay} from './overlay.js'; -import {Pointer} from './pointer.js'; -import {ModeSwitch} from './tool/modes.js'; -import {Options} from './tool/options.js'; -import {PlayPause} from './tool/play-pause.js'; -import {Zoom} from './tool/zoom.js'; -import {Toolbar} from './toolbar.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 { Toolbar } from './toolbar.js'; +import { ToolbarGroup } from './toolbar-group.js'; export class Sim { info = {}; @@ -26,8 +28,8 @@ export class Sim { overlay = undefined; pointer = undefined; objects = undefined; - toolbar = undefined; - toolbar2 = undefined; + toolbars = {}; + toolbarGroups = {}; isCurrentMode = () => undefined; getCurrentMode = () => undefined; @@ -41,23 +43,37 @@ export class Sim { const div = document.getElementById(this.divId); this.div = div; + this.options = new Options(this); this.display = new Display(this); this.objects = new Objects(this); - this.toolbar = new Toolbar(this, 'Tools'); - this.toolbar2 = new Toolbar(this, 'Options'); + this.toolbarGroups = { + left: new ToolbarGroup(this), + right: new ToolbarGroup(this).topRight(), + }; + this.toolbars = { + tools: new Toolbar(this, 'Tools', this.toolbarGroups.left), + modes: new Toolbar(this, 'Modes', this.toolbarGroups.left), + options: new Toolbar(this, 'Options', this.toolbarGroups.right), + params: new Toolbar(this, 'Parameters', this.toolbarGroups.right), + } this.overlay = new Overlay(this); - - // Set up toolbar - this.toolbar.addTool(new Zoom(this.toolbar)); - this.toolbar.addTool(new PlayPause(this.toolbar)); - this.toolbar.addTool(new ModeSwitch(this.toolbar)); - - // Set up second toolbar - this.toolbar2.topRight(); - this.toolbar2.addTool(new Options(this.toolbar)); - this.pointer = new Pointer(this); + // Primary Toolbar + this.toolbars.tools.addTool(new Zoom(this.toolbars.tools)); + this.toolbars.tools.addTool(new PlayPause(this.toolbars.tools)); + + // Secondary Toolbar; Mode Switches + this.toolbars.modes.addTool(new ModeSwitch(this.toolbars.modes)); + + // Options Toolbar + this.toolbars.options.addTool(new OptionsTool(this.toolbars.options, + ['pauseDuring', 'display', 'collision'])); + + // Parameters Toolbar + this.toolbars.params.addTool(new OptionsTool(this.toolbars.options, + ['param'])); + // Initiate main loop this.rawTime = document.timeline.currentTime / 1000; this.time = 0; @@ -151,7 +167,9 @@ export class Sim { this.overlay.renderInfo(); this.display.fillCanvas(); this.display.drawObjects(); - this.toolbar.frame(); + for (const group in this.toolbarGroups) { + this.toolbarGroups[group].frame(); + } requestAnimationFrame(t => this.loop(t)); } diff --git a/style.css b/style.css index 0b29af6..245089b 100644 --- a/style.css +++ b/style.css @@ -28,10 +28,14 @@ div[id=simulator] { } div.lhg-toolbar { - position: fixed; + position: relative; z-index: 2; width: fit-content; + margin: 0.5EM; border-radius: 0.5EM; + border-width: 1px; + border-color: #282; + border-style: solid; } div.lhg-tool { @@ -39,12 +43,8 @@ div.lhg-tool { top: 0; left: 0; width: 12EM; - padding: 0.5EM; + /* padding: 0.5EM; */ margin: 0.5EM; - border-radius: 0.5EM; - border-width: 1px; - border-style: solid; - border-color: #282; text-align: middle; } diff --git a/tool/options.js b/tool/options.js index 6738de8..5dd0163 100644 --- a/tool/options.js +++ b/tool/options.js @@ -1,58 +1,22 @@ // Options picker import { - DISPLAY_ACCELERATION_VECTORS, - DISPLAY_VELOCITY_VECTORS, - GRAVITATIONAL_CONSTANT, - MASS_CREATION_RATE, - MERGE_ON_COLLIDE, - MOTION_TIME_SCALE, - PATH_TRACES_DASHED, - PAUSE_DURING_CREATION, - PAUSE_DURING_SELECTION, TOOL_INFO_CLASSNAME, WIDE_CLASSNAME, } from '../config.js'; -import {Tool} from '../tool.js'; - -export class Options extends Tool { - options = [{ - type: 'group', name: 'pauseDuring', title: 'Pause During', - items: [ - {type: 'boolean', name: 'creation', title: 'Create', default: PAUSE_DURING_CREATION}, - {type: 'boolean', name: 'selection', title: 'Select', default: PAUSE_DURING_SELECTION}, - ] - }, { - type: 'group', name: 'display', title: 'Display', - items: [ - {type: 'boolean', name: 'velocity', title: 'Velocity', default: DISPLAY_VELOCITY_VECTORS}, - {type: 'boolean', name: 'acceleration', title: 'Accel', default: DISPLAY_ACCELERATION_VECTORS}, - {type: 'boolean', name: 'traces', title: 'Traces', default: DISPLAY_ACCELERATION_VECTORS}, - {type: 'boolean', name: 'dashedTraces', title: 'Dashed', default: PATH_TRACES_DASHED}, - ] - }, { - type: 'group', name: 'collision', title: 'Collision', - items: [ - {type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true}, - ] - }, { - type: 'group', name: 'param', title: 'Parameters', - items: [ - {type: 'number', name: 'gravity', title: 'Gravity', default: GRAVITATIONAL_CONSTANT}, - {type: 'number', name: 'timeScale', title: 'Time Scale', default: MOTION_TIME_SCALE}, - {type: 'number', name: 'massCreationRate', title: 'Mass Creation Rate', default: MASS_CREATION_RATE}, - ] - }]; - - values = {}; +import { Tool } from '../tool.js'; +import { Options, optionsLayout } from '../options.js'; +export class OptionsTool extends Tool { visitItem(item, path) { path = [path, item.name].filter(x => !!x).join('.'); switch (item.type) { case 'group': { const group = document.createElement('div'); - const heading = document.createElement('h3'); - heading.innerHTML = item.title; - group.appendChild(heading); + if (item.title) { + const heading = document.createElement('h3'); + heading.innerHTML = item.title; + group.appendChild(heading); + } for (const next of item.items) { const child = this.visitItem(next, path); group.appendChild(child); @@ -65,11 +29,14 @@ export class Options extends Tool { if (item.wide === true) { button.classList.add(WIDE_CLASSNAME); } - this.setOption(path, item.default); - button.style.opacity = this.values[path] ? '100%' : '50%'; + this.sim.setOption(path, item.default); + button.style.opacity = this.sim.getOption(path) ? '100%' : '50%'; button.addEventListener('click', () => { - this.setOption(path, !this.getOption(path)); - button.style.opacity = this.values[path] ? '100%' : '50%'; + this.sim.setOption(path, !this.sim.getOption(path)); + button.style.opacity = this.sim.getOption(path) ? '100%' : '50%'; + }); + this.sim.onOptionSet(path, value => { + button.style.opacity = value ? '100%' : '50%'; }); return button; } @@ -88,44 +55,35 @@ export class Options extends Tool { } title.innerHTML = item.title; input.value = item.default; - this.setOption(path, item.default); + this.sim.setOption(path, item.default); input.addEventListener('input', () => { input.value = input.value.slice(0, maxLength); }); input.addEventListener('change', () => { - this.setOption(path, input.value); + this.sim.setOption(path, input.value); }); + + this.sim.onOptionSet(path, value => { + input.value = value; + }); + return div; } default: - throw new Error('unknown option type'); + console.error('Unknown option type', item); + throw new Error('Unknown option type'); } } - constructor(toolbar) { + constructor(toolbar, sections) { super(toolbar); - const heading = document.createElement('h2'); - heading.innerHTML = 'Options'; - // this.div.appendChild(heading); - for (const item of this.options) { + + for (const sectionName of sections) { + const item = Options.getSection(optionsLayout, sectionName); const child = this.visitItem(item); this.div.appendChild(child); } - - // Global methods to get/set current option values - this.sim.getOption = (path) => this.getOption(path); - this.sim.setOption = (path, value) => this.setOption(path, value); - } - - getOption(path) { - const val = this.values[path]; - return val; - } - - setOption(path, value) { - this.values[path] = value; - // TODO Update button opacity in case this is called from elsewhere } } diff --git a/toolbar-group.js b/toolbar-group.js new file mode 100644 index 0000000..7fa9b90 --- /dev/null +++ b/toolbar-group.js @@ -0,0 +1,31 @@ +export class ToolbarGroup { + sim = undefined; + toolbars = []; + + constructor(sim) { + this.sim = sim; + const div = document.createElement('div'); + this.div = div; + this.sim.div.appendChild(div); + } + + topRight() { + this.div.style.position = 'fixed'; + this.div.style.top = '0px'; + this.div.style.right = '0px'; + return this; + } + + addToolbar(toolbar) { + this.div.appendChild(toolbar.div); + this.toolbars.push(toolbar); + return this; + } + + frame() { + for (let toolbar of this.toolbars) { + toolbar.frame(); + } + } +} + diff --git a/toolbar.js b/toolbar.js index 941787e..8ffa0b1 100644 --- a/toolbar.js +++ b/toolbar.js @@ -7,13 +7,17 @@ export class Toolbar { sim = undefined; tools = []; - constructor(sim, title) { + constructor(sim, title, group) { this.sim = sim; // Create ourselves a div, as child of sim's div const div = document.createElement('div'); this.div = div; - this.sim.div.appendChild(div); + if (group) { + group.addToolbar(this); + } else { + this.sim.div.appendChild(div); + } div.classList.add(TOOLBAR_CLASSNAME); // Create a collapse/expand tool @@ -21,16 +25,11 @@ export class Toolbar { this.addTool(header); } - topRight() { - this.div.style.top = '0px'; - this.div.style.right = '0px'; - return this; - } - // tool: instance of Tool addTool(tool) { this.div.appendChild(tool.div); this.tools.push(tool); + return this; } frame() {