mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-04-05 23:11:02 +00:00
1266 lines
36 KiB
JavaScript
1266 lines
36 KiB
JavaScript
(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
|
|
*/
|
|
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, /** @scope fabric.StaticCanvas.prototype */ {
|
|
|
|
/**
|
|
* Background color of canvas instance
|
|
* @property
|
|
* @type String
|
|
*/
|
|
backgroundColor: '',
|
|
|
|
/**
|
|
* Background image of canvas instance
|
|
* Should be set via `setBackgroundImage`
|
|
* @property
|
|
* @type String
|
|
*/
|
|
backgroundImage: '',
|
|
|
|
/**
|
|
* Opacity of the background image of the canvas instance
|
|
* @property
|
|
* @type Float
|
|
*/
|
|
backgroundImageOpacity: 1.0,
|
|
|
|
/**
|
|
* Indicates whether the background image should be stretched to fit the
|
|
* dimensions of the canvas instance.
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
backgroundImageStretch: true,
|
|
|
|
/**
|
|
* Overlay image of canvas instance
|
|
* Should be set via `setOverlayImage`
|
|
* @property
|
|
* @type String
|
|
*/
|
|
overlayImage: '',
|
|
|
|
/**
|
|
* Left offset of overlay image (if present)
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
overlayImageLeft: 0,
|
|
|
|
/**
|
|
* Top offset of overlay image (if present)
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
overlayImageTop: 0,
|
|
|
|
/**
|
|
* Indicates whether toObject/toDatalessObject should include default values
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
includeDefaultValues: true,
|
|
|
|
/**
|
|
* Indicates whether objects' state should be saved
|
|
* @property
|
|
* @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)
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
renderOnAddition: true,
|
|
|
|
/**
|
|
* Function that determines clipping of entire canvas area
|
|
* Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ
|
|
* @property
|
|
* @type Function
|
|
*/
|
|
clipTo: null,
|
|
|
|
/**
|
|
* Indicates whether object controls (borders/controls) are rendered above overlay image
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
controlsAboveOverlay: false,
|
|
|
|
/**
|
|
* 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 () {
|
|
/* NOOP */
|
|
},
|
|
|
|
/**
|
|
* @method _initStatic
|
|
* @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
|
|
* @method calcOffset
|
|
* @return {fabric.Canvas} instance
|
|
* @chainable
|
|
*/
|
|
calcOffset: function () {
|
|
this._offset = getElementOffset(this.lowerCanvasEl);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets overlay image for this canvas
|
|
* @method setOverlayImage
|
|
* @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
|
|
* @method setBackgroundImage
|
|
* @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
|
|
* @method setBackgroundColor
|
|
* @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
|
|
* @method _createCanvasElement
|
|
*/
|
|
_createCanvasElement: function() {
|
|
var element = fabric.document.createElement('canvas');
|
|
if (!element.style) {
|
|
element.style = { };
|
|
}
|
|
if (!element) {
|
|
throw CANVAS_INIT_ERROR;
|
|
}
|
|
this._initCanvasElement(element);
|
|
return element;
|
|
},
|
|
|
|
/**
|
|
* @method _initCanvasElement
|
|
* @param {HTMLElement} element
|
|
*/
|
|
_initCanvasElement: function(element) {
|
|
fabric.util.createCanvasElement(element);
|
|
|
|
if (typeof element.getContext === 'undefined') {
|
|
throw CANVAS_INIT_ERROR;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @method _initOptions
|
|
* @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
|
|
* @method _createLowerCanvas
|
|
*/
|
|
_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)
|
|
* @method getWidth
|
|
* @return {Number}
|
|
*/
|
|
getWidth: function () {
|
|
return this.width;
|
|
},
|
|
|
|
/**
|
|
* Returns canvas height (in px)
|
|
* @method getHeight
|
|
* @return {Number}
|
|
*/
|
|
getHeight: function () {
|
|
return this.height;
|
|
},
|
|
|
|
/**
|
|
* Sets width of this canvas instance
|
|
* @method setWidth
|
|
* @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
|
|
* @method setHeight
|
|
* @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
|
|
* @method setDimensions
|
|
* @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
|
|
* @method _setDimensions
|
|
* @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
|
|
* @method getElement
|
|
* @return {HTMLCanvasElement}
|
|
*/
|
|
getElement: function () {
|
|
return this.lowerCanvasEl;
|
|
},
|
|
|
|
/**
|
|
* Returns currently selected object, if any
|
|
* @method getActiveObject
|
|
* @return {fabric.Object}
|
|
*/
|
|
getActiveObject: function() {
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Returns currently selected group of object, if any
|
|
* @method getActiveGroup
|
|
* @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
|
|
* @method _initObject
|
|
*/
|
|
_onObjectAdded: function(obj) {
|
|
this.stateful && obj.setupState();
|
|
obj.setCoords();
|
|
obj.canvas = this;
|
|
this.fire('object:added', { target: obj });
|
|
obj.fire('added');
|
|
},
|
|
|
|
/**
|
|
* @method private
|
|
*/
|
|
_onObjectRemoved: function(obj) {
|
|
this.fire('object:removed', { target: obj });
|
|
obj.fire('removed');
|
|
},
|
|
|
|
/**
|
|
* Returns an array of objects this instance has
|
|
* @method getObjects
|
|
* @return {Array}
|
|
*/
|
|
getObjects: function () {
|
|
return this._objects;
|
|
},
|
|
|
|
/**
|
|
* Clears specified context of canvas element
|
|
* @method clearContext
|
|
* @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
|
|
* @method getContext
|
|
* @return {CanvasRenderingContext2D}
|
|
*/
|
|
getContext: function () {
|
|
return this.contextContainer;
|
|
},
|
|
|
|
/**
|
|
* Clears all contexts (background, main, top) of an instance
|
|
* @method clear
|
|
* @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.
|
|
* @method renderAll
|
|
* @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.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(0, 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.drawControls(canvasToDrawOn);
|
|
}
|
|
|
|
this.fire('after:render');
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _drawBackroundImage
|
|
*/
|
|
_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.
|
|
* @method renderTop
|
|
* @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;
|
|
},
|
|
|
|
/**
|
|
* Draws objects' controls (borders/controls)
|
|
* @method drawControls
|
|
* @param {Object} ctx context to render controls on
|
|
*/
|
|
drawControls: function(ctx) {
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
ctx.save();
|
|
fabric.Group.prototype.transform.call(activeGroup, ctx);
|
|
activeGroup.drawBorders(ctx).drawControls(ctx);
|
|
ctx.restore();
|
|
}
|
|
else {
|
|
for (var i = 0, len = this._objects.length; i < len; ++i) {
|
|
if (!this._objects[i] || !this._objects[i].active) continue;
|
|
|
|
ctx.save();
|
|
fabric.Object.prototype.transform.call(this._objects[i], ctx);
|
|
this._objects[i].drawBorders(ctx).drawControls(ctx);
|
|
ctx.restore();
|
|
|
|
this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Exports canvas element to a dataurl image.
|
|
* @method toDataURL
|
|
* @param {Object} options
|
|
*
|
|
* `format` the format of the output image. Either "jpeg" or "png".
|
|
* `quality` quality level (0..1)
|
|
* `multiplier` multiplier to scale by {Number}
|
|
*
|
|
* @return {String}
|
|
*/
|
|
toDataURL: function (options) {
|
|
options || (options = { });
|
|
|
|
var format = options.format || 'png',
|
|
quality = options.quality || 1,
|
|
multiplier = options.multiplier || 1;
|
|
|
|
if (multiplier !== 1) {
|
|
return this.__toDataURLWithMultiplier(format, quality, multiplier);
|
|
}
|
|
else {
|
|
return this.__toDataURL(format, quality);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @method _toDataURL
|
|
* @private
|
|
*/
|
|
__toDataURL: function(format, quality) {
|
|
this.renderAll(true);
|
|
var canvasEl = this.upperCanvasEl || this.lowerCanvasEl;
|
|
var data = (fabric.StaticCanvas.supports('toDataURLWithQuality'))
|
|
? canvasEl.toDataURL('image/' + format, quality)
|
|
: canvasEl.toDataURL('image/' + format);
|
|
|
|
this.contextTop && this.clearContext(this.contextTop);
|
|
this.renderAll();
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* @method _toDataURLWithMultiplier
|
|
* @private
|
|
*/
|
|
__toDataURLWithMultiplier: function(format, quality, multiplier) {
|
|
|
|
var origWidth = this.getWidth(),
|
|
origHeight = this.getHeight(),
|
|
scaledWidth = origWidth * multiplier,
|
|
scaledHeight = origHeight * multiplier,
|
|
activeObject = this.getActiveObject(),
|
|
activeGroup = this.getActiveGroup(),
|
|
|
|
ctx = this.contextTop || this.contextContainer;
|
|
|
|
this.setWidth(scaledWidth).setHeight(scaledHeight);
|
|
ctx.scale(multiplier, multiplier);
|
|
|
|
if (activeGroup) {
|
|
// not removing group due to complications with restoring it with correct state afterwords
|
|
this._tempRemoveBordersControlsFromGroup(activeGroup);
|
|
}
|
|
else if (activeObject && this.deactivateAll) {
|
|
this.deactivateAll();
|
|
}
|
|
|
|
// restoring width, height for `renderAll` to draw
|
|
// background properly (while context is scaled)
|
|
this.width = origWidth;
|
|
this.height = origHeight;
|
|
|
|
this.renderAll(true);
|
|
|
|
var data = this.__toDataURL(format, quality);
|
|
|
|
ctx.scale(1 / multiplier, 1 / multiplier);
|
|
this.setWidth(origWidth).setHeight(origHeight);
|
|
|
|
if (activeGroup) {
|
|
this._restoreBordersControlsOnGroup(activeGroup);
|
|
}
|
|
else if (activeObject && this.setActiveObject) {
|
|
this.setActiveObject(activeObject);
|
|
}
|
|
|
|
this.contextTop && this.clearContext(this.contextTop);
|
|
this.renderAll();
|
|
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Exports canvas element to a dataurl image (allowing to change image size via multiplier).
|
|
* @deprecated since 1.0.13
|
|
* @method toDataURLWithMultiplier
|
|
* @param {String} format (png|jpeg)
|
|
* @param {Number} multiplier
|
|
* @param {Number} quality (0..1)
|
|
* @return {String}
|
|
*/
|
|
toDataURLWithMultiplier: function (format, multiplier, quality) {
|
|
return this.toDataURL({
|
|
format: format,
|
|
multiplier: multiplier,
|
|
quality: quality
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _tempRemoveBordersControlsFromGroup
|
|
*/
|
|
_tempRemoveBordersControlsFromGroup: function(group) {
|
|
group.origHasControls = group.hasControls;
|
|
group.origBorderColor = group.borderColor;
|
|
|
|
group.hasControls = true;
|
|
group.borderColor = 'rgba(0,0,0,0)';
|
|
|
|
group.forEachObject(function(o) {
|
|
o.origBorderColor = o.borderColor;
|
|
o.borderColor = 'rgba(0,0,0,0)';
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _restoreBordersControlsOnGroup
|
|
*/
|
|
_restoreBordersControlsOnGroup: function(group) {
|
|
group.hideControls = group.origHideControls;
|
|
group.borderColor = group.origBorderColor;
|
|
|
|
group.forEachObject(function(o) {
|
|
o.borderColor = o.origBorderColor;
|
|
delete o.origBorderColor;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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.Canvas} thisArg
|
|
*/
|
|
centerObjectH: function (object) {
|
|
object.set('left', this.getCenter().left);
|
|
this.renderAll();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Centers object vertically.
|
|
* @method centerObjectH
|
|
* @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.
|
|
* @method centerObject
|
|
* @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
|
|
* @method toDatalessJSON
|
|
* @param {Array} propertiesToInclude
|
|
* @return {String} json string
|
|
*/
|
|
toDatalessJSON: function (propertiesToInclude) {
|
|
return this.toDatalessObject(propertiesToInclude);
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of canvas
|
|
* @method toObject
|
|
* @param {Array} propertiesToInclude
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function (propertiesToInclude) {
|
|
return this._toObjectMethod('toObject', propertiesToInclude);
|
|
},
|
|
|
|
/**
|
|
* Returns dataless object representation of canvas
|
|
* @method toDatalessObject
|
|
* @param {Array} propertiesToInclude
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toDatalessObject: function (propertiesToInclude) {
|
|
return this._toObjectMethod('toDatalessObject', propertiesToInclude);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _toObjectMethod
|
|
*/
|
|
_toObjectMethod: function (methodName, propertiesToInclude) {
|
|
var data = {
|
|
objects: this._objects.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);
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* Returns SVG representation of canvas
|
|
* @function
|
|
* @method toSVG
|
|
* @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(
|
|
'<?xml version="1.0" standalone="no" ?>',
|
|
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ',
|
|
'"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
|
|
);
|
|
}
|
|
markup.push(
|
|
'<svg ',
|
|
'xmlns="http://www.w3.org/2000/svg" ',
|
|
'xmlns:xlink="http://www.w3.org/1999/xlink" ',
|
|
'version="1.1" ',
|
|
'width="', (options.viewBox ? options.viewBox.width : this.width), '" ',
|
|
'height="', (options.viewBox ? options.viewBox.height : this.height), '" ',
|
|
(this.backgroundColor && !this.backgroundColor.source ? 'style="background-color: ' + this.backgroundColor +'" ' : null),
|
|
(options.viewBox ? 'viewBox="' + options.viewBox.x + ' ' + options.viewBox.y + ' ' + options.viewBox.width + ' ' + options.viewBox.height + '" ' : null),
|
|
'xml:space="preserve">',
|
|
'<desc>Created with Fabric.js ', fabric.version, '</desc>',
|
|
'<defs>', fabric.createSVGFontFacesMarkup(this.getObjects()), fabric.createSVGRefElementsMarkup(this), '</defs>'
|
|
);
|
|
|
|
if (this.backgroundColor && this.backgroundColor.source) {
|
|
markup.push(
|
|
'<rect x="0" y="0" ',
|
|
'width="', (this.backgroundColor.repeat === 'repeat-y' || this.backgroundColor.repeat === 'no-repeat' ? this.backgroundColor.source.width : this.width),
|
|
'" height="', (this.backgroundColor.repeat === 'repeat-x' || this.backgroundColor.repeat === 'no-repeat' ? this.backgroundColor.source.height : this.height),
|
|
'" fill="url(#backgroundColorPattern)"',
|
|
'></rect>'
|
|
);
|
|
}
|
|
|
|
if (this.backgroundImage) {
|
|
markup.push(
|
|
'<image x="0" y="0" ',
|
|
'width="', (this.backgroundImageStretch ? this.width : this.backgroundImage.width),
|
|
'" height="', (this.backgroundImageStretch ? this.height : this.backgroundImage.height),
|
|
'" preserveAspectRatio="', (this.backgroundImageStretch ? 'none' : 'defer'),
|
|
'" xlink:href="', this.backgroundImage.src,
|
|
'" style="opacity:', this.backgroundImageOpacity,
|
|
'"></image>'
|
|
);
|
|
}
|
|
|
|
if (this.overlayImage) {
|
|
markup.push(
|
|
'<image x="', this.overlayImageLeft,
|
|
'" y="', this.overlayImageTop,
|
|
'" width="', this.overlayImage.width,
|
|
'" height="', this.overlayImage.height,
|
|
'" xlink:href="', this.overlayImage.src,
|
|
'"></image>'
|
|
);
|
|
}
|
|
|
|
for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) {
|
|
markup.push(objects[i].toSVG());
|
|
}
|
|
markup.push('</svg>');
|
|
|
|
return markup.join('');
|
|
},
|
|
|
|
/**
|
|
* Removes an object from canvas and returns it
|
|
* @method remove
|
|
* @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
|
|
* @method sendToBack
|
|
* @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
|
|
* @method bringToFront
|
|
* @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
|
|
* @method sendBackwards
|
|
* @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
|
|
* @method bringForward
|
|
* @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
|
|
* @method moveTo
|
|
* @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.
|
|
* @method dispose
|
|
* @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;
|
|
},
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns a string representation of an instance
|
|
* @method toString
|
|
* @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 '#<fabric.Canvas (' + this.complexity() + '): '+
|
|
'{ objects: ' + this.getObjects().length + ' }>';
|
|
};
|
|
|
|
extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ {
|
|
|
|
/**
|
|
* @static
|
|
* @property EMPTY_JSON
|
|
* @type String
|
|
*/
|
|
EMPTY_JSON: '{"objects": [], "background": "white"}',
|
|
|
|
/**
|
|
* Takes <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, 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)
|
|
*
|
|
* @method supports
|
|
* @param methodName {String} Method to check support for;
|
|
* Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality"
|
|
* @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 '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
|
|
* @method toJSON
|
|
* @param {Array} propertiesToInclude
|
|
* @return {String} json string
|
|
*/
|
|
fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject;
|
|
|
|
})();
|