diff --git a/CHANGELOG.md b/CHANGELOG.md index d60515cfbf3..23e03fc632a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): Conver Geometry and Origin to classes/e6/ts [#8390](https://github.com/fabricjs/fabric.js/pull/8390) - ci(): build stats report [#8395](https://github.com/fabricjs/fabric.js/pull/8395) - chore(TS): convert object to es6 class [#8322](https://github.com/fabricjs/fabric.js/pull/8322) - docs(): guides follow up, feature request template [#8379](https://github.com/fabricjs/fabric.js/pull/8379) diff --git a/index.js b/index.js index f2452d3ffaa..dbc3ed08da1 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import './HEADER'; -// import './lib/event'), // optional gestures +// import './lib/event', // optional gestures import './src/mixins/collection.mixin'; import './src/util/misc/misc'; // import './src/util/named_accessors.mixin'; i would imagine dead forever or proper setters/getters @@ -18,8 +18,6 @@ import './src/mixins/canvas_dataurl_exporter.mixin'; import './src/mixins/canvas_serialization.mixin'; // optiona serialization import './src/mixins/canvas_gestures.mixin'; // optional gestures import './src/shapes/object.class'; -import './src/mixins/object_origin.mixin'; -import './src/mixins/object_geometry.mixin'; import './src/mixins/object_ancestry.mixin'; import './src/mixins/object_stacking.mixin'; import './src/mixins/object.svg_export'; diff --git a/rollup.config.js b/rollup.config.js index 5399a38e676..03bdc93c3bf 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,7 +5,7 @@ import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; import { terser } from 'rollup-plugin-terser'; import ts from 'rollup-plugin-ts'; -const runStats = process.env.BUILD_STATS; +const runStats = Number(process.env.BUILD_STATS); let analyzed = false; const splitter = /\n|\s|,/g; diff --git a/scripts/buildStats.mjs b/scripts/buildStats.mjs index 7bf1061e007..7460fdf7fcf 100644 --- a/scripts/buildStats.mjs +++ b/scripts/buildStats.mjs @@ -9,20 +9,9 @@ const MAX_COMMENT_CHARS = 65536; const INACCURATE_COMMENT = '\n*inaccurate, see [link](https://github.com/doesdev/rollup-plugin-analyzer#why-is-the-reported-size-not-the-same-as-the-file-on-disk)'; -function getSign(n) { - switch (Math.sign(n)) { - case 0: - return ''; - case 1: - return '+'; - case -1: - return '-'; - } -} - function printSize(a, b) { const diff = b - a; - return `${b} (${getSign(diff)}${diff})`; + return `${b} (${Math.sign(diff) > 0 ? '+' : ''}${diff})`; } function printSizeByte(a, b) { diff --git a/src/intersection.class.ts b/src/intersection.class.ts index c5c0a0e525f..31a270e357c 100644 --- a/src/intersection.class.ts +++ b/src/intersection.class.ts @@ -4,7 +4,7 @@ import { fabric } from '../HEADER'; /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ -type IntersectionType = 'Intersection' | 'Coincident' | 'Parallel'; +export type IntersectionType = 'Intersection' | 'Coincident' | 'Parallel'; /** * **Assuming `T`, `A`, `B` are points on the same line**, diff --git a/src/mixins/itext_click_behavior.mixin.ts b/src/mixins/itext_click_behavior.mixin.ts index 124d7a51a0e..33ebfe5f172 100644 --- a/src/mixins/itext_click_behavior.mixin.ts +++ b/src/mixins/itext_click_behavior.mixin.ts @@ -1,4 +1,7 @@ //@ts-nocheck +import { invertTransform, transformPoint } from '../util/misc/matrix'; +import { Point } from '../point.class'; + (function (global) { var fabric = global.fabric; fabric.util.object.extend( @@ -226,6 +229,20 @@ } }, + /** + * Returns coordinates of a pointer relative to object's top left corner in object's plane + * @param {Event} e Event to operate upon + * @param {Object} [pointer] Pointer to operate upon (instead of event) + * @return {Point} Coordinates of a pointer (x, y) + */ + getLocalPointer: function (e: Event, pointer?: IPoint): Point { + const thePointer = pointer || this.canvas.getPointer(e); + return transformPoint( + thePointer, + invertTransform(this.calcTransformMatrix()) + ).add(new Point(this.width / 2, this.height / 2)); + }, + /** * Returns index of a character corresponding to where an object was clicked * @param {Event} e Event object diff --git a/src/mixins/object_geometry.mixin.ts b/src/mixins/object_geometry.mixin.ts index 4ca0b4393f2..cfec785a260 100644 --- a/src/mixins/object_geometry.mixin.ts +++ b/src/mixins/object_geometry.mixin.ts @@ -1,870 +1,897 @@ -//@ts-nocheck - +import type { TBBox, TDegree, TMat2D, TOriginX, TOriginY } from '../typedefs'; +import { iMatrix } from '../constants'; import { Intersection } from '../intersection.class'; -import { Point } from '../point.class'; +import { IPoint, Point } from '../point.class'; import { FabricObject } from '../shapes/object.class'; +import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; +import { cos } from '../util/misc/cos'; +import { + calcRotateMatrix, + composeMatrix, + invertTransform, + multiplyTransformMatrices, + qrDecompose, + transformPoint, +} from '../util/misc/matrix'; +import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { sin } from '../util/misc/sin'; +import { Canvas } from '../__types__'; +import { ObjectOrigin } from './object_origin.mixin'; + +type TCornerPoint = { + tl: Point; + tr: Point; + bl: Point; + br: Point; +}; + +type TOCoord = IPoint & { + corner: TCornerPoint; +}; + +type TLineDescriptor = { + o: Point; + d: Point; +}; + +type TBBoxLines = { + topline: TLineDescriptor; + leftline: TLineDescriptor; + bottomline: TLineDescriptor; + rightline: TLineDescriptor; +}; + +type TMatrixCache = { + key: string; + value: TMat2D; +}; + +type TControlSet = Record; + +type TACoords = TCornerPoint; + +export class ObjectGeometry extends ObjectOrigin { + /** + * When true, an object is rendered as flipped horizontally + * @type Boolean + * @default false + */ + flipX: boolean; + + /** + * When true, an object is rendered as flipped vertically + * @type Boolean + * @default false + */ + flipY: boolean; + + /** + * Padding between object and its controlling borders (in pixels) + * @type Number + * @default 0 + */ + padding: number; + + /** + * Describe object's corner position in canvas element coordinates. + * properties are depending on control keys and padding the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from the controls positionHandler and are used + * to draw and locate controls + * @memberOf fabric.Object.prototype + */ + oCoords: Record = {}; + + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are useful to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcACoords(); + * @memberOf fabric.Object.prototype + */ + aCoords: TACoords; + + /** + * Describe object's corner position in canvas element coordinates. + * includes padding. Used of object detection. + * set and refreshed with setCoords. + * Those could go away + * @todo investigate how to get rid of those + * @memberOf fabric.Object.prototype + */ + lineCoords: TCornerPoint; + + /** + * storage cache for object transform matrix + */ + ownMatrixCache?: TMatrixCache; + + /** + * storage cache for object full transform matrix + */ + matrixCache?: TMatrixCache; + + /** + * custom controls interface + * controls are added by default_controls.js + */ + controls: TControlSet; + + /** + * Object containing this object. + * can influence its size and position + */ + canvas?: Canvas; + + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + getX(): number { + return this.getXY().x; + } -(function (global) { - function arrayFromCoords(coords) { - return [ - new Point(coords.tl.x, coords.tl.y), - new Point(coords.tr.x, coords.tr.y), - new Point(coords.br.x, coords.br.y), - new Point(coords.bl.x, coords.bl.y), - ]; - } - - var fabric = global.fabric, - util = fabric.util, - degreesToRadians = util.degreesToRadians, - multiplyMatrices = util.multiplyTransformMatrices, - transformPoint = util.transformPoint; - - util.object.extend( - FabricObject.prototype, - /** @lends FabricObject.prototype */ { - /** - * Describe object's corner position in canvas element coordinates. - * properties are depending on control keys and padding the main controls. - * each property is an object with x, y and corner. - * The `corner` property contains in a similar manner the 4 points of the - * interactive area of the corner. - * The coordinates depends from the controls positionHandler and are used - * to draw and locate controls - * @memberOf fabric.Object.prototype - */ - oCoords: null, - - /** - * Describe object's corner position in canvas object absolute coordinates - * properties are tl,tr,bl,br and describe the four main corner. - * each property is an object with x, y, instance of Fabric.Point. - * The coordinates depends from this properties: width, height, scaleX, scaleY - * skewX, skewY, angle, strokeWidth, top, left. - * Those coordinates are useful to understand where an object is. They get updated - * with oCoords but they do not need to be updated when zoom or panning change. - * The coordinates get updated with @method setCoords. - * You can calculate them without updating with @method calcACoords(); - * @memberOf fabric.Object.prototype - */ - aCoords: null, - - /** - * Describe object's corner position in canvas element coordinates. - * includes padding. Used of object detection. - * set and refreshed with setCoords. - * @memberOf fabric.Object.prototype - */ - lineCoords: null, - - /** - * storage for object transform matrix - */ - ownMatrixCache: null, - - /** - * storage for object full transform matrix - */ - matrixCache: null, - - /** - * custom controls interface - * controls are added by default_controls.js - */ - controls: {}, - - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane - */ - getX: function () { - return this.getXY().x; - }, + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane + */ + setX(value: number) { + this.setXY(this.getXY().setX(value)); + } - /** - * @param {number} value x position according to object's {@link fabric.Object#originX} property in canvas coordinate plane - */ - setX: function (value) { - this.setXY(this.getXY().setX(value)); - }, + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + getY(): number { + return this.getXY().y; + } - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#getX} - */ - getRelativeX: function () { - return this.left; - }, + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane + */ + setY(value: number) { + this.setXY(this.getXY().setY(value)); + } - /** - * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ - * if parent is canvas then this method is identical to {@link fabric.Object#setX} - */ - setRelativeX: function (value) { - this.left = value; - }, + /** + * @returns {number} x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getX} + */ + getRelativeX(): number { + return this.left; + } - /** - * @returns {number} y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane - */ - getY: function () { - return this.getXY().y; - }, + /** + * @param {number} value x position according to object's {@link fabric.Object#originX} property in parent's coordinate plane\ + * if parent is canvas then this method is identical to {@link fabric.Object#setX} + */ + setRelativeX(value: number) { + this.left = value; + } - /** - * @param {number} value y position according to object's {@link fabric.Object#originY} property in canvas coordinate plane - */ - setY: function (value) { - this.setXY(this.getXY().setY(value)); - }, + /** + * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#getY} + */ + getRelativeY(): number { + return this.top; + } - /** - * @returns {number} y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#getY} - */ - getRelativeY: function () { - return this.top; - }, + /** + * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ + * if parent is canvas then this property is identical to {@link fabric.Object#setY} + */ + setRelativeY(value: number) { + this.top = value; + } - /** - * @param {number} value y position according to object's {@link fabric.Object#originY} property in parent's coordinate plane\ - * if parent is canvas then this property is identical to {@link fabric.Object#setY} - */ - setRelativeY: function (value) { - this.top = value; - }, + /** + * @returns {Point} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane + */ + getXY(): Point { + const relativePosition = this.getRelativeXY(); + return this.group + ? transformPoint(relativePosition, this.group.calcTransformMatrix()) + : relativePosition; + } - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in canvas coordinate plane - */ - getXY: function () { - var relativePosition = this.getRelativeXY(); - return this.group - ? fabric.util.transformPoint( - relativePosition, - this.group.calcTransformMatrix() - ) - : relativePosition; - }, + /** + * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate. + * You can specify {@link fabric.Object#originX} and {@link fabric.Object#originY} values, + * that otherwise are the object's current values. + * @example Set object's bottom left corner to point (5,5) on canvas + * object.setXY(new Point(5, 5), 'left', 'bottom'). + * @param {Point} point position in canvas coordinate plane + * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setXY(point: Point, originX?: TOriginX, originY?: TOriginY) { + if (this.group) { + point = transformPoint( + point, + invertTransform(this.group.calcTransformMatrix()) + ); + } + this.setRelativeXY(point, originX, originY); + } - /** - * Set an object position to a particular point, the point is intended in absolute ( canvas ) coordinate. - * You can specify {@link fabric.Object#originX} and {@link fabric.Object#originY} values, - * that otherwise are the object's current values. - * @example Set object's bottom left corner to point (5,5) on canvas - * object.setXY(new Point(5, 5), 'left', 'bottom'). - * @param {Point} point position in canvas coordinate plane - * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setXY: function (point, originX, originY) { - if (this.group) { - point = fabric.util.transformPoint( - point, - fabric.util.invertTransform(this.group.calcTransformMatrix()) - ); - } - this.setRelativeXY(point, originX, originY); - }, + /** + * @returns {Point} x,y position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + */ + getRelativeXY(): Point { + return new Point(this.left, this.top); + } - /** - * @returns {number} x position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane - */ - getRelativeXY: function () { - return new Point(this.left, this.top); - }, + /** + * As {@link fabric.Object#setXY}, but in current parent's coordinate plane ( the current group if any or the canvas) + * @param {Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane + * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' + */ + setRelativeXY(point: Point, originX?: TOriginX, originY?: TOriginY) { + this.setPositionByOrigin( + point, + originX || this.originX, + originY || this.originY + ); + } - /** - * As {@link fabric.Object#setXY}, but in current parent's coordinate plane ( the current group if any or the canvas) - * @param {Point} point position according to object's {@link fabric.Object#originX} {@link fabric.Object#originY} properties in parent's coordinate plane - * @param {'left'|'center'|'right'|number} [originX] Horizontal origin: 'left', 'center' or 'right' - * @param {'top'|'center'|'bottom'|number} [originY] Vertical origin: 'top', 'center' or 'bottom' - */ - setRelativeXY: function (point, originX, originY) { - this.setPositionByOrigin( - point, - originX || this.originX, - originY || this.originY - ); - }, + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * @param {boolean} absolute will return aCoords if true or lineCoords + * @param {boolean} calculate will calculate the coords or use the one + * that are attached to the object instance + * @return {Object} {tl, tr, br, bl} points + */ + _getCoords(absolute = false, calculate = false): TCornerPoint { + if (calculate) { + return absolute ? this.calcACoords() : this.calcLineCoords(); + } + if (!this.aCoords || !this.lineCoords) { + this.setCoords(true); + } + return absolute ? this.aCoords : this.lineCoords; + } - /** - * return correct set of coordinates for intersection - * this will return either aCoords or lineCoords. - * @param {Boolean} absolute will return aCoords if true or lineCoords - * @return {Object} {tl, tr, br, bl} points - */ - _getCoords: function (absolute, calculate) { - if (calculate) { - return absolute ? this.calcACoords() : this.calcLineCoords(); - } - if (!this.aCoords || !this.lineCoords) { - this.setCoords(true); - } - return absolute ? this.aCoords : this.lineCoords; - }, + /** + * return correct set of coordinates for intersection + * this will return either aCoords or lineCoords. + * The coords are returned in an array. + * @param {boolean} absolute will return aCoords if true or lineCoords + * @param {boolean} calculate will return aCoords if true or lineCoords + * @return {Array} [tl, tr, br, bl] of points + */ + getCoords(absolute = false, calculate = false): Point[] { + const { tl, tr, br, bl } = this._getCoords(absolute, calculate); + const coords = [tl, tr, br, bl]; + if (this.group) { + const t = this.group.calcTransformMatrix(); + return coords.map((p) => transformPoint(p, t)); + } + return coords; + } - /** - * return correct set of coordinates for intersection - * this will return either aCoords or lineCoords. - * The coords are returned in an array. - * @return {Array} [tl, tr, br, bl] of points - */ - getCoords: function (absolute, calculate) { - var coords = arrayFromCoords(this._getCoords(absolute, calculate)); - if (this.group) { - var t = this.group.calcTransformMatrix(); - return coords.map(function (p) { - return util.transformPoint(p, t); - }); - } - return coords; - }, + /** + * Checks if object intersects with an area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with an area formed by 2 points + */ + intersectsWithRect( + pointTL: Point, + pointBR: Point, + absolute: boolean, + calculate: boolean + ): boolean { + const coords = this.getCoords(absolute, calculate), + intersection = Intersection.intersectPolygonRectangle( + coords, + pointTL, + pointBR + ); + return intersection.status === 'Intersection'; + } - /** - * Checks if object intersects with an area formed by 2 points - * @param {Object} pointTL top-left point of area - * @param {Object} pointBR bottom-right point of area - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object intersects with an area formed by 2 points - */ - intersectsWithRect: function (pointTL, pointBR, absolute, calculate) { - var coords = this.getCoords(absolute, calculate), - intersection = fabric.Intersection.intersectPolygonRectangle( - coords, - pointTL, - pointBR - ); - return intersection.status === 'Intersection'; - }, + /** + * Checks if object intersects with another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object intersects with another object + */ + intersectsWithObject( + other: FabricObject, + absolute: boolean, + calculate: boolean + ): boolean { + const intersection = Intersection.intersectPolygonPolygon( + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) + ); + + return ( + intersection.status === 'Intersection' || + intersection.status === 'Coincident' || + other.isContainedWithinObject(this, absolute, calculate) || + this.isContainedWithinObject(other, absolute, calculate) + ); + } - /** - * Checks if object intersects with another object - * @param {Object} other Object to test - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object intersects with another object - */ - intersectsWithObject: function (other, absolute, calculate) { - var intersection = Intersection.intersectPolygonPolygon( - this.getCoords(absolute, calculate), - other.getCoords(absolute, calculate) - ); - - return ( - intersection.status === 'Intersection' || - intersection.status === 'Coincident' || - other.isContainedWithinObject(this, absolute, calculate) || - this.isContainedWithinObject(other, absolute, calculate) - ); - }, + /** + * Checks if object is fully contained within area of another object + * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area of another object + */ + isContainedWithinObject( + other: ObjectGeometry, + absolute: boolean, + calculate: boolean + ): boolean { + const points = this.getCoords(absolute, calculate), + otherCoords = absolute ? other.aCoords : other.lineCoords, + lines = other._getImageLines(otherCoords); + for (let i = 0; i < 4; i++) { + if (!other.containsPoint(points[i], lines)) { + return false; + } + } + return true; + } - /** - * Checks if object is fully contained within area of another object - * @param {Object} other Object to test - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is fully contained within area of another object - */ - isContainedWithinObject: function (other, absolute, calculate) { - var points = this.getCoords(absolute, calculate), - otherCoords = absolute ? other.aCoords : other.lineCoords, - i = 0, - lines = other._getImageLines(otherCoords); - for (; i < 4; i++) { - if (!other.containsPoint(points[i], lines)) { - return false; - } - } - return true; - }, + /** + * Checks if object is fully contained within area formed by 2 points + * @param {Object} pointTL top-left point of area + * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within area formed by 2 points + */ + isContainedWithinRect( + pointTL: Point, + pointBR: Point, + absolute: boolean, + calculate: boolean + ): boolean { + const boundingRect = this.getBoundingRect(absolute, calculate); + return ( + boundingRect.left >= pointTL.x && + boundingRect.left + boundingRect.width <= pointBR.x && + boundingRect.top >= pointTL.y && + boundingRect.top + boundingRect.height <= pointBR.y + ); + } - /** - * Checks if object is fully contained within area formed by 2 points - * @param {Object} pointTL top-left point of area - * @param {Object} pointBR bottom-right point of area - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is fully contained within area formed by 2 points - */ - isContainedWithinRect: function (pointTL, pointBR, absolute, calculate) { - var boundingRect = this.getBoundingRect(absolute, calculate); - - return ( - boundingRect.left >= pointTL.x && - boundingRect.left + boundingRect.width <= pointBR.x && - boundingRect.top >= pointTL.y && - boundingRect.top + boundingRect.height <= pointBR.y - ); - }, + /** + * Checks if point is inside the object + * @param {Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if point is inside the object + */ + containsPoint( + point: Point, + lines: TBBoxLines | undefined, + absolute = false, + calculate = false + ): boolean { + const coords = this._getCoords(absolute, calculate), + imageLines = lines || this._getImageLines(coords), + xPoints = this._findCrossPoints(point, imageLines); + // if xPoints is odd then point is inside the object + return xPoints !== 0 && xPoints % 2 === 1; + } - /** - * Checks if point is inside the object - * @param {Point} point Point to check against - * @param {Object} [lines] object returned from @method _getImageLines - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if point is inside the object - */ - containsPoint: function (point, lines, absolute, calculate) { - var coords = this._getCoords(absolute, calculate), - lines = lines || this._getImageLines(coords), - xPoints = this._findCrossPoints(point, lines); - // if xPoints is odd then point is inside the object - return xPoints !== 0 && xPoints % 2 === 1; - }, + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appears on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords + * @return {Boolean} true if object is fully or partially contained within canvas + */ + isOnScreen(calculate = false): boolean { + if (!this.canvas) { + return false; + } + const { tl, br } = this.canvas.vptCoords; + const points = this.getCoords(true, calculate); + // if some point is on screen, the object is on screen. + if ( + points.some( + (point) => + point.x <= br.x && + point.x >= tl.x && + point.y <= br.y && + point.y >= tl.y + ) + ) { + return true; + } + // no points on screen, check intersection with absolute coordinates + if (this.intersectsWithRect(tl, br, true, calculate)) { + return true; + } + return this._containsCenterOfCanvas(tl, br, calculate); + } - /** - * Checks if object is contained within the canvas with current viewportTransform - * the check is done stopping at first point that appears on screen - * @param {Boolean} [calculate] use coordinates of current position instead of .aCoords - * @return {Boolean} true if object is fully or partially contained within canvas - */ - isOnScreen: function (calculate) { - if (!this.canvas) { - return false; - } - var pointTL = this.canvas.vptCoords.tl, - pointBR = this.canvas.vptCoords.br; - var points = this.getCoords(true, calculate); - // if some point is on screen, the object is on screen. - if ( - points.some(function (point) { - return ( - point.x <= pointBR.x && - point.x >= pointTL.x && - point.y <= pointBR.y && - point.y >= pointTL.y - ); - }) - ) { - return true; - } - // no points on screen, check intersection with absolute coordinates - if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { - return true; - } - return this._containsCenterOfCanvas(pointTL, pointBR, calculate); - }, + /** + * Checks if the object contains the midpoint between canvas extremities + * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen + * @private + * @param {Point} pointTL Top Left point + * @param {Point} pointBR Top Right point + * @param {Boolean} calculate use coordinates of current position instead of .oCoords + * @return {Boolean} true if the object contains the point + */ + _containsCenterOfCanvas( + pointTL: Point, + pointBR: Point, + calculate: boolean + ): boolean { + // worst case scenario the object is so big that contains the screen + const centerPoint = pointTL.midPointFrom(pointBR); + return this.containsPoint(centerPoint, undefined, true, calculate); + } - /** - * Checks if the object contains the midpoint between canvas extremities - * Does not make sense outside the context of isOnScreen and isPartiallyOnScreen - * @private - * @param {Fabric.Point} pointTL Top Left point - * @param {Fabric.Point} pointBR Top Right point - * @param {Boolean} calculate use coordinates of current position instead of .oCoords - * @return {Boolean} true if the object contains the point - */ - _containsCenterOfCanvas: function (pointTL, pointBR, calculate) { - // worst case scenario the object is so big that contains the screen - var centerPoint = { - x: (pointTL.x + pointBR.x) / 2, - y: (pointTL.y + pointBR.y) / 2, - }; - if (this.containsPoint(centerPoint, null, true, calculate)) { - return true; - } - return false; - }, + /** + * Checks if object is partially contained within the canvas with current viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is partially contained within canvas + */ + isPartiallyOnScreen(calculate: boolean): boolean { + if (!this.canvas) { + return false; + } + const { tl, br } = this.canvas.vptCoords; + if (this.intersectsWithRect(tl, br, true, calculate)) { + return true; + } + const allPointsAreOutside = this.getCoords(true, calculate).every( + (point) => + (point.x >= br.x || point.x <= tl.x) && + (point.y >= br.y || point.y <= tl.y) + ); + return ( + allPointsAreOutside && this._containsCenterOfCanvas(tl, br, calculate) + ); + } - /** - * Checks if object is partially contained within the canvas with current viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords - * @return {Boolean} true if object is partially contained within canvas - */ - isPartiallyOnScreen: function (calculate) { - if (!this.canvas) { - return false; - } - var pointTL = this.canvas.vptCoords.tl, - pointBR = this.canvas.vptCoords.br; - if (this.intersectsWithRect(pointTL, pointBR, true, calculate)) { - return true; - } - var allPointsAreOutside = this.getCoords(true, calculate).every( - function (point) { - return ( - (point.x >= pointBR.x || point.x <= pointTL.x) && - (point.y >= pointBR.y || point.y <= pointTL.y) - ); - } - ); - return ( - allPointsAreOutside && - this._containsCenterOfCanvas(pointTL, pointBR, calculate) - ); - }, + /** + * Method that returns an object with the object edges in it, given the coordinates of the corners + * @private + * @param {Object} oCoords Coordinates of the object corners + */ + _getImageLines({ tl, tr, bl, br }: TCornerPoint): TBBoxLines { + const lines = { + topline: { + o: tl, + d: tr, + }, + rightline: { + o: tr, + d: br, + }, + bottomline: { + o: br, + d: bl, + }, + leftline: { + o: bl, + d: tl, + }, + }; + + // // debugging + // if (this.canvas.contextTop) { + // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + // + // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + // } + + return lines; + } - /** - * Method that returns an object with the object edges in it, given the coordinates of the corners - * @private - * @param {Object} oCoords Coordinates of the object corners - */ - _getImageLines: function (oCoords) { - var lines = { - topline: { - o: oCoords.tl, - d: oCoords.tr, - }, - rightline: { - o: oCoords.tr, - d: oCoords.br, - }, - bottomline: { - o: oCoords.br, - d: oCoords.bl, - }, - leftline: { - o: oCoords.bl, - d: oCoords.tl, - }, - }; - - // // debugging - // if (this.canvas.contextTop) { - // this.canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - // - // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - // } - - return lines; - }, + /** + * Helper method to determine how many cross points are between the 4 object edges + * and the horizontal line determined by a point on canvas + * @private + * @param {Point} point Point to check + * @param {Object} lines Coordinates of the object being evaluated + * @return {number} number of crossPoint + */ + _findCrossPoints(point: Point, lines: TBBoxLines): number { + let xcount = 0; + + for (const lineKey in lines) { + let xi; + const iLine = lines[lineKey as keyof TBBoxLines]; + // optimization 1: line below point. no cross + if (iLine.o.y < point.y && iLine.d.y < point.y) { + continue; + } + // optimization 2: line above point. no cross + if (iLine.o.y >= point.y && iLine.d.y >= point.y) { + continue; + } + // optimization 3: vertical line case + if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { + xi = iLine.o.x; + } + // calculate the intersection point + else { + const b1 = 0; + const b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); + const a1 = point.y - b1 * point.x; + const a2 = iLine.o.y - b2 * iLine.o.x; + + xi = -(a1 - a2) / (b1 - b2); + } + // don't count xi < point.x cases + if (xi >= point.x) { + xcount += 1; + } + // optimization 4: specific for square images + if (xcount === 2) { + break; + } + } + return xcount; + } - /** - * Helper method to determine how many cross points are between the 4 object edges - * and the horizontal line determined by a point on canvas - * @private - * @param {Point} point Point to check - * @param {Object} lines Coordinates of the object being evaluated - */ - // remove yi, not used but left code here just in case. - _findCrossPoints: function (point, lines) { - var b1, - b2, - a1, - a2, - xi, // yi, - xcount = 0, - iLine; - - for (var lineKey in lines) { - iLine = lines[lineKey]; - // optimisation 1: line below point. no cross - if (iLine.o.y < point.y && iLine.d.y < point.y) { - continue; - } - // optimisation 2: line above point. no cross - if (iLine.o.y >= point.y && iLine.d.y >= point.y) { - continue; - } - // optimisation 3: vertical line case - if (iLine.o.x === iLine.d.x && iLine.o.x >= point.x) { - xi = iLine.o.x; - // yi = point.y; - } - // calculate the intersection point - else { - b1 = 0; - b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); - a1 = point.y - b1 * point.x; - a2 = iLine.o.y - b2 * iLine.o.x; - - xi = -(a1 - a2) / (b1 - b2); - // yi = a1 + b1 * xi; - } - // dont count xi < point.x cases - if (xi >= point.x) { - xcount += 1; - } - // optimisation 4: specific for square images - if (xcount === 2) { - break; - } - } - return xcount; - }, + /** + * Returns coordinates of object's bounding rectangle (left, top, width, height) + * the box is intended as aligned to axis of canvas. + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords + * @return {Object} Object with left, top, width, height properties + */ + getBoundingRect(absolute?: boolean, calculate?: boolean): TBBox { + return makeBoundingBoxFromPoints(this.getCoords(absolute, calculate)); + } - /** - * Returns coordinates of object's bounding rectangle (left, top, width, height) - * the box is intended as aligned to axis of canvas. - * @param {Boolean} [absolute] use coordinates without viewportTransform - * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords / .aCoords - * @return {Object} Object with left, top, width, height properties - */ - getBoundingRect: function (absolute, calculate) { - var coords = this.getCoords(absolute, calculate); - return util.makeBoundingBoxFromPoints(coords); - }, + /** + * Returns width of an object's bounding box counting transformations + * @todo shouldn't this account for group transform and return the actual size in canvas coordinate plane? + * @return {Number} width value + */ + getScaledWidth(): number { + return this._getTransformedDimensions().x; + } - /** - * Returns width of an object's bounding box counting transformations - * before 2.0 it was named getWidth(); - * @return {Number} width value - */ - getScaledWidth: function () { - return this._getTransformedDimensions().x; - }, + /** + * Returns height of an object bounding box counting transformations + * @todo shouldn't this account for group transform and return the actual size in canvas coordinate plane? + * @return {Number} height value + */ + getScaledHeight(): number { + return this._getTransformedDimensions().y; + } - /** - * Returns height of an object bounding box counting transformations - * before 2.0 it was named getHeight(); - * @return {Number} height value - */ - getScaledHeight: function () { - return this._getTransformedDimensions().y; - }, + /** + * Scales an object (equally by x and y) + * @param {Number} value Scale factor + * @return {void} + */ + scale(value: number): void { + this._set('scaleX', value); + this._set('scaleY', value); + this.setCoords(); + } - /** - * Makes sure the scale is valid and modifies it if necessary - * @private - * @param {Number} value - * @return {Number} - */ - _constrainScale: function (value) { - if (Math.abs(value) < this.minScaleLimit) { - if (value < 0) { - return -this.minScaleLimit; - } else { - return this.minScaleLimit; - } - } else if (value === 0) { - return 0.0001; - } - return value; - }, + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New width value + * @param {Boolean} absolute ignore viewport + * @return {void} + */ + scaleToWidth(value: number, absolute: boolean) { + // adjust to bounding rect factor so that rotated shapes would fit as well + const boundingRectFactor = + this.getBoundingRect(absolute).width / this.getScaledWidth(); + return this.scale(value / this.width / boundingRectFactor); + } - /** - * Scales an object (equally by x and y) - * @param {Number} value Scale factor - * @return {fabric.Object} thisArg - * @chainable - */ - scale: function (value) { - this._set('scaleX', value); - this._set('scaleY', value); - return this.setCoords(); - }, + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @param {Number} value New height value + * @param {Boolean} absolute ignore viewport + * @return {void} + */ + scaleToHeight(value: number, absolute = false) { + // adjust to bounding rect factor so that rotated shapes would fit as well + const boundingRectFactor = + this.getBoundingRect(absolute).height / this.getScaledHeight(); + return this.scale(value / this.height / boundingRectFactor); + } - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New width value - * @param {Boolean} absolute ignore viewport - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function (value, absolute) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = - this.getBoundingRect(absolute).width / this.getScaledWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, + /** + * Returns the object angle relative to canvas counting also the group property + * @returns {TDegree} + */ + getTotalAngle(): TDegree { + return this.group + ? qrDecompose(this.calcTransformMatrix()).angle + : this.angle; + } - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @param {Number} value New height value - * @param {Boolean} absolute ignore viewport - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function (value, absolute) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = - this.getBoundingRect(absolute).height / this.getScaledHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, + /** + * return the coordinate of the 4 corners of the bounding box in HTMLCanvasElement coordinates + * used for bounding box interactivity with the mouse + * @returns {TCornerPoint} + */ + calcLineCoords(): TCornerPoint { + const vpt = this.getViewportTransform(), + padding = this.padding, + angle = degreesToRadians(this.getTotalAngle()), + cosP = cos(angle) * padding, + sinP = sin(angle) * padding, + cosPSinP = cosP + sinP, + cosPMinusSinP = cosP - sinP, + { tl, tr, bl, br } = this.calcACoords(); + + const lineCoords: TCornerPoint = { + tl: transformPoint(tl, vpt), + tr: transformPoint(tr, vpt), + bl: transformPoint(bl, vpt), + br: transformPoint(br, vpt), + }; + + if (padding) { + lineCoords.tl.x -= cosPMinusSinP; + lineCoords.tl.y -= cosPSinP; + lineCoords.tr.x += cosPSinP; + lineCoords.tr.y -= cosPMinusSinP; + lineCoords.bl.x -= cosPSinP; + lineCoords.bl.y += cosPMinusSinP; + lineCoords.br.x += cosPMinusSinP; + lineCoords.br.y += cosPSinP; + } - calcLineCoords: function () { - var vpt = this.getViewportTransform(), - padding = this.padding, - angle = degreesToRadians(this.getTotalAngle()), - cos = util.cos(angle), - sin = util.sin(angle), - cosP = cos * padding, - sinP = sin * padding, - cosPSinP = cosP + sinP, - cosPMinusSinP = cosP - sinP, - aCoords = this.calcACoords(); - - var lineCoords = { - tl: transformPoint(aCoords.tl, vpt), - tr: transformPoint(aCoords.tr, vpt), - bl: transformPoint(aCoords.bl, vpt), - br: transformPoint(aCoords.br, vpt), - }; - - if (padding) { - lineCoords.tl.x -= cosPMinusSinP; - lineCoords.tl.y -= cosPSinP; - lineCoords.tr.x += cosPSinP; - lineCoords.tr.y -= cosPMinusSinP; - lineCoords.bl.x -= cosPSinP; - lineCoords.bl.y += cosPMinusSinP; - lineCoords.br.x += cosPMinusSinP; - lineCoords.br.y += cosPSinP; - } - - return lineCoords; - }, + return lineCoords; + } - calcOCoords: function () { - var vpt = this.getViewportTransform(), - center = this.getCenterPoint(), - tMatrix = [1, 0, 0, 1, center.x, center.y], - rMatrix = util.calcRotateMatrix({ - angle: - this.getTotalAngle() - (!!this.group && this.flipX ? 180 : 0), - }), - positionMatrix = multiplyMatrices(tMatrix, rMatrix), - startMatrix = multiplyMatrices(vpt, positionMatrix), - finalMatrix = multiplyMatrices(startMatrix, [ - 1 / vpt[0], - 0, - 0, - 1 / vpt[3], - 0, - 0, - ]), - transformOptions = this.group - ? fabric.util.qrDecompose(this.calcTransformMatrix()) - : undefined, - dim = this._calculateCurrentDimensions(transformOptions), - coords = {}; - this.forEachControl(function (control, key, fabricObject) { - coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); - }); - - // debug code - /* - var canvas = this.canvas; - setTimeout(function () { - if (!canvas) return; - canvas.contextTop.clearRect(0, 0, 700, 700); - canvas.contextTop.fillStyle = 'green'; - Object.keys(coords).forEach(function(key) { - var control = coords[key]; - canvas.contextTop.fillRect(control.x, control.y, 3, 3); - }); - }, 50); - */ - return coords; - }, + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf FabricObject.prototype + * @return {TMat2D} + */ + getViewportTransform(): TMat2D { + if (this.canvas && this.canvas.viewportTransform) { + return this.canvas.viewportTransform; + } + return iMatrix.concat() as TMat2D; + } - calcACoords: function () { - var rotateMatrix = util.calcRotateMatrix({ angle: this.angle }), - center = this.getRelativeCenterPoint(), - translateMatrix = [1, 0, 0, 1, center.x, center.y], - finalMatrix = multiplyMatrices(translateMatrix, rotateMatrix), - dim = this._getTransformedDimensions(), - w = dim.x / 2, - h = dim.y / 2; - return { - // corners - tl: transformPoint({ x: -w, y: -h }, finalMatrix), - tr: transformPoint({ x: w, y: -h }, finalMatrix), - bl: transformPoint({ x: -w, y: h }, finalMatrix), - br: transformPoint({ x: w, y: h }, finalMatrix), - }; - }, + /** + * Calculates the coordinates of the center of each control plus the corners of the control itself + * This basically just delegates to each control positionHandler + * WARNING: changing what is passed to positionHandler is a breaking change, since position handler + * is a public api and should be done just if extremely necessary + * @todo needs to be moved to interactivity mixin + * @return {Record} + */ + calcOCoords(): Record { + const vpt = this.getViewportTransform(), + center = this.getCenterPoint(), + tMatrix = [1, 0, 0, 1, center.x, center.y] as TMat2D, + rMatrix = calcRotateMatrix({ + angle: this.getTotalAngle() - (!!this.group && this.flipX ? 180 : 0), + }), + positionMatrix = multiplyTransformMatrices(tMatrix, rMatrix), + startMatrix = multiplyTransformMatrices(vpt, positionMatrix), + finalMatrix = multiplyTransformMatrices(startMatrix, [ + 1 / vpt[0], + 0, + 0, + 1 / vpt[3], + 0, + 0, + ]), + transformOptions = this.group + ? qrDecompose(this.calcTransformMatrix()) + : undefined, + dim = this._calculateCurrentDimensions(transformOptions), + coords: Record = {}; + // @ts-ignore + this.forEachControl((control, key, fabricObject) => { + coords[key] = control.positionHandler(dim, finalMatrix, fabricObject); + }); + + // debug code + /* + const canvas = this.canvas; + setTimeout(function () { + if (!canvas) return; + canvas.contextTop.clearRect(0, 0, 700, 700); + canvas.contextTop.fillStyle = 'green'; + Object.keys(coords).forEach(function(key) { + const control = coords[key]; + canvas.contextTop.fillRect(control.x, control.y, 3, 3); + }); + } 50); + */ + return coords; + } - /** - * Sets corner and controls position coordinates based on current angle, width and height, left and top. - * oCoords are used to find the corners - * aCoords are used to quickly find an object on the canvas - * lineCoords are used to quickly find object during pointer events. - * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} - * - * @param {Boolean} [skipCorners] skip calculation of oCoords. - * @return {fabric.Object} thisArg - * @chainable - */ - setCoords: function (skipCorners) { - this.aCoords = this.calcACoords(); - // in case we are in a group, for how the inner group target check works, - // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. - this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); - if (skipCorners) { - return this; - } - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this.oCoords = this.calcOCoords(); - this._setCornerCoords && this._setCornerCoords(); - return this; - }, + /** + * Calculates the coordinates of the 4 corner of the bbox, in absolute coordinates. + * those never change with zoom or viewport changes. + * @return {TCornerPoint} + */ + calcACoords(): TCornerPoint { + const rotateMatrix = calcRotateMatrix({ angle: this.angle }), + center = this.getRelativeCenterPoint(), + translateMatrix = [1, 0, 0, 1, center.x, center.y] as TMat2D, + finalMatrix = multiplyTransformMatrices(translateMatrix, rotateMatrix), + dim = this._getTransformedDimensions(), + w = dim.x / 2, + h = dim.y / 2; + return { + // corners + tl: transformPoint({ x: -w, y: -h }, finalMatrix), + tr: transformPoint({ x: w, y: -h }, finalMatrix), + bl: transformPoint({ x: -w, y: h }, finalMatrix), + br: transformPoint({ x: w, y: h }, finalMatrix), + }; + } - transformMatrixKey: function (skipGroup) { - var sep = '_', - prefix = ''; - if (!skipGroup && this.group) { - prefix = this.group.transformMatrixKey(skipGroup) + sep; - } - return ( - prefix + - this.top + - sep + - this.left + - sep + - this.scaleX + - sep + - this.scaleY + - sep + - this.skewX + - sep + - this.skewY + - sep + - this.angle + - sep + - this.originX + - sep + - this.originY + - sep + - this.width + - sep + - this.height + - sep + - this.strokeWidth + - this.flipX + - this.flipY - ); - }, + /** + * Sets corner and controls position coordinates based on current angle, width and height, left and top. + * oCoords are used to find the corners + * aCoords are used to quickly find an object on the canvas + * lineCoords are used to quickly find object during pointer events. + * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} + * + * @param {Boolean} [skipCorners] skip calculation of oCoords. + * @return {void} + */ + setCoords(skipCorners = false): void { + this.aCoords = this.calcACoords(); + // in case we are in a group, for how the inner group target check works, + // lineCoords are exactly aCoords. Since the vpt gets absorbed by the normalized pointer. + this.lineCoords = this.group ? this.aCoords : this.calcLineCoords(); + if (!skipCorners) { + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this.oCoords = this.calcOCoords(); + // @ts-ignore + this._setCornerCoords && this._setCornerCoords(); + } + } - /** - * calculate transform matrix that represents the current transformations from the - * object's properties. - * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations - * There are some situation in which this is useful to avoid the fake rotation. - * @return {Array} transform matrix for the object - */ - calcTransformMatrix: function (skipGroup) { - var matrix = this.calcOwnMatrix(); - if (skipGroup || !this.group) { - return matrix; - } - var key = this.transformMatrixKey(skipGroup), - cache = this.matrixCache || (this.matrixCache = {}); - if (cache.key === key) { - return cache.value; - } - if (this.group) { - matrix = multiplyMatrices( - this.group.calcTransformMatrix(false), - matrix - ); - } - cache.key = key; - cache.value = matrix; - return matrix; - }, + transformMatrixKey(skipGroup = false): string { + const sep = '_'; + let prefix = ''; + if (!skipGroup && this.group) { + prefix = this.group.transformMatrixKey(skipGroup) + sep; + } + return ( + prefix + + this.top + + sep + + this.left + + sep + + this.scaleX + + sep + + this.scaleY + + sep + + this.skewX + + sep + + this.skewY + + sep + + this.angle + + sep + + this.originX + + sep + + this.originY + + sep + + this.width + + sep + + this.height + + sep + + this.strokeWidth + + this.flipX + + this.flipY + ); + } - /** - * calculate transform matrix that represents the current transformations from the - * object's properties, this matrix does not include the group transformation - * @return {Array} transform matrix for the object - */ - calcOwnMatrix: function () { - var key = this.transformMatrixKey(true), - cache = this.ownMatrixCache || (this.ownMatrixCache = {}); - if (cache.key === key) { - return cache.value; - } - var center = this.getRelativeCenterPoint(), - options = { - angle: this.angle, - translateX: center.x, - translateY: center.y, - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - flipX: this.flipX, - flipY: this.flipY, - }; - cache.key = key; - cache.value = util.composeMatrix(options); - return cache.value; - }, + /** + * calculate transform matrix that represents the current transformations from the + * object's properties. + * @param {Boolean} [skipGroup] return transform matrix for object not counting parent transformations + * There are some situation in which this is useful to avoid the fake rotation. + * @return {TMat2D} transform matrix for the object + */ + calcTransformMatrix(skipGroup = false): TMat2D { + let matrix = this.calcOwnMatrix(); + if (skipGroup || !this.group) { + return matrix; + } + const key = this.transformMatrixKey(skipGroup), + cache = this.matrixCache; + if (cache && cache.key === key) { + return cache.value; + } + if (this.group) { + matrix = multiplyTransformMatrices( + this.group.calcTransformMatrix(false), + matrix + ); + } + this.matrixCache = { + key, + value: matrix, + }; + return matrix; + } - /** - * Calculate object dimensions from its properties - * @private - * @returns {Point} dimensions - */ - _getNonTransformedDimensions: function () { - return new Point(this.width, this.height).scalarAdd(this.strokeWidth); - }, + /** + * calculate transform matrix that represents the current transformations from the + * object's properties, this matrix does not include the group transformation + * @return {TMat2D} transform matrix for the object + */ + calcOwnMatrix(): TMat2D { + const key = this.transformMatrixKey(true), + cache = this.ownMatrixCache; + if (cache && cache.key === key) { + return cache.value; + } + const center = this.getRelativeCenterPoint(), + options = { + angle: this.angle, + translateX: center.x, + translateY: center.y, + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + flipX: this.flipX, + flipY: this.flipY, + }, + value = composeMatrix(options); + this.ownMatrixCache = { + key, + value, + }; + return value; + } - /** - * Calculate object bounding box dimensions from its properties scale, skew. - * @param {Object} [options] - * @param {Number} [options.scaleX] - * @param {Number} [options.scaleY] - * @param {Number} [options.skewX] - * @param {Number} [options.skewY] - * @private - * @returns {Point} dimensions - */ - _getTransformedDimensions: function (options) { - options = Object.assign( - { - scaleX: this.scaleX, - scaleY: this.scaleY, - skewX: this.skewX, - skewY: this.skewY, - width: this.width, - height: this.height, - strokeWidth: this.strokeWidth, - }, - options || {} - ); - // stroke is applied before/after transformations are applied according to `strokeUniform` - var preScalingStrokeValue, - postScalingStrokeValue, - strokeWidth = options.strokeWidth; - if (this.strokeUniform) { - preScalingStrokeValue = 0; - postScalingStrokeValue = strokeWidth; - } else { - preScalingStrokeValue = strokeWidth; - postScalingStrokeValue = 0; - } - var dimX = options.width + preScalingStrokeValue, - dimY = options.height + preScalingStrokeValue, - finalDimensions, - noSkew = options.skewX === 0 && options.skewY === 0; - if (noSkew) { - finalDimensions = new Point( - dimX * options.scaleX, - dimY * options.scaleY - ); - } else { - var bbox = util.sizeAfterTransform(dimX, dimY, options); - finalDimensions = new Point(bbox.x, bbox.y); - } - - return finalDimensions.scalarAdd(postScalingStrokeValue); - }, + /** + * Calculate object dimensions from its properties + * @private + * @returns {Point} dimensions + */ + _getNonTransformedDimensions(): Point { + return new Point(this.width, this.height).scalarAdd(this.strokeWidth); + } - /** - * Calculate object dimensions for controls box, including padding and canvas zoom. - * and active selection - * @private - * @param {object} [options] transform options - * @returns {Point} dimensions - */ - _calculateCurrentDimensions: function (options) { - var vpt = this.getViewportTransform(), - dim = this._getTransformedDimensions(options), - p = transformPoint(dim, vpt, true); - return p.scalarAdd(2 * this.padding); - }, - } - ); -})(typeof exports !== 'undefined' ? exports : window); + /** + * Calculate object dimensions for controls box, including padding and canvas zoom. + * and active selection + * @private + * @param {object} [options] transform options + * @returns {Point} dimensions + */ + _calculateCurrentDimensions(options?: any): Point { + return this._getTransformedDimensions(options) + .transform(this.getViewportTransform(), true) + .scalarAdd(2 * this.padding); + } +} diff --git a/src/mixins/object_origin.mixin.ts b/src/mixins/object_origin.mixin.ts index d423af20d82..0cc1d88adcc 100644 --- a/src/mixins/object_origin.mixin.ts +++ b/src/mixins/object_origin.mixin.ts @@ -1,345 +1,404 @@ -//@ts-nocheck import { Point } from '../point.class'; -import { FabricObject } from '../shapes/object.class'; - -(function (global) { - var fabric = global.fabric, - degreesToRadians = fabric.util.degreesToRadians, - originXOffset = { - left: -0.5, - center: 0, - right: 0.5, - }, - originYOffset = { - top: -0.5, - center: 0, - bottom: 0.5, +import { transformPoint } from '../util/misc/matrix'; +import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { CommonMethods } from './shared_methods.mixin'; +import { TDegree, TOriginX, TOriginY } from '../typedefs'; +import { Group } from '../shapes/group.class'; +import { sizeAfterTransform } from '../util/misc/objectTransforms'; + +const originOffset = { + left: -0.5, + top: -0.5, + center: 0, + bottom: 0.5, + right: 0.5, +}; + +/** + * Resolves origin value relative to center + * @private + * @param {TOriginX | TOriginY} originValue originX / originY + * @returns number + */ +export const resolveOrigin = ( + originValue: TOriginX | TOriginY | number +): number => + typeof originValue === 'string' + ? originOffset[originValue] + : originValue - 0.5; + +export class ObjectOrigin extends CommonMethods { + /** + * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} + * @type Number + * @default 0 + */ + top: number; + + /** + * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} + * @type Number + * @default 0 + */ + left: number; + + /** + * Object width + * @type Number + * @default + */ + width: number; + + /** + * Object height + * @type Number + * @default + */ + height: number; + + /** + * Object scale factor (horizontal) + * @type Number + * @default 1 + */ + scaleX: number; + + /** + * Object scale factor (vertical) + * @type Number + * @default 1 + */ + scaleY: number; + + /** + * Angle of skew on x axes of an object (in degrees) + * @type Number + * @default 0 + */ + skewX: number; + + /** + * Angle of skew on y axes of an object (in degrees) + * @type Number + * @default 0 + */ + skewY: number; + + /** + * Horizontal origin of transformation of an object (one of "left", "right", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default 'left' + */ + originX: TOriginX; + + /** + * Vertical origin of transformation of an object (one of "top", "bottom", "center") + * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups + * @type String + * @default 'top' + */ + originY: TOriginY; + + /** + * Angle of rotation of an object (in degrees) + * @type Number + * @default 0 + */ + angle: TDegree; + + /** + * Width of a stroke used to render this object + * @type Number + * @default 1 + */ + strokeWidth: number; + + /** + * When `false`, the stoke width will scale with the object. + * When `true`, the stroke will always match the exact pixel size entered for stroke width. + * this Property does not work on Text classes or drawing call that uses strokeText,fillText methods + * default to false + * @since 2.6.0 + * @type Boolean + * @default false + * @type Boolean + * @default false + */ + strokeUniform: boolean; + + /** + * Object containing this object. + * can influence its size and position + */ + group?: Group; + + _originalOriginX?: TOriginX; + + _originalOriginY?: TOriginY; + + /** + * Calculate object bounding box dimensions from its properties scale, skew. + * @param {Object} [options] + * @param {Number} [options.scaleX] + * @param {Number} [options.scaleY] + * @param {Number} [options.skewX] + * @param {Number} [options.skewY] + * @private + * @returns {Point} dimensions + */ + _getTransformedDimensions(options: any = {}): Point { + const dimOptions = { + scaleX: this.scaleX, + scaleY: this.scaleY, + skewX: this.skewX, + skewY: this.skewY, + width: this.width, + height: this.height, + strokeWidth: this.strokeWidth, + ...options, }; + // stroke is applied before/after transformations are applied according to `strokeUniform` + const strokeWidth = dimOptions.strokeWidth; + let preScalingStrokeValue = strokeWidth, + postScalingStrokeValue = 0; + + if (this.strokeUniform) { + preScalingStrokeValue = 0; + postScalingStrokeValue = strokeWidth; + } + const dimX = dimOptions.width + preScalingStrokeValue, + dimY = dimOptions.height + preScalingStrokeValue, + noSkew = dimOptions.skewX === 0 && dimOptions.skewY === 0; + let finalDimensions; + if (noSkew) { + finalDimensions = new Point( + dimX * dimOptions.scaleX, + dimY * dimOptions.scaleY + ); + } else { + finalDimensions = sizeAfterTransform(dimX, dimY, dimOptions); + } + + return finalDimensions.scalarAdd(postScalingStrokeValue); + } + + /** + * Translates the coordinates from a set of origin to another (based on the object's dimensions) + * @param {Point} point The point which corresponds to the originX and originY params + * @param {TOriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' + * @param {TOriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' + * @return {Point} + */ + translateToGivenOrigin( + point: Point, + fromOriginX: TOriginX, + fromOriginY: TOriginY, + toOriginX: TOriginX, + toOriginY: TOriginY + ): Point { + let x = point.x, + y = point.y; + const offsetX = resolveOrigin(toOriginX) - resolveOrigin(fromOriginX), + offsetY = resolveOrigin(toOriginY) - resolveOrigin(fromOriginY); + + if (offsetX || offsetY) { + const dim = this._getTransformedDimensions(); + x += offsetX * dim.x; + y += offsetY * dim.y; + } + + return new Point(x, y); + } /** - * @typedef {number | 'left' | 'center' | 'right'} OriginX - * @typedef {number | 'top' | 'center' | 'bottom'} OriginY + * Translates the coordinates from origin to center coordinates (based on the object's dimensions) + * @param {Point} point The point which corresponds to the originX and originY params + * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {Point} */ + translateToCenterPoint( + point: Point, + originX: TOriginX, + originY: TOriginY + ): Point { + const p = this.translateToGivenOrigin( + point, + originX, + originY, + 'center', + 'center' + ); + if (this.angle) { + return p.rotate(degreesToRadians(this.angle), point); + } + return p; + } - fabric.util.object.extend( - FabricObject.prototype, - /** @lends FabricObject.prototype */ { - /** - * Resolves origin value relative to center - * @private - * @param {OriginX} originX - * @returns number - */ - resolveOriginX: function (originX) { - return typeof originX === 'string' - ? originXOffset[originX] - : originX - 0.5; - }, - - /** - * Resolves origin value relative to center - * @private - * @param {OriginY} originY - * @returns number - */ - resolveOriginY: function (originY) { - return typeof originY === 'string' - ? originYOffset[originY] - : originY - 0.5; - }, - - /** - * Translates the coordinates from a set of origin to another (based on the object's dimensions) - * @param {Point} point The point which corresponds to the originX and originY params - * @param {OriginX} fromOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} fromOriginY Vertical origin: 'top', 'center' or 'bottom' - * @param {OriginX} toOriginX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} toOriginY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToGivenOrigin: function ( - point, - fromOriginX, - fromOriginY, - toOriginX, - toOriginY - ) { - var x = point.x, - y = point.y, - dim, - offsetX = - this.resolveOriginX(toOriginX) - this.resolveOriginX(fromOriginX), - offsetY = - this.resolveOriginY(toOriginY) - this.resolveOriginY(fromOriginY); - - if (offsetX || offsetY) { - dim = this._getTransformedDimensions(); - x = point.x + offsetX * dim.x; - y = point.y + offsetY * dim.y; - } - - return new Point(x, y); - }, - - /** - * Translates the coordinates from origin to center coordinates (based on the object's dimensions) - * @param {Point} point The point which corresponds to the originX and originY params - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToCenterPoint: function (point, originX, originY) { - var p = this.translateToGivenOrigin( - point, - originX, - originY, - 'center', - 'center' - ); - if (this.angle) { - return fabric.util.rotatePoint( - p, - point, - degreesToRadians(this.angle) - ); - } - return p; - }, - - /** - * Translates the coordinates from center to origin coordinates (based on the object's dimensions) - * @param {Point} center The point which corresponds to center of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - translateToOriginPoint: function (center, originX, originY) { - var p = this.translateToGivenOrigin( - center, - 'center', - 'center', - originX, - originY - ); - if (this.angle) { - return fabric.util.rotatePoint( - p, - center, - degreesToRadians(this.angle) - ); - } - return p; - }, - - /** - * Returns the center coordinates of the object relative to canvas - * @return {Point} - */ - getCenterPoint: function () { - var relCenter = this.getRelativeCenterPoint(); - return this.group - ? fabric.util.transformPoint( - relCenter, - this.group.calcTransformMatrix() - ) - : relCenter; - }, - - /** - * Returns the center coordinates of the object relative to it's containing group or null - * @return {Point|null} point or null of object has no parent group - */ - getCenterPointRelativeToParent: function () { - return this.group ? this.getRelativeCenterPoint() : null; - }, - - /** - * Returns the center coordinates of the object relative to it's parent - * @return {Point} - */ - getRelativeCenterPoint: function () { - return this.translateToCenterPoint( - new Point(this.left, this.top), - this.originX, - this.originY - ); - }, - - /** - * Returns the coordinates of the object based on center coordinates - * @param {Point} point The point which corresponds to the originX and originY params - * @return {Point} - */ - // getOriginPoint: function(center) { - // return this.translateToOriginPoint(center, this.originX, this.originY); - // }, - - /** - * Returns the coordinates of the object as if it has a different origin - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - getPointByOrigin: function (originX, originY) { - var center = this.getRelativeCenterPoint(); - return this.translateToOriginPoint(center, originX, originY); - }, - - /** - * Returns the normalized point (rotated relative to center) in local coordinates - * @param {Point} point The point relative to instance coordinate system - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {Point} - */ - normalizePoint: function (point, originX, originY) { - var center = this.getRelativeCenterPoint(), - p, - p2; - if (typeof originX !== 'undefined' && typeof originY !== 'undefined') { - p = this.translateToGivenOrigin( - center, - 'center', - 'center', - originX, - originY - ); - } else { - p = new Point(this.left, this.top); - } - - p2 = new Point(point.x, point.y); - if (this.angle) { - p2 = fabric.util.rotatePoint( - p2, - center, - -degreesToRadians(this.angle) - ); - } - return p2.subtract(p); - }, - - /** - * Returns coordinates of a pointer relative to object's top left corner in object's plane - * @param {Event} e Event to operate upon - * @param {Object} [pointer] Pointer to operate upon (instead of event) - * @return {Object} Coordinates of a pointer (x, y) - */ - getLocalPointer: function (e, pointer) { - pointer = pointer || this.canvas.getPointer(e); - return fabric.util - .transformPoint( - new Point(pointer.x, pointer.y), - fabric.util.invertTransform(this.calcTransformMatrix()) - ) - .add(new Point(this.width / 2, this.height / 2)); - }, - - /** - * Returns the point in global coordinates - * @param {Point} The point relative to the local coordinate system - * @return {Point} - */ - // toGlobalPoint: function(point) { - // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).add(new Point(this.left, this.top)); - // }, - - /** - * Sets the position of the object taking into consideration the object's origin - * @param {Point} pos The new position of the object - * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' - * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' - * @return {void} - */ - setPositionByOrigin: function (pos, originX, originY) { - var center = this.translateToCenterPoint(pos, originX, originY), - position = this.translateToOriginPoint( - center, - this.originX, - this.originY - ); - this.set('left', position.x); - this.set('top', position.y); - }, - - /** - * @param {String} to One of 'left', 'center', 'right' - */ - adjustPosition: function (to) { - var angle = degreesToRadians(this.angle), - hypotFull = this.getScaledWidth(), - xFull = fabric.util.cos(angle) * hypotFull, - yFull = fabric.util.sin(angle) * hypotFull, - offsetFrom, - offsetTo; - - //TODO: this function does not consider mixed situation like top, center. - if (typeof this.originX === 'string') { - offsetFrom = originXOffset[this.originX]; - } else { - offsetFrom = this.originX - 0.5; - } - if (typeof to === 'string') { - offsetTo = originXOffset[to]; - } else { - offsetTo = to - 0.5; - } - this.left += xFull * (offsetTo - offsetFrom); - this.top += yFull * (offsetTo - offsetFrom); - this.setCoords(); - this.originX = to; - }, - - /** - * Sets the origin/position of the object to it's center point - * @private - * @return {void} - */ - _setOriginToCenter: function () { - this._originalOriginX = this.originX; - this._originalOriginY = this.originY; - - var center = this.getRelativeCenterPoint(); - - this.originX = 'center'; - this.originY = 'center'; - - this.left = center.x; - this.top = center.y; - }, - - /** - * Resets the origin/position of the object to it's original origin - * @private - * @return {void} - */ - _resetOrigin: function () { - var originPoint = this.translateToOriginPoint( - this.getRelativeCenterPoint(), - this._originalOriginX, - this._originalOriginY - ); - - this.originX = this._originalOriginX; - this.originY = this._originalOriginY; - - this.left = originPoint.x; - this.top = originPoint.y; - - this._originalOriginX = null; - this._originalOriginY = null; - }, - - /** - * @private - */ - _getLeftTopCoords: function () { - return this.translateToOriginPoint( - this.getRelativeCenterPoint(), - 'left', - 'top' - ); - }, + /** + * Translates the coordinates from center to origin coordinates (based on the object's dimensions) + * @param {Point} center The point which corresponds to center of the object + * @param {OriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {OriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {Point} + */ + translateToOriginPoint( + center: Point, + originX: TOriginX, + originY: TOriginY + ): Point { + const p = this.translateToGivenOrigin( + center, + 'center', + 'center', + originX, + originY + ); + if (this.angle) { + return p.rotate(degreesToRadians(this.angle), center); } - ); -})(typeof exports !== 'undefined' ? exports : window); + return p; + } + + /** + * Returns the center coordinates of the object relative to canvas + * @return {Point} + */ + getCenterPoint(): Point { + const relCenter = this.getRelativeCenterPoint(); + return this.group + ? transformPoint(relCenter, this.group.calcTransformMatrix()) + : relCenter; + } + + /** + * Returns the center coordinates of the object relative to it's parent + * @return {Point} + */ + getRelativeCenterPoint(): Point { + return this.translateToCenterPoint( + new Point(this.left, this.top), + this.originX, + this.originY + ); + } + + /** + * Returns the coordinates of the object as if it has a different origin + * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {Point} + */ + getPointByOrigin(originX: TOriginX, originY: TOriginY): Point { + return this.translateToOriginPoint( + this.getRelativeCenterPoint(), + originX, + originY + ); + } + + /** + * Returns the normalized point (rotated relative to center) in local coordinates + * @param {Point} point The point relative to instance coordinate system + * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {Point} + */ + normalizePoint(point: Point, originX: TOriginX, originY: TOriginY): Point { + const center = this.getRelativeCenterPoint(); + let p, p2; + if (typeof originX !== 'undefined' && typeof originY !== 'undefined') { + p = this.translateToGivenOrigin( + center, + 'center', + 'center', + originX, + originY + ); + } else { + p = new Point(this.left, this.top); + } + + if (this.angle) { + p2 = point.rotate(-degreesToRadians(this.angle), center); + } else { + p2 = point; + } + return p2.subtract(p); + } + + /** + * Sets the position of the object taking into consideration the object's origin + * @param {Point} pos The new position of the object + * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' + * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' + * @return {void} + */ + setPositionByOrigin(pos: Point, originX: TOriginX, originY: TOriginY) { + const center = this.translateToCenterPoint(pos, originX, originY), + position = this.translateToOriginPoint( + center, + this.originX, + this.originY + ); + this.set({ left: position.x, top: position.y }); + } + + /** + * Sets the origin/position of the object to it's center point + * @private + * @return {void} + */ + _setOriginToCenter() { + this._originalOriginX = this.originX; + this._originalOriginY = this.originY; + + const center = this.getRelativeCenterPoint(); + + this.originX = 'center'; + this.originY = 'center'; + + this.left = center.x; + this.top = center.y; + } + + /** + * Resets the origin/position of the object to it's original origin + * @private + * @return {void} + */ + _resetOrigin() { + if ( + this._originalOriginX !== undefined && + this._originalOriginY !== undefined + ) { + const originPoint = this.translateToOriginPoint( + this.getRelativeCenterPoint(), + this._originalOriginX, + this._originalOriginY + ); + + this.left = originPoint.x; + this.top = originPoint.y; + + this.originX = this._originalOriginX; + this.originY = this._originalOriginY; + this._originalOriginX = undefined; + this._originalOriginY = undefined; + } + } + + /** + * @private + */ + _getLeftTopCoords() { + return this.translateToOriginPoint( + this.getRelativeCenterPoint(), + 'left', + 'top' + ); + } +} diff --git a/src/shapes/group.class.ts b/src/shapes/group.class.ts index de3c5d00709..70decb852d2 100644 --- a/src/shapes/group.class.ts +++ b/src/shapes/group.class.ts @@ -1,6 +1,9 @@ //@ts-nocheck import { Point } from '../point.class'; import { FabricObject } from './object.class'; +import { resolveOrigin } from '../mixins/object_origin.mixin'; + +export class Group extends FabricObject {} (function (global) { var fabric = global.fabric || (global.fabric = {}), @@ -803,8 +806,8 @@ import { FabricObject } from './object.class'; height = hasHeight ? this.height : bbox.height || 0, calculatedCenter = new Point(bbox.centerX || 0, bbox.centerY || 0), origin = new Point( - this.resolveOriginX(this.originX), - this.resolveOriginY(this.originY) + resolveOrigin(this.originX), + resolveOrigin(this.originY) ), size = new Point(width, height), strokeWidthVector = this._getTransformedDimensions({ diff --git a/src/shapes/object.class.ts b/src/shapes/object.class.ts index 260ac14fafa..24ee70cd4b9 100644 --- a/src/shapes/object.class.ts +++ b/src/shapes/object.class.ts @@ -4,7 +4,6 @@ import { fabric } from '../../HEADER'; import { cache } from '../cache'; import { config } from '../config'; import { VERSION } from '../constants'; -import { CommonMethods } from '../mixins/shared_methods.mixin'; import { Point } from '../point.class'; import { capValue } from '../util/misc/capValue'; import { pick } from '../util/misc/pick'; @@ -15,6 +14,8 @@ import { toFixed } from '../util/misc/toFixed'; import { capitalize } from '../util/lang_string'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { createCanvasElement } from '../util/misc/dom'; +import { ObjectGeometry } from '../mixins/object_geometry.mixin'; +import { qrDecompose, transformPoint } from '../util/misc/matrix'; type StaticCanvas = any; type Canvas = any; @@ -56,81 +57,9 @@ const ALIASING_LIMIT = 2; * @fires dragleave * @fires drop */ -export class FabricObject extends CommonMethods { +export class FabricObject extends ObjectGeometry { type: string; - /** - * Horizontal origin of transformation of an object (one of "left", "right", "center") - * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups - * @type String - * @default 'left' - */ - originX: string; - - /** - * Vertical origin of transformation of an object (one of "top", "bottom", "center") - * See http://jsfiddle.net/1ow02gea/244/ on how originX/originY affect objects in groups - * @type String - * @default 'top' - */ - originY: string; - - /** - * Top position of an object. Note that by default it's relative to object top. You can change this by setting originY={top/center/bottom} - * @type Number - * @default 0 - */ - top: number; - - /** - * Left position of an object. Note that by default it's relative to object left. You can change this by setting originX={left/center/right} - * @type Number - * @default 0 - */ - left: number; - - /** - * Object width - * @type Number - * @default - */ - width: number; - - /** - * Object height - * @type Number - * @default - */ - height: number; - - /** - * Object scale factor (horizontal) - * @type Number - * @default 1 - */ - scaleX: number; - - /** - * Object scale factor (vertical) - * @type Number - * @default 1 - */ - scaleY: number; - - /** - * When true, an object is rendered as flipped horizontally - * @type Boolean - * @default false - */ - flipX: boolean; - - /** - * When true, an object is rendered as flipped vertically - * @type Boolean - * @default false - */ - flipY: boolean; - /** * Opacity of an object * @type Number @@ -138,27 +67,6 @@ export class FabricObject extends CommonMethods { */ opacity: number; - /** - * Angle of rotation of an object (in degrees) - * @type Number - * @default 0 - */ - angle: TDegree; - - /** - * Angle of skew on x axes of an object (in degrees) - * @type Number - * @default 0 - */ - skewX: number; - - /** - * Angle of skew on y axes of an object (in degrees) - * @type Number - * @default 0 - */ - skewY: number; - /** * Size of object's controlling corners (in pixels) * @type Number @@ -194,13 +102,6 @@ export class FabricObject extends CommonMethods { */ moveCursor: null; - /** - * Padding between object and its controlling borders (in pixels) - * @type Number - * @default 0 - */ - padding: number; - /** * Color of controlling borders of an object (when it's active) * @type String @@ -266,6 +167,14 @@ export class FabricObject extends CommonMethods { */ centeredRotation: true; + /** + * When defined, an object is rendered via stroke and this property specifies its color + * takes css colors https://www.w3.org/TR/css-color-3/ + * @type String + * @default null + */ + stroke: string | TFiller | null; + /** * Color of object's fill * takes css colors https://www.w3.org/TR/css-color-3/ @@ -306,21 +215,6 @@ export class FabricObject extends CommonMethods { */ selectionBackgroundColor: string; - /** - * When defined, an object is rendered via stroke and this property specifies its color - * takes css colors https://www.w3.org/TR/css-color-3/ - * @type String - * @default null - */ - stroke: string | TFiller | null; - - /** - * Width of a stroke used to render this object - * @type Number - * @default 1 - */ - strokeWidth: number; - /** * Array specifying dash pattern of an object's stroke (stroke must be defined) * @type Array @@ -534,19 +428,6 @@ export class FabricObject extends CommonMethods { */ noScaleCache: boolean; - /** - * When `false`, the stoke width will scale with the object. - * When `true`, the stroke will always match the exact pixel size entered for stroke width. - * this Property does not work on Text classes or drawing call that uses strokeText,fillText methods - * default to false - * @since 2.6.0 - * @type Boolean - * @default false - * @type Boolean - * @default false - */ - strokeUniform: boolean; - /** * When set to `true`, object's cache will be rerendered next render call. * since 1.7.0 @@ -1090,7 +971,7 @@ export class FabricObject extends CommonMethods { return new Point(Math.abs(this.scaleX), Math.abs(this.scaleY)); } // if we are inside a group total zoom calculation is complex, we defer to generic matrices - const options = fabric.util.qrDecompose(this.calcTransformMatrix()); + const options = qrDecompose(this.calcTransformMatrix()); return new Point(Math.abs(options.scaleX), Math.abs(options.scaleY)); } @@ -1121,13 +1002,23 @@ export class FabricObject extends CommonMethods { } /** - * Returns the object angle relative to canvas counting also the group property - * @returns {number} + * Makes sure the scale is valid and modifies it if necessary + * @todo: this is a control action issue, not a geometry one + * @private + * @param {Number} value, unconstrained + * @return {Number} constrained value; */ - getTotalAngle() { - return this.group - ? fabric.util.qrDecompose(this.calcTransformMatrix()).angle - : this.angle; + _constrainScale(value: number): number { + if (Math.abs(value) < this.minScaleLimit) { + if (value < 0) { + return -this.minScaleLimit; + } else { + return this.minScaleLimit; + } + } else if (value === 0) { + return 0.0001; + } + return value; } /** @@ -1169,19 +1060,6 @@ export class FabricObject extends CommonMethods { return this; } - /** - * Retrieves viewportTransform from Object's canvas if possible - * @method getViewportTransform - * @memberOf FabricObject.prototype - * @return {Array} - */ - getViewportTransform() { - if (this.canvas && this.canvas.viewportTransform) { - return this.canvas.viewportTransform; - } - return fabric.iMatrix.concat(); - } - /* * @private * return if the object would be visible in rendering @@ -1553,6 +1431,7 @@ export class FabricObject extends CommonMethods { /** * Renders controls and borders for the object * the context here is not transformed + * @todo move to interactivity * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Object} [styleOverride] properties to override the object style */ @@ -1794,7 +1673,7 @@ export class FabricObject extends CommonMethods { */ _assignTransformMatrixProps() { if (this.transformMatrix) { - const options = fabric.util.qrDecompose(this.transformMatrix); + const options = qrDecompose(this.transformMatrix); this.flipX = false; this.flipY = false; this.set('scaleX', options.scaleX); @@ -1816,7 +1695,7 @@ export class FabricObject extends CommonMethods { let center = this._findCenterFromElement(); if (this.transformMatrix) { this._assignTransformMatrixProps(); - center = fabric.util.transformPoint(center, this.transformMatrix); + center = transformPoint(center, this.transformMatrix); } this.transformMatrix = null; if (preserveAspectRatioOptions) { @@ -2250,6 +2129,7 @@ const fabricObjectDefaultValues: TClassProperties = { clipPath: undefined, inverted: false, absolutePositioned: false, + controls: {}, }; Object.assign(FabricObject.prototype, fabricObjectDefaultValues); diff --git a/src/typedefs.ts b/src/typedefs.ts index 1f6306e1102..38d9936bc7f 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -86,3 +86,6 @@ export type TransformEvent = TEvent & * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes */ export type TCrossOrigin = '' | 'anonymous' | 'use-credentials' | null; + +export type TOriginX = 'center' | 'left' | 'right' | number; +export type TOriginY = 'center' | 'top' | 'bottom' | number; diff --git a/src/util/misc/boundingBoxFromPoints.ts b/src/util/misc/boundingBoxFromPoints.ts index 9703295d376..253d4f9ae00 100644 --- a/src/util/misc/boundingBoxFromPoints.ts +++ b/src/util/misc/boundingBoxFromPoints.ts @@ -1,4 +1,5 @@ import { IPoint, Point } from '../../point.class'; +import { TBBox } from '../../typedefs'; /** * Calculates bounding box (left, top, width, height) from given `points` @@ -7,7 +8,7 @@ import { IPoint, Point } from '../../point.class'; * @param {IPoint[]} points * @return {Object} Object with left, top, width, height properties */ -export const makeBoundingBoxFromPoints = (points: IPoint[]) => { +export const makeBoundingBoxFromPoints = (points: IPoint[]): TBBox => { if (points.length === 0) { return { left: 0, diff --git a/test/unit/object.js b/test/unit/object.js index 1f88774f4af..4ebe7bda7f8 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -317,7 +317,6 @@ cObj.scale(1.5); assert.equal(cObj.get('scaleX'), 1.5); assert.equal(cObj.get('scaleY'), 1.5); - assert.equal(cObj.scale(2), cObj, 'chainable'); }); QUnit.test('setOpacity', function(assert) { diff --git a/test/unit/object_geometry.js b/test/unit/object_geometry.js index 1534463219e..561d82802e7 100644 --- a/test/unit/object_geometry.js +++ b/test/unit/object_geometry.js @@ -246,8 +246,7 @@ QUnit.test('setCoords', function(assert) { var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100, strokeWidth: 0}); assert.ok(typeof cObj.setCoords === 'function'); - assert.equal(cObj.setCoords(), cObj, 'chainable'); - + cObj.setCoords(); assert.equal(cObj.oCoords.tl.x, 150); assert.equal(cObj.oCoords.tl.y, 150); assert.equal(cObj.oCoords.tr.x, 250); @@ -470,8 +469,8 @@ QUnit.test('scaleToWidth', function(assert) { var cObj = new fabric.Object({ width: 560, strokeWidth: 0 }); - assert.ok(typeof cObj.scaleToWidth === 'function', 'scaleToWidth should exist'); - assert.equal(cObj.scaleToWidth(100), cObj, 'chainable'); + assert.ok(typeof cObj.scaleToWidth === 'function', 'scaleToWidth should exist'); + cObj.scaleToWidth(100); assert.equal(cObj.getScaledWidth(), 100); assert.equal(cObj.get('scaleX'), 100 / 560); }); @@ -481,10 +480,10 @@ cObj.canvas = { viewportTransform: [2, 0, 0, 2, 0, 0] }; - assert.equal(cObj.scaleToWidth(100, true), cObj, 'chainable'); + cObj.scaleToWidth(100, true); assert.equal(cObj.getScaledWidth(), 100, 'is not influenced by zoom - width'); assert.equal(cObj.get('scaleX'), 100 / 560); - assert.equal(cObj.scaleToWidth(100), cObj, 'chainable'); + cObj.scaleToWidth(100); assert.equal(cObj.getScaledWidth(), 50, 'is influenced by zoom - width'); assert.equal(cObj.get('scaleX'), 100 / 560 / 2); }); @@ -493,7 +492,7 @@ QUnit.test('scaleToHeight', function(assert) { var cObj = new fabric.Object({ height: 560, strokeWidth: 0 }); assert.ok(typeof cObj.scaleToHeight === 'function', 'scaleToHeight should exist'); - assert.equal(cObj.scaleToHeight(100), cObj, 'chainable'); + cObj.scaleToHeight(100); assert.equal(cObj.getScaledHeight(), 100); assert.equal(cObj.get('scaleY'), 100 / 560); }); @@ -503,10 +502,10 @@ cObj.canvas = { viewportTransform: [2, 0, 0, 2, 0, 0] }; - assert.equal(cObj.scaleToHeight(100, true), cObj, 'chainable'); + cObj.scaleToHeight(100, true); assert.equal(cObj.getScaledHeight(), 100, 'is not influenced by zoom - height'); assert.equal(cObj.get('scaleY'), 100 / 560); - assert.equal(cObj.scaleToHeight(100), cObj, 'chainable'); + cObj.scaleToHeight(100); assert.equal(cObj.getScaledHeight(), 50, 'is influenced by zoom - height'); assert.equal(cObj.get('scaleY'), 100 / 560 / 2); }); @@ -566,14 +565,16 @@ assert.equal(boundingRect.width, 123); assert.equal(boundingRect.height, 0); - cObj.set('height', 167).setCoords(); + cObj.set('height', 167); + cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); assert.equal(Math.abs(boundingRect.top).toFixed(13), 0); assert.equal(boundingRect.width, 123); assert.equal(boundingRect.height, 167); - cObj.scale(2).setCoords(); + cObj.scale(2) + cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left, 0); assert.equal(Math.abs(boundingRect.top).toFixed(13), 0); @@ -593,21 +594,24 @@ assert.equal(boundingRect.width.toFixed(2), 1); assert.equal(boundingRect.height.toFixed(2), 1); - cObj.set('width', 123).setCoords(); + cObj.set('width', 123) + cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); assert.equal(boundingRect.width.toFixed(2), 124); assert.equal(boundingRect.height.toFixed(2), 1); - cObj.set('height', 167).setCoords(); + cObj.set('height', 167) + cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); assert.equal(boundingRect.width.toFixed(2), 124); assert.equal(boundingRect.height.toFixed(2), 168); - cObj.scale(2).setCoords(); + cObj.scale(2) + cObj.setCoords(); boundingRect = cObj.getBoundingRect(); assert.equal(boundingRect.left.toFixed(2), 0); assert.equal(boundingRect.top.toFixed(2), 0); diff --git a/test/unit/object_origin.js b/test/unit/object_origin.js index 8c5eac317d8..4385c81b00f 100644 --- a/test/unit/object_origin.js +++ b/test/unit/object_origin.js @@ -234,120 +234,6 @@ assert.deepEqual(p, new fabric.Point(-58.791317146942106, -3.9842049203432026)); }); - - QUnit.test('adjustPosition', function(assert) { - var rect = new fabric.Rect(rectOptions); - - rect.strokeWidth = 0; - rect.originX = 'left'; - rect.originY = 'top'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 35); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'right'); - - rect.originX = 'center'; - rect.originY = 'center'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 95); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'right'); - - rect.originX = 'right'; - rect.originY = 'bottom'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 95); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'right'); - }); - - QUnit.test('adjustPositionRotated', function(assert) { - var rect = new fabric.Rect(rectOptions); - - rect.angle = 35; - rect.strokeWidth = 0; - rect.originX = 'left'; - rect.originY = 'top'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 35); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.471528727020925); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 'right'); - - rect.originX = 'center'; - rect.originY = 'center'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.471528727020925); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 84.1491226573395); - assert.deepEqual(rect.top, 79.41458618106277); - assert.equal(rect.originX, 'right'); - - rect.originX = 'right'; - rect.originY = 'bottom'; - rect.adjustPosition('left'); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.47152872702093); - assert.equal(rect.originX, 'left'); - - rect.adjustPosition('center'); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 'center'); - - rect.adjustPosition('right'); - assert.deepEqual(rect.left, 84.1491226573395); - assert.deepEqual(rect.top, 79.41458618106277); - assert.equal(rect.originX, 'right'); - }); - QUnit.test('translateToCenterPoint with numeric origins', function(assert) { var rect = new fabric.Rect(rectOptions), p, @@ -560,118 +446,4 @@ assert.deepEqual(p, new fabric.Point(-58.791317146942106, -3.9842049203432026)); }); - - QUnit.test('adjustPosition with numeric origins', function(assert) { - var rect = new fabric.Rect(rectOptions); - - rect.strokeWidth = 0; - rect.originX = 'left'; - rect.originY = 'top'; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 35); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 1); - - rect.originX = 0.5; - rect.originY = 0.5; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 95); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 1); - - rect.originX = 1; - rect.originY = 1; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 55); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 75); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 95); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 1); - }); - - QUnit.test('adjustPositionRotated with numeric origins', function(assert) { - var rect = new fabric.Rect(rectOptions); - - rect.angle = 35; - rect.strokeWidth = 0; - rect.originX = 0; - rect.originY = 0; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 35); - assert.deepEqual(rect.top, 45); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.471528727020925); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 1); - - rect.originX = 0.5; - rect.originY = 0.5; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.471528727020925); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 84.1491226573395); - assert.deepEqual(rect.top, 79.41458618106277); - assert.equal(rect.originX, 1); - - rect.originX = 1; - rect.originY = 1; - rect.adjustPosition(0); - assert.deepEqual(rect.left, 51.383040885779835); - assert.deepEqual(rect.top, 56.47152872702093); - assert.equal(rect.originX, 0); - - rect.adjustPosition(0.5); - assert.deepEqual(rect.left, 67.76608177155967); - assert.deepEqual(rect.top, 67.94305745404185); - assert.equal(rect.originX, 0.5); - - rect.adjustPosition(1); - assert.deepEqual(rect.left, 84.1491226573395); - assert.deepEqual(rect.top, 79.41458618106277); - assert.equal(rect.originX, 1); - }); - })();