diff --git a/dist/all.js b/dist/all.js index ebd1f3a3..05561a57 100644 --- a/dist/all.js +++ b/dist/all.js @@ -1,6 +1,6 @@ /*! Fabric.js Copyright 2008-2011, Bitsonnet (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: "0.3.2" }; +var fabric = fabric || { version: "0.3.3" }; /** * Wrapper around `console.log` (when available) @@ -1627,11 +1627,55 @@ fabric.Observable = { return false; } + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @method animate + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + options || (options = { }); + + var start = +new Date(), + duration = options.duration || 500, + finish = start + duration, time, pos, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; }, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + isReversed = startValue > endValue; + + options.onStart && options.onStart(); + + var interval = setInterval(function() { + time = +new Date(); + pos = time > finish ? 1 : (time - start) / duration; + onChange(isReversed + ? (startValue - (startValue - endValue) * easing(pos)) + : (startValue + (endValue - startValue) * easing(pos))); + if (time > finish || abort()) { + clearInterval(interval); + options.onComplete && options.onComplete(); + } + }, 10); + + return interval; + } + fabric.util.removeFromArray = removeFromArray; fabric.util.degreesToRadians = degreesToRadians; fabric.util.toFixed = toFixed; fabric.util.getRandomInt = getRandomInt; fabric.util.falseFunction = falseFunction; + fabric.util.animate = animate; })(); if (!Array.prototype.indexOf) { @@ -2445,56 +2489,12 @@ function getElementOffset(element) { } })(); -/** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @method animate - * @memberOf fabric.util - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Number} [options.startValue=0] Starting value - * @param {Number} [options.endValue=100] Ending value - * @param {Function} [options.easing] Easing function - * @param {Number} [options.duration=500] Duration of change - */ -function animate(options) { - - options || (options = { }); - - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, pos, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; }, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - isReversed = startValue > endValue; - - options.onStart && options.onStart(); - - var interval = setInterval(function() { - time = +new Date(); - pos = time > finish ? 1 : (time - start) / duration; - onChange(isReversed - ? (startValue - (startValue - endValue) * easing(pos)) - : (startValue + (endValue - startValue) * easing(pos))); - if (time > finish || abort()) { - clearInterval(interval); - options.onComplete && options.onComplete(); - } - }, 10); - - return interval; -} - fabric.util.getById = getById; fabric.util.toArray = toArray; fabric.util.makeElement = makeElement; fabric.util.addClass = addClass; fabric.util.wrapElement = wrapElement; fabric.util.getElementOffset = getElementOffset; -fabric.util.animate = animate; (function(){ diff --git a/docs/symbols/src/src_canvas.class.js.html b/docs/symbols/src/src_canvas.class.js.html new file mode 100644 index 00000000..a9c62bba --- /dev/null +++ b/docs/symbols/src/src_canvas.class.js.html @@ -0,0 +1,2445 @@ +
1 (function (global) { + 2 + 3 "use strict"; + 4 + 5 if (fabric.Canvas) { + 6 fabric.warn('fabric.Canvas is already defined.'); + 7 return; + 8 } + 9 + 10 var window = global.window, + 11 document = window.document, + 12 + 13 // aliases for faster resolution + 14 extend = fabric.util.object.extend, + 15 capitalize = fabric.util.string.capitalize, + 16 camelize = fabric.util.string.camelize, + 17 getPointer = fabric.util.getPointer, + 18 getElementOffset = fabric.util.getElementOffset, + 19 removeFromArray = fabric.util.removeFromArray, + 20 addListener = fabric.util.addListener, + 21 removeListener = fabric.util.removeListener, + 22 + 23 utilMin = fabric.util.array.min, + 24 utilMax = fabric.util.array.max, + 25 + 26 sqrt = Math.sqrt, + 27 pow = Math.pow, + 28 atan2 = Math.atan2, + 29 abs = Math.abs, + 30 min = Math.min, + 31 max = Math.max, + 32 + 33 CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'), + 34 FX_DURATION = 500, + 35 STROKE_OFFSET = 0.5, + 36 FX_TRANSITION = 'decel', + 37 + 38 cursorMap = { + 39 'tr': 'ne-resize', + 40 'br': 'se-resize', + 41 'bl': 'sw-resize', + 42 'tl': 'nw-resize', + 43 'ml': 'w-resize', + 44 'mt': 'n-resize', + 45 'mr': 'e-resize', + 46 'mb': 's-resize' + 47 }; + 48 + 49 /** + 50 * @class fabric.Canvas + 51 * @constructor + 52 * @param {HTMLElement | String} el <canvas> element to initialize instance on + 53 * @param {Object} [options] Options object + 54 */ + 55 fabric.Canvas = function (el, options) { + 56 + 57 options || (options = { }); + 58 + 59 /** + 60 * The object literal containing mouse position if clicked in an empty area (no image) + 61 * @property _groupSelector + 62 * @type object + 63 */ + 64 this._groupSelector = null; + 65 + 66 /** + 67 * The array literal containing all objects on canvas + 68 * @property _objects + 69 * @type array + 70 */ + 71 this._objects = []; + 72 + 73 /** + 74 * The element that references the canvas interface implementation + 75 * @property _context + 76 * @type object + 77 */ + 78 this._context = null; + 79 + 80 /** + 81 * The object literal containing the current x,y params of the transformation + 82 * @property _currentTransform + 83 * @type object + 84 */ + 85 this._currentTransform = null; + 86 + 87 /** + 88 * References instance of fabric.Group - when multiple objects are selected + 89 * @property _activeGroup + 90 * @type object + 91 */ + 92 this._activeGroup = null; + 93 + 94 /** + 95 * X coordinates of a path, captured during free drawing + 96 */ + 97 this._freeDrawingXPoints = [ ]; + 98 + 99 /** +100 * Y coordinates of a path, captured during free drawing +101 */ +102 this._freeDrawingYPoints = [ ]; +103 +104 this._createUpperCanvas(el); +105 this._initOptions(options); +106 this._initWrapperElement(); +107 this._createLowerCanvas(); +108 +109 this._initEvents(); +110 +111 if (options.overlayImage) { +112 this.setOverlayImage(options.overlayImage); +113 } +114 +115 this.calcOffset(); +116 +117 fabric.Canvas.activeInstance = this; +118 }; +119 +120 extend(fabric.Canvas.prototype, fabric.Observable); +121 +122 extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ { +123 +124 /** +125 * Background color of this canvas instance +126 * @property +127 * @type String +128 */ +129 backgroundColor: 'rgba(0, 0, 0, 0)', +130 +131 /** +132 * Indicates whether object selection should be enabled +133 * @property +134 * @type Boolean +135 */ +136 selection: true, +137 +138 /** +139 * Color of selection +140 * @property +141 * @type String +142 */ +143 selectionColor: 'rgba(100, 100, 255, 0.3)', // blue +144 +145 /** +146 * Color of the border of selection (usually slightly darker than color of selection itself) +147 * @property +148 * @type String +149 */ +150 selectionBorderColor: 'rgba(255, 255, 255, 0.3)', +151 +152 /** +153 * Width of a line used in selection +154 * @property +155 * @type Number +156 */ +157 selectionLineWidth: 1, +158 +159 /** +160 * Color of the line used in free drawing mode +161 * @property +162 * @type String +163 */ +164 freeDrawingColor: 'rgb(0, 0, 0)', +165 +166 /** +167 * Width of a line used in free drawing mode +168 * @property +169 * @type Number +170 */ +171 freeDrawingLineWidth: 1, +172 +173 /** +174 * @property +175 * @type Boolean +176 */ +177 includeDefaultValues: true, +178 +179 /** +180 * Indicates whether images loaded via `fabric.Canvas#loadImageFromUrl` should be cached +181 * @property +182 * @type Boolean +183 */ +184 shouldCacheImages: false, +185 +186 /** +187 * Indicates whether objects' state should be saved +188 * @property +189 * @type Boolean +190 */ +191 stateful: true, +192 +193 /** +194 * Indicates whether fabric.Canvas#add should also re-render canvas. +195 * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once +196 * (followed by a manual rendering after addition) +197 */ +198 renderOnAddition: true, +199 +200 /** +201 * @constant +202 * @type Number +203 */ +204 CANVAS_WIDTH: 600, +205 +206 /** +207 * @constant +208 * @type Number +209 */ +210 CANVAS_HEIGHT: 600, +211 +212 /** +213 * @constant +214 * @type String +215 */ +216 CONTAINER_CLASS: 'canvas-container', +217 +218 /** +219 * @constant +220 * @type String +221 */ +222 HOVER_CURSOR: 'move', +223 +224 /** +225 * Callback; invoked right before object is about to be scaled/rotated +226 * @method onBeforeScaleRotate +227 * @param {fabric.Object} target Object that's about to be scaled/rotated +228 */ +229 onBeforeScaleRotate: function (target) { +230 /* NOOP */ +231 }, +232 +233 /** +234 * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps +235 * @method onFpsUpdate +236 * @param {Number} fps +237 */ +238 onFpsUpdate: null, +239 +240 /** +241 * Calculates canvas element offset relative to the document +242 * This method is also attached as "resize" event handler of window +243 * @method calcOffset +244 * @return {fabric.Canvas} instance +245 * @chainable +246 */ +247 calcOffset: function () { +248 this._offset = getElementOffset(this.upperCanvasEl); +249 return this; +250 }, +251 +252 /** +253 * Sets overlay image for this canvas +254 * @method setOverlayImage +255 * @param {String} url url of an image to set background to +256 * @param {Function} callback callback to invoke when image is loaded and set as an overlay one +257 * @return {fabric.Canvas} thisArg +258 * @chainable +259 */ +260 setOverlayImage: function (url, callback) { // TODO (kangax): test callback +261 if (url) { +262 var _this = this, img = new Image(); +263 +264 /** @ignore */ +265 img.onload = function () { +266 _this.overlayImage = img; +267 if (callback) { +268 callback(); +269 } +270 img = img.onload = null; +271 }; +272 img.src = url; +273 } +274 return this; +275 }, +276 +277 /** +278 * @private +279 * @method _initWrapperElement +280 * @param {Number} width +281 * @param {Number} height +282 */ +283 _initWrapperElement: function () { +284 this.wrapperEl = fabric.util.wrapElement(this.upperCanvasEl, 'div', { +285 'class': this.CONTAINER_CLASS +286 }); +287 fabric.util.setStyle(this.wrapperEl, { +288 width: this.getWidth() + 'px', +289 height: this.getHeight() + 'px', +290 position: 'relative' +291 }); +292 fabric.util.makeElementUnselectable(this.wrapperEl); +293 }, +294 +295 /** +296 * @private +297 * @method _applyCanvasStyle +298 * @param {Element} element +299 */ +300 _applyCanvasStyle: function (element) { +301 var width = this.getWidth() || element.width, +302 height = this.getHeight() || element.height; +303 +304 fabric.util.setStyle(element, { +305 position: 'absolute', +306 width: width + 'px', +307 height: height + 'px', +308 left: 0, +309 top: 0 +310 }); +311 element.width = width; +312 element.height = height; +313 fabric.util.makeElementUnselectable(element); +314 }, +315 +316 /** +317 * @private +318 * @method _createCanvasElement +319 * @param {Element} element +320 */ +321 _createCanvasElement: function() { +322 var element = document.createElement('canvas'); +323 if (!element) { +324 throw CANVAS_INIT_ERROR; +325 } +326 this._initCanvasElement(element); +327 return element; +328 }, +329 +330 _initCanvasElement: function(element) { +331 if (typeof element.getContext === 'undefined' && +332 typeof G_vmlCanvasManager !== 'undefined' && +333 G_vmlCanvasManager.initElement) { +334 +335 G_vmlCanvasManager.initElement(element); +336 } +337 if (typeof element.getContext === 'undefined') { +338 throw CANVAS_INIT_ERROR; +339 } +340 }, +341 +342 /** +343 * @method _initOptions +344 * @param {Object} options +345 */ +346 _initOptions: function (options) { +347 for (var prop in options) { +348 this[prop] = options[prop]; +349 } +350 +351 this.width = parseInt(this.upperCanvasEl.width, 10) || 0; +352 this.height = parseInt(this.upperCanvasEl.height, 10) || 0; +353 +354 this.upperCanvasEl.style.width = this.width + 'px'; +355 this.upperCanvasEl.style.height = this.height + 'px'; +356 }, +357 +358 /** +359 * Adds mouse listeners to canvas +360 * @method _initEvents +361 * @private +362 * See configuration documentation for more details. +363 */ +364 _initEvents: function () { +365 +366 var _this = this; +367 +368 this._onMouseDown = function (e) { +369 _this.__onMouseDown(e); +370 addListener(document, 'mouseup', _this._onMouseUp); +371 }; +372 this._onMouseUp = function (e) { +373 _this.__onMouseUp(e); +374 removeListener(document, 'mouseup', _this._onMouseUp); +375 }; +376 this._onMouseMove = function (e) { _this.__onMouseMove(e); }; +377 this._onResize = function (e) { _this.calcOffset() }; +378 +379 addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); +380 addListener(document, 'mousemove', this._onMouseMove); +381 addListener(window, 'resize', this._onResize); +382 }, +383 +384 /** +385 * @method _createUpperCanvas +386 * @param {HTMLElement|String} canvasEl Canvas element +387 * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized +388 */ +389 _createUpperCanvas: function (canvasEl) { +390 this.upperCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); +391 this._initCanvasElement(this.upperCanvasEl); +392 +393 fabric.util.addClass(this.upperCanvasEl, 'upper-canvas'); +394 this._applyCanvasStyle(this.upperCanvasEl); +395 +396 this.contextTop = this.upperCanvasEl.getContext('2d'); +397 }, +398 +399 /** +400 * Creates a secondary canvas +401 * @method _createLowerCanvas +402 */ +403 _createLowerCanvas: function () { +404 this.lowerCanvasEl = this._createCanvasElement(); +405 this.lowerCanvasEl.className = 'lower-canvas'; +406 +407 this.wrapperEl.insertBefore(this.lowerCanvasEl, this.upperCanvasEl); +408 +409 this._applyCanvasStyle(this.lowerCanvasEl); +410 this.contextContainer = this.lowerCanvasEl.getContext('2d'); +411 }, +412 +413 /** +414 * Returns canvas width +415 * @method getWidth +416 * @return {Number} +417 */ +418 getWidth: function () { +419 return this.width; +420 }, +421 +422 /** +423 * Returns canvas height +424 * @method getHeight +425 * @return {Number} +426 */ +427 getHeight: function () { +428 return this.height; +429 }, +430 +431 /** +432 * Sets width of this canvas instance +433 * @method setWidth +434 * @param {Number} width value to set width to +435 * @return {fabric.Canvas} instance +436 * @chainable true +437 */ +438 setWidth: function (value) { +439 return this._setDimension('width', value); +440 }, +441 +442 /** +443 * Sets height of this canvas instance +444 * @method setHeight +445 * @param {Number} height value to set height to +446 * @return {fabric.Canvas} instance +447 * @chainable true +448 */ +449 setHeight: function (value) { +450 return this._setDimension('height', value); +451 }, +452 +453 /** +454 * Sets dimensions (width, height) of this canvas instance +455 * @method setDimensions +456 * @param {Object} dimensions +457 * @return {fabric.Canvas} thisArg +458 * @chainable +459 */ +460 setDimensions: function(dimensions) { +461 for (var prop in dimensions) { +462 this._setDimension(prop, dimensions[prop]); +463 } +464 return this; +465 }, +466 +467 /** +468 * Helper for setting width/height +469 * @private +470 * @method _setDimensions +471 * @param {String} prop property (width|height) +472 * @param {Number} value value to set property to +473 * @return {fabric.Canvas} instance +474 * @chainable true +475 */ +476 _setDimension: function (prop, value) { +477 this.lowerCanvasEl[prop] = value; +478 this.lowerCanvasEl.style[prop] = value + 'px'; +479 +480 this.upperCanvasEl[prop] = value; +481 this.upperCanvasEl.style[prop] = value + 'px'; +482 +483 this.wrapperEl.style[prop] = value + 'px'; +484 +485 this[prop] = value; +486 +487 this.calcOffset(); +488 this.renderAll(); +489 +490 return this; +491 }, +492 +493 /** +494 * Method that defines the actions when mouse is released on canvas. +495 * The method resets the currentTransform parameters, store the image corner +496 * position in the image object and render the canvas on top. +497 * @method __onMouseUp +498 * @param {Event} e Event object fired on mouseup +499 * +500 */ +501 __onMouseUp: function (e) { +502 +503 if (this.isDrawingMode && this._isCurrentlyDrawing) { +504 this._finalizeDrawingPath(); +505 return; +506 } +507 +508 if (this._currentTransform) { +509 +510 var transform = this._currentTransform, +511 target = transform.target; +512 +513 if (target._scaling) { +514 this.fire('object:scaled', { target: target }); +515 target._scaling = false; +516 } +517 +518 // determine the new coords everytime the image changes its position +519 var i = this._objects.length; +520 while (i--) { +521 this._objects[i].setCoords(); +522 } +523 +524 // only fire :modified event if target coordinates were changed during mousedown-mouseup +525 if (this.stateful && target.hasStateChanged()) { +526 target.isMoving = false; +527 this.fire('object:modified', { target: target }); +528 } +529 } +530 +531 this._currentTransform = null; +532 +533 if (this._groupSelector) { +534 // group selection was completed, determine its bounds +535 this._findSelectedObjects(e); +536 } +537 var activeGroup = this.getActiveGroup(); +538 if (activeGroup) { +539 if (this.stateful && activeGroup.hasStateChanged() && +540 activeGroup.containsPoint(this.getPointer(e))) { +541 this.fire('group:modified', { target: activeGroup }); +542 } +543 activeGroup.setObjectsCoords(); +544 activeGroup.set('isMoving', false); +545 this._setCursor('default'); +546 } +547 +548 // clear selection +549 this._groupSelector = null; +550 this.renderAll(); +551 +552 this._setCursorFromEvent(e, target); +553 +554 // fix for FF +555 this._setCursor(''); +556 +557 var _this = this; +558 setTimeout(function () { +559 _this._setCursorFromEvent(e, target); +560 }, 50); +561 +562 this.fire('mouse:up', { target: target }); +563 }, +564 +565 _shouldClearSelection: function (e) { +566 var target = this.findTarget(e), +567 activeGroup = this.getActiveGroup(); +568 return ( +569 !target || ( +570 target && +571 activeGroup && +572 !activeGroup.contains(target) && +573 activeGroup !== target && +574 !e.shiftKey +575 ) +576 ); +577 }, +578 +579 /** +580 * Method that defines the actions when mouse is clic ked on canvas. +581 * The method inits the currentTransform parameters and renders all the +582 * canvas so the current image can be placed on the top canvas and the rest +583 * in on the container one. +584 * @method __onMouseDown +585 * @param e {Event} Event object fired on mousedown +586 * +587 */ +588 __onMouseDown: function (e) { +589 +590 // accept only left clicks +591 if (e.which !== 1) return; +592 +593 if (this.isDrawingMode) { +594 this._prepareForDrawing(e); +595 +596 // capture coordinates immediately; this allows to draw dots (when movement never occurs) +597 this._captureDrawingPath(e); +598 +599 return; +600 } +601 +602 // ignore if some object is being transformed at this moment +603 if (this._currentTransform) return; +604 +605 var target = this.findTarget(e), +606 pointer = this.getPointer(e), +607 activeGroup = this.getActiveGroup(), +608 corner; +609 +610 if (this._shouldClearSelection(e)) { +611 +612 this._groupSelector = { +613 ex: pointer.x, +614 ey: pointer.y, +615 top: 0, +616 left: 0 +617 }; +618 +619 this.deactivateAllWithDispatch(); +620 } +621 else { +622 // determine if it's a drag or rotate case +623 // rotate and scale will happen at the same time +624 this.stateful && target.saveState(); +625 +626 if (corner = target._findTargetCorner(e, this._offset)) { +627 this.onBeforeScaleRotate(target); +628 } +629 +630 this._setupCurrentTransform(e, target); +631 +632 var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()); +633 if (shouldHandleGroupLogic) { +634 this._handleGroupLogic(e, target); +635 } +636 else { +637 if (target !== this.getActiveGroup()) { +638 this.deactivateAll(); +639 } +640 this.setActiveObject(target); +641 } +642 } +643 // we must renderAll so that active image is placed on the top canvas +644 this.renderAll(); +645 +646 this.fire('mouse:down', { target: target }); +647 }, +648 +649 /** +650 * Returns <canvas> element corresponding to this instance +651 * @method getElement +652 * @return {HTMLCanvasElement} +653 */ +654 getElement: function () { +655 return this.upperCanvasEl; +656 }, +657 +658 /** +659 * Deactivates all objects and dispatches appropriate events +660 * @method deactivateAllWithDispatch +661 * @return {fabric.Canvas} thisArg +662 */ +663 deactivateAllWithDispatch: function () { +664 var activeGroup = this.getActiveGroup(); +665 if (activeGroup) { +666 this.fire('before:group:destroyed', { +667 target: activeGroup +668 }); +669 } +670 this.deactivateAll(); +671 if (activeGroup) { +672 this.fire('after:group:destroyed'); +673 } +674 this.fire('selection:cleared'); +675 return this; +676 }, +677 +678 /** +679 * @private +680 * @method _setupCurrentTransform +681 */ +682 _setupCurrentTransform: function (e, target) { +683 var action = 'drag', +684 corner, +685 pointer = getPointer(e); +686 +687 if (corner = target._findTargetCorner(e, this._offset)) { +688 action = (corner === 'ml' || corner === 'mr') +689 ? 'scaleX' +690 : (corner === 'mt' || corner === 'mb') +691 ? 'scaleY' +692 : 'rotate'; +693 } +694 +695 this._currentTransform = { +696 target: target, +697 action: action, +698 scaleX: target.scaleX, +699 scaleY: target.scaleY, +700 offsetX: pointer.x - target.left, +701 offsetY: pointer.y - target.top, +702 ex: pointer.x, +703 ey: pointer.y, +704 left: target.left, +705 top: target.top, +706 theta: target.theta, +707 width: target.width * target.scaleX +708 }; +709 +710 this._currentTransform.original = { +711 left: target.left, +712 top: target.top +713 }; +714 }, +715 +716 _handleGroupLogic: function (e, target) { +717 if (target.isType('group')) { +718 // if it's a group, find target again, this time skipping group +719 target = this.findTarget(e, true); +720 // if even object is not found, bail out +721 if (!target || target.isType('group')) { +722 return; +723 } +724 } +725 var activeGroup = this.getActiveGroup(); +726 if (activeGroup) { +727 if (activeGroup.contains(target)) { +728 activeGroup.remove(target); +729 target.setActive(false); +730 if (activeGroup.size() === 1) { +731 // remove group alltogether if after removal it only contains 1 object +732 this.removeActiveGroup(); +733 } +734 } +735 else { +736 activeGroup.add(target); +737 } +738 this.fire('group:selected', { target: activeGroup }); +739 activeGroup.setActive(true); +740 } +741 else { +742 // group does not exist +743 if (this._activeObject) { +744 // only if there's an active object +745 if (target !== this._activeObject) { +746 // and that object is not the actual target +747 var group = new fabric.Group([ this._activeObject,target ]); +748 this.setActiveGroup(group); +749 activeGroup = this.getActiveGroup(); +750 } +751 } +752 // activate target object in any case +753 target.setActive(true); +754 } +755 +756 if (activeGroup) { +757 activeGroup.saveCoords(); +758 } +759 }, +760 +761 /** +762 * @private +763 * @method _prepareForDrawing +764 */ +765 _prepareForDrawing: function(e) { +766 +767 this._isCurrentlyDrawing = true; +768 +769 this.removeActiveObject().renderAll(); +770 +771 var pointer = this.getPointer(e); +772 +773 this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; +774 +775 this._freeDrawingXPoints.push(pointer.x); +776 this._freeDrawingYPoints.push(pointer.y); +777 +778 this.contextTop.beginPath(); +779 this.contextTop.moveTo(pointer.x, pointer.y); +780 this.contextTop.strokeStyle = this.freeDrawingColor; +781 this.contextTop.lineWidth = this.freeDrawingLineWidth; +782 this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; +783 }, +784 +785 /** +786 * @private +787 * @method _captureDrawingPath +788 */ +789 _captureDrawingPath: function(e) { +790 var pointer = this.getPointer(e); +791 +792 this._freeDrawingXPoints.push(pointer.x); +793 this._freeDrawingYPoints.push(pointer.y); +794 +795 this.contextTop.lineTo(pointer.x, pointer.y); +796 this.contextTop.stroke(); +797 }, +798 +799 /** +800 * @private +801 * @method _finalizeDrawingPath +802 */ +803 _finalizeDrawingPath: function() { +804 +805 this.contextTop.closePath(); +806 +807 this._isCurrentlyDrawing = false; +808 +809 var minX = utilMin(this._freeDrawingXPoints), +810 minY = utilMin(this._freeDrawingYPoints), +811 maxX = utilMax(this._freeDrawingXPoints), +812 maxY = utilMax(this._freeDrawingYPoints), +813 ctx = this.contextTop, +814 path = [ ], +815 xPoint, +816 yPoint, +817 xPoints = this._freeDrawingXPoints, +818 yPoints = this._freeDrawingYPoints; +819 +820 path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); +821 +822 for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) { +823 path.push('L ', xPoint - minX, ' ', yPoint - minY, ' '); +824 } +825 +826 // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, +827 // and instead fire something like "drawing:completed" event with path string +828 +829 path = path.join(''); +830 +831 if (path === "M 0 0 L 0 0 ") { +832 // do not create 0 width/height paths, as they are rendered inconsistently across browsers +833 // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing +834 return; +835 } +836 +837 var p = new fabric.Path(path); +838 +839 p.fill = null; +840 p.stroke = this.freeDrawingColor; +841 p.strokeWidth = this.freeDrawingLineWidth; +842 this.add(p); +843 p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords(); +844 this.renderAll(); +845 this.fire('path:created', { path: p }); +846 }, +847 +848 /** +849 * Method that defines the actions when mouse is hovering the canvas. +850 * The currentTransform parameter will definde whether the user is rotating/scaling/translating +851 * an image or neither of them (only hovering). A group selection is also possible and would cancel +852 * all any other type of action. +853 * In case of an image transformation only the top canvas will be rendered. +854 * @method __onMouseMove +855 * @param e {Event} Event object fired on mousemove +856 * +857 */ +858 __onMouseMove: function (e) { +859 +860 if (this.isDrawingMode) { +861 if (this._isCurrentlyDrawing) { +862 this._captureDrawingPath(e); +863 } +864 return; +865 } +866 +867 var groupSelector = this._groupSelector; +868 +869 // We initially clicked in an empty area, so we draw a box for multiple selection. +870 if (groupSelector !== null) { +871 var pointer = getPointer(e); +872 groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; +873 groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; +874 this.renderTop(); +875 } +876 else if (!this._currentTransform) { +877 +878 // alias style to elimintate unnecessary lookup +879 var style = this.upperCanvasEl.style; +880 +881 // Here we are hovering the canvas then we will determine +882 // what part of the pictures we are hovering to change the caret symbol. +883 // We won't do that while dragging or rotating in order to improve the +884 // performance. +885 var target = this.findTarget(e); +886 +887 if (!target) { +888 // image/text was hovered-out from, we remove its borders +889 for (var i = this._objects.length; i--; ) { +890 if (!this._objects[i].active) { +891 this._objects[i].setActive(false); +892 } +893 } +894 style.cursor = 'default'; +895 } +896 else { +897 // set proper cursor +898 this._setCursorFromEvent(e, target); +899 if (target.isActive()) { +900 // display corners when hovering over an image +901 target.setCornersVisibility && target.setCornersVisibility(true); +902 } +903 } +904 } +905 else { +906 // object is being transformed (scaled/rotated/moved/etc.) +907 var pointer = getPointer(e), +908 x = pointer.x, +909 y = pointer.y; +910 +911 this._currentTransform.target.isMoving = true; +912 +913 if (this._currentTransform.action === 'rotate') { +914 // rotate object only if shift key is not pressed +915 // and if it is not a group we are transforming +916 +917 if (!e.shiftKey) { +918 this._rotateObject(x, y); +919 } +920 this._scaleObject(x, y); +921 } +922 else if (this._currentTransform.action === 'scaleX') { +923 this._scaleObject(x, y, 'x'); +924 } +925 else if (this._currentTransform.action === 'scaleY') { +926 this._scaleObject(x, y, 'y'); +927 } +928 else { +929 this._translateObject(x, y); +930 +931 this.fire('object:moved', { +932 target: this._currentTransform.target +933 }); +934 } +935 // only commit here. when we are actually moving the pictures +936 this.renderAll(); +937 } +938 }, +939 +940 /** +941 * Translates object by "setting" its left/top +942 * @method _translateObject +943 * @param x {Number} pointer's x coordinate +944 * @param y {Number} pointer's y coordinate +945 */ +946 _translateObject: function (x, y) { +947 var target = this._currentTransform.target; +948 target.lockMovementX || target.set('left', x - this._currentTransform.offsetX); +949 target.lockMovementY || target.set('top', y - this._currentTransform.offsetY); +950 }, +951 +952 /** +953 * Scales object by invoking its scaleX/scaleY methods +954 * @method _scaleObject +955 * @param x {Number} pointer's x coordinate +956 * @param y {Number} pointer's y coordinate +957 * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. +958 * When not provided, an object is scaled by both dimensions equally +959 */ +960 _scaleObject: function (x, y, by) { +961 var t = this._currentTransform, +962 offset = this._offset, +963 target = t.target; +964 +965 if (target.lockScalingX && target.lockScalingY) return; +966 +967 var lastLen = sqrt(pow(t.ey - t.top - offset.top, 2) + pow(t.ex - t.left - offset.left, 2)), +968 curLen = sqrt(pow(y - t.top - offset.top, 2) + pow(x - t.left - offset.left, 2)); +969 +970 target._scaling = true; +971 +972 if (!by) { +973 target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); +974 target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); +975 } +976 else if (by === 'x' && !target.lockUniScaling) { +977 target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); +978 } +979 else if (by === 'y' && !target.lockUniScaling) { +980 target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); +981 } +982 }, +983 +984 /** +985 * Rotates object by invoking its rotate method +986 * @method _rotateObject +987 * @param x {Number} pointer's x coordinate +988 * @param y {Number} pointer's y coordinate +989 */ +990 _rotateObject: function (x, y) { +991 +992 var t = this._currentTransform, +993 o = this._offset; +994 +995 if (t.target.lockRotation) return; +996 +997 var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left), +998 curAngle = atan2(y - t.top - o.top, x - t.left - o.left); +999 +1000 t.target.set('theta', (curAngle - lastAngle) + t.theta); +1001 }, +1002 +1003 /** +1004 * @method _setCursor +1005 */ +1006 _setCursor: function (value) { +1007 this.upperCanvasEl.style.cursor = value; +1008 }, +1009 +1010 /** +1011 * Sets the cursor depending on where the canvas is being hovered. +1012 * Note: very buggy in Opera +1013 * @method _setCursorFromEvent +1014 * @param e {Event} Event object +1015 * @param target {Object} Object that the mouse is hovering, if so. +1016 */ +1017 _setCursorFromEvent: function (e, target) { +1018 var s = this.upperCanvasEl.style; +1019 if (!target) { +1020 s.cursor = 'default'; +1021 return false; +1022 } +1023 else { +1024 var activeGroup = this.getActiveGroup(); +1025 // only show proper corner when group selection is not active +1026 var corner = !!target._findTargetCorner +1027 && (!activeGroup || !activeGroup.contains(target)) +1028 && target._findTargetCorner(e, this._offset); +1029 +1030 if (!corner) { +1031 s.cursor = this.HOVER_CURSOR; +1032 } +1033 else { +1034 if (corner in cursorMap) { +1035 s.cursor = cursorMap[corner]; +1036 } +1037 else { +1038 s.cursor = 'default'; +1039 return false; +1040 } +1041 } +1042 } +1043 return true; +1044 }, +1045 +1046 /** +1047 * Given a context, renders an object on that context +1048 * @param ctx {Object} context to render object on +1049 * @param object {Object} object to render +1050 * @private +1051 */ +1052 _draw: function (ctx, object) { +1053 object && object.render(ctx); +1054 }, +1055 +1056 /** +1057 * @method _drawSelection +1058 * @private +1059 */ +1060 _drawSelection: function () { +1061 var groupSelector = this._groupSelector, +1062 left = groupSelector.left, +1063 top = groupSelector.top, +1064 aleft = abs(left), +1065 atop = abs(top); +1066 +1067 this.contextTop.fillStyle = this.selectionColor; +1068 +1069 this.contextTop.fillRect( +1070 groupSelector.ex - ((left > 0) ? 0 : -left), +1071 groupSelector.ey - ((top > 0) ? 0 : -top), +1072 aleft, +1073 atop +1074 ); +1075 +1076 this.contextTop.lineWidth = this.selectionLineWidth; +1077 this.contextTop.strokeStyle = this.selectionBorderColor; +1078 +1079 this.contextTop.strokeRect( +1080 groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), +1081 groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), +1082 aleft, +1083 atop +1084 ); +1085 }, +1086 +1087 _findSelectedObjects: function (e) { +1088 var target, +1089 targetRegion, +1090 group = [ ], +1091 x1 = this._groupSelector.ex, +1092 y1 = this._groupSelector.ey, +1093 x2 = x1 + this._groupSelector.left, +1094 y2 = y1 + this._groupSelector.top, +1095 currentObject, +1096 selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), +1097 selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)); +1098 +1099 for (var i = 0, len = this._objects.length; i < len; ++i) { +1100 currentObject = this._objects[i]; +1101 +1102 if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || +1103 currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { +1104 +1105 if (this.selection && currentObject.selectable) { +1106 currentObject.setActive(true); +1107 group.push(currentObject); +1108 } +1109 } +1110 } +1111 +1112 // do not create group for 1 element only +1113 if (group.length === 1) { +1114 this.setActiveObject(group[0]); +1115 this.fire('object:selected', { +1116 target: group[0] +1117 }); +1118 } +1119 else if (group.length > 1) { +1120 var group = new fabric.Group(group); +1121 this.setActiveGroup(group); +1122 group.saveCoords(); +1123 this.fire('group:selected', { target: group }); +1124 } +1125 +1126 this.renderAll(); +1127 }, +1128 +1129 /** +1130 * Adds objects to canvas, then renders canvas; +1131 * Objects should be instances of (or inherit from) fabric.Object +1132 * @method add +1133 * @return {fabric.Canvas} thisArg +1134 * @chainable +1135 */ +1136 add: function () { +1137 this._objects.push.apply(this._objects, arguments); +1138 for (var i = arguments.length; i--; ) { +1139 this.stateful && arguments[i].setupState(); +1140 arguments[i].setCoords(); +1141 } +1142 this.renderOnAddition && this.renderAll(); +1143 return this; +1144 }, +1145 +1146 /** +1147 * Inserts an object to canvas at specified index and renders canvas. +1148 * An object should be an instance of (or inherit from) fabric.Object +1149 * @method insertAt +1150 * @param object {Object} Object to insert +1151 * @param index {Number} index to insert object at +1152 * @return {fabric.Canvas} instance +1153 */ +1154 insertAt: function (object, index) { +1155 this._objects.splice(index, 0, object); +1156 this.stateful && object.setupState(); +1157 object.setCoords(); +1158 this.renderAll(); +1159 return this; +1160 }, +1161 +1162 /** +1163 * Returns an array of objects this instance has +1164 * @method getObjects +1165 * @return {Array} +1166 */ +1167 getObjects: function () { +1168 return this._objects; +1169 }, +1170 +1171 /** +1172 * Returns topmost canvas context +1173 * @method getContext +1174 * @return {CanvasRenderingContext2D} +1175 */ +1176 getContext: function () { +1177 return this.contextTop; +1178 }, +1179 +1180 /** +1181 * Clears specified context of canvas element +1182 * @method clearContext +1183 * @param context {Object} ctx context to clear +1184 * @return {fabric.Canvas} thisArg +1185 * @chainable +1186 */ +1187 clearContext: function(ctx) { +1188 ctx.clearRect(0, 0, this.width, this.height); +1189 return this; +1190 }, +1191 +1192 /** +1193 * Clears all contexts (background, main, top) of an instance +1194 * @method clear +1195 * @return {fabric.Canvas} thisArg +1196 * @chainable +1197 */ +1198 clear: function () { +1199 this._objects.length = 0; +1200 this.clearContext(this.contextTop); +1201 this.clearContext(this.contextContainer); +1202 this.renderAll(); +1203 return this; +1204 }, +1205 +1206 /** +1207 * Renders both the top canvas and the secondary container canvas. +1208 * @method renderAll +1209 * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas +1210 * @return {fabric.Canvas} instance +1211 * @chainable +1212 */ +1213 renderAll: function (allOnTop) { +1214 +1215 var containerCanvas = this[allOnTop ? 'contextTop' : 'contextContainer']; +1216 +1217 this.clearContext(this.contextTop); +1218 +1219 if (!allOnTop) { +1220 this.clearContext(containerCanvas); +1221 } +1222 +1223 var length = this._objects.length, +1224 activeGroup = this.getActiveGroup(), +1225 startTime = new Date(); +1226 +1227 if (this.clipTo) { +1228 containerCanvas.save(); +1229 containerCanvas.beginPath(); +1230 this.clipTo(containerCanvas); +1231 containerCanvas.clip(); +1232 } +1233 +1234 containerCanvas.fillStyle = this.backgroundColor; +1235 containerCanvas.fillRect(0, 0, this.width, this.height); +1236 +1237 if (length) { +1238 for (var i = 0; i < length; ++i) { +1239 if (!activeGroup || +1240 (activeGroup && +1241 !activeGroup.contains(this._objects[i]))) { +1242 this._draw(containerCanvas, this._objects[i]); +1243 } +1244 } +1245 } +1246 +1247 if (this.clipTo) { +1248 containerCanvas.restore(); +1249 } +1250 +1251 // delegate rendering to group selection (if one exists) +1252 if (activeGroup) { +1253 this._draw(this.contextTop, activeGroup); +1254 } +1255 +1256 if (this.overlayImage) { +1257 this.contextTop.drawImage(this.overlayImage, 0, 0); +1258 } +1259 +1260 if (this.onFpsUpdate) { +1261 var elapsedTime = new Date() - startTime; +1262 this.onFpsUpdate(~~(1000 / elapsedTime)); +1263 } +1264 +1265 this.fire('after:render'); +1266 +1267 return this; +1268 }, +1269 +1270 /** +1271 * Method to render only the top canvas. +1272 * Also used to render the group selection box. +1273 * @method renderTop +1274 * @return {fabric.Canvas} thisArg +1275 * @chainable +1276 */ +1277 renderTop: function () { +1278 +1279 this.clearContext(this.contextTop); +1280 if (this.overlayImage) { +1281 this.contextTop.drawImage(this.overlayImage, 0, 0); +1282 } +1283 +1284 // we render the top context - last object +1285 if (this.selection && this._groupSelector) { +1286 this._drawSelection(); +1287 } +1288 +1289 // delegate rendering to group selection if one exists +1290 // used for drawing selection borders/corners +1291 var activeGroup = this.getActiveGroup(); +1292 if (activeGroup) { +1293 activeGroup.render(this.contextTop); +1294 } +1295 +1296 this.fire('after:render'); +1297 +1298 return this; +1299 }, +1300 +1301 /** +1302 * Applies one implementation of 'point inside polygon' algorithm +1303 * @method containsPoint +1304 * @param e { Event } event object +1305 * @param target { fabric.Object } object to test against +1306 * @return {Boolean} true if point contains within area of given object +1307 */ +1308 containsPoint: function (e, target) { +1309 var pointer = this.getPointer(e), +1310 xy = this._normalizePointer(target, pointer), +1311 x = xy.x, +1312 y = xy.y; +1313 +1314 // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html +1315 // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html +1316 +1317 // we iterate through each object. If target found, return it. +1318 var iLines = target._getImageLines(target.oCoords), +1319 xpoints = target._findCrossPoints(x, y, iLines); +1320 +1321 // if xcount is odd then we clicked inside the object +1322 // For the specific case of square images xcount === 1 in all true cases +1323 if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { +1324 return true; +1325 } +1326 return false; +1327 }, +1328 +1329 /** +1330 * @private +1331 * @method _normalizePointer +1332 */ +1333 _normalizePointer: function (object, pointer) { +1334 +1335 var activeGroup = this.getActiveGroup(), +1336 x = pointer.x, +1337 y = pointer.y; +1338 +1339 var isObjectInGroup = ( +1340 activeGroup && +1341 object.type !== 'group' && +1342 activeGroup.contains(object) +1343 ); +1344 +1345 if (isObjectInGroup) { +1346 x -= activeGroup.left; +1347 y -= activeGroup.top; +1348 } +1349 return { x: x, y: y }; +1350 }, +1351 +1352 /** +1353 * Method that determines what object we are clicking on +1354 * @method findTarget +1355 * @param {Event} e mouse event +1356 * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through +1357 */ +1358 findTarget: function (e, skipGroup) { +1359 +1360 var target, +1361 pointer = this.getPointer(e); +1362 +1363 // first check current group (if one exists) +1364 var activeGroup = this.getActiveGroup(); +1365 +1366 if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { +1367 target = activeGroup; +1368 return target; +1369 } +1370 +1371 // then check all of the objects on canvas +1372 for (var i = this._objects.length; i--; ) { +1373 if (this.containsPoint(e, this._objects[i])) { +1374 target = this._objects[i]; +1375 this.relatedTarget = target; +1376 break; +1377 } +1378 } +1379 if (this.selection && target && target.selectable) { +1380 return target; +1381 } +1382 }, +1383 +1384 /** +1385 * Exports canvas element to a dataurl image. +1386 * @method toDataURL +1387 * @param {String} format the format of the output image. Either "jpeg" or "png". +1388 * @return {String} +1389 */ +1390 toDataURL: function (format) { +1391 var data; +1392 if (!format) { +1393 format = 'png'; +1394 } +1395 if (format === 'jpeg' || format === 'png') { +1396 this.renderAll(true); +1397 data = this.upperCanvasEl.toDataURL('image/' + format); +1398 this.renderAll(); +1399 } +1400 return data; +1401 }, +1402 +1403 /** +1404 * Exports canvas element to a dataurl image (allowing to change image size via multiplier). +1405 * @method toDataURLWithMultiplier +1406 * @param {String} format (png|jpeg) +1407 * @param {Number} multiplier +1408 * @return {String} +1409 */ +1410 toDataURLWithMultiplier: function (format, multiplier) { +1411 +1412 var origWidth = this.getWidth(), +1413 origHeight = this.getHeight(), +1414 scaledWidth = origWidth * multiplier, +1415 scaledHeight = origHeight * multiplier, +1416 activeObject = this.getActiveObject(); +1417 +1418 this.setWidth(scaledWidth).setHeight(scaledHeight); +1419 this.contextTop.scale(multiplier, multiplier); +1420 +1421 if (activeObject) { +1422 this.deactivateAll().renderAll(); +1423 } +1424 var dataURL = this.toDataURL(format); +1425 +1426 this.contextTop.scale( 1 / multiplier, 1 / multiplier); +1427 this.setWidth(origWidth).setHeight(origHeight); +1428 +1429 if (activeObject) { +1430 this.setActiveObject(activeObject); +1431 } +1432 this.renderAll(); +1433 +1434 return dataURL; +1435 }, +1436 +1437 /** +1438 * Returns pointer coordinates relative to canvas. +1439 * @method getPointer +1440 * @return {Object} object with "x" and "y" number values +1441 */ +1442 getPointer: function (e) { +1443 var pointer = getPointer(e); +1444 return { +1445 x: pointer.x - this._offset.left, +1446 y: pointer.y - this._offset.top +1447 }; +1448 }, +1449 +1450 /** +1451 * Returns coordinates of a center of canvas. +1452 * Returned value is an object with top and left properties +1453 * @method getCenter +1454 * @return {Object} object with "top" and "left" number values +1455 */ +1456 getCenter: function () { +1457 return { +1458 top: this.getHeight() / 2, +1459 left: this.getWidth() / 2 +1460 }; +1461 }, +1462 +1463 /** +1464 * Centers object horizontally. +1465 * @method centerObjectH +1466 * @param {fabric.Object} object Object to center +1467 * @return {fabric.Canvas} thisArg +1468 */ +1469 centerObjectH: function (object) { +1470 object.set('left', this.getCenter().left); +1471 this.renderAll(); +1472 return this; +1473 }, +1474 +1475 /** +1476 * Centers object horizontally with animation. +1477 * @method fxCenterObjectH +1478 * @param {fabric.Object} object Object to center +1479 * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties +1480 * @return {fabric.Canvas} thisArg +1481 * @chainable +1482 */ +1483 fxCenterObjectH: function (object, callbacks) { +1484 callbacks = callbacks || { }; +1485 +1486 var empty = function() { }, +1487 onComplete = callbacks.onComplete || empty, +1488 onChange = callbacks.onChange || empty, +1489 _this = this; +1490 +1491 fabric.util.animate({ +1492 startValue: object.get('left'), +1493 endValue: this.getCenter().left, +1494 duration: this.FX_DURATION, +1495 onChange: function(value) { +1496 object.set('left', value); +1497 _this.renderAll(); +1498 onChange(); +1499 }, +1500 onComplete: function() { +1501 object.setCoords(); +1502 onComplete(); +1503 } +1504 }); +1505 +1506 return this; +1507 }, +1508 +1509 /** +1510 * Centers object vertically. +1511 * @method centerObjectH +1512 * @param {fabric.Object} object Object to center +1513 * @return {fabric.Canvas} thisArg +1514 * @chainable +1515 */ +1516 centerObjectV: function (object) { +1517 object.set('top', this.getCenter().top); +1518 this.renderAll(); +1519 return this; +1520 }, +1521 +1522 /** +1523 * Centers object vertically with animation. +1524 * @method fxCenterObjectV +1525 * @param {fabric.Object} object Object to center +1526 * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties +1527 * @return {fabric.Canvas} thisArg +1528 * @chainable +1529 */ +1530 fxCenterObjectV: function (object, callbacks) { +1531 callbacks = callbacks || { }; +1532 +1533 var empty = function() { }, +1534 onComplete = callbacks.onComplete || empty, +1535 onChange = callbacks.onChange || empty, +1536 _this = this; +1537 +1538 fabric.util.animate({ +1539 startValue: object.get('top'), +1540 endValue: this.getCenter().top, +1541 duration: this.FX_DURATION, +1542 onChange: function(value) { +1543 object.set('top', value); +1544 _this.renderAll(); +1545 onChange(); +1546 }, +1547 onComplete: function() { +1548 object.setCoords(); +1549 onComplete(); +1550 } +1551 }); +1552 +1553 return this; +1554 }, +1555 +1556 /** +1557 * Straightens object, then rerenders canvas +1558 * @method straightenObject +1559 * @param {fabric.Object} object Object to straighten +1560 * @return {fabric.Canvas} thisArg +1561 * @chainable +1562 */ +1563 straightenObject: function (object) { +1564 object.straighten(); +1565 this.renderAll(); +1566 return this; +1567 }, +1568 +1569 /** +1570 * Same as `fabric.Canvas#straightenObject`, but animated +1571 * @method fxStraightenObject +1572 * @param {fabric.Object} object Object to straighten +1573 * @return {fabric.Canvas} thisArg +1574 * @chainable +1575 */ +1576 fxStraightenObject: function (object) { +1577 object.fxStraighten({ +1578 onChange: this.renderAll.bind(this) +1579 }); +1580 return this; +1581 }, +1582 +1583 /** +1584 * Returs dataless JSON representation of canvas +1585 * @method toDatalessJSON +1586 * @return {String} json string +1587 */ +1588 toDatalessJSON: function () { +1589 return this.toDatalessObject(); +1590 }, +1591 +1592 /** +1593 * Returns object representation of canvas +1594 * @method toObject +1595 * @return {Object} +1596 */ +1597 toObject: function () { +1598 return this._toObjectMethod('toObject'); +1599 }, +1600 +1601 /** +1602 * Returns dataless object representation of canvas +1603 * @method toDatalessObject +1604 * @return {Object} +1605 */ +1606 toDatalessObject: function () { +1607 return this._toObjectMethod('toDatalessObject'); +1608 }, +1609 +1610 /** +1611 * @private +1612 * @method _toObjectMethod +1613 */ +1614 _toObjectMethod: function (methodName) { +1615 return { +1616 objects: this._objects.map(function (instance){ +1617 // TODO (kangax): figure out how to clean this up +1618 if (!this.includeDefaultValues) { +1619 var originalValue = instance.includeDefaultValues; +1620 instance.includeDefaultValues = false; +1621 } +1622 var object = instance[methodName](); +1623 if (!this.includeDefaultValues) { +1624 instance.includeDefaultValues = originalValue; +1625 } +1626 return object; +1627 }, this), +1628 background: this.backgroundColor +1629 } +1630 }, +1631 +1632 /** +1633 * Returns true if canvas contains no objects +1634 * @method isEmpty +1635 * @return {Boolean} true if canvas is empty +1636 */ +1637 isEmpty: function () { +1638 return this._objects.length === 0; +1639 }, +1640 +1641 /** +1642 * Populates canvas with data from the specified JSON +1643 * JSON format must conform to the one of `fabric.Canvas#toJSON` +1644 * @method loadFromJSON +1645 * @param {String} json JSON string +1646 * @param {Function} callback Callback, invoked when json is parsed +1647 * and corresponding objects (e.g: fabric.Image) +1648 * are initialized +1649 * @return {fabric.Canvas} instance +1650 * @chainable +1651 */ +1652 loadFromJSON: function (json, callback) { +1653 if (!json) return; +1654 +1655 var serialized = JSON.parse(json); +1656 if (!serialized || (serialized && !serialized.objects)) return; +1657 +1658 this.clear(); +1659 var _this = this; +1660 this._enlivenObjects(serialized.objects, function () { +1661 _this.backgroundColor = serialized.background; +1662 if (callback) { +1663 callback(); +1664 } +1665 }); +1666 +1667 return this; +1668 }, +1669 +1670 /** +1671 * @method _enlivenObjects +1672 * @param {Array} objects +1673 * @param {Function} callback +1674 */ +1675 _enlivenObjects: function (objects, callback) { +1676 var numLoadedImages = 0, +1677 // get length of all images +1678 numTotalImages = objects.filter(function (o) { +1679 return o.type === 'image'; +1680 }).length; +1681 +1682 var _this = this; +1683 +1684 objects.forEach(function (o, index) { +1685 if (!o.type) { +1686 return; +1687 } +1688 switch (o.type) { +1689 case 'image': +1690 case 'font': +1691 fabric[capitalize(o.type)].fromObject(o, function (o) { +1692 _this.insertAt(o, index); +1693 if (++numLoadedImages === numTotalImages) { +1694 if (callback) { +1695 callback(); +1696 } +1697 } +1698 }); +1699 break; +1700 default: +1701 var klass = fabric[camelize(capitalize(o.type))]; +1702 if (klass && klass.fromObject) { +1703 _this.insertAt(klass.fromObject(o), index); +1704 } +1705 break; +1706 } +1707 }); +1708 +1709 if (numTotalImages === 0 && callback) { +1710 callback(); +1711 } +1712 }, +1713 +1714 /** +1715 * Populates canvas with data from the specified dataless JSON +1716 * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` +1717 * @method loadFromDatalessJSON +1718 * @param {String} json JSON string +1719 * @param {Function} callback Callback, invoked when json is parsed +1720 * and corresponding objects (e.g: fabric.Image) +1721 * are initialized +1722 * @return {fabric.Canvas} instance +1723 * @chainable +1724 */ +1725 loadFromDatalessJSON: function (json, callback) { +1726 +1727 if (!json) { +1728 return; +1729 } +1730 +1731 // serialize if it wasn't already +1732 var serialized = (typeof json === 'string') +1733 ? JSON.parse(json) +1734 : json; +1735 +1736 if (!serialized || (serialized && !serialized.objects)) return; +1737 +1738 this.clear(); +1739 +1740 // TODO: test this +1741 this.backgroundColor = serialized.background; +1742 this._enlivenDatalessObjects(serialized.objects, callback); +1743 }, +1744 +1745 /** +1746 * @method _enlivenDatalessObjects +1747 * @param {Array} objects +1748 * @param {Function} callback +1749 */ +1750 _enlivenDatalessObjects: function (objects, callback) { +1751 +1752 /** @ignore */ +1753 function onObjectLoaded(object, index) { +1754 _this.insertAt(object, index); +1755 object.setCoords(); +1756 if (++numLoadedObjects === numTotalObjects) { +1757 callback && callback(); +1758 } +1759 } +1760 +1761 var _this = this, +1762 numLoadedObjects = 0, +1763 numTotalObjects = objects.length; +1764 +1765 if (numTotalObjects === 0 && callback) { +1766 callback(); +1767 } +1768 +1769 try { +1770 objects.forEach(function (obj, index) { +1771 +1772 var pathProp = obj.paths ? 'paths' : 'path'; +1773 var path = obj[pathProp]; +1774 +1775 delete obj[pathProp]; +1776 +1777 if (typeof path !== 'string') { +1778 switch (obj.type) { +1779 case 'image': +1780 case 'text': +1781 fabric[capitalize(obj.type)].fromObject(obj, function (o) { +1782 onObjectLoaded(o, index); +1783 }); +1784 break; +1785 default: +1786 var klass = fabric[camelize(capitalize(obj.type))]; +1787 if (klass && klass.fromObject) { +1788 // restore path +1789 if (path) { +1790 obj[pathProp] = path; +1791 } +1792 onObjectLoaded(klass.fromObject(obj), index); +1793 } +1794 break; +1795 } +1796 } +1797 else { +1798 if (obj.type === 'image') { +1799 _this.loadImageFromURL(path, function (image) { +1800 image.setSourcePath(path); +1801 +1802 extend(image, obj); +1803 image.setAngle(obj.angle); +1804 +1805 onObjectLoaded(image, index); +1806 }); +1807 } +1808 else if (obj.type === 'text') { +1809 +1810 obj.path = path; +1811 var object = fabric.Text.fromObject(obj); +1812 var onscriptload = function () { +1813 // TODO (kangax): find out why Opera refuses to work without this timeout +1814 if (Object.prototype.toString.call(window.opera) === '[object Opera]') { +1815 setTimeout(function () { +1816 onObjectLoaded(object, index); +1817 }, 500); +1818 } +1819 else { +1820 onObjectLoaded(object, index); +1821 } +1822 } +1823 +1824 fabric.util.getScript(path, onscriptload); +1825 } +1826 else { +1827 _this.loadSVGFromURL(path, function (elements, options) { +1828 if (elements.length > 1) { +1829 var object = new fabric.PathGroup(elements, obj); +1830 } +1831 else { +1832 var object = elements[0]; +1833 } +1834 object.setSourcePath(path); +1835 +1836 // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) +1837 // skip this step if an object is a PathGroup, since we already passed it options object before +1838 if (!(object instanceof fabric.PathGroup)) { +1839 extend(object, obj); +1840 if (typeof obj.angle !== 'undefined') { +1841 object.setAngle(obj.angle); +1842 } +1843 } +1844 +1845 onObjectLoaded(object, index); +1846 }); +1847 } +1848 } +1849 }, this); +1850 } +1851 catch(e) { +1852 fabric.log(e.message); +1853 } +1854 }, +1855 +1856 /** +1857 * Loads an image from URL, creates an instance of fabric.Image and passes it to a callback +1858 * @function +1859 * @method loadImageFromURL +1860 * @param url {String} url of image to load +1861 * @param callback {Function} calback, invoked when image is loaded +1862 */ +1863 loadImageFromURL: (function () { +1864 var imgCache = { }; +1865 +1866 return function (url, callback) { +1867 // check cache first +1868 +1869 var _this = this; +1870 +1871 function checkIfLoaded() { +1872 var imgEl = document.getElementById(imgCache[url]); +1873 if (imgEl.width && imgEl.height) { +1874 callback(new fabric.Image(imgEl)); +1875 } +1876 else { +1877 setTimeout(checkIfLoaded, 50); +1878 } +1879 } +1880 +1881 // get by id from cache +1882 if (imgCache[url]) { +1883 // id can be cached but image might still not be loaded, so we poll here +1884 checkIfLoaded(); +1885 } +1886 // else append a new image element +1887 else { +1888 var imgEl = new Image(); +1889 +1890 /** @ignore */ +1891 imgEl.onload = function () { +1892 imgEl.onload = null; +1893 +1894 if (imgEl.width && imgEl.height) { +1895 callback(new fabric.Image(imgEl)); +1896 } +1897 }; +1898 +1899 imgEl.className = 'canvas-img-clone'; +1900 imgEl.style.cssText = 'position:absolute;left:-9999px;top:-9999px;'; +1901 imgEl.src = url; +1902 +1903 if (this.shouldCacheImages) { +1904 // TODO (kangax): replace Element.identify w. fabric -based alternative +1905 imgCache[url] = Element.identify(imgEl); +1906 } +1907 document.body.appendChild(imgEl); +1908 } +1909 } +1910 })(), +1911 +1912 /** +1913 * Takes url corresponding to an SVG document, and parses it to a set of objects +1914 * @method loadSVGFromURL +1915 * @param {String} url +1916 * @param {Function} callback +1917 */ +1918 loadSVGFromURL: function (url, callback) { +1919 +1920 var _this = this; +1921 +1922 url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); +1923 +1924 this.cache.has(url, function (hasUrl) { +1925 if (hasUrl) { +1926 _this.cache.get(url, function (value) { +1927 var enlivedRecord = _this._enlivenCachedObject(value); +1928 callback(enlivedRecord.objects, enlivedRecord.options); +1929 }); +1930 } +1931 else { +1932 new fabric.util.request(url, { +1933 method: 'get', +1934 onComplete: onComplete +1935 }); +1936 } +1937 }); +1938 +1939 function onComplete(r) { +1940 +1941 var xml = r.responseXML; +1942 if (!xml) return; +1943 +1944 var doc = xml.documentElement; +1945 if (!doc) return; +1946 +1947 fabric.parseSVGDocument(doc, function (results, options) { +1948 _this.cache.set(url, { +1949 objects: fabric.util.array.invoke(results, 'toObject'), +1950 options: options +1951 }); +1952 callback(results, options); +1953 }); +1954 } +1955 }, +1956 +1957 /** +1958 * @method _enlivenCachedObject +1959 */ +1960 _enlivenCachedObject: function (cachedObject) { +1961 +1962 var objects = cachedObject.objects, +1963 options = cachedObject.options; +1964 +1965 objects = objects.map(function (o) { +1966 return fabric[capitalize(o.type)].fromObject(o); +1967 }); +1968 +1969 return ({ objects: objects, options: options }); +1970 }, +1971 +1972 /** +1973 * Removes an object from canvas and returns it +1974 * @method remove +1975 * @param object {Object} Object to remove +1976 * @return {Object} removed object +1977 */ +1978 remove: function (object) { +1979 removeFromArray(this._objects, object); +1980 if (this.getActiveObject() === object) { +1981 this.removeActiveObject(); +1982 } +1983 this.renderAll(); +1984 return object; +1985 }, +1986 +1987 /** +1988 * Same as `fabric.Canvas#remove` but animated +1989 * @method fxRemove +1990 * @param {fabric.Object} object Object to remove +1991 * @param {Function} callback Callback, invoked on effect completion +1992 * @return {fabric.Canvas} thisArg +1993 * @chainable +1994 */ +1995 fxRemove: function (object, callback) { +1996 var _this = this; +1997 object.fxRemove({ +1998 onChange: this.renderAll.bind(this), +1999 onComplete: function () { +2000 _this.remove(object); +2001 if (typeof callback === 'function') { +2002 callback(); +2003 } +2004 } +2005 }); +2006 return this; +2007 }, +2008 +2009 /** +2010 * Moves an object to the bottom of the stack of drawn objects +2011 * @method sendToBack +2012 * @param object {fabric.Object} Object to send to back +2013 * @return {fabric.Canvas} thisArg +2014 * @chainable +2015 */ +2016 sendToBack: function (object) { +2017 removeFromArray(this._objects, object); +2018 this._objects.unshift(object); +2019 return this.renderAll(); +2020 }, +2021 +2022 /** +2023 * Moves an object to the top of the stack of drawn objects +2024 * @method bringToFront +2025 * @param object {fabric.Object} Object to send +2026 * @return {fabric.Canvas} thisArg +2027 * @chainable +2028 */ +2029 bringToFront: function (object) { +2030 removeFromArray(this._objects, object); +2031 this._objects.push(object); +2032 return this.renderAll(); +2033 }, +2034 +2035 /** +2036 * Moves an object one level down in stack of drawn objects +2037 * @method sendBackwards +2038 * @param object {fabric.Object} Object to send +2039 * @return {fabric.Canvas} thisArg +2040 * @chainable +2041 */ +2042 sendBackwards: function (object) { +2043 var idx = this._objects.indexOf(object), +2044 nextIntersectingIdx = idx; +2045 +2046 // if object is not on the bottom of stack +2047 if (idx !== 0) { +2048 +2049 // traverse down the stack looking for the nearest intersecting object +2050 for (var i=idx-1; i>=0; --i) { +2051 if (object.intersectsWithObject(this._objects[i])) { +2052 nextIntersectingIdx = i; +2053 break; +2054 } +2055 } +2056 removeFromArray(this._objects, object); +2057 this._objects.splice(nextIntersectingIdx, 0, object); +2058 } +2059 return this.renderAll(); +2060 }, +2061 +2062 /** +2063 * Moves an object one level up in stack of drawn objects +2064 * @method sendForward +2065 * @param object {fabric.Object} Object to send +2066 * @return {fabric.Canvas} thisArg +2067 * @chainable +2068 */ +2069 bringForward: function (object) { +2070 var objects = this.getObjects(), +2071 idx = objects.indexOf(object), +2072 nextIntersectingIdx = idx; +2073 +2074 +2075 // if object is not on top of stack (last item in an array) +2076 if (idx !== objects.length-1) { +2077 +2078 // traverse up the stack looking for the nearest intersecting object +2079 for (var i = idx + 1, l = this._objects.length; i < l; ++i) { +2080 if (object.intersectsWithObject(objects[i])) { +2081 nextIntersectingIdx = i; +2082 break; +2083 } +2084 } +2085 removeFromArray(objects, object); +2086 objects.splice(nextIntersectingIdx, 0, object); +2087 } +2088 this.renderAll(); +2089 }, +2090 +2091 /** +2092 * Sets given object as active +2093 * @method setActiveObject +2094 * @param object {fabric.Object} Object to set as an active one +2095 * @return {fabric.Canvas} thisArg +2096 * @chainable +2097 */ +2098 setActiveObject: function (object) { +2099 if (this._activeObject) { +2100 this._activeObject.setActive(false); +2101 } +2102 this._activeObject = object; +2103 object.setActive(true); +2104 +2105 this.renderAll(); +2106 +2107 this.fire('object:selected', { target: object }); +2108 return this; +2109 }, +2110 +2111 /** +2112 * Returns currently active object +2113 * @method getActiveObject +2114 * @return {fabric.Object} active object +2115 */ +2116 getActiveObject: function () { +2117 return this._activeObject; +2118 }, +2119 +2120 /** +2121 * Removes currently active object +2122 * @method removeActiveObject +2123 * @return {fabric.Canvas} thisArg +2124 * @chainable +2125 */ +2126 removeActiveObject: function () { +2127 if (this._activeObject) { +2128 this._activeObject.setActive(false); +2129 } +2130 this._activeObject = null; +2131 return this; +2132 }, +2133 +2134 /** +2135 * Sets active group to a speicified one +2136 * @method setActiveGroup +2137 * @param {fabric.Group} group Group to set as a current one +2138 * @return {fabric.Canvas} thisArg +2139 * @chainable +2140 */ +2141 setActiveGroup: function (group) { +2142 this._activeGroup = group; +2143 return this; +2144 }, +2145 +2146 /** +2147 * Returns currently active group +2148 * @method getActiveGroup +2149 * @return {fabric.Group} Current group +2150 */ +2151 getActiveGroup: function () { +2152 return this._activeGroup; +2153 }, +2154 +2155 /** +2156 * Removes currently active group +2157 * @method removeActiveGroup +2158 * @return {fabric.Canvas} thisArg +2159 */ +2160 removeActiveGroup: function () { +2161 var g = this.getActiveGroup(); +2162 if (g) { +2163 g.destroy(); +2164 } +2165 return this.setActiveGroup(null); +2166 }, +2167 +2168 /** +2169 * Returns object at specified index +2170 * @method item +2171 * @param {Number} index +2172 * @return {fabric.Object} +2173 */ +2174 item: function (index) { +2175 return this.getObjects()[index]; +2176 }, +2177 +2178 /** +2179 * Deactivates all objects by calling their setActive(false) +2180 * @method deactivateAll +2181 * @return {fabric.Canvas} thisArg +2182 */ +2183 deactivateAll: function () { +2184 var allObjects = this.getObjects(), +2185 i = 0, +2186 len = allObjects.length; +2187 for ( ; i < len; i++) { +2188 allObjects[i].setActive(false); +2189 } +2190 this.removeActiveGroup(); +2191 this.removeActiveObject(); +2192 return this; +2193 }, +2194 +2195 /** +2196 * Returns number representation of an instance complexity +2197 * @method complexity +2198 * @return {Number} complexity +2199 */ +2200 complexity: function () { +2201 return this.getObjects().reduce(function (memo, current) { +2202 memo += current.complexity ? current.complexity() : 0; +2203 return memo; +2204 }, 0); +2205 }, +2206 +2207 /** +2208 * Iterates over all objects, invoking callback for each one of them +2209 * @method forEachObject +2210 * @return {fabric.Canvas} thisArg +2211 */ +2212 forEachObject: function(callback, context) { +2213 var objects = this.getObjects(), +2214 i = objects.length; +2215 while (i--) { +2216 callback.call(context, objects[i], i, objects); +2217 } +2218 return this; +2219 }, +2220 +2221 /** +2222 * Clears a canvas element and removes all event handlers. +2223 * @method dispose +2224 * @return {fabric.Canvas} thisArg +2225 * @chainable +2226 */ +2227 dispose: function () { +2228 this.clear(); +2229 removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); +2230 removeListener(document, 'mousemove', this._onMouseMove); +2231 removeListener(window, 'resize', this._onResize); +2232 return this; +2233 }, +2234 +2235 /** +2236 * Clones canvas instance +2237 * @method clone +2238 * @param {Object} [callback] Expects `onBeforeClone` and `onAfterClone` functions +2239 * @return {fabric.Canvas} Clone of this instance +2240 */ +2241 clone: function (callback) { +2242 var el = document.createElement('canvas'); +2243 +2244 el.width = this.getWidth(); +2245 el.height = this.getHeight(); +2246 +2247 // cache +2248 var clone = this.__clone || (this.__clone = new fabric.Canvas(el)); +2249 clone.clipTo = this.clipTo; +2250 +2251 return clone.loadFromJSON(JSON.stringify(this.toJSON()), function () { +2252 if (callback) { +2253 callback(clone); +2254 } +2255 }); +2256 }, +2257 +2258 /** +2259 * @private +2260 * @method _toDataURL +2261 * @param {String} format +2262 * @param {Function} callback +2263 */ +2264 _toDataURL: function (format, callback) { +2265 this.clone(function (clone) { +2266 callback(clone.toDataURL(format)); +2267 }); +2268 }, +2269 +2270 /** +2271 * @private +2272 * @method _toDataURLWithMultiplier +2273 * @param {String} format +2274 * @param {Number} multiplier +2275 * @param {Function} callback +2276 */ +2277 _toDataURLWithMultiplier: function (format, multiplier, callback) { +2278 this.clone(function (clone) { +2279 callback(clone.toDataURLWithMultiplier(format, multiplier)); +2280 }); +2281 }, +2282 +2283 /** +2284 * @private +2285 * @method _resizeImageToFit +2286 * @param {HTMLImageElement} imgEl +2287 */ +2288 _resizeImageToFit: function (imgEl) { +2289 +2290 var imageWidth = imgEl.width || imgEl.offsetWidth, +2291 widthScaleFactor = this.getWidth() / imageWidth; +2292 +2293 // scale image down so that it has original dimensions when printed in large resolution +2294 if (imageWidth) { +2295 imgEl.width = imageWidth * widthScaleFactor; +2296 } +2297 }, +2298 +2299 /** +2300 * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) +2301 * @property +2302 * @namespace +2303 */ +2304 cache: { +2305 +2306 /** +2307 * @method has +2308 * @param {String} name +2309 * @param {Function} callback +2310 */ +2311 has: function (name, callback) { +2312 callback(false); +2313 }, +2314 +2315 /** +2316 * @method get +2317 * @param {String} url +2318 * @param {Function} callback +2319 */ +2320 get: function (url, callback) { +2321 /* NOOP */ +2322 }, +2323 +2324 /** +2325 * @method set +2326 * @param {String} url +2327 * @param {Object} object +2328 */ +2329 set: function (url, object) { +2330 /* NOOP */ +2331 } +2332 } +2333 }); +2334 +2335 /** +2336 * Returns a string representation of an instance +2337 * @method toString +2338 * @return {String} string representation of an instance +2339 */ +2340 fabric.Canvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet +2341 return '#<fabric.Canvas (' + this.complexity() + '): '+ +2342 '{ objects: ' + this.getObjects().length + ' }>'; +2343 }; +2344 +2345 extend(fabric.Canvas, /** @scope fabric.Canvas */ { +2346 +2347 /** +2348 * @static +2349 * @property EMPTY_JSON +2350 * @type String +2351 */ +2352 EMPTY_JSON: '{"objects": [], "background": "white"}', +2353 +2354 /** +2355 * Takes <canvas> element and transforms its data in such way that it becomes grayscale +2356 * @static +2357 * @method toGrayscale +2358 * @param {HTMLCanvasElement} canvasEl +2359 */ +2360 toGrayscale: function (canvasEl) { +2361 var context = canvasEl.getContext('2d'), +2362 imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), +2363 data = imageData.data, +2364 iLen = imageData.width, +2365 jLen = imageData.height, +2366 index, average, i, j; +2367 +2368 for (i = 0; i < iLen; i++) { +2369 for (j = 0; j < jLen; j++) { +2370 +2371 index = (i * 4) * jLen + (j * 4); +2372 average = (data[index] + data[index + 1] + data[index + 2]) / 3; +2373 +2374 data[index] = average; +2375 data[index + 1] = average; +2376 data[index + 2] = average; +2377 } +2378 } +2379 +2380 context.putImageData(imageData, 0, 0); +2381 }, +2382 +2383 /** +2384 * Provides a way to check support of some of the canvas methods +2385 * (either those of HTMLCanvasElement itself, or rendering context) +2386 * +2387 * @method supports +2388 * @param methodName {String} Method to check support for; +2389 * Could be one of "getImageData" or "toDataURL" +2390 * @return {Boolean | null} `true` if method is supported (or at least exists), +2391 * `null` if canvas element or context can not be initialized +2392 */ +2393 supports: function (methodName) { +2394 var el = document.createElement('canvas'); +2395 +2396 if (typeof G_vmlCanvasManager !== 'undefined') { +2397 G_vmlCanvasManager.initElement(el); +2398 } +2399 if (!el || !el.getContext) { +2400 return null; +2401 } +2402 +2403 var ctx = el.getContext('2d'); +2404 if (!ctx) { +2405 return null; +2406 } +2407 +2408 switch (methodName) { +2409 +2410 case 'getImageData': +2411 return typeof ctx.getImageData !== 'undefined'; +2412 +2413 case 'toDataURL': +2414 return typeof el.toDataURL !== 'undefined'; +2415 +2416 default: +2417 return null; +2418 } +2419 } +2420 }); +2421 +2422 /** +2423 * Returs JSON representation of canvas +2424 * @function +2425 * @method toJSON +2426 * @return {String} json string +2427 */ +2428 fabric.Canvas.prototype.toJSON = fabric.Canvas.prototype.toObject; +2429 +2430 /** +2431 * @class fabric.Element +2432 * @alias fabric.Canvas +2433 * @deprecated +2434 * @constructor +2435 */ +2436 fabric.Element = fabric.Canvas; +2437 +2438 })(this);\ No newline at end of file diff --git a/src/util/dom_misc.js b/src/util/dom_misc.js index fb42e6b7..c033ec7b 100644 --- a/src/util/dom_misc.js +++ b/src/util/dom_misc.js @@ -197,53 +197,9 @@ function getElementOffset(element) { } })(); -/** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @method animate - * @memberOf fabric.util - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Number} [options.startValue=0] Starting value - * @param {Number} [options.endValue=100] Ending value - * @param {Function} [options.easing] Easing function - * @param {Number} [options.duration=500] Duration of change - */ -function animate(options) { - - options || (options = { }); - - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, pos, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; }, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - isReversed = startValue > endValue; - - options.onStart && options.onStart(); - - var interval = setInterval(function() { - time = +new Date(); - pos = time > finish ? 1 : (time - start) / duration; - onChange(isReversed - ? (startValue - (startValue - endValue) * easing(pos)) - : (startValue + (endValue - startValue) * easing(pos))); - if (time > finish || abort()) { - clearInterval(interval); - options.onComplete && options.onComplete(); - } - }, 10); - - return interval; -} - fabric.util.getById = getById; fabric.util.toArray = toArray; fabric.util.makeElement = makeElement; fabric.util.addClass = addClass; fabric.util.wrapElement = wrapElement; -fabric.util.getElementOffset = getElementOffset; -fabric.util.animate = animate; \ No newline at end of file +fabric.util.getElementOffset = getElementOffset; \ No newline at end of file diff --git a/src/util/misc.js b/src/util/misc.js index eb6fa78d..d2b21040 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -68,10 +68,54 @@ function falseFunction() { return false; } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @method animate + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + options || (options = { }); + + var start = +new Date(), + duration = options.duration || 500, + finish = start + duration, time, pos, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; }, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + isReversed = startValue > endValue; + + options.onStart && options.onStart(); + + var interval = setInterval(function() { + time = +new Date(); + pos = time > finish ? 1 : (time - start) / duration; + onChange(isReversed + ? (startValue - (startValue - endValue) * easing(pos)) + : (startValue + (endValue - startValue) * easing(pos))); + if (time > finish || abort()) { + clearInterval(interval); + options.onComplete && options.onComplete(); + } + }, 10); + + return interval; + } fabric.util.removeFromArray = removeFromArray; fabric.util.degreesToRadians = degreesToRadians; fabric.util.toFixed = toFixed; fabric.util.getRandomInt = getRandomInt; fabric.util.falseFunction = falseFunction; + fabric.util.animate = animate; })(); \ No newline at end of file