RotatedDOMRect
This commit is contained in:
parent
f8ab26a946
commit
74cb31bf4b
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue