more fun collisions
This commit is contained in:
parent
46a6af6a11
commit
4af3cf270b
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue