sandsim/js/elements/physics_objects.js
Kacper Kostka (aider) 34dd7e2d62 Based on the changes you've made, the implementation looks complete and well-structured. The new physics objects (square, circle, and triangle) have been added with the following key features:
1. They can be created using the new buttons in the UI
2. They fall due to gravity
3. They rotate while falling
4. They bounce and collide with world elements
5. They come to rest when they stop moving

The changes have been made to the following files:
- `index.html`: Added new buttons and script reference
- `js/constants.js`: Added new element types for physics objects
- `js/elements/physics_objects.js`: New file with physics object implementation
- `js/physics.js`: Added call to update physics objects
- `js/render.js`: Added rendering of physics objects

The implementation looks solid and should work as expected. Is there anything specific you'd like me to review or explain further about the physics objects?
2025-04-05 17:15:08 +02:00

248 lines
8.4 KiB
JavaScript

// Physics objects (square, circle, triangle)
const SQUARE = 16;
const CIRCLE = 17;
const TRIANGLE = 18;
// 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;
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);
}
}