(function() { var getPointer = fabric.util.getPointer, degreesToRadians = fabric.util.degreesToRadians, radiansToDegrees = fabric.util.radiansToDegrees, atan2 = Math.atan2, abs = Math.abs, supportLineDash = fabric.StaticCanvas.supports('setLineDash'), 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:added * @fires object:modified * @fires object:rotating * @fires object:scaling * @fires object:moving * @fires object:selected * * @fires before:selection:cleared * @fires selection:cleared * @fires selection:created * * @fires path:created * @fires mouse:down * @fires mouse:move * @fires mouse:up * @fires mouse:over * @fires mouse:out * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { /** * Constructor * @param {HTMLElement | String} el <canvas> element to initialize instance on * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(el, options) { options || (options = { }); this._initStatic(el, options); this._initInteractive(); this._createCacheCanvas(); }, /** * When true, objects can be transformed by one side (unproportionally) * @type Boolean * @default */ uniScaleTransform: false, /** * Indicates which key enable unproportional scaling * values: altKey, shiftKey, ctrlKey * @since 1.6.2 * @type String * @default */ uniScaleKey: 'shiftKey', /** * When true, objects use center point as the origin of scale transformation. * Backwards incompatibility note: 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. * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredRotation: false, /** * Indicates which key enable centered Transfrom * values: altKey, shiftKey, ctrlKey * @since 1.6.2 * @type String * @default */ centeredKey: 'altKey', /** * Indicates which key enable alternate action on corner * values: altKey, shiftKey, ctrlKey * @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 enable multiple click selection * values: altKey, shiftKey, ctrlKey, cmdKey * @since 1.6.2 * @type String * @default */ selectionKey: 'shiftKey', /** * Indicates which key enable alternative selection * in case of target overlapping with active object * values: altKey, shiftKey, ctrlKey, cmdKey * @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, /** * 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', /** * 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 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, /** * @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 activeGroup = this.getActiveGroup(), activeObject = this.getActiveObject(), object, objsToRender = [], activeGroupObjects = []; if ((activeGroup || activeObject) && !this.preserveObjectStacking) { for (var i = 0, length = this._objects.length; i < length; i++) { object = this._objects[i]; if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { objsToRender.push(object); } else { activeGroupObjects.push(object); } } if (activeGroup) { activeGroup._set('_objects', activeGroupObjects); objsToRender.push(activeGroup); } activeObject && objsToRender.push(activeObject); } 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.selection && !this._groupSelector && !this.isDrawingMode) { this.clearContext(this.contextTop); } var canvasToDrawOn = this.contextContainer; this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); return this; }, /** * 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); // we render the top context - last object if (this.selection && this._groupSelector) { this._drawSelection(ctx); } this.fire('after:render'); return this; }, /** * Resets the current transform to its original values and chooses the type of resizing based on the event * @private */ _resetCurrentTransform: function() { var t = this._currentTransform; t.target.set({ scaleX: t.original.scaleX, scaleY: t.original.scaleY, skewX: t.original.skewX, skewY: t.original.skewY, left: t.original.left, top: t.original.top }); if (this._shouldCenterTransform(t.target)) { if (t.action === 'rotate') { this._setOriginToCenter(t.target); } else { if (t.originX !== 'center') { if (t.originX === 'right') { t.mouseXSign = -1; } else { t.mouseXSign = 1; } } if (t.originY !== 'center') { if (t.originY === 'bottom') { t.mouseYSign = -1; } else { t.mouseYSign = 1; } } t.originX = 'center'; t.originY = 'center'; } } else { t.originX = t.original.originX; t.originY = t.original.originY; } }, /** * 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; if (target.group && target.group === this.getActiveGroup()) { 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)); }, /** * @private */ _normalizePointer: function (object, pointer) { var m = object.calcTransformMatrix(), invertedM = fabric.util.invertTransform(m), vpt = this.viewportTransform, vptPointer = this.restorePointerVpt(pointer), p = fabric.util.transformPoint(vptPointer, invertedM); return fabric.util.transformPoint(p, vpt); }, /** * 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) { var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners, ctx = this.contextCache, originalColor = target.selectionBackgroundColor; target.hasBorders = target.transparentCorners = false; target.selectionBackgroundColor = ''; ctx.save(); ctx.transform.apply(ctx, this.viewportTransform); target.render(ctx); ctx.restore(); target.active && target._renderControls(ctx); target.hasBorders = hasBorders; target.transparentCorners = transparentCorners; target.selectionBackgroundColor = originalColor; var isTransparent = fabric.util.isTransparent( ctx, x, y, this.targetFindTolerance); this.clearContext(ctx); return isTransparent; }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { var activeGroup = this.getActiveGroup(), activeObject = this.getActiveObject(); return ( !target || (target && activeGroup && !activeGroup.contains(target) && activeGroup !== target && !e[this.selectionKey]) || (target && !target.evented) || (target && !target.selectable && activeObject && activeObject !== target) ); }, /** * @private * @param {fabric.Object} target */ _shouldCenterTransform: function (target) { if (!target) { return; } var t = this._currentTransform, centerTransform; if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { centerTransform = this.centeredScaling || target.centeredScaling; } else if (t.action === 'rotate') { centerTransform = this.centeredRotation || target.centeredRotation; } return centerTransform ? !t.altKey : t.altKey; }, /** * @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'; } return origin; }, /** * @private */ _getActionFromCorner: function(target, corner, e) { if (!corner) { return 'drag'; } switch (corner) { case 'mtr': return 'rotate'; case 'ml': case 'mr': return e[this.altActionKey] ? 'skewY' : 'scaleX'; case 'mt': case 'mb': return e[this.altActionKey] ? 'skewX' : 'scaleY'; default: return 'scale'; } }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _setupCurrentTransform: function (e, target) { if (!target) { return; } var pointer = this.getPointer(e), corner = target._findTargetCorner(this.getPointer(e, true)), action = this._getActionFromCorner(target, corner, e), origin = this._getOriginFromCorner(target, corner); this._currentTransform = { target: target, action: action, corner: corner, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, 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, left: target.left, top: target.top, theta: degreesToRadians(target.angle), width: target.width * target.scaleX, mouseXSign: 1, mouseYSign: 1, shiftKey: e.shiftKey, altKey: e[this.centeredKey] }; this._currentTransform.original = { left: target.left, top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, skewX: target.skewX, skewY: target.skewY, originX: origin.x, originY: origin.y }; this._resetCurrentTransform(); }, /** * 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; }, /** * Check if we are increasing a positive skew or lower it, * checking mouse direction and pressed corner. * @private */ _changeSkewTransformOrigin: function(mouseMove, t, by) { var property = 'originX', origins = { 0: 'center' }, skew = t.target.skewX, originA = 'left', originB = 'right', corner = t.corner === 'mt' || t.corner === 'ml' ? 1 : -1, flipSign = 1; mouseMove = mouseMove > 0 ? 1 : -1; if (by === 'y') { skew = t.target.skewY; originA = 'top'; originB = 'bottom'; property = 'originY'; } origins[-1] = originA; origins[1] = originB; t.target.flipX && (flipSign *= -1); t.target.flipY && (flipSign *= -1); if (skew === 0) { t.skewSign = -corner * mouseMove * flipSign; t[property] = origins[-mouseMove]; } else { skew = skew > 0 ? 1 : -1; t.skewSign = skew; t[property] = origins[skew * corner * flipSign]; } }, /** * Skew object by mouse events * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate * @param {String} by Either 'x' or 'y' * @return {Boolean} true if the skewing occurred */ _skewObject: function (x, y, by) { var t = this._currentTransform, target = t.target, skewed = false, lockSkewingX = target.get('lockSkewingX'), lockSkewingY = target.get('lockSkewingY'); if ((lockSkewingX && by === 'x') || (lockSkewingY && by === 'y')) { return false; } // Get the constraint point var center = target.getCenterPoint(), actualMouseByCenter = target.toLocalPoint(new fabric.Point(x, y), 'center', 'center')[by], lastMouseByCenter = target.toLocalPoint(new fabric.Point(t.lastX, t.lastY), 'center', 'center')[by], actualMouseByOrigin, constraintPosition, dim = target._getTransformedDimensions(); this._changeSkewTransformOrigin(actualMouseByCenter - lastMouseByCenter, t, by); actualMouseByOrigin = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY)[by]; constraintPosition = target.translateToOriginPoint(center, t.originX, t.originY); // Actually skew the object skewed = this._setObjectSkew(actualMouseByOrigin, t, by, dim); t.lastX = x; t.lastY = y; // Make sure the constraints apply target.setPositionByOrigin(constraintPosition, t.originX, t.originY); return skewed; }, /** * Set object skew * @private * @return {Boolean} true if the skewing occurred */ _setObjectSkew: function(localMouse, transform, by, _dim) { var target = transform.target, newValue, skewed = false, skewSign = transform.skewSign, newDim, dimNoSkew, otherBy, _otherBy, _by, newDimMouse, skewX, skewY; if (by === 'x') { otherBy = 'y'; _otherBy = 'Y'; _by = 'X'; skewX = 0; skewY = target.skewY; } else { otherBy = 'x'; _otherBy = 'X'; _by = 'Y'; skewX = target.skewX; skewY = 0; } dimNoSkew = target._getTransformedDimensions(skewX, skewY); newDimMouse = 2 * Math.abs(localMouse) - dimNoSkew[by]; if (newDimMouse <= 2) { newValue = 0; } else { newValue = skewSign * Math.atan((newDimMouse / target['scale' + _by]) / (dimNoSkew[otherBy] / target['scale' + _otherBy])); newValue = fabric.util.radiansToDegrees(newValue); } skewed = target['skew' + _by] !== newValue; target.set('skew' + _by, newValue); if (target['skew' + _otherBy] !== 0) { newDim = target._getTransformedDimensions(); newValue = (_dim[otherBy] / newDim[otherBy]) * target['scale' + _otherBy]; target.set('scale' + _otherBy, newValue); } return skewed; }, /** * Scales object by invoking its scaleX/scaleY methods * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate * @param {String} by Either 'x' or 'y' - specifies dimension constraint by which to scale an object. * When not provided, an object is scaled by both dimensions equally * @return {Boolean} true if the scaling occurred */ _scaleObject: function (x, y, by) { var t = this._currentTransform, target = t.target, lockScalingX = target.get('lockScalingX'), lockScalingY = target.get('lockScalingY'), lockScalingFlip = target.get('lockScalingFlip'); if (lockScalingX && lockScalingY) { return false; } // Get the constraint point var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY), localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY), dim = target._getTransformedDimensions(), scaled = false; this._setLocalMouse(localMouse, t); // Actually scale the object scaled = this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip, dim); // Make sure the constraints apply target.setPositionByOrigin(constraintPosition, t.originX, t.originY); return scaled; }, /** * @private * @return {Boolean} true if the scaling occurred */ _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip, _dim) { var target = transform.target, forbidScalingX = false, forbidScalingY = false, scaled = false, changeX, changeY, scaleX, scaleY; scaleX = localMouse.x * target.scaleX / _dim.x; scaleY = localMouse.y * target.scaleY / _dim.y; changeX = target.scaleX !== scaleX; changeY = target.scaleY !== scaleY; if (lockScalingFlip && scaleX <= 0 && scaleX < target.scaleX) { forbidScalingX = true; } if (lockScalingFlip && scaleY <= 0 && scaleY < target.scaleY) { forbidScalingY = true; } if (by === 'equally' && !lockScalingX && !lockScalingY) { forbidScalingX || forbidScalingY || (scaled = this._scaleObjectEqually(localMouse, target, transform, _dim)); } else if (!by) { forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX)); forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); } else if (by === 'x' && !target.get('lockUniScaling')) { forbidScalingX || lockScalingX || (target.set('scaleX', scaleX) && (scaled = scaled || changeX)); } else if (by === 'y' && !target.get('lockUniScaling')) { forbidScalingY || lockScalingY || (target.set('scaleY', scaleY) && (scaled = scaled || changeY)); } transform.newScaleX = scaleX; transform.newScaleY = scaleY; forbidScalingX || forbidScalingY || this._flipObject(transform, by); return scaled; }, /** * @private * @return {Boolean} true if the scaling occurred */ _scaleObjectEqually: function(localMouse, target, transform, _dim) { var dist = localMouse.y + localMouse.x, lastDist = _dim.y * transform.original.scaleY / target.scaleY + _dim.x * transform.original.scaleX / target.scaleX, scaled; // We use transform.scaleX/Y instead of target.scaleX/Y // because the object may have a min scale and we'll loose the proportions transform.newScaleX = transform.original.scaleX * dist / lastDist; transform.newScaleY = transform.original.scaleY * dist / lastDist; scaled = transform.newScaleX !== target.scaleX || transform.newScaleY !== target.scaleY; target.set('scaleX', transform.newScaleX); target.set('scaleY', transform.newScaleY); return scaled; }, /** * @private */ _flipObject: function(transform, by) { if (transform.newScaleX < 0 && by !== 'y') { if (transform.originX === 'left') { transform.originX = 'right'; } else if (transform.originX === 'right') { transform.originX = 'left'; } } if (transform.newScaleY < 0 && by !== 'x') { if (transform.originY === 'top') { transform.originY = 'bottom'; } else if (transform.originY === 'bottom') { transform.originY = 'top'; } } }, /** * @private */ _setLocalMouse: function(localMouse, t) { var target = t.target; if (t.originX === 'right') { localMouse.x *= -1; } else if (t.originX === 'center') { localMouse.x *= t.mouseXSign * 2; if (localMouse.x < 0) { t.mouseXSign = -t.mouseXSign; } } if (t.originY === 'bottom') { localMouse.y *= -1; } else if (t.originY === 'center') { localMouse.y *= t.mouseYSign * 2; if (localMouse.y < 0) { t.mouseYSign = -t.mouseYSign; } } // adjust the mouse coordinates when dealing with padding if (abs(localMouse.x) > target.padding) { if (localMouse.x < 0) { localMouse.x += target.padding; } else { localMouse.x -= target.padding; } } else { // mouse is within the padding, set to 0 localMouse.x = 0; } if (abs(localMouse.y) > target.padding) { if (localMouse.y < 0) { localMouse.y += target.padding; } else { localMouse.y -= target.padding; } } else { localMouse.y = 0; } }, /** * Rotates object by invoking its rotate method * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate * @return {Boolean} true if the rotation occurred */ _rotateObject: function (x, y) { var t = this._currentTransform; if (t.target.get('lockRotation')) { return false; } var lastAngle = atan2(t.ey - t.top, t.ex - t.left), curAngle = atan2(y - t.top, x - t.left), angle = radiansToDegrees(curAngle - lastAngle + t.theta); // normalize angle to positive value if (angle < 0) { angle = 360 + angle; } t.target.angle = angle % 360; return true; }, /** * 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; }, /** * @param {fabric.Object} target to reset transform * @private */ _resetObjectTransform: function (target) { target.scaleX = 1; target.scaleY = 1; target.skewX = 0; target.skewY = 0; target.setAngle(0); }, /** * @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 * @param {Event} e mouse event * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through */ findTarget: function (e, skipGroup) { if (this.skipTargetFind) { return; } var ignoreZoom = true, pointer = this.getPointer(e, ignoreZoom), activeGroup = this.getActiveGroup(), activeObject = this.getActiveObject(), activeTarget; // first check current group (if one exists) // active group does not check sub targets like normal groups. // if active group just exits. if (activeGroup && !skipGroup && this._checkTarget(pointer, activeGroup)) { this._fireOverOutEvents(activeGroup, e); return activeGroup; } // if we hit the corner of an activeObject, let's return that. if (activeObject && activeObject._findTargetCorner(pointer)) { this._fireOverOutEvents(activeObject, e); return activeObject; } if (activeObject && this._checkTarget(pointer, activeObject)) { if (!this.preserveObjectStacking) { this._fireOverOutEvents(activeObject, e); return activeObject; } else { activeTarget = activeObject; } } this.targets = []; var target = this._searchPossibleTargets(this._objects, pointer); if (e[this.altSelectionKey] && target && activeTarget && target !== activeTarget) { target = activeTarget; } this._fireOverOutEvents(target, e); return target; }, /** * @private */ _fireOverOutEvents: function(target, e) { if (target) { if (this._hoveredTarget !== target) { if (this._hoveredTarget) { this.fire('mouse:out', { target: this._hoveredTarget, e: e }); this._hoveredTarget.fire('mouseout'); } this.fire('mouse:over', { target: target, e: e }); target.fire('mouseover'); this._hoveredTarget = target; } } else if (this._hoveredTarget) { this.fire('mouse:out', { target: this._hoveredTarget, e: e }); this._hoveredTarget.fire('mouseout'); this._hoveredTarget = null; } }, /** * @private */ _checkTarget: function(pointer, obj) { if (obj && obj.visible && obj.evented && this.containsPoint(null, obj, pointer)){ if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { var isTransparent = this.isTargetTransparent(obj, pointer.x, pointer.y); if (!isTransparent) { return true; } } else { return true; } } }, /** * @private */ _searchPossibleTargets: function(objects, pointer) { // Cache all targets where their bounding box contains point. var target, i = objects.length, normalizedPointer, subTarget; // Do not check for currently grouped objects, since we check the parent group itself. // untill we call this function specifically to search inside the activeGroup while (i--) { if (this._checkTarget(pointer, objects[i])) { target = objects[i]; if (target.type === 'group' && target.subTargetCheck) { normalizedPointer = this._normalizePointer(target, pointer); subTarget = this._searchPossibleTargets(target._objects, normalizedPointer); 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. * @param {Event} e * @param {Boolean} ignoreZoom * @return {Object} object with "x" and "y" number values */ getPointer: function (e, ignoreZoom, upperCanvasEl) { if (!upperCanvasEl) { upperCanvasEl = this.upperCanvasEl; } var pointer = getPointer(e), 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); } 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*/, ''); this.upperCanvasEl = this._createCanvasElement(); fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); this.wrapperEl.appendChild(this.upperCanvasEl); this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl); this._applyCanvasStyle(this.upperCanvasEl); this.contextTop = this.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.getWidth() + 'px', height: this.getHeight() + 'px', position: 'relative' }); fabric.util.makeElementUnselectable(this.wrapperEl); }, /** * @private * @param {HTMLElement} element canvas element to apply styles on */ _applyCanvasStyle: function (element) { var width = this.getWidth() || element.width, height = this.getHeight() || element.height; fabric.util.setStyle(element, { position: 'absolute', width: width + 'px', height: height + 'px', left: 0, top: 0 }); element.width = width; element.height = height; fabric.util.makeElementUnselectable(element); }, /** * Copys the 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 <canvas> element on which object selection is drawn * @return {HTMLCanvasElement} */ getSelectionElement: function () { return this.upperCanvasEl; }, /** * @private * @param {Object} object */ _setActiveObject: function(object) { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = object; object.set('active', true); }, /** * 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) { this._setActiveObject(object); this.renderAll(); this.fire('object:selected', { target: object, e: e }); object.fire('selected', { e: e }); return this; }, /** * Returns currently active object * @return {fabric.Object} active object */ getActiveObject: function () { return this._activeObject; }, /** * @private * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { // removing active object should fire "selection:cleared" events if (this.getActiveObject() === obj) { this.fire('before:selection:cleared', { target: obj }); this._discardActiveObject(); this.fire('selection:cleared', { target: obj }); obj.fire('deselected'); } this.callSuper('_onObjectRemoved', obj); }, /** * @private */ _discardActiveObject: function() { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = null; }, /** * Discards currently active object and fire events * @param {event} e * @return {fabric.Canvas} thisArg * @chainable */ discardActiveObject: function (e) { var activeObject = this._activeObject; this.fire('before:selection:cleared', { target: activeObject, e: e }); this._discardActiveObject(); this.fire('selection:cleared', { e: e }); activeObject && activeObject.fire('deselected', { e: e }); return this; }, /** * @private * @param {fabric.Group} group */ _setActiveGroup: function(group) { this._activeGroup = group; if (group) { group.set('active', true); } }, /** * Sets active group to a specified one * @param {fabric.Group} group Group to set as a current one * @param {Event} e Event object * @return {fabric.Canvas} thisArg * @chainable */ setActiveGroup: function (group, e) { this._setActiveGroup(group); if (group) { this.fire('object:selected', { target: group, e: e }); group.fire('selected', { e: e }); } return this; }, /** * Returns currently active group * @return {fabric.Group} Current group */ getActiveGroup: function () { return this._activeGroup; }, /** * @private */ _discardActiveGroup: function() { var g = this.getActiveGroup(); if (g) { g.destroy(); } this.setActiveGroup(null); }, /** * Discards currently active group and fire events * @return {fabric.Canvas} thisArg * @chainable */ discardActiveGroup: function (e) { var g = this.getActiveGroup(); this.fire('before:selection:cleared', { e: e, target: g }); this._discardActiveGroup(); this.fire('selection:cleared', { e: e }); return this; }, /** * Deactivates all objects on canvas, removing any active group or object * @return {fabric.Canvas} thisArg * @chainable */ deactivateAll: function () { var allObjects = this.getObjects(), i = 0, len = allObjects.length; for ( ; i < len; i++) { allObjects[i].set('active', false); } this._discardActiveGroup(); this._discardActiveObject(); return this; }, /** * Deactivates all objects and dispatches appropriate events * @return {fabric.Canvas} thisArg * @chainable */ deactivateAllWithDispatch: function (e) { var activeGroup = this.getActiveGroup(), activeObject = this.getActiveObject(); if (activeObject || activeGroup) { this.fire('before:selection:cleared', { target: activeObject || activeGroup, e: e }); } this.deactivateAll(); if (activeObject || activeGroup) { this.fire('selection:cleared', { e: e, target: activeObject }); activeObject && activeObject.fire('deselected'); } return this; }, /** * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { this.callSuper('dispose'); var wrapper = this.wrapperEl; this.removeListeners(); wrapper.removeChild(this.upperCanvasEl); wrapper.removeChild(this.lowerCanvasEl); delete this.upperCanvasEl; if (wrapper.parentNode) { wrapper.parentNode.replaceChild(this.lowerCanvasEl, this.wrapperEl); } delete this.wrapperEl; 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 activeGroup = this.getActiveGroup(); if (activeGroup) { activeGroup._renderControls(ctx); } else { this._drawObjectsControls(ctx); } }, /** * @private */ _drawObjectsControls: function(ctx) { for (var i = 0, len = this._objects.length; i < len; ++i) { if (!this._objects[i] || !this._objects[i].active) { continue; } this._objects[i]._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) { var layoutProps = ['angle', 'flipX', 'flipY', 'height', 'left', 'scaleX', 'scaleY', 'top', 'width']; if (instance.group && instance.group === this.getActiveGroup()) { //Copy all the positionally relevant properties across now var originalValues = {}; layoutProps.forEach(function(prop) { originalValues[prop] = instance[prop]; }); this.getActiveGroup().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) { var originalProperties; //If the object is in a selection group, simulate what would happen to that //object when the group is deselected originalProperties = this._realizeGroupTransformOnObject(instance); this.callSuper('_setSVGObject', markup, instance, reviver); this._unwindGroupTransformOnObject(instance, originalProperties); }, }); // 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]; } } if (fabric.isTouchSupported) { /** @ignore */ fabric.Canvas.prototype._setCursorFromEvent = function() { }; } /** * @ignore * @class fabric.Element * @alias fabric.Canvas * @deprecated Use {@link fabric.Canvas} instead. * @constructor */ fabric.Element = fabric.Canvas; })();