UNPKG

fabric-pure-browser

Version:

Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in

1,354 lines (1,221 loc) 43.9 kB
(function() { var getPointer = fabric.util.getPointer, degreesToRadians = fabric.util.degreesToRadians, abs = Math.abs, supportLineDash = fabric.StaticCanvas.supports('setLineDash'), isTouchEvent = fabric.util.isTouchEvent, STROKE_OFFSET = 0.5; /** * Canvas class * @class fabric.Canvas * @extends fabric.StaticCanvas * @tutorial {@link http://fabricjs.com/fabric-intro-part-1#canvas} * @see {@link fabric.Canvas#initialize} for constructor definition * * @fires object:modified at the end of a transform or any change when statefull is true * @fires object:rotated at the end of a rotation transform * @fires object:scaled at the end of a scale transform * @fires object:moved at the end of translation transform * @fires object:skewed at the end of a skew transform * @fires object:rotating while an object is being rotated from the control * @fires object:scaling while an object is being scaled by controls * @fires object:moving while an object is being dragged * @fires object:skewing while an object is being skewed from the controls * * @fires before:transform before a transform is is started * @fires before:selection:cleared * @fires selection:cleared * @fires selection:updated * @fires selection:created * * @fires path:created after a drawing operation ends and the path is added * @fires mouse:down * @fires mouse:move * @fires mouse:up * @fires mouse:down:before on mouse down, before the inner fabric logic runs * @fires mouse:move:before on mouse move, before the inner fabric logic runs * @fires mouse:up:before on mouse up, before the inner fabric logic runs * @fires mouse:over * @fires mouse:out * @fires mouse:dblclick whenever a native dbl click event fires on the canvas. * * @fires dragover * @fires dragenter * @fires dragleave * @fires drop * @fires after:render at the end of the render process, receives the context in the callback * @fires before:render at start the render process, receives the context in the callback * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { /** * Constructor * @param {HTMLElement | String} el &lt;canvas> element to initialize instance on * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(el, options) { options || (options = { }); this.renderAndResetBound = this.renderAndReset.bind(this); this.requestRenderAllBound = this.requestRenderAll.bind(this); this._initStatic(el, options); this._initInteractive(); this._createCacheCanvas(); }, /** * When true, objects can be transformed by one side (unproportionally) * when dragged on the corners that normally would not do that. * @type Boolean * @default * @since fabric 4.0 // changed name and default value */ uniformScaling: true, /** * Indicates which key switches uniform scaling. * values: 'altKey', 'shiftKey', 'ctrlKey'. * If `null` or 'none' or any other string that is not a modifier key * feature is disabled. * totally wrong named. this sounds like `uniform scaling` * if Canvas.uniformScaling is true, pressing this will set it to false * and viceversa. * @since 1.6.2 * @type String * @default */ uniScaleKey: 'shiftKey', /** * When true, objects use center point as the origin of scale transformation. * <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredScaling: false, /** * When true, objects use center point as the origin of rotate transformation. * <b>Backwards incompatibility note:</b> This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredRotation: false, /** * Indicates which key enable centered Transform * values: 'altKey', 'shiftKey', 'ctrlKey'. * If `null` or 'none' or any other string that is not a modifier key * feature is disabled feature disabled. * @since 1.6.2 * @type String * @default */ centeredKey: 'altKey', /** * Indicates which key enable alternate action on corner * values: 'altKey', 'shiftKey', 'ctrlKey'. * If `null` or 'none' or any other string that is not a modifier key * feature is disabled feature disabled. * @since 1.6.2 * @type String * @default */ altActionKey: 'shiftKey', /** * Indicates that canvas is interactive. This property should not be changed. * @type Boolean * @default */ interactive: true, /** * Indicates whether group selection should be enabled * @type Boolean * @default */ selection: true, /** * Indicates which key or keys enable multiple click selection * Pass value as a string or array of strings * values: 'altKey', 'shiftKey', 'ctrlKey'. * If `null` or empty or containing any other string that is not a modifier key * feature is disabled. * @since 1.6.2 * @type String|Array * @default */ selectionKey: 'shiftKey', /** * Indicates which key enable alternative selection * in case of target overlapping with active object * values: 'altKey', 'shiftKey', 'ctrlKey'. * For a series of reason that come from the general expectations on how * things should work, this feature works only for preserveObjectStacking true. * If `null` or 'none' or any other string that is not a modifier key * feature is disabled. * @since 1.6.5 * @type null|String * @default */ altSelectionKey: null, /** * Color of selection * @type String * @default */ selectionColor: 'rgba(100, 100, 255, 0.3)', // blue /** * Default dash array pattern * If not empty the selection border is dashed * @type Array */ selectionDashArray: [], /** * Color of the border of selection (usually slightly darker than color of selection itself) * @type String * @default */ selectionBorderColor: 'rgba(255, 255, 255, 0.3)', /** * Width of a line used in object/group selection * @type Number * @default */ selectionLineWidth: 1, /** * Select only shapes that are fully contained in the dragged selection rectangle. * @type Boolean * @default */ selectionFullyContained: false, /** * Default cursor value used when hovering over an object on canvas * @type String * @default */ hoverCursor: 'move', /** * Default cursor value used when moving an object on canvas * @type String * @default */ moveCursor: 'move', /** * Default cursor value used for the entire canvas * @type String * @default */ defaultCursor: 'default', /** * Cursor value used during free drawing * @type String * @default */ freeDrawingCursor: 'crosshair', /** * Cursor value used for rotation point * @type String * @default */ rotationCursor: 'crosshair', /** * Cursor value used for disabled elements ( corners with disabled action ) * @type String * @since 2.0.0 * @default */ notAllowedCursor: 'not-allowed', /** * Default element class that's given to wrapper (div) element of canvas * @type String * @default */ containerClass: 'canvas-container', /** * When true, object detection happens on per-pixel basis rather than on per-bounding-box * @type Boolean * @default */ perPixelTargetFind: false, /** * Number of pixels around target pixel to tolerate (consider active) during object detection * @type Number * @default */ targetFindTolerance: 0, /** * When true, target detection is skipped when hovering over canvas. This can be used to improve performance. * @type Boolean * @default */ skipTargetFind: false, /** * When true, mouse events on canvas (mousedown/mousemove/mouseup) result in free drawing. * After mousedown, mousemove creates a shape, * and then mouseup finalizes it and adds an instance of `fabric.Path` onto canvas. * @tutorial {@link http://fabricjs.com/fabric-intro-part-4#free_drawing} * @type Boolean * @default */ isDrawingMode: false, /** * Indicates whether objects should remain in current stack position when selected. * When false objects are brought to top and rendered as part of the selection group * @type Boolean * @default */ preserveObjectStacking: false, /** * Indicates the angle that an object will lock to while rotating. * @type Number * @since 1.6.7 * @default */ snapAngle: 0, /** * Indicates the distance from the snapAngle the rotation will lock to the snapAngle. * When `null`, the snapThreshold will default to the snapAngle. * @type null|Number * @since 1.6.7 * @default */ snapThreshold: null, /** * Indicates if the right click on canvas can output the context menu or not * @type Boolean * @since 1.6.5 * @default */ stopContextMenu: false, /** * Indicates if the canvas can fire right click events * @type Boolean * @since 1.6.5 * @default */ fireRightClick: false, /** * Indicates if the canvas can fire middle click events * @type Boolean * @since 1.7.8 * @default */ fireMiddleClick: false, /** * Keep track of the subTargets for Mouse Events * @type fabric.Object[] */ targets: [], /** * Keep track of the hovered target * @type fabric.Object * @private */ _hoveredTarget: null, /** * hold the list of nested targets hovered * @type fabric.Object[] * @private */ _hoveredTargets: [], /** * @private */ _initInteractive: function() { this._currentTransform = null; this._groupSelector = null; this._initWrapperElement(); this._createUpperCanvas(); this._initEventListeners(); this._initRetinaScaling(); this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); this.calcOffset(); }, /** * Divides objects in two groups, one to render immediately * and one to render as activeGroup. * @return {Array} objects to render immediately and pushes the other in the activeGroup. */ _chooseObjectsToRender: function() { var activeObjects = this.getActiveObjects(), object, objsToRender, activeGroupObjects; if (activeObjects.length > 0 && !this.preserveObjectStacking) { objsToRender = []; activeGroupObjects = []; for (var i = 0, length = this._objects.length; i < length; i++) { object = this._objects[i]; if (activeObjects.indexOf(object) === -1 ) { objsToRender.push(object); } else { activeGroupObjects.push(object); } } if (activeObjects.length > 1) { this._activeObject._objects = activeGroupObjects; } objsToRender.push.apply(objsToRender, activeGroupObjects); } else { objsToRender = this._objects; } return objsToRender; }, /** * Renders both the top canvas and the secondary container canvas. * @return {fabric.Canvas} instance * @chainable */ renderAll: function () { if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) { this.clearContext(this.contextTop); this.contextTopDirty = false; } if (this.hasLostContext) { this.renderTopLayer(this.contextTop); } var canvasToDrawOn = this.contextContainer; this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); return this; }, renderTopLayer: function(ctx) { ctx.save(); if (this.isDrawingMode && this._isCurrentlyDrawing) { this.freeDrawingBrush && this.freeDrawingBrush._render(); this.contextTopDirty = true; } // we render the top context - last object if (this.selection && this._groupSelector) { this._drawSelection(ctx); this.contextTopDirty = true; } ctx.restore(); }, /** * Method to render only the top canvas. * Also used to render the group selection box. * @return {fabric.Canvas} thisArg * @chainable */ renderTop: function () { var ctx = this.contextTop; this.clearContext(ctx); this.renderTopLayer(ctx); this.fire('after:render'); return this; }, /** * Checks if point is contained within an area of given object * @param {Event} e Event object * @param {fabric.Object} target Object to test against * @param {Object} [point] x,y object of point coordinates we want to check. * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target, point) { var ignoreZoom = true, pointer = point || this.getPointer(e, ignoreZoom), xy, isTouch = e ? isTouchEvent(e) : false; if (target.group && target.group === this._activeObject && target.group.type === 'activeSelection') { xy = this._normalizePointer(target.group, pointer); } else { xy = { x: pointer.x, y: pointer.y }; } // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html return (target.containsPoint(xy) || !!target._findTargetCorner(pointer, isTouch)); }, /** * @private */ _normalizePointer: function (object, pointer) { var m = object.calcTransformMatrix(), invertedM = fabric.util.invertTransform(m), vptPointer = this.restorePointerVpt(pointer); return fabric.util.transformPoint(vptPointer, invertedM); }, /** * Returns true if object is transparent at a certain location * @param {fabric.Object} target Object to check * @param {Number} x Left coordinate * @param {Number} y Top coordinate * @return {Boolean} */ isTargetTransparent: function (target, x, y) { // in case the target is the activeObject, we cannot execute this optimization // because we need to draw controls too. if (target.shouldCache() && target._cacheCanvas && target !== this._activeObject) { var normalizedPointer = this._normalizePointer(target, {x: x, y: y}), targetRelativeX = Math.max(target.cacheTranslationX + (normalizedPointer.x * target.zoomX), 0), targetRelativeY = Math.max(target.cacheTranslationY + (normalizedPointer.y * target.zoomY), 0); var isTransparent = fabric.util.isTransparent( target._cacheContext, Math.round(targetRelativeX), Math.round(targetRelativeY), this.targetFindTolerance); return isTransparent; } var ctx = this.contextCache, originalColor = target.selectionBackgroundColor, v = this.viewportTransform; target.selectionBackgroundColor = ''; this.clearContext(ctx); ctx.save(); ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); target.render(ctx); ctx.restore(); target === this._activeObject && target._renderControls(ctx, { hasBorders: false, transparentCorners: false }, { hasBorders: false, }); target.selectionBackgroundColor = originalColor; var isTransparent = fabric.util.isTransparent( ctx, x, y, this.targetFindTolerance); return isTransparent; }, /** * takes an event and determins if selection key has been pressed * @private * @param {Event} e Event object */ _isSelectionKeyPressed: function(e) { var selectionKeyPressed = false; if (Object.prototype.toString.call(this.selectionKey) === '[object Array]') { selectionKeyPressed = !!this.selectionKey.find(function(key) { return e[key] === true; }); } else { selectionKeyPressed = e[this.selectionKey]; } return selectionKeyPressed; }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { var activeObjects = this.getActiveObjects(), activeObject = this._activeObject; return ( !target || (target && activeObject && activeObjects.length > 1 && activeObjects.indexOf(target) === -1 && activeObject !== target && !this._isSelectionKeyPressed(e)) || (target && !target.evented) || (target && !target.selectable && activeObject && activeObject !== target) ); }, /** * centeredScaling from object can't override centeredScaling from canvas. * this should be fixed, since object setting should take precedence over canvas. * also this should be something that will be migrated in the control properties. * as ability to define the origin of the transformation that the control provide. * @private * @param {fabric.Object} target * @param {String} action */ _shouldCenterTransform: function (target, action, altKey) { if (!target) { return; } var centerTransform; if (action === 'scale' || action === 'scaleX' || action === 'scaleY') { centerTransform = this.centeredScaling || target.centeredScaling; } else if (action === 'rotate') { centerTransform = this.centeredRotation || target.centeredRotation; } return centerTransform ? !altKey : altKey; }, /** * should disappear before release 4.0 * @private */ _getOriginFromCorner: function(target, corner) { var origin = { x: target.originX, y: target.originY }; if (corner === 'ml' || corner === 'tl' || corner === 'bl') { origin.x = 'right'; } else if (corner === 'mr' || corner === 'tr' || corner === 'br') { origin.x = 'left'; } if (corner === 'tl' || corner === 'mt' || corner === 'tr') { origin.y = 'bottom'; } else if (corner === 'bl' || corner === 'mb' || corner === 'br') { origin.y = 'top'; } else if (corner === 'mtr') { origin.x = 'center'; origin.y = 'center'; } return origin; }, /** * @private * @param {Boolean} alreadySelected true if target is already selected * @param {String} corner a string representing the corner ml, mr, tl ... * @param {Event} e Event object * @param {fabric.Object} [target] inserted back to help overriding. Unused */ _getActionFromCorner: function(alreadySelected, corner, e, target) { if (!corner || !alreadySelected) { return 'drag'; } var control = target.controls[corner]; return control.getActionName(e, control, target); }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _setupCurrentTransform: function (e, target, alreadySelected) { if (!target) { return; } var pointer = this.getPointer(e), corner = target.__corner, actionHandler = !!corner && target.controls[corner].getActionHandler(), action = this._getActionFromCorner(alreadySelected, corner, e, target), origin = this._getOriginFromCorner(target, corner), altKey = e[this.centeredKey], transform = { target: target, action: action, actionHandler: actionHandler, corner: corner, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, // used by transation offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, lastX: pointer.x, lastY: pointer.y, // unsure they are usefull anymore. // left: target.left, // top: target.top, theta: degreesToRadians(target.angle), // end of unsure width: target.width * target.scaleX, shiftKey: e.shiftKey, altKey: altKey, original: fabric.util.saveObjectTransform(target), }; if (this._shouldCenterTransform(target, action, altKey)) { transform.originX = 'center'; transform.originY = 'center'; } transform.original.originX = origin.x; transform.original.originY = origin.y; this._currentTransform = transform; this._beforeTransform(e); }, /** * Translates object by "setting" its left/top * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate * @return {Boolean} true if the translation occurred */ _translateObject: function (x, y) { var transform = this._currentTransform, target = transform.target, newLeft = x - transform.offsetX, newTop = y - transform.offsetY, moveX = !target.get('lockMovementX') && target.left !== newLeft, moveY = !target.get('lockMovementY') && target.top !== newTop; moveX && target.set('left', newLeft); moveY && target.set('top', newTop); return moveX || moveY; }, /** * Set the cursor type of the canvas element * @param {String} value Cursor type of the canvas element. * @see http://www.w3.org/TR/css3-ui/#cursor */ setCursor: function (value) { this.upperCanvasEl.style.cursor = value; }, /** * @private * @param {CanvasRenderingContext2D} ctx to draw the selection on */ _drawSelection: function (ctx) { var groupSelector = this._groupSelector, left = groupSelector.left, top = groupSelector.top, aleft = abs(left), atop = abs(top); if (this.selectionColor) { ctx.fillStyle = this.selectionColor; ctx.fillRect( groupSelector.ex - ((left > 0) ? 0 : -left), groupSelector.ey - ((top > 0) ? 0 : -top), aleft, atop ); } if (!this.selectionLineWidth || !this.selectionBorderColor) { return; } ctx.lineWidth = this.selectionLineWidth; ctx.strokeStyle = this.selectionBorderColor; // selection border if (this.selectionDashArray.length > 1 && !supportLineDash) { var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop); ctx.beginPath(); fabric.util.drawDashedLine(ctx, px, py, px + aleft, py, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px, py + atop - 1, px + aleft, py + atop - 1, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px, py, px, py + atop, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px + aleft - 1, py, px + aleft - 1, py + atop, this.selectionDashArray); ctx.closePath(); ctx.stroke(); } else { fabric.Object.prototype._setLineDash.call(this, ctx, this.selectionDashArray); ctx.strokeRect( groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), aleft, atop ); } }, /** * Method that determines what object we are clicking on * the skipGroup parameter is for internal use, is needed for shift+click action * 11/09/2018 TODO: would be cool if findTarget could discern between being a full target * or the outside part of the corner. * @param {Event} e mouse event * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through * @return {fabric.Object} the target found */ findTarget: function (e, skipGroup) { if (this.skipTargetFind) { return; } var ignoreZoom = true, pointer = this.getPointer(e, ignoreZoom), activeObject = this._activeObject, aObjects = this.getActiveObjects(), activeTarget, activeTargetSubs, isTouch = isTouchEvent(e); // first check current group (if one exists) // active group does not check sub targets like normal groups. // if active group just exits. this.targets = []; if (aObjects.length > 1 && !skipGroup && activeObject === this._searchPossibleTargets([activeObject], pointer)) { return activeObject; } // if we hit the corner of an activeObject, let's return that. if (aObjects.length === 1 && activeObject._findTargetCorner(pointer, isTouch)) { return activeObject; } if (aObjects.length === 1 && activeObject === this._searchPossibleTargets([activeObject], pointer)) { if (!this.preserveObjectStacking) { return activeObject; } else { activeTarget = activeObject; activeTargetSubs = this.targets; this.targets = []; } } var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; this.targets = activeTargetSubs; } return target; }, /** * Checks point is inside the object. * @param {Object} [pointer] x,y object of point coordinates we want to check. * @param {fabric.Object} obj Object to test against * @param {Object} [globalPointer] x,y object of point coordinates relative to canvas used to search per pixel target. * @return {Boolean} true if point is contained within an area of given object * @private */ _checkTarget: function(pointer, obj, globalPointer) { if (obj && obj.visible && obj.evented && this.containsPoint(null, obj, pointer)) { if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { var isTransparent = this.isTargetTransparent(obj, globalPointer.x, globalPointer.y); if (!isTransparent) { return true; } } else { return true; } } }, /** * Function used to search inside objects an object that contains pointer in bounding box or that contains pointerOnCanvas when painted * @param {Array} [objects] objects array to look into * @param {Object} [pointer] x,y object of point coordinates we want to check. * @return {fabric.Object} object that contains pointer * @private */ _searchPossibleTargets: function(objects, pointer) { // Cache all targets where their bounding box contains point. var target, i = objects.length, subTarget; // Do not check for currently grouped objects, since we check the parent group itself. // until we call this function specifically to search inside the activeGroup while (i--) { var objToCheck = objects[i]; var pointerToUse = objToCheck.group && objToCheck.group.type !== 'activeSelection' ? this._normalizePointer(objToCheck.group, pointer) : pointer; if (this._checkTarget(pointerToUse, objToCheck, pointer)) { target = objects[i]; if (target.subTargetCheck && target instanceof fabric.Group) { subTarget = this._searchPossibleTargets(target._objects, pointer); subTarget && this.targets.push(subTarget); } break; } } return target; }, /** * Returns pointer coordinates without the effect of the viewport * @param {Object} pointer with "x" and "y" number values * @return {Object} object with "x" and "y" number values */ restorePointerVpt: function(pointer) { return fabric.util.transformPoint( pointer, fabric.util.invertTransform(this.viewportTransform) ); }, /** * Returns pointer coordinates relative to canvas. * Can return coordinates with or without viewportTransform. * ignoreZoom false gives back coordinates that represent * the point clicked on canvas element. * ignoreZoom true gives back coordinates after being processed * by the viewportTransform ( sort of coordinates of what is displayed * on the canvas where you are clicking. * ignoreZoom true = HTMLElement coordinates relative to top,left * ignoreZoom false, default = fabric space coordinates, the same used for shape position * To interact with your shapes top and left you want to use ignoreZoom true * most of the time, while ignoreZoom false will give you coordinates * compatible with the object.oCoords system. * of the time. * @param {Event} e * @param {Boolean} ignoreZoom * @return {Object} object with "x" and "y" number values */ getPointer: function (e, ignoreZoom) { // return cached values if we are in the event processing chain if (this._absolutePointer && !ignoreZoom) { return this._absolutePointer; } if (this._pointer && ignoreZoom) { return this._pointer; } var pointer = getPointer(e), upperCanvasEl = this.upperCanvasEl, bounds = upperCanvasEl.getBoundingClientRect(), boundsWidth = bounds.width || 0, boundsHeight = bounds.height || 0, cssScale; if (!boundsWidth || !boundsHeight ) { if ('top' in bounds && 'bottom' in bounds) { boundsHeight = Math.abs( bounds.top - bounds.bottom ); } if ('right' in bounds && 'left' in bounds) { boundsWidth = Math.abs( bounds.right - bounds.left ); } } this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; if (!ignoreZoom) { pointer = this.restorePointerVpt(pointer); } var retinaScaling = this.getRetinaScaling(); if (retinaScaling !== 1) { pointer.x /= retinaScaling; pointer.y /= retinaScaling; } if (boundsWidth === 0 || boundsHeight === 0) { // If bounds are not available (i.e. not visible), do not apply scale. cssScale = { width: 1, height: 1 }; } else { cssScale = { width: upperCanvasEl.width / boundsWidth, height: upperCanvasEl.height / boundsHeight }; } return { x: pointer.x * cssScale.width, y: pointer.y * cssScale.height }; }, /** * @private * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized */ _createUpperCanvas: function () { var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''), lowerCanvasEl = this.lowerCanvasEl, upperCanvasEl = this.upperCanvasEl; // there is no need to create a new upperCanvas element if we have already one. if (upperCanvasEl) { upperCanvasEl.className = ''; } else { upperCanvasEl = this._createCanvasElement(); this.upperCanvasEl = upperCanvasEl; } fabric.util.addClass(upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); this.wrapperEl.appendChild(upperCanvasEl); this._copyCanvasStyle(lowerCanvasEl, upperCanvasEl); this._applyCanvasStyle(upperCanvasEl); this.contextTop = upperCanvasEl.getContext('2d'); }, /** * @private */ _createCacheCanvas: function () { this.cacheCanvasEl = this._createCanvasElement(); this.cacheCanvasEl.setAttribute('width', this.width); this.cacheCanvasEl.setAttribute('height', this.height); this.contextCache = this.cacheCanvasEl.getContext('2d'); }, /** * @private */ _initWrapperElement: function () { this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { 'class': this.containerClass }); fabric.util.setStyle(this.wrapperEl, { width: this.width + 'px', height: this.height + 'px', position: 'relative' }); fabric.util.makeElementUnselectable(this.wrapperEl); }, /** * @private * @param {HTMLElement} element canvas element to apply styles on */ _applyCanvasStyle: function (element) { var width = this.width || element.width, height = this.height || element.height; fabric.util.setStyle(element, { position: 'absolute', width: width + 'px', height: height + 'px', left: 0, top: 0, 'touch-action': this.allowTouchScrolling ? 'manipulation' : 'none', '-ms-touch-action': this.allowTouchScrolling ? 'manipulation' : 'none' }); element.width = width; element.height = height; fabric.util.makeElementUnselectable(element); }, /** * Copy the entire inline style from one element (fromEl) to another (toEl) * @private * @param {Element} fromEl Element style is copied from * @param {Element} toEl Element copied style is applied to */ _copyCanvasStyle: function (fromEl, toEl) { toEl.style.cssText = fromEl.style.cssText; }, /** * Returns context of canvas where object selection is drawn * @return {CanvasRenderingContext2D} */ getSelectionContext: function() { return this.contextTop; }, /** * Returns &lt;canvas> element on which object selection is drawn * @return {HTMLCanvasElement} */ getSelectionElement: function () { return this.upperCanvasEl; }, /** * Returns currently active object * @return {fabric.Object} active object */ getActiveObject: function () { return this._activeObject; }, /** * Returns an array with the current selected objects * @return {fabric.Object} active object */ getActiveObjects: function () { var active = this._activeObject; if (active) { if (active.type === 'activeSelection' && active._objects) { return active._objects.slice(0); } else { return [active]; } } return []; }, /** * @private * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { // removing active object should fire "selection:cleared" events if (obj === this._activeObject) { this.fire('before:selection:cleared', { target: obj }); this._discardActiveObject(); this.fire('selection:cleared', { target: obj }); obj.fire('deselected'); } if (obj === this._hoveredTarget){ this._hoveredTarget = null; this._hoveredTargets = []; } this.callSuper('_onObjectRemoved', obj); }, /** * @private * Compares the old activeObject with the current one and fires correct events * @param {fabric.Object} obj old activeObject */ _fireSelectionEvents: function(oldObjects, e) { var somethingChanged = false, objects = this.getActiveObjects(), added = [], removed = [], opt = { e: e }; oldObjects.forEach(function(oldObject) { if (objects.indexOf(oldObject) === -1) { somethingChanged = true; oldObject.fire('deselected', opt); removed.push(oldObject); } }); objects.forEach(function(object) { if (oldObjects.indexOf(object) === -1) { somethingChanged = true; object.fire('selected', opt); added.push(object); } }); if (oldObjects.length > 0 && objects.length > 0) { opt.selected = added; opt.deselected = removed; // added for backward compatibility opt.updated = added[0] || removed[0]; opt.target = this._activeObject; somethingChanged && this.fire('selection:updated', opt); } else if (objects.length > 0) { opt.selected = added; // added for backward compatibility opt.target = this._activeObject; this.fire('selection:created', opt); } else if (oldObjects.length > 0) { opt.deselected = removed; this.fire('selection:cleared', opt); } }, /** * Sets given object as the only active object on canvas * @param {fabric.Object} object Object to set as an active one * @param {Event} [e] Event (passed along when firing "object:selected") * @return {fabric.Canvas} thisArg * @chainable */ setActiveObject: function (object, e) { var currentActives = this.getActiveObjects(); this._setActiveObject(object, e); this._fireSelectionEvents(currentActives, e); return this; }, /** * @private * @param {Object} object to set as active * @param {Event} [e] Event (passed along when firing "object:selected") * @return {Boolean} true if the selection happened */ _setActiveObject: function(object, e) { if (this._activeObject === object) { return false; } if (!this._discardActiveObject(e, object)) { return false; } if (object.onSelect({ e: e })) { return false; } this._activeObject = object; return true; }, /** * @private */ _discardActiveObject: function(e, object) { var obj = this._activeObject; if (obj) { // onDeselect return TRUE to cancel selection; if (obj.onDeselect({ e: e, object: object })) { return false; } this._activeObject = null; } return true; }, /** * Discards currently active object and fire events. If the function is called by fabric * as a consequence of a mouse event, the event is passed as a parameter and * sent to the fire function for the custom events. When used as a method the * e param does not have any application. * @param {event} e * @return {fabric.Canvas} thisArg * @chainable */ discardActiveObject: function (e) { var currentActives = this.getActiveObjects(), activeObject = this.getActiveObject(); if (currentActives.length) { this.fire('before:selection:cleared', { target: activeObject, e: e }); } this._discardActiveObject(e); this._fireSelectionEvents(currentActives, e); return this; }, /** * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { var wrapper = this.wrapperEl; this.removeListeners(); wrapper.removeChild(this.upperCanvasEl); wrapper.removeChild(this.lowerCanvasEl); this.contextCache = null; this.contextTop = null; ['upperCanvasEl', 'cacheCanvasEl'].forEach((function(element) { fabric.util.cleanUpJsdomNode(this[element]); this[element] = undefined; }).bind(this)); if (wrapper.parentNode) { wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); } delete this.wrapperEl; fabric.StaticCanvas.prototype.dispose.call(this); return this; }, /** * Clears all contexts (background, main, top) of an instance * @return {fabric.Canvas} thisArg * @chainable */ clear: function () { // this.discardActiveGroup(); this.discardActiveObject(); this.clearContext(this.contextTop); return this.callSuper('clear'); }, /** * Draws objects' controls (borders/controls) * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls: function(ctx) { var activeObject = this._activeObject; if (activeObject) { activeObject._renderControls(ctx); } }, /** * @private */ _toObject: function(instance, methodName, propertiesToInclude) { //If the object is part of the current selection group, it should //be transformed appropriately //i.e. it should be serialised as it would appear if the selection group //were to be destroyed. var originalProperties = this._realizeGroupTransformOnObject(instance), object = this.callSuper('_toObject', instance, methodName, propertiesToInclude); //Undo the damage we did by changing all of its properties this._unwindGroupTransformOnObject(instance, originalProperties); return object; }, /** * Realises an object's group transformation on it * @private * @param {fabric.Object} [instance] the object to transform (gets mutated) * @returns the original values of instance which were changed */ _realizeGroupTransformOnObject: function(instance) { if (instance.group && instance.group.type === 'activeSelection' && this._activeObject === instance.group) { var layoutProps = ['angle', 'flipX', 'flipY', 'left', 'scaleX', 'scaleY', 'skewX', 'skewY', 'top']; //Copy all the positionally relevant properties across now var originalValues = {}; layoutProps.forEach(function(prop) { originalValues[prop] = instance[prop]; }); this._activeObject.realizeTransform(instance); return originalValues; } else { return null; } }, /** * Restores the changed properties of instance * @private * @param {fabric.Object} [instance] the object to un-transform (gets mutated) * @param {Object} [originalValues] the original values of instance, as returned by _realizeGroupTransformOnObject */ _unwindGroupTransformOnObject: function(instance, originalValues) { if (originalValues) { instance.set(originalValues); } }, /** * @private */ _setSVGObject: function(markup, instance, reviver) { //If the object is in a selection group, simulate what would happen to that //object when the group is deselected var originalProperties = this._realizeGroupTransformOnObject(instance); this.callSuper('_setSVGObject', markup, instance, reviver); this._unwindGroupTransformOnObject(instance, originalProperties); }, setViewportTransform: function (vpt) { if (this.renderOnAddRemove && this._activeObject && this._activeObject.isEditing) { this._activeObject.clearContextTop(); } fabric.StaticCanvas.prototype.setViewportTransform.call(this, vpt); } }); // copying static properties manually to work around Opera's bug, // where "prototype" property is enumerable and overrides existing prototype for (var prop in fabric.StaticCanvas) { if (prop !== 'prototype') { fabric.Canvas[prop] = fabric.StaticCanvas[prop]; } } })();