Compare commits
2 Commits
08c3657baf
...
757e85bbcf
| Author | SHA1 | Date | |
|---|---|---|---|
| 757e85bbcf | |||
| 205b1cf899 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.swp
|
||||||
5
config.js
Normal file
5
config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const MASS_CREATION_RATE = 0.001;
|
||||||
|
export const DISPLAY_OBJECTS_INFO = false;
|
||||||
|
export const DISPLAY_CURSOR_INFO = false;
|
||||||
|
export const DISPLAY_VELOCITY_VECTORS = true;
|
||||||
|
export const POINTER_HISTORY_SIZE = 20;
|
||||||
64
display.js
Normal file
64
display.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
export class Display {
|
||||||
|
sim = undefined;
|
||||||
|
|
||||||
|
VELOCITY_VECTOR_SCALE = 0.2;
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
// Create canvas that fills the window
|
||||||
|
// If the window resizes, also resize the canvas
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext("2d");
|
||||||
|
this.sim.div.appendChild(canvas);
|
||||||
|
canvas.style.position = 'absolute';
|
||||||
|
canvas.style.top = 0;
|
||||||
|
canvas.style.left = 0;
|
||||||
|
this.fullscreen();
|
||||||
|
window.addEventListener('resize', () => this.fullscreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreen() {
|
||||||
|
this.canvas.width = document.documentElement.clientWidth;
|
||||||
|
this.canvas.height = document.documentElement.clientHeight;
|
||||||
|
// this.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillCanvas() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawObject(idx) {
|
||||||
|
const obj = this.sim.objects.object(idx);
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const {r, g, b} = obj.color;
|
||||||
|
const {x, y} = obj.position;
|
||||||
|
const {x: vx, y: vy} = obj.velocity;
|
||||||
|
const radius = obj.radius;
|
||||||
|
|
||||||
|
// Draw filled circle for the object
|
||||||
|
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, radius, 0, 2*Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Draw line for the velocity
|
||||||
|
// TODO: Arrow
|
||||||
|
ctx.strokeStyle = ctx.fillStyle;
|
||||||
|
ctx.lineWidth = 2.0;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
ctx.lineTo(x + this.VELOCITY_VECTOR_SCALE * vx, y + this.VELOCITY_VECTOR_SCALE * vy);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// TODO: Draw line for acceleration
|
||||||
|
}
|
||||||
|
|
||||||
|
drawObjects() {
|
||||||
|
for (let i = 0; i < this.sim.objects.length; i++) {
|
||||||
|
this.drawObject(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
object.js
Normal file
27
object.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export class MassObject {
|
||||||
|
mass = 0;
|
||||||
|
density = 1;
|
||||||
|
position = {x: undefined, y: undefined};
|
||||||
|
velocity = {x: 0, y: 0};
|
||||||
|
color = {r: undefined, g: undefined, b: undefined};
|
||||||
|
created = undefined;
|
||||||
|
|
||||||
|
constructor(x, y) {
|
||||||
|
this.position.x = x;
|
||||||
|
this.position.y = y;
|
||||||
|
this.color.r = Math.random() * 256;
|
||||||
|
this.color.g = Math.random() * 256;
|
||||||
|
this.color.b = Math.random() * 256;
|
||||||
|
this.created = document.timeline.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
get age() {
|
||||||
|
return document.timeline.currentTime - this.created;
|
||||||
|
}
|
||||||
|
|
||||||
|
get radius() {
|
||||||
|
// radius should be proportional to cube root of mass
|
||||||
|
return Math.pow(this.mass / this.density, 1/3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
84
objects.js
Normal file
84
objects.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { MassObject } from './object.js';
|
||||||
|
import { MASS_CREATION_RATE, DISPLAY_OBJECTS_INFO } from './config.js';
|
||||||
|
|
||||||
|
export class Objects {
|
||||||
|
objects = [];
|
||||||
|
creatingObject = undefined;
|
||||||
|
selectedObject = undefined;
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an object with mass that grows as pointer is held down
|
||||||
|
createObject(x, y) {
|
||||||
|
const obj = new MassObject(x, y);
|
||||||
|
const idx = this.objects.length;
|
||||||
|
this.creatingObject = idx;
|
||||||
|
this.objects.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
doneCreatingObject() {
|
||||||
|
this.creatingObject = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
object(i) {
|
||||||
|
return this.objects[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
selectObject(i) {
|
||||||
|
this.selectedObject = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
deselect() {
|
||||||
|
this.selectedObject = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedOrCreating() {
|
||||||
|
let i = this.creatingObject ?? this.selectedObject;
|
||||||
|
if (i !== undefined) {
|
||||||
|
return this.objects[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this.objects.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectAtLocation(x, y) {
|
||||||
|
for (let i = 0; i < this.objects.length; i++) {
|
||||||
|
const obj = this.objects[i];
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computeFrame(elapsedTime) {
|
||||||
|
// If we're creating an object, increment its mass
|
||||||
|
// with the mass creation rate accelerating over time
|
||||||
|
if (this.creatingObject !== undefined) {
|
||||||
|
const obj = this.objects[this.creatingObject];
|
||||||
|
const rate = MASS_CREATION_RATE * obj.age;
|
||||||
|
obj.mass += rate * elapsedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display objects info
|
||||||
|
if (DISPLAY_OBJECTS_INFO) {
|
||||||
|
for (let i = 0; i < this.objects.length; i++) {
|
||||||
|
const obj = this.objects[i];
|
||||||
|
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;
|
||||||
|
this.sim.info[`Object ${i}`] = [
|
||||||
|
`${obj.position.x}, `,
|
||||||
|
`${obj.position.y}, `,
|
||||||
|
`${obj.mass.toPrecision(6)} kg, `,
|
||||||
|
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
overlay.js
Normal file
37
overlay.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export class Overlay {
|
||||||
|
sim = undefined;
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
|
||||||
|
// Add info text box
|
||||||
|
const infoBox = document.createElement('div');
|
||||||
|
this.sim.div.appendChild(infoBox);
|
||||||
|
this.infoBox = infoBox;
|
||||||
|
infoBox.style.position = 'absolute';
|
||||||
|
infoBox.style.top = 0;
|
||||||
|
infoBox.style.left = 0;
|
||||||
|
infoBox.width = 'fit-content';
|
||||||
|
infoBox.style.zIndex = 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInfo() {
|
||||||
|
this.infoBox.innerHTML = '';
|
||||||
|
const table = document.createElement('table');
|
||||||
|
for (let [k, v] of Object.entries(this.sim.info)) {
|
||||||
|
let row = document.createElement('tr');
|
||||||
|
let keyCell = document.createElement('td');
|
||||||
|
keyCell.innerHTML = `${k}: `;
|
||||||
|
row.appendChild(keyCell);
|
||||||
|
let vs = Array.isArray(v) ? v : [v];
|
||||||
|
for (let x of vs) {
|
||||||
|
let valueCell = document.createElement('td');
|
||||||
|
valueCell.innerHTML = x;
|
||||||
|
row.appendChild(valueCell);
|
||||||
|
}
|
||||||
|
table.appendChild(row);
|
||||||
|
}
|
||||||
|
this.infoBox.appendChild(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
112
pointer.js
Normal file
112
pointer.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { DISPLAY_CURSOR_INFO, POINTER_HISTORY_SIZE } from './config.js';
|
||||||
|
|
||||||
|
export class Pointer {
|
||||||
|
sim = undefined;
|
||||||
|
|
||||||
|
pointerHistory = [];
|
||||||
|
|
||||||
|
constructor(sim) {
|
||||||
|
this.sim = sim;
|
||||||
|
|
||||||
|
// Monitor mouse movements
|
||||||
|
const el = window;
|
||||||
|
el.addEventListener('mousemove', e => {
|
||||||
|
if (DISPLAY_CURSOR_INFO) {
|
||||||
|
this.sim.info['Mouse move'] = [`${e.clientX}, `, `${e.clientY}`];
|
||||||
|
}
|
||||||
|
this.handlePointerMove(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor touch events
|
||||||
|
el.addEventListener('touchmove', e => {
|
||||||
|
if (DISPLAY_CURSOR_INFO) {
|
||||||
|
this.sim.info['Touch move'] = [`${e.touches[0].pageX}, `, `${e.touches[0].pageY}`];
|
||||||
|
}
|
||||||
|
this.handlePointerMove(e.touches[0].pageX, e.touches[0].pageY);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('pointerdown', e => {
|
||||||
|
if (DISPLAY_CURSOR_INFO) {
|
||||||
|
this.sim.info['Pointer down'] = [`${e.clientX}, `, `${e.clientY}`];
|
||||||
|
}
|
||||||
|
this.handlePointerDown(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('pointerup', e => {
|
||||||
|
if (DISPLAY_CURSOR_INFO) {
|
||||||
|
this.sim.info['Pointer up'] = [`${e.clientX}, `, `${e.clientY}`];
|
||||||
|
}
|
||||||
|
this.handlePointerUp(e.clientX, e.clientY);
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener('click', e => {
|
||||||
|
if (DISPLAY_CURSOR_INFO) {
|
||||||
|
this.sim.info['Click'] = [`${e.clientX}, `, `${e.clientY}`];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getPointerVelocity() {
|
||||||
|
// Average over pointer history
|
||||||
|
if (this.pointerHistory.length < 2) {
|
||||||
|
return {x: 0, y: 0};
|
||||||
|
}
|
||||||
|
const start = this.pointerHistory[0];
|
||||||
|
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
||||||
|
const dt = (end.t - start.t) / 1000;
|
||||||
|
return {
|
||||||
|
x: (end.x - start.x) / dt,
|
||||||
|
y: (end.y - start.y) / dt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPointerHistory() {
|
||||||
|
this.pointerHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePointer(x, y) {
|
||||||
|
const t = document.timeline.currentTime;
|
||||||
|
this.pointerHistory.push({x, y, t});
|
||||||
|
if (this.pointerHistory.length > POINTER_HISTORY_SIZE) {
|
||||||
|
this.pointerHistory.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerDown(x, y) {
|
||||||
|
this.clearPointerHistory();
|
||||||
|
this.updatePointer(x, y);
|
||||||
|
|
||||||
|
// If pointer is touching an object, select the object
|
||||||
|
const touchingObject = this.sim.objects.objectAtLocation(x, y);
|
||||||
|
|
||||||
|
if (touchingObject !== undefined) {
|
||||||
|
this.sim.objects.selectObject(touchingObject);
|
||||||
|
} else {
|
||||||
|
// Otherwise, create a new object
|
||||||
|
this.sim.objects.createObject(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePointerUp(x, y) {
|
||||||
|
this.sim.objects.doneCreatingObject();
|
||||||
|
this.sim.objects.deselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor (mouse or touch) movement
|
||||||
|
handlePointerMove(x, y) {
|
||||||
|
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
||||||
|
|
||||||
|
// If the cursor moves while creating an object, or while an object is selected,
|
||||||
|
// update the position and velocity of the object
|
||||||
|
const obj = this.sim.objects.getSelectedOrCreating();
|
||||||
|
if (obj !== undefined) {
|
||||||
|
this.updatePointer(x, y);
|
||||||
|
const {x: vx, y: vy} = this.getPointerVelocity();
|
||||||
|
obj.position.x = x;
|
||||||
|
obj.position.y = y;
|
||||||
|
obj.velocity.x = vx;
|
||||||
|
obj.velocity.y = vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
286
simulator.js
286
simulator.js
@ -1,298 +1,44 @@
|
|||||||
export class MassObject {
|
import { Display } from './display.js';
|
||||||
mass = 0;
|
import { Overlay } from './overlay.js';
|
||||||
density = 1;
|
import { Pointer } from './pointer.js';
|
||||||
position = {x: undefined, y: undefined};
|
import { Objects } from './objects.js';
|
||||||
velocity = {x: 0, y: 0};
|
|
||||||
color = {r: undefined, g: undefined, b: undefined};
|
|
||||||
created = undefined;
|
|
||||||
|
|
||||||
constructor(x, y) {
|
|
||||||
this.position.x = x;
|
|
||||||
this.position.y = y;
|
|
||||||
this.color.r = Math.random() * 256;
|
|
||||||
this.color.g = Math.random() * 256;
|
|
||||||
this.color.b = Math.random() * 256;
|
|
||||||
this.created = document.timeline.currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
get age() {
|
|
||||||
return document.timeline.currentTime - this.created;
|
|
||||||
}
|
|
||||||
|
|
||||||
get radius() {
|
|
||||||
// radius should be proportional to cube root of mass
|
|
||||||
return Math.pow(this.mass / this.density, 1/3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Sim {
|
export class Sim {
|
||||||
info = {};
|
info = {};
|
||||||
objects = [];
|
|
||||||
frame = 0;
|
frame = 0;
|
||||||
time = undefined;
|
time = undefined;
|
||||||
pointerHistory = [];
|
|
||||||
|
|
||||||
POINTER_HISTORY_SIZE = 20;
|
display = undefined;
|
||||||
MASS_CREATION_RATE = 0.001;
|
overlay = undefined;
|
||||||
DISPLAY_OBJECTS_INFO = false;
|
pointer = undefined;
|
||||||
DISPLAY_CURSOR_INFO = false;
|
objects = undefined;
|
||||||
DISPLAY_VELOCITY_VECTORS = true;
|
|
||||||
VELOCITY_VECTOR_SCALE = 0.2;
|
|
||||||
|
|
||||||
fullscreen() {
|
|
||||||
this.canvas.width = document.documentElement.clientWidth;
|
|
||||||
this.canvas.height = document.documentElement.clientHeight;
|
|
||||||
// this.info['Canvas'] = `${this.canvas.width} x ${this.canvas.height}`;
|
|
||||||
this.fillCanvas();
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
fillCanvas() {
|
|
||||||
const ctx = this.ctx;
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderInfo() {
|
|
||||||
this.infoBox.innerHTML = '';
|
|
||||||
const table = document.createElement('table');
|
|
||||||
for (let [k, v] of Object.entries(this.info)) {
|
|
||||||
let row = document.createElement('tr');
|
|
||||||
let keyCell = document.createElement('td');
|
|
||||||
keyCell.innerHTML = `${k}: `;
|
|
||||||
row.appendChild(keyCell);
|
|
||||||
let vs = Array.isArray(v) ? v : [v];
|
|
||||||
for (let x of vs) {
|
|
||||||
let valueCell = document.createElement('td');
|
|
||||||
valueCell.innerHTML = x;
|
|
||||||
row.appendChild(valueCell);
|
|
||||||
}
|
|
||||||
table.appendChild(row);
|
|
||||||
}
|
|
||||||
this.infoBox.appendChild(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
init(divId) {
|
init(divId) {
|
||||||
this.divId = divId;
|
this.divId = divId;
|
||||||
const div = document.getElementById(this.divId);
|
const div = document.getElementById(this.divId);
|
||||||
this.div = div;
|
this.div = div;
|
||||||
|
|
||||||
// Add info text box
|
this.display = new Display(this);
|
||||||
const infoBox = document.createElement('div');
|
this.overlay = new Overlay(this);
|
||||||
this.div.appendChild(infoBox);
|
this.pointer = new Pointer(this);
|
||||||
this.infoBox = infoBox;
|
this.objects = new Objects(this);
|
||||||
infoBox.style.position = 'absolute';
|
|
||||||
infoBox.style.top = 0;
|
|
||||||
infoBox.style.left = 0;
|
|
||||||
infoBox.width = 'fit-content';
|
|
||||||
infoBox.style.zIndex = 1;
|
|
||||||
|
|
||||||
// Create canvas that fills the window
|
|
||||||
// If the window resizes, also resize the canvas
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.ctx = canvas.getContext("2d");
|
|
||||||
this.div.appendChild(canvas);
|
|
||||||
canvas.style.position = 'absolute';
|
|
||||||
canvas.style.top = 0;
|
|
||||||
canvas.style.left = 0;
|
|
||||||
this.fullscreen();
|
|
||||||
window.addEventListener('resize', () => this.fullscreen());
|
|
||||||
|
|
||||||
// Monitor mouse movements
|
|
||||||
const el = window;
|
|
||||||
el.addEventListener('mousemove', e => {
|
|
||||||
if (this.DISPLAY_CURSOR_INFO) {
|
|
||||||
this.info['Mouse move'] = [`${e.clientX}, `, `${e.clientY}`];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
this.handlePointerMove(e.clientX, e.clientY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Monitor touch events
|
|
||||||
el.addEventListener('touchmove', e => {
|
|
||||||
if (this.DISPLAY_CURSOR_INFO) {
|
|
||||||
this.info['Touch move'] = [`${e.touches[0].pageX}, `, `${e.touches[0].pageY}`];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
this.handlePointerMove(e.touches[0].pageX, e.touches[0].pageY);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('pointerdown', e => {
|
|
||||||
if (this.DISPLAY_CURSOR_INFO) {
|
|
||||||
this.info['Pointer down'] = [`${e.clientX}, `, `${e.clientY}`];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
this.handlePointerDown(e.clientX, e.clientY);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('pointerup', e => {
|
|
||||||
if (this.DISPLAY_CURSOR_INFO) {
|
|
||||||
this.info['Pointer up'] = [`${e.clientX}, `, `${e.clientY}`];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
this.handlePointerUp(e.clientX, e.clientY);
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener('click', e => {
|
|
||||||
if (this.DISPLAY_CURSOR_INFO) {
|
|
||||||
this.info['Click'] = [`${e.clientX}, `, `${e.clientY}`];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initiate main loop
|
// Initiate main loop
|
||||||
this.time = document.timeline.currentTime;
|
this.time = document.timeline.currentTime;
|
||||||
requestAnimationFrame(t => this.loop(t));
|
requestAnimationFrame(t => this.loop(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPointerHistory() {
|
|
||||||
this.pointerHistory = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePointer(x, y) {
|
|
||||||
const t = document.timeline.currentTime;
|
|
||||||
this.pointerHistory.push({x, y, t});
|
|
||||||
if (this.pointerHistory.length > this.POINTER_HISTORY_SIZE) {
|
|
||||||
this.pointerHistory.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPointerVelocity() {
|
|
||||||
// Average over pointer history
|
|
||||||
if (this.pointerHistory.length < 2) {
|
|
||||||
return {x: 0, y: 0};
|
|
||||||
}
|
|
||||||
const start = this.pointerHistory[0];
|
|
||||||
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
|
||||||
const dt = (end.t - start.t) / 1000;
|
|
||||||
this.renderInfo();
|
|
||||||
return {
|
|
||||||
x: (end.x - start.x) / dt,
|
|
||||||
y: (end.y - start.y) / dt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePointerDown(x, y) {
|
|
||||||
this.clearPointerHistory();
|
|
||||||
this.updatePointer(x, y);
|
|
||||||
|
|
||||||
// If pointer is touching an object, select the object
|
|
||||||
let touchingObject = undefined;
|
|
||||||
for (let i = 0; i < this.objects.length; i++) {
|
|
||||||
const obj = this.objects[i];
|
|
||||||
// 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) {
|
|
||||||
touchingObject = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (touchingObject !== undefined) {
|
|
||||||
this.selectedObject = touchingObject;
|
|
||||||
} else {
|
|
||||||
// Otherwise, create a new object
|
|
||||||
this.createObject(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePointerUp(x, y) {
|
|
||||||
this.creatingObject = undefined;
|
|
||||||
this.selectedObject = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle cursor (mouse or touch) movement
|
|
||||||
handlePointerMove(x, y) {
|
|
||||||
// TODO: If e.touches.length > 1, user may be engaging pinch to zoom
|
|
||||||
|
|
||||||
// If the cursor moves while creating an object, or if an object is selected,
|
|
||||||
// update the position and velocity of the object
|
|
||||||
let selectedObject = this.creatingObject ?? this.selectedObject;
|
|
||||||
if (selectedObject !== undefined) {
|
|
||||||
const obj = this.objects[selectedObject];
|
|
||||||
this.updatePointer(x, y);
|
|
||||||
const {x: vx, y: vy} = this.getPointerVelocity();
|
|
||||||
obj.position.x = x;
|
|
||||||
obj.position.y = y;
|
|
||||||
obj.velocity.x = vx;
|
|
||||||
obj.velocity.y = vy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an object with mass that grows as pointer is held down
|
|
||||||
createObject(x, y) {
|
|
||||||
const obj = new MassObject(x, y);
|
|
||||||
const idx = this.objects.length;
|
|
||||||
this.creatingObject = idx;
|
|
||||||
this.objects.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop(currentTime) {
|
loop(currentTime) {
|
||||||
const elapsedTime = currentTime - this.time;
|
const elapsedTime = currentTime - this.time;
|
||||||
this.time = currentTime;
|
this.time = currentTime;
|
||||||
|
|
||||||
// If we're creating an object, increment its mass
|
this.objects.computeFrame(elapsedTime);
|
||||||
// with the mass creation rate accelerating over time
|
|
||||||
if (this.creatingObject !== undefined) {
|
|
||||||
const obj = this.objects[this.creatingObject];
|
|
||||||
const rate = this.MASS_CREATION_RATE * obj.age;
|
|
||||||
obj.mass += rate * elapsedTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display objects info
|
this.display.fillCanvas();
|
||||||
if (this.DISPLAY_OBJECTS_INFO) {
|
this.display.drawObjects();
|
||||||
for (let i = 0; i < this.objects.length; i++) {
|
this.overlay.renderInfo();
|
||||||
const obj = this.objects[i];
|
|
||||||
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;
|
|
||||||
this.info[`Object ${i}`] = [
|
|
||||||
`${obj.position.x}, `,
|
|
||||||
`${obj.position.y}, `,
|
|
||||||
`${obj.mass.toPrecision(6)} kg, `,
|
|
||||||
`${speed.toPrecision(2)} m/s, ${direction.toPrecision(2)}°`,
|
|
||||||
];
|
|
||||||
this.renderInfo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fillCanvas();
|
|
||||||
this.renderObjects();
|
|
||||||
|
|
||||||
requestAnimationFrame(t => this.loop(t));
|
requestAnimationFrame(t => this.loop(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
renderObject(idx) {
|
|
||||||
const obj = this.objects[idx];
|
|
||||||
const ctx = this.ctx;
|
|
||||||
const {r, g, b} = obj.color;
|
|
||||||
const {x, y} = obj.position;
|
|
||||||
const {x: vx, y: vy} = obj.velocity;
|
|
||||||
const radius = obj.radius;
|
|
||||||
|
|
||||||
// Draw filled circle for the object
|
|
||||||
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(x, y, radius, 0, 2*Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Draw line for the velocity
|
|
||||||
// TODO: Arrow
|
|
||||||
ctx.strokeStyle = ctx.fillStyle;
|
|
||||||
ctx.lineWidth = 2.0;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
ctx.lineTo(x + this.VELOCITY_VECTOR_SCALE * vx, y + this.VELOCITY_VECTOR_SCALE * vy);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// TODO: Draw line for acceleration
|
|
||||||
}
|
|
||||||
|
|
||||||
renderObjects() {
|
|
||||||
for (let i = 0; i < this.objects.length; i++) {
|
|
||||||
this.renderObject(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user