UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

618 lines (550 loc) 17.7 kB
/** * Transformations object to store position, rotation and scale, that also support transformations inheritance. * * |-- copyright and license --| * @module Shaku * @file shaku\src\utils\transformation.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const MathHelper = require("./math_helper"); const TransformModes = require("./transform_modes"); const Matrix = require("./matrix"); const Vector2 = require("./vector2"); // some default values const _defaults = {}; _defaults.position = Vector2.zero(); _defaults.positionMode = TransformModes.Relative; _defaults.scale = Vector2.one(); _defaults.scaleMode = TransformModes.AxisAligned; _defaults.rotation = 0; _defaults.rotationMode = TransformModes.Relative; /** * Transformations helper class to store 2d position, rotation and scale. * Can also perform transformations inheritance, where we combine local with parent transformations. * * @example * // create local and world transformations * const transform = new Shaku.utils.Transformation(); * const worldTransform = new Shaku.utils.Transformation(); * // set offset to world transofm and rotation to local transform * worldTransform.setPosition({x: 100, y:50}); * transform.setRotation(5); * // combine transformations and convert to a matrix * const combined = Shaku.utils.Transformation.combine(transform, worldTransform); * const matrix = combined.asMatrix(); */ class Transformation { /** * Create the transformations. * @param {Vector2} position Optional position value. * @param {Number} rotation Optional rotation value. * @param {Vector2} scale Optional sscale value. */ constructor(position, rotation, scale) { /** * @private * Transformation local position. * @name Transformation#position * @type {Vector2} */ this._position = position || _defaults.position.clone(); /** * @private * Position transformation mode. * @name Transformation#positionMode * @type {TransformModes} */ this._positionMode = _defaults.positionMode; /** * @private * Transformation local scale. * @name Transformation#scale * @type {Vector2} */ this._scale = scale || _defaults.scale.clone(); /** * @private * Scale transformation mode. * @name Transformation#scaleMode * @type {TransformModes} */ this._scaleMode = _defaults.scaleMode; /** * @private * Transformation local rotation. * @name Transformation#rotation * @type {Number} */ this._rotation = rotation || _defaults.rotation; /** * @private * Rotation transformation mode. * @name Transformation#rotationMode * @type {TransformModes} */ this._rotationMode = _defaults.rotationMode; /** * Method to call when this transformation change. * Function params: transformation instance (this), properties changed (boolean), transform modes changed (boolean). * @name Transformation#onChange * @type {Function} */ this.onChange = null; } /** * Get position. * @returns {Vector2} Position. */ getPosition() { return this._position.clone(); } /** * Get position transformations mode. * @returns {TransformModes} Position transformation mode. */ getPositionMode() { return this._positionMode; } /** * Set position. * @param {Vector2} value New position. * @returns {Transformation} this. */ setPosition(value) { if (this._position.equals(value)) { return; } this._position.copy(value); this.#_markDirty(true, false); return this; } /** * Set position X value. * @param {Number} value New position.x value. * @returns {Transformation} this. */ setPositionX(value) { if (this._position.x === value) { return; } this._position.x = value; this.#_markDirty(true, false); return this; } /** * Set position Y value. * @param {Number} value New position.y value. * @returns {Transformation} this. */ setPositionY(value) { if (this._position.y === value) { return; } this._position.y = value; this.#_markDirty(true, false); return this; } /** * Move position by a given vector. * @param {Vector2} value Vector to move position by. * @returns {Transformation} this. */ move(value) { this._position.addSelf(value); this.#_markDirty(true, false); return this; } /** * Set position transformations mode. * @param {TransformModes} value Position transformation mode. * @returns {Transformation} this. */ setPositionMode(value) { if (this._positionMode === value) { return; } this._positionMode = value; this.#_markDirty(false, true); return this; } /** * Get scale. * @returns {Vector2} Scale. */ getScale() { return this._scale.clone(); } /** * Get scale transformations mode. * @returns {TransformModes} Scale transformation mode. */ getScaleMode() { return this._scaleMode; } /** * Set scale. * @param {Vector2} value New scale. * @returns {Transformation} this. */ setScale(value) { if (this._scale.equals(value)) { return; } this._scale.copy(value); this.#_markDirty(true, false); return this; } /** * Set scale X value. * @param {Number} value New scale.x value. * @returns {Transformation} this. */ setScaleX(value) { if (this._scale.x === value) { return; } this._scale.x = value; this.#_markDirty(true, false); return this; } /** * Set scale Y value. * @param {Number} value New scale.y value. * @returns {Transformation} this. */ setScaleY(value) { if (this._scale.y === value) { return; } this._scale.y = value; this.#_markDirty(true, false); return this; } /** * Scale by a given vector. * @param {Vector2} value Vector to scale by. * @returns {Transformation} this. */ scale(value) { this._scale.mulSelf(value); this.#_markDirty(true, false); return this; } /** * Set scale transformations mode. * @param {TransformModes} value Scale transformation mode. * @returns {Transformation} this. */ setScaleMode(value) { if (this._scaleMode === value) { return; } this._scaleMode = value; this.#_markDirty(false, true); return this; } /** * Get rotation. * @returns {Number} rotation. */ getRotation() { return this._rotation; } /** * Get rotation as degrees. * @returns {Number} rotation. */ getRotationDegrees() { return MathHelper.toDegrees(this._rotation); } /** * Get rotation as degrees, wrapped between 0 to 360. * @returns {Number} rotation. */ getRotationDegreesWrapped() { let ret = this.getRotationDegrees(); return MathHelper.wrapDegrees(ret); } /** * Get rotation transformations mode. * @returns {TransformModes} Rotation transformations mode. */ getRotationMode() { return this._rotationMode; } /** * Set rotation. * @param {Number} value New rotation. * @param {Boolean} wrap If true, will wrap value if out of possible range. * @returns {Transformation} this. */ setRotation(value, wrap) { if (this._rotation === value) { return; } this._rotation = value; if (wrap && ((this._rotation < 0) || (this._rotation > (Math.PI * 2)))) { this._rotation = Math.atan2(Math.sin(this._rotation), Math.cos(this._rotation)); } this.#_markDirty(true, false); return this; } /** * Rotate transformation by given radians. * @param {Number} value Rotate value in radians. * @param {Boolean} wrap If true, will wrap value if out of possible range. * @returns {Transformation} this. */ rotate(value, wrap) { this.setRotation(this._rotation + value, wrap); return this; } /** * Set rotation as degrees. * @param {Number} value New rotation. * @param {Boolean} wrap If true, will wrap value if out of possible range. * @returns {Transformation} this. */ setRotationDegrees(value, wrap) { const rads = MathHelper.toRadians(value, wrap); return this.setRotation(rads); } /** * Rotate transformation by given degrees. * @param {Number} value Rotate value in degrees. * @returns {Transformation} this. */ rotateDegrees(value) { this._rotation += MathHelper.toRadians(value); this.#_markDirty(true, false); return this; } /** * Set rotation transformations mode. * @param {TransformModes} value Rotation transformation mode. * @returns {Transformation} this. */ setRotationMode(value) { if (this._rotationMode === value) { return; } this._rotationMode = value; this.#_markDirty(false, true); return this; } /** * Notify about changes in values. * @param {Boolean} localTransform Local transformations changed. * @param {Boolean} transformationModes Transformation modes changed. */ #_markDirty(localTransform, transformationModes) { this._matrix = null; if (this.onChange) { this.onChange(this, localTransform, transformationModes); } } /** * Check if this transformation equals another. * @param {Transformation} other Other transform to compare to. * @returns {Boolean} True if equal, false otherwise. */ equals(other) { return (this._rotation === other._rotation) && (this._position.equals(other._position)) && (this._scale.equals(other._scale)); } /** * Return a clone of this transformations. * @returns {Transformation} Cloned transformations. */ clone() { // create cloned transformation const ret = new Transformation(this._position.clone(), this._rotation, this._scale.clone()); // copy transformation modes ret._rotationMode = this._rotationMode; ret._positionMode = this._positionMode; ret._scaleMode = this._scaleMode; // clone matrix ret._matrix = this._matrix; // return clone return ret; } /** * Serialize this transformation into a dictionary. */ serialize() { const ret = {}; // position + mode if (!this._position.equals(_defaults.position)) { ret.pos = this._position; } if (this._positionMode !== _defaults.positionMode) { ret.posm = this._positionMode; } // scale + mode if (!this._scale.equals(_defaults.scale)) { ret.scl = this._scale; } if (this._scaleMode !== _defaults.scaleMode) { ret.sclm = this._scaleMode; } // rotation + mode if (this._rotation !== _defaults.rotation) { ret.rot = Math.floor(MathHelper.toDegrees(this._rotation)); } if (this._rotationMode !== _defaults.rotationMode) { ret.rotm = this._rotationMode; } return ret; } /** * Deserialize this transformation from a dictionary. * @param {*} data Data to set. */ deserialize(data) { this._position.copy(data.pos || _defaults.position); this._scale.copy(data.scl || _defaults.scale); this._rotation = MathHelper.toRadians(data.rot || _defaults.rotation); this._positionMode = data.posm || _defaults.positionMode; this._scaleMode = data.sclm || _defaults.scaleMode; this._rotationMode = data.rotm || _defaults.rotationMode; this.#_markDirty(true, true); } /** * Create and return a transformation matrix. * @returns {Matrix} New transformation matrix. */ asMatrix() { // return cached if (this._matrix) { return this._matrix; } // get matrix type and create list of matrices to combine let matrices = []; // apply position if ((this._position.x !== 0) || (this._position.y !== 0)) { matrices.push(Matrix.createTranslation(this._position.x, this._position.y, 0)); } // apply rotation if (this._rotation) { matrices.push(Matrix.createRotationZ(-this._rotation)); } // apply scale if ((this._scale.x !== 1) || (this._scale.y !== 1)) { matrices.push(Matrix.createScale(this._scale.x, this._scale.y)); } // no transformations? identity matrix if (matrices.length === 0) { this._matrix = Matrix.identity; } // only one transformation? return it else if (matrices.length === 1) { this._matrix = matrices[0]; } // more than one transformation? combine matrices else { this._matrix = Matrix.multiplyMany(matrices); } // return matrix return this._matrix; } /** * Combine child transformations with parent transformations. * @param {Transformation} child Child transformations. * @param {Transformation} parent Parent transformations. * @returns {Transformation} Combined transformations. */ static combine(child, parent) { var position = combineVector(child._position, parent._position, parent, child._positionMode); var scale = combineVectorMul(child._scale, parent._scale, parent, child._scaleMode); var rotation = combineScalar(child._rotation, parent._rotation, parent, child._rotationMode); return new Transformation(position, rotation, scale); } } /** * Combine child scalar value with parent using a transformation mode. * @param {Number} childValue Child value. * @param {Number} parentValue Parent value. * @param {Transformation} parent Parent transformations. * @param {TransformModes} mode Transformation mode. * @returns {Number} Combined value. */ function combineScalar(childValue, parentValue, parent, mode) { switch (mode) { case TransformModes.Absolute: return childValue; case TransformModes.AxisAligned: case TransformModes.Relative: return parentValue + childValue; default: throw new Error("Unknown transform mode!"); } } /** * Combine child vector value with parent using a transformation mode. * @param {Vector2} childValue Child value. * @param {Vector2} parentValue Parent value. * @param {Transformation} parent Parent transformations. * @param {TransformModes} mode Transformation mode. * @returns {Vector2} Combined value. */ function combineVector(childValue, parentValue, parent, mode) { switch (mode) { case TransformModes.Absolute: return childValue.clone(); case TransformModes.AxisAligned: return parentValue.add(childValue); case TransformModes.Relative: return parentValue.add(childValue.rotatedByRadians(parent._rotation)); default: throw new Error("Unknown transform mode!"); } } /** * Combine child vector value with parent using a transformation mode and multiplication. * @param {Vector2} childValue Child value. * @param {Vector2} parentValue Parent value. * @param {Transformation} parent Parent transformations. * @param {TransformModes} mode Transformation mode. * @returns {Vector2} Combined value. */ function combineVectorMul(childValue, parentValue, parent, mode) { switch (mode) { case TransformModes.Absolute: return childValue.clone(); case TransformModes.AxisAligned: return parentValue.mul(childValue); case TransformModes.Relative: return parentValue.mul(childValue.rotatedByRadians(parent._rotation)); default: throw new Error("Unknown transform mode!"); } } // export the transformation object module.exports = Transformation;