more fun collisions

This commit is contained in:
“chrisshank” 2024-12-06 20:06:53 -08:00
parent 46a6af6a11
commit 4af3cf270b
5 changed files with 103 additions and 51 deletions

View File

@ -62,7 +62,7 @@
<script type="module">
import '../src/standalone/folk-shape.ts';
import './src/record-player.ts';
import { collisionDetection } from '../src/common/collision.ts';
import { aabbIntersection } from '../src/common/collision.ts';
let proximityDistance = 150;
const proximitySet = new Set();
@ -82,9 +82,9 @@
function updateFlowerProximity(flower) {
const alreadyIntersection = proximitySet.has(flower);
// TODO: refactor this hack once resizing and the vertices API are figured out
const isNowIntersecting = collisionDetection(
recordPlayerGeometry.getClientRect(),
flower.getClientRect(),
const isNowIntersecting = aabbIntersection(
recordPlayerGeometry.getTransformDOMRect(),
flower.getTransformDOMRect(),
proximityDistance
);

View File

@ -25,21 +25,29 @@
</style>
</head>
<body>
<folk-shape x="100" y="100" width="50" height="50"> Hello World </folk-shape>
<folk-shape x="100" y="100" width="50" height="50"></folk-shape>
<folk-shape x="200" y="200" width="50" height="50"></folk-shape>
<folk-shape x="200" y="275" width="50" height="50"></folk-shape>
<folk-shape x="200" y="150" width="50" height="50"></folk-shape>
<folk-shape x="200" y="100" width="50" height="50"></folk-shape>
<script type="module">
import '../src/standalone/folk-shape.ts';
import { collisionDetection } from '../src/common/collision.ts';
import { aabbHitDetection } from '../src/common/collision.ts';
const geometryElements = document.querySelectorAll('folk-shape');
const shapes = Array.from(document.querySelectorAll('folk-shape'));
function handleCollision(e) {
geometryElements.forEach((el) => {
if (el !== e.target && collisionDetection(el.getClientRect(), e.target.getClientRect())) {
e.preventDefault();
}
});
for (const shape of shapes) {
if (shape === e.target) continue;
const hit = aabbHitDetection(shape.getTransformDOMRect(), e.target.getTransformDOMRect());
if (hit === null) continue;
shape.x -= hit.delta.x;
shape.y -= hit.delta.y;
}
}
document.addEventListener('transform', handleCollision);

View File

@ -1,4 +1,51 @@
export function collisionDetection(rect1: DOMRect, rect2: DOMRect, proximity = 0) {
import { TransformDOMRect } from './TransformDOMRect';
import { Point } from './types';
const sign = (value: number): -1 | 1 => (value < 0 ? -1 : 1);
export class Hit {
/** The point of contact between the two objects. */
pos: Point = { x: 0, y: 0 };
/** The a vector representing the overlap between the two objects. */
delta: Point = { x: 0, y: 0 };
/** The surface normal at the point of contact. */
normal: Point = { x: 0, y: 0 };
}
/** Test collisions of axis-aligned bounding boxes. */
export function aabbHitDetection(rect1: DOMRect, rect2: DOMRect, proximity = 0): Hit | null {
const dx = rect2.x - rect1.x;
const px = rect2.width / 2 + rect1.width / 2 - Math.abs(dx);
if (px <= 0) return null;
const dy = rect2.y - rect1.y;
const py = rect2.height / 2 + rect1.height / 2 - Math.abs(dy);
if (py <= 0) return null;
const hit = new Hit();
if (px < py) {
const sx = sign(dx);
hit.delta.x = px * sx;
hit.normal.x = sx;
hit.pos.x = rect1.x + (rect1.width / 2) * sx;
hit.pos.y = rect2.y;
} else {
const sy = sign(dy);
hit.delta.y = py * sy;
hit.normal.y = sy;
hit.pos.x = rect2.x;
hit.pos.y = rect1.y + (rect1.height / 2) * sy;
}
return hit;
}
export function aabbIntersection(rect1: DOMRect, rect2: DOMRect, proximity = 0) {
return (
rect1.left - rect2.right < proximity &&
rect2.left - rect1.right < proximity &&
@ -6,3 +53,8 @@ export function collisionDetection(rect1: DOMRect, rect2: DOMRect, proximity = 0
rect2.top - rect1.bottom < proximity
);
}
export function collisionDetection(rect1: TransformDOMRect, rect2: TransformDOMRect) {
// Performance optimization to test if
if (!aabbIntersection(rect1, rect2)) return false;
}

View File

@ -1,4 +1,4 @@
import { collisionDetection } from './common/collision.ts';
import { aabbIntersection } from './common/collision.ts';
import { FolkHull } from './folk-hull';
import { FolkShape } from './folk-shape.ts';
@ -41,7 +41,7 @@ export class FolkCluster extends FolkHull {
isElementInProximity(element: FolkShape) {
for (const el of this.sourceElements as FolkShape[]) {
if (collisionDetection(el.getClientRect(), element.getClientRect(), PROXIMITY)) return true;
if (aabbIntersection(el.getTransformDOMRect(), element.getTransformDOMRect(), PROXIMITY)) return true;
}
return false;
}
@ -164,7 +164,7 @@ export class FolkProximity extends HTMLElement {
for (const geometry of this.#geometries) {
if (geometry === el) break;
if (collisionDetection(geometry.getClientRect(), el.getClientRect(), PROXIMITY)) {
if (aabbIntersection(geometry.getTransformDOMRect(), el.getTransformDOMRect(), PROXIMITY)) {
const cluster = document.createElement('folk-cluster');
cluster.addElements(geometry, el);
this.#clusters.add(cluster);
@ -175,7 +175,7 @@ export class FolkProximity extends HTMLElement {
} else {
const isInCluster = (cluster.sourceElements as FolkShape[])
.filter((element) => el !== element)
.some((element) => collisionDetection(el.getClientRect(), element.getClientRect(), PROXIMITY));
.some((element) => aabbIntersection(el.getTransformDOMRect(), element.getTransformDOMRect(), PROXIMITY));
if (!isInCluster) {
cluster.removeElement(el);

View File

@ -182,8 +182,10 @@ export class FolkShape extends HTMLElement {
}
set x(x) {
if (this.#rect.x === x) return;
this.#previousRect.x = this.#rect.x;
this.#rect.x = x;
this.#requestUpdate('x');
this.#requestUpdate();
}
get y() {
@ -191,8 +193,10 @@ export class FolkShape extends HTMLElement {
}
set y(y) {
if (this.#rect.y === y) return;
this.#previousRect.y = this.#rect.y;
this.#rect.y = y;
this.#requestUpdate('y');
this.#requestUpdate();
}
get width(): number {
@ -203,13 +207,14 @@ export class FolkShape extends HTMLElement {
}
set width(width: Dimension) {
if (this.#attrWidth === width) return;
if (width === 'auto') {
resizeObserver.observe(this, this.#onAutoResize);
} else if (this.#attrWidth === 'auto' && this.#attrHeight !== 'auto') {
resizeObserver.unobserve(this, this.#onAutoResize);
}
this.#attrWidth = width;
this.#requestUpdate('width');
this.#requestUpdate();
}
get height(): number {
@ -220,6 +225,7 @@ export class FolkShape extends HTMLElement {
}
set height(height: Dimension) {
if (this.#attrHeight === height) return;
if (height === 'auto') {
resizeObserver.observe(this, this.#onAutoResize);
} else if (this.#attrHeight === 'auto' && this.#attrWidth !== 'auto') {
@ -227,7 +233,7 @@ export class FolkShape extends HTMLElement {
}
this.#attrHeight = height;
this.#requestUpdate('height');
this.#requestUpdate();
}
get rotation(): number {
@ -235,8 +241,10 @@ export class FolkShape extends HTMLElement {
}
set rotation(rotation: number) {
if (this.#rect.rotation === rotation) return;
this.#previousRect.rotation = this.#rect.rotation;
this.#rect.rotation = rotation;
this.#requestUpdate('rotation');
this.#requestUpdate();
}
#rect: TransformDOMRect;
@ -364,13 +372,11 @@ export class FolkShape extends HTMLElement {
if (event.altKey) {
switch (event.key) {
case 'ArrowLeft':
this.#rect.rotation -= ROTATION_DELTA;
this.#requestUpdate('rotation');
this.rotation -= ROTATION_DELTA;
event.preventDefault();
return;
case 'ArrowRight':
this.#rect.rotation += ROTATION_DELTA;
this.#requestUpdate('rotation');
this.rotation += ROTATION_DELTA;
event.preventDefault();
return;
}
@ -378,23 +384,19 @@ export class FolkShape extends HTMLElement {
switch (event.key) {
case 'ArrowLeft':
this.#rect.x -= MOVEMENT_DELTA;
this.#requestUpdate('x');
this.x -= MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowRight':
this.#rect.x += MOVEMENT_DELTA;
this.#requestUpdate('x');
this.x += MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowUp':
this.#rect.y -= MOVEMENT_DELTA;
this.#requestUpdate('y');
this.y -= MOVEMENT_DELTA;
event.preventDefault();
return;
case 'ArrowDown':
this.#rect.y += MOVEMENT_DELTA;
this.#requestUpdate('y');
this.y += MOVEMENT_DELTA;
event.preventDefault();
return;
}
@ -433,10 +435,8 @@ export class FolkShape extends HTMLElement {
if (target === null) return;
if (target === this) {
this.#rect.x += event.movementX;
this.#rect.y += event.movementY;
this.#requestUpdate('x');
this.#requestUpdate('y');
this.x += event.movementX;
this.y += event.movementY;
return;
}
@ -452,9 +452,9 @@ export class FolkShape extends HTMLElement {
if (handle.startsWith('rotation')) {
const center = this.#rect.center;
const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
this.#rect.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
const rotation = this.#initialRotation + (currentAngle - this.#startAngle);
let degrees = (this.#rect.rotation * 180) / Math.PI;
let degrees = (rotation * 180) / Math.PI;
switch (handle) {
case 'rotation-ne':
degrees = (degrees + 90) % 360;
@ -470,8 +470,7 @@ export class FolkShape extends HTMLElement {
const target = event.composedPath()[0] as HTMLElement;
const rotateCursor = getRotateCursorUrl(degrees);
target.style.setProperty('cursor', rotateCursor);
this.#requestUpdate('rotation');
this.rotation = rotation;
return;
}
@ -495,21 +494,17 @@ export class FolkShape extends HTMLElement {
}
}
#updatedProperties = new Set<string>();
#isUpdating = false;
async #requestUpdate(property: string) {
async #requestUpdate() {
if (!this.#isConnected) return;
this.#updatedProperties.add(property);
if (this.#isUpdating) return;
this.#isUpdating = true;
await true;
this.#isUpdating = false;
this.#update();
this.#updatedProperties.clear();
}
// Any updates that should be batched should happen here like updating the DOM or emitting events should be executed here.
@ -666,9 +661,6 @@ export class FolkShape extends HTMLElement {
}
}
this.#requestUpdate('x');
this.#requestUpdate('y');
this.#requestUpdate('width');
this.#requestUpdate('height');
this.#requestUpdate();
}
}