Source: Transform.js

import { BoundingBox } from "./BoundingBox";

/**
 * @typedef {Array<number>} APoint
 * A tuple of [x, y] representing a 2D point.
 * @property {number} 0 - X coordinate
 * @property {number} 1 - Y coordinate
 * 
 * @example
 * ```javascript
 * const point: APoint = [10, 20]; // [x, y]
 * const x = point[0];  // x coordinate
 * const y = point[1];  // y coordinate
 * ```
 */

/**
 * @typedef {Object} Point
 * Object representation of a 2D point
 * @property {number} x - X coordinate
 * @property {number} y - Y coordinate
 */

/**
 * @typedef {Object} TransformParameters
 * @property {number} [x=0] - X translation component
 * @property {number} [y=0] - Y translation component
 * @property {number} [a=0] - Rotation angle in degrees
 * @property {number} [z=1] - Scale factor
 * @property {number} [t=0] - Timestamp for animations
 */

/**
 * @typedef {'linear'|'ease-out'|'ease-in-out'} EasingFunction
 * Animation easing function type
 */

/**
 * 
 * Implements a 2D affine transformation system for coordinate manipulation.
 * Provides a complete set of operations for coordinate system conversions,
 * camera transformations, and animation support.
 * 
 * Mathematical Model:
 * Transformation of point P to P' follows the equation:
 *
 * P' = z * R(a) * P + T
 *
 * where:
 * - z: scale factor
 * - R(a): rotation matrix for angle 'a'
 * - T: translation vector (x,y)
 * 
 * Key Features:
 * - Full affine transformation support
 * - Camera positioning utilities
 * - Animation interpolation
 * - Viewport projection
 * - Coordinate system conversions
 * - Bounding box transformations
 * 
 *
 * Coordinate Systems and Transformations:
 * 
 * 1. Scene Space:
 * - Origin at image center
 * - Y-axis points up
 * - Unit scale
 * 
 * 2. Viewport Space:
 * - Origin at top-left
 * - Y-axis points down
 * - Pixel units [0..w-1, 0..h-1]
 * 
 * 3. WebGL Space:
 * - Origin at center
 * - Y-axis points up
 * - Range [-1..1, -1..1]
 * 
 * Transform Pipeline:
 * ```
 * Scene -> Transform -> Viewport -> WebGL
 * ```
 * 
 * Animation System:
 * - Time-based interpolation
 * - Multiple easing functions
 * - Smooth transitions
 * 
 * Performance Considerations:
 * - Matrix operations optimized for 2D
 * - Cached transformation results
 * - Efficient composition
 */
class Transform { //FIXME Add translation to P?
	/**
	 * Creates a new Transform instance
	 * @param {TransformParameters} [options] - Transform configuration
	 * 
	 * @example
	 * ```javascript
	 * // Create identity transform
	 * const t1 = new Transform();
	 * 
	 * // Create custom transform
	 * const t2 = new Transform({
	 *     x: 100,    // Translate 100 units in x
	 *     y: 50,     // Translate 50 units in y
	 *     a: 45,     // Rotate 45 degrees
	 *     z: 2       // Scale by factor of 2
	 * });
	 * ```
	 */
	constructor(options) {
		Object.assign(this, { x: 0, y: 0, z: 1, a: 0, t: 0 });

		if (!this.t) this.t = performance.now();

		if (typeof (options) == 'object')
			Object.assign(this, options);
	}

	/**
	 * Creates a deep copy of the transform
	 * @returns {Transform} New transform with identical parameters
	 */
	copy() {
		let transform = new Transform();
		Object.assign(transform, this);
		return transform;
	}

	/**
	 * Applies transform to a point (x,y)
	 * Performs full affine transformation: scale, rotate, translate
	 * 
	 * @param {number} x - X coordinate to transform
	 * @param {number} y - Y coordinate to transform
	 * @returns {Point} Transformed point
	 * 
	 * @example
	 * ```javascript
	 * const transform = new Transform({x: 10, y: 20, a: 45, z: 2});
	 * const result = transform.apply(5, 5);
	 * // Returns rotated, scaled, and translated point
	 * ```
	 */
	apply(x, y) {
		//TODO! ROTATE
		let r = Transform.rotate(x, y, this.a);
		return {
			x: r.x * this.z + this.x,
			y: r.y * this.z + this.y
		}
	}

	/**
	 * Computes inverse transformation
	 * Creates transform that undoes this transform's effects
	 * @returns {Transform} Inverse transform
	 */
	inverse() {
		let r = Transform.rotate(this.x / this.z, this.y / this.z, -this.a);
		return new Transform({ x: -r.x, y: -r.y, z: 1 / this.z, a: -this.a, t: this.t });
	}

	/**
	 * Normalizes angle to range [0, 360]
	 * @param {number} a - Angle in degrees
	 * @returns {number} Normalized angle
	 * @static
	 */
	static normalizeAngle(a) {
		while (a > 360) a -= 360;
		while (a < 0) a += 360;
		return a;
	}

	/**
	 * Rotates point (x,y) by angle a around Z axis
	 * @param {number} x - X coordinate to rotate
	 * @param {number} y - Y coordinate to rotate
	 * @param {number} a - Rotation angle in degrees
	 * @returns {Point} Rotated point
	 * @static
	 */
	static rotate(x, y, a) {
		a = Math.PI * (a / 180);
		let ex = Math.cos(a) * x - Math.sin(a) * y;
		let ey = Math.sin(a) * x + Math.cos(a) * y;
		return { x: ex, y: ey };
	}

	/**
	 * Composes two transforms: this * transform
	 * Applies this transform first, then the provided transform
	 * 
	 * @param {Transform} transform - Transform to compose with
	 * @returns {Transform} Combined transformation
	 * 
	 * @example
	 * ```javascript
	 * const t1 = new Transform({x: 10, a: 45});
	 * const t2 = new Transform({z: 2});
	 * const combined = t1.compose(t2);
	 * // Results in rotation, then scale, then translation
	 * ```
	 */
	compose(transform) {
		let a = this.copy();
		let b = transform;
		a.z *= b.z;
		a.a += b.a;
		var r = Transform.rotate(a.x, a.y, b.a);
		a.x = r.x * b.z + b.x;
		a.y = r.y * b.z + b.y;
		return a;
	}

	/**
	 * Transforms a bounding box through this transform
	 * @param {BoundingBox} box - Box to transform
	 * @returns {BoundingBox} Transformed bounding box
	 */
	transformBox(lbox) {
		let box = new BoundingBox();
		for (let i = 0; i < 4; i++) {
			let c = lbox.corner(i);
			let p = this.apply(c.x, c.y);
			box.mergePoint(p);
		}
		return box;
	}

	/**
	 * Computes viewport bounds in image space
	 * Accounts for coordinate system differences between viewport and image
	 * 
	 * @param {Viewport} viewport - Current viewport
	 * @returns {BoundingBox} Bounds in image space
	 */
	getInverseBox(viewport) {
		let inverse = this.inverse();
		let corners = [
			{ x: viewport.x, y: viewport.y },
			{ x: viewport.x + viewport.dx, y: viewport.y },
			{ x: viewport.x, y: viewport.y + viewport.dy },
			{ x: viewport.x + viewport.dx, y: viewport.y + viewport.dy }
		];
		let box = new BoundingBox();
		for (let corner of corners) {
			let p = inverse.apply(corner.x - viewport.w / 2, -corner.y + viewport.h / 2);
			box.mergePoint(p);
		}
		return box;
	}

	/**
	 * Interpolates between two transforms
	 * @param {Transform} source - Starting transform
	 * @param {Transform} target - Ending transform
	 * @param {number} time - Current time for interpolation
	 * @param {EasingFunction} easing - Easing function type
	 * @returns {Transform} Interpolated transform
	 * @static
	 * 
	 * @example
	 * ```javascript
	 * const start = new Transform({x: 0, y: 0});
	 * const end = new Transform({x: 100, y: 100});
	 * const mid = Transform.interpolate(start, end, 500, 'ease-out');
	 * ```
	 */
	static interpolate(source, target, time, easing) { //FIXME STATIC
		console.assert(!isNaN(source.x));
		console.assert(!isNaN(target.x));
		const pos = new Transform();
		let dt = (target.t - source.t);
		if (time < source.t) {
			Object.assign(pos, source);
		} else if (time > target.t || dt < 0.001) {
			Object.assign(pos, target);
		} else {
			let tt = (time - source.t) / dt;
			switch (easing) {
				case 'ease-out': tt = 1 - Math.pow(1 - tt, 2); break;
				case 'ease-in-out': tt = tt < 0.5 ? 2 * tt * tt : 1 - Math.pow(-2 * tt + 2, 2) / 2; break;
			}
			let st = 1 - tt;
			for (let i of ['x', 'y', 'z', 'a'])
				pos[i] = (st * source[i] + tt * target[i]);
		}
		pos.t = time;
		return pos;
	}

	/**
	 * Generates WebGL projection matrix
	 * Combines transform with viewport for rendering
	 * 
	 * @param {Viewport} viewport - Current viewport
	 * @returns {number[]} 4x4 projection matrix in column-major order
	 */
	projectionMatrix(viewport) {
		let z = this.z;

		// In coords with 0 in lower left corner map x0 to -1, and x0+v.w to 1
		// In coords with 0 at screen center and x0 at 0, map -v.w/2 -> -1, v.w/2 -> 1 
		// With x0 != 0: x0 -> x0-v.w/2 -> -1, and x0+dx -> x0+v.dx-v.w/2 -> 1
		// Where dx is viewport width, while w is window width
		//0, 0 <-> viewport.x + viewport.dx/2 (if x, y =

		let zx = 2 / viewport.dx;
		let zy = 2 / viewport.dy;

		let dx = zx * this.x + (2 / viewport.dx) * (viewport.w / 2 - viewport.x) - 1;
		let dy = zy * this.y + (2 / viewport.dy) * (viewport.h / 2 - viewport.y) - 1;

		let a = Math.PI * this.a / 180;
		let matrix = [
			Math.cos(a) * zx * z, Math.sin(a) * zy * z, 0, 0,
			-Math.sin(a) * zx * z, Math.cos(a) * zy * z, 0, 0,
			0, 0, 1, 0,
			dx, dy, 0, 1];
		return matrix;
	}

	/**
	 * Converts scene coordinates to viewport coordinates
	 * @param {Viewport} viewport - Current viewport
	 * @param {APoint} p - Point in scene space
	 * @returns {APoint} Point in viewport space [0..w-1, 0..h-1]
	 */
	sceneToViewportCoords(viewport, p) { //FIXME Point is an array, but in other places it is an Object...
		return [p[0] * this.z + this.x - viewport.x + viewport.w / 2,
		p[1] * this.z - this.y + viewport.y + viewport.h / 2];
	}

	/**
	 * Converts viewport coordinates to scene coordinates
	 * @param {Viewport} viewport - Current viewport
	 * @param {APoint} p - Point in viewport space [0..w-1, 0..h-1]
	 * @returns {APoint} Point in scene space
	 */
	viewportToSceneCoords(viewport, p) {
		return [(p[0] + viewport.x - viewport.w / 2 - this.x) / this.z,
		(p[1] - viewport.y - viewport.h / 2 + this.y) / this.z];
	}

	/**
	 * Prints transform parameters for debugging
	 * @param {string} [str=""] - Prefix string
	 * @param {number} [precision=0] - Decimal precision
	 */
	print(str = "", precision = 0) {
		const p = precision;
		console.log(str + " x:" + this.x.toFixed(p) + ", y:" + this.y.toFixed(p) + ", z:" + this.z.toFixed(p) + ", a:" + this.a.toFixed(p) + ", t:" + this.t.toFixed(p));
	}
}

export { Transform }