Compare commits
No commits in common. "c63f44ab8f323dc456068471432a89f7b6d5a5d5" and "25796a5ec3b2b076e36e2c8513a35cc7ad2fc5c2" have entirely different histories.
c63f44ab8f
...
25796a5ec3
22
Readme.md
22
Readme.md
@ -1,22 +0,0 @@
|
|||||||
Gravity Simulator
|
|
||||||
=================
|
|
||||||
|
|
||||||
Runs in a browser. Just serve `index.html` and associated `css` and `js`.
|
|
||||||
|
|
||||||
Uses `npm` for `eslint`.
|
|
||||||
|
|
||||||
TODO
|
|
||||||
----
|
|
||||||
|
|
||||||
- [x] Numeric Option Type
|
|
||||||
- [x] Time Indicator
|
|
||||||
- [ ] Selection Box
|
|
||||||
- [ ] Object List
|
|
||||||
- [ ] Object Detail
|
|
||||||
- [ ] Left Button Panning
|
|
||||||
- [ ] Middle Button Pause
|
|
||||||
- [ ] Grid
|
|
||||||
- [ ] Zoom Easing
|
|
||||||
- [ ] 2-touch Pan & Zoom
|
|
||||||
- [ ] Multi-touch Mass Create
|
|
||||||
- [ ] Tool to Clear Traces
|
|
||||||
22
commit
22
commit
@ -8,16 +8,22 @@ fi
|
|||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
git checkout dev
|
git checkout dev
|
||||||
|
|
||||||
./sync
|
./sync
|
||||||
|
|
||||||
if git diff; then
|
if git diff; then
|
||||||
git add .
|
git add .
|
||||||
|
git commit "$@"
|
||||||
|
git push
|
||||||
fi
|
fi
|
||||||
|
ssh lentilz "
|
||||||
git commit "$@"
|
set -oe pipefail
|
||||||
|
export NVM_DIR=\"\$HOME/.nvm\"
|
||||||
git push
|
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" # This loads nvm
|
||||||
|
cd ~/code/gravity-dev
|
||||||
|
git add .
|
||||||
|
git stash
|
||||||
|
git pull
|
||||||
|
git stash apply
|
||||||
|
npm i
|
||||||
|
"
|
||||||
echo >&2
|
echo >&2
|
||||||
echo >&2 "Committed and pushed dev branch"
|
echo >&2 "Committed and deployed to https://laddhoffman.com/gravity-dev/"
|
||||||
|
|||||||
11
config.js
11
config.js
@ -28,12 +28,10 @@ export const PATH_TRACES_DASHED = true;
|
|||||||
export const PATH_TRACES_DASHED_OPACITY = 1.0;
|
export const PATH_TRACES_DASHED_OPACITY = 1.0;
|
||||||
|
|
||||||
// SCALING FACTORS
|
// SCALING FACTORS
|
||||||
export const MASS_CREATION_RATE = 10;
|
export const MASS_CREATION_RATE = 1E1;
|
||||||
export const POINTER_HISTORY_SIZE = 20;
|
export const POINTER_HISTORY_SIZE = 15;
|
||||||
export const MOTION_TIME_SCALE = 0.3;
|
export const MOTION_TIME_SCALE = 1E-4;
|
||||||
export const GRAVITATIONAL_CONSTANT = 2E4;
|
export const PAN_ACCELERATION = 1E1;
|
||||||
|
|
||||||
// SIZES
|
|
||||||
export const ARROWHEAD_LENGTH = 7;
|
export const ARROWHEAD_LENGTH = 7;
|
||||||
export const ARROWHEAD_WIDTH = 5;
|
export const ARROWHEAD_WIDTH = 5;
|
||||||
export const OFFSCREEN_OBJECT_LINE_SCALE = 7;
|
export const OFFSCREEN_OBJECT_LINE_SCALE = 7;
|
||||||
@ -44,6 +42,7 @@ export const ZOOM_IN_FACTOR = 1;
|
|||||||
export const ZOOM_OUT_FACTOR = -1;
|
export const ZOOM_OUT_FACTOR = -1;
|
||||||
export const SCALE_POWER_MAX = 8;
|
export const SCALE_POWER_MAX = 8;
|
||||||
export const SCALE_POWER_MIN = -8;
|
export const SCALE_POWER_MIN = -8;
|
||||||
|
export const GRAVITATIONAL_CONSTANT = 1E5;
|
||||||
|
|
||||||
// CSS CLASS NAMES
|
// CSS CLASS NAMES
|
||||||
export const TOOL_CLASSNAME = 'lhg-tool';
|
export const TOOL_CLASSNAME = 'lhg-tool';
|
||||||
|
|||||||
@ -6,10 +6,7 @@
|
|||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { Sim } from './simulator.js';
|
import { Sim } from './simulator.js';
|
||||||
// Wait for document load
|
const sim = new Sim('simulator');
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
new Sim('simulator');
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
64
objects.js
64
objects.js
@ -1,6 +1,8 @@
|
|||||||
import { MassObject } from './object.js';
|
import { MassObject } from './object.js';
|
||||||
import {
|
import {
|
||||||
|
MASS_CREATION_RATE,
|
||||||
DISPLAY_OBJECTS_INFO,
|
DISPLAY_OBJECTS_INFO,
|
||||||
|
GRAVITATIONAL_CONSTANT,
|
||||||
ZOOM_TO_FIT_PADDING,
|
ZOOM_TO_FIT_PADDING,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
@ -141,18 +143,18 @@ export class Objects {
|
|||||||
handlePointerMove({x, y, vx, vy}) {
|
handlePointerMove({x, y, vx, vy}) {
|
||||||
// If the cursor moves while creating an object, or while an object is selected,
|
// If the cursor moves while creating an object, or while an object is selected,
|
||||||
// update the position and velocity of the object
|
// update the position and velocity of the object
|
||||||
// update the position using the pointer motion but the velocity using the pointer velocity
|
|
||||||
const obj = this.getSelectedOrCreating();
|
const obj = this.getSelectedOrCreating();
|
||||||
if (obj === undefined) return;
|
|
||||||
if (this.sim.pointer.panning?.velocity) {
|
if (this.sim.pointer.panning?.velocity) {
|
||||||
vx += this.sim.pointer.panning.velocity.x;
|
vx += this.sim.pointer.panning.velocity.x;
|
||||||
vy += this.sim.pointer.panning.velocity.y;
|
vy += this.sim.pointer.panning.velocity.y;
|
||||||
}
|
}
|
||||||
|
if (obj !== undefined) {
|
||||||
obj.position.x = x;
|
obj.position.x = x;
|
||||||
obj.position.y = y;
|
obj.position.y = y;
|
||||||
obj.velocity.x = vx;
|
obj.velocity.x = vx;
|
||||||
obj.velocity.y = vy;
|
obj.velocity.y = vy;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// cb: (obj, idx) => {}
|
// cb: (obj, idx) => {}
|
||||||
// TODO: Reducer
|
// TODO: Reducer
|
||||||
@ -167,7 +169,6 @@ export class Objects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
computeForces() {
|
computeForces() {
|
||||||
const gravity = this.sim.getOption('param.gravity');
|
|
||||||
if (this.objects.length < 2) return;
|
if (this.objects.length < 2) return;
|
||||||
this.forEachObject(obj => {
|
this.forEachObject(obj => {
|
||||||
obj.forces = [];
|
obj.forces = [];
|
||||||
@ -178,7 +179,7 @@ export class Objects {
|
|||||||
const dy = (B.position.y - A.position.y);
|
const dy = (B.position.y - A.position.y);
|
||||||
const dSquared = dx ** 2 + dy ** 2;
|
const dSquared = dx ** 2 + dy ** 2;
|
||||||
const d = Math.sqrt(dSquared);
|
const d = Math.sqrt(dSquared);
|
||||||
const F = gravity * A.mass * B.mass / dSquared;
|
const F = GRAVITATIONAL_CONSTANT * A.mass * B.mass / dSquared;
|
||||||
const Fx = F * dx / d;
|
const Fx = F * dx / d;
|
||||||
const Fy = F * dy / d;
|
const Fy = F * dy / d;
|
||||||
A.forces.push({ x: Fx, y: Fy });
|
A.forces.push({ x: Fx, y: Fy });
|
||||||
@ -194,11 +195,9 @@ export class Objects {
|
|||||||
computeFrame(elapsedTime) {
|
computeFrame(elapsedTime) {
|
||||||
// If we're creating an object, increment its mass
|
// If we're creating an object, increment its mass
|
||||||
// with the mass creation rate accelerating over time
|
// with the mass creation rate accelerating over time
|
||||||
const massCreationRate = this.sim.getOption('param.massCreationRate');
|
|
||||||
if (this.creatingObject !== undefined) {
|
if (this.creatingObject !== undefined) {
|
||||||
const obj = this.objects[this.creatingObject];
|
const obj = this.objects[this.creatingObject];
|
||||||
const rate = massCreationRate * obj.age;
|
const rate = MASS_CREATION_RATE * obj.age;
|
||||||
// TODO: After objects merge during creation, mass creation rate can accelerate
|
|
||||||
obj.mass += rate * elapsedTime;
|
obj.mass += rate * elapsedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,42 +224,29 @@ export class Objects {
|
|||||||
const dSquared = dx ** 2 + dy ** 2;
|
const dSquared = dx ** 2 + dy ** 2;
|
||||||
const d = Math.sqrt(dSquared);
|
const d = Math.sqrt(dSquared);
|
||||||
if (d < A.radius + B.radius) {
|
if (d < A.radius + B.radius) {
|
||||||
let S, T;
|
// Merge B into A:
|
||||||
// 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 position = center of mass
|
||||||
// Set velocity = total momentum / total mass
|
// Set velocity = total momentum / total mass
|
||||||
// Combine forces
|
// Combine forces
|
||||||
// Sdd masses
|
// Add masses
|
||||||
// Sverage color
|
// Average color
|
||||||
S.position.x = (S.position.x * S.mass + T.position.x * T.mass) / (S.mass + T.mass);
|
// If B was selected or being created, select A instead
|
||||||
S.position.y = (S.position.y * S.mass + T.position.y * T.mass) / (S.mass + T.mass);
|
// Remove B using splice
|
||||||
S.velocity.x = (S.velocity.x * S.mass + T.velocity.x * T.mass) / (S.mass + T.mass);
|
A.position.x = (A.position.x * A.mass + B.position.x * B.mass) / (A.mass + B.mass);
|
||||||
S.velocity.y = (S.velocity.y * S.mass + T.velocity.y * T.mass) / (S.mass + T.mass);
|
A.position.y = (A.position.y * A.mass + B.position.y * B.mass) / (A.mass + B.mass);
|
||||||
S.forces.push(...T.forces);
|
A.velocity.x = (A.velocity.x * A.mass + B.velocity.x * B.mass) / (A.mass + B.mass);
|
||||||
S.mass += T.mass;
|
A.velocity.y = (A.velocity.y * A.mass + B.velocity.y * B.mass) / (A.mass + B.mass);
|
||||||
S.color = {
|
A.forces.push(...B.forces);
|
||||||
r: (S.mass * S.color.r + T.mass * T.color.r) / (S.mass + T.mass),
|
A.mass += B.mass;
|
||||||
g: (S.mass * S.color.g + T.mass * T.color.g) / (S.mass + T.mass),
|
A.color = {
|
||||||
b: (S.mass * S.color.b + T.mass * T.color.b) / (S.mass + T.mass),
|
r: (A.mass * A.color.r + B.mass * B.color.r) / (A.mass + B.mass),
|
||||||
|
g: (A.mass * A.color.g + B.mass * B.color.g) / (A.mass + B.mass),
|
||||||
|
b: (A.mass * A.color.b + B.mass * B.color.b) / (A.mass + B.mass),
|
||||||
};
|
};
|
||||||
T.alive = false;
|
if (this.creatingObject === j) this.creatingObject = i;
|
||||||
T.forces = [];
|
if (this.selectedObject === j) this.selectedObject = i;
|
||||||
|
B.alive = false;
|
||||||
|
B.forces = [];
|
||||||
}
|
}
|
||||||
}, true, i + 1);
|
}, true, i + 1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -59,12 +59,12 @@ export class Pointer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getPointerVelocity(points = POINTER_HISTORY_SIZE) {
|
getPointerVelocity(points) {
|
||||||
// Average over pointer history
|
// Average over pointer history
|
||||||
if (this.pointerHistory.length < 2) {
|
if (this.pointerHistory.length < 2) {
|
||||||
return {x: 0, y: 0, dt: 1};
|
return {x: 0, y: 0, dt: 1};
|
||||||
}
|
}
|
||||||
points = Math.min(points, POINTER_HISTORY_SIZE, this.pointerHistory.length);
|
points = points || this.pointerHistory.length;
|
||||||
const start = this.pointerHistory[this.pointerHistory.length - points];
|
const start = this.pointerHistory[this.pointerHistory.length - points];
|
||||||
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
const end = this.pointerHistory[this.pointerHistory.length - 1];
|
||||||
const dt = (end.t - start.t) / 1000;
|
const dt = (end.t - start.t) / 1000;
|
||||||
@ -122,7 +122,7 @@ export class Pointer {
|
|||||||
if (!dt) {
|
if (!dt) {
|
||||||
this.panning = undefined;
|
this.panning = undefined;
|
||||||
} else {
|
} else {
|
||||||
const v = this.getPointerVelocity();
|
const v = this.getPointerVelocity(10);
|
||||||
// Convert pointer velocity to simulation scale
|
// Convert pointer velocity to simulation scale
|
||||||
v.x /= this.sim.display.scale;
|
v.x /= this.sim.display.scale;
|
||||||
v.y /= this.sim.display.scale;
|
v.y /= this.sim.display.scale;
|
||||||
|
|||||||
37
simulator.js
37
simulator.js
@ -1,23 +1,23 @@
|
|||||||
|
import { Display } from './display.js';
|
||||||
|
import { Overlay } from './overlay.js';
|
||||||
|
import { Pointer } from './pointer.js';
|
||||||
|
import { Objects } from './objects.js';
|
||||||
|
import { Toolbar } from './toolbar.js';
|
||||||
|
import { PlayPause } from './tool/play-pause.js';
|
||||||
|
import { Zoom } from './tool/zoom.js';
|
||||||
|
import { ModeSwitch } from './tool/modes.js';
|
||||||
|
import { Options } from './tool/options.js';
|
||||||
import {
|
import {
|
||||||
DISPLAY_CURRENT_MODE,
|
|
||||||
DISPLAY_CURRENT_SCALE,
|
|
||||||
EVENT_ZOOM,
|
|
||||||
SCALE_POWER_MAX,
|
SCALE_POWER_MAX,
|
||||||
SCALE_POWER_MIN,
|
SCALE_POWER_MIN,
|
||||||
|
DISPLAY_CURRENT_SCALE,
|
||||||
|
DISPLAY_CURRENT_MODE,
|
||||||
|
MOTION_TIME_SCALE,
|
||||||
|
EVENT_ZOOM,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import {Display} from './display.js';
|
|
||||||
import {Objects} from './objects.js';
|
|
||||||
import {Overlay} from './overlay.js';
|
|
||||||
import {Pointer} from './pointer.js';
|
|
||||||
import {ModeSwitch} from './tool/modes.js';
|
|
||||||
import {Options} from './tool/options.js';
|
|
||||||
import {PlayPause} from './tool/play-pause.js';
|
|
||||||
import {Zoom} from './tool/zoom.js';
|
|
||||||
import {Toolbar} from './toolbar.js';
|
|
||||||
|
|
||||||
export class Sim {
|
export class Sim {
|
||||||
info = {};
|
info = {};
|
||||||
rawTime = undefined;
|
|
||||||
time = undefined;
|
time = undefined;
|
||||||
nextZoom = undefined;
|
nextZoom = undefined;
|
||||||
playing = true;
|
playing = true;
|
||||||
@ -59,8 +59,7 @@ export class Sim {
|
|||||||
this.pointer = new Pointer(this);
|
this.pointer = new Pointer(this);
|
||||||
|
|
||||||
// Initiate main loop
|
// Initiate main loop
|
||||||
this.rawTime = document.timeline.currentTime / 1000;
|
this.time = document.timeline.currentTime;
|
||||||
this.time = 0;
|
|
||||||
requestAnimationFrame(t => this.loop(t));
|
requestAnimationFrame(t => this.loop(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,11 +126,8 @@ export class Sim {
|
|||||||
|
|
||||||
// Main loop
|
// Main loop
|
||||||
loop(currentTime) {
|
loop(currentTime) {
|
||||||
currentTime /= 1000;
|
const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE;
|
||||||
const timeScale = this.getOption('param.timeScale');
|
this.time = currentTime;
|
||||||
const elapsedTime = (currentTime - this.rawTime) * timeScale;
|
|
||||||
this.rawTime = currentTime;
|
|
||||||
this.time += elapsedTime;
|
|
||||||
|
|
||||||
if (DISPLAY_CURRENT_MODE) {
|
if (DISPLAY_CURRENT_MODE) {
|
||||||
this.info['Mode'] = this.getCurrentMode();
|
this.info['Mode'] = this.getCurrentMode();
|
||||||
@ -151,7 +147,6 @@ export class Sim {
|
|||||||
this.overlay.renderInfo();
|
this.overlay.renderInfo();
|
||||||
this.display.fillCanvas();
|
this.display.fillCanvas();
|
||||||
this.display.drawObjects();
|
this.display.drawObjects();
|
||||||
this.toolbar.frame();
|
|
||||||
|
|
||||||
requestAnimationFrame(t => this.loop(t));
|
requestAnimationFrame(t => this.loop(t));
|
||||||
}
|
}
|
||||||
|
|||||||
35
style.css
35
style.css
@ -17,7 +17,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 14pt;
|
font-size: 16pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[id=simulator] {
|
div[id=simulator] {
|
||||||
@ -34,7 +34,7 @@ div.lhg-toolbar {
|
|||||||
border-radius: 0.5EM;
|
border-radius: 0.5EM;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-tool {
|
div.lhg-toolbar div.lhg-tool {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -45,15 +45,9 @@ div.lhg-tool {
|
|||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: #282;
|
border-color: #282;
|
||||||
text-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-tool div.lhg-wide {
|
div.lhg-tool button {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.lhg-tool button, div.lhg-tool input {
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
width: 6em;
|
width: 6em;
|
||||||
@ -61,7 +55,7 @@ div.lhg-tool button, div.lhg-tool input {
|
|||||||
color: #5f5;
|
color: #5f5;
|
||||||
border-radius: 0.5EM;
|
border-radius: 0.5EM;
|
||||||
border-color: #000;
|
border-color: #000;
|
||||||
border-width: 2px;
|
border-width: 4px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
padding-top: 0.5EM;
|
padding-top: 0.5EM;
|
||||||
padding-bottom: 0.5EM;
|
padding-bottom: 0.5EM;
|
||||||
@ -70,11 +64,6 @@ div.lhg-tool button, div.lhg-tool input {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.lhg-tool input {
|
|
||||||
width: 6EM;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-tool button:hover {
|
div.lhg-tool button:hover {
|
||||||
@ -88,22 +77,20 @@ div.lhg-tool button:active {
|
|||||||
div.lhg-toolbar-header:hover button {
|
div.lhg-toolbar-header:hover button {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-toolbar-header:active button {
|
div.lhg-toolbar-header:active button {
|
||||||
background-color: #252;
|
background-color: #252;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-toolbar-header > * {
|
div.lhg-tool button.lhg-tool-info {
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.lhg-tool .lhg-tool-info {
|
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
border-color: #000;
|
border-color: #282;
|
||||||
border-width: 2px;
|
border-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.lhg-tool .lhg-wide {
|
div.lhg-tool button.lhg-wide {
|
||||||
width: 12em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.lhg-tool > div {
|
||||||
|
/* border: 2px red solid; */
|
||||||
|
}
|
||||||
|
|||||||
4
sync
4
sync
@ -8,7 +8,7 @@ fi
|
|||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
do_rsync() {
|
do_rsync() {
|
||||||
rsync -vru \
|
rsync -ru \
|
||||||
--exclude='.git' \
|
--exclude='.git' \
|
||||||
--exclude='node_modules' \
|
--exclude='node_modules' \
|
||||||
--exclude='.*.sw*' \
|
--exclude='.*.sw*' \
|
||||||
@ -18,7 +18,5 @@ do_rsync() {
|
|||||||
do_rsync ~/code/gravity-dev/ lentilz:code/gravity-dev/
|
do_rsync ~/code/gravity-dev/ lentilz:code/gravity-dev/
|
||||||
do_rsync lentilz:code/gravity-dev/ ~/code/gravity-dev/
|
do_rsync lentilz:code/gravity-dev/ ~/code/gravity-dev/
|
||||||
|
|
||||||
git status
|
|
||||||
|
|
||||||
echo >&2
|
echo >&2
|
||||||
echo >&2 "Synced with https://laddhoffman.com/gravity-dev/"
|
echo >&2 "Synced with https://laddhoffman.com/gravity-dev/"
|
||||||
|
|||||||
@ -6,20 +6,22 @@ export class Header extends Tool {
|
|||||||
|
|
||||||
constructor(toolbar, title = 'Tools') {
|
constructor(toolbar, title = 'Tools') {
|
||||||
super(toolbar);
|
super(toolbar);
|
||||||
|
|
||||||
this.title = document.createElement('h1');
|
this.title = document.createElement('h1');
|
||||||
this.title.innerHTML = title;
|
this.title.innerHTML = title;
|
||||||
|
|
||||||
this.toggleButton = document.createElement('button');
|
this.toggleButton = document.createElement('button');
|
||||||
this.div.addEventListener('click', () => this.toggle());
|
|
||||||
this.updateButton();
|
this.updateButton();
|
||||||
|
this.div.addEventListener('click', () => this.toggle());
|
||||||
|
|
||||||
this.div.appendChild(this.title);
|
this.div.appendChild(this.title);
|
||||||
this.div.appendChild(this.toggleButton);
|
this.div.appendChild(this.toggleButton);
|
||||||
|
|
||||||
|
this.title.style.verticalAlign = 'center';
|
||||||
|
this.toggleButton.style.width = '3EM';
|
||||||
this.div.style.display = 'flex';
|
this.div.style.display = 'flex';
|
||||||
this.div.style.justifyContent = 'space-around';
|
this.div.style.justifyContent = 'space-around';
|
||||||
this.title.style.width = '9EM';
|
|
||||||
this.toggleButton.style.width = '3EM';
|
|
||||||
this.div.classList.add(TOOLBAR_HEADER_CLASSNAME);
|
this.div.classList.add(TOOLBAR_HEADER_CLASSNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,10 @@
|
|||||||
import {
|
import {
|
||||||
DISPLAY_ACCELERATION_VECTORS,
|
DISPLAY_ACCELERATION_VECTORS,
|
||||||
DISPLAY_VELOCITY_VECTORS,
|
DISPLAY_VELOCITY_VECTORS,
|
||||||
GRAVITATIONAL_CONSTANT,
|
|
||||||
MASS_CREATION_RATE,
|
|
||||||
MERGE_ON_COLLIDE,
|
MERGE_ON_COLLIDE,
|
||||||
MOTION_TIME_SCALE,
|
|
||||||
PATH_TRACES_DASHED,
|
PATH_TRACES_DASHED,
|
||||||
PAUSE_DURING_CREATION,
|
PAUSE_DURING_CREATION,
|
||||||
PAUSE_DURING_SELECTION,
|
PAUSE_DURING_SELECTION,
|
||||||
TOOL_INFO_CLASSNAME,
|
|
||||||
WIDE_CLASSNAME,
|
WIDE_CLASSNAME,
|
||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import {Tool} from '../tool.js';
|
import {Tool} from '../tool.js';
|
||||||
@ -34,13 +30,6 @@ export class Options extends Tool {
|
|||||||
items: [
|
items: [
|
||||||
{type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true},
|
{type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true},
|
||||||
]
|
]
|
||||||
}, {
|
|
||||||
type: 'group', name: 'param', title: 'Parameters',
|
|
||||||
items: [
|
|
||||||
{type: 'number', name: 'gravity', title: 'Gravity', default: GRAVITATIONAL_CONSTANT},
|
|
||||||
{type: 'number', name: 'timeScale', title: 'Time Scale', default: MOTION_TIME_SCALE},
|
|
||||||
{type: 'number', name: 'massCreationRate', title: 'Mass Creation Rate', default: MASS_CREATION_RATE},
|
|
||||||
]
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
values = {};
|
values = {};
|
||||||
@ -73,32 +62,6 @@ export class Options extends Tool {
|
|||||||
});
|
});
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
case 'number': {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
const title = document.createElement('button');
|
|
||||||
const input = document.createElement('input');
|
|
||||||
const maxLength = item.maxLength || 8;
|
|
||||||
div.appendChild(title);
|
|
||||||
div.appendChild(input);
|
|
||||||
div.classList.add(WIDE_CLASSNAME);
|
|
||||||
title.classList.add(TOOL_INFO_CLASSNAME);
|
|
||||||
if (item.wide) {
|
|
||||||
title.classList.add(WIDE_CLASSNAME);
|
|
||||||
input.classList.add(WIDE_CLASSNAME);
|
|
||||||
}
|
|
||||||
title.innerHTML = item.title;
|
|
||||||
input.value = item.default;
|
|
||||||
this.setOption(path, item.default);
|
|
||||||
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
input.value = input.value.slice(0, maxLength);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.addEventListener('change', () => {
|
|
||||||
this.setOption(path, input.value);
|
|
||||||
});
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new Error('unknown option type');
|
throw new Error('unknown option type');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +1,20 @@
|
|||||||
import {TOOL_INFO_CLASSNAME, WIDE_CLASSNAME} from '../config.js';
|
import { Tool } from '../tool.js';
|
||||||
import {Tool} from '../tool.js';
|
|
||||||
|
|
||||||
export class PlayPause extends Tool {
|
export class PlayPause extends Tool {
|
||||||
playHTML = 'Play';
|
playHTML = 'Play';
|
||||||
pauseHTML = 'Pause';
|
pauseHTML = 'Pause';
|
||||||
currentTimeEl = undefined;
|
|
||||||
|
|
||||||
get timeText() {
|
|
||||||
let time = this.sim.time;
|
|
||||||
// Time in seconds
|
|
||||||
const s = time % 60;
|
|
||||||
time = (time - s) / 60;
|
|
||||||
const m = time % 60;
|
|
||||||
time = (time - m) / 60;
|
|
||||||
const h = time % 24;
|
|
||||||
time = (time - h) / 24;
|
|
||||||
const d = time;
|
|
||||||
time -= m * 60;
|
|
||||||
|
|
||||||
const ms = (s - Math.floor(s)) * 1000;
|
|
||||||
return [
|
|
||||||
d || undefined,
|
|
||||||
h.toString().padStart(2, '0'),
|
|
||||||
m.toString().padStart(2, '0'),
|
|
||||||
[
|
|
||||||
s.toFixed(0).padStart(2, '0'),
|
|
||||||
ms.toFixed(0).padStart(3, '0'),
|
|
||||||
].join('.')
|
|
||||||
].filter(x => x !== undefined).join(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
frame() {
|
|
||||||
if (this.currentTimeEl) {
|
|
||||||
this.currentTimeEl.innerHTML = this.timeText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(toolbar) {
|
constructor(toolbar) {
|
||||||
super(toolbar);
|
super(toolbar);
|
||||||
|
|
||||||
const currentTime = document.createElement('button');
|
|
||||||
const pauseButton = document.createElement('button');
|
const pauseButton = document.createElement('button');
|
||||||
const playButton = document.createElement('button');
|
const playButton = document.createElement('button');
|
||||||
this.currentTimeEl = currentTime;
|
|
||||||
|
|
||||||
this.div.appendChild(currentTime);
|
|
||||||
this.div.appendChild(pauseButton);
|
this.div.appendChild(pauseButton);
|
||||||
this.div.appendChild(playButton);
|
this.div.appendChild(playButton);
|
||||||
|
|
||||||
currentTime.classList.add(TOOL_INFO_CLASSNAME);
|
|
||||||
currentTime.classList.add(WIDE_CLASSNAME);
|
|
||||||
|
|
||||||
pauseButton.innerHTML = this.pauseHTML;
|
pauseButton.innerHTML = this.pauseHTML;
|
||||||
playButton.innerHTML = this.playHTML;
|
playButton.innerHTML = this.playHTML;
|
||||||
currentTime.innerHTML = this.timeText;
|
|
||||||
|
|
||||||
pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
|
pauseButton.style.opacity = this.sim.playing ? '100%' : '50%';
|
||||||
playButton.style.opacity = this.sim.playing ? '50%' : '100%';
|
playButton.style.opacity = this.sim.playing ? '50%' : '100%';
|
||||||
|
|||||||
46
tool/zoom.js
46
tool/zoom.js
@ -7,14 +7,6 @@ import {
|
|||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
|
|
||||||
export class Zoom extends Tool {
|
export class Zoom extends Tool {
|
||||||
get displayScale() {
|
|
||||||
return this.sim.getScaleDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
get displayScaleText() {
|
|
||||||
return `Scale: ${this.displayScale}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(toolbar) {
|
constructor(toolbar) {
|
||||||
super(toolbar);
|
super(toolbar);
|
||||||
|
|
||||||
@ -22,27 +14,23 @@ export class Zoom extends Tool {
|
|||||||
const zoomOut = document.createElement('button');
|
const zoomOut = document.createElement('button');
|
||||||
const zoomIn = document.createElement('button');
|
const zoomIn = document.createElement('button');
|
||||||
const zoomAll = document.createElement('button');
|
const zoomAll = document.createElement('button');
|
||||||
const zeroVelocity = document.createElement('button');
|
|
||||||
|
|
||||||
this.div.appendChild(currentScale);
|
this.div.appendChild(currentScale);
|
||||||
this.div.appendChild(zoomOut);
|
this.div.appendChild(zoomOut);
|
||||||
this.div.appendChild(zoomIn);
|
this.div.appendChild(zoomIn);
|
||||||
this.div.appendChild(zoomAll);
|
this.div.appendChild(zoomAll);
|
||||||
this.div.appendChild(zeroVelocity);
|
|
||||||
|
|
||||||
zoomAll.classList.add(WIDE_CLASSNAME);
|
zoomAll.classList.add(WIDE_CLASSNAME);
|
||||||
zeroVelocity.classList.add(WIDE_CLASSNAME);
|
|
||||||
currentScale.classList.add(WIDE_CLASSNAME);
|
currentScale.classList.add(WIDE_CLASSNAME);
|
||||||
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
currentScale.classList.add(TOOL_INFO_CLASSNAME);
|
||||||
|
|
||||||
zoomOut.innerHTML = 'Zoom<br>Out';
|
zoomOut.innerHTML = 'Zoom<br>Out';
|
||||||
zoomIn.innerHTML = 'Zoom<br>In';
|
zoomIn.innerHTML = 'Zoom<br>In';
|
||||||
zoomAll.innerHTML = 'Zoom to Fit';
|
zoomAll.innerHTML = 'Zoom to Fit';
|
||||||
zeroVelocity.innerHTML = 'Zero Momentum';
|
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
|
||||||
|
|
||||||
this.sim.onZoom(() => {
|
this.sim.onZoom(() => {
|
||||||
currentScale.innerHTML = this.displayScaleText;
|
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
zoomOut.addEventListener('click', () => {
|
zoomOut.addEventListener('click', () => {
|
||||||
@ -88,35 +76,5 @@ export class Zoom extends Tool {
|
|||||||
};
|
};
|
||||||
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
this.sim.scheduleZoom({x, y}, factor, netVelocity)
|
||||||
});
|
});
|
||||||
|
|
||||||
zeroVelocity.addEventListener('click', () => {
|
|
||||||
// Determine average momentum
|
|
||||||
const netMomentum = {x: 0, y: 0};
|
|
||||||
let totalMass = 0;
|
|
||||||
let count = 0;
|
|
||||||
this.sim.objects.forEachObject(obj => {
|
|
||||||
count++;
|
|
||||||
netMomentum.x += obj.mass * obj.velocity.x;
|
|
||||||
netMomentum.y += obj.mass * obj.velocity.y;
|
|
||||||
totalMass += obj.mass;
|
|
||||||
});
|
|
||||||
if (!count) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const netVelocity = {
|
|
||||||
x: netMomentum.x / totalMass,
|
|
||||||
y: netMomentum.y / totalMass,
|
|
||||||
};
|
|
||||||
// Apply offset to all object velocities
|
|
||||||
this.sim.objects.forEachObject(obj => {
|
|
||||||
obj.velocity.x -= netVelocity.x;
|
|
||||||
obj.velocity.y -= netVelocity.y;
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Zero net angular momentum
|
|
||||||
|
|
||||||
// Cancel panning
|
|
||||||
this.sim.pointer.panning = undefined;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,8 @@ export class Toolbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frame() {
|
frame() {
|
||||||
for (let tool of this.tools) {
|
for (let tool in this.tools) {
|
||||||
|
// TODO: tool.frame()
|
||||||
tool.frame();
|
tool.frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user