path traces; collisions
This commit is contained in:
parent
952ea553c7
commit
103fd7f694
@ -6,6 +6,7 @@ export const DISPLAY_CURRENT_SCALE = true;
|
||||
export const DISPLAY_CURRENT_MODE = false;
|
||||
export const DISPLAY_VELOCITY_VECTORS = true;
|
||||
export const DISPLAY_ACCELERATION_VECTORS = true;
|
||||
export const DISPLAY_PATH_TRACES = false;
|
||||
|
||||
// VELOCITY
|
||||
export const VELOCITY_VECTOR_SCALE = 5E0;
|
||||
@ -19,6 +20,11 @@ export const ACCELERATION_VECTOR_COLOR = 'rgb(0, 128, 0)'; // optionally set to
|
||||
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_WIDTH = 1.5;
|
||||
|
||||
// SCALING FACTORS
|
||||
export const MASS_CREATION_RATE = 1E1;
|
||||
export const POINTER_HISTORY_SIZE = 15;
|
||||
@ -45,3 +51,4 @@ export const MODE_PAN_VIEW = 'pan-view';
|
||||
// OPTIONS
|
||||
export const PAUSE_DURING_CREATION = true;
|
||||
export const PAUSE_DURING_SELECTION = true;
|
||||
export const MERGE_ON_COLLIDE = true;
|
||||
|
||||
63
display.js
63
display.js
@ -13,8 +13,8 @@ import {
|
||||
OFFSCREEN_OBJECT_LINE_WIDTH,
|
||||
OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
|
||||
DISPLAY_CANVAS_SIZE,
|
||||
DISPLAY_VELOCITY_VECTORS,
|
||||
DISPLAY_ACCELERATION_VECTORS,
|
||||
PATH_TRACES_COLOR,
|
||||
PATH_TRACES_WIDTH,
|
||||
} from './config.js';
|
||||
|
||||
export class Display {
|
||||
@ -63,6 +63,13 @@ export class Display {
|
||||
};
|
||||
}
|
||||
|
||||
simToScreen(x, y) {
|
||||
return {
|
||||
x: (x - this.viewOrigin.x) * this.scale,
|
||||
y: (y - this.viewOrigin.y) * this.scale,
|
||||
};
|
||||
}
|
||||
|
||||
fullscreen() {
|
||||
this.canvas.width = document.documentElement.clientWidth;
|
||||
this.canvas.height = document.documentElement.clientHeight;
|
||||
@ -77,8 +84,7 @@ export class Display {
|
||||
ctx.fillRect(this.viewOrigin.x, this.viewOrigin.y, this.width, this.height);
|
||||
}
|
||||
|
||||
drawObject(idx) {
|
||||
const obj = this.sim.objects.object(idx);
|
||||
drawObject(obj) {
|
||||
const ctx = this.ctx;
|
||||
const {r, g, b} = obj.color;
|
||||
const {x, y} = obj.position;
|
||||
@ -90,6 +96,47 @@ export class Display {
|
||||
const oy = this.viewOrigin.y;
|
||||
const cx = ox + W / 2;
|
||||
const cy = oy + H / 2;
|
||||
|
||||
// Draw path traces
|
||||
if (this.sim.getOption('display.traces') && obj.history?.length) {
|
||||
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
|
||||
`rgb(${r}, ${g}, ${b})` : PATH_TRACES_COLOR;
|
||||
ctx.lineWidth = PATH_TRACES_WIDTH / this.scale;
|
||||
ctx.beginPath();
|
||||
let dash = false;
|
||||
const skip = 1;
|
||||
let skipped = 0;
|
||||
{
|
||||
const [{position}] = obj.history;
|
||||
const x = position.x;
|
||||
const y = position.y;
|
||||
|
||||
if (Math.abs(x - cx) <= W / 2 &&
|
||||
Math.abs(y - cy) <= H / 2) {
|
||||
ctx.moveTo(x, y);
|
||||
dash = true;
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < obj.history.length; i++) {
|
||||
if (++skipped < skip) continue;
|
||||
skipped = 0;
|
||||
const {position} = obj.history[i];
|
||||
const x = position.x;
|
||||
const y = position.y;
|
||||
if (dash) {
|
||||
ctx.lineTo(x, y);
|
||||
dash = false;
|
||||
} else if (Math.abs(x - cx) <= W / 2 &&
|
||||
Math.abs(y - cy) <= H / 2) {
|
||||
ctx.moveTo(x, y);
|
||||
dash = true;
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (!obj.alive) return;
|
||||
|
||||
// If the object is outside the display area, draw an arrow at the edge of the display
|
||||
if (Math.abs(x - cx) - radius >= W / 2 ||
|
||||
Math.abs(y - cy) - radius >= H / 2) {
|
||||
@ -146,7 +193,7 @@ export class Display {
|
||||
ctx.fill();
|
||||
|
||||
// Draw arrow for the velocity
|
||||
if (DISPLAY_VELOCITY_VECTORS) {
|
||||
if (this.sim.getOption('show.velocity')) {
|
||||
const speed = Math.sqrt(vx ** 2 + vy ** 2);
|
||||
const endVx = x + VELOCITY_VECTOR_SCALE * vx / speed * Math.log(speed);
|
||||
const endVy = y + VELOCITY_VECTOR_SCALE * vy / speed * Math.log(speed);
|
||||
@ -162,7 +209,7 @@ export class Display {
|
||||
}
|
||||
|
||||
// Draw arrow for acceleration
|
||||
if (DISPLAY_ACCELERATION_VECTORS) {
|
||||
if (this.sim.getOption('show.acceleration')) {
|
||||
const accelerationMagnitude = Math.sqrt(acceleration.x ** 2 + acceleration.y ** 2);
|
||||
const endAx = x + ACCELERATION_VECTOR_SCALE * acceleration.x /
|
||||
accelerationMagnitude * Math.log(accelerationMagnitude);
|
||||
@ -181,9 +228,7 @@ export class Display {
|
||||
}
|
||||
|
||||
drawObjects() {
|
||||
for (let i = 0; i < this.sim.objects.length; i++) {
|
||||
this.drawObject(i);
|
||||
}
|
||||
this.sim.objects.forEachObject(obj => this.drawObject(obj), null);
|
||||
}
|
||||
|
||||
drawArrow(startX, startY, endX, endY, {style, width, arrowhead, arrowheadLength, fill, ifShort}) {
|
||||
|
||||
@ -8,7 +8,7 @@ body {
|
||||
background-color: #000;
|
||||
color: #8f8;
|
||||
font-family: monospace;
|
||||
font-size: 14pt;
|
||||
font-size: 12pt;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export class MassObject {
|
||||
index = undefined;
|
||||
mass = 0;
|
||||
density = 1;
|
||||
position = {x: undefined, y: undefined};
|
||||
@ -8,9 +7,10 @@ export class MassObject {
|
||||
color = {r: undefined, g: undefined, b: undefined};
|
||||
created = undefined;
|
||||
forces = []; // [{x, y}]
|
||||
history = [];
|
||||
alive = true;
|
||||
|
||||
constructor(x, y, index) {
|
||||
this.index = index;
|
||||
constructor(x, y) {
|
||||
this.position.x = x;
|
||||
this.position.y = y;
|
||||
this.color.r = Math.random() * 256;
|
||||
|
||||
97
objects.js
97
objects.js
@ -83,8 +83,7 @@ export class Objects {
|
||||
start: {x: undefined, y: undefined},
|
||||
end: {x: undefined, y: undefined},
|
||||
};
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
this.forEachObject(obj => {
|
||||
if (box.start.x === undefined) {
|
||||
box.start = {...obj.position};
|
||||
box.end = {...obj.position};
|
||||
@ -94,19 +93,18 @@ export class Objects {
|
||||
if (obj.position.y < box.start.y) box.start.y = obj.position.y;
|
||||
if (obj.position.y > box.end.y) box.end.y = obj.position.y;
|
||||
}
|
||||
}
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
objectAtLocation(x, y) {
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
this.forEachObject(obj => {
|
||||
// If distance to object is less than object's radius, we are touching the object
|
||||
const dist = Math.pow((obj.position.x - x)**2 + (obj.position.y - y)**2, 1/2);
|
||||
if (dist <= obj.radius) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handlePointerDown({x, y}) {
|
||||
@ -138,15 +136,23 @@ export class Objects {
|
||||
}
|
||||
}
|
||||
|
||||
// cb: (obj, idx) => {}
|
||||
forEachObject(cb, alive = true, startWith = 0) {
|
||||
for (let i = startWith; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
if (alive === null || alive == obj.alive) {
|
||||
cb(obj, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computeForces() {
|
||||
if (this.objects.length < 2) return;
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
this.objects[i].forces = [];
|
||||
}
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const A = this.objects[i];
|
||||
for (let j = i + 1; j < this.objects.length; j++) {
|
||||
const B = this.objects[j];
|
||||
this.forEachObject(obj => {
|
||||
obj.forces = [];
|
||||
});
|
||||
this.forEachObject((A, i) => {
|
||||
this.forEachObject(B => {
|
||||
const dx = (B.position.x - A.position.x);
|
||||
const dy = (B.position.y - A.position.y);
|
||||
const dSquared = dx ** 2 + dy ** 2;
|
||||
@ -156,14 +162,12 @@ export class Objects {
|
||||
const Fy = F * dy / d;
|
||||
A.forces.push({ x: Fx, y: Fy });
|
||||
B.forces.push({ x: -Fx, y: -Fy });
|
||||
}
|
||||
}
|
||||
}, true, i + 1);
|
||||
});
|
||||
// Also compute acceleration
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
this.forEachObject(obj => {
|
||||
obj.acceleration = obj.getAcceleration();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
computeFrame(elapsedTime) {
|
||||
@ -181,22 +185,55 @@ export class Objects {
|
||||
|
||||
if (this.sim.playing) {
|
||||
// Predict positions (Velocity verlet method)
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
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
|
||||
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) {
|
||||
// Merge B into A:
|
||||
// Set position = center of mass
|
||||
// Set velocity = total momentum / total mass
|
||||
// Combine forces
|
||||
// Add masses
|
||||
// Average color?
|
||||
// If B was selected or being created, select A instead
|
||||
// Remove B using splice
|
||||
A.position.x = (A.position.x * A.mass + B.position.x * B.mass) / (A.mass + B.mass);
|
||||
A.position.y = (A.position.y * A.mass + B.position.y * B.mass) / (A.mass + B.mass);
|
||||
A.velocity.x = (A.velocity.x * A.mass + B.velocity.x * B.mass) / (A.mass + B.mass);
|
||||
A.velocity.y = (A.velocity.y * A.mass + B.velocity.y * B.mass) / (A.mass + B.mass);
|
||||
A.forces.push(...B.forces);
|
||||
A.mass += B.mass;
|
||||
A.color = {
|
||||
r: (A.color.r + B.color.r) / 2,
|
||||
g: (A.color.g + B.color.g) / 2,
|
||||
b: (A.color.b + B.color.b) / 2,
|
||||
};
|
||||
if (this.creatingObject === j) this.creatingObject = i;
|
||||
if (this.selectedObject === j) this.selectedObject = i;
|
||||
B.alive = false;
|
||||
B.forces = [];
|
||||
}
|
||||
}, true, i + 1);
|
||||
});
|
||||
|
||||
// Recompute forces
|
||||
this.computeForces();
|
||||
|
||||
// Predict velocities
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
this.forEachObject(obj => {
|
||||
const acceleration = {...obj.acceleration};
|
||||
obj.acceleration = {
|
||||
x: (obj.currentAcceleration.x + acceleration.x) / 2,
|
||||
@ -204,15 +241,17 @@ export class Objects {
|
||||
};
|
||||
obj.velocity.x += obj.acceleration.x * elapsedTime;
|
||||
obj.velocity.y += obj.acceleration.y * elapsedTime;
|
||||
}
|
||||
// TODO: Collisions
|
||||
|
||||
// Append to object history
|
||||
// TODO: enforce object history length
|
||||
// TODO: store object color changes in history
|
||||
obj.history.push({position: {...obj.position}});
|
||||
});
|
||||
}
|
||||
|
||||
// Display objects info
|
||||
if (DISPLAY_OBJECTS_INFO) {
|
||||
for (let i = 0; i < this.objects.length; i++) {
|
||||
const obj = this.objects[i];
|
||||
this.forEachObject(obj => {
|
||||
const speed = Math.pow(obj.velocity.x ** 2 + obj.velocity.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;
|
||||
@ -222,7 +261,7 @@ export class Objects {
|
||||
`${obj.mass.toPrecision(6)} kg, `,
|
||||
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
DISPLAY_VELOCITY_VECTORS,
|
||||
PAUSE_DURING_CREATION,
|
||||
PAUSE_DURING_SELECTION,
|
||||
MERGE_ON_COLLIDE,
|
||||
} from '../config.js';
|
||||
|
||||
export class Options extends Tool {
|
||||
@ -13,13 +14,18 @@ export class Options extends Tool {
|
||||
items: [
|
||||
{ type: 'boolean', name: 'creation', title: 'Creation', default: PAUSE_DURING_CREATION },
|
||||
{ type: 'boolean', name: 'selection', title: 'Selection', default: PAUSE_DURING_SELECTION },
|
||||
]}, {
|
||||
type: 'group', name: 'show', title: 'Show Vectors',
|
||||
]}, {
|
||||
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: 'traces', title: 'Trace Path', default: DISPLAY_ACCELERATION_VECTORS },
|
||||
]}, {
|
||||
type: 'group', name: 'collision', title: 'Collisions',
|
||||
items: [
|
||||
{ type: 'boolean', name: 'merge', title: 'Merge', default: MERGE_ON_COLLIDE },
|
||||
]},
|
||||
];
|
||||
|
||||
values = {};
|
||||
|
||||
@ -58,6 +64,7 @@ export class Options extends Tool {
|
||||
const heading = document.createElement('h2');
|
||||
heading.innerHTML = 'Options';
|
||||
this.div.appendChild(heading);
|
||||
this.div.style.paddingTop = '0px';
|
||||
for (const item of this.options) {
|
||||
const child = this.visitItem(item);
|
||||
this.div.appendChild(child);
|
||||
|
||||
31
tool/zoom.js
31
tool/zoom.js
@ -11,22 +11,18 @@ export class Zoom extends Tool {
|
||||
const zoomOut = document.createElement('button');
|
||||
const zoomIn = document.createElement('button');
|
||||
const zoomAll = document.createElement('button');
|
||||
const zeroNetMomentum = document.createElement('button');
|
||||
|
||||
this.div.appendChild(zoomOut);
|
||||
this.div.appendChild(zoomIn);
|
||||
this.div.appendChild(document.createElement('br'));
|
||||
this.div.appendChild(zoomAll);
|
||||
this.div.appendChild(document.createElement('br'));
|
||||
this.div.appendChild(zeroNetMomentum);
|
||||
|
||||
zoomAll.classList.add('wide');
|
||||
zeroNetMomentum.classList.add('wide');
|
||||
|
||||
zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>';
|
||||
zoomIn.innerHTML = '<h2>Zoom<br>In</h2>';
|
||||
zoomAll.innerHTML = '<h2>Zoom to Fit</h2>';
|
||||
zeroNetMomentum.innerHTML = '<h2>Zero Net Momentum</h2>';
|
||||
|
||||
zoomOut.addEventListener('click', (e) => {
|
||||
// Aim at center of view
|
||||
@ -60,32 +56,5 @@ export class Zoom extends Tool {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
zeroNetMomentum.addEventListener('click', (e) => {
|
||||
const { objects } = this.sim.objects;
|
||||
|
||||
// Find total momentum
|
||||
let totalMomentum = objects.reduce((total, obj) => {
|
||||
const px = obj.mass * obj.velocity.x;
|
||||
const py = obj.mass * obj.velocity.y;
|
||||
return {
|
||||
x: total.x + px,
|
||||
y: total.y + py,
|
||||
};
|
||||
}, {x: 0, y: 0});
|
||||
|
||||
// Find average momentum
|
||||
let averageMomentum = {
|
||||
x: totalMomentum.x / objects.length,
|
||||
y: totalMomentum.y / objects.length,
|
||||
};
|
||||
|
||||
// Subtract average from each
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
const obj = objects[i];
|
||||
obj.velocity.x -= averageMomentum.x / obj.mass;
|
||||
obj.velocity.y -= averageMomentum.y / obj.mass;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export class Toolbar {
|
||||
this.div = div;
|
||||
this.sim.div.appendChild(div);
|
||||
div.style.position = 'relative';
|
||||
div.style.width = 'fit-content';
|
||||
div.style.width = '20EM';
|
||||
div.style.top = 0;
|
||||
div.style.left = 0;
|
||||
div.style.zIndex = 2;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user