perf: optimize marble physics via spatial grid and reduce CSS rendering overhead

This commit is contained in:
2025-12-13 10:27:39 +08:00
parent f768ac7827
commit aed9f7a92f
2 changed files with 118 additions and 47 deletions

View File

@@ -140,15 +140,13 @@ import Particles from "./Particles.astro";
border: 1px solid rgba(255, 255, 255, 0.18);
background: linear-gradient(
145deg,
rgba(255, 255, 255, 0.06),
rgba(255, 255, 255, 0.03)
rgba(255, 255, 255, 0.12),
rgba(255, 255, 255, 0.04)
);
backdrop-filter: blur(16px);
backdrop-filter: blur(6px);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08),
0 30px 80px rgba(0, 0, 0, 0.35),
0 0 60px rgba(255, 127, 209, 0.28),
0 0 40px rgba(124, 251, 255, 0.24);
0 10px 40px rgba(0, 0, 0, 0.5);
display: inline-block;
width: auto;
max-width: 80vw;
@@ -241,13 +239,12 @@ import Particles from "./Particles.astro";
background-size: cover;
background-position: center;
cursor: pointer;
margin: 0;
box-shadow:
inset -3px -5px 10px rgba(0, 0, 0, 0.5),
inset 2px 4px 6px rgba(255, 255, 255, 0.35),
0 0 14px rgba(255, 255, 255, 0.45),
0 0 4px rgba(255, 255, 255, 0.6),
0 8px 20px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
inset -2px -2px 6px rgba(0, 0, 0, 0.6),
inset 2px 2px 4px rgba(255, 255, 255, 0.4),
0 2px 8px rgba(0, 0, 0, 0.4);
/* backdrop-filter: blur(4px); -- Removed for performance */
overflow: hidden;
transform: translate3d(0, 0, 0);
will-change: transform;
@@ -271,11 +268,9 @@ import Particles from "./Particles.astro";
:global(.marble:hover) {
transform: scale(1.15) translateY(-2px);
box-shadow:
inset -3px -5px 12px rgba(0, 0, 0, 0.4),
inset 2px 4px 8px rgba(255, 255, 255, 0.45),
0 0 25px rgba(255, 255, 255, 0.65),
0 0 8px rgba(255, 255, 255, 0.8),
0 15px 35px rgba(0, 0, 0, 0.45);
inset -2px -2px 6px rgba(0, 0, 0, 0.5),
inset 2px 2px 6px rgba(255, 255, 255, 0.5),
0 4px 12px rgba(0, 0, 0, 0.5);
filter: brightness(1.05);
}
@@ -292,8 +287,8 @@ import Particles from "./Particles.astro";
font-weight: 600;
letter-spacing: 0.02em;
text-align: center;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(6px);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
/* backdrop-filter: blur(6px); -- Removed for performance */
pointer-events: none;
}

View File

@@ -62,40 +62,116 @@ export class MarblePhysics {
}
}
// Handle collisions between marbles
// Handle collisions between marbles using Spatial Grid
public handleCollisions(marbles: Marble[]): void {
const restitution = this.config.restitution ?? 1;
const { fieldWidth, fieldHeight } = this.config;
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;
// 1. Determine grid cell size
// Using the maximum diameter of any marble ensures we only need to check adjacent cells.
// If marbles can vary wildly in size, this might need tuning, but max diameter is safe.
let maxDiameter = 0;
for (const m of marbles) {
if (m.radius * 2 > maxDiameter) maxDiameter = m.radius * 2;
}
// Fallback if no marbles or something goes wrong
if (maxDiameter === 0) return;
if (dist < minDist) {
const nx = dx / dist;
const ny = dy / dist;
const relativeVelocity = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
const cellSize = maxDiameter;
const gridWidth = Math.ceil(fieldWidth / cellSize);
const gridHeight = Math.ceil(fieldHeight / cellSize);
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;
// 2. Build the grid
// Map: cellIndex -> Particle[]
const grid = new Map<number, Marble[]>();
const getGridIndex = (x: number, y: number) => {
const gx = Math.floor(x / cellSize);
const gy = Math.floor(y / cellSize);
// Clamp to valid range to handle out-of-bounds marbles gracefully
if (gx < 0 || gx >= gridWidth || gy < 0 || gy >= gridHeight) return -1;
return gx + gy * gridWidth;
};
for (const m of marbles) {
const index = getGridIndex(m.x, m.y);
if (index === -1) continue; // Skip out of bounds marbles (handled by boundaries)
if (!grid.has(index)) {
grid.set(index, []);
}
grid.get(index)?.push(m);
}
// 3. Check collisions (Grid-based)
// We iterate through each marble, find its cell, and check that cell + neighbors
for (const i_marble of marbles) {
const gx = Math.floor(i_marble.x / cellSize);
const gy = Math.floor(i_marble.y / cellSize);
// Check 3x3 neighbors (including own cell)
// Optimization: We could only check "forward" cells to avoid double checks,
// but since we need to resolve for both, and the grid logic is simpler to iterate neighbors:
// Standard way to avoid double checking A vs B and B vs A is to check all neighbors
// and only resolve if ID(A) < ID(B) or similar check.
for (let nx = gx - 1; nx <= gx + 1; nx++) {
for (let ny = gy - 1; ny <= gy + 1; ny++) {
if (nx < 0 || nx >= gridWidth || ny < 0 || ny >= gridHeight) continue;
const neighborIndex = nx + ny * gridWidth;
const cellMarbles = grid.get(neighborIndex);
if (!cellMarbles) continue;
for (const j_marble of cellMarbles) {
// Avoid self-collision
if (i_marble === j_marble) continue;
// Avoid double checking: only check if index(i) < index(j)
// But here we rely on the object content.
// Since marbles is an array, we can check if marbles.indexOf(i) < marbles.indexOf(j)?
// That's O(N) inside loop.
// Instead, let's just do the check and rely on the fact that if we resolve A vs B,
// we might resolve B vs A later.
// Ideally: We iterate unique pairs.
// Optimization: Only check half-neighborhood?
// Or simpler: check all, but only act if i_marble.id < j_marble.id
if (i_marble.id >= j_marble.id) continue;
const a = i_marble;
const b = j_marble;
const dx = b.x - a.x;
const dy = b.y - a.y;
const distSq = dx * dx + dy * dy;
const minDist = a.radius + b.radius;
const minDistSq = minDist * minDist;
if (distSq < minDistSq) {
const dist = Math.sqrt(distSq) || 0.001;
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;
}
}
// 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;
}
}
}