sandsim/js/elements/physics_objects.js
2025-04-05 18:39:47 +02:00

266 lines
9.0 KiB
JavaScript

// Physics objects (square, circle, triangle)
// Constants are already defined in constants.js
// Physics object properties
const PHYSICS_OBJECT_COLORS = ['#FF5733', '#33FF57', '#3357FF', '#F3FF33', '#FF33F3'];
// Physics constants
const GRAVITY_ACCELERATION = 0.2;
const BOUNCE_FACTOR = 0.7;
const FRICTION = 0.98;
const ROTATION_SPEED = 0.05;
// Store physics objects
const physicsObjects = [];
// Physics object class
class PhysicsObject {
constructor(type, x, y, size) {
this.type = type;
this.x = x;
this.y = y;
this.size = size || 10;
this.vx = 0;
this.vy = 0;
this.rotation = 0;
this.angularVelocity = (Math.random() - 0.5) * ROTATION_SPEED;
this.color = PHYSICS_OBJECT_COLORS[Math.floor(Math.random() * PHYSICS_OBJECT_COLORS.length)];
this.isStatic = false;
this.lastUpdate = performance.now();
}
update() {
const now = performance.now();
const deltaTime = Math.min(50, now - this.lastUpdate); // Cap at 50ms to prevent huge jumps
this.lastUpdate = now;
if (this.isStatic) return false;
// Apply gravity
this.vy += GRAVITY_ACCELERATION;
// Calculate new position
const newX = this.x + this.vx;
const newY = this.y + this.vy;
// Check for collisions with world elements
const collisionResult = this.checkCollisions(newX, newY);
if (collisionResult.collision) {
// Handle collision response
if (collisionResult.horizontal) {
this.vx *= -BOUNCE_FACTOR;
}
if (collisionResult.vertical) {
this.vy *= -BOUNCE_FACTOR;
}
// Apply friction
this.vx *= FRICTION;
this.vy *= FRICTION;
// If object is almost stopped, make it static
if (Math.abs(this.vx) < 0.1 && Math.abs(this.vy) < 0.1 && Math.abs(this.angularVelocity) < 0.01) {
this.isStatic = true;
}
} else {
// No collision, update position
this.x = newX;
this.y = newY;
}
// Update rotation
this.rotation += this.angularVelocity;
this.angularVelocity *= FRICTION;
return true;
}
checkCollisions(newX, newY) {
const result = {
collision: false,
horizontal: false,
vertical: false
};
// Check points around the object based on its type
const checkPoints = this.getCollisionCheckPoints(newX, newY);
for (const point of checkPoints) {
const pixel = getPixel(Math.floor(point.x), Math.floor(point.y));
if (pixel !== EMPTY && pixel !== WATER &&
pixel !== SQUARE && pixel !== CIRCLE && pixel !== TRIANGLE) {
result.collision = true;
// Determine collision direction
if (point.type === 'horizontal') {
result.horizontal = true;
} else if (point.type === 'vertical') {
result.vertical = true;
} else {
// Corner collision, check both directions
result.horizontal = true;
result.vertical = true;
}
}
}
return result;
}
getCollisionCheckPoints(x, y) {
const points = [];
const halfSize = this.size / 2;
if (this.type === SQUARE) {
// For a square, check corners and edges
const corners = [
{ x: x - halfSize, y: y - halfSize },
{ x: x + halfSize, y: y - halfSize },
{ x: x - halfSize, y: y + halfSize },
{ x: x + halfSize, y: y + halfSize }
];
// Add rotated corners
for (const corner of corners) {
const rotatedCorner = this.rotatePoint(corner.x, corner.y, x, y, this.rotation);
points.push({ x: rotatedCorner.x, y: rotatedCorner.y, type: 'corner' });
}
// Add edge midpoints
points.push({ x: x, y: y - halfSize, type: 'vertical' });
points.push({ x: x, y: y + halfSize, type: 'vertical' });
points.push({ x: x - halfSize, y: y, type: 'horizontal' });
points.push({ x: x + halfSize, y: y, type: 'horizontal' });
} else if (this.type === CIRCLE) {
// For a circle, check points around the circumference
const numPoints = 12;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
points.push({
x: x + Math.cos(angle) * halfSize,
y: y + Math.sin(angle) * halfSize,
type: angle < Math.PI / 4 || angle > Math.PI * 7/4 || (angle > Math.PI * 3/4 && angle < Math.PI * 5/4) ? 'horizontal' : 'vertical'
});
}
} else if (this.type === TRIANGLE) {
// For a triangle, check vertices and edges
const vertices = [
{ x: x, y: y - halfSize },
{ x: x - halfSize, y: y + halfSize },
{ x: x + halfSize, y: y + halfSize }
];
// Add rotated vertices
for (const vertex of vertices) {
const rotatedVertex = this.rotatePoint(vertex.x, vertex.y, x, y, this.rotation);
points.push({ x: rotatedVertex.x, y: rotatedVertex.y, type: 'corner' });
}
// Add edge midpoints
const midpoints = [
{ x: (vertices[0].x + vertices[1].x) / 2, y: (vertices[0].y + vertices[1].y) / 2, type: 'edge' },
{ x: (vertices[1].x + vertices[2].x) / 2, y: (vertices[1].y + vertices[2].y) / 2, type: 'horizontal' },
{ x: (vertices[2].x + vertices[0].x) / 2, y: (vertices[2].y + vertices[0].y) / 2, type: 'edge' }
];
for (const midpoint of midpoints) {
const rotatedMidpoint = this.rotatePoint(midpoint.x, midpoint.y, x, y, this.rotation);
points.push({ x: rotatedMidpoint.x, y: rotatedMidpoint.y, type: midpoint.type });
}
}
return points;
}
rotatePoint(px, py, cx, cy, angle) {
const s = Math.sin(angle);
const c = Math.cos(angle);
// Translate point back to origin
px -= cx;
py -= cy;
// Rotate point
const xnew = px * c - py * s;
const ynew = px * s + py * c;
// Translate point back
return {
x: xnew + cx,
y: ynew + cy
};
}
render(ctx, offsetX, offsetY) {
const screenX = (this.x - offsetX) * PIXEL_SIZE;
const screenY = (this.y - offsetY) * PIXEL_SIZE;
ctx.save();
ctx.translate(screenX, screenY);
ctx.rotate(this.rotation);
ctx.fillStyle = this.color;
// Draw collision box in debug mode
if (debugMode) {
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 1;
// Draw a circle for the collision radius
ctx.beginPath();
ctx.arc(0, 0, this.size * PIXEL_SIZE / 2, 0, Math.PI * 2);
ctx.stroke();
// Draw a dot at the center
ctx.fillStyle = '#ffff00';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
// Restore original fill color
ctx.fillStyle = this.color;
}
if (this.type === SQUARE) {
const halfSize = this.size / 2 * PIXEL_SIZE;
ctx.fillRect(-halfSize, -halfSize, this.size * PIXEL_SIZE, this.size * PIXEL_SIZE);
} else if (this.type === CIRCLE) {
ctx.beginPath();
ctx.arc(0, 0, this.size / 2 * PIXEL_SIZE, 0, Math.PI * 2);
ctx.fill();
} else if (this.type === TRIANGLE) {
const halfSize = this.size / 2 * PIXEL_SIZE;
ctx.beginPath();
ctx.moveTo(0, -halfSize);
ctx.lineTo(-halfSize, halfSize);
ctx.lineTo(halfSize, halfSize);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
}
function createPhysicsObject(type, x, y, size) {
const obj = new PhysicsObject(type, x, y, size || 10);
physicsObjects.push(obj);
return obj;
}
function updatePhysicsObjects() {
// Update all physics objects
for (let i = physicsObjects.length - 1; i >= 0; i--) {
physicsObjects[i].update();
}
}
function renderPhysicsObjects(ctx, offsetX, offsetY) {
// Render all physics objects
for (const obj of physicsObjects) {
obj.render(ctx, offsetX, offsetY);
}
}