UNPKG

@dill-pixel/plugin-snap-physics

Version:

Snap Physics

334 lines (290 loc) 10.5 kB
import { Circle, Point, Rectangle } from 'pixi.js'; import { Entity } from './Entity'; import { ICollider } from './ICollider'; import { System } from './System'; import { Collision, CollisionDirection } from './types'; export function checkPointIntersection(point: Point, collider: ICollider): boolean { return point.x > collider.left && point.x < collider.right && point.y > collider.top && point.y < collider.bottom; } type Overlap = { x: number; y: number; area: number; }; const EPSILON = 1e-10; export function getRectToRectIntersectionArea(rectA: Rectangle, rectB: Rectangle): Overlap { const xOverlap = Math.max(0, Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left)); const yOverlap = Math.max(0, Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top)); const area = xOverlap * yOverlap; return { x: xOverlap, y: yOverlap, area }; } export function getRectToCircleIntersectionArea(rect: Rectangle, circle: Circle): Overlap { const closestX = Math.max(rect.x, Math.min(rect.x + rect.width, circle.x)); const closestY = Math.max(rect.y, Math.min(rect.y + rect.height, circle.y)); const dx = circle.x - closestX; const dy = circle.y - closestY; const distanceSquared = dx * dx + dy * dy; const distance = Math.sqrt(distanceSquared); const angle = Math.acos(distance / circle.radius); const sectorArea = angle * circle.radius * circle.radius; const triangleArea = distance * Math.sqrt(circle.radius * circle.radius - distanceSquared); const intersectionArea = sectorArea - triangleArea; return { x: 0, y: 0, area: Math.max(0, intersectionArea) }; } export function getCircleToCircleIntersectionArea(circleA: Circle, circleB: Circle): Overlap { const dx = circleB.x - circleA.x; const dy = circleB.y - circleA.y; const distanceSquared = dx * dx + dy * dy; const distance = Math.sqrt(distanceSquared); const r1 = circleA.radius; const r2 = circleB.radius; const radiiSum = r1 + r2; // No overlap if the distance is greater than or equal to the sum of the radii if (distance >= radiiSum - EPSILON) { return { x: 0, y: 0, area: 0 }; } // One circle is completely within the other if (distance <= Math.abs(r1 - r2) + EPSILON) { const smallerRadius = Math.min(r1, r2); const area = Math.PI * smallerRadius * smallerRadius; return { x: circleA.x, y: circleA.y, area }; } // Calculate intersection area const a = (r1 * r1 - r2 * r2 + distanceSquared) / (2 * distance); const h = Math.sqrt(r1 * r1 - a * a); const area = r1 * r1 * Math.acos(a / r1) + r2 * r2 * Math.acos((distance - a) / r2) - distance * h; const cx = circleA.x + (a * dx) / distance; const cy = circleA.y + (a * dy) / distance; return { x: cx, y: cy, area: Math.max(0, area) }; } export function checkCollision( shapeA: Rectangle | Circle, shapeB: Rectangle | Circle, entity1: Entity, entity2: Entity, ): Collision | false { const collision: Collision = { type: `${entity1.type}|${entity2.type}`, entity1, entity2, top: 0, bottom: 0, left: 0, right: 0, area: 0, direction: undefined, overlap: { x: 0, y: 0 }, }; let hasCollision = false; if (entity1.isCircle && entity2.isCircle) { const circleA = shapeA as Circle; const circleB = shapeB as Circle; const dx = circleB.x - circleA.x; const dy = circleB.y - circleA.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < circleA.radius + circleB.radius) { hasCollision = true; circleToCircleCollision(circleA, circleB, collision); } } else if (entity1.isCircle !== entity2.isCircle) { // One shape is a circle, the other is a rectangle const circle = entity1.isCircle ? (shapeA as Circle) : (shapeB as Circle); const rect = entity1.isCircle ? (shapeB as Rectangle) : (shapeA as Rectangle); // Find the closest point on the rectangle to the circle's center const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height)); const dx = circle.x - closestX; const dy = circle.y - closestY; const distanceSquared = dx * dx + dy * dy; if (distanceSquared <= circle.radius * circle.radius) { hasCollision = true; rectToCircleCollision(rect, circle, closestX, closestY, collision); } } else { // Both shapes are rectangles const rectA = shapeA as Rectangle; const rectB = shapeB as Rectangle; if ( rectA.x < rectB.x + rectB.width && rectA.x + rectA.width > rectB.x && rectA.y < rectB.y + rectB.height && rectA.y + rectA.height > rectB.y ) { hasCollision = true; rectToRectCollision(rectA, rectB, collision); } } if (hasCollision && collision.area > System.collisionThreshold) { collision.direction = getCollisionDirection(collision); return collision; } return false; } function getCollisionDirection(collision: Collision): CollisionDirection | undefined { // get the max value of all the collision sides let dir: CollisionDirection | undefined = undefined; let value = 0; if (collision.top > value) { value = collision.top; dir = 'top'; } if (collision.bottom > value) { value = collision.bottom; dir = 'bottom'; } if (collision.left > value) { value = collision.left; dir = 'left'; } if (collision.right > value) { value = collision.right; dir = 'right'; } return dir; } function rectToCircleCollision( rect: Rectangle, circle: Circle, closestX: number, closestY: number, collision: Collision, ) { const dx = circle.x - closestX; const dy = circle.y - closestY; const distanceSquared = dx * dx + dy * dy; if (distanceSquared >= circle.radius * circle.radius - EPSILON) { // No intersection return { x: 0, y: 0, area: 0 }; } // Circle center is inside the rectangle if (circle.x >= rect.x && circle.x <= rect.x + rect.width && circle.y >= rect.y && circle.y <= rect.y + rect.height) { return { x: circle.x, y: circle.y, area: Math.PI * circle.radius * circle.radius }; } // Partial intersection const distance = Math.sqrt(distanceSquared); const angle = Math.acos(distance / circle.radius); const sectorArea = angle * circle.radius * circle.radius; const triangleArea = distance * Math.sqrt(circle.radius * circle.radius - distanceSquared); const intersectionArea = sectorArea - triangleArea; collision.overlap = { x: closestX, y: closestY }; collision.area = Math.max(0, intersectionArea); if (distance < circle.radius) { // Reset collision sides collision.top = 0; collision.bottom = 0; collision.left = 0; collision.right = 0; // Set collision sides based on overlap distances and circle's position relative to the rectangle if (dx > 0) { collision.left = Math.abs(dx); } else { collision.right = Math.abs(dx); } if (dy > 0) { collision.top = Math.abs(dy); } else { collision.bottom = Math.abs(dy); } } } function circleToCircleCollision(circleA: Circle, circleB: Circle, collision: Collision) { const dx = circleB.x - circleA.x; const dy = circleB.y - circleA.y; const distanceSquared = dx * dx + dy * dy; const distance = Math.sqrt(distanceSquared); const r1 = circleA.radius; const r2 = circleB.radius; const radiiSum = r1 + r2; // Early exit if no collision if (distance >= radiiSum) { return; } // Calculate penetration depth const penetration = radiiSum - distance; // Calculate normalized collision normal const nx = dx / distance; const ny = dy / distance; // Set collision sides based on the normal vector // We use a larger threshold for circle collisions to ensure proper reflection if (Math.abs(nx) > 0.1) { if (nx > 0) { collision.left = penetration; } else { collision.right = penetration; } } if (Math.abs(ny) > 0.1) { if (ny > 0) { collision.top = penetration; } else { collision.bottom = penetration; } } // Calculate intersection area (for collision strength) if (distance <= Math.abs(r1 - r2)) { // One circle contains the other const smallerRadius = Math.min(r1, r2); collision.area = Math.PI * smallerRadius * smallerRadius; } else { // Partial intersection const a = (r1 * r1 - r2 * r2 + distanceSquared) / (2 * distance); const h = Math.sqrt(r1 * r1 - a * a); collision.area = r1 * r1 * Math.acos(a / r1) + r2 * r2 * Math.acos((distance - a) / r2) - distance * h; } // Set overlap point at the collision point collision.overlap = { x: circleA.x + nx * r1, y: circleA.y + ny * r1, }; } function rectToRectCollision(r1: Rectangle, r2: Rectangle, collision: Collision) { const dx = r2.x - r1.x; const dy = r2.y - r1.y; const r1HalfWidth = r1.width / 2; const r1HalfHeight = r1.height / 2; const r2HalfWidth = r2.width / 2; const r2HalfHeight = r2.height / 2; const r1CenterX = r1.x + r1HalfWidth; const r1CenterY = r1.y + r1HalfHeight; const r2CenterX = r2.x + r2HalfWidth; const r2CenterY = r2.y + r2HalfHeight; const intersectX = Math.abs(r2CenterX - r1CenterX) - (r1HalfWidth + r2HalfWidth); const intersectY = Math.abs(r2CenterY - r1CenterY) - (r1HalfHeight + r2HalfHeight); // Calculate the coordinates of the intersection rectangle collision.overlap.x = Math.max(0, Math.min(r1.x + r1.width, r2.x + r2.width) - Math.max(r1.x, r2.x)); collision.overlap.y = Math.max(0, Math.min(r1.y + r1.height, r2.y + r2.height) - Math.max(r1.y, r2.y)); collision.area = collision.overlap.x * collision.overlap.y; if (intersectX < 0 && intersectY < 0) { const dx = r2CenterX - r1CenterX; const dy = r2CenterY - r1CenterY; if (dy > 0) { collision.bottom = Math.abs(dy); } else { collision.top = Math.abs(dy); } if (dx > 0) { collision.right = Math.abs(dx); } else { collision.left = Math.abs(dx); } } else { if (dx > 0) { collision.right = Math.abs(dx); } else { collision.left = Math.abs(dx); } if (dy > 0) { collision.bottom = Math.abs(dy); } else { collision.top = Math.abs(dy); } } } export function approach(current: number, target: number, step: number): number { if (current < target) { return Math.min(current + step, target); } else if (current > target) { return Math.max(current - step, target); } return current; }