UNPKG

fabric

Version:

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

288 lines (271 loc) 9.76 kB
import type { ControlCursorCallback, TPointerEvent, Transform, TransformActionHandler, } from '../EventTypeDefs'; import type { FabricObject } from '../shapes/Object/FabricObject'; import type { TAxis } from '../typedefs'; import type { Canvas } from '../canvas/Canvas'; import { findCornerQuadrant, getLocalPoint, invertOrigin, isLocked, isTransformCentered, NOT_ALLOWED_CURSOR, } from './util'; import { wrapWithFireEvent } from './wrapWithFireEvent'; import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; import { SCALE_X, SCALE_Y, SCALING } from '../constants'; type ScaleTransform = Transform & { gestureScale?: number; signX?: number; signY?: number; }; type ScaleBy = TAxis | 'equally' | '' | undefined; /** * Inspect event and fabricObject properties to understand if the scaling action * @param {Event} eventData from the user action * @param {FabricObject} fabricObject the fabric object about to scale * @return {Boolean} true if scale is proportional */ export function scaleIsProportional( eventData: TPointerEvent, fabricObject: FabricObject, ): boolean { const canvas = fabricObject.canvas as Canvas, uniformIsToggled = eventData[canvas.uniScaleKey!]; return ( (canvas.uniformScaling && !uniformIsToggled) || (!canvas.uniformScaling && uniformIsToggled) ); } /** * Inspect fabricObject to understand if the current scaling action is allowed * @param {FabricObject} fabricObject the fabric object about to scale * @param {String} by 'x' or 'y' or '' * @param {Boolean} scaleProportionally true if we are trying to scale proportionally * @return {Boolean} true if scaling is not allowed at current conditions */ export function scalingIsForbidden( fabricObject: FabricObject, by: ScaleBy, scaleProportionally: boolean, ) { const lockX = isLocked(fabricObject, 'lockScalingX'), lockY = isLocked(fabricObject, 'lockScalingY'); if (lockX && lockY) { return true; } if (!by && (lockX || lockY) && scaleProportionally) { return true; } if (lockX && by === 'x') { return true; } if (lockY && by === 'y') { return true; } // code crashes because of a division by 0 if a 0 sized object is scaled // forbid to prevent scaling to happen. ISSUE-9475 const { width, height, strokeWidth } = fabricObject; if (width === 0 && strokeWidth === 0 && by !== 'y') { return true; } if (height === 0 && strokeWidth === 0 && by !== 'x') { return true; } return false; } const scaleMap = ['e', 'se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e']; /** * return the correct cursor style for the scale action * @param {Event} eventData the javascript event that is causing the scale * @param {Control} control the control that is interested in the action * @param {FabricObject} fabricObject the fabric object that is interested in the action * @return {String} a valid css string for the cursor */ export const scaleCursorStyleHandler: ControlCursorCallback = ( eventData, control, fabricObject, ) => { const scaleProportionally = scaleIsProportional(eventData, fabricObject), by = control.x !== 0 && control.y === 0 ? 'x' : control.x === 0 && control.y !== 0 ? 'y' : ''; if (scalingIsForbidden(fabricObject, by, scaleProportionally)) { return NOT_ALLOWED_CURSOR; } const n = findCornerQuadrant(fabricObject, control); return `${scaleMap[n]}-resize`; }; /** * Basic scaling logic, reused with different constrain for scaling X,Y, freely or equally. * Needs to be wrapped with `wrapWithFixedAnchor` to be effective * @param {Event} eventData javascript event that is doing the transform * @param {Object} transform javascript object containing a series of information around the current transform * @param {number} x current mouse x position, canvas normalized * @param {number} y current mouse y position, canvas normalized * @param {Object} options additional information for scaling * @param {String} options.by 'x', 'y', 'equally' or '' to indicate type of scaling * @return {Boolean} true if some change happened * @private */ function scaleObject( eventData: TPointerEvent, transform: ScaleTransform, x: number, y: number, options: { by?: ScaleBy } = {}, ) { const target = transform.target, by = options.by, scaleProportionally = scaleIsProportional(eventData, target), forbidScaling = scalingIsForbidden(target, by, scaleProportionally); let newPoint, scaleX, scaleY, dim, signX, signY; if (forbidScaling) { return false; } if (transform.gestureScale) { scaleX = transform.scaleX * transform.gestureScale; scaleY = transform.scaleY * transform.gestureScale; } else { newPoint = getLocalPoint( transform, transform.originX, transform.originY, x, y, ); // use of sign: We use sign to detect change of direction of an action. sign usually change when // we cross the origin point with the mouse. So a scale flip for example. There is an issue when scaling // by center and scaling using one middle control ( default: mr, mt, ml, mb), the mouse movement can easily // cross many time the origin point and flip the object. so we need a way to filter out the noise. // This ternary here should be ok to filter out X scaling when we want Y only and vice versa. signX = by !== 'y' ? Math.sign(newPoint.x || transform.signX || 1) : 1; signY = by !== 'x' ? Math.sign(newPoint.y || transform.signY || 1) : 1; if (!transform.signX) { transform.signX = signX; } if (!transform.signY) { transform.signY = signY; } if ( isLocked(target, 'lockScalingFlip') && (transform.signX !== signX || transform.signY !== signY) ) { return false; } dim = target._getTransformedDimensions(); // missing detection of flip and logic to switch the origin if (scaleProportionally && !by) { // uniform scaling const distance = Math.abs(newPoint.x) + Math.abs(newPoint.y), { original } = transform, originalDistance = Math.abs((dim.x * original.scaleX) / target.scaleX) + Math.abs((dim.y * original.scaleY) / target.scaleY), scale = distance / originalDistance; scaleX = original.scaleX * scale; scaleY = original.scaleY * scale; } else { scaleX = Math.abs((newPoint.x * target.scaleX) / dim.x); scaleY = Math.abs((newPoint.y * target.scaleY) / dim.y); } // if we are scaling by center, we need to double the scale if (isTransformCentered(transform)) { scaleX *= 2; scaleY *= 2; } if (transform.signX !== signX && by !== 'y') { transform.originX = invertOrigin(transform.originX); scaleX *= -1; transform.signX = signX; } if (transform.signY !== signY && by !== 'x') { transform.originY = invertOrigin(transform.originY); scaleY *= -1; transform.signY = signY; } } // minScale is taken care of in the setter. const oldScaleX = target.scaleX, oldScaleY = target.scaleY; if (!by) { !isLocked(target, 'lockScalingX') && target.set(SCALE_X, scaleX); !isLocked(target, 'lockScalingY') && target.set(SCALE_Y, scaleY); } else { // forbidden cases already handled on top here. by === 'x' && target.set(SCALE_X, scaleX); by === 'y' && target.set(SCALE_Y, scaleY); } return oldScaleX !== target.scaleX || oldScaleY !== target.scaleY; } /** * Generic scaling logic, to scale from corners either equally or freely. * Needs to be wrapped with `wrapWithFixedAnchor` to be effective * @param {Event} eventData javascript event that is doing the transform * @param {Object} transform javascript object containing a series of information around the current transform * @param {number} x current mouse x position, canvas normalized * @param {number} y current mouse y position, canvas normalized * @return {Boolean} true if some change happened */ export const scaleObjectFromCorner: TransformActionHandler<ScaleTransform> = ( eventData, transform, x, y, ) => { return scaleObject(eventData, transform, x, y); }; /** * Scaling logic for the X axis. * Needs to be wrapped with `wrapWithFixedAnchor` to be effective * @param {Event} eventData javascript event that is doing the transform * @param {Object} transform javascript object containing a series of information around the current transform * @param {number} x current mouse x position, canvas normalized * @param {number} y current mouse y position, canvas normalized * @return {Boolean} true if some change happened */ const scaleObjectX: TransformActionHandler<ScaleTransform> = ( eventData, transform, x, y, ) => { return scaleObject(eventData, transform, x, y, { by: 'x' }); }; /** * Scaling logic for the Y axis. * Needs to be wrapped with `wrapWithFixedAnchor` to be effective * @param {Event} eventData javascript event that is doing the transform * @param {Object} transform javascript object containing a series of information around the current transform * @param {number} x current mouse x position, canvas normalized * @param {number} y current mouse y position, canvas normalized * @return {Boolean} true if some change happened */ const scaleObjectY: TransformActionHandler<ScaleTransform> = ( eventData, transform, x, y, ) => { return scaleObject(eventData, transform, x, y, { by: 'y' }); }; export const scalingEqually = wrapWithFireEvent( SCALING, wrapWithFixedAnchor(scaleObjectFromCorner), ); export const scalingX = wrapWithFireEvent( SCALING, wrapWithFixedAnchor(scaleObjectX), ); export const scalingY = wrapWithFireEvent( SCALING, wrapWithFixedAnchor(scaleObjectY), );