/**
* @fileoverview Advanced physics engine for particle simulations
* @module PhysicsEngine
*/
/**
* Vector2D utility class
*/
export class Vector2D {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
/**
* Add another vector
* @param {Vector2D} vector - Vector to add
* @returns {Vector2D} New vector
*/
add(vector) {
return new Vector2D(this.x + vector.x, this.y + vector.y);
}
/**
* Subtract another vector
* @param {Vector2D} vector - Vector to subtract
* @returns {Vector2D} New vector
*/
subtract(vector) {
return new Vector2D(this.x - vector.x, this.y - vector.y);
}
/**
* Multiply by scalar
* @param {number} scalar - Scalar value
* @returns {Vector2D} New vector
*/
multiply(scalar) {
return new Vector2D(this.x * scalar, this.y * scalar);
}
/**
* Divide by scalar
* @param {number} scalar - Scalar value
* @returns {Vector2D} New vector
*/
divide(scalar) {
return new Vector2D(this.x / scalar, this.y / scalar);
}
/**
* Get magnitude
* @returns {number} Magnitude
*/
magnitude() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
/**
* Normalize vector
* @returns {Vector2D} Normalized vector
*/
normalize() {
const mag = this.magnitude();
return mag > 0 ? this.divide(mag) : new Vector2D(0, 0);
}
/**
* Get distance to another vector
* @param {Vector2D} vector - Other vector
* @returns {number} Distance
*/
distanceTo(vector) {
return this.subtract(vector).magnitude();
}
/**
* Dot product
* @param {Vector2D} vector - Other vector
* @returns {number} Dot product
*/
dot(vector) {
return this.x * vector.x + this.y * vector.y;
}
/**
* Cross product (2D returns scalar)
* @param {Vector2D} vector - Other vector
* @returns {number} Cross product
*/
cross(vector) {
return this.x * vector.y - this.y * vector.x;
}
/**
* Rotate vector
* @param {number} angle - Angle in radians
* @returns {Vector2D} Rotated vector
*/
rotate(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return new Vector2D(
this.x * cos - this.y * sin,
this.x * sin + this.y * cos
);
}
/**
* Clone vector
* @returns {Vector2D} Cloned vector
*/
clone() {
return new Vector2D(this.x, this.y);
}
}
/**
* Physics particle class
*/
export class PhysicsParticle {
constructor(x, y, mass = 1) {
this.position = new Vector2D(x, y);
this.velocity = new Vector2D(0, 0);
this.acceleration = new Vector2D(0, 0);
this.force = new Vector2D(0, 0);
this.mass = mass;
this.radius = Math.sqrt(mass) * 2;
this.restitution = 0.8; // Bounce factor
this.friction = 0.98; // Air resistance
this.damping = 0.99; // Velocity damping
this.color = '#ffffff';
this.alpha = 1.0;
this.lifespan = Infinity;
this.age = 0;
this.fixed = false; // Immovable particle
this.active = true;
// Trail effect
this.trail = [];
this.maxTrailLength = 10;
}
/**
* Apply force to particle
* @param {Vector2D} force - Force vector
*/
applyForce(force) {
this.force = this.force.add(force);
}
/**
* Update particle physics
* @param {number} deltaTime - Time step
*/
update(deltaTime) {
if (!this.active || this.fixed) return;
// Update age
this.age += deltaTime;
// Check lifespan
if (this.age > this.lifespan) {
this.active = false;
return;
}
// Calculate acceleration from force (F = ma)
this.acceleration = this.force.divide(this.mass);
// Update velocity
this.velocity = this.velocity.add(this.acceleration.multiply(deltaTime));
// Apply friction and damping
this.velocity = this.velocity.multiply(this.friction * this.damping);
// Update position
const deltaPosition = this.velocity.multiply(deltaTime);
this.position = this.position.add(deltaPosition);
// Update trail
this.updateTrail();
// Reset force for next frame
this.force = new Vector2D(0, 0);
}
/**
* Update particle trail
*/
updateTrail() {
this.trail.push(this.position.clone());
if (this.trail.length > this.maxTrailLength) {
this.trail.shift();
}
}
/**
* Check collision with another particle
* @param {PhysicsParticle} other - Other particle
* @returns {boolean} Collision detected
*/
collidesWith(other) {
const distance = this.position.distanceTo(other.position);
return distance < (this.radius + other.radius);
}
/**
* Resolve collision with another particle
* @param {PhysicsParticle} other - Other particle
*/
resolveCollision(other) {
const distance = this.position.distanceTo(other.position);
const minDistance = this.radius + other.radius;
if (distance >= minDistance) return;
// Calculate collision normal
const normal = other.position.subtract(this.position).normalize();
// Separate particles
const overlap = minDistance - distance;
const separation = normal.multiply(overlap * 0.5);
if (!this.fixed) this.position = this.position.subtract(separation);
if (!other.fixed) other.position = other.position.add(separation);
// Calculate relative velocity
const relativeVelocity = other.velocity.subtract(this.velocity);
const velocityAlongNormal = relativeVelocity.dot(normal);
// Don't resolve if velocities are separating
if (velocityAlongNormal > 0) return;
// Calculate restitution
const e = Math.min(this.restitution, other.restitution);
// Calculate impulse scalar
let j = -(1 + e) * velocityAlongNormal;
j /= 1 / this.mass + 1 / other.mass;
// Apply impulse
const impulse = normal.multiply(j);
if (!this.fixed) {
this.velocity = this.velocity.subtract(impulse.divide(this.mass));
}
if (!other.fixed) {
other.velocity = other.velocity.add(impulse.divide(other.mass));
}
}
/**
* Apply gravitational force from another particle
* @param {PhysicsParticle} other - Other particle
* @param {number} G - Gravitational constant
*/
applyGravity(other, G = 0.1) {
const direction = other.position.subtract(this.position);
const distance = direction.magnitude();
if (distance === 0) return;
const force = G * this.mass * other.mass / (distance * distance);
const forceVector = direction.normalize().multiply(force);
this.applyForce(forceVector);
}
/**
* Bounce off boundaries
* @param {number} width - Boundary width
* @param {number} height - Boundary height
*/
bounceOffBoundaries(width, height) {
if (this.position.x - this.radius < 0) {
this.position.x = this.radius;
this.velocity.x *= -this.restitution;
} else if (this.position.x + this.radius > width) {
this.position.x = width - this.radius;
this.velocity.x *= -this.restitution;
}
if (this.position.y - this.radius < 0) {
this.position.y = this.radius;
this.velocity.y *= -this.restitution;
} else if (this.position.y + this.radius > height) {
this.position.y = height - this.radius;
this.velocity.y *= -this.restitution;
}
}
/**
* Wrap around boundaries
* @param {number} width - Boundary width
* @param {number} height - Boundary height
*/
wrapAroundBoundaries(width, height) {
if (this.position.x < -this.radius) {
this.position.x = width + this.radius;
} else if (this.position.x > width + this.radius) {
this.position.x = -this.radius;
}
if (this.position.y < -this.radius) {
this.position.y = height + this.radius;
} else if (this.position.y > height + this.radius) {
this.position.y = -this.radius;
}
}
}
/**
* Force generators for physics system
*/
export class ForceGenerator {
/**
* Gravity force
* @param {number} strength - Gravity strength
* @returns {Vector2D} Gravity force
*/
static gravity(strength = 9.81) {
return new Vector2D(0, strength);
}
/**
* Wind force
* @param {number} strength - Wind strength
* @param {number} direction - Wind direction in radians
* @returns {Vector2D} Wind force
*/
static wind(strength = 1, direction = 0) {
return new Vector2D(
Math.cos(direction) * strength,
Math.sin(direction) * strength
);
}
/**
* Magnetic field force
* @param {Vector2D} fieldCenter - Center of magnetic field
* @param {Vector2D} particlePos - Particle position
* @param {number} strength - Field strength
* @returns {Vector2D} Magnetic force
*/
static magneticField(fieldCenter, particlePos, strength = 1) {
const direction = fieldCenter.subtract(particlePos);
const distance = direction.magnitude();
if (distance === 0) return new Vector2D(0, 0);
const force = strength / (distance * distance);
return direction.normalize().multiply(force);
}
/**
* Turbulence force
* @param {Vector2D} position - Particle position
* @param {number} strength - Turbulence strength
* @param {number} time - Current time
* @returns {Vector2D} Turbulence force
*/
static turbulence(position, strength = 1, time = 0) {
const x = Math.sin(position.x * 0.01 + time * 0.001) * strength;
const y = Math.cos(position.y * 0.01 + time * 0.001) * strength;
return new Vector2D(x, y);
}
/**
* Vortex force
* @param {Vector2D} center - Vortex center
* @param {Vector2D} particlePos - Particle position
* @param {number} strength - Vortex strength
* @returns {Vector2D} Vortex force
*/
static vortex(center, particlePos, strength = 1) {
const direction = particlePos.subtract(center);
const distance = direction.magnitude();
if (distance === 0) return new Vector2D(0, 0);
const force = strength / distance;
return direction.rotate(Math.PI / 2).normalize().multiply(force);
}
/**
* Spring force
* @param {Vector2D} anchor - Spring anchor point
* @param {Vector2D} particlePos - Particle position
* @param {number} restLength - Spring rest length
* @param {number} stiffness - Spring stiffness
* @returns {Vector2D} Spring force
*/
static spring(anchor, particlePos, restLength = 0, stiffness = 0.1) {
const direction = anchor.subtract(particlePos);
const distance = direction.magnitude();
const extension = distance - restLength;
return direction.normalize().multiply(extension * stiffness);
}
/**
* Electromagnetic force
* @param {PhysicsParticle} particle1 - First particle
* @param {PhysicsParticle} particle2 - Second particle
* @param {number} k - Coulomb's constant
* @returns {Vector2D} Electromagnetic force
*/
static electromagnetic(particle1, particle2, k = 1) {
const direction = particle2.position.subtract(particle1.position);
const distance = direction.magnitude();
if (distance === 0) return new Vector2D(0, 0);
// Assume particles have charge proportional to mass
const force = k * particle1.mass * particle2.mass / (distance * distance);
return direction.normalize().multiply(force);
}
}
/**
* Advanced physics engine
*/
export class PhysicsEngine {
constructor() {
this.particles = [];
this.constraints = [];
this.forces = [];
this.bounds = { width: 800, height: 600 };
this.boundaryMode = 'bounce'; // 'bounce', 'wrap', 'none'
this.gravity = new Vector2D(0, 0);
this.globalFriction = 1.0;
this.timeStep = 1/60;
this.spatialGrid = new SpatialGrid(50); // For collision optimization
}
/**
* Add particle to simulation
* @param {PhysicsParticle} particle - Particle to add
*/
addParticle(particle) {
this.particles.push(particle);
}
/**
* Remove particle from simulation
* @param {PhysicsParticle} particle - Particle to remove
*/
removeParticle(particle) {
const index = this.particles.indexOf(particle);
if (index > -1) {
this.particles.splice(index, 1);
}
}
/**
* Add global force
* @param {Function} forceFunction - Function that returns force vector
*/
addForce(forceFunction) {
this.forces.push(forceFunction);
}
/**
* Set boundary behavior
* @param {string} mode - Boundary mode ('bounce', 'wrap', 'none')
*/
setBoundaryMode(mode) {
this.boundaryMode = mode;
}
/**
* Set simulation boundaries
* @param {number} width - Boundary width
* @param {number} height - Boundary height
*/
setBounds(width, height) {
this.bounds.width = width;
this.bounds.height = height;
}
/**
* Update physics simulation
* @param {number} deltaTime - Time step
*/
update(deltaTime = this.timeStep) {
// Apply global forces
this.applyGlobalForces();
// Update particle positions
this.updateParticles(deltaTime);
// Handle collisions
this.handleCollisions();
// Apply boundary conditions
this.applyBoundaryConditions();
// Remove inactive particles
this.removeInactiveParticles();
}
/**
* Apply global forces to all particles
*/
applyGlobalForces() {
this.particles.forEach(particle => {
if (!particle.active || particle.fixed) return;
// Apply gravity
particle.applyForce(this.gravity.multiply(particle.mass));
// Apply custom forces
this.forces.forEach(forceFunction => {
const force = forceFunction(particle);
if (force) particle.applyForce(force);
});
});
}
/**
* Update all particles
* @param {number} deltaTime - Time step
*/
updateParticles(deltaTime) {
this.particles.forEach(particle => {
particle.update(deltaTime);
});
}
/**
* Handle particle collisions
*/
handleCollisions() {
// Use spatial grid for optimization
this.spatialGrid.clear();
this.particles.forEach(particle => {
this.spatialGrid.insert(particle);
});
// Check collisions only for nearby particles
this.particles.forEach(particle => {
const nearby = this.spatialGrid.getNearby(particle);
nearby.forEach(other => {
if (particle !== other && particle.collidesWith(other)) {
particle.resolveCollision(other);
}
});
});
}
/**
* Apply boundary conditions
*/
applyBoundaryConditions() {
this.particles.forEach(particle => {
if (!particle.active) return;
switch (this.boundaryMode) {
case 'bounce':
particle.bounceOffBoundaries(this.bounds.width, this.bounds.height);
break;
case 'wrap':
particle.wrapAroundBoundaries(this.bounds.width, this.bounds.height);
break;
// 'none' - particles can move freely outside bounds
}
});
}
/**
* Remove inactive particles
*/
removeInactiveParticles() {
this.particles = this.particles.filter(particle => particle.active);
}
/**
* Create particle explosion effect
* @param {Vector2D} center - Explosion center
* @param {number} particleCount - Number of particles
* @param {number} force - Explosion force
*/
createExplosion(center, particleCount = 20, force = 100) {
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI * 2;
const velocity = new Vector2D(
Math.cos(angle) * force,
Math.sin(angle) * force
);
const particle = new PhysicsParticle(center.x, center.y, Math.random() * 2 + 1);
particle.velocity = velocity;
particle.lifespan = Math.random() * 3000 + 2000;
particle.color = `hsl(${Math.random() * 60 + 15}, 100%, 60%)`; // Orange/red colors
this.addParticle(particle);
}
}
/**
* Create particle fountain effect
* @param {Vector2D} source - Fountain source
* @param {number} particlesPerFrame - Particles spawned per frame
*/
createFountain(source, particlesPerFrame = 3) {
for (let i = 0; i < particlesPerFrame; i++) {
const angle = -Math.PI/2 + (Math.random() - 0.5) * 0.5; // Upward with spread
const speed = Math.random() * 50 + 30;
const velocity = new Vector2D(
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
const particle = new PhysicsParticle(
source.x + (Math.random() - 0.5) * 10,
source.y,
Math.random() + 0.5
);
particle.velocity = velocity;
particle.lifespan = Math.random() * 4000 + 3000;
particle.color = `hsl(${200 + Math.random() * 60}, 70%, 60%)`; // Blue colors
this.addParticle(particle);
}
}
/**
* Get all particles
* @returns {Array} Array of particles
*/
getParticles() {
return this.particles;
}
/**
* Clear all particles
*/
clear() {
this.particles = [];
}
/**
* Get simulation statistics
* @returns {Object} Simulation stats
*/
getStats() {
return {
particleCount: this.particles.length,
activeParticles: this.particles.filter(p => p.active).length,
boundaryMode: this.boundaryMode,
bounds: this.bounds
};
}
}
/**
* Spatial grid for collision optimization
*/
class SpatialGrid {
constructor(cellSize) {
this.cellSize = cellSize;
this.grid = new Map();
}
/**
* Clear the grid
*/
clear() {
this.grid.clear();
}
/**
* Insert particle into grid
* @param {PhysicsParticle} particle - Particle to insert
*/
insert(particle) {
const cellX = Math.floor(particle.position.x / this.cellSize);
const cellY = Math.floor(particle.position.y / this.cellSize);
const key = `${cellX},${cellY}`;
if (!this.grid.has(key)) {
this.grid.set(key, []);
}
this.grid.get(key).push(particle);
}
/**
* Get nearby particles
* @param {PhysicsParticle} particle - Reference particle
* @returns {Array} Nearby particles
*/
getNearby(particle) {
const cellX = Math.floor(particle.position.x / this.cellSize);
const cellY = Math.floor(particle.position.y / this.cellSize);
const nearby = [];
// Check surrounding cells
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const key = `${cellX + dx},${cellY + dy}`;
if (this.grid.has(key)) {
nearby.push(...this.grid.get(key));
}
}
}
return nearby;
}
}
// Export singleton instance
export const physicsEngine = new PhysicsEngine();
Source