separate parameters section

This commit is contained in:
Lentil Hoffman 2025-12-29 11:40:34 -06:00
parent 56414a7ca1
commit 1b14843070
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
10 changed files with 213 additions and 120 deletions

View File

@ -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
View File

@ -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 .

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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');
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
View 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();
}
}
}

View File

@ -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;
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() {