separate parameters section
This commit is contained in:
parent
56414a7ca1
commit
1b14843070
@ -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
|
||||
|
||||
10
commit
10
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 .
|
||||
|
||||
@ -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';
|
||||
|
||||
90
options.js
Normal file
90
options.js
Normal file
@ -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<br>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;
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
66
simulator.js
66
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));
|
||||
}
|
||||
|
||||
12
style.css
12
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
31
toolbar-group.js
Normal file
31
toolbar-group.js
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
toolbar.js
15
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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user