added camera vector

This commit is contained in:
Ladd 2026-01-04 15:09:26 -06:00
parent 54ed2838f7
commit 71854d2a95
11 changed files with 99 additions and 58 deletions

View File

@ -19,6 +19,9 @@ export const PATH_TRACES_OPACITY = 0.8;
export const PATH_TRACES_WIDTH = 1.5; export const PATH_TRACES_WIDTH = 1.5;
export const PATH_TRACES_DASHED_OPACITY = 1.0; export const PATH_TRACES_DASHED_OPACITY = 1.0;
// PANNING
export const PANNING_ZERO_TOUCH_THRESHOLD = 200;
// SIZES // SIZES
export const POINTER_HISTORY_SIZE = 20; export const POINTER_HISTORY_SIZE = 20;
export const OBJECT_HISTORY_SIZE = 1e5; export const OBJECT_HISTORY_SIZE = 1e5;

View File

@ -140,7 +140,7 @@ export class Display {
ctx.resetTransform(); ctx.resetTransform();
} }
drawBox({start, end}) { drawBox(start, end) {
const ctx = this.ctx; const ctx = this.ctx;
ctx.strokeStyle = 'rgb(0, 255, 0)'; ctx.strokeStyle = 'rgb(0, 255, 0)';
ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y); ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y);

View File

@ -2,6 +2,10 @@
// `item` must let us read/write property `hidden` // `item` must let us read/write property `hidden`
// `parentEl` is the containing element for `itemEl` // `parentEl` is the containing element for `itemEl`
// `itemEl` is the // `itemEl` is the
//
// The idea is that item remains a member of items, but
// its elementmay be added and removed from the parent element.
// We use the items array to determine the placement of itemEl
export function show({items, item, parentEl, itemEl}) { export function show({items, item, parentEl, itemEl}) {
if (items.length < 2) { if (items.length < 2) {
parentEl.appendChild(itemEl); parentEl.appendChild(itemEl);

View File

@ -1,4 +1,5 @@
import {add, copy, div, mult, sub, zero} from "./vector.js"; import {PANNING_ZERO_TOUCH_THRESHOLD} from "./config.js";
import {add, copy, div, mult, zero} from "./vector.js";
export class Panning { export class Panning {
sim = undefined; sim = undefined;
@ -11,10 +12,6 @@ export class Panning {
this.sim = sim; this.sim = sim;
} }
handlePointerDown({x, y}) {
this.initializeTouch({x, y});
}
initializeTouch({x, y}) { initializeTouch({x, y}) {
this.touchStart = { this.touchStart = {
x, x,
@ -30,6 +27,13 @@ export class Panning {
}; };
} }
handlePointerDown({x, y}) {
this.initializeTouch({x, y});
if (this.paused) {
this.paused = false;
}
}
// With fast panning, panning velocity calculation happens every move; // With fast panning, panning velocity calculation happens every move;
// With normal panning, calculation only happens at pointer up. // With normal panning, calculation only happens at pointer up.
handlePointerMove({x, y}) { handlePointerMove({x, y}) {
@ -42,49 +46,46 @@ export class Panning {
dy: x - this.touchStart.y, dy: x - this.touchStart.y,
dt: this.sim.rawTime - this.touchStart.t, dt: this.sim.rawTime - this.touchStart.t,
}; };
if (this.sim.getOption('compensate.fastPanning')) {
this.updateVelocity(); // Convert pointer velocity to simulation scale
let velocity = div(this.sim.pointer.latestVelocity, this.sim.display.scale);
// Optional time scale compensation
if (this.sim.getOption('compensate.timeScale')) {
velocity = div(velocity, this.sim.timeScale);
} }
// Additional scaling factor
velocity = mult(velocity, this.sim.getOption('display.panningSpeed'));
// TODO: Make it easier to slow down the camera
// Add pointer velocity to current panning velocity
this.velocity = add(this.velocity, velocity);
} }
} }
handlePointerUp() { handlePointerUp() {
if (this.touchStart && this.touchLatest) { if (this.touchStart && this.touchLatest) {
if (this.touchLatest.dt === 0) { if (this.touchLatest.dt < PANNING_ZERO_TOUCH_THRESHOLD) {
this.velocity = zero; this.velocity = zero;
} }
this.touchStart = undefined; this.touchStart = undefined;
if (this.sim.getOption('compensate.fastPanning')) { if (this.sim.getOption('compensate.fastPanning')) {
this.velocity = zero; this.velocity = zero;
} else {
this.updateVelocity();
} }
} }
} }
frame(elapsedTime) { frame(elapsedTime) {
const {touchStart: start, touchLatest: latest} = this;
const {display} = this.sim; const {display} = this.sim;
// Direct translate, unless using fast panning
if (start && latest && !this.sim.getOption('compensate.fastPanning')) {
// start and latest are in screen coordinates, need to convert to sim scale
const delta = div(sub(latest, start), display.scale);
display.viewOrigin = sub(start.viewOrigin, delta);
}
// Apply update to viewOrigin based on panning // Apply update to viewOrigin based on panning
if (!this.paused) { if (!this.paused) {
// elapsedTime is scaled by time scale, is that what we want? // elapsedTime is scaled by time scale, is that what we want?
// Yes because if panning.velocity == obj.velocity, object should stay in view // Yes because if panning.velocity == obj.velocity, object should stay in view
const delta = mult(this.velocity, elapsedTime); display.viewOrigin = add(display.viewOrigin, mult(this.velocity, elapsedTime));
display.viewOrigin = add(display.viewOrigin, delta);
}
// Update what's considered start
if (start && latest) {
this.initializeTouch(this.touchLatest);
} }
if (this.sim.getOption('debug.panningInfo')) { if (this.sim.getOption('debug.panningInfo')) {
@ -96,18 +97,6 @@ export class Panning {
} }
} }
updateVelocity() {
// Convert pointer velocity to simulation scale, and multiply by -1
// because the camera is panning opposite to the pointer velocity.
let velocity = div(this.sim.pointer.latestVelocity, -this.sim.display.scale);
if (this.sim.getOption('compensate.timeScale')) {
velocity = div(velocity, this.sim.timeScale);
}
// Also add current panning
velocity = add(velocity, this.velocity);
this.velocity = velocity;
}
setVelocity(velocity) { setVelocity(velocity) {
this.velocity = velocity; this.velocity = velocity;
if (!this.sim.playing) { if (!this.sim.playing) {

View File

@ -3,24 +3,21 @@ import {
MODE_OBJECT_SELECT, MODE_OBJECT_SELECT,
MODE_PAN_VIEW, MODE_PAN_VIEW,
POINTER_HISTORY_SIZE, POINTER_HISTORY_SIZE,
TOOLBAR_CLASSNAME,
ZOOM_IN_FACTOR, ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR, ZOOM_OUT_FACTOR
} from './config.js'; } from './config.js';
export class Pointer { export class Pointer {
sim = undefined; sim = undefined;
pointerHistory = []; pointerHistory = [];
touchStart = undefined; // {x: undefined, y: undefined, t: undefined};
touchLatest = undefined; // {x: undefined, y: undefined, t: undefined};
suppressClick = false; suppressClick = false;
constructor(sim) { constructor(sim) {
this.sim = sim; this.sim = sim;
// Monitor mouse movements // Monitor mouse movements
const el = window; const el = this.sim.display.canvas;
el.addEventListener('pointermove', e => { el.addEventListener('pointermove', e => {
if (this.sim.getOption('debug.cursorInfo')) { if (this.sim.getOption('debug.cursorInfo')) {
@ -30,14 +27,6 @@ export class Pointer {
}); });
el.addEventListener('pointerdown', e => { el.addEventListener('pointerdown', e => {
let target = e.target;
while (target && !target.classList?.contains(TOOLBAR_CLASSNAME)) {
target = target.parentNode;
}
if (target) {
return;
}
this.handlePointerDown({x: e.clientX, y: e.clientY}); this.handlePointerDown({x: e.clientX, y: e.clientY});
}); });

View File

@ -1,4 +1,4 @@
import {copy} from './vector.js'; import {add, copy, mult} from './vector.js';
export class Select { export class Select {
sim = undefined; sim = undefined;
@ -67,8 +67,14 @@ export class Select {
this.selectedSingle = this.selectedGroup[0] ?? undefined; this.selectedSingle = this.selectedGroup[0] ?? undefined;
} }
frame() { frame(elapsedTime) {
if (!this.box.start) return; if (!this.box.start) return;
this.sim.display.drawBox(this.box) // If panning, let's update the position of our box so it doesn't drift away
const {velocity} = this.sim.panning;
const delta = mult(velocity, elapsedTime);
this.box.start = add(this.box.start, delta);
this.box.end = add(this.box.end, delta);
// Display the box
this.sim.display.drawBox(this.box.start, this.box.end);
} }
} }

View File

@ -11,7 +11,8 @@ export const simOptions = {
dashedTraces: ['Dashed', 'boolean', false, {tall: true, showIf: 'display.traces'}], dashedTraces: ['Dashed', 'boolean', false, {tall: true, showIf: 'display.traces'}],
velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {showIf: 'display.velocity'}], velocityScale: ['Velocity<br>Vec Scale', 'number', 80, {showIf: 'display.velocity'}],
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {showIf: 'display.acceleration'}], accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {showIf: 'display.acceleration'}],
zoomVectors: ['Zoom Vectors', 'boolean', true] zoomVectors: ['Zoom Vectors', 'boolean', true],
panningSpeed: ['Pan<br>Speed', 'number', 0.1],
}, },
compensate: { compensate: {
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}], timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],

View File

@ -1,3 +1,4 @@
import {CameraTool} from './tool/camera.js';
import {ModeSwitch} from './tool/modes.js'; import {ModeSwitch} from './tool/modes.js';
import {ObjectTool} from './tool/object.js'; import {ObjectTool} from './tool/object.js';
import {ObjectsTool} from './tool/objects.js'; import {ObjectsTool} from './tool/objects.js';
@ -11,6 +12,9 @@ import {Toolbar} from './toolbar.js';
export function initializeTools(sim) { export function initializeTools(sim) {
sim.toolbarGroups = { sim.toolbarGroups = {
left: new ToolbarGroup(sim) left: new ToolbarGroup(sim)
.addToolbar(new Toolbar(sim, 'Camera')
.addTool(new CameraTool())
)
.addToolbar(new Toolbar(sim, 'Tools') .addToolbar(new Toolbar(sim, 'Tools')
.addTool(new Zoom()) .addTool(new Zoom())
.addTool(new PlayPause()) .addTool(new PlayPause())

View File

@ -33,6 +33,10 @@ div[id=simulator] > canvas {
left: 0; left: 0;
} }
div.lhg-toolbar-group button {
/* opacity: 0.8; */
}
/* normal toolbar group */ /* normal toolbar group */
div.lhg-toolbar-group div.lhg-tool { div.lhg-toolbar-group div.lhg-tool {
width: 12em; width: 12em;
@ -102,7 +106,7 @@ div.lhg-tool button, div.lhg-tool input {
box-sizing: border-box; box-sizing: border-box;
} }
div.lhg-tool button:hover { div.lhg-tool button:hover, div.lhg-tool input:hover {
background-color: #444; background-color: #444;
} }
@ -122,7 +126,7 @@ div.lhg-toolbar-header > * {
display: inline-block; display: inline-block;
} }
div.lhg-tool .lhg-tool-info { div.lhg-tool .lhg-tool-info, div.lhg-tool .lhg-tool-info:hover {
background-color: #111; background-color: #111;
border-color: #000; border-color: #000;
border-width: 2px; border-width: 2px;

42
tool/camera.js Normal file
View File

@ -0,0 +1,42 @@
import {VELOCITY_VECTOR_COLOR} from '../config.js';
import {Tool} from '../tool.js';
import {add, components, direction, div, magnitude} from '../vector.js';
export class CameraTool extends Tool {
setContainer(container) {
super.setContainer(container);
// Use the main sim display, but create a placeholder and draw inside it.
// That way we aren't blocking the main display more than necessary
}
constructor() {
super();
this.div.style.width = '150px';
this.div.style.height = '150px';
}
frame() {
if (!this.container.expanded) return;
const {display, panning} = this.sim;
const {left, top, width, height} = this.div.getBoundingClientRect();
const vecScale = this.sim.getOption('display.velocityScale');
// Draw a vector for the camera velocity
const offset = add(display.viewOrigin, div({x: left, y: top}, display.scale));
const start = add(offset, div({x: width, y: height}, 2 * display.scale));
let speed = magnitude(panning.velocity);
let arrowLength = Math.log10(speed + 1) * vecScale;
const arrowDirection = direction(panning.velocity);
if (!this.sim.getOption('display.zoomVectors')) {
arrowLength /= display.scale;
}
const end = add(start, components(arrowLength, arrowDirection));
display.drawArrow(start.x, start.y, end.x, end.y, {
style: VELOCITY_VECTOR_COLOR,
ifShort: 'head',
});
}
}

View File

@ -60,7 +60,6 @@ export class OptionsTool extends Tool {
} }
for (const next of item.items) { for (const next of item.items) {
const optionEl = this.visitItem(next, path); const optionEl = this.visitItem(next, path);
// const option = {itemEl: optionEl};
group.items.push(next); group.items.push(next);
if (this.shouldShow(next)) { if (this.shouldShow(next)) {
groupEl.appendChild(optionEl); groupEl.appendChild(optionEl);