added zoom buttons. added modes.

This commit is contained in:
Lentil Hoffman 2025-12-26 16:19:34 -06:00
parent da250955fe
commit 2dc55377d3
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
10 changed files with 179 additions and 37 deletions

View File

@ -1,8 +1,10 @@
export const MASS_CREATION_RATE = 0.001;
export const DISPLAY_OBJECTS_INFO = false; export const DISPLAY_OBJECTS_INFO = false;
export const DISPLAY_CURSOR_INFO = false; export const DISPLAY_CURSOR_INFO = false;
export const DISPLAY_CANVAS_SIZE = true;
export const DISPLAY_VELOCITY_VECTORS = true; export const DISPLAY_VELOCITY_VECTORS = true;
export const POINTER_HISTORY_SIZE = 20; export const MASS_CREATION_RATE = 0.001;
export const POINTER_HISTORY_SIZE = 10;
export const VELOCITY_VECTOR_SCALE = 0.2; export const VELOCITY_VECTOR_SCALE = 0.2;
export const VELOCITY_VECTOR_COLOR = 'rgb(150, 150, 150)'; // optionally set to 'object color' export const VELOCITY_VECTOR_COLOR = 'rgb(150, 150, 150)'; // optionally set to 'object color'
export const VELOCITY_VECTOR_WIDTH = 1.5; export const VELOCITY_VECTOR_WIDTH = 1.5;
@ -18,3 +20,6 @@ export const ZOOM_OUT_FACTOR = 0.5;
export const SCALE_MAX = 256; export const SCALE_MAX = 256;
export const SCALE_MIN = 1/256; export const SCALE_MIN = 1/256;
export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element'; export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element';
export const MODE_MASS_GENERATION = 'mass-gen';
export const MODE_PAN_VIEW = 'pan-view';

View File

@ -8,6 +8,7 @@ import {
OFFSCREEN_OBJECT_LINE_SCALE, OFFSCREEN_OBJECT_LINE_SCALE,
OFFSCREEN_OBJECT_LINE_WIDTH, OFFSCREEN_OBJECT_LINE_WIDTH,
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH, OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
DISPLAY_CANVAS_SIZE,
} from './config.js'; } from './config.js';
export class Display { export class Display {
@ -55,7 +56,9 @@ export class Display {
fullscreen() { fullscreen() {
this.canvas.width = document.documentElement.clientWidth; this.canvas.width = document.documentElement.clientWidth;
this.canvas.height = document.documentElement.clientHeight; this.canvas.height = document.documentElement.clientHeight;
// this.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`; if (DISPLAY_CANVAS_SIZE) {
this.sim.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`;
}
} }
fillCanvas() { fillCanvas() {

View File

@ -20,6 +20,17 @@ div[id=simulator] {
left: 0; left: 0;
width: 100%; width: 100%;
} }
button {
width: 8em;
padding-left: 0.5em;
padding-left: 0.5em;
}
button.wide {
width: 16em;
}
</style> </style>
<script type="module"> <script type="module">
import { Sim } from './simulator.js'; import { Sim } from './simulator.js';

View File

@ -78,6 +78,18 @@ export class Objects {
this.deselect(); this.deselect();
} }
handlePointerMove({x, y, vx, vy}) {
// If the cursor moves while creating an object, or while an object is selected,
// update the position and velocity of the object
const obj = this.sim.objects.getSelectedOrCreating();
if (obj !== undefined) {
obj.position.x = x;
obj.position.y = y;
obj.velocity.x = vx;
obj.velocity.y = vy;
}
}
computeFrame(elapsedTime) { computeFrame(elapsedTime) {
// If we're creating an object, increment its mass // If we're creating an object, increment its mass
// with the mass creation rate accelerating over time // with the mass creation rate accelerating over time

View File

@ -4,6 +4,7 @@ import {
ZOOM_OUT_FACTOR, ZOOM_OUT_FACTOR,
DISPLAY_CURSOR_INFO, DISPLAY_CURSOR_INFO,
DRAGGABLE_ELEMENT_CLASSNAME, DRAGGABLE_ELEMENT_CLASSNAME,
MODE_MASS_GENERATION,
} from './config.js'; } from './config.js';
function dispatchEvent(target, eventType, data) { function dispatchEvent(target, eventType, data) {
@ -87,7 +88,8 @@ export class Pointer {
// e.preventDefault(); // e.preventDefault();
// Wheel scroll down => positive deltaY => ZOOM IN // Wheel scroll down => positive deltaY => ZOOM IN
const factor = e.deltaY > 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR; const factor = e.deltaY > 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
this.sim.scheduleZoom(this.sim.screenToSim(e.clientX, e.clientY), factor);
this.sim.scheduleZoom({x: e.clientX, y: e.clientY}, factor);
}); });
} }
@ -123,12 +125,15 @@ export class Pointer {
this.clearPointerHistory(); this.clearPointerHistory();
this.updatePointer({x, y}); this.updatePointer({x, y});
this.sim.objects.handlePointerDown({x, y}); if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
this.sim.objects.handlePointerDown({x, y});
}
} }
handlePointerUp({x, y}) { handlePointerUp({x, y}) {
// TODO: Conditional? if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
this.sim.objects.handlePointerUp({x, y}); this.sim.objects.handlePointerUp({x, y});
}
} }
// Handle cursor (mouse or touch) movement // Handle cursor (mouse or touch) movement
@ -137,14 +142,8 @@ export class Pointer {
this.updatePointer({x, y}); this.updatePointer({x, y});
const {x: vx, y: vy} = this.getPointerVelocity(); const {x: vx, y: vy} = this.getPointerVelocity();
// If the cursor moves while creating an object, or while an object is selected, if (this.sim.isCurrentMode(MODE_MASS_GENERATION)) {
// update the position and velocity of the object this.sim.objects.handlePointerMove({x, y, vx, vy});
const obj = this.sim.objects.getSelectedOrCreating();
if (obj !== undefined) {
obj.position.x = x;
obj.position.y = y;
obj.velocity.x = vx;
obj.velocity.y = vy;
} }
} }
} }

View File

@ -3,7 +3,9 @@ import { Overlay } from './overlay.js';
import { Pointer } from './pointer.js'; import { Pointer } from './pointer.js';
import { Objects } from './objects.js'; import { Objects } from './objects.js';
import { Toolbar } from './toolbar.js'; import { Toolbar } from './toolbar.js';
import { PlayPause } from './tools/play-pause.js'; import { PlayPause } from './tool/play-pause.js';
import { Zoom } from './tool/zoom.js';
import { ModeSwitch } from './tool/modes.js';
import { SCALE_MAX, SCALE_MIN} from './config.js'; import { SCALE_MAX, SCALE_MIN} from './config.js';
export class Sim { export class Sim {
@ -17,6 +19,8 @@ export class Sim {
pointer = undefined; pointer = undefined;
objects = undefined; objects = undefined;
isCurrentMode = () => false;
init(divId) { init(divId) {
this.divId = divId; this.divId = divId;
const div = document.getElementById(this.divId); const div = document.getElementById(this.divId);
@ -24,23 +28,28 @@ export class Sim {
this.display = new Display(this); this.display = new Display(this);
this.overlay = new Overlay(this); this.overlay = new Overlay(this);
this.pointer = new Pointer(this);
this.objects = new Objects(this); this.objects = new Objects(this);
this.pointer = new Pointer(this);
this.toolbar = new Toolbar(this); this.toolbar = new Toolbar(this);
// Set up toolbar // Set up toolbar
this.toolbar.addTool(new Zoom(this.toolbar));
this.toolbar.addTool(new PlayPause(this.toolbar)); this.toolbar.addTool(new PlayPause(this.toolbar));
this.toolbar.addTool(new ModeSwitch(this.toolbar));
// Initiate main loop // Initiate main loop
this.time = document.timeline.currentTime; this.time = document.timeline.currentTime;
requestAnimationFrame(t => this.loop(t)); requestAnimationFrame(t => this.loop(t));
} }
// It's better not to change the scale in the middle of possible frame calculations,
// so use this to schedule it and let the event loop pick it up.
scheduleZoom({x, y}, factor) { scheduleZoom({x, y}, factor) {
this.nextZoom = {x, y, factor}; this.nextZoom = {x, y, factor};
} }
zoom({x, y, factor}) { zoom({x: screenX, y: screenY, factor}) {
const {x, y} = this.screenToSim(screenX, screenY);
// x, y are the mouse coordinates, which should be the center of the new view frame // x, y are the mouse coordinates, which should be the center of the new view frame
// the new view origin should be x, y minus half the new view width and height // the new view origin should be x, y minus half the new view width and height
// compute new scale // compute new scale

View File

@ -17,8 +17,8 @@ export class Tool {
div.style.top = 0; div.style.top = 0;
div.style.left = 0; div.style.left = 0;
div.style.border = '1px #0fb solid'; div.style.border = '1px #0fb solid';
div.style.margin = '5px'; div.style.margin = '25px';
div.style.padding = '5px'; div.style.padding = '25px';
div.classList.add(DRAGGABLE_ELEMENT_CLASSNAME); div.classList.add(DRAGGABLE_ELEMENT_CLASSNAME);
} }

54
tool/modes.js Normal file
View File

@ -0,0 +1,54 @@
// Mode Switcher
import { Tool } from '../tool.js';
import {
MODE_MASS_GENERATION,
MODE_PAN_VIEW,
} from '../config.js';
export class ModeSwitch extends Tool {
currentMode = undefined;
modes = [
[MODE_MASS_GENERATION, 'Generate Mass'],
[MODE_PAN_VIEW, 'Pan View'],
];
constructor(toolbar) {
super(toolbar);
const [[currentModeID, _]] = this.modes;
this.currentMode = currentModeID;
const modesDiv = document.createElement('div');
const titleDiv = document.createElement('div');
this.div.appendChild(titleDiv);
this.div.appendChild(modesDiv);
titleDiv.innerHTML = `<h2>Modes</h2>`;
// Since we have a heading, we need to reduce top padding
this.div.style.paddingTop = '0px';
modesDiv.style.display = 'flex';
modesDiv.style.flexDirection = 'column';
for (let [modeID, modeTitle] of this.modes) {
const button = document.createElement('button');
modesDiv.appendChild(button);
button.innerHTML = `<h3>${modeTitle}</h3>`;
button.classList.add('wide');
button.style.opacity = modeID === currentModeID ? '50%' : '100%';
button.addEventListener('click', (e) => {
if (this.currentMode !== modeID) {
this.currentMode = modeID;
}
});
}
// Add global method to get current mode / check mode
this.sim.getCurrentMode = () => this.currentMode;
this.sim.isCurrentMode = (modeID) => modeID === this.currentMode;
}
}

View File

@ -1,32 +1,38 @@
import { Tool } from '../tool.js'; import { Tool } from '../tool.js';
export class PlayPause extends Tool { export class PlayPause extends Tool {
playHTML = 'Play'; playHTML = '<h2>Play</h2>';
pauseHTML = 'Pause'; pauseHTML = '<h2>Pause</h2>';
constructor(toolbar) { constructor(toolbar) {
super(toolbar); super(toolbar);
// For now, use a regular button const pauseButton = document.createElement('button');
const button = document.createElement('button'); const playButton = document.createElement('button');
button.style.width = '100px';
button.style.height = '50px';
button.style.margin = '25px';
this.div.appendChild(button);
if (this.playing) {
button.innerHTML = this.pauseHTML;
} else {
button.innerHTML = this.playHTML;
}
button.addEventListener('click', (e) => { this.div.appendChild(pauseButton);
this.div.appendChild(playButton);
pauseButton.innerHTML = this.pauseHTML;
playButton.innerHTML = this.playHTML;
pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
playButton.style.opacity = this.sim.playing ? '50%' : '100%';
pauseButton.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
if (this.playing) { if (this.playing) {
button.innerHTML = this.playHTML;
this.playing = false; this.playing = false;
} else { pauseButton.style.opacity = '50%';
button.innerHTML = this.pauseHTML; playButton.style.opacity = '100%';
}
});
playButton.addEventListener('click', (e) => {
if (!this.playing) {
this.playing = true; this.playing = true;
pauseButton.style.opacity = '100%';
playButton.style.opacity = '50%';
} }
}); });
} }

43
tool/zoom.js Normal file
View File

@ -0,0 +1,43 @@
import { Tool } from '../tool.js';
import {
ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR,
} from '../config.js';
export class Zoom extends Tool {
constructor(toolbar) {
super(toolbar);
const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button');
for (let b of [zoomIn, zoomOut]) {
// b.style.width = '100px';
// b.style.height = '50px';
// b.style['padding-left'] = '25px';
// b.style['padding-right'] = '25px';
}
this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn);
zoomOut.innerHTML = '<h2>Zoom<br>-<br>Out</h2>';
zoomIn.innerHTML = '<h2>Zoom<br>+<br>In</h2>';
zoomOut.addEventListener('click', (e) => {
// Aim at center of view
const x = this.sim.display.width * this.sim.display.scale / 2;
const y = this.sim.display.height * this.sim.display.scale / 2;
console.log(`zoom out, x`, x, 'y', y);
this.sim.scheduleZoom({x, y}, ZOOM_OUT_FACTOR);
});
zoomIn.addEventListener('click', (e) => {
// Aim at center of view
const x = this.sim.display.width * this.sim.display.scale / 2;
const y = this.sim.display.height * this.sim.display.scale / 2;
console.log(`zoom in, x`, x, 'y', y);
this.sim.scheduleZoom({x, y}, ZOOM_IN_FACTOR);
});
}
}