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

View File

@ -25,21 +25,29 @@
</style> </style>
</head> </head>
<body> <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="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"> <script type="module">
import '../src/standalone/folk-shape.ts'; 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) { function handleCollision(e) {
geometryElements.forEach((el) => { for (const shape of shapes) {
if (el !== e.target && collisionDetection(el.getClientRect(), e.target.getClientRect())) { if (shape === e.target) continue;
e.preventDefault();
} 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); 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 ( return (
rect1.left - rect2.right < proximity && rect1.left - rect2.right < proximity &&
rect2.left - rect1.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 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 { FolkHull } from './folk-hull';
import { FolkShape } from './folk-shape.ts'; import { FolkShape } from './folk-shape.ts';
@ -41,7 +41,7 @@ export class FolkCluster extends FolkHull {
isElementInProximity(element: FolkShape) { isElementInProximity(element: FolkShape) {
for (const el of this.sourceElements as 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; return false;
} }
@ -164,7 +164,7 @@ export class FolkProximity extends HTMLElement {
for (const geometry of this.#geometries) { for (const geometry of this.#geometries) {
if (geometry === el) break; if (geometry === el) break;
if (collisionDetection(geometry.getClientRect(), el.getClientRect(), PROXIMITY)) { if (aabbIntersection(geometry.getTransformDOMRect(), el.getTransformDOMRect(), PROXIMITY)) {
const cluster = document.createElement('folk-cluster'); const cluster = document.createElement('folk-cluster');
cluster.addElements(geometry, el); cluster.addElements(geometry, el);
this.#clusters.add(cluster); this.#clusters.add(cluster);
@ -175,7 +175,7 @@ export class FolkProximity extends HTMLElement {
} else { } else {
const isInCluster = (cluster.sourceElements as FolkShape[]) const isInCluster = (cluster.sourceElements as FolkShape[])
.filter((element) => el !== element) .filter((element) => el !== element)
.some((element) => collisionDetection(el.getClientRect(), element.getClientRect(), PROXIMITY)); .some((element) => aabbIntersection(el.getTransformDOMRect(), element.getTransformDOMRect(), PROXIMITY));
if (!isInCluster) { if (!isInCluster) {
cluster.removeElement(el); cluster.removeElement(el);

View File

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