batch updates and make move events cancellable

This commit is contained in:
“chrisshank” 2024-08-17 20:19:22 -07:00
parent cc0618feda
commit e9ded756a8
2 changed files with 60 additions and 18 deletions

View File

@ -7,4 +7,5 @@ spatial-geometry {
display: block;
position: absolute;
cursor: pointer;
content-visibility: auto;
}

View File

@ -3,12 +3,12 @@ export type Shape = 'rectangle' | 'circle' | 'triangle';
// Can we make adding new shapes extensible via a static property?
const shapes = new Set(['rectangle', 'circle', 'triangle']);
export type Vector = { x: number; y: number };
export type Vector = { x: number; y: number; movementX: number; movementY: number };
// Should the move event bubble?
export class MoveEvent extends CustomEvent<Vector> {
constructor(vector: Vector) {
super('move', { detail: vector, bubbles: false });
super('move', { detail: vector, cancelable: true, bubbles: false });
}
}
@ -40,6 +40,7 @@ export class SpatialGeometry extends HTMLElement {
this.setAttribute('type', type);
}
#previousX = 0;
#x = 0;
get x(): number {
return this.#x;
@ -49,6 +50,7 @@ export class SpatialGeometry extends HTMLElement {
this.setAttribute('x', x.toString());
}
#previousY = 0;
#y = 0;
get y(): number {
return this.#y;
@ -60,23 +62,25 @@ export class SpatialGeometry extends HTMLElement {
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
if (name === 'x') {
this.#previousX = this.#x;
this.#x = Number(newValue);
// In the future, when CSS `attr()` is supported we could define this x/y projection in CSS.
this.style.left = `${this.#x}px`;
this.#emitMoveEvent();
this.#requestUpdate('x');
} else if (name === 'y') {
this.#previousY = 0;
this.#y = Number(newValue);
this.style.top = `${this.#y}px`;
this.#emitMoveEvent();
this.#requestUpdate('y');
} else if (name === 'type') {
if (shapes.has(newValue)) {
this.#type = newValue as Shape;
// TODO: Update shape styles. Ideally we could just use clip-path to style the shape.
// See https://www.smashingmagazine.com/2024/05/modern-guide-making-css-shapes/
this.#requestUpdate('type');
}
}
}
disconnectedCallback() {
cancelAnimationFrame(this.#rAFId);
}
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
// We might also want some kind of utility function that maps a path into an approximate set of vertices.
getBoundingPath(): string {
@ -111,17 +115,54 @@ export class SpatialGeometry extends HTMLElement {
}
}
#isMoveScheduled = false;
#updatedProperties = new Set<string>();
#rAFId = -1;
#isUpdating = false;
// Without some form of changes to x and y will cause separate events to be dispatched.
// Should we only emit a move event every animation frame or with something like `queueMicrotask`?
#emitMoveEvent() {
if (this.#isMoveScheduled) return;
#requestUpdate(property: string) {
this.#updatedProperties.add(property);
this.#isMoveScheduled = true;
requestAnimationFrame(() => {
this.dispatchEvent(new MoveEvent({ x: this.#x, y: this.#y }));
this.#isMoveScheduled = false;
if (this.#isUpdating) return;
this.#isUpdating = true;
this.#rAFId = requestAnimationFrame(() => {
this.#isUpdating = false;
this.#update(this.#updatedProperties);
this.#updatedProperties.clear();
this.#rAFId = -1;
});
}
#update(updatedProperties: Set<string>) {
if (updatedProperties.has('type')) {
// TODO: Update shape styles. Ideally we could just use clip-path to style the shape.
// See https://www.smashingmagazine.com/2024/05/modern-guide-making-css-shapes/
}
if (updatedProperties.has('x') || updatedProperties.has('y')) {
const notCancelled = this.dispatchEvent(
new MoveEvent({
x: this.#x,
y: this.#y,
movementX: this.#x - this.#previousX,
movementY: this.#y - this.#previousY,
})
);
if (notCancelled) {
if (updatedProperties.has('x')) {
// In the future, when CSS `attr()` is supported we could define this x/y projection in CSS.
this.style.left = `${this.#x}px`;
}
if (updatedProperties.has('y')) {
this.style.top = `${this.#y}px`;
}
} else {
// Revert changes to movement
this.#x = this.#previousX;
this.#y = this.#previousY;
}
}
}
}