两体

显示所有步骤

万有引力公式直接揭示两个物体之间的吸引力, 也就是万有引力公式是宇宙里两体运行规律的数学表达。 当然牛顿第一、第二、第三定律也有公式形式, 万有引力公式更是牛顿三大定律的精彩集成。

先看看实现的两体运行效果(全屏体验):

实现代码分三部分,一部分是十几行的html页面代码(如下黑色背景区域里的内容),剩下两部分都是javascript代码。javascript代码实现了两方面的功能,一方面是绘制图像,另一方面是用牛顿三定律及其万有引力计算物体的速度,加速度,位置等信息。

页面代码非常之少,几乎是最精简的web代码了,与一般页面不同的是定义了一个canvas, 我们将在其中绘制万有引力下的运动物体:

<!DOCTYPE html>
<html>
<head>
    <script>通用的第一部分javascript代码</script>
</head>
<body>
    <div><canvas id='2-objects-canvas'></canvas></div>
    <script>少量变化的第二部分javascript代码</script>
</body>
<html>

通用的第一部分javascript代码里,大部分是我们根据牛顿定律,万有引力公式实现的几个函数:

var G = 6.67259 * Math.pow(10, -11);
var requestAnimationFrame = window.requestAnimationFrame
                            || window.mozRequestAnimationFrame
                            || window.webkitRequestAnimationFrame
                            || window.msRequestAnimationFrame;
function drawObject(context, obj) {
    context.beginPath();
    context.arc(obj.x, obj.y, obj.r, 0, Math.PI * 2, true);
    context.closePath();
    context.fillStyle = obj.color;
    context.fill();

    context.beginPath();
    context.strokeStyle = obj.color;
    for(var i=0; i < obj.path.length; i++){
        if (i === 0){
            context.moveTo(obj.path[i].x, obj.path[i].y);
        }
        else{
            context.lineTo(obj.path[i].x, obj.path[i].y);
        }
    }
    context.stroke();
    context.closePath();
}
function distance(A, B){
    var dX = A.x - B.x;
    var dY = A.y - B.y;
    return Math.sqrt(dX*dX + dY* dY);
}

function force(A, B, d){
    if ( d === undefined ) d = distance(A,B);
    if ( !d ) return 0;

    return G * A.m * B.m / d / d;
}

function direction(A, B, normalize, dist){
    if (normalize){
        if (!dist) dist = distance(A,B);

        if(!dist)
            return {x: 0, y: 0};
        else
            return { x: (B.x - A.x)/dist, y: (B.y - A.y)/dist};
    }
    else{
        return {x: B.x - A.x, y: B.y - A.y};
    }
}

function updateObject(obj, scalarF, normDirect, deltaT){
    obj.forced.x = scalarF * normDirect.x;
    obj.forced.y = scalarF * normDirect.y;

    obj.acc.x = obj.forced.x / obj.m;
    obj.acc.y = obj.forced.y / obj.m;

    obj.dv.x = obj.acc.x * deltaT;
    obj.dv.y = obj.acc.y * deltaT;

    obj.x = obj.x + (obj.v.x + obj.dv.x/2) * deltaT;
    obj.y = obj.y + (obj.v.y + obj.dv.y/2) * deltaT;

    obj.v.x = obj.v.x + obj.dv.x;
    obj.v.y = obj.v.y + obj.dv.y;

    obj.path.push( {x: obj.x, y: obj.y});
    if (obj.path.length > max_len){
        obj.path = obj.path.slice(-max_len);
    }
}

function transformObit(obj, offset){
    obj.x = obj.x + offset.x;
    obj.y = obj.y + offset.y;
    for(var i=0; i < obj.path.length; i++){
        obj.path[i].x = obj.path[i].x + offset.x;
        obj.path[i].y = obj.path[i].y + offset.y;
    }
}

function calcUpdate2Object(A, B, deltaT){
    var d = distance(A, B);
    var f = force(A, B, d);
    var fAB = direction(A, B, true, d);
    var fBA = {x: -fAB.x, y: -fAB.y};

    updateObject(A, f, fAB, deltaT);
    updateObject(B, f, fBA, deltaT);
}

function makeObject(name, m, r, x, y, v, c){
     return {
        name: name,
      m: m,
      r:  r,
      x: x,
      y: y,
      v: v,
      color: c,

      forced: {x: 0, y: 0},
      acc: {x: 0, y: 0},
      dv: {x: 0, y: 0},
      path: [],
  };
}

少量变化的第二部份代码,是根据上面的公式计算刷新数据,并根据数据更新绘制两体到页面上:

var max_len = 1024 * 30;
var canvas = document.getElementById('2-objects-canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var context = canvas ? canvas.getContext('2d') : null;

var posX = 250, posY = canvas.height * 2 / 3;
var objA = makeObject('sun',1.000 * Math.pow(10, 14), 30,
                            posX, posY, {x:0.2,y:0}, 'red')
var objB = makeObject('earth',100.000 * Math.pow(10, 10), 8,
                            posX, posY-400, {x:2.2,y:0}, 'blue')

var limitDraw = 1000/30, limitCalc = 10, calcTimes = 10;
var lastDrawTime = Date.now() - limitDraw;
var lastCalcTime = Date.now() - limitCalc;

function update(){
    var now = Date.now();
    var deltaDraw = now - lastDrawTime;
    if (context && deltaDraw >= limitDraw) {
        lastDrawTime = now;
        context.fillStyle = 'black';
        context.fillRect(0, 0, canvas.width, canvas.height);
        drawObject(context, objB);
        drawObject(context, objA);
    }

    var deltaCalc = now - lastCalcTime;
    if (deltaCalc >= limitCalc) {
        lastCalcTime = now;
        for(var i = 0; i < calcTimes; i++){
            calcUpdate2Object(objA, objB, limitCalc/1000);
        }
    }
    requestAnimationFrame(update);
}

requestAnimationFrame(update);

整个代码里我们没有使用任何第三方代码,三部分全部加起来不到两百行。在后面三体有变化的部分,是根据物体个数而做少量调整, 这个在下一步我们将看到。