首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >“我知道情况有多严重吗?”

“我知道情况有多严重吗?”
EN

Code Review用户
提问于 2017-01-10 04:46:56
回答 2查看 270关注 0票数 17

,这是什么?

这是一个二维物理模拟器,用来模拟重力和圆形物体之间的碰撞(尽管它不模拟旋转)。

你是怎么用的?

您可以在仿真运行时调整以下参数:

  • 物体数量:当单击“重置”时,指定数量的重力物体将以随机大小和速度(替换任何现有物体)添加到场景中。
  • 延迟:当模拟在自动上运行时,故意延迟仿真的毫秒数(更好地查看正在发生的事情)。您还可以手动推进模拟,在这种情况下,延迟不会产生任何影响。
  • 边界行为:确定当物体越过场景的边界时发生了什么。选项是湮灭,无限(默认),循环,Ricochet (这是完全弹性的),和50%的Ricochet (不是这样)。
  • 展示时空的结构:切换是否显示视觉标记,通过它们的位移来表示引力场的强度。
  • 织物粒度:决定所显示标记数量的数字(数字越低,标记密度越高)。
  • 处理碰撞:切换是否执行碰撞计算时,物体的速度会导致他们相交。
  • 恢复系数:确定碰撞弹性的0到1之间的数字。在1.0时,碰撞是完全弹性的。在0.0时,碰撞是完全非弹性的,物体在碰撞时互相粘着。

尽管更多的是模拟而不是游戏,但除了修改参数之外,还有一些用户交互的方面:

  • 单击此处可添加对象。
  • 单击并拖动可添加具有初始速度的对象。
  • 右击可移除对象。

我关心什么?

改进性能

在没有指定延迟的情况下可以提高性能的任何东西。

基于我在重构康威生命游戏的实现时看到的大量性能改进,我尝试尽可能地在变量中缓存和重用值,并将其重新计算或重新检索限制到将检索到的值作为参数传递给我知道需要它们的函数,即使它们可以自己检索或计算它们。

我将感激任何数学快捷键可以减少计算开销而不牺牲准确性。

符合最佳实践的

无论是可读性、可维护性、可测试性还是任何其他-bility,我都很欣赏任何改进的见解和建议。

提高仿真精度

一些不现实的行为似乎是重力模拟固有的。我想知道是否有任何常规或标准的方法来减轻这种行为。

具体而言:应用线性速度移动一个质量点意味着你跳过任何速度重新计算,这将导致有效重力的变化,所有的直线运动路径。当物体以高速运动时(特别是在强烈的引力影响下加速),这会导致不准确的现象。

类似但不同的不精确性涉及碰撞;我的碰撞代码检查重叠对象,但如果两个对象之间的相对速度足够高,则其中一个对象可能会被另一个对象的线性运动“跳过”。一种解决办法是限制物体的最大速度,但我很想知道其他更合适的解,比如(假设)一种有效计算物体轨迹和检查交叉点的方法。

我并不真正关心

关注点分离(除非你说服我)

作为一种模拟而不是游戏,代码的计算和渲染部分是交织在一起的;因为在每次计算之后发生了一些有趣的事情,我希望在每一步之后在屏幕上重新绘制这些事件。我还没有将呈现拆分到它自己的虚拟线程。

在Chrome

中看起来有点古怪

在撰写这篇文章时,Chrome似乎处理2D画布上下文的globalAlpha与Firefox、Edge和Internet不同。在Chrome中,重力场织物标记的褪色速度还不够快,所以在屏幕上漂浮的物体上似乎会有飞散的斑点。如果有人对如何解决这种行为提出建议,我愿意接受建议,但在这一点上我并不太担心。

代码语言:javascript
复制
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) + ")";
}
代码语言:javascript
复制
#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;
}
代码语言:javascript
复制
<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>
EN

回答 2

Code Review用户

发布于 2017-04-19 20:19:34

我在Konijn的建议和我自己的观察的基础上对此做了一些改进,尽管我始终无法将仿真设计与任何类型的MVC方法相协调。

最重要的改进是速度算法。

提高可读性(变量和函数名)

只对原语和字符串常量使用ALL_CAPS,将画布和画布上下文变量名更改为小写。

将大多数匿名内联函数(例如附加到HTML控件的事件侦听器)更改为命名函数。

将不明确的resetGravityField函数重命名为reinitializeGravityField

running字段更改为isRunning以反映其预期的布尔类型值。

删除了Konijn指出的一些不必要的中间变量。

提高计算精度(Verlet积分)

我将速度verlet算法集成到moveObjects函数中,以获得比原来的欧拉算法更精确的仿真结果。这要求我跟踪每个物体的加速度,而不仅仅是它的速度,并加入一个TIMESTEP变量。

更新后的算法能更准确地模拟稳定的轨道,并且在高重力加速度下不会产生与原算法非常不准确的结果。这仍然是一个不完美的模拟重力,但稳定性更自然。

最新仿真:

代码语言:javascript
复制
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) + ")";
}
代码语言:javascript
复制
#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;
}
代码语言:javascript
复制
<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>
票数 2
EN

Code Review用户

发布于 2017-04-20 03:00:19

对数字使用科学符号:

代码语言:javascript
复制
const G = 6.674e-11;

使用自动代码格式化程序。目前您的代码缩进不一致。

删除多余的括号,如在a = (b * (c))中。

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/152212

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档