(function () {
"use strict";
if (fabric.StaticCanvas) {
fabric.warn('fabric.StaticCanvas is already defined.');
return;
}
// aliases for faster resolution
var extend = fabric.util.object.extend,
getElementOffset = fabric.util.getElementOffset,
removeFromArray = fabric.util.removeFromArray,
removeListener = fabric.util.removeListener,
CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element');
/**
* Static canvas class
* @class fabric.StaticCanvas
* @constructor
*
* @param {HTMLElement | String} el <canvas> element to initialize instance on
* @param {Object} [options] Options object
*
* @extends fabric.Collection
* @extends fabric.Observable
*/
fabric.StaticCanvas = function (el, options) {
options || (options = { });
this._initStatic(el, options);
fabric.StaticCanvas.activeInstance = this;
};
extend(fabric.StaticCanvas.prototype, fabric.Observable);
extend(fabric.StaticCanvas.prototype, fabric.Collection);
extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter);
extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ {
/**
* Background color of canvas instance
* @type String
*/
backgroundColor: '',
/**
* Background image of canvas instance
* Should be set via {@link fabric.StaticCanvas#setBackgroundImage}
* @type String
*/
backgroundImage: '',
/**
* Opacity of the background image of the canvas instance
* @type Float
*/
backgroundImageOpacity: 1,
/**
* Indicates whether the background image should be stretched to fit the
* dimensions of the canvas instance.
* @type Boolean
*/
backgroundImageStretch: true,
/**
* Overlay image of canvas instance
* Should be set via {@link fabric.StaticCanvas#setOverlayImage}
* @type String
*/
overlayImage: '',
/**
* Left offset of overlay image (if present)
* @type Number
*/
overlayImageLeft: 0,
/**
* Top offset of overlay image (if present)
* @type Number
*/
overlayImageTop: 0,
/**
* Indicates whether toObject/toDatalessObject should include default values
* @type Boolean
*/
includeDefaultValues: true,
/**
* Indicates whether objects' state should be saved
* @type Boolean
*/
stateful: true,
/**
* Indicates whether {@link fabric.Canvas.prototype.add} should also re-render canvas.
* Disabling this option could give a great performance boost when adding a lot of objects to canvas at once
* (followed by a manual rendering after addition)
* @type Boolean
*/
renderOnAddition: true,
/**
* Function that determines clipping of entire canvas area
* Being passed context as first argument. See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ}
* @type Function
*/
clipTo: null,
/**
* Indicates whether object controls (borders/controls) are rendered above overlay image
* @type Boolean
*/
controlsAboveOverlay: false,
/**
* Callback; invoked right before object is about to be scaled/rotated
* @param {fabric.Object} target Object that's about to be scaled/rotated
*/
onBeforeScaleRotate: function () {
/* NOOP */
},
/**
* @private
*/
_initStatic: function(el, options) {
this._objects = [];
this._createLowerCanvas(el);
this._initOptions(options);
if (options.overlayImage) {
this.setOverlayImage(options.overlayImage, this.renderAll.bind(this));
}
if (options.backgroundImage) {
this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this));
}
if (options.backgroundColor) {
this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this));
}
this.calcOffset();
},
/**
* Calculates canvas element offset relative to the document
* This method is also attached as "resize" event handler of window
* @return {fabric.Canvas} instance
* @chainable
*/
calcOffset: function () {
this._offset = getElementOffset(this.lowerCanvasEl);
return this;
},
/**
* Sets overlay image for this canvas
* @param {String} url url of an image to set overlay to
* @param {Function} callback callback to invoke when image is loaded and set as an overlay
* @param {Object} [options] optional options to set for the overlay image
* @return {fabric.Canvas} thisArg
* @chainable
*/
setOverlayImage: function (url, callback, options) { // TODO (kangax): test callback
fabric.util.loadImage(url, function(img) {
this.overlayImage = img;
if (options && ('overlayImageLeft' in options)) {
this.overlayImageLeft = options.overlayImageLeft;
}
if (options && ('overlayImageTop' in options)) {
this.overlayImageTop = options.overlayImageTop;
}
callback && callback();
}, this);
return this;
},
/**
* Sets background image for this canvas
* @param {String} url url of an image to set background to
* @param {Function} callback callback to invoke when image is loaded and set as background
* @param {Object} [options] optional options to set for the background image
* @return {fabric.Canvas} thisArg
* @chainable
*/
setBackgroundImage: function (url, callback, options) {
fabric.util.loadImage(url, function(img) {
this.backgroundImage = img;
if (options && ('backgroundImageOpacity' in options)) {
this.backgroundImageOpacity = options.backgroundImageOpacity;
}
if (options && ('backgroundImageStretch' in options)) {
this.backgroundImageStretch = options.backgroundImageStretch;
}
callback && callback();
}, this);
return this;
},
/**
* Sets background color for this canvas
* @param {String|fabric.Pattern} Color of pattern to set background color to
* @param {Function} callback callback to invoke when background color is set
* @return {fabric.Canvas} thisArg
* @chainable
*/
setBackgroundColor: function(backgroundColor, callback) {
if (backgroundColor.source) {
var _this = this;
fabric.util.loadImage(backgroundColor.source, function(img) {
_this.backgroundColor = new fabric.Pattern({
source: img,
repeat: backgroundColor.repeat
});
callback && callback();
});
}
else {
this.backgroundColor = backgroundColor;
callback && callback();
}
return this;
},
/**
* @private
*/
_createCanvasElement: function() {
var element = fabric.document.createElement('canvas');
if (!element.style) {
element.style = { };
}
if (!element) {
throw CANVAS_INIT_ERROR;
}
this._initCanvasElement(element);
return element;
},
/**
* @private
* @param {HTMLElement} element
*/
_initCanvasElement: function(element) {
fabric.util.createCanvasElement(element);
if (typeof element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
},
/**
* @private
* @param {Object} [options]
*/
_initOptions: function (options) {
for (var prop in options) {
this[prop] = options[prop];
}
this.width = parseInt(this.lowerCanvasEl.width, 10) || 0;
this.height = parseInt(this.lowerCanvasEl.height, 10) || 0;
if (!this.lowerCanvasEl.style) return;
this.lowerCanvasEl.style.width = this.width + 'px';
this.lowerCanvasEl.style.height = this.height + 'px';
},
/**
* Creates a bottom canvas
* @private
*/
_createLowerCanvas: function (canvasEl) {
this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement();
this._initCanvasElement(this.lowerCanvasEl);
fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas');
if (this.interactive) {
this._applyCanvasStyle(this.lowerCanvasEl);
}
this.contextContainer = this.lowerCanvasEl.getContext('2d');
},
/**
* Returns canvas width (in px)
* @return {Number}
*/
getWidth: function () {
return this.width;
},
/**
* Returns canvas height (in px)
* @return {Number}
*/
getHeight: function () {
return this.height;
},
/**
* Sets width of this canvas instance
* @param {Number} width value to set width to
* @return {fabric.Canvas} instance
* @chainable true
*/
setWidth: function (value) {
return this._setDimension('width', value);
},
/**
* Sets height of this canvas instance
* @param {Number} height value to set height to
* @return {fabric.Canvas} instance
* @chainable true
*/
setHeight: function (value) {
return this._setDimension('height', value);
},
/**
* Sets dimensions (width, height) of this canvas instance
* @param {Object} dimensions
* @return {fabric.Canvas} thisArg
* @chainable
*/
setDimensions: function(dimensions) {
for (var prop in dimensions) {
this._setDimension(prop, dimensions[prop]);
}
return this;
},
/**
* Helper for setting width/height
* @private
* @param {String} prop property (width|height)
* @param {Number} value value to set property to
* @return {fabric.Canvas} instance
* @chainable true
*/
_setDimension: function (prop, value) {
this.lowerCanvasEl[prop] = value;
this.lowerCanvasEl.style[prop] = value + 'px';
if (this.upperCanvasEl) {
this.upperCanvasEl[prop] = value;
this.upperCanvasEl.style[prop] = value + 'px';
}
if (this.cacheCanvasEl) {
this.cacheCanvasEl[prop] = value;
}
if (this.wrapperEl) {
this.wrapperEl.style[prop] = value + 'px';
}
this[prop] = value;
this.calcOffset();
this.renderAll();
return this;
},
/**
* Returns <canvas> element corresponding to this instance
* @return {HTMLCanvasElement}
*/
getElement: function () {
return this.lowerCanvasEl;
},
/**
* Returns currently selected object, if any
* @return {fabric.Object}
*/
getActiveObject: function() {
return null;
},
/**
* Returns currently selected group of object, if any
* @return {fabric.Group}
*/
getActiveGroup: function() {
return null;
},
/**
* 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) {
if (!object) return;
if (this.controlsAboveOverlay) {
var hasBorders = object.hasBorders, hasControls = object.hasControls;
object.hasBorders = object.hasControls = false;
object.render(ctx);
object.hasBorders = hasBorders;
object.hasControls = hasControls;
}
else {
object.render(ctx);
}
},
/**
* @private
*/
_onObjectAdded: function(obj) {
this.stateful && obj.setupState();
obj.setCoords();
obj.canvas = this;
this.fire('object:added', { target: obj });
obj.fire('added');
},
/**
* @private
*/
_onObjectRemoved: function(obj) {
this.fire('object:removed', { target: obj });
obj.fire('removed');
},
/**
* Returns an array of objects this instance has
* @return {Array}
*/
getObjects: function () {
return this._objects;
},
/**
* Clears specified context of canvas element
* @param context {Object} ctx context to clear
* @return {fabric.Canvas} thisArg
* @chainable
*/
clearContext: function(ctx) {
ctx.clearRect(0, 0, this.width, this.height);
return this;
},
/**
* Returns context of canvas where objects are drawn
* @return {CanvasRenderingContext2D}
*/
getContext: function () {
return this.contextContainer;
},
/**
* Clears all contexts (background, main, top) of an instance
* @return {fabric.Canvas} thisArg
* @chainable
*/
clear: function () {
this._objects.length = 0;
if (this.discardActiveGroup) {
this.discardActiveGroup();
}
if (this.discardActiveObject) {
this.discardActiveObject();
}
this.clearContext(this.contextContainer);
if (this.contextTop) {
this.clearContext(this.contextTop);
}
this.fire('canvas:cleared');
this.renderAll();
return this;
},
/**
* Renders both the top canvas and the secondary container canvas.
* @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas
* @return {fabric.Canvas} instance
* @chainable
*/
renderAll: function (allOnTop) {
var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer'];
if (this.contextTop && this.selection && !this._groupSelector) {
this.clearContext(this.contextTop);
}
if (!allOnTop) {
this.clearContext(canvasToDrawOn);
}
this.fire('before:render');
if (this.clipTo) {
fabric.util.clipContext(this, canvasToDrawOn);
}
if (this.backgroundColor) {
canvasToDrawOn.fillStyle = this.backgroundColor.toLive
? this.backgroundColor.toLive(canvasToDrawOn)
: this.backgroundColor;
canvasToDrawOn.fillRect(
this.backgroundColor.offsetX || 0,
this.backgroundColor.offsetY || 0,
this.width,
this.height);
}
if (typeof this.backgroundImage === 'object') {
this._drawBackroundImage(canvasToDrawOn);
}
var activeGroup = this.getActiveGroup();
for (var i = 0, length = this._objects.length; i < length; ++i) {
if (!activeGroup ||
(activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) {
this._draw(canvasToDrawOn, this._objects[i]);
}
}
// delegate rendering to group selection (if one exists)
if (activeGroup) {
//Store objects in group preserving order, then replace
var sortedObjects = [];
this.forEachObject(function (object) {
if (activeGroup.contains(object)) {
sortedObjects.push(object);
}
});
activeGroup._set('objects', sortedObjects);
this._draw(canvasToDrawOn, activeGroup);
}
if (this.clipTo) {
canvasToDrawOn.restore();
}
if (this.overlayImage) {
canvasToDrawOn.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop);
}
if (this.controlsAboveOverlay && this.interactive) {
this.drawControls(canvasToDrawOn);
}
this.fire('after:render');
return this;
},
/**
* @private
*/
_drawBackroundImage: function(canvasToDrawOn) {
canvasToDrawOn.save();
canvasToDrawOn.globalAlpha = this.backgroundImageOpacity;
if (this.backgroundImageStretch) {
canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height);
}
else {
canvasToDrawOn.drawImage(this.backgroundImage, 0, 0);
}
canvasToDrawOn.restore();
},
/**
* Method to render only the top canvas.
* Also used to render the group selection box.
* @return {fabric.Canvas} thisArg
* @chainable
*/
renderTop: function () {
var ctx = this.contextTop || this.contextContainer;
this.clearContext(ctx);
// we render the top context - last object
if (this.selection && this._groupSelector) {
this._drawSelection();
}
// delegate rendering to group selection if one exists
// used for drawing selection borders/controls
var activeGroup = this.getActiveGroup();
if (activeGroup) {
activeGroup.render(ctx);
}
if (this.overlayImage) {
ctx.drawImage(this.overlayImage, this.overlayImageLeft, this.overlayImageTop);
}
this.fire('after:render');
return this;
},
/**
* Returns coordinates of a center of canvas.
* Returned value is an object with top and left properties
* @return {Object} object with "top" and "left" number values
*/
getCenter: function () {
return {
top: this.getHeight() / 2,
left: this.getWidth() / 2
};
},
/**
* Centers object horizontally.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
*/
centerObjectH: function (object) {
object.set('left', this.getCenter().left);
this.renderAll();
return this;
},
/**
* Centers object vertically.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
* @chainable
*/
centerObjectV: function (object) {
object.set('top', this.getCenter().top);
this.renderAll();
return this;
},
/**
* Centers object vertically and horizontally.
* @param {fabric.Object} object Object to center
* @return {fabric.Canvas} thisArg
* @chainable
*/
centerObject: function (object) {
return this.centerObjectH(object).centerObjectV(object);
},
/**
* Returs dataless JSON representation of canvas
* @param {Array} propertiesToInclude
* @return {String} json string
*/
toDatalessJSON: function (propertiesToInclude) {
return this.toDatalessObject(propertiesToInclude);
},
/**
* Returns object representation of canvas
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toObject: function (propertiesToInclude) {
return this._toObjectMethod('toObject', propertiesToInclude);
},
/**
* Returns dataless object representation of canvas
* @param {Array} propertiesToInclude
* @return {Object} object representation of an instance
*/
toDatalessObject: function (propertiesToInclude) {
return this._toObjectMethod('toDatalessObject', propertiesToInclude);
},
/**
* @private
*/
_toObjectMethod: function (methodName, propertiesToInclude) {
var activeGroup = this.getActiveGroup();
if (activeGroup) {
this.discardActiveGroup();
}
var data = {
objects: this.getObjects().map(function (instance) {
// TODO (kangax): figure out how to clean this up
var originalValue;
if (!this.includeDefaultValues) {
originalValue = instance.includeDefaultValues;
instance.includeDefaultValues = false;
}
var object = instance[methodName](propertiesToInclude);
if (!this.includeDefaultValues) {
instance.includeDefaultValues = originalValue;
}
return object;
}, this),
background: (this.backgroundColor && this.backgroundColor.toObject)
? this.backgroundColor.toObject()
: this.backgroundColor
};
if (this.backgroundImage) {
data.backgroundImage = this.backgroundImage.src;
data.backgroundImageOpacity = this.backgroundImageOpacity;
data.backgroundImageStretch = this.backgroundImageStretch;
}
if (this.overlayImage) {
data.overlayImage = this.overlayImage.src;
data.overlayImageLeft = this.overlayImageLeft;
data.overlayImageTop = this.overlayImageTop;
}
fabric.util.populateWithProperties(this, data, propertiesToInclude);
if (activeGroup) {
this.setActiveGroup(new fabric.Group(activeGroup.objects));
activeGroup.forEachObject(function(o) { o.set('active', true) });
}
return data;
},
/* _TO_SVG_START_ */
/**
* Returns SVG representation of canvas
* @function
* @param {Object} [options] Options for SVG output (suppressPreamble: true/false (if true xml tag is not included),
* viewBox: {x, y, width, height} to define the svg output viewBox)
* @return {String}
*/
toSVG: function(options) {
options || (options = { });
var markup = [];
if (!options.suppressPreamble) {
markup.push(
'',
''
);
}
markup.push(
'');
return markup.join('');
},
/* _TO_SVG_END_ */
/**
* Removes an object from canvas and returns it
* @param object {Object} Object to remove
* @return {Object} removed object
*/
remove: function (object) {
// removing active object should fire "selection:cleared" events
if (this.getActiveObject() === object) {
this.fire('before:selection:cleared', { target: object });
this.discardActiveObject();
this.fire('selection:cleared');
}
return fabric.Collection.remove.call(this, object);
},
/**
* Moves an object to the bottom of the stack of drawn objects
* @param object {fabric.Object} Object to send to back
* @return {fabric.Canvas} thisArg
* @chainable
*/
sendToBack: function (object) {
removeFromArray(this._objects, object);
this._objects.unshift(object);
return this.renderAll && this.renderAll();
},
/**
* Moves an object to the top of the stack of drawn objects
* @param object {fabric.Object} Object to send
* @return {fabric.Canvas} thisArg
* @chainable
*/
bringToFront: function (object) {
removeFromArray(this._objects, object);
this._objects.push(object);
return this.renderAll && this.renderAll();
},
/**
* Moves an object one level down in stack of drawn objects
* @param object {fabric.Object} Object to send
* @return {fabric.Canvas} 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) {
var isIntersecting = object.intersectsWithObject(this._objects[i]) ||
object.isContainedWithinObject(this._objects[i]) ||
this._objects[i].isContainedWithinObject(object);
if (isIntersecting) {
nextIntersectingIdx = i;
break;
}
}
removeFromArray(this._objects, object);
this._objects.splice(nextIntersectingIdx, 0, object);
}
return this.renderAll && this.renderAll();
},
/**
* Moves an object one level up in stack of drawn objects
* @param object {fabric.Object} Object to send
* @return {fabric.Canvas} 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) {
var isIntersecting = object.intersectsWithObject(objects[i]) ||
object.isContainedWithinObject(this._objects[i]) ||
this._objects[i].isContainedWithinObject(object);
if (isIntersecting) {
nextIntersectingIdx = i;
break;
}
}
removeFromArray(objects, object);
objects.splice(nextIntersectingIdx, 0, object);
}
return this.renderAll && this.renderAll();
},
/**
* Moves an object to specified level in stack of drawn objects
* @param object {fabric.Object} Object to send
* @param {Number} index Position to move to
* @return {fabric.Canvas} thisArg
* @chainable
*/
moveTo: function (object, index) {
removeFromArray(this._objects, object);
this._objects.splice(index, 0, object);
return this.renderAll && this.renderAll();
},
/**
* Clears a canvas element and removes all event handlers.
* @return {fabric.Canvas} thisArg
* @chainable
*/
dispose: function () {
this.clear();
if (!this.interactive) return this;
if (fabric.isTouchSupported) {
removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
if (typeof Event !== 'undefined' && 'remove' in Event) {
Event.remove(this.upperCanvasEl, 'gesture', this._onGesture);
}
}
else {
removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
removeListener(fabric.window, 'resize', this._onResize);
}
return this;
}
});
/**
* Returns a string representation of an instance
* @return {String} string representation of an instance
*/
fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet
return '#';
};
extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ {
/**
* @static
* @type String
*/
EMPTY_JSON: '{"objects": [], "background": "white"}',
/**
* Takes <canvas> element and transforms its data in such way that it becomes grayscale
* @static
* @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, i, j;
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)
*
* @param methodName {String} Method to check support for;
* Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash"
* @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 = fabric.util.createCanvasElement();
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 'setLineDash':
return typeof ctx.setLineDash !== 'undefined';
case 'toDataURL':
return typeof el.toDataURL !== 'undefined';
case 'toDataURLWithQuality':
try {
el.toDataURL('image/jpeg', 0);
return true;
}
catch (e) { }
return false;
default:
return null;
}
}
});
/**
* Returs JSON representation of canvas
* @function
* @param {Array} propertiesToInclude
* @return {String} json string
*/
fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject;
})();