collapsable toolbars

This commit is contained in:
Ladd 2025-12-28 15:19:58 -06:00
parent 54868aeba8
commit a6eda0226b
14 changed files with 223 additions and 152 deletions

View File

@ -2,7 +2,7 @@
export const DISPLAY_OBJECTS_INFO = false;
export const DISPLAY_CURSOR_INFO = false;
export const DISPLAY_CANVAS_SIZE = false;
export const DISPLAY_CURRENT_SCALE = true;
export const DISPLAY_CURRENT_SCALE = false;
export const DISPLAY_CURRENT_MODE = false;
export const DISPLAY_VELOCITY_VECTORS = true;
export const DISPLAY_ACCELERATION_VECTORS = true;
@ -21,7 +21,6 @@ export const ACCELERATION_VECTOR_WIDTH = 1.5;
export const ACCELERATION_VECTOR_ARROWHEAD = true;
// PATH TRACES
// export const PATH_TRACES_COLOR = 'rgb(128, 128, 0)'; // optionally set to 'object color'
export const PATH_TRACES_COLOR = 'object color';
export const PATH_TRACES_OPACITY = 0.8;
export const PATH_TRACES_WIDTH = 1.5;
@ -46,13 +45,15 @@ export const SCALE_POWER_MIN = -8;
export const GRAVITATIONAL_CONSTANT = 1E5;
// CSS CLASS NAMES
export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element';
export const TOOL_CLASSNAME = 'lhg-tool';
export const TOOL_INFO_CLASSNAME = 'lhg-tool-info';
export const TOOLBAR_CLASSNAME = 'lhg-toolbar';
export const WIDE_CLASSNAME = 'lhg-wide';
// EVENT NAMES
export const EVENT_MODE_LEAVE = 'lhg-mode-leave';
export const EVENT_MODE_ENTER = 'lhg-mode-enter';
export const EVENT_ZOOM = 'lhg-zoom-event';
// MODES
export const MODE_MASS_GENERATION = 'mass-gen';

View File

@ -3,60 +3,7 @@
<head>
<title>Gravity Simulator</title>
<meta name="viewport" content="width=device-width" />
<style>
body {
background-color: #000;
color: #8f8;
font-family: monospace;
font-size: 12pt;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
div[id=simulator] {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
button {
width: 8em;
border-radius: 0.5EM;
border-color: #000;
border-width: 2px;
/* margin-left: 0.5em; */
/* margin-right: 0.5em; */
/* padding-top: 0; */
/* padding-bottom: 0; */
}
button.wide {
width: 16em;
}
div.lhg-tool {
position: relative;
top: 0;
left: 0;
width: 14EM;
/* border: 1px #0fb solid; */
/* margin: 1EM; */
padding: 1EM;
}
div.lhg-toolbar {
position: fixed;
z-index: 2;
width: fit-content;
/* border: 2px #00f dashed; */
/* margin: '1EM'; */
/* padding: '1EM'; */
}
</style>
<link rel="stylesheet" href="./style.css" />
<script type="module">
import { Sim } from './simulator.js';
const sim = new Sim('simulator');

View File

@ -1,5 +1,3 @@
import {DRAGGABLE_ELEMENT_CLASSNAME} from './config.js';
export class Overlay {
sim = undefined;
constructor(sim) {
@ -12,10 +10,9 @@ export class Overlay {
infoBox.style.position = 'relative';
infoBox.style.display = 'inline-block';
infoBox.style.top = 0;
infoBox.style.left = 0;
infoBox.style.left = '14em';
infoBox.width = 'fit-content';
infoBox.style.zIndex = 1;
infoBox.classList.add(DRAGGABLE_ELEMENT_CLASSNAME);
}
renderInfo() {
@ -36,23 +33,4 @@ export class Overlay {
}
this.infoBox.appendChild(table);
}
// Update positions of draggable items
updateDraggable() {
const elements = document.querySelectorAll(`.${DRAGGABLE_ELEMENT_CLASSNAME}`);
for (let el of elements) {
if (!el.dragging) continue;
const {
elementStart: {x: x0, y: y0},
pointerStart: {x: x1, y: y1},
pointerEnd: {x: x2, y: y2}
} = el.dragging;
const dx = x2 - x1;
const dy = y2 - y1;
const left = x0 + dx;
const top = y0 + dy;
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
}
}

View File

@ -1,6 +1,5 @@
import {
DISPLAY_CURSOR_INFO,
DRAGGABLE_ELEMENT_CLASSNAME,
MODE_MASS_GENERATION,
MODE_OBJECT_SELECT,
MODE_PAN_VIEW,
@ -13,7 +12,6 @@ export class Pointer {
sim = undefined;
pointerHistory = [];
draggingElement = undefined;
panning = undefined; // { velocity: {x and y in sim coordinates}, paused: boolean }
panTouchStart = undefined; // {x: undefined, y: undefined, t: undefined};
panTouchLatest = undefined; // {x: undefined, y: undefined, t: undefined};
@ -29,53 +27,20 @@ export class Pointer {
if (DISPLAY_CURSOR_INFO) {
this.sim.info['pointermove'] = [`${e.clientX}, `, `${e.clientY}`];
}
if (this.draggingElement) {
this.draggingElement.dragging.pointerEnd = {
x: e.clientX,
y: e.clientY,
};
} else {
this.handlePointerMove({x: e.clientX, y: e.clientY});
}
this.handlePointerMove({x: e.clientX, y: e.clientY});
});
el.addEventListener('pointerdown', e => {
// If this is a child of a draggable element, handle dragging
let target = e.target;
while (target && !target.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) {
target = target.parentElement;
if (target.nodeName.toLowerCase() === 'button') {
return;
}
if (target?.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) {
this.draggingElement = target;
this.draggingElement.dragging = {
elementStart: {
x: parseInt(this.draggingElement.style.left),
y: parseInt(this.draggingElement.style.top),
},
pointerStart: {
x: e.clientX,
y: e.clientY,
},
pointerEnd: {
x: e.clientX,
y: e.clientY,
},
};
} else {
this.handlePointerDown({x: e.clientX, y: e.clientY});
}
this.handlePointerDown({x: e.clientX, y: e.clientY});
});
el.addEventListener('pointerup', e => {
if (this.draggingElement) {
this.draggingElement.dragging = undefined;
this.draggingElement = undefined;
this.lastPosition = {x: undefined, y: undefined};
} else {
this.handlePointerUp({x: e.clientX, y: e.clientY});
}
this.handlePointerUp({x: e.clientX, y: e.clientY});
});
// Monitor wheel events

View File

@ -13,6 +13,7 @@ import {
DISPLAY_CURRENT_SCALE,
DISPLAY_CURRENT_MODE,
MOTION_TIME_SCALE,
EVENT_ZOOM,
} from './config.js';
export class Sim {
@ -41,9 +42,10 @@ export class Sim {
this.div = div;
this.display = new Display(this);
this.overlay = new Overlay(this);
this.objects = new Objects(this);
this.toolbar = new Toolbar(this);
this.toolbar = new Toolbar(this, 'Tools');
this.toolbar2 = new Toolbar(this, 'Options');
this.overlay = new Overlay(this);
// Set up toolbar
this.toolbar.addTool(new Zoom(this.toolbar));
@ -51,7 +53,7 @@ export class Sim {
this.toolbar.addTool(new ModeSwitch(this.toolbar));
// Set up second toolbar
this.toolbar2 = new Toolbar(this).topRight();
this.toolbar2.topRight();
this.toolbar2.addTool(new Options(this.toolbar));
this.pointer = new Pointer(this);
@ -92,6 +94,9 @@ export class Sim {
}
};
}
const e = new CustomEvent(EVENT_ZOOM);
this.div.dispatchEvent(e);
}
// Transform display coordinates to simulator coordinates using scale and viewOrigin
@ -107,6 +112,18 @@ export class Sim {
this.playing = false;
}
getScaleDisplay() {
const scale = 2 ** Math.abs(this.display.scalePower);
return this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
}
// cb: () => undefined
onZoom(cb) {
this.div.addEventListener(EVENT_ZOOM, () => {
cb();
});
}
// Main loop
loop(currentTime) {
const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE;
@ -122,16 +139,12 @@ export class Sim {
}
if (DISPLAY_CURRENT_SCALE) {
const scale = 2 ** Math.abs(this.display.scalePower);
this.info['Scale'] = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
this.info['Scale'] = this.getScaleDisplay();
}
this.pointer.computeFrame(elapsedTime);
this.objects.computeFrame(elapsedTime);
this.overlay.updateDraggable();
this.overlay.renderInfo();
this.display.fillCanvas();
this.display.drawObjects();

91
style.css Normal file
View File

@ -0,0 +1,91 @@
body {
background-color: #000;
color: #5f5;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
h1, h2, h3, h4, h5, h6 {
margin-block-start: 4pt;
margin-block-end: 4pt;
text-align: center;
font-size: 12pt;
font-weight: normal;
}
h1 {
font-size: 16pt;
}
div[id=simulator] {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
div.lhg-toolbar {
position: fixed;
z-index: 2;
width: fit-content;
border-radius: 0.5EM;
}
div.lhg-toolbar div.lhg-tool {
position: relative;
top: 0;
left: 0;
width: 12EM;
padding: 0.5EM;
margin: 0.5EM;
border-radius: 0.5EM;
border-width: 1px;
border-style: solid;
border-color: #282;
}
div.lhg-tool button {
font-family: monospace;
font-size: 10pt;
width: 6em;
background-color: #333;
color: #5f5;
border-radius: 0.5EM;
border-color: #000;
border-width: 4px;
border-style: solid;
padding-top: 0.5EM;
padding-bottom: 0.5EM;
padding-left: 0EM;
padding-right: 0EM;
text-align: center;
/* margin-top: 0.125EM;
margin-bottom: 0.125EM; */
margin-left: 0;
margin-right: 0;
}
div.lhg-tool button:hover {
background-color: #444;
}
div.lhg-tool button:active {
background-color: #252;
}
div.lhg-tool button.lhg-tool-info {
background-color: #111;
border-color: #282;
border-width: 0px;
}
div.lhg-tool button.lhg-wide {
width: 12em;
}
div.lhg-tool > div {
/* border: 2px red solid; */
}

14
sync
View File

@ -5,8 +5,18 @@ if [[ $(hostname) != "ladd76" ]]; then
exit 1
fi
rsync -ru --exclude='.git' --exclude='node_modules' ~/code/gravity-dev/ lentilz:code/gravity-dev/
rsync -ru --exclude='.git' --exclude='node_modules' lentilz:code/gravity-dev/ ~/code/gravity-dev/
set -eo pipefail
do_rsync() {
rsync -ru \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.*.sw*' \
"$@"
}
do_rsync ~/code/gravity-dev/ lentilz:code/gravity-dev/
do_rsync lentilz:code/gravity-dev/ ~/code/gravity-dev/
echo >&2
echo >&2 "Synced with https://laddhoffman.com/gravity-dev/"

View File

@ -3,7 +3,6 @@
// through toolbar can access sim
import {
DRAGGABLE_ELEMENT_CLASSNAME,
TOOL_CLASSNAME,
} from './config.js';
@ -17,7 +16,6 @@ export class Tool {
const div = document.createElement('div');
this.div = div;
div.classList.add(TOOL_CLASSNAME)
div.classList.add(DRAGGABLE_ELEMENT_CLASSNAME);
}
frame() {}

49
tool/header.js Normal file
View File

@ -0,0 +1,49 @@
import { Tool } from '../tool.js';
export class Header extends Tool {
expanded = true;
constructor(toolbar, title = 'Tools') {
super(toolbar);
this.title = document.createElement('h1');
this.title.innerHTML = title;
this.toggleButton = document.createElement('button');
this.updateButton();
this.toggleButton.addEventListener('click', () => this.toggle());
this.div.appendChild(this.title);
this.div.appendChild(this.toggleButton);
this.title.style.verticalAlign = 'center';
this.toggleButton.style.width = '3EM';
this.div.style.display = 'flex';
this.div.style.justifyContent = 'space-around';
}
updateButton() {
this.toggleButton.innerHTML = this.expanded ? '˄' : '˅';
}
toggle() {
this.expanded = !this.expanded;
this.updateButton();
this.apply();
}
apply() {
for (const tool of this.toolbar.tools) {
if (tool === this) continue;
if (this.expanded) {
if (!this.toolbar.div.contains(tool.div)) {
this.toolbar.div.appendChild(tool.div);
}
} else {
if (this.toolbar.div.contains(tool.div)) {
this.toolbar.div.removeChild(tool.div);
}
}
}
}
}

View File

@ -6,6 +6,7 @@ import {
MODE_OBJECT_SELECT,
EVENT_MODE_LEAVE,
EVENT_MODE_ENTER,
WIDE_CLASSNAME,
} from '../config.js';
export class ModeSwitch extends Tool {
@ -21,7 +22,11 @@ export class ModeSwitch extends Tool {
super(toolbar);
const modesDiv = document.createElement('div');
const heading = document.createElement('h2');
heading.innerHTML = 'Modes';
// this.div.appendChild(heading);
this.div.appendChild(modesDiv);
modesDiv.style.display = 'flex';
@ -32,8 +37,8 @@ export class ModeSwitch extends Tool {
this.buttons.push(button);
button.modeID = modeID;
modesDiv.appendChild(button);
button.innerHTML = `<h3>${modeTitle}</h3>`;
button.classList.add('wide');
button.innerHTML = modeTitle;
button.classList.add(WIDE_CLASSNAME);
button.addEventListener('click', () => this.setCurrentMode(modeID));
}

View File

@ -6,21 +6,22 @@ import {
PATH_TRACES_DASHED,
PAUSE_DURING_CREATION,
PAUSE_DURING_SELECTION,
WIDE_CLASSNAME,
} from '../config.js';
import {Tool} from '../tool.js';
export class Options extends Tool {
options = [{
type: 'group', name: 'pauseDuring', title: 'Pause During Mass',
type: 'group', name: 'pauseDuring', title: 'Pause During',
items: [
{type: 'boolean', name: 'creation', title: 'Creation', default: PAUSE_DURING_CREATION},
{type: 'boolean', name: 'selection', title: 'Selection', default: PAUSE_DURING_SELECTION},
{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: 'Acceleration', default: DISPLAY_ACCELERATION_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},
]
@ -29,8 +30,7 @@ export class Options extends Tool {
items: [
{type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true},
]
},
];
}];
values = {};
@ -50,9 +50,9 @@ export class Options extends Tool {
}
case 'boolean': {
const button = document.createElement('button');
button.innerHTML = `<h4>${item.title}</h4>`;
button.innerHTML = item.title;
if (item.wide === true) {
button.classList.add('wide');
button.classList.add(WIDE_CLASSNAME);
}
this.setOption(path, item.default);
button.style.opacity = this.values[path] ? '100%' : '50%';
@ -71,7 +71,7 @@ export class Options extends Tool {
super(toolbar);
const heading = document.createElement('h2');
heading.innerHTML = 'Options';
this.div.appendChild(heading);
// this.div.appendChild(heading);
for (const item of this.options) {
const child = this.visitItem(item);
this.div.appendChild(child);

View File

@ -1,8 +1,8 @@
import { Tool } from '../tool.js';
export class PlayPause extends Tool {
playHTML = '<h2>Play</h2>';
pauseHTML = '<h2>Pause</h2>';
playHTML = 'Play';
pauseHTML = 'Pause';
constructor(toolbar) {
super(toolbar);

View File

@ -1,5 +1,7 @@
import { Tool } from '../tool.js';
import {
TOOL_INFO_CLASSNAME,
WIDE_CLASSNAME,
ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR,
} from '../config.js';
@ -8,21 +10,28 @@ export class Zoom extends Tool {
constructor(toolbar) {
super(toolbar);
const currentScale = document.createElement('button')
const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button');
const zoomAll = document.createElement('button');
this.div.appendChild(currentScale);
this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn);
this.div.appendChild(document.createElement('br'));
this.div.appendChild(zoomAll);
this.div.appendChild(document.createElement('br'));
zoomAll.classList.add('wide');
zoomAll.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(TOOL_INFO_CLASSNAME);
zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>';
zoomIn.innerHTML = '<h2>Zoom<br>In</h2>';
zoomAll.innerHTML = '<h2>Zoom to Fit</h2>';
zoomOut.innerHTML = 'Zoom<br>Out';
zoomIn.innerHTML = 'Zoom<br>In';
zoomAll.innerHTML = 'Zoom to Fit';
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
this.sim.onZoom(() => {
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
});
zoomOut.addEventListener('click', () => {
// Aim at center of view

View File

@ -1,12 +1,13 @@
import {
TOOLBAR_CLASSNAME,
} from './config.js';
import {Header} from './tool/header.js';
export class Toolbar {
sim = undefined;
tools = [];
constructor(sim) {
constructor(sim, title) {
this.sim = sim;
// Create ourselves a div, as child of sim's div
@ -14,6 +15,10 @@ export class Toolbar {
this.div = div;
this.sim.div.appendChild(div);
div.classList.add(TOOLBAR_CLASSNAME);
// Create a collapse/expand tool
const header = new Header(this, title);
this.addTool(header);
}
topRight() {