三体运动没有解析解,这种遵循物理定律的混沌运动很适合用来做屏保,如:
# 质点 ↵
将星球运动简化为质点,有二维坐标及速度向量,最后加上其质量:
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, 不然会跑到画布之外