RotatedDOMRect

This commit is contained in:
“chrisshank” 2024-12-04 00:49:57 -08:00
parent f8ab26a946
commit 74cb31bf4b
6 changed files with 202 additions and 81 deletions

View File

@ -0,0 +1,170 @@
import { Point } from './types';
import { Vector } from './Vector';
interface RotatedDOMRectInit {
height?: number;
width?: number;
x?: number;
y?: number;
rotation?: number;
}
export class RotatedDOMRect implements DOMRect {
#other: RotatedDOMRectInit;
constructor(other: RotatedDOMRectInit = {}) {
this.#other = other;
}
get x(): number {
return this.#other.x ?? 0;
}
set x(x: number) {
this.#other.x = x;
this.#reset();
}
get y(): number {
return this.#other.y ?? 0;
}
set y(y: number) {
this.#other.y = y;
this.#reset();
}
get height(): number {
return this.#other.height ?? 0;
}
set height(height: number) {
this.#other.height = height;
this.#reset();
}
get width(): number {
return this.#other.width ?? 0;
}
set width(width: number) {
this.#other.width = width;
this.#reset();
}
get rotation(): number {
return this.#other.rotation ?? 0;
}
set rotation(rotation: number) {
this.#other.rotation = rotation;
this.#reset();
}
get left(): number {
return this.x;
}
get top(): number {
return this.y;
}
get right(): number {
return this.x + this.width;
}
get bottom(): number {
return this.y + this.height;
}
#center: Point | null = null;
/** Returns the center point in worldspace coordinates */
get center(): Point {
if (this.#center === null) {
this.#center = {
x: this.x + this.width / 2,
y: this.y + this.height / 2,
};
}
return this.#center;
}
#topLeftCorner: Point | null = null;
get topLeftCorner() {
if (this.#topLeftCorner === null) {
this.#topLeftCorner = Vector.rotateAround({ x: this.x, y: this.y }, this.center, this.rotation);
}
return this.#topLeftCorner;
}
#topRightCorner: Point | null = null;
get topRightCorner() {
if (this.#topRightCorner === null) {
this.#topRightCorner = Vector.rotateAround({ x: this.right, y: this.y }, this.center, this.rotation);
}
return this.#topRightCorner;
}
#bottomRightCorner: Point | null = null;
get bottomRightCorner() {
if (this.#bottomRightCorner === null) {
this.#bottomRightCorner = Vector.rotateAround({ x: this.right, y: this.bottom }, this.center, this.rotation);
}
return this.#bottomRightCorner;
}
#bottomLeftCorner: Point | null = null;
get bottomLeftCorner() {
if (this.#bottomLeftCorner === null) {
this.#bottomLeftCorner = Vector.rotateAround({ x: this.x, y: this.bottom }, this.center, this.rotation);
}
return this.#bottomLeftCorner;
}
#reset() {
this.#center = null;
this.#topLeftCorner = null;
this.#topRightCorner = null;
this.#bottomLeftCorner = null;
this.#bottomRightCorner = null;
}
/** Returns all the vertices in worldspace coordinates */
vertices(): Point[] {
return [];
}
toJSON() {
return {};
}
}
// We cant just override the setter, we need to override the getter and setter.
export class RotatedDOMRectReadonly extends RotatedDOMRect {
#other: RotatedDOMRectInit;
constructor(other: RotatedDOMRectInit = {}) {
super(other);
this.#other = other;
}
get x(): number {
return this.#other.x ?? 0;
}
set x(x: number) {}
get y(): number {
return this.#other.y ?? 0;
}
set y(y: number) {}
get height(): number {
return this.#other.height ?? 0;
}
set height(height: number) {}
get width(): number {
return this.#other.width ?? 0;
}
set width(width: number) {}
get rotation(): number {
return this.#other.rotation ?? 0;
}
set rotation(rotation: number) {}
}

View File

@ -1,15 +1 @@
export type Point = { x: number; y: number };
export type RotatedDOMRect = DOMRect & {
/** in radians */
rotation: number;
/** Returns the center point in worldspace coordinates */
center(): Point;
/** Returns the four corners in worldspace coordinates, in clockwise order starting from the top left */
corners(): [Point, Point, Point, Point];
/** Returns all the vertices in worldspace coordinates */
vertices(): Point[];
};

View File

@ -151,7 +151,12 @@ export class FolkDistanceField extends HTMLElement {
// Collect positions and assign unique IDs to all shapes
this.shapes.forEach((geometry, index) => {
const rect = geometry.getClientRect();
const [topLeft, topRight, bottomRight, bottomLeft] = rect.corners();
const {
topLeftCorner: topLeft,
topRightCorner: topRight,
bottomRightCorner: bottomRight,
bottomLeftCorner: bottomLeft,
} = rect;
// Convert rotated coordinates to NDC using container dimensions
const x1 = (topLeft.x / containerWidth) * 2 - 1;

View File

@ -1,5 +1,4 @@
import { css } from './common/tags.ts';
import type { RotatedDOMRect } from './common/types';
import { FolkRope } from './folk-rope.ts';
import * as parser from '@babel/parser';
import type { Node } from '@babel/types';
@ -137,10 +136,6 @@ to.${key} = ${value};`);
this.expression = this.#expressionTextarea.value = this.getAttribute('expression') || '';
}
override render(sourceRect: RotatedDOMRect | DOMRectReadOnly, targetRect: RotatedDOMRect | DOMRectReadOnly) {
super.render(sourceRect, targetRect);
}
override draw() {
super.draw();

View File

@ -1,7 +1,8 @@
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
import { Vector } from './common/Vector.ts';
import type { Point, RotatedDOMRect } from './common/types.ts';
import type { Point } from './common/types.ts';
import { RotatedDOMRect } from './common/rotated-dom-rect.ts';
import { FolkBaseConnection } from './folk-base-connection.ts';
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
@ -123,9 +124,8 @@ export class FolkRope extends FolkBaseConnection {
let source: Point;
let target: Point;
if ('corners' in sourceRect) {
const [_a, _b, bottomRight, bottomLeft] = sourceRect.corners();
source = Vector.lerp(bottomRight, bottomLeft, 0.5);
if (sourceRect instanceof RotatedDOMRect) {
source = Vector.lerp(sourceRect.bottomRightCorner, sourceRect.bottomLeftCorner, 0.5);
} else {
source = {
x: sourceRect.x + sourceRect.width / 2,
@ -133,9 +133,8 @@ export class FolkRope extends FolkBaseConnection {
};
}
if ('corners' in targetRect) {
const [_a, _b, bottomRight, bottomLeft] = targetRect.corners();
target = Vector.lerp(bottomRight, bottomLeft, 0.5);
if (targetRect instanceof RotatedDOMRect) {
target = Vector.lerp(targetRect.bottomRightCorner, targetRect.bottomLeftCorner, 0.5);
} else {
target = {
x: targetRect.x + targetRect.width / 2,

View File

@ -1,6 +1,7 @@
import { css, html } from './common/tags';
import { ResizeObserverManager } from './common/resize-observer';
import type { Point, RotatedDOMRect } from './common/types';
import { Point } from './common/types';
import { RotatedDOMRectReadonly } from './common/rotated-dom-rect';
import { Vector } from './common/Vector';
import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors';
@ -354,45 +355,10 @@ export class FolkShape extends HTMLElement {
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation']));
}
getClientRect(): RotatedDOMRect {
getClientRect() {
const { x, y, width, height, rotation } = this;
return {
x,
y,
width,
height,
left: x,
top: y,
right: x + width,
bottom: y + height,
rotation,
center(): Point {
return {
x: this.x + this.width / 2,
y: this.y + this.height / 2,
};
},
vertices(): Point[] {
// TODO: Implement
return [];
},
corners() {
const center = this.center();
const { x, y, width, height, rotation } = this;
return [
Vector.rotateAround({ x, y }, center, rotation),
Vector.rotateAround({ x: x + width, y }, center, rotation),
Vector.rotateAround({ x: x + width, y: y + height }, center, rotation),
Vector.rotateAround({ x, y: y + height }, center, rotation),
];
},
toJSON: undefined as any,
};
return new RotatedDOMRectReadonly({ x, y, width, height, rotation });
}
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
@ -426,18 +392,17 @@ export class FolkShape extends HTMLElement {
if (!anyChange) return;
// Get the corner coordinates of the shape for the corresponding handle
const corners = this.getClientRect().corners(); // Returns an array of Points: [NW, NE, SE, SW]
const rect = this.getClientRect();
// Map handle names to corner indices
const handleToCornerIndex: { [key: string]: number } = {
'resize-nw': 0, // Top-left corner
'resize-ne': 1, // Top-right corner
'resize-se': 2, // Bottom-right corner
'resize-sw': 3, // Bottom-left corner
const handleToCornerIndex: Record<string, Point> = {
'resize-nw': rect.topLeftCorner,
'resize-ne': rect.topRightCorner,
'resize-se': rect.bottomRightCorner,
'resize-sw': rect.bottomLeftCorner,
};
const cornerIndex = handleToCornerIndex[handle];
const currentPos = corners[cornerIndex];
const currentPos = handleToCornerIndex[handle];
// Calculate movement based on arrow keys
const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
@ -499,7 +464,7 @@ export class FolkShape extends HTMLElement {
// Store initial angle on rotation start
if (target.getAttribute('part')?.startsWith('rotation')) {
const center = this.getClientRect().center();
const center = this.getClientRect().center;
this.#initialRotation = this.#rotation;
this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
}
@ -537,7 +502,7 @@ export class FolkShape extends HTMLElement {
}
if (handle.startsWith('rotation')) {
const center = this.getClientRect().center();
const center = this.getClientRect().center;
const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
this.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
@ -700,17 +665,18 @@ export class FolkShape extends HTMLElement {
// Updated helper method to handle resize operations
#handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) {
const rect = this.getClientRect();
// Map each resize handle to its opposite corner index
const OPPOSITE_CORNERS = {
'resize-se': 0,
'resize-sw': 1,
'resize-nw': 2,
'resize-ne': 3,
'resize-se': rect.topLeftCorner,
'resize-sw': rect.topRightCorner,
'resize-nw': rect.bottomRightCorner,
'resize-ne': rect.bottomLeftCorner,
} as const;
// Get the opposite corner for the current resize handle
const corners = this.getClientRect().corners();
const oppositeCorner = corners[OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]];
const oppositeCorner = OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS];
// Calculate new dimensions based on mouse position and opposite corner
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);