(function() { var extend = fabric.util.object.extend, getPointer = fabric.util.getPointer, addListener = fabric.util.addListener, removeListener = fabric.util.removeListener, cursorMap = { 'tr': 'ne-resize', 'br': 'se-resize', 'bl': 'sw-resize', 'tl': 'nw-resize', 'ml': 'w-resize', 'mt': 'n-resize', 'mr': 'e-resize', 'mb': 's-resize' }, utilMin = fabric.util.array.min, utilMax = fabric.util.array.max, sqrt = Math.sqrt, pow = Math.pow, atan2 = Math.atan2, abs = Math.abs, min = Math.min, max = Math.max, STROKE_OFFSET = 0.5; /** * @class fabric.Canvas * @constructor * @extends fabric.StaticCanvas * @param {HTMLElement | String} el <canvas> element to initialize instance on * @param {Object} [options] Options object */ fabric.Canvas = function(el, options) { options || (options = { }); this._initStatic(el, options); this._initInteractive(); fabric.Canvas.activeInstance = this; }; function ProtoProxy(){ } ProtoProxy.prototype = fabric.StaticCanvas.prototype; fabric.Canvas.prototype = new ProtoProxy; var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { /** * Indicates that canvas is interactive. This property should not be changed. * @property * @type Boolean */ interactive: true, /** * Indicates whether group selection should be enabled * @property * @type Boolean */ selection: true, /** * Color of selection * @property * @type String */ selectionColor: 'rgba(100, 100, 255, 0.3)', // blue /** * Color of the border of selection (usually slightly darker than color of selection itself) * @property * @type String */ selectionBorderColor: 'rgba(255, 255, 255, 0.3)', /** * Width of a line used in object/group selection * @property * @type Number */ selectionLineWidth: 1, /** * Color of the line used in free drawing mode * @property * @type String */ freeDrawingColor: 'rgb(0, 0, 0)', /** * Width of a line used in free drawing mode * @property * @type Number */ freeDrawingLineWidth: 1, /** * Default cursor value used when hovering over an object on canvas * @constant * @type String */ HOVER_CURSOR: 'move', /** * Default cursor value used for the entire canvas * @constant * @type String */ CURSOR: 'default', /** * Default element class that's given to wrapper (div) element of canvas * @constant * @type String */ CONTAINER_CLASS: 'canvas-container', _initInteractive: function() { this._currentTransform = null; this._groupSelector = null; this._freeDrawingXPoints = [ ]; this._freeDrawingYPoints = [ ]; this._initWrapperElement(); this._createUpperCanvas(); this._initEvents(); this.calcOffset(); }, /** * Adds mouse listeners to canvas * @method _initEvents * @private * See configuration documentation for more details. */ _initEvents: function () { var _this = this; this._onMouseDown = function (e) { _this.__onMouseDown(e); addListener(fabric.document, 'mouseup', _this._onMouseUp); fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); addListener(fabric.document, 'mousemove', _this._onMouseMove); fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); }; this._onMouseUp = function (e) { _this.__onMouseUp(e); removeListener(fabric.document, 'mouseup', _this._onMouseUp); fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); removeListener(fabric.document, 'mousemove', _this._onMouseMove); fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); }; this._onMouseMove = function (e) { e.preventDefault && e.preventDefault(); _this.__onMouseMove(e); }; this._onResize = function (e) { _this.calcOffset(); }; addListener(fabric.window, 'resize', this._onResize); if (fabric.isTouchSupported) { addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); } else { addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); } }, /** * Method that defines the actions when mouse is released on canvas. * The method resets the currentTransform parameters, store the image corner * position in the image object and render the canvas on top. * @method __onMouseUp * @param {Event} e Event object fired on mouseup * */ __onMouseUp: function (e) { if (this.isDrawingMode && this._isCurrentlyDrawing) { this._finalizeDrawingPath(); return; } if (this._currentTransform) { var transform = this._currentTransform, target = transform.target; if (target._scaling) { target._scaling = false; } // determine the new coords everytime the image changes its position var i = this._objects.length; while (i--) { this._objects[i].setCoords(); } // only fire :modified event if target coordinates were changed during mousedown-mouseup if (this.stateful && target.hasStateChanged()) { target.isMoving = false; this.fire('object:modified', { target: target }); } } this._currentTransform = null; if (this._groupSelector) { // group selection was completed, determine its bounds this._findSelectedObjects(e); } var activeGroup = this.getActiveGroup(); if (activeGroup) { activeGroup.setObjectsCoords(); activeGroup.set('isMoving', false); this._setCursor(this.CURSOR); } // clear selection this._groupSelector = null; this.renderAll(); this._setCursorFromEvent(e, target); // fix for FF this._setCursor(''); var _this = this; setTimeout(function () { _this._setCursorFromEvent(e, target); }, 50); this.fire('mouse:up', { target: target, e: e }); }, /** * Method that defines the actions when mouse is clic ked on canvas. * The method inits the currentTransform parameters and renders all the * canvas so the current image can be placed on the top canvas and the rest * in on the container one. * @method __onMouseDown * @param e {Event} Event object fired on mousedown * */ __onMouseDown: function (e) { // accept only left clicks var isLeftClick = 'which' in e ? e.which == 1 : e.button == 1; if (!isLeftClick && !fabric.isTouchSupported) return; if (this.isDrawingMode) { this._prepareForDrawing(e); // capture coordinates immediately; this allows to draw dots (when movement never occurs) this._captureDrawingPath(e); return; } // ignore if some object is being transformed at this moment if (this._currentTransform) return; var target = this.findTarget(e), pointer = this.getPointer(e), activeGroup = this.getActiveGroup(), corner; if (this._shouldClearSelection(e)) { this._groupSelector = { ex: pointer.x, ey: pointer.y, top: 0, left: 0 }; this.deactivateAllWithDispatch(); } else { // determine if it's a drag or rotate case // rotate and scale will happen at the same time this.stateful && target.saveState(); if (corner = target._findTargetCorner(e, this._offset)) { this.onBeforeScaleRotate(target); } this._setupCurrentTransform(e, target); var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()); if (shouldHandleGroupLogic) { this._handleGroupLogic(e, target); } else { if (target !== this.getActiveGroup()) { this.deactivateAll(); } this.setActiveObject(target, e); } } // we must renderAll so that active image is placed on the top canvas this.renderAll(); this.fire('mouse:down', { target: target, e: e }); }, /** * Method that defines the actions when mouse is hovering the canvas. * The currentTransform parameter will definde whether the user is rotating/scaling/translating * an image or neither of them (only hovering). A group selection is also possible and would cancel * all any other type of action. * In case of an image transformation only the top canvas will be rendered. * @method __onMouseMove * @param e {Event} Event object fired on mousemove * */ __onMouseMove: function (e) { if (this.isDrawingMode) { if (this._isCurrentlyDrawing) { this._captureDrawingPath(e); } return; } var groupSelector = this._groupSelector; // We initially clicked in an empty area, so we draw a box for multiple selection. if (groupSelector !== null) { var pointer = getPointer(e); groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; this.renderTop(); } else if (!this._currentTransform) { // alias style to elimintate unnecessary lookup var style = this.upperCanvasEl.style; // Here we are hovering the canvas then we will determine // what part of the pictures we are hovering to change the caret symbol. // We won't do that while dragging or rotating in order to improve the // performance. var target = this.findTarget(e); if (!target) { // image/text was hovered-out from, we remove its borders for (var i = this._objects.length; i--; ) { if (this._objects[i] && !this._objects[i].active) { this._objects[i].setActive(false); } } style.cursor = this.CURSOR; } else { // set proper cursor this._setCursorFromEvent(e, target); if (target.isActive()) { // display corners when hovering over an image target.setCornersVisibility && target.setCornersVisibility(true); } } } else { // object is being transformed (scaled/rotated/moved/etc.) var pointer = getPointer(e), x = pointer.x, y = pointer.y; this._currentTransform.target.isMoving = true; if (this._currentTransform.action === 'rotate') { // rotate object only if shift key is not pressed // and if it is not a group we are transforming if (!e.shiftKey) { this._rotateObject(x, y); this.fire('object:rotating', { target: this._currentTransform.target }); } this._scaleObject(x, y); this.fire('object:scaling', { target: this._currentTransform.target }); } else if (this._currentTransform.action === 'scaleX') { this._scaleObject(x, y, 'x'); this.fire('object:scaling', { target: this._currentTransform.target }); } else if (this._currentTransform.action === 'scaleY') { this._scaleObject(x, y, 'y'); this.fire('object:scaling', { target: this._currentTransform.target }); } else { this._translateObject(x, y); this.fire('object:moving', { target: this._currentTransform.target }); } // only commit here. when we are actually moving the pictures this.renderAll(); } this.fire('mouse:move', { target: target, e: e }); }, /** * Applies one implementation of 'point inside polygon' algorithm * @method containsPoint * @param e { Event } event object * @param target { fabric.Object } object to test against * @return {Boolean} true if point contains within area of given object */ containsPoint: function (e, target) { var pointer = this.getPointer(e), xy = this._normalizePointer(target, pointer), x = xy.x, y = xy.y; // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html // we iterate through each object. If target found, return it. var iLines = target._getImageLines(target.oCoords), xpoints = target._findCrossPoints(x, y, iLines); // if xcount is odd then we clicked inside the object // For the specific case of square images xcount === 1 in all true cases if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { return true; } return false; }, /** * @private * @method _normalizePointer */ _normalizePointer: function (object, pointer) { var activeGroup = this.getActiveGroup(), x = pointer.x, y = pointer.y; var isObjectInGroup = ( activeGroup && object.type !== 'group' && activeGroup.contains(object) ); if (isObjectInGroup) { x -= activeGroup.left; y -= activeGroup.top; } return { x: x, y: y }; }, /** * @private * @method _shouldClearSelection */ _shouldClearSelection: function (e) { var target = this.findTarget(e), activeGroup = this.getActiveGroup(); return ( !target || ( target && activeGroup && !activeGroup.contains(target) && activeGroup !== target && !e.shiftKey ) ); }, /** * @private * @method _setupCurrentTransform */ _setupCurrentTransform: function (e, target) { var action = 'drag', corner, pointer = getPointer(e); if (corner = target._findTargetCorner(e, this._offset)) { action = (corner === 'ml' || corner === 'mr') ? 'scaleX' : (corner === 'mt' || corner === 'mb') ? 'scaleY' : 'rotate'; } this._currentTransform = { target: target, action: action, scaleX: target.scaleX, scaleY: target.scaleY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, ex: pointer.x, ey: pointer.y, left: target.left, top: target.top, theta: target.theta, width: target.width * target.scaleX }; this._currentTransform.original = { left: target.left, top: target.top }; }, _handleGroupLogic: function (e, target) { if (target.isType('group')) { // if it's a group, find target again, this time skipping group target = this.findTarget(e, true); // if even object is not found, bail out if (!target || target.isType('group')) { return; } } var activeGroup = this.getActiveGroup(); if (activeGroup) { if (activeGroup.contains(target)) { activeGroup.remove(target); target.setActive(false); if (activeGroup.size() === 1) { // remove group alltogether if after removal it only contains 1 object this.discardActiveGroup(); } } else { activeGroup.add(target); } this.fire('selection:created', { target: activeGroup, e: e }); activeGroup.setActive(true); } else { // group does not exist if (this._activeObject) { // only if there's an active object if (target !== this._activeObject) { // and that object is not the actual target var group = new fabric.Group([ this._activeObject,target ]); this.setActiveGroup(group); activeGroup = this.getActiveGroup(); } } // activate target object in any case target.setActive(true); } if (activeGroup) { activeGroup.saveCoords(); } }, /** * @private * @method _prepareForDrawing */ _prepareForDrawing: function(e) { this._isCurrentlyDrawing = true; this.discardActiveObject().renderAll(); var pointer = this.getPointer(e); this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; this._freeDrawingXPoints.push(pointer.x); this._freeDrawingYPoints.push(pointer.y); this.contextTop.beginPath(); this.contextTop.moveTo(pointer.x, pointer.y); this.contextTop.strokeStyle = this.freeDrawingColor; this.contextTop.lineWidth = this.freeDrawingLineWidth; this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; }, /** * @private * @method _captureDrawingPath */ _captureDrawingPath: function(e) { var pointer = this.getPointer(e); this._freeDrawingXPoints.push(pointer.x); this._freeDrawingYPoints.push(pointer.y); this.contextTop.lineTo(pointer.x, pointer.y); this.contextTop.stroke(); }, /** * @private * @method _finalizeDrawingPath */ _finalizeDrawingPath: function() { this.contextTop.closePath(); this._isCurrentlyDrawing = false; var minX = utilMin(this._freeDrawingXPoints), minY = utilMin(this._freeDrawingYPoints), maxX = utilMax(this._freeDrawingXPoints), maxY = utilMax(this._freeDrawingYPoints), ctx = this.contextTop, path = [ ], xPoint, yPoint, xPoints = this._freeDrawingXPoints, yPoints = this._freeDrawingYPoints; path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) { path.push('L ', xPoint - minX, ' ', yPoint - minY, ' '); } // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, // and instead fire something like "drawing:completed" event with path string path = path.join(''); if (path === "M 0 0 L 0 0 ") { // do not create 0 width/height paths, as they are rendered inconsistently across browsers // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing return; } var p = new fabric.Path(path); p.fill = null; p.stroke = this.freeDrawingColor; p.strokeWidth = this.freeDrawingLineWidth; this.add(p); p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords(); this.renderAll(); this.fire('path:created', { path: p }); }, /** * Translates object by "setting" its left/top * @method _translateObject * @param x {Number} pointer's x coordinate * @param y {Number} pointer's y coordinate */ _translateObject: function (x, y) { var target = this._currentTransform.target; target.lockMovementX || target.set('left', x - this._currentTransform.offsetX); target.lockMovementY || target.set('top', y - this._currentTransform.offsetY); }, /** * Scales object by invoking its scaleX/scaleY methods * @method _scaleObject * @param x {Number} pointer's x coordinate * @param y {Number} pointer's y coordinate * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. * When not provided, an object is scaled by both dimensions equally */ _scaleObject: function (x, y, by) { var t = this._currentTransform, offset = this._offset, target = t.target; if (target.lockScalingX && target.lockScalingY) return; var lastLen = sqrt(pow(t.ey - t.top - offset.top, 2) + pow(t.ex - t.left - offset.left, 2)), curLen = sqrt(pow(y - t.top - offset.top, 2) + pow(x - t.left - offset.left, 2)); target._scaling = true; if (!by) { target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); } else if (by === 'x' && !target.lockUniScaling) { target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); } else if (by === 'y' && !target.lockUniScaling) { target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); } }, /** * Rotates object by invoking its rotate method * @method _rotateObject * @param x {Number} pointer's x coordinate * @param y {Number} pointer's y coordinate */ _rotateObject: function (x, y) { var t = this._currentTransform, o = this._offset; if (t.target.lockRotation) return; var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left), curAngle = atan2(y - t.top - o.top, x - t.left - o.left); t.target.set('theta', (curAngle - lastAngle) + t.theta); }, /** * @method _setCursor */ _setCursor: function (value) { this.upperCanvasEl.style.cursor = value; }, /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera * @method _setCursorFromEvent * @param e {Event} Event object * @param target {Object} Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { var s = this.upperCanvasEl.style; if (!target) { s.cursor = this.CURSOR; return false; } else { var activeGroup = this.getActiveGroup(); // only show proper corner when group selection is not active var corner = !!target._findTargetCorner && (!activeGroup || !activeGroup.contains(target)) && target._findTargetCorner(e, this._offset); if (!corner) { s.cursor = this.HOVER_CURSOR; } else { if (corner in cursorMap) { s.cursor = cursorMap[corner]; } else { s.cursor = this.CURSOR; return false; } } } return true; }, /** * @method _drawSelection * @private */ _drawSelection: function () { var groupSelector = this._groupSelector, left = groupSelector.left, top = groupSelector.top, aleft = abs(left), atop = abs(top); this.contextTop.fillStyle = this.selectionColor; this.contextTop.fillRect( groupSelector.ex - ((left > 0) ? 0 : -left), groupSelector.ey - ((top > 0) ? 0 : -top), aleft, atop ); this.contextTop.lineWidth = this.selectionLineWidth; this.contextTop.strokeStyle = this.selectionBorderColor; this.contextTop.strokeRect( groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), aleft, atop ); }, _findSelectedObjects: function (e) { var target, targetRegion, group = [ ], x1 = this._groupSelector.ex, y1 = this._groupSelector.ey, x2 = x1 + this._groupSelector.left, y2 = y1 + this._groupSelector.top, currentObject, selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)); for (var i = 0, len = this._objects.length; i < len; ++i) { currentObject = this._objects[i]; if (!currentObject) continue; if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { if (this.selection && currentObject.selectable) { currentObject.setActive(true); group.push(currentObject); } } } // do not create group for 1 element only if (group.length === 1) { this.setActiveObject(group[0], e); } else if (group.length > 1) { var group = new fabric.Group(group); this.setActiveGroup(group); group.saveCoords(); this.fire('selection:created', { target: group }); } this.renderAll(); }, /** * Method that determines what object we are clicking on * @method findTarget * @param {Event} e mouse event * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through */ findTarget: function (e, skipGroup) { var target, pointer = this.getPointer(e); // first check current group (if one exists) var activeGroup = this.getActiveGroup(); if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { target = activeGroup; return target; } // then check all of the objects on canvas for (var i = this._objects.length; i--; ) { if (this._objects[i] && this.containsPoint(e, this._objects[i])) { target = this._objects[i]; this.relatedTarget = target; break; } } if (target && target.selectable) { return target; } }, /** * Returns pointer coordinates relative to canvas. * @method getPointer * @return {Object} object with "x" and "y" number values */ getPointer: function (e) { var pointer = getPointer(e); return { x: pointer.x - this._offset.left, y: pointer.y - this._offset.top }; }, /** * @method _createUpperCanvas * @param {HTMLElement|String} canvasEl Canvas element * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized */ _createUpperCanvas: function () { this.upperCanvasEl = this._createCanvasElement(); this.upperCanvasEl.className = 'upper-canvas'; this.wrapperEl.appendChild(this.upperCanvasEl); this._applyCanvasStyle(this.upperCanvasEl); this.contextTop = this.upperCanvasEl.getContext('2d'); }, /** * @private * @method _initWrapperElement * @param {Number} width * @param {Number} height */ _initWrapperElement: function () { this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { 'class': this.CONTAINER_CLASS }); fabric.util.setStyle(this.wrapperEl, { width: this.getWidth() + 'px', height: this.getHeight() + 'px', position: 'relative' }); fabric.util.makeElementUnselectable(this.wrapperEl); }, /** * @private * @method _applyCanvasStyle * @param {Element} element */ _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); }, /** * Returns topmost canvas context * @method getContext * @return {CanvasRenderingContext2D} */ getContext: function () { return this.contextTop; }, /** * Sets given object as active * @method setActiveObject * @param object {fabric.Object} Object to set as an active one * @return {fabric.Canvas} thisArg * @chainable */ setActiveObject: function (object, e) { if (this._activeObject) { this._activeObject.setActive(false); } this._activeObject = object; object.setActive(true); this.renderAll(); this.fire('object:selected', { target: object, e: e }); return this; }, /** * Returns currently active object * @method getActiveObject * @return {fabric.Object} active object */ getActiveObject: function () { return this._activeObject; }, /** * Discards currently active object * @method discardActiveObject * @return {fabric.Canvas} thisArg * @chainable */ discardActiveObject: function () { if (this._activeObject) { this._activeObject.setActive(false); } this._activeObject = null; return this; }, /** * Sets active group to a speicified one * @method setActiveGroup * @param {fabric.Group} group Group to set as a current one * @return {fabric.Canvas} thisArg * @chainable */ setActiveGroup: function (group) { this._activeGroup = group; return this; }, /** * Returns currently active group * @method getActiveGroup * @return {fabric.Group} Current group */ getActiveGroup: function () { return this._activeGroup; }, /** * Removes currently active group * @method discardActiveGroup * @return {fabric.Canvas} thisArg */ discardActiveGroup: function () { var g = this.getActiveGroup(); if (g) { g.destroy(); } return this.setActiveGroup(null); }, /** * Deactivates all objects by calling their setActive(false) * @method deactivateAll * @return {fabric.Canvas} thisArg */ deactivateAll: function () { var allObjects = this.getObjects(), i = 0, len = allObjects.length; for ( ; i < len; i++) { allObjects[i].setActive(false); } this.discardActiveGroup(); this.discardActiveObject(); return this; }, /** * Deactivates all objects and dispatches appropriate events * @method deactivateAllWithDispatch * @return {fabric.Canvas} thisArg */ deactivateAllWithDispatch: function () { var activeObject = this.getActiveGroup() || this.getActiveObject(); if (activeObject) { this.fire('before:selection:cleared', { target: activeObject }); } this.deactivateAll(); if (activeObject) { this.fire('selection:cleared'); } return this; } }; fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; extend(fabric.Canvas.prototype, InteractiveMethods); // iterating manually to workaround 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) { fabric.Canvas.prototype._setCursorFromEvent = function() { }; } /** * @class fabric.Element * @alias fabric.Canvas * @deprecated * @constructor */ fabric.Element = fabric.Canvas; })();