fabric.js/dist/all.js

7861 lines
203 KiB
JavaScript

/*! Fabric.js Copyright 2010, Bitsonnet (Juriy Zaytsev, Maxim Chernyak) */
var console = console || {
log: function() { },
warn: function() { }
};
var fabric = fabric || { version: 0.1 };
/*
http://www.JSON.org/json2.js
2010-03-20
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or ' '),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
if (!this.JSON) {
this.JSON = {};
}
(function () {
function f(n) {
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
escapable.lastIndex = 0;
return escapable.test(string) ?
'"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
return String(value);
case 'object':
if (!value) {
return 'null';
}
gap += indent;
partial = [];
if (Object.prototype.toString.apply(value) === '[object Array]') {
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
var i;
gap = '';
indent = '';
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
} else if (typeof space === 'string') {
indent = space;
}
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
return str('', {'': value});
};
}
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
var j;
function walk(holder, key) {
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
j = eval('(' + text + ')');
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
throw new SyntaxError('JSON.parse');
};
}
}());
(function (global) {
var fabric = this.fabric || (this.fabric = { }),
slice = Array.prototype.slice,
apply = Function.prototype.apply;
fabric.util = { };
(function() {
/**
* Removes value from an array.
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
* @static
* @method removeFromArray
* @param {Array} array
* @param {Any} value
* @return {Array} original array
*/
function removeFromArray(array, value) {
var idx = array.indexOf(value);
if (idx !== -1) {
array.splice(idx, 1);
}
return array;
};
/**
* @static
* @method getRandomInt
* @param {Number} min lower limit
* @param {Number} max upper limit
* @return {Number} random value (between min and max)
*/
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Transforms degrees to radians
* @static
* @method degreesToRadians
* @param {Number} degrees value in degrees
* @return {Number} value in radians
*/
var PiBy180 = Math.PI / 180;
function degreesToRadians(degrees) {
return degrees * PiBy180;
}
/**
* A wrapper around Number#toFixed, which contrary to native method returns number, not string
* @static
* @method toFixed
* @param {Number | String} number number to operate on
* @param {Number} fractionDigits number of fraction digits to "leave"
* @return {Number}
*/
function toFixed(number, fractionDigits) {
return parseFloat(Number(number).toFixed(fractionDigits));
}
/**
* Function which always returns `false`
* @static
* @method falseFunction
* @return {Boolean}
*/
function falseFunction() {
return false;
}
fabric.util.removeFromArray = removeFromArray;
fabric.util.degreesToRadians = degreesToRadians;
fabric.util.toFixed = toFixed;
fabric.util.getRandomInt = getRandomInt;
fabric.util.falseFunction = falseFunction;
})();
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(value, from) {
var len = this.length >>> 0;
from = Number(from) || 0;
from = Math[from < 0 ? 'ceil' : 'floor'](from);
if (from < 0) {
from += len;
}
for (; from < len; from++) {
if (from in this && this[from] === value) {
return from;
}
}
return -1;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fn, context) {
for (var i = 0, len = this.length >>> 0; i < len; i++) {
if (i in this) {
fn.call(context, this[i], i, this);
}
}
};
}
if (!Array.prototype.map) {
Array.prototype.map = function(fn, context) {
var result = [ ];
for (var i = 0, len = this.length >>> 0; i < len; i++) {
if (i in this) {
result[i] = fn.call(context, this[i], i, this);
}
}
return result;
};
}
if (!Array.prototype.every) {
Array.prototype.every = function(fn, context) {
for (var i = 0, len = this.length >>> 0; i < len; i++) {
if (i in this && !fn.call(context, this[i], i, this)) {
return false;
}
}
return true;
};
}
if (!Array.prototype.some) {
Array.prototype.some = function(fn, context) {
for (var i = 0, len = this.length >>> 0; i < len; i++) {
if (i in this && fn.call(context, this[i], i, this)) {
return true;
}
}
return false;
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function(fn, context) {
var result = [ ], val;
for (var i = 0, len = this.length >>> 0; i < len; i++) {
if (i in this) {
val = this[i]; // in case fn mutates this
if (fn.call(context, val, i, this)) {
result.push(val);
}
}
}
return result;
};
}
if (!Array.prototype.reduce) {
Array.prototype.reduce = function(fn /*, initial*/) {
var len = this.length >>> 0,
i = 0,
rv;
if (arguments.length > 1) {
rv = arguments[1];
}
else {
do {
if (i in this) {
rv = this[i++];
break;
}
if (++i >= len) {
throw new TypeError();
}
}
while (true);
}
for (; i < len; i++) {
if (i in this) {
rv = fn.call(null, rv, this[i], i, this);
}
}
return rv;
};
}
function invoke(array, method) {
var args = slice.call(arguments, 2), result = [ ];
for (var i = 0, len = array.length; i < len; i++) {
result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]);
}
return result;
}
function max(array, byProperty) {
var i = array.length - 1,
result = byProperty ? array[i][byProperty] : array[i];
if (byProperty) {
while (i--) {
if (array[i][byProperty] >= result) {
result = array[i][byProperty];
}
}
}
else {
while (i--) {
if (array[i] >= result) {
result = array[i];
}
}
}
return result;
}
function min(array, byProperty) {
var i = array.length - 1,
result = byProperty ? array[i][byProperty] : array[i];
if (byProperty) {
while (i--) {
if (array[i][byProperty] < result) {
result = array[i][byProperty];
}
}
}
else {
while (i--) {
if (array[i] < result) {
result = array[i];
}
}
}
return result;
}
fabric.util.array = {
invoke: invoke,
min: min,
max: max
};
function extend(destination, source) {
for (var property in source) {
destination[property] = source[property];
}
return destination;
}
function clone(object) {
return extend({ }, object);
}
fabric.util.object = {
extend: extend,
clone: clone
};
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, '');
};
}
function camelize(string) {
return string.replace(/-+(.)?/g, function(match, character) {
return character ? character.toUpperCase() : '';
});
}
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
}
fabric.util.string = {
camelize: camelize,
capitalize: capitalize
};
if (!Function.prototype.bind) {
Function.prototype.bind = function(thisArg) {
var fn = this, args = slice.call(arguments, 1);
return args.length
? function() { return apply.call(fn, thisArg, args.concat(slice.call(arguments))); }
: function() { return apply.call(fn, thisArg, arguments) };
};
}
(function() {
var IS_DONTENUM_BUGGY = (function(){
for (var p in { toString: 1 }) {
if (p === 'toString') return false;
}
return true;
})();
var addMethods;
if (IS_DONTENUM_BUGGY) {
addMethods = function(klass, source) {
if (source.toString !== Object.prototype.toString) {
klass.prototype.toString = source.toString;
}
if (source.valueOf !== Object.prototype.valueOf) {
klass.prototype.valueOf = source.valueOf;
}
for (var property in source) {
klass.prototype[property] = source[property];
}
};
}
else {
addMethods = function(klass, source) {
for (var property in source) {
klass.prototype[property] = source[property];
}
};
}
function subclass() { };
function createClass() {
var parent = null,
properties = slice.call(arguments, 0);
if (typeof properties[0] === 'function') {
parent = properties.shift();
}
function klass() {
this.initialize.apply(this, arguments);
}
klass.superclass = parent;
klass.subclasses = [ ];
if (parent) {
subclass.prototype = parent.prototype;
klass.prototype = new subclass;
parent.subclasses.push(klass);
}
for (var i = 0, length = properties.length; i < length; i++) {
addMethods(klass, properties[i]);
}
if (!klass.prototype.initialize) {
klass.prototype.initialize = emptyFunction;
}
klass.prototype.constructor = klass;
return klass;
}
fabric.util.createClass = createClass;
})();
(function (global) {
/* EVENT HANDLING */
function areHostMethods(object) {
var methodNames = Array.prototype.slice.call(arguments, 1),
t, i, len = methodNames.length;
for (i = 0; i < len; i++) {
t = typeof object[methodNames[i]];
if (!(/^(?:function|object|unknown)$/).test(t)) return false;
}
return true;
}
var getUniqueId = (function () {
if (typeof document.documentElement.uniqueID !== 'undefined') {
return function (element) {
return element.uniqueID;
};
}
var uid = 0;
return function (element) {
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
};
})();
var getElement, setElement;
(function () {
var elements = { };
getElement = function (uid) {
return elements[uid];
};
setElement = function (uid, element) {
elements[uid] = element;
};
})();
function createListener(uid, handler) {
return {
handler: handler,
wrappedHandler: createWrappedHandler(uid, handler)
};
}
function createWrappedHandler(uid, handler) {
return function (e) {
handler.call(getElement(uid), e || window.event);
};
}
function createDispatcher(uid, eventName) {
return function (e) {
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
handlersForEvent[i].call(this, e || window.event);
}
}
};
}
var shouldUseAddListenerRemoveListener = (
areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&
areHostMethods(window, 'addEventListener', 'removeEventListener')),
shouldUseAttachEventDetachEvent = (
areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
areHostMethods(window, 'attachEvent', 'detachEvent')),
listeners = { },
handlers = { };
if (shouldUseAddListenerRemoveListener) {
addListener = function (element, eventName, handler) {
element.addEventListener(eventName, handler, false);
};
removeListener = function (element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
};
}
else if (shouldUseAttachEventDetachEvent) {
addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
setElement(uid, element);
if (!listeners[uid]) {
listeners[uid] = { };
}
if (!listeners[uid][eventName]) {
listeners[uid][eventName] = [ ];
}
var listener = createListener(uid, handler);
listeners[uid][eventName].push(listener);
element.attachEvent('on' + eventName, listener.wrappedHandler);
};
removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element), listener;
if (listeners[uid] && listeners[uid][eventName]) {
for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) {
listener = listeners[uid][eventName][i];
if (listener && listener.handler === handler) {
element.detachEvent('on' + eventName, listener.wrappedHandler);
listeners[uid][eventName][i] = null;
}
}
}
};
}
else {
addListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (!handlers[uid]) {
handlers[uid] = { };
}
if (!handlers[uid][eventName]) {
handlers[uid][eventName] = [ ];
var existingHandler = element['on' + eventName];
if (existingHandler) {
handlers[uid][eventName].push(existingHandler);
}
element['on' + eventName] = createDispatcher(uid, eventName);
}
handlers[uid][eventName].push(handler);
};
removeListener = function (element, eventName, handler) {
var uid = getUniqueId(element);
if (handlers[uid] && handlers[uid][eventName]) {
var handlersForEvent = handlers[uid][eventName];
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
if (handlersForEvent[i] === handler) {
handlersForEvent.splice(i, 1);
}
}
}
};
}
fabric.util.addListener = addListener;
fabric.util.removeListener = removeListener;
var customEventListeners = { };
function observeEvent(eventName, handler) {
if (!customEventListeners[eventName]) {
customEventListeners[eventName] = [ ];
}
customEventListeners[eventName].push(handler);
}
function fireEvent(eventName, memo) {
var listenersForEvent = customEventListeners[eventName];
if (!listenersForEvent) return;
for (var i = 0, len = listenersForEvent.length; i < len; i++) {
listenersForEvent[i]({ memo: memo });
}
}
fabric.util.observeEvent = observeEvent;
fabric.util.fireEvent = fireEvent;
function getPointer(event) {
return { x: pointerX(event), y: pointerY(event) };
}
function pointerX(event) {
var docElement = document.documentElement,
body = document.body || { scrollLeft: 0 };
return event.pageX || ((typeof event.clientX != 'unknown' ? event.clientX : 0) +
(docElement.scrollLeft || body.scrollLeft) -
(docElement.clientLeft || 0));
}
function pointerY(event) {
var docElement = document.documentElement,
body = document.body || { scrollTop: 0 };
return event.pageY || ((typeof event.clientY != 'unknown' ? event.clientY : 0) +
(docElement.scrollTop || body.scrollTop) -
(docElement.clientTop || 0));
}
fabric.util.getPointer = getPointer;
})(this);
(function () {
function setStyle(element, styles) {
var elementStyle = element.style, match;
if (typeof styles === 'string') {
element.style.cssText += ';' + styles;
return styles.indexOf('opacity') > -1
? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1])
: element;
}
for (var property in styles) {
if (property === 'opacity') {
setOpacity(element, styles[property]);
}
else {
var normalizedProperty = (property === 'float' || property === 'cssFloat')
? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat')
: property;
elementStyle[normalizedProperty] = styles[property];
}
}
return element;
}
var parseEl = document.createElement('div'),
supportsOpacity = typeof parseEl.style.opacity === 'string',
supportsFilters = typeof parseEl.style.filter === 'string',
view = document.defaultView,
supportsGCS = view && typeof view.getComputedStyle !== 'undefined',
reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,
setOpacity = function (element) { return element; };
if (supportsOpacity) {
setOpacity = function(element, value) {
element.style.opacity = value;
return element;
};
}
else if (supportsFilters) {
setOpacity = function(element, value) {
var es = element.style;
if (element.currentStyle && !element.currentStyle.hasLayout) {
es.zoom = 1;
}
if (reOpacity.test(es.filter)) {
value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')');
es.filter = es.filter.replace(reOpacity, value);
}
else {
es.filter += ' alpha(opacity=' + (value * 100) + ')';
}
return element;
};
}
fabric.util.setStyle = setStyle;
})();
function getById(id) {
return typeof id === 'string' ? document.getElementById(id) : id;
}
function toArray(arrayLike) {
var arr = [ ], i = arrayLike.length;
while (i--) {
arr[i] = arrayLike[i];
}
return arr;
}
function makeElement(tagName, attributes) {
var el = document.createElement(tagName);
for (var prop in attributes) {
if (prop === 'class') {
el.className = attributes[prop];
}
else if (prop === 'for') {
el.htmlFor = attributes[prop];
}
else {
el.setAttribute(prop, attributes[prop]);
}
}
return el;
}
function addClass(element, className) {
if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) {
element.className += (element.className ? ' ' : '') + className;
}
}
function wrapElement(element, wrapper, attributes) {
if (typeof wrapper === 'string') {
wrapper = makeElement(wrapper, attributes);
}
if (element.parentNode) {
element.parentNode.replaceChild(wrapper, element);
}
wrapper.appendChild(element);
return wrapper;
}
function getElementOffset(element) {
var valueT = 0, valueL = 0;
do {
valueT += element.offsetTop || 0;
valueL += element.offsetLeft || 0;
element = element.offsetParent;
}
while (element);
return ({ left: valueL, top: valueT });
}
(function () {
var style = document.documentElement.style;
var selectProp = 'userSelect' in style
? 'userSelect'
: 'MozUserSelect' in style
? 'MozUserSelect'
: 'WebkitUserSelect' in style
? 'WebkitUserSelect'
: 'KhtmlUserSelect' in style
? 'KhtmlUserSelect'
: '';
function makeElementUnselectable(element) {
if (typeof element.onselectstart !== 'undefined') {
element.onselectstart = fabric.util.falseFunction;
}
if (selectProp) {
element.style[selectProp] = 'none';
}
else if (typeof element.unselectable == 'string') {
element.unselectable = 'on';
}
return element;
}
fabric.util.makeElementUnselectable = makeElementUnselectable;
})();
(function(){
function getScript(url, callback) {
var headEl = document.getElementsByTagName("head")[0],
scriptEl = document.createElement('script'),
loading = true;
scriptEl.type = 'text/javascript';
scriptEl.setAttribute('runat', 'server');
scriptEl.onload = scriptEl.onreadystatechange = function(e) {
if (loading) {
if (typeof this.readyState == 'string' &&
this.readyState !== 'loaded' &&
this.readyState !== 'complete') return;
loading = false;
callback(e || window.event);
scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null;
}
};
scriptEl.src = url;
headEl.appendChild(scriptEl);
}
function getScriptJaxer(url, callback) {
Jaxer.load(url);
callback();
}
fabric.util.getScript = getScript;
var Jaxer = this.Jaxer;
if (Jaxer && Jaxer.load) {
fabric.util.getScript = getScriptJaxer;
}
})();
function animate(options) {
options || (options = { });
var start = +new Date(),
duration = options.duration || 500,
finish = start + duration, time, pos,
onChange = options.onChange || function() { },
easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; },
startValue = 'startValue' in options ? options.startValue : 0,
endValue = 'endValue' in options ? options.endValue : 100,
isReversed = startValue > endValue
options.onStart && options.onStart();
var interval = setInterval(function() {
time = +new Date();
pos = time > finish ? 1 : (time - start) / duration;
onChange(isReversed
? (startValue - (startValue - endValue) * easing(pos))
: (startValue + (endValue - startValue) * easing(pos)));
if (time > finish) {
clearInterval(interval);
options.onComplete && options.onComplete();
}
}, 10);
}
fabric.util.getById = getById;
fabric.util.toArray = toArray;
fabric.util.makeElement = makeElement;
fabric.util.addClass = addClass;
fabric.util.wrapElement = wrapElement;
fabric.util.getElementOffset = getElementOffset;
fabric.util.animate = animate;
(function(){
function addParamToUrl(url, param) {
return url + (/\?/.test(url) ? '&' : '?') + param;
}
var makeXHR = (function() {
var factories = [
function() { return new ActiveXObject("Microsoft.XMLHTTP"); },
function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
function() { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); },
function() { return new XMLHttpRequest(); }
];
for (var i = factories.length; i--; ) {
try {
var req = factories[i]();
if (req) {
return factories[i];
}
}
catch (err) { }
}
})();
function emptyFn() { };
function request(url, options) {
options || (options = { });
var method = options.method ? options.method.toUpperCase() : 'GET',
onComplete = options.onComplete || function() { },
request = makeXHR(),
body;
request.onreadystatechange = function() {
if (request.readyState === 4) {
onComplete(request);
request.onreadystatechange = emptyFn;
}
};
if (method === 'GET') {
body = null;
if (typeof options.parameters == 'string') {
url = addParamToUrl(url, options.parameters);
}
}
request.open(method, url, true);
if (method === 'POST' || method === 'PUT') {
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
request.send(body);
return request;
};
fabric.util.request = request;
})();
})(this);
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend,
capitalize = fabric.util.string.capitalize,
clone = fabric.util.object.clone;
var attributesMap = {
'cx': 'left',
'x': 'left',
'cy': 'top',
'y': 'top',
'r': 'radius',
'fill-opacity': 'opacity',
'fill-rule': 'fillRule',
'stroke-width': 'strokeWidth',
'transform': 'transformMatrix'
};
/**
* Returns an object of attributes' name/value, given element and an array of attribute names
* Parses parent "g" nodes recursively upwards
* @param {DOMElement} element Element to parse
* @param {Array} attributes Array of attributes to parse
* @return {Object} object containing parsed attributes' names/values
*/
function parseAttributes(element, attributes) {
if (!element) {
return;
}
var value,
parsed,
parentAttributes = { };
if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) {
parentAttributes = fabric.parseAttributes(element.parentNode, attributes);
}
var ownAttributes = attributes.reduce(function(memo, attr) {
value = element.getAttribute(attr);
parsed = parseFloat(value);
if (value) {
if ((attr === 'fill' || attr === 'stroke') && value === 'none') {
value = '';
}
if (attr === 'fill-rule') {
value = (value === 'evenodd') ? 'destination-over' : value;
}
if (attr === 'transform') {
value = fabric.parseTransformAttribute(value);
}
if (attr in attributesMap) {
attr = attributesMap[attr];
}
memo[attr] = isNaN(parsed) ? value : parsed;
}
return memo;
}, { });
ownAttributes = extend(fabric.parseStyleAttribute(element), ownAttributes);
return extend(parentAttributes, ownAttributes);
};
/**
* @static
* @method fabric.parseTransformAttribute
* @param attributeValue {String} string containing attribute value
* @return {Array} array of 6 elements representing transformation matrix
*/
fabric.parseTransformAttribute = (function() {
function rotateMatrix(matrix, args) {
var angle = args[0];
matrix[0] = Math.cos(angle);
matrix[1] = Math.sin(angle);
matrix[2] = -Math.sin(angle);
matrix[3] = Math.cos(angle);
}
function scaleMatrix(matrix, args) {
var multiplierX = args[0],
multiplierY = (args.length === 2) ? args[1] : args[0];
matrix[0] = multiplierX;
matrix[3] = multiplierY;
}
function skewXMatrix(matrix, args) {
matrix[2] = args[0];
}
function skewYMatrix(matrix, args) {
matrix[1] = args[0];
}
function translateMatrix(matrix, args) {
matrix[4] = args[0];
if (args.length === 2) {
matrix[5] = args[1];
}
}
var iMatrix = [
1, // a
0, // b
0, // c
1, // d
0, // e
0 // f
],
number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)',
comma_wsp = '(?:\\s+,?\\s*|,\\s*)',
skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))',
skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))',
rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))',
scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
matrix = '(?:(matrix)\\s*\\(\\s*' +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' + comma_wsp +
'(' + number + ')' +
'\\s*\\))',
transform = '(?:' +
matrix + '|' +
translate + '|' +
scale + '|' +
rotate + '|' +
skewX + '|' +
skewY +
')',
transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')',
transform_list = '^\\s*(?:' + transforms + '?)\\s*$',
reTransformList = new RegExp(transform_list),
reTransform = new RegExp(transform);
return function(attributeValue) {
var matrix = iMatrix.concat();
if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) {
return matrix;
}
attributeValue.replace(reTransform, function(match) {
var m = new RegExp(transform).exec(match).filter(function (match) {
return (match !== '' && match != null);
}),
operation = m[1],
args = m.slice(2).map(parseFloat);
switch(operation) {
case 'translate':
translateMatrix(matrix, args);
break;
case 'rotate':
rotateMatrix(matrix, args);
break;
case 'scale':
scaleMatrix(matrix, args);
break;
case 'skewX':
skewXMatrix(matrix, args);
break;
case 'skewY':
skewYMatrix(matrix, args);
break;
case 'matrix':
matrix = args;
break;
}
})
return matrix;
}
})();
/**
* @static
* @method fabric.parsePointsAttribute
* @param points {String} points attribute string
* @return {Array} array of points
*/
function parsePointsAttribute(points) {
if (!points) return null;
points = points.trim();
var asPairs = points.indexOf(',') > -1;
points = points.split(/\s+/);
var parsedPoints = [ ];
if (asPairs) {
for (var i = 0, len = points.length; i < len; i++) {
var pair = points[i].split(',');
parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) });
}
}
else {
for (var i = 0, len = points.length; i < len; i+=2) {
parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) });
}
}
if (parsedPoints.length % 2 !== 0) {
}
return parsedPoints;
};
/**
* @static
* @method fabric.parseStyleAttribute
* @param element {SVGElement} element to parse
* @return {Object} objects with values parsed from style attribute of an element
*/
function parseStyleAttribute(element) {
var oStyle = { },
style = element.getAttribute('style');
if (style) {
if (typeof style == 'string') {
style = style.split(';');
style.pop();
oStyle = style.reduce(function(memo, current) {
var attr = current.split(':'),
key = attr[0].trim(),
value = attr[1].trim();
memo[key] = value;
return memo;
}, { });
}
else {
for (var prop in style) {
if (typeof style[prop] !== 'undefined') {
oStyle[prop] = style[prop];
}
}
}
}
return oStyle;
};
/**
* @static
* @method fabric.parseElements
* @param elements {Array} array of elements to parse
* @param options {Object} options object
* @return {Array} array of corresponding instances (transformed from SVG elements)
*/
function parseElements(elements, options) {
var _elements = elements.map(function(el) {
var klass = fabric[capitalize(el.tagName)];
if (klass && klass.fromElement) {
try {
return klass.fromElement(el, options);
}
catch(e) {
console.log(e.message || e);
}
}
});
_elements = _elements.filter(function(el) {
return el != null;
});
return _elements;
};
/**
* @static
* @method fabric.parseSVGDocument
* @param doc {SVGDocument} SVG document to parse
* @param callback {Function} callback to call when parsing is finished.
* Callback is being passed array of elements (parsed from a document)
*/
fabric.parseSVGDocument = (function(){
var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line)$/;
var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)';
var reViewBoxAttrValue = new RegExp(
'^' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*,?' +
'\\s*(' + reNum + '+)\\s*' +
'$'
);
function hasParentWithNodeName(element, parentNodeName) {
while (element && (element = element.parentNode)) {
if (element.nodeName === parentNodeName) {
return true;
}
}
return false;
}
return function(doc, callback) {
if (!doc) return;
var descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
var elements = descendants.filter(function(el) {
return reAllowedSVGTagNames.test(el.tagName) &&
!hasParentWithNodeName(el, 'pattern');
});
if (!elements || (elements && !elements.length)) return;
var viewBoxAttr = doc.getAttribute('viewBox'),
widthAttr = doc.getAttribute('width'),
heightAttr = doc.getAttribute('height'),
width = null,
height = null,
minX,
minY;
if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) {
minX = parseInt(viewBoxAttr[1], 10);
minY = parseInt(viewBoxAttr[2], 10);
width = parseInt(viewBoxAttr[3], 10);
height = parseInt(viewBoxAttr[4], 10);
}
width = widthAttr ? parseFloat(widthAttr) : width;
height = heightAttr ? parseFloat(heightAttr) : height;
var options = {
width: width,
height: height
};
var elements = fabric.parseElements(elements, clone(options));
if (!elements || (elements && !elements.length)) return;
if (callback) {
callback(elements, options);
}
};
})();
extend(fabric, {
parseAttributes: parseAttributes,
parseElements: parseElements,
parseStyleAttribute: parseStyleAttribute,
parsePointsAttribute: parsePointsAttribute
});
})();
(function() {
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
var fabric = this.fabric || (this.fabric = { });
if (fabric.Point) {
console.warn('fabric.Point is already defined');
return;
}
function Point(x, y) {
if (arguments.length > 0) {
this.init(x, y);
}
}
Point.prototype = {
constructor: Point,
init: function (x, y) {
this.x = x;
this.y = y;
},
add: function (that) {
return new Point(this.x + that.x, this.y + that.y);
},
addEquals: function (that) {
this.x += that.x;
this.y += that.y;
return this;
},
scalarAdd: function (scalar) {
return new Point(this.x + scalar, this.y + scalar);
},
scalarAddEquals: function (scalar) {
this.x += scalar;
this.y += scalar;
return this;
},
subtract: function (that) {
return new Point(this.x - that.x, this.y - that.y);
},
subtractEquals: function (that) {
this.x -= that.x;
this.y -= that.y;
return this;
},
scalarSubtract: function (scalar) {
return new Point(this.x - scalar, this.y - scalar);
},
scalarSubtractEquals: function (scalar) {
this.x -= scalar;
this.y -= scalar;
return this;
},
multiply: function (scalar) {
return new Point(this.x * scalar, this.y * scalar);
},
multiplyEquals: function (scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
},
divide: function (scalar) {
return new Point(this.x / scalar, this.y / scalar);
},
divideEquals: function (scalar) {
this.x /= scalar;
this.y /= scalar;
return this;
},
eq: function (that) {
return (this.x == that.x && this.y == that.y);
},
lt: function (that) {
return (this.x < that.x && this.y < that.y);
},
lte: function (that) {
return (this.x <= that.x && this.y <= that.y);
},
gt: function (that) {
return (this.x > that.x && this.y > that.y);
},
gte: function (that) {
return (this.x >= that.x && this.y >= that.y);
},
lerp: function (that, t) {
return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t);
},
distanceFrom: function (that) {
var dx = this.x - that.x,
dy = this.y - that.y;
return Math.sqrt(dx * dx + dy * dy);
},
min: function (that) {
return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y));
},
max: function (that) {
return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y));
},
toString: function () {
return this.x + "," + this.y;
},
setXY: function (x, y) {
this.x = x;
this.y = y;
},
setFromPoint: function (that) {
this.x = that.x;
this.y = that.y;
},
swap: function (that) {
var x = this.x,
y = this.y;
this.x = that.x;
this.y = that.y;
that.x = x;
that.y = y;
}
};
fabric.Point = Point;
})();
(function() {
/* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */
var global = this,
fabric = global.fabric || (global.fabric = { });
if (fabric.Intersection) {
console.warn('fabric.Intersection is already defined');
return;
}
function Intersection(status) {
if (arguments.length > 0) {
this.init(status);
}
}
Intersection.prototype.init = function (status) {
this.status = status;
this.points = [];
};
Intersection.prototype.appendPoint = function (point) {
this.points.push(point);
};
Intersection.prototype.appendPoints = function (points) {
this.points = this.points.concat(points);
};
Intersection.intersectLineLine = function (a1, a2, b1, b2) {
var result,
ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
if (u_b != 0) {
var ua = ua_t / u_b,
ub = ub_t / u_b;
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
result = new Intersection("Intersection");
result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)));
}
else {
result = new Intersection("No Intersection");
}
}
else {
if (ua_t == 0 || ub_t == 0) {
result = new Intersection("Coincident");
}
else {
result = new Intersection("Parallel");
}
}
return result;
};
Intersection.intersectLinePolygon = function(a1,a2,points){
var result = new Intersection("No Intersection"),
length = points.length;
for (var i = 0; i < length; i++) {
var b1 = points[i],
b2 = points[(i+1) % length],
inter = Intersection.intersectLineLine(a1, a2, b1, b2);
result.appendPoints(inter.points);
}
if (result.points.length > 0) {
result.status = "Intersection";
}
return result;
};
Intersection.intersectPolygonPolygon = function (points1, points2) {
var result = new Intersection("No Intersection"),
length = points1.length;
for (var i = 0; i < length; i++) {
var a1 = points1[i],
a2 = points1[(i+1) % length],
inter = Intersection.intersectLinePolygon(a1, a2, points2);
result.appendPoints(inter.points);
}
if (result.points.length > 0) {
result.status = "Intersection";
}
return result;
};
Intersection.intersectPolygonRectangle = function (points, r1, r2) {
var min = r1.min(r2),
max = r1.max(r2),
topRight = new fabric.Point(max.x, min.y),
bottomLeft = new fabric.Point(min.x, max.y),
inter1 = Intersection.intersectLinePolygon(min, topRight, points),
inter2 = Intersection.intersectLinePolygon(topRight, max, points),
inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points),
inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points),
result = new Intersection("No Intersection");
result.appendPoints(inter1.points);
result.appendPoints(inter2.points);
result.appendPoints(inter3.points);
result.appendPoints(inter4.points);
if (result.points.length > 0) {
result.status="Intersection";
}
return result;
};
fabric.Intersection = Intersection;
})();
(function() {
var fabric = this.fabric || (this.fabric = { });
if (fabric.Color) {
console.warn('fabric.Color is already defined.');
return;
}
fabric.Color = Color;
/**
* @constructor
* @param {String} color (optional) in hex or rgb(a) format
*/
function Color(color) {
if (!color) {
this.setSource([0, 0, 0, 1]);
}
else {
this._tryParsingColor(color);
}
}
/**
* @private
* @method _tryParsingColor
*/
Color.prototype._tryParsingColor = function(color) {
var source = Color.sourceFromHex(color);
if (!source) {
source = Color.sourceFromRgb(color);
}
if (source) {
this.setSource(source);
}
}
/**
* @method getSource
* @return {Array}
*/
Color.prototype.getSource = function() {
return this._source;
};
/**
* @method setSource
* @param {Array} source
*/
Color.prototype.setSource = function(source) {
this._source = source;
};
/**
* @method toRgb
* @return {String} ex: rgb(0-255,0-255,0-255)
*/
Color.prototype.toRgb = function() {
var source = this.getSource();
return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')';
};
/**
* @method toRgba
* @return {String} ex: rgba(0-255,0-255,0-255,0-1)
*/
Color.prototype.toRgba = function() {
var source = this.getSource();
return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')';
};
/**
* @method toHex
* @return {String} ex: FF5555
*/
Color.prototype.toHex = function() {
var source = this.getSource();
var r = source[0].toString(16);
r = (r.length == 1) ? ('0' + r) : r;
var g = source[1].toString(16);
g = (g.length == 1) ? ('0' + g) : g;
var b = source[2].toString(16);
b = (b.length == 1) ? ('0' + b) : b;
return r.toUpperCase() + g.toUpperCase() + b.toUpperCase();
};
/**
* @method getAlpha
* @return {Number} 0-1
*/
Color.prototype.getAlpha = function() {
return this.getSource()[3];
};
/**
* @method setAlpha
* @param {Number} 0-1
* @return {Color} thisArg
*/
Color.prototype.setAlpha = function(alpha) {
var source = this.getSource();
source[3] = alpha;
this.setSource(source);
return this;
};
/**
* Transforms color to its grayscale representation
* @method toGrayscale
* @return {Color} thisArg
*/
Color.prototype.toGrayscale = function() {
var source = this.getSource(),
average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10),
currentAlpha = source[3];
this.setSource([average, average, average, currentAlpha]);
return this;
};
/**
* Transforms color to its black and white representation
* @method toGrayscale
* @return {Color} thisArg
*/
Color.prototype.toBlackWhite = function(threshold) {
var source = this.getSource(),
average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0),
currentAlpha = source[3],
threshold = threshold || 127;
average = (Number(average) < Number(threshold)) ? 0 : 255;
this.setSource([average, average, average, currentAlpha]);
return this;
};
/**
* Overlays color with another color
* @method overlayWith
* @param {Color} otherColor
* @return {Color} thisArg
*/
Color.prototype.overlayWith = function(otherColor) {
otherColor = new Color(otherColor);
var result = [],
alpha = this.getAlpha(),
otherAlpha = 0.5,
source = this.getSource(),
otherSource = otherColor.getSource();
for (var i = 0; i < 3; i++) {
result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha)));
}
result[4] = alpha;
this.setSource(result);
return this;
};
/**
* @static
* @field reRGBa
*/
Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/;
/**
* @static
* @field reHex
*/
Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i;
/**
* @method fromRgb
* @param {String} color ex: rgb(0-255,0-255,0-255)
* @return {Color}
*/
Color.fromRgb = function(color) {
return Color.fromSource(Color.sourceFromRgb(color));
};
/**
* @method sourceFromRgb
* @param {String} color ex: rgb(0-255,0-255,0-255)
* @return {Array} source
*/
Color.sourceFromRgb = function(color) {
var match = color.match(Color.reRGBa);
if (match) {
return [
parseInt(match[1], 10),
parseInt(match[2], 10),
parseInt(match[3], 10),
match[4] ? parseFloat(match[4]) : 1
];
}
};
/**
* @static
* @method fromRgba
* @return {Color}
*/
Color.fromRgba = Color.fromRgb;
/**
* @static
* @method fromHex
* @return {Color}
*/
Color.fromHex = function(color) {
return Color.fromSource(Color.sourceFromHex(color));
};
/**
* @static
* @method sourceFromHex
* @param {String} ex: FF5555
* @return {Array} source
*/
Color.sourceFromHex = function(color) {
if (color.match(Color.reHex)) {
var value = color.slice(color.indexOf('#') + 1),
isShortNotation = (value.length === 3),
r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2),
g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4),
b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6);
return [
parseInt(r, 16),
parseInt(g, 16),
parseInt(b, 16),
1
];
}
};
/**
* @static
* @method fromSource
* @return {Color}
*/
Color.fromSource = function(source) {
var oColor = new Color();
oColor.setSource(source);
return oColor;
};
})();
(function () {
if (fabric.Element) {
console.warn('fabric.Element is already defined.');
return;
}
var global = this,
window = global.window,
document = window.document,
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 Container element for the canvas.
*/
fabric.Element = function (el, config) {
/**
* 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
};
config = config || { };
this._initElement(el);
this._initConfig(config);
if (config.overlayImage) {
this.setOverlayImage(config.overlayImage);
}
if (config.afterRender) {
this.afterRender = config.afterRender;
}
this._createCanvasBackground();
this._createCanvasContainer();
this._initEvents();
this.calcOffset();
};
extend(fabric.Element.prototype, {
selectionColor: 'rgba(100, 100, 255, 0.3)', // blue
selectionBorderColor: 'rgba(255, 255, 255, 0.3)',
freeDrawingColor: 'rgb(0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
freeDrawingLineWidth: 1,
selectionLineWidth: 1,
includeDefaultValues: true,
shouldCacheImages: false,
CANVAS_WIDTH: 600,
CANVAS_HEIGHT: 600,
onBeforeScaleRotate: function () {
/* NOOP */
},
onFpsUpdate: function() {
/* 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;
},
/**
* @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) {
if (url) {
var _this = this, img = new Image();
img.onload = function () {
_this.overlayImage = img;
if (callback) {
callback();
}
img = img.onload = null;
};
img.src = url;
}
return this;
},
/**
* canvas class's initialization method. This method is automatically
* called by constructor, and sets up all DOM references for
* pre-existing markup, and creates required markup if it is not
* already present.
* @method _initElement
* @param canvasEl {HTMLElement|String} canvasEl canvas element
*
*/
_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
*/
_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
*/
_setElementStyle: function (width, height) {
fabric.util.setStyle(this.getElement(), {
position: 'absolute',
width: width + 'px',
height: height + 'px',
left: 0,
top: 0
});
},
/**
* For now we 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 main mouse listeners to the whole 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') {
G_vmlCanvasManager.initElement(element);
}
if (typeof element.getContext === 'undefined') {
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;
},
/**
* @method setWidth
* @param {Number} width value to set width to
* @return {fabric.Element} instance
* @chainable true
*/
setWidth: function (value) {
return this._setDimension('width', value);
},
/**
* @method setHeight
* @param {Number} height value to set height to
* @return {fabric.Element} instance
* @chainable true
*/
setHeight: function (value) {
return this._setDimension('height', value);
},
setDimensions: function(dimensions) {
for (var prop in dimensions) {
this._setDimension(prop, dimensions[prop]);
}
return this;
},
/**
* private helper for setting width/height
* @method _setDimensions
* @private
* @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';
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;
}
var i = this._objects.length;
while (i--) {
this._objects[i].setCoords();
}
if (target.hasStateChanged()) {
target.isMoving = false;
fireEvent('object:modified', { target: target });
}
}
this._currentTransform = null;
if (this._groupSelector) {
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');
}
this._groupSelector = null;
this.renderAll();
this._setCursorFromEvent(e, target);
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);
this._captureDrawingPath(e);
return;
}
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 {
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);
}
}
this.renderAll();
},
/**
* @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')) {
target = this.findTarget(e, true);
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) {
this.removeActiveGroup();
}
}
else {
activeGroup.add(target);
}
fireEvent('group:selected', { target: activeGroup });
activeGroup.setActive(true);
}
else {
if (this._activeObject) {
if (target !== this._activeObject) {
var group = new fabric.Group([ this._activeObject,target ]);
this.setActiveGroup(group);
activeGroup = this.getActiveGroup();
}
}
target.setActive(true);
}
if (activeGroup) {
activeGroup.saveCoords();
}
},
_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';
},
_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();
},
_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, ' ');
}
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;
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) {
var style = this._element.style;
var target = this.findTarget(e);
if (!target) {
for (var i = this._objects.length; i--; ) {
if (!this._objects[i].active) {
this._objects[i].setActive(false);
}
}
style.cursor = 'default';
}
else {
this._setCursorFromEvent(e, target);
if (target.isActive()) {
target.setCornersVisibility && target.setCornersVisibility(true);
}
}
}
else {
var pointer = getPointer(e),
x = pointer.x,
y = pointer.y;
this._currentTransform.target.isMoving = true;
if (this._currentTransform.action === 'rotate') {
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);
}
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();
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);
}
}
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 an object to canvas and renders canvas
* An object should be an instance 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) {
ctx.clearRect(0, 0, this._config.width, this._config.height);
return this;
},
/**
* Clears all contexts of canvas element
* @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) {
var w = this._config.width,
h = this._config.height;
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]);
}
}
}
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);
}
if (this._groupSelector) {
this._drawSelection();
}
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;
var iLines = target._getImageLines(target.oCoords),
xpoints = target._findCrossPoints(x, y, iLines);
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);
var activeGroup = this.getActiveGroup();
if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) {
target = activeGroup;
return target;
}
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;
},
/**
* @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;
},
/**
* @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
* @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 object {fabric.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
* @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;
},
/**
* @method straightenObject
* @param {fabric.Object} object Object to straighten
* @return {fabric.Element} thisArg
* @chainable
*/
straightenObject: function (object) {
object.straighten();
this.renderAll();
return this;
},
/**
* @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){
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 json {String} json string
* @param callback {Function} 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;
},
_enlivenObjects: function (objects, callback) {
var numLoadedImages = 0,
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();
}
},
loadFromDatalessJSON: function (json, callback) {
if (!json) {
return;
}
var serialized = (typeof json === 'string')
? JSON.parse(json)
: json;
if (!serialized || (serialized && !serialized.objects)) return;
this.clear();
this.backgroundColor = serialized.background;
this._enlivenDatalessObjects(serialized.objects, callback);
},
_enlivenDatalessObjects: function (objects, callback) {
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 () {
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);
if (!(object instanceof fabric.PathGroup)) {
extend(object, obj);
if (typeof obj.angle !== 'undefined') {
object.setAngle(obj.angle);
}
}
onObjectLoaded(object, index);
});
}
}
}, this);
}
catch(e) {
console.log(e.message);
}
},
/**
* Loads an image from URL
* @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) {
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);
}
}
if (imgCache[url]) {
checkIfLoaded();
}
else {
var imgEl = new Image();
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);
}
}
})(),
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 {
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() {
console.log('ERROR!');
}
},
_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 `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
* @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
* @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
* @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 (idx !== 0) {
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
* @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 (idx !== objects.length-1) {
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 an active object
* @method removeActiveObject
* @return {fabric.Element} thisArg
* @chainable
*/
removeActiveObject: function () {
if (this._activeObject) {
this._activeObject.setActive(false);
}
this._activeObject = null;
return this;
},
/**
* Sets current group to a speicified one
* @method setActiveGroup
* @param group {fabric.Group} group to set as a current one
* @return {fabric.Element} thisArg
* @chainable
*/
setActiveGroup: function (group) {
this._activeGroup = group;
return this;
},
/**
* Returns current group
* @method getActiveGroup
* @return {fabric.Group} Current group
*/
getActiveGroup: function () {
return this._activeGroup;
},
/**
* Removes current group
* @method removeActiveGroup
* @return {fabric.Element} thisArg
*/
removeActiveGroup: function () {
var g = this.getActiveGroup();
if (g) {
g.destroy();
}
return this.setActiveGroup(null);
},
/**
* @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;
},
/**
* @method clone
* @param {Object} callback OPTIONAL expects `onBeforeClone` and `onAfterClone` functions
* @return {fabric.Element} instance clone
*/
clone: function (callback) {
var el = document.createElement('canvas');
el.width = this.getWidth();
el.height = this.getHeight();
var clone = this.__clone || (this.__clone = new fabric.Element(el));
return clone.loadFromJSON(JSON.stringify(this.toJSON()), function () {
if (callback) {
callback(clone);
}
});
},
_toDataURL: function (format, callback) {
this.clone(function (clone) {
callback(clone.toDataURL(format));
});
},
_toDataURLWithMultiplier: function (format, multiplier, callback) {
this.clone(function (clone) {
callback(clone.toDataURLWithMultiplier(format, multiplier));
});
},
_resizeImageToFit: function (imgEl) {
var imageWidth = imgEl.width || imgEl.offsetWidth,
widthScaleFactor = this.getWidth() / imageWidth;
if (imageWidth) {
imgEl.width = imageWidth * widthScaleFactor;
}
},
/* stubs */
cache: {
has: function (name, callback){ callback(false) },
get: function () { },
set: function () { }
}
});
/**
* Returns a string representation of an instance
* @method toString
* @return {String} string representation of an instance
*/
fabric.Element.prototype.toString = function () {
return '#<fabric.Element (' + this.complexity() + '): '+
'{ objects: ' + this.getObjects().length + ' }>';
};
extend(fabric.Element, {
/**
* @property EMPTY_JSON
*/
EMPTY_JSON: '{"objects": [], "background": "white"}',
/**
* @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
* @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
* @method toJSON
* @return {String} json string
*/
fabric.Element.prototype.toJSON = fabric.Element.prototype.toObject;
})();
(function(){
var global = this,
/**
* @name Canvas
* @namespace
*/
fabric = global.fabric || (global.fabric = { }),
extend = fabric.util.object.extend,
clone = fabric.util.object.clone,
toFixed = fabric.util.toFixed,
capitalize = fabric.util.string.capitalize,
getPointer = fabric.util.getPointer,
slice = Array.prototype.slice
if (fabric.Object) {
return;
}
/**
* @class Object
* @memberOf Canvas
*/
fabric.Object = fabric.util.createClass({
type: 'object',
includeDefaultValues: true,
/**
* @constant
*/
NUM_FRACTION_DIGITS: 2,
/**
* @constant
*/
FX_DURATION: 500,
/**
* @constant
*/
FX_TRANSITION: 'decel',
/**
* @constant
*/
MIN_SCALE_LIMIT: 0.1,
/**
* @field
*/
stateProperties: ('top left width height scaleX scaleY flipX flipY ' +
'theta angle opacity cornersize fill overlayFill stroke ' +
'strokeWidth fillRule borderScaleFactor transformMatrix').split(' '),
/**
* @field
*/
options: {
top: 0,
left: 0,
width: 100,
height: 100,
scaleX: 1,
scaleY: 1,
flipX: false,
flipY: false,
theta: 0,
opacity: 1,
angle: 0,
cornersize: 10,
padding: 0,
borderColor: 'rgba(102,153,255,0.75)',
cornerColor: 'rgba(102,153,255,0.5)',
fill: 'rgb(0,0,0)',
overlayFill: null,
stroke: null,
strokeWidth: 1,
fillRule: 'source-over',
borderOpacityWhenMoving: 0.4,
borderScaleFactor: 1,
transformMatrix: null
},
callSuper: function(methodName) {
var fn = this.constructor.superclass.prototype[methodName];
return (arguments.length > 1)
? fn.apply(this, slice.call(arguments, 1))
: fn.call(this);
},
/**
* @constructs
* @param options {Object} options
*/
initialize: function(options) {
this.setOptions(options);
this._importProperties();
this.originalState = { };
this.setCoords();
this.saveState();
},
setOptions: function(options) {
this.options = extend(this._getOptions(), options);
},
/**
* @private
* @method _getOptions
*/
_getOptions: function() {
return extend(clone(this._getSuperOptions()), this.options);
},
/**
* @private
* @method _getSuperOptions
*/
_getSuperOptions: function() {
var c = this.constructor;
if (c) {
var s = c.superclass;
if (s) {
var p = s.prototype;
if (p && typeof p._getOptions == 'function') {
return p._getOptions();
}
}
}
return { };
},
/**
* @private
* @method _importProperties
*/
_importProperties: function() {
this.stateProperties.forEach(function(prop) {
(prop === 'angle')
? this.setAngle(this.options[prop])
: (this[prop] = this.options[prop]);
}, this);
},
/**
* @method transform
* @param {CanvasRenderingContext2D} ctx Context
*/
transform: function(ctx) {
ctx.globalAlpha = this.opacity;
ctx.translate(this.left, this.top);
ctx.rotate(this.theta);
ctx.scale(
this.scaleX * (this.flipX ? -1 : 1),
this.scaleY * (this.flipY ? -1 : 1)
);
},
/**
* Returns an object representation of an instance
* @method toObject
* @return {Object}
*/
toObject: function() {
var object = {
type: this.type,
left: toFixed(this.left, this.NUM_FRACTION_DIGITS),
top: toFixed(this.top, this.NUM_FRACTION_DIGITS),
width: toFixed(this.width, this.NUM_FRACTION_DIGITS),
height: toFixed(this.height, this.NUM_FRACTION_DIGITS),
fill: this.fill,
overlayFill: this.overlayFill,
stroke: this.stroke,
strokeWidth: this.strokeWidth,
scaleX: toFixed(this.scaleX, this.NUM_FRACTION_DIGITS),
scaleY: toFixed(this.scaleY, this.NUM_FRACTION_DIGITS),
angle: toFixed(this.getAngle(), this.NUM_FRACTION_DIGITS),
flipX: this.flipX,
flipY: this.flipY,
opacity: toFixed(this.opacity, this.NUM_FRACTION_DIGITS)
};
if (!this.includeDefaultValues) {
object = this._removeDefaultValues(object);
}
return object;
},
/**
* @method toDatalessObject
*/
toDatalessObject: function() {
return this.toObject();
},
/**
* @private
* @method _removeDefaultValues
*/
_removeDefaultValues: function(object) {
var defaultOptions = fabric.Object.prototype.options;
this.stateProperties.forEach(function(prop) {
if (object[prop] === defaultOptions[prop]) {
delete object[prop];
}
});
return object;
},
/**
* Returns true if an object is in its active state
* @return {Boolean} true if an object is in its active state
*/
isActive: function() {
return !!this.active;
},
/**
* Sets state of an object - `true` makes it active, `false` - inactive
* @param {Boolean} active
* @return {fabric.Object} thisArg
* @chainable
*/
setActive: function(active) {
this.active = !!active;
return this;
},
/**
* Returns a string representation of an instance
* @return {String}
*/
toString: function() {
return "#<fabric." + capitalize(this.type) + ">";
},
/**
* Basic setter
* @param {Any} property
* @param {Any} value
* @return {fabric.Object} thisArg
* @chainable
*/
set: function(property, value) {
var shouldConstrainValue = (property === 'scaleX' || property === 'scaleY') && value < this.MIN_SCALE_LIMIT;
if (shouldConstrainValue) {
value = this.MIN_SCALE_LIMIT;
}
if (property === 'angle') {
this.setAngle(value);
}
else {
this[property] = value;
}
return this;
},
/**
* Toggles specified property from `true` to `false` or from `false` to `true`
* @method toggle
* @param {String} property property to toggle
* @return {fabric.Object} thisArg
* @chainable
*/
toggle: function(property) {
var value = this.get(property);
if (typeof value === 'boolean') {
this.set(property, !value);
}
return this;
},
/**
* @method setSourcePath
* @param {String} value
* @return {fabric.Object} thisArg
* @chainable
*/
setSourcePath: function(value) {
this.sourcePath = value;
return this;
},
/**
* Basic getter
* @method get
* @param {Any} property
* @return {Any} value of a property
*/
get: function(property) {
return (property === 'angle')
? this.getAngle()
: this[property];
},
/**
* @method render
* @param {CanvasRenderingContext2D} ctx context to render on
* @param {Boolean} noTransform
*/
render: function(ctx, noTransform) {
if (this.width === 0 || this.height === 0) return;
ctx.save();
var m = this.transformMatrix;
if (m) {
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
if (this.stroke) {
ctx.lineWidth = this.strokeWidth;
ctx.strokeStyle = this.stroke;
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill;
}
this._render(ctx, noTransform);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
}
ctx.restore();
},
/**
* Returns width of an object
* @method getWidth
* @return {Number} width value
*/
getWidth: function() {
return this.width * this.scaleX;
},
/**
* Returns height of an object
* @method getHeight
* @return {Number} height value
*/
getHeight: function() {
return this.height * this.scaleY;
},
/**
* Scales an object (equally by x and y)
* @method scale
* @param value {Number} scale factor
* @return {fabric.Object} thisArg
* @chainable
*/
scale: function(value) {
this.scaleX = value;
this.scaleY = value;
return this;
},
/**
* Scales an object to a given width (scaling by x/y equally)
* @method scaleToWidth
* @param value {Number} new width value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToWidth: function(value) {
return this.scale(value / this.width);
},
/**
* Scales an object to a given height (scaling by x/y equally)
* @method scaleToHeight
* @param value {Number} new height value
* @return {fabric.Object} thisArg
* @chainable
*/
scaleToHeight: function(value) {
return this.scale(value / this.height);
},
/**
* Sets object opacity
* @method setOpacity
* @param value {Number} value 0-1
* @return {fabric.Object} thisArg
* @chainable
*/
setOpacity: function(value) {
this.set('opacity', value);
return this;
},
/**
* Returns object's angle value
* @method getAngle
* @return {Number} angle value
*/
getAngle: function() {
return this.theta * 180 / Math.PI;
},
/**
* Sets object's angle
* @method setAngle
* @param value {Number} angle value
* @return {Object} thisArg
*/
setAngle: function(value) {
this.theta = value / 180 * Math.PI;
this.angle = value;
return this;
},
/**
* Sets corner position coordinates based on current angle, width and height.
* @method setCoords
* return {fabric.Object} thisArg
* @chainable
*/
setCoords: function() {
this.currentWidth = this.width * this.scaleX;
this.currentHeight = this.height * this.scaleY;
this._hypotenuse = Math.sqrt(
Math.pow(this.currentWidth / 2, 2) +
Math.pow(this.currentHeight / 2, 2));
this._angle = Math.atan(this.currentHeight / this.currentWidth);
var offsetX = Math.cos(this._angle + this.theta) * this._hypotenuse,
offsetY = Math.sin(this._angle + this.theta) * this._hypotenuse,
theta = this.theta,
sinTh = Math.sin(theta),
cosTh = Math.cos(theta);
var tl = {
x: this.left - offsetX,
y: this.top - offsetY
};
var tr = {
x: tl.x + (this.currentWidth * cosTh),
y: tl.y + (this.currentWidth * sinTh)
};
var br = {
x: tr.x - (this.currentHeight * sinTh),
y: tr.y + (this.currentHeight * cosTh)
};
var bl = {
x: tl.x - (this.currentHeight * sinTh),
y: tl.y + (this.currentHeight * cosTh)
};
var ml = {
x: tl.x - (this.currentHeight/2 * sinTh),
y: tl.y + (this.currentHeight/2 * cosTh)
};
var mt = {
x: tl.x + (this.currentWidth/2 * cosTh),
y: tl.y + (this.currentWidth/2 * sinTh)
};
var mr = {
x: tr.x - (this.currentHeight/2 * sinTh),
y: tr.y + (this.currentHeight/2 * cosTh)
}
var mb = {
x: bl.x + (this.currentWidth/2 * cosTh),
y: bl.y + (this.currentWidth/2 * sinTh)
}
this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb };
this._setCornerCoords();
return this;
},
/**
* Draws borders of an object's bounding box.
* Requires public properties: width, height
* Requires public options: padding, borderColor
* @method drawBorders
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawBorders: function(ctx) {
var o = this.options,
padding = o.padding,
padding2 = padding * 2;
ctx.save();
ctx.globalAlpha = this.isMoving ? o.borderOpacityWhenMoving : 1;
ctx.strokeStyle = o.borderColor;
var scaleX = 1 / (this.scaleX < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleX),
scaleY = 1 / (this.scaleY < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleY);
ctx.lineWidth = 1 / this.borderScaleFactor;
ctx.scale(scaleX, scaleY);
var w = this.getWidth(),
h = this.getHeight();
ctx.strokeRect(
~~(-(w / 2) - padding) + 0.5, // offset needed to make lines look sharper
~~(-(h / 2) - padding) + 0.5,
~~(w + padding2),
~~(h + padding2)
);
ctx.restore();
return this;
},
/**
* Draws corners of an object's bounding box.
* Requires public properties: width, height, scaleX, scaleY
* Requires public options: cornersize, padding
* @method drawCorners
* @param {CanvasRenderingContext2D} ctx Context to draw on
* @return {fabric.Object} thisArg
* @chainable
*/
drawCorners: function(ctx) {
var size = this.options.cornersize,
size2 = size / 2,
padding = this.options.padding,
left = -(this.width / 2),
top = -(this.height / 2),
_left,
_top,
sizeX = size / this.scaleX,
sizeY = size / this.scaleY,
scaleOffsetY = (padding + size2) / this.scaleY,
scaleOffsetX = (padding + size2) / this.scaleX,
scaleOffsetSizeX = (padding + size2 - size) / this.scaleX,
scaleOffsetSizeY = (padding + size2 - size) / this.scaleY;
ctx.save();
ctx.globalAlpha = this.isMoving ? this.options.borderOpacityWhenMoving : 1;
ctx.fillStyle = this.options.cornerColor;
_left = left - scaleOffsetX;
_top = top - scaleOffsetY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left + this.width - scaleOffsetX;
_top = top - scaleOffsetY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left - scaleOffsetX;
_top = top + this.height + scaleOffsetSizeY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left + this.width + scaleOffsetSizeX;
_top = top + this.height + scaleOffsetSizeY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left + this.width/2 - scaleOffsetX;
_top = top - scaleOffsetY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left + this.width/2 - scaleOffsetX;
_top = top + this.height + scaleOffsetSizeY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left + this.width + scaleOffsetSizeX;
_top = top + this.height/2 - scaleOffsetY;
ctx.fillRect(_left, _top, sizeX, sizeY);
_left = left - scaleOffsetX;
_top = top + this.height/2 - scaleOffsetY;
ctx.fillRect(_left, _top, sizeX, sizeY);
ctx.restore();
return this;
},
/**
* Clones an instance
* @method clone
* @param {Object} options object
* @return {fabric.Object} clone of an instance
*/
clone: function(options) {
if (this.constructor.fromObject) {
return this.constructor.fromObject(this.toObject(), options);
}
return new fabric.Object(this.toObject());
},
/**
* Creates an instance of fabric.Image out of an object
* @method cloneAsImage
* @param callback {Function} callback, invoked with an instance as a first argument
* @return {fabric.Object} thisArg
* @chainable
*/
cloneAsImage: function(callback) {
if (fabric.Image) {
var i = new Image();
i.onload = function() {
if (callback) {
callback(new fabric.Image(i), orig);
}
i = i.onload = null;
}
var orig = {
angle: this.get('angle'),
flipX: this.get('flipX'),
flipY: this.get('flipY')
}
this.set('angle', 0).set('flipX', false).set('flipY', false);
i.src = this.toDataURL();
}
return this;
},
/**
* Converts an object into a data-url-like string
* @method toDataURL
* @return {String} string of data
*/
toDataURL: function() {
var el = document.createElement('canvas');
el.width = this.getWidth();
el.height = this.getHeight();
fabric.util.wrapElement(el, 'div');
var canvas = new fabric.Element(el);
canvas.backgroundColor = 'transparent';
canvas.renderAll();
var clone = this.clone();
clone.left = el.width / 2;
clone.top = el.height / 2;
clone.setActive(false);
canvas.add(clone);
var data = canvas.toDataURL('png');
canvas.dispose();
canvas = clone = null;
return data;
},
/**
* @method hasStateChanged
* @return {Boolean} true if instance' state has changed
*/
hasStateChanged: function() {
return this.stateProperties.some(function(prop) {
return this[prop] !== this.originalState[prop];
}, this);
},
/**
* @method saveState
* @return {fabric.Object} thisArg
* @chainable
*/
saveState: function() {
this.stateProperties.forEach(function(prop) {
this.originalState[prop] = this.get(prop);
}, this);
return this;
},
/**
* Returns true if object intersects with an area formed by 2 points
* @method intersectsWithRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
intersectsWithRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
var intersection = fabric.Intersection.intersectPolygonRectangle(
[tl, tr, br, bl],
selectionTL,
selectionBR
);
return (intersection.status === 'Intersection');
},
/**
* Returns true if object intersects with another object
* @method intersectsWithObject
* @param {Object} other Object to test
* @return {Boolean}
*/
intersectsWithObject: function(other) {
function getCoords(oCoords) {
return {
tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br: new fabric.Point(oCoords.br.x, oCoords.br.y)
}
}
var thisCoords = getCoords(this.oCoords),
otherCoords = getCoords(other.oCoords);
var intersection = fabric.Intersection.intersectPolygonPolygon(
[thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
[otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
);
return (intersection.status === 'Intersection');
},
/**
* Returns true if object is fully contained within area formed by 2 points
* @method isContainedWithinRect
* @param {Object} selectionTL
* @param {Object} selectionBR
* @return {Boolean}
*/
isContainedWithinRect: function(selectionTL, selectionBR) {
var oCoords = this.oCoords,
tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
return tl.x > selectionTL.x
&& tr.x < selectionBR.x
&& tl.y > selectionTL.y
&& bl.y < selectionBR.y;
},
/**
* @method isType
* @param type {String} type to check against
* @return {Boolean} true if specified type is identical to the type of instance
*/
isType: function(type) {
return this.type === type;
},
/**
* Determines which one of the four corners has been clicked
* @method _findTargetCorner
* @private
* @param e {Event} event object
* @param offset {Object} canvas offset
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
*/
_findTargetCorner: function(e, offset) {
var pointer = getPointer(e),
ex = pointer.x - offset.left,
ey = pointer.y - offset.top,
xpoints,
lines;
for (var i in this.oCoords) {
lines = this._getImageLines(this.oCoords[i].corner, i);
xpoints = this._findCrossPoints(ex, ey, lines);
if (xpoints % 2 == 1 && xpoints != 0) {
this.__corner = i;
return i;
}
}
return false;
},
/**
* Helper method to determine how many cross points are between the 4 image edges
* and the horizontal line determined by the position of our mouse when clicked on canvas
* @method _findCrossPoints
* @private
* @param ex {Number} x coordinate of the mouse
* @param ey {Number} y coordinate of the mouse
* @param oCoords {Object} Coordinates of the image being evaluated
*/
_findCrossPoints: function(ex, ey, oCoords) {
var b1, b2, a1, a2, xi, yi,
xcount = 0,
iLine;
for (var lineKey in oCoords) {
iLine = oCoords[lineKey];
if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
continue;
}
if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
continue;
}
if ((iLine.o.x == iLine.d.x) && (iLine.o.x >= ex)) {
xi = iLine.o.x;
yi = ey;
}
else {
b1 = 0;
b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
a1 = ey-b1*ex;
a2 = iLine.o.y-b2*iLine.o.x;
xi = - (a1-a2)/(b1-b2);
yi = a1+b1*xi;
}
if (xi >= ex) {
xcount += 1;
}
if (xcount == 2) {
break;
}
}
return xcount;
},
/**
* Method that returns an object with the image lines in it given the coordinates of the corners
* @method _getImageLines
* @private
* @param oCoords {Object} coordinates of the image corners
*/
_getImageLines: function(oCoords, i) {
return {
topline: {
o: oCoords.tl,
d: oCoords.tr
},
rightline: {
o: oCoords.tr,
d: oCoords.br
},
bottomline: {
o: oCoords.br,
d: oCoords.bl
},
leftline: {
o: oCoords.bl,
d: oCoords.tl
}
}
},
/**
* Sets the coordinates of the draggable boxes in the corners of
* the image used to scale/rotate it.
* @method _setCornerCoords
* @private
*/
_setCornerCoords: function() {
var coords = this.oCoords,
theta = this.theta,
cosOffset = this.cornersize * /*this.scaleX * */ Math.cos(theta),
sinOffset = this.cornersize * /*this.scaleY * */ Math.sin(theta),
size2 = this.cornersize / 2,
size2x = size2 - sinOffset,
size2y = size2,
corner;
coords.tl.x -= size2x;
coords.tl.y -= size2y;
coords.tl.corner = {
tl: {
x: coords.tl.x,
y: coords.tl.y
},
tr: {
x: coords.tl.x + cosOffset,
y: coords.tl.y + sinOffset
},
bl: {
x: coords.tl.x - sinOffset,
y: coords.tl.y + cosOffset
}
};
coords.tl.corner.br = {
x: coords.tl.corner.tr.x - sinOffset,
y: coords.tl.corner.tr.y + cosOffset
};
coords.tl.x += size2x;
coords.tl.y += size2y;
coords.tr.x += size2;
coords.tr.y -= size2;
coords.tr.corner = {
tl: {
x: coords.tr.x - cosOffset,
y: coords.tr.y - sinOffset
},
tr: {
x: coords.tr.x,
y: coords.tr.y
},
br: {
x: coords.tr.x - sinOffset,
y: coords.tr.y + cosOffset
}
};
coords.tr.corner.bl = {
x: coords.tr.corner.tl.x - sinOffset,
y: coords.tr.corner.tl.y + cosOffset
};
coords.tr.x -= size2;
coords.tr.y += size2;
coords.bl.x -= size2;
coords.bl.y += size2;
coords.bl.corner = {
tl: {
x: coords.bl.x + sinOffset,
y: coords.bl.y - cosOffset
},
bl: {
x: coords.bl.x,
y: coords.bl.y
},
br: {
x: coords.bl.x + cosOffset,
y: coords.bl.y + sinOffset
}
};
coords.bl.corner.tr = {
x: coords.bl.corner.br.x + sinOffset,
y: coords.bl.corner.br.y - cosOffset
};
coords.bl.x += size2;
coords.bl.y -= size2;
coords.br.x += size2;
coords.br.y += size2;
coords.br.corner = {
tr: {
x: coords.br.x + sinOffset,
y: coords.br.y - cosOffset
},
bl: {
x: coords.br.x - cosOffset,
y: coords.br.y - sinOffset
},
br: {
x: coords.br.x,
y: coords.br.y
}
};
coords.br.corner.tl = {
x: coords.br.corner.bl.x + sinOffset,
y: coords.br.corner.bl.y - cosOffset
};
coords.br.x -= size2;
coords.br.y -= size2;
coords.ml.x -= size2;
coords.ml.y -= size2;
coords.ml.corner = {
tl: {
x: coords.ml.x,
y: coords.ml.y
},
tr: {
x: coords.ml.x + cosOffset,
y: coords.ml.y + sinOffset
},
bl: {
x: coords.ml.x - sinOffset,
y: coords.ml.y + cosOffset
}
};
coords.ml.corner.br = {
x: coords.ml.corner.tr.x - sinOffset,
y: coords.ml.corner.tr.y + cosOffset
};
coords.ml.x += size2;
coords.ml.y += size2;
coords.mt.x -= size2;
coords.mt.y -= size2;
coords.mt.corner = {
tl: {
x: coords.mt.x,
y: coords.mt.y
},
tr: {
x: coords.mt.x + cosOffset,
y: coords.mt.y + sinOffset
},
bl: {
x: coords.mt.x - sinOffset,
y: coords.mt.y + cosOffset
}
};
coords.mt.corner.br = {
x: coords.mt.corner.tr.x - sinOffset,
y: coords.mt.corner.tr.y + cosOffset
};
coords.mt.x += size2;
coords.mt.y += size2;
coords.mr.x -= size2;
coords.mr.y -= size2;
coords.mr.corner = {
tl: {
x: coords.mr.x,
y: coords.mr.y
},
tr: {
x: coords.mr.x + cosOffset,
y: coords.mr.y + sinOffset
},
bl: {
x: coords.mr.x - sinOffset,
y: coords.mr.y + cosOffset
}
};
coords.mr.corner.br = {
x: coords.mr.corner.tr.x - sinOffset,
y: coords.mr.corner.tr.y + cosOffset
};
coords.mr.x += size2;
coords.mr.y += size2;
coords.mb.x -= size2;
coords.mb.y -= size2;
coords.mb.corner = {
tl: {
x: coords.mb.x,
y: coords.mb.y
},
tr: {
x: coords.mb.x + cosOffset,
y: coords.mb.y + sinOffset
},
bl: {
x: coords.mb.x - sinOffset,
y: coords.mb.y + cosOffset
}
};
coords.mb.corner.br = {
x: coords.mb.corner.tr.x - sinOffset,
y: coords.mb.corner.tr.y + cosOffset
};
coords.mb.x += size2;
coords.mb.y += size2;
corner = coords.mb.corner;
corner.tl.x -= size2;
corner.tl.y -= size2;
corner.tr.x -= size2;
corner.tr.y -= size2;
corner.br.x -= size2;
corner.br.y -= size2;
corner.bl.x -= size2;
corner.bl.y -= size2;
},
/**
* Makes object's color grayscale
* @method toGrayscale
* @return {fabric.Object} thisArg
*/
toGrayscale: function() {
var fillValue = this.get('fill');
if (fillValue) {
this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb());
}
return this;
},
/**
* @method complexity
* @return {Number}
*/
complexity: function() {
return 0;
},
/**
* @method getCenter
* @return {Object} object with `x`, `y` properties corresponding to path center coordinates
*/
getCenter: function() {
return {
x: this.get('left') + this.width / 2,
y: this.get('top') + this.height / 2
};
},
/**
* @method straighten
* @return {fabric.Object} thisArg
* @chainable
*/
straighten: function() {
var angle = this._getAngleValueForStraighten();
this.setAngle(angle);
return this;
},
/**
* @method fxStraighten
* @param {Object} callbacks
* - onComplete: invoked on completion
* - onChange: invoked on every step of animation
*
* @return {fabric.Object} thisArg
* @chainable
*/
fxStraighten: function(callbacks) {
callbacks = callbacks || { };
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: this.get('angle'),
endValue: this._getAngleValueForStraighten(),
duration: this.FX_DURATION,
onChange: function(value) {
_this.setAngle(value);
onChange();
},
onComplete: function() {
_this.setCoords();
onComplete();
},
onStart: function() {
_this.setActive(false);
}
});
return this;
},
/**
* @method fxRemove
* @param {Object} callbacks
* @return {fabric.Object} thisArg
* @chainable
*/
fxRemove: function(callbacks) {
callbacks || (callbacks = { });
var empty = function() { },
onComplete = callbacks.onComplete || empty,
onChange = callbacks.onChange || empty,
_this = this;
fabric.util.animate({
startValue: this.get('opacity'),
endValue: 0,
duration: this.FX_DURATION,
onChange: function(value) {
_this.set('opacity', value);
onChange();
},
onComplete: onComplete,
onStart: function() {
_this.setActive(false);
}
});
return this;
},
/**
* @method _getAngleValueForStraighten
* @return {Number} angle value
* @private
*/
_getAngleValueForStraighten: function() {
var angle = this.get('angle');
if (angle > -225 && angle <= -135) { return -180; }
else if (angle > -135 && angle <= -45) { return -90; }
else if (angle > -45 && angle <= 45) { return 0; }
else if (angle > 45 && angle <= 135) { return 90; }
else if (angle > 135 && angle <= 225 ) { return 180; }
else if (angle > 225 && angle <= 315) { return 270; }
else if (angle > 315) { return 360; }
return 0;
},
/**
* Returns a JSON representation of an instance
* @method toJSON
* @return {String} json
*/
toJSON: function() {
return this.toObject();
}
});
/**
* @alias rotate -> setAngle
*/
fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle;
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend;
if (fabric.Line) {
return;
}
fabric.Line = fabric.util.createClass(fabric.Object, {
type: 'line',
/**
* @constructor
* @method initialize
* @param points {Array} array of points
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(points, options) {
if (!points) {
points = [0, 0, 0, 0];
}
this.callSuper('initialize', options);
this.set('x1', points[0]);
this.set('y1', points[1]);
this.set('x2', points[2]);
this.set('y2', points[3]);
this.set('width', this.x2 - this.x1);
this.set('height', this.y2 - this.y1);
this.set('left', this.x1 + this.width / 2);
this.set('top', this.y1 + this.height / 2);
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
ctx.beginPath();
ctx.moveTo(-this.width / 2, -this.height / 2);
ctx.lineTo(this.width / 2, this.height / 2);
var origStrokeStyle = ctx.strokeStyle;
ctx.strokeStyle = ctx.fillStyle;
ctx.stroke();
ctx.strokeStyle = origStrokeStyle;
},
/**
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return 1;
},
/**
* @methd toObject
* @return {Object}
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
x1: this.get('x1'),
y1: this.get('y1'),
x2: this.get('x2'),
y2: this.get('y2')
});
}
});
fabric.Element.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' ');
/**
* @static
* @method fabric.Line.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @return {Object} instance of fabric.Line
*/
fabric.Line.fromElement = function(element, options) {
var parsedAttributes = fabric.parseAttributes(element, fabric.Element.ATTRIBUTE_NAMES);
var points = [
parsedAttributes.x1 || 0,
parsedAttributes.y1 || 0,
parsedAttributes.x2 || 0,
parsedAttributes.y2 || 0
];
return new fabric.Line(points, extend(parsedAttributes, options));
};
/**
* @static
* @method fabric.Line.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Line
*/
fabric.Line.fromObject = function(object) {
var points = [object.x1, object.y1, object.x2, object.y2];
return new fabric.Line(points, object);
};
})();
(function() {
var fabric = this.fabric || (this.fabric = { }),
piBy2 = Math.PI * 2,
extend = fabric.util.object.extend;
if (fabric.Circle) {
console.warn('fabric.Circle is already defined.');
return;
}
fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ {
/**
* @field
*/
type: 'circle',
/**
* @constructs
* @method initialize
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(options) {
options = options || { };
this.set('radius', options.radius || 0);
this.callSuper('initialize', options);
var radiusBy2ByScale = this.get('radius') * 2 * this.get('scaleX');
this.set('width', radiusBy2ByScale).set('height', radiusBy2ByScale);
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
radius: this.get('radius')
});
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx, noTransform) {
ctx.beginPath();
ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.radius, 0, piBy2, false);
ctx.closePath();
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.stroke();
}
},
/**
* Returns complexity of an instance
* @method complexity
* @return {Number} complexity of this instance
*/
complexity: function() {
return 1;
}
});
/**
* @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement
*/
fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity stroke stroke-width transform'.split(' ');
/**
* @static
* @method fabric.Circle.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @throws {Error} If value of `r` attribute is missing or invalid
* @return {Object} instance of fabric.Circle
*/
fabric.Circle.fromElement = function(element, options) {
options || (options = { });
var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES);
if (!isValidRadius(parsedAttributes)) {
throw Error('value of `r` attribute is required and can not be negative');
}
if ('left' in parsedAttributes) {
parsedAttributes.left -= (options.width / 2) || 0;
}
if ('top' in parsedAttributes) {
parsedAttributes.top -= (options.height / 2) || 0;
}
return new fabric.Circle(extend(parsedAttributes, options));
};
/**
* @private
*/
function isValidRadius(attributes) {
return (('radius' in attributes) && (attributes.radius > 0));
}
/**
* @static
* @method fabric.Circle.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Circle
*/
fabric.Circle.fromObject = function(object) {
return new fabric.Circle(object);
}
})();
(function(){
var fabric = this.fabric || (this.fabric = { });
if (fabric.Triangle) return;
fabric.Triangle = fabric.util.createClass(fabric.Object, {
/**
* @field
*/
type: 'triangle',
/**
* @constructs
* @method initialize
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(options) {
options = options || { };
this.callSuper('initialize', options);
this.set('width', options.width || 100)
.set('height', options.height || 100);
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
var widthBy2 = this.width / 2,
heightBy2 = this.height / 2;
ctx.beginPath();
ctx.moveTo(-widthBy2, heightBy2);
ctx.lineTo(0, -heightBy2);
ctx.lineTo(widthBy2, heightBy2);
ctx.closePath();
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.stroke();
}
},
/**
* Returns complexity of an instance
* @method complexity
* @return {Number} complexity of this instance
*/
complexity: function() {
return 1;
}
});
/**
* @static
* @method Canvas.Trangle.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of Canvas.Triangle
*/
fabric.Triangle.fromObject = function(object) {
return new fabric.Triangle(object);
};
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
piBy2 = Math.PI * 2,
extend = fabric.util.object.extend;
if (fabric.Ellipse) {
console.warn('fabric.Ellipse is already defined.');
return;
}
fabric.Ellipse = fabric.util.createClass(fabric.Object, {
type: 'ellipse',
/**
* @constructor
* @method initialize
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(options) {
options = options || { };
this.callSuper('initialize', options);
this.set('rx', options.rx || 0);
this.set('ry', options.ry || 0);
this.set('width', this.get('rx') * 2);
this.set('height', this.get('ry') * 2);
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
rx: this.get('rx'),
ry: this.get('ry')
});
},
/**
* @method render
* @param ctx {CanvasRenderingContext2D} context to render on
* @param noTransform {Boolean} context is not transformed when set to true
*/
render: function(ctx, noTransform) {
if (this.rx === 0 || this.ry === 0) return;
return this.callSuper('render', ctx, noTransform);
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx, noTransform) {
ctx.beginPath();
ctx.save();
ctx.transform(1, 0, 0, this.ry/this.rx, 0, 0);
ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.rx, 0, piBy2, false);
ctx.restore();
if (this.stroke) {
ctx.stroke();
}
if (this.fill) {
ctx.fill();
}
},
/**
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity stroke stroke-width transform'.split(' ');
/**
* @static
* @method fabric.Ellipse.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @return {Object} instance of fabric.Ellipse
*/
fabric.Ellipse.fromElement = function(element, options) {
options || (options = { });
var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES);
if ('left' in parsedAttributes) {
parsedAttributes.left -= (options.width / 2) || 0;
}
if ('top' in parsedAttributes) {
parsedAttributes.top -= (options.height / 2) || 0;
}
return new fabric.Ellipse(extend(parsedAttributes, options));
};
/**
* @static
* @method fabric.Ellipse.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Ellipse
*/
fabric.Ellipse.fromObject = function(object) {
return new fabric.Ellipse(object);
}
})();
(function(){
var fabric = this.fabric || (this.fabric = { });
if (fabric.Rect) {
return;
}
/**
* @class Rect
* @extends fabric.Object
*/
fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ {
type: 'rect',
options: {
rx: 0,
ry: 0
},
/**
* @constructs
* @method initialize
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(options) {
this.callSuper('initialize', options);
this._initRxRy();
},
/**
* @private
* @method _initRxRy
*/
_initRxRy: function() {
if (this.options.rx && !this.options.ry) {
this.options.ry = this.options.rx;
}
else if (this.options.ry && !this.options.rx) {
this.options.rx = this.options.ry;
}
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
var rx = this.options.rx || 0,
ry = this.options.ry || 0,
x = -this.width / 2,
y = -this.height / 2,
w = this.width,
h = this.height;
ctx.beginPath();
ctx.moveTo(x+rx, y);
ctx.lineTo(x+w-rx, y);
ctx.bezierCurveTo(x+w, y, x+w, y+ry, x+w, y+ry);
ctx.lineTo(x+w, y+h-ry);
ctx.bezierCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h);
ctx.lineTo(x+rx,y+h);
ctx.bezierCurveTo(x,y+h,x,y+h-ry,x,y+h-ry);
ctx.lineTo(x,y+ry);
ctx.bezierCurveTo(x,y,x+rx,y,x+rx,y);
ctx.closePath();
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.stroke();
}
},
_normalizeLeftTopProperties: function(parsedAttributes) {
if (parsedAttributes.left) {
this.set('left', parsedAttributes.left + this.getWidth() / 2);
}
if (parsedAttributes.top) {
this.set('top', parsedAttributes.top + this.getHeight() / 2);
}
return this;
},
/**
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return 1;
}
});
fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity stroke stroke-width transform'.split(' ');
/**
* @private
*/
function _setDefaultLeftTopValues(attributes) {
attributes.left = attributes.left || 0;
attributes.top = attributes.top || 0;
return attributes;
}
/**
* @static
* @method fabric.Rect.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @return {Object} instance of fabric.Rect
*/
fabric.Rect.fromElement = function(element, options) {
if (!element) {
return null;
}
var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES);
parsedAttributes = _setDefaultLeftTopValues(parsedAttributes);
var rect = new fabric.Rect(fabric.util.object.extend(options || { }, parsedAttributes));
rect._normalizeLeftTopProperties(parsedAttributes);
return rect;
};
/**
* @static
* @method fabric.Rect.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Rect
*/
fabric.Rect.fromObject = function(object) {
return new fabric.Rect(object);
};
})();
(function(){
var fabric = this.fabric || (this.fabric = { });
if (fabric.Polyline) {
console.warn('fabric.Polyline is already defined');
return;
}
fabric.Polyline = fabric.util.createClass(fabric.Object, {
type: 'polyline',
/**
* @constructor
* @method initialize
* @param points {Array} array of points
* @param options {Object} options object
* @return {Object} thisArg
*/
initialize: function(points, options) {
options = options || { };
this.set('points', points);
this.callSuper('initialize', options);
this._calcDimensions();
},
/**
* @private
* @method _calcDimensions
*/
_calcDimensions: function() {
return fabric.Polygon.prototype._calcDimensions.call(this);
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return fabric.Polygon.prototype.toObject.call(this);
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
var point;
ctx.beginPath();
for (var i = 0, len = this.points.length; i < len; i++) {
point = this.points[i];
ctx.lineTo(point.x, point.y);
}
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.stroke();
}
},
/**
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return this.get('points').length;
}
});
var ATTRIBUTE_NAMES = 'fill fill-opacity stroke stroke-width transform'.split(' ');
/**
* @static
* @method fabric.Polyline.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @return {Object} instance of fabric.Polyline
*/
fabric.Polyline.fromElement = function(element, options) {
if (!element) {
return null;
}
options || (options = { });
var points = fabric.parsePointsAttribute(element.getAttribute('points')),
parsedAttributes = fabric.parseAttributes(element, ATTRIBUTE_NAMES);
for (var i = 0, len = points.length; i < len; i++) {
points[i].x -= (options.width / 2) || 0;
points[i].y -= (options.height / 2) || 0;
}
return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options));
};
/**
* @static
* @method fabric.Polyline.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Polyline
*/
fabric.Polyline.fromObject = function(object) {
var points = object.points;
return new fabric.Polyline(points, object);
}
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
max = fabric.util.array.max;
if (fabric.Polygon) {
console.warn('fabric.Polygon is already defined');
return;
}
function byX(p) { return p.x; }
function byY(p) { return p.y; }
fabric.Polygon = fabric.util.createClass(fabric.Object, {
type: 'polygon',
/**
* @constructor
* @method initialize
* @param points {Array} array of points
* @param options {Object} options object
* @return thisArg
*/
initialize: function(points, options) {
options = options || { };
this.points = points;
this.callSuper('initialize', options);
this._calcDimensions();
},
/**
* @private
* @method _calcDimensions
*/
_calcDimensions: function() {
var points = this.points,
minX = min(points, 'x'),
minY = min(points, 'y'),
maxX = max(points, 'x'),
maxY = max(points, 'y');
this.width = maxX - minX;
this.height = maxY - minY;
this.minX = minX;
this.minY = minY;
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
points: this.points.concat()
});
},
/**
* @private
* @method _render
* @param ctx {CanvasRenderingContext2D} context to render on
*/
_render: function(ctx) {
var point;
ctx.beginPath();
for (var i = 0, len = this.points.length; i < len; i++) {
point = this.points[i];
ctx.lineTo(point.x, point.y);
}
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.closePath();
ctx.stroke();
}
},
/**
* Returns complexity of an instance
* @method complexity
* @return {Number} complexity of this instance
*/
complexity: function() {
return this.points.length;
}
});
fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity stroke stroke-width transform'.split(' ');
/**
* @static
* @method fabric.Polygon.fromElement
* @param element {SVGElement} element to parse
* @param options {Object} options object
* @return {Object} instance of fabric.Polygon
*/
fabric.Polygon.fromElement = function(element, options) {
if (!element) {
return null;
}
options || (options = { });
var points = fabric.parsePointsAttribute(element.getAttribute('points')),
parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES);
for (var i = 0, len = points.length; i < len; i++) {
points[i].x -= (options.width / 2) || 0;
points[i].y -= (options.height / 2) || 0;
}
return new fabric.Polygon(points, extend(parsedAttributes, options));
};
/**
* @static
* @method fabric.Polygon.fromObject
* @param object {Object} object to create an instance from
* @return {Object} instance of fabric.Polygon
*/
fabric.Polygon.fromObject = function(object) {
return new fabric.Polygon(object.points, object);
}
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
min = fabric.util.array.min,
max = fabric.util.array.max,
extend = fabric.util.object.extend;
if (fabric.Path) {
console.warn('fabric.Path is already defined');
return;
}
if (!fabric.Object) {
console.warn('fabric.Path requires fabric.Object');
return;
}
fabric.Path = fabric.util.createClass(fabric.Object, {
type: 'path',
/**
* @constructor
* @param path {Array | String} path data
* (i.e. sequence of coordinates and corresponding "command" tokens)
* @param options {Object} options object
*/
initialize: function(path, options) {
options = options || { };
this.setOptions(options);
this._importProperties();
this.originalState = { };
if (!path) {
throw Error('`path` argument is required');
}
var fromArray = Object.prototype.toString.call(path) === '[object Array]';
this.path = fromArray
? path
: path.match && path.match(/[a-zA-Z][^a-zA-Z]*/g);
if (!this.path) return;
if (!fromArray) {
this._initializeFromArray(options);
};
this.setCoords();
if (options.sourcePath) {
this.setSourcePath(options.sourcePath);
}
},
_initializeFromArray: function(options) {
var isWidthSet = 'width' in options,
isHeightSet = 'height' in options;
this.path = this._parsePath();
if (!isWidthSet || !isHeightSet) {
extend(this, this._parseDimensions());
if (isWidthSet) {
this.width = this.options.width;
}
if (isHeightSet) {
this.height = this.options.height;
}
}
},
_render: function(ctx) {
var current, // current instruction
x = 0, // current x
y = 0, // current y
controlX = 0, // current control point x
controlY = 0, // current control point y
tempX,
tempY,
l = -(this.width / 2),
t = -(this.height / 2);
for (var i = 0, len = this.path.length; i < len; ++i) {
current = this.path[i];
switch (current[0]) { // first letter
case 'l': // lineto, relative
x += current[1];
y += current[2];
ctx.lineTo(x + l, y + t);
break;
case 'L': // lineto, absolute
x = current[1];
y = current[2];
ctx.lineTo(x + l, y + t);
break;
case 'h': // horizontal lineto, relative
x += current[1];
ctx.lineTo(x + l, y + t);
break;
case 'H': // horizontal lineto, absolute
x = current[1];
ctx.lineTo(x + l, y + t);
break;
case 'v': // vertical lineto, relative
y += current[1];
ctx.lineTo(x + l, y + t);
break;
case 'V': // verical lineto, absolute
y = current[1];
ctx.lineTo(x + l, y + t);
break;
case 'm': // moveTo, relative
x += current[1];
y += current[2];
ctx.moveTo(x + l, y + t);
break;
case 'M': // moveTo, absolute
x = current[1];
y = current[2];
ctx.moveTo(x + l, y + t);
break;
case 'c': // bezierCurveTo, relative
tempX = x + current[5];
tempY = y + current[6];
controlX = x + current[3];
controlY = y + current[4];
ctx.bezierCurveTo(
x + current[1] + l, // x1
y + current[2] + t, // y1
controlX + l, // x2
controlY + t, // y2
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'C': // bezierCurveTo, absolute
x = current[5];
y = current[6];
controlX = current[3];
controlY = current[4];
ctx.bezierCurveTo(
current[1] + l,
current[2] + t,
controlX + l,
controlY + t,
x + l,
y + t
);
break;
case 's': // shorthand cubic bezierCurveTo, relative
tempX = x + current[3];
tempY = y + current[4];
controlX = 2 * x - controlX;
controlY = 2 * y - controlY;
ctx.bezierCurveTo(
controlX + l,
controlY + t,
x + current[1] + l,
y + current[2] + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'S': // shorthand cubic bezierCurveTo, absolute
tempX = current[3];
tempY = current[4];
controlX = 2*x - controlX;
controlY = 2*y - controlY;
ctx.bezierCurveTo(
controlX + l,
controlY + t,
current[1] + l,
current[2] + t,
tempX + l,
tempY + t
);
x = tempX;
y = tempY;
break;
case 'q': // quadraticCurveTo, relative
x += current[3];
y += current[4];
ctx.quadraticCurveTo(
current[1] + l,
current[2] + t,
x + l,
y + t
);
break;
case 'Q': // quadraticCurveTo, absolute
x = current[3];
y = current[4];
controlX = current[1];
controlY = current[2];
ctx.quadraticCurveTo(
controlX + l,
controlY + t,
x + l,
y + t
);
break;
case 'T':
tempX = x;
tempY = y;
x = current[1];
y = current[2];
controlX = -controlX + 2 * tempX;
controlY = -controlY + 2 * tempY;
ctx.quadraticCurveTo(
controlX + l,
controlY + t,
x + l,
y + t
);
break;
case 'a':
break;
case 'A':
break;
case 'z':
case 'Z':
ctx.closePath();
break;
}
}
},
render: function(ctx, noTransform) {
ctx.save();
var m = this.transformMatrix;
if (m) {
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (!noTransform) {
this.transform(ctx);
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
ctx.fillStyle = this.fill;
}
if (this.stroke) {
ctx.strokeStyle = this.stroke;
}
ctx.beginPath();
this._render(ctx);
if (this.fill) {
ctx.fill();
}
if (this.stroke) {
ctx.strokeStyle = this.stroke;
ctx.lineWidth = this.strokeWidth;
ctx.lineCap = ctx.lineJoin = 'round';
ctx.stroke();
}
if (!noTransform && this.active) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
}
ctx.restore();
},
/**
* Returns string representation of an instance
* @method toString
* @return {String} string representation of an instance
*/
toString: function() {
return '#<fabric.Path ('+ this.complexity() +'): ' +
JSON.stringify({ top: this.top, left: this.left }) +'>';
},
/**
* @method toObject
* @return {Object}
*/
toObject: function() {
var o = extend(this.callSuper('toObject'), {
path: this.path
});
if (this.sourcePath) {
o.sourcePath = this.sourcePath;
}
if (this.transformMatrix) {
o.transformMatrix = this.transformMatrix;
}
return o;
},
/**
* @method toDatalessObject
* @return {Object}
*/
toDatalessObject: function() {
var o = this.toObject();
if (this.sourcePath) {
o.path = this.sourcePath;
}
delete o.sourcePath;
return o;
},
complexity: function() {
return this.path.length;
},
set: function(prop, value) {
return this.callSuper('set', prop, value);
},
_parsePath: function() {
var result = [],
currentPath,
chunks;
for (var i = 0, len = this.path.length; i < len; i++) {
currentPath = this.path[i];
chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/);
result.push([currentPath.charAt(0)].concat(chunks.map(parseFloat)));
}
return result;
},
_parseDimensions: function() {
var aX = [],
aY = [],
previousX,
previousY,
isLowerCase = false,
x,
y;
function getX(item) {
if (item[0] === 'H') {
return item[1];
}
return item[item.length - 2];
}
function getY(item) {
if (item[0] === 'V') {
return item[1];
}
return item[item.length - 1];
}
this.path.forEach(function(item, i) {
if (item[0] !== 'H') {
previousX = (i === 0) ? getX(item) : getX(this.path[i-1]);
}
if (item[0] !== 'V') {
previousY = (i === 0) ? getY(item) : getY(this.path[i-1]);
}
if (item[0] === item[0].toLowerCase()) {
isLowerCase = true;
}
x = isLowerCase
? previousX + getX(item)
: item[0] === 'V'
? previousX
: getX(item);
y = isLowerCase
? previousY + getY(item)
: item[0] === 'H'
? previousY
: getY(item);
var val = parseInt(x, 10);
if (!isNaN(val)) aX.push(val);
val = parseInt(y, 10);
if (!isNaN(val)) aY.push(val);
}, this);
var minX = min(aX),
minY = min(aY),
deltaX = deltaY = 0;
var o = {
top: minY - deltaY,
left: minX - deltaX,
bottom: max(aY) - deltaY,
right: max(aX) - deltaX
};
o.width = o.right - o.left;
o.height = o.bottom - o.top;
return o;
}
});
/**
* Creates an instance of fabric.Path from an object
* @static
* @method fabric.Path.fromObject
* @return {fabric.Path} Instance of fabric.Path
*/
fabric.Path.fromObject = function(object) {
return new fabric.Path(object.path, object);
};
var ATTRIBUTE_NAMES = fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity fill-rule stroke stroke-width transform'.split(' ');
/**
* Creates an instance of fabric.Path from an SVG <PATH> element
* @static
* @method fabric.Path.fromElement
* @param {SVGElement} element to parse
* @param {Object} options object
* @return {fabric.Path} Instance of fabric.Path
*/
fabric.Path.fromElement = function(element, options) {
var parsedAttributes = fabric.parseAttributes(element, ATTRIBUTE_NAMES),
path = parsedAttributes.d;
delete parsedAttributes.d;
return new fabric.Path(path, extend(parsedAttributes, options));
};
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend,
invoke = fabric.util.array.invoke,
parentSet = fabric.Object.prototype.set,
parentToObject = fabric.Object.prototype.toObject,
camelize = fabric.util.string.camelize,
capitalize = fabric.util.string.capitalize;
if (fabric.PathGroup) {
console.warn('fabric.PathGroup is already defined');
return;
}
fabric.PathGroup = fabric.util.createClass(fabric.Path, {
type: 'path-group',
forceFillOverwrite: false,
initialize: function(paths, options) {
options = options || { };
this.originalState = { };
this.paths = paths;
this.setOptions(options);
this.initProperties();
this.setCoords();
if (options.sourcePath) {
this.setSourcePath(options.sourcePath);
}
},
initProperties: function() {
this.stateProperties.forEach(function(prop) {
if (prop === 'fill') {
this.set(prop, this.options[prop]);
}
else if (prop === 'angle') {
this.setAngle(this.options[prop]);
}
else {
this[prop] = this.options[prop];
}
}, this);
},
render: function(ctx) {
if (this.stub) {
ctx.save();
this.transform(ctx);
this.stub.render(ctx, false /* no transform */);
if (this.active) {
this.drawBorders(ctx);
this.drawCorners(ctx);
}
ctx.restore();
}
else {
ctx.save();
var m = this.transformMatrix;
if (m) {
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
this.transform(ctx);
for (var i = 0, l = this.paths.length; i < l; ++i) {
this.paths[i].render(ctx, true);
}
if (this.active) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
}
ctx.restore();
}
},
/**
* @method set
* @param {String} prop
* @param {Any} value
* @return {fabric.PathGroup} thisArg
*/
set: function(prop, value) {
if ((prop === 'fill' || prop === 'overlayFill') && this.isSameColor()) {
this[prop] = value;
var i = this.paths.length;
while (i--) {
this.paths[i].set(prop, value);
}
}
else {
parentSet.call(this, prop, value);
}
return this;
},
/**
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(parentToObject.call(this), {
paths: invoke(this.getObjects(), 'clone'),
sourcePath: this.sourcePath
});
},
/**
* @method toDatalessObject
* @return {Object} dataless object representation of an instance
*/
toDatalessObject: function() {
var o = this.toObject();
if (this.sourcePath) {
o.paths = this.sourcePath;
}
return o;
},
/**
* Returns a string representation of an object
* @method toString
* @return {String} string representation of an object
*/
toString: function() {
return '#<fabric.PathGroup (' + this.complexity() +
'): { top: ' + this.top + ', left: ' + this.left + ' }>';
},
/**
* @method isSameColor
* @return {Boolean} true if all paths are of the same color (`fill`)
*/
isSameColor: function() {
var firstPathFill = this.getObjects()[0].get('fill');
return this.getObjects().every(function(path) {
return path.get('fill') === firstPathFill;
});
},
/**
* Returns number representation of object's complexity
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return this.paths.reduce(function(total, path) {
return total + ((path && path.complexity) ? path.complexity() : 0);
}, 0);
},
/**
* Makes path group grayscale
* @method toGrayscale
* @return {fabric.PathGroup} thisArg
*/
toGrayscale: function() {
var i = this.paths.length;
while (i--) {
this.paths[i].toGrayscale();
}
return this;
},
/**
* @method getObjects
* @return {Array} array of path objects included in this path group
*/
getObjects: function() {
return this.paths;
}
});
/**
* @private
* @method instantiatePaths
*/
function instantiatePaths(paths) {
for (var i = 0, len = paths.length; i < len; i++) {
if (!(paths[i] instanceof fabric.Object)) {
var klassName = camelize(capitalize(paths[i].type));
paths[i] = fabric[klassName].fromObject(paths[i]);
}
}
return paths;
}
/**
* @static
* @method fabric.PathGroup.fromObject
* @param {Object} object
* @return {fabric.PathGroup}
*/
fabric.PathGroup.fromObject = function(object) {
var paths = instantiatePaths(object.paths);
return new fabric.PathGroup(paths, object);
};
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend,
min = fabric.util.array.min,
max = fabric.util.array.max,
invoke = fabric.util.array.invoke,
removeFromArray = fabric.util.removeFromArray;
if (fabric.Group) {
return;
}
fabric.Group = fabric.util.createClass(fabric.Object, {
/**
* @property type
*/
type: 'group',
/**
* @constructor
* @param {Object} objects Group objects
* @return {Object} thisArg
*/
initialize: function(objects, options) {
this.objects = objects || [];
this.originalState = { };
this.callSuper('initialize');
this._calcBounds();
this._updateObjectsCoords();
if (options) {
extend(this, options);
}
this._setOpacityIfSame();
this.setCoords(true);
this.saveCoords();
this.activateAllObjects();
},
/**
* @private
* @method _updateObjectsCoords
*/
_updateObjectsCoords: function() {
var groupDeltaX = this.left,
groupDeltaY = this.top;
this.forEachObject(function(object) {
var objectLeft = object.get('left'),
objectTop = object.get('top');
object.set('originalLeft', objectLeft);
object.set('originalTop', objectTop);
object.set('left', objectLeft - groupDeltaX);
object.set('top', objectTop - groupDeltaY);
object.setCoords();
object.hideCorners = true;
}, this);
},
/**
* @method toString
* @return {String}
*/
toString: function() {
return '#<fabric.Group: (' + this.complexity() + ')>';
},
/**
* @method getObjects
* @return {Array} group objects
*/
getObjects: function() {
return this.objects;
},
/**
* Adds an object to a group. Recalculates group's dimension, position.
* @method add
* @param {Object} object
* @return {Object} thisArg
* @chainable
*/
add: function(object) {
this._restoreObjectsState();
this.objects.push(object);
object.setActive(true);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* Removes an object from a group. Recalculates group's dimension, position.
* @param {Object} object
* @return {Object} thisArg
* @chainable
*/
remove: function(object) {
this._restoreObjectsState();
removeFromArray(this.objects, object);
object.setActive(false);
this._calcBounds();
this._updateObjectsCoords();
return this;
},
/**
* Returns a size of a group (i.e. length of an array containing its objects)
* @return {Number} Group size
*/
size: function() {
return this.getObjects().length;
},
/**
* Sets property to a given value
* @method set
* @param {String} name
* @param {Object | Function} value
* @return {Object} thisArg
* @chainable
*/
set: function(name, value) {
if (typeof value == 'function') {
this.set(name, value(this[name]));
}
else {
if (name === 'fill' || name === 'opacity') {
var i = this.objects.length;
this[name] = value;
while (i--) {
this.objects[i].set(name, value);
}
}
else {
this[name] = value;
}
}
return this;
},
/**
* Returns true if a group contains an object
* @method contains
* @param {Object} object Object to check against
* @return {Boolean} true if group contains an object
*/
contains: function(object) {
return this.objects.indexOf(object) > -1;
},
/**
* Returns object representation of an instance
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
objects: invoke(this.objects, 'clone')
});
},
/**
* Renders instance on a given context
* @method render
* @param ctx {CanvasRenderingContext2D} context to render instance on
*/
render: function(ctx) {
ctx.save();
this.transform(ctx);
var groupScaleFactor = Math.max(this.scaleX, this.scaleY);
for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) {
var originalScaleFactor = object.borderScaleFactor;
object.borderScaleFactor = groupScaleFactor;
object.render(ctx);
object.borderScaleFactor = originalScaleFactor;
}
this.hideBorders || this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
ctx.restore();
this.setCoords();
},
/**
* @method item
* @param index {Number} index of item to get
* @return {fabric.Object}
*/
item: function(index) {
return this.getObjects()[index];
},
/**
* @method complexity
* @return {Number} complexity
*/
complexity: function() {
return this.getObjects().reduce(function(total, object) {
total += (typeof object.complexity == 'function') ? object.complexity() : 0;
return total;
}, 0);
},
/**
* Retores original state of each of group objects
* @private
* @method _restoreObjectsState
* @return {fabric.Group} thisArg
* @chainable
*/
_restoreObjectsState: function() {
this.objects.forEach(this._restoreObjectState, this);
return this;
},
/**
* @private
* @method _restoreObjectState
* @param {fabric.Object} object
*/
_restoreObjectState: function(object) {
var groupLeft = this.get('left'),
groupTop = this.get('top'),
groupAngle = this.getAngle() * (Math.PI / 180),
objectLeft = object.get('originalLeft'),
objectTop = object.get('originalTop'),
rotatedTop = Math.cos(groupAngle) * object.get('top') + Math.sin(groupAngle) * object.get('left'),
rotatedLeft = -Math.sin(groupAngle) * object.get('top') + Math.cos(groupAngle) * object.get('left');
object.setAngle(object.getAngle() + this.getAngle());
object.set('left', groupLeft + rotatedLeft * this.get('scaleX'));
object.set('top', groupTop + rotatedTop * this.get('scaleY'));
object.set('scaleX', object.get('scaleX') * this.get('scaleX'));
object.set('scaleY', object.get('scaleY') * this.get('scaleY'));
object.setCoords();
object.hideCorners = false;
object.setActive(false);
object.setCoords();
return this;
},
/**
* @method destroy
* @return {fabric.Group} thisArg
* @chainable
*/
destroy: function() {
return this._restoreObjectsState();
},
/**
* @saveCoords
* @return {fabric.Group} thisArg
* @chainable
*/
saveCoords: function() {
this._originalLeft = this.get('left');
this._originalTop = this.get('top');
return this;
},
hasMoved: function() {
return this._originalLeft !== this.get('left') ||
this._originalTop !== this.get('top');
},
/**
* Sets coordinates of all group objects
* @method setObjectsCoords
* @return {fabric.Group} thisArg
* @chainable
*/
setObjectsCoords: function() {
this.forEachObject(function(object) {
object.setCoords();
});
return this;
},
/**
* Activates (makes active) all group objects
* @method activateAllObjects
* @return {fabric.Group} thisArg
* @chainable
*/
activateAllObjects: function() {
return this.setActive(true);
},
/**
* @method setActive
* @param {Boolean} value `true` to activate object, `false` otherwise
* @return {fabric.Group} thisArg
* @chainable
*/
setActive: function(value) {
this.forEachObject(function(object) {
object.setActive(value);
});
return this;
},
/**
* @method forEachObject
* @param {Function} callback
* Callback invoked with current object as first argument,
* index - as second and an array of all objects - as third.
* Iteration happens in reverse order (for performance reasons).
* Callback is invoked in a context of Global Object (e.g. `window`)
* when no `context` argument is given
*
* @param {Object} context a.k.a. thisObject
*
* @return {fabric.Group}
* @chainable
*/
forEachObject: function(callback, context) {
var objects = this.getObjects(),
i = objects.length;
while (i--) {
callback.call(context, objects[i], i, objects);
}
return this;
},
/**
* @private
* @method _setOpacityIfSame
*/
_setOpacityIfSame: function() {
var objects = this.getObjects(),
firstValue = objects[0] ? objects[0].get('opacity') : 1;
var isSameOpacity = objects.every(function(o) {
return o.get('opacity') === firstValue;
});
if (isSameOpacity) {
this.opacity = firstValue;
}
},
/**
* @private
* @method _calcBounds
*/
_calcBounds: function() {
var aX = [],
aY = [],
minX, minY, maxX, maxY, o, width, height,
i = 0,
len = this.objects.length;
for (; i < len; ++i) {
o = this.objects[i];
o.setCoords();
for (var prop in o.oCoords) {
aX.push(o.oCoords[prop].x);
aY.push(o.oCoords[prop].y);
}
};
minX = min(aX);
maxX = max(aX);
minY = min(aY);
maxY = max(aY);
width = maxX - minX;
height = maxY - minY;
this.width = width;
this.height = height;
this.left = minX + width / 2;
this.top = minY + height / 2;
},
/**
* @method containsPoint
* @param {Object} point point with `x` and `y` properties
* @return {Boolean} true if point is contained within group
*/
containsPoint: function(point) {
var halfWidth = this.get('width') / 2,
halfHeight = this.get('height') / 2,
centerX = this.get('left'),
centerY = this.get('top');
return centerX - halfWidth < point.x &&
centerX + halfWidth > point.x &&
centerY - halfHeight < point.y &&
centerY + halfHeight > point.y;
},
toGrayscale: function() {
var i = this.objects.length;
while (i--) {
this.objects[i].toGrayscale();
}
}
});
/**
* @static
* @method fabric.Group.fromObject
* @param object {Object} object to create a group from
* @param options {Object} options object
* @return {fabric.Group} an instance of fabric.Group
*/
fabric.Group.fromObject = function(object) {
return new fabric.Group(object.objects, object);
}
})();
(function(){
var fabric = this.fabric || (this.fabric = { }),
extend = fabric.util.object.extend,
clone = fabric.util.object.clone;
if (fabric.Text) {
console.warn('fabric.Text is already defined');
return;
}
if (!fabric.Object) {
console.warn('fabric.Text requires fabric.Object');
return;
}
fabric.Text = fabric.util.createClass(fabric.Object, {
options: {
top: 10,
left: 10,
fontsize: 20,
fontweight: 100,
fontfamily: 'Modernist_One_400',
path: null
},
type: 'text',
initialize: function(text, options) {
this.originalState = { };
this.initStateProperties();
this.text = text;
this.setOptions(options);
extend(this, this.options);
this.theta = this.angle * (Math.PI/180);
this.width = this.getWidth();
this.setCoords();
},
initStateProperties: function() {
var o;
if ((o = this.constructor) &&
(o = o.superclass) &&
(o = o.prototype) &&
(o = o.stateProperties) &&
o.clone) {
this.stateProperties = o.clone();
this.stateProperties.push('fontfamily', 'fontweight', 'path');
}
},
toString: function() {
return '#<fabric.Text ('+ this.complexity() +'): ' +
JSON.stringify({ text: this.text, fontfamily: this.fontfamily }) + '>';
},
_render: function(context) {
var o = Cufon.textOptions || (Cufon.textOptions = { });
o.left = this.left;
o.top = this.top;
o.context = context;
o.color = this.fill;
var el = this._initDummyElement();
this.transform(context);
Cufon.replaceElement(el, {
separate: 'none',
fontFamily: this.fontfamily
});
this.width = o.width;
this.height = o.height;
},
_initDummyElement: function() {
var el = document.createElement('div');
el.innerHTML = this.text;
el.style.fontSize = '40px';
el.style.fontWeight = '400';
el.style.fontStyle = 'normal';
el.style.letterSpacing = 'normal';
el.style.color = '#000000';
el.style.fontWeight = '600';
el.style.fontFamily = 'Verdana';
return el;
},
render: function(context) {
context.save();
this._render(context);
if (this.active) {
this.drawBorders(context);
this.drawCorners(context);
}
context.restore();
},
/**
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
text: this.text,
fontsize: this.fontsize,
fontweight: this.fontweight,
fontfamily: this.fontfamily,
path: this.path
});
},
/**
* @method setColor
* @param {String} value
* @return {fabric.Text} thisArg
* @chainable
*/
setColor: function(value) {
this.set('fill', value);
return this;
},
/**
* @method setFontsize
* @param {Number} value
* @return {fabric.Text} thisArg
* @chainable
*/
setFontsize: function(value) {
this.set('fontsize', value);
this.setCoords();
return this;
},
/**
* @method getText
* @return {String}
*/
getText: function() {
return this.text;
},
/**
* @method setText
* @param {String} value
* @return {fabric.Text} thisArg
*/
setText: function(value) {
this.set('text', value);
this.setCoords();
return this;
},
set: function(name, value) {
this[name] = value;
if (name === 'fontfamily') {
this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3');
}
return this;
}
});
/**
* @static
* @method fromObject
* @param {Object} object to create an instance from
* @return {fabric.Text} an instance
*/
fabric.Text.fromObject = function(object) {
return new fabric.Text(object.text, clone(object));
};
/**
* @static
* @method fabric.Text.fromElement
* @return {fabric.Text} an instance
*/
fabric.Text.fromElement = function(element) {
};
})();
(function() {
var global = this,
extend = fabric.util.object.extend;
if (!global.fabric) {
global.fabric = { };
}
if (global.fabric.Image) {
console.warn('fabric.Image is already defined.');
return;
};
if (!fabric.Object) {
console.warn('fabric.Object is required for fabric.Image initialization');
return;
}
fabric.Image = fabric.util.createClass(fabric.Object, {
maxwidth: null,
maxheight: null,
active: false,
bordervisibility: false,
cornervisibility: false,
type: 'image',
__isGrayscaled: false,
/**
* @constructor
* @param {HTMLImageElement | String} element Image element
* @param {Object} options optional
*/
initialize: function(element, options) {
this.callSuper('initialize', options);
this._initElement(element);
this._initConfig(options || { });
},
/**
* @method getElement
* @return {HTMLImageElement} image element
*/
getElement: function() {
return this._element;
},
/**
* @method setElement
* @return {fabric.Image} thisArg
*/
setElement: function(element) {
this._element = element;
return this;
},
/**
* Method that resizes an image depending on whether maxwidth and maxheight are set up.
* Width and height have to mantain the same proportion in the final image as it was in the initial one.
* @method getNormalizedSize
* @param {Object} oImg
* @param {Number} maxwidth maximum width of the image in px
* @param {Number} maxheight maximum height of the image in px
*/
getNormalizedSize: function(oImg, maxwidth, maxheight) {
if (maxheight && maxwidth && (oImg.width > oImg.height && (oImg.width / oImg.height) < (maxwidth / maxheight))) {
normalizedWidth = ~~((oImg.width * maxheight) / oImg.height);
normalizedHeight = maxheight;
}
else if (maxheight && ((oImg.height == oImg.width) || (oImg.height > oImg.width) || (oImg.height > maxheight))) {
normalizedWidth = ~~((oImg.width * maxheight) / oImg.height);
normalizedHeight = maxheight;
}
else if (maxwidth && (maxwidth < oImg.width)) {
normalizedHeight = ~~((oImg.height * maxwidth) / oImg.width);
normalizedWidth = maxwidth;
}
else {
normalizedWidth = oImg.width;
normalizedHeight = oImg.height;
}
return {
width: normalizedWidth,
height: normalizedHeight
};
},
/**
* @method getOriginalSize
* @return {Object} object with "width" and "height" properties
*/
getOriginalSize: function() {
var element = this.getElement();
return {
width: element.width,
height: element.height
};
},
/**
* @method setBorderVisibility
* @param showBorder {Boolean} when true, border is being set visible
*/
setBorderVisibility: function(showBorder) {
this._resetWidthHeight();
this._adjustWidthHeightToBorders(showBorder);
this.setCoords();
},
/**
* @method setCornersVisibility
*/
setCornersVisibility: function(visible) {
this.cornervisibility = !!visible;
},
/**
* @method render
*/
render: function(ctx, noTransform) {
ctx.save();
if (!noTransform) {
this.transform(ctx);
}
this._render(ctx);
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
}
ctx.restore();
},
/**
* @method toObject
* @return {Object} object representation of an instance
*/
toObject: function() {
return extend(this.callSuper('toObject'), {
src: this.getSrc()
});
},
/**
* @method getSrc
* @return {String} source of an image
*/
getSrc: function() {
return this.getElement().src;
},
/**
* @method toString
* @return {String} string representation of an instance
*/
toString: function() {
return '#<fabric.Image: { src: "' + this.getSrc() + '" }>';
},
/**
* @mthod clone
* @param {Function} callback
*/
clone: function(callback) {
this.constructor.fromObject(this.toObject(), callback);
},
/**
* @mthod toGrayscale
* @param {Function} callback
*/
toGrayscale: function(callback) {
if (this.__isGrayscaled) {
return;
}
var imgEl = this.getElement(),
canvasEl = document.createElement('canvas'),
replacement = document.createElement('img'),
_this = this;
canvasEl.width = imgEl.width;
canvasEl.height = imgEl.height;
canvasEl.getContext('2d').drawImage(imgEl, 0, 0);
fabric.Element.toGrayscale(canvasEl);
replacement.onload = function() {
_this.setElement(replacement);
callback && callback();
replacement.onload = canvasEl = imgEl = imageData = null;
};
replacement.width = imgEl.width;
replacement.height = imgEl.height;
replacement.src = canvasEl.toDataURL('image/png');
this.__isGrayscaled = true;
return this;
},
/**
* @private
*/
_render: function(ctx) {
var originalImgSize = this.getOriginalSize();
ctx.drawImage(
this.getElement(),
- originalImgSize.width / 2,
- originalImgSize.height / 2,
originalImgSize.width,
originalImgSize.height
);
},
/**
* @private
*/
_adjustWidthHeightToBorders: function(showBorder) {
if (showBorder) {
this.currentBorder = this.borderwidth;
this.width += (2 * this.currentBorder);
this.height += (2 * this.currentBorder);
}
else {
this.currentBorder = 0;
}
},
/**
* @private
*/
_resetWidthHeight: function() {
var element = this.getElement();
this.set('width', element.width);
this.set('height', element.height);
},
/**
* The Image class's initialization method. This method is automatically
* called by the constructor.
* @method _initElement
* @param {HTMLImageElement|String} el The element representing the image
*/
_initElement: function(element) {
this.setElement(fabric.util.getById(element));
fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS);
},
/**
* @method _initConfig
* @param {Object} options Options object
*/
_initConfig: function(options) {
this.setOptions(options);
this._setBorder();
this._setWidthHeight(options);
},
/**
* @private
*/
_setBorder: function() {
if (this.bordervisibility) {
this.currentBorder = this.borderwidth;
}
else {
this.currentBorder = 0;
}
},
/**
* @private
*/
_setWidthHeight: function(options) {
var sidesBorderWidth = 2 * this.currentBorder;
this.width = (this.getElement().width || 0) + sidesBorderWidth;
this.height = (this.getElement().height || 0) + sidesBorderWidth;
},
complexity: function() {
return 1;
}
});
/**
* Constant for the default CSS class name that represents a Canvas
* @property fabric.Image.CSS_CANVAS
* @static
* @final
* @type String
*/
fabric.Image.CSS_CANVAS = "canvas-img";
/**
* Creates an instance of fabric.Image from its object representation
* @method fromObject
* @param object {Object}
* @param callback {Function} optional
* @static
*/
fabric.Image.fromObject = function(object, callback) {
var img = document.createElement('img'),
src = object.src;
if (object.width) {
img.width = object.width;
}
if (object.height) {
img.height = object.height;
}
img.onload = function() {
if (callback) {
callback(new fabric.Image(img, object));
}
img = img.onload = null;
};
img.src = src;
};
/**
* Creates an instance of fabric.Image from an URL string
* @method fromURL
* @param url {String}
* @param callback {Function} optional
* @param imgOptions {Object} optional
* @static
*/
fabric.Image.fromURL = function(url, callback, imgOptions) {
var img = document.createElement('img');
img.onload = function() {
if (callback) {
callback(new fabric.Image(img, imgOptions));
}
img = img.onload = null;
};
img.src = url;
};
})();