2021-06-16
三体运动模拟器
三体运动没有解析解,这种遵循物理定律的混沌运动很适合用来做屏保,如:
placeholder

# 质点

将星球运动简化为质点,有二维坐标及速度向量,最后加上其质量:
00// point-static.ts
01export interface PointStatic {
02  /** 坐标 x */
03  x: number;
04  /** 坐标 y */
05  y: number;
06  /** 速度向量 x */
07  dx: number;
08  /** 速度向量 y */
09  dy: number;
10  /** 质量 */
11  m: number;
12}

# 速度对坐标, 引力对速度

速度对坐标的影响比较容易处理将上面的定义的 PointStatic 结构进行 (p.x += p.dx) 即可, 而引力对速度的影响, 则要计算加速度 (f = ma) 以及展开后在二维上的分量进行累加才行
这里有个情况需要注意,因为是质点,所以距离近的时候力会瞬间增大到无穷导致 number 溢出或者除 0 出现的 NaN, 所以要做最小距离处理
具体实现如下
00// point.ts
01import { PointStatic } from './point-static';
02
03export class Point implements PointStatic {
04  public x!: number;
05  public y!: number;
06  public dx!: number;
07  public dy!: number;
08  public m!: number;
09
10  public constructor(data: PointStatic) {
11    Object.assign(this, data);
12  }
13
14  /** 当前速度对坐标的影响 */
15  public move() {
16    this.x += this.dx;
17    this.y += this.dy;
18  }
19
20  /**
21   * p 和 this 间的引力对 this 速度影响
22   * @param p 其他点
23   * @param g 引力常数
24   */
25  public effect(p: Point, g: number) {
26    const r = this.distance(p);
27
28    const distanceX = p.x - this.x;
29    const distanceY = p.y - this.y;
30
31    // f = ma 展开后可以得到 a = g * p.m / (r * r)
32    // 这个 a 是带方向的,它的方向跟 this -> p 练成的线的方向是一致的
33    // 根据这个来做 x y 方向的分量拆解得到 k 最后得到各分量 ax ay
34    const k = g * p.m / (r * r * r);
35    const ax = k * distanceX;
36    const ay = k * distanceY;
37
38    this.dx += ax;
39    this.dy += ay;
40  }
41
42  /** 求与 p1 的距离 */
43  private distance(p1: Point): number {
44    const y = this.y - p1.y;
45    const x = this.x - p1.x;
46    const yy = y * y;
47    const xx = x * x;
48    const rr = yy + xx;
49    // 最小距离
50    const min = 100;
51    if (rr <= (min * min)) return 100;
52    const r = Math.sqrt(rr);
53    return r;
54  }
55
56  /** 根据 m 给一个合适的半径 方便绘图 */
57  public getPointRadius(): number {
58    const r = Math.sqrt(this.m);
59    if (r <= 4) return 2; // 最小值 免得太小了看不到了
60    if (r >= 24) return 12; // 免得太大了
61    return r / 2;
62  }
63}

# 绘图控制 & 多质点

00// gravity.tsx
01import { PointStatic } from './point-static';
02import { Point } from './point';
03
04export interface GravityOptions {
05  points: PointStatic[]; // 各个点的实例
06  g?: number; // 引力常数
07}
08
09export class Gravity {
10  public $dom!: HTMLCanvasElement;
11  public c2d!: CanvasRenderingContext2D;
12  public g: number;
13  public points: Point[] = [];
14  public timer: ReturnType<typeof requestAnimationFrame> | null = null;
15  public drawLine = false;
16
17  public constructor(opts: GravityOptions) {
18    const { points, g } = opts;
19    this.g = g || 1;
20    this.initFromPoints(points);
21  }
22
23  public initFromPoints(points: PointStatic[]) {
24    this.points = points.map(p => new Point(p));
25  }
26
27  public addPoint(p: PointStatic) {
28    this.points.push(new Point(p));
29  }
30
31  public setDom($dom: HTMLCanvasElement) {
32    this.$dom = $dom;
33    const c2d = $dom.getContext('2d')!;
34    if (!c2d) throw new Error('no 2d context');
35    this.c2d = c2d;
36  }
37
38  public updatePointsInfo() {
39    this.points.forEach(p => {
40      this.points.forEach(otherP => {
41        if (otherP === p) return; // 排除自己
42        p.effect(otherP, this.g);
43      });
44    });
45
46    this.points.forEach(p => {
47      p.move();
48    });
49  }
50
51  public drawPoints() {
52    this.c2d.clearRect(0, 0, this.$dom.width, this.$dom.height);
53    this.c2d.fillStyle = 'rgba(77, 77, 77, 1)';
54    this.points.forEach(p => {
55      this.c2d.beginPath();
56      const r = p.getPointRadius();
57      this.c2d.arc(p.x, p.y, r, 0, Math.PI * 2);
58      this.c2d.fill();
59    });
60
61    if (this.drawLine) {
62      this.points.forEach((p, idx) => {
63        this.c2d.lineTo(p.x, p.y);
64      });
65
66      this.c2d.lineTo(this.points[0].x, this.points[0].y);
67      this.c2d.fillStyle = 'rgba(77, 77, 77, 0.3)';
68      this.c2d.fill();
69    }
70  }
71
72  public toggleDrawLine() {
73    this.drawLine = !this.drawLine;
74  }
75
76  public start = () => {
77    if (this.timer) return;
78    this.render();
79  };
80
81  public stop = () => {
82    if (!this.timer) return;
83    cancelAnimationFrame(this.timer);
84    this.timer = null;
85  };
86
87  public render = () => {
88    this.updatePointsInfo();
89    this.drawPoints();
90    this.timer = requestAnimationFrame(this.render);
91  };
92};

# playground

调整参数控制, 建议保持动量和为 0, 不然会跑到画布之外
placeholder




回到顶部