feat(physics): migrate marble physics engine to wasm

This commit is contained in:
2025-12-13 01:09:29 +08:00
parent f768ac7827
commit a32ef907ad
8 changed files with 451 additions and 355 deletions

View File

@@ -4,9 +4,10 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"build:wasm": "cd src/wasm/marble-physics && ~/.cargo/bin/wasm-pack build --target web",
"dev": "npm run build:wasm && astro dev",
"start": "astro dev",
"build": "astro build",
"build": "npm run build:wasm && astro build",
"preview": "astro preview",
"format": "biome format --write .",
"lint": "biome lint --write .",
@@ -20,4 +21,4 @@
"devDependencies": {
"@biomejs/biome": "2.3.8"
}
}
}

View File

@@ -1,241 +0,0 @@
# 弹珠系统重构指南
## 项目结构
```
src/
├── config/
│ └── marbleConfig.ts # 配置文件:尺寸、速度、物理参数等
├── data/
│ └── users.ts # 数据层:用户数据 API 获取
├── utils/
│ ├── mouseInteraction.ts # 鼠标交互系统
│ ├── marblePhysics.ts # 物理引擎
│ ├── marbleFactory.ts # 弹珠工厂
│ ├── animationLoop.ts # 动画循环管理器
│ ├── marbleSystem.ts # 核心系统管理器
│ └── index.ts # 统一导出
└── components/
└── MainView.astro # 使用封装后的系统
```
## 1. 系统模块化
将原本集中在 `MainView.astro` 中的 ~260 行内联逻辑拆分为多个独立的、可复用的 TypeScript 模块:
### 核心模块
- **`marbleSystem.ts`**: 核心管理器,整合所有子系统,提供统一的 API
- 管理弹珠集合
- 协调各子系统工作
- 提供添加/删除弹珠、启动/停止动画等方法
- 处理窗口大小变化和缩放功能
- **`marbleFactory.ts`**: 弹珠工厂负责弹珠的创建、DOM 生成和异步加载
- 异步加载头像图片
- 创建弹珠 DOM 节点wrapper + anchor + label
- 生成随机初始位置和速度
- 支持批量创建(使用 `Promise.allSettled`
- 响应式计算弹珠大小
- 支持缩放级别调整
- **`marblePhysics.ts`**: 物理引擎,处理弹珠的运动、碰撞和边界检测
- 更新弹珠位置(应用速度、阻尼)
- 弹珠间的碰撞检测和响应
- 边界碰撞处理
- 速度限制(最小/最大速度)
- DOM 渲染transform
- **`mouseInteraction.ts`**: 鼠标交互系统,实现对弹珠的吸引和排斥力场
- 追踪鼠标位置和 Shift 键状态
- 判断是否应用力场(距离、移动状态)
- 应用吸引/排斥力(对数衰减曲线)
- 悬停在弹珠上时禁用力场(允许点击链接)
- **`animationLoop.ts`**: 独立的动画循环,采用固定时间步长以保证物理模拟的稳定性
- 固定时间步长Fixed Timestep+ 累加器
- 防止长时间暂停导致的时间跳跃
- 提供启动/停止/暂停/恢复方法
## 2. 配置与数据分离
### 配置层 (`src/config/marbleConfig.ts`)
集中管理所有弹珠系统配置:
```typescript
export const MARBLE_CONFIG = {
size: {
base: 192, // 基础大小
min: 96, // 最小大小
maxScreenRatio: 0.25, // 最大屏幕占比
},
speed: {
min: 60, // 最小初始速度
max: 150, // 最大初始速度
},
physics: {
massScale: 0.01, // 质量计算比例
massOffset: 1, // 质量偏移
damping: 0.9985, // 空气阻力系数
restitution: 0.92, // 碰撞恢复系数
wallBounce: 0.85, // 墙壁反弹系数
minSpeed: 50, // 最小速度阈值
maxSpeed: 800, // 最大速度限制
},
animation: {
fadeInDelay: 100, // 淡入延迟
fixedDeltaTime: 1 / 60, // 固定时间步长
maxFrameTime: 0.1, // 最大帧时间
},
mouseInteraction: {
attractRadius: 500, // 吸引范围
repelRadius: 300, // 排斥范围
repelForce: 400, // 排斥力度
attractForce: 600, // 吸引力度
},
};
```
### 数据层 (`src/data/users.ts`)
将用户数据获取逻辑从组件中分离:
```typescript
export async function fetchUsers(): Promise<UserEntry[]> {
// 从 API 获取用户数据
// 包含错误处理
}
```
## 3. 功能保持与优化
### 保持的原有功能
- ✅ 弹珠碰撞检测和响应
- ✅ 边界反弹
- ✅ 从 API 加载用户数据
- ✅ 缩放功能zoom in/out
- ✅ 动画循环
- ✅ 响应式弹珠大小
### 优化改进
**鼠标交互**
- 按住 `Shift` 键切换为吸引模式(默认排斥)
- 力场仅在鼠标移动时激活300ms 超时)
- 悬停在弹珠上时禁用力场(方便点击链接)
- 力场强度采用对数衰减曲线(更平滑)
- 窗口失焦/页面隐藏时自动重置状态
**物理引擎**
- 引入阻尼damping模拟空气阻力
- 恢复系数restitution控制碰撞弹性
- 墙壁反弹系数wallBounce独立控制
- 最小速度限制(防止完全静止)
- 最大速度限制(防止速度过快)
- 帧率独立的物理计算
**弹珠创建**
- 使用 `Promise.allSettled` 批量创建
- 部分头像加载失败不影响其他弹珠
- 添加淡入动画效果
- 支持动态缩放
**动画循环**
- 固定时间步长Fixed Timestep确保物理模拟一致性
- 累加器处理帧率波动
- 最大帧时间限制(防止标签页后台长时间挂起导致的卡顿)
## 4. 代码简化
### 重构前MainView.astro
- ~260 行内联脚本代码
- 所有逻辑混在一起
- 难以维护和测试
### 重构后MainView.astro
- ~60 行简洁代码
- 只负责初始化和连接
- 清晰的职责划分
```typescript
// 重构后的使用示例
import { MarbleSystem } from "../utils/marbleSystem";
import { fetchUsers } from "../data/users";
const field = document.getElementById("marble-field");
const marbleSystem = new MarbleSystem({
container: field,
fieldWidth: window.innerWidth,
fieldHeight: window.innerHeight,
});
marbleSystem.start();
fetchUsers()
.then((users) => marbleSystem.addMarbles(users))
.catch(console.error);
```
## 5. 类型定义
所有模块都有完整的 TypeScript 类型定义:
```typescript
export interface Marble {
id: string;
node: HTMLElement;
x: number;
y: number;
vx: number;
vy: number;
radius: number;
mass: number;
}
export interface UserEntry {
name: string;
id: string;
link?: string;
}
// 更多类型定义...
```
## 6. 统一导出 (`src/utils/index.ts`)
```typescript
export { MarbleSystem } from "./marbleSystem";
export { MarblePhysics } from "./marblePhysics";
export { MouseInteraction } from "./mouseInteraction";
export { MarbleFactory } from "./marbleFactory";
export { AnimationLoop } from "./animationLoop";
export type { Marble } from "./mouseInteraction";
export type { PhysicsConfig } from "./marblePhysics";
export type { MouseInteractionConfig } from "./mouseInteraction";
export type { UpdateCallback } from "./animationLoop";
export type { MarbleSystemConfig } from "./marbleSystem";
```
## 7. 优势总结
**模块化**:清晰的职责分离,每个模块专注一个功能
**可维护性**:代码结构清晰,易于理解和修改
**可复用性**:封装后的系统可在其他项目中使用
**可测试性**:独立的模块便于单元测试
**类型安全**:完整的 TypeScript 类型定义
**配置集中**:所有配置参数统一管理
**功能完整**:保持所有原有功能的同时进行了优化
**代码精简**:组件代码减少 ~75%

View File

@@ -1,149 +1,132 @@
// Marble physics system: Handles marble movement, collision detection, and boundary processing
import type { Marble } from "./mouseInteraction";
// @ts-ignore
import init, { PhysicsWorld, PhysicsConfig as WasmPhysicsConfig } from "../wasm/marble-physics/pkg/marble_physics.js";
export interface PhysicsConfig {
fieldWidth: number;
fieldHeight: number;
damping?: number; // Air resistance coefficient
restitution?: number; // Collision restitution coefficient
wallBounce?: number; // Wall bounce coefficient
minSpeed?: number; // Minimum speed
maxSpeed?: number; // Maximum speed
damping?: number;
restitution?: number;
wallBounce?: number;
minSpeed?: number;
maxSpeed?: number;
}
export class MarblePhysics {
private config: PhysicsConfig;
private world: PhysicsWorld | null = null;
private wasmMemory: WebAssembly.Memory | null = null;
private isReady = false;
constructor(config: PhysicsConfig) {
this.config = {
fieldWidth: config.fieldWidth,
fieldHeight: config.fieldHeight,
damping: config.damping ?? 0.9985,
restitution: config.restitution ?? 0.92,
wallBounce: config.wallBounce ?? 0.85,
minSpeed: config.minSpeed ?? 30,
maxSpeed: config.maxSpeed ?? 800,
};
this.config = config;
}
public async init(): Promise<void> {
const wasm = await init();
this.wasmMemory = wasm.memory;
const canvasWidth = this.config.fieldWidth;
const canvasHeight = this.config.fieldHeight;
const wasmConfig = WasmPhysicsConfig.new(
canvasWidth,
canvasHeight,
this.config.damping ?? 0.9985,
this.config.restitution ?? 0.92,
this.config.wallBounce ?? 0.85,
this.config.minSpeed ?? 30,
this.config.maxSpeed ?? 800
);
this.world = PhysicsWorld.new(wasmConfig);
this.isReady = true;
}
// Update marble positions
public updatePositions(marbles: Marble[], dt: number): void {
const { damping, minSpeed, maxSpeed } = this.config;
if (!this.world || !this.isReady || !this.wasmMemory) return;
// Sync JS marbles to WASM (only if needed, e.g. added/removed or modified externally)
// For now, simpler approach: Re-create world content if count mismatch
// Optimization: Maintain a mapping or only add new ones.
// BUT since current `marbleSystem` manages the array, we need to be careful.
// If we want O(N^2) in WASM, WASM MUST have all marbles.
// Check if we need to sync state FROM JS TO WASM (e.g. mouse interaction changed velocity)
// Or if we need to sync structure.
// To keep it performant and simple for this migration:
// We will assume WASM has the authoritative state for position/velocity.
// However, MouseInteraction modifies JS objects. We need to copy JS -> WASM before step.
// And WASM -> JS after step.
// 1. Sync JS -> WASM
// This copy is O(N).
// If we have a lot of marbles, this might be slow, but much faster than N^2 physics.
// But wait, allocating/deallocating in WASM every frame is bad.
// We should maintain the marbles in WASM.
// `marbleSystem` adds marbles one by one.
// Let's change the pattern:
// `updatePositions` implies moving them.
// `handleCollisions` implies colliding them.
// We can do `world.clear_marbles()` and `world.add_marble(...)` every frame?
// 500 marbles * call overhead. Might be okay for simple WASM.
this.world.clear_marbles();
for (const m of marbles) {
// Apply air resistance
if (damping !== undefined && damping < 1) {
const dampingFactor = damping ** (dt * 60); // Frame rate independent
m.vx *= dampingFactor;
m.vy *= dampingFactor;
}
this.world.add_marble(m.x, m.y, m.vx, m.vy, m.radius, m.mass);
}
// Calculate current speed
const speed = Math.hypot(m.vx, m.vy);
// 2. Step
this.world.step(dt);
// Maintain minimum speed (prevent complete stop)
if (minSpeed !== undefined && speed > 0 && speed < minSpeed) {
const scale = minSpeed / speed;
m.vx *= scale;
m.vy *= scale;
}
// 3. Sync WASM -> JS
const ptr = this.world.get_marbles_ptr();
const len = this.world.get_marbles_len();
const float64Array = new Float64Array(this.wasmMemory.buffer, ptr, len * 6);
// Limit maximum speed
if (maxSpeed !== undefined && speed > maxSpeed) {
const scale = maxSpeed / speed;
m.vx *= scale;
m.vy *= scale;
}
// Update position
m.x += m.vx * dt;
m.y += m.vy * dt;
for (let i = 0; i < len; i++) {
const base = i * 6;
const m = marbles[i]; // strict order assumption
m.x = float64Array[base + 0];
m.y = float64Array[base + 1];
m.vx = float64Array[base + 2];
m.vy = float64Array[base + 3];
// radius and mass shouldn't change in physics step
}
}
// Handle collisions between marbles
public handleCollisions(marbles: Marble[]): void {
const restitution = this.config.restitution ?? 1;
for (let i = 0; i < marbles.length; i++) {
for (let j = i + 1; j < marbles.length; j++) {
const a = marbles[i];
const b = marbles[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 0.001;
const minDist = a.radius + b.radius;
if (dist < minDist) {
const nx = dx / dist;
const ny = dy / dist;
const relativeVelocity = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
if (relativeVelocity < 0) {
// Calculate impulse using restitution coefficient
const impulse =
((1 + restitution) * relativeVelocity) / (a.mass + b.mass);
a.vx -= impulse * b.mass * nx;
a.vy -= impulse * b.mass * ny;
b.vx += impulse * a.mass * nx;
b.vy += impulse * a.mass * ny;
}
// Position correction to prevent overlap
const overlap = minDist - dist + 0.5;
a.x -= nx * overlap * 0.5;
a.y -= ny * overlap * 0.5;
b.x += nx * overlap * 0.5;
b.y += ny * overlap * 0.5;
}
}
}
// Handled in updatePositions (WASM step covers both)
}
// Handle boundary collisions
public handleBoundaries(marbles: Marble[]): void {
const { fieldWidth, fieldHeight, wallBounce } = this.config;
const bounce = wallBounce ?? 1;
for (const m of marbles) {
// Left boundary
if (m.x - m.radius < 0) {
m.x = m.radius;
if (m.vx < 0) m.vx *= -bounce;
}
// Right boundary
if (m.x + m.radius > fieldWidth) {
m.x = fieldWidth - m.radius;
if (m.vx > 0) m.vx *= -bounce;
}
// Top boundary
if (m.y - m.radius < 0) {
m.y = m.radius;
if (m.vy < 0) m.vy *= -bounce;
}
// Bottom boundary
if (m.y + m.radius > fieldHeight) {
m.y = fieldHeight - m.radius;
if (m.vy > 0) m.vy *= -bounce;
}
}
// Handled in updatePositions (WASM step covers both)
}
// Render marbles to DOM
public render(marbles: Marble[]): void {
for (const m of marbles) {
m.node.style.transform = `translate(${m.x - m.radius}px, ${m.y - m.radius}px)`;
}
}
// Update configuration
public updateConfig(config: Partial<PhysicsConfig>): void {
this.config = { ...this.config, ...config };
}
// Get current configuration
public getConfig(): PhysicsConfig {
return { ...this.config };
if (this.world) {
// Need to create new config object for WASM
const wasmConfig = WasmPhysicsConfig.new(
this.config.fieldWidth,
this.config.fieldHeight,
this.config.damping ?? 0.9985,
this.config.restitution ?? 0.92,
this.config.wallBounce ?? 0.85,
this.config.minSpeed ?? 30,
this.config.maxSpeed ?? 800
);
this.world.update_config(wasmConfig);
}
}
}

View File

@@ -67,6 +67,7 @@ export class MarbleSystem {
minSpeed: MARBLE_CONFIG.physics.minSpeed,
maxSpeed: MARBLE_CONFIG.physics.maxSpeed,
});
this.physics.init();
// MarbleFactory Init
this.factory = new MarbleFactory(

2
src/wasm/marble-physics/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
pkg/
target/

147
src/wasm/marble-physics/Cargo.lock generated Normal file
View File

@@ -0,0 +1,147 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "marble-physics"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasm-bindgen"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
]

View File

@@ -0,0 +1,13 @@
[package]
name = "marble-physics"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
console_error_panic_hook = "0.1.7"

View File

@@ -0,0 +1,190 @@
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct Marble {
pub x: f64,
pub y: f64,
pub vx: f64,
pub vy: f64,
pub radius: f64,
pub mass: f64,
}
#[wasm_bindgen]
pub struct PhysicsConfig {
pub field_width: f64,
pub field_height: f64,
pub damping: f64,
pub restitution: f64,
pub wall_bounce: f64,
pub min_speed: f64,
pub max_speed: f64,
}
#[wasm_bindgen]
impl PhysicsConfig {
pub fn new(
field_width: f64,
field_height: f64,
damping: f64,
restitution: f64,
wall_bounce: f64,
min_speed: f64,
max_speed: f64,
) -> PhysicsConfig {
PhysicsConfig {
field_width,
field_height,
damping,
restitution,
wall_bounce,
min_speed,
max_speed,
}
}
}
#[wasm_bindgen]
pub struct PhysicsWorld {
marbles: Vec<Marble>,
config: PhysicsConfig,
}
#[wasm_bindgen]
impl PhysicsWorld {
pub fn new(config: PhysicsConfig) -> PhysicsWorld {
console_error_panic_hook::set_once();
PhysicsWorld {
marbles: Vec::new(),
config,
}
}
pub fn add_marble(&mut self, x: f64, y: f64, vx: f64, vy: f64, radius: f64, mass: f64) {
self.marbles.push(Marble {
x,
y,
vx,
vy,
radius,
mass,
});
}
pub fn clear_marbles(&mut self) {
self.marbles.clear();
}
pub fn update_config(&mut self, config: PhysicsConfig) {
self.config = config;
}
pub fn get_marbles_ptr(&self) -> *const Marble {
self.marbles.as_ptr()
}
pub fn get_marbles_len(&self) -> usize {
self.marbles.len()
}
pub fn step(&mut self, dt: f64) {
let damping_factor = if self.config.damping < 1.0 {
self.config.damping.powf(dt * 60.0)
} else {
1.0
};
// 1. Update positions and apply forces
for marble in self.marbles.iter_mut() {
// Damping
if damping_factor != 1.0 {
marble.vx *= damping_factor;
marble.vy *= damping_factor;
}
let speed = (marble.vx * marble.vx + marble.vy * marble.vy).sqrt();
// Min speed
if speed > 0.0 && speed < self.config.min_speed {
let scale = self.config.min_speed / speed;
marble.vx *= scale;
marble.vy *= scale;
}
// Max speed
if speed > self.config.max_speed {
let scale = self.config.max_speed / speed;
marble.vx *= scale;
marble.vy *= scale;
}
marble.x += marble.vx * dt;
marble.y += marble.vy * dt;
}
// 2. Collisions (O(N^2)) - moved to Rust for speed
// To avoid borrowing issues, we use indices
let len = self.marbles.len();
for i in 0..len {
for j in (i + 1)..len {
let (a, b) = {
let (left, right) = self.marbles.split_at_mut(j);
(&mut left[i], &mut right[0])
};
let dx = b.x - a.x;
let dy = b.y - a.y;
let dist_sq = dx * dx + dy * dy;
let min_dist = a.radius + b.radius;
if dist_sq < min_dist * min_dist {
let dist = dist_sq.sqrt().max(0.001);
let nx = dx / dist;
let ny = dy / dist;
let relative_velocity = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
if relative_velocity < 0.0 {
let restitution = self.config.restitution;
let impulse = ((1.0 + restitution) * relative_velocity) / (a.mass + b.mass);
a.vx -= impulse * b.mass * nx;
a.vy -= impulse * b.mass * ny;
b.vx += impulse * a.mass * nx;
b.vy += impulse * a.mass * ny;
}
// Position correction
let overlap = min_dist - dist + 0.5;
a.x -= nx * overlap * 0.5;
a.y -= ny * overlap * 0.5;
b.x += nx * overlap * 0.5;
b.y += ny * overlap * 0.5;
}
}
}
// 3. Boundaries
for marble in self.marbles.iter_mut() {
if marble.x - marble.radius < 0.0 {
marble.x = marble.radius;
if marble.vx < 0.0 { marble.vx *= -self.config.wall_bounce; }
}
if marble.x + marble.radius > self.config.field_width {
marble.x = self.config.field_width - marble.radius;
if marble.vx > 0.0 { marble.vx *= -self.config.wall_bounce; }
}
if marble.y - marble.radius < 0.0 {
marble.y = marble.radius;
if marble.vy < 0.0 { marble.vy *= -self.config.wall_bounce; }
}
if marble.y + marble.radius > self.config.field_height {
marble.y = self.config.field_height - marble.radius;
if marble.vy > 0.0 { marble.vy *= -self.config.wall_bounce; }
}
}
}
}