Adjustments to panning; Added Selection Box
This commit is contained in:
parent
fe7e9f43ad
commit
6922011ca7
23
Layout.md
23
Layout.md
@ -1,23 +0,0 @@
|
|||||||
Overlay > Info Box
|
|
||||||
|
|
||||||
Each toolbar gives itself a header tool which expands/collapses the toolbar.
|
|
||||||
|
|
||||||
- Toolbar Group: Left Side
|
|
||||||
- Toolbar: Tools
|
|
||||||
- Tool: Zoom
|
|
||||||
- Toolbar: Modes
|
|
||||||
- Tool: Mode Switch
|
|
||||||
- Toolbar: Utils
|
|
||||||
- Tool: Utility Tool
|
|
||||||
- Toolbar Group: Right Side
|
|
||||||
- Toolbar: Options
|
|
||||||
- Tool: Options Tool
|
|
||||||
- Section: Pause During
|
|
||||||
- Section: Display
|
|
||||||
- Section: Collision
|
|
||||||
- Toolbar: Params
|
|
||||||
- Tool: Options Tool
|
|
||||||
- Section: Param
|
|
||||||
- Toolbar: Debug
|
|
||||||
- Tool: Options Tool
|
|
||||||
- Section: Debug
|
|
||||||
81
Readme.md
81
Readme.md
@ -15,27 +15,66 @@ Screenshots
|
|||||||
TODO
|
TODO
|
||||||
----
|
----
|
||||||
|
|
||||||
- [ ] Selection Box
|
- [x] Selection Box
|
||||||
- [ ] Object List
|
|
||||||
- [ ] Object Detail
|
- [ ] Enhancement: Create Vector class and refactor to use
|
||||||
- [ ] Left Button Panning
|
|
||||||
- [ ] Middle Button Pause
|
- [ ] Feature: Object List
|
||||||
- [ ] Parameter Slider (Invisible, mouse/touch drag)
|
|
||||||
- [ ] Grid
|
- [ ] Feature: Object Detail
|
||||||
- [ ] Zoom Easing
|
|
||||||
- [ ] 2-touch Pan & Zoom
|
- [ ] Enhancement: Handle pointerleave or other mechanism when window loses focus
|
||||||
- Either continuous zoom, or discrete with animation of bounding box proposed changes
|
|
||||||
- [ ] Multi-touch Mass Create
|
- [ ] Enhancement: Calculate Work as FxD as measure of energy flux
|
||||||
- [ ] Camera Rotation
|
|
||||||
|
- [ ] Feature: Automatically slow time when energy flux is greater
|
||||||
|
|
||||||
|
- [ ] Enhancement: Add z-axis (Initially nothing changes, z = 0)
|
||||||
|
|
||||||
|
- [ ] Feature: Isometric 3d View
|
||||||
|
|
||||||
|
- [ ] Feature: Grid
|
||||||
|
|
||||||
|
- [ ] Feature: Polar Coordinates
|
||||||
|
|
||||||
|
- [ ] Feature: Cylindrical Coordinates
|
||||||
|
|
||||||
|
- [ ] Feature: Spherical Coordinates
|
||||||
|
|
||||||
|
- [ ] Feature: Camera Velocity Display
|
||||||
|
|
||||||
|
- [ ] Enhancement: World State Snapshots
|
||||||
|
|
||||||
|
- [ ] Feature: List / Save / Load World States
|
||||||
|
|
||||||
|
- [ ] Feature: Left Button Panning
|
||||||
|
|
||||||
|
- [ ] Feature: Middle Button Pause
|
||||||
|
|
||||||
|
- [ ] Feature: Parameter Slider (Invisible, mouse/touch drag)
|
||||||
|
|
||||||
|
- [ ] Enhancement: Zoom Easing
|
||||||
|
|
||||||
|
- [ ] Feature: 2-touch Pan & Zoom
|
||||||
|
Either continuous zoom, or discrete with animation of bounding box proposed changes
|
||||||
|
|
||||||
|
- [ ] Feature: Multi-touch Mass Create
|
||||||
|
|
||||||
|
- [ ] Feature: Camera Rotation
|
||||||
|
|
||||||
- [ ] 2-touch Rotation
|
- [ ] 2-touch Rotation
|
||||||
- [ ] Spinning Frame
|
- [ ] Spinning Frame
|
||||||
- [ ] Undo feature:
|
|
||||||
|
- [ ] Feature: Undo
|
||||||
|
|
||||||
- [ ] Undo "Clear Traces" Action
|
- [ ] Undo "Clear Traces" Action
|
||||||
- [ ] Undo "Reset
|
- [ ] In general have a user action history
|
||||||
- [ ] Time Control: Reverse Time
|
- [ ] Point-in-time restoral of world state (Marked points, i.e. Snapshots)
|
||||||
- [ ] Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
|
||||||
- [ ] Track farthest reaches, min/max in each dimension (x, y)
|
- [ ] Feature: Time Control: Reverse Time
|
||||||
- [ ] Calculate Work as FxD as measure of energy flux
|
|
||||||
- [ ] Option to automatically slow time when energy flux is greater
|
- [ ] Feature: Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
||||||
- [ ] Handle pointerleave or other mechanism when window loses focus
|
|
||||||
- [ ] Verify stationary pointer leads to zero pointer velocity
|
- [ ] Enhancement: Track farthest reaches, min/max in each dimension (x, y)
|
||||||
|
|
||||||
|
- [x] Task: Verify stationary pointer leads to zero pointer velocity
|
||||||
|
|||||||
91
display.js
91
display.js
@ -22,6 +22,45 @@ export class Display {
|
|||||||
window.addEventListener('resize', () => this.fullscreen());
|
window.addEventListener('resize', () => this.fullscreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frame(elapsedTime) {
|
||||||
|
const {
|
||||||
|
touchStart: start,
|
||||||
|
touchLatest: latest,
|
||||||
|
} = this.sim.pointer ?? {};
|
||||||
|
|
||||||
|
// Direct translate, unless using fast panning
|
||||||
|
if (start && latest && !this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
|
||||||
|
this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply update to viewOrigin based on panning
|
||||||
|
if (this.sim.panning && !this.sim.panning.paused) {
|
||||||
|
// elapsedTime is scaled by time scale, is that what we want?
|
||||||
|
// Yes because if panning.velocity == obj.velocity, object should stay in view
|
||||||
|
this.viewOrigin.x += this.sim.panning.velocity.x * elapsedTime;
|
||||||
|
this.viewOrigin.y += this.sim.panning.velocity.y * elapsedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update what's considered start
|
||||||
|
if (start && latest) {
|
||||||
|
start.viewOrigin = {...this.viewOrigin};
|
||||||
|
start.x = latest.x;
|
||||||
|
start.y = latest.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sim.getOption('debug.panningInfo')) {
|
||||||
|
const {x, y} = this.sim.panning?.velocity ?? {};
|
||||||
|
this.sim.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
|
||||||
|
const {centerOfMass} = this.sim.system.computeSystemCenter();
|
||||||
|
this.sim.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
|
||||||
|
this.sim.info['Net Angular Momentum'] = this.sim.system.computeSystemAngularMomentum().toPrecision(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear canvas in preparation for other modules to render this frame
|
||||||
|
this.fillCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
get scale() {
|
get scale() {
|
||||||
return 2 ** this.scalePower;
|
return 2 ** this.scalePower;
|
||||||
}
|
}
|
||||||
@ -61,13 +100,15 @@ export class Display {
|
|||||||
this.canvas.height = document.documentElement.clientHeight;
|
this.canvas.height = document.documentElement.clientHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
fillCanvas() {
|
fillCanvas() {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
ctx.fillStyle = '#000';
|
ctx.fillStyle = '#000';
|
||||||
ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height);
|
ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) {
|
drawArrow(startX, startY, endX, endY, {
|
||||||
|
style, width, arrowhead, arrowheadLength, fill, ifShort
|
||||||
|
}) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
ctx.strokeStyle = style;
|
ctx.strokeStyle = style;
|
||||||
// Keep arrows at normal scale
|
// Keep arrows at normal scale
|
||||||
@ -135,47 +176,9 @@ export class Display {
|
|||||||
ctx.resetTransform();
|
ctx.resetTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(elapsedTime) {
|
drawBox({start, end}) {
|
||||||
const {
|
const ctx = this.ctx;
|
||||||
touchStart: start,
|
ctx.strokeStyle = 'rgb(0, 255, 0)';
|
||||||
touchLatest: latest,
|
ctx.strokeRect(start.x, start.y, end.x - start.x, end.y - start.y);
|
||||||
} = this.sim.pointer ?? {};
|
|
||||||
|
|
||||||
if (start && latest) {
|
|
||||||
// Direct translate
|
|
||||||
this.viewOrigin.x = start.viewOrigin.x - (latest.x - start.x) / this.scale;
|
|
||||||
this.viewOrigin.y = start.viewOrigin.y - (latest.y - start.y) / this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pdx = 0;
|
|
||||||
let pdy = 0;
|
|
||||||
|
|
||||||
if (this.sim.panning && !this.sim.panning.paused) {
|
|
||||||
// Apply update to viewOrigin based on panning
|
|
||||||
// TODO: elapsedTime is scaled by time scale, is that what we want?
|
|
||||||
pdx = this.sim.panning.velocity.x * elapsedTime;
|
|
||||||
pdy = this.sim.panning.velocity.y * elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.viewOrigin.x += pdx;
|
|
||||||
this.viewOrigin.y += pdy;
|
|
||||||
|
|
||||||
if (start && latest) {
|
|
||||||
// Update what's considered start
|
|
||||||
start.viewOrigin = {...this.viewOrigin};
|
|
||||||
start.x = latest.x;
|
|
||||||
start.y = latest.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.sim.getOption('debug.panningInfo')) {
|
|
||||||
const {x, y} = this.sim.panning?.velocity ?? {};
|
|
||||||
this.sim.info['Panning Velocity'] = [`${x?.toPrecision(6)}, `, y?.toPrecision(6)];
|
|
||||||
const { centerOfMass } = this.sim.system.computeSystemCenter();
|
|
||||||
this.sim.info['Center of Mass'] = [`${centerOfMass.x.toPrecision(6)}, `, centerOfMass.y.toPrecision(6)];
|
|
||||||
this.sim.info['Net Angular Momentum'] = this.sim.system.computeSystemAngularMomentum().toPrecision(6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear canvas in preparation for other modules to render this frame
|
|
||||||
this.fillCanvas();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
object.js
27
object.js
@ -28,6 +28,9 @@ export class MassObject {
|
|||||||
history = [];
|
history = [];
|
||||||
alive = true;
|
alive = true;
|
||||||
|
|
||||||
|
currentPosition = undefined;
|
||||||
|
currentAcceleration = undefined;
|
||||||
|
|
||||||
constructor(sim, x, y) {
|
constructor(sim, x, y) {
|
||||||
this.sim = sim;
|
this.sim = sim;
|
||||||
this.id = crypto.randomUUID();
|
this.id = crypto.randomUUID();
|
||||||
@ -192,12 +195,16 @@ export class MassObject {
|
|||||||
const arrowDirection = Math.atan2(velocity.y, velocity.x);
|
const arrowDirection = Math.atan2(velocity.y, velocity.x);
|
||||||
// Prevent negative numbers by adding 1
|
// Prevent negative numbers by adding 1
|
||||||
// TODO: Make logarithmic vector length scale optional
|
// TODO: Make logarithmic vector length scale optional
|
||||||
const arrowLength = Math.log10(speed + 1) * vecScale + radius;
|
const arrowLength = Math.log10(speed + 1) * vecScale;
|
||||||
const endVx = x + arrowLength * Math.cos(arrowDirection);
|
const offsetX = radius * Math.cos(arrowDirection);
|
||||||
const endVy = y + arrowLength * Math.sin(arrowDirection);
|
const offsetY = radius * Math.sin(arrowDirection);
|
||||||
|
const startVx = x + offsetX;
|
||||||
|
const startVy = y + offsetY;
|
||||||
|
const endVx = startVx + arrowLength * Math.cos(arrowDirection);
|
||||||
|
const endVy = startVy + arrowLength * Math.sin(arrowDirection);
|
||||||
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
||||||
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
||||||
sim.display.drawArrow(x, y, endVx, endVy, {
|
sim.display.drawArrow(startVx, startVy, endVx, endVy, {
|
||||||
style,
|
style,
|
||||||
width: VELOCITY_VECTOR_WIDTH,
|
width: VELOCITY_VECTOR_WIDTH,
|
||||||
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
||||||
@ -213,13 +220,17 @@ export class MassObject {
|
|||||||
this.sim.display.scale;
|
this.sim.display.scale;
|
||||||
const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
|
const arrowDirection = Math.atan2(acceleration.y, acceleration.x);
|
||||||
// Prevent negative numbers by adding 1
|
// Prevent negative numbers by adding 1
|
||||||
const arrowLength = Math.log10(accelerationMagnitude + 1) * vecScale + radius;
|
const arrowLength = Math.log10(accelerationMagnitude + 1) * vecScale;
|
||||||
//const arrowLength = accelerationMagnitude * vecScale;
|
//const arrowLength = accelerationMagnitude * vecScale;
|
||||||
const endAx = x + arrowLength * Math.cos(arrowDirection);
|
const offsetX = radius * Math.cos(arrowDirection);
|
||||||
const endAy = y + arrowLength * Math.sin(arrowDirection);
|
const offsetY = radius * Math.sin(arrowDirection);
|
||||||
|
const startAx = x + offsetX;
|
||||||
|
const startAy = y + offsetY;
|
||||||
|
const endAx = startAx + arrowLength * Math.cos(arrowDirection);
|
||||||
|
const endAy = startAy + arrowLength * Math.sin(arrowDirection);
|
||||||
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
||||||
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
||||||
sim.display.drawArrow(x, y, endAx, endAy, {
|
sim.display.drawArrow(startAx, startAy, endAx, endAy, {
|
||||||
style,
|
style,
|
||||||
width: ACCELERATION_VECTOR_WIDTH,
|
width: ACCELERATION_VECTOR_WIDTH,
|
||||||
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
||||||
|
|||||||
129
options-1.js
Normal file
129
options-1.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
EVENT_OPTION_SET,
|
||||||
|
OBJECT_MAGIC_PROP_PREFIX,
|
||||||
|
} from './config.js';
|
||||||
|
|
||||||
|
export class Options {
|
||||||
|
sim = undefined;
|
||||||
|
options = undefined;
|
||||||
|
values = {};
|
||||||
|
undefinedObj = {[OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true};
|
||||||
|
nullObj = {[OBJECT_MAGIC_PROP_PREFIX + 'null']: true};
|
||||||
|
|
||||||
|
getStorageKey(path) {
|
||||||
|
return `${path}:options`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(sim, options) {
|
||||||
|
this.sim = sim;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Initialize values from localStorage
|
||||||
|
for (const groupName of Object.keys(options)) {
|
||||||
|
for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
|
||||||
|
const path = [groupName, name].join('.');
|
||||||
|
let value = this.getFromLocalStorage(path);
|
||||||
|
if (value === undefined) {
|
||||||
|
value = defaultValue;
|
||||||
|
}
|
||||||
|
this.values[path] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toStored(value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
// Do we want to interpret this as removing from storage?
|
||||||
|
// Let's just treat it as a value for now;
|
||||||
|
// Semantically it works because when retrieved, it will return undefined,
|
||||||
|
// which is the same result you get if the key is not set
|
||||||
|
return JSON.stringify(this.undefinedObj);
|
||||||
|
} else if (value === null) {
|
||||||
|
return JSON.stringify(this.nullObj);
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// value: string
|
||||||
|
fromStored(value) {
|
||||||
|
if (value === null) {
|
||||||
|
return undefined;
|
||||||
|
} else if (value === JSON.stringify(this.undefinedObj)) {
|
||||||
|
return undefined;
|
||||||
|
} else if (value === JSON.stringify(this.nullObj)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFromLocalStorage(path) {
|
||||||
|
const storageKey = this.getStorageKey(path);
|
||||||
|
const value = this.fromStored(window.localStorage.getItem(storageKey));
|
||||||
|
this.values[path] = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOption(path) {
|
||||||
|
const [groupName, name] = path.split('.');
|
||||||
|
const group = this.options[groupName];
|
||||||
|
const item = group[name];
|
||||||
|
const {type} = item;
|
||||||
|
const value = this.values[path];
|
||||||
|
switch (type) {
|
||||||
|
case 'number': return Number(value);
|
||||||
|
case 'boolean': return value === true || value === 'true';
|
||||||
|
default: {
|
||||||
|
console.log({
|
||||||
|
path, groupName, name,
|
||||||
|
group, item,
|
||||||
|
type, value
|
||||||
|
});
|
||||||
|
throw new Error('unknown option type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOption(path, value) {
|
||||||
|
this.values[path] = value;
|
||||||
|
const storageKey = this.getStorageKey(path);
|
||||||
|
window.localStorage.setItem(storageKey, this.toStored(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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSection(sectionName) {
|
||||||
|
const section = this.options[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
options.js
25
options.js
@ -7,8 +7,8 @@ export class Options {
|
|||||||
sim = undefined;
|
sim = undefined;
|
||||||
options = undefined;
|
options = undefined;
|
||||||
values = {};
|
values = {};
|
||||||
undefinedObj = { [OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true};
|
undefinedObj = {[OBJECT_MAGIC_PROP_PREFIX + 'undefined']: true};
|
||||||
nullObj = { [OBJECT_MAGIC_PROP_PREFIX + 'null']: true};
|
nullObj = {[OBJECT_MAGIC_PROP_PREFIX + 'null']: true};
|
||||||
|
|
||||||
getStorageKey(path) {
|
getStorageKey(path) {
|
||||||
return `${path}:options`;
|
return `${path}:options`;
|
||||||
@ -22,7 +22,7 @@ export class Options {
|
|||||||
this.sim.getOption = (path) => this.getOption(path);
|
this.sim.getOption = (path) => this.getOption(path);
|
||||||
this.sim.setOption = (path, value) => this.setOption(path, value);
|
this.sim.setOption = (path, value) => this.setOption(path, value);
|
||||||
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
|
this.sim.onOptionSet = (path, cb) => this.onOptionSet(path, cb);
|
||||||
|
|
||||||
// Initialize values from localStorage
|
// Initialize values from localStorage
|
||||||
for (const groupName of Object.keys(options)) {
|
for (const groupName of Object.keys(options)) {
|
||||||
for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
|
for (const [name, [, , defaultValue]] of Object.entries(this.options[groupName])) {
|
||||||
@ -69,13 +69,22 @@ export class Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOption(path) {
|
getOption(path) {
|
||||||
const [group, name] = path.split('.');
|
const [groupName, name] = path.split('.');
|
||||||
const {type} = this.options[group][name];
|
const group = this.options[groupName];
|
||||||
|
const item = group[name];
|
||||||
|
const [,type] = item;
|
||||||
const value = this.values[path];
|
const value = this.values[path];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'number': return Number(value);
|
case 'number': return Number(value);
|
||||||
case 'boolean': return value === true || value === 'true';
|
case 'boolean': return value === true || value === 'true';
|
||||||
default: return value;
|
default: {
|
||||||
|
console.error({
|
||||||
|
path, groupName, name,
|
||||||
|
group, item,
|
||||||
|
type, value
|
||||||
|
});
|
||||||
|
throw new Error('unknown option type');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,8 +108,8 @@ export class Options {
|
|||||||
getSection(sectionName) {
|
getSection(sectionName) {
|
||||||
const section = this.options[sectionName];
|
const section = this.options[sectionName];
|
||||||
const group = {
|
const group = {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
name: sectionName,
|
name: sectionName,
|
||||||
title: section._title,
|
title: section._title,
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
|
|||||||
262
pointer.js
262
pointer.js
@ -57,6 +57,152 @@ export class Pointer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
|
// this.clearPointerHistory(POINTER_DOWN_HISTORY_SIZE);
|
||||||
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
|
|
||||||
|
switch (this.sim.getCurrentMode()) {
|
||||||
|
case MODE_MASS_GENERATION: {
|
||||||
|
const {x, y} = this.sim.screenToSim(clientX, clientY)
|
||||||
|
this.sim.system.handlePointerDown({x, y});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_PAN_VIEW: {
|
||||||
|
this.touchStart = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY,
|
||||||
|
t: this.sim.rawTime,
|
||||||
|
viewOrigin: {...this.sim.display.viewOrigin},
|
||||||
|
};
|
||||||
|
// Since we've processed this increment, reset
|
||||||
|
this.touchLatest = {
|
||||||
|
...this.touchStart,
|
||||||
|
dx: 0,
|
||||||
|
dy: 0,
|
||||||
|
dt: 0,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_OBJECT_SELECT: {
|
||||||
|
this.sim.select.handlePointerDown({x: clientX, y: clientY});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor (mouse or touch) movement
|
||||||
|
handlePointerMove({x: clientX, y: clientY}) {
|
||||||
|
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
||||||
|
this.updatePointer({x: clientX, y: clientY});
|
||||||
|
|
||||||
|
switch (this.sim.getCurrentMode()) {
|
||||||
|
case MODE_MASS_GENERATION: {
|
||||||
|
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
||||||
|
this.sim.system.handlePointerMove({x, y});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_PAN_VIEW: {
|
||||||
|
if (this.touchStart) {
|
||||||
|
this.touchLatest = {
|
||||||
|
x: clientX,
|
||||||
|
y: clientY,
|
||||||
|
t: this.sim.rawTime,
|
||||||
|
dx: clientX - this.touchStart.x,
|
||||||
|
dy: clientY - this.touchStart.y,
|
||||||
|
dt: this.sim.rawTime - this.touchStart.t,
|
||||||
|
};
|
||||||
|
|
||||||
|
// With fast panning, this calculation happens every move
|
||||||
|
// With normal panning, this calculation only happens at pointer up
|
||||||
|
if (this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
const panning = {...this.latestVelocity};
|
||||||
|
// Convert pointer velocity to simulation scale.
|
||||||
|
// Also multiply by -1 because the camera is
|
||||||
|
// panning opposite to the pointer velocity.
|
||||||
|
panning.x /= -this.sim.display.scale;
|
||||||
|
panning.y /= -this.sim.display.scale;
|
||||||
|
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
panning.x /= this.sim.timeScale;
|
||||||
|
panning.y /= this.sim.timeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also add current panning
|
||||||
|
panning.x += this.sim.panning?.velocity.x ?? 0;
|
||||||
|
panning.y += this.sim.panning?.velocity.y ?? 0;
|
||||||
|
|
||||||
|
this.sim.panning = {
|
||||||
|
velocity: panning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_OBJECT_SELECT: {
|
||||||
|
this.sim.select.handlePointerMove({x: clientX, y: clientY});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp({x: clientX, y: clientY}) {
|
||||||
|
switch (this.sim.getCurrentMode()) {
|
||||||
|
case MODE_MASS_GENERATION: {
|
||||||
|
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
||||||
|
this.sim.system.handlePointerUp({x, y});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_PAN_VIEW: {
|
||||||
|
// Set panning velocity
|
||||||
|
if (this.touchStart && this.touchLatest) {
|
||||||
|
if (this.touchLatest.dt === 0) {
|
||||||
|
this.sim.panning = undefined;
|
||||||
|
}
|
||||||
|
this.touchStart = undefined;
|
||||||
|
|
||||||
|
if (this.sim.getOption('compensate.fastPanning')) {
|
||||||
|
this.sim.panning = undefined;
|
||||||
|
} else {
|
||||||
|
const panning = {...this.latestVelocity};
|
||||||
|
// Convert pointer velocity to simulation scale.
|
||||||
|
// Also multiply by -1 because the camera is
|
||||||
|
// panning opposite to the pointer velocity.
|
||||||
|
panning.x /= -this.sim.display.scale;
|
||||||
|
panning.y /= -this.sim.display.scale;
|
||||||
|
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
panning.x /= this.sim.timeScale;
|
||||||
|
panning.y /= this.sim.timeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also add current panning
|
||||||
|
panning.x += this.sim.panning?.velocity.x ?? 0;
|
||||||
|
panning.y += this.sim.panning?.velocity.y ?? 0;
|
||||||
|
|
||||||
|
this.sim.panning = {
|
||||||
|
velocity: panning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MODE_OBJECT_SELECT: {
|
||||||
|
this.sim.select.handlePointerUp({x: clientX, y: clientY});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame() {
|
||||||
|
// Add another entry for the current pointer position
|
||||||
|
const {pointerHistory} = this;
|
||||||
|
if (pointerHistory.length) {
|
||||||
|
const currentPointer = pointerHistory[pointerHistory.length - 1];
|
||||||
|
this.updatePointer(currentPointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getPointerVelocity(points = POINTER_HISTORY_SIZE) {
|
getPointerVelocity(points = POINTER_HISTORY_SIZE) {
|
||||||
// Average over pointer history
|
// Average over pointer history
|
||||||
if (this.pointerHistory.length < 2) {
|
if (this.pointerHistory.length < 2) {
|
||||||
@ -95,120 +241,4 @@ export class Pointer {
|
|||||||
...latestPointer?.v
|
...latestPointer?.v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x: clientX, y: clientY}) {
|
|
||||||
// this.clearPointerHistory(POINTER_DOWN_HISTORY_SIZE);
|
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
|
||||||
|
|
||||||
switch (this.sim.getCurrentMode()) {
|
|
||||||
case MODE_MASS_GENERATION: {
|
|
||||||
const {x, y} = this.sim.screenToSim(clientX, clientY)
|
|
||||||
this.sim.system.handlePointerDown({x, y});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_PAN_VIEW: {
|
|
||||||
this.touchStart = {
|
|
||||||
x: clientX,
|
|
||||||
y: clientY,
|
|
||||||
t: this.sim.rawTime,
|
|
||||||
viewOrigin: {...this.sim.display.viewOrigin},
|
|
||||||
};
|
|
||||||
// Since we've processed this increment, reset
|
|
||||||
this.touchLatest = {
|
|
||||||
...this.touchStart,
|
|
||||||
dx: 0,
|
|
||||||
dy: 0,
|
|
||||||
dt: 0,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_OBJECT_SELECT: {
|
|
||||||
// TODO: Start a selection box
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle cursor (mouse or touch) movement
|
|
||||||
handlePointerMove({x: clientX, y: clientY}) {
|
|
||||||
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
|
||||||
this.updatePointer({x: clientX, y: clientY});
|
|
||||||
|
|
||||||
switch (this.sim.getCurrentMode()) {
|
|
||||||
case MODE_MASS_GENERATION: {
|
|
||||||
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
|
||||||
this.sim.system.handlePointerMove({x, y});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_PAN_VIEW: {
|
|
||||||
if (this.touchStart) {
|
|
||||||
// Event loop should be able to read
|
|
||||||
this.touchLatest = {
|
|
||||||
x: clientX,
|
|
||||||
y: clientY,
|
|
||||||
t: this.sim.rawTime,
|
|
||||||
dx: clientX - this.touchStart.x,
|
|
||||||
dy: clientY - this.touchStart.y,
|
|
||||||
dt: this.sim.rawTime - this.touchStart.t,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_OBJECT_SELECT: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePointerUp({x: clientX, y: clientY}) {
|
|
||||||
switch (this.sim.getCurrentMode()) {
|
|
||||||
case MODE_MASS_GENERATION: {
|
|
||||||
const {x, y} = this.sim.screenToSim(clientX, clientY);
|
|
||||||
this.sim.system.handlePointerUp({x, y});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_PAN_VIEW: {
|
|
||||||
// Set panning velocity
|
|
||||||
if (this.touchStart && this.touchLatest) {
|
|
||||||
if (!this.touchLatest.dt) {
|
|
||||||
this.sim.panning = undefined;
|
|
||||||
} else {
|
|
||||||
const panning = {...this.latestVelocity};
|
|
||||||
// Convert pointer velocity to simulation scale.
|
|
||||||
// Also multiply by -1 because the camera is
|
|
||||||
// panning opposite to the pointer velocity.
|
|
||||||
panning.x /= -this.sim.display.scale;
|
|
||||||
panning.y /= -this.sim.display.scale;
|
|
||||||
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
panning.x /= this.sim.timeScale;
|
|
||||||
panning.y /= this.sim.timeScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add current panning
|
|
||||||
panning.x += this.sim.panning?.velocity.x ?? 0;
|
|
||||||
panning.y += this.sim.panning?.velocity.y ?? 0;
|
|
||||||
|
|
||||||
this.sim.panning = {
|
|
||||||
velocity: panning
|
|
||||||
};
|
|
||||||
}
|
|
||||||
this.touchStart = undefined;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MODE_OBJECT_SELECT: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame() {
|
|
||||||
// Add another entry for the current pointer position
|
|
||||||
const { pointerHistory } = this;
|
|
||||||
if (pointerHistory.length) {
|
|
||||||
const currentPointer = pointerHistory[pointerHistory.length - 1];
|
|
||||||
this.updatePointer(currentPointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
select.js
Normal file
33
select.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export class Select {
|
||||||
|
sim = undefined;
|
||||||
|
box = {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
|
this.box.start = this.sim.screenToSim(clientX, clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerMove({x: clientX, y: clientY}) {
|
||||||
|
this.box.end = this.sim.screenToSim(clientX, clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp() {
|
||||||
|
console.log('selection box', {...this.box});
|
||||||
|
this.box = {
|
||||||
|
start: undefined,
|
||||||
|
end: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
frame() {
|
||||||
|
if (!this.box.start) return;
|
||||||
|
|
||||||
|
this.sim.display.drawBox(this.box)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,11 +12,9 @@ export const simOptions = {
|
|||||||
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {hideUnless: 'display.acceleration'}],
|
accelerationScale: ['Accel<br>Vec Scale', 'number', 800, {hideUnless: 'display.acceleration'}],
|
||||||
targetFrameRate: ['Frame Rate', 'number', 60],
|
targetFrameRate: ['Frame Rate', 'number', 60],
|
||||||
},
|
},
|
||||||
collision: {
|
|
||||||
merge: ['Merge Masses on Collision', 'boolean', true, {wide: true}],
|
|
||||||
},
|
|
||||||
compensate: {
|
compensate: {
|
||||||
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
timeScale: ['Time Scale Compensator', 'boolean', false, {wide: true}],
|
||||||
|
fastPanning: ['Fast Panning', 'boolean', false],
|
||||||
},
|
},
|
||||||
param: {
|
param: {
|
||||||
gravity: ['Gravity', 'number', 1],
|
gravity: ['Gravity', 'number', 1],
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import {Overlay} from './overlay.js';
|
|
||||||
import {Pointer} from './pointer.js';
|
|
||||||
import {ModeSwitch} from './tool/modes.js';
|
import {ModeSwitch} from './tool/modes.js';
|
||||||
import {OptionsTool} from './tool/options.js';
|
import {OptionsTool} from './tool/options.js';
|
||||||
import {PlayPause} from './tool/play-pause.js';
|
import {PlayPause} from './tool/play-pause.js';
|
||||||
@ -21,7 +19,6 @@ export function initializeTools(sim) {
|
|||||||
.addTool(new OptionsTool([
|
.addTool(new OptionsTool([
|
||||||
'pauseDuring',
|
'pauseDuring',
|
||||||
'display',
|
'display',
|
||||||
'collision',
|
|
||||||
'compensate',
|
'compensate',
|
||||||
])),
|
])),
|
||||||
params: new Toolbar(sim, 'Params')
|
params: new Toolbar(sim, 'Params')
|
||||||
@ -44,8 +41,6 @@ export function initializeTools(sim) {
|
|||||||
.addToolbar(params)
|
.addToolbar(params)
|
||||||
.addToolbar(debug),
|
.addToolbar(debug),
|
||||||
};
|
};
|
||||||
sim.overlay = new Overlay(sim);
|
|
||||||
sim.pointer = new Pointer(sim);
|
|
||||||
|
|
||||||
for (const id in sim.toolbars) {
|
for (const id in sim.toolbars) {
|
||||||
const toolbar = sim.toolbars[id];
|
const toolbar = sim.toolbars[id];
|
||||||
|
|||||||
34
simulator.js
34
simulator.js
@ -2,12 +2,15 @@ import {
|
|||||||
EVENT_ZOOM,
|
EVENT_ZOOM,
|
||||||
FRAMERATE_SAMPLE_DURATION,
|
FRAMERATE_SAMPLE_DURATION,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { Display } from './display.js';
|
import {Display} from './display.js';
|
||||||
import { System } from './system.js';
|
import {System} from './system.js';
|
||||||
import { Options } from './options.js';
|
import {Overlay} from './overlay.js';
|
||||||
import { Zoom } from './zoom.js';
|
import {Pointer} from './pointer.js';
|
||||||
import { simOptions } from './sim-options.js';
|
import {Options} from './options.js';
|
||||||
import { initializeTools } from './sim-tools.js';
|
import {Zoom} from './zoom.js';
|
||||||
|
import {Select} from './select.js';
|
||||||
|
import {simOptions} from './sim-options.js';
|
||||||
|
import {initializeTools} from './sim-tools.js';
|
||||||
|
|
||||||
export class Sim {
|
export class Sim {
|
||||||
info = {};
|
info = {};
|
||||||
@ -25,7 +28,8 @@ export class Sim {
|
|||||||
overlay = undefined;
|
overlay = undefined;
|
||||||
pointer = undefined;
|
pointer = undefined;
|
||||||
zoom = undefined;
|
zoom = undefined;
|
||||||
|
select = undefined;
|
||||||
|
|
||||||
toolbarGroups = {};
|
toolbarGroups = {};
|
||||||
toolbars = {};
|
toolbars = {};
|
||||||
|
|
||||||
@ -45,7 +49,10 @@ export class Sim {
|
|||||||
this.options = new Options(this, simOptions);
|
this.options = new Options(this, simOptions);
|
||||||
this.display = new Display(this);
|
this.display = new Display(this);
|
||||||
this.system = new System(this);
|
this.system = new System(this);
|
||||||
|
this.overlay = new Overlay(this);
|
||||||
|
this.pointer = new Pointer(this);
|
||||||
this.zoom = new Zoom(this);
|
this.zoom = new Zoom(this);
|
||||||
|
this.select = new Select(this);
|
||||||
|
|
||||||
initializeTools(this);
|
initializeTools(this);
|
||||||
|
|
||||||
@ -78,6 +85,7 @@ export class Sim {
|
|||||||
this.zoom.frame();
|
this.zoom.frame();
|
||||||
this.pointer.frame();
|
this.pointer.frame();
|
||||||
this.display.frame(elapsedTime);
|
this.display.frame(elapsedTime);
|
||||||
|
this.select.frame();
|
||||||
this.system.frame(elapsedTime);
|
this.system.frame(elapsedTime);
|
||||||
this.overlay.frame();
|
this.overlay.frame();
|
||||||
for (const group in this.toolbarGroups) {
|
for (const group in this.toolbarGroups) {
|
||||||
@ -87,7 +95,7 @@ export class Sim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markFrame(t) {
|
markFrame(t) {
|
||||||
const { recentFrames: rfs } = this;
|
const {recentFrames: rfs} = this;
|
||||||
if (!rfs.length) {
|
if (!rfs.length) {
|
||||||
rfs.push(t);
|
rfs.push(t);
|
||||||
return;
|
return;
|
||||||
@ -96,7 +104,7 @@ export class Sim {
|
|||||||
let duration = t - oldest;
|
let duration = t - oldest;
|
||||||
const count = rfs.length + 1;
|
const count = rfs.length + 1;
|
||||||
const frameRate = 1000 * count / duration;
|
const frameRate = 1000 * count / duration;
|
||||||
const targetFrameRate = parseInt(this.getOption('display.targetFrameRate'));
|
const targetFrameRate = this.getOption('display.targetFrameRate');
|
||||||
if (frameRate > targetFrameRate + 1) {
|
if (frameRate > targetFrameRate + 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -122,10 +130,18 @@ export class Sim {
|
|||||||
|
|
||||||
play() {
|
play() {
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
|
||||||
|
if (this.panning?.paused) {
|
||||||
|
this.panning.paused = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
|
||||||
|
if (this.panning?.velocity) {
|
||||||
|
this.panning.paused = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getScaleDisplay() {
|
getScaleDisplay() {
|
||||||
|
|||||||
386
system.js
386
system.js
@ -6,30 +6,208 @@ export class System {
|
|||||||
creatingObject = undefined;
|
creatingObject = undefined;
|
||||||
selectedObject = undefined;
|
selectedObject = undefined;
|
||||||
selectObjectStart = undefined;
|
selectObjectStart = undefined;
|
||||||
paused = false;
|
|
||||||
panVelocityPaused = undefined;
|
panVelocityPaused = undefined;
|
||||||
|
paused = false;
|
||||||
|
|
||||||
constructor(sim) {
|
constructor(sim) {
|
||||||
this.sim = sim;
|
this.sim = sim;
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
handlePointerDown({x, y}) {
|
||||||
if (this.sim.playing) {
|
// If pointer is touching an object, select the object
|
||||||
this.sim.playing = false;
|
const touchingObject = this.objectAtLocation(x, y);
|
||||||
this.paused = true;
|
|
||||||
if (this.sim.panning?.velocity) {
|
if (touchingObject !== undefined) {
|
||||||
this.sim.panning.paused = true;
|
this.selectObject(touchingObject, {x, y});
|
||||||
}
|
} else {
|
||||||
|
// Otherwise, create a new object
|
||||||
|
this.createObject(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePointerUp() {
|
||||||
|
const obj = this.getSelectedOrCreating();
|
||||||
|
if (obj === undefined) return;
|
||||||
|
this.doneCreatingObject();
|
||||||
|
this.deselect();
|
||||||
|
// Convert pointer velocity to simulation scale
|
||||||
|
const pointer = {...this.sim.pointer.latestVelocity};
|
||||||
|
obj.velocity.x = pointer.x / this.sim.display.scale;
|
||||||
|
obj.velocity.y = pointer.y / this.sim.display.scale;
|
||||||
|
|
||||||
|
// Including time scale - if time is slow, our motion is relatively faster
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
obj.velocity.x /= this.sim.timeScale;
|
||||||
|
obj.velocity.y /= this.sim.timeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sim.panning?.velocity) {
|
||||||
|
obj.velocity.x += this.sim.panning.velocity.x;
|
||||||
|
obj.velocity.y += this.sim.panning.velocity.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerMove({x, y}) {
|
||||||
|
// If the cursor moves while creating an object, or while an object is selected,
|
||||||
|
// update the position using the pointer motion but the velocity using the pointer velocity
|
||||||
|
const obj = this.getSelectedOrCreating();
|
||||||
|
if (obj === undefined) return;
|
||||||
|
const start = this.selectedObjectStart;
|
||||||
|
obj.position.x = start.x + (x - start.pointer.x);
|
||||||
|
obj.position.y = start.y + (y - start.pointer.y);
|
||||||
|
obj.velocity.x = 0;
|
||||||
|
obj.velocity.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// elapsedTime is given in milliseconds
|
||||||
|
frame(elapsedTime) {
|
||||||
|
// If we're creating an object, increment its mass
|
||||||
|
// with the mass creation rate accelerating over time
|
||||||
|
|
||||||
|
// Scaling this parameter because of millisecond conversion
|
||||||
|
|
||||||
|
if (this.creatingObject !== undefined) {
|
||||||
|
const obj = this.objects[this.creatingObject];
|
||||||
|
let massCreationRate = this.sim.getOption('param.massCreationRate');
|
||||||
|
massCreationRate /= this.sim.display.scale;
|
||||||
|
// Keep consistent time scale
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
massCreationRate /= this.sim.timeScale;
|
||||||
|
}
|
||||||
|
obj.mass += massCreationRate * elapsedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate forces due to gravity.
|
||||||
|
this.computeForces();
|
||||||
|
|
||||||
|
if (this.sim.playing) {
|
||||||
|
// Predict positions (Velocity verlet method)
|
||||||
|
this.forEachObject(obj => {
|
||||||
|
obj.currentAcceleration = {...obj.acceleration};
|
||||||
|
|
||||||
|
// If this object is being created/selected, clamp its position
|
||||||
|
if (obj.id === this.getSelectedOrCreating()?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.position.x += elapsedTime *
|
||||||
|
(obj.velocity.x + 1 / 2 * obj.currentAcceleration.x * elapsedTime);
|
||||||
|
obj.position.y += elapsedTime *
|
||||||
|
(obj.velocity.y + 1 / 2 * obj.currentAcceleration.y * elapsedTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collisions
|
||||||
|
this.forEachObject((A, i) => {
|
||||||
|
this.forEachObject((B, j) => {
|
||||||
|
const dx = (B.position.x - A.position.x);
|
||||||
|
const dy = (B.position.y - A.position.y);
|
||||||
|
const dSquared = dx ** 2 + dy ** 2;
|
||||||
|
const d = Math.sqrt(dSquared);
|
||||||
|
if (d < A.radius + B.radius) {
|
||||||
|
let S, T;
|
||||||
|
// Merge the older into the newer, in order to provide mass creation rate continuity
|
||||||
|
if (A.age > B.age) {
|
||||||
|
// A merges into B; B survives
|
||||||
|
S = B;
|
||||||
|
T = A;
|
||||||
|
// If A was selected or being created, select S instead
|
||||||
|
if (this.creatingObject === i) this.creatingObject = j;
|
||||||
|
if (this.selectedObject === i) this.selectedObject = j;
|
||||||
|
} else {
|
||||||
|
// B merges into A; A survives
|
||||||
|
S = A;
|
||||||
|
T = B;
|
||||||
|
// If B was selected or being created, select S instead
|
||||||
|
if (this.creatingObject === j) this.creatingObject = i;
|
||||||
|
if (this.selectedObject === j) this.selectedObject = i;
|
||||||
|
}
|
||||||
|
// Merge T into S:
|
||||||
|
// Set position = center of mass
|
||||||
|
// Set velocity = total momentum / total mass
|
||||||
|
// Combine forces
|
||||||
|
// Sdd masses
|
||||||
|
// Sverage color
|
||||||
|
S.position.x = (S.position.x * S.mass + T.position.x * T.mass) / (S.mass + T.mass);
|
||||||
|
S.position.y = (S.position.y * S.mass + T.position.y * T.mass) / (S.mass + T.mass);
|
||||||
|
S.velocity.x = (S.velocity.x * S.mass + T.velocity.x * T.mass) / (S.mass + T.mass);
|
||||||
|
S.velocity.y = (S.velocity.y * S.mass + T.velocity.y * T.mass) / (S.mass + T.mass);
|
||||||
|
S.forces.push(...T.forces);
|
||||||
|
S.mass += T.mass;
|
||||||
|
S.color = {
|
||||||
|
r: (S.mass * S.color.r + T.mass * T.color.r) / (S.mass + T.mass),
|
||||||
|
g: (S.mass * S.color.g + T.mass * T.color.g) / (S.mass + T.mass),
|
||||||
|
b: (S.mass * S.color.b + T.mass * T.color.b) / (S.mass + T.mass),
|
||||||
|
};
|
||||||
|
T.alive = false;
|
||||||
|
T.forces = [];
|
||||||
|
}
|
||||||
|
}, {alive: true, startWith: i + 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recompute forces
|
||||||
|
this.computeForces();
|
||||||
|
|
||||||
|
// Predict velocities
|
||||||
|
this.forEachObject(obj => {
|
||||||
|
const acceleration = {...obj.acceleration};
|
||||||
|
obj.acceleration = {
|
||||||
|
x: (obj.currentAcceleration.x + acceleration.x) / 2,
|
||||||
|
y: (obj.currentAcceleration.y + acceleration.y) / 2,
|
||||||
|
};
|
||||||
|
obj.velocity.x += obj.acceleration.x * elapsedTime;
|
||||||
|
obj.velocity.y += obj.acceleration.y * elapsedTime;
|
||||||
|
|
||||||
|
// Append to object history
|
||||||
|
obj.history.push({position: {...obj.position}});
|
||||||
|
|
||||||
|
// TODO: store object color changes in history
|
||||||
|
|
||||||
|
// Enforce object history length
|
||||||
|
while (obj.history.length > OBJECT_HISTORY_SIZE) {
|
||||||
|
obj.history.shift();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display objects info
|
||||||
|
// First clear info from previous frame
|
||||||
|
this.forEachObject((_obj, i) => {
|
||||||
|
delete this.sim.info[`Object ${i}`];
|
||||||
|
}, {alive: null});
|
||||||
|
if (this.sim.getOption('debug.objectsInfo')) {
|
||||||
|
const aliveOnly = this.sim.getOption('debug.aliveObjects');
|
||||||
|
this.forEachObject((obj, i) => {
|
||||||
|
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1 / 2);
|
||||||
|
const accel = Math.pow(obj.acceleration.x ** 2 + obj.acceleration.y ** 2, 1 / 2);
|
||||||
|
// Invert y so that the angle is counterclockwise from x-axis
|
||||||
|
const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI;
|
||||||
|
const accelDir = Math.atan2(-obj.acceleration.y, obj.acceleration.x) * 180 / Math.PI;
|
||||||
|
const {r, g, b} = obj.color;
|
||||||
|
this.sim.info[`Object ${i}`] = [
|
||||||
|
`<span style="background-color: rgb(${r},${g},${b});"> </span>`,
|
||||||
|
`${obj.position.x.toPrecision(4)}, `,
|
||||||
|
`${obj.position.y.toPrecision(4)}, `,
|
||||||
|
`${obj.mass.toPrecision(4)} kg, `,
|
||||||
|
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
||||||
|
`${accel.toPrecision(2)} m/s<sup>2</sup>, ${accelDir.toPrecision(2)}°`,
|
||||||
|
`Alive: ${obj.alive}`,
|
||||||
|
];
|
||||||
|
}, {alive: aliveOnly || null});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the objects
|
||||||
|
this.drawObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.sim.pause();
|
||||||
|
this.paused = true;
|
||||||
|
}
|
||||||
|
|
||||||
resume() {
|
resume() {
|
||||||
if (this.paused) {
|
if (this.paused) {
|
||||||
this.sim.playing = true;
|
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
if (this.sim.panning?.paused) {
|
this.sim.play();
|
||||||
this.sim.panning.paused = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,52 +316,6 @@ export class System {
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x, y}) {
|
|
||||||
// If pointer is touching an object, select the object
|
|
||||||
const touchingObject = this.objectAtLocation(x, y);
|
|
||||||
|
|
||||||
if (touchingObject !== undefined) {
|
|
||||||
this.selectObject(touchingObject, {x, y});
|
|
||||||
} else {
|
|
||||||
// Otherwise, create a new object
|
|
||||||
this.createObject(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePointerUp() {
|
|
||||||
const obj = this.getSelectedOrCreating();
|
|
||||||
if (obj === undefined) return;
|
|
||||||
this.doneCreatingObject();
|
|
||||||
this.deselect();
|
|
||||||
// Convert pointer velocity to simulation scale
|
|
||||||
const pointer = {...this.sim.pointer.latestVelocity};
|
|
||||||
obj.velocity.x = pointer.x / this.sim.display.scale;
|
|
||||||
obj.velocity.y = pointer.y / this.sim.display.scale;
|
|
||||||
|
|
||||||
// Including time scale - if time is slow, our motion is relatively faster
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
obj.velocity.x /= this.sim.timeScale;
|
|
||||||
obj.velocity.y /= this.sim.timeScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.sim.panning?.velocity) {
|
|
||||||
obj.velocity.x += this.sim.panning.velocity.x;
|
|
||||||
obj.velocity.y += this.sim.panning.velocity.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePointerMove({x, y}) {
|
|
||||||
// If the cursor moves while creating an object, or while an object is selected,
|
|
||||||
// update the position using the pointer motion but the velocity using the pointer velocity
|
|
||||||
const obj = this.getSelectedOrCreating();
|
|
||||||
if (obj === undefined) return;
|
|
||||||
const start = this.selectedObjectStart;
|
|
||||||
obj.position.x = start.x + (x - start.pointer.x);
|
|
||||||
obj.position.y = start.y + (y - start.pointer.y);
|
|
||||||
obj.velocity.x = 0;
|
|
||||||
obj.velocity.y = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cb: (obj, idx) => {}
|
// cb: (obj, idx) => {}
|
||||||
forEachObject(cb, {alive, startWith} = {}) {
|
forEachObject(cb, {alive, startWith} = {}) {
|
||||||
if (alive === undefined) alive = true;
|
if (alive === undefined) alive = true;
|
||||||
@ -237,143 +369,6 @@ export class System {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// elapsedTime is given in milliseconds
|
|
||||||
frame(elapsedTime) {
|
|
||||||
// If we're creating an object, increment its mass
|
|
||||||
// with the mass creation rate accelerating over time
|
|
||||||
|
|
||||||
// Scaling this parameter because of millisecond conversion
|
|
||||||
|
|
||||||
if (this.creatingObject !== undefined) {
|
|
||||||
const obj = this.objects[this.creatingObject];
|
|
||||||
let massCreationRate = this.sim.getOption('param.massCreationRate');
|
|
||||||
massCreationRate /= this.sim.display.scale;
|
|
||||||
// Keep consistent time scale
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
massCreationRate /= this.sim.timeScale;
|
|
||||||
}
|
|
||||||
obj.mass += massCreationRate * elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate forces due to gravity.
|
|
||||||
this.computeForces();
|
|
||||||
|
|
||||||
if (this.sim.playing) {
|
|
||||||
// Predict positions (Velocity verlet method)
|
|
||||||
this.forEachObject(obj => {
|
|
||||||
obj.currentAcceleration = {...obj.acceleration};
|
|
||||||
|
|
||||||
obj.position.x += elapsedTime *
|
|
||||||
(obj.velocity.x + 1 / 2 * obj.currentAcceleration.x * elapsedTime);
|
|
||||||
obj.position.y += elapsedTime *
|
|
||||||
(obj.velocity.y + 1 / 2 * obj.currentAcceleration.y * elapsedTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collisions
|
|
||||||
if (this.sim.getOption('collision.merge')) {
|
|
||||||
this.forEachObject((A, i) => {
|
|
||||||
this.forEachObject((B, j) => {
|
|
||||||
const dx = (B.position.x - A.position.x);
|
|
||||||
const dy = (B.position.y - A.position.y);
|
|
||||||
const dSquared = dx ** 2 + dy ** 2;
|
|
||||||
const d = Math.sqrt(dSquared);
|
|
||||||
if (d < A.radius + B.radius) {
|
|
||||||
let S, T;
|
|
||||||
// Merge the older into the newer, in order to provide mass creation rate continuity
|
|
||||||
if (A.age > B.age) {
|
|
||||||
// A merges into B; B survives
|
|
||||||
S = B;
|
|
||||||
T = A;
|
|
||||||
// If A was selected or being created, select S instead
|
|
||||||
if (this.creatingObject === i) this.creatingObject = j;
|
|
||||||
if (this.selectedObject === i) this.selectedObject = j;
|
|
||||||
} else {
|
|
||||||
// B merges into A; A survives
|
|
||||||
S = A;
|
|
||||||
T = B;
|
|
||||||
// If B was selected or being created, select S instead
|
|
||||||
if (this.creatingObject === j) this.creatingObject = i;
|
|
||||||
if (this.selectedObject === j) this.selectedObject = i;
|
|
||||||
}
|
|
||||||
// Merge T into S:
|
|
||||||
// Set position = center of mass
|
|
||||||
// Set velocity = total momentum / total mass
|
|
||||||
// Combine forces
|
|
||||||
// Sdd masses
|
|
||||||
// Sverage color
|
|
||||||
S.position.x = (S.position.x * S.mass + T.position.x * T.mass) / (S.mass + T.mass);
|
|
||||||
S.position.y = (S.position.y * S.mass + T.position.y * T.mass) / (S.mass + T.mass);
|
|
||||||
S.velocity.x = (S.velocity.x * S.mass + T.velocity.x * T.mass) / (S.mass + T.mass);
|
|
||||||
S.velocity.y = (S.velocity.y * S.mass + T.velocity.y * T.mass) / (S.mass + T.mass);
|
|
||||||
S.forces.push(...T.forces);
|
|
||||||
S.mass += T.mass;
|
|
||||||
S.color = {
|
|
||||||
r: (S.mass * S.color.r + T.mass * T.color.r) / (S.mass + T.mass),
|
|
||||||
g: (S.mass * S.color.g + T.mass * T.color.g) / (S.mass + T.mass),
|
|
||||||
b: (S.mass * S.color.b + T.mass * T.color.b) / (S.mass + T.mass),
|
|
||||||
};
|
|
||||||
T.alive = false;
|
|
||||||
T.forces = [];
|
|
||||||
}
|
|
||||||
}, {alive: true, startWith: i + 1});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recompute forces
|
|
||||||
this.computeForces();
|
|
||||||
|
|
||||||
// Predict velocities
|
|
||||||
this.forEachObject(obj => {
|
|
||||||
const acceleration = {...obj.acceleration};
|
|
||||||
obj.acceleration = {
|
|
||||||
x: (obj.currentAcceleration.x + acceleration.x) / 2,
|
|
||||||
y: (obj.currentAcceleration.y + acceleration.y) / 2,
|
|
||||||
};
|
|
||||||
obj.velocity.x += obj.acceleration.x * elapsedTime;
|
|
||||||
obj.velocity.y += obj.acceleration.y * elapsedTime;
|
|
||||||
|
|
||||||
// Append to object history
|
|
||||||
obj.history.push({position: {...obj.position}});
|
|
||||||
|
|
||||||
// TODO: store object color changes in history
|
|
||||||
|
|
||||||
// Enforce object history length
|
|
||||||
while (obj.history.length > OBJECT_HISTORY_SIZE) {
|
|
||||||
obj.history.shift();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display objects info
|
|
||||||
// First clear info from previous frame
|
|
||||||
this.forEachObject((_obj, i) => {
|
|
||||||
delete this.sim.info[`Object ${i}`];
|
|
||||||
}, {alive: null});
|
|
||||||
if (this.sim.getOption('debug.objectsInfo')) {
|
|
||||||
const aliveOnly = this.sim.getOption('debug.aliveObjects');
|
|
||||||
this.forEachObject((obj, i) => {
|
|
||||||
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.y ** 2, 1 / 2);
|
|
||||||
const accel = Math.pow(obj.acceleration.x ** 2 + obj.acceleration.y ** 2, 1 / 2);
|
|
||||||
// Invert y so that the angle is counterclockwise from x-axis
|
|
||||||
const direction = Math.atan2(-obj.velocity.y, obj.velocity.x) * 180 / Math.PI;
|
|
||||||
const accelDir = Math.atan2(-obj.acceleration.y, obj.acceleration.x) * 180 / Math.PI;
|
|
||||||
const {r, g, b} = obj.color;
|
|
||||||
this.sim.info[`Object ${i}`] = [
|
|
||||||
`<span style="background-color: rgb(${r},${g},${b});"> </span>`,
|
|
||||||
`${obj.position.x.toPrecision(4)}, `,
|
|
||||||
`${obj.position.y.toPrecision(4)}, `,
|
|
||||||
`${obj.mass.toPrecision(4)} kg, `,
|
|
||||||
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
|
||||||
`${accel.toPrecision(2)} m/s<sup>2</sup>, ${accelDir.toPrecision(2)}°`,
|
|
||||||
`Alive: ${obj.alive}`,
|
|
||||||
];
|
|
||||||
}, {alive: aliveOnly || null});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the objects
|
|
||||||
this.drawObjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
computeSystemCenter() {
|
computeSystemCenter() {
|
||||||
// Determine center of mass
|
// Determine center of mass
|
||||||
const {totalMass, count, totalMassLocation} =
|
const {totalMass, count, totalMassLocation} =
|
||||||
@ -423,4 +418,5 @@ export class System {
|
|||||||
return acc + obj.mass * s / d;
|
return acc + obj.mass * s / d;
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export class OptionsTool extends Tool {
|
|||||||
super.setContainer(container);
|
super.setContainer(container);
|
||||||
for (const sectionName of this.sections) {
|
for (const sectionName of this.sections) {
|
||||||
const option = this.sim.options.getSection(sectionName);
|
const option = this.sim.options.getSection(sectionName);
|
||||||
const child = this.visitItem(option);
|
const item = this.visitItem(option);
|
||||||
this.div.appendChild(child);
|
this.div.appendChild(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user