fabric.js/src/element.class.js

2374 lines
No EOL
67 KiB
JavaScript

(function () {
if (fabric.Element) {
fabric.warn('fabric.Element is already defined.');
return;
}
var global = this,
window = global.window,
document = window.document,
// aliases for faster resolution
extend = fabric.util.object.extend,
capitalize = fabric.util.string.capitalize,
camelize = fabric.util.string.camelize,
fireEvent = fabric.util.fireEvent,
getPointer = fabric.util.getPointer,
getElementOffset = fabric.util.getElementOffset,
removeFromArray = fabric.util.removeFromArray,
addListener = fabric.util.addListener,
removeListener = fabric.util.removeListener,
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,
CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'),
FX_DURATION = 500,
STROKE_OFFSET = 0.5,
FX_TRANSITION = 'decel',
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'
};
/**
* @class fabric.Element
* @constructor
* @param {HTMLElement | String} el <canvas> element to initialize instance on
* @param {Object} [options] Options object
*/
fabric.Element = function (el, options) {
/**
* The object literal containing mouse position if clicked in an empty area (no image)
* @property _groupSelector
* @type object
*/
this._groupSelector = null;
/**
* The array literal containing all objects on canvas
* @property _objects
* @type array
*/
this._objects = [];
/**
* The element that references the canvas interface implementation
* @property _context
* @type object
*/
this._context = null;
/**
* The main element that contains the canvas
* @property _element
* @type object
*/
this._element = null;
/**
* The object literal containing the current x,y params of the transformation
* @property _currentTransform
* @type object
*/
this._currentTransform = null;
/**
* References instance of fabric.Group - when multiple objects are selected
* @property _activeGroup
* @type object
*/
this._activeGroup = null;
/**
* X coordinates of a path, captured during free drawing
*/
this._freeDrawingXPoints = [ ];
/**
* Y coordinates of a path, captured during free drawing
*/
this._freeDrawingYPoints = [ ];
/**
* An object containing config parameters
* @property _config
* @type object
*/
this._config = {
width: 300,
height: 150
};
options = options || { };
this._initElement(el);
this._initConfig(options);
if (options.overlayImage) {
this.setOverlayImage(options.overlayImage);
}
if (options.afterRender) {
this.afterRender = options.afterRender;
}
this._createCanvasBackground();
this._createCanvasContainer();
this._initEvents();
this.calcOffset();
};
extend(fabric.Element.prototype, /** @scope fabric.Element.prototype */ {
/**
* @property
* @type String
*/
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
/**
* @property
* @type String
*/
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
/**
* @property
* @type String
*/
freeDrawingColor: 'rgb(0, 0, 0)',
/**
* @property
* @type String
*/
backgroundColor: 'rgba(0, 0, 0, 0)',
/**
* @property
* @type Number
*/
freeDrawingLineWidth: 1,
/**
* @property
* @type Number
*/
selectionLineWidth: 1,
/**
* @property
* @type Boolean
*/
includeDefaultValues: true,
/**
* @property
* @type Boolean
*/
shouldCacheImages: false,
/**
* @constant
* @type Number
*/
CANVAS_WIDTH: 600,
/**
* @constant
* @type Number
*/
CANVAS_HEIGHT: 600,
/**
* Callback; invoked right before object is about to be scaled/rotated
* @method onBeforeScaleRotate
* @param {fabric.Object} target Object that's about to be scaled/rotated
*/
onBeforeScaleRotate: function (target) {
/* NOOP */
},
/**
* Callback; invoked on every redraw of canvas and is being passed a number indicating current fps
* @method onFpsUpdate
* @param {Number} fps
*/
onFpsUpdate: function(fps) {
/* NOOP */
},
/**
* Calculates canvas element offset relative to the document
* This method is also attached as "resize" event handler of window
* @method calcOffset
* @return {fabric.Element} instance
* @chainable
*/
calcOffset: function () {
this._offset = getElementOffset(this.getElement());
return this;
},
/**
* Sets overlay image for this canvas
* @method setOverlayImage
* @param {String} url url of an image to set background to
* @param {Function} callback callback to invoke when image is loaded and set as an overlay one
* @return {fabric.Element} thisArg
* @chainable
*/
setOverlayImage: function (url, callback) { // TODO (kangax): test callback
if (url) {
var _this = this, img = new Image();
/** @ignore */
img.onload = function () {
_this.overlayImage = img;
if (callback) {
callback();
}
img = img.onload = null;
};
img.src = url;
}
return this;
},
/**
* Canvas class' initialization method; Automatically called by constructor;
* Sets up all DOM references for pre-existing markup and creates required markup if it's not yet created.
* already present.
* @method _initElement
* @param {HTMLElement|String} canvasEl Canvas element
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
*/
_initElement: function (canvasEl) {
var el = fabric.util.getById(canvasEl);
this._element = el || document.createElement('canvas');
if (typeof this._element.getContext === 'undefined' && typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(this._element);
}
if (typeof this._element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
if (!(this.contextTop = this._element.getContext('2d'))) {
throw CANVAS_INIT_ERROR;
}
var width = this._element.width || 0,
height = this._element.height || 0;
this._initWrapperElement(width, height);
this._setElementStyle(width, height);
},
/**
* @private
* @method _initWrapperElement
* @param {Number} width
* @param {Number} height
*/
_initWrapperElement: function (width, height) {
var wrapper = fabric.util.wrapElement(this.getElement(), 'div', { 'class': 'canvas_container' });
fabric.util.setStyle(wrapper, {
width: width + 'px',
height: height + 'px'
});
fabric.util.makeElementUnselectable(wrapper);
this.wrapper = wrapper;
},
/**
* @private
* @method _setElementStyle
* @param {Number} width
* @param {Number} height
*/
_setElementStyle: function (width, height) {
fabric.util.setStyle(this.getElement(), {
position: 'absolute',
width: width + 'px',
height: height + 'px',
left: 0,
top: 0
});
},
/**
* For now, use an object literal without methods to store the config params
* @method _initConfig
* @param config {Object} userConfig The configuration Object literal
* containing the configuration that should be set for this module;
* See configuration documentation for more details.
*/
_initConfig: function (config) {
extend(this._config, config || { });
this._config.width = parseInt(this._element.width, 10) || 0;
this._config.height = parseInt(this._element.height, 10) || 0;
this._element.style.width = this._config.width + 'px';
this._element.style.height = this._config.height + 'px';
},
/**
* 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); };
this._onMouseUp = function (e) { _this.__onMouseUp(e); };
this._onMouseMove = function (e) { _this.__onMouseMove(e); };
this._onResize = function (e) { _this.calcOffset() };
addListener(this._element, 'mousedown', this._onMouseDown);
addListener(document, 'mousemove', this._onMouseMove);
addListener(document, 'mouseup', this._onMouseUp);
addListener(window, 'resize', this._onResize);
},
/**
* Creates canvas elements
* @method _createCanvasElement
* @private
*/
_createCanvasElement: function (className) {
var element = document.createElement('canvas');
if (!element) {
return;
}
element.className = className;
var oContainer = this._element.parentNode.insertBefore(element, this._element);
oContainer.width = this.getWidth();
oContainer.height = this.getHeight();
oContainer.style.width = this.getWidth() + 'px';
oContainer.style.height = this.getHeight() + 'px';
oContainer.style.position = 'absolute';
oContainer.style.left = 0;
oContainer.style.top = 0;
if (typeof element.getContext === 'undefined' && typeof G_vmlCanvasManager !== 'undefined') {
// try augmenting element with excanvas' G_vmlCanvasManager
G_vmlCanvasManager.initElement(element);
}
if (typeof element.getContext === 'undefined') {
// if that didn't work, throw error
throw CANVAS_INIT_ERROR;
}
fabric.util.makeElementUnselectable(oContainer);
return oContainer;
},
/**
* Creates a secondary canvas to contain all the images are not being translated/rotated/scaled
* @method _createCanvasContainer
*/
_createCanvasContainer: function () {
var canvas = this._createCanvasElement('canvas-container');
this.contextContainerEl = canvas;
this.contextContainer = canvas.getContext('2d');
},
/**
* Creates a "background" canvas
* @method _createCanvasBackground
*/
_createCanvasBackground: function () {
var canvas = this._createCanvasElement('canvas-container');
this._contextBackgroundEl = canvas;
this._contextBackground = canvas.getContext('2d');
},
/**
* Returns canvas width
* @method getWidth
* @return {Number}
*/
getWidth: function () {
return this._config.width;
},
/**
* Returns canvas height
* @method getHeight
* @return {Number}
*/
getHeight: function () {
return this._config.height;
},
/**
* Sets width of this canvas instance
* @method setWidth
* @param {Number} width value to set width to
* @return {fabric.Element} instance
* @chainable true
*/
setWidth: function (value) {
return this._setDimension('width', value);
},
/**
* Sets height of this canvas instance
* @method setHeight
* @param {Number} height value to set height to
* @return {fabric.Element} instance
* @chainable true
*/
setHeight: function (value) {
return this._setDimension('height', value);
},
/**
* Sets dimensions (width, height) of this canvas instance
* @method setDimensions
* @param {Object} dimensions
* @return {fabric.Element} thisArg
* @chainable
*/
setDimensions: function(dimensions) {
for (var prop in dimensions) {
this._setDimension(prop, dimensions[prop]);
}
return this;
},
/**
* Helper for setting width/height
* @private
* @method _setDimensions
* @param {String} prop property (width|height)
* @param {Number} value value to set property to
* @return {fabric.Element} instance
* @chainable true
*/
_setDimension: function (prop, value) {
this.contextContainerEl[prop] = value;
this.contextContainerEl.style[prop] = value + 'px';
this._contextBackgroundEl[prop] = value;
this._contextBackgroundEl.style[prop] = value + 'px';
this._element[prop] = value;
this._element.style[prop] = value + 'px';
// <DIV> container (parent of all <CANVAS> elements)
this._element.parentNode.style[prop] = value + 'px';
this._config[prop] = value;
this.calcOffset();
this.renderAll();
return this;
},
/**
* 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) {
fireEvent('object:scaled', { target: target });
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 (target.hasStateChanged()) {
target.isMoving = false;
fireEvent('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) {
if (activeGroup.hasStateChanged() &&
activeGroup.containsPoint(this.getPointer(e))) {
fireEvent('group:modified', { target: activeGroup });
}
activeGroup.setObjectsCoords();
activeGroup.set('isMoving', false);
this._setCursor('default');
}
// 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);
},
_shouldClearSelection: function (e) {
var target = this.findTarget(e),
activeGroup = this.getActiveGroup();
return (
!target || (
target &&
activeGroup &&
!activeGroup.contains(target) &&
activeGroup !== target &&
!e.shiftKey
)
);
},
/**
* 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) {
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
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);
}
}
// we must renderAll so that active image is placed on the top canvas
this.renderAll();
},
/**
* Returns &lt;canvas> element corresponding to this instance
* @method getElement
* @return {HTMLCanvasElement}
*/
getElement: function () {
return this._element;
},
/**
* Deactivates all objects and dispatches appropriate events
* @method deactivateAllWithDispatch
* @return {fabric.Element} thisArg
*/
deactivateAllWithDispatch: function () {
var activeGroup = this.getActiveGroup();
if (activeGroup) {
fireEvent('before:group:destroyed', {
target: activeGroup
});
}
this.deactivateAll();
if (activeGroup) {
fireEvent('after:group:destroyed');
}
fireEvent('selection:cleared');
return this;
},
/**
* @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.removeActiveGroup();
}
}
else {
activeGroup.add(target);
}
fireEvent('group:selected', { target: activeGroup });
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.removeActiveObject().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 = [ ],
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.Element from fabric.Path,
// and instead fire something like "drawing:completed" event with path string
var p = new fabric.Path(path.join(''));
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();
fireEvent('path:created', { path: p });
},
/**
* 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._element.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].active) {
this._objects[i].setActive(false);
}
}
style.cursor = 'default';
}
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._scaleObject(x, y);
}
else if (this._currentTransform.action === 'scaleX') {
this._scaleObject(x, y, 'x');
}
else if (this._currentTransform.action === 'scaleY') {
this._scaleObject(x, y, 'y');
}
else {
this._translateObject(x, y);
}
// only commit here. when we are actually moving the pictures
this.renderAll();
}
},
/**
* 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.lockHorizontally || target.set('left', x - this._currentTransform.offsetX);
target.lockVertically || 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.lockScaling) 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.set('scaleX', t.scaleX * curLen/lastLen);
target.set('scaleY', t.scaleY * curLen/lastLen);
}
else if (by === 'x') {
target.set('scaleX', t.scaleX * curLen/lastLen);
}
else if (by === 'y') {
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._element.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._element.style;
if (!target) {
s.cursor = 'default';
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 = 'move';
}
else {
if (corner in cursorMap) {
s.cursor = cursorMap[corner];
}
else {
s.cursor = 'default';
return false;
}
}
}
return true;
},
/**
* Given a context, renders an object on that context
* @param ctx {Object} context to render object on
* @param object {Object} object to render
* @private
*/
_draw: function (ctx, object) {
object && object.render(ctx);
},
/**
* @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.intersectsWithRect(selectionX1Y1, selectionX2Y2) ||
currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) {
currentObject.setActive(true);
group.push(currentObject);
}
}
// do not create group for 1 element only
if (group.length === 1) {
this.setActiveObject(group[0]);
fireEvent('object:selected', {
target: group[0]
});
}
else if (group.length > 1) {
var group = new fabric.Group(group);
this.setActiveGroup(group);
group.saveCoords();
fireEvent('group:selected', { target: group });
}
this.renderAll();
},
/**
* Adds objects to canvas, then renders canvas;
* Objects should be instances of (or inherit from) fabric.Object
* @method add
* @return {fabric.Element} thisArg
* @chainable
*/
add: function () {
this._objects.push.apply(this._objects, arguments);
this.renderAll();
return this;
},
/**
* Inserts an object to canvas at specified index and renders canvas.
* An object should be an instance of (or inherit from) fabric.Object
* @method insertAt
* @param object {Object} Object to insert
* @param index {Number} index to insert object at
* @return {fabric.Element} instance
*/
insertAt: function (object, index) {
this._objects.splice(index, 0, object);
this.renderAll();
return this;
},
/**
* Returns an array of objects this instance has
* @method getObjects
* @return {Array}
*/
getObjects: function () {
return this._objects;
},
/**
* Returns topmost canvas context
* @method getContext
* @return {CanvasRenderingContext2D}
*/
getContext: function () {
return this.contextTop;
},
/**
* Clears specified context of canvas element
* @method clearContext
* @param context {Object} ctx context to clear
* @return {fabric.Element} thisArg
* @chainable
*/
clearContext: function(ctx) {
// this sucks, but we can't use `getWidth`/`getHeight` here for perf. reasons
ctx.clearRect(0, 0, this._config.width, this._config.height);
return this;
},
/**
* Clears all contexts (background, main, top) of an instance
* @method clear
* @return {fabric.Element} thisArg
* @chainable
*/
clear: function () {
this._objects.length = 0;
this.clearContext(this.contextTop);
this.clearContext(this.contextContainer);
this.renderAll();
return this;
},
/**
* Renders both the top canvas and the secondary container canvas.
* @method renderAll
* @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas
* @return {fabric.Element} instance
* @chainable
*/
renderAll: function (allOnTop) {
// this sucks, but we can't use `getWidth`/`getHeight` here for perf. reasons
var w = this._config.width,
h = this._config.height;
// when allOnTop is true all images are rendered in the top canvas.
// This is used for actions like toDataUrl that needs to take some actions on a unique canvas.
var containerCanvas = allOnTop ? this.contextTop : this.contextContainer;
this.clearContext(this.contextTop);
if (!allOnTop) {
this.clearContext(containerCanvas);
}
containerCanvas.fillStyle = this.backgroundColor;
containerCanvas.fillRect(0, 0, w, h);
var length = this._objects.length,
activeGroup = this.getActiveGroup();
var startTime = new Date();
if (length) {
for (var i = 0; i < length; ++i) {
if (!activeGroup ||
(activeGroup &&
!activeGroup.contains(this._objects[i]))) {
this._draw(containerCanvas, this._objects[i]);
}
}
}
// delegate rendering to group selection (if one exists)
if (activeGroup) {
this._draw(this.contextTop, activeGroup);
}
if (this.overlayImage) {
this.contextTop.drawImage(this.overlayImage, 0, 0);
}
var elapsedTime = new Date() - startTime;
this.onFpsUpdate(~~(1000 / elapsedTime));
if (this.afterRender) {
this.afterRender();
}
return this;
},
/**
* Method to render only the top canvas.
* Also used to render the group selection box.
* @method renderTop
* @return {fabric.Element} thisArg
* @chainable
*/
renderTop: function () {
this.clearContext(this.contextTop);
if (this.overlayImage) {
this.contextTop.drawImage(this.overlayImage, 0, 0);
}
// we render the top context - last object
if (this._groupSelector) {
this._drawSelection();
}
// delegate rendering to group selection if one exists
// used for drawing selection borders/corners
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.render(this.contextTop);
}
if (this.afterRender) {
this.afterRender();
}
return this;
},
/**
* 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 };
},
/**
* 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.containsPoint(e, this._objects[i])) {
target = this._objects[i];
this.relatedTarget = target;
break;
}
}
return target;
},
/**
* Exports canvas element to a dataurl image.
* @method toDataURL
* @param {String} format the format of the output image. Either "jpeg" or "png".
* @return {String}
*/
toDataURL: function (format) {
var data;
if (!format) {
format = 'png';
}
if (format === 'jpeg' || format === 'png') {
this.renderAll(true);
data = this.getElement().toDataURL('image/' + format);
this.renderAll();
}
return data;
},
/**
* Exports canvas element to a dataurl image (allowing to change image size via multiplier).
* @method toDataURLWithMultiplier
* @param {String} format (png|jpeg)
* @param {Number} multiplier
* @return {String}
*/
toDataURLWithMultiplier: function (format, multiplier) {
var origWidth = this.getWidth(),
origHeight = this.getHeight(),
scaledWidth = origWidth * multiplier,
scaledHeight = origHeight * multiplier,
activeObject = this.getActiveObject();
this.setWidth(scaledWidth).setHeight(scaledHeight);
this.contextTop.scale(multiplier, multiplier);
if (activeObject) {
this.deactivateAll().renderAll();
}
var dataURL = this.toDataURL(format);
this.contextTop.scale( 1 / multiplier, 1 / multiplier);
this.setWidth(origWidth).setHeight(origHeight);
if (activeObject) {
this.setActiveObject(activeObject);
}
this.renderAll();
return dataURL;
},
/**
* 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
};
},
/**
* Returns coordinates of a center of canvas.
* Returned value is an object with top and left properties
* @method getCenter
* @return {Object} object with "top" and "left" number values
*/
getCenter: function () {
return {
top: this.getHeight() / 2,
left: this.getWidth() / 2
};
},
/**
* Centers object horizontally.
* @method centerObjectH
* @param {fabric.Object} object Object to center
* @return {fabric.Element} thisArg
*/
centerObjectH: function (object) {
object.set('left', this.getCenter().left);
this.renderAll();
return this;
},
/**
* Centers object horizontally with animation.
* @method fxCenterObjectH
* @param {fabric.Object} object Object to center
* @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties
* @return {fabric.Element} thisArg
* @chainable
*/
fxCenterObjectH: function (object, callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: object.get('left'),
endValue: this.getCenter().left,
duration: this.FX_DURATION,
onChange: function(value) {
object.set('left', value);
_this.renderAll();
onChange();
},
onComplete: function() {
object.setCoords();
onComplete();
}
});
return this;
},
/**
* Centers object vertically.
* @method centerObjectH
* @param {fabric.Object} object Object to center
* @return {fabric.Element} thisArg
* @chainable
*/
centerObjectV: function (object) {
object.set('top', this.getCenter().top);
this.renderAll();
return this;
},
/**
* Centers object vertically with animation.
* @method fxCenterObjectV
* @param {fabric.Object} object Object to center
* @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties
* @return {fabric.Element} thisArg
* @chainable
*/
fxCenterObjectV: function (object, callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: object.get('top'),
endValue: this.getCenter().top,
duration: this.FX_DURATION,
onChange: function(value) {
object.set('top', value);
_this.renderAll();
onChange();
},
onComplete: function() {
object.setCoords();
onComplete();
}
});
return this;
},
/**
* Straightens object, then rerenders canvas
* @method straightenObject
* @param {fabric.Object} object Object to straighten
* @return {fabric.Element} thisArg
* @chainable
*/
straightenObject: function (object) {
object.straighten();
this.renderAll();
return this;
},
/**
* Same as `fabric.Element#straightenObject`, but animated
* @method fxStraightenObject
* @param {fabric.Object} object Object to straighten
* @return {fabric.Element} thisArg
* @chainable
*/
fxStraightenObject: function (object) {
object.fxStraighten({
onChange: this.renderAll.bind(this)
});
return this;
},
/**
* Returs dataless JSON representation of canvas
* @method toDatalessJSON
* @return {String} json string
*/
toDatalessJSON: function () {
return this.toDatalessObject();
},
/**
* Returns object representation of canvas
* @method toObject
* @return {Object}
*/
toObject: function () {
return this._toObjectMethod('toObject');
},
/**
* Returns dataless object representation of canvas
* @method toDatalessObject
* @return {Object}
*/
toDatalessObject: function () {
return this._toObjectMethod('toDatalessObject');
},
/**
* @private
* @method _toObjectMethod
*/
_toObjectMethod: function (methodName) {
return {
objects: this._objects.map(function (instance){
// TODO (kangax): figure out how to clean this up
if (!this.includeDefaultValues) {
var originalValue = instance.includeDefaultValues;
instance.includeDefaultValues = false;
}
var object = instance[methodName]();
if (!this.includeDefaultValues) {
instance.includeDefaultValues = originalValue;
}
return object;
}, this),
background: this.backgroundColor
}
},
/**
* Returns true if canvas contains no objects
* @method isEmpty
* @return {Boolean} true if canvas is empty
*/
isEmpty: function () {
return this._objects.length === 0;
},
/**
* Populates canvas with data from the specified JSON
* JSON format must conform to the one of `fabric.Element#toJSON`
* @method loadFromJSON
* @param {String} json JSON string
* @param {Function} callback Callback, invoked when json is parsed
* and corresponding objects (e.g: fabric.Image)
* are initialized
* @return {fabric.Element} instance
* @chainable
*/
loadFromJSON: function (json, callback) {
if (!json) return;
var serialized = JSON.parse(json);
if (!serialized || (serialized && !serialized.objects)) return;
this.clear();
var _this = this;
this._enlivenObjects(serialized.objects, function () {
_this.backgroundColor = serialized.background;
if (callback) {
callback();
}
});
return this;
},
/**
* @method _enlivenObjects
* @param {Array} objects
* @param {Function} callback
*/
_enlivenObjects: function (objects, callback) {
var numLoadedImages = 0,
// get length of all images
numTotalImages = objects.filter(function (o) {
return o.type === 'image';
}).length;
var _this = this;
objects.forEach(function (o, index) {
if (!o.type) {
return;
}
switch (o.type) {
case 'image':
case 'font':
fabric[capitalize(o.type)].fromObject(o, function (o) {
_this.insertAt(o, index);
if (++numLoadedImages === numTotalImages) {
if (callback) {
callback();
}
}
});
break;
default:
var klass = fabric[camelize(capitalize(o.type))];
if (klass && klass.fromObject) {
_this.insertAt(klass.fromObject(o), index);
}
break;
}
});
if (numTotalImages === 0 && callback) {
callback();
}
},
/**
* Populates canvas with data from the specified dataless JSON
* JSON format must conform to the one of `fabric.Element#toDatalessJSON`
* @method loadFromDatalessJSON
* @param {String} json JSON string
* @param {Function} callback Callback, invoked when json is parsed
* and corresponding objects (e.g: fabric.Image)
* are initialized
* @return {fabric.Element} instance
* @chainable
*/
loadFromDatalessJSON: function (json, callback) {
if (!json) {
return;
}
// serialize if it wasn't already
var serialized = (typeof json === 'string')
? JSON.parse(json)
: json;
if (!serialized || (serialized && !serialized.objects)) return;
this.clear();
// TODO: test this
this.backgroundColor = serialized.background;
this._enlivenDatalessObjects(serialized.objects, callback);
},
/**
* @method _enlivenDatalessObjects
* @param {Array} objects
* @param {Function} callback
*/
_enlivenDatalessObjects: function (objects, callback) {
/** @ignore */
function onObjectLoaded(object, index) {
_this.insertAt(object, index);
object.setCoords();
if (++numLoadedObjects === numTotalObjects) {
callback && callback();
}
}
var _this = this,
numLoadedObjects = 0,
numTotalObjects = objects.length;
if (numTotalObjects === 0 && callback) {
callback();
}
try {
objects.forEach(function (obj, index) {
var pathProp = obj.paths ? 'paths' : 'path';
var path = obj[pathProp];
delete obj[pathProp];
if (typeof path !== 'string') {
switch (obj.type) {
case 'image':
case 'text':
fabric[capitalize(obj.type)].fromObject(obj, function (o) {
onObjectLoaded(o, index);
});
break;
default:
var klass = fabric[camelize(capitalize(obj.type))];
if (klass && klass.fromObject) {
onObjectLoaded(klass.fromObject(obj), index);
}
break;
}
}
else {
if (obj.type === 'image') {
_this.loadImageFromURL(path, function (image) {
image.setSourcePath(path);
extend(image, obj);
image.setAngle(obj.angle);
onObjectLoaded(image, index);
});
}
else if (obj.type === 'text') {
obj.path = path;
var object = fabric.Text.fromObject(obj);
var onscriptload = function () {
// TODO (kangax): find out why Opera refuses to work without this timeout
if (Object.prototype.toString.call(window.opera) === '[object Opera]') {
setTimeout(function () {
onObjectLoaded(object, index);
}, 500);
}
else {
onObjectLoaded(object, index);
}
}
fabric.util.getScript(path, onscriptload);
}
else {
_this.loadSVGFromURL(path, function (elements, options) {
if (elements.length > 1) {
var object = new fabric.PathGroup(elements, obj);
}
else {
var object = elements[0];
}
object.setSourcePath(path);
// copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.)
// skip this step if an object is a PathGroup, since we already passed it options object before
if (!(object instanceof fabric.PathGroup)) {
extend(object, obj);
if (typeof obj.angle !== 'undefined') {
object.setAngle(obj.angle);
}
}
onObjectLoaded(object, index);
});
}
}
}, this);
}
catch(e) {
fabric.log(e.message);
}
},
/**
* Loads an image from URL
* @function
* @method loadImageFromURL
* @param url {String} url of image to load
* @param callback {Function} calback, invoked when image is loaded
*/
loadImageFromURL: (function () {
var imgCache = { };
return function (url, callback) {
// check cache first
var _this = this;
function checkIfLoaded() {
var imgEl = document.getElementById(imgCache[url]);
if (imgEl.width && imgEl.height) {
callback(new fabric.Image(imgEl));
}
else {
setTimeout(checkIfLoaded, 50);
}
}
// get by id from cache
if (imgCache[url]) {
// id can be cached but image might still not be loaded, so we poll here
checkIfLoaded();
}
// else append a new image element
else {
var imgEl = new Image();
/** @ignore */
imgEl.onload = function () {
imgEl.onload = null;
_this._resizeImageToFit(imgEl);
var oImg = new fabric.Image(imgEl);
callback(oImg);
};
imgEl.className = 'canvas-img-clone';
imgEl.src = url;
if (this.shouldCacheImages) {
imgCache[url] = Element.identify(imgEl);
}
document.body.appendChild(imgEl);
}
}
})(),
/**
* Takes url corresponding to an SVG document, and parses it to a set of objects
* @method loadSVGFromURL
* @param {String} url
* @param {Function} callback
*/
loadSVGFromURL: function (url, callback) {
var _this = this;
url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim();
this.cache.has(url, function (hasUrl) {
if (hasUrl) {
_this.cache.get(url, function (value) {
var enlivedRecord = _this._enlivenCachedObject(value);
callback(enlivedRecord.objects, enlivedRecord.options);
});
}
else {
// TODO (kangax): replace Prototype's API with fabric's util one
new Ajax.Request(url, {
method: 'get',
onComplete: onComplete,
onFailure: onFailure
});
}
});
function onComplete(r) {
var xml = r.responseXML;
if (!xml) return;
var doc = xml.documentElement;
if (!doc) return;
fabric.parseSVGDocument(doc, function (results, options) {
_this.cache.set(url, {
objects: results.invoke('toObject'),
options: options
});
callback(results, options);
});
}
function onFailure() {
fabric.log('ERROR!');
}
},
/**
* @method _enlivenCachedObject
*/
_enlivenCachedObject: function (cachedObject) {
var objects = cachedObject.objects,
options = cachedObject.options;
objects = objects.map(function (o) {
return fabric[capitalize(o.type)].fromObject(o);
});
return ({ objects: objects, options: options });
},
/**
* Removes an object from canvas and returns it
* @method remove
* @param object {Object} Object to remove
* @return {Object} removed object
*/
remove: function (object) {
removeFromArray(this._objects, object);
this.renderAll();
return object;
},
/**
* Same as `fabric.Element#remove` but animated
* @method fxRemove
* @param {fabric.Object} object Object to remove
* @param {Function} callback Callback, invoked on effect completion
* @return {fabric.Element} thisArg
* @chainable
*/
fxRemove: function (object, callback) {
var _this = this;
object.fxRemove({
onChange: this.renderAll.bind(this),
onComplete: function () {
_this.remove(object);
if (typeof callback === 'function') {
callback();
}
}
});
return this;
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @method sendToBack
* @param object {fabric.Object} Object to send to back
* @return {fabric.Element} thisArg
* @chainable
*/
sendToBack: function (object) {
removeFromArray(this._objects, object);
this._objects.unshift(object);
return this.renderAll();
},
/**
* Moves an object to the top of the stack of drawn objects
* @method bringToFront
* @param object {fabric.Object} Object to send
* @return {fabric.Element} thisArg
* @chainable
*/
bringToFront: function (object) {
removeFromArray(this._objects, object);
this._objects.push(object);
return this.renderAll();
},
/**
* Moves an object one level down in stack of drawn objects
* @method sendBackwards
* @param object {fabric.Object} Object to send
* @return {fabric.Element} thisArg
* @chainable
*/
sendBackwards: function (object) {
var idx = this._objects.indexOf(object),
nextIntersectingIdx = idx;
// if object is not on the bottom of stack
if (idx !== 0) {
// traverse down the stack looking for the nearest intersecting object
for (var i=idx-1; i>=0; --i) {
if (object.intersectsWithObject(this._objects[i])) {
nextIntersectingIdx = i;
break;
}
}
removeFromArray(this._objects, object);
this._objects.splice(nextIntersectingIdx, 0, object);
}
return this.renderAll();
},
/**
* Moves an object one level up in stack of drawn objects
* @method sendForward
* @param object {fabric.Object} Object to send
* @return {fabric.Element} thisArg
* @chainable
*/
bringForward: function (object) {
var objects = this.getObjects(),
idx = objects.indexOf(object),
nextIntersectingIdx = idx;
// if object is not on top of stack (last item in an array)
if (idx !== objects.length-1) {
// traverse up the stack looking for the nearest intersecting object
for (var i = idx + 1, l = this._objects.length; i < l; ++i) {
if (object.intersectsWithObject(objects[i])) {
nextIntersectingIdx = i;
break;
}
}
removeFromArray(objects, object);
objects.splice(nextIntersectingIdx, 0, object);
}
this.renderAll();
},
/**
* Sets given object as active
* @method setActiveObject
* @param object {fabric.Object} Object to set as an active one
* @return {fabric.Element} thisArg
* @chainable
*/
setActiveObject: function (object) {
if (this._activeObject) {
this._activeObject.setActive(false);
}
this._activeObject = object;
object.setActive(true);
this.renderAll();
fireEvent('object:selected', { target: object });
return this;
},
/**
* Returns currently active object
* @method getActiveObject
* @return {fabric.Object} active object
*/
getActiveObject: function () {
return this._activeObject;
},
/**
* Removes currently active object
* @method removeActiveObject
* @return {fabric.Element} thisArg
* @chainable
*/
removeActiveObject: 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.Element} 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 removeActiveGroup
* @return {fabric.Element} thisArg
*/
removeActiveGroup: function () {
var g = this.getActiveGroup();
if (g) {
g.destroy();
}
return this.setActiveGroup(null);
},
/**
* Returns object at specified index
* @method item
* @param {Number} index
* @return {fabric.Object}
*/
item: function (index) {
return this.getObjects()[index];
},
/**
* Deactivates all objects by calling their setActive(false)
* @method deactivateAll
* @return {fabric.Element} thisArg
*/
deactivateAll: function () {
var allObjects = this.getObjects(),
i = 0,
len = allObjects.length;
for ( ; i < len; i++) {
allObjects[i].setActive(false);
}
this.removeActiveGroup();
this.removeActiveObject();
return this;
},
/**
* Returns number representation of an instance complexity
* @method complexity
* @return {Number} complexity
*/
complexity: function () {
return this.getObjects().reduce(function (memo, current) {
memo += current.complexity ? current.complexity() : 0;
return memo;
}, 0);
},
/**
* Clears a canvas element and removes all event handlers.
* @method dispose
* @return {fabric.Element} thisArg
* @chainable
*/
dispose: function () {
this.clear();
removeListener(this.getElement(), 'mousedown', this._onMouseDown);
removeListener(document, 'mouseup', this._onMouseUp);
removeListener(document, 'mousemove', this._onMouseMove);
removeListener(window, 'resize', this._onResize);
return this;
},
/**
* Clones canvas instance
* @method clone
* @param {Object} [callback] Expects `onBeforeClone` and `onAfterClone` functions
* @return {fabric.Element} Clone of this instance
*/
clone: function (callback) {
var el = document.createElement('canvas');
el.width = this.getWidth();
el.height = this.getHeight();
// cache
var clone = this.__clone || (this.__clone = new fabric.Element(el));
return clone.loadFromJSON(JSON.stringify(this.toJSON()), function () {
if (callback) {
callback(clone);
}
});
},
/**
* @private
* @method _toDataURL
* @param {String} format
* @param {Function} callback
*/
_toDataURL: function (format, callback) {
this.clone(function (clone) {
callback(clone.toDataURL(format));
});
},
/**
* @private
* @method _toDataURLWithMultiplier
* @param {String} format
* @param {Number} multiplier
* @param {Function} callback
*/
_toDataURLWithMultiplier: function (format, multiplier, callback) {
this.clone(function (clone) {
callback(clone.toDataURLWithMultiplier(format, multiplier));
});
},
/**
* @private
* @method _resizeImageToFit
* @param {HTMLImageElement} imgEl
*/
_resizeImageToFit: function (imgEl) {
var imageWidth = imgEl.width || imgEl.offsetWidth,
widthScaleFactor = this.getWidth() / imageWidth;
// scale image down so that it has original dimensions when printed in large resolution
if (imageWidth) {
imgEl.width = imageWidth * widthScaleFactor;
}
},
/**
* @property
* @namespace
*/
cache: {
/**
* @method has
* @param {String} name
* @param {Function} callback
*/
has: function (name, callback) {
callback(false);
},
/**
* @method get
* @param {String} url
* @param {Function} callback
*/
get: function (url, callback) {
/* NOOP */
},
/**
* @method set
* @param {String} url
* @param {Object} object
*/
set: function (url, object) {
/* NOOP */
}
}
});
/**
* Returns a string representation of an instance
* @method toString
* @return {String} string representation of an instance
*/
fabric.Element.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet
return '#<fabric.Element (' + this.complexity() + '): '+
'{ objects: ' + this.getObjects().length + ' }>';
};
extend(fabric.Element, /** @scope fabric.Element */ {
/**
* @static
* @property EMPTY_JSON
* @type String
*/
EMPTY_JSON: '{"objects": [], "background": "white"}',
/**
* Takes &lt;canvas> element and transforms its data in such way that it becomes grayscale
* @static
* @method toGrayscale
* @param {HTMLCanvasElement} canvasEl
*/
toGrayscale: function (canvasEl) {
var context = canvasEl.getContext('2d'),
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
data = imageData.data,
iLen = imageData.width,
jLen = imageData.height,
index, average;
for (i = 0; i < iLen; i++) {
for (j = 0; j < jLen; j++) {
index = (i * 4) * jLen + (j * 4);
average = (data[index] + data[index + 1] + data[index + 2]) / 3;
data[index] = average;
data[index + 1] = average;
data[index + 2] = average;
}
}
context.putImageData(imageData, 0, 0);
},
/**
* Provides a way to check support of some of the canvas methods
* (either those of HTMLCanvasElement itself, or rendering context)
*
* @method supports
* @param methodName {String} Method to check support for;
* Could be one of "getImageData" or "toDataURL"
* @return {Boolean | null} `true` if method is supported (or at least exists),
* `null` if canvas element or context can not be initialized
*/
supports: function (methodName) {
var el = document.createElement('canvas');
if (typeof G_vmlCanvasManager !== 'undefined') {
G_vmlCanvasManager.initElement(el);
}
if (!el || !el.getContext) {
return null;
}
var ctx = el.getContext('2d');
if (!ctx) {
return null;
}
switch (methodName) {
case 'getImageData':
return typeof ctx.getImageData !== 'undefined';
case 'toDataURL':
return typeof el.toDataURL !== 'undefined';
default:
return null;
}
}
});
/**
* Returs JSON representation of canvas
* @function
* @method toJSON
* @return {String} json string
*/
fabric.Element.prototype.toJSON = fabric.Element.prototype.toObject;
})();