SOURCE

console 命令行工具 X clear

                    
>
console
/** @type {HTMLCanvasElement} */
/*****************数学与工具函数**********************/

/**圆周率 */
const pi = Math.PI;
/**常用角度 */
// const deg180 = pi;
const deg90 = pi / 2;
// const deg60 = pi/3;
// const deg45 = pi/4;
// const deg30 = pi/6;
// const deg15 = pi/12;

// 一定要有返回值!!
/**生成随机颜色*/
function getRandomColor() {
    var arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
    var color = "#";
    for (let i = 0; i < 6; i++) {
        let random = parseInt(Math.random() * 16);
        color += arr[random];
    }
    return color;
}

/**向量和*/
function vecSum(v1, v2) {
    return [v1[0] + v2[0], v1[1] + v2[1]];
}

/**向量差*/
function vecMinus(v1, v2) {
    return [v1[0] - v2[0], v1[1] - v2[1]];
}

/**向量模长*/
function vecMold(vec) {

    return Math.hypot(vec[0], vec[1]);
}

/**向量数乘(数字必须放后面!)*/
function vecZoom(vec, num) {
    if (isNaN(vec[0]) || isNaN(vec[1])) {
        console.error("向量数乘错误!", vec, " 乘以 ", num)
        return [0, 0];
    }
    return [num * vec[0], num * vec[1]];
}

/**向量与x轴夹角(弧度!)*/
function vecAngle(vec) {
    return Math.atan(vec[1] / vec[0]);
}

/**向量顺时针旋转角度(角度为弧度制,放后面!) */
function vecRotate(vec, angle) {
    let tempVec = [0, 0];
    tempVec[0] = vec[0] * Math.cos(angle) - vec[1] * Math.sin(angle);
    tempVec[1] = vec[0] * Math.sin(angle) + vec[1] * Math.cos(angle);
    return tempVec;
}

/**计算两点间的距离*/
function getDistance(v1, v2) {
    return vecMold(vecMinus(v1, v2));
}

/**计算质心位矢*/
function getMassCenter(starArr) {
    let vector = [0, 0];
    let sumMass = 0;
    starArr.forEach(star => {
        if (star != null) {
            vector = vecSum(vector, vecZoom(star.pos, star.mass));
            sumMass += star.mass;
        }
    });
    return vecZoom(vector, 1 / sumMass);
}

/**计算万有引力 (力作用在s1上)*/
function getGravity(s1, s2) {
    let dis = getDistance(s1.pos, s2.pos);
    return vecZoom(vecZoom(vecMinus(s2.pos, s1.pos), 1 / dis), G * s1.mass * s2.mass / dis);
    //返回矢量!
}

/**获得两个完全非弹性碰撞后融合天体*/
function getBump(s1, s2) {
    let newstar = new Star(
        s1.id,
        getMassCenter([s1, s2]),
        Math.pow(s1.radius ** 3 + s2.radius ** 3, 1 / 3),
        s1.mass + s2.mass,
        [(s1.vel[0] * s1.mass + s2.vel[0] * s2.mass) / (s1.mass + s2.mass),
        (s1.vel[1] * s1.mass + s2.vel[1] * s2.mass) / (s1.mass + s2.mass)
        ]
    );
    return newstar;
}

/**从中心坐标系变换到canvas渲染坐标系*/
function transform(pos) {
    return vecSum(vecZoom(pos, zoomIndex), vecSum([hosX, hosY], offsetUser));
}


function zoom(type) {
    offsetStep = parseInt(document.getElementById('offsetStep').value);
    switch (type) {
        case 'out':
            zoomIndex *= 0.8;
            break;
        case 'in':
            zoomIndex *= 1.2;
            break;
        case 'center':
            let center = getMassCenter(stars);
            stars.forEach(star => {
                star.pos = vecMinus(star.pos, center)
            })
            break;
        case 'left':
            offsetUser = vecSum(offsetUser, [offsetStep, 0])
            break;
        case 'right':
            offsetUser = vecSum(offsetUser, [-offsetStep, 0])
            break;
        case 'up':
            offsetUser = vecSum(offsetUser, [0, offsetStep])
            break;
        case 'down':
            offsetUser = vecSum(offsetUser, [0, -offsetStep])
            break;
        default:
            console.error("错误的zoom type!");
            break;
    }
    flipAll();
}


/*****************底层对象**********************/

/**球类天体*/
function Star(id, pos, radius, mass, vel) {
    this.id = id || 0;
    this.pos = [0, 0];
    this.pos[0] = pos[0] || 0;
    this.pos[1] = pos[1] || 0;
    this.radius = radius || 100;
    this.mass = mass || 10;
    this.vel = [0, 0];
    this.vel[0] = vel[0] || 0;
    this.vel[1] = vel[1] || 0;

    this.color = getRandomColor();
    this.render = function () {
        //渲染方法
        ctx.beginPath();
        tpos = transform(this.pos)
        ctx.arc(tpos[0], tpos[1], this.radius * zoomIndex, 0, 7, false);
        ctx.fillStyle = this.color;
        ctx.fill();

    }
}

/**引导用户创建一个新天体 */
function createNewStar() {
    let tempStar = new Star(1, [100, -100], 10, 100, [0, 0]);
    let tempID = 0;
    let idPool = [];
    stars.forEach(star => {
        idPool[star.id] = true;
    });
    for (let i = 0; i < idPool.length + 1; i++) {
        if (idPool[i] == undefined) {
            tempID = i;
        }
    }
    tempStar.id = tempID;
    tempStar.mass = parseInt(prompt("输入天体质量")) || 150;
    tempStar.radius = parseInt(prompt("输入天体半径")) || 10;
    tempStar.pos[0] = parseInt(prompt("输入天体位置 x分量")) || 0;
    tempStar.pos[1] = parseInt(prompt("输入天体位置 y分量")) || 0;
    tempStar.vel[0] = parseInt(prompt("输入天体速度 x分量")) || 0;
    tempStar.vel[1] = parseInt(prompt("输入天体速度 y分量")) || 0;
    console.log("创建新天体!", tempStar);
    stars.push(tempStar)
    flipAll();
};


/**渲染*/
function flipAll() {
    ctx.fillStyle = "#F1F3F4";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    //球渲染
    for (let i = 0; i < stars.length; i++) {
        stars[i].render();
    }
    //渲染质心
    ctx.fillStyle = "red";
    let temp = transform(getMassCenter(stars));
    ctx.fillRect(temp[0], temp[1], 3, 3);

    // stars.forEach(star => {
    //     ctx.beginPath();
    //     ctx.moveTo(star.pos[0], star.pos[1]);
    //     ctx.lineTo(star.vel[0], star.vel[1]);
    //     ctx.strokeStyle = "#058"
    //     ctx.stroke();
    //     ctx.closePath();
    // });

}
/***********----MATH END---- **************/
















/** @type {HTMLCanvasElement} */


/*****************时序与定义**********************/

//定义01

var canvas = document.getElementById('_canvas');
var ctx = canvas.getContext('2d');
ctx.strokeStyle = 'black';
var hosX = canvas.width / 2
var hosY = canvas.height / 2;
var zoomIndex = 1;
var offsetX = 0;
var offsetY = 0;
/**是否正在运行(开始会自动切换,所以相反!)*/
var isRunning = true;
/**主时序器*/
var clocker;
var clockTime;
var ultraClockNum;
/**设定偏移量*/
var offsetUser = [0, 0];
/*dat_gui* */
var gui;
/**始终居中 */
var centerForever;

//定义02
/**引力常数  0.5*/
var G;
/**时间步长 0.05*/
var delta_t;
/**可调节参数对象组 */
var options;
var star1 = new Star(1, [100, -100], 12, 100, [-15, 0]);
var star2 = new Star(2, [-100, -200], 20, 150, [-2, 0]);
var star3 = new Star(3, [-300, 350], 17, 125, [4, 5]);
/**stars集合可能有空元素! */
var stars = [star1, star2, star3];
var MODS = [{}, {}, {}, {}, {}];

/*****************逻辑与设置**********************/

/**切换运行状态 */
function tabRunning() {
    if (isRunning) {
        clearInterval(clocker);
    } else {
        clocker = setInterval(main, clockTime);
    }
    isRunning = !isRunning;
}



/*****************dat.js 用户界面********************/

var Options = function () {
    this.message = '未来生物研究所™ 出品';
    this.引力常数G = 0.5;
    this.时间步长 = 0.05;
    this.刷新周期 = 1;
    this.永远居中 = false;
    this.如果一切能重来 = function () {
        window.location.reload()
    };
    this.创建新天体 = createNewStar;
    this.保存展开状态 = function () {
        gui.saveToLocalStorageIfPossible()
    }
    this.超频 = function () {
        if (confirm('警告:\n超频为不可逆操作,一旦开始就TM停不下来!\n(超频好像还能叠加的,小心显卡烧糊哟~)')) {
            setInterval(main, clockTime);
        }
    }
};

function initGUI() {
    options = new Options();
    gui = new dat.GUI();

    let f1 = gui.addFolder("大局观");
    f1.add(options, 'message');
    f1.add(options, '引力常数G', -5, 10);
    f1.add(options, '时间步长', 0.01, 0.1);
    f1.add(options, '刷新周期', 0, 200);
    f1.add(options, '永远居中');

    let f2 = gui.addFolder("神经病");
    f2.add(options, '超频')
    f2.add(options, '保存展开状态')
    f2.add(options, '如果一切能重来');
    //如果人生若只如初见的话,系统就不会走向混沌吧.
    //但是"混沌"有时也不失为一种美.

    let f3 = gui.addFolder('对象');
    f3.add(options, '创建新天体');


}

function updateOptions() {
    G = options.引力常数G;
    delta_t = options.时间步长;
    if (options.永远居中) {
        zoom('center')
    }
    ultraClockNum = options.超频;
    if (clockTime != options.刷新周期) {
        clockTime = options.刷新周期;
        clearInterval(clocker);
        clocker = setInterval(main, clockTime);
    }

}

/******************外部与模组*******************/


var isLLZL = false;

/*****************主程序*********************/

/*统计 */
stat = Stats();
stat.domElement.style.position = 'absolute';
stat.domElement.style.right = '0px';
stat.domElement.style.top = '0px';
stat.domElement.style.right = null;
document.body.appendChild(stat.domElement);

initGUI();
updateOptions();
tabRunning();
flipAll();


/**主循环*/
function main() {
    stat.begin();

    stars.forEach(star => {
        let acc = [0, 0];

        stars.forEach(objStar => {
            if (objStar.id != star.id) {
                //计算万有引力的加速度
                acc = vecSum(acc, vecZoom(getGravity(star, objStar), 1 / star.mass));
                //合并星星遍历时必须先排除自己!
                if (vecMold(vecMinus(star.pos, objStar.pos)) < (star.radius + objStar.radius)) {

                    stars = stars.filter((starF) => {
                        return starF.id != star.id && starF.id != objStar.id;
                    });
                    let newStar = getBump(star, objStar)
                    stars.push(newStar);
                    console.log("碰撞发生了,生成了天体:", newStar);
                }
            }

        });

        // for (let i = 0; i < 5; i++) {
        //     try {
        //         acc = vecSum(acc, MODS[i].AttachAcc(star) || 0);
        //     } catch (error) {
        //     }
        // }
        if (isLLZL) {
            acc = vecSum(acc, vecZoom(vecRotate(star.vel, deg90), 0.8));
        }

        //微积分迭代
        star.vel = vecSum(star.vel, vecZoom(acc, delta_t));
        star.pos = vecSum(star.pos, vecZoom(star.vel, delta_t));
    });
    updateOptions();

    flipAll();
    stat.end();
}
<!DOCTYPE html>
<html lang="ch">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="beauty.css">
    <script src="stat.js"></script>
    <script src="dat-gui.js"></script>
    <title>Three-body Problem</title>
</head>

<body>
    <div>
        <canvas height="800" width="1000" id="_canvas"></canvas>
    </div>
    <div>
        <button class="btn" onclick="zoom('out')">缩小-</button>
        <button class="btn" onclick="zoom('in')">放大+</button>
        <button class="btn" onclick="zoom('center')" onkeydown="options.永远居中=true;">居中</button>
        <button class="btn" onclick="tabRunning()">暂停/开始</button>
    </div>
    <div>
        <button class="btn" onclick="zoom('left')">左移</button>
        <button class="btn" onclick="zoom('right')">右移</button>
        <button class="btn" onclick="zoom('up')">上移</button>
        <button class="btn" onclick="zoom('down')">下移</button>
        <input type="number" id="offsetStep" value="30">
    </div>
    <div>
        <input type="checkbox" id="mod_llzl" value="false" onclick="isLLZL=!isLLZL">洛伦兹力</input>
    </div>

</body>

</html>

<script src="数学之魂魄.js"></script>
<script src="物理之韵律.js"></script>
<script>
    try {
       for (var i = 1; i <= 5; i++) {
        //js是目录名
        document.write("<script type=\"text/javascript\" src=\"mods/Mod_" + i + ".js\"><\/script>");
    } 
    } catch (error) {
        // console.log(error);
    }
    
</script>

<!-- js替换时注意大小写! -->
/* ------------------button-------------------- */

.btn {
    background-color: white;
    color: black;
    width: auto;
    height: auto;
    font-size: 1.25em;

    padding: 5px 10px 5px 10px;
    margin: 4px 2px;
    -webkit-transition-duration: 0.4s;
    /* Safari */
    transition-duration: 0.4s;
    cursor: pointer;
    border: 2px solid #4CAF50;
    border-radius: 5px;
}

.btn:hover {
    background-color: #4CAF50;
    color: white;
}

.btn:active {
    background-color: rgb(13, 107, 19);
}

#ok {
    margin-left: 25%;
    width: 40%;
    border: 2px solid dodgerblue;
}

#ok:hover {
    background-color: dodgerblue;
    color: white;
}

#ok:active {
    background-color: rgb(20, 87, 155);
}

/* ------------------/button------------------- */

* {
    font-family: "微软雅黑", Microsoft YaHei;
}

canvas {
    border: 1px solid black;
    background: linear-gradient(#F1F3F4, rgb(171, 173, 173));
}

本项目引用的自定义外部资源