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 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
|
// 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 [topLeft, topRight, bottomRight, bottomLeft] = rect.corners();
|
const {
|
||||||
|
topLeftCorner: topLeft,
|
||||||
|
topRightCorner: topRight,
|
||||||
|
bottomRightCorner: bottomRight,
|
||||||
|
bottomLeftCorner: bottomLeft,
|
||||||
|
} = rect;
|
||||||
|
|
||||||
// Convert rotated coordinates to NDC using container dimensions
|
// Convert rotated coordinates to NDC using container dimensions
|
||||||
const x1 = (topLeft.x / containerWidth) * 2 - 1;
|
const x1 = (topLeft.x / containerWidth) * 2 - 1;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { css } from './common/tags.ts';
|
import { css } from './common/tags.ts';
|
||||||
import type { RotatedDOMRect } from './common/types';
|
|
||||||
import { FolkRope } from './folk-rope.ts';
|
import { FolkRope } from './folk-rope.ts';
|
||||||
import * as parser from '@babel/parser';
|
import * as parser from '@babel/parser';
|
||||||
import type { Node } from '@babel/types';
|
import type { Node } from '@babel/types';
|
||||||
|
|
@ -137,10 +136,6 @@ to.${key} = ${value};`);
|
||||||
this.expression = this.#expressionTextarea.value = this.getAttribute('expression') || '';
|
this.expression = this.#expressionTextarea.value = this.getAttribute('expression') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
override render(sourceRect: RotatedDOMRect | DOMRectReadOnly, targetRect: RotatedDOMRect | DOMRectReadOnly) {
|
|
||||||
super.render(sourceRect, targetRect);
|
|
||||||
}
|
|
||||||
|
|
||||||
override draw() {
|
override draw() {
|
||||||
super.draw();
|
super.draw();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
|
// This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope
|
||||||
|
|
||||||
import { Vector } from './common/Vector.ts';
|
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';
|
import { FolkBaseConnection } from './folk-base-connection.ts';
|
||||||
|
|
||||||
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
|
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 source: Point;
|
||||||
let target: Point;
|
let target: Point;
|
||||||
|
|
||||||
if ('corners' in sourceRect) {
|
if (sourceRect instanceof RotatedDOMRect) {
|
||||||
const [_a, _b, bottomRight, bottomLeft] = sourceRect.corners();
|
source = Vector.lerp(sourceRect.bottomRightCorner, sourceRect.bottomLeftCorner, 0.5);
|
||||||
source = Vector.lerp(bottomRight, bottomLeft, 0.5);
|
|
||||||
} else {
|
} else {
|
||||||
source = {
|
source = {
|
||||||
x: sourceRect.x + sourceRect.width / 2,
|
x: sourceRect.x + sourceRect.width / 2,
|
||||||
|
|
@ -133,9 +133,8 @@ export class FolkRope extends FolkBaseConnection {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('corners' in targetRect) {
|
if (targetRect instanceof RotatedDOMRect) {
|
||||||
const [_a, _b, bottomRight, bottomLeft] = targetRect.corners();
|
target = Vector.lerp(targetRect.bottomRightCorner, targetRect.bottomLeftCorner, 0.5);
|
||||||
target = Vector.lerp(bottomRight, bottomLeft, 0.5);
|
|
||||||
} else {
|
} else {
|
||||||
target = {
|
target = {
|
||||||
x: targetRect.x + targetRect.width / 2,
|
x: targetRect.x + targetRect.width / 2,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { css, html } from './common/tags';
|
import { css, html } from './common/tags';
|
||||||
import { ResizeObserverManager } from './common/resize-observer';
|
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 { Vector } from './common/Vector';
|
||||||
import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors';
|
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']));
|
this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation']));
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientRect(): RotatedDOMRect {
|
getClientRect() {
|
||||||
const { x, y, width, height, rotation } = this;
|
const { x, y, width, height, rotation } = this;
|
||||||
|
|
||||||
return {
|
return new RotatedDOMRectReadonly({ x, y, width, height, rotation });
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
|
// 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;
|
if (!anyChange) return;
|
||||||
|
|
||||||
// Get the corner coordinates of the shape for the corresponding handle
|
// 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
|
// Map handle names to corner indices
|
||||||
const handleToCornerIndex: { [key: string]: number } = {
|
const handleToCornerIndex: Record<string, Point> = {
|
||||||
'resize-nw': 0, // Top-left corner
|
'resize-nw': rect.topLeftCorner,
|
||||||
'resize-ne': 1, // Top-right corner
|
'resize-ne': rect.topRightCorner,
|
||||||
'resize-se': 2, // Bottom-right corner
|
'resize-se': rect.bottomRightCorner,
|
||||||
'resize-sw': 3, // Bottom-left corner
|
'resize-sw': rect.bottomLeftCorner,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cornerIndex = handleToCornerIndex[handle];
|
const currentPos = handleToCornerIndex[handle];
|
||||||
const currentPos = corners[cornerIndex];
|
|
||||||
|
|
||||||
// Calculate movement based on arrow keys
|
// Calculate movement based on arrow keys
|
||||||
const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
|
const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';
|
||||||
|
|
@ -499,7 +464,7 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
// Store initial angle on rotation start
|
// Store initial angle on rotation start
|
||||||
if (target.getAttribute('part')?.startsWith('rotation')) {
|
if (target.getAttribute('part')?.startsWith('rotation')) {
|
||||||
const center = this.getClientRect().center();
|
const center = this.getClientRect().center;
|
||||||
this.#initialRotation = this.#rotation;
|
this.#initialRotation = this.#rotation;
|
||||||
this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
||||||
}
|
}
|
||||||
|
|
@ -537,7 +502,7 @@ export class FolkShape extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handle.startsWith('rotation')) {
|
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);
|
const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center);
|
||||||
this.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
|
this.rotation = this.#initialRotation + (currentAngle - this.#startAngle);
|
||||||
|
|
||||||
|
|
@ -700,17 +665,18 @@ export class FolkShape extends HTMLElement {
|
||||||
|
|
||||||
// Updated helper method to handle resize operations
|
// Updated helper method to handle resize operations
|
||||||
#handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) {
|
#handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) {
|
||||||
|
const rect = this.getClientRect();
|
||||||
|
|
||||||
// Map each resize handle to its opposite corner index
|
// Map each resize handle to its opposite corner index
|
||||||
const OPPOSITE_CORNERS = {
|
const OPPOSITE_CORNERS = {
|
||||||
'resize-se': 0,
|
'resize-se': rect.topLeftCorner,
|
||||||
'resize-sw': 1,
|
'resize-sw': rect.topRightCorner,
|
||||||
'resize-nw': 2,
|
'resize-nw': rect.bottomRightCorner,
|
||||||
'resize-ne': 3,
|
'resize-ne': rect.bottomLeftCorner,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Get the opposite corner for the current resize handle
|
// Get the opposite corner for the current resize handle
|
||||||
const corners = this.getClientRect().corners();
|
const oppositeCorner = OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS];
|
||||||
const oppositeCorner = corners[OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]];
|
|
||||||
|
|
||||||
// Calculate new dimensions based on mouse position and opposite corner
|
// Calculate new dimensions based on mouse position and opposite corner
|
||||||
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);
|
const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue