shape transform event

This commit is contained in:
“chrisshank” 2024-12-02 17:51:52 -08:00
parent a5b5d390dd
commit 14f78e3871
9 changed files with 99 additions and 107 deletions

View File

@ -46,8 +46,8 @@
<folk-event-propagator
source="#box3"
target="#box4"
triggers="move"
expression="y: from.x,
triggers="transform"
expression="y: from.x,
rotation: from.x"
></folk-event-propagator>

View File

@ -100,7 +100,7 @@
</ul>
<h2>Demos</h2>
<p id="disclaimer">(Make sure to checkout the dev tools, everything you see is authored just in HTML!)</p>
<p id="disclaimer">(Make sure to checkout the dev tools, all of the you see are just authored in HTML!)</p>
<ul id="links">
{{ LINKS }}

View File

@ -126,8 +126,7 @@
}
}
document.addEventListener('move', handleProximity);
document.addEventListener('resize', handleProximity);
document.addEventListener('transform', handleProximity);
document.addEventListener('playing', (e) => {
proximitySet.forEach((el) => el.firstElementChild.play?.());

View File

@ -44,8 +44,7 @@
});
}
document.addEventListener('move', handleCollision);
document.addEventListener('resize', handleCollision);
document.addEventListener('transform', handleCollision);
</script>
</body>
</html>

View File

@ -191,8 +191,7 @@ export class AbstractArrow extends HTMLElement {
if (this.#sourceElement === null) {
throw new Error('source is not a valid element');
} else if (this.#sourceElement instanceof FolkShape) {
this.#sourceElement.addEventListener('resize', this.#sourceHandler);
this.#sourceElement.addEventListener('move', this.#sourceHandler);
this.#sourceElement.addEventListener('transform', this.#sourceHandler);
this.#sourceRect = this.#sourceElement.getBoundingClientRect();
} else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) {
window.addEventListener('message', this.#sourcePostMessage);
@ -212,8 +211,7 @@ export class AbstractArrow extends HTMLElement {
if (this.#sourceElement === null) return;
if (this.#sourceElement instanceof FolkShape) {
this.#sourceElement.removeEventListener('resize', this.#sourceHandler);
this.#sourceElement.removeEventListener('move', this.#sourceHandler);
this.#sourceElement.removeEventListener('transform', this.#sourceHandler);
} else if (this.#sourceElement instanceof HTMLIFrameElement && this.#sourceIframeSelector) {
window.removeEventListener('message', this.#sourcePostMessage);
clientRectObserver.unobserve(this.#sourceElement, this.#sourceIframeCallback);
@ -242,8 +240,7 @@ export class AbstractArrow extends HTMLElement {
if (!this.#targetElement) {
throw new Error('target is not a valid element');
} else if (this.#targetElement instanceof FolkShape) {
this.#targetElement.addEventListener('resize', this.#targetHandler);
this.#targetElement.addEventListener('move', this.#targetHandler);
this.#targetElement.addEventListener('transform', this.#targetHandler);
} else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) {
window.addEventListener('message', this.#targetPostMessage);
clientRectObserver.observe(this.#targetElement, this.#targetIframeCallback);
@ -262,8 +259,7 @@ export class AbstractArrow extends HTMLElement {
if (this.#targetElement === null) return;
if (this.#targetElement instanceof FolkShape) {
this.#targetElement.removeEventListener('resize', this.#targetHandler);
this.#targetElement.removeEventListener('move', this.#targetHandler);
this.#targetElement.removeEventListener('transform', this.#targetHandler);
} else if (this.#targetElement instanceof HTMLIFrameElement && this.#targetIframeSelector) {
window.removeEventListener('message', this.#targetPostMessage);
clientRectObserver.unobserve(this.#targetElement, this.#targetIframeCallback);

View File

@ -81,8 +81,7 @@ if (window.parent !== window) {
observedSelectors.set(element, selector);
if (element instanceof FolkShape) {
element.addEventListener('move', onGeometryChange);
element.addEventListener('resize', onGeometryChange);
element.addEventListener('transform', onGeometryChange);
window.parent.postMessage({
type: 'folk-element-change',
@ -101,8 +100,7 @@ if (window.parent !== window) {
if (element === undefined) return;
if (element instanceof FolkShape) {
element.removeEventListener('move', onGeometryChange);
element.removeEventListener('resize', onGeometryChange);
element.removeEventListener('transform', onGeometryChange);
observedElements.delete(selector);
observedSelectors.delete(element);
} else {

View File

@ -42,18 +42,14 @@ export class DistanceField extends HTMLElement {
window.addEventListener('resize', this.handleResize);
this.shapes.forEach((geometry) => {
geometry.addEventListener('move', this.handleGeometryUpdate);
geometry.addEventListener('resize', this.handleGeometryUpdate);
geometry.addEventListener('rotate', this.handleGeometryUpdate);
geometry.addEventListener('transform', this.handleGeometryUpdate);
});
}
disconnectedCallback() {
window.removeEventListener('resize', this.handleResize);
this.shapes.forEach((geometry) => {
geometry.removeEventListener('move', this.handleGeometryUpdate);
geometry.removeEventListener('resize', this.handleGeometryUpdate);
geometry.removeEventListener('rotate', this.handleGeometryUpdate);
geometry.removeEventListener('transform', this.handleGeometryUpdate);
});
this.cleanupWebGLResources();
}

View File

@ -141,8 +141,7 @@ export class FolkProximity extends HTMLElement {
constructor() {
super();
this.addEventListener('move', this.#handleProximity);
this.addEventListener('resize', this.#handleProximity);
this.addEventListener('transform', this.#handleProximity);
}
#handleProximity = (e: Event) => {

View File

@ -7,27 +7,54 @@ const resizeObserver = new ResizeObserverManager();
export type Shape = 'rectangle' | 'circle' | 'triangle';
export type MoveEventDetail = { movementX: number; movementY: number };
export type TransformEventDetail = {
rotate: number;
};
export class MoveEvent extends CustomEvent<MoveEventDetail> {
constructor(detail: MoveEventDetail) {
super('move', { detail, cancelable: true, bubbles: true });
// TODO: expose previous and current rects
export class TransformEvent extends Event {
constructor() {
super('transform', { cancelable: true, bubbles: true });
}
}
export type ResizeEventDetail = { movementX: number; movementY: number };
export class ResizeEvent extends CustomEvent<MoveEventDetail> {
constructor(detail: MoveEventDetail) {
super('resize', { detail, cancelable: true, bubbles: true });
#xPrevented = false;
get xPrevented() {
return this.defaultPrevented || this.#xPrevented;
}
preventX() {
this.#xPrevented = true;
}
}
export type RotateEventDetail = { rotate: number };
#yPrevented = false;
get yPrevented() {
return this.defaultPrevented || this.#yPrevented;
}
preventY() {
this.#yPrevented = true;
}
export class RotateEvent extends CustomEvent<RotateEventDetail> {
constructor(detail: RotateEventDetail) {
super('rotate', { detail, cancelable: true, bubbles: true });
#heightPrevented = false;
get heightPrevented() {
return this.defaultPrevented || this.#heightPrevented;
}
preventHeight() {
this.#heightPrevented = true;
}
#widthPrevented = false;
get widthPrevented() {
return this.defaultPrevented || this.#widthPrevented;
}
preventWidth() {
this.#widthPrevented = true;
}
#rotatePrevented = false;
get rotatePrevented() {
return this.defaultPrevented || this.#rotatePrevented;
}
preventRotate() {
this.#rotatePrevented = true;
}
}
@ -209,9 +236,9 @@ export class FolkShape extends HTMLElement {
set width(width: Dimension) {
if (width === 'auto') {
resizeObserver.observe(this, this.#onResize);
resizeObserver.observe(this, this.#onAutoResize);
} else if (this.#width === 'auto' && this.#height !== 'auto') {
resizeObserver.unobserve(this, this.#onResize);
resizeObserver.unobserve(this, this.#onAutoResize);
}
this.#previousWidth = this.#width;
this.#width = width;
@ -229,9 +256,9 @@ export class FolkShape extends HTMLElement {
set height(height: Dimension) {
if (height === 'auto') {
resizeObserver.observe(this, this.#onResize);
resizeObserver.observe(this, this.#onAutoResize);
} else if (this.#height === 'auto' && this.#width !== 'auto') {
resizeObserver.unobserve(this, this.#onResize);
resizeObserver.unobserve(this, this.#onAutoResize);
}
this.#previousHeight = this.#height;
@ -280,8 +307,10 @@ export class FolkShape extends HTMLElement {
this.width = Number(this.getAttribute('width')) || 'auto';
}
#isConnected = false;
connectedCallback() {
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation']));
this.#isConnected = true;
}
getClientRect(): RotatedDOMRect {
@ -453,6 +482,8 @@ export class FolkShape extends HTMLElement {
#isUpdating = false;
async #requestUpdate(property: string) {
if (!this.#isConnected) return;
this.#updatedProperties.add(property);
if (this.#isUpdating) return;
@ -473,86 +504,60 @@ export class FolkShape extends HTMLElement {
// See https://www.smashingmagazine.com/2024/05/modern-guide-making-css-shapes/
}
if (updatedProperties.has('x') || updatedProperties.has('y')) {
// Although the change in movement isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
const notCancelled = this.dispatchEvent(
new MoveEvent({
movementX: this.#x - this.#previousX,
movementY: this.#y - this.#previousY,
})
);
this.#dispatchTransformEvent(updatedProperties);
}
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`;
}
#dispatchTransformEvent(updatedProperties: Set<string>) {
const event = new TransformEvent();
if (updatedProperties.has('y')) {
this.style.top = `${this.#y}px`;
}
} else {
this.dispatchEvent(event);
if (updatedProperties.has('x')) {
if (event.xPrevented) {
this.#x = this.#previousX;
this.#y = this.#previousY;
} else {
this.style.left = `${this.#x}px`;
}
}
if (updatedProperties.has('width') || updatedProperties.has('height')) {
// Although the change in resize isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
const notCancelled = this.dispatchEvent(
new ResizeEvent({
movementX: this.width - (this.#previousWidth === 'auto' ? 0 : this.#previousWidth),
movementY: this.height - (this.#previousHeight === 'auto' ? 0 : this.#previousHeight),
})
);
if (notCancelled) {
if (updatedProperties.has('width')) {
this.style.width = this.#width === 'auto' ? '' : `${this.#width}px`;
}
if (updatedProperties.has('height')) {
this.style.height = this.#height === 'auto' ? '' : `${this.#height}px`;
}
if (updatedProperties.has('y')) {
if (event.yPrevented) {
this.#y = this.#previousY;
} else {
// TODO: Revert changes to position too
this.style.top = `${this.#y}px`;
}
}
if (updatedProperties.has('height')) {
if (event.heightPrevented) {
this.#height = this.#previousHeight;
} else {
this.style.height = this.#height === 'auto' ? '' : `${this.#height}px`;
}
}
if (updatedProperties.has('width')) {
if (event.widthPrevented) {
this.#width = this.#previousWidth;
} else {
this.style.width = this.#width === 'auto' ? '' : `${this.#width}px`;
}
}
if (updatedProperties.has('rotation')) {
// Although the change in resize isn't useful inside this component, the outside world might find it helpful to calculate acceleration and other physics
const notCancelled = this.dispatchEvent(new RotateEvent({ rotate: this.#rotation - this.#previousRotation }));
if (notCancelled) {
if (updatedProperties.has('rotation')) {
this.style.rotate = `${this.#rotation}rad`;
}
} else {
if (event.rotatePrevented) {
this.#rotation = this.#previousRotation;
} else {
this.style.rotate = `${this.#rotation}rad`;
}
}
}
#onResize = (entry: ResizeObserverEntry) => {
#onAutoResize = (entry: ResizeObserverEntry) => {
const previousRect = this.#autoContentRect;
this.#autoContentRect = entry.contentRect;
const notCancelled = this.dispatchEvent(
new ResizeEvent({
movementX: this.width - (this.#previousWidth === 'auto' ? previousRect.width : this.#previousWidth),
movementY: this.height - (this.#previousHeight === 'auto' ? previousRect.height : this.#previousHeight),
})
);
if (!notCancelled) {
if (this.#height === 'auto') {
this.height = previousRect?.height || 0;
}
if (this.#width === 'auto') {
this.width = previousRect?.width || 0;
}
}
this.#previousHeight = previousRect.height;
this.#previousWidth = previousRect.width;
this.#dispatchTransformEvent(new Set(['width', 'height']));
};
}