UNPKG

fabric

Version:

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

251 lines (234 loc) 7.95 kB
import type { ControlCursorCallback, TPointerEvent, Transform, TransformActionHandler, } from '../EventTypeDefs'; import { resolveOrigin } from '../util/misc/resolveOrigin'; import { Point } from '../Point'; import type { TAxis, TAxisKey } from '../typedefs'; import { degreesToRadians, radiansToDegrees, } from '../util/misc/radiansDegreesConversion'; import { findCornerQuadrant, getLocalPoint, isLocked, NOT_ALLOWED_CURSOR, } from './util'; import { wrapWithFireEvent } from './wrapWithFireEvent'; import { wrapWithFixedAnchor } from './wrapWithFixedAnchor'; import { CENTER, SCALE_X, SCALE_Y, SKEWING, SKEW_X, SKEW_Y, } from '../constants'; export type SkewTransform = Transform & { skewingSide: -1 | 1 }; const AXIS_KEYS: Record< TAxis, { counterAxis: TAxis; scale: TAxisKey<'scale'>; skew: TAxisKey<'skew'>; lockSkewing: TAxisKey<'lockSkewing'>; origin: TAxisKey<'origin'>; flip: TAxisKey<'flip'>; } > = { x: { counterAxis: 'y', scale: SCALE_X, skew: SKEW_X, lockSkewing: 'lockSkewingX', origin: 'originX', flip: 'flipX', }, y: { counterAxis: 'x', scale: SCALE_Y, skew: SKEW_Y, lockSkewing: 'lockSkewingY', origin: 'originY', flip: 'flipY', }, }; const skewMap = ['ns', 'nesw', 'ew', 'nwse']; /** * return the correct cursor style for the skew 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 skewCursorStyleHandler: ControlCursorCallback = ( eventData, control, fabricObject, ) => { if (control.x !== 0 && isLocked(fabricObject, 'lockSkewingY')) { return NOT_ALLOWED_CURSOR; } if (control.y !== 0 && isLocked(fabricObject, 'lockSkewingX')) { return NOT_ALLOWED_CURSOR; } const n = findCornerQuadrant(fabricObject, control) % 4; return `${skewMap[n]}-resize`; }; /** * Since skewing is applied before scaling, calculations are done in a scaleless plane * @see https://github.com/fabricjs/fabric.js/pull/8380 */ function skewObject( axis: TAxis, { target, ex, ey, skewingSide, ...transform }: SkewTransform, pointer: Point, ) { const { skew: skewKey } = AXIS_KEYS[axis], offset = pointer .subtract(new Point(ex, ey)) .divide(new Point(target.scaleX, target.scaleY))[axis], skewingBefore = target[skewKey], skewingStart = transform[skewKey], shearingStart = Math.tan(degreesToRadians(skewingStart)), // let a, b be the size of target // let a' be the value of a after applying skewing // then: // a' = a + b * skewA => skewA = (a' - a) / b // the value b is tricky since skewY is applied before skewX b = axis === 'y' ? target._getTransformedDimensions({ scaleX: 1, scaleY: 1, // since skewY is applied before skewX, b (=width) is not affected by skewX skewX: 0, }).x : target._getTransformedDimensions({ scaleX: 1, scaleY: 1, }).y; const shearing = (2 * offset * skewingSide) / // we max out fractions to safeguard from asymptotic behavior Math.max(b, 1) + // add starting state shearingStart; const skewing = radiansToDegrees(Math.atan(shearing)); target.set(skewKey, skewing); const changed = skewingBefore !== target[skewKey]; if (changed && axis === 'y') { // we don't want skewing to affect scaleX // so we factor it by the inverse skewing diff to make it seem unchanged to the viewer const { skewX, scaleX } = target, dimBefore = target._getTransformedDimensions({ skewY: skewingBefore }), dimAfter = target._getTransformedDimensions(), compensationFactor = skewX !== 0 ? dimBefore.x / dimAfter.x : 1; compensationFactor !== 1 && target.set(SCALE_X, compensationFactor * scaleX); } return changed; } /** * Wrapped Action handler for skewing on a given axis, takes care of the * skew direction and determines the correct transform origin for the anchor point * @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 */ function skewHandler( axis: TAxis, eventData: TPointerEvent, transform: Transform, x: number, y: number, ) { const { target } = transform, { counterAxis, origin: originKey, lockSkewing: lockSkewingKey, skew: skewKey, flip: flipKey, } = AXIS_KEYS[axis]; if (isLocked(target, lockSkewingKey)) { return false; } const { origin: counterOriginKey, flip: counterFlipKey } = AXIS_KEYS[counterAxis], counterOriginFactor = resolveOrigin(transform[counterOriginKey]) * (target[counterFlipKey] ? -1 : 1), // if the counter origin is top/left (= -0.5) then we are skewing x/y values on the bottom/right side of target respectively. // if the counter origin is bottom/right (= 0.5) then we are skewing x/y values on the top/left side of target respectively. // skewing direction on the top/left side of target is OPPOSITE to the direction of the movement of the pointer, // so we factor skewing direction by this value. skewingSide = (-Math.sign(counterOriginFactor) * (target[flipKey] ? -1 : 1)) as 1 | -1, skewingDirection = ((target[skewKey] === 0 && // in case skewing equals 0 we use the pointer offset from target center to determine the direction of skewing getLocalPoint(transform, CENTER, CENTER, x, y)[axis] > 0) || // in case target has skewing we use that as the direction target[skewKey] > 0 ? 1 : -1) * skewingSide, // anchor to the opposite side of the skewing direction // normalize value from [-1, 1] to origin value [0, 1] origin = -skewingDirection * 0.5 + 0.5; const finalHandler = wrapWithFireEvent<SkewTransform>( SKEWING, wrapWithFixedAnchor((eventData, transform, x, y) => skewObject(axis, transform, new Point(x, y)), ), ); return finalHandler( eventData, { ...transform, [originKey]: origin, skewingSide, }, x, y, ); } /** * Wrapped Action handler for skewing on the X axis, takes care of the * skew direction and determines the correct transform origin for the anchor point * @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 skewHandlerX: TransformActionHandler = ( eventData, transform, x, y, ) => { return skewHandler('x', eventData, transform, x, y); }; /** * Wrapped Action handler for skewing on the Y axis, takes care of the * skew direction and determines the correct transform origin for the anchor point * @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 skewHandlerY: TransformActionHandler = ( eventData, transform, x, y, ) => { return skewHandler('y', eventData, transform, x, y); };