UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

659 lines (604 loc) 22.9 kB
import { objectSpread2 as _objectSpread2 } from '../../../_virtual/_rollupPluginBabelHelpers.mjs'; import { SCALE_X, SCALE_Y, iMatrix, CENTER, LEFT, TOP } from '../../constants.mjs'; import { Intersection } from '../../Intersection.mjs'; import { Point } from '../../Point.mjs'; import { makeBoundingBoxFromPoints } from '../../util/misc/boundingBoxFromPoints.mjs'; import { transformPoint, invertTransform, calcPlaneRotation, createRotateMatrix, multiplyTransformMatrices, composeMatrix, calcDimensionsMatrix, createTranslateMatrix } from '../../util/misc/matrix.mjs'; import { radiansToDegrees, degreesToRadians } from '../../util/misc/radiansDegreesConversion.mjs'; import { resolveOrigin } from '../../util/misc/resolveOrigin.mjs'; import { sizeAfterTransform } from '../../util/misc/objectTransforms.mjs'; import { CommonMethods } from '../../CommonMethods.mjs'; class ObjectGeometry extends CommonMethods { // #region Geometry /** * Describe object's corner position in scene coordinates. * The coordinates are derived from the following: * left, top, width, height, scaleX, scaleY, skewX, skewY, angle, strokeWidth. * The coordinates do not depend on viewport changes. * The coordinates get updated with {@link setCoords}. * You can calculate them without updating with {@link calcACoords()} */ /** * storage cache for object transform matrix */ /** * storage cache for object full transform matrix */ /** * A Reference of the Canvas where the object is actually added * @type StaticCanvas | Canvas; * @default undefined * @private */ /** * @returns {number} x position according to object's {@link FabricObject#originX} property in canvas coordinate plane */ getX() { return this.getXY().x; } /** * @param {number} value x position according to object's {@link FabricObject#originX} property in canvas coordinate plane */ setX(value) { this.setXY(this.getXY().setX(value)); } /** * @returns {number} y position according to object's {@link FabricObject#originY} property in canvas coordinate plane */ getY() { return this.getXY().y; } /** * @param {number} value y position according to object's {@link FabricObject#originY} property in canvas coordinate plane */ setY(value) { this.setXY(this.getXY().setY(value)); } /** * @returns {number} x position according to object's {@link FabricObject#originX} property in parent's coordinate plane\ * if parent is canvas then this property is identical to {@link getX} */ getRelativeX() { return this.left; } /** * @param {number} value x position according to object's {@link FabricObject#originX} property in parent's coordinate plane\ * if parent is canvas then this method is identical to {@link setX} */ setRelativeX(value) { this.left = value; } /** * @returns {number} y position according to object's {@link FabricObject#originY} property in parent's coordinate plane\ * if parent is canvas then this property is identical to {@link getY} */ getRelativeY() { return this.top; } /** * @param {number} value y position according to object's {@link FabricObject#originY} property in parent's coordinate plane\ * if parent is canvas then this property is identical to {@link setY} */ setRelativeY(value) { this.top = value; } /** * @returns {Point} x position according to object's {@link FabricObject#originX} {@link FabricObject#originY} properties in canvas coordinate plane */ getXY() { const relativePosition = this.getRelativeXY(); return this.group ? 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 FabricObject#originX} and {@link FabricObject#originY} values, * that otherwise are the object's current values. * @example <caption>Set object's bottom left corner to point (5,5) on canvas</caption> * object.setXY(new Point(5, 5), 'left', 'bottom'). * @param {Point} point position in scene coordinate plane * @param {TOriginX} [originX] Horizontal origin: 'left', 'center' or 'right' * @param {TOriginY} [originY] Vertical origin: 'top', 'center' or 'bottom' */ setXY(point, originX, originY) { if (this.group) { point = transformPoint(point, invertTransform(this.group.calcTransformMatrix())); } this.setRelativeXY(point, originX, originY); } /** * @returns {Point} x,y position according to object's {@link FabricObject#originX} {@link FabricObject#originY} properties in parent's coordinate plane */ getRelativeXY() { return new Point(this.left, this.top); } /** * As {@link 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 FabricObject#originX} {@link FabricObject#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) { let originX = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.originX; let originY = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.originY; this.setPositionByOrigin(point, originX, originY); } /** * @deprecated intermidiate method to be removed, do not use */ isStrokeAccountedForInDimensions() { return false; } /** * @return {Point[]} [tl, tr, br, bl] in the scene plane */ getCoords() { const { tl, tr, br, bl } = this.aCoords || (this.aCoords = this.calcACoords()); const coords = [tl, tr, br, bl]; if (this.group) { const t = this.group.calcTransformMatrix(); return coords.map(p => transformPoint(p, t)); } return coords; } /** * Checks if object intersects with the scene rect formed by TL and BR * In this case the rect is meant aligned with the axis of the canvas. * TL is the TOP LEFT point while br is the BOTTOM RIGHT point */ intersectsWithRect(tl, br) { const intersection = Intersection.intersectPolygonRectangle(this.getCoords(), tl, br); return intersection.status === 'Intersection'; } /** * Checks if object intersects with another object * @param {Object} other Object to test * @return {Boolean} true if object intersects with another object */ intersectsWithObject(other) { const intersection = Intersection.intersectPolygonPolygon(this.getCoords(), other.getCoords()); return intersection.status === 'Intersection' || intersection.status === 'Coincident' || other.isContainedWithinObject(this) || this.isContainedWithinObject(other); } /** * Checks if object is fully contained within area of another object * @param {Object} other Object to test * @return {Boolean} true if object is fully contained within area of another object */ isContainedWithinObject(other) { const points = this.getCoords(); return points.every(point => other.containsPoint(point)); } /** * Checks if object is fully contained within the scene rect formed by TL and BR */ isContainedWithinRect(tl, br) { const { left, top, width, height } = this.getBoundingRect(); return left >= tl.x && left + width <= br.x && top >= tl.y && top + height <= br.y; } isOverlapping(other) { return this.intersectsWithObject(other) || this.isContainedWithinObject(other) || other.isContainedWithinObject(this); } /** * Checks if point is inside the object * @param {Point} point Point to check against * @return {Boolean} true if point is inside the object */ containsPoint(point) { return Intersection.isPointInPolygon(point, this.getCoords()); } /** * Checks if object is contained within the canvas with current viewportTransform * the check is done stopping at first point that appears on screen * @return {Boolean} true if object is fully or partially contained within canvas */ isOnScreen() { if (!this.canvas) { return false; } const { tl, br } = this.canvas.vptCoords; const points = this.getCoords(); // 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)) { return true; } // check if the object is so big that it contains the entire viewport return this.containsPoint(tl.midPointFrom(br)); } /** * Checks if object is partially contained within the canvas with current viewportTransform * @return {Boolean} true if object is partially contained within canvas */ isPartiallyOnScreen() { if (!this.canvas) { return false; } const { tl, br } = this.canvas.vptCoords; if (this.intersectsWithRect(tl, br)) { return true; } const allPointsAreOutside = this.getCoords().every(point => (point.x >= br.x || point.x <= tl.x) && (point.y >= br.y || point.y <= tl.y)); // check if the object is so big that it contains the entire viewport return allPointsAreOutside && this.containsPoint(tl.midPointFrom(br)); } /** * Returns coordinates of object's bounding rectangle (left, top, width, height) * the box is intended as aligned to axis of canvas. * @return {Object} Object with left, top, width, height properties */ getBoundingRect() { return makeBoundingBoxFromPoints(this.getCoords()); } /** * 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() { 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() { return this._getTransformedDimensions().y; } /** * Scales an object (equally by x and y) * @param {Number} value Scale factor * @return {void} */ scale(value) { this._set(SCALE_X, value); this._set(SCALE_Y, value); this.setCoords(); } /** * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) * @param {Number} value New width value * @return {void} */ scaleToWidth(value) { // adjust to bounding rect factor so that rotated shapes would fit as well const boundingRectFactor = this.getBoundingRect().width / this.getScaledWidth(); return this.scale(value / this.width / boundingRectFactor); } /** * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) * @param {Number} value New height value * @return {void} */ scaleToHeight(value) { // adjust to bounding rect factor so that rotated shapes would fit as well const boundingRectFactor = this.getBoundingRect().height / this.getScaledHeight(); return this.scale(value / this.height / boundingRectFactor); } getCanvasRetinaScaling() { var _this$canvas; return ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.getRetinaScaling()) || 1; } /** * Returns the object angle relative to canvas counting also the group property * @returns {TDegree} */ getTotalAngle() { return this.group ? radiansToDegrees(calcPlaneRotation(this.calcTransformMatrix())) : this.angle; } /** * Retrieves viewportTransform from Object's canvas if available * @return {TMat2D} */ getViewportTransform() { var _this$canvas2; return ((_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 ? void 0 : _this$canvas2.viewportTransform) || iMatrix.concat(); } /** * Calculates the coordinates of the 4 corner of the bbox, in absolute coordinates. * those never change with zoom or viewport changes. * @return {TCornerPoint} */ calcACoords() { const rotateMatrix = createRotateMatrix({ angle: this.angle }), { x, y } = this.getRelativeCenterPoint(), tMatrix = createTranslateMatrix(x, y), finalMatrix = multiplyTransformMatrices(tMatrix, 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) }; } /** * Sets corner and controls position coordinates based on current angle, width and height, left and top. * aCoords are used to quickly find an object on the canvas. * See {@link https://github.com/fabricjs/fabric.js/wiki/When-to-call-setCoords} and {@link http://fabricjs.com/fabric-gotchas} */ setCoords() { this.aCoords = this.calcACoords(); } transformMatrixKey() { let skipGroup = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; let prefix = []; if (!skipGroup && this.group) { prefix = this.group.transformMatrixKey(skipGroup); } prefix.push(this.top, this.left, this.width, this.height, this.scaleX, this.scaleY, this.angle, this.strokeWidth, this.skewX, this.skewY, +this.flipX, +this.flipY, resolveOrigin(this.originX), resolveOrigin(this.originY)); return prefix; } /** * 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() { let skipGroup = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; let matrix = this.calcOwnMatrix(); if (skipGroup || !this.group) { return matrix; } const key = this.transformMatrixKey(skipGroup), cache = this.matrixCache; if (cache && cache.key.every((x, i) => x === key[i])) { return cache.value; } if (this.group) { matrix = multiplyTransformMatrices(this.group.calcTransformMatrix(false), matrix); } this.matrixCache = { key, value: matrix }; return matrix; } /** * 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() { 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 dimensions from its properties * @private * @returns {Point} dimensions */ _getNonTransformedDimensions() { 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(options) { return this._getTransformedDimensions(options).transform(this.getViewportTransform(), true).scalarAdd(2 * this.padding); } // #region Origin /** * @deprecated please use 'center' as value in new projects * */ /** * @deprecated please use 'center' as value in new projects * */ /** * Object containing this object. * can influence its size and position */ /** * Calculate object bounding box dimensions from its properties scale, skew. * This bounding box is aligned with object angle and not with canvas axis or screen. * @param {Object} [options] * @param {Number} [options.scaleX] * @param {Number} [options.scaleY] * @param {Number} [options.skewX] * @param {Number} [options.skewY] * @private * @returns {Point} dimensions */ _getTransformedDimensions() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; const dimOptions = _objectSpread2({ // if scaleX or scaleY are negative numbers, // this will return dimensions that are negative. // and this will break assumptions around the codebase 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, calcDimensionsMatrix(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, fromOriginX, fromOriginY, toOriginX, toOriginY) { 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); } /** * 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, originX, originY) { if (originX === CENTER && originY === CENTER) { return point; } const p = this.translateToGivenOrigin(point, originX, originY, CENTER, CENTER); if (this.angle) { return p.rotate(degreesToRadians(this.angle), point); } 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(center, originX, originY) { const p = this.translateToGivenOrigin(center, CENTER, CENTER, originX, originY); if (this.angle) { return p.rotate(degreesToRadians(this.angle), center); } return p; } /** * Returns the center coordinates of the object relative to canvas * @return {Point} */ getCenterPoint() { 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() { return this.translateToCenterPoint(new Point(this.left, this.top), this.originX, this.originY); } /** * Returns the position of the object as if it has a different origin. * Take an object that has left, top set to 100, 100 with origin 'left', 'top'. * Return the values of left top ( wrapped in a point ) that you would need to keep * the same position if origin where different. * Alternatively you can use this to also find which point in the parent plane is a specific origin * ( where is the bottom right corner of my object? ) * @param {TOriginX} originX Horizontal origin: 'left', 'center' or 'right' * @param {TOriginY} originY Vertical origin: 'top', 'center' or 'bottom' * @return {Point} */ getPointByOrigin(originX, originY) { return this.translateToOriginPoint(this.getRelativeCenterPoint(), originX, originY); } /** * 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, originX, originY) { const center = this.translateToCenterPoint(pos, originX, originY), position = this.translateToOriginPoint(center, this.originX, this.originY); this.set({ left: position.x, top: position.y }); } /** * @private */ _getLeftTopCoords() { return this.translateToOriginPoint(this.getRelativeCenterPoint(), LEFT, TOP); } } export { ObjectGeometry }; //# sourceMappingURL=ObjectGeometry.mjs.map