mirror of
https://github.com/101island/lolisland.us.git
synced 2026-03-01 03:49:42 +08:00
feat(physics): migrate marble physics engine to wasm
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
2
src/wasm/marble-physics/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
pkg/
|
||||
target/
|
||||
147
src/wasm/marble-physics/Cargo.lock
generated
Normal file
147
src/wasm/marble-physics/Cargo.lock
generated
Normal 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",
|
||||
]
|
||||
13
src/wasm/marble-physics/Cargo.toml
Normal file
13
src/wasm/marble-physics/Cargo.toml
Normal 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"
|
||||
190
src/wasm/marble-physics/src/lib.rs
Normal file
190
src/wasm/marble-physics/src/lib.rs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user