From 14d94f83e76ce9f43ac69090b31ae6a89eecbad7 Mon Sep 17 00:00:00 2001 From: Orion Reed Date: Mon, 2 Dec 2024 06:15:25 -0500 Subject: [PATCH] rotation in RotatedDOMRect --- src/distance-field.ts | 64 +++++++++++++++++++++++++++++++------------ src/folk-shape.ts | 34 +++++++++++++---------- 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/src/distance-field.ts b/src/distance-field.ts index 1722e4c..1ae2a95 100644 --- a/src/distance-field.ts +++ b/src/distance-field.ts @@ -38,12 +38,13 @@ export class DistanceField extends HTMLElement { this.initWebGL(); this.initShaders(); this.initPingPongTextures(); - this.initSeedPointRendering(); + this.populateSeedPoints(); window.addEventListener('resize', this.handleResize); this.shapes.forEach((geometry) => { geometry.addEventListener('move', this.handleGeometryUpdate); geometry.addEventListener('resize', this.handleGeometryUpdate); + geometry.addEventListener('rotate', this.handleGeometryUpdate); }); } @@ -52,6 +53,7 @@ export class DistanceField extends HTMLElement { this.shapes.forEach((geometry) => { geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate); + geometry.removeEventListener('rotate', this.handleGeometryUpdate); }); this.cleanupWebGLResources(); } @@ -71,7 +73,8 @@ export class DistanceField extends HTMLElement { * Handles updates to geometry elements by re-initializing seed points and rerunning the JFA. */ private handleGeometryUpdate = () => { - this.initSeedPointRendering(); + console.log('handleGeometryUpdate'); + this.populateSeedPoints(); this.runJumpFloodingAlgorithm(); }; @@ -140,7 +143,7 @@ export class DistanceField extends HTMLElement { * Initializes rendering of seed points (shapes) into a texture. * Seed points are the starting locations for distance calculations. */ - private initSeedPointRendering() { + private populateSeedPoints() { const gl = this.glContext; const positions: number[] = []; @@ -150,12 +153,39 @@ export class DistanceField extends HTMLElement { // Collect positions and assign unique IDs to all shapes this.shapes.forEach((geometry, index) => { const rect = geometry.getClientRect(); + const rotation = (geometry.rotation * Math.PI) / 180; // Convert to radians - // Convert DOM coordinates to Normalized Device Coordinates (NDC) - const x1 = (rect.left / windowWidth) * 2 - 1; - const y1 = -((rect.top / windowHeight) * 2 - 1); - const x2 = (rect.right / windowWidth) * 2 - 1; - const y2 = -((rect.bottom / windowHeight) * 2 - 1); + // Calculate the center of the rectangle + const centerX = (rect.left + rect.right) / 2; + const centerY = (rect.top + rect.bottom) / 2; + + // Function to rotate a point around the center + const rotatePoint = (x: number, y: number) => { + const dx = x - centerX; + const dy = y - centerY; + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + return { + x: centerX + dx * cos - dy * sin, + y: centerY + dx * sin + dy * cos, + }; + }; + + // Rotate each corner of the rectangle + const topLeft = rotatePoint(rect.left, rect.top); + const topRight = rotatePoint(rect.right, rect.top); + const bottomLeft = rotatePoint(rect.left, rect.bottom); + const bottomRight = rotatePoint(rect.right, rect.bottom); + + // Convert rotated coordinates to NDC + const x1 = (topLeft.x / windowWidth) * 2 - 1; + const y1 = -((topLeft.y / windowHeight) * 2 - 1); + const x2 = (topRight.x / windowWidth) * 2 - 1; + const y2 = -((topRight.y / windowHeight) * 2 - 1); + const x3 = (bottomLeft.x / windowWidth) * 2 - 1; + const y3 = -((bottomLeft.y / windowHeight) * 2 - 1); + const x4 = (bottomRight.x / windowWidth) * 2 - 1; + const y4 = -((bottomRight.y / windowHeight) * 2 - 1); const shapeID = index + 1; // Avoid zero to prevent hash function issues @@ -165,20 +195,20 @@ export class DistanceField extends HTMLElement { y1, shapeID, x2, - y1, - shapeID, - x1, y2, shapeID, + x3, + y3, + shapeID, - x1, - y2, - shapeID, - x2, - y1, + x3, + y3, shapeID, x2, y2, + shapeID, + x4, + y4, shapeID ); }); @@ -376,7 +406,7 @@ export class DistanceField extends HTMLElement { this.initPingPongTextures(); // Re-initialize seed point rendering to update positions - this.initSeedPointRendering(); + this.populateSeedPoints(); // Rerun the Jump Flooding Algorithm with the new sizes this.runJumpFloodingAlgorithm(); diff --git a/src/folk-shape.ts b/src/folk-shape.ts index f9b4919..d6dd859 100644 --- a/src/folk-shape.ts +++ b/src/folk-shape.ts @@ -5,6 +5,8 @@ const resizeObserver = new ResizeObserverManager(); export type Shape = 'rectangle' | 'circle' | 'triangle'; +type RotatedDOMRect = DOMRect & { rotation: number }; + export type MoveEventDetail = { movementX: number; movementY: number }; export class MoveEvent extends CustomEvent { @@ -239,15 +241,18 @@ export class FolkShape extends HTMLElement { #initialRotation = 0; #startAngle = 0; - #previousRotate = 0; - #rotate = Number(this.getAttribute('rotate')) || 0; - get rotate(): number { - return this.#rotate; + #previousRotation = 0; + + // TODO: consider using radians instead of degrees + #rotation = Number(this.getAttribute('rotate')) || 0; + + get rotation(): number { + return this.#rotation; } - set rotate(rotate: number) { - this.#previousRotate = this.#rotate; - this.#rotate = rotate; + set rotation(rotation: number) { + this.#previousRotation = this.#rotation; + this.#rotation = rotation; this.#requestUpdate('rotate'); } @@ -279,8 +284,8 @@ export class FolkShape extends HTMLElement { this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotate'])); } - getClientRect(): DOMRect { - const { x, y, width, height } = this; + getClientRect(): RotatedDOMRect { + const { x, y, width, height, rotation } = this; return { x, @@ -291,6 +296,7 @@ export class FolkShape extends HTMLElement { top: y, right: x + width, bottom: y + height, + rotation, toJSON: undefined as any, }; // return DOMRectReadOnly.fromRect({ x: this.x, y: this.y, width: this.width, height: this.height }); @@ -320,7 +326,7 @@ export class FolkShape extends HTMLElement { // Might be an argument for making elements dumber (i.e. not have them manage their own state) and do this from the outside. // But we also want to preserve the self-sufficient nature of elements' behaviour... // Maybe some kind of shared utility, used by both the element and the outside environment? - this.#initialRotation = this.#rotate; + this.#initialRotation = this.#rotation; const centerX = this.#x + this.width / 2; const centerY = this.#y + this.height / 2; this.#startAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX); @@ -383,7 +389,7 @@ export class FolkShape extends HTMLElement { const currentAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX); const deltaAngle = currentAngle - this.#startAngle; - this.rotate = this.#initialRotation + (deltaAngle * 180) / Math.PI; + this.rotation = this.#initialRotation + (deltaAngle * 180) / Math.PI; return; } @@ -473,14 +479,14 @@ export class FolkShape extends HTMLElement { if (updatedProperties.has('rotate')) { // 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.#rotate - this.#previousRotate })); + const notCancelled = this.dispatchEvent(new RotateEvent({ rotate: this.#rotation - this.#previousRotation })); if (notCancelled) { if (updatedProperties.has('rotate')) { - this.style.rotate = `${this.#rotate}deg`; + this.style.rotate = `${this.#rotation}deg`; } } else { - this.#rotate = this.#previousRotate; + this.#rotation = this.#previousRotation; } } }