From 71854d2a953b0eba16fbe59344e7e5aea56442e1 Mon Sep 17 00:00:00 2001 From: Ladd Date: Sun, 4 Jan 2026 15:09:26 -0600 Subject: [PATCH] added camera vector --- config.js | 3 +++ display.js | 2 +- helper.js | 4 ++++ panning.js | 63 ++++++++++++++++++++----------------------------- pointer.js | 15 ++---------- select.js | 12 +++++++--- sim-options.js | 3 ++- sim-tools.js | 4 ++++ style.css | 8 +++++-- tool/camera.js | 42 +++++++++++++++++++++++++++++++++ tool/options.js | 1 - 11 files changed, 99 insertions(+), 58 deletions(-) create mode 100644 tool/camera.js diff --git a/config.js b/config.js index 8ce6543..c67d7f8 100644 --- a/config.js +++ b/config.js @@ -19,6 +19,9 @@ export const PATH_TRACES_OPACITY = 0.8; export const PATH_TRACES_WIDTH = 1.5; export const PATH_TRACES_DASHED_OPACITY = 1.0; +// PANNING +export const PANNING_ZERO_TOUCH_THRESHOLD = 200; + // SIZES export const POINTER_HISTORY_SIZE = 20; export const OBJECT_HISTORY_SIZE = 1e5; diff --git a/display.js b/display.js index 38ed700..1e48cc2 100644 --- a/display.js +++ b/display.js @@ -140,7 +140,7 @@ export class Display { ctx.resetTransform(); } - drawBox({start, end}) { + drawBox(start, end) { const ctx = this.ctx; ctx.strokeStyle = 'rgb(0, 255, 0)'; ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y); diff --git a/helper.js b/helper.js index 3517273..79cd9b4 100644 --- a/helper.js +++ b/helper.js @@ -2,6 +2,10 @@ // `item` must let us read/write property `hidden` // `parentEl` is the containing element for `itemEl` // `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}) { if (items.length < 2) { parentEl.appendChild(itemEl); diff --git a/panning.js b/panning.js index fefd8ba..6bdfafc 100644 --- a/panning.js +++ b/panning.js @@ -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 { sim = undefined; @@ -11,10 +12,6 @@ export class Panning { this.sim = sim; } - handlePointerDown({x, y}) { - this.initializeTouch({x, y}); - } - initializeTouch({x, y}) { this.touchStart = { 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 normal panning, calculation only happens at pointer up. handlePointerMove({x, y}) { @@ -42,49 +46,46 @@ export class Panning { dy: x - this.touchStart.y, 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() { if (this.touchStart && this.touchLatest) { - if (this.touchLatest.dt === 0) { + if (this.touchLatest.dt < PANNING_ZERO_TOUCH_THRESHOLD) { this.velocity = zero; } this.touchStart = undefined; if (this.sim.getOption('compensate.fastPanning')) { this.velocity = zero; - } else { - this.updateVelocity(); } } } frame(elapsedTime) { - const {touchStart: start, touchLatest: latest} = this; 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 if (!this.paused) { // elapsedTime is scaled by time scale, is that what we want? // Yes because if panning.velocity == obj.velocity, object should stay in view - const delta = mult(this.velocity, elapsedTime); - display.viewOrigin = add(display.viewOrigin, delta); - } - - // Update what's considered start - if (start && latest) { - this.initializeTouch(this.touchLatest); + display.viewOrigin = add(display.viewOrigin, mult(this.velocity, elapsedTime)); } 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) { this.velocity = velocity; if (!this.sim.playing) { diff --git a/pointer.js b/pointer.js index 0977cb4..479f734 100644 --- a/pointer.js +++ b/pointer.js @@ -3,24 +3,21 @@ import { MODE_OBJECT_SELECT, MODE_PAN_VIEW, POINTER_HISTORY_SIZE, - TOOLBAR_CLASSNAME, ZOOM_IN_FACTOR, - ZOOM_OUT_FACTOR, + ZOOM_OUT_FACTOR } from './config.js'; export class Pointer { sim = undefined; pointerHistory = []; - touchStart = undefined; // {x: undefined, y: undefined, t: undefined}; - touchLatest = undefined; // {x: undefined, y: undefined, t: undefined}; suppressClick = false; constructor(sim) { this.sim = sim; // Monitor mouse movements - const el = window; + const el = this.sim.display.canvas; el.addEventListener('pointermove', e => { if (this.sim.getOption('debug.cursorInfo')) { @@ -30,14 +27,6 @@ export class Pointer { }); 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}); }); diff --git a/select.js b/select.js index 67144c4..29479ef 100644 --- a/select.js +++ b/select.js @@ -1,4 +1,4 @@ -import {copy} from './vector.js'; +import {add, copy, mult} from './vector.js'; export class Select { sim = undefined; @@ -67,8 +67,14 @@ export class Select { this.selectedSingle = this.selectedGroup[0] ?? undefined; } - frame() { + frame(elapsedTime) { 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); } } diff --git a/sim-options.js b/sim-options.js index f4a6cd5..c79a310 100644 --- a/sim-options.js +++ b/sim-options.js @@ -11,7 +11,8 @@ export const simOptions = { dashedTraces: ['Dashed', 'boolean', false, {tall: true, showIf: 'display.traces'}], velocityScale: ['Velocity
Vec Scale', 'number', 80, {showIf: 'display.velocity'}], accelerationScale: ['Accel
Vec Scale', 'number', 800, {showIf: 'display.acceleration'}], - zoomVectors: ['Zoom Vectors', 'boolean', true] + zoomVectors: ['Zoom Vectors', 'boolean', true], + panningSpeed: ['Pan
Speed', 'number', 0.1], }, compensate: { timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}], diff --git a/sim-tools.js b/sim-tools.js index 0abf9ab..a626e53 100644 --- a/sim-tools.js +++ b/sim-tools.js @@ -1,3 +1,4 @@ +import {CameraTool} from './tool/camera.js'; import {ModeSwitch} from './tool/modes.js'; import {ObjectTool} from './tool/object.js'; import {ObjectsTool} from './tool/objects.js'; @@ -11,6 +12,9 @@ import {Toolbar} from './toolbar.js'; export function initializeTools(sim) { sim.toolbarGroups = { left: new ToolbarGroup(sim) + .addToolbar(new Toolbar(sim, 'Camera') + .addTool(new CameraTool()) + ) .addToolbar(new Toolbar(sim, 'Tools') .addTool(new Zoom()) .addTool(new PlayPause()) diff --git a/style.css b/style.css index 22652d5..4c78af1 100644 --- a/style.css +++ b/style.css @@ -33,6 +33,10 @@ div[id=simulator] > canvas { left: 0; } +div.lhg-toolbar-group button { + /* opacity: 0.8; */ +} + /* normal toolbar group */ div.lhg-toolbar-group div.lhg-tool { width: 12em; @@ -102,7 +106,7 @@ div.lhg-tool button, div.lhg-tool input { box-sizing: border-box; } -div.lhg-tool button:hover { +div.lhg-tool button:hover, div.lhg-tool input:hover { background-color: #444; } @@ -122,7 +126,7 @@ div.lhg-toolbar-header > * { 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; border-color: #000; border-width: 2px; diff --git a/tool/camera.js b/tool/camera.js new file mode 100644 index 0000000..a645a58 --- /dev/null +++ b/tool/camera.js @@ -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', + }); + } +} diff --git a/tool/options.js b/tool/options.js index fa81043..76027da 100644 --- a/tool/options.js +++ b/tool/options.js @@ -60,7 +60,6 @@ export class OptionsTool extends Tool { } for (const next of item.items) { const optionEl = this.visitItem(next, path); - // const option = {itemEl: optionEl}; group.items.push(next); if (this.shouldShow(next)) { groupEl.appendChild(optionEl);