(function (global) {
"use strict";
if (fabric.Element) {
fabric.warn('fabric.Element is already defined.');
return;
}
var 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);
}
this._createCanvasBackground();
this._createCanvasContainer();
this._initEvents();
this.calcOffset();
};
extend(fabric.Element.prototype, /** @scope fabric.Element.prototype */ {
/**
* Background color of this canvas instance
* @property
* @type String
*/
backgroundColor: 'rgba(0, 0, 0, 0)',
/**
* Color of selection
* @property
* @type String
*/
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
/**
* Color of the border of selection (usually slightly darker than color of selection itself)
* @property
* @type String
*/
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
/**
* Width of a line used in selection
* @property
* @type Number
*/
selectionLineWidth: 1,
/**
* Color of the line used in free drawing mode
* @property
* @type String
*/
freeDrawingColor: 'rgb(0, 0, 0)',
/**
* Width of a line used in free drawing mode
* @property
* @type Number
*/
freeDrawingLineWidth: 1,
/**
* @property
* @type Boolean
*/
includeDefaultValues: true,
/**
* Indicates whether images loaded via `fabric.Element#loadImageFromUrl` should be cached
* @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';
//
container (parent of all