rotation in RotatedDOMRect

This commit is contained in:
Orion Reed 2024-12-02 06:15:25 -05:00
parent 9b22f838af
commit 14d94f83e7
2 changed files with 67 additions and 31 deletions

View File

@ -38,12 +38,13 @@ export class DistanceField extends HTMLElement {
this.initWebGL(); this.initWebGL();
this.initShaders(); this.initShaders();
this.initPingPongTextures(); this.initPingPongTextures();
this.initSeedPointRendering(); this.populateSeedPoints();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
this.shapes.forEach((geometry) => { this.shapes.forEach((geometry) => {
geometry.addEventListener('move', this.handleGeometryUpdate); geometry.addEventListener('move', this.handleGeometryUpdate);
geometry.addEventListener('resize', 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) => { this.shapes.forEach((geometry) => {
geometry.removeEventListener('move', this.handleGeometryUpdate); geometry.removeEventListener('move', this.handleGeometryUpdate);
geometry.removeEventListener('resize', this.handleGeometryUpdate); geometry.removeEventListener('resize', this.handleGeometryUpdate);
geometry.removeEventListener('rotate', this.handleGeometryUpdate);
}); });
this.cleanupWebGLResources(); 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. * Handles updates to geometry elements by re-initializing seed points and rerunning the JFA.
*/ */
private handleGeometryUpdate = () => { private handleGeometryUpdate = () => {
this.initSeedPointRendering(); console.log('handleGeometryUpdate');
this.populateSeedPoints();
this.runJumpFloodingAlgorithm(); this.runJumpFloodingAlgorithm();
}; };
@ -140,7 +143,7 @@ export class DistanceField extends HTMLElement {
* Initializes rendering of seed points (shapes) into a texture. * Initializes rendering of seed points (shapes) into a texture.
* Seed points are the starting locations for distance calculations. * Seed points are the starting locations for distance calculations.
*/ */
private initSeedPointRendering() { private populateSeedPoints() {
const gl = this.glContext; const gl = this.glContext;
const positions: number[] = []; const positions: number[] = [];
@ -150,12 +153,39 @@ export class DistanceField extends HTMLElement {
// Collect positions and assign unique IDs to all shapes // Collect positions and assign unique IDs to all shapes
this.shapes.forEach((geometry, index) => { this.shapes.forEach((geometry, index) => {
const rect = geometry.getClientRect(); const rect = geometry.getClientRect();
const rotation = (geometry.rotation * Math.PI) / 180; // Convert to radians
// Convert DOM coordinates to Normalized Device Coordinates (NDC) // Calculate the center of the rectangle
const x1 = (rect.left / windowWidth) * 2 - 1; const centerX = (rect.left + rect.right) / 2;
const y1 = -((rect.top / windowHeight) * 2 - 1); const centerY = (rect.top + rect.bottom) / 2;
const x2 = (rect.right / windowWidth) * 2 - 1;
const y2 = -((rect.bottom / windowHeight) * 2 - 1); // 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 const shapeID = index + 1; // Avoid zero to prevent hash function issues
@ -165,20 +195,20 @@ export class DistanceField extends HTMLElement {
y1, y1,
shapeID, shapeID,
x2, x2,
y1,
shapeID,
x1,
y2, y2,
shapeID, shapeID,
x3,
y3,
shapeID,
x1, x3,
y2, y3,
shapeID,
x2,
y1,
shapeID, shapeID,
x2, x2,
y2, y2,
shapeID,
x4,
y4,
shapeID shapeID
); );
}); });
@ -376,7 +406,7 @@ export class DistanceField extends HTMLElement {
this.initPingPongTextures(); this.initPingPongTextures();
// Re-initialize seed point rendering to update positions // Re-initialize seed point rendering to update positions
this.initSeedPointRendering(); this.populateSeedPoints();
// Rerun the Jump Flooding Algorithm with the new sizes // Rerun the Jump Flooding Algorithm with the new sizes
this.runJumpFloodingAlgorithm(); this.runJumpFloodingAlgorithm();

View File

@ -5,6 +5,8 @@ const resizeObserver = new ResizeObserverManager();
export type Shape = 'rectangle' | 'circle' | 'triangle'; export type Shape = 'rectangle' | 'circle' | 'triangle';
type RotatedDOMRect = DOMRect & { rotation: number };
export type MoveEventDetail = { movementX: number; movementY: number }; export type MoveEventDetail = { movementX: number; movementY: number };
export class MoveEvent extends CustomEvent<MoveEventDetail> { export class MoveEvent extends CustomEvent<MoveEventDetail> {
@ -239,15 +241,18 @@ export class FolkShape extends HTMLElement {
#initialRotation = 0; #initialRotation = 0;
#startAngle = 0; #startAngle = 0;
#previousRotate = 0; #previousRotation = 0;
#rotate = Number(this.getAttribute('rotate')) || 0;
get rotate(): number { // TODO: consider using radians instead of degrees
return this.#rotate; #rotation = Number(this.getAttribute('rotate')) || 0;
get rotation(): number {
return this.#rotation;
} }
set rotate(rotate: number) { set rotation(rotation: number) {
this.#previousRotate = this.#rotate; this.#previousRotation = this.#rotation;
this.#rotate = rotate; this.#rotation = rotation;
this.#requestUpdate('rotate'); this.#requestUpdate('rotate');
} }
@ -279,8 +284,8 @@ export class FolkShape extends HTMLElement {
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotate'])); this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotate']));
} }
getClientRect(): DOMRect { getClientRect(): RotatedDOMRect {
const { x, y, width, height } = this; const { x, y, width, height, rotation } = this;
return { return {
x, x,
@ -291,6 +296,7 @@ export class FolkShape extends HTMLElement {
top: y, top: y,
right: x + width, right: x + width,
bottom: y + height, bottom: y + height,
rotation,
toJSON: undefined as any, toJSON: undefined as any,
}; };
// return DOMRectReadOnly.fromRect({ x: this.x, y: this.y, width: this.width, height: this.height }); // 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. // 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... // 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? // 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 centerX = this.#x + this.width / 2;
const centerY = this.#y + this.height / 2; const centerY = this.#y + this.height / 2;
this.#startAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX); 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 currentAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX);
const deltaAngle = currentAngle - this.#startAngle; const deltaAngle = currentAngle - this.#startAngle;
this.rotate = this.#initialRotation + (deltaAngle * 180) / Math.PI; this.rotation = this.#initialRotation + (deltaAngle * 180) / Math.PI;
return; return;
} }
@ -473,14 +479,14 @@ export class FolkShape extends HTMLElement {
if (updatedProperties.has('rotate')) { 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 // 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 (notCancelled) {
if (updatedProperties.has('rotate')) { if (updatedProperties.has('rotate')) {
this.style.rotate = `${this.#rotate}deg`; this.style.rotate = `${this.#rotation}deg`;
} }
} else { } else {
this.#rotate = this.#previousRotate; this.#rotation = this.#previousRotation;
} }
} }
} }