这是一个二维物理模拟器,用来模拟重力和圆形物体之间的碰撞(尽管它不模拟旋转)。
您可以在仿真运行时调整以下参数:
尽管更多的是模拟而不是游戏,但除了修改参数之外,还有一些用户交互的方面:
在没有指定延迟的情况下可以提高性能的任何东西。
基于我在重构康威生命游戏的实现时看到的大量性能改进,我尝试尽可能地在变量中缓存和重用值,并将其重新计算或重新检索限制到将检索到的值作为参数传递给我知道需要它们的函数,即使它们可以自己检索或计算它们。
我将感激任何数学快捷键可以减少计算开销而不牺牲准确性。
符合最佳实践的
无论是可读性、可维护性、可测试性还是任何其他-bility,我都很欣赏任何改进的见解和建议。
一些不现实的行为似乎是重力模拟固有的。我想知道是否有任何常规或标准的方法来减轻这种行为。
具体而言:应用线性速度移动一个质量点意味着你跳过任何速度重新计算,这将导致有效重力的变化,所有的直线运动路径。当物体以高速运动时(特别是在强烈的引力影响下加速),这会导致不准确的现象。
类似但不同的不精确性涉及碰撞;我的碰撞代码检查重叠对象,但如果两个对象之间的相对速度足够高,则其中一个对象可能会被另一个对象的线性运动“跳过”。一种解决办法是限制物体的最大速度,但我很想知道其他更合适的解,比如(假设)一种有效计算物体轨迹和检查交叉点的方法。
作为一种模拟而不是游戏,代码的计算和渲染部分是交织在一起的;因为在每次计算之后发生了一些有趣的事情,我希望在每一步之后在屏幕上重新绘制这些事件。我还没有将呈现拆分到它自己的虚拟线程。
中看起来有点古怪
在撰写这篇文章时,Chrome似乎处理2D画布上下文的globalAlpha与Firefox、Edge和Internet不同。在Chrome中,重力场织物标记的褪色速度还不够快,所以在屏幕上漂浮的物体上似乎会有飞散的斑点。如果有人对如何解决这种行为提出建议,我愿意接受建议,但在这一点上我并不太担心。
const G = 6.674 * Math.pow(10, -11), // gravitational constant
MAX_RND_VEL = 5,
MAX_RND_MASS = 5 * Math.pow(10, 12),
MIN_RND_MASS = 50,
MAX_RND_SIZE = 8,
PI = Math.PI,
TWOPI = Math.PI * 2,
FADE_RATE = 4,
CANVAS = document.getElementById("canvas"),
CURSOR_LINE_COLOR = "cyan",
OBJECT_BORDER_COLOR = "white",
GRAVITY_FIELD_MARKER_COLOR = "rgb(150,200,150)",
GRAVITY_FIELD_MARKER_RADIUS = 0.5,
txtNumObjects = document.getElementById("NumObjects"),
chkShowFabric = document.getElementById("ShowFabric"),
ddlFabricGranularity = document.getElementById("FabricGranularity"),
ddlRestitution = document.getElementById("Restitution"),
ddlBorderBehavior = document.getElementById("BorderBehavior"),
chkHandleCollisions = document.getElementById("chkCollisions"),
btnNextStep = document.getElementById("NextStep"),
txtDelay = document.getElementById("Delay");
const WIDTH = CANVAS.width,
HEIGHT = CANVAS.height,
DOUBLE_SIZE = MAX_RND_SIZE * 2,
CTX = CANVAS.getContext("2d");
let numObjects = txtNumObjects.value,
cellSize = ddlFabricGranularity.value,
borderBehavior = ddlBorderBehavior.value,
showFabric = chkShowFabric.checked,
shouldHandleCollisions = chkHandleCollisions.checked,
Cr = ddlRestitution.value, // coefficient of restitution
delay = txtDelay.value,
stellarObjects = [],
running = false,
planting = false,
plantx, planty, mouseX, mouseY, // vars for adding new objects
gField = [],
gHeight, gWidth; // vars for spacetime fabric visualization
let fadeMultiplier = cellSize / FADE_RATE;
reset();
ddlRestitution.addEventListener("change", function() {
Cr = this.value;
});
chkHandleCollisions.addEventListener("change", function() {
shouldHandleCollisions = this.checked;
});
CANVAS.addEventListener("mousedown", startDragCursor);
CANVAS.addEventListener("mousemove", dragCursor);
CANVAS.addEventListener("mouseup", releaseCursor);
CANVAS.addEventListener("mouseout", function(event) {
planting = false;
});
ddlBorderBehavior.addEventListener("change", function() {
borderBehavior = this.value;
});
document.getElementById("Reset").addEventListener("click", function() {
reset();
});
document.getElementById("NextStep").addEventListener("click", function() {
step();
});
ddlFabricGranularity.addEventListener("change", function() {
cellSize = this.value;
fadeMultiplier = cellSize / FADE_RATE;
resetGravityField();
recalculateGravityField();
CTX.clearRect(0, 0, WIDTH, HEIGHT);
if (showFabric) {
drawGravityField();
}
drawObjects();
});
chkShowFabric.addEventListener("change", function() {
showFabric = this.checked;
});
document.getElementById("AutoStep").addEventListener("click", function() {
if (running) {
clearTimeout(running);
btnNextStep.disabled = false;
running = null;
this.value = "Auto";
} else {
btnNextStep.disabled = true;
this.value = "Stop";
running = setTimeout(loopStep, delay);
}
});
txtDelay.addEventListener("change", function() {
delay = +(this.value);
});
function startDragCursor(event) {
if (event.which === 3 || event.button === 2) {
event.preventDefault();
return false;
} else {
planting = true;
plantx = event.pageX - CANVAS.offsetLeft;
planty = event.pageY - CANVAS.offsetTop;
mouseX = plantx;
mouseY = planty;
}
event.preventDefault();
}
function dragCursor(event) {
if (planting) {
mouseX = event.pageX - CANVAS.offsetLeft;
mouseY = event.pageY - CANVAS.offsetTop;
if (!running) {
CTX.clearRect(0, 0, WIDTH, HEIGHT);
if (showFabric) {
drawGravityField();
}
drawObjects();
}
drawCursorLine();
}
event.preventDefault();
}
function releaseCursor(event) {
if (event.which === 3 || event.button === 2) {
removeObjectAt(event.pageX - CANVAS.offsetLeft, event.pageY - CANVAS.offsetTop);
event.preventDefault();
return false;
} else {
if (planting) {
let thing = getRandomStellarObject(),
upX = event.pageX - CANVAS.offsetLeft,
upY = event.pageY - CANVAS.offsetTop;
if (typeof plantx === "undefined") {
plantx = upX;
planty = upY;
}
thing.x = upX;
thing.y = upY;
thing.velocity.x = MAX_RND_VEL * MAX_RND_VEL * (plantx - upX) / WIDTH;
thing.velocity.y = MAX_RND_VEL * MAX_RND_VEL * (planty - upY) / HEIGHT;
stellarObjects.push(thing);
numObjects++;
planting = false;
CTX.clearRect(0, 0, WIDTH, HEIGHT);
applyObjectGravityToFabric(upX, upY, thing.mass * G);
if (showFabric) {
drawGravityField();
}
drawObjects();
}
}
event.preventDefault();
}
// Clears all objects and regenerates them based on the chosen number of objects
function reset() {
resetGravityField();
numObjects = document.getElementById("NumObjects").value;
stellarObjects = [];
generateObjects(numObjects);
step();
}
// Repeatedly advances time based on the chosen delay
function loopStep() {
let startTime = +new Date,
delta;
step();
if (delay === 0 || (delta = +new Date - startTime) >= delay) {
running = setTimeout(loopStep, 0);
} else {
running = setTimeout(loopStep, delay - delta);
}
}
// Advances time forward and recalculates all object positions/velocities as necessary, redrawing the canvas as necessary
function step() {
CTX.clearRect(0, 0, WIDTH, HEIGHT);
moveObjects();
if (showFabric) {
resetGravityFieldInPlace();
applyObjectsGravity();
drawGravityField();
} else {
applyObjectsGravity();
}
drawObjects();
}
// adds the specified number of objects to the universe
function generateObjects(num) {
for (let i = 0; i < num; i++) {
stellarObjects.push(getRandomStellarObject());
}
}
// Removes any stellar objects that overlap at the given xy coordinates
function removeObjectAt(x, y) {
for (let i = stellarObjects.length - 1; i >= 0; i--) {
let obj = stellarObjects[i];
let difX = obj.x - x,
difY = obj.y - y;
if (getDistance(difX, difY) <= obj.size) {
stellarObjects.splice(i, 1);
}
}
let newLength = stellarObjects.length;
if (newLength != numObjects) {
numObjects = newLength;
CTX.clearRect(0, 0, WIDTH, HEIGHT);
if (showFabric) {
resetGravityFieldInPlace();
recalculateGravityField();
drawGravityField();
}
drawObjects();
}
}
// Moves all objects based on their current velocities
function moveObjects() {
if (shouldHandleCollisions) {
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
let ov = o.velocity;
checkCollision(o, ov.x, ov.y, []);
}
}
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
let ov = o.velocity;
let ovx = ov.x,
ovy = ov.y;
o.x += ovx;
o.y += ovy;
let ox = o.x,
oy = o.y;
if (borderBehavior !== "Unbounded") {
if (borderBehavior === "Loop") {
if (ox > WIDTH) {
o.x = 0;
} else if (ox < 0) {
o.x = WIDTH;
}
if (oy > HEIGHT) {
o.y = 0;
} else if (oy < 0) {
o.y = HEIGHT;
}
} else if (borderBehavior === "Ricochet") {
if (ox > WIDTH || ox < 0) {
o.velocity.x = ovx * -1;
}
if (oy > HEIGHT || oy < 0) {
o.velocity.y = -1 * ovy;
}
} else if (borderBehavior === "HalfRicochet") {
if (ox > WIDTH || ox < 0) {
o.velocity.x = ovx * (-0.5);
if (ox > WIDTH) {
o.x = WIDTH;
} else if (ox < 0) {
o.x = 0;
}
}
if (oy > HEIGHT || oy < 0) {
o.velocity.y = (-0.5) * ovy;
if (oy > HEIGHT) {
o.y = HEIGHT;
} else if (oy < 0) {
o.y = 0;
}
}
} else if (borderBehavior === "Annihilate") {
if (ox > WIDTH || oy > HEIGHT || ox < 0 || oy < 0) {
removeObjectAt(ox, oy);
}
}
}
}
}
// Checks whether two objects are about to collide, and adjusts their velocities if necessary
function checkCollision(obj, ovx, ovy, objectsToIgnore) {
for (let i = 0; i < numObjects; i++) {
let test = stellarObjects[i],
shortCircuit = false;
if (test === obj) {
continue;
}
for (let j = 0, len = objectsToIgnore.length; j < len; j++) {
if (test === objectsToIgnore[j]) {
shortCircuit = true;
break;
}
}
if (shortCircuit) {
continue;
}
let ox = obj.x + ovx,
oy = obj.y + ovy,
tv = test.velocity;
let tvx = tv.x,
tvy = tv.y;
let tx = test.x + tvx,
ty = test.y + tvy;
let difx = tx - ox,
dify = ty - oy;
if (difx > DOUBLE_SIZE || dify > DOUBLE_SIZE || (difx === 0 && dify === 0)) {
continue;
}
let aSize = obj.size,
bSize = test.size;
let cumulativeSize = aSize + bSize;
let distance = getDistance(difx, dify);
if (distance <= cumulativeSize) {
handleCollision(obj, test, cumulativeSize, difx, dify, distance, ox, oy, tx, ty);
checkCollision(test, tvx, tvy, [obj].concat(objectsToIgnore)); // objectsToIgnore prevents a "Night at the Roxbury" collision loop
}
}
}
// Given two objects, their combined size, differences in their xy coordinates, distance, and their coordinates
// updates their coordinates to ensure the objects don't overlap and adjusts their velocities
function handleCollision(first, second, cumulativeSize, difX, difY, distance, x1, y1, x2, y2) {
let mass1 = first.mass,
mass2 = second.mass;
let cumulativeMass = mass1 + mass2;
let v1x = first.velocity.x,
v1y = first.velocity.y,
v2x = second.velocity.x,
v2y = second.velocity.y;
let v1 = Math.sqrt(v1x * v1x + v1y * v1y),
v2 = Math.sqrt(v2x * v2x + v2y * v2y);
let collisionAngle = Math.atan2(difY, difX);
let dir1 = Math.atan2(v1y, v1x),
dir2 = Math.atan2(v2y, v2x);
let d1cA = dir1 - collisionAngle,
d2cA = dir2 - collisionAngle;
let newXv1 = v1 * Math.cos(d1cA),
newYv1 = v1 * Math.sin(d1cA),
newXv2 = v2 * Math.cos(d2cA),
newYv2 = v2 * Math.sin(d2cA);
let massVCalc = mass1 * newXv1 + mass2 * newXv2;
let finalXv1 = (massVCalc + mass2 * Cr * (newXv2 - newXv1)) / cumulativeMass,
finalXv2 = (massVCalc + mass1 * Cr * (newXv1 - newXv2)) / cumulativeMass,
finalYv1 = newYv1,
finalYv2 = newYv2;
let cosAngle = Math.cos(collisionAngle),
sinAngle = Math.sin(collisionAngle);
first.velocity = {
x: cosAngle * finalXv1 - sinAngle * finalYv1,
y: sinAngle * finalXv1 + cosAngle * finalYv1
};
second.velocity = {
x: cosAngle * finalXv2 - sinAngle * finalYv2,
y: sinAngle * finalXv2 + cosAngle * finalYv2
};
// minimum translation difference to prevent overlaps:
let dx = first.x - second.x,
dy = first.y - second.y;
let d = Math.sqrt(dx * dx + dy * dy);
if (d < cumulativeSize) {
let mtd_multiplier = ((first.size + second.size - d) / d);
let mtd_x = mtd_multiplier * dx;
let mtd_y = mtd_multiplier * dy;
let im1 = 1 / mass1,
im2 = 1 / mass2;
let cumIm = im1 + im2;
let imCalc1 = (im1 / (cumIm)),
imCalc2 = (im2 / (cumIm));
first.x += mtd_x * imCalc1;
first.y += mtd_y * imCalc1;
second.x -= mtd_x * imCalc2;
second.y -= mtd_y * imCalc2;
}
}
// Applies an object's gravity to all other objects and to the gravity field fabric (if displayed)
function applyObjectsGravity() {
for (let i = numObjects - 1; i >= 0; i--) {
let o = stellarObjects[i];
let ox = o.x,
oy = o.y,
om = o.massEffect;
applyObjectGravityToObjects(o, ox, oy, om, i);
if (showFabric) {
applyObjectGravityToFabric(ox, oy, om);
}
}
}
// Given an object, its xy coordinates, and its precalculated mass effect, applies given object's gravity to all other objects
function applyObjectGravityToObjects(stellarObject, x, y, massEffect, init) {
let objVel = stellarObject.velocity;
for (let i = init; i >= 0; i--) {
let currentTarget = stellarObjects[i];
if (currentTarget !== stellarObject) {
let targetX = currentTarget.x,
targetY = currentTarget.y,
targetME = currentTarget.massEffect,
targetVel = currentTarget.velocity;
let difY = y - targetY,
difX = x - targetX;
let distance = getDistance(difY, difX);
if (distance !== 0) {
let distSqr = distance * distance;
// F = G*m1*m2 / distance^2... acceleration = F / m... the current object's mass cancels out of Force equation to produce acceleration
let accelTarg = massEffect / distSqr,
accelObj = targetME / distSqr;
let yIsNegative = difY < 0;
let theta = Math.atan(difX / difY);
targetVel.x += difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelTarg * Math.sin(theta));
targetVel.y += difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelTarg * Math.cos(theta));
objVel.x -= difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelObj * Math.sin(theta));
objVel.y -= difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelObj * Math.cos(theta));
}
}
}
}
// For a given object (and its precalculated mass * the gravitational constant) adjusts the gravity field fabric accordingly
function applyObjectGravityToFabric(x, y, massEffect) {
let xMeasure = x / cellSize,
yMeasure = y / cellSize;
for (let i = 0; i < gHeight; i++) {
let row = gField[i];
for (let j = 0; j < gWidth; j++) {
let currentVector = row[j];
let oX = currentVector[0],
oY = currentVector[1],
difX = xMeasure - j,
difY = yMeasure - i;
let distance = getDistance(difX, difY);
if (distance !== 0) {
let force = (massEffect) / (distance * distance);
let xIsNegative = difX < 0,
yIsNegative = difY < 0;
let theta = Math.atan(difX / difY);
currentVector[0] += difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * force * Math.sin(theta));
currentVector[1] += difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * force * Math.cos(theta));
}
}
}
}
// Warps the gravity field fabric based on the mass of each stellar object
function recalculateGravityField() {
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
let ox = o.x,
oy = o.y,
om = o.massEffect;
applyObjectGravityToFabric(ox, oy, om);
}
}
// Resets all the gravity field fabric markers to their places
function resetGravityFieldInPlace() {
for (let i = 0; i < gHeight; i++) {
let row = gField[i];
for (let w = 0; w < gWidth; w++) {
row[w] = [0, 0];
}
}
}
// Calculates the number of gravity field fabric markers based on granularity and initializes them
function resetGravityField() {
gField = [];
let maxH = HEIGHT / cellSize,
maxW = WIDTH / cellSize;
for (let h = 0; h <= maxH; h++) {
let row = [];
for (let w = 0; w <= maxW; w++) {
row.push([0, 0]);
}
gField.push(row);
}
gHeight = gField.length;
gWidth = gField[0].length
}
// draws all stellar objects
function drawObjects() {
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
drawObject(o);
}
if (planting) {
drawCursorLine();
}
}
// draws a line from the cursor planted point to the cursor
function drawCursorLine() {
CTX.beginPath();
CTX.strokeStyle = CURSOR_LINE_COLOR;
CTX.moveTo(plantx, planty);
CTX.lineTo(mouseX, mouseY);
CTX.stroke();
CTX.closePath();
}
// draws a stellar object on the canvas
function drawObject(o) {
let x = o.x,
y = o.y,
radius = o.size,
color = o.color;
CTX.beginPath();
CTX.arc(x, y, radius, 0, TWOPI);
CTX.fillStyle = color;
CTX.fill();
CTX.strokeStyle = OBJECT_BORDER_COLOR;
CTX.arc(x, y, radius, 0, TWOPI);
CTX.stroke();
CTX.closePath();
}
// Draws the markers for the gravity field fabric
function drawGravityField() {
CTX.strokeStyle = GRAVITY_FIELD_MARKER_COLOR;
for (let i = 0; i < gHeight; i++) {
let row = gField[i],
iMeasure = i * cellSize;
for (let j = 0; j < gWidth; j++) {
drawVectorDot(j * cellSize, iMeasure, row[j]);
}
}
}
// Draws a marker for the gravity field fabric, given the XY coordinates of the dot and a vector representing how much it's been warped
function drawVectorDot(x, y, vector) {
let vx = vector[0],
vy = vector[1];
let endX = x + vx,
endY = y + vy;
CTX.globalAlpha = fadeMultiplier / Math.sqrt(vx * vx + vy * vy); // the farther the marker is pulled, the more it fades from view
CTX.beginPath();
CTX.arc(endX, endY, GRAVITY_FIELD_MARKER_RADIUS, 0, TWOPI);
CTX.stroke();
CTX.globalAlpha = 1;
}
// returns an object with random color, position, velocity, and mass/size
function getRandomStellarObject() {
let randomMass = MIN_RND_MASS + Math.random() * (MAX_RND_MASS - MIN_RND_MASS);
let scale = MAX_RND_SIZE / Math.pow(3 * MAX_RND_MASS / 4 / PI, 1 / 3); // size formula based on volume of a sphere
let randomSize = Math.pow(3 * randomMass / 4 / PI, 1 / 3) * scale,
randomColor = getRandomColor();
let velocity = Math.random() * MAX_RND_VEL;
let bsq = Math.random() * velocity;
return {
color: randomColor,
size: randomSize,
mass: randomMass,
x: WIDTH / 6 + (4 * WIDTH / 6 * Math.random()),
y: HEIGHT / 6 + (4 * HEIGHT / 6 * Math.random()),
velocity: {
x: (Math.random() * 2 > 1 ? -1 : 1) * Math.sqrt(bsq),
y: (Math.random() * 2 > 1 ? -1 : 1) * Math.sqrt(velocity - bsq)
},
massEffect: randomMass * G
}
}
// Returns the distance between two points given the difference between their x and y values
function getDistance(difX, difY) {
return Math.sqrt(difX * difX + difY * difY);
}
function getRandomColor() {
return "rgb(" + (Math.random() * 256 >>> 0) + "," + (Math.random() * 256 >>> 0) + "," + (Math.random() * 256 >>> 0) + ")";
}#AutoStep {
font-weight: bold;
}
canvas {
cursor: crosshair;
background-color: black;
border: 1px solid black;
vertical-align: text-top;
-ms-touch-action: none;
}
input,
.controls {
font-size: 12pt;
font-family: Calibri;
}
input {
padding: 2px;
}
.controls {
text-align: center;
background-color: #bfbfbf;
display: inline-block;
vertical-align: text-top;
border: 1px solid black;
}
label {
cursor: pointer;
}
.container {
display: inline-block;
width: 552px;
}
.controls .inner {
display: inline-block;
}
.controls .section {
text-align: left;
background-color: #dfdfdf;
border: 1px solid #9f9f9f;
padding: 2px;
margin: 1px;
display: inline-block;
vertical-align: text-top;
}
input[type="number"] {
max-width: 2em;
}<div class="container">
<div class="controls">
<div class="section">
<div class="inner">
Objects:
<input id="NumObjects" type="number" value=3 />
</div>
<input type="button" id="Reset" value="Reset" />
</div>
<div class="section">
<div class="inner">
<input type="button" id="NextStep" value="Next" />
<input type="button" id="AutoStep" value="Auto" />
</div>
<div class="inner">Delay:
<select id="Delay">
<options>
<option value=0>none</option>
<option value=10>10 ms</option>
<option value=30 selected="selected">30 ms</option>
<option value=60>60 ms</option>
<option value=500>500ms</option>
<option value=1000>1 sec</option>
</options>
</select>
</div>
</div>
<div class="section">Border:
<select id="BorderBehavior">
<options>
<option value="Annihilate">Annihilate</option>
<option selected="selected" value="Unbounded">Unbounded</option>
<option value="Loop">Loop</option>
<option value="Ricochet">Ricochet</option>
<option value="HalfRicochet">50% Ricochet</option>
</options>
</select>
</div>
<div class="section">
<label>
<input type="checkbox" id="ShowFabric" checked="checked" />Show Fabric</label>
<div>Granularity:
<select id="FabricGranularity">
<options>
<option value="5">5</option>
<option value="7">7</option>
<option value="10">10</option>
<option value="13">13</option>
<option value="15">15</option>
<option value="20" selected="selected">20</option>
<option value="30">30</option>
</options>
</select>
</div>
</div>
<div class="section">
<div class="inner">
<label for="chkCollisions">
<input type="checkbox" checked="checked" id="chkCollisions" />Handle Collisions</label>
</div>
<div>Restitution:
<select id="Restitution">
<options>
<option value=1>1.0</option>
<option value=0.9 selected="selected">0.9</option>
<option value=0.8>0.8</option>
<option value=0.7>0.7</option>
<option value=0.6>0.6</option>
<option value=0.5>0.5</option>
<option value=0.4>0.4</option>
<option value=0.3>0.3</option>
<option value=0.2>0.2</option>
<option value=0.1>0.1</option>
<option value=0.0>0.0</option>
</options>
</select>
</div>
</div>
</div>
<canvas id="canvas" height="550" oncontextmenu="return false;" width="550">no canvas available</canvas>
</div>发布于 2017-04-19 20:19:34
我在Konijn的建议和我自己的观察的基础上对此做了一些改进,尽管我始终无法将仿真设计与任何类型的MVC方法相协调。
最重要的改进是速度算法。
只对原语和字符串常量使用ALL_CAPS,将画布和画布上下文变量名更改为小写。
将大多数匿名内联函数(例如附加到HTML控件的事件侦听器)更改为命名函数。
将不明确的resetGravityField函数重命名为reinitializeGravityField
将running字段更改为isRunning以反映其预期的布尔类型值。
删除了Konijn指出的一些不必要的中间变量。
我将速度verlet算法集成到moveObjects函数中,以获得比原来的欧拉算法更精确的仿真结果。这要求我跟踪每个物体的加速度,而不仅仅是它的速度,并加入一个TIMESTEP变量。
更新后的算法能更准确地模拟稳定的轨道,并且在高重力加速度下不会产生与原算法非常不准确的结果。这仍然是一个不完美的模拟重力,但稳定性更自然。
const G = 6.674 * Math.pow(10, -11), // gravitational constant
MAX_RND_VEL = 5,
MAX_RND_MASS = 5 * Math.pow(10, 12),
MIN_RND_MASS = 50,
MAX_RND_SIZE = 8,
PI = Math.PI,
TWOPI = Math.PI * 2,
FADE_RATE = 4,
CURSOR_LINE_COLOR = "cyan",
OBJECT_BORDER_COLOR = "white",
GRAVITY_FIELD_MARKER_COLOR = "rgb(150,200,150)",
GRAVITY_FIELD_MARKER_RADIUS = 0.5,
TIMESTEP = 1,
MAX_FIELD_DISTANCE = 30*30,
canvas = document.getElementById("canvas"),
txtNumObjects = document.getElementById("NumObjects"),
chkShowFabric = document.getElementById("ShowFabric"),
ddlFabricGranularity = document.getElementById("FabricGranularity"),
ddlRestitution = document.getElementById("Restitution"),
ddlBorderBehavior = document.getElementById("BorderBehavior"),
chkHandleCollisions = document.getElementById("chkCollisions"),
btnNextStep = document.getElementById("NextStep"),
txtDelay = document.getElementById("Delay");
const WIDTH = canvas.width,
HEIGHT = canvas.height,
DOUBLE_SIZE = MAX_RND_SIZE * 2,
ctx = canvas.getContext("2d");
let numObjects = txtNumObjects.value,
cellSize = ddlFabricGranularity.value,
borderBehavior = ddlBorderBehavior.value,
showFabric = chkShowFabric.checked,
shouldHandleCollisions = chkHandleCollisions.checked,
Cr = ddlRestitution.value, // coefficient of restitution
delay = txtDelay.value,
time = 0,
stellarObjects = [],
isRunning = false,
isPlanting = false,
plantX, plantY, mouseX, mouseY, // vars for adding new objects
gField = [],
gHeight, gWidth; // vars for spacetime fabric visualization
let fadeMultiplier = cellSize / FADE_RATE;
ddlRestitution.addEventListener("change", changeCoefficientOfRestitution);
chkHandleCollisions.addEventListener("change", changeHandleCollisions);
canvas.addEventListener("mousedown", touchDown);
canvas.addEventListener("mousemove", touchMove);
canvas.addEventListener("mouseup", touchUp);
canvas.addEventListener("mouseout", stopPlanting);
ddlBorderBehavior.addEventListener("change", changeBorderBehavior)
document.getElementById("Reset").addEventListener("click",reset);
document.getElementById("NextStep").addEventListener("click", step);
ddlFabricGranularity.addEventListener("change",changeFabricGranularity);
chkShowFabric.addEventListener("change",changeShowFabric);
document.getElementById("AutoStep").addEventListener("click", toggleAutoStep);
txtDelay.addEventListener("change", changeDelay);
reset();
function changeCoefficientOfRestitution() {
Cr = ddlRestitution.value;
}
function changeHandleCollisions() {
shouldHandleCollisions = chkHandleCollisions.checked;
}
function stopPlanting() {
isPlanting = false;
if (!isRunning) {
drawFieldAndObjects();
}
}
function changeBorderBehavior() {
borderBehavior = ddlBorderBehavior.value;
}
function changeFabricGranularity() {
cellSize = ddlFabricGranularity.value;
fadeMultiplier = cellSize / FADE_RATE;
reinitializeGravityField();
recalculateGravityField();
drawFieldAndObjects();
}
function changeShowFabric() {
showFabric = chkShowFabric.checked;
}
function toggleAutoStep() {
if (isRunning) {
clearTimeout(isRunning);
btnNextStep.disabled = false;
isRunning = null;
this.value = "Auto";
} else {
btnNextStep.disabled = true;
this.value = "Stop";
isRunning = setTimeout(loopStep, delay);
}
}
function changeDelay () {
delay = +(txtDelay.value);
}
function touchDown(event) {
if (event.which === 3 || event.button === 2) {
event.preventDefault();
return false;
} else {
isPlanting = true;
plantX = event.pageX - canvas.offsetLeft;
plantY = event.pageY - canvas.offsetTop;
mouseX = plantX;
mouseY = plantY;
}
event.preventDefault();
}
function touchMove(event) {
if (isPlanting) {
mouseX = event.pageX - canvas.offsetLeft;
mouseY = event.pageY - canvas.offsetTop;
if (!isRunning) {
drawFieldAndObjects();
}
drawCursorLine();
}
event.preventDefault();
}
function touchUp(event) {
if (event.which === 3 || event.button === 2) {
removeObjectAt(event.pageX - canvas.offsetLeft, event.pageY - canvas.offsetTop);
event.preventDefault();
return false;
} else {
if (isPlanting) {
let thing = getRandomStellarObject(),
upX = event.pageX - canvas.offsetLeft,
upY = event.pageY - canvas.offsetTop;
if (typeof plantX === "undefined") {
plantX = upX;
plantY = upY;
}
thing.x = upX;
thing.y = upY;
thing.velocity.x = MAX_RND_VEL * MAX_RND_VEL * (plantX - upX) / WIDTH;
thing.velocity.y = MAX_RND_VEL * MAX_RND_VEL * (plantY - upY) / HEIGHT;
stellarObjects.push(thing);
numObjects++;
isPlanting = false;
applyObjectGravityToFabric(upX, upY, thing.mass * G);
drawFieldAndObjects();
}
}
event.preventDefault();
}
// Clears all objects and regenerates them based on the chosen number of objects
function reset() {
reinitializeGravityField();
numObjects = +(document.getElementById("NumObjects").value);
numObjects = numObjects < 0 ? 0 : numObjects >>> 0;
stellarObjects = [];
generateObjects(numObjects);
step();
}
// Repeatedly advances time based on the chosen delay
function loopStep() {
let startTime = +new Date,
delta;
step();
if (delay === 0 || (delta = +new Date - startTime) >= delay) {
isRunning = setTimeout(loopStep, 0);
} else {
isRunning = setTimeout(loopStep, delay - delta);
}
}
// Advances time forward and recalculates all object positions/velocities as necessary, redrawing the canvas as necessary
function step() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
if(showFabric){
resetGravityFieldInPlace();
moveObjects();
drawGravityField();
}else{
moveObjects();
}
drawObjects();
}
// adds the specified number of objects to the universe
function generateObjects(num) {
for (let i = 0; i < num; i++) {
stellarObjects.push(getRandomStellarObject());
}
}
// Removes any stellar objects that overlap at the given xy coordinates
function removeObjectAt(x, y) {
for (let i = stellarObjects.length - 1; i >= 0; i--) {
let obj = stellarObjects[i];
if (getDistance(obj.x - x, obj.y - y) <= obj.size) {
stellarObjects.splice(i, 1);
}
}
let newLength = stellarObjects.length;
if (newLength != numObjects) {
numObjects = newLength;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
if (showFabric) {
resetGravityFieldInPlace();
recalculateGravityField();
drawGravityField();
}
drawObjects();
}
}
// Moves all objects based on their current velocities
function moveObjects() {
time += TIMESTEP;
if (shouldHandleCollisions) {
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
let ov = o.velocity, oa = o.acc;
checkCollision(o, TIMESTEP*(ov.x + TIMESTEP*oa.x/2), TIMESTEP*(ov.y + TIMESTEP*oa.y/2), []);
}
}
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
let ov = o.velocity, oa = o.acc, ooa = o.oldAcc;
let ovx = ov.x,ovy = ov.y, oax = oa.x, oay = oa.y;
o.x += TIMESTEP*(ovx + TIMESTEP * oa.x/2);
o.y += TIMESTEP*(ovy + TIMESTEP * oa.y/2);
ooa.x = oax, ooa.y = oay;
oa.x = 0, oa.y = 0;
let ox = o.x,
oy = o.y;
if (borderBehavior !== "Unbounded") {
if (borderBehavior === "Loop") {
if (ox > WIDTH) {
o.x = 0;
} else if (ox < 0) {
o.x = WIDTH;
}
if (oy > HEIGHT) {
o.y = 0;
} else if (oy < 0) {
o.y = HEIGHT;
}
} else if (borderBehavior === "Ricochet") {
if (ox > WIDTH || ox < 0) {
o.velocity.x = ovx * -1;
}
if (oy > HEIGHT || oy < 0) {
o.velocity.y = -1 * ovy;
}
} else if (borderBehavior === "HalfRicochet") {
if (ox > WIDTH || ox < 0) {
o.velocity.x = ovx * (-0.5);
if (ox > WIDTH) {
o.x = WIDTH;
} else if (ox < 0) {
o.x = 0;
}
}
if (oy > HEIGHT || oy < 0) {
o.velocity.y = (-0.5) * ovy;
if (oy > HEIGHT) {
o.y = HEIGHT;
} else if (oy < 0) {
o.y = 0;
}
}
} else if (borderBehavior === "Annihilate") {
if (ox > WIDTH || oy > HEIGHT || ox < 0 || oy < 0) {
removeObjectAt(ox, oy);
}
}
}
}
applyObjectsGravity();
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
o.velocity.x += TIMESTEP * (o.acc.x + o.oldAcc.x) / 2;
o.velocity.y += TIMESTEP * (o.acc.y + o.oldAcc.y) / 2;
}
}
// Checks whether two objects are about to collide, and adjusts their velocities if necessary
function checkCollision(obj, ovx, ovy, objectsToIgnore) {
for (let i = 0; i < numObjects; i++) {
let test = stellarObjects[i],
shortCircuit = false;
if (test === obj) {
continue;
}
for (let j = 0, len = objectsToIgnore.length; j < len; j++) {
if (test === objectsToIgnore[j]) {
shortCircuit = true;
break;
}
}
if (shortCircuit) {
continue;
}
let oa = obj.acc, ta = test.acc;
let ox = obj.x + TIMESTEP*(ovx + TIMESTEP * oa.x/2),
oy = obj.y + TIMESTEP*(ovy + TIMESTEP * oa.y/2),
tv = test.velocity;
let tvx = tv.x,
tvy = tv.y;
let tx = test.x + TIMESTEP*(tvx + TIMESTEP * ta.x/2),
ty = test.y + TIMESTEP*(tvy + TIMESTEP * ta.y/2);
let difx = tx - ox,
dify = ty - oy;
if (difx === 0 && dify === 0){
continue;
}
let aSize = obj.size,
bSize = test.size;
let cumulativeSize = aSize + bSize;
let distance = getDistance(difx, dify);
if (distance < cumulativeSize) {
handleCollision(obj, test, cumulativeSize, difx, dify, distance, ox, oy, tx, ty);
checkCollision(test, tvx, tvy, [obj].concat(objectsToIgnore)); // objectsToIgnore prevents a "Night at the Roxbury" collision loop // [obj].concat(objectsToIgnore)
checkCollision(obj, ovx, ovy, [test].concat(objectsToIgnore));
}
}
}
// Given two objects, their combined size, differences in their xy coordinates, distance, and their coordinates
// updates their coordinates to ensure the objects don't overlap and adjusts their velocities
function handleCollision(first, second, cumulativeSize, difX, difY, distance, x1, y1, x2, y2) {
let mass1 = first.mass,
mass2 = second.mass;
let cumulativeMass = mass1 + mass2;
let v1x = first.velocity.x,
v1y = first.velocity.y,
v2x = second.velocity.x,
v2y = second.velocity.y;
let v1 = Math.sqrt(v1x * v1x + v1y * v1y),
v2 = Math.sqrt(v2x * v2x + v2y * v2y);
let collisionAngle = Math.atan2(difY, difX);
let dir1 = Math.atan2(v1y, v1x),
dir2 = Math.atan2(v2y, v2x);
let d1cA = dir1 - collisionAngle,
d2cA = dir2 - collisionAngle;
let newXv1 = v1 * Math.cos(d1cA),
newYv1 = v1 * Math.sin(d1cA),
newXv2 = v2 * Math.cos(d2cA),
newYv2 = v2 * Math.sin(d2cA);
let massVCalc = mass1 * newXv1 + mass2 * newXv2;
let finalXv1 = (massVCalc + mass2 * Cr * (newXv2 - newXv1)) / cumulativeMass,
finalXv2 = (massVCalc + mass1 * Cr * (newXv1 - newXv2)) / cumulativeMass,
finalYv1 = newYv1,
finalYv2 = newYv2;
let cosAngle = Math.cos(collisionAngle),
sinAngle = Math.sin(collisionAngle);
first.velocity = {
x: cosAngle * finalXv1 - sinAngle * finalYv1,
y: sinAngle * finalXv1 + cosAngle * finalYv1
};
second.velocity = {
x: cosAngle * finalXv2 - sinAngle * finalYv2,
y: sinAngle * finalXv2 + cosAngle * finalYv2
};
// minimum translation difference to prevent overlaps:
let dx = first.x - second.x,
dy = first.y - second.y;
if(dx === 0 && dy === 0){ // special case for shared centers of gravity, offsets objects in random directions before continuing
let xOffset = (Math.random()*1);
let yOffset = (1 - xOffset)*(Math.random()*2 > 1 ? -1 : 1 );
xOffset *= (Math.random()*2 > 1 ? -1 : 1 );
first.x += xOffset / mass1;
first.y += yOffset / mass1;
second.x -= xOffset / mass2;
second.y -= yOffset / mass2;
dx = first.x - second.x;
dy = first.y - second.y;
}
let d_squared = (dx * dx + dy * dy);
if (d_squared <= cumulativeSize*cumulativeSize) {
let d = Math.sqrt(d_squared);
let mtd_multiplier = ((first.size + second.size - d) / d);
let mtd_x = mtd_multiplier * dx;
let mtd_y = mtd_multiplier * dy;
let im1 = 1 / mass1,
im2 = 1 / mass2;
let cumIm = im1 + im2;
let imCalc1 = (im1 / (cumIm)),
imCalc2 = (im2 / (cumIm));
first.x += mtd_x * imCalc1;
first.y += mtd_y * imCalc1;
second.x -= mtd_x * imCalc2;
second.y -= mtd_y * imCalc2;
first.acc = {x:0,y:0};
first.oldAcc = {x:0,y:0};
second.acc = {x:0,y:0};
second.oldAcc = {x:0,y:0};
}
}
// Applies an object's gravity to all other objects and to the gravity field fabric (if displayed)
function applyObjectsGravity() {
for (let i = numObjects-1; i >= 0; i--) {
let o = stellarObjects[i];
let ox = o.x,
oy = o.y,
om = o.massEffect;
applyObjectGravityToObjects(o, ox, oy, om, i);
if (showFabric) {
applyObjectGravityToFabric(ox, oy, om);
}
}
}
// Given an object, its xy coordinates, and its precalculated mass effect, applies given object's gravity to all other objects
function applyObjectGravityToObjects(stellarObject, x, y, massEffect, init) {
let objAcc = stellarObject.acc, objOldAcc = stellarObject.oldAcc;
for (let i = init; i >= 0; i--) {
let currentTarget = stellarObjects[i];
if (currentTarget !== stellarObject) {
let targetX = currentTarget.x,
targetY = currentTarget.y,
targetME = currentTarget.massEffect,
targetAcc = currentTarget.acc, targetOldAcc = currentTarget.oldAcc;
let difY = y - targetY,
difX = x - targetX;
let distance = getDistance(difY, difX);
if (distance !== 0) {
let distSqr = distance * distance;
// F = G*m1*m2 / distance^2... acceleration = F / m... the current object's mass cancels out of Force equation to produce acceleration
let accelTarg = massEffect / distSqr,
accelObj = targetME / distSqr;
let yIsNegative = difY < 0;
let theta = Math.atan(difX / difY);
targetAcc.x += difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelTarg * Math.sin(theta));
targetAcc.y += difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelTarg * Math.cos(theta));
objAcc.x -= difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelObj * Math.sin(theta));
objAcc.y -= difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * accelObj * Math.cos(theta));
}
}
}
}
// For a given object (and its precalculated mass * the gravitational constant) adjusts the gravity field fabric accordingly
function applyObjectGravityToFabric(x, y, massEffect) {
let xMeasure = x/cellSize, yMeasure = y/cellSize;
for (let i = 0; i < gHeight; i++) {
let row = gField[i];
for (let j = 0; j < gWidth; j++) {
let currentVector = row[j];
let oX = currentVector[0],
oY = currentVector[1],
difX = xMeasure - j,
difY = yMeasure - i;
if(difX*difX + difY*difY < MAX_FIELD_DISTANCE ){
let distance = getDistance(difX, difY);
if ( distance !== 0) {
let force = (massEffect) / (distance * distance);
let xIsNegative = difX < 0,
yIsNegative = difY < 0;
let theta = Math.atan(difX / difY);
currentVector[0] += difX === 0 ? 0 : ((yIsNegative ? -1 : 1) * force * Math.sin(theta));
currentVector[1] += difY === 0 ? 0 : ((yIsNegative ? -1 : 1) * force * Math.cos(theta));
}
}
}
}
}
// Warps the gravity field fabric based on the mass of each stellar object
function recalculateGravityField() {
for (let i = 0; i < numObjects; i++) {
let o = stellarObjects[i];
applyObjectGravityToFabric(o.x, o.y, o.massEffect);
}
}
// Resets all the gravity field fabric markers to their places
function resetGravityFieldInPlace() {
for (let i = 0; i < gHeight; i++) {
let row = gField[i];
for (let w = 0; w < gWidth; w++) {
row[w] = [0,0];
}
}
}
// Calculates the number of gravity field fabric markers based on granularity and initializes them
function reinitializeGravityField() {
gField = [];
let maxH = HEIGHT / cellSize,
maxW = WIDTH / cellSize;
for (let h = 0; h <= maxH; h++) {
let row = [];
for (let w = 0; w <= maxW; w++) {
row.push([0,0]);
}
gField.push(row);
}
gHeight = gField.length;
gWidth = gField[0].length
}
function drawFieldAndObjects(){
ctx.clearRect(0, 0, WIDTH, HEIGHT);
if (showFabric) {
drawGravityField();
}
drawObjects();
}
// draws all stellar objects
function drawObjects() {
for (let i = 0; i < numObjects; i++) {
drawObject(stellarObjects[i]);
}
if (isPlanting) {
drawCursorLine();
}
}
// draws a line from the cursor planted point to the cursor
function drawCursorLine() {
ctx.beginPath();
ctx.strokeStyle = CURSOR_LINE_COLOR;
ctx.moveTo(plantX, plantY);
ctx.lineTo(mouseX, mouseY);
ctx.stroke();
ctx.closePath();
}
// draws a stellar object on the canvas
function drawObject(o) {
let x = o.x,
y = o.y,
radius = o.size;
ctx.beginPath();
ctx.arc(x, y, radius, 0, TWOPI);
ctx.fillStyle = o.color;
ctx.fill();
ctx.strokeStyle = OBJECT_BORDER_COLOR;
ctx.arc(x, y, radius, 0, TWOPI);
ctx.stroke();
ctx.closePath();
}
// Draws the markers for the gravity field fabric
function drawGravityField() {
ctx.strokeStyle = GRAVITY_FIELD_MARKER_COLOR;
for (let i = 0; i < gHeight; i++) {
let row = gField[i], iMeasure = i * cellSize;
for (let j = 0; j < gWidth; j++) {
drawVectorDot(j * cellSize, iMeasure, row[j]);
}
}
}
// Draws a marker for the gravity field fabric, given the XY coordinates of the dot and a vector representing how much it's been warped
function drawVectorDot(x, y, vector) {
let vx = vector[0],
vy = vector[1];
ctx.globalAlpha = fadeMultiplier / Math.sqrt(vx*vx + vy*vy); // the farther the marker is pulled, the more it fades from view
ctx.beginPath();
ctx.arc(x + vx, y + vy, GRAVITY_FIELD_MARKER_RADIUS, 0, TWOPI);
ctx.stroke();
ctx.globalAlpha = 1;
}
// returns an object with random color, position, velocity, and mass/size
function getRandomStellarObject() {
let randomMass = MIN_RND_MASS + Math.random() * (MAX_RND_MASS - MIN_RND_MASS);
let scale = MAX_RND_SIZE / Math.pow(3 * MAX_RND_MASS / 4 / PI, 1 / 3); // size formula based on volume of a sphere
let randomSize = Math.pow(3 * randomMass / 4 / PI, 1 / 3) * scale,
randomColor = getRandomColor();
let velocity = Math.random() * MAX_RND_VEL;
let xComponent = Math.random() * velocity;
return {
color: randomColor,
size: randomSize,
mass: randomMass,
x: WIDTH / 6 + (2 * WIDTH / 3 * Math.random()),
y: HEIGHT / 6 + (2 * HEIGHT / 3 * Math.random()),
velocity: {
x: (Math.random() * 2 > 1 ? -1 : 1) * Math.sqrt(xComponent),
y: (Math.random() * 2 > 1 ? -1 : 1) * Math.sqrt(velocity - xComponent)
},
acc:{
x:0,y:0,xh:0,yh:0
},
oldAcc: {
x:0,y:0
},
massEffect: randomMass * G
}
}
// Returns the distance between two points given the difference between their x and y values
function getDistance(difX, difY) {
return Math.sqrt(difX * difX + difY * difY);
}
function getRandomColor() {
return "rgb(" + (Math.random() * 256 >>> 0) + "," + (Math.random() * 256 >>> 0) + "," + (Math.random() * 256 >>> 0) + ")";
}#AutoStep {
font-weight: bold;
}
canvas {
cursor: crosshair;
background-color: black;
border: 1px solid black;
vertical-align: text-top;
-ms-touch-action: none;
}
input,
.controls {
font-size: 12pt;
font-family: Calibri;
}
input {
padding: 2px;
}
.controls {
text-align: center;
background-color: #bfbfbf;
display: inline-block;
vertical-align: text-top;
border: 1px solid black;
}
label {
cursor: pointer;
}
.container {
display: inline-block;
width: 552px;
}
.controls .inner {
display: inline-block;
}
.controls .section {
text-align: left;
background-color: #dfdfdf;
border: 1px solid #9f9f9f;
padding: 2px;
margin: 1px;
display: inline-block;
vertical-align: text-top;
}
input[type="number"] {
max-width: 2em;
}<div class="container">
<div class="controls">
<div class="section">
<div class="inner">
Objects:
<input id="NumObjects" type="number" value=3 />
</div>
<input type="button" id="Reset" value="Reset" />
</div>
<div class="section">
<div class="inner">
<input type="button" id="NextStep" value="Next" />
<input type="button" id="AutoStep" value="Auto" />
</div>
<div class="inner">Delay:
<select id="Delay">
<options>
<option value=0>none</option>
<option value=10>10 ms</option>
<option value=30 selected="selected">30 ms</option>
<option value=60>60 ms</option>
<option value=500>500ms</option>
<option value=1000>1 sec</option>
</options>
</select>
</div>
</div>
<div class="section">Border:
<select id="BorderBehavior">
<options>
<option value="Annihilate">Annihilate</option>
<option selected="selected" value="Unbounded">Unbounded</option>
<option value="Loop">Loop</option>
<option value="Ricochet">Ricochet</option>
<option value="HalfRicochet">50% Ricochet</option>
</options>
</select>
</div>
<div class="section">
<label>
<input type="checkbox" id="ShowFabric" checked="checked" />Show Fabric</label>
<div>Granularity:
<select id="FabricGranularity">
<options>
<option value="5">5</option>
<option value="7">7</option>
<option value="10">10</option>
<option value="13">13</option>
<option value="15">15</option>
<option value="20" selected="selected">20</option>
<option value="30">30</option>
</options>
</select>
</div>
</div>
<div class="section">
<div class="inner">
<label for="chkCollisions">
<input type="checkbox" checked="checked" id="chkCollisions" />Handle Collisions</label>
</div>
<div>Restitution:
<select id="Restitution">
<options>
<option value=1>1.0</option>
<option value=0.95 selected="selected">0.95</option>
<option value=0.9>0.9</option>
<option value=0.8>0.8</option>
<option value=0.7>0.7</option>
<option value=0.6>0.6</option>
<option value=0.5>0.5</option>
<option value=0.4>0.4</option>
<option value=0.3>0.3</option>
<option value=0.2>0.2</option>
<option value=0.1>0.1</option>
<option value=0.0>0.0</option>
</options>
</select>
</div>
</div>
</div>
<canvas id="canvas" height="550" oncontextmenu="return false;" width="550">no canvas available</canvas>
</div>发布于 2017-04-20 03:00:19
对数字使用科学符号:
const G = 6.674e-11;使用自动代码格式化程序。目前您的代码缩进不一致。
删除多余的括号,如在a = (b * (c))中。
https://codereview.stackexchange.com/questions/152212
复制相似问题