folk-canvas/lib/DOMRectTransform.ts

556 lines
16 KiB
TypeScript

import { Point } from './types';
import { Matrix } from './Matrix';
interface DOMRectTransformInit {
height?: number;
width?: number;
x?: number;
y?: number;
rotation?: number;
transformOrigin?: Point;
rotateOrigin?: Point;
}
/**
* Represents a rectangle with position, size, and rotation,
* capable of transforming points between local and parent coordinate spaces.
*
* **Coordinate System:**
* - The origin `(0, 0)` is at the **top-left corner**.
* - Positive `x` values extend **to the right**.
* - Positive `y` values extend **downward**.
* - Rotation is **clockwise**, in **radians**, around the rectangle's **center**.
*/
export class DOMRectTransform implements DOMRect {
// Private properties for position, size, rotation, and origins
#x: number; // X-coordinate of the top-left corner
#y: number; // Y-coordinate of the top-left corner
#width: number; // Width of the rectangle
#height: number; // Height of the rectangle
#rotation: number; // Rotation angle in radians, clockwise
// New properties for transform origin and rotation origin
#transformOrigin: Point; // Origin for transformations
#rotateOrigin: Point; // Origin for rotation
// Internal transformation matrices
#transformMatrix: Matrix; // Transforms from local to parent space
#inverseMatrix: Matrix; // Transforms from parent to local space
/**
* Constructs a new `TransformDOMRect`.
* @param init - Optional initial values.
*/
constructor(init: DOMRectTransformInit = {}) {
this.#x = init.x ?? 0;
this.#y = init.y ?? 0;
this.#width = init.width ?? 0;
this.#height = init.height ?? 0;
this.#rotation = init.rotation ?? 0;
// Initialize origins with relative values (0.5, 0.5 is center)
this.#transformOrigin = init.transformOrigin ?? { x: 0.5, y: 0.5 };
this.#rotateOrigin = init.rotateOrigin ?? { x: 0.5, y: 0.5 };
// Initialize transformation matrices
this.#transformMatrix = Matrix.Identity();
this.#inverseMatrix = Matrix.Identity();
this.#updateMatrices();
}
// Getters and setters for properties
/** Gets or sets the **x-coordinate** of the top-left corner. */
get x(): number {
return this.#x;
}
set x(value: number) {
this.#x = value;
this.#updateMatrices();
}
/** Gets or sets the **y-coordinate** of the top-left corner. */
get y(): number {
return this.#y;
}
set y(value: number) {
this.#y = value;
this.#updateMatrices();
}
/** Gets or sets the **width** of the rectangle. */
get width(): number {
return this.#width;
}
set width(value: number) {
this.#width = value;
this.#updateMatrices();
}
/** Gets or sets the **height** of the rectangle. */
get height(): number {
return this.#height;
}
set height(value: number) {
this.#height = value;
this.#updateMatrices();
}
/** Gets or sets the **rotation angle** in radians, **clockwise**. */
get rotation(): number {
return this.#rotation;
}
set rotation(value: number) {
this.#rotation = value;
this.#updateMatrices();
}
/** Gets or sets the **transform origin** as relative values (0 to 1). */
get transformOrigin(): Point {
return this.#transformOrigin;
}
set transformOrigin(value: Point) {
this.#transformOrigin = value;
this.#updateMatrices();
}
/** Gets or sets the **rotation origin** as relative values (0 to 1). */
get rotateOrigin(): Point {
return this.#rotateOrigin;
}
set rotateOrigin(value: Point) {
this.#rotateOrigin = value;
this.#updateMatrices();
}
// DOMRect read-only properties
/** The **left** coordinate of the rectangle (same as `x`). */
get left(): number {
return this.x;
}
/** The **top** coordinate of the rectangle (same as `y`). */
get top(): number {
return this.y;
}
/** The **right** coordinate of the rectangle (`x + width`). */
get right(): number {
return this.x + this.width;
}
/** The **bottom** coordinate of the rectangle (`y + height`). */
get bottom(): number {
return this.y + this.height;
}
/**
* Updates the transformation matrices based on the current position,
* size, rotation, and origins of the rectangle.
*
* The transformation sequence is:
* 1. **Translate** to the global position.
* 2. **Translate** to the transform origin.
* 3. **Rotate** around the rotation origin.
* 4. **Translate** back from the transform origin.
*/
#updateMatrices() {
// Reset the transformMatrix to identity
this.#transformMatrix.identity();
// Get absolute positions for origins
const transformOrigin = this.#getAbsoluteTransformOrigin();
const rotateOrigin = this.#getAbsoluteRotateOrigin();
// Apply transformations
this.#transformMatrix
// Step 1: Translate to global position
.translate(this.#x, this.#y)
// Step 2: Translate to the transform origin
.translate(transformOrigin.x, transformOrigin.y)
// Step 3: Rotate around the rotation origin
.translate(rotateOrigin.x - transformOrigin.x, rotateOrigin.y - transformOrigin.y)
.rotate(this.#rotation)
.translate(-(rotateOrigin.x - transformOrigin.x), -(rotateOrigin.y - transformOrigin.y))
// Step 4: Translate back from the transform origin
.translate(-transformOrigin.x, -transformOrigin.y);
// Update inverseMatrix as the inverse of transformMatrix
this.#inverseMatrix = this.#transformMatrix.clone().invert();
}
// Convert relative origins to absolute points
#getAbsoluteTransformOrigin(): Point {
return {
x: this.#width * this.#transformOrigin.x,
y: this.#height * this.#transformOrigin.y,
};
}
#getAbsoluteRotateOrigin(): Point {
return {
x: this.#width * this.#rotateOrigin.x,
y: this.#height * this.#rotateOrigin.y,
};
}
// Accessors for the transformation matrices
get transformMatrix(): Matrix {
return this.#transformMatrix;
}
get inverseMatrix(): Matrix {
return this.#inverseMatrix;
}
/**
* Converts a point from **parent space** to **local space**.
* @param point - The point in parent coordinate space.
* @returns The point in local coordinate space.
*/
toLocalSpace(point: Point): Point {
return this.#inverseMatrix.applyToPoint(point);
}
/**
* Converts a point from **local space** to **parent space**.
* @param point - The point in local coordinate space.
* @returns The point in parent coordinate space.
*/
toParentSpace(point: Point): Point {
return this.#transformMatrix.applyToPoint(point);
}
// Local space corners
/**
* Gets the **top-left** corner of the rectangle in **local space** (before transformation).
*/
get topLeft(): Point {
return { x: 0, y: 0 };
}
/**
* Gets the **top-right** corner of the rectangle in **local space** (before transformation).
*/
get topRight(): Point {
return { x: this.width, y: 0 };
}
/**
* Gets the **bottom-right** corner of the rectangle in **local space** (before transformation).
*/
get bottomRight(): Point {
return { x: this.width, y: this.height };
}
/**
* Gets the **bottom-left** corner of the rectangle in **local space** (before transformation).
*/
get bottomLeft(): Point {
return { x: 0, y: this.height };
}
/**
* Gets the **center point** of the rectangle in **parent space**.
*/
get center(): Point {
return {
x: this.x + this.width / 2,
y: this.y + this.height / 2,
};
}
/**
* Gets the four corner vertices of the rectangle in **local space**.
* @returns An array of points in the order: top-left, top-right, bottom-right, bottom-left.
*/
vertices(): Point[] {
return [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft];
}
/**
* Generates a CSS transform string representing the rectangle's transformation.
* @returns A string suitable for use in CSS `transform` properties.
*/
toCssString(): string {
return this.transformMatrix.toCssString();
}
/**
* Converts the rectangle's properties to a JSON serializable object.
* @returns An object containing the rectangle's `x`, `y`, `width`, `height`, and `rotation`.
*/
toJSON() {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height,
rotation: this.rotation,
};
}
// TODO: these setters work but surely there's a better way
/**
* Sets the **top-left** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly,
* and keeps the **bottom-right corner** fixed in the **parent space**.
* @param point - The new top-left corner point in local coordinate space.
*/
set topLeft(point: Point) {
// Compute the parent-space position of the bottom-right corner before resizing
const bottomRightBefore = this.toParentSpace(this.bottomRight);
// Update x, y, width, and height
const deltaWidth = this.#width - point.x;
const deltaHeight = this.#height - point.y;
this.#x += point.x;
this.#y += point.y;
this.#width = deltaWidth;
this.#height = deltaHeight;
// Update transformation matrices after changing size and position
this.#updateMatrices();
// Compute the parent-space position of the bottom-right corner after resizing
const bottomRightAfter = this.toParentSpace(this.bottomRight);
// Compute the difference in position
const deltaX = bottomRightAfter.x - bottomRightBefore.x;
const deltaY = bottomRightAfter.y - bottomRightBefore.y;
// Adjust x and y to compensate for the movement
this.#x -= deltaX;
this.#y -= deltaY;
// Update matrices again after adjusting position
this.#updateMatrices();
}
/**
* Sets the **top-right** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly,
* and keeps the **bottom-left corner** fixed in the **parent space**.
* @param point - The new top-right corner point in local coordinate space.
*/
set topRight(point: Point) {
// Compute the parent-space position of the bottom-left corner before resizing
const bottomLeftBefore = this.toParentSpace(this.bottomLeft);
// Update y, width, and height
const deltaWidth = point.x;
const deltaHeight = this.#height - point.y;
this.#y += point.y;
this.#width = deltaWidth;
this.#height = deltaHeight;
// Update transformation matrices after changing size and position
this.#updateMatrices();
// Compute the parent-space position of the bottom-left corner after resizing
const bottomLeftAfter = this.toParentSpace(this.bottomLeft);
// Compute the difference in position
const deltaX = bottomLeftAfter.x - bottomLeftBefore.x;
const deltaY = bottomLeftAfter.y - bottomLeftBefore.y;
// Adjust x and y to compensate for the movement
this.#x -= deltaX;
this.#y -= deltaY;
// Update matrices again after adjusting position
this.#updateMatrices();
}
/**
* Sets the **bottom-right** corner of the rectangle in **local space**, adjusting the width and height accordingly,
* and keeps the **top-left corner** fixed in the **parent space**.
* @param point - The new bottom-right corner point in local coordinate space.
*/
set bottomRight(point: Point) {
// Compute the parent-space position of the top-left corner before resizing
const topLeftBefore = this.toParentSpace(this.topLeft);
// Update width and height
this.#width = point.x;
this.#height = point.y;
// Update transformation matrices after changing size
this.#updateMatrices();
// Compute the parent-space position of the top-left corner after resizing
const topLeftAfter = this.toParentSpace(this.topLeft);
// Compute the difference in position
const deltaX = topLeftAfter.x - topLeftBefore.x;
const deltaY = topLeftAfter.y - topLeftBefore.y;
// Adjust x and y to compensate for the movement
this.#x -= deltaX;
this.#y -= deltaY;
// Update matrices again after adjusting position
this.#updateMatrices();
}
/**
* Sets the **bottom-left** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly,
* and keeps the **top-right corner** fixed in the **parent space**.
* @param point - The new bottom-left corner point in local coordinate space.
*/
set bottomLeft(point: Point) {
// Compute the parent-space position of the top-right corner before resizing
const topRightBefore = this.toParentSpace(this.topRight);
// Update x, width, and height
const deltaWidth = this.#width - point.x;
const deltaHeight = point.y;
this.#x += point.x;
this.#width = deltaWidth;
this.#height = deltaHeight;
// Update transformation matrices after changing size and position
this.#updateMatrices();
// Compute the parent-space position of the top-right corner after resizing
const topRightAfter = this.toParentSpace(this.topRight);
// Compute the difference in position
const deltaX = topRightAfter.x - topRightBefore.x;
const deltaY = topRightAfter.y - topRightBefore.y;
// Adjust x and y to compensate for the movement
this.#x -= deltaX;
this.#y -= deltaY;
// Update matrices again after adjusting position
this.#updateMatrices();
}
/**
* Computes the **axis-aligned bounding box** of the transformed rectangle in **parent space**.
* @returns An object representing the bounding rectangle with properties: `x`, `y`, `width`, `height`.
*/
getBounds(): DOMRectInit {
// Transform all vertices to parent space
const transformedVertices = this.vertices().map((v) => this.toParentSpace(v));
// Find min and max coordinates
const xs = transformedVertices.map((v) => v.x);
const ys = transformedVertices.map((v) => v.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
}
/**
* A **read-only** version of `TransformDOMRect` that prevents modification of position,
* size, and rotation properties.
*/
export class DOMRectTransformReadonly extends DOMRectTransform {
constructor(init: DOMRectTransformInit = {}) {
super(init);
}
// Explicit overrides for all getters from parent class
override get x(): number {
return super.x;
}
override get y(): number {
return super.y;
}
override get width(): number {
return super.width;
}
override get height(): number {
return super.height;
}
override get rotation(): number {
return super.rotation;
}
override get left(): number {
return super.left;
}
override get top(): number {
return super.top;
}
override get right(): number {
return super.right;
}
override get bottom(): number {
return super.bottom;
}
override get transformMatrix(): Matrix {
return super.transformMatrix;
}
override get inverseMatrix(): Matrix {
return super.inverseMatrix;
}
override get topLeft(): Point {
return super.topLeft;
}
override get topRight(): Point {
return super.topRight;
}
override get bottomRight(): Point {
return super.bottomRight;
}
override get bottomLeft(): Point {
return super.bottomLeft;
}
override get center(): Point {
return super.center;
}
// Add no-op setters
override set x(value: number) {
// no-op
}
override set y(value: number) {
// no-op
}
override set width(value: number) {
// no-op
}
override set height(value: number) {
// no-op
}
override set rotation(value: number) {
// no-op
}
}