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?
248 lines
8.4 KiB
JavaScript
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);
|
|
}
|
|
}
|