From 55853039cdea47c4c26548258df0bd724b4caecf Mon Sep 17 00:00:00 2001 From: Steve Pemberton Date: Sun, 14 Oct 2012 13:27:41 +0100 Subject: [PATCH 1/3] Fix for negative width on objects preventing selection --- dist/all.js | 27983 +++++++++++++++++++++--------------------- src/object.class.js | 3459 +++--- 2 files changed, 15726 insertions(+), 15716 deletions(-) diff --git a/dist/all.js b/dist/all.js index e17e8ce2..159e36c0 100644 --- a/dist/all.js +++ b/dist/all.js @@ -1,13989 +1,13994 @@ -/* build: `node build.js modules=ALL` */ -/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */ - -var fabric = fabric || { version: "0.9.14" }; - -if (typeof exports != 'undefined') { - exports.fabric = fabric; -} - -if (typeof document != 'undefined' && typeof window != 'undefined') { - fabric.document = document; - fabric.window = window; -} -else { - // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom").jsdom(""); - fabric.window = fabric.document.createWindow(); -} - -/** - * True when in environment that supports touch events - * @property isTouchSupported - * @type boolean - */ -fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; - -/** - * True when in environment that's probably Node.js - * @property isLikelyNode - * @type boolean - */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; -/*! - * Copyright (c) 2009 Simo Kinnunen. - * Licensed under the MIT license. - */ - -var Cufon = (function() { - - var api = function() { - return api.replace.apply(null, arguments); - }; - - var DOM = api.DOM = { - - ready: (function() { - - var complete = false, readyStatus = { loaded: 1, complete: 1 }; - - var queue = [], perform = function() { - if (complete) return; - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Gecko, Opera, WebKit r26101+ - - if (fabric.document.addEventListener) { - fabric.document.addEventListener('DOMContentLoaded', perform, false); - fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages - } - - // Old WebKit, Internet Explorer - - if (!fabric.window.opera && fabric.document.readyState) (function() { - readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); - })(); - - // Internet Explorer - - if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { - try { - fabric.document.body.doScroll('left'); - perform(); - } - catch (e) { - setTimeout(arguments.callee, 1); - } - })(); - - addEvent(fabric.window, 'load', perform); // Fallback - - return function(listener) { - if (!arguments.length) perform(); - else complete ? listener() : queue.push(listener); - }; - - })() - - }; - - var CSS = api.CSS = { - - Size: function(value, base) { - - this.value = parseFloat(value); - this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; - - this.convert = function(value) { - return value / base * this.value; - }; - - this.convertFrom = function(value) { - return value / this.value * base; - }; - - this.toString = function() { - return this.value + this.unit; - }; - - }, - - getStyle: function(el) { - return new Style(el.style); - /* - var view = document.defaultView; - if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); - if (el.currentStyle) return new Style(el.currentStyle); - return new Style(el.style); - */ - }, - - quotedList: cached(function(value) { - // doesn't work properly with empty quoted strings (""), but - // it's not worth the extra code. - var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; - while (match = re.exec(value)) list.push(match[3] || match[1]); - return list; - }), - - ready: (function() { - - var complete = false; - - var queue = [], perform = function() { - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Safari 2 does not include '); - - function getFontSizeInPixels(el, value) { - return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); - } - - // Original by Dead Edwards. - // Combined with getFontSizeInPixels it also works with relative units. - function getSizeInPixels(el, value) { - if (/px$/i.test(value)) return parseFloat(value); - var style = el.style.left, runtimeStyle = el.runtimeStyle.left; - el.runtimeStyle.left = el.currentStyle.left; - el.style.left = value; - var result = el.style.pixelLeft; - el.style.left = style; - el.runtimeStyle.left = runtimeStyle; - return result; - } - - return function(font, text, style, options, node, el, hasNext) { - var redraw = (text === null); - - if (redraw) text = node.alt; - - // @todo word-spacing, text-decoration - - var viewBox = font.viewBox; - - var size = style.computedFontSize || - (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); - - var letterSpacing = style.computedLSpacing; - - if (letterSpacing == undefined) { - letterSpacing = style.get('letterSpacing'); - style.computedLSpacing = letterSpacing = - (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); - } - - var wrapper, canvas; - - if (redraw) { - wrapper = node; - canvas = node.firstChild; - } - else { - wrapper = fabric.document.createElement('span'); - wrapper.className = 'cufon cufon-vml'; - wrapper.alt = text; - - canvas = fabric.document.createElement('span'); - canvas.className = 'cufon-vml-canvas'; - wrapper.appendChild(canvas); - - if (options.printable) { - var print = fabric.document.createElement('span'); - print.className = 'cufon-alt'; - print.appendChild(fabric.document.createTextNode(text)); - wrapper.appendChild(print); - } - - // ie6, for some reason, has trouble rendering the last VML element in the document. - // we can work around this by injecting a dummy element where needed. - // @todo find a better solution - if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); - } - - var wStyle = wrapper.style; - var cStyle = canvas.style; - - var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); - var roundingFactor = roundedHeight / height; - var minX = viewBox.minX, minY = viewBox.minY; - - cStyle.height = roundedHeight; - cStyle.top = Math.round(size.convert(minY - font.ascent)); - cStyle.left = Math.round(size.convert(minX)); - - wStyle.height = size.convert(font.height) + 'px'; - - var textDecoration = Cufon.getTextDecoration(options); - - var color = style.get('color'); - - var chars = Cufon.CSS.textTransform(text, style).split(''); - - var width = 0, offsetX = 0, advance = null; - - var glyph, shape, shadows = options.textShadow; - - // pre-calculate width - for (var i = 0, k = 0, l = chars.length; i < l; ++i) { - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; - } - - if (advance === null) return null; - - var fullWidth = -minX + width + (viewBox.width - advance); - - var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); - - var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; - var stretch = 'r' + coordSize + 'nsnf'; - - for (i = 0; i < l; ++i) { - - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (!glyph) continue; - - if (redraw) { - // some glyphs may be missing so we can't use i - shape = canvas.childNodes[k]; - if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow - } - else { - shape = fabric.document.createElement('cvml:shape'); - canvas.appendChild(shape); - } - - shape.stroked = 'f'; - shape.coordsize = coordSize; - shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; - shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; - shape.fillcolor = color; - - // it's important to not set top/left or IE8 will grind to a halt - var sStyle = shape.style; - sStyle.width = roundedShapeWidth; - sStyle.height = roundedHeight; - - if (shadows) { - // due to the limitations of the VML shadow element there - // can only be two visible shadows. opacity is shared - // for all shadows. - var shadow1 = shadows[0], shadow2 = shadows[1]; - var color1 = Cufon.CSS.color(shadow1.color), color2; - var shadow = fabric.document.createElement('cvml:shadow'); - shadow.on = 't'; - shadow.color = color1.color; - shadow.offset = shadow1.offX + ',' + shadow1.offY; - if (shadow2) { - color2 = Cufon.CSS.color(shadow2.color); - shadow.type = 'double'; - shadow.color2 = color2.color; - shadow.offset2 = shadow2.offX + ',' + shadow2.offY; - } - shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; - shape.appendChild(shadow); - } - - offsetX += ~~(glyph.w || font.w) + letterSpacing; - - ++k; - - } - - wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); - - return wrapper; - - }; - -})()); - -Cufon.getTextDecoration = function(options) { - return { - underline: options.textDecoration === 'underline', - overline: options.textDecoration === 'overline', - 'line-through': options.textDecoration === 'line-through' - }; -}; - -if (typeof exports != 'undefined') { - exports.Cufon = Cufon; -} - -/* - json2.js - 2011-10-19 - - 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) { - // Format integers to have at least two digits. - 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 is '["e",{"pluribus":"unum"}]' - - - text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); - // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' - - text = JSON.stringify([new Date()], function (key, value) { - return this[key] instanceof Date ? - 'Date(' + this[key] + ')' : value; - }); - // text is '["Date(---current time---)"]' - - - 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: - - // Parse the text. Values that look like ISO date strings will - // be converted to Date objects. - - 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, regexp: true */ - -/*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 -*/ - - -// Create a JSON object only if one does not already exist. We create the -// methods in a closure to avoid creating global variables. - -var JSON; -if (!JSON) { - JSON = {}; -} - -(function () { - 'use strict'; - - function f(n) { - // Format integers to have at least two digits. - 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) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - 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) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 - ? '[]' - : gap - ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' - : '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 - ? '{}' - : gap - ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' - : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - if (typeof JSON.stringify !== 'function') { - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - } - - -// If the JSON object does not yet have a parse method, give it one. - - if (typeof JSON.parse !== 'function') { - JSON.parse = function (text, reviver) { - -// The parse method takes a text and an optional reviver function, and returns -// a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - -// The walk method is used to recursively walk the resulting structure so -// that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - -// Parsing happens in four stages. In the first stage, we replace certain -// Unicode characters with escape sequences. JavaScript handles many characters -// incorrectly, either silently deleting them, or treating them as line endings. - - 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); - }); - } - -// In the second stage, we run the text against regular expressions that look -// for non-JSON patterns. We are especially concerned with '()' and 'new' -// because they can cause invocation, and '=' because it can cause mutation. -// But just to be safe, we want to reject all unexpected forms. - -// We split the second stage into 4 regexp operations in order to work around -// crippling inefficiencies in IE's and Safari's regexp engines. First we -// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we -// replace all simple value tokens with ']' characters. Third, we delete all -// open brackets that follow a colon or comma or that begin the text. Finally, -// we look to see that the remaining characters are only whitespace or ']' or -// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - 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, ''))) { - -// In the third stage we use the eval function to compile the text into a -// JavaScript structure. The '{' operator is subject to a syntactic ambiguity -// in JavaScript: it can begin a block or an object literal. We wrap the text -// in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - -// In the optional fourth stage, we recursively walk the new structure, passing -// each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' - ? walk({'': j}, '') - : j; - } - -// If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - } -}()); -/** - * Wrapper around `console.log` (when available) - * @method log - * @param {Any} Values to log - */ -fabric.log = function() { }; - -/** - * Wrapper around `console.warn` (when available) - * @method warn - * @param {Any} Values to log as a warning - */ -fabric.warn = function() { }; - -if (typeof console !== 'undefined') { - if (typeof console.log !== 'undefined' && console.log.apply) { - fabric.log = function() { - return console.log.apply(console, arguments); - }; - } - if (typeof console.warn !== 'undefined' && console.warn.apply) { - fabric.warn = function() { - return console.warn.apply(console, arguments); - }; - } -} - -/** - * @namespace - */ -fabric.Observable = { - - /** - * Observes specified event - * @method observe - * @depracated Since 0.8.34. Use `on` instead. - * @param {String} eventName - * @param {Function} handler - */ - observe: function(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - // one object with key/value pairs was passed - if (arguments.length === 1) { - for (var prop in eventName) { - this.on(prop, eventName[prop]); - } - } - else { - if (!this.__eventListeners[eventName]) { - this.__eventListeners[eventName] = [ ]; - } - this.__eventListeners[eventName].push(handler); - } - }, - - /** - * Stops event observing for a particular event handler - * @method stopObserving - * @depracated Since 0.8.34. Use `off` instead. - * @param {String} eventName - * @param {Function} handler - */ - stopObserving: function(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - if (this.__eventListeners[eventName]) { - fabric.util.removeFromArray(this.__eventListeners[eventName], handler); - } - }, - - /** - * Fires event with an optional options object - * @method fire - * @param {String} eventName - * @param {Object} [options] - */ - fire: function(eventName, options) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - var listenersForEvent = this.__eventListeners[eventName]; - if (!listenersForEvent) return; - for (var i = 0, len = listenersForEvent.length; i < len; i++) { - // avoiding try/catch for perf. reasons - listenersForEvent[i](options || { }); - } - } -}; - -/** - * Alias for observe - * @method observe - * @memberOf fabric.Observable - */ -fabric.Observable.on = fabric.Observable.observe; - -/** - * Alias for stopObserving - * @method off - */ -fabric.Observable.off = fabric.Observable.stopObserving; -(function() { - - /** - * @namespace - */ - fabric.util = { }; - - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @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; - } - - /** - * Returns random number between 2 specified ones. - * @static - * @method getRandomInt - * @memberOf fabric.util - * @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; - } - - var PiBy180 = Math.PI / 180; - - /** - * Transforms degrees to radians. - * @static - * @method degreesToRadians - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - function degreesToRadians(degrees) { - return degrees * PiBy180; - } - - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @method toFixed - * @memberOf fabric.util - * @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 - * @memberOf fabric.util - * @return {Boolean} - */ - function falseFunction() { - return false; - } - - /** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @method animate - * @memberOf fabric.util - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Number} [options.startValue=0] Starting value - * @param {Number} [options.endValue=100] Ending value - * @param {Number} [options.byValue=100] Value to modify the property by - * @param {Function} [options.easing] Easing function - * @param {Number} [options.duration=500] Duration of change - */ - function animate(options) { - - options || (options = { }); - - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; - - options.onStart && options.onStart(); - - (function tick() { - time = +new Date(); - var currentTime = time > finish ? duration : (time - start); - onChange(easing(currentTime, startValue, byValue, duration)); - if (time > finish || abort()) { - options.onComplete && options.onComplete(); - return; - } - requestAnimFrame(tick); - })(); - } - - var _requestAnimFrame = fabric.window.requestAnimationFrame || - fabric.window.webkitRequestAnimationFrame || - fabric.window.mozRequestAnimationFrame || - fabric.window.oRequestAnimationFrame || - fabric.window.msRequestAnimationFrame || - function(callback) { - fabric.window.setTimeout(callback, 1000 / 60); - }; - /** - * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * @method requestAnimFrame - * @memberOf fabric.util - * @param {Function} callback Callback to invoke - * @param {DOMElement} element optional Element to associate with animation - */ - var requestAnimFrame = function() { - return _requestAnimFrame.apply(fabric.window, arguments); - }; - - /** - * Loads image element from given url and passes it to a callback - * @method loadImage - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {Any} context optional Context to invoke callback in - */ - function loadImage(url, callback, context) { - if (url) { - var img = new Image(); - /** @ignore */ - img.onload = function () { - callback && callback.call(context, img); - img = img.onload = null; - }; - img.src = url; - } - else { - callback && callback.call(context, url); - } - } - - function enlivenObjects(objects, callback) { - - function getKlass(type) { - return fabric[fabric.util.string.camelize(fabric.util.string.capitalize(type))]; - } - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - if (callback) { - callback(enlivenedObjects); - } - } - } - - var enlivenedObjects = [ ], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - objects.forEach(function (o, index) { - if (!o.type) { - return; - } - var klass = getKlass(o.type); - if (klass.async) { - klass.fromObject(o, function (o) { - enlivenedObjects[index] = o; - onLoaded(); - }); - } - else { - enlivenedObjects[index] = klass.fromObject(o); - onLoaded(); - } - }); - } - - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @method groupSVGElements - * @param {Array} elements - * @param {Object} options optional - * @return {String} path optional - */ - function groupSVGElements(elements, options, path) { - var object = elements.length > 1 - ? new fabric.PathGroup(elements, options) - : elements[0]; - - if (typeof path !== 'undefined') { - object.setSourcePath(path); - } - return object; - } - - fabric.util.removeFromArray = removeFromArray; - fabric.util.degreesToRadians = degreesToRadians; - fabric.util.toFixed = toFixed; - fabric.util.getRandomInt = getRandomInt; - fabric.util.falseFunction = falseFunction; - fabric.util.animate = animate; - fabric.util.requestAnimFrame = requestAnimFrame; - fabric.util.loadImage = loadImage; - fabric.util.enlivenObjects = enlivenObjects; - fabric.util.groupSVGElements = groupSVGElements; -})(); -(function() { - - var slice = Array.prototype.slice; - - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { - if (this === void 0 || this === null) { - throw new TypeError(); - } - var t = Object(this), len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 0) { - n = Number(arguments[1]); - if (n !== n) { // shortcut for verifying if it's NaN - n = 0; - } - else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - 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 array contains no values, no initial value to return - 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; - }; - } - - /** - * Invokes method on all items in a given array - * @method invoke - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} method Name of a method to invoke - */ - 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; - } - - /** - * Finds maximum value in array (not necessarily "first" one) - * @method max - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - */ - function max(array, byProperty) { - if (!array || array.length === 0) return undefined; - - 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; - } - - /** - * Finds minimum value in array (not necessarily "first" one) - * @method min - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - */ - function min(array, byProperty) { - if (!array || array.length === 0) return undefined; - - 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; - } - - /** @namespace */ - fabric.util.array = { - invoke: invoke, - min: min, - max: max - }; - -})(); -(function(){ - - /** - * Copies all enumerable properties of one object to another - * @memberOf fabric.util.object - * @method extend - * @param {Object} destination Where to copy to - * @param {Object} source Where to copy from - */ - function extend(destination, source) { - // JScript DontEnum bug is not taken care of - for (var property in source) { - destination[property] = source[property]; - } - return destination; - } - - /** - * Creates an empty object and copies all enumerable properties of another object to it - * @method clone - * @memberOf fabric.util.object - * @param {Object} object Object to clone - */ - function clone(object) { - return extend({ }, object); - } - - /** @namespace fabric.util.object */ - fabric.util.object = { - extend: extend, - clone: clone - }; - -})(); -(function() { - -if (!String.prototype.trim) { - /** - * Trims a string (removing whitespace from the beginning and the end) - * @method trim - * @see String#trim on MDN - */ - String.prototype.trim = function () { - // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now - return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); - }; -} - -/** - * Camelizes a string - * @memberOf fabric.util.string - * @method camelize - * @param {String} string String to camelize - * @return {String} Camelized version of a string - */ -function camelize(string) { - return string.replace(/-+(.)?/g, function(match, character) { - return character ? character.toUpperCase() : ''; - }); -} - -/** - * Capitalizes a string - * @memberOf fabric.util.string - * @method capitalize - * @param {String} string String to capitalize - * @return {String} Capitalized version of a string - */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); -} - -function escapeXml(string) { - return string.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); -} - -/** @namespace */ -fabric.util.string = { - camelize: camelize, - capitalize: capitalize, - escapeXml: escapeXml -}; -}()); - -(function() { - - var slice = Array.prototype.slice, - apply = Function.prototype.apply, - Dummy = function() { }; - - if (!Function.prototype.bind) { - /** - * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) - * @see Function#bind on MDN - * @param {Object} thisArg Object to bind function to - * @param {Any[]} [...] Values to pass to a bound function - * @return {Function} - */ - Function.prototype.bind = function(thisArg) { - var fn = this, args = slice.call(arguments, 1), bound; - if (args.length) { - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); - }; - } - else { - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); - }; - } - Dummy.prototype = this.prototype; - bound.prototype = new Dummy(); - - return bound; - }; - } - -})(); -(function() { - - var slice = Array.prototype.slice, emptyFunction = function() { }; - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - /** @ignore */ - var addMethods = function(klass, source, parent) { - for (var property in source) { - - if (property in klass.prototype && typeof klass.prototype[property] === 'function') { - - klass.prototype[property] = (function(property) { - return function() { - - var superclass = this.constructor.superclass; - this.constructor.superclass = parent; - var returnValue = source[property].apply(this, arguments); - this.constructor.superclass = superclass; - - if (property !== 'initialize') { - return returnValue; - } - }; - })(property); - } - else { - klass.prototype[property] = source[property]; - } - - if (IS_DONTENUM_BUGGY) { - if (source.toString !== Object.prototype.toString) { - klass.prototype.toString = source.toString; - } - if (source.valueOf !== Object.prototype.valueOf) { - klass.prototype.valueOf = source.valueOf; - } - } - } - }; - - function Subclass() { } - - /** - * Helper for creation of "classes" - * @method createClass - * @memberOf fabric.util - */ - 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], parent); - } - if (!klass.prototype.initialize) { - klass.prototype.initialize = emptyFunction; - } - klass.prototype.constructor = klass; - return klass; - } - - fabric.util.createClass = createClass; -})(); -(function () { - - /* 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 fabric.document.documentElement.uniqueID !== 'undefined') { - return function (element) { - return element.uniqueID; - }; - } - var uid = 0; - return function (element) { - return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); - }; - })(); - - /** @ignore */ - var getElement, setElement; - - (function () { - var elements = { }; - /** @ignore */ - getElement = function (uid) { - return elements[uid]; - }; - /** @ignore */ - 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 || fabric.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 || fabric.window.event); - } - } - }; - } - - var shouldUseAddListenerRemoveListener = ( - areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && - areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), - - shouldUseAttachEventDetachEvent = ( - areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && - areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), - - // IE branch - listeners = { }, - - // DOM L0 branch - handlers = { }, - - addListener, removeListener; - - if (shouldUseAddListenerRemoveListener) { - /** @ignore */ - addListener = function (element, eventName, handler) { - element.addEventListener(eventName, handler, false); - }; - /** @ignore */ - removeListener = function (element, eventName, handler) { - element.removeEventListener(eventName, handler, false); - }; - } - - else if (shouldUseAttachEventDetachEvent) { - /** @ignore */ - 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); - }; - /** @ignore */ - 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 { - /** @ignore */ - 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); - }; - /** @ignore */ - 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); - } - } - } - }; - } - - /** - * Adds an event listener to an element - * @mthod addListener - * @memberOf fabric.util - * @function - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.addListener = addListener; - - /** - * Removes an event listener from an element - * @mthod removeListener - * @memberOf fabric.util - * @function - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.removeListener = removeListener; - - /** - * Cross-browser wrapper for getting event's coordinates - * @method getPointer - * @memberOf fabric.util - * @param {Event} event - */ - function getPointer(event) { - // TODO (kangax): this method needs fixing - return { x: pointerX(event), y: pointerY(event) }; - } - - var pointerX = function(event) { - var docElement = fabric.document.documentElement, - body = fabric.document.body || { scrollLeft: 0 }; - - // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) - // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] - // need to investigate later - return event.pageX || ((typeof event.clientX !== 'unknown' ? event.clientX : 0) + - (docElement.scrollLeft || body.scrollLeft) - - (docElement.clientLeft || 0)); - }; - - var pointerY = function(event) { - var docElement = fabric.document.documentElement, - body = fabric.document.body || { scrollTop: 0 }; - - return event.pageY || ((typeof event.clientY !== 'unknown' ? event.clientY : 0) + - (docElement.scrollTop || body.scrollTop) - - (docElement.clientTop || 0)); - }; - - if (fabric.isTouchSupported) { - pointerX = function(event) { - return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX; - }; - pointerY = function(event) { - return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY; - }; - } - - fabric.util.getPointer = getPointer; - - fabric.util.object.extend(fabric.util, fabric.Observable); - -})(); -(function () { - - /** - * Cross-browser wrapper for setting element's style - * @method setStyle - * @memberOf fabric.util - * @param {HTMLElement} element - * @param {Object} styles - * @return {HTMLElement} Element that was passed as a first argument - */ - function setStyle(element, styles) { - var elementStyle = element.style; - if (!elementStyle) { - return element; - } - 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 = fabric.document.createElement('div'), - supportsOpacity = typeof parseEl.style.opacity === 'string', - supportsFilters = typeof parseEl.style.filter === 'string', - reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, - - /** @ignore */ - setOpacity = function (element) { return element; }; - - if (supportsOpacity) { - /** @ignore */ - setOpacity = function(element, value) { - element.style.opacity = value; - return element; - }; - } - else if (supportsFilters) { - /** @ignore */ - 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() { - - var _slice = Array.prototype.slice; - - /** - * Takes id and returns an element with that id (if one exists in a document) - * @method getById - * @memberOf fabric.util - * @param {String|HTMLElement} id - * @return {HTMLElement|null} - */ - function getById(id) { - return typeof id === 'string' ? fabric.document.getElementById(id) : id; - } - - /** - * Converts an array-like object (e.g. arguments or NodeList) to an array - * @method toArray - * @memberOf fabric.util - * @param {Object} arrayLike - * @return {Array} - */ - var toArray = function(arrayLike) { - return _slice.call(arrayLike, 0); - }; - - var sliceCanConvertNodelists; - try { - sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; - } - catch(err) { } - - if (!sliceCanConvertNodelists) { - toArray = function(arrayLike) { - var arr = new Array(arrayLike.length), i = arrayLike.length; - while (i--) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - /** - * Creates specified element with specified attributes - * @method makeElement - * @memberOf fabric.util - * @param {String} tagName Type of an element to create - * @param {Object} [attributes] Attributes to set on an element - * @return {HTMLElement} Newly created element - */ - function makeElement(tagName, attributes) { - var el = fabric.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; - } - - /** - * Adds class to an element - * @method addClass - * @memberOf fabric.util - * @param {HTMLElement} element Element to add class to - * @param {String} className Class to add to an element - */ - function addClass(element, className) { - if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { - element.className += (element.className ? ' ' : '') + className; - } - } - - /** - * Wraps element with another element - * @method wrapElement - * @memberOf fabric.util - * @param {HTMLElement} element Element to wrap - * @param {HTMLElement|String} wrapper Element to wrap with - * @param {Object} [attributes] Attributes to set on a wrapper - * @return {HTMLElement} wrapper - */ - 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; - } - - /** - * Returns offset for a given element - * @method getElementOffset - * @function - * @memberOf fabric.util - * @param {HTMLElement} element Element to get offset for - * @return {Object} Object with "left" and "top" properties - */ - function getElementOffset(element) { - // TODO (kangax): need to fix this method - 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 = fabric.document.documentElement.style; - - var selectProp = 'userSelect' in style - ? 'userSelect' - : 'MozUserSelect' in style - ? 'MozUserSelect' - : 'WebkitUserSelect' in style - ? 'WebkitUserSelect' - : 'KhtmlUserSelect' in style - ? 'KhtmlUserSelect' - : ''; - - /** - * Makes element unselectable - * @method makeElementUnselectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make unselectable - * @return {HTMLElement} Element that was passed in - */ - 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; - } - - /** - * Makes element selectable - * @method makeElementSelectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make selectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementSelectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = null; - } - if (selectProp) { - element.style[selectProp] = ''; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = ''; - } - return element; - } - - fabric.util.makeElementUnselectable = makeElementUnselectable; - fabric.util.makeElementSelectable = makeElementSelectable; - })(); - - (function() { - - /** - * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading - * @method getScript - * @memberOf fabric.util - * @param {String} url URL of a script to load - * @param {Function} callback Callback to execute when script is finished loading - */ - function getScript(url, callback) { - var headEl = fabric.document.getElementsByTagName("head")[0], - scriptEl = fabric.document.createElement('script'), - loading = true; - - scriptEl.type = 'text/javascript'; - scriptEl.setAttribute('runat', 'server'); - - /** @ignore */ - scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { - if (loading) { - if (typeof this.readyState === 'string' && - this.readyState !== 'loaded' && - this.readyState !== 'complete') return; - loading = false; - callback(e || fabric.window.event); - scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; - } - }; - scriptEl.src = url; - headEl.appendChild(scriptEl); - // causes issue in Opera - // headEl.removeChild(scriptEl); - } - - fabric.util.getScript = getScript; - })(); - - fabric.util.getById = getById; - fabric.util.toArray = toArray; - fabric.util.makeElement = makeElement; - fabric.util.addClass = addClass; - fabric.util.wrapElement = wrapElement; - fabric.util.getElementOffset = getElementOffset; - -})(); -(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() { } - - /** - * Cross-browser abstraction for sending XMLHttpRequest - * @method request - * @memberOf fabric.util - * @param {String} url URL to send XMLHttpRequest to - * @param {Object} [options] Options object - * @param {String} [options.method="GET"] - * @param {Function} options.onComplete Callback to invoke when request is completed - * @return {XMLHttpRequest} request - */ - function request(url, options) { - - options || (options = { }); - - var method = options.method ? options.method.toUpperCase() : 'GET', - onComplete = options.onComplete || function() { }, - xhr = makeXHR(), - body; - - /** @ignore */ - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - onComplete(xhr); - xhr.onreadystatechange = emptyFn; - } - }; - - if (method === 'GET') { - body = null; - if (typeof options.parameters === 'string') { - url = addParamToUrl(url, options.parameters); - } - } - - xhr.open(method, url, true); - - if (method === 'POST' || method === 'PUT') { - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - } - - xhr.send(body); - return xhr; - } - - fabric.util.request = request; -})(); -(function() { - - /** - * @method easeInQuad - * @memberOf fabric.util.ease - */ - function easeInQuad(t, b, c, d) { - return c*(t/=d)*t + b; - } - - /** - * @method easeOutQuad - * @memberOf fabric.util.ease - */ - function easeOutQuad(t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - } - - /** - * @method easeInOutQuad - * @memberOf fabric.util.ease - */ - function easeInOutQuad(t, b, c, d) { - t /= (d/2); - if (t < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - } - - /** - * @method easeInCubic - * @memberOf fabric.util.ease - */ - function easeInCubic(t, b, c, d) { - return c*(t/=d)*t*t + b; - } - - /** - * @method easeOutCubic - * @memberOf fabric.util.ease - */ - function easeOutCubic(t, b, c, d) { - return c*((t=t/d-1)*t*t + 1) + b; - } - - /** - * @method easeInOutCubic - * @memberOf fabric.util.ease - */ - function easeInOutCubic(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t + b; - return c/2*((t-=2)*t*t + 2) + b; - } - - /** - * @method easeInQuart - * @memberOf fabric.util.ease - */ - function easeInQuart(t, b, c, d) { - return c*(t/=d)*t*t*t + b; - } - - /** - * @method easeOutQuart - * @memberOf fabric.util.ease - */ - function easeOutQuart(t, b, c, d) { - return -c * ((t=t/d-1)*t*t*t - 1) + b; - } - - /** - * @method easeInOutQuart - * @memberOf fabric.util.ease - */ - function easeInOutQuart(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t*t + b; - return -c/2 * ((t-=2)*t*t*t - 2) + b; - } - - /** - * @method easeInQuint - * @memberOf fabric.util.ease - */ - function easeInQuint(t, b, c, d) { - return c*(t/=d)*t*t*t*t + b; - } - - /** - * @method easeOutQuint - * @memberOf fabric.util.ease - */ - function easeOutQuint(t, b, c, d) { - return c*((t=t/d-1)*t*t*t*t + 1) + b; - } - - /** - * @method easeInOutQuint - * @memberOf fabric.util.ease - */ - function easeInOutQuint(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t*t*t + b; - return c/2*((t-=2)*t*t*t*t + 2) + b; - } - - /** - * @method easeInSine - * @memberOf fabric.util.ease - */ - function easeInSine(t, b, c, d) { - return -c * Math.cos(t/d * (Math.PI/2)) + c + b; - } - - /** - * @method easeOutSine - * @memberOf fabric.util.ease - */ - function easeOutSine(t, b, c, d) { - return c * Math.sin(t/d * (Math.PI/2)) + b; - } - - /** - * @method easeInOutSine - * @memberOf fabric.util.ease - */ - function easeInOutSine(t, b, c, d) { - return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; - } - - /** - * @method easeInExpo - * @memberOf fabric.util.ease - */ - function easeInExpo(t, b, c, d) { - return (t===0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; - } - - /** - * @method easeOutExpo - * @memberOf fabric.util.ease - */ - function easeOutExpo(t, b, c, d) { - return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; - } - - /** - * @method easeInOutExpo - * @memberOf fabric.util.ease - */ - function easeInOutExpo(t, b, c, d) { - if (t===0) return b; - if (t===d) return b+c; - t /= d/2; - if (t < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; - return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; - } - - /** - * @method easeInCirc - * @memberOf fabric.util.ease - */ - function easeInCirc(t, b, c, d) { - return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; - } - - /** - * @method easeOutCirc - * @memberOf fabric.util.ease - */ - function easeOutCirc(t, b, c, d) { - return c * Math.sqrt(1 - (t=t/d-1)*t) + b; - } - - /** - * @method easeInOutCirc - * @memberOf fabric.util.ease - */ - function easeInOutCirc(t, b, c, d) { - t /= d/2; - if (t < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; - return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; - } - - /** - * @method easeInElastic - * @memberOf fabric.util.ease - */ - function easeInElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d; - if (t===1) return b+c; - if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - } - - /** - * @method easeOutElastic - * @memberOf fabric.util.ease - */ - function easeOutElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d; - if (t===1) return b+c; - if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; - } - - /** - * @method easeInOutElastic - * @memberOf fabric.util.ease - */ - function easeInOutElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d/2; - if (t===2) return b+c; - if (!p) p=d*(0.3*1.5); - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; - } - - /** - * @method easeInBack - * @memberOf fabric.util.ease - */ - function easeInBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - return c*(t/=d)*t*((s+1)*t - s) + b; - } - - /** - * @method easeOutBack - * @memberOf fabric.util.ease - */ - function easeOutBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; - } - - /** - * @method easeInOutBack - * @memberOf fabric.util.ease - */ - function easeInOutBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - t /= d/2; - if (t < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; - return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; - } - - /** - * @method easeInBounce - * @memberOf fabric.util.ease - */ - function easeInBounce(t, b, c, d) { - return c - easeOutBounce (d-t, 0, c, d) + b; - } - - /** - * @method easeOutBounce - * @memberOf fabric.util.ease - */ - function easeOutBounce(t, b, c, d) { - if ((t/=d) < (1/2.75)) { - return c*(7.5625*t*t) + b; - } else if (t < (2/2.75)) { - return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; - } else if (t < (2.5/2.75)) { - return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; - } else { - return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; - } - } - - /** - * @method easeInOutBounce - * @memberOf fabric.util.ease - */ - function easeInOutBounce(t, b, c, d) { - if (t < d/2) return easeInBounce (t*2, 0, c, d) * 0.5 + b; - return easeOutBounce (t*2-d, 0, c, d) * 0.5 + c*0.5 + b; - } - - /** @namespace fabric.util.ease */ - fabric.util.ease = { - easeInQuad: easeInQuad, - easeOutQuad: easeOutQuad, - easeInOutQuad: easeInOutQuad, - easeInCubic: easeInCubic, - easeOutCubic: easeOutCubic, - easeInOutCubic: easeInOutCubic, - easeInQuart: easeInQuart, - easeOutQuart: easeOutQuart, - easeInOutQuart: easeInOutQuart, - easeInQuint: easeInQuint, - easeOutQuint: easeOutQuint, - easeInOutQuint: easeInOutQuint, - easeInSine: easeInSine, - easeOutSine: easeOutSine, - easeInOutSine: easeInOutSine, - easeInExpo: easeInExpo, - easeOutExpo: easeOutExpo, - easeInOutExpo: easeInOutExpo, - easeInCirc: easeInCirc, - easeOutCirc: easeOutCirc, - easeInOutCirc: easeInOutCirc, - easeInElastic: easeInElastic, - easeOutElastic: easeOutElastic, - easeInOutElastic: easeInOutElastic, - easeInBack: easeInBack, - easeOutBack: easeOutBack, - easeInOutBack: easeInOutBack, - easeInBounce: easeInBounce, - easeOutBounce: easeOutBounce, - easeInOutBounce: easeInOutBounce - }; - -}()); -(function(global) { - - "use strict"; - - /** - * @name fabric - * @namespace - */ - - var fabric = global.fabric || (global.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', - 'text-decoration': 'textDecoration', - 'font-size': 'fontSize', - 'font-weight': 'fontWeight', - 'font-style': 'fontStyle', - 'font-family': 'fontFamily' - }; - - function normalizeAttr(attr) { - // transform attribute names - if (attr in attributesMap) { - return attributesMap[attr]; - } - return attr; - } - - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @method parseAttributes - * @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 there's a parent container (`g` node), parse its attributes recursively upwards - 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) { - // "normalize" attribute values - 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); - } - attr = normalizeAttr(attr); - memo[attr] = isNaN(parsed) ? value : parsed; - } - return memo; - }, { }); - - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - - ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); - return extend(parentAttributes, ownAttributes); - } - - /** - * Parses "transform" attribute, returning an array of values - * @static - * @function - * @memberOf fabric - * @method 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]; - } - } - - // identity matrix - var iMatrix = [ - 1, // a - 0, // b - 0, // c - 1, // d - 0, // e - 0 // f - ], - - // == begin transform regexp - 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*$', - - // http://www.w3.org/TR/SVG/coords.html#TransformAttribute - reTransformList = new RegExp(transform_list), - // == end transform regexp - - reTransform = new RegExp(transform); - - return function(attributeValue) { - - // start with identity matrix - var matrix = iMatrix.concat(); - - // return if no argument was given or - // an argument does not match transform attribute regexp - 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; - }; - })(); - - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @method parsePointsAttribute - * @param points {String} points attribute string - * @return {Array} array of points - */ - function parsePointsAttribute(points) { - - // points attribute is required and must not be empty - if (!points) return null; - - points = points.trim(); - var asPairs = points.indexOf(',') > -1; - - points = points.split(/\s+/); - var parsedPoints = [ ], i, len; - - // points could look like "10,20 30,40" or "10 20 30 40" - if (asPairs) { - i = 0; - len = points.length; - for (; i < len; i++) { - var pair = points[i].split(','); - parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); - } - } - else { - i = 0; - len = points.length; - for (; i < len; i+=2) { - parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); - } - } - - // odd number of points is an error - if (parsedPoints.length % 2 !== 0) { - // return null; - } - - return parsedPoints; - } - - /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @method parseStyleAttribute - * @param {SVGElement} element 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) return oStyle; - - if (typeof style === 'string') { - style = style.replace(/;$/, '').split(';').forEach(function (current) { - var attr = current.split(':'); - oStyle[normalizeAttr(attr[0].trim().toLowerCase())] = attr[1].trim(); - }); - } - else { - for (var prop in style) { - if (typeof style[prop] === 'undefined') continue; - oStyle[normalizeAttr(prop.toLowerCase())] = style[prop]; - } - } - - return oStyle; - } - - function resolveGradients(instances) { - for (var i = instances.length; i--; ) { - var instanceFillValue = instances[i].get('fill'); - - if (/^url\(/.test(instanceFillValue)) { - - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - - if (fabric.gradientDefs[gradientId]) { - instances[i].set('fill', - fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); - } - } - } - } - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @method parseElements - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} options Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function parseElements(elements, callback, options, reviver) { - var instances = new Array(elements.length), i = elements.length; - - function checkIfDone() { - if (--i === 0) { - instances = instances.filter(function(el) { - return el != null; - }); - resolveGradients(instances); - callback(instances); - } - } - - for (var index = 0, el, len = elements.length; index < len; index++) { - el = elements[index]; - var klass = fabric[capitalize(el.tagName)]; - if (klass && klass.fromElement) { - try { - if (klass.async) { - klass.fromElement(el, (function(index, el) { - return function(obj) { - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - }; - })(index), options); - } - else { - var obj = klass.fromElement(el, options); - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - } - } - catch(e) { - fabric.log(e.message || e); - } - } - else { - checkIfDone(); - } - } - } - - /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @method getCSSRules - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document - */ - function getCSSRules(doc) { - var styles = doc.getElementsByTagName('style'), - allRules = { }, - rules; - - // very crude parsing of style contents - for (var i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[0].textContent; - - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - - rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = rules.map(function(rule) { return rule.trim(); }); - - rules.forEach(function(rule) { - var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); - rule = match[1]; - var declaration = match[2].trim(), - propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); - - if (!allRules[rule]) { - allRules[rule] = { }; - } - - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; - - allRules[rule][property] = value; - } - }); - } - - return allRules; - } - - function getGlobalStylesForElement(element) { - var nodeName = element.nodeName, - className = element.getAttribute('class'), - id = element.getAttribute('id'), - styles = { }; - - for (var rule in fabric.cssRules) { - var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || - (id && new RegExp('^#' + id).test(rule)) || - (new RegExp('^' + nodeName).test(rule)); - - if (ruleMatchesElement) { - for (var property in fabric.cssRules[rule]) { - styles[property] = fabric.cssRules[rule][property]; - } - } - } - - return styles; - } - - /** - * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback - * @static - * @function - * @memberOf fabric - * @method parseSVGDocument - * @param {SVGDocument} doc SVG document to parse - * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - fabric.parseSVGDocument = (function() { - - var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; - - // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute - // \d doesn't quite cut it (as we need to match an actual float number) - - // matches, e.g.: +14.56e-12, etc. - var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; - - var reViewBoxAttrValue = new RegExp( - '^' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*' + - '$' - ); - - function hasAncestorWithNodeName(element, nodeName) { - while (element && (element = element.parentNode)) { - if (nodeName.test(element.nodeName)) { - return true; - } - } - return false; - } - - return function(doc, callback, reviver) { - if (!doc) return; - - var startTime = new Date(), - descendants = fabric.util.toArray(doc.getElementsByTagName('*')); - - if (descendants.length === 0) { - // we're likely in node, where "o3-xml" library fails to gEBTN("*") - // https://github.com/ajaxorg/node-o3-xml/issues/21 - descendants = doc.selectNodes("//*[name(.)!='svg']"); - var arr = [ ]; - for (var i = 0, len = descendants.length; i < len; i++) { - arr[i] = descendants[i]; - } - descendants = arr; - } - - var elements = descendants.filter(function(el) { - return reAllowedSVGTagNames.test(el.tagName) && - !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement - }); - - 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); - } - - // values of width/height attributes overwrite those extracted from viewbox attribute - width = widthAttr ? parseFloat(widthAttr) : width; - height = heightAttr ? parseFloat(heightAttr) : height; - - var options = { - width: width, - height: height - }; - - fabric.gradientDefs = fabric.getGradientDefs(doc); - fabric.cssRules = getCSSRules(doc); - - // Precedence of rules: style > class > attribute - - fabric.parseElements(elements, function(instances) { - fabric.documentParsingTime = new Date() - startTime; - if (callback) { - callback(instances, options); - } - }, clone(options), reviver); - }; - })(); - - /** - * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) - * @property - * @namespace - */ - var svgCache = { - - /** - * @method has - * @param {String} name - * @param {Function} callback - */ - has: function (name, callback) { - callback(false); - }, - - /** - * @method get - * @param {String} url - * @param {Function} callback - */ - get: function () { - /* NOOP */ - }, - - /** - * @method set - * @param {String} url - * @param {Object} object - */ - set: function () { - /* NOOP */ - } - }; - - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects - * @method loadSVGFromURL - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromURL(url, callback, reviver) { - - url = url.replace(/^\n\s*/, '').trim(); - - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { - xml = new ActiveXObject('Microsoft.XMLDOM'); - xml.async = 'false'; - //IE chokes on DOCTYPE - xml.loadXML(r.responseText.replace(//i,'')); - } - if (!xml.documentElement) return; - - fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); - callback(results, options); - }, reviver); - } - } - - /** - * @method _enlivenCachedObject - */ - function _enlivenCachedObject(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 }); - } - - /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @method loadSVGFromString - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromString(string, callback, reviver) { - string = string.trim(); - var doc; - if (typeof DOMParser !== 'undefined') { - var parser = new DOMParser(); - if (parser && parser.parseFromString) { - doc = parser.parseFromString(string, 'text/xml'); - } - } - else if (fabric.window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - //IE chokes on DOCTYPE - doc.loadXML(string.replace(//i,'')); - } - - fabric.parseSVGDocument(doc.documentElement, function (results, options) { - callback(results, options); - }, reviver); - } - - function createSVGFontFacesMarkup(objects) { - var markup = ''; - - for (var i = 0, len = objects.length; i < len; i++) { - if (objects[i].type !== 'text' || !objects[i].path) continue; - - markup += [ - '@font-face {', - 'font-family: ', objects[i].fontFamily, '; ', - 'src: url(\'', objects[i].path, '\')', - '}' - ].join(''); - } - - if (markup) { - markup = [ - '', - '', - '' - ].join(''); - } - - return markup; - } - - extend(fabric, { - - parseAttributes: parseAttributes, - parseElements: parseElements, - parseStyleAttribute: parseStyleAttribute, - parsePointsAttribute: parsePointsAttribute, - getCSSRules: getCSSRules, - - loadSVGFromURL: loadSVGFromURL, - loadSVGFromString: loadSVGFromString, - - createSVGFontFacesMarkup: createSVGFontFacesMarkup - }); - -})(typeof exports !== 'undefined' ? exports : this); - -(function() { - - function getColorStopFromStyle(el) { - var style = el.getAttribute('style'); - - if (style) { - var keyValuePairs = style.split(/\s*;\s*/); - - if (keyValuePairs[keyValuePairs.length-1] === '') { - keyValuePairs.pop(); - } - - for (var i = keyValuePairs.length; i--; ) { - - var split = keyValuePairs[i].split(/\s*:\s*/), - key = split[0].trim(), - value = split[1].trim(); - - if (key === 'stop-color') { - return value; - } - } - } - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Gradient = fabric.util.createClass(/** @scope fabric.Gradient.prototype */ { - - initialize: function(options) { - - options || (options = { }); - - this.x1 = options.x1 || 0; - this.y1 = options.y1 || 0; - this.x2 = options.x2 || 0; - this.y2 = options.y2 || 0; - - this.colorStops = options.colorStops; - }, - - toObject: function() { - return { - x1: this.x1, - x2: this.x2, - y1: this.y1, - y2: this.y2, - colorStops: this.colorStops - }; - }, - - toLiveGradient: function(ctx) { - var gradient = ctx.createLinearGradient( - this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); - - for (var position in this.colorStops) { - var colorValue = this.colorStops[position]; - gradient.addColorStop(parseFloat(position), colorValue); - } - - return gradient; - } - }); - - fabric.util.object.extend(fabric.Gradient, { - - /** - * @method fromElement - * @static - * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement - */ - fromElement: function(el, instance) { - - /** - * @example: - * - * - * - * - * - * - * OR - * - * - * - * - * - * - */ - - var colorStopEls = el.getElementsByTagName('stop'), - offset, - colorStops = { }, - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; - - for (var i = colorStopEls.length; i--; ) { - el = colorStopEls[i]; - offset = el.getAttribute('offset'); - - // convert percents to absolute values - offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); - colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); - } - - _convertPercentUnitsToValues(instance, coords); - - return new fabric.Gradient({ - x1: coords.x1, - y1: coords.y1, - x2: coords.x2, - y2: coords.y2, - colorStops: colorStops - }); - }, - - /** - * @method forObject - * @static - */ - forObject: function(obj, options) { - options || (options = { }); - _convertPercentUnitsToValues(obj, options); - return new fabric.Gradient(options); - } - }); - - function _convertPercentUnitsToValues(object, options) { - for (var prop in options) { - if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { - var percents = parseFloat(options[prop], 10); - if (prop === 'x1' || prop === 'x2') { - options[prop] = object.width * percents / 100; - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] = object.height * percents / 100; - } - } - // normalize rendering point (should be from top/left corner rather than center of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] -= object.width / 2; - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] -= object.height / 2; - } - } - } - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @method getGradientDefs - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - function getGradientDefs(doc) { - var linearGradientEls = doc.getElementsByTagName('linearGradient'), - radialGradientEls = doc.getElementsByTagName('radialGradient'), - el, i, - gradientDefs = { }; - - i = linearGradientEls.length; - for (; i--; ) { - el = linearGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - i = radialGradientEls.length; - for (; i--; ) { - el = radialGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - return gradientDefs; - } - - fabric.getGradientDefs = getGradientDefs; - -})(); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Point) { - fabric.warn('fabric.Point is already defined'); - return; - } - - fabric.Point = Point; - - /** - * @name Point - * @memberOf fabric - * @constructor - * @param {Number} x - * @param {Number} y - * @return {fabric.Point} thisArg - */ - function Point(x, y) { - if (arguments.length > 0) { - this.init(x, y); - } - } - - Point.prototype = /** @scope fabric.Point.prototype */ { - - constructor: Point, - - /** - * @method init - * @param {Number} x - * @param {Number} y - */ - init: function (x, y) { - this.x = x; - this.y = y; - }, - - /** - * @method add - * @param {fabric.Point} that - * @return {fabric.Point} new Point instance with added values - */ - add: function (that) { - return new Point(this.x + that.x, this.y + that.y); - }, - - /** - * @method addEquals - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - */ - addEquals: function (that) { - this.x += that.x; - this.y += that.y; - return this; - }, - - /** - * @method scalarAdd - * @param {Number} scalar - * @return {fabric.Point} new Point with added value - */ - scalarAdd: function (scalar) { - return new Point(this.x + scalar, this.y + scalar); - }, - - /** - * @method scalarAddEquals - * @param {Number} scalar - * @param {fabric.Point} thisArg - */ - scalarAddEquals: function (scalar) { - this.x += scalar; - this.y += scalar; - return this; - }, - - /** - * @method subtract - * @param {fabric.Point} that - * @return {fabric.Point} new Point object with subtracted values - */ - subtract: function (that) { - return new Point(this.x - that.x, this.y - that.y); - }, - - /** - * @method subtractEquals - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - */ - 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; - } - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Intersection) { - fabric.warn('fabric.Intersection is already defined'); - return; - } - - /** - * @class Intersection - * @memberOf fabric - */ - function Intersection(status) { - if (arguments.length > 0) { - this.init(status); - } - } - - fabric.Intersection = Intersection; - - fabric.Intersection.prototype = /** @scope fabric.Intersection.prototype */ { - - /** - * @method init - * @param {String} status - */ - init: function (status) { - this.status = status; - this.points = []; - }, - - /** - * @method appendPoint - * @param {String} status - */ - appendPoint: function (point) { - this.points.push(point); - }, - - /** - * @method appendPoints - * @param {String} status - */ - appendPoints: function (points) { - this.points = this.points.concat(points); - } - }; - - /** - * @static - * @method intersectLineLine - */ - fabric.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; - }; - - /** - * @method intersectLinePolygon - */ - fabric.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; - }; - - /** - * @method intersectPolygonPolygon - */ - fabric.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; - }; - - /** - * @method intersectPolygonRectangle - */ - fabric.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; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Color) { - fabric.warn('fabric.Color is already defined.'); - return; - } - - /** - * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; - * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. - * - * @class Color - * @memberOf fabric - * @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); - } - } - - fabric.Color = Color; - - fabric.Color.prototype = /** @scope fabric.Color.prototype */ { - - /** - * @private - * @method _tryParsingColor - */ - _tryParsingColor: function(color) { - var source = Color.sourceFromHex(color); - if (!source) { - source = Color.sourceFromRgb(color); - } - if (source) { - this.setSource(source); - } - }, - - /** - * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @method getSource - * @return {Array} - */ - getSource: function() { - return this._source; - }, - - /** - * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @method setSource - * @param {Array} source - */ - setSource: function(source) { - this._source = source; - }, - - /** - * Returns color represenation in RGB format - * @method toRgb - * @return {String} ex: rgb(0-255,0-255,0-255) - */ - toRgb: function() { - var source = this.getSource(); - return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; - }, - - /** - * Returns color represenation in RGBA format - * @method toRgba - * @return {String} ex: rgba(0-255,0-255,0-255,0-1) - */ - toRgba: function() { - var source = this.getSource(); - return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; - }, - - /** - * Returns color represenation in HEX format - * @method toHex - * @return {String} ex: FF5555 - */ - 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(); - }, - - /** - * Gets value of alpha channel for this color - * @method getAlpha - * @return {Number} 0-1 - */ - getAlpha: function() { - return this.getSource()[3]; - }, - - /** - * Sets value of alpha channel for this color - * @method setAlpha - * @param {Number} 0-1 - * @return {fabric.Color} thisArg - */ - setAlpha: function(alpha) { - var source = this.getSource(); - source[3] = alpha; - this.setSource(source); - return this; - }, - - /** - * Transforms color to its grayscale representation - * @method toGrayscale - * @return {fabric.Color} thisArg - */ - 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 {fabric.Color} thisArg - */ - 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 {String|fabric.Color} otherColor - * @return {fabric.Color} thisArg - */ - overlayWith: function(otherColor) { - if (!(otherColor instanceof Color)) { - 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[3] = alpha; - this.setSource(result); - return this; - } - }; - - /** - * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgb(255, 100, 10, 0.5), rgb(1,1,1)) - * @static - * @field - */ - fabric.Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/; - - /** - * Regex matching color in HEX format (ex: #FF5555, 010155, aff) - * @static - * @field - */ - fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; - - /** - * Returns new color object, when given a color in RGB format - * @method fromRgb - * @param {String} color ex: rgb(0-255,0-255,0-255) - * @return {fabric.Color} - */ - fabric.Color.fromRgb = function(color) { - return Color.fromSource(Color.sourceFromRgb(color)); - }; - - /** - * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format - * @method sourceFromRgb - * @param {String} color ex: rgb(0-255,0-255,0-255) - * @return {Array} source - */ - fabric.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 - ]; - } - }; - - /** - * Returns new color object, when given a color in RGBA format - * @static - * @function - * @method fromRgba - * @param {String} color - * @return {fabric.Color} - */ - fabric.Color.fromRgba = Color.fromRgb; - - /** - * Returns new color object, when given a color in HEX format - * @static - * @method fromHex - * @return {fabric.Color} - */ - fabric.Color.fromHex = function(color) { - return Color.fromSource(Color.sourceFromHex(color)); - }; - - /** - * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format - * @static - * @method sourceFromHex - * @param {String} color ex: FF5555 - * @return {Array} source - */ - fabric.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 - ]; - } - }; - - /** - * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) - * @static - * @method fromSource - * @return {fabric.Color} - */ - fabric.Color.fromSource = function(source) { - var oColor = new Color(); - oColor.setSource(source); - return oColor; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function () { - - "use strict"; - - if (fabric.StaticCanvas) { - fabric.warn('fabric.StaticCanvas is already defined.'); - return; - } - - // aliases for faster resolution - var extend = fabric.util.object.extend, - getElementOffset = fabric.util.getElementOffset, - removeFromArray = fabric.util.removeFromArray, - removeListener = fabric.util.removeListener, - - CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); - - /** - * @class fabric.StaticCanvas - * @constructor - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - fabric.StaticCanvas = function (el, options) { - options || (options = { }); - - this._initStatic(el, options); - fabric.StaticCanvas.activeInstance = this; - }; - - extend(fabric.StaticCanvas.prototype, fabric.Observable); - - extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ { - - /** - * Background color of canvas instance - * @property - * @type String - */ - backgroundColor: 'rgba(0, 0, 0, 0)', - - /** - * Background image of canvas instance - * Should be set via `setBackgroundImage` - * @property - * @type String - */ - backgroundImage: '', - - /** - * Opacity of the background image of the canvas instance - * @property - * @type Float - */ - backgroundImageOpacity: 1.0, - - /** - * Indicatus whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @property - * @type Boolean - */ - backgroundImageStretch: true, - - /** - * Indicates whether toObject/toDatalessObject should include default values - * @property - * @type Boolean - */ - includeDefaultValues: true, - - /** - * Indicates whether objects' state should be saved - * @property - * @type Boolean - */ - stateful: true, - - /** - * Indicates whether fabric.Canvas#add should also re-render canvas. - * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once - * (followed by a manual rendering after addition) - */ - renderOnAddition: true, - - /** - * Function that determines clipping of entire canvas area - * Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ - * @property - * @type Function - */ - clipTo: null, - - /** - * Indicates whether object controls (borders/corners) are rendered above overlay image - * @property - * @type Boolean - */ - controlsAboveOverlay: false, - - /** - * Callback; invoked right before object is about to be scaled/rotated - * @method onBeforeScaleRotate - * @param {fabric.Object} target Object that's about to be scaled/rotated - */ - onBeforeScaleRotate: function () { - /* NOOP */ - }, - - /** - * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps - * @method onFpsUpdate - * @param {Number} fps - */ - onFpsUpdate: null, - - _initStatic: function(el, options) { - this._objects = []; - - this._createLowerCanvas(el); - this._initOptions(options); - - if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); - } - if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); - } - this.calcOffset(); - }, - - /** - * Calculates canvas element offset relative to the document - * This method is also attached as "resize" event handler of window - * @method calcOffset - * @return {fabric.Canvas} instance - * @chainable - */ - calcOffset: function () { - this._offset = getElementOffset(this.lowerCanvasEl); - return this; - }, - - /** - * Sets overlay image for this canvas - * @method setOverlayImage - * @param {String} url url of an image to set overlay to - * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @return {fabric.Canvas} thisArg - * @chainable - */ - setOverlayImage: function (url, callback) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - callback && callback(); - }, this); - return this; - }, - - /** - * Sets background image for this canvas - * @method setBackgroundImage - * @param {String} url url of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} options optional options to set for the background image - * @return {fabric.Canvas} thisArg - * @chainable - */ - setBackgroundImage: function (url, callback, options) { - return fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); - }, - - /** - * @private - * @method _createCanvasElement - * @param {Element} element - */ - _createCanvasElement: function() { - var element = fabric.document.createElement('canvas'); - if (!element.style) { - element.style = { }; - } - if (!element) { - throw CANVAS_INIT_ERROR; - } - this._initCanvasElement(element); - return element; - }, - - _initCanvasElement: function(element) { - if (typeof element.getContext === 'undefined' && - typeof G_vmlCanvasManager !== 'undefined' && - G_vmlCanvasManager.initElement) { - - G_vmlCanvasManager.initElement(element); - } - if (typeof element.getContext === 'undefined') { - throw CANVAS_INIT_ERROR; - } - }, - - /** - * @method _initOptions - * @param {Object} options - */ - _initOptions: function (options) { - for (var prop in options) { - this[prop] = options[prop]; - } - - this.width = parseInt(this.lowerCanvasEl.width, 10) || 0; - this.height = parseInt(this.lowerCanvasEl.height, 10) || 0; - - if (!this.lowerCanvasEl.style) return; - - this.lowerCanvasEl.style.width = this.width + 'px'; - this.lowerCanvasEl.style.height = this.height + 'px'; - }, - - /** - * Creates a secondary canvas - * @method _createLowerCanvas - */ - _createLowerCanvas: function (canvasEl) { - this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); - this._initCanvasElement(this.lowerCanvasEl); - - fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); - - if (this.interactive) { - this._applyCanvasStyle(this.lowerCanvasEl); - } - - this.contextContainer = this.lowerCanvasEl.getContext('2d'); - }, - - /** - * Returns canvas width - * @method getWidth - * @return {Number} - */ - getWidth: function () { - return this.width; - }, - - /** - * Returns canvas height - * @method getHeight - * @return {Number} - */ - getHeight: function () { - return this.height; - }, - - /** - * Sets width of this canvas instance - * @method setWidth - * @param {Number} width value to set width to - * @return {fabric.Canvas} instance - * @chainable true - */ - setWidth: function (value) { - return this._setDimension('width', value); - }, - - /** - * Sets height of this canvas instance - * @method setHeight - * @param {Number} height value to set height to - * @return {fabric.Canvas} instance - * @chainable true - */ - setHeight: function (value) { - return this._setDimension('height', value); - }, - - /** - * Sets dimensions (width, height) of this canvas instance - * @method setDimensions - * @param {Object} dimensions - * @return {fabric.Canvas} thisArg - * @chainable - */ - setDimensions: function(dimensions) { - for (var prop in dimensions) { - this._setDimension(prop, dimensions[prop]); - } - return this; - }, - - /** - * Helper for setting width/height - * @private - * @method _setDimensions - * @param {String} prop property (width|height) - * @param {Number} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setDimension: function (prop, value) { - this.lowerCanvasEl[prop] = value; - this.lowerCanvasEl.style[prop] = value + 'px'; - - if (this.upperCanvasEl) { - this.upperCanvasEl[prop] = value; - this.upperCanvasEl.style[prop] = value + 'px'; - } - - if (this.wrapperEl) { - this.wrapperEl.style[prop] = value + 'px'; - } - - this[prop] = value; - - this.calcOffset(); - this.renderAll(); - - return this; - }, - - /** - * Returns <canvas> element corresponding to this instance - * @method getElement - * @return {HTMLCanvasElement} - */ - getElement: function () { - return this.lowerCanvasEl; - }, - - // placeholder - getActiveObject: function() { - return null; - }, - - // placeholder - getActiveGroup: function() { - return null; - }, - - /** - * Given a context, renders an object on that context - * @param ctx {Object} context to render object on - * @param object {Object} object to render - * @private - */ - _draw: function (ctx, object) { - if (!object) return; - - if (this.controlsAboveOverlay) { - var hasBorders = object.hasBorders, hasCorners = object.hasCorners; - object.hasBorders = object.hasCorners = false; - object.render(ctx); - object.hasBorders = hasBorders; - object.hasCorners = hasCorners; - } - else { - object.render(ctx); - } - }, - - /** - * Adds objects to canvas, then renders canvas; - * Objects should be instances of (or inherit from) fabric.Object - * @method add - * @return {fabric.Canvas} thisArg - * @chainable - */ - add: function () { - this._objects.push.apply(this._objects, arguments); - for (var i = arguments.length; i--; ) { - this._initObject(arguments[i]); - } - this.renderOnAddition && this.renderAll(); - return this; - }, - - /** - * @private - * @method _initObject - */ - _initObject: function(obj) { - this.stateful && obj.setupState(); - obj.setCoords(); - obj.canvas = this; - this.fire('object:added', { target: obj }); - obj.fire('added'); - }, - - /** - * 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 - * @param nonSplicing {Boolean} when `true`, no splicing (shifting) of objects occurs - * @return {fabric.Canvas} instance - */ - insertAt: function (object, index, nonSplicing) { - if (nonSplicing) { - this._objects[index] = object; - } - else { - this._objects.splice(index, 0, object); - } - this._initObject(object); - this.renderOnAddition && this.renderAll(); - return this; - }, - - /** - * Returns an array of objects this instance has - * @method getObjects - * @return {Array} - */ - getObjects: function () { - return this._objects; - }, - - /** - * Clears specified context of canvas element - * @method clearContext - * @param context {Object} ctx context to clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clearContext: function(ctx) { - ctx.clearRect(0, 0, this.width, this.height); - return this; - }, - - /** - * Returns context of canvas where objects are drawn - * @method getContext - * @return {CanvasRenderingContext2D} - */ - getContext: function () { - return this.contextContainer; - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @method clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - this._objects.length = 0; - this.clearContext(this.contextContainer); - if (this.contextTop) { - this.clearContext(this.contextTop); - } - this.renderAll(); - return this; - }, - - /** - * Renders both the top canvas and the secondary container canvas. - * @method renderAll - * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas - * @return {fabric.Canvas} instance - * @chainable - */ - renderAll: function (allOnTop) { - - var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; - - if (this.contextTop) { - this.clearContext(this.contextTop); - } - - if (allOnTop === false || (typeof allOnTop === 'undefined')) { - this.clearContext(canvasToDrawOn); - } - - var activeGroup = this.getActiveGroup(), - startTime = new Date(); - - if (this.clipTo) { - this._clipCanvas(canvasToDrawOn); - } - - canvasToDrawOn.fillStyle = this.backgroundColor; - canvasToDrawOn.fillRect(0, 0, this.width, this.height); - - if (typeof this.backgroundImage === 'object') { - this._drawBackroundImage(canvasToDrawOn); - } - - for (var i = 0, length = this._objects.length; i < length; ++i) { - if (!activeGroup || - (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); - } - } - - if (this.clipTo) { - canvasToDrawOn.restore(); - } - - // delegate rendering to group selection (if one exists) - if (activeGroup) { - this._draw(this.contextTop, activeGroup); - } - - if (this.overlayImage) { - this.contextContainer.drawImage(this.overlayImage, 0, 0); - } - - if (this.controlsAboveOverlay) { - this.drawControls(this.contextContainer); - } - - if (this.onFpsUpdate) { - var elapsedTime = new Date() - startTime; - this.onFpsUpdate(~~(1000 / elapsedTime)); - } - - this.fire('after:render'); - - return this; - }, - - _clipCanvas: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.beginPath(); - this.clipTo(canvasToDrawOn); - canvasToDrawOn.clip(); - }, - - _drawBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; - - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); - } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); - } - canvasToDrawOn.restore(); - }, - - /** - * Method to render only the top canvas. - * Also used to render the group selection box. - * @method renderTop - * @return {fabric.Canvas} thisArg - * @chainable - */ - renderTop: function () { - this.clearContext(this.contextTop || this.contextContainer); - - if (this.overlayImage) { - this.contextContainer.drawImage(this.overlayImage, 0, 0); - } - - // we render the top context - last object - if (this.selection && this._groupSelector) { - this._drawSelection(); - } - - // delegate rendering to group selection if one exists - // used for drawing selection borders/corners - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.render(this.contextTop); - } - - this.fire('after:render'); - - return this; - }, - - /** - * Draws objects' controls (borders/corners) - * @method drawControls - * @param {Object} ctx context to render controls on - */ - drawControls: function(ctx) { - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - ctx.save(); - fabric.Group.prototype.transform.call(activeGroup, ctx); - activeGroup.drawBorders(ctx).drawCorners(ctx); - ctx.restore(); - } - else { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i].active) continue; - - ctx.save(); - fabric.Object.prototype.transform.call(this._objects[i], ctx); - this._objects[i].drawBorders(ctx).drawCorners(ctx); - ctx.restore(); - } - } - }, - - /** - * Exports canvas element to a dataurl image. - * @method toDataURL - * @param {String} format the format of the output image. Either "jpeg" or "png". - * @param {Number} quality quality level (0..1) - * @return {String} - */ - toDataURL: function (format, quality) { - var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; - - this.renderAll(true); - var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) - ? canvasEl.toDataURL('image/' + format, quality) - : canvasEl.toDataURL('image/' + format); - this.renderAll(); - return data; - }, - - /** - * Exports canvas element to a dataurl image (allowing to change image size via multiplier). - * @method toDataURLWithMultiplier - * @param {String} format (png|jpeg) - * @param {Number} multiplier - * @param {Number} quality (0..1) - * @return {String} - */ - toDataURLWithMultiplier: function (format, multiplier, quality) { - - var origWidth = this.getWidth(), - origHeight = this.getHeight(), - scaledWidth = origWidth * multiplier, - scaledHeight = origHeight * multiplier, - activeObject = this.getActiveObject(), - activeGroup = this.getActiveGroup(); - - this.setWidth(scaledWidth).setHeight(scaledHeight); - this.contextTop.scale(multiplier, multiplier); - - if (activeGroup) { - // not removing group due to complications with restoring it with correct state afterwords - this._tempRemoveBordersCornersFromGroup(activeGroup); - } - else if (activeObject) { - this.deactivateAll(); - } - - // restoring width, height for `renderAll` to draw - // background properly (while context is scaled) - this.width = origWidth; - this.height = origHeight; - - this.renderAll(true); - - var dataURL = this.toDataURL(format, quality); - - this.contextTop.scale(1 / multiplier, 1 / multiplier); - this.setWidth(origWidth).setHeight(origHeight); - - if (activeGroup) { - this._restoreBordersCornersOnGroup(activeGroup); - } - else if (activeObject) { - this.setActiveObject(activeObject); - } - - this.renderAll(); - - return dataURL; - }, - - _tempRemoveBordersCornersFromGroup: function(group) { - group.origHideCorners = group.hideCorners; - group.origBorderColor = group.borderColor; - - group.hideCorners = true; - group.borderColor = 'rgba(0,0,0,0)'; - - group.forEachObject(function(o) { - o.origBorderColor = o.borderColor; - o.borderColor = 'rgba(0,0,0,0)'; - }); - }, - _restoreBordersCornersOnGroup: function(group) { - group.hideCorners = group.origHideCorners; - group.borderColor = group.origBorderColor; - - group.forEachObject(function(o) { - o.borderColor = o.origBorderColor; - delete o.origBorderColor; - }); - }, - - /** - * Returns coordinates of a center of canvas. - * Returned value is an object with top and left properties - * @method getCenter - * @return {Object} object with "top" and "left" number values - */ - getCenter: function () { - return { - top: this.getHeight() / 2, - left: this.getWidth() / 2 - }; - }, - - /** - * Centers object horizontally. - * @method centerObjectH - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - */ - centerObjectH: function (object) { - object.set('left', this.getCenter().left); - this.renderAll(); - return this; - }, - - /** - * Centers object vertically. - * @method centerObjectH - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObjectV: function (object) { - object.set('top', this.getCenter().top); - this.renderAll(); - return this; - }, - - /** - * Centers object vertically and horizontally. - * @method centerObject - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObject: function (object) { - return this.centerObjectH(object).centerObjectV(object); - }, - - /** - * Returs dataless JSON representation of canvas - * @method toDatalessJSON - * @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) { - var data = { - objects: this._objects.map(function (instance) { - // TODO (kangax): figure out how to clean this up - var originalValue; - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - var object = instance[methodName](); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, this), - background: this.backgroundColor - }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - return data; - }, - - /** - * Returns SVG representation of canvas - * @function - * @method toSVG - * @return {String} - */ - toSVG: function() { - var markup = [ - '', - '', - '', - 'Created with Fabric.js ', fabric.version, '', - fabric.createSVGFontFacesMarkup(this.getObjects()) - ]; - - if (this.backgroundImage) { - markup.push( - '' - ); - } - - for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { - markup.push(objects[i].toSVG()); - } - markup.push(''); - - return markup.join(''); - }, - - /** - * Returns true if canvas contains no objects - * @method isEmpty - * @return {Boolean} true if canvas is empty - */ - isEmpty: function () { - return this._objects.length === 0; - }, - - /** - * 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); - if (this.getActiveObject() === object) { - - // removing active object should fire "selection:cleared" events - this.fire('before:selection:cleared', { target: object }); - this.discardActiveObject(); - this.fire('selection:cleared'); - } - this.renderAll(); - return object; - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @param object {fabric.Object} Object to send to back - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendToBack: function (object) { - removeFromArray(this._objects, object); - this._objects.unshift(object); - return this.renderAll(); - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringToFront: function (object) { - removeFromArray(this._objects, object); - this._objects.push(object); - return this.renderAll(); - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendBackwards: function (object) { - var idx = this._objects.indexOf(object), - nextIntersectingIdx = idx; - - // if object is not on the bottom of stack - if (idx !== 0) { - - // traverse down the stack looking for the nearest intersecting object - for (var i=idx-1; i>=0; --i) { - if (object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i])) { - nextIntersectingIdx = i; - break; - } - } - removeFromArray(this._objects, object); - this._objects.splice(nextIntersectingIdx, 0, object); - } - return this.renderAll(); - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringForward: function (object) { - var objects = this.getObjects(), - idx = objects.indexOf(object), - nextIntersectingIdx = idx; - - - // if object is not on top of stack (last item in an array) - if (idx !== objects.length-1) { - - // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1, l = this._objects.length; i < l; ++i) { - if (object.intersectsWithObject(objects[i]) || object.isContainedWithinObject(this._objects[i])) { - nextIntersectingIdx = i; - break; - } - } - removeFromArray(objects, object); - objects.splice(nextIntersectingIdx, 0, object); - } - this.renderAll(); - }, - - /** - * Returns object at specified index - * @method item - * @param {Number} index - * @return {fabric.Object} - */ - item: function (index) { - return this.getObjects()[index]; - }, - - /** - * 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); - }, - - /** - * Iterates over all objects, invoking callback for each one of them - * @method forEachObject - * @return {fabric.Canvas} thisArg - */ - forEachObject: function(callback, context) { - var objects = this.getObjects(), - i = objects.length; - while (i--) { - callback.call(context, objects[i], i, objects); - } - return this; - }, - - /** - * Clears a canvas element and removes all event handlers. - * @method dispose - * @return {fabric.Canvas} thisArg - * @chainable - */ - dispose: function () { - this.clear(); - if (this.interactive) { - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(fabric.window, 'resize', this._onResize); - } - return this; - }, - - /** - * @private - * @method _resizeImageToFit - * @param {HTMLImageElement} imgEl - */ - _resizeImageToFit: function (imgEl) { - - var imageWidth = imgEl.width || imgEl.offsetWidth, - widthScaleFactor = this.getWidth() / imageWidth; - - // scale image down so that it has original dimensions when printed in large resolution - if (imageWidth) { - imgEl.width = imageWidth * widthScaleFactor; - } - } - }); - - /** - * Returns a string representation of an instance - * @method toString - * @return {String} string representation of an instance - */ - fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet - return '#'; - }; - - extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ { - - /** - * @static - * @property EMPTY_JSON - * @type String - */ - EMPTY_JSON: '{"objects": [], "background": "white"}', - - /** - * Takes element and transforms its data in such way that it becomes grayscale - * @static - * @method toGrayscale - * @param {HTMLCanvasElement} canvasEl - */ - toGrayscale: function (canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = imageData.width, - jLen = imageData.height, - index, average, i, j; - - for (i = 0; i < iLen; i++) { - for (j = 0; j < jLen; j++) { - - index = (i * 4) * jLen + (j * 4); - average = (data[index] + data[index + 1] + data[index + 2]) / 3; - - data[index] = average; - data[index + 1] = average; - data[index + 2] = average; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Provides a way to check support of some of the canvas methods - * (either those of HTMLCanvasElement itself, or rendering context) - * - * @method supports - * @param methodName {String} Method to check support for; - * Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality" - * @return {Boolean | null} `true` if method is supported (or at least exists), - * `null` if canvas element or context can not be initialized - */ - supports: function (methodName) { - var el = fabric.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'; - - case 'toDataURLWithQuality': - try { - el.toDataURL('image/jpeg', 0); - return true; - } - catch (e) { } - return false; - - default: - return null; - } - } - }); - - /** - * Returs JSON representation of canvas - * @function - * @method toJSON - * @return {String} json string - */ - fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; - -})(); -(function() { - - var extend = fabric.util.object.extend, - getPointer = fabric.util.getPointer, - addListener = fabric.util.addListener, - removeListener = fabric.util.removeListener, - 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' - }, - - 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, - - STROKE_OFFSET = 0.5; - - /** - * @class fabric.Canvas - * @constructor - * @extends fabric.StaticCanvas - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - fabric.Canvas = function(el, options) { - options || (options = { }); - - this._initStatic(el, options); - this._initInteractive(); - this._createCacheCanvas(); - - fabric.Canvas.activeInstance = this; - }; - - function ProtoProxy(){ } - ProtoProxy.prototype = fabric.StaticCanvas.prototype; - fabric.Canvas.prototype = new ProtoProxy(); - - var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { - - /** - * Indicates that canvas is interactive. This property should not be changed. - * @property - * @type Boolean - */ - interactive: true, - - /** - * Indicates whether group selection should be enabled - * @property - * @type Boolean - */ - selection: true, - - /** - * Color of selection - * @property - * @type String - */ - selectionColor: 'rgba(100, 100, 255, 0.3)', // blue - - /** - * Color of the border of selection (usually slightly darker than color of selection itself) - * @property - * @type String - */ - selectionBorderColor: 'rgba(255, 255, 255, 0.3)', - - /** - * Width of a line used in object/group selection - * @property - * @type Number - */ - selectionLineWidth: 1, - - /** - * Color of the line used in free drawing mode - * @property - * @type String - */ - freeDrawingColor: 'rgb(0, 0, 0)', - - /** - * Width of a line used in free drawing mode - * @property - * @type Number - */ - freeDrawingLineWidth: 1, - - /** - * Default cursor value used when hovering over an object on canvas - * @property - * @type String - */ - hoverCursor: 'move', - - /** - * Default cursor value used when moving an object on canvas - * @property - * @type String - */ - moveCursor: 'move', - - /** - * Default cursor value used for the entire canvas - * @property - * @type String - */ - defaultCursor: 'default', - - /** - * Cursor value used for rotation point - * @property - * @type String - */ - rotationCursor: 'crosshair', - - /** - * Default element class that's given to wrapper (div) element of canvas - * @property - * @type String - */ - containerClass: 'canvas-container', - - perPixelTargetFind: false, - - targetFindTolerance: 0, - - _initInteractive: function() { - this._currentTransform = null; - this._groupSelector = null; - this._freeDrawingXPoints = [ ]; - this._freeDrawingYPoints = [ ]; - this._initWrapperElement(); - this._createUpperCanvas(); - this._initEvents(); - this.calcOffset(); - }, - - /** - * Adds mouse listeners to canvas - * @method _initEvents - * @private - * See configuration documentation for more details. - */ - _initEvents: function () { - var _this = this; - - this._onMouseDown = function (e) { - _this.__onMouseDown(e); - - addListener(fabric.document, 'mouseup', _this._onMouseUp); - fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); - - addListener(fabric.document, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); - - removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); - }; - - this._onMouseUp = function (e) { - _this.__onMouseUp(e); - - removeListener(fabric.document, 'mouseup', _this._onMouseUp); - fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); - - removeListener(fabric.document, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); - - addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); - }; - - this._onMouseMove = function (e) { - e.preventDefault && e.preventDefault(); - _this.__onMouseMove(e); - }; - - this._onResize = function () { - _this.calcOffset(); - }; - - - addListener(fabric.window, 'resize', this._onResize); - - if (fabric.isTouchSupported) { - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - } - else { - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - } - }, - - /** - * 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) { - - var target; - - if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._finalizeDrawingPath(); - this.fire('mouse:up', { e: e }); - return; - } - - if (this._currentTransform) { - - var transform = this._currentTransform; - - target = transform.target; - if (target._scaling) { - target._scaling = false; - } - - // determine the new coords everytime the image changes its position - var i = this._objects.length; - while (i--) { - this._objects[i].setCoords(); - } - - // only fire :modified event if target coordinates were changed during mousedown-mouseup - if (this.stateful && target.hasStateChanged()) { - target.isMoving = false; - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - } - - this._currentTransform = null; - - if (this._groupSelector) { - // group selection was completed, determine its bounds - this._findSelectedObjects(e); - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords(); - activeGroup.set('isMoving', false); - this._setCursor(this.defaultCursor); - } - - // clear selection - this._groupSelector = null; - this.renderAll(); - - this._setCursorFromEvent(e, target); - - // fix for FF - this._setCursor(''); - - var _this = this; - setTimeout(function () { - _this._setCursorFromEvent(e, target); - }, 50); - - this.fire('mouse:up', { target: target, e: e }); - target && target.fire('mouseup', { e: e }); - }, - - /** - * 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) { - - // accept only left clicks - var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; - if (!isLeftClick && !fabric.isTouchSupported) return; - - if (this.isDrawingMode) { - this._prepareForDrawing(e); - - // capture coordinates immediately; this allows to draw dots (when movement never occurs) - this._captureDrawingPath(e); - this.fire('mouse:down', { e: e }); - return; - } - - // ignore if some object is being transformed at this moment - if (this._currentTransform) return; - - var target = this.findTarget(e), - pointer = this.getPointer(e), - activeGroup = this.getActiveGroup(), - corner; - - if (this._shouldClearSelection(e)) { - - this._groupSelector = { - ex: pointer.x, - ey: pointer.y, - top: 0, - left: 0 - }; - - this.deactivateAllWithDispatch(); - } - else { - // determine if it's a drag or rotate case - // rotate and scale will happen at the same time - this.stateful && target.saveState(); - - if ((corner = target._findTargetCorner(e, this._offset))) { - this.onBeforeScaleRotate(target); - } - - this._setupCurrentTransform(e, target); - - var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()) && this.selection; - if (shouldHandleGroupLogic) { - this._handleGroupLogic(e, target); - } - else { - if (target !== this.getActiveGroup()) { - this.deactivateAll(); - } - this.setActiveObject(target, e); - } - } - // we must renderAll so that active image is placed on the top canvas - this.renderAll(); - - this.fire('mouse:down', { target: target, e: e }); - target && target.fire('mousedown', { e: e }); - }, - - /** - * 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) { - - var target; - - if (this.isDrawingMode) { - if (this._isCurrentlyDrawing) { - this._captureDrawingPath(e); - } - this.fire('mouse:move', { e: e }); - return; - } - - var groupSelector = this._groupSelector, pointer; - - // We initially clicked in an empty area, so we draw a box for multiple selection. - if (groupSelector !== null) { - pointer = getPointer(e); - - groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; - groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; - this.renderTop(); - } - else if (!this._currentTransform) { - - // alias style to elimintate unnecessary lookup - var style = this.upperCanvasEl.style; - - // Here we are hovering the canvas then we will determine - // what part of the pictures we are hovering to change the caret symbol. - // We won't do that while dragging or rotating in order to improve the - // performance. - target = this.findTarget(e); - - if (!target) { - // image/text was hovered-out from, we remove its borders - for (var i = this._objects.length; i--; ) { - if (this._objects[i] && !this._objects[i].active) { - this._objects[i].setActive(false); - } - } - style.cursor = this.defaultCursor; - } - else { - // set proper cursor - this._setCursorFromEvent(e, target); - if (target.isActive()) { - // display corners when hovering over an image - target.setCornersVisibility && target.setCornersVisibility(true); - } - } - } - else { - // object is being transformed (scaled/rotated/moved/etc.) - pointer = getPointer(e); - - var x = pointer.x, - y = pointer.y; - - this._currentTransform.target.isMoving = true; - - if (this._currentTransform.action === 'rotate') { - // rotate object only if shift key is not pressed - // and if it is not a group we are transforming - - if (!e.shiftKey) { - this._rotateObject(x, y); - - this.fire('object:rotating', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('rotating'); - } - if (!this._currentTransform.target.hasRotatingPoint) { - this._scaleObject(x, y); - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - } - else if (this._currentTransform.action === 'scale') { - this._scaleObject(x, y); - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else if (this._currentTransform.action === 'scaleX') { - this._scaleObject(x, y, 'x'); - - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else if (this._currentTransform.action === 'scaleY') { - this._scaleObject(x, y, 'y'); - - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else { - this._translateObject(x, y); - - this.fire('object:moving', { - target: this._currentTransform.target - }); - - this._setCursor(this.moveCursor); - - this._currentTransform.target.fire('moving'); - } - // only commit here. when we are actually moving the pictures - this.renderAll(); - } - this.fire('mouse:move', { target: target, e: e }); - target && target.fire('mousemove', { e: e }); - }, - - /** - * Applies one implementation of 'point inside polygon' algorithm - * @method containsPoint - * @param e { Event } event object - * @param target { fabric.Object } object to test against - * @return {Boolean} true if point contains within area of given object - */ - containsPoint: function (e, target) { - var pointer = this.getPointer(e), - xy = this._normalizePointer(target, pointer), - x = xy.x, - y = xy.y; - - // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html - // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html - - // we iterate through each object. If target found, return it. - var iLines = target._getImageLines(target.oCoords), - xpoints = target._findCrossPoints(x, y, iLines); - - // if xcount is odd then we clicked inside the object - // For the specific case of square images xcount === 1 in all true cases - if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { - return true; - } - return false; - }, - - /** - * @private - * @method _normalizePointer - */ - _normalizePointer: function (object, pointer) { - - var activeGroup = this.getActiveGroup(), - x = pointer.x, - y = pointer.y; - - var isObjectInGroup = ( - activeGroup && - object.type !== 'group' && - activeGroup.contains(object) - ); - - if (isObjectInGroup) { - x -= activeGroup.left; - y -= activeGroup.top; - } - return { x: x, y: y }; - }, - - _isTargetTransparent: function (target, x, y) { - var cacheContext = this.contextCache; - - var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; - target.hasBorders = target.transparentCorners = false; - - this._draw(cacheContext, target); - - target.hasBorders = hasBorders; - target.transparentCorners = transparentCorners; - - // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 - if (this.targetFindTolerance > 0) { - if (x > this.targetFindTolerance) { - x -= this.targetFindTolerance; - } - else { - x = 0; - } - if (y > this.targetFindTolerance) { - y -= this.targetFindTolerance; - } - else { - y = 0; - } - } - - var isTransparent = true; - var imageData = cacheContext.getImageData( - x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (var i = 3; i < imageData.data.length; i += 4) { - var temp = imageData.data[i]; - isTransparent = temp <= 0; - if (isTransparent === false) break; //Stop if colour found - } - - imageData = null; - this.clearContext(cacheContext); - return isTransparent; - }, - - /** - * @private - * @method _shouldClearSelection - */ - _shouldClearSelection: function (e) { - var target = this.findTarget(e), - activeGroup = this.getActiveGroup(); - return ( - !target || ( - target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && - !e.shiftKey - ) - ); - }, - - /** - * @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' - : corner === 'mtr' - ? 'rotate' - : (target.hasRotatingPoint) - ? 'scale' - : '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 === this.getActiveGroup()) { - // if it's a group, find target again, this time skipping group - target = this.findTarget(e, true); - // if even object is not found, bail out - if (!target || target.isType('group')) { - return; - } - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - if (activeGroup.contains(target)) { - activeGroup.removeWithUpdate(target); - target.setActive(false); - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(); - } - } - else { - activeGroup.addWithUpdate(target); - } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.setActive(true); - } - else { - // group does not exist - if (this._activeObject) { - // only if there's an active object - if (target !== this._activeObject) { - // and that object is not the actual target - var group = new fabric.Group([ this._activeObject, target ]); - this.setActiveGroup(group); - activeGroup = this.getActiveGroup(); - } - } - // activate target object in any case - target.setActive(true); - } - - if (activeGroup) { - activeGroup.saveCoords(); - } - }, - - /** - * @private - * @method _prepareForDrawing - */ - _prepareForDrawing: function(e) { - - this._isCurrentlyDrawing = true; - - this.discardActiveObject().renderAll(); - - var pointer = this.getPointer(e); - - this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; - - this._freeDrawingXPoints.push(pointer.x); - this._freeDrawingYPoints.push(pointer.y); - - this.contextTop.beginPath(); - this.contextTop.moveTo(pointer.x, pointer.y); - this.contextTop.strokeStyle = this.freeDrawingColor; - this.contextTop.lineWidth = this.freeDrawingLineWidth; - this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; - }, - - /** - * @private - * @method _captureDrawingPath - */ - _captureDrawingPath: function(e) { - var pointer = this.getPointer(e); - - this._freeDrawingXPoints.push(pointer.x); - this._freeDrawingYPoints.push(pointer.y); - - this.contextTop.lineTo(pointer.x, pointer.y); - this.contextTop.stroke(); - }, - - /** - * @private - * @method _finalizeDrawingPath - */ - _finalizeDrawingPath: function() { - - this.contextTop.closePath(); - - this._isCurrentlyDrawing = false; - - var minX = utilMin(this._freeDrawingXPoints), - minY = utilMin(this._freeDrawingYPoints), - maxX = utilMax(this._freeDrawingXPoints), - maxY = utilMax(this._freeDrawingYPoints), - path = [ ], - xPoints = this._freeDrawingXPoints, - yPoints = this._freeDrawingYPoints; - - path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); - - for (var i = 1, len = xPoints.length; i < len; i++) { - path.push('L ', xPoints[i] - minX, ' ', yPoints[i] - minY, ' '); - } - - // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, - // and instead fire something like "drawing:completed" event with path string - - path = path.join(''); - - if (path === "M 0 0 L 0 0 ") { - // do not create 0 width/height paths, as they are rendered inconsistently across browsers - // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing - this.renderAll(); - return; - } - - var p = new fabric.Path(path); - - 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(); - this.fire('path:created', { path: p }); - }, - - /** - * 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.lockMovementX || target.set('left', x - this._currentTransform.offsetX); - target.lockMovementY || 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.lockScalingX && target.lockScalingY) 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.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); - target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); - } - else if (by === 'x' && !target.lockUniScaling) { - target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); - } - else if (by === 'y' && !target.lockUniScaling) { - target.lockScalingY || 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._theta = (curAngle - lastAngle) + t.theta; - }, - - /** - * @method _setCursor - */ - _setCursor: function (value) { - this.upperCanvasEl.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.upperCanvasEl.style; - if (!target) { - s.cursor = this.defaultCursor; - return false; - } - else { - var activeGroup = this.getActiveGroup(); - // only show proper corner when group selection is not active - var corner = !!target._findTargetCorner - && (!activeGroup || !activeGroup.contains(target)) - && target._findTargetCorner(e, this._offset); - - if (!corner) { - s.cursor = this.hoverCursor; - } - else { - if (corner in cursorMap) { - s.cursor = cursorMap[corner]; - } else if (corner === 'mtr' && target.hasRotatingPoint) { - s.cursor = this.rotationCursor; - } else { - s.cursor = this.defaultCursor; - return false; - } - } - } - return true; - }, - - /** - * @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 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) continue; - - if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { - - if (this.selection && currentObject.selectable) { - currentObject.setActive(true); - group.push(currentObject); - } - } - } - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } - else if (group.length > 1) { - group = new fabric.Group(group); - this.setActiveGroup(group); - group.saveCoords(); - this.fire('selection:created', { target: group }); - } - - this.renderAll(); - }, - - /** - * Method that determines what object we are clicking on - * @method findTarget - * @param {Event} e mouse event - * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through - */ - findTarget: function (e, skipGroup) { - - var target, - pointer = this.getPointer(e); - - // first check current group (if one exists) - var activeGroup = this.getActiveGroup(); - - if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { - target = activeGroup; - return target; - } - - // then check all of the objects on canvas - // Cache all targets where their bounding box contains point. - var possibleTargets = []; - for (var i = this._objects.length; i--; ) { - if (this._objects[i] && this.containsPoint(e, this._objects[i])) { - if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { - possibleTargets[possibleTargets.length] = this._objects[i]; - } - else { - target = this._objects[i]; - this.relatedTarget = target; - break; - } - } - } - for (var j = 0, len = possibleTargets.length; j < len; j++) { - pointer = this.getPointer(e); - var isTransparent = this._isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); - if (!isTransparent) { - target = possibleTargets[j]; - this.relatedTarget = target; - break; - } - } - if (target && target.selectable) { - return target; - } - }, - - /** - * Returns pointer coordinates relative to canvas. - * @method getPointer - * @return {Object} object with "x" and "y" number values - */ - getPointer: function (e) { - var pointer = getPointer(e); - return { - x: pointer.x - this._offset.left, - y: pointer.y - this._offset.top - }; - }, - - /** - * @method _createUpperCanvas - * @param {HTMLElement|String} canvasEl Canvas element - * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized - */ - _createUpperCanvas: function () { - this.upperCanvasEl = this._createCanvasElement(); - this.upperCanvasEl.className = 'upper-canvas'; - - this.wrapperEl.appendChild(this.upperCanvasEl); - - this._applyCanvasStyle(this.upperCanvasEl); - this.contextTop = this.upperCanvasEl.getContext('2d'); - }, - - _createCacheCanvas: function () { - this.cacheCanvasEl = this._createCanvasElement(); - this.cacheCanvasEl.setAttribute('width', this.width); - this.cacheCanvasEl.setAttribute('height', this.height); - this.contextCache = this.cacheCanvasEl.getContext('2d'); - }, - - /** - * @private - * @method _initWrapperElement - * @param {Number} width - * @param {Number} height - */ - _initWrapperElement: function () { - this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { - 'class': this.containerClass - }); - fabric.util.setStyle(this.wrapperEl, { - width: this.getWidth() + 'px', - height: this.getHeight() + 'px', - position: 'relative' - }); - fabric.util.makeElementUnselectable(this.wrapperEl); - }, - - /** - * @private - * @method _applyCanvasStyle - * @param {Element} element - */ - _applyCanvasStyle: function (element) { - var width = this.getWidth() || element.width, - height = this.getHeight() || element.height; - - fabric.util.setStyle(element, { - position: 'absolute', - width: width + 'px', - height: height + 'px', - left: 0, - top: 0 - }); - element.width = width; - element.height = height; - fabric.util.makeElementUnselectable(element); - }, - - /** - * Returns context of canvas where object selection is drawn - * @method getSelectionContext - * @return {CanvasRenderingContext2D} - */ - getSelectionContext: function() { - return this.contextTop; - }, - - /** - * Returns <canvas> element on which object selection is drawn - * @method getSelectionElement - * @return {HTMLCanvasElement} - */ - getSelectionElement: function () { - return this.upperCanvasEl; - }, - - /** - * Sets given object as active - * @method setActiveObject - * @param object {fabric.Object} Object to set as an active one - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveObject: function (object, e) { - if (this._activeObject) { - this._activeObject.setActive(false); - } - this._activeObject = object; - object.setActive(true); - - this.renderAll(); - - this.fire('object:selected', { target: object, e: e }); - object.fire('selected', { e: e }); - return this; - }, - - /** - * Returns currently active object - * @method getActiveObject - * @return {fabric.Object} active object - */ - getActiveObject: function () { - return this._activeObject; - }, - - /** - * Discards currently active object - * @method discardActiveObject - * @return {fabric.Canvas} thisArg - * @chainable - */ - discardActiveObject: function () { - if (this._activeObject) { - this._activeObject.setActive(false); - } - this._activeObject = null; - return this; - }, - - /** - * Sets active group to a speicified one - * @method setActiveGroup - * @param {fabric.Group} group Group to set as a current one - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveGroup: function (group) { - this._activeGroup = group; - group && group.setActive(true); - return this; - }, - - /** - * Returns currently active group - * @method getActiveGroup - * @return {fabric.Group} Current group - */ - getActiveGroup: function () { - return this._activeGroup; - }, - - /** - * Removes currently active group - * @method discardActiveGroup - * @return {fabric.Canvas} thisArg - */ - discardActiveGroup: function () { - var g = this.getActiveGroup(); - if (g) { - g.destroy(); - } - return this.setActiveGroup(null); - }, - - /** - * Deactivates all objects by calling their setActive(false) - * @method deactivateAll - * @return {fabric.Canvas} thisArg - */ - deactivateAll: function () { - var allObjects = this.getObjects(), - i = 0, - len = allObjects.length; - for ( ; i < len; i++) { - allObjects[i].setActive(false); - } - this.discardActiveGroup(); - this.discardActiveObject(); - return this; - }, - - /** - * Deactivates all objects and dispatches appropriate events - * @method deactivateAllWithDispatch - * @return {fabric.Canvas} thisArg - */ - deactivateAllWithDispatch: function () { - var activeObject = this.getActiveGroup() || this.getActiveObject(); - if (activeObject) { - this.fire('before:selection:cleared', { target: activeObject }); - } - this.deactivateAll(); - if (activeObject) { - this.fire('selection:cleared'); - } - return this; - } - }; - - fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; - extend(fabric.Canvas.prototype, InteractiveMethods); - - // iterating manually to workaround Opera's bug - // where "prototype" property is enumerable and overrides existing prototype - for (var prop in fabric.StaticCanvas) { - if (prop !== 'prototype') { - fabric.Canvas[prop] = fabric.StaticCanvas[prop]; - } - } - - if (fabric.isTouchSupported) { - fabric.Canvas.prototype._setCursorFromEvent = function() { }; - } - - /** - * @class fabric.Element - * @alias fabric.Canvas - * @deprecated - * @constructor - */ - fabric.Element = fabric.Canvas; -})(); -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - FX_DURATION: 500, - - /** - * Centers object horizontally with animation. - * @method fxCenterObjectH - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @return {fabric.Canvas} 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 with animation. - * @method fxCenterObjectV - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @return {fabric.Canvas} 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; - }, - - /** - * Same as `fabric.Canvas#remove` but animated - * @method fxRemove - * @param {fabric.Object} object Object to remove - * @param {Function} callback Callback, invoked on effect completion - * @return {fabric.Canvas} thisArg - * @chainable - */ - fxRemove: function (object, callbacks) { - callbacks = callbacks || { }; - - var empty = function() { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - fabric.util.animate({ - startValue: object.get('opacity'), - endValue: 0, - duration: this.FX_DURATION, - onStart: function() { - object.setActive(false); - }, - onChange: function(value) { - object.set('opacity', value); - _this.renderAll(); - onChange(); - }, - onComplete: function () { - _this.remove(object); - onComplete(); - } - }); - - return this; - } -}); -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - /** - * Populates canvas with data from the specified dataless JSON - * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` - * @method loadFromDatalessJSON - * @param {String} json JSON string - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: fabric.Image) - * are initialized - * @return {fabric.Canvas} instance - * @chainable - */ - loadFromDatalessJSON: function (json, callback) { - - if (!json) { - return; - } - - // serialize if it wasn't already - var serialized = (typeof json === 'string') - ? JSON.parse(json) - : json; - - if (!serialized || (serialized && !serialized.objects)) return; - - this.clear(); - - // TODO: test this - this.backgroundColor = serialized.background; - this._enlivenDatalessObjects(serialized.objects, callback); - }, - - /** - * @method _enlivenDatalessObjects - * @param {Array} objects - * @param {Function} callback - */ - _enlivenDatalessObjects: function (objects, callback) { - - /** @ignore */ - function onObjectLoaded(object, index) { - _this.insertAt(object, index, true); - object.setCoords(); - if (++numLoadedObjects === numTotalObjects) { - callback && callback(); - } - } - - /** @ignore */ - function loadObject(obj, index) { - - var pathProp = obj.paths ? 'paths' : 'path'; - var path = obj[pathProp]; - - delete obj[pathProp]; - - if (typeof path !== 'string') { - if (obj.type === 'image') { - fabric[fabric.util.string.capitalize(obj.type)].fromObject(obj, function (o) { - onObjectLoaded(o, index); - }); - } - else { - var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(obj.type))]; - if (!klass || !klass.fromObject) return; - - // restore path - if (path) { - obj[pathProp] = path; - } - onObjectLoaded(klass.fromObject(obj), index); - } - } - else { - if (obj.type === 'image') { - fabric.util.loadImage(path, function (image) { - var oImg = new fabric.Image(image); - - oImg.setSourcePath(path); - - fabric.util.object.extend(oImg, obj); - oImg.setAngle(obj.angle); - - onObjectLoaded(oImg, index); - }); - } - else if (obj.type === 'text') { - - if (obj.useNative) { - onObjectLoaded(fabric.Text.fromObject(obj), index); - } - else { - obj.path = path; - var object = fabric.Text.fromObject(obj); - var onscriptload = function () { - // TODO (kangax): find out why Opera refuses to work without this timeout - if (Object.prototype.toString.call(fabric.window.opera) === '[object Opera]') { - setTimeout(function () { - onObjectLoaded(object, index); - }, 500); - } - else { - onObjectLoaded(object, index); - } - }; - - fabric.util.getScript(path, onscriptload); - } - } - else { - fabric.loadSVGFromURL(path, function (elements) { - var object = fabric.util.groupSVGElements(elements, obj, path); - - // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) - // skip this step if an object is a PathGroup, since we already passed it options object before - if (!(object instanceof fabric.PathGroup)) { - fabric.util.object.extend(object, obj); - if (typeof obj.angle !== 'undefined') { - object.setAngle(obj.angle); - } - } - - onObjectLoaded(object, index); - }); - } - } - } - - var _this = this, - numLoadedObjects = 0, - numTotalObjects = objects.length; - - if (numTotalObjects === 0 && callback) { - callback(); - } - - try { - objects.forEach(loadObject, this); - } - catch(e) { - fabric.log(e.message); - } - }, - - /** - * Populates canvas with data from the specified JSON - * JSON format must conform to the one of `fabric.Canvas#toJSON` - * @method loadFromJSON - * @param {String} json JSON string - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: fabric.Image) - * are initialized - * @return {fabric.Canvas} 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 (serialized.backgroundImage) { - _this.setBackgroundImage(serialized.backgroundImage, function() { - - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; - - _this.renderAll(); - - callback && callback(); - }); - } - else { - callback && callback(); - } - }); - - return this; - }, - - /** - * @method _enlivenObjects - * @param {Array} objects - * @param {Function} callback - */ - _enlivenObjects: function (objects, callback) { - var _this = this; - fabric.util.enlivenObjects(objects, function(enlivenedObjects) { - enlivenedObjects.forEach(function(obj, index) { - _this.insertAt(obj, index, true); - }); - callback && callback(); - }); - }, - - /** - * @private - * @method _toDataURL - * @param {String} format - * @param {Function} callback - */ - _toDataURL: function (format, callback) { - this.clone(function (clone) { - callback(clone.toDataURL(format)); - }); - }, - - /** - * @private - * @method _toDataURLWithMultiplier - * @param {String} format - * @param {Number} multiplier - * @param {Function} callback - */ - _toDataURLWithMultiplier: function (format, multiplier, callback) { - this.clone(function (clone) { - callback(clone.toDataURLWithMultiplier(format, multiplier)); - }); - }, - - /** - * Clones canvas instance - * @method clone - * @param {Object} [callback] Receives cloned instance as a first argument - */ - clone: function (callback) { - var data = JSON.stringify(this); - this.cloneWithoutData(function(clone) { - clone.loadFromJSON(data, function() { - callback && callback(clone); - }); - }); - }, - - /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @method cloneWithoutData - * @param {Object} [callback] Receives cloned instance as a first argument - */ - cloneWithoutData: function(callback) { - var el = fabric.document.createElement('canvas'); - - el.width = this.getWidth(); - el.height = this.getHeight(); - - var clone = new fabric.Canvas(el); - clone.clipTo = this.clipTo; - if (this.backgroundImage) { - clone.setBackgroundImage(this.backgroundImage.src, function() { - clone.renderAll(); - callback && callback(clone); - }); - clone.backgroundImageOpacity = this.backgroundImageOpacity; - clone.backgroundImageStretch = this.backgroundImageStretch; - } - else { - callback && callback(clone); - } - } -}); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - toFixed = fabric.util.toFixed, - capitalize = fabric.util.string.capitalize, - getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians, - slice = Array.prototype.slice; - - if (fabric.Object) { - return; - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { - - /** - * Type of an object (rect, circle, path, etc) - * @property - * @type String - */ - type: 'object', - - /** - * @property - * @type Number - */ - top: 0, - - /** - * @property - * @type Number - */ - left: 0, - - /** - * @property - * @type Number - */ - width: 0, - - /** - * @property - * @type Number - */ - height: 0, - - /** - * @property - * @type Number - */ - scaleX: 1, - - /** - * @property - * @type Number - */ - scaleY: 1, - - /** - * @property - * @type Boolean - */ - flipX: false, - - /** - * @property - * @type Boolean - */ - flipY: false, - - /** - * @property - * @type Number - */ - opacity: 1, - - /** - * @property - * @type Number - */ - angle: 0, - - /** - * @property - * @type Number - */ - cornersize: 12, - - /** - * @property - * @type Boolean - */ - transparentCorners: true, - - /** - * @property - * @type Number - */ - padding: 0, - - /** - * @property - * @type String - */ - borderColor: 'rgba(102,153,255,0.75)', - - /** - * @property - * @type String - */ - cornerColor: 'rgba(102,153,255,0.5)', - - /** - * @property - * @type String - */ - fill: 'rgb(0,0,0)', - - /** - * @property - * @type String - */ - fillRule: 'source-over', - - /** - * @property - * @type String - */ - overlayFill: null, - - /** - * @property - * @type String - */ - stroke: null, - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type Array - */ - strokeDashArray: null, - - /** - * @property - * @type Number - */ - borderOpacityWhenMoving: 0.4, - - /** - * @property - * @type Number - */ - borderScaleFactor: 1, - - /** - * Transform matrix - * @property - * @type Array - */ - transformMatrix: null, - - /** - * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) - * @property - * @type Boolean - */ - selectable: true, - - /** - * When set to `false`, object's controls are not displayed and can not be used to manipulate object - * @property - * @type Boolean - */ - hasControls: true, - - /** - * When set to `false`, object's borders are not rendered - * @property - * @type Boolean - */ - hasBorders: true, - - /** - * When set to `false`, object's rotating point will not be visible or selectable - * @property - * @type Boolean - */ - hasRotatingPoint: false, - - /** - * Offset for object's rotating point (when enabled) - * @property - * @type Number - */ - rotatingPointOffset: 40, - - /** - * @private - * @property - * @type Number - */ - _theta: 0, - - perPixelTargetFind: false, - - includeDefaultValues: true, - - /** - * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); - * as well as for history (undo/redo) purposes - * @property - * @type Array - */ - stateProperties: ( - 'top left width height scaleX scaleY flipX flipY ' + - 'theta angle opacity cornersize fill overlayFill ' + - 'stroke strokeWidth strokeDashArray fillRule ' + - 'borderScaleFactor transformMatrix selectable' - ).split(' '), - - /** - * @method callSuper - * @param {String} methodName - */ - callSuper: function(methodName) { - var fn = this.constructor.superclass.prototype[methodName]; - return (arguments.length > 1) - ? fn.apply(this, slice.call(arguments, 1)) - : fn.call(this); - }, - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - */ - initialize: function(options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * @method initGradient - */ - _initGradient: function(options) { - if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { - this.set('fill', new fabric.Gradient(options.fill)); - } - }, - - /** - * @method setOptions - * @param {Object} [options] - */ - setOptions: function(options) { - var i = this.stateProperties.length, prop; - while (i--) { - prop = this.stateProperties[i]; - if (prop in options) { - this.set(prop, options[prop]); - } - } - this._initGradient(options); - }, - - /** - * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var object = { - type: this.type, - left: toFixed(this.left, NUM_FRACTION_DIGITS), - top: toFixed(this.top, NUM_FRACTION_DIGITS), - width: toFixed(this.width, NUM_FRACTION_DIGITS), - height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, - overlayFill: this.overlayFill, - stroke: this.stroke, - strokeWidth: this.strokeWidth, - strokeDashArray: this.strokeDashArray, - scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), - scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), - flipX: this.flipX, - flipY: this.flipY, - opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), - selectable: this.selectable, - hasControls: this.hasControls, - hasBorders: this.hasBorders, - hasRotatingPoint: this.hasRotatingPoint, - transparentCorners: this.transparentCorners, - perPixelTargetFind: this.perPixelTargetFind - }; - - if (!this.includeDefaultValues) { - object = this._removeDefaultValues(object); - } - - return object; - }, - - /** - * Returns (dataless) object representation of an instance - * @method toDatalessObject - */ - toDatalessObject: function() { - // will be overwritten by subclasses - return this.toObject(); - }, - - /** - * Returns styles-string for svg-export - * @method getSvgStyles - * @return {string} - */ - getSvgStyles: function() { - return [ - "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", - "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", - "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), - "fill: ", (this.fill ? this.fill : 'none'), "; ", - "opacity: ", (this.opacity ? this.opacity : '1'), ";" - ].join(""); - }, - - /** - * Returns transform-string for svg-export - * @method getSvgTransform - * @return {string} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - return [ - "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", - angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', - (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") - ].join(''); - }, - - /** - * @private - * @method _removeDefaultValues - */ - _removeDefaultValues: function(object) { - var defaultOptions = fabric.Object.prototype.options; - if (defaultOptions) { - 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 "#"; - }, - - /** - * Sets property to a given value - * @method set - * @param {String} name - * @param {Object|Function} value - * @return {fabric.Group} thisArg - * @chainable - */ - set: function(key, value) { - if (typeof key === 'object') { - for (var prop in key) { - this._set(prop, key[prop]); - } - } - else { - if (typeof value === 'function') { - this._set(key, value(this.get(key))); - } - else { - this._set(key, value); - } - } - return this; - }, - - _set: function(key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && - value < fabric.Object.MIN_SCALE_LIMIT; - - if (shouldConstrainValue) { - value = fabric.Object.MIN_SCALE_LIMIT; - } - if (key === 'angle') { - this.setAngle(value); - } - else { - this[key] = value; - } - }, - - /** - * 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) { - - // do not render if width or height are zeros - if (this.width === 0 || this.height === 0) return; - - ctx.save(); - - var m = this.transformMatrix; - if (m && !this.group) { - ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - if (!noTransform) { - this.transform(ctx); - } - - if (this.stroke || this.strokeDashArray) { - ctx.lineWidth = this.strokeWidth; - ctx.strokeStyle = this.stroke; - } - - if (this.overlayFill) { - ctx.fillStyle = this.overlayFill; - } - else if (this.fill) { - ctx.fillStyle = this.fill.toLiveGradient - ? this.fill.toLiveGradient(ctx) - : this.fill; - } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._render(ctx, noTransform); - - if (this.active && !noTransform) { - this.drawBorders(ctx); - 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; - this.setCoords(); - return this; - }, - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @method scaleToWidth - * @param value {Number} new width value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @method scaleToHeight - * @param value {Number} new height value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, - - /** - * 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() { - - var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, - padding = this.padding; - - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; - this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; - - this._hypotenuse = Math.sqrt( - Math.pow(this.currentWidth / 2, 2) + - Math.pow(this.currentHeight / 2, 2)); - - this._angle = Math.atan(this.currentHeight / this.currentWidth); - - // offset added for rotate and scale actions - 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) - }; - var mtr = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }; - - // debugging - - // setTimeout(function() { - // canvas.contextTop.fillStyle = 'green'; - // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); - // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); - // canvas.contextTop.fillRect(br.x, br.y, 3, 3); - // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); - // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); - // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); - // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); - // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); - // }, 50); - - // clockwise - this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; - - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this._setCornerCoords(); - - return this; - }, - - /** - * Returns width of an object's bounding rectangle - * @method getBoundingRectWidth - * @return {Number} width value - */ - getBoundingRectWidth: function() { - this.oCoords || this.setCoords(); - var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; - var minX = fabric.util.array.min(xCoords); - var maxX = fabric.util.array.max(xCoords); - return Math.abs(minX - maxX); - }, - - /** - * Returns height of an object's bounding rectangle - * @method getBoundingRectHeight - * @return {Number} height value - */ - getBoundingRectHeight: function() { - this.oCoords || this.setCoords(); - var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; - var minY = fabric.util.array.min(yCoords); - var maxY = fabric.util.array.max(yCoords); - return Math.abs(minY - maxY); - }, - - /** - * 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) { - if (!this.hasBorders) return; - - var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, - padding = this.padding, - padding2 = padding * 2, - strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; - - ctx.save(); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = this.borderColor; - - var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), - scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX), - ~~(h + padding2 + strokeWidth * this.scaleY) - ); - - if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { - - var rotateHeight = ( - this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) - ) / 2; - - ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); - ctx.closePath(); - ctx.stroke(); - } - - ctx.restore(); - return this; - }, - - _renderDashedStroke: function(ctx) { - - if (1 & this.strokeDashArray.length /* if odd number of items */) { - /* duplicate items */ - this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); - } - - var i = 0, - x = -this.width/2, y = -this.height/2, - _this = this, - padding = this.padding, - dashedArrayLength = this.strokeDashArray.length; - - ctx.save(); - ctx.beginPath(); - - function renderSide(xMultiplier, yMultiplier) { - - var lineLength = 0, - lengthDiff = 0, - sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; - - while (lineLength < sideLength) { - - var lengthOfSubPath = _this.strokeDashArray[i++]; - lineLength += lengthOfSubPath; - - if (lineLength > sideLength) { - lengthDiff = lineLength - sideLength; - } - - // track coords - if (xMultiplier) { - x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); - } - else { - y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); - } - - ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); - if (i >= dashedArrayLength) { - i = 0; - } - } - } - - renderSide(1, 0); - renderSide(0, 1); - renderSide(-1, 0); - renderSide(0, -1); - - ctx.stroke(); - ctx.closePath(); - ctx.restore(); - }, - - /** - * 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) { - if (!this.hasControls) return; - - var size = this.cornersize, - size2 = size / 2, - strokeWidth2 = this.strokeWidth / 2, - left = -(this.width / 2), - top = -(this.height / 2), - _left, - _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; - - ctx.save(); - - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = ctx.fillStyle = this.cornerColor; - - // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - if (!this.lockUniScaling) { - // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - } - - // middle-top-rotate - if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffsetX; - - _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_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(); - - /** @ignore */ - 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') - }; - - // normalize angle - this.set('angle', 0).set('flipX', false).set('flipY', false); - this.toDataURL(function(dataURL) { - i.src = dataURL; - }); - } - return this; - }, - - /** - * Converts an object into a data-url-like string - * @method toDataURL - * @return {String} string of data - */ - toDataURL: function(callback) { - var el = fabric.document.createElement('canvas'); - if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(el); - } - - el.width = this.getBoundingRectWidth(); - el.height = this.getBoundingRectHeight(); - - fabric.util.wrapElement(el, 'div'); - - var canvas = new fabric.Canvas(el); - canvas.backgroundColor = 'transparent'; - canvas.renderAll(); - - if (this.constructor.async) { - this.clone(proceed); - } - else { - proceed(this.clone()); - } - - function proceed(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; - - callback && callback(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; - }, - - /** - * @method setupState - */ - setupState: function() { - this.originalState = { }; - this.saveState(); - }, - - /** - * 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) { - // extracts coords - 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 of another object - * @method isContainedWithinObject - * @param {Object} other Object to test - * @return {Boolean} - */ - isContainedWithinObject: function(other) { - return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); - }, - - /** - * 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); - - 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) { - if (!this.hasControls || !this.active) return false; - - var pointer = getPointer(e), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, - xpoints, - lines; - - for (var i in this.oCoords) { - - if (i === 'mtr' && !this.hasRotatingPoint) { - continue; - } - - if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { - continue; - } - - lines = this._getImageLines(this.oCoords[i].corner, i); - - // debugging - - // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - - 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]; - // optimisation 1: line below dot. no cross - if ((iLine.o.y < ey) && (iLine.d.y < ey)) { - continue; - } - // optimisation 2: line above dot. no cross - if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { - continue; - } - // optimisation 3: vertical line case - if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { - xi = iLine.o.x; - yi = ey; - } - // calculate the intersection point - 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; - } - // dont count xi < ex cases - if (xi >= ex) { - xcount += 1; - } - // optimisation 4: specific for square images - 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) { - 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 = degreesToRadians(45 - this.getAngle()), - cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, - cosHalfOffset = cornerHypotenuse * Math.cos(theta), - sinHalfOffset = cornerHypotenuse * Math.sin(theta), - sinTh = Math.sin(this._theta), - cosTh = Math.cos(this._theta); - - coords.tl.corner = { - tl: { - x: coords.tl.x - sinHalfOffset, - y: coords.tl.y - cosHalfOffset - }, - tr: { - x: coords.tl.x + cosHalfOffset, - y: coords.tl.y - sinHalfOffset - }, - bl: { - x: coords.tl.x - cosHalfOffset, - y: coords.tl.y + sinHalfOffset - }, - br: { - x: coords.tl.x + sinHalfOffset, - y: coords.tl.y + cosHalfOffset - } - }; - - coords.tr.corner = { - tl: { - x: coords.tr.x - sinHalfOffset, - y: coords.tr.y - cosHalfOffset - }, - tr: { - x: coords.tr.x + cosHalfOffset, - y: coords.tr.y - sinHalfOffset - }, - br: { - x: coords.tr.x + sinHalfOffset, - y: coords.tr.y + cosHalfOffset - }, - bl: { - x: coords.tr.x - cosHalfOffset, - y: coords.tr.y + sinHalfOffset - } - }; - - coords.bl.corner = { - tl: { - x: coords.bl.x - sinHalfOffset, - y: coords.bl.y - cosHalfOffset - }, - bl: { - x: coords.bl.x - cosHalfOffset, - y: coords.bl.y + sinHalfOffset - }, - br: { - x: coords.bl.x + sinHalfOffset, - y: coords.bl.y + cosHalfOffset - }, - tr: { - x: coords.bl.x + cosHalfOffset, - y: coords.bl.y - sinHalfOffset - } - }; - - coords.br.corner = { - tr: { - x: coords.br.x + cosHalfOffset, - y: coords.br.y - sinHalfOffset - }, - bl: { - x: coords.br.x - cosHalfOffset, - y: coords.br.y + sinHalfOffset - }, - br: { - x: coords.br.x + sinHalfOffset, - y: coords.br.y + cosHalfOffset - }, - tl: { - x: coords.br.x - sinHalfOffset, - y: coords.br.y - cosHalfOffset - } - }; - - coords.ml.corner = { - tl: { - x: coords.ml.x - sinHalfOffset, - y: coords.ml.y - cosHalfOffset - }, - tr: { - x: coords.ml.x + cosHalfOffset, - y: coords.ml.y - sinHalfOffset - }, - bl: { - x: coords.ml.x - cosHalfOffset, - y: coords.ml.y + sinHalfOffset - }, - br: { - x: coords.ml.x + sinHalfOffset, - y: coords.ml.y + cosHalfOffset - } - }; - - coords.mt.corner = { - tl: { - x: coords.mt.x - sinHalfOffset, - y: coords.mt.y - cosHalfOffset - }, - tr: { - x: coords.mt.x + cosHalfOffset, - y: coords.mt.y - sinHalfOffset - }, - bl: { - x: coords.mt.x - cosHalfOffset, - y: coords.mt.y + sinHalfOffset - }, - br: { - x: coords.mt.x + sinHalfOffset, - y: coords.mt.y + cosHalfOffset - } - }; - - coords.mr.corner = { - tl: { - x: coords.mr.x - sinHalfOffset, - y: coords.mr.y - cosHalfOffset - }, - tr: { - x: coords.mr.x + cosHalfOffset, - y: coords.mr.y - sinHalfOffset - }, - bl: { - x: coords.mr.x - cosHalfOffset, - y: coords.mr.y + sinHalfOffset - }, - br: { - x: coords.mr.x + sinHalfOffset, - y: coords.mr.y + cosHalfOffset - } - }; - - coords.mb.corner = { - tl: { - x: coords.mb.x - sinHalfOffset, - y: coords.mb.y - cosHalfOffset - }, - tr: { - x: coords.mb.x + cosHalfOffset, - y: coords.mb.y - sinHalfOffset - }, - bl: { - x: coords.mb.x - cosHalfOffset, - y: coords.mb.y + sinHalfOffset - }, - br: { - x: coords.mb.x + sinHalfOffset, - y: coords.mb.y + cosHalfOffset - } - }; - - coords.mtr.corner = { - tl: { - x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) - }, - tr: { - x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - bl: { - x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - br: { - x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) - } - }; - }, - - /** - * 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; - }, - - /** - * Returns a JSON representation of an instance - * @method toJSON - * @return {String} json - */ - toJSON: function() { - // delegate, not alias - return this.toObject(); - }, - - /** - * @method setGradientFill - */ - setGradientFill: function(options) { - this.set('fill', fabric.Gradient.forObject(this, options)); - }, - - /** - * @method animate - * - * As object — multiple properties - * - * object.animate({ left: ..., top: ... }); - * object.animate({ left: ..., top: ... }, { duration: ... }); - * - * As string — one property - * - * object.animate('left', ...); - * object.animate('left', { duration: ... }); - * - */ - animate: function() { - if (arguments[0] && typeof arguments[0] === 'object') { - for (var prop in arguments[0]) { - this._animate(prop, arguments[0][prop], arguments[1]); - } - } - else { - this._animate.apply(this, arguments); - } - return this; - }, - - /** - * @private - * @method _animate - */ - _animate: function(property, to, options) { - var obj = this; - - options || (options = { }); - - if (!('from' in options)) { - options.from = this.get(property); - } - - if (/[+\-]/.test((to + '').charAt(0))) { - to = this.get(property) + parseFloat(to); - } - - fabric.util.animate({ - startValue: options.from, - endValue: to, - byValue: options.by, - easing: options.easing, - duration: options.duration, - onChange: function(value) { - obj.set(property, value); - options.onChange && options.onChange(); - }, - onComplete: function() { - obj.setCoords(); - options.onComplete && options.onComplete(); - } - }); - }, - - /** - * Centers object horizontally on canvas to which it was added last - * @method centerH - * @return {fabric.Object} thisArg - */ - centerH: function () { - this.canvas.centerObjectH(this); - return this; - }, - - /** - * Centers object vertically on canvas to which it was added last - * @method centerV - * @return {fabric.Object} thisArg - * @chainable - */ - centerV: function () { - this.canvas.centerObjectV(this); - return this; - }, - - /** - * Centers object vertically and horizontally on canvas to which is was added last - * @method center - * @return {fabric.Object} thisArg - * @chainable - */ - center: function () { - return this.centerH().centerV(); - }, - - /** - * Removes object from canvas to which it was added last - * @method remove - * @return {fabric.Object} thisArg - * @chainable - */ - remove: function() { - return this.canvas.remove(this); - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @return {fabric.Object} thisArg - * @chainable - */ - sendToBack: function() { - this.canvas.sendToBack(this); - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - this.canvas.bringToFront(this); - return this; - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function() { - this.canvas.sendBackwards(this); - return this; - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function() { - this.canvas.bringForward(this); - return this; - } - }); - - /** - * @alias rotate -> setAngle - */ - fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; - - var proto = fabric.Object.prototype; - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - - extend(fabric.Object.prototype, fabric.Observable); - - extend(fabric.Object, { - - /** - * @static - * @constant - * @type Number - */ - NUM_FRACTION_DIGITS: 2, - - /** - * @static - * @constant - * @type Number - */ - MIN_SCALE_LIMIT: 0.1 - - }); - -})(typeof exports !== 'undefined' ? exports : this); - -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 }; - - if (fabric.Line) { - fabric.warn('fabric.Line is already defined'); - return; - } - - /** - * @class Line - * @extends fabric.Object - */ - fabric.Line = fabric.util.createClass(fabric.Object, /** @scope fabric.Line.prototype */ { - - /** - * @property - * @type String - */ - type: 'line', - - /** - * Constructor - * @method initialize - * @param {Array} points Array of points - * @param {Object} [options] Options object - * @return {fabric.Line} 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._setWidthHeight(options); - }, - - /** - * @private - * @method _setWidthHeight - * @param {Object} options - */ - _setWidthHeight: function(options) { - options || (options = { }); - - this.set('width', (this.x2 - this.x1) || 1); - this.set('height', (this.y2 - this.y1) || 1); - - this.set('left', 'left' in options ? options.left : (this.x1 + this.width / 2)); - this.set('top', 'top' in options ? options.top : (this.y1 + this.height / 2)); - }, - - /** - * @private - * @method _set - * @param {String} key - * @param {Any} value - */ - _set: function(key, value) { - this[key] = value; - if (key in coordProps) { - this._setWidthHeight(); - } - return this; - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - ctx.beginPath(); - - if (this.group) { - ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); - } - - // move from center (of virtual box) to its left/top corner - ctx.moveTo(this.width === 1 ? 0 : (-this.width / 2), this.height === 1 ? 0 : (-this.height / 2)); - ctx.lineTo(this.width === 1 ? 0 : (this.width / 2), this.height === 1 ? 0 : (this.height / 2)); - - ctx.lineWidth = this.strokeWidth; - - // TODO: test this - // make sure setting "fill" changes color of a line - // (by copying fillStyle to strokeStyle, since line is stroked, not filled) - var origStrokeStyle = ctx.strokeStyle; - ctx.strokeStyle = ctx.fillStyle; - ctx.stroke(); - ctx.strokeStyle = origStrokeStyle; - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - }, - - /** - * Returns object representation of an instance - * @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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return [ - '' - ].join(''); - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Line.fromElement`) - * @static - * @see http://www.w3.org/TR/SVG/shapes.html#LineElement - */ - fabric.Line.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Line instance from an SVG element - * @static - * @method fabric.Line.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @return {fabric.Line} instance of fabric.Line - */ - fabric.Line.fromElement = function(element, options) { - var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); - var points = [ - parsedAttributes.x1 || 0, - parsedAttributes.y1 || 0, - parsedAttributes.x2 || 0, - parsedAttributes.y2 || 0 - ]; - return new fabric.Line(points, extend(parsedAttributes, options)); - }; - - /** - * Returns fabric.Line instance from an object representation - * @static - * @method fabric.Line.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Line} 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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - piBy2 = Math.PI * 2, - extend = fabric.util.object.extend; - - if (fabric.Circle) { - fabric.warn('fabric.Circle is already defined.'); - return; - } - - /** - * @class Circle - * @extends fabric.Object - */ - fabric.Circle = fabric.util.createClass(fabric.Object, /** @scope fabric.Circle.prototype */ { - - /** - * @property - * @type String - */ - type: 'circle', - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - * @return {fabric.Circle} thisArg - */ - initialize: function(options) { - options = options || { }; - - this.set('radius', options.radius || 0); - this.callSuper('initialize', options); - - var diameter = this.get('radius') * 2; - this.set('width', diameter).set('height', diameter); - }, - - /** - * 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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return (''); - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx, noTransform) { - ctx.beginPath(); - // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) - ctx.globalAlpha *= this.opacity; - 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 horizontal radius of an object (according to how an object is scaled) - * @method getRadiusX - * @return {Number} - */ - getRadiusX: function() { - return this.get('radius') * this.get('scaleX'); - }, - - /** - * Returns vertical radius of an object (according to how an object is scaled) - * @method getRadiusY - * @return {Number} - */ - getRadiusY: function() { - return this.get('radius') * this.get('scaleY'); - }, - - /** - * Sets radius of an object (and updates width accordingly) - * @method setRadius - * @return {Number} - */ - setRadius: function(value) { - this.radius = value; - this.set('width', value * 2).set('height', value * 2); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity of this instance - */ - complexity: function() { - return 1; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement - */ - fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns {@link fabric.Circle} instance from an SVG element - * @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 new 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)); - } - - /** - * Returns {@link fabric.Circle} instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Triangle) { - fabric.warn('fabric.Triangle is already defined'); - return; - } - - /** - * @class Triangle - * @extends fabric.Object - */ - fabric.Triangle = fabric.util.createClass(fabric.Object, /** @scope fabric.Triangle.prototype */ { - - /** - * @property - * @type String - */ - type: 'triangle', - - /** - * Constructor - * @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; - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - - var widthBy2 = this.width / 2, - heightBy2 = this.height / 2; - - var points = [ - -widthBy2 + " " + heightBy2, - "0 " + -heightBy2, - widthBy2 + " " + heightBy2 - ].join(","); - - return ''; - } - }); - - /** - * Returns fabric.Triangle instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global){ - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - piBy2 = Math.PI * 2, - extend = fabric.util.object.extend; - - if (fabric.Ellipse) { - fabric.warn('fabric.Ellipse is already defined.'); - return; - } - - /** - * @class Ellipse - * @extends fabric.Object - */ - fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @scope fabric.Ellipse.prototype */ { - - /** - * @property - * @type String - */ - type: 'ellipse', - - /** - * Constructor - * @method initialize - * @param {Object} [options] 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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return [ - '' - ].join(''); - }, - - /** - * Renders this instance on a given context - * @method render - * @param ctx {CanvasRenderingContext2D} context to render on - * @param noTransform {Boolean} context is not transformed when set to true - */ - render: function(ctx, noTransform) { - // do not use `get` for perf. reasons - 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.globalAlpha *= this.opacity; - if (this.transformMatrix && this.group) { - ctx.translate(this.cx, this.cy); - } - 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); - if (this.stroke) { - ctx.stroke(); - } - if (this.fill) { - ctx.fill(); - } - ctx.restore(); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) - * @static - * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement - */ - fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns {@link fabric.Ellipse} instance from an SVG element - * @static - * @method fabric.Ellipse.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @return {fabric.Ellipse} - */ - fabric.Ellipse.fromElement = function(element, options) { - options || (options = { }); - - var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); - var cx = parsedAttributes.left; - var cy = parsedAttributes.top; - - if ('left' in parsedAttributes) { - parsedAttributes.left -= (options.width / 2) || 0; - } - if ('top' in parsedAttributes) { - parsedAttributes.top -= (options.height / 2) || 0; - } - - var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); - - ellipse.cx = cx || 0; - ellipse.cy = cy || 0; - - return ellipse; - }; - - /** - * Returns fabric.Ellipse instance from an object representation - * @static - * @method fabric.Ellipse.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Ellipse} - */ - fabric.Ellipse.fromObject = function(object) { - return new fabric.Ellipse(object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Rect) { - console.warn('fabric.Rect is already defined'); - return; - } - - /** - * @class Rect - * @extends fabric.Object - */ - fabric.Rect = fabric.util.createClass(fabric.Object, /** @scope fabric.Rect.prototype */ { - - /** - * @property - * @type String - */ - type: 'rect', - - /** - * @property - * @type Number - */ - rx: 0, - - /** - * @property - * @type Number - */ - ry: 0, - - /** - * Constructor - * @method initialize - * @param options {Object} options object - * @return {Object} thisArg - */ - initialize: function(options) { - this._initStateProperties(); - this.callSuper('initialize', options); - this._initRxRy(); - }, - - /** - * Creates `stateProperties` list on an instance, and adds `fabric.Rect` -specific ones to it - * (such as "rx", "ry", etc.) - * @private - * @method _initStateProperties - */ - _initStateProperties: function() { - this.stateProperties = this.stateProperties.concat(['rx', 'ry']); - }, - - /** - * @private - * @method _initRxRy - */ - _initRxRy: function() { - if (this.rx && !this.ry) { - this.ry = this.rx; - } - else if (this.ry && !this.rx) { - this.rx = this.ry; - } - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx) { - var rx = this.rx || 0, - ry = this.ry || 0, - x = -this.width / 2, - y = -this.height / 2, - w = this.width, - h = this.height; - - ctx.beginPath(); - ctx.globalAlpha *= this.opacity; - - if (this.transformMatrix && this.group) { - ctx.translate( - this.width / 2 + this.x, - this.height / 2 + this.y); - } - if (!this.transformMatrix && this.group) { - ctx.translate( - -this.group.width / 2 + this.width / 2 + this.x, - -this.group.height / 2 + this.height / 2 + this.y); - } - - ctx.moveTo(x+rx, y); - ctx.lineTo(x+w-rx, y); - ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry); - ctx.lineTo(x+w, y+h-ry); - ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h); - ctx.lineTo(x+rx,y+h); - ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry); - ctx.lineTo(x,y+ry); - ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y); - ctx.closePath(); - - if (this.fill) { - ctx.fill(); - } - - if (this.strokeDashArray) { - this._renderDashedStroke(ctx); - } - else if (this.stroke) { - ctx.stroke(); - } - }, - - // since our coordinate system differs from that of SVG - _normalizeLeftTopProperties: function(parsedAttributes) { - if (parsedAttributes.left) { - this.set('left', parsedAttributes.left + this.getWidth() / 2); - } - this.set('x', parsedAttributes.left || 0); - if (parsedAttributes.top) { - this.set('top', parsedAttributes.top + this.getHeight() / 2); - } - this.set('y', parsedAttributes.top || 0); - return this; - }, - - /** - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} object representation of an instance - */ - toObject: function() { - return fabric.util.object.extend(this.callSuper('toObject'), { - rx: this.get('rx') || 0, - ry: this.get('ry') || 0 - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return ''; - } - }); - - // TODO (kangax): implement rounded rectangles (both parsing and rendering) - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) - * @static - */ - fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * @private - */ - function _setDefaultLeftTopValues(attributes) { - attributes.left = attributes.left || 0; - attributes.top = attributes.top || 0; - return attributes; - } - - /** - * Returns fabric.Rect instance from an SVG element - * @static - * @method fabric.Rect.fromElement - * @param element {SVGElement} element to parse - * @param options {Object} options object - * @return {fabric.Rect} 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 ? fabric.util.object.clone(options) : { }), parsedAttributes)); - rect._normalizeLeftTopProperties(parsedAttributes); - - return rect; - }; - - /** - * Returns fabric.Rect instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed; - - if (fabric.Polyline) { - fabric.warn('fabric.Polyline is already defined'); - return; - } - - /** - * @class Polyline - * @extends fabric.Object - */ - fabric.Polyline = fabric.util.createClass(fabric.Object, /** @scope fabric.Polyline.prototype */ { - - /** - * @property - * @type String - */ - type: 'polyline', - - /** - * Constructor - * @method initialize - * @param {Array} points array of points - * @param {Object} [options] 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); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var points = []; - for (var i = 0, len = this.points.length; i < len; i++) { - points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); - } - - return [ - '' - ].join(''); - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - var point; - ctx.beginPath(); - ctx.moveTo(this.points[0].x, this.points[0].y); - 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(); - } - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return this.get('points').length; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polyline.fromElement`) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement - */ - fabric.Polyline.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Polyline instance from an SVG element - * @static - * @method fabric.Polyline.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] 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, fabric.Polyline.ATTRIBUTE_NAMES); - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - 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)); - }; - - /** - * Returns fabric.Polyline instance from an object representation - * @static - * @method fabric.Polyline.fromObject - * @param {Object} [object] Object to create an instance from - * @return {fabric.Polyline} - */ - fabric.Polyline.fromObject = function(object) { - var points = object.points; - return new fabric.Polyline(points, object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - min = fabric.util.array.min, - max = fabric.util.array.max, - toFixed = fabric.util.toFixed; - - if (fabric.Polygon) { - fabric.warn('fabric.Polygon is already defined'); - return; - } - - /** - * @class Polygon - * @extends fabric.Object - */ - fabric.Polygon = fabric.util.createClass(fabric.Object, /** @scope fabric.Polygon.prototype */ { - - /** - * @property - * @type String - */ - type: 'polygon', - - /** - * Constructor - * @method initialize - * @param {Array} points Array of points - * @param {Object} options Options object - * @return {fabric.Polygon} 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) || 1; - this.height = (maxY - minY) || 1; - - 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() - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var points = []; - for (var i = 0, len = this.points.length; i < len; i++) { - points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); - } - - return [ - '' - ].join(''); - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx) { - var point; - ctx.beginPath(); - ctx.moveTo(this.points[0].x, this.points[0].y); - 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; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement - */ - fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Polygon instance from an SVG element - * @static - * @method fabric.Polygon.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} options Options object - * @return {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++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2) || 0; - points[i].y -= (options.height / 2) || 0; - } - - return new fabric.Polygon(points, extend(parsedAttributes, options)); - }; - - /** - * Returns fabric.Polygon instance from an object representation - * @static - * @method fabric.Polygon.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Polygon} - */ - fabric.Polygon.fromObject = function(object) { - return new fabric.Polygon(object.points, object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - var commandLengths = { - m: 2, - l: 2, - h: 1, - v: 1, - c: 6, - s: 4, - q: 4, - t: 2, - a: 7 - }; - - function drawArc(ctx, x, y, coords) { - var rx = coords[0]; - var ry = coords[1]; - var rot = coords[2]; - var large = coords[3]; - var sweep = coords[4]; - var ex = coords[5]; - var ey = coords[6]; - var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); - for (var i=0; i 1) { - pl = Math.sqrt(pl); - rx *= pl; - ry *= pl; - } - - var a00 = cos_th / rx; - var a01 = sin_th / rx; - var a10 = (-sin_th) / ry; - var a11 = (cos_th) / ry; - var x0 = a00 * ox + a01 * oy; - var y0 = a10 * ox + a11 * oy; - var x1 = a00 * x + a01 * y; - var y1 = a10 * x + a11 * y; - - var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); - var sfactor_sq = 1 / d - 0.25; - if (sfactor_sq < 0) sfactor_sq = 0; - var sfactor = Math.sqrt(sfactor_sq); - if (sweep === large) sfactor = -sfactor; - var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); - var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); - - var th0 = Math.atan2(y0-yc, x0-xc); - var th1 = Math.atan2(y1-yc, x1-xc); - - var th_arc = th1-th0; - if (th_arc < 0 && sweep === 1){ - th_arc += 2*Math.PI; - } else if (th_arc > 0 && sweep === 0) { - th_arc -= 2 * Math.PI; - } - - var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); - var result = []; - for (var i=0; i'; - }, - - /** - * Returns object representation of an instance - * @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; - }, - - /** - * Returns dataless object representation of an instance - * @method toDatalessObject - * @return {Object} - */ - toDatalessObject: function() { - var o = this.toObject(); - if (this.sourcePath) { - o.path = this.sourcePath; - } - delete o.sourcePath; - return o; - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var chunks = []; - for (var i = 0, len = this.path.length; i < len; i++) { - chunks.push(this.path[i].join(' ')); - } - var path = chunks.join(' '); - - return [ - '', - '', - '' - ].join(''); - }, - - /** - * Returns number representation of an instance complexity - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return this.path.length; - }, - - /** - * @private - * @method _parsePath - */ - _parsePath: function() { - var result = [ ], - currentPath, - chunks, - parsed; - - for (var i = 0, chunksParsed, len = this.path.length; i < len; i++) { - currentPath = this.path[i]; - chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/); - chunksParsed = [ currentPath.charAt(0) ]; - - for (var j = 0, jlen = chunks.length; j < jlen; j++) { - parsed = parseFloat(chunks[j]); - if (!isNaN(parsed)) { - chunksParsed.push(parsed); - } - } - - var command = chunksParsed[0].toLowerCase(), - commandLength = commandLengths[command]; - - if (chunksParsed.length - 1 > commandLength) { - for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) { - result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); - } - } - else { - result.push(chunksParsed); - } - } - - return result; - }, - - /** - * @method _parseDimensions - */ - _parseDimensions: function() { - var aX = [], - aY = [], - previousX, - previousY, - isLowerCase = false, - x, - y; - - 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]); - } - - // lowercased letter denotes relative position; - // transform to absolute - if (item[0] === item[0].toLowerCase()) { - isLowerCase = true; - } - - // last 2 items in an array of coordinates are the actualy x/y (except H/V); - // collect them - - // TODO (kangax): support relative h/v commands - - 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 = 0, - 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); - }; - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) - * @static - * @see http://www.w3.org/TR/SVG/paths.html#PathElement - */ - fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' '); - - /** - * Creates an instance of fabric.Path from an SVG 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, fabric.Path.ATTRIBUTE_NAMES); - return new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options)); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - invoke = fabric.util.array.invoke, - parentToObject = fabric.Object.prototype.toObject, - camelize = fabric.util.string.camelize, - capitalize = fabric.util.string.capitalize; - - if (fabric.PathGroup) { - fabric.warn('fabric.PathGroup is already defined'); - return; - } - - /** - * @class PathGroup - * @extends fabric.Path - */ - fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @scope fabric.PathGroup.prototype */ { - - /** - * @property - * @type String - */ - type: 'path-group', - - /** - * @property - * @type String - */ - fill: '', - - /** - * @property - * @type Boolean - */ - forceFillOverwrite: false, - - /** - * Constructor - * @method initialize - * @param {Array} paths - * @param {Object} [options] Options object - * @return {fabric.PathGroup} thisArg - */ - initialize: function(paths, options) { - - options = options || { }; - this.paths = paths || [ ]; - - for (var i = this.paths.length; i--; ) { - this.paths[i].group = this; - } - - this.setOptions(options); - this.setCoords(); - - if (options.sourcePath) { - this.setSourcePath(options.sourcePath); - } - }, - - /** - * Renders this group on a specified context - * @method render - * @param {CanvasRenderingContext2D} ctx Context to render this instance on - */ - render: function(ctx) { - 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(); - }, - - /** - * Sets certain property to a certain value - * @method _set - * @param {String} prop - * @param {Any} value - * @return {fabric.PathGroup} thisArg - */ - _set: function(prop, value) { - - if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) { - var i = this.paths.length; - while (i--) { - this.paths[i]._set(prop, value); - } - } - - return this.callSuper('_set', prop, value); - }, - - /** - * Returns object representation of this path group - * @method toObject - * @return {Object} object representation of an instance - */ - toObject: function() { - return extend(parentToObject.call(this), { - paths: invoke(this.getObjects(), 'toObject'), - sourcePath: this.sourcePath - }); - }, - - /** - * Returns dataless object representation of this path group - * @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 svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var objects = this.getObjects(); - var markup = [ - '' - ]; - - for (var i = 0, len = objects.length; i < len; i++) { - markup.push(objects[i].toSVG()); - } - markup.push(''); - - return markup.join(''); - }, - - /** - * Returns a string representation of this path group - * @method toString - * @return {String} string representation of an object - */ - toString: function() { - return '#'; - }, - - /** - * Returns true if all paths in this group are of same color - * @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; - }, - - /** - * Returns all paths in this path group - * @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; - } - - /** - * Creates fabric.Triangle instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global){ - - "use strict"; - - var fabric = global.fabric || (global.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; - } - - /** - * @class Group - * @extends fabric.Object - */ - fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { - - /** - * @property - * @type String - */ - type: 'group', - - /** - * Constructor - * @method initialized - * @param {Object} objects Group objects - * @param {Object} [options] Options object - * @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(); - - // group is active by default - 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(); - - // do not display corners of objects enclosed in a group - object.hideCorners = true; - }, this); - }, - - /** - * Returns string represenation of a group - * @method toString - * @return {String} - */ - toString: function() { - return '#'; - }, - - /** - * Returns an array of all objects in this group - * @method getObjects - * @return {Array} group objects - */ - getObjects: function() { - return this.objects; - }, - - /** - * Adds an object to a group; Then recalculates group's dimension, position. - * @method addWithUpdate - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - addWithUpdate: function(object) { - this._restoreObjectsState(); - this.objects.push(object); - this._calcBounds(); - this._updateObjectsCoords(); - return this; - }, - - /** - * Removes an object from a group; Then recalculates group's dimension, position. - * @method removeWithUpdate - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - removeWithUpdate: function(object) { - this._restoreObjectsState(); - removeFromArray(this.objects, object); - object.setActive(false); - this._calcBounds(); - this._updateObjectsCoords(); - return this; - }, - - /** - * Adds an object to a group - * @method add - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - add: function(object) { - this.objects.push(object); - return this; - }, - - /** - * Removes an object from a group - * @method remove - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - remove: function(object) { - removeFromArray(this.objects, object); - 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; - }, - - /** - * @private - */ - _set: function(key, value) { - if (key === 'fill' || key === 'opacity') { - var i = this.objects.length; - this[key] = value; - while (i--) { - this.objects[i].set(key, value); - } - } - else { - this[key] = value; - } - }, - - /** - * 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, 'toObject') - }); - }, - - /** - * Renders instance on a given context - * @method render - * @param {CanvasRenderingContext2D} ctx context to render instance on - */ - render: function(ctx, noTransform) { - ctx.save(); - this.transform(ctx); - - var groupScaleFactor = Math.max(this.scaleX, this.scaleY); - - for (var i = 0, len = this.objects.length; i < len; i++) { - - var object = this.objects[i]; - var originalScaleFactor = object.borderScaleFactor; - - object.borderScaleFactor = groupScaleFactor; - object.render(ctx); - object.borderScaleFactor = originalScaleFactor; - } - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - this.setCoords(); - }, - - /** - * Returns object from the group at the specified index - * @method item - * @param index {Number} index of item to get - * @return {fabric.Object} - */ - item: function(index) { - return this.getObjects()[index]; - }, - - /** - * Returns complexity of an instance - * @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 (original state is that which was before group was created). - * @private - * @method _restoreObjectsState - * @return {fabric.Group} thisArg - * @chainable - */ - _restoreObjectsState: function() { - this.objects.forEach(this._restoreObjectState, this); - return this; - }, - - /** - * Restores original state of a specified object in group - * @private - * @method _restoreObjectState - * @param {fabric.Object} object - * @return {fabric.Group} thisArg - */ - _restoreObjectState: function(object) { - - var groupLeft = this.get('left'), - groupTop = this.get('top'), - groupAngle = this.getAngle() * (Math.PI / 180), - 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; - }, - - /** - * Destroys a group (restoring state of its objects) - * @method destroy - * @return {fabric.Group} thisArg - * @chainable - */ - destroy: function() { - return this._restoreObjectsState(); - }, - - /** - * Saves coordinates of this instance (to be used together with `hasMoved`) - * @saveCoords - * @return {fabric.Group} thisArg - * @chainable - */ - saveCoords: function() { - this._originalLeft = this.get('left'); - this._originalTop = this.get('top'); - return this; - }, - - /** - * Checks whether this group was moved (since `saveCoords` was called last) - * @method hasMoved - * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) - */ - 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() { - this.forEachObject(function(object) { - object.setActive(); - }); - return this; - }, - - /** - * Executes given function for each object in this group - * @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 Context (aka thisObject) - * - * @return {fabric.Group} thisArg - * @chainable - */ - forEachObject: fabric.StaticCanvas.prototype.forEachObject, - - /** - * @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) || 0; - height = (maxY - minY) || 0; - - this.width = width; - this.height = height; - - this.left = (minX + width / 2) || 0; - this.top = (minY + height / 2) || 0; - }, - - /** - * Checks if point is contained within the group - * @method containsPoint - * @param {fabric.Point} 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; - }, - - /** - * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) - * @method toGrayscale - */ - toGrayscale: function() { - var i = this.objects.length; - while (i--) { - this.objects[i].toGrayscale(); - } - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var objectsMarkup = [ ]; - for (var i = 0, len = this.objects.length; i < len; i++) { - objectsMarkup.push(this.objects[i].toSVG()); - } - - return ( - '' + - objectsMarkup.join('') + - ''); - } - }); - - /** - * Returns fabric.Group instance from an object representation - * @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, callback) { - fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { - delete object.objects; - callback && callback(new fabric.Group(enlivenedObjects, object)); - }); - }; - - fabric.Group.async = true; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var extend = fabric.util.object.extend; - - if (!global.fabric) { - global.fabric = { }; - } - - if (global.fabric.Image) { - fabric.warn('fabric.Image is already defined.'); - return; - } - - if (!fabric.Object) { - fabric.warn('fabric.Object is required for fabric.Image initialization'); - return; - } - - /** - * @class Image - * @extends fabric.Object - */ - fabric.Image = fabric.util.createClass(fabric.Object, /** @scope fabric.Image.prototype */ { - - /** - * @property - * @type Boolean - */ - active: false, - - /** - * @property - * @type Boolean - */ - bordervisibility: false, - - /** - * @property - * @type Boolean - */ - cornervisibility: false, - - /** - * @property - * @type String - */ - type: 'image', - - /** - * Constructor - * @param {HTMLImageElement | String} element Image element - * @param {Object} options optional - */ - initialize: function(element, options) { - options || (options = { }); - - this.callSuper('initialize', options); - this._initElement(element); - this._originalImage = this.getElement(); - this._initConfig(options); - - this.filters = [ ]; - - if (options.filters) { - this.filters = options.filters; - this.applyFilters(); - } - }, - - /** - * Returns image element which this instance if based on - * @method getElement - * @return {HTMLImageElement} image element - */ - getElement: function() { - return this._element; - }, - - /** - * Sets image element for this instance to a specified one - * @method setElement - * @param {HTMLImageElement} element - * @return {fabric.Image} thisArg - * @chainable - */ - setElement: function(element) { - this._element = element; - this._initConfig(); - return this; - }, - - /** - * Returns original size of an image - * @method getOriginalSize - * @return {Object} object with "width" and "height" properties - */ - getOriginalSize: function() { - var element = this.getElement(); - return { - width: element.width, - height: element.height - }; - }, - - /** - * Sets border visibility - * @method setBorderVisibility - * @param {Boolean} visible When true, border is set to be visible - */ - setBorderVisibility: function() { - this._resetWidthHeight(); - this._adjustWidthHeightToBorders(); - this.setCoords(); - }, - - /** - * Sets corner visibility - * @method setCornersVisibility - * @param {Boolean} visible When true, corners are set to be visible - */ - setCornersVisibility: function(visible) { - this.cornervisibility = !!visible; - }, - - /** - * Renders image on a specified context - * @method render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - render: function(ctx, noTransform) { - ctx.save(); - var m = this.transformMatrix; - this._resetWidthHeight(); - if (this.group) { - ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); - } - if (m) { - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - if (!noTransform) { - this.transform(ctx); - } - this._render(ctx); - if (this.active && !noTransform) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - src: this._originalImage.src || this._originalImage._src, - filters: this.filters.concat() - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return ''+ - ' element with actual transformation, then offsetting object to the top/left - // so that object's center aligns with container's left/top - 'transform="translate('+ (-this.width/2) + ' ' + (-this.height/2) + ')" ' + - 'width="' + this.width + '" ' + - 'height="' + this.height + '"' + '/>'+ - ''; - }, - - /** - * Returns source of an image - * @method getSrc - * @return {String} Source of an image - */ - getSrc: function() { - return this.getElement().src || this.getElement()._src; - }, - - /** - * Returns string representation of an instance - * @method toString - * @return {String} String representation of an instance - */ - toString: function() { - return '#'; - }, - - /** - * Returns a clone of an instance - * @mthod clone - * @param {Function} callback Callback is invoked with a clone as a first argument - */ - clone: function(callback) { - this.constructor.fromObject(this.toObject(), callback); - }, - - /** - * Applies filters assigned to this image (from "filters" array) - * @mthod applyFilters - * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated - */ - applyFilters: function(callback) { - - if (this.filters.length === 0) { - this.setElement(this._originalImage); - callback && callback(); - return; - } - - var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined', - imgEl = this._originalImage, - canvasEl = fabric.document.createElement('canvas'), - replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'), - _this = this; - - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - - canvasEl.width = imgEl.width; - canvasEl.height = imgEl.height; - - canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); - - this.filters.forEach(function(filter) { - filter && filter.applyTo(canvasEl); - }); - - /** @ignore */ - replacement.onload = function() { - _this._element = replacement; - callback && callback(); - replacement.onload = canvasEl = imgEl = null; - }; - replacement.width = imgEl.width; - replacement.height = imgEl.height; - - if (isLikelyNode) { - var base64str = canvasEl.toDataURL('image/png').replace(/data:image\/png;base64,/, ''); - replacement.src = new Buffer(base64str, 'base64'); - _this._element = replacement; - - // onload doesn't fire in node, so we invoke callback manually - callback && callback(); - } - else { - replacement.src = canvasEl.toDataURL('image/png'); - } - - return this; - }, - - /** - * @private - */ - _render: function(ctx) { - ctx.drawImage( - this.getElement(), - - this.width / 2, - -this.height / 2, - this.width, - this.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) { - options || (options = { }); - this.setOptions(options); - this._setBorder(); - this._setWidthHeight(options); - }, - - /** - * @method _initFilters - * @param {Object} object Object with filters property - */ - _initFilters: function(object) { - if (object.filters && object.filters.length) { - this.filters = object.filters.map(function(filterObj) { - return filterObj && fabric.Image.filters[filterObj.type].fromObject(filterObj); - }); - } - }, - - /** - * @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 = 'width' in options - ? options.width - : ((this.getElement().width || 0) + sidesBorderWidth); - - this.height = 'height' in options - ? options.height - : ((this.getElement().height || 0) + sidesBorderWidth); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - } - }); - - /** - * Default CSS class name for canvas - * @static - * @type String - */ - fabric.Image.CSS_CANVAS = "canvas-img"; - - fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; - - /** - * Creates an instance of fabric.Image from its object representation - * @static - * @method fromObject - * @param object {Object} - * @param callback {Function} optional - */ - fabric.Image.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.src; - - if (object.width) { - img.width = object.width; - } - if (object.height) { - img.height = object.height; - } - - /** @ignore */ - img.onload = function() { - fabric.Image.prototype._initFilters.call(object, object); - - var instance = new fabric.Image(img, object); - callback && callback(instance); - img = img.onload = null; - }; - img.src = src; - }; - - /** - * Creates an instance of fabric.Image from an URL string - * @static - * @method fromURL - * @param {String} url URL to create an image from - * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) - * @param {Object} [imgOptions] Options object - */ - fabric.Image.fromURL = function(url, callback, imgOptions) { - var img = fabric.document.createElement('img'); - - /** @ignore */ - img.onload = function() { - if (callback) { - callback(new fabric.Image(img, imgOptions)); - } - img = img.onload = null; - }; - img.src = url; - }; - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) - * @static - * @see http://www.w3.org/TR/SVG/struct.html#ImageElement - */ - fabric.Image.ATTRIBUTE_NAMES = 'x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href'.split(' '); - - /** - * Returns {@link fabric.Image} instance from an SVG element - * @static - * @method fabric.Image.fromElement - * @param {SVGElement} element Element to parse - * @param {Function} callback Callback to execute when fabric.Image object is created - * @param {Object} [options] Options object - * @return {fabric.Image} - */ - fabric.Image.fromElement = function(element, callback, options) { - options || (options = { }); - - var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - - fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend(parsedAttributes, options)); - }; - - fabric.Image.async = true; - -})(typeof exports !== 'undefined' ? exports : this); - -fabric.util.object.extend(fabric.Object.prototype, { - - /** - * @method _getAngleValueForStraighten - * @return {Number} angle value - * @private - */ - _getAngleValueForStraighten: function() { - var angle = this.get('angle'); - - // TODO (kangax): can this be simplified? - - 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; - }, - - /** - * @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; - } -}); - -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - /** - * Straightens object, then rerenders canvas - * @method straightenObject - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - straightenObject: function (object) { - object.straighten(); - this.renderAll(); - return this; - }, - - /** - * Same as `fabric.Canvas#straightenObject`, but animated - * @method fxStraightenObject - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - fxStraightenObject: function (object) { - object.fxStraighten({ - onChange: this.renderAll.bind(this) - }); - return this; - } -}); -/** - * @namespace - */ -fabric.Image.filters = { }; - -/** - * @class fabric.Image.filters.Grayscale - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Grayscale = fabric.util.createClass( /** @scope fabric.Image.filters.Grayscale.prototype */ { - - /** - * @param {String} type - */ - type: "Grayscale", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Grayscale.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = imageData.width, - jLen = imageData.height, - index, average, i, j; - - for (i = 0; i < iLen; i++) { - for (j = 0; j < jLen; j++) { - - index = (i * 4) * jLen + (j * 4); - average = (data[index] + data[index + 1] + data[index + 2]) / 3; - - data[index] = average; - data[index + 1] = average; - data[index + 2] = average; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Grayscale.fromObject = function() { - return new fabric.Image.filters.Grayscale(); -}; - -/** - * @class fabric.Image.filters.RemoveWhite - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.RemoveWhite = fabric.util.createClass( /** @scope fabric.Image.filters.RemoveWhite.prototype */ { - - /** - * @param {String} type - */ - type: "RemoveWhite", - - /** - * @memberOf fabric.Image.filters.RemoveWhite.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.threshold = options.threshold || 30; - this.distance = options.distance || 20; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - distance = this.distance, - limit = 255 - threshold, - abs = Math.abs, - r, g, b; - - for (var i = 0, len = data.length; i < len; i += 4) { - - r = data[i]; - g = data[i+1]; - b = data[i+2]; - - if (r > limit && - g > limit && - b > limit && - abs(r-g) < distance && - abs(r-b) < distance && - abs(g-b) < distance) { - - data[i+3] = 1; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - threshold: this.threshold, - distance: this.distance - }; - } -}); - -fabric.Image.filters.RemoveWhite.fromObject = function(object) { - return new fabric.Image.filters.RemoveWhite(object); -}; - -/** - * @class fabric.Image.filters.Invert - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Invert = fabric.util.createClass( /** @scope fabric.Image.filters.Invert.prototype */ { - - /** - * @param {String} type - */ - type: "Invert", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Invert.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i; - - for (i = 0; i < iLen; i+=4) { - data[i] = 255 - data[i]; - data[i + 1] = 255 - data[i + 1]; - data[i + 2] = 255 - data[i + 2]; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Invert.fromObject = function() { - return new fabric.Image.filters.Invert(); -}; - -/** - * @class fabric.Image.filters.Sepia - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Sepia = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia.prototype */ { - - /** - * @param {String} type - */ - type: "Sepia", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, avg; - - for (i = 0; i < iLen; i+=4) { - avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; - data[i] = avg + 100; - data[i + 1] = avg + 50; - data[i + 2] = avg + 255; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Sepia.fromObject = function() { - return new fabric.Image.filters.Sepia(); -}; - -/** - * @class fabric.Image.filters.Sepia2 - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Sepia2 = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia2.prototype */ { - - /** - * @param {String} type - */ - type: "Sepia2", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, r, g, b; - - for (i = 0; i < iLen; i+=4) { - - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; - data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; - data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Sepia2.fromObject = function() { - return new fabric.Image.filters.Sepia2(); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Brightness = fabric.util.createClass( /** @scope fabric.Image.filters.Brightness.prototype */ { - - /** - * @param {String} type - */ - type: "Brightness", - - /** - * @memberOf fabric.Image.filters.Brightness.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.brightness = options.brightness || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - brightness = this.brightness; - - for (var i = 0, len = data.length; i < len; i += 4) { - data[i] += brightness; - data[i + 1] += brightness; - data[i + 2] += brightness; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - brightness: this.brightness - }; - } -}); - -fabric.Image.filters.Brightness.fromObject = function(object) { - return new fabric.Image.filters.Brightness(object); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Noise = fabric.util.createClass( /** @scope fabric.Image.filters.Noise.prototype */ { - - /** - * @param {String} type - */ - type: "Noise", - - /** - * @memberOf fabric.Image.filters.Brightness.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.noise = options.noise || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - noise = this.noise, rand; - - for (var i = 0, len = data.length; i < len; i += 4) { - - rand = (0.5 - Math.random()) * noise; - - data[i] += rand; - data[i + 1] += rand; - data[i + 2] += rand; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - noise: this.noise - }; - } -}); - -fabric.Image.filters.Noise.fromObject = function(object) { - return new fabric.Image.filters.Noise(object); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.GradientTransparency = fabric.util.createClass( /** @scope fabric.Image.filters.GradientTransparency.prototype */ { - - /** - * @param {String} type - */ - type: "GradientTransparency", - - /** - * @memberOf fabric.Image.filters.GradientTransparency.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.threshold = options.threshold || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - total = data.length; - - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = threshold + 255 * (total - i) / total; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - threshold: this.threshold - }; - } -}); - -fabric.Image.filters.GradientTransparency.fromObject = function(object) { - return new fabric.Image.filters.GradientTransparency(object); -}; - -/** - * @class fabric.Image.filters.Tint - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Tint = fabric.util.createClass( /** @scope fabric.Image.filters.Tint.prototype */ { - - /** - * @param {String} type - */ - type: "Tint", - - /** - * @memberOf fabric.Image.filters.RemoveWhite.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.color = options.color || 0; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, a; - - var rgb = parseInt(this.color, 10).toString(16); - - var cr = parseInt('0x' + rgb.substr(0, 2), 16); - var cg = parseInt('0x' + rgb.substr(2, 2), 16); - var cb = parseInt('0x' + rgb.substr(4, 2), 16); - - for (i = 0; i < iLen; i+=4) { - - a = data[i+3]; - - if (a > 0){ - data[i] = cr; - data[i+1] = cg; - data[i+2] = cb; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - color: this.color - }; - } -}); - -fabric.Image.filters.Tint.fromObject = function(object) { - return new fabric.Image.filters.Tint(object); -}; -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - toFixed = fabric.util.toFixed; - - if (fabric.Text) { - fabric.warn('fabric.Text is already defined'); - return; - } - if (!fabric.Object) { - fabric.warn('fabric.Text requires fabric.Object'); - return; - } - - /** - * @class Text - * @extends fabric.Object - */ - fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ { - - /** - * @property - * @type Number - */ - fontSize: 40, - - /** - * @property - * @type Number - */ - fontWeight: 100, - - /** - * @property - * @type String - */ - fontFamily: 'Times New Roman', - - /** - * @property - * @type String - */ - textDecoration: '', - - /** - * @property - * @type String | null - */ - textShadow: '', - - /** - * Determines text alignment. Possible values: "left", "center", or "right". - * @property - * @type String - */ - textAlign: 'left', - - /** - * @property - * @type String - */ - fontStyle: '', - - /** - * @property - * @type Number - */ - lineHeight: 1.3, - - /** - * @property - * @type String - */ - strokeStyle: '', - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type String - */ - backgroundColor: '', - - - /** - * @property - * @type String | null - */ - path: null, - - /** - * @property - * @type String - */ - type: 'text', - - /** - * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) - * @property - * @type Boolean - */ - useNative: true, - - /** - * Constructor - * @method initialize - * @param {String} text - * @param {Object} [options] - * @return {fabric.Text} thisArg - */ - initialize: function(text, options) { - this._initStateProperties(); - this.text = text; - this.setOptions(options || { }); - this._theta = this.angle * Math.PI / 180; - this._initDimensions(); - this.setCoords(); - }, - - /** - * Renders text object on offscreen canvas, so that it would get dimensions - * @private - * @method _initDimensions - */ - _initDimensions: function() { - var canvasEl = fabric.document.createElement('canvas'); - - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - - this._render(canvasEl.getContext('2d')); - }, - - /** - * Creates `stateProperties` list on an instance, and adds `fabric.Text` -specific ones to it - * (such as "fontFamily", "fontWeight", etc.) - * @private - * @method _initStateProperties - */ - _initStateProperties: function() { - this.stateProperties = this.stateProperties.concat(); - this.stateProperties.push( - 'fontFamily', - 'fontWeight', - 'fontSize', - 'path', - 'text', - 'textDecoration', - 'textShadow', - 'textAlign', - 'fontStyle', - 'lineHeight', - 'strokeStyle', - 'strokeWidth', - 'backgroundColor', - 'useNative' - ); - fabric.util.removeFromArray(this.stateProperties, 'width'); - }, - - /** - * Returns string representation of an instance - * @method toString - * @return {String} String representation of text object - */ - toString: function() { - return '#'; - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - if (typeof Cufon === 'undefined' || this.useNative === true) { - this._renderViaNative(ctx); - } - else { - this._renderViaCufon(ctx); - } - }, - - /** - * @private - * @method _renderViaCufon - */ - _renderViaCufon: function(ctx) { - var o = Cufon.textOptions || (Cufon.textOptions = { }); - - // export options to be used by cufon.js - o.left = this.left; - o.top = this.top; - o.context = ctx; - o.color = this.fill; - - var el = this._initDummyElementForCufon(); - - // set "cursor" to top/left corner - this.transform(ctx); - - // draw text - Cufon.replaceElement(el, { - engine: 'canvas', - separate: 'none', - fontFamily: this.fontFamily, - fontWeight: this.fontWeight, - textDecoration: this.textDecoration, - textShadow: this.textShadow, - textAlign: this.textAlign, - fontStyle: this.fontStyle, - lineHeight: this.lineHeight, - strokeStyle: this.strokeStyle, - strokeWidth: this.strokeWidth, - backgroundColor: this.backgroundColor - }); - - // update width, height - this.width = o.width; - this.height = o.height; - - this._totalLineHeight = o.totalLineHeight; - this._fontAscent = o.fontAscent; - this._boundaries = o.boundaries; - this._shadowOffsets = o.shadowOffsets; - this._shadows = o.shadows || [ ]; - - el = null; - - // need to set coords _after_ the width/height was retreived from Cufon - this.setCoords(); - }, - - /** - * @private - * @method _render_native - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderViaNative: function(ctx) { - - this.transform(ctx); - this._setTextStyles(ctx); - - var textLines = this.text.split(/\r?\n/); - - this.width = this._getTextWidth(ctx, textLines); - this.height = this._getTextHeight(ctx, textLines); - - this._renderTextBackground(ctx, textLines); - - if (this.textAlign !== 'left') { - ctx.save(); - ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); - } - - this._setTextShadow(ctx); - this._renderTextFill(ctx, textLines); - this.textShadow && ctx.restore(); - - this._renderTextStroke(ctx, textLines); - if (this.textAlign !== 'left') { - ctx.restore(); - } - - this._renderTextDecoration(ctx, textLines); - this._setBoundaries(ctx, textLines); - this._totalLineHeight = 0; - - this.setCoords(); - }, - - /** - * @private - * @method _setBoundaries - */ - _setBoundaries: function(ctx, textLines) { - this._boundaries = [ ]; - - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = this._getLineLeftOffset(lineWidth); - - this._boundaries.push({ - height: this.fontSize, - width: lineWidth, - left: lineLeftOffset - }); - } - }, - - /** - * @private - * @method _setTextStyles - */ - _setTextStyles: function(ctx) { - ctx.fillStyle = this.fill; - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.strokeWidth; - ctx.textBaseline = 'top'; - ctx.textAlign = this.textAlign; - ctx.font = this._getFontDeclaration(); - }, - - /** - * @private - * @method _getTextHeight - */ - _getTextHeight: function(ctx, textLines) { - return this.fontSize * textLines.length * this.lineHeight; - }, - - /** - * @private - * @method _getTextWidth - */ - _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0]).width; - - for (var i = 1, len = textLines.length; i < len; i++) { - var currentLineWidth = ctx.measureText(textLines[i]).width; - if (currentLineWidth > maxWidth) { - maxWidth = currentLineWidth; - } - } - return maxWidth; - }, - - /** - * @private - * @method _setTextShadow - */ - _setTextShadow: function(ctx) { - if (this.textShadow) { - - // "rgba(0,0,0,0.2) 2px 2px 10px" - // "rgb(0, 100, 0) 0 0 5px" - // "red 2px 2px 1px" - // "#f55 123 345 567" - var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; - - var shadowDeclaration = this.textShadow; - var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); - var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); - - ctx.save(); - ctx.shadowColor = shadowColor; - ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); - ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); - ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); - - this._shadows = [{ - blur: ctx.shadowBlur, - color: ctx.shadowColor, - offX: ctx.shadowOffsetX, - offY: ctx.shadowOffsetY - }]; - - this._shadowOffsets = [[ - parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) - ]]; - } - }, - - _renderTextFill: function(ctx, textLines) { - this._boundaries = [ ]; - for (var i = 0, len = textLines.length; i < len; i++) { - ctx.fillText( - textLines[i], - -this.width / 2, - (-this.height / 2) + (i * this.fontSize * this.lineHeight) - ); - } - }, - - /** - * @private - * @method _renderTextStroke - */ - _renderTextStroke: function(ctx, textLines) { - if (this.strokeStyle) { - for (var i = 0, len = textLines.length; i < len; i++) { - ctx.strokeText( - textLines[i], - -this.width / 2, - (-this.height / 2) + (i * this.fontSize * this.lineHeight) - ); - } - } - }, - - /** - * @private - * @_renderTextBackground - */ - _renderTextBackground: function(ctx, textLines) { - if (this.backgroundColor) { - ctx.save(); - ctx.fillStyle = this.backgroundColor; - - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = this._getLineLeftOffset(lineWidth); - - ctx.fillRect( - (-this.width / 2) + lineLeftOffset, - (-this.height / 2) + (i * this.fontSize * this.lineHeight), - lineWidth, - this.fontSize - ); - } - ctx.restore(); - } - }, - - /** - * @private - * @method _getLineLeftOffset - */ - _getLineLeftOffset: function(lineWidth) { - if (this.textAlign === 'center') { - return (this.width - lineWidth) / 2; - } - if (this.textAlign === 'right') { - return this.width - lineWidth; - } - return 0; - }, - - /** - * @private - * @method _renderTextDecoration - */ - _renderTextDecoration: function(ctx, textLines) { - - var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; - var _this = this; - - function renderLinesAtOffset(offset) { - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = _this._getLineLeftOffset(lineWidth); - - ctx.fillRect( - (-_this.width / 2) + lineLeftOffset, - (offset + (i * _this.fontSize * _this.lineHeight)) - halfOfVerticalBox, - lineWidth, - 1); - } - } - - if (this.textDecoration.indexOf('underline') > -1) { - renderLinesAtOffset(this.fontSize); - } - if (this.textDecoration.indexOf('line-through') > -1) { - renderLinesAtOffset(this.fontSize / 2); - } - if (this.textDecoration.indexOf('overline') > -1) { - renderLinesAtOffset(0); - } - }, - - /** - * @private - * @method _getFontDeclaration - */ - _getFontDeclaration: function() { - return [ - this.fontStyle, - this.fontWeight, - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) - ].join(' '); - }, - - /** - * @private - * @method _initDummyElement - */ - _initDummyElementForCufon: function() { - var el = fabric.document.createElement('pre'), - container = fabric.document.createElement('div'); - - // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent - container.appendChild(el); - - if (typeof G_vmlCanvasManager === 'undefined') { - el.innerHTML = this.text; - } - else { - // IE 7 & 8 drop newlines and white space on text nodes - // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html - // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp - el.innerText = this.text.replace(/\r?\n/gi, '\r'); - } - - el.style.fontSize = this.fontSize + 'px'; - el.style.letterSpacing = 'normal'; - - return el; - }, - - /** - * Renders text instance on a specified context - * @method render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - render: function(ctx, noTransform) { - ctx.save(); - this._render(ctx); - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} Object representation of text object - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - text: this.text, - fontSize: this.fontSize, - fontWeight: this.fontWeight, - fontFamily: this.fontFamily, - fontStyle: this.fontStyle, - lineHeight: this.lineHeight, - textDecoration: this.textDecoration, - textShadow: this.textShadow, - textAlign: this.textAlign, - path: this.path, - strokeStyle: this.strokeStyle, - strokeWidth: this.strokeWidth, - backgroundColor: this.backgroundColor, - useNative: this.useNative - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - - var textLines = this.text.split(/\r?\n/), - lineTopOffset = this.useNative - ? this.fontSize * this.lineHeight - : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - - textLeftOffset = -(this.width/2), - textTopOffset = this.useNative - ? this.fontSize - 1 - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, - - textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), - shadowSpans = this._getSVGShadows(lineTopOffset, textLines); - - // move top offset by an ascent - textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); - - return [ - '', - textAndBg.textBgRects.join(''), - '', - shadowSpans.join(''), - textAndBg.textSpans.join(''), - '', - '' - ].join(''); - }, - - _getSVGShadows: function(lineTopOffset, textLines) { - var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; - - if (!this._shadows || !this._boundaries) { - return shadowSpans; - } - - for (j = 0, jlen = this._shadows.length; j < jlen; j++) { - for (i = 0, ilen = textLines.length; i < ilen; i++) { - if (textLines[i] !== '') { - var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; - shadowSpans.push( - '', - fabric.util.string.escapeXml(textLines[i]), - ''); - lineTopOffsetMultiplier = 1; - } else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - } - } - return shadowSpans; - }, - - _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { - var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; - - // text and background - for (i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; - textSpans.push( - ' elements since setting opacity on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLines[i]), - '' - ); - lineTopOffsetMultiplier = 1; - } else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - - if (!this.backgroundColor || !this._boundaries) continue; - - textBgRects.push( - ''); - } - return { - textSpans: textSpans, - textBgRects: textBgRects - }; - }, - - // Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values - // we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 - _getFillAttributes: function(value) { - var fillColor = value ? new fabric.Color(value) : ''; - if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { - return 'fill="' + value + '"'; - } - return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; - }, - - /** - * Sets "color" of an instance (alias of `set('fill', …)`) - * @method setColor - * @param {String} value - * @return {fabric.Text} thisArg - * @chainable - */ - setColor: function(value) { - this.set('fill', value); - return this; - }, - - /** - * Sets fontSize of an instance and updates its coordinates - * @method setFontsize - * @param {Number} value - * @return {fabric.Text} thisArg - * @chainable - */ - setFontsize: function(value) { - this.set('fontSize', value); - this._initDimensions(); - this.setCoords(); - return this; - }, - - /** - * Returns actual text value of an instance - * @method getText - * @return {String} - */ - getText: function() { - return this.text; - }, - - /** - * Sets text of an instance, and updates its coordinates - * @method setText - * @param {String} value - * @return {fabric.Text} thisArg - * @chainable - */ - setText: function(value) { - this.set('text', value); - this._initDimensions(); - this.setCoords(); - return this; - }, - - /** - * Sets specified property to a specified value - * @method set - * @param {String} name - * @param {Any} value - * @return {fabric.Text} thisArg - * @chainable - */ - _set: function(name, value) { - if (name === 'fontFamily' && this.path) { - this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); - } - this.callSuper('_set', name, value); - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Text.fromElement`) - * @static - */ - fabric.Text.ATTRIBUTE_NAMES = - ('x y fill fill-opacity opacity stroke stroke-width transform ' + - 'font-family font-style font-weight font-size text-decoration').split(' '); - - /** - * Returns fabric.Text instance from an object representation - * @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)); - }; - - /** - * Returns fabric.Text instance from an SVG element (not yet implemented) - * @static - * @method fabric.Text.fromElement - * @param element - * @param options - * @return {fabric.Text} an instance - */ - fabric.Text.fromElement = function(element, options) { - if (!element) { - return null; - } - - var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); - options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); - var text = new fabric.Text(element.textContent, options); - - return text; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function() { - - if (typeof document !== 'undefined' && typeof window !== 'undefined') { - return; - } - - var DOMParser = new require('xmldom').DOMParser, - URL = require('url'), - HTTP = require('http'), - - Canvas = require('canvas'), - Image = require('canvas').Image; - - function request(url, encoding, callback) { - var oURL = URL.parse(url), - client = HTTP.createClient(oURL.port, oURL.hostname), - req = client.request('GET', oURL.pathname, { 'host': oURL.hostname }); - - client.addListener('error', function(err) { - if (err.errno === process.ECONNREFUSED) { - fabric.log('ECONNREFUSED: connection refused to ' + client.host + ':' + client.port); - } - else { - fabric.log(err.message); - } - }); - - req.end(); - req.on('response', function (response) { - var body = ""; - if (encoding) { - response.setEncoding(encoding); - } - response.on('end', function () { - callback(body); - }); - response.on('data', function (chunk) { - if (response.statusCode === 200) { - body += chunk; - } - }); - }); - } - - fabric.util.loadImage = function(url, callback) { - request(url, 'binary', function(body) { - var img = new Image(); - img.src = new Buffer(body, 'binary'); - // preserving original url, which seems to be lost in node-canvas - img._src = url; - callback(img); - }); - }; - - fabric.loadSVGFromURL = function(url, callback) { - url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); - request(url, '', function(body) { - fabric.loadSVGFromString(body, callback); - }); - }; - - fabric.loadSVGFromString = function(string, callback) { - var doc = new DOMParser().parseFromString(string); - fabric.parseSVGDocument(doc.documentElement, function(results, options) { - callback(results, options); - }); - }; - - fabric.util.getScript = function(url, callback) { - request(url, '', function(body) { - eval(body); - callback && callback(); - }); - }; - - fabric.Image.fromObject = function(object, callback) { - fabric.util.loadImage(object.src, function(img) { - var oImg = new fabric.Image(img); - - oImg._initConfig(object); - oImg._initFilters(object); - callback(oImg); - }); - }; - - /** - * Only available when running fabric on node.js - * @method createCanvasForNode - * @param width Canvas width - * @param height Canvas height - * @return {Object} wrapped canvas instance - */ - fabric.createCanvasForNode = function(width, height) { - - var canvasEl = fabric.document.createElement('canvas'), - nodeCanvas = new Canvas(width || 600, height || 600); - - // jsdom doesn't create style on canvas element, so here be temp. workaround - canvasEl.style = { }; - - canvasEl.width = nodeCanvas.width; - canvasEl.height = nodeCanvas.height; - - var FabricCanvas = fabric.Canvas || fabric.StaticCanvas; - var fabricCanvas = new FabricCanvas(canvasEl); - fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); - fabricCanvas.nodeCanvas = nodeCanvas; - - return fabricCanvas; - }; - - fabric.StaticCanvas.prototype.createPNGStream = function() { - return this.nodeCanvas.createPNGStream(); - }; - - var origSetWidth = fabric.StaticCanvas.prototype.setWidth; - fabric.StaticCanvas.prototype.setWidth = function(width) { - origSetWidth.call(this); - this.nodeCanvas.width = width; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; - } - - var origSetHeight = fabric.StaticCanvas.prototype.setHeight; - fabric.StaticCanvas.prototype.setHeight = function(height) { - origSetHeight.call(this); - this.nodeCanvas.height = height; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; - } - -})(); +/* build: `node build.js modules=ALL` */ +/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: "0.9.14" }; + +if (typeof exports != 'undefined') { + exports.fabric = fabric; +} + +if (typeof document != 'undefined' && typeof window != 'undefined') { + fabric.document = document; + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + fabric.document = require("jsdom").jsdom(""); + fabric.window = fabric.document.createWindow(); +} + +/** + * True when in environment that supports touch events + * @property isTouchSupported + * @type boolean + */ +fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; + +/** + * True when in environment that's probably Node.js + * @property isLikelyNode + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +/*! + * Copyright (c) 2009 Simo Kinnunen. + * Licensed under the MIT license. + */ + +var Cufon = (function() { + + var api = function() { + return api.replace.apply(null, arguments); + }; + + var DOM = api.DOM = { + + ready: (function() { + + var complete = false, readyStatus = { loaded: 1, complete: 1 }; + + var queue = [], perform = function() { + if (complete) return; + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Gecko, Opera, WebKit r26101+ + + if (fabric.document.addEventListener) { + fabric.document.addEventListener('DOMContentLoaded', perform, false); + fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages + } + + // Old WebKit, Internet Explorer + + if (!fabric.window.opera && fabric.document.readyState) (function() { + readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); + })(); + + // Internet Explorer + + if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { + try { + fabric.document.body.doScroll('left'); + perform(); + } + catch (e) { + setTimeout(arguments.callee, 1); + } + })(); + + addEvent(fabric.window, 'load', perform); // Fallback + + return function(listener) { + if (!arguments.length) perform(); + else complete ? listener() : queue.push(listener); + }; + + })() + + }; + + var CSS = api.CSS = { + + Size: function(value, base) { + + this.value = parseFloat(value); + this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; + + this.convert = function(value) { + return value / base * this.value; + }; + + this.convertFrom = function(value) { + return value / this.value * base; + }; + + this.toString = function() { + return this.value + this.unit; + }; + + }, + + getStyle: function(el) { + return new Style(el.style); + /* + var view = document.defaultView; + if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); + if (el.currentStyle) return new Style(el.currentStyle); + return new Style(el.style); + */ + }, + + quotedList: cached(function(value) { + // doesn't work properly with empty quoted strings (""), but + // it's not worth the extra code. + var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; + while (match = re.exec(value)) list.push(match[3] || match[1]); + return list; + }), + + ready: (function() { + + var complete = false; + + var queue = [], perform = function() { + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Safari 2 does not include '); + + function getFontSizeInPixels(el, value) { + return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); + } + + // Original by Dead Edwards. + // Combined with getFontSizeInPixels it also works with relative units. + function getSizeInPixels(el, value) { + if (/px$/i.test(value)) return parseFloat(value); + var style = el.style.left, runtimeStyle = el.runtimeStyle.left; + el.runtimeStyle.left = el.currentStyle.left; + el.style.left = value; + var result = el.style.pixelLeft; + el.style.left = style; + el.runtimeStyle.left = runtimeStyle; + return result; + } + + return function(font, text, style, options, node, el, hasNext) { + var redraw = (text === null); + + if (redraw) text = node.alt; + + // @todo word-spacing, text-decoration + + var viewBox = font.viewBox; + + var size = style.computedFontSize || + (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); + + var letterSpacing = style.computedLSpacing; + + if (letterSpacing == undefined) { + letterSpacing = style.get('letterSpacing'); + style.computedLSpacing = letterSpacing = + (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); + } + + var wrapper, canvas; + + if (redraw) { + wrapper = node; + canvas = node.firstChild; + } + else { + wrapper = fabric.document.createElement('span'); + wrapper.className = 'cufon cufon-vml'; + wrapper.alt = text; + + canvas = fabric.document.createElement('span'); + canvas.className = 'cufon-vml-canvas'; + wrapper.appendChild(canvas); + + if (options.printable) { + var print = fabric.document.createElement('span'); + print.className = 'cufon-alt'; + print.appendChild(fabric.document.createTextNode(text)); + wrapper.appendChild(print); + } + + // ie6, for some reason, has trouble rendering the last VML element in the document. + // we can work around this by injecting a dummy element where needed. + // @todo find a better solution + if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); + } + + var wStyle = wrapper.style; + var cStyle = canvas.style; + + var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); + var roundingFactor = roundedHeight / height; + var minX = viewBox.minX, minY = viewBox.minY; + + cStyle.height = roundedHeight; + cStyle.top = Math.round(size.convert(minY - font.ascent)); + cStyle.left = Math.round(size.convert(minX)); + + wStyle.height = size.convert(font.height) + 'px'; + + var textDecoration = Cufon.getTextDecoration(options); + + var color = style.get('color'); + + var chars = Cufon.CSS.textTransform(text, style).split(''); + + var width = 0, offsetX = 0, advance = null; + + var glyph, shape, shadows = options.textShadow; + + // pre-calculate width + for (var i = 0, k = 0, l = chars.length; i < l; ++i) { + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; + } + + if (advance === null) return null; + + var fullWidth = -minX + width + (viewBox.width - advance); + + var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); + + var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; + var stretch = 'r' + coordSize + 'nsnf'; + + for (i = 0; i < l; ++i) { + + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (!glyph) continue; + + if (redraw) { + // some glyphs may be missing so we can't use i + shape = canvas.childNodes[k]; + if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow + } + else { + shape = fabric.document.createElement('cvml:shape'); + canvas.appendChild(shape); + } + + shape.stroked = 'f'; + shape.coordsize = coordSize; + shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; + shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; + shape.fillcolor = color; + + // it's important to not set top/left or IE8 will grind to a halt + var sStyle = shape.style; + sStyle.width = roundedShapeWidth; + sStyle.height = roundedHeight; + + if (shadows) { + // due to the limitations of the VML shadow element there + // can only be two visible shadows. opacity is shared + // for all shadows. + var shadow1 = shadows[0], shadow2 = shadows[1]; + var color1 = Cufon.CSS.color(shadow1.color), color2; + var shadow = fabric.document.createElement('cvml:shadow'); + shadow.on = 't'; + shadow.color = color1.color; + shadow.offset = shadow1.offX + ',' + shadow1.offY; + if (shadow2) { + color2 = Cufon.CSS.color(shadow2.color); + shadow.type = 'double'; + shadow.color2 = color2.color; + shadow.offset2 = shadow2.offX + ',' + shadow2.offY; + } + shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; + shape.appendChild(shadow); + } + + offsetX += ~~(glyph.w || font.w) + letterSpacing; + + ++k; + + } + + wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); + + return wrapper; + + }; + +})()); + +Cufon.getTextDecoration = function(options) { + return { + underline: options.textDecoration === 'underline', + overline: options.textDecoration === 'overline', + 'line-through': options.textDecoration === 'line-through' + }; +}; + +if (typeof exports != 'undefined') { + exports.Cufon = Cufon; +} + +/* + json2.js + 2011-10-19 + + 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) { + // Format integers to have at least two digits. + 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 is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + 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: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + 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, regexp: true */ + +/*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 +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +var JSON; +if (!JSON) { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + 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) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + 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) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + 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); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + 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, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); +/** + * Wrapper around `console.log` (when available) + * @method log + * @param {Any} Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @method warn + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; + +if (typeof console !== 'undefined') { + if (typeof console.log !== 'undefined' && console.log.apply) { + fabric.log = function() { + return console.log.apply(console, arguments); + }; + } + if (typeof console.warn !== 'undefined' && console.warn.apply) { + fabric.warn = function() { + return console.warn.apply(console, arguments); + }; + } +} + +/** + * @namespace + */ +fabric.Observable = { + + /** + * Observes specified event + * @method observe + * @depracated Since 0.8.34. Use `on` instead. + * @param {String} eventName + * @param {Function} handler + */ + observe: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = [ ]; + } + this.__eventListeners[eventName].push(handler); + } + }, + + /** + * Stops event observing for a particular event handler + * @method stopObserving + * @depracated Since 0.8.34. Use `off` instead. + * @param {String} eventName + * @param {Function} handler + */ + stopObserving: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + if (this.__eventListeners[eventName]) { + fabric.util.removeFromArray(this.__eventListeners[eventName], handler); + } + }, + + /** + * Fires event with an optional options object + * @method fire + * @param {String} eventName + * @param {Object} [options] + */ + fire: function(eventName, options) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) return; + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + // avoiding try/catch for perf. reasons + listenersForEvent[i](options || { }); + } + } +}; + +/** + * Alias for observe + * @method observe + * @memberOf fabric.Observable + */ +fabric.Observable.on = fabric.Observable.observe; + +/** + * Alias for stopObserving + * @method off + */ +fabric.Observable.off = fabric.Observable.stopObserving; +(function() { + + /** + * @namespace + */ + fabric.util = { }; + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @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; + } + + /** + * Returns random number between 2 specified ones. + * @static + * @method getRandomInt + * @memberOf fabric.util + * @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; + } + + var PiBy180 = Math.PI / 180; + + /** + * Transforms degrees to radians. + * @static + * @method degreesToRadians + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + function degreesToRadians(degrees) { + return degrees * PiBy180; + } + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @method toFixed + * @memberOf fabric.util + * @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 + * @memberOf fabric.util + * @return {Boolean} + */ + function falseFunction() { + return false; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @method animate + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + options || (options = { }); + + var start = +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick() { + time = +new Date(); + var currentTime = time > finish ? duration : (time - start); + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish || abort()) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(); + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + fabric.window.setTimeout(callback, 1000 / 60); + }; + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * @method requestAnimFrame + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + var requestAnimFrame = function() { + return _requestAnimFrame.apply(fabric.window, arguments); + }; + + /** + * Loads image element from given url and passes it to a callback + * @method loadImage + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} context optional Context to invoke callback in + */ + function loadImage(url, callback, context) { + if (url) { + var img = new Image(); + /** @ignore */ + img.onload = function () { + callback && callback.call(context, img); + img = img.onload = null; + }; + img.src = url; + } + else { + callback && callback.call(context, url); + } + } + + function enlivenObjects(objects, callback) { + + function getKlass(type) { + return fabric[fabric.util.string.camelize(fabric.util.string.capitalize(type))]; + } + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + if (callback) { + callback(enlivenedObjects); + } + } + } + + var enlivenedObjects = [ ], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + objects.forEach(function (o, index) { + if (!o.type) { + return; + } + var klass = getKlass(o.type); + if (klass.async) { + klass.fromObject(o, function (o) { + enlivenedObjects[index] = o; + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + onLoaded(); + } + }); + } + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @method groupSVGElements + * @param {Array} elements + * @param {Object} options optional + * @return {String} path optional + */ + function groupSVGElements(elements, options, path) { + var object = elements.length > 1 + ? new fabric.PathGroup(elements, options) + : elements[0]; + + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + } + + fabric.util.removeFromArray = removeFromArray; + fabric.util.degreesToRadians = degreesToRadians; + fabric.util.toFixed = toFixed; + fabric.util.getRandomInt = getRandomInt; + fabric.util.falseFunction = falseFunction; + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.loadImage = loadImage; + fabric.util.enlivenObjects = enlivenObjects; + fabric.util.groupSVGElements = groupSVGElements; +})(); +(function() { + + var slice = Array.prototype.slice; + + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + var t = Object(this), len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + 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 array contains no values, no initial value to return + 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; + }; + } + + /** + * Invokes method on all items in a given array + * @method invoke + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + */ + 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; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @method max + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function max(array, byProperty) { + if (!array || array.length === 0) return undefined; + + 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; + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @method min + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function min(array, byProperty) { + if (!array || array.length === 0) return undefined; + + 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; + } + + /** @namespace */ + fabric.util.array = { + invoke: invoke, + min: min, + max: max + }; + +})(); +(function(){ + + /** + * Copies all enumerable properties of one object to another + * @memberOf fabric.util.object + * @method extend + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + */ + function extend(destination, source) { + // JScript DontEnum bug is not taken care of + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * @method clone + * @memberOf fabric.util.object + * @param {Object} object Object to clone + */ + function clone(object) { + return extend({ }, object); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + +})(); +(function() { + +if (!String.prototype.trim) { + /** + * Trims a string (removing whitespace from the beginning and the end) + * @method trim + * @see String#trim on MDN + */ + String.prototype.trim = function () { + // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now + return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); + }; +} + +/** + * Camelizes a string + * @memberOf fabric.util.string + * @method camelize + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ +function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); +} + +/** + * Capitalizes a string + * @memberOf fabric.util.string + * @method capitalize + * @param {String} string String to capitalize + * @return {String} Capitalized version of a string + */ +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +} + +function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + +/** @namespace */ +fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml +}; +}()); + +(function() { + + var slice = Array.prototype.slice, + apply = Function.prototype.apply, + Dummy = function() { }; + + if (!Function.prototype.bind) { + /** + * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) + * @see Function#bind on MDN + * @param {Object} thisArg Object to bind function to + * @param {Any[]} [...] Values to pass to a bound function + * @return {Function} + */ + Function.prototype.bind = function(thisArg) { + var fn = this, args = slice.call(arguments, 1), bound; + if (args.length) { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); + }; + } + Dummy.prototype = this.prototype; + bound.prototype = new Dummy(); + + return bound; + }; + } + +})(); +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }; + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + + /** @ignore */ + var addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && typeof klass.prototype[property] === 'function') { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + /** + * Helper for creation of "classes" + * @method createClass + * @memberOf fabric.util + */ + 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], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + return klass; + } + + fabric.util.createClass = createClass; +})(); +(function () { + + /* 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 fabric.document.documentElement.uniqueID !== 'undefined') { + return function (element) { + return element.uniqueID; + }; + } + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + /** @ignore */ + var getElement, setElement; + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + 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 || fabric.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 || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + + // DOM L0 branch + handlers = { }, + + addListener, removeListener; + + if (shouldUseAddListenerRemoveListener) { + /** @ignore */ + addListener = function (element, eventName, handler) { + element.addEventListener(eventName, handler, false); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + element.removeEventListener(eventName, handler, false); + }; + } + + else if (shouldUseAttachEventDetachEvent) { + /** @ignore */ + 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); + }; + /** @ignore */ + 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 { + /** @ignore */ + 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); + }; + /** @ignore */ + 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); + } + } + } + }; + } + + /** + * Adds an event listener to an element + * @mthod addListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = addListener; + + /** + * Removes an event listener from an element + * @mthod removeListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = removeListener; + + /** + * Cross-browser wrapper for getting event's coordinates + * @method getPointer + * @memberOf fabric.util + * @param {Event} event + */ + function getPointer(event) { + // TODO (kangax): this method needs fixing + return { x: pointerX(event), y: pointerY(event) }; + } + + var pointerX = function(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollLeft: 0 }; + + // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) + // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] + // need to investigate later + return event.pageX || ((typeof event.clientX !== 'unknown' ? event.clientX : 0) + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)); + }; + + var pointerY = function(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollTop: 0 }; + + return event.pageY || ((typeof event.clientY !== 'unknown' ? event.clientY : 0) + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)); + }; + + if (fabric.isTouchSupported) { + pointerX = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX; + }; + pointerY = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY; + }; + } + + fabric.util.getPointer = getPointer; + + fabric.util.object.extend(fabric.util, fabric.Observable); + +})(); +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @method setStyle + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + 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 = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + 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() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @method getById + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @method toArray + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + var toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + var sliceCanConvertNodelists; + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch(err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @method makeElement + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.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; + } + + /** + * Adds class to an element + * @method addClass + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @method wrapElement + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + 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; + } + + /** + * Returns offset for a given element + * @method getElementOffset + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + // TODO (kangax): need to fix this method + 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 = fabric.document.documentElement.style; + + var selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @method makeElementUnselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + 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; + } + + /** + * Makes element selectable + * @method makeElementSelectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + (function() { + + /** + * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading + * @method getScript + * @memberOf fabric.util + * @param {String} url URL of a script to load + * @param {Function} callback Callback to execute when script is finished loading + */ + function getScript(url, callback) { + var headEl = fabric.document.getElementsByTagName("head")[0], + scriptEl = fabric.document.createElement('script'), + loading = true; + + scriptEl.type = 'text/javascript'; + scriptEl.setAttribute('runat', 'server'); + + /** @ignore */ + scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { + if (loading) { + if (typeof this.readyState === 'string' && + this.readyState !== 'loaded' && + this.readyState !== 'complete') return; + loading = false; + callback(e || fabric.window.event); + scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; + } + }; + scriptEl.src = url; + headEl.appendChild(scriptEl); + // causes issue in Opera + // headEl.removeChild(scriptEl); + } + + fabric.util.getScript = getScript; + })(); + + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.makeElement = makeElement; + fabric.util.addClass = addClass; + fabric.util.wrapElement = wrapElement; + fabric.util.getElementOffset = getElementOffset; + +})(); +(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() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @method request + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = makeXHR(), + body; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); +(function() { + + /** + * @method easeInQuad + * @memberOf fabric.util.ease + */ + function easeInQuad(t, b, c, d) { + return c*(t/=d)*t + b; + } + + /** + * @method easeOutQuad + * @memberOf fabric.util.ease + */ + function easeOutQuad(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } + + /** + * @method easeInOutQuad + * @memberOf fabric.util.ease + */ + function easeInOutQuad(t, b, c, d) { + t /= (d/2); + if (t < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + } + + /** + * @method easeInCubic + * @memberOf fabric.util.ease + */ + function easeInCubic(t, b, c, d) { + return c*(t/=d)*t*t + b; + } + + /** + * @method easeOutCubic + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c*((t=t/d-1)*t*t + 1) + b; + } + + /** + * @method easeInOutCubic + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t + b; + return c/2*((t-=2)*t*t + 2) + b; + } + + /** + * @method easeInQuart + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c*(t/=d)*t*t*t + b; + } + + /** + * @method easeOutQuart + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t=t/d-1)*t*t*t - 1) + b; + } + + /** + * @method easeInOutQuart + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t*t + b; + return -c/2 * ((t-=2)*t*t*t - 2) + b; + } + + /** + * @method easeInQuint + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c*(t/=d)*t*t*t*t + b; + } + + /** + * @method easeOutQuint + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c*((t=t/d-1)*t*t*t*t + 1) + b; + } + + /** + * @method easeInOutQuint + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t*t*t + b; + return c/2*((t-=2)*t*t*t*t + 2) + b; + } + + /** + * @method easeInSine + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t/d * (Math.PI/2)) + c + b; + } + + /** + * @method easeOutSine + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t/d * (Math.PI/2)) + b; + } + + /** + * @method easeInOutSine + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; + } + + /** + * @method easeInExpo + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t===0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + } + + /** + * @method easeOutExpo + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + } + + /** + * @method easeInOutExpo + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t===0) return b; + if (t===d) return b+c; + t /= d/2; + if (t < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * @method easeInCirc + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; + } + + /** + * @method easeOutCirc + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t=t/d-1)*t) + b; + } + + /** + * @method easeInOutCirc + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d/2; + if (t < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; + return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; + } + + /** + * @method easeInElastic + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + } + + /** + * @method easeOutElastic + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + } + + /** + * @method easeInOutElastic + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d/2; + if (t===2) return b+c; + if (!p) p=d*(0.3*1.5); + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; + } + + /** + * @method easeInBack + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + return c*(t/=d)*t*((s+1)*t - s) + b; + } + + /** + * @method easeOutBack + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; + } + + /** + * @method easeInOutBack + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + t /= d/2; + if (t < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; + return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; + } + + /** + * @method easeInBounce + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d-t, 0, c, d) + b; + } + + /** + * @method easeOutBounce + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t/=d) < (1/2.75)) { + return c*(7.5625*t*t) + b; + } else if (t < (2/2.75)) { + return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; + } else if (t < (2.5/2.75)) { + return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; + } else { + return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; + } + } + + /** + * @method easeInOutBounce + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d/2) return easeInBounce (t*2, 0, c, d) * 0.5 + b; + return easeOutBounce (t*2-d, 0, c, d) * 0.5 + c*0.5 + b; + } + + /** @namespace fabric.util.ease */ + fabric.util.ease = { + easeInQuad: easeInQuad, + easeOutQuad: easeOutQuad, + easeInOutQuad: easeInOutQuad, + easeInCubic: easeInCubic, + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +}()); +(function(global) { + + "use strict"; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.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', + 'text-decoration': 'textDecoration', + 'font-size': 'fontSize', + 'font-weight': 'fontWeight', + 'font-style': 'fontStyle', + 'font-family': 'fontFamily' + }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @method parseAttributes + * @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 there's a parent container (`g` node), parse its attributes recursively upwards + 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) { + // "normalize" attribute values + 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); + } + attr = normalizeAttr(attr); + memo[attr] = isNaN(parsed) ? value : parsed; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + + ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + return extend(parentAttributes, ownAttributes); + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @method 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]; + } + } + + // identity matrix + var iMatrix = [ + 1, // a + 0, // b + 0, // c + 1, // d + 0, // e + 0 // f + ], + + // == begin transform regexp + 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*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transform_list), + // == end transform regexp + + reTransform = new RegExp(transform); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(); + + // return if no argument was given or + // an argument does not match transform attribute regexp + 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; + }; + })(); + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @method parsePointsAttribute + * @param points {String} points attribute string + * @return {Array} array of points + */ + function parsePointsAttribute(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + } + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @method parseStyleAttribute + * @param {SVGElement} element 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) return oStyle; + + if (typeof style === 'string') { + style = style.replace(/;$/, '').split(';').forEach(function (current) { + var attr = current.split(':'); + oStyle[normalizeAttr(attr[0].trim().toLowerCase())] = attr[1].trim(); + }); + } + else { + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; + oStyle[normalizeAttr(prop.toLowerCase())] = style[prop]; + } + } + + return oStyle; + } + + function resolveGradients(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); + + if (/^url\(/.test(instanceFillValue)) { + + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); + + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + } + } + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @method parseElements + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} options Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function parseElements(elements, callback, options, reviver) { + var instances = new Array(elements.length), i = elements.length; + + function checkIfDone() { + if (--i === 0) { + instances = instances.filter(function(el) { + return el != null; + }); + resolveGradients(instances); + callback(instances); + } + } + + for (var index = 0, el, len = elements.length; index < len; index++) { + el = elements[index]; + var klass = fabric[capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + if (klass.async) { + klass.fromElement(el, (function(index, el) { + return function(obj) { + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + }; + })(index), options); + } + else { + var obj = klass.fromElement(el, options); + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + } + } + catch(e) { + fabric.log(e.message || e); + } + } + else { + checkIfDone(); + } + } + } + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @method getCSSRules + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + function getCSSRules(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + } + + function getGlobalStylesForElement(element) { + var nodeName = element.nodeName, + className = element.getAttribute('class'), + id = element.getAttribute('id'), + styles = { }; + + for (var rule in fabric.cssRules) { + var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || + (id && new RegExp('^#' + id).test(rule)) || + (new RegExp('^' + nodeName).test(rule)); + + if (ruleMatchesElement) { + for (var property in fabric.cssRules[rule]) { + styles[property] = fabric.cssRules[rule][property]; + } + } + } + + return styles; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @method parseSVGDocument + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + fabric.parseSVGDocument = (function() { + + var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // \d doesn't quite cut it (as we need to match an actual float number) + + // matches, e.g.: +14.56e-12, etc. + var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; + + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*' + + '$' + ); + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (nodeName.test(element.nodeName)) { + return true; + } + } + return false; + } + + return function(doc, callback, reviver) { + if (!doc) return; + + var startTime = new Date(), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + + if (descendants.length === 0) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes("//*[name(.)!='svg']"); + var arr = [ ]; + for (var i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + return reAllowedSVGTagNames.test(el.tagName) && + !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + 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); + } + + // values of width/height attributes overwrite those extracted from viewbox attribute + width = widthAttr ? parseFloat(widthAttr) : width; + height = heightAttr ? parseFloat(heightAttr) : height; + + var options = { + width: width, + height: height + }; + + fabric.gradientDefs = fabric.getGradientDefs(doc); + fabric.cssRules = getCSSRules(doc); + + // Precedence of rules: style > class > attribute + + fabric.parseElements(elements, function(instances) { + fabric.documentParsingTime = new Date() - startTime; + if (callback) { + callback(instances, options); + } + }, clone(options), reviver); + }; + })(); + + /** + * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) + * @property + * @namespace + */ + var svgCache = { + + /** + * @method has + * @param {String} name + * @param {Function} callback + */ + has: function (name, callback) { + callback(false); + }, + + /** + * @method get + * @param {String} url + * @param {Function} callback + */ + get: function () { + /* NOOP */ + }, + + /** + * @method set + * @param {String} url + * @param {Object} object + */ + set: function () { + /* NOOP */ + } + }; + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects + * @method loadSVGFromURL + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromURL(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + } + + /** + * @method _enlivenCachedObject + */ + function _enlivenCachedObject(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 }); + } + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @method loadSVGFromString + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromString(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + } + + function createSVGFontFacesMarkup(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '', + '', + '' + ].join(''); + } + + return markup; + } + + extend(fabric, { + + parseAttributes: parseAttributes, + parseElements: parseElements, + parseStyleAttribute: parseStyleAttribute, + parsePointsAttribute: parsePointsAttribute, + getCSSRules: getCSSRules, + + loadSVGFromURL: loadSVGFromURL, + loadSVGFromString: loadSVGFromString, + + createSVGFontFacesMarkup: createSVGFontFacesMarkup + }); + +})(typeof exports !== 'undefined' ? exports : this); + +(function() { + + function getColorStopFromStyle(el) { + var style = el.getAttribute('style'); + + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length-1] === '') { + keyValuePairs.pop(); + } + + for (var i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + return value; + } + } + } + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Gradient = fabric.util.createClass(/** @scope fabric.Gradient.prototype */ { + + initialize: function(options) { + + options || (options = { }); + + this.x1 = options.x1 || 0; + this.y1 = options.y1 || 0; + this.x2 = options.x2 || 0; + this.y2 = options.y2 || 0; + + this.colorStops = options.colorStops; + }, + + toObject: function() { + return { + x1: this.x1, + x2: this.x2, + y1: this.y1, + y2: this.y2, + colorStops: this.colorStops + }; + }, + + toLiveGradient: function(ctx) { + var gradient = ctx.createLinearGradient( + this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); + + for (var position in this.colorStops) { + var colorValue = this.colorStops[position]; + gradient.addColorStop(parseFloat(position), colorValue); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /** + * @method fromElement + * @static + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + */ + fromElement: function(el, instance) { + + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + */ + + var colorStopEls = el.getElementsByTagName('stop'), + offset, + colorStops = { }, + coords = { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + + for (var i = colorStopEls.length; i--; ) { + el = colorStopEls[i]; + offset = el.getAttribute('offset'); + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); + } + + _convertPercentUnitsToValues(instance, coords); + + return new fabric.Gradient({ + x1: coords.x1, + y1: coords.y1, + x2: coords.x2, + y2: coords.y2, + colorStops: colorStops + }); + }, + + /** + * @method forObject + * @static + */ + forObject: function(obj, options) { + options || (options = { }); + _convertPercentUnitsToValues(obj, options); + return new fabric.Gradient(options); + } + }); + + function _convertPercentUnitsToValues(object, options) { + for (var prop in options) { + if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { + var percents = parseFloat(options[prop], 10); + if (prop === 'x1' || prop === 'x2') { + options[prop] = object.width * percents / 100; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = object.height * percents / 100; + } + } + // normalize rendering point (should be from top/left corner rather than center of the shape) + if (prop === 'x1' || prop === 'x2') { + options[prop] -= object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= object.height / 2; + } + } + } + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @method getGradientDefs + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + function getGradientDefs(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + } + + fabric.getGradientDefs = getGradientDefs; + +})(); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * @name Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + if (arguments.length > 0) { + this.init(x, y); + } + } + + Point.prototype = /** @scope fabric.Point.prototype */ { + + constructor: Point, + + /** + * @method init + * @param {Number} x + * @param {Number} y + */ + init: function (x, y) { + this.x = x; + this.y = y; + }, + + /** + * @method add + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * @method addEquals + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * @method scalarAdd + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * @method scalarAddEquals + * @param {Number} scalar + * @param {fabric.Point} thisArg + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * @method subtract + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * @method subtractEquals + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + 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; + } + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * @class Intersection + * @memberOf fabric + */ + function Intersection(status) { + if (arguments.length > 0) { + this.init(status); + } + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @scope fabric.Intersection.prototype */ { + + /** + * @method init + * @param {String} status + */ + init: function (status) { + this.status = status; + this.points = []; + }, + + /** + * @method appendPoint + * @param {String} status + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * @method appendPoints + * @param {String} status + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * @static + * @method intersectLineLine + */ + fabric.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; + }; + + /** + * @method intersectLinePolygon + */ + fabric.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; + }; + + /** + * @method intersectPolygonPolygon + */ + fabric.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; + }; + + /** + * @method intersectPolygonRectangle + */ + fabric.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; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class Color + * @memberOf fabric + * @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); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @scope fabric.Color.prototype */ { + + /** + * @private + * @method _tryParsingColor + */ + _tryParsingColor: function(color) { + var source = Color.sourceFromHex(color); + if (!source) { + source = Color.sourceFromRgb(color); + } + if (source) { + this.setSource(source); + } + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method getSource + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method setSource + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color represenation in RGB format + * @method toRgb + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color represenation in RGBA format + * @method toRgba + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color represenation in HEX format + * @method toHex + * @return {String} ex: FF5555 + */ + 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(); + }, + + /** + * Gets value of alpha channel for this color + * @method getAlpha + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @method setAlpha + * @param {Number} 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @method toGrayscale + * @return {fabric.Color} thisArg + */ + 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 {fabric.Color} thisArg + */ + 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 {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + 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[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgb(255, 100, 10, 0.5), rgb(1,1,1)) + * @static + * @field + */ + fabric.Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/; + + /** + * Regex matching color in HEX format (ex: #FF5555, 010155, aff) + * @static + * @field + */ + fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; + + /** + * Returns new color object, when given a color in RGB format + * @method fromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @method sourceFromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {Array} source + */ + fabric.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 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @method fromRgba + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @method fromHex + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @method sourceFromHex + * @param {String} color ex: FF5555 + * @return {Array} source + */ + fabric.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 + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @method fromSource + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function () { + + "use strict"; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + removeListener = fabric.util.removeListener, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * @class fabric.StaticCanvas + * @constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.StaticCanvas = function (el, options) { + options || (options = { }); + + this._initStatic(el, options); + fabric.StaticCanvas.activeInstance = this; + }; + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + + extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ { + + /** + * Background color of canvas instance + * @property + * @type String + */ + backgroundColor: 'rgba(0, 0, 0, 0)', + + /** + * Background image of canvas instance + * Should be set via `setBackgroundImage` + * @property + * @type String + */ + backgroundImage: '', + + /** + * Opacity of the background image of the canvas instance + * @property + * @type Float + */ + backgroundImageOpacity: 1.0, + + /** + * Indicatus whether the background image should be stretched to fit the + * dimensions of the canvas instance. + * @property + * @type Boolean + */ + backgroundImageStretch: true, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * @property + * @type Boolean + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @property + * @type Boolean + */ + stateful: true, + + /** + * Indicates whether fabric.Canvas#add should also re-render canvas. + * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once + * (followed by a manual rendering after addition) + */ + renderOnAddition: true, + + /** + * Function that determines clipping of entire canvas area + * Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ + * @property + * @type Function + */ + clipTo: null, + + /** + * Indicates whether object controls (borders/corners) are rendered above overlay image + * @property + * @type Boolean + */ + controlsAboveOverlay: false, + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @method onBeforeScaleRotate + * @param {fabric.Object} target Object that's about to be scaled/rotated + */ + onBeforeScaleRotate: function () { + /* NOOP */ + }, + + /** + * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps + * @method onFpsUpdate + * @param {Number} fps + */ + onFpsUpdate: null, + + _initStatic: function(el, options) { + this._objects = []; + + this._createLowerCanvas(el); + this._initOptions(options); + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); + } + this.calcOffset(); + }, + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @method calcOffset + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets overlay image for this canvas + * @method setOverlayImage + * @param {String} url url of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @return {fabric.Canvas} thisArg + * @chainable + */ + setOverlayImage: function (url, callback) { // TODO (kangax): test callback + fabric.util.loadImage(url, function(img) { + this.overlayImage = img; + callback && callback(); + }, this); + return this; + }, + + /** + * Sets background image for this canvas + * @method setBackgroundImage + * @param {String} url url of an image to set background to + * @param {Function} callback callback to invoke when image is loaded and set as background + * @param {Object} options optional options to set for the background image + * @return {fabric.Canvas} thisArg + * @chainable + */ + setBackgroundImage: function (url, callback, options) { + return fabric.util.loadImage(url, function(img) { + this.backgroundImage = img; + if (options && ('backgroundImageOpacity' in options)) { + this.backgroundImageOpacity = options.backgroundImageOpacity; + } + if (options && ('backgroundImageStretch' in options)) { + this.backgroundImageStretch = options.backgroundImageStretch; + } + callback && callback(); + }, this); + }, + + /** + * @private + * @method _createCanvasElement + * @param {Element} element + */ + _createCanvasElement: function() { + var element = fabric.document.createElement('canvas'); + if (!element.style) { + element.style = { }; + } + if (!element) { + throw CANVAS_INIT_ERROR; + } + this._initCanvasElement(element); + return element; + }, + + _initCanvasElement: function(element) { + if (typeof element.getContext === 'undefined' && + typeof G_vmlCanvasManager !== 'undefined' && + G_vmlCanvasManager.initElement) { + + G_vmlCanvasManager.initElement(element); + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + }, + + /** + * @method _initOptions + * @param {Object} options + */ + _initOptions: function (options) { + for (var prop in options) { + this[prop] = options[prop]; + } + + this.width = parseInt(this.lowerCanvasEl.width, 10) || 0; + this.height = parseInt(this.lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) return; + + this.lowerCanvasEl.style.width = this.width + 'px'; + this.lowerCanvasEl.style.height = this.height + 'px'; + }, + + /** + * Creates a secondary canvas + * @method _createLowerCanvas + */ + _createLowerCanvas: function (canvasEl) { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + this._initCanvasElement(this.lowerCanvasEl); + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width + * @method getWidth + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height + * @method getHeight + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @method setWidth + * @param {Number} width value to set width to + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value) { + return this._setDimension('width', value); + }, + + /** + * Sets height of this canvas instance + * @method setHeight + * @param {Number} height value to set height to + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value) { + return this._setDimension('height', value); + }, + + /** + * Sets dimensions (width, height) of this canvas instance + * @method setDimensions + * @param {Object} dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function(dimensions) { + for (var prop in dimensions) { + this._setDimension(prop, dimensions[prop]); + } + return this; + }, + + /** + * Helper for setting width/height + * @private + * @method _setDimensions + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + this.lowerCanvasEl.style[prop] = value + 'px'; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + this.upperCanvasEl.style[prop] = value + 'px'; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value + 'px'; + } + + this[prop] = value; + + this.calcOffset(); + this.renderAll(); + + return this; + }, + + /** + * Returns <canvas> element corresponding to this instance + * @method getElement + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + // placeholder + getActiveObject: function() { + return null; + }, + + // placeholder + getActiveGroup: function() { + return null; + }, + + /** + * Given a context, renders an object on that context + * @param ctx {Object} context to render object on + * @param object {Object} object to render + * @private + */ + _draw: function (ctx, object) { + if (!object) return; + + if (this.controlsAboveOverlay) { + var hasBorders = object.hasBorders, hasCorners = object.hasCorners; + object.hasBorders = object.hasCorners = false; + object.render(ctx); + object.hasBorders = hasBorders; + object.hasCorners = hasCorners; + } + else { + object.render(ctx); + } + }, + + /** + * Adds objects to canvas, then renders canvas; + * Objects should be instances of (or inherit from) fabric.Object + * @method add + * @return {fabric.Canvas} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + for (var i = arguments.length; i--; ) { + this._initObject(arguments[i]); + } + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * @private + * @method _initObject + */ + _initObject: function(obj) { + this.stateful && obj.setupState(); + obj.setCoords(); + obj.canvas = this; + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * 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 + * @param nonSplicing {Boolean} when `true`, no splicing (shifting) of objects occurs + * @return {fabric.Canvas} instance + */ + insertAt: function (object, index, nonSplicing) { + if (nonSplicing) { + this._objects[index] = object; + } + else { + this._objects.splice(index, 0, object); + } + this._initObject(object); + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * Returns an array of objects this instance has + * @method getObjects + * @return {Array} + */ + getObjects: function () { + return this._objects; + }, + + /** + * Clears specified context of canvas element + * @method clearContext + * @param context {Object} ctx context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @method getContext + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @method clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this._objects.length = 0; + this.clearContext(this.contextContainer); + if (this.contextTop) { + this.clearContext(this.contextTop); + } + this.renderAll(); + return this; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @method renderAll + * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function (allOnTop) { + + var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + + if (this.contextTop) { + this.clearContext(this.contextTop); + } + + if (allOnTop === false || (typeof allOnTop === 'undefined')) { + this.clearContext(canvasToDrawOn); + } + + var activeGroup = this.getActiveGroup(), + startTime = new Date(); + + if (this.clipTo) { + this._clipCanvas(canvasToDrawOn); + } + + canvasToDrawOn.fillStyle = this.backgroundColor; + canvasToDrawOn.fillRect(0, 0, this.width, this.height); + + if (typeof this.backgroundImage === 'object') { + this._drawBackroundImage(canvasToDrawOn); + } + + for (var i = 0, length = this._objects.length; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(canvasToDrawOn, this._objects[i]); + } + } + + if (this.clipTo) { + canvasToDrawOn.restore(); + } + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + this._draw(this.contextTop, activeGroup); + } + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + if (this.controlsAboveOverlay) { + this.drawControls(this.contextContainer); + } + + if (this.onFpsUpdate) { + var elapsedTime = new Date() - startTime; + this.onFpsUpdate(~~(1000 / elapsedTime)); + } + + this.fire('after:render'); + + return this; + }, + + _clipCanvas: function(canvasToDrawOn) { + canvasToDrawOn.save(); + canvasToDrawOn.beginPath(); + this.clipTo(canvasToDrawOn); + canvasToDrawOn.clip(); + }, + + _drawBackroundImage: function(canvasToDrawOn) { + canvasToDrawOn.save(); + canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + + if (this.backgroundImageStretch) { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + } + else { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + } + canvasToDrawOn.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @method renderTop + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + this.clearContext(this.contextTop || this.contextContainer); + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(); + } + + // delegate rendering to group selection if one exists + // used for drawing selection borders/corners + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.render(this.contextTop); + } + + this.fire('after:render'); + + return this; + }, + + /** + * Draws objects' controls (borders/corners) + * @method drawControls + * @param {Object} ctx context to render controls on + */ + drawControls: function(ctx) { + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + ctx.save(); + fabric.Group.prototype.transform.call(activeGroup, ctx); + activeGroup.drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + else { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i].active) continue; + + ctx.save(); + fabric.Object.prototype.transform.call(this._objects[i], ctx); + this._objects[i].drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + } + }, + + /** + * Exports canvas element to a dataurl image. + * @method toDataURL + * @param {String} format the format of the output image. Either "jpeg" or "png". + * @param {Number} quality quality level (0..1) + * @return {String} + */ + toDataURL: function (format, quality) { + var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; + + this.renderAll(true); + var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) + ? canvasEl.toDataURL('image/' + format, quality) + : canvasEl.toDataURL('image/' + format); + this.renderAll(); + return data; + }, + + /** + * Exports canvas element to a dataurl image (allowing to change image size via multiplier). + * @method toDataURLWithMultiplier + * @param {String} format (png|jpeg) + * @param {Number} multiplier + * @param {Number} quality (0..1) + * @return {String} + */ + toDataURLWithMultiplier: function (format, multiplier, quality) { + + var origWidth = this.getWidth(), + origHeight = this.getHeight(), + scaledWidth = origWidth * multiplier, + scaledHeight = origHeight * multiplier, + activeObject = this.getActiveObject(), + activeGroup = this.getActiveGroup(); + + this.setWidth(scaledWidth).setHeight(scaledHeight); + this.contextTop.scale(multiplier, multiplier); + + if (activeGroup) { + // not removing group due to complications with restoring it with correct state afterwords + this._tempRemoveBordersCornersFromGroup(activeGroup); + } + else if (activeObject) { + this.deactivateAll(); + } + + // restoring width, height for `renderAll` to draw + // background properly (while context is scaled) + this.width = origWidth; + this.height = origHeight; + + this.renderAll(true); + + var dataURL = this.toDataURL(format, quality); + + this.contextTop.scale(1 / multiplier, 1 / multiplier); + this.setWidth(origWidth).setHeight(origHeight); + + if (activeGroup) { + this._restoreBordersCornersOnGroup(activeGroup); + } + else if (activeObject) { + this.setActiveObject(activeObject); + } + + this.renderAll(); + + return dataURL; + }, + + _tempRemoveBordersCornersFromGroup: function(group) { + group.origHideCorners = group.hideCorners; + group.origBorderColor = group.borderColor; + + group.hideCorners = true; + group.borderColor = 'rgba(0,0,0,0)'; + + group.forEachObject(function(o) { + o.origBorderColor = o.borderColor; + o.borderColor = 'rgba(0,0,0,0)'; + }); + }, + _restoreBordersCornersOnGroup: function(group) { + group.hideCorners = group.origHideCorners; + group.borderColor = group.origBorderColor; + + group.forEachObject(function(o) { + o.borderColor = o.origBorderColor; + delete o.origBorderColor; + }); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @method getCenter + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.getHeight() / 2, + left: this.getWidth() / 2 + }; + }, + + /** + * Centers object horizontally. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + object.set('left', this.getCenter().left); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + object.set('top', this.getCenter().top); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically and horizontally. + * @method centerObject + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function (object) { + return this.centerObjectH(object).centerObjectV(object); + }, + + /** + * Returs dataless JSON representation of canvas + * @method toDatalessJSON + * @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) { + var data = { + objects: this._objects.map(function (instance) { + // TODO (kangax): figure out how to clean this up + var originalValue; + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + var object = instance[methodName](); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, this), + background: this.backgroundColor + }; + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.src; + data.backgroundImageOpacity = this.backgroundImageOpacity; + data.backgroundImageStretch = this.backgroundImageStretch; + } + return data; + }, + + /** + * Returns SVG representation of canvas + * @function + * @method toSVG + * @return {String} + */ + toSVG: function() { + var markup = [ + '', + '', + '', + 'Created with Fabric.js ', fabric.version, '', + fabric.createSVGFontFacesMarkup(this.getObjects()) + ]; + + if (this.backgroundImage) { + markup.push( + '' + ); + } + + for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push(''); + + return markup.join(''); + }, + + /** + * Returns true if canvas contains no objects + * @method isEmpty + * @return {Boolean} true if canvas is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * 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); + if (this.getActiveObject() === object) { + + // removing active object should fire "selection:cleared" events + this.fire('before:selection:cleared', { target: object }); + this.discardActiveObject(); + this.fire('selection:cleared'); + } + this.renderAll(); + return object; + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @param object {fabric.Object} Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + removeFromArray(this._objects, object); + this._objects.unshift(object); + return this.renderAll(); + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + removeFromArray(this._objects, object); + this._objects.push(object); + return this.renderAll(); + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object) { + var idx = this._objects.indexOf(object), + nextIntersectingIdx = idx; + + // if object is not on the bottom of stack + if (idx !== 0) { + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + if (object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(this._objects, object); + this._objects.splice(nextIntersectingIdx, 0, object); + } + return this.renderAll(); + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object) { + var objects = this.getObjects(), + idx = objects.indexOf(object), + nextIntersectingIdx = idx; + + + // if object is not on top of stack (last item in an array) + if (idx !== objects.length-1) { + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1, l = this._objects.length; i < l; ++i) { + if (object.intersectsWithObject(objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(objects, object); + objects.splice(nextIntersectingIdx, 0, object); + } + this.renderAll(); + }, + + /** + * Returns object at specified index + * @method item + * @param {Number} index + * @return {fabric.Object} + */ + item: function (index) { + return this.getObjects()[index]; + }, + + /** + * 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); + }, + + /** + * Iterates over all objects, invoking callback for each one of them + * @method forEachObject + * @return {fabric.Canvas} thisArg + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(), + i = objects.length; + while (i--) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Clears a canvas element and removes all event handlers. + * @method dispose + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + this.clear(); + if (this.interactive) { + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(fabric.window, 'resize', this._onResize); + } + return this; + }, + + /** + * @private + * @method _resizeImageToFit + * @param {HTMLImageElement} imgEl + */ + _resizeImageToFit: function (imgEl) { + + var imageWidth = imgEl.width || imgEl.offsetWidth, + widthScaleFactor = this.getWidth() / imageWidth; + + // scale image down so that it has original dimensions when printed in large resolution + if (imageWidth) { + imgEl.width = imageWidth * widthScaleFactor; + } + } + }); + + /** + * Returns a string representation of an instance + * @method toString + * @return {String} string representation of an instance + */ + fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet + return '#'; + }; + + extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ { + + /** + * @static + * @property EMPTY_JSON + * @type String + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Takes element and transforms its data in such way that it becomes grayscale + * @static + * @method toGrayscale + * @param {HTMLCanvasElement} canvasEl + */ + toGrayscale: function (canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @method supports + * @param methodName {String} Method to check support for; + * Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = fabric.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'; + + case 'toDataURLWithQuality': + try { + el.toDataURL('image/jpeg', 0); + return true; + } + catch (e) { } + return false; + + default: + return null; + } + } + }); + + /** + * Returs JSON representation of canvas + * @function + * @method toJSON + * @return {String} json string + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + +})(); +(function() { + + var extend = fabric.util.object.extend, + getPointer = fabric.util.getPointer, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + 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' + }, + + 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, + + STROKE_OFFSET = 0.5; + + /** + * @class fabric.Canvas + * @constructor + * @extends fabric.StaticCanvas + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.Canvas = function(el, options) { + options || (options = { }); + + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + + fabric.Canvas.activeInstance = this; + }; + + function ProtoProxy(){ } + ProtoProxy.prototype = fabric.StaticCanvas.prototype; + fabric.Canvas.prototype = new ProtoProxy(); + + var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @property + * @type Boolean + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @property + * @type Boolean + */ + selection: true, + + /** + * Color of selection + * @property + * @type String + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @property + * @type String + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @property + * @type Number + */ + selectionLineWidth: 1, + + /** + * Color of the line used in free drawing mode + * @property + * @type String + */ + freeDrawingColor: 'rgb(0, 0, 0)', + + /** + * Width of a line used in free drawing mode + * @property + * @type Number + */ + freeDrawingLineWidth: 1, + + /** + * Default cursor value used when hovering over an object on canvas + * @property + * @type String + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @property + * @type String + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @property + * @type String + */ + defaultCursor: 'default', + + /** + * Cursor value used for rotation point + * @property + * @type String + */ + rotationCursor: 'crosshair', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @property + * @type String + */ + containerClass: 'canvas-container', + + perPixelTargetFind: false, + + targetFindTolerance: 0, + + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._freeDrawingXPoints = [ ]; + this._freeDrawingYPoints = [ ]; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEvents(); + this.calcOffset(); + }, + + /** + * Adds mouse listeners to canvas + * @method _initEvents + * @private + * See configuration documentation for more details. + */ + _initEvents: function () { + var _this = this; + + this._onMouseDown = function (e) { + _this.__onMouseDown(e); + + addListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); + + addListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); + + removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseUp = function (e) { + _this.__onMouseUp(e); + + removeListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); + + removeListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); + + addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseMove = function (e) { + e.preventDefault && e.preventDefault(); + _this.__onMouseMove(e); + }; + + this._onResize = function () { + _this.calcOffset(); + }; + + + addListener(fabric.window, 'resize', this._onResize); + + if (fabric.isTouchSupported) { + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + } + else { + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + } + }, + + /** + * 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) { + + var target; + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._finalizeDrawingPath(); + this.fire('mouse:up', { e: e }); + return; + } + + if (this._currentTransform) { + + var transform = this._currentTransform; + + target = transform.target; + if (target._scaling) { + target._scaling = false; + } + + // determine the new coords everytime the image changes its position + var i = this._objects.length; + while (i--) { + this._objects[i].setCoords(); + } + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + target.isMoving = false; + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + } + + this._currentTransform = null; + + if (this._groupSelector) { + // group selection was completed, determine its bounds + this._findSelectedObjects(e); + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords(); + activeGroup.set('isMoving', false); + this._setCursor(this.defaultCursor); + } + + // clear selection + this._groupSelector = null; + this.renderAll(); + + this._setCursorFromEvent(e, target); + + // fix for FF + this._setCursor(''); + + var _this = this; + setTimeout(function () { + _this._setCursorFromEvent(e, target); + }, 50); + + this.fire('mouse:up', { target: target, e: e }); + target && target.fire('mouseup', { e: e }); + }, + + /** + * 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) { + + // accept only left clicks + var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; + if (!isLeftClick && !fabric.isTouchSupported) return; + + if (this.isDrawingMode) { + this._prepareForDrawing(e); + + // capture coordinates immediately; this allows to draw dots (when movement never occurs) + this._captureDrawingPath(e); + this.fire('mouse:down', { e: e }); + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) return; + + var target = this.findTarget(e), + pointer = this.getPointer(e), + activeGroup = this.getActiveGroup(), + corner; + + if (this._shouldClearSelection(e)) { + + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + + this.deactivateAllWithDispatch(); + } + else { + // determine if it's a drag or rotate case + // rotate and scale will happen at the same time + this.stateful && target.saveState(); + + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + this._setupCurrentTransform(e, target); + + var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()) && this.selection; + if (shouldHandleGroupLogic) { + this._handleGroupLogic(e, target); + } + else { + if (target !== this.getActiveGroup()) { + this.deactivateAll(); + } + this.setActiveObject(target, e); + } + } + // we must renderAll so that active image is placed on the top canvas + this.renderAll(); + + this.fire('mouse:down', { target: target, e: e }); + target && target.fire('mousedown', { e: e }); + }, + + /** + * 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) { + + var target; + + if (this.isDrawingMode) { + if (this._isCurrentlyDrawing) { + this._captureDrawingPath(e); + } + this.fire('mouse:move', { e: e }); + return; + } + + var groupSelector = this._groupSelector, pointer; + + // We initially clicked in an empty area, so we draw a box for multiple selection. + if (groupSelector !== null) { + pointer = getPointer(e); + + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + this.renderTop(); + } + else if (!this._currentTransform) { + + // alias style to elimintate unnecessary lookup + var style = this.upperCanvasEl.style; + + // Here we are hovering the canvas then we will determine + // what part of the pictures we are hovering to change the caret symbol. + // We won't do that while dragging or rotating in order to improve the + // performance. + target = this.findTarget(e); + + if (!target) { + // image/text was hovered-out from, we remove its borders + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && !this._objects[i].active) { + this._objects[i].setActive(false); + } + } + style.cursor = this.defaultCursor; + } + else { + // set proper cursor + this._setCursorFromEvent(e, target); + if (target.isActive()) { + // display corners when hovering over an image + target.setCornersVisibility && target.setCornersVisibility(true); + } + } + } + else { + // object is being transformed (scaled/rotated/moved/etc.) + pointer = getPointer(e); + + var x = pointer.x, + y = pointer.y; + + this._currentTransform.target.isMoving = true; + + if (this._currentTransform.action === 'rotate') { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + + if (!e.shiftKey) { + this._rotateObject(x, y); + + this.fire('object:rotating', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('rotating'); + } + if (!this._currentTransform.target.hasRotatingPoint) { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + } + else if (this._currentTransform.action === 'scale') { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleX') { + this._scaleObject(x, y, 'x'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleY') { + this._scaleObject(x, y, 'y'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else { + this._translateObject(x, y); + + this.fire('object:moving', { + target: this._currentTransform.target + }); + + this._setCursor(this.moveCursor); + + this._currentTransform.target.fire('moving'); + } + // only commit here. when we are actually moving the pictures + this.renderAll(); + } + this.fire('mouse:move', { target: target, e: e }); + target && target.fire('mousemove', { e: e }); + }, + + /** + * Applies one implementation of 'point inside polygon' algorithm + * @method containsPoint + * @param e { Event } event object + * @param target { fabric.Object } object to test against + * @return {Boolean} true if point contains within area of given object + */ + containsPoint: function (e, target) { + var pointer = this.getPointer(e), + xy = this._normalizePointer(target, pointer), + x = xy.x, + y = xy.y; + + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + + // we iterate through each object. If target found, return it. + var iLines = target._getImageLines(target.oCoords), + xpoints = target._findCrossPoints(x, y, iLines); + + // if xcount is odd then we clicked inside the object + // For the specific case of square images xcount === 1 in all true cases + if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { + return true; + } + return false; + }, + + /** + * @private + * @method _normalizePointer + */ + _normalizePointer: function (object, pointer) { + + var activeGroup = this.getActiveGroup(), + x = pointer.x, + y = pointer.y; + + var isObjectInGroup = ( + activeGroup && + object.type !== 'group' && + activeGroup.contains(object) + ); + + if (isObjectInGroup) { + x -= activeGroup.left; + y -= activeGroup.top; + } + return { x: x, y: y }; + }, + + _isTargetTransparent: function (target, x, y) { + var cacheContext = this.contextCache; + + var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; + target.hasBorders = target.transparentCorners = false; + + this._draw(cacheContext, target); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + + // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 + if (this.targetFindTolerance > 0) { + if (x > this.targetFindTolerance) { + x -= this.targetFindTolerance; + } + else { + x = 0; + } + if (y > this.targetFindTolerance) { + y -= this.targetFindTolerance; + } + else { + y = 0; + } + } + + var isTransparent = true; + var imageData = cacheContext.getImageData( + x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3; i < imageData.data.length; i += 4) { + var temp = imageData.data[i]; + isTransparent = temp <= 0; + if (isTransparent === false) break; //Stop if colour found + } + + imageData = null; + this.clearContext(cacheContext); + return isTransparent; + }, + + /** + * @private + * @method _shouldClearSelection + */ + _shouldClearSelection: function (e) { + var target = this.findTarget(e), + activeGroup = this.getActiveGroup(); + return ( + !target || ( + target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey + ) + ); + }, + + /** + * @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' + : corner === 'mtr' + ? 'rotate' + : (target.hasRotatingPoint) + ? 'scale' + : '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 === this.getActiveGroup()) { + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + if (activeGroup.contains(target)) { + activeGroup.removeWithUpdate(target); + target.setActive(false); + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(); + } + } + else { + activeGroup.addWithUpdate(target); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.setActive(true); + } + else { + // group does not exist + if (this._activeObject) { + // only if there's an active object + if (target !== this._activeObject) { + // and that object is not the actual target + var group = new fabric.Group([ this._activeObject, target ]); + this.setActiveGroup(group); + activeGroup = this.getActiveGroup(); + } + } + // activate target object in any case + target.setActive(true); + } + + if (activeGroup) { + activeGroup.saveCoords(); + } + }, + + /** + * @private + * @method _prepareForDrawing + */ + _prepareForDrawing: function(e) { + + this._isCurrentlyDrawing = true; + + this.discardActiveObject().renderAll(); + + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.beginPath(); + this.contextTop.moveTo(pointer.x, pointer.y); + this.contextTop.strokeStyle = this.freeDrawingColor; + this.contextTop.lineWidth = this.freeDrawingLineWidth; + this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; + }, + + /** + * @private + * @method _captureDrawingPath + */ + _captureDrawingPath: function(e) { + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.lineTo(pointer.x, pointer.y); + this.contextTop.stroke(); + }, + + /** + * @private + * @method _finalizeDrawingPath + */ + _finalizeDrawingPath: function() { + + this.contextTop.closePath(); + + this._isCurrentlyDrawing = false; + + var minX = utilMin(this._freeDrawingXPoints), + minY = utilMin(this._freeDrawingYPoints), + maxX = utilMax(this._freeDrawingXPoints), + maxY = utilMax(this._freeDrawingYPoints), + path = [ ], + xPoints = this._freeDrawingXPoints, + yPoints = this._freeDrawingYPoints; + + path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); + + for (var i = 1, len = xPoints.length; i < len; i++) { + path.push('L ', xPoints[i] - minX, ' ', yPoints[i] - minY, ' '); + } + + // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, + // and instead fire something like "drawing:completed" event with path string + + path = path.join(''); + + if (path === "M 0 0 L 0 0 ") { + // do not create 0 width/height paths, as they are rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing + this.renderAll(); + return; + } + + var p = new fabric.Path(path); + + 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(); + this.fire('path:created', { path: p }); + }, + + /** + * 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.lockMovementX || target.set('left', x - this._currentTransform.offsetX); + target.lockMovementY || 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.lockScalingX && target.lockScalingY) 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.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); + } + else if (by === 'x' && !target.lockUniScaling) { + target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + } + else if (by === 'y' && !target.lockUniScaling) { + target.lockScalingY || 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._theta = (curAngle - lastAngle) + t.theta; + }, + + /** + * @method _setCursor + */ + _setCursor: function (value) { + this.upperCanvasEl.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.upperCanvasEl.style; + if (!target) { + s.cursor = this.defaultCursor; + return false; + } + else { + var activeGroup = this.getActiveGroup(); + // only show proper corner when group selection is not active + var corner = !!target._findTargetCorner + && (!activeGroup || !activeGroup.contains(target)) + && target._findTargetCorner(e, this._offset); + + if (!corner) { + s.cursor = this.hoverCursor; + } + else { + if (corner in cursorMap) { + s.cursor = cursorMap[corner]; + } else if (corner === 'mtr' && target.hasRotatingPoint) { + s.cursor = this.rotationCursor; + } else { + s.cursor = this.defaultCursor; + return false; + } + } + } + return true; + }, + + /** + * @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 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) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { + + if (this.selection && currentObject.selectable) { + currentObject.setActive(true); + group.push(currentObject); + } + } + } + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + group = new fabric.Group(group); + this.setActiveGroup(group); + group.saveCoords(); + this.fire('selection:created', { target: group }); + } + + this.renderAll(); + }, + + /** + * Method that determines what object we are clicking on + * @method findTarget + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through + */ + findTarget: function (e, skipGroup) { + + var target, + pointer = this.getPointer(e); + + // first check current group (if one exists) + var activeGroup = this.getActiveGroup(); + + if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { + target = activeGroup; + return target; + } + + // then check all of the objects on canvas + // Cache all targets where their bounding box contains point. + var possibleTargets = []; + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && this.containsPoint(e, this._objects[i])) { + if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { + possibleTargets[possibleTargets.length] = this._objects[i]; + } + else { + target = this._objects[i]; + this.relatedTarget = target; + break; + } + } + } + for (var j = 0, len = possibleTargets.length; j < len; j++) { + pointer = this.getPointer(e); + var isTransparent = this._isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); + if (!isTransparent) { + target = possibleTargets[j]; + this.relatedTarget = target; + break; + } + } + if (target && target.selectable) { + return target; + } + }, + + /** + * Returns pointer coordinates relative to canvas. + * @method getPointer + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e) { + var pointer = getPointer(e); + return { + x: pointer.x - this._offset.left, + y: pointer.y - this._offset.top + }; + }, + + /** + * @method _createUpperCanvas + * @param {HTMLElement|String} canvasEl Canvas element + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + this.upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl.className = 'upper-canvas'; + + this.wrapperEl.appendChild(this.upperCanvasEl); + + this._applyCanvasStyle(this.upperCanvasEl); + this.contextTop = this.upperCanvasEl.getContext('2d'); + }, + + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + * @method _initWrapperElement + * @param {Number} width + * @param {Number} height + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.getWidth() + 'px', + height: this.getHeight() + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @method _applyCanvasStyle + * @param {Element} element + */ + _applyCanvasStyle: function (element) { + var width = this.getWidth() || element.width, + height = this.getHeight() || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0 + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Returns context of canvas where object selection is drawn + * @method getSelectionContext + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @method getSelectionElement + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Sets given object as active + * @method setActiveObject + * @param object {fabric.Object} Object to set as an active one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = object; + object.setActive(true); + + this.renderAll(); + + this.fire('object:selected', { target: object, e: e }); + object.fire('selected', { e: e }); + return this; + }, + + /** + * Returns currently active object + * @method getActiveObject + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Discards currently active object + * @method discardActiveObject + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function () { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = null; + return this; + }, + + /** + * Sets active group to a speicified one + * @method setActiveGroup + * @param {fabric.Group} group Group to set as a current one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveGroup: function (group) { + this._activeGroup = group; + group && group.setActive(true); + return this; + }, + + /** + * Returns currently active group + * @method getActiveGroup + * @return {fabric.Group} Current group + */ + getActiveGroup: function () { + return this._activeGroup; + }, + + /** + * Removes currently active group + * @method discardActiveGroup + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function () { + var g = this.getActiveGroup(); + if (g) { + g.destroy(); + } + return this.setActiveGroup(null); + }, + + /** + * Deactivates all objects by calling their setActive(false) + * @method deactivateAll + * @return {fabric.Canvas} thisArg + */ + deactivateAll: function () { + var allObjects = this.getObjects(), + i = 0, + len = allObjects.length; + for ( ; i < len; i++) { + allObjects[i].setActive(false); + } + this.discardActiveGroup(); + this.discardActiveObject(); + return this; + }, + + /** + * Deactivates all objects and dispatches appropriate events + * @method deactivateAllWithDispatch + * @return {fabric.Canvas} thisArg + */ + deactivateAllWithDispatch: function () { + var activeObject = this.getActiveGroup() || this.getActiveObject(); + if (activeObject) { + this.fire('before:selection:cleared', { target: activeObject }); + } + this.deactivateAll(); + if (activeObject) { + this.fire('selection:cleared'); + } + return this; + } + }; + + fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; + extend(fabric.Canvas.prototype, InteractiveMethods); + + // iterating manually to workaround Opera's bug + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } + + if (fabric.isTouchSupported) { + fabric.Canvas.prototype._setCursorFromEvent = function() { }; + } + + /** + * @class fabric.Element + * @alias fabric.Canvas + * @deprecated + * @constructor + */ + fabric.Element = fabric.Canvas; +})(); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @method fxCenterObjectH + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} 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 with animation. + * @method fxCenterObjectV + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} 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; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @method fxRemove + * @param {fabric.Object} object Object to remove + * @param {Function} callback Callback, invoked on effect completion + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.get('opacity'), + endValue: 0, + duration: this.FX_DURATION, + onStart: function() { + object.setActive(false); + }, + onChange: function(value) { + object.set('opacity', value); + _this.renderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Populates canvas with data from the specified dataless JSON + * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` + * @method loadFromDatalessJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} instance + * @chainable + */ + loadFromDatalessJSON: function (json, callback) { + + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : json; + + if (!serialized || (serialized && !serialized.objects)) return; + + this.clear(); + + // TODO: test this + this.backgroundColor = serialized.background; + this._enlivenDatalessObjects(serialized.objects, callback); + }, + + /** + * @method _enlivenDatalessObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenDatalessObjects: function (objects, callback) { + + /** @ignore */ + function onObjectLoaded(object, index) { + _this.insertAt(object, index, true); + object.setCoords(); + if (++numLoadedObjects === numTotalObjects) { + callback && callback(); + } + } + + /** @ignore */ + function loadObject(obj, index) { + + var pathProp = obj.paths ? 'paths' : 'path'; + var path = obj[pathProp]; + + delete obj[pathProp]; + + if (typeof path !== 'string') { + if (obj.type === 'image') { + fabric[fabric.util.string.capitalize(obj.type)].fromObject(obj, function (o) { + onObjectLoaded(o, index); + }); + } + else { + var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(obj.type))]; + if (!klass || !klass.fromObject) return; + + // restore path + if (path) { + obj[pathProp] = path; + } + onObjectLoaded(klass.fromObject(obj), index); + } + } + else { + if (obj.type === 'image') { + fabric.util.loadImage(path, function (image) { + var oImg = new fabric.Image(image); + + oImg.setSourcePath(path); + + fabric.util.object.extend(oImg, obj); + oImg.setAngle(obj.angle); + + onObjectLoaded(oImg, index); + }); + } + else if (obj.type === 'text') { + + if (obj.useNative) { + onObjectLoaded(fabric.Text.fromObject(obj), index); + } + else { + obj.path = path; + var object = fabric.Text.fromObject(obj); + var onscriptload = function () { + // TODO (kangax): find out why Opera refuses to work without this timeout + if (Object.prototype.toString.call(fabric.window.opera) === '[object Opera]') { + setTimeout(function () { + onObjectLoaded(object, index); + }, 500); + } + else { + onObjectLoaded(object, index); + } + }; + + fabric.util.getScript(path, onscriptload); + } + } + else { + fabric.loadSVGFromURL(path, function (elements) { + var object = fabric.util.groupSVGElements(elements, obj, path); + + // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) + // skip this step if an object is a PathGroup, since we already passed it options object before + if (!(object instanceof fabric.PathGroup)) { + fabric.util.object.extend(object, obj); + if (typeof obj.angle !== 'undefined') { + object.setAngle(obj.angle); + } + } + + onObjectLoaded(object, index); + }); + } + } + } + + var _this = this, + numLoadedObjects = 0, + numTotalObjects = objects.length; + + if (numTotalObjects === 0 && callback) { + callback(); + } + + try { + objects.forEach(loadObject, this); + } + catch(e) { + fabric.log(e.message); + } + }, + + /** + * Populates canvas with data from the specified JSON + * JSON format must conform to the one of `fabric.Canvas#toJSON` + * @method loadFromJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} 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 (serialized.backgroundImage) { + _this.setBackgroundImage(serialized.backgroundImage, function() { + + _this.backgroundImageOpacity = serialized.backgroundImageOpacity; + _this.backgroundImageStretch = serialized.backgroundImageStretch; + + _this.renderAll(); + + callback && callback(); + }); + } + else { + callback && callback(); + } + }); + + return this; + }, + + /** + * @method _enlivenObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenObjects: function (objects, callback) { + var _this = this; + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + enlivenedObjects.forEach(function(obj, index) { + _this.insertAt(obj, index, true); + }); + callback && callback(); + }); + }, + + /** + * @private + * @method _toDataURL + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @method _toDataURLWithMultiplier + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @method clone + * @param {Object} [callback] Receives cloned instance as a first argument + */ + clone: function (callback) { + var data = JSON.stringify(this); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @method cloneWithoutData + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.document.createElement('canvas'); + + el.width = this.getWidth(); + el.height = this.getHeight(); + + var clone = new fabric.Canvas(el); + clone.clipTo = this.clipTo; + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + slice = Array.prototype.slice; + + if (fabric.Object) { + return; + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc) + * @property + * @type String + */ + type: 'object', + + /** + * @property + * @type Number + */ + top: 0, + + /** + * @property + * @type Number + */ + left: 0, + + /** + * @property + * @type Number + */ + width: 0, + + /** + * @property + * @type Number + */ + height: 0, + + /** + * @property + * @type Number + */ + scaleX: 1, + + /** + * @property + * @type Number + */ + scaleY: 1, + + /** + * @property + * @type Boolean + */ + flipX: false, + + /** + * @property + * @type Boolean + */ + flipY: false, + + /** + * @property + * @type Number + */ + opacity: 1, + + /** + * @property + * @type Number + */ + angle: 0, + + /** + * @property + * @type Number + */ + cornersize: 12, + + /** + * @property + * @type Boolean + */ + transparentCorners: true, + + /** + * @property + * @type Number + */ + padding: 0, + + /** + * @property + * @type String + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * @property + * @type String + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * @property + * @type String + */ + fill: 'rgb(0,0,0)', + + /** + * @property + * @type String + */ + fillRule: 'source-over', + + /** + * @property + * @type String + */ + overlayFill: null, + + /** + * @property + * @type String + */ + stroke: null, + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type Array + */ + strokeDashArray: null, + + /** + * @property + * @type Number + */ + borderOpacityWhenMoving: 0.4, + + /** + * @property + * @type Number + */ + borderScaleFactor: 1, + + /** + * Transform matrix + * @property + * @type Array + */ + transformMatrix: null, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) + * @property + * @type Boolean + */ + selectable: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @property + * @type Boolean + */ + hasControls: true, + + /** + * When set to `false`, object's borders are not rendered + * @property + * @type Boolean + */ + hasBorders: true, + + /** + * When set to `false`, object's rotating point will not be visible or selectable + * @property + * @type Boolean + */ + hasRotatingPoint: false, + + /** + * Offset for object's rotating point (when enabled) + * @property + * @type Number + */ + rotatingPointOffset: 40, + + /** + * @private + * @property + * @type Number + */ + _theta: 0, + + perPixelTargetFind: false, + + includeDefaultValues: true, + + /** + * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); + * as well as for history (undo/redo) purposes + * @property + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY ' + + 'theta angle opacity cornersize fill overlayFill ' + + 'stroke strokeWidth strokeDashArray fillRule ' + + 'borderScaleFactor transformMatrix selectable' + ).split(' '), + + /** + * @method callSuper + * @param {String} methodName + */ + callSuper: function(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + }, + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * @method initGradient + */ + _initGradient: function(options) { + if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @method setOptions + * @param {Object} [options] + */ + setOptions: function(options) { + var i = this.stateProperties.length, prop; + while (i--) { + prop = this.stateProperties[i]; + if (prop in options) { + this.set(prop, options[prop]); + } + } + this._initGradient(options); + }, + + /** + * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + overlayFill: this.overlayFill, + stroke: this.stroke, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray, + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + selectable: this.selectable, + hasControls: this.hasControls, + hasBorders: this.hasBorders, + hasRotatingPoint: this.hasRotatingPoint, + transparentCorners: this.transparentCorners, + perPixelTargetFind: this.perPixelTargetFind + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @method toDatalessObject + */ + toDatalessObject: function() { + // will be overwritten by subclasses + return this.toObject(); + }, + + /** + * Returns styles-string for svg-export + * @method getSvgStyles + * @return {string} + */ + getSvgStyles: function() { + return [ + "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", + "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), + "fill: ", (this.fill ? this.fill : 'none'), "; ", + "opacity: ", (this.opacity ? this.opacity : '1'), ";" + ].join(""); + }, + + /** + * Returns transform-string for svg-export + * @method getSvgTransform + * @return {string} + */ + getSvgTransform: function() { + var angle = this.getAngle(); + return [ + "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", + angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', + (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") + ].join(''); + }, + + /** + * @private + * @method _removeDefaultValues + */ + _removeDefaultValues: function(object) { + var defaultOptions = fabric.Object.prototype.options; + if (defaultOptions) { + 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 "#"; + }, + + /** + * Sets property to a given value + * @method set + * @param {String} name + * @param {Object|Function} value + * @return {fabric.Group} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && + value < fabric.Object.MIN_SCALE_LIMIT; + + if (shouldConstrainValue) { + value = fabric.Object.MIN_SCALE_LIMIT; + } + if (key === 'angle') { + this.setAngle(value); + } + else { + this[key] = value; + } + }, + + /** + * 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) { + + // do not render if width or height are zeros + if (this.width === 0 || this.height === 0) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + if (!noTransform) { + this.transform(ctx); + } + + if (this.stroke || this.strokeDashArray) { + ctx.lineWidth = this.strokeWidth; + ctx.strokeStyle = this.stroke; + } + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : this.fill; + } + + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._render(ctx, noTransform); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + 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; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @method scaleToWidth + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @method scaleToHeight + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * 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() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; + this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + + //If width is negative, make postive. Fixes path selection issue + if(this.currentWidth < 0){ + this.currentWidth = Math.abs(this.currentWidth); + } + + this._hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + this._angle = Math.atan(this.currentHeight / this.currentWidth); + + // offset added for rotate and scale actions + 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) + }; + var mtr = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + // clockwise + this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords(); + + return this; + }, + + /** + * Returns width of an object's bounding rectangle + * @method getBoundingRectWidth + * @return {Number} width value + */ + getBoundingRectWidth: function() { + this.oCoords || this.setCoords(); + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + return Math.abs(minX - maxX); + }, + + /** + * Returns height of an object's bounding rectangle + * @method getBoundingRectHeight + * @return {Number} height value + */ + getBoundingRectHeight: function() { + this.oCoords || this.setCoords(); + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + return Math.abs(minY - maxY); + }, + + /** + * 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) { + if (!this.hasBorders) return; + + var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, + padding = this.padding, + padding2 = padding * 2, + strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), + scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX), + ~~(h + padding2 + strokeWidth * this.scaleY) + ); + + if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + _renderDashedStroke: function(ctx) { + + if (1 & this.strokeDashArray.length /* if odd number of items */) { + /* duplicate items */ + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + var i = 0, + x = -this.width/2, y = -this.height/2, + _this = this, + padding = this.padding, + dashedArrayLength = this.strokeDashArray.length; + + ctx.save(); + ctx.beginPath(); + + function renderSide(xMultiplier, yMultiplier) { + + var lineLength = 0, + lengthDiff = 0, + sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; + + while (lineLength < sideLength) { + + var lengthOfSubPath = _this.strokeDashArray[i++]; + lineLength += lengthOfSubPath; + + if (lineLength > sideLength) { + lengthDiff = lineLength - sideLength; + } + + // track coords + if (xMultiplier) { + x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); + } + else { + y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); + } + + ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); + if (i >= dashedArrayLength) { + i = 0; + } + } + } + + renderSide(1, 0); + renderSide(0, 1); + renderSide(-1, 0); + renderSide(0, -1); + + ctx.stroke(); + ctx.closePath(); + ctx.restore(); + }, + + /** + * 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) { + if (!this.hasControls) return; + + var size = this.cornersize, + size2 = size / 2, + strokeWidth2 = this.strokeWidth / 2, + left = -(this.width / 2), + top = -(this.height / 2), + _left, + _top, + sizeX = size / this.scaleX, + sizeY = size / this.scaleY, + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // top-right + _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + if (!this.lockUniScaling) { + // middle-top + _left = left + width/2 - scaleOffsetX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-bottom + _left = left + width/2 - scaleOffsetX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + + _left = left + width/2 - scaleOffsetX; + + _top = this.flipY ? + (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_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(); + + /** @ignore */ + 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') + }; + + // normalize angle + this.set('angle', 0).set('flipX', false).set('flipY', false); + this.toDataURL(function(dataURL) { + i.src = dataURL; + }); + } + return this; + }, + + /** + * Converts an object into a data-url-like string + * @method toDataURL + * @return {String} string of data + */ + toDataURL: function(callback) { + var el = fabric.document.createElement('canvas'); + if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(el); + } + + el.width = this.getBoundingRectWidth(); + el.height = this.getBoundingRectHeight(); + + fabric.util.wrapElement(el, 'div'); + + var canvas = new fabric.Canvas(el); + canvas.backgroundColor = 'transparent'; + canvas.renderAll(); + + if (this.constructor.async) { + this.clone(proceed); + } + else { + proceed(this.clone()); + } + + function proceed(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; + + callback && callback(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; + }, + + /** + * @method setupState + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + }, + + /** + * 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) { + // extracts coords + 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 of another object + * @method isContainedWithinObject + * @param {Object} other Object to test + * @return {Boolean} + */ + isContainedWithinObject: function(other) { + return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); + }, + + /** + * 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); + + 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) { + if (!this.hasControls || !this.active) return false; + + var pointer = getPointer(e), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xpoints, + lines; + + for (var i in this.oCoords) { + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner, i); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + 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]; + // optimisation 1: line below dot. no cross + if ((iLine.o.y < ey) && (iLine.d.y < ey)) { + continue; + } + // optimisation 2: line above dot. no cross + if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { + xi = iLine.o.x; + yi = ey; + } + // calculate the intersection point + 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; + } + // dont count xi < ex cases + if (xi >= ex) { + xcount += 1; + } + // optimisation 4: specific for square images + 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) { + 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 = degreesToRadians(45 - this.getAngle()), + cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, + cosHalfOffset = cornerHypotenuse * Math.cos(theta), + sinHalfOffset = cornerHypotenuse * Math.sin(theta), + sinTh = Math.sin(this._theta), + cosTh = Math.cos(this._theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + + /** + * 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; + }, + + /** + * Returns a JSON representation of an instance + * @method toJSON + * @return {String} json + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + }, + + /** + * @method setGradientFill + */ + setGradientFill: function(options) { + this.set('fill', fabric.Gradient.forObject(this, options)); + }, + + /** + * @method animate + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + for (var prop in arguments[0]) { + this._animate(prop, arguments[0][prop], arguments[1]); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @method _animate + */ + _animate: function(property, to, options) { + var obj = this; + + options || (options = { }); + + if (!('from' in options)) { + options.from = this.get(property); + } + + if (/[+\-]/.test((to + '').charAt(0))) { + to = this.get(property) + parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + onChange: function(value) { + obj.set(property, value); + options.onChange && options.onChange(); + }, + onComplete: function() { + obj.setCoords(); + options.onComplete && options.onComplete(); + } + }); + }, + + /** + * Centers object horizontally on canvas to which it was added last + * @method centerH + * @return {fabric.Object} thisArg + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last + * @method centerV + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * @method center + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + return this.centerH().centerV(); + }, + + /** + * Removes object from canvas to which it was added last + * @method remove + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + return this.canvas.remove(this); + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + this.canvas.sendToBack(this); + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + this.canvas.bringToFront(this); + return this; + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function() { + this.canvas.sendBackwards(this); + return this; + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function() { + this.canvas.bringForward(this); + return this; + } + }); + + /** + * @alias rotate -> setAngle + */ + fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + + var proto = fabric.Object.prototype; + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + + extend(fabric.Object.prototype, fabric.Observable); + + extend(fabric.Object, { + + /** + * @static + * @constant + * @type Number + */ + NUM_FRACTION_DIGITS: 2, + + /** + * @static + * @constant + * @type Number + */ + MIN_SCALE_LIMIT: 0.1 + + }); + +})(typeof exports !== 'undefined' ? exports : this); + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * @class Line + * @extends fabric.Object + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @scope fabric.Line.prototype */ { + + /** + * @property + * @type String + */ + type: 'line', + + /** + * Constructor + * @method initialize + * @param {Array} points Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} 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._setWidthHeight(options); + }, + + /** + * @private + * @method _setWidthHeight + * @param {Object} options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.set('width', (this.x2 - this.x1) || 1); + this.set('height', (this.y2 - this.y1) || 1); + + this.set('left', 'left' in options ? options.left : (this.x1 + this.width / 2)); + this.set('top', 'top' in options ? options.top : (this.y1 + this.height / 2)); + }, + + /** + * @private + * @method _set + * @param {String} key + * @param {Any} value + */ + _set: function(key, value) { + this[key] = value; + if (key in coordProps) { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + if (this.group) { + ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); + } + + // move from center (of virtual box) to its left/top corner + ctx.moveTo(this.width === 1 ? 0 : (-this.width / 2), this.height === 1 ? 0 : (-this.height / 2)); + ctx.lineTo(this.width === 1 ? 0 : (this.width / 2), this.height === 1 ? 0 : (this.height / 2)); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = ctx.fillStyle; + ctx.stroke(); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '' + ].join(''); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Line.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @method fabric.Line.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromElement = function(element, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); + var points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + return new fabric.Line(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Line instance from an object representation + * @static + * @method fabric.Line.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Line} 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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * @class Circle + * @extends fabric.Object + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @scope fabric.Circle.prototype */ { + + /** + * @property + * @type String + */ + type: 'circle', + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + * @return {fabric.Circle} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.set('radius', options.radius || 0); + this.callSuper('initialize', options); + + var diameter = this.get('radius') * 2; + this.set('width', diameter).set('height', diameter); + }, + + /** + * 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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return (''); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) + ctx.globalAlpha *= this.opacity; + 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 horizontal radius of an object (according to how an object is scaled) + * @method getRadiusX + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @method getRadiusY + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @method setRadius + * @return {Number} + */ + setRadius: function(value) { + this.radius = value; + this.set('width', value * 2).set('height', value * 2); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @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 new 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)); + } + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * @class Triangle + * @extends fabric.Object + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @scope fabric.Triangle.prototype */ { + + /** + * @property + * @type String + */ + type: 'triangle', + + /** + * Constructor + * @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; + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + var points = [ + -widthBy2 + " " + heightBy2, + "0 " + -heightBy2, + widthBy2 + " " + heightBy2 + ].join(","); + + return ''; + } + }); + + /** + * Returns fabric.Triangle instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * @class Ellipse + * @extends fabric.Object + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @scope fabric.Ellipse.prototype */ { + + /** + * @property + * @type String + */ + type: 'ellipse', + + /** + * Constructor + * @method initialize + * @param {Object} [options] 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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '' + ].join(''); + }, + + /** + * Renders this instance on a given context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + * @param noTransform {Boolean} context is not transformed when set to true + */ + render: function(ctx, noTransform) { + // do not use `get` for perf. reasons + 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.globalAlpha *= this.opacity; + if (this.transformMatrix && this.group) { + ctx.translate(this.cx, this.cy); + } + 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); + if (this.stroke) { + ctx.stroke(); + } + if (this.fill) { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @method fabric.Ellipse.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + var cx = parsedAttributes.left; + var cy = parsedAttributes.top; + + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + + var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); + + ellipse.cx = cx || 0; + ellipse.cy = cy || 0; + + return ellipse; + }; + + /** + * Returns fabric.Ellipse instance from an object representation + * @static + * @method fabric.Ellipse.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object) { + return new fabric.Ellipse(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Rect) { + console.warn('fabric.Rect is already defined'); + return; + } + + /** + * @class Rect + * @extends fabric.Object + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @scope fabric.Rect.prototype */ { + + /** + * @property + * @type String + */ + type: 'rect', + + /** + * @property + * @type Number + */ + rx: 0, + + /** + * @property + * @type Number + */ + ry: 0, + + /** + * Constructor + * @method initialize + * @param options {Object} options object + * @return {Object} thisArg + */ + initialize: function(options) { + this._initStateProperties(); + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Rect` -specific ones to it + * (such as "rx", "ry", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(['rx', 'ry']); + }, + + /** + * @private + * @method _initRxRy + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var rx = this.rx || 0, + ry = this.ry || 0, + x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.beginPath(); + ctx.globalAlpha *= this.opacity; + + if (this.transformMatrix && this.group) { + ctx.translate( + this.width / 2 + this.x, + this.height / 2 + this.y); + } + if (!this.transformMatrix && this.group) { + ctx.translate( + -this.group.width / 2 + this.width / 2 + this.x, + -this.group.height / 2 + this.height / 2 + this.y); + } + + ctx.moveTo(x+rx, y); + ctx.lineTo(x+w-rx, y); + ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry); + ctx.lineTo(x+w, y+h-ry); + ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h); + ctx.lineTo(x+rx,y+h); + ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry); + ctx.lineTo(x,y+ry); + ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y); + ctx.closePath(); + + if (this.fill) { + ctx.fill(); + } + + if (this.strokeDashArray) { + this._renderDashedStroke(ctx); + } + else if (this.stroke) { + ctx.stroke(); + } + }, + + // since our coordinate system differs from that of SVG + _normalizeLeftTopProperties: function(parsedAttributes) { + if (parsedAttributes.left) { + this.set('left', parsedAttributes.left + this.getWidth() / 2); + } + this.set('x', parsedAttributes.left || 0); + if (parsedAttributes.top) { + this.set('top', parsedAttributes.top + this.getHeight() / 2); + } + this.set('y', parsedAttributes.top || 0); + return this; + }, + + /** + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + rx: this.get('rx') || 0, + ry: this.get('ry') || 0 + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return ''; + } + }); + + // TODO (kangax): implement rounded rectangles (both parsing and rendering) + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + */ + fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * @private + */ + function _setDefaultLeftTopValues(attributes) { + attributes.left = attributes.left || 0; + attributes.top = attributes.top || 0; + return attributes; + } + + /** + * Returns fabric.Rect instance from an SVG element + * @static + * @method fabric.Rect.fromElement + * @param element {SVGElement} element to parse + * @param options {Object} options object + * @return {fabric.Rect} 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 ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect._normalizeLeftTopProperties(parsedAttributes); + + return rect; + }; + + /** + * Returns fabric.Rect instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * @class Polyline + * @extends fabric.Object + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @scope fabric.Polyline.prototype */ { + + /** + * @property + * @type String + */ + type: 'polyline', + + /** + * Constructor + * @method initialize + * @param {Array} points array of points + * @param {Object} [options] 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); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '' + ].join(''); + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + 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(); + } + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.get('points').length; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polyline.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @method fabric.Polyline.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] 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, fabric.Polyline.ATTRIBUTE_NAMES); + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + 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)); + }; + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @method fabric.Polyline.fromObject + * @param {Object} [object] Object to create an instance from + * @return {fabric.Polyline} + */ + fabric.Polyline.fromObject = function(object) { + var points = object.points; + return new fabric.Polyline(points, object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * @class Polygon + * @extends fabric.Object + */ + fabric.Polygon = fabric.util.createClass(fabric.Object, /** @scope fabric.Polygon.prototype */ { + + /** + * @property + * @type String + */ + type: 'polygon', + + /** + * Constructor + * @method initialize + * @param {Array} points Array of points + * @param {Object} options Options object + * @return {fabric.Polygon} 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) || 1; + this.height = (maxY - minY) || 1; + + 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() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '' + ].join(''); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + 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; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polygon instance from an SVG element + * @static + * @method fabric.Polygon.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} options Options object + * @return {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++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2) || 0; + points[i].y -= (options.height / 2) || 0; + } + + return new fabric.Polygon(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @method fabric.Polygon.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Polygon} + */ + fabric.Polygon.fromObject = function(object) { + return new fabric.Polygon(object.points, object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + var commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }; + + function drawArc(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; + } + + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; + var x0 = a00 * ox + a01 * oy; + var y0 = a10 * ox + a11 * oy; + var x1 = a00 * x + a01 * y; + var y1 = a10 * x + a11 * y; + + var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + var sfactor = Math.sqrt(sfactor_sq); + if (sweep === large) sfactor = -sfactor; + var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); + var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); + + var th0 = Math.atan2(y0-yc, x0-xc); + var th1 = Math.atan2(y1-yc, x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep === 1){ + th_arc += 2*Math.PI; + } else if (th_arc > 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i'; + }, + + /** + * Returns object representation of an instance + * @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; + }, + + /** + * Returns dataless object representation of an instance + * @method toDatalessObject + * @return {Object} + */ + toDatalessObject: function() { + var o = this.toObject(); + if (this.sourcePath) { + o.path = this.sourcePath; + } + delete o.sourcePath; + return o; + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var chunks = []; + for (var i = 0, len = this.path.length; i < len; i++) { + chunks.push(this.path[i].join(' ')); + } + var path = chunks.join(' '); + + return [ + '', + '', + '' + ].join(''); + }, + + /** + * Returns number representation of an instance complexity + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + * @method _parsePath + */ + _parsePath: function() { + var result = [ ], + currentPath, + chunks, + parsed; + + for (var i = 0, chunksParsed, len = this.path.length; i < len; i++) { + currentPath = this.path[i]; + chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/); + chunksParsed = [ currentPath.charAt(0) ]; + + for (var j = 0, jlen = chunks.length; j < jlen; j++) { + parsed = parseFloat(chunks[j]); + if (!isNaN(parsed)) { + chunksParsed.push(parsed); + } + } + + var command = chunksParsed[0].toLowerCase(), + commandLength = commandLengths[command]; + + if (chunksParsed.length - 1 > commandLength) { + for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) { + result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); + } + } + else { + result.push(chunksParsed); + } + } + + return result; + }, + + /** + * @method _parseDimensions + */ + _parseDimensions: function() { + var aX = [], + aY = [], + previousX, + previousY, + isLowerCase = false, + x, + y; + + 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]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + // last 2 items in an array of coordinates are the actualy x/y (except H/V); + // collect them + + // TODO (kangax): support relative h/v commands + + 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 = 0, + 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); + }; + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' '); + + /** + * Creates an instance of fabric.Path from an SVG 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, fabric.Path.ATTRIBUTE_NAMES); + return new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options)); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + invoke = fabric.util.array.invoke, + parentToObject = fabric.Object.prototype.toObject, + camelize = fabric.util.string.camelize, + capitalize = fabric.util.string.capitalize; + + if (fabric.PathGroup) { + fabric.warn('fabric.PathGroup is already defined'); + return; + } + + /** + * @class PathGroup + * @extends fabric.Path + */ + fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @scope fabric.PathGroup.prototype */ { + + /** + * @property + * @type String + */ + type: 'path-group', + + /** + * @property + * @type String + */ + fill: '', + + /** + * @property + * @type Boolean + */ + forceFillOverwrite: false, + + /** + * Constructor + * @method initialize + * @param {Array} paths + * @param {Object} [options] Options object + * @return {fabric.PathGroup} thisArg + */ + initialize: function(paths, options) { + + options = options || { }; + this.paths = paths || [ ]; + + for (var i = this.paths.length; i--; ) { + this.paths[i].group = this; + } + + this.setOptions(options); + this.setCoords(); + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * Renders this group on a specified context + * @method render + * @param {CanvasRenderingContext2D} ctx Context to render this instance on + */ + render: function(ctx) { + 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(); + }, + + /** + * Sets certain property to a certain value + * @method _set + * @param {String} prop + * @param {Any} value + * @return {fabric.PathGroup} thisArg + */ + _set: function(prop, value) { + + if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) { + var i = this.paths.length; + while (i--) { + this.paths[i]._set(prop, value); + } + } + + return this.callSuper('_set', prop, value); + }, + + /** + * Returns object representation of this path group + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(parentToObject.call(this), { + paths: invoke(this.getObjects(), 'toObject'), + sourcePath: this.sourcePath + }); + }, + + /** + * Returns dataless object representation of this path group + * @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 svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objects = this.getObjects(); + var markup = [ + '' + ]; + + for (var i = 0, len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push(''); + + return markup.join(''); + }, + + /** + * Returns a string representation of this path group + * @method toString + * @return {String} string representation of an object + */ + toString: function() { + return '#'; + }, + + /** + * Returns true if all paths in this group are of same color + * @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; + }, + + /** + * Returns all paths in this path group + * @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; + } + + /** + * Creates fabric.Triangle instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.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; + } + + /** + * @class Group + * @extends fabric.Object + */ + fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { + + /** + * @property + * @type String + */ + type: 'group', + + /** + * Constructor + * @method initialized + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @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(); + + // group is active by default + 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(); + + // do not display corners of objects enclosed in a group + object.hideCorners = true; + }, this); + }, + + /** + * Returns string represenation of a group + * @method toString + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Returns an array of all objects in this group + * @method getObjects + * @return {Array} group objects + */ + getObjects: function() { + return this.objects; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @method addWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + this._restoreObjectsState(); + this.objects.push(object); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @method removeWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + removeFromArray(this.objects, object); + object.setActive(false); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Adds an object to a group + * @method add + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + add: function(object) { + this.objects.push(object); + return this; + }, + + /** + * Removes an object from a group + * @method remove + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + remove: function(object) { + removeFromArray(this.objects, object); + 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; + }, + + /** + * @private + */ + _set: function(key, value) { + if (key === 'fill' || key === 'opacity') { + var i = this.objects.length; + this[key] = value; + while (i--) { + this.objects[i].set(key, value); + } + } + else { + this[key] = value; + } + }, + + /** + * 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, 'toObject') + }); + }, + + /** + * Renders instance on a given context + * @method render + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx, noTransform) { + ctx.save(); + this.transform(ctx); + + var groupScaleFactor = Math.max(this.scaleX, this.scaleY); + + for (var i = 0, len = this.objects.length; i < len; i++) { + + var object = this.objects[i]; + var originalScaleFactor = object.borderScaleFactor; + + object.borderScaleFactor = groupScaleFactor; + object.render(ctx); + object.borderScaleFactor = originalScaleFactor; + } + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + this.setCoords(); + }, + + /** + * Returns object from the group at the specified index + * @method item + * @param index {Number} index of item to get + * @return {fabric.Object} + */ + item: function(index) { + return this.getObjects()[index]; + }, + + /** + * Returns complexity of an instance + * @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 (original state is that which was before group was created). + * @private + * @method _restoreObjectsState + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this.objects.forEach(this._restoreObjectState, this); + return this; + }, + + /** + * Restores original state of a specified object in group + * @private + * @method _restoreObjectState + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + + var groupLeft = this.get('left'), + groupTop = this.get('top'), + groupAngle = this.getAngle() * (Math.PI / 180), + 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; + }, + + /** + * Destroys a group (restoring state of its objects) + * @method destroy + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + return this._restoreObjectsState(); + }, + + /** + * Saves coordinates of this instance (to be used together with `hasMoved`) + * @saveCoords + * @return {fabric.Group} thisArg + * @chainable + */ + saveCoords: function() { + this._originalLeft = this.get('left'); + this._originalTop = this.get('top'); + return this; + }, + + /** + * Checks whether this group was moved (since `saveCoords` was called last) + * @method hasMoved + * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) + */ + 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() { + this.forEachObject(function(object) { + object.setActive(); + }); + return this; + }, + + /** + * Executes given function for each object in this group + * @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 Context (aka thisObject) + * + * @return {fabric.Group} thisArg + * @chainable + */ + forEachObject: fabric.StaticCanvas.prototype.forEachObject, + + /** + * @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) || 0; + height = (maxY - minY) || 0; + + this.width = width; + this.height = height; + + this.left = (minX + width / 2) || 0; + this.top = (minY + height / 2) || 0; + }, + + /** + * Checks if point is contained within the group + * @method containsPoint + * @param {fabric.Point} 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; + }, + + /** + * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) + * @method toGrayscale + */ + toGrayscale: function() { + var i = this.objects.length; + while (i--) { + this.objects[i].toGrayscale(); + } + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objectsMarkup = [ ]; + for (var i = 0, len = this.objects.length; i < len; i++) { + objectsMarkup.push(this.objects[i].toSVG()); + } + + return ( + '' + + objectsMarkup.join('') + + ''); + } + }); + + /** + * Returns fabric.Group instance from an object representation + * @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, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.Group(enlivenedObjects, object)); + }); + }; + + fabric.Group.async = true; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + if (!fabric.Object) { + fabric.warn('fabric.Object is required for fabric.Image initialization'); + return; + } + + /** + * @class Image + * @extends fabric.Object + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @scope fabric.Image.prototype */ { + + /** + * @property + * @type Boolean + */ + active: false, + + /** + * @property + * @type Boolean + */ + bordervisibility: false, + + /** + * @property + * @type Boolean + */ + cornervisibility: false, + + /** + * @property + * @type String + */ + type: 'image', + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} options optional + */ + initialize: function(element, options) { + options || (options = { }); + + this.callSuper('initialize', options); + this._initElement(element); + this._originalImage = this.getElement(); + this._initConfig(options); + + this.filters = [ ]; + + if (options.filters) { + this.filters = options.filters; + this.applyFilters(); + } + }, + + /** + * Returns image element which this instance if based on + * @method getElement + * @return {HTMLImageElement} image element + */ + getElement: function() { + return this._element; + }, + + /** + * Sets image element for this instance to a specified one + * @method setElement + * @param {HTMLImageElement} element + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element) { + this._element = element; + this._initConfig(); + return this; + }, + + /** + * Returns original size of an image + * @method getOriginalSize + * @return {Object} object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.width, + height: element.height + }; + }, + + /** + * Sets border visibility + * @method setBorderVisibility + * @param {Boolean} visible When true, border is set to be visible + */ + setBorderVisibility: function() { + this._resetWidthHeight(); + this._adjustWidthHeightToBorders(); + this.setCoords(); + }, + + /** + * Sets corner visibility + * @method setCornersVisibility + * @param {Boolean} visible When true, corners are set to be visible + */ + setCornersVisibility: function(visible) { + this.cornervisibility = !!visible; + }, + + /** + * Renders image on a specified context + * @method render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx, noTransform) { + ctx.save(); + var m = this.transformMatrix; + this._resetWidthHeight(); + if (this.group) { + ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); + } + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + this._render(ctx); + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + src: this._originalImage.src || this._originalImage._src, + filters: this.filters.concat() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return ''+ + ' element with actual transformation, then offsetting object to the top/left + // so that object's center aligns with container's left/top + 'transform="translate('+ (-this.width/2) + ' ' + (-this.height/2) + ')" ' + + 'width="' + this.width + '" ' + + 'height="' + this.height + '"' + '/>'+ + ''; + }, + + /** + * Returns source of an image + * @method getSrc + * @return {String} Source of an image + */ + getSrc: function() { + return this.getElement().src || this.getElement()._src; + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns a clone of an instance + * @mthod clone + * @param {Function} callback Callback is invoked with a clone as a first argument + */ + clone: function(callback) { + this.constructor.fromObject(this.toObject(), callback); + }, + + /** + * Applies filters assigned to this image (from "filters" array) + * @mthod applyFilters + * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated + */ + applyFilters: function(callback) { + + if (this.filters.length === 0) { + this.setElement(this._originalImage); + callback && callback(); + return; + } + + var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined', + imgEl = this._originalImage, + canvasEl = fabric.document.createElement('canvas'), + replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'), + _this = this; + + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + canvasEl.width = imgEl.width; + canvasEl.height = imgEl.height; + + canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); + + this.filters.forEach(function(filter) { + filter && filter.applyTo(canvasEl); + }); + + /** @ignore */ + replacement.onload = function() { + _this._element = replacement; + callback && callback(); + replacement.onload = canvasEl = imgEl = null; + }; + replacement.width = imgEl.width; + replacement.height = imgEl.height; + + if (isLikelyNode) { + var base64str = canvasEl.toDataURL('image/png').replace(/data:image\/png;base64,/, ''); + replacement.src = new Buffer(base64str, 'base64'); + _this._element = replacement; + + // onload doesn't fire in node, so we invoke callback manually + callback && callback(); + } + else { + replacement.src = canvasEl.toDataURL('image/png'); + } + + return this; + }, + + /** + * @private + */ + _render: function(ctx) { + ctx.drawImage( + this.getElement(), + - this.width / 2, + -this.height / 2, + this.width, + this.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) { + options || (options = { }); + this.setOptions(options); + this._setBorder(); + this._setWidthHeight(options); + }, + + /** + * @method _initFilters + * @param {Object} object Object with filters property + */ + _initFilters: function(object) { + if (object.filters && object.filters.length) { + this.filters = object.filters.map(function(filterObj) { + return filterObj && fabric.Image.filters[filterObj.type].fromObject(filterObj); + }); + } + }, + + /** + * @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 = 'width' in options + ? options.width + : ((this.getElement().width || 0) + sidesBorderWidth); + + this.height = 'height' in options + ? options.height + : ((this.getElement().height || 0) + sidesBorderWidth); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + */ + fabric.Image.CSS_CANVAS = "canvas-img"; + + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @method fromObject + * @param object {Object} + * @param callback {Function} optional + */ + fabric.Image.fromObject = function(object, callback) { + var img = fabric.document.createElement('img'), + src = object.src; + + if (object.width) { + img.width = object.width; + } + if (object.height) { + img.height = object.height; + } + + /** @ignore */ + img.onload = function() { + fabric.Image.prototype._initFilters.call(object, object); + + var instance = new fabric.Image(img, object); + callback && callback(instance); + img = img.onload = null; + }; + img.src = src; + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @method fromURL + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + var img = fabric.document.createElement('img'); + + /** @ignore */ + img.onload = function() { + if (callback) { + callback(new fabric.Image(img, imgOptions)); + } + img = img.onload = null; + }; + img.src = url; + }; + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/struct.html#ImageElement + */ + fabric.Image.ATTRIBUTE_NAMES = 'x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href'.split(' '); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @method fabric.Image.fromElement + * @param {SVGElement} element Element to parse + * @param {Function} callback Callback to execute when fabric.Image object is created + * @param {Object} [options] Options object + * @return {fabric.Image} + */ + fabric.Image.fromElement = function(element, callback, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend(parsedAttributes, options)); + }; + + fabric.Image.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + +fabric.util.object.extend(fabric.Object.prototype, { + + /** + * @method _getAngleValueForStraighten + * @return {Number} angle value + * @private + */ + _getAngleValueForStraighten: function() { + var angle = this.get('angle'); + + // TODO (kangax): can this be simplified? + + 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; + }, + + /** + * @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; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Straightens object, then rerenders canvas + * @method straightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.renderAll(); + return this; + }, + + /** + * Same as `fabric.Canvas#straightenObject`, but animated + * @method fxStraightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.renderAll.bind(this) + }); + return this; + } +}); +/** + * @namespace + */ +fabric.Image.filters = { }; + +/** + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Grayscale = fabric.util.createClass( /** @scope fabric.Image.filters.Grayscale.prototype */ { + + /** + * @param {String} type + */ + type: "Grayscale", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Grayscale.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Grayscale.fromObject = function() { + return new fabric.Image.filters.Grayscale(); +}; + +/** + * @class fabric.Image.filters.RemoveWhite + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.RemoveWhite = fabric.util.createClass( /** @scope fabric.Image.filters.RemoveWhite.prototype */ { + + /** + * @param {String} type + */ + type: "RemoveWhite", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 30; + this.distance = options.distance || 20; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + distance = this.distance, + limit = 255 - threshold, + abs = Math.abs, + r, g, b; + + for (var i = 0, len = data.length; i < len; i += 4) { + + r = data[i]; + g = data[i+1]; + b = data[i+2]; + + if (r > limit && + g > limit && + b > limit && + abs(r-g) < distance && + abs(r-b) < distance && + abs(g-b) < distance) { + + data[i+3] = 1; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold, + distance: this.distance + }; + } +}); + +fabric.Image.filters.RemoveWhite.fromObject = function(object) { + return new fabric.Image.filters.RemoveWhite(object); +}; + +/** + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Invert = fabric.util.createClass( /** @scope fabric.Image.filters.Invert.prototype */ { + + /** + * @param {String} type + */ + type: "Invert", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Invert.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i; + + for (i = 0; i < iLen; i+=4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Invert.fromObject = function() { + return new fabric.Image.filters.Invert(); +}; + +/** + * @class fabric.Image.filters.Sepia + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, avg; + + for (i = 0; i < iLen; i+=4) { + avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; + data[i] = avg + 100; + data[i + 1] = avg + 50; + data[i + 2] = avg + 255; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia.fromObject = function() { + return new fabric.Image.filters.Sepia(); +}; + +/** + * @class fabric.Image.filters.Sepia2 + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia2 = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia2.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia2", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, r, g, b; + + for (i = 0; i < iLen; i+=4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; + data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; + data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia2.fromObject = function() { + return new fabric.Image.filters.Sepia2(); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Brightness = fabric.util.createClass( /** @scope fabric.Image.filters.Brightness.prototype */ { + + /** + * @param {String} type + */ + type: "Brightness", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.brightness = options.brightness || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + brightness = this.brightness; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i] += brightness; + data[i + 1] += brightness; + data[i + 2] += brightness; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + brightness: this.brightness + }; + } +}); + +fabric.Image.filters.Brightness.fromObject = function(object) { + return new fabric.Image.filters.Brightness(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Noise = fabric.util.createClass( /** @scope fabric.Image.filters.Noise.prototype */ { + + /** + * @param {String} type + */ + type: "Noise", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.noise = options.noise || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + noise = this.noise, rand; + + for (var i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + noise: this.noise + }; + } +}); + +fabric.Image.filters.Noise.fromObject = function(object) { + return new fabric.Image.filters.Noise(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.GradientTransparency = fabric.util.createClass( /** @scope fabric.Image.filters.GradientTransparency.prototype */ { + + /** + * @param {String} type + */ + type: "GradientTransparency", + + /** + * @memberOf fabric.Image.filters.GradientTransparency.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + total = data.length; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = threshold + 255 * (total - i) / total; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold + }; + } +}); + +fabric.Image.filters.GradientTransparency.fromObject = function(object) { + return new fabric.Image.filters.GradientTransparency(object); +}; + +/** + * @class fabric.Image.filters.Tint + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Tint = fabric.util.createClass( /** @scope fabric.Image.filters.Tint.prototype */ { + + /** + * @param {String} type + */ + type: "Tint", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.color = options.color || 0; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, a; + + var rgb = parseInt(this.color, 10).toString(16); + + var cr = parseInt('0x' + rgb.substr(0, 2), 16); + var cg = parseInt('0x' + rgb.substr(2, 2), 16); + var cb = parseInt('0x' + rgb.substr(4, 2), 16); + + for (i = 0; i < iLen; i+=4) { + + a = data[i+3]; + + if (a > 0){ + data[i] = cr; + data[i+1] = cg; + data[i+2] = cb; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + color: this.color + }; + } +}); + +fabric.Image.filters.Tint.fromObject = function(object) { + return new fabric.Image.filters.Tint(object); +}; +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + if (!fabric.Object) { + fabric.warn('fabric.Text requires fabric.Object'); + return; + } + + /** + * @class Text + * @extends fabric.Object + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ { + + /** + * @property + * @type Number + */ + fontSize: 40, + + /** + * @property + * @type Number + */ + fontWeight: 100, + + /** + * @property + * @type String + */ + fontFamily: 'Times New Roman', + + /** + * @property + * @type String + */ + textDecoration: '', + + /** + * @property + * @type String | null + */ + textShadow: '', + + /** + * Determines text alignment. Possible values: "left", "center", or "right". + * @property + * @type String + */ + textAlign: 'left', + + /** + * @property + * @type String + */ + fontStyle: '', + + /** + * @property + * @type Number + */ + lineHeight: 1.3, + + /** + * @property + * @type String + */ + strokeStyle: '', + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type String + */ + backgroundColor: '', + + + /** + * @property + * @type String | null + */ + path: null, + + /** + * @property + * @type String + */ + type: 'text', + + /** + * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) + * @property + * @type Boolean + */ + useNative: true, + + /** + * Constructor + * @method initialize + * @param {String} text + * @param {Object} [options] + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this._initStateProperties(); + this.text = text; + this.setOptions(options || { }); + this._theta = this.angle * Math.PI / 180; + this._initDimensions(); + this.setCoords(); + }, + + /** + * Renders text object on offscreen canvas, so that it would get dimensions + * @private + * @method _initDimensions + */ + _initDimensions: function() { + var canvasEl = fabric.document.createElement('canvas'); + + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + this._render(canvasEl.getContext('2d')); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Text` -specific ones to it + * (such as "fontFamily", "fontWeight", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(); + this.stateProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'path', + 'text', + 'textDecoration', + 'textShadow', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'strokeStyle', + 'strokeWidth', + 'backgroundColor', + 'useNative' + ); + fabric.util.removeFromArray(this.stateProperties, 'width'); + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (typeof Cufon === 'undefined' || this.useNative === true) { + this._renderViaNative(ctx); + } + else { + this._renderViaCufon(ctx); + } + }, + + /** + * @private + * @method _renderViaCufon + */ + _renderViaCufon: function(ctx) { + var o = Cufon.textOptions || (Cufon.textOptions = { }); + + // export options to be used by cufon.js + o.left = this.left; + o.top = this.top; + o.context = ctx; + o.color = this.fill; + + var el = this._initDummyElementForCufon(); + + // set "cursor" to top/left corner + this.transform(ctx); + + // draw text + Cufon.replaceElement(el, { + engine: 'canvas', + separate: 'none', + fontFamily: this.fontFamily, + fontWeight: this.fontWeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor + }); + + // update width, height + this.width = o.width; + this.height = o.height; + + this._totalLineHeight = o.totalLineHeight; + this._fontAscent = o.fontAscent; + this._boundaries = o.boundaries; + this._shadowOffsets = o.shadowOffsets; + this._shadows = o.shadows || [ ]; + + el = null; + + // need to set coords _after_ the width/height was retreived from Cufon + this.setCoords(); + }, + + /** + * @private + * @method _render_native + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderViaNative: function(ctx) { + + this.transform(ctx); + this._setTextStyles(ctx); + + var textLines = this.text.split(/\r?\n/); + + this.width = this._getTextWidth(ctx, textLines); + this.height = this._getTextHeight(ctx, textLines); + + this._renderTextBackground(ctx, textLines); + + if (this.textAlign !== 'left') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + + this._setTextShadow(ctx); + this._renderTextFill(ctx, textLines); + this.textShadow && ctx.restore(); + + this._renderTextStroke(ctx, textLines); + if (this.textAlign !== 'left') { + ctx.restore(); + } + + this._renderTextDecoration(ctx, textLines); + this._setBoundaries(ctx, textLines); + this._totalLineHeight = 0; + + this.setCoords(); + }, + + /** + * @private + * @method _setBoundaries + */ + _setBoundaries: function(ctx, textLines) { + this._boundaries = [ ]; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + this._boundaries.push({ + height: this.fontSize, + width: lineWidth, + left: lineLeftOffset + }); + } + }, + + /** + * @private + * @method _setTextStyles + */ + _setTextStyles: function(ctx) { + ctx.fillStyle = this.fill; + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.textBaseline = 'top'; + ctx.textAlign = this.textAlign; + ctx.font = this._getFontDeclaration(); + }, + + /** + * @private + * @method _getTextHeight + */ + _getTextHeight: function(ctx, textLines) { + return this.fontSize * textLines.length * this.lineHeight; + }, + + /** + * @private + * @method _getTextWidth + */ + _getTextWidth: function(ctx, textLines) { + var maxWidth = ctx.measureText(textLines[0]).width; + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = ctx.measureText(textLines[i]).width; + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @method _setTextShadow + */ + _setTextShadow: function(ctx) { + if (this.textShadow) { + + // "rgba(0,0,0,0.2) 2px 2px 10px" + // "rgb(0, 100, 0) 0 0 5px" + // "red 2px 2px 1px" + // "#f55 123 345 567" + var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; + + var shadowDeclaration = this.textShadow; + var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); + var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); + + ctx.save(); + ctx.shadowColor = shadowColor; + ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); + ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); + ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); + + this._shadows = [{ + blur: ctx.shadowBlur, + color: ctx.shadowColor, + offX: ctx.shadowOffsetX, + offY: ctx.shadowOffsetY + }]; + + this._shadowOffsets = [[ + parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) + ]]; + } + }, + + _renderTextFill: function(ctx, textLines) { + this._boundaries = [ ]; + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.fillText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + }, + + /** + * @private + * @method _renderTextStroke + */ + _renderTextStroke: function(ctx, textLines) { + if (this.strokeStyle) { + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.strokeText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + } + }, + + /** + * @private + * @_renderTextBackground + */ + _renderTextBackground: function(ctx, textLines) { + if (this.backgroundColor) { + ctx.save(); + ctx.fillStyle = this.backgroundColor; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-this.width / 2) + lineLeftOffset, + (-this.height / 2) + (i * this.fontSize * this.lineHeight), + lineWidth, + this.fontSize + ); + } + ctx.restore(); + } + }, + + /** + * @private + * @method _getLineLeftOffset + */ + _getLineLeftOffset: function(lineWidth) { + if (this.textAlign === 'center') { + return (this.width - lineWidth) / 2; + } + if (this.textAlign === 'right') { + return this.width - lineWidth; + } + return 0; + }, + + /** + * @private + * @method _renderTextDecoration + */ + _renderTextDecoration: function(ctx, textLines) { + + var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; + var _this = this; + + function renderLinesAtOffset(offset) { + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = _this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-_this.width / 2) + lineLeftOffset, + (offset + (i * _this.fontSize * _this.lineHeight)) - halfOfVerticalBox, + lineWidth, + 1); + } + } + + if (this.textDecoration.indexOf('underline') > -1) { + renderLinesAtOffset(this.fontSize); + } + if (this.textDecoration.indexOf('line-through') > -1) { + renderLinesAtOffset(this.fontSize / 2); + } + if (this.textDecoration.indexOf('overline') > -1) { + renderLinesAtOffset(0); + } + }, + + /** + * @private + * @method _getFontDeclaration + */ + _getFontDeclaration: function() { + return [ + this.fontStyle, + this.fontWeight, + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + + /** + * @private + * @method _initDummyElement + */ + _initDummyElementForCufon: function() { + var el = fabric.document.createElement('pre'), + container = fabric.document.createElement('div'); + + // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent + container.appendChild(el); + + if (typeof G_vmlCanvasManager === 'undefined') { + el.innerHTML = this.text; + } + else { + // IE 7 & 8 drop newlines and white space on text nodes + // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html + // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp + el.innerText = this.text.replace(/\r?\n/gi, '\r'); + } + + el.style.fontSize = this.fontSize + 'px'; + el.style.letterSpacing = 'normal'; + + return el; + }, + + /** + * Renders text instance on a specified context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + render: function(ctx, noTransform) { + ctx.save(); + this._render(ctx); + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of text object + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + text: this.text, + fontSize: this.fontSize, + fontWeight: this.fontWeight, + fontFamily: this.fontFamily, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + path: this.path, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor, + useNative: this.useNative + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var textLines = this.text.split(/\r?\n/), + lineTopOffset = this.useNative + ? this.fontSize * this.lineHeight + : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), + + textLeftOffset = -(this.width/2), + textTopOffset = this.useNative + ? this.fontSize - 1 + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + + textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), + shadowSpans = this._getSVGShadows(lineTopOffset, textLines); + + // move top offset by an ascent + textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + return [ + '', + textAndBg.textBgRects.join(''), + '', + shadowSpans.join(''), + textAndBg.textSpans.join(''), + '', + '' + ].join(''); + }, + + _getSVGShadows: function(lineTopOffset, textLines) { + var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; + + if (!this._shadows || !this._boundaries) { + return shadowSpans; + } + + for (j = 0, jlen = this._shadows.length; j < jlen; j++) { + for (i = 0, ilen = textLines.length; i < ilen; i++) { + if (textLines[i] !== '') { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; + shadowSpans.push( + '', + fabric.util.string.escapeXml(textLines[i]), + ''); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + } + } + return shadowSpans; + }, + + _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { + var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; + + // text and background + for (i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; + textSpans.push( + ' elements since setting opacity on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLines[i]), + '' + ); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.backgroundColor || !this._boundaries) continue; + + textBgRects.push( + ''); + } + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + // Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + // we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + _getFillAttributes: function(value) { + var fillColor = value ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @method setColor + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setColor: function(value) { + this.set('fill', value); + return this; + }, + + /** + * Sets fontSize of an instance and updates its coordinates + * @method setFontsize + * @param {Number} value + * @return {fabric.Text} thisArg + * @chainable + */ + setFontsize: function(value) { + this.set('fontSize', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Returns actual text value of an instance + * @method getText + * @return {String} + */ + getText: function() { + return this.text; + }, + + /** + * Sets text of an instance, and updates its coordinates + * @method setText + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setText: function(value) { + this.set('text', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Sets specified property to a specified value + * @method set + * @param {String} name + * @param {Any} value + * @return {fabric.Text} thisArg + * @chainable + */ + _set: function(name, value) { + if (name === 'fontFamily' && this.path) { + this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); + } + this.callSuper('_set', name, value); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Text.fromElement`) + * @static + */ + fabric.Text.ATTRIBUTE_NAMES = + ('x y fill fill-opacity opacity stroke stroke-width transform ' + + 'font-family font-style font-weight font-size text-decoration').split(' '); + + /** + * Returns fabric.Text instance from an object representation + * @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)); + }; + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @method fabric.Text.fromElement + * @param element + * @param options + * @return {fabric.Text} an instance + */ + fabric.Text.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); + options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); + var text = new fabric.Text(element.textContent, options); + + return text; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function() { + + if (typeof document !== 'undefined' && typeof window !== 'undefined') { + return; + } + + var DOMParser = new require('xmldom').DOMParser, + URL = require('url'), + HTTP = require('http'), + + Canvas = require('canvas'), + Image = require('canvas').Image; + + function request(url, encoding, callback) { + var oURL = URL.parse(url), + client = HTTP.createClient(oURL.port, oURL.hostname), + req = client.request('GET', oURL.pathname, { 'host': oURL.hostname }); + + client.addListener('error', function(err) { + if (err.errno === process.ECONNREFUSED) { + fabric.log('ECONNREFUSED: connection refused to ' + client.host + ':' + client.port); + } + else { + fabric.log(err.message); + } + }); + + req.end(); + req.on('response', function (response) { + var body = ""; + if (encoding) { + response.setEncoding(encoding); + } + response.on('end', function () { + callback(body); + }); + response.on('data', function (chunk) { + if (response.statusCode === 200) { + body += chunk; + } + }); + }); + } + + fabric.util.loadImage = function(url, callback) { + request(url, 'binary', function(body) { + var img = new Image(); + img.src = new Buffer(body, 'binary'); + // preserving original url, which seems to be lost in node-canvas + img._src = url; + callback(img); + }); + }; + + fabric.loadSVGFromURL = function(url, callback) { + url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); + request(url, '', function(body) { + fabric.loadSVGFromString(body, callback); + }); + }; + + fabric.loadSVGFromString = function(string, callback) { + var doc = new DOMParser().parseFromString(string); + fabric.parseSVGDocument(doc.documentElement, function(results, options) { + callback(results, options); + }); + }; + + fabric.util.getScript = function(url, callback) { + request(url, '', function(body) { + eval(body); + callback && callback(); + }); + }; + + fabric.Image.fromObject = function(object, callback) { + fabric.util.loadImage(object.src, function(img) { + var oImg = new fabric.Image(img); + + oImg._initConfig(object); + oImg._initFilters(object); + callback(oImg); + }); + }; + + /** + * Only available when running fabric on node.js + * @method createCanvasForNode + * @param width Canvas width + * @param height Canvas height + * @return {Object} wrapped canvas instance + */ + fabric.createCanvasForNode = function(width, height) { + + var canvasEl = fabric.document.createElement('canvas'), + nodeCanvas = new Canvas(width || 600, height || 600); + + // jsdom doesn't create style on canvas element, so here be temp. workaround + canvasEl.style = { }; + + canvasEl.width = nodeCanvas.width; + canvasEl.height = nodeCanvas.height; + + var FabricCanvas = fabric.Canvas || fabric.StaticCanvas; + var fabricCanvas = new FabricCanvas(canvasEl); + fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); + fabricCanvas.nodeCanvas = nodeCanvas; + + return fabricCanvas; + }; + + fabric.StaticCanvas.prototype.createPNGStream = function() { + return this.nodeCanvas.createPNGStream(); + }; + + var origSetWidth = fabric.StaticCanvas.prototype.setWidth; + fabric.StaticCanvas.prototype.setWidth = function(width) { + origSetWidth.call(this); + this.nodeCanvas.width = width; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; + } + + var origSetHeight = fabric.StaticCanvas.prototype.setHeight; + fabric.StaticCanvas.prototype.setHeight = function(height) { + origSetHeight.call(this); + this.nodeCanvas.height = height; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; + } + +})(); diff --git a/src/object.class.js b/src/object.class.js index 99d22d75..c825aa4e 100644 --- a/src/object.class.js +++ b/src/object.class.js @@ -1,1729 +1,1734 @@ -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - toFixed = fabric.util.toFixed, - capitalize = fabric.util.string.capitalize, - getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians, - slice = Array.prototype.slice; - - if (fabric.Object) { - return; - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { - - /** - * Type of an object (rect, circle, path, etc) - * @property - * @type String - */ - type: 'object', - - /** - * @property - * @type Number - */ - top: 0, - - /** - * @property - * @type Number - */ - left: 0, - - /** - * @property - * @type Number - */ - width: 0, - - /** - * @property - * @type Number - */ - height: 0, - - /** - * @property - * @type Number - */ - scaleX: 1, - - /** - * @property - * @type Number - */ - scaleY: 1, - - /** - * @property - * @type Boolean - */ - flipX: false, - - /** - * @property - * @type Boolean - */ - flipY: false, - - /** - * @property - * @type Number - */ - opacity: 1, - - /** - * @property - * @type Number - */ - angle: 0, - - /** - * @property - * @type Number - */ - cornersize: 12, - - /** - * @property - * @type Boolean - */ - transparentCorners: true, - - /** - * @property - * @type Number - */ - padding: 0, - - /** - * @property - * @type String - */ - borderColor: 'rgba(102,153,255,0.75)', - - /** - * @property - * @type String - */ - cornerColor: 'rgba(102,153,255,0.5)', - - /** - * @property - * @type String - */ - fill: 'rgb(0,0,0)', - - /** - * @property - * @type String - */ - fillRule: 'source-over', - - /** - * @property - * @type String - */ - overlayFill: null, - - /** - * @property - * @type String - */ - stroke: null, - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type Array - */ - strokeDashArray: null, - - /** - * @property - * @type Number - */ - borderOpacityWhenMoving: 0.4, - - /** - * @property - * @type Number - */ - borderScaleFactor: 1, - - /** - * Transform matrix - * @property - * @type Array - */ - transformMatrix: null, - - /** - * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) - * @property - * @type Boolean - */ - selectable: true, - - /** - * When set to `false`, object's controls are not displayed and can not be used to manipulate object - * @property - * @type Boolean - */ - hasControls: true, - - /** - * When set to `false`, object's borders are not rendered - * @property - * @type Boolean - */ - hasBorders: true, - - /** - * When set to `false`, object's rotating point will not be visible or selectable - * @property - * @type Boolean - */ - hasRotatingPoint: false, - - /** - * Offset for object's rotating point (when enabled) - * @property - * @type Number - */ - rotatingPointOffset: 40, - - /** - * @private - * @property - * @type Number - */ - _theta: 0, - - perPixelTargetFind: false, - - includeDefaultValues: true, - - /** - * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); - * as well as for history (undo/redo) purposes - * @property - * @type Array - */ - stateProperties: ( - 'top left width height scaleX scaleY flipX flipY ' + - 'theta angle opacity cornersize fill overlayFill ' + - 'stroke strokeWidth strokeDashArray fillRule ' + - 'borderScaleFactor transformMatrix selectable' - ).split(' '), - - /** - * @method callSuper - * @param {String} methodName - */ - callSuper: function(methodName) { - var fn = this.constructor.superclass.prototype[methodName]; - return (arguments.length > 1) - ? fn.apply(this, slice.call(arguments, 1)) - : fn.call(this); - }, - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - */ - initialize: function(options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * @method initGradient - */ - _initGradient: function(options) { - if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { - this.set('fill', new fabric.Gradient(options.fill)); - } - }, - - /** - * @method setOptions - * @param {Object} [options] - */ - setOptions: function(options) { - var i = this.stateProperties.length, prop; - while (i--) { - prop = this.stateProperties[i]; - if (prop in options) { - this.set(prop, options[prop]); - } - } - this._initGradient(options); - }, - - /** - * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var object = { - type: this.type, - left: toFixed(this.left, NUM_FRACTION_DIGITS), - top: toFixed(this.top, NUM_FRACTION_DIGITS), - width: toFixed(this.width, NUM_FRACTION_DIGITS), - height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, - overlayFill: this.overlayFill, - stroke: this.stroke, - strokeWidth: this.strokeWidth, - strokeDashArray: this.strokeDashArray, - scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), - scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), - flipX: this.flipX, - flipY: this.flipY, - opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), - selectable: this.selectable, - hasControls: this.hasControls, - hasBorders: this.hasBorders, - hasRotatingPoint: this.hasRotatingPoint, - transparentCorners: this.transparentCorners, - perPixelTargetFind: this.perPixelTargetFind - }; - - if (!this.includeDefaultValues) { - object = this._removeDefaultValues(object); - } - - return object; - }, - - /** - * Returns (dataless) object representation of an instance - * @method toDatalessObject - */ - toDatalessObject: function() { - // will be overwritten by subclasses - return this.toObject(); - }, - - /** - * Returns styles-string for svg-export - * @method getSvgStyles - * @return {string} - */ - getSvgStyles: function() { - return [ - "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", - "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", - "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), - "fill: ", (this.fill ? this.fill : 'none'), "; ", - "opacity: ", (this.opacity ? this.opacity : '1'), ";" - ].join(""); - }, - - /** - * Returns transform-string for svg-export - * @method getSvgTransform - * @return {string} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - return [ - "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", - angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', - (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") - ].join(''); - }, - - /** - * @private - * @method _removeDefaultValues - */ - _removeDefaultValues: function(object) { - var defaultOptions = fabric.Object.prototype.options; - if (defaultOptions) { - 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 "#"; - }, - - /** - * Sets property to a given value - * @method set - * @param {String} name - * @param {Object|Function} value - * @return {fabric.Group} thisArg - * @chainable - */ - set: function(key, value) { - if (typeof key === 'object') { - for (var prop in key) { - this._set(prop, key[prop]); - } - } - else { - if (typeof value === 'function') { - this._set(key, value(this.get(key))); - } - else { - this._set(key, value); - } - } - return this; - }, - - _set: function(key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && - value < fabric.Object.MIN_SCALE_LIMIT; - - if (shouldConstrainValue) { - value = fabric.Object.MIN_SCALE_LIMIT; - } - if (key === 'angle') { - this.setAngle(value); - } - else { - this[key] = value; - } - }, - - /** - * 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) { - - // do not render if width or height are zeros - if (this.width === 0 || this.height === 0) return; - - ctx.save(); - - var m = this.transformMatrix; - if (m && !this.group) { - ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - if (!noTransform) { - this.transform(ctx); - } - - if (this.stroke || this.strokeDashArray) { - ctx.lineWidth = this.strokeWidth; - ctx.strokeStyle = this.stroke; - } - - if (this.overlayFill) { - ctx.fillStyle = this.overlayFill; - } - else if (this.fill) { - ctx.fillStyle = this.fill.toLiveGradient - ? this.fill.toLiveGradient(ctx) - : this.fill; - } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._render(ctx, noTransform); - - if (this.active && !noTransform) { - this.drawBorders(ctx); - 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; - this.setCoords(); - return this; - }, - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @method scaleToWidth - * @param value {Number} new width value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @method scaleToHeight - * @param value {Number} new height value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, - - /** - * 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() { - - var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, - padding = this.padding; - - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + slice = Array.prototype.slice; + + if (fabric.Object) { + return; + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc) + * @property + * @type String + */ + type: 'object', + + /** + * @property + * @type Number + */ + top: 0, + + /** + * @property + * @type Number + */ + left: 0, + + /** + * @property + * @type Number + */ + width: 0, + + /** + * @property + * @type Number + */ + height: 0, + + /** + * @property + * @type Number + */ + scaleX: 1, + + /** + * @property + * @type Number + */ + scaleY: 1, + + /** + * @property + * @type Boolean + */ + flipX: false, + + /** + * @property + * @type Boolean + */ + flipY: false, + + /** + * @property + * @type Number + */ + opacity: 1, + + /** + * @property + * @type Number + */ + angle: 0, + + /** + * @property + * @type Number + */ + cornersize: 12, + + /** + * @property + * @type Boolean + */ + transparentCorners: true, + + /** + * @property + * @type Number + */ + padding: 0, + + /** + * @property + * @type String + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * @property + * @type String + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * @property + * @type String + */ + fill: 'rgb(0,0,0)', + + /** + * @property + * @type String + */ + fillRule: 'source-over', + + /** + * @property + * @type String + */ + overlayFill: null, + + /** + * @property + * @type String + */ + stroke: null, + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type Array + */ + strokeDashArray: null, + + /** + * @property + * @type Number + */ + borderOpacityWhenMoving: 0.4, + + /** + * @property + * @type Number + */ + borderScaleFactor: 1, + + /** + * Transform matrix + * @property + * @type Array + */ + transformMatrix: null, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) + * @property + * @type Boolean + */ + selectable: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @property + * @type Boolean + */ + hasControls: true, + + /** + * When set to `false`, object's borders are not rendered + * @property + * @type Boolean + */ + hasBorders: true, + + /** + * When set to `false`, object's rotating point will not be visible or selectable + * @property + * @type Boolean + */ + hasRotatingPoint: false, + + /** + * Offset for object's rotating point (when enabled) + * @property + * @type Number + */ + rotatingPointOffset: 40, + + /** + * @private + * @property + * @type Number + */ + _theta: 0, + + perPixelTargetFind: false, + + includeDefaultValues: true, + + /** + * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); + * as well as for history (undo/redo) purposes + * @property + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY ' + + 'theta angle opacity cornersize fill overlayFill ' + + 'stroke strokeWidth strokeDashArray fillRule ' + + 'borderScaleFactor transformMatrix selectable' + ).split(' '), + + /** + * @method callSuper + * @param {String} methodName + */ + callSuper: function(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + }, + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * @method initGradient + */ + _initGradient: function(options) { + if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @method setOptions + * @param {Object} [options] + */ + setOptions: function(options) { + var i = this.stateProperties.length, prop; + while (i--) { + prop = this.stateProperties[i]; + if (prop in options) { + this.set(prop, options[prop]); + } + } + this._initGradient(options); + }, + + /** + * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + overlayFill: this.overlayFill, + stroke: this.stroke, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray, + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + selectable: this.selectable, + hasControls: this.hasControls, + hasBorders: this.hasBorders, + hasRotatingPoint: this.hasRotatingPoint, + transparentCorners: this.transparentCorners, + perPixelTargetFind: this.perPixelTargetFind + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @method toDatalessObject + */ + toDatalessObject: function() { + // will be overwritten by subclasses + return this.toObject(); + }, + + /** + * Returns styles-string for svg-export + * @method getSvgStyles + * @return {string} + */ + getSvgStyles: function() { + return [ + "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", + "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), + "fill: ", (this.fill ? this.fill : 'none'), "; ", + "opacity: ", (this.opacity ? this.opacity : '1'), ";" + ].join(""); + }, + + /** + * Returns transform-string for svg-export + * @method getSvgTransform + * @return {string} + */ + getSvgTransform: function() { + var angle = this.getAngle(); + return [ + "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", + angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', + (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") + ].join(''); + }, + + /** + * @private + * @method _removeDefaultValues + */ + _removeDefaultValues: function(object) { + var defaultOptions = fabric.Object.prototype.options; + if (defaultOptions) { + 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 "#"; + }, + + /** + * Sets property to a given value + * @method set + * @param {String} name + * @param {Object|Function} value + * @return {fabric.Group} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && + value < fabric.Object.MIN_SCALE_LIMIT; + + if (shouldConstrainValue) { + value = fabric.Object.MIN_SCALE_LIMIT; + } + if (key === 'angle') { + this.setAngle(value); + } + else { + this[key] = value; + } + }, + + /** + * 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) { + + // do not render if width or height are zeros + if (this.width === 0 || this.height === 0) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + if (!noTransform) { + this.transform(ctx); + } + + if (this.stroke || this.strokeDashArray) { + ctx.lineWidth = this.strokeWidth; + ctx.strokeStyle = this.stroke; + } + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : this.fill; + } + + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._render(ctx, noTransform); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + 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; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @method scaleToWidth + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @method scaleToHeight + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * 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() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; - this._hypotenuse = Math.sqrt( - Math.pow(this.currentWidth / 2, 2) + - Math.pow(this.currentHeight / 2, 2)); - - this._angle = Math.atan(this.currentHeight / this.currentWidth); - - // offset added for rotate and scale actions - 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) - }; - var mtr = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }; - - // debugging - - // setTimeout(function() { - // canvas.contextTop.fillStyle = 'green'; - // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); - // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); - // canvas.contextTop.fillRect(br.x, br.y, 3, 3); - // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); - // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); - // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); - // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); - // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); - // }, 50); - - // clockwise - this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; - - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this._setCornerCoords(); - - return this; - }, - - /** - * Returns width of an object's bounding rectangle - * @method getBoundingRectWidth - * @return {Number} width value - */ - getBoundingRectWidth: function() { - this.oCoords || this.setCoords(); - var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; - var minX = fabric.util.array.min(xCoords); - var maxX = fabric.util.array.max(xCoords); - return Math.abs(minX - maxX); - }, - - /** - * Returns height of an object's bounding rectangle - * @method getBoundingRectHeight - * @return {Number} height value - */ - getBoundingRectHeight: function() { - this.oCoords || this.setCoords(); - var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; - var minY = fabric.util.array.min(yCoords); - var maxY = fabric.util.array.max(yCoords); - return Math.abs(minY - maxY); - }, - - /** - * 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) { - if (!this.hasBorders) return; - - var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, - padding = this.padding, - padding2 = padding * 2, - strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; - - ctx.save(); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = this.borderColor; - - var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), - scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX), - ~~(h + padding2 + strokeWidth * this.scaleY) - ); - - if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { - - var rotateHeight = ( - this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) - ) / 2; - - ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); - ctx.closePath(); - ctx.stroke(); - } - - ctx.restore(); - return this; - }, - - _renderDashedStroke: function(ctx) { - - if (1 & this.strokeDashArray.length /* if odd number of items */) { - /* duplicate items */ - this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); - } - - var i = 0, - x = -this.width/2, y = -this.height/2, - _this = this, - padding = this.padding, - dashedArrayLength = this.strokeDashArray.length; - - ctx.save(); - ctx.beginPath(); - - function renderSide(xMultiplier, yMultiplier) { - - var lineLength = 0, - lengthDiff = 0, - sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; - - while (lineLength < sideLength) { - - var lengthOfSubPath = _this.strokeDashArray[i++]; - lineLength += lengthOfSubPath; - - if (lineLength > sideLength) { - lengthDiff = lineLength - sideLength; - } - - // track coords - if (xMultiplier) { - x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); - } - else { - y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); - } - - ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); - if (i >= dashedArrayLength) { - i = 0; - } - } - } - - renderSide(1, 0); - renderSide(0, 1); - renderSide(-1, 0); - renderSide(0, -1); - - ctx.stroke(); - ctx.closePath(); - ctx.restore(); - }, - - /** - * 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) { - if (!this.hasControls) return; - - var size = this.cornersize, - size2 = size / 2, - strokeWidth2 = this.strokeWidth / 2, - left = -(this.width / 2), - top = -(this.height / 2), - _left, - _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; - - ctx.save(); - - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = ctx.fillStyle = this.cornerColor; - - // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - if (!this.lockUniScaling) { - // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - } - - // middle-top-rotate - if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffsetX; - - _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_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(); - - /** @ignore */ - 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') - }; - - // normalize angle - this.set('angle', 0).set('flipX', false).set('flipY', false); - this.toDataURL(function(dataURL) { - i.src = dataURL; - }); - } - return this; - }, - - /** - * Converts an object into a data-url-like string - * @method toDataURL - * @return {String} string of data - */ - toDataURL: function(callback) { - var el = fabric.document.createElement('canvas'); - if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(el); - } - - el.width = this.getBoundingRectWidth(); - el.height = this.getBoundingRectHeight(); - - fabric.util.wrapElement(el, 'div'); - - var canvas = new fabric.Canvas(el); - canvas.backgroundColor = 'transparent'; - canvas.renderAll(); - - if (this.constructor.async) { - this.clone(proceed); - } - else { - proceed(this.clone()); - } - - function proceed(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; - - callback && callback(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; - }, - - /** - * @method setupState - */ - setupState: function() { - this.originalState = { }; - this.saveState(); - }, - - /** - * 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) { - // extracts coords - 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 of another object - * @method isContainedWithinObject - * @param {Object} other Object to test - * @return {Boolean} - */ - isContainedWithinObject: function(other) { - return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); - }, - - /** - * 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); - - 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) { - if (!this.hasControls || !this.active) return false; - - var pointer = getPointer(e), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, - xpoints, - lines; - - for (var i in this.oCoords) { - - if (i === 'mtr' && !this.hasRotatingPoint) { - continue; - } - - if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { - continue; - } - - lines = this._getImageLines(this.oCoords[i].corner, i); - - // debugging - - // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - - 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]; - // optimisation 1: line below dot. no cross - if ((iLine.o.y < ey) && (iLine.d.y < ey)) { - continue; - } - // optimisation 2: line above dot. no cross - if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { - continue; - } - // optimisation 3: vertical line case - if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { - xi = iLine.o.x; - yi = ey; - } - // calculate the intersection point - 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; - } - // dont count xi < ex cases - if (xi >= ex) { - xcount += 1; - } - // optimisation 4: specific for square images - 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) { - 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 = degreesToRadians(45 - this.getAngle()), - cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, - cosHalfOffset = cornerHypotenuse * Math.cos(theta), - sinHalfOffset = cornerHypotenuse * Math.sin(theta), - sinTh = Math.sin(this._theta), - cosTh = Math.cos(this._theta); - - coords.tl.corner = { - tl: { - x: coords.tl.x - sinHalfOffset, - y: coords.tl.y - cosHalfOffset - }, - tr: { - x: coords.tl.x + cosHalfOffset, - y: coords.tl.y - sinHalfOffset - }, - bl: { - x: coords.tl.x - cosHalfOffset, - y: coords.tl.y + sinHalfOffset - }, - br: { - x: coords.tl.x + sinHalfOffset, - y: coords.tl.y + cosHalfOffset - } - }; - - coords.tr.corner = { - tl: { - x: coords.tr.x - sinHalfOffset, - y: coords.tr.y - cosHalfOffset - }, - tr: { - x: coords.tr.x + cosHalfOffset, - y: coords.tr.y - sinHalfOffset - }, - br: { - x: coords.tr.x + sinHalfOffset, - y: coords.tr.y + cosHalfOffset - }, - bl: { - x: coords.tr.x - cosHalfOffset, - y: coords.tr.y + sinHalfOffset - } - }; - - coords.bl.corner = { - tl: { - x: coords.bl.x - sinHalfOffset, - y: coords.bl.y - cosHalfOffset - }, - bl: { - x: coords.bl.x - cosHalfOffset, - y: coords.bl.y + sinHalfOffset - }, - br: { - x: coords.bl.x + sinHalfOffset, - y: coords.bl.y + cosHalfOffset - }, - tr: { - x: coords.bl.x + cosHalfOffset, - y: coords.bl.y - sinHalfOffset - } - }; - - coords.br.corner = { - tr: { - x: coords.br.x + cosHalfOffset, - y: coords.br.y - sinHalfOffset - }, - bl: { - x: coords.br.x - cosHalfOffset, - y: coords.br.y + sinHalfOffset - }, - br: { - x: coords.br.x + sinHalfOffset, - y: coords.br.y + cosHalfOffset - }, - tl: { - x: coords.br.x - sinHalfOffset, - y: coords.br.y - cosHalfOffset - } - }; - - coords.ml.corner = { - tl: { - x: coords.ml.x - sinHalfOffset, - y: coords.ml.y - cosHalfOffset - }, - tr: { - x: coords.ml.x + cosHalfOffset, - y: coords.ml.y - sinHalfOffset - }, - bl: { - x: coords.ml.x - cosHalfOffset, - y: coords.ml.y + sinHalfOffset - }, - br: { - x: coords.ml.x + sinHalfOffset, - y: coords.ml.y + cosHalfOffset - } - }; - - coords.mt.corner = { - tl: { - x: coords.mt.x - sinHalfOffset, - y: coords.mt.y - cosHalfOffset - }, - tr: { - x: coords.mt.x + cosHalfOffset, - y: coords.mt.y - sinHalfOffset - }, - bl: { - x: coords.mt.x - cosHalfOffset, - y: coords.mt.y + sinHalfOffset - }, - br: { - x: coords.mt.x + sinHalfOffset, - y: coords.mt.y + cosHalfOffset - } - }; - - coords.mr.corner = { - tl: { - x: coords.mr.x - sinHalfOffset, - y: coords.mr.y - cosHalfOffset - }, - tr: { - x: coords.mr.x + cosHalfOffset, - y: coords.mr.y - sinHalfOffset - }, - bl: { - x: coords.mr.x - cosHalfOffset, - y: coords.mr.y + sinHalfOffset - }, - br: { - x: coords.mr.x + sinHalfOffset, - y: coords.mr.y + cosHalfOffset - } - }; - - coords.mb.corner = { - tl: { - x: coords.mb.x - sinHalfOffset, - y: coords.mb.y - cosHalfOffset - }, - tr: { - x: coords.mb.x + cosHalfOffset, - y: coords.mb.y - sinHalfOffset - }, - bl: { - x: coords.mb.x - cosHalfOffset, - y: coords.mb.y + sinHalfOffset - }, - br: { - x: coords.mb.x + sinHalfOffset, - y: coords.mb.y + cosHalfOffset - } - }; - - coords.mtr.corner = { - tl: { - x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) - }, - tr: { - x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - bl: { - x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - br: { - x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) - } - }; - }, - - /** - * 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; - }, - - /** - * Returns a JSON representation of an instance - * @method toJSON - * @return {String} json - */ - toJSON: function() { - // delegate, not alias - return this.toObject(); - }, - - /** - * @method setGradientFill - */ - setGradientFill: function(options) { - this.set('fill', fabric.Gradient.forObject(this, options)); - }, - - /** - * @method animate - * - * As object — multiple properties - * - * object.animate({ left: ..., top: ... }); - * object.animate({ left: ..., top: ... }, { duration: ... }); - * - * As string — one property - * - * object.animate('left', ...); - * object.animate('left', { duration: ... }); - * - */ - animate: function() { - if (arguments[0] && typeof arguments[0] === 'object') { - for (var prop in arguments[0]) { - this._animate(prop, arguments[0][prop], arguments[1]); - } - } - else { - this._animate.apply(this, arguments); - } - return this; - }, - - /** - * @private - * @method _animate - */ - _animate: function(property, to, options) { - var obj = this; - - options || (options = { }); - - if (!('from' in options)) { - options.from = this.get(property); - } - - if (/[+\-]/.test((to + '').charAt(0))) { - to = this.get(property) + parseFloat(to); - } - - fabric.util.animate({ - startValue: options.from, - endValue: to, - byValue: options.by, - easing: options.easing, - duration: options.duration, - onChange: function(value) { - obj.set(property, value); - options.onChange && options.onChange(); - }, - onComplete: function() { - obj.setCoords(); - options.onComplete && options.onComplete(); - } - }); - }, - - /** - * Centers object horizontally on canvas to which it was added last - * @method centerH - * @return {fabric.Object} thisArg - */ - centerH: function () { - this.canvas.centerObjectH(this); - return this; - }, - - /** - * Centers object vertically on canvas to which it was added last - * @method centerV - * @return {fabric.Object} thisArg - * @chainable - */ - centerV: function () { - this.canvas.centerObjectV(this); - return this; - }, - - /** - * Centers object vertically and horizontally on canvas to which is was added last - * @method center - * @return {fabric.Object} thisArg - * @chainable - */ - center: function () { - return this.centerH().centerV(); - }, - - /** - * Removes object from canvas to which it was added last - * @method remove - * @return {fabric.Object} thisArg - * @chainable - */ - remove: function() { - return this.canvas.remove(this); - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @return {fabric.Object} thisArg - * @chainable - */ - sendToBack: function() { - this.canvas.sendToBack(this); - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - this.canvas.bringToFront(this); - return this; - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function() { - this.canvas.sendBackwards(this); - return this; - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function() { - this.canvas.bringForward(this); - return this; - } - }); - - /** - * @alias rotate -> setAngle - */ - fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; - - var proto = fabric.Object.prototype; - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - - extend(fabric.Object.prototype, fabric.Observable); - - extend(fabric.Object, { - - /** - * @static - * @constant - * @type Number - */ - NUM_FRACTION_DIGITS: 2, - - /** - * @static - * @constant - * @type Number - */ - MIN_SCALE_LIMIT: 0.1 - - }); - -})(typeof exports !== 'undefined' ? exports : this); + //If width is negative, make postive. Fixes path selection issue + if(this.currentWidth < 0){ + this.currentWidth = Math.abs(this.currentWidth); + } + + this._hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + this._angle = Math.atan(this.currentHeight / this.currentWidth); + + // offset added for rotate and scale actions + 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) + }; + var mtr = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + // clockwise + this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords(); + + return this; + }, + + /** + * Returns width of an object's bounding rectangle + * @method getBoundingRectWidth + * @return {Number} width value + */ + getBoundingRectWidth: function() { + this.oCoords || this.setCoords(); + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + return Math.abs(minX - maxX); + }, + + /** + * Returns height of an object's bounding rectangle + * @method getBoundingRectHeight + * @return {Number} height value + */ + getBoundingRectHeight: function() { + this.oCoords || this.setCoords(); + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + return Math.abs(minY - maxY); + }, + + /** + * 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) { + if (!this.hasBorders) return; + + var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, + padding = this.padding, + padding2 = padding * 2, + strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), + scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX), + ~~(h + padding2 + strokeWidth * this.scaleY) + ); + + if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + _renderDashedStroke: function(ctx) { + + if (1 & this.strokeDashArray.length /* if odd number of items */) { + /* duplicate items */ + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + var i = 0, + x = -this.width/2, y = -this.height/2, + _this = this, + padding = this.padding, + dashedArrayLength = this.strokeDashArray.length; + + ctx.save(); + ctx.beginPath(); + + function renderSide(xMultiplier, yMultiplier) { + + var lineLength = 0, + lengthDiff = 0, + sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; + + while (lineLength < sideLength) { + + var lengthOfSubPath = _this.strokeDashArray[i++]; + lineLength += lengthOfSubPath; + + if (lineLength > sideLength) { + lengthDiff = lineLength - sideLength; + } + + // track coords + if (xMultiplier) { + x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); + } + else { + y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); + } + + ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); + if (i >= dashedArrayLength) { + i = 0; + } + } + } + + renderSide(1, 0); + renderSide(0, 1); + renderSide(-1, 0); + renderSide(0, -1); + + ctx.stroke(); + ctx.closePath(); + ctx.restore(); + }, + + /** + * 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) { + if (!this.hasControls) return; + + var size = this.cornersize, + size2 = size / 2, + strokeWidth2 = this.strokeWidth / 2, + left = -(this.width / 2), + top = -(this.height / 2), + _left, + _top, + sizeX = size / this.scaleX, + sizeY = size / this.scaleY, + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // top-right + _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + if (!this.lockUniScaling) { + // middle-top + _left = left + width/2 - scaleOffsetX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-bottom + _left = left + width/2 - scaleOffsetX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + + _left = left + width/2 - scaleOffsetX; + + _top = this.flipY ? + (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_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(); + + /** @ignore */ + 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') + }; + + // normalize angle + this.set('angle', 0).set('flipX', false).set('flipY', false); + this.toDataURL(function(dataURL) { + i.src = dataURL; + }); + } + return this; + }, + + /** + * Converts an object into a data-url-like string + * @method toDataURL + * @return {String} string of data + */ + toDataURL: function(callback) { + var el = fabric.document.createElement('canvas'); + if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(el); + } + + el.width = this.getBoundingRectWidth(); + el.height = this.getBoundingRectHeight(); + + fabric.util.wrapElement(el, 'div'); + + var canvas = new fabric.Canvas(el); + canvas.backgroundColor = 'transparent'; + canvas.renderAll(); + + if (this.constructor.async) { + this.clone(proceed); + } + else { + proceed(this.clone()); + } + + function proceed(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; + + callback && callback(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; + }, + + /** + * @method setupState + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + }, + + /** + * 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) { + // extracts coords + 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 of another object + * @method isContainedWithinObject + * @param {Object} other Object to test + * @return {Boolean} + */ + isContainedWithinObject: function(other) { + return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); + }, + + /** + * 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); + + 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) { + if (!this.hasControls || !this.active) return false; + + var pointer = getPointer(e), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xpoints, + lines; + + for (var i in this.oCoords) { + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner, i); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + 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]; + // optimisation 1: line below dot. no cross + if ((iLine.o.y < ey) && (iLine.d.y < ey)) { + continue; + } + // optimisation 2: line above dot. no cross + if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { + xi = iLine.o.x; + yi = ey; + } + // calculate the intersection point + 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; + } + // dont count xi < ex cases + if (xi >= ex) { + xcount += 1; + } + // optimisation 4: specific for square images + 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) { + 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 = degreesToRadians(45 - this.getAngle()), + cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, + cosHalfOffset = cornerHypotenuse * Math.cos(theta), + sinHalfOffset = cornerHypotenuse * Math.sin(theta), + sinTh = Math.sin(this._theta), + cosTh = Math.cos(this._theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + + /** + * 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; + }, + + /** + * Returns a JSON representation of an instance + * @method toJSON + * @return {String} json + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + }, + + /** + * @method setGradientFill + */ + setGradientFill: function(options) { + this.set('fill', fabric.Gradient.forObject(this, options)); + }, + + /** + * @method animate + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + for (var prop in arguments[0]) { + this._animate(prop, arguments[0][prop], arguments[1]); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @method _animate + */ + _animate: function(property, to, options) { + var obj = this; + + options || (options = { }); + + if (!('from' in options)) { + options.from = this.get(property); + } + + if (/[+\-]/.test((to + '').charAt(0))) { + to = this.get(property) + parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + onChange: function(value) { + obj.set(property, value); + options.onChange && options.onChange(); + }, + onComplete: function() { + obj.setCoords(); + options.onComplete && options.onComplete(); + } + }); + }, + + /** + * Centers object horizontally on canvas to which it was added last + * @method centerH + * @return {fabric.Object} thisArg + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last + * @method centerV + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * @method center + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + return this.centerH().centerV(); + }, + + /** + * Removes object from canvas to which it was added last + * @method remove + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + return this.canvas.remove(this); + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + this.canvas.sendToBack(this); + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + this.canvas.bringToFront(this); + return this; + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function() { + this.canvas.sendBackwards(this); + return this; + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function() { + this.canvas.bringForward(this); + return this; + } + }); + + /** + * @alias rotate -> setAngle + */ + fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + + var proto = fabric.Object.prototype; + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + + extend(fabric.Object.prototype, fabric.Observable); + + extend(fabric.Object, { + + /** + * @static + * @constant + * @type Number + */ + NUM_FRACTION_DIGITS: 2, + + /** + * @static + * @constant + * @type Number + */ + MIN_SCALE_LIMIT: 0.1 + + }); + +})(typeof exports !== 'undefined' ? exports : this); From da28b363e4235508838883b6f253ffd4fdebd463 Mon Sep 17 00:00:00 2001 From: Steve Pemberton Date: Sun, 14 Oct 2012 18:23:39 +0100 Subject: [PATCH 2/3] Revert "Fix for negative width on objects preventing selection" This reverts commit 55853039cdea47c4c26548258df0bd724b4caecf. --- dist/all.js | 27983 +++++++++++++++++++++--------------------- src/object.class.js | 3459 +++--- 2 files changed, 15716 insertions(+), 15726 deletions(-) diff --git a/dist/all.js b/dist/all.js index 159e36c0..e17e8ce2 100644 --- a/dist/all.js +++ b/dist/all.js @@ -1,13994 +1,13989 @@ -/* build: `node build.js modules=ALL` */ -/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */ - -var fabric = fabric || { version: "0.9.14" }; - -if (typeof exports != 'undefined') { - exports.fabric = fabric; -} - -if (typeof document != 'undefined' && typeof window != 'undefined') { - fabric.document = document; - fabric.window = window; -} -else { - // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom").jsdom(""); - fabric.window = fabric.document.createWindow(); -} - -/** - * True when in environment that supports touch events - * @property isTouchSupported - * @type boolean - */ -fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; - -/** - * True when in environment that's probably Node.js - * @property isLikelyNode - * @type boolean - */ -fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; -/*! - * Copyright (c) 2009 Simo Kinnunen. - * Licensed under the MIT license. - */ - -var Cufon = (function() { - - var api = function() { - return api.replace.apply(null, arguments); - }; - - var DOM = api.DOM = { - - ready: (function() { - - var complete = false, readyStatus = { loaded: 1, complete: 1 }; - - var queue = [], perform = function() { - if (complete) return; - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Gecko, Opera, WebKit r26101+ - - if (fabric.document.addEventListener) { - fabric.document.addEventListener('DOMContentLoaded', perform, false); - fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages - } - - // Old WebKit, Internet Explorer - - if (!fabric.window.opera && fabric.document.readyState) (function() { - readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); - })(); - - // Internet Explorer - - if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { - try { - fabric.document.body.doScroll('left'); - perform(); - } - catch (e) { - setTimeout(arguments.callee, 1); - } - })(); - - addEvent(fabric.window, 'load', perform); // Fallback - - return function(listener) { - if (!arguments.length) perform(); - else complete ? listener() : queue.push(listener); - }; - - })() - - }; - - var CSS = api.CSS = { - - Size: function(value, base) { - - this.value = parseFloat(value); - this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; - - this.convert = function(value) { - return value / base * this.value; - }; - - this.convertFrom = function(value) { - return value / this.value * base; - }; - - this.toString = function() { - return this.value + this.unit; - }; - - }, - - getStyle: function(el) { - return new Style(el.style); - /* - var view = document.defaultView; - if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); - if (el.currentStyle) return new Style(el.currentStyle); - return new Style(el.style); - */ - }, - - quotedList: cached(function(value) { - // doesn't work properly with empty quoted strings (""), but - // it's not worth the extra code. - var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; - while (match = re.exec(value)) list.push(match[3] || match[1]); - return list; - }), - - ready: (function() { - - var complete = false; - - var queue = [], perform = function() { - complete = true; - for (var fn; fn = queue.shift(); fn()); - }; - - // Safari 2 does not include '); - - function getFontSizeInPixels(el, value) { - return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); - } - - // Original by Dead Edwards. - // Combined with getFontSizeInPixels it also works with relative units. - function getSizeInPixels(el, value) { - if (/px$/i.test(value)) return parseFloat(value); - var style = el.style.left, runtimeStyle = el.runtimeStyle.left; - el.runtimeStyle.left = el.currentStyle.left; - el.style.left = value; - var result = el.style.pixelLeft; - el.style.left = style; - el.runtimeStyle.left = runtimeStyle; - return result; - } - - return function(font, text, style, options, node, el, hasNext) { - var redraw = (text === null); - - if (redraw) text = node.alt; - - // @todo word-spacing, text-decoration - - var viewBox = font.viewBox; - - var size = style.computedFontSize || - (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); - - var letterSpacing = style.computedLSpacing; - - if (letterSpacing == undefined) { - letterSpacing = style.get('letterSpacing'); - style.computedLSpacing = letterSpacing = - (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); - } - - var wrapper, canvas; - - if (redraw) { - wrapper = node; - canvas = node.firstChild; - } - else { - wrapper = fabric.document.createElement('span'); - wrapper.className = 'cufon cufon-vml'; - wrapper.alt = text; - - canvas = fabric.document.createElement('span'); - canvas.className = 'cufon-vml-canvas'; - wrapper.appendChild(canvas); - - if (options.printable) { - var print = fabric.document.createElement('span'); - print.className = 'cufon-alt'; - print.appendChild(fabric.document.createTextNode(text)); - wrapper.appendChild(print); - } - - // ie6, for some reason, has trouble rendering the last VML element in the document. - // we can work around this by injecting a dummy element where needed. - // @todo find a better solution - if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); - } - - var wStyle = wrapper.style; - var cStyle = canvas.style; - - var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); - var roundingFactor = roundedHeight / height; - var minX = viewBox.minX, minY = viewBox.minY; - - cStyle.height = roundedHeight; - cStyle.top = Math.round(size.convert(minY - font.ascent)); - cStyle.left = Math.round(size.convert(minX)); - - wStyle.height = size.convert(font.height) + 'px'; - - var textDecoration = Cufon.getTextDecoration(options); - - var color = style.get('color'); - - var chars = Cufon.CSS.textTransform(text, style).split(''); - - var width = 0, offsetX = 0, advance = null; - - var glyph, shape, shadows = options.textShadow; - - // pre-calculate width - for (var i = 0, k = 0, l = chars.length; i < l; ++i) { - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; - } - - if (advance === null) return null; - - var fullWidth = -minX + width + (viewBox.width - advance); - - var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); - - var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; - var stretch = 'r' + coordSize + 'nsnf'; - - for (i = 0; i < l; ++i) { - - glyph = font.glyphs[chars[i]] || font.missingGlyph; - if (!glyph) continue; - - if (redraw) { - // some glyphs may be missing so we can't use i - shape = canvas.childNodes[k]; - if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow - } - else { - shape = fabric.document.createElement('cvml:shape'); - canvas.appendChild(shape); - } - - shape.stroked = 'f'; - shape.coordsize = coordSize; - shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; - shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; - shape.fillcolor = color; - - // it's important to not set top/left or IE8 will grind to a halt - var sStyle = shape.style; - sStyle.width = roundedShapeWidth; - sStyle.height = roundedHeight; - - if (shadows) { - // due to the limitations of the VML shadow element there - // can only be two visible shadows. opacity is shared - // for all shadows. - var shadow1 = shadows[0], shadow2 = shadows[1]; - var color1 = Cufon.CSS.color(shadow1.color), color2; - var shadow = fabric.document.createElement('cvml:shadow'); - shadow.on = 't'; - shadow.color = color1.color; - shadow.offset = shadow1.offX + ',' + shadow1.offY; - if (shadow2) { - color2 = Cufon.CSS.color(shadow2.color); - shadow.type = 'double'; - shadow.color2 = color2.color; - shadow.offset2 = shadow2.offX + ',' + shadow2.offY; - } - shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; - shape.appendChild(shadow); - } - - offsetX += ~~(glyph.w || font.w) + letterSpacing; - - ++k; - - } - - wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); - - return wrapper; - - }; - -})()); - -Cufon.getTextDecoration = function(options) { - return { - underline: options.textDecoration === 'underline', - overline: options.textDecoration === 'overline', - 'line-through': options.textDecoration === 'line-through' - }; -}; - -if (typeof exports != 'undefined') { - exports.Cufon = Cufon; -} - -/* - json2.js - 2011-10-19 - - 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) { - // Format integers to have at least two digits. - 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 is '["e",{"pluribus":"unum"}]' - - - text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); - // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' - - text = JSON.stringify([new Date()], function (key, value) { - return this[key] instanceof Date ? - 'Date(' + this[key] + ')' : value; - }); - // text is '["Date(---current time---)"]' - - - 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: - - // Parse the text. Values that look like ISO date strings will - // be converted to Date objects. - - 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, regexp: true */ - -/*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 -*/ - - -// Create a JSON object only if one does not already exist. We create the -// methods in a closure to avoid creating global variables. - -var JSON; -if (!JSON) { - JSON = {}; -} - -(function () { - 'use strict'; - - function f(n) { - // Format integers to have at least two digits. - 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) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - 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) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value && typeof value === 'object' && - typeof value.toJSON === 'function') { - value = value.toJSON(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 - ? '[]' - : gap - ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' - : '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 - ? '{}' - : gap - ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' - : '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - if (typeof JSON.stringify !== 'function') { - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - } - - -// If the JSON object does not yet have a parse method, give it one. - - if (typeof JSON.parse !== 'function') { - JSON.parse = function (text, reviver) { - -// The parse method takes a text and an optional reviver function, and returns -// a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - -// The walk method is used to recursively walk the resulting structure so -// that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - -// Parsing happens in four stages. In the first stage, we replace certain -// Unicode characters with escape sequences. JavaScript handles many characters -// incorrectly, either silently deleting them, or treating them as line endings. - - 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); - }); - } - -// In the second stage, we run the text against regular expressions that look -// for non-JSON patterns. We are especially concerned with '()' and 'new' -// because they can cause invocation, and '=' because it can cause mutation. -// But just to be safe, we want to reject all unexpected forms. - -// We split the second stage into 4 regexp operations in order to work around -// crippling inefficiencies in IE's and Safari's regexp engines. First we -// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we -// replace all simple value tokens with ']' characters. Third, we delete all -// open brackets that follow a colon or comma or that begin the text. Finally, -// we look to see that the remaining characters are only whitespace or ']' or -// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - 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, ''))) { - -// In the third stage we use the eval function to compile the text into a -// JavaScript structure. The '{' operator is subject to a syntactic ambiguity -// in JavaScript: it can begin a block or an object literal. We wrap the text -// in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - -// In the optional fourth stage, we recursively walk the new structure, passing -// each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' - ? walk({'': j}, '') - : j; - } - -// If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - } -}()); -/** - * Wrapper around `console.log` (when available) - * @method log - * @param {Any} Values to log - */ -fabric.log = function() { }; - -/** - * Wrapper around `console.warn` (when available) - * @method warn - * @param {Any} Values to log as a warning - */ -fabric.warn = function() { }; - -if (typeof console !== 'undefined') { - if (typeof console.log !== 'undefined' && console.log.apply) { - fabric.log = function() { - return console.log.apply(console, arguments); - }; - } - if (typeof console.warn !== 'undefined' && console.warn.apply) { - fabric.warn = function() { - return console.warn.apply(console, arguments); - }; - } -} - -/** - * @namespace - */ -fabric.Observable = { - - /** - * Observes specified event - * @method observe - * @depracated Since 0.8.34. Use `on` instead. - * @param {String} eventName - * @param {Function} handler - */ - observe: function(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - // one object with key/value pairs was passed - if (arguments.length === 1) { - for (var prop in eventName) { - this.on(prop, eventName[prop]); - } - } - else { - if (!this.__eventListeners[eventName]) { - this.__eventListeners[eventName] = [ ]; - } - this.__eventListeners[eventName].push(handler); - } - }, - - /** - * Stops event observing for a particular event handler - * @method stopObserving - * @depracated Since 0.8.34. Use `off` instead. - * @param {String} eventName - * @param {Function} handler - */ - stopObserving: function(eventName, handler) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - if (this.__eventListeners[eventName]) { - fabric.util.removeFromArray(this.__eventListeners[eventName], handler); - } - }, - - /** - * Fires event with an optional options object - * @method fire - * @param {String} eventName - * @param {Object} [options] - */ - fire: function(eventName, options) { - if (!this.__eventListeners) { - this.__eventListeners = { }; - } - var listenersForEvent = this.__eventListeners[eventName]; - if (!listenersForEvent) return; - for (var i = 0, len = listenersForEvent.length; i < len; i++) { - // avoiding try/catch for perf. reasons - listenersForEvent[i](options || { }); - } - } -}; - -/** - * Alias for observe - * @method observe - * @memberOf fabric.Observable - */ -fabric.Observable.on = fabric.Observable.observe; - -/** - * Alias for stopObserving - * @method off - */ -fabric.Observable.off = fabric.Observable.stopObserving; -(function() { - - /** - * @namespace - */ - fabric.util = { }; - - /** - * Removes value from an array. - * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` - * @static - * @memberOf fabric.util - * @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; - } - - /** - * Returns random number between 2 specified ones. - * @static - * @method getRandomInt - * @memberOf fabric.util - * @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; - } - - var PiBy180 = Math.PI / 180; - - /** - * Transforms degrees to radians. - * @static - * @method degreesToRadians - * @memberOf fabric.util - * @param {Number} degrees value in degrees - * @return {Number} value in radians - */ - function degreesToRadians(degrees) { - return degrees * PiBy180; - } - - /** - * A wrapper around Number#toFixed, which contrary to native method returns number, not string. - * @static - * @method toFixed - * @memberOf fabric.util - * @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 - * @memberOf fabric.util - * @return {Boolean} - */ - function falseFunction() { - return false; - } - - /** - * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. - * @method animate - * @memberOf fabric.util - * @param {Object} [options] Animation options - * @param {Function} [options.onChange] Callback; invoked on every value change - * @param {Function} [options.onComplete] Callback; invoked when value change is completed - * @param {Number} [options.startValue=0] Starting value - * @param {Number} [options.endValue=100] Ending value - * @param {Number} [options.byValue=100] Value to modify the property by - * @param {Function} [options.easing] Easing function - * @param {Number} [options.duration=500] Duration of change - */ - function animate(options) { - - options || (options = { }); - - var start = +new Date(), - duration = options.duration || 500, - finish = start + duration, time, - onChange = options.onChange || function() { }, - abort = options.abort || function() { return false; }, - easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, - startValue = 'startValue' in options ? options.startValue : 0, - endValue = 'endValue' in options ? options.endValue : 100, - byValue = options.byValue || endValue - startValue; - - options.onStart && options.onStart(); - - (function tick() { - time = +new Date(); - var currentTime = time > finish ? duration : (time - start); - onChange(easing(currentTime, startValue, byValue, duration)); - if (time > finish || abort()) { - options.onComplete && options.onComplete(); - return; - } - requestAnimFrame(tick); - })(); - } - - var _requestAnimFrame = fabric.window.requestAnimationFrame || - fabric.window.webkitRequestAnimationFrame || - fabric.window.mozRequestAnimationFrame || - fabric.window.oRequestAnimationFrame || - fabric.window.msRequestAnimationFrame || - function(callback) { - fabric.window.setTimeout(callback, 1000 / 60); - }; - /** - * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * @method requestAnimFrame - * @memberOf fabric.util - * @param {Function} callback Callback to invoke - * @param {DOMElement} element optional Element to associate with animation - */ - var requestAnimFrame = function() { - return _requestAnimFrame.apply(fabric.window, arguments); - }; - - /** - * Loads image element from given url and passes it to a callback - * @method loadImage - * @memberOf fabric.util - * @param {String} url URL representing an image - * @param {Function} callback Callback; invoked with loaded image - * @param {Any} context optional Context to invoke callback in - */ - function loadImage(url, callback, context) { - if (url) { - var img = new Image(); - /** @ignore */ - img.onload = function () { - callback && callback.call(context, img); - img = img.onload = null; - }; - img.src = url; - } - else { - callback && callback.call(context, url); - } - } - - function enlivenObjects(objects, callback) { - - function getKlass(type) { - return fabric[fabric.util.string.camelize(fabric.util.string.capitalize(type))]; - } - - function onLoaded() { - if (++numLoadedObjects === numTotalObjects) { - if (callback) { - callback(enlivenedObjects); - } - } - } - - var enlivenedObjects = [ ], - numLoadedObjects = 0, - numTotalObjects = objects.length; - - objects.forEach(function (o, index) { - if (!o.type) { - return; - } - var klass = getKlass(o.type); - if (klass.async) { - klass.fromObject(o, function (o) { - enlivenedObjects[index] = o; - onLoaded(); - }); - } - else { - enlivenedObjects[index] = klass.fromObject(o); - onLoaded(); - } - }); - } - - /** - * Groups SVG elements (usually those retrieved from SVG document) - * @static - * @memberOf fabric.util - * @method groupSVGElements - * @param {Array} elements - * @param {Object} options optional - * @return {String} path optional - */ - function groupSVGElements(elements, options, path) { - var object = elements.length > 1 - ? new fabric.PathGroup(elements, options) - : elements[0]; - - if (typeof path !== 'undefined') { - object.setSourcePath(path); - } - return object; - } - - fabric.util.removeFromArray = removeFromArray; - fabric.util.degreesToRadians = degreesToRadians; - fabric.util.toFixed = toFixed; - fabric.util.getRandomInt = getRandomInt; - fabric.util.falseFunction = falseFunction; - fabric.util.animate = animate; - fabric.util.requestAnimFrame = requestAnimFrame; - fabric.util.loadImage = loadImage; - fabric.util.enlivenObjects = enlivenObjects; - fabric.util.groupSVGElements = groupSVGElements; -})(); -(function() { - - var slice = Array.prototype.slice; - - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { - if (this === void 0 || this === null) { - throw new TypeError(); - } - var t = Object(this), len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 0) { - n = Number(arguments[1]); - if (n !== n) { // shortcut for verifying if it's NaN - n = 0; - } - else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - 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 array contains no values, no initial value to return - 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; - }; - } - - /** - * Invokes method on all items in a given array - * @method invoke - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} method Name of a method to invoke - */ - 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; - } - - /** - * Finds maximum value in array (not necessarily "first" one) - * @method max - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - */ - function max(array, byProperty) { - if (!array || array.length === 0) return undefined; - - 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; - } - - /** - * Finds minimum value in array (not necessarily "first" one) - * @method min - * @memberOf fabric.util.array - * @param {Array} array Array to iterate over - * @param {String} byProperty - */ - function min(array, byProperty) { - if (!array || array.length === 0) return undefined; - - 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; - } - - /** @namespace */ - fabric.util.array = { - invoke: invoke, - min: min, - max: max - }; - -})(); -(function(){ - - /** - * Copies all enumerable properties of one object to another - * @memberOf fabric.util.object - * @method extend - * @param {Object} destination Where to copy to - * @param {Object} source Where to copy from - */ - function extend(destination, source) { - // JScript DontEnum bug is not taken care of - for (var property in source) { - destination[property] = source[property]; - } - return destination; - } - - /** - * Creates an empty object and copies all enumerable properties of another object to it - * @method clone - * @memberOf fabric.util.object - * @param {Object} object Object to clone - */ - function clone(object) { - return extend({ }, object); - } - - /** @namespace fabric.util.object */ - fabric.util.object = { - extend: extend, - clone: clone - }; - -})(); -(function() { - -if (!String.prototype.trim) { - /** - * Trims a string (removing whitespace from the beginning and the end) - * @method trim - * @see String#trim on MDN - */ - String.prototype.trim = function () { - // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now - return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); - }; -} - -/** - * Camelizes a string - * @memberOf fabric.util.string - * @method camelize - * @param {String} string String to camelize - * @return {String} Camelized version of a string - */ -function camelize(string) { - return string.replace(/-+(.)?/g, function(match, character) { - return character ? character.toUpperCase() : ''; - }); -} - -/** - * Capitalizes a string - * @memberOf fabric.util.string - * @method capitalize - * @param {String} string String to capitalize - * @return {String} Capitalized version of a string - */ -function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); -} - -function escapeXml(string) { - return string.replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); -} - -/** @namespace */ -fabric.util.string = { - camelize: camelize, - capitalize: capitalize, - escapeXml: escapeXml -}; -}()); - -(function() { - - var slice = Array.prototype.slice, - apply = Function.prototype.apply, - Dummy = function() { }; - - if (!Function.prototype.bind) { - /** - * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) - * @see Function#bind on MDN - * @param {Object} thisArg Object to bind function to - * @param {Any[]} [...] Values to pass to a bound function - * @return {Function} - */ - Function.prototype.bind = function(thisArg) { - var fn = this, args = slice.call(arguments, 1), bound; - if (args.length) { - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); - }; - } - else { - bound = function() { - return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); - }; - } - Dummy.prototype = this.prototype; - bound.prototype = new Dummy(); - - return bound; - }; - } - -})(); -(function() { - - var slice = Array.prototype.slice, emptyFunction = function() { }; - - var IS_DONTENUM_BUGGY = (function(){ - for (var p in { toString: 1 }) { - if (p === 'toString') return false; - } - return true; - })(); - - /** @ignore */ - var addMethods = function(klass, source, parent) { - for (var property in source) { - - if (property in klass.prototype && typeof klass.prototype[property] === 'function') { - - klass.prototype[property] = (function(property) { - return function() { - - var superclass = this.constructor.superclass; - this.constructor.superclass = parent; - var returnValue = source[property].apply(this, arguments); - this.constructor.superclass = superclass; - - if (property !== 'initialize') { - return returnValue; - } - }; - })(property); - } - else { - klass.prototype[property] = source[property]; - } - - if (IS_DONTENUM_BUGGY) { - if (source.toString !== Object.prototype.toString) { - klass.prototype.toString = source.toString; - } - if (source.valueOf !== Object.prototype.valueOf) { - klass.prototype.valueOf = source.valueOf; - } - } - } - }; - - function Subclass() { } - - /** - * Helper for creation of "classes" - * @method createClass - * @memberOf fabric.util - */ - 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], parent); - } - if (!klass.prototype.initialize) { - klass.prototype.initialize = emptyFunction; - } - klass.prototype.constructor = klass; - return klass; - } - - fabric.util.createClass = createClass; -})(); -(function () { - - /* 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 fabric.document.documentElement.uniqueID !== 'undefined') { - return function (element) { - return element.uniqueID; - }; - } - var uid = 0; - return function (element) { - return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); - }; - })(); - - /** @ignore */ - var getElement, setElement; - - (function () { - var elements = { }; - /** @ignore */ - getElement = function (uid) { - return elements[uid]; - }; - /** @ignore */ - 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 || fabric.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 || fabric.window.event); - } - } - }; - } - - var shouldUseAddListenerRemoveListener = ( - areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && - areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), - - shouldUseAttachEventDetachEvent = ( - areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && - areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), - - // IE branch - listeners = { }, - - // DOM L0 branch - handlers = { }, - - addListener, removeListener; - - if (shouldUseAddListenerRemoveListener) { - /** @ignore */ - addListener = function (element, eventName, handler) { - element.addEventListener(eventName, handler, false); - }; - /** @ignore */ - removeListener = function (element, eventName, handler) { - element.removeEventListener(eventName, handler, false); - }; - } - - else if (shouldUseAttachEventDetachEvent) { - /** @ignore */ - 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); - }; - /** @ignore */ - 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 { - /** @ignore */ - 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); - }; - /** @ignore */ - 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); - } - } - } - }; - } - - /** - * Adds an event listener to an element - * @mthod addListener - * @memberOf fabric.util - * @function - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.addListener = addListener; - - /** - * Removes an event listener from an element - * @mthod removeListener - * @memberOf fabric.util - * @function - * @param {HTMLElement} element - * @param {String} eventName - * @param {Function} handler - */ - fabric.util.removeListener = removeListener; - - /** - * Cross-browser wrapper for getting event's coordinates - * @method getPointer - * @memberOf fabric.util - * @param {Event} event - */ - function getPointer(event) { - // TODO (kangax): this method needs fixing - return { x: pointerX(event), y: pointerY(event) }; - } - - var pointerX = function(event) { - var docElement = fabric.document.documentElement, - body = fabric.document.body || { scrollLeft: 0 }; - - // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) - // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] - // need to investigate later - return event.pageX || ((typeof event.clientX !== 'unknown' ? event.clientX : 0) + - (docElement.scrollLeft || body.scrollLeft) - - (docElement.clientLeft || 0)); - }; - - var pointerY = function(event) { - var docElement = fabric.document.documentElement, - body = fabric.document.body || { scrollTop: 0 }; - - return event.pageY || ((typeof event.clientY !== 'unknown' ? event.clientY : 0) + - (docElement.scrollTop || body.scrollTop) - - (docElement.clientTop || 0)); - }; - - if (fabric.isTouchSupported) { - pointerX = function(event) { - return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX; - }; - pointerY = function(event) { - return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY; - }; - } - - fabric.util.getPointer = getPointer; - - fabric.util.object.extend(fabric.util, fabric.Observable); - -})(); -(function () { - - /** - * Cross-browser wrapper for setting element's style - * @method setStyle - * @memberOf fabric.util - * @param {HTMLElement} element - * @param {Object} styles - * @return {HTMLElement} Element that was passed as a first argument - */ - function setStyle(element, styles) { - var elementStyle = element.style; - if (!elementStyle) { - return element; - } - 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 = fabric.document.createElement('div'), - supportsOpacity = typeof parseEl.style.opacity === 'string', - supportsFilters = typeof parseEl.style.filter === 'string', - reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, - - /** @ignore */ - setOpacity = function (element) { return element; }; - - if (supportsOpacity) { - /** @ignore */ - setOpacity = function(element, value) { - element.style.opacity = value; - return element; - }; - } - else if (supportsFilters) { - /** @ignore */ - 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() { - - var _slice = Array.prototype.slice; - - /** - * Takes id and returns an element with that id (if one exists in a document) - * @method getById - * @memberOf fabric.util - * @param {String|HTMLElement} id - * @return {HTMLElement|null} - */ - function getById(id) { - return typeof id === 'string' ? fabric.document.getElementById(id) : id; - } - - /** - * Converts an array-like object (e.g. arguments or NodeList) to an array - * @method toArray - * @memberOf fabric.util - * @param {Object} arrayLike - * @return {Array} - */ - var toArray = function(arrayLike) { - return _slice.call(arrayLike, 0); - }; - - var sliceCanConvertNodelists; - try { - sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; - } - catch(err) { } - - if (!sliceCanConvertNodelists) { - toArray = function(arrayLike) { - var arr = new Array(arrayLike.length), i = arrayLike.length; - while (i--) { - arr[i] = arrayLike[i]; - } - return arr; - }; - } - - /** - * Creates specified element with specified attributes - * @method makeElement - * @memberOf fabric.util - * @param {String} tagName Type of an element to create - * @param {Object} [attributes] Attributes to set on an element - * @return {HTMLElement} Newly created element - */ - function makeElement(tagName, attributes) { - var el = fabric.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; - } - - /** - * Adds class to an element - * @method addClass - * @memberOf fabric.util - * @param {HTMLElement} element Element to add class to - * @param {String} className Class to add to an element - */ - function addClass(element, className) { - if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { - element.className += (element.className ? ' ' : '') + className; - } - } - - /** - * Wraps element with another element - * @method wrapElement - * @memberOf fabric.util - * @param {HTMLElement} element Element to wrap - * @param {HTMLElement|String} wrapper Element to wrap with - * @param {Object} [attributes] Attributes to set on a wrapper - * @return {HTMLElement} wrapper - */ - 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; - } - - /** - * Returns offset for a given element - * @method getElementOffset - * @function - * @memberOf fabric.util - * @param {HTMLElement} element Element to get offset for - * @return {Object} Object with "left" and "top" properties - */ - function getElementOffset(element) { - // TODO (kangax): need to fix this method - 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 = fabric.document.documentElement.style; - - var selectProp = 'userSelect' in style - ? 'userSelect' - : 'MozUserSelect' in style - ? 'MozUserSelect' - : 'WebkitUserSelect' in style - ? 'WebkitUserSelect' - : 'KhtmlUserSelect' in style - ? 'KhtmlUserSelect' - : ''; - - /** - * Makes element unselectable - * @method makeElementUnselectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make unselectable - * @return {HTMLElement} Element that was passed in - */ - 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; - } - - /** - * Makes element selectable - * @method makeElementSelectable - * @memberOf fabric.util - * @param {HTMLElement} element Element to make selectable - * @return {HTMLElement} Element that was passed in - */ - function makeElementSelectable(element) { - if (typeof element.onselectstart !== 'undefined') { - element.onselectstart = null; - } - if (selectProp) { - element.style[selectProp] = ''; - } - else if (typeof element.unselectable === 'string') { - element.unselectable = ''; - } - return element; - } - - fabric.util.makeElementUnselectable = makeElementUnselectable; - fabric.util.makeElementSelectable = makeElementSelectable; - })(); - - (function() { - - /** - * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading - * @method getScript - * @memberOf fabric.util - * @param {String} url URL of a script to load - * @param {Function} callback Callback to execute when script is finished loading - */ - function getScript(url, callback) { - var headEl = fabric.document.getElementsByTagName("head")[0], - scriptEl = fabric.document.createElement('script'), - loading = true; - - scriptEl.type = 'text/javascript'; - scriptEl.setAttribute('runat', 'server'); - - /** @ignore */ - scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { - if (loading) { - if (typeof this.readyState === 'string' && - this.readyState !== 'loaded' && - this.readyState !== 'complete') return; - loading = false; - callback(e || fabric.window.event); - scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; - } - }; - scriptEl.src = url; - headEl.appendChild(scriptEl); - // causes issue in Opera - // headEl.removeChild(scriptEl); - } - - fabric.util.getScript = getScript; - })(); - - fabric.util.getById = getById; - fabric.util.toArray = toArray; - fabric.util.makeElement = makeElement; - fabric.util.addClass = addClass; - fabric.util.wrapElement = wrapElement; - fabric.util.getElementOffset = getElementOffset; - -})(); -(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() { } - - /** - * Cross-browser abstraction for sending XMLHttpRequest - * @method request - * @memberOf fabric.util - * @param {String} url URL to send XMLHttpRequest to - * @param {Object} [options] Options object - * @param {String} [options.method="GET"] - * @param {Function} options.onComplete Callback to invoke when request is completed - * @return {XMLHttpRequest} request - */ - function request(url, options) { - - options || (options = { }); - - var method = options.method ? options.method.toUpperCase() : 'GET', - onComplete = options.onComplete || function() { }, - xhr = makeXHR(), - body; - - /** @ignore */ - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - onComplete(xhr); - xhr.onreadystatechange = emptyFn; - } - }; - - if (method === 'GET') { - body = null; - if (typeof options.parameters === 'string') { - url = addParamToUrl(url, options.parameters); - } - } - - xhr.open(method, url, true); - - if (method === 'POST' || method === 'PUT') { - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - } - - xhr.send(body); - return xhr; - } - - fabric.util.request = request; -})(); -(function() { - - /** - * @method easeInQuad - * @memberOf fabric.util.ease - */ - function easeInQuad(t, b, c, d) { - return c*(t/=d)*t + b; - } - - /** - * @method easeOutQuad - * @memberOf fabric.util.ease - */ - function easeOutQuad(t, b, c, d) { - return -c *(t/=d)*(t-2) + b; - } - - /** - * @method easeInOutQuad - * @memberOf fabric.util.ease - */ - function easeInOutQuad(t, b, c, d) { - t /= (d/2); - if (t < 1) return c/2*t*t + b; - return -c/2 * ((--t)*(t-2) - 1) + b; - } - - /** - * @method easeInCubic - * @memberOf fabric.util.ease - */ - function easeInCubic(t, b, c, d) { - return c*(t/=d)*t*t + b; - } - - /** - * @method easeOutCubic - * @memberOf fabric.util.ease - */ - function easeOutCubic(t, b, c, d) { - return c*((t=t/d-1)*t*t + 1) + b; - } - - /** - * @method easeInOutCubic - * @memberOf fabric.util.ease - */ - function easeInOutCubic(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t + b; - return c/2*((t-=2)*t*t + 2) + b; - } - - /** - * @method easeInQuart - * @memberOf fabric.util.ease - */ - function easeInQuart(t, b, c, d) { - return c*(t/=d)*t*t*t + b; - } - - /** - * @method easeOutQuart - * @memberOf fabric.util.ease - */ - function easeOutQuart(t, b, c, d) { - return -c * ((t=t/d-1)*t*t*t - 1) + b; - } - - /** - * @method easeInOutQuart - * @memberOf fabric.util.ease - */ - function easeInOutQuart(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t*t + b; - return -c/2 * ((t-=2)*t*t*t - 2) + b; - } - - /** - * @method easeInQuint - * @memberOf fabric.util.ease - */ - function easeInQuint(t, b, c, d) { - return c*(t/=d)*t*t*t*t + b; - } - - /** - * @method easeOutQuint - * @memberOf fabric.util.ease - */ - function easeOutQuint(t, b, c, d) { - return c*((t=t/d-1)*t*t*t*t + 1) + b; - } - - /** - * @method easeInOutQuint - * @memberOf fabric.util.ease - */ - function easeInOutQuint(t, b, c, d) { - t /= d/2; - if (t < 1) return c/2*t*t*t*t*t + b; - return c/2*((t-=2)*t*t*t*t + 2) + b; - } - - /** - * @method easeInSine - * @memberOf fabric.util.ease - */ - function easeInSine(t, b, c, d) { - return -c * Math.cos(t/d * (Math.PI/2)) + c + b; - } - - /** - * @method easeOutSine - * @memberOf fabric.util.ease - */ - function easeOutSine(t, b, c, d) { - return c * Math.sin(t/d * (Math.PI/2)) + b; - } - - /** - * @method easeInOutSine - * @memberOf fabric.util.ease - */ - function easeInOutSine(t, b, c, d) { - return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; - } - - /** - * @method easeInExpo - * @memberOf fabric.util.ease - */ - function easeInExpo(t, b, c, d) { - return (t===0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; - } - - /** - * @method easeOutExpo - * @memberOf fabric.util.ease - */ - function easeOutExpo(t, b, c, d) { - return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; - } - - /** - * @method easeInOutExpo - * @memberOf fabric.util.ease - */ - function easeInOutExpo(t, b, c, d) { - if (t===0) return b; - if (t===d) return b+c; - t /= d/2; - if (t < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; - return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; - } - - /** - * @method easeInCirc - * @memberOf fabric.util.ease - */ - function easeInCirc(t, b, c, d) { - return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; - } - - /** - * @method easeOutCirc - * @memberOf fabric.util.ease - */ - function easeOutCirc(t, b, c, d) { - return c * Math.sqrt(1 - (t=t/d-1)*t) + b; - } - - /** - * @method easeInOutCirc - * @memberOf fabric.util.ease - */ - function easeInOutCirc(t, b, c, d) { - t /= d/2; - if (t < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; - return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; - } - - /** - * @method easeInElastic - * @memberOf fabric.util.ease - */ - function easeInElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d; - if (t===1) return b+c; - if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - } - - /** - * @method easeOutElastic - * @memberOf fabric.util.ease - */ - function easeOutElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d; - if (t===1) return b+c; - if (!p) p=d*0.3; - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; - } - - /** - * @method easeInOutElastic - * @memberOf fabric.util.ease - */ - function easeInOutElastic(t, b, c, d) { - var s=1.70158;var p=0;var a=c; - if (t===0) return b; - t /= d/2; - if (t===2) return b+c; - if (!p) p=d*(0.3*1.5); - if (a < Math.abs(c)) { a=c; s=p/4; } - else s = p/(2*Math.PI) * Math.asin (c/a); - if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; - return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; - } - - /** - * @method easeInBack - * @memberOf fabric.util.ease - */ - function easeInBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - return c*(t/=d)*t*((s+1)*t - s) + b; - } - - /** - * @method easeOutBack - * @memberOf fabric.util.ease - */ - function easeOutBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; - } - - /** - * @method easeInOutBack - * @memberOf fabric.util.ease - */ - function easeInOutBack(t, b, c, d, s) { - if (s === undefined) s = 1.70158; - t /= d/2; - if (t < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; - return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; - } - - /** - * @method easeInBounce - * @memberOf fabric.util.ease - */ - function easeInBounce(t, b, c, d) { - return c - easeOutBounce (d-t, 0, c, d) + b; - } - - /** - * @method easeOutBounce - * @memberOf fabric.util.ease - */ - function easeOutBounce(t, b, c, d) { - if ((t/=d) < (1/2.75)) { - return c*(7.5625*t*t) + b; - } else if (t < (2/2.75)) { - return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; - } else if (t < (2.5/2.75)) { - return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; - } else { - return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; - } - } - - /** - * @method easeInOutBounce - * @memberOf fabric.util.ease - */ - function easeInOutBounce(t, b, c, d) { - if (t < d/2) return easeInBounce (t*2, 0, c, d) * 0.5 + b; - return easeOutBounce (t*2-d, 0, c, d) * 0.5 + c*0.5 + b; - } - - /** @namespace fabric.util.ease */ - fabric.util.ease = { - easeInQuad: easeInQuad, - easeOutQuad: easeOutQuad, - easeInOutQuad: easeInOutQuad, - easeInCubic: easeInCubic, - easeOutCubic: easeOutCubic, - easeInOutCubic: easeInOutCubic, - easeInQuart: easeInQuart, - easeOutQuart: easeOutQuart, - easeInOutQuart: easeInOutQuart, - easeInQuint: easeInQuint, - easeOutQuint: easeOutQuint, - easeInOutQuint: easeInOutQuint, - easeInSine: easeInSine, - easeOutSine: easeOutSine, - easeInOutSine: easeInOutSine, - easeInExpo: easeInExpo, - easeOutExpo: easeOutExpo, - easeInOutExpo: easeInOutExpo, - easeInCirc: easeInCirc, - easeOutCirc: easeOutCirc, - easeInOutCirc: easeInOutCirc, - easeInElastic: easeInElastic, - easeOutElastic: easeOutElastic, - easeInOutElastic: easeInOutElastic, - easeInBack: easeInBack, - easeOutBack: easeOutBack, - easeInOutBack: easeInOutBack, - easeInBounce: easeInBounce, - easeOutBounce: easeOutBounce, - easeInOutBounce: easeInOutBounce - }; - -}()); -(function(global) { - - "use strict"; - - /** - * @name fabric - * @namespace - */ - - var fabric = global.fabric || (global.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', - 'text-decoration': 'textDecoration', - 'font-size': 'fontSize', - 'font-weight': 'fontWeight', - 'font-style': 'fontStyle', - 'font-family': 'fontFamily' - }; - - function normalizeAttr(attr) { - // transform attribute names - if (attr in attributesMap) { - return attributesMap[attr]; - } - return attr; - } - - /** - * Returns an object of attributes' name/value, given element and an array of attribute names; - * Parses parent "g" nodes recursively upwards. - * @static - * @memberOf fabric - * @method parseAttributes - * @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 there's a parent container (`g` node), parse its attributes recursively upwards - 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) { - // "normalize" attribute values - 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); - } - attr = normalizeAttr(attr); - memo[attr] = isNaN(parsed) ? value : parsed; - } - return memo; - }, { }); - - // add values parsed from style, which take precedence over attributes - // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) - - ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); - return extend(parentAttributes, ownAttributes); - } - - /** - * Parses "transform" attribute, returning an array of values - * @static - * @function - * @memberOf fabric - * @method 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]; - } - } - - // identity matrix - var iMatrix = [ - 1, // a - 0, // b - 0, // c - 1, // d - 0, // e - 0 // f - ], - - // == begin transform regexp - 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*$', - - // http://www.w3.org/TR/SVG/coords.html#TransformAttribute - reTransformList = new RegExp(transform_list), - // == end transform regexp - - reTransform = new RegExp(transform); - - return function(attributeValue) { - - // start with identity matrix - var matrix = iMatrix.concat(); - - // return if no argument was given or - // an argument does not match transform attribute regexp - 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; - }; - })(); - - /** - * Parses "points" attribute, returning an array of values - * @static - * @memberOf fabric - * @method parsePointsAttribute - * @param points {String} points attribute string - * @return {Array} array of points - */ - function parsePointsAttribute(points) { - - // points attribute is required and must not be empty - if (!points) return null; - - points = points.trim(); - var asPairs = points.indexOf(',') > -1; - - points = points.split(/\s+/); - var parsedPoints = [ ], i, len; - - // points could look like "10,20 30,40" or "10 20 30 40" - if (asPairs) { - i = 0; - len = points.length; - for (; i < len; i++) { - var pair = points[i].split(','); - parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); - } - } - else { - i = 0; - len = points.length; - for (; i < len; i+=2) { - parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); - } - } - - // odd number of points is an error - if (parsedPoints.length % 2 !== 0) { - // return null; - } - - return parsedPoints; - } - - /** - * Parses "style" attribute, retuning an object with values - * @static - * @memberOf fabric - * @method parseStyleAttribute - * @param {SVGElement} element 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) return oStyle; - - if (typeof style === 'string') { - style = style.replace(/;$/, '').split(';').forEach(function (current) { - var attr = current.split(':'); - oStyle[normalizeAttr(attr[0].trim().toLowerCase())] = attr[1].trim(); - }); - } - else { - for (var prop in style) { - if (typeof style[prop] === 'undefined') continue; - oStyle[normalizeAttr(prop.toLowerCase())] = style[prop]; - } - } - - return oStyle; - } - - function resolveGradients(instances) { - for (var i = instances.length; i--; ) { - var instanceFillValue = instances[i].get('fill'); - - if (/^url\(/.test(instanceFillValue)) { - - var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); - - if (fabric.gradientDefs[gradientId]) { - instances[i].set('fill', - fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); - } - } - } - } - - /** - * Transforms an array of svg elements to corresponding fabric.* instances - * @static - * @memberOf fabric - * @method parseElements - * @param {Array} elements Array of elements to parse - * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) - * @param {Object} options Options object - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function parseElements(elements, callback, options, reviver) { - var instances = new Array(elements.length), i = elements.length; - - function checkIfDone() { - if (--i === 0) { - instances = instances.filter(function(el) { - return el != null; - }); - resolveGradients(instances); - callback(instances); - } - } - - for (var index = 0, el, len = elements.length; index < len; index++) { - el = elements[index]; - var klass = fabric[capitalize(el.tagName)]; - if (klass && klass.fromElement) { - try { - if (klass.async) { - klass.fromElement(el, (function(index, el) { - return function(obj) { - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - }; - })(index), options); - } - else { - var obj = klass.fromElement(el, options); - reviver && reviver(el, obj); - instances.splice(index, 0, obj); - checkIfDone(); - } - } - catch(e) { - fabric.log(e.message || e); - } - } - else { - checkIfDone(); - } - } - } - - /** - * Returns CSS rules for a given SVG document - * @static - * @function - * @memberOf fabric - * @method getCSSRules - * @param {SVGDocument} doc SVG document to parse - * @return {Object} CSS rules of this document - */ - function getCSSRules(doc) { - var styles = doc.getElementsByTagName('style'), - allRules = { }, - rules; - - // very crude parsing of style contents - for (var i = 0, len = styles.length; i < len; i++) { - var styleContents = styles[0].textContent; - - // remove comments - styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); - - rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); - rules = rules.map(function(rule) { return rule.trim(); }); - - rules.forEach(function(rule) { - var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); - rule = match[1]; - var declaration = match[2].trim(), - propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); - - if (!allRules[rule]) { - allRules[rule] = { }; - } - - for (var i = 0, len = propertyValuePairs.length; i < len; i++) { - var pair = propertyValuePairs[i].split(/\s*:\s*/), - property = pair[0], - value = pair[1]; - - allRules[rule][property] = value; - } - }); - } - - return allRules; - } - - function getGlobalStylesForElement(element) { - var nodeName = element.nodeName, - className = element.getAttribute('class'), - id = element.getAttribute('id'), - styles = { }; - - for (var rule in fabric.cssRules) { - var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || - (id && new RegExp('^#' + id).test(rule)) || - (new RegExp('^' + nodeName).test(rule)); - - if (ruleMatchesElement) { - for (var property in fabric.cssRules[rule]) { - styles[property] = fabric.cssRules[rule][property]; - } - } - } - - return styles; - } - - /** - * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback - * @static - * @function - * @memberOf fabric - * @method parseSVGDocument - * @param {SVGDocument} doc SVG document to parse - * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - fabric.parseSVGDocument = (function() { - - var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; - - // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute - // \d doesn't quite cut it (as we need to match an actual float number) - - // matches, e.g.: +14.56e-12, etc. - var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; - - var reViewBoxAttrValue = new RegExp( - '^' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*,?' + - '\\s*(' + reNum + '+)\\s*' + - '$' - ); - - function hasAncestorWithNodeName(element, nodeName) { - while (element && (element = element.parentNode)) { - if (nodeName.test(element.nodeName)) { - return true; - } - } - return false; - } - - return function(doc, callback, reviver) { - if (!doc) return; - - var startTime = new Date(), - descendants = fabric.util.toArray(doc.getElementsByTagName('*')); - - if (descendants.length === 0) { - // we're likely in node, where "o3-xml" library fails to gEBTN("*") - // https://github.com/ajaxorg/node-o3-xml/issues/21 - descendants = doc.selectNodes("//*[name(.)!='svg']"); - var arr = [ ]; - for (var i = 0, len = descendants.length; i < len; i++) { - arr[i] = descendants[i]; - } - descendants = arr; - } - - var elements = descendants.filter(function(el) { - return reAllowedSVGTagNames.test(el.tagName) && - !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement - }); - - 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); - } - - // values of width/height attributes overwrite those extracted from viewbox attribute - width = widthAttr ? parseFloat(widthAttr) : width; - height = heightAttr ? parseFloat(heightAttr) : height; - - var options = { - width: width, - height: height - }; - - fabric.gradientDefs = fabric.getGradientDefs(doc); - fabric.cssRules = getCSSRules(doc); - - // Precedence of rules: style > class > attribute - - fabric.parseElements(elements, function(instances) { - fabric.documentParsingTime = new Date() - startTime; - if (callback) { - callback(instances, options); - } - }, clone(options), reviver); - }; - })(); - - /** - * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) - * @property - * @namespace - */ - var svgCache = { - - /** - * @method has - * @param {String} name - * @param {Function} callback - */ - has: function (name, callback) { - callback(false); - }, - - /** - * @method get - * @param {String} url - * @param {Function} callback - */ - get: function () { - /* NOOP */ - }, - - /** - * @method set - * @param {String} url - * @param {Object} object - */ - set: function () { - /* NOOP */ - } - }; - - /** - * Takes url corresponding to an SVG document, and parses it into a set of fabric objects - * @method loadSVGFromURL - * @param {String} url - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromURL(url, callback, reviver) { - - url = url.replace(/^\n\s*/, '').trim(); - - svgCache.has(url, function (hasUrl) { - if (hasUrl) { - svgCache.get(url, function (value) { - var enlivedRecord = _enlivenCachedObject(value); - callback(enlivedRecord.objects, enlivedRecord.options); - }); - } - else { - new fabric.util.request(url, { - method: 'get', - onComplete: onComplete - }); - } - }); - - function onComplete(r) { - - var xml = r.responseXML; - if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { - xml = new ActiveXObject('Microsoft.XMLDOM'); - xml.async = 'false'; - //IE chokes on DOCTYPE - xml.loadXML(r.responseText.replace(//i,'')); - } - if (!xml.documentElement) return; - - fabric.parseSVGDocument(xml.documentElement, function (results, options) { - svgCache.set(url, { - objects: fabric.util.array.invoke(results, 'toObject'), - options: options - }); - callback(results, options); - }, reviver); - } - } - - /** - * @method _enlivenCachedObject - */ - function _enlivenCachedObject(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 }); - } - - /** - * Takes string corresponding to an SVG document, and parses it into a set of fabric objects - * @method loadSVGFromString - * @param {String} string - * @param {Function} callback - * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. - */ - function loadSVGFromString(string, callback, reviver) { - string = string.trim(); - var doc; - if (typeof DOMParser !== 'undefined') { - var parser = new DOMParser(); - if (parser && parser.parseFromString) { - doc = parser.parseFromString(string, 'text/xml'); - } - } - else if (fabric.window.ActiveXObject) { - doc = new ActiveXObject('Microsoft.XMLDOM'); - doc.async = 'false'; - //IE chokes on DOCTYPE - doc.loadXML(string.replace(//i,'')); - } - - fabric.parseSVGDocument(doc.documentElement, function (results, options) { - callback(results, options); - }, reviver); - } - - function createSVGFontFacesMarkup(objects) { - var markup = ''; - - for (var i = 0, len = objects.length; i < len; i++) { - if (objects[i].type !== 'text' || !objects[i].path) continue; - - markup += [ - '@font-face {', - 'font-family: ', objects[i].fontFamily, '; ', - 'src: url(\'', objects[i].path, '\')', - '}' - ].join(''); - } - - if (markup) { - markup = [ - '', - '', - '' - ].join(''); - } - - return markup; - } - - extend(fabric, { - - parseAttributes: parseAttributes, - parseElements: parseElements, - parseStyleAttribute: parseStyleAttribute, - parsePointsAttribute: parsePointsAttribute, - getCSSRules: getCSSRules, - - loadSVGFromURL: loadSVGFromURL, - loadSVGFromString: loadSVGFromString, - - createSVGFontFacesMarkup: createSVGFontFacesMarkup - }); - -})(typeof exports !== 'undefined' ? exports : this); - -(function() { - - function getColorStopFromStyle(el) { - var style = el.getAttribute('style'); - - if (style) { - var keyValuePairs = style.split(/\s*;\s*/); - - if (keyValuePairs[keyValuePairs.length-1] === '') { - keyValuePairs.pop(); - } - - for (var i = keyValuePairs.length; i--; ) { - - var split = keyValuePairs[i].split(/\s*:\s*/), - key = split[0].trim(), - value = split[1].trim(); - - if (key === 'stop-color') { - return value; - } - } - } - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Gradient = fabric.util.createClass(/** @scope fabric.Gradient.prototype */ { - - initialize: function(options) { - - options || (options = { }); - - this.x1 = options.x1 || 0; - this.y1 = options.y1 || 0; - this.x2 = options.x2 || 0; - this.y2 = options.y2 || 0; - - this.colorStops = options.colorStops; - }, - - toObject: function() { - return { - x1: this.x1, - x2: this.x2, - y1: this.y1, - y2: this.y2, - colorStops: this.colorStops - }; - }, - - toLiveGradient: function(ctx) { - var gradient = ctx.createLinearGradient( - this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); - - for (var position in this.colorStops) { - var colorValue = this.colorStops[position]; - gradient.addColorStop(parseFloat(position), colorValue); - } - - return gradient; - } - }); - - fabric.util.object.extend(fabric.Gradient, { - - /** - * @method fromElement - * @static - * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement - */ - fromElement: function(el, instance) { - - /** - * @example: - * - * - * - * - * - * - * OR - * - * - * - * - * - * - */ - - var colorStopEls = el.getElementsByTagName('stop'), - offset, - colorStops = { }, - coords = { - x1: el.getAttribute('x1') || 0, - y1: el.getAttribute('y1') || 0, - x2: el.getAttribute('x2') || '100%', - y2: el.getAttribute('y2') || 0 - }; - - for (var i = colorStopEls.length; i--; ) { - el = colorStopEls[i]; - offset = el.getAttribute('offset'); - - // convert percents to absolute values - offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); - colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); - } - - _convertPercentUnitsToValues(instance, coords); - - return new fabric.Gradient({ - x1: coords.x1, - y1: coords.y1, - x2: coords.x2, - y2: coords.y2, - colorStops: colorStops - }); - }, - - /** - * @method forObject - * @static - */ - forObject: function(obj, options) { - options || (options = { }); - _convertPercentUnitsToValues(obj, options); - return new fabric.Gradient(options); - } - }); - - function _convertPercentUnitsToValues(object, options) { - for (var prop in options) { - if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { - var percents = parseFloat(options[prop], 10); - if (prop === 'x1' || prop === 'x2') { - options[prop] = object.width * percents / 100; - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] = object.height * percents / 100; - } - } - // normalize rendering point (should be from top/left corner rather than center of the shape) - if (prop === 'x1' || prop === 'x2') { - options[prop] -= object.width / 2; - } - else if (prop === 'y1' || prop === 'y2') { - options[prop] -= object.height / 2; - } - } - } - - /** - * Parses an SVG document, returning all of the gradient declarations found in it - * @static - * @function - * @memberOf fabric - * @method getGradientDefs - * @param {SVGDocument} doc SVG document to parse - * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element - */ - function getGradientDefs(doc) { - var linearGradientEls = doc.getElementsByTagName('linearGradient'), - radialGradientEls = doc.getElementsByTagName('radialGradient'), - el, i, - gradientDefs = { }; - - i = linearGradientEls.length; - for (; i--; ) { - el = linearGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - i = radialGradientEls.length; - for (; i--; ) { - el = radialGradientEls[i]; - gradientDefs[el.getAttribute('id')] = el; - } - - return gradientDefs; - } - - fabric.getGradientDefs = getGradientDefs; - -})(); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Point) { - fabric.warn('fabric.Point is already defined'); - return; - } - - fabric.Point = Point; - - /** - * @name Point - * @memberOf fabric - * @constructor - * @param {Number} x - * @param {Number} y - * @return {fabric.Point} thisArg - */ - function Point(x, y) { - if (arguments.length > 0) { - this.init(x, y); - } - } - - Point.prototype = /** @scope fabric.Point.prototype */ { - - constructor: Point, - - /** - * @method init - * @param {Number} x - * @param {Number} y - */ - init: function (x, y) { - this.x = x; - this.y = y; - }, - - /** - * @method add - * @param {fabric.Point} that - * @return {fabric.Point} new Point instance with added values - */ - add: function (that) { - return new Point(this.x + that.x, this.y + that.y); - }, - - /** - * @method addEquals - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - */ - addEquals: function (that) { - this.x += that.x; - this.y += that.y; - return this; - }, - - /** - * @method scalarAdd - * @param {Number} scalar - * @return {fabric.Point} new Point with added value - */ - scalarAdd: function (scalar) { - return new Point(this.x + scalar, this.y + scalar); - }, - - /** - * @method scalarAddEquals - * @param {Number} scalar - * @param {fabric.Point} thisArg - */ - scalarAddEquals: function (scalar) { - this.x += scalar; - this.y += scalar; - return this; - }, - - /** - * @method subtract - * @param {fabric.Point} that - * @return {fabric.Point} new Point object with subtracted values - */ - subtract: function (that) { - return new Point(this.x - that.x, this.y - that.y); - }, - - /** - * @method subtractEquals - * @param {fabric.Point} that - * @return {fabric.Point} thisArg - */ - 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; - } - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Intersection) { - fabric.warn('fabric.Intersection is already defined'); - return; - } - - /** - * @class Intersection - * @memberOf fabric - */ - function Intersection(status) { - if (arguments.length > 0) { - this.init(status); - } - } - - fabric.Intersection = Intersection; - - fabric.Intersection.prototype = /** @scope fabric.Intersection.prototype */ { - - /** - * @method init - * @param {String} status - */ - init: function (status) { - this.status = status; - this.points = []; - }, - - /** - * @method appendPoint - * @param {String} status - */ - appendPoint: function (point) { - this.points.push(point); - }, - - /** - * @method appendPoints - * @param {String} status - */ - appendPoints: function (points) { - this.points = this.points.concat(points); - } - }; - - /** - * @static - * @method intersectLineLine - */ - fabric.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; - }; - - /** - * @method intersectLinePolygon - */ - fabric.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; - }; - - /** - * @method intersectPolygonPolygon - */ - fabric.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; - }; - - /** - * @method intersectPolygonRectangle - */ - fabric.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; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Color) { - fabric.warn('fabric.Color is already defined.'); - return; - } - - /** - * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; - * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. - * - * @class Color - * @memberOf fabric - * @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); - } - } - - fabric.Color = Color; - - fabric.Color.prototype = /** @scope fabric.Color.prototype */ { - - /** - * @private - * @method _tryParsingColor - */ - _tryParsingColor: function(color) { - var source = Color.sourceFromHex(color); - if (!source) { - source = Color.sourceFromRgb(color); - } - if (source) { - this.setSource(source); - } - }, - - /** - * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @method getSource - * @return {Array} - */ - getSource: function() { - return this._source; - }, - - /** - * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @method setSource - * @param {Array} source - */ - setSource: function(source) { - this._source = source; - }, - - /** - * Returns color represenation in RGB format - * @method toRgb - * @return {String} ex: rgb(0-255,0-255,0-255) - */ - toRgb: function() { - var source = this.getSource(); - return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; - }, - - /** - * Returns color represenation in RGBA format - * @method toRgba - * @return {String} ex: rgba(0-255,0-255,0-255,0-1) - */ - toRgba: function() { - var source = this.getSource(); - return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; - }, - - /** - * Returns color represenation in HEX format - * @method toHex - * @return {String} ex: FF5555 - */ - 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(); - }, - - /** - * Gets value of alpha channel for this color - * @method getAlpha - * @return {Number} 0-1 - */ - getAlpha: function() { - return this.getSource()[3]; - }, - - /** - * Sets value of alpha channel for this color - * @method setAlpha - * @param {Number} 0-1 - * @return {fabric.Color} thisArg - */ - setAlpha: function(alpha) { - var source = this.getSource(); - source[3] = alpha; - this.setSource(source); - return this; - }, - - /** - * Transforms color to its grayscale representation - * @method toGrayscale - * @return {fabric.Color} thisArg - */ - 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 {fabric.Color} thisArg - */ - 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 {String|fabric.Color} otherColor - * @return {fabric.Color} thisArg - */ - overlayWith: function(otherColor) { - if (!(otherColor instanceof Color)) { - 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[3] = alpha; - this.setSource(result); - return this; - } - }; - - /** - * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgb(255, 100, 10, 0.5), rgb(1,1,1)) - * @static - * @field - */ - fabric.Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/; - - /** - * Regex matching color in HEX format (ex: #FF5555, 010155, aff) - * @static - * @field - */ - fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; - - /** - * Returns new color object, when given a color in RGB format - * @method fromRgb - * @param {String} color ex: rgb(0-255,0-255,0-255) - * @return {fabric.Color} - */ - fabric.Color.fromRgb = function(color) { - return Color.fromSource(Color.sourceFromRgb(color)); - }; - - /** - * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format - * @method sourceFromRgb - * @param {String} color ex: rgb(0-255,0-255,0-255) - * @return {Array} source - */ - fabric.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 - ]; - } - }; - - /** - * Returns new color object, when given a color in RGBA format - * @static - * @function - * @method fromRgba - * @param {String} color - * @return {fabric.Color} - */ - fabric.Color.fromRgba = Color.fromRgb; - - /** - * Returns new color object, when given a color in HEX format - * @static - * @method fromHex - * @return {fabric.Color} - */ - fabric.Color.fromHex = function(color) { - return Color.fromSource(Color.sourceFromHex(color)); - }; - - /** - * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format - * @static - * @method sourceFromHex - * @param {String} color ex: FF5555 - * @return {Array} source - */ - fabric.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 - ]; - } - }; - - /** - * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) - * @static - * @method fromSource - * @return {fabric.Color} - */ - fabric.Color.fromSource = function(source) { - var oColor = new Color(); - oColor.setSource(source); - return oColor; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function () { - - "use strict"; - - if (fabric.StaticCanvas) { - fabric.warn('fabric.StaticCanvas is already defined.'); - return; - } - - // aliases for faster resolution - var extend = fabric.util.object.extend, - getElementOffset = fabric.util.getElementOffset, - removeFromArray = fabric.util.removeFromArray, - removeListener = fabric.util.removeListener, - - CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); - - /** - * @class fabric.StaticCanvas - * @constructor - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - fabric.StaticCanvas = function (el, options) { - options || (options = { }); - - this._initStatic(el, options); - fabric.StaticCanvas.activeInstance = this; - }; - - extend(fabric.StaticCanvas.prototype, fabric.Observable); - - extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ { - - /** - * Background color of canvas instance - * @property - * @type String - */ - backgroundColor: 'rgba(0, 0, 0, 0)', - - /** - * Background image of canvas instance - * Should be set via `setBackgroundImage` - * @property - * @type String - */ - backgroundImage: '', - - /** - * Opacity of the background image of the canvas instance - * @property - * @type Float - */ - backgroundImageOpacity: 1.0, - - /** - * Indicatus whether the background image should be stretched to fit the - * dimensions of the canvas instance. - * @property - * @type Boolean - */ - backgroundImageStretch: true, - - /** - * Indicates whether toObject/toDatalessObject should include default values - * @property - * @type Boolean - */ - includeDefaultValues: true, - - /** - * Indicates whether objects' state should be saved - * @property - * @type Boolean - */ - stateful: true, - - /** - * Indicates whether fabric.Canvas#add should also re-render canvas. - * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once - * (followed by a manual rendering after addition) - */ - renderOnAddition: true, - - /** - * Function that determines clipping of entire canvas area - * Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ - * @property - * @type Function - */ - clipTo: null, - - /** - * Indicates whether object controls (borders/corners) are rendered above overlay image - * @property - * @type Boolean - */ - controlsAboveOverlay: false, - - /** - * Callback; invoked right before object is about to be scaled/rotated - * @method onBeforeScaleRotate - * @param {fabric.Object} target Object that's about to be scaled/rotated - */ - onBeforeScaleRotate: function () { - /* NOOP */ - }, - - /** - * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps - * @method onFpsUpdate - * @param {Number} fps - */ - onFpsUpdate: null, - - _initStatic: function(el, options) { - this._objects = []; - - this._createLowerCanvas(el); - this._initOptions(options); - - if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); - } - if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); - } - this.calcOffset(); - }, - - /** - * Calculates canvas element offset relative to the document - * This method is also attached as "resize" event handler of window - * @method calcOffset - * @return {fabric.Canvas} instance - * @chainable - */ - calcOffset: function () { - this._offset = getElementOffset(this.lowerCanvasEl); - return this; - }, - - /** - * Sets overlay image for this canvas - * @method setOverlayImage - * @param {String} url url of an image to set overlay to - * @param {Function} callback callback to invoke when image is loaded and set as an overlay - * @return {fabric.Canvas} thisArg - * @chainable - */ - setOverlayImage: function (url, callback) { // TODO (kangax): test callback - fabric.util.loadImage(url, function(img) { - this.overlayImage = img; - callback && callback(); - }, this); - return this; - }, - - /** - * Sets background image for this canvas - * @method setBackgroundImage - * @param {String} url url of an image to set background to - * @param {Function} callback callback to invoke when image is loaded and set as background - * @param {Object} options optional options to set for the background image - * @return {fabric.Canvas} thisArg - * @chainable - */ - setBackgroundImage: function (url, callback, options) { - return fabric.util.loadImage(url, function(img) { - this.backgroundImage = img; - if (options && ('backgroundImageOpacity' in options)) { - this.backgroundImageOpacity = options.backgroundImageOpacity; - } - if (options && ('backgroundImageStretch' in options)) { - this.backgroundImageStretch = options.backgroundImageStretch; - } - callback && callback(); - }, this); - }, - - /** - * @private - * @method _createCanvasElement - * @param {Element} element - */ - _createCanvasElement: function() { - var element = fabric.document.createElement('canvas'); - if (!element.style) { - element.style = { }; - } - if (!element) { - throw CANVAS_INIT_ERROR; - } - this._initCanvasElement(element); - return element; - }, - - _initCanvasElement: function(element) { - if (typeof element.getContext === 'undefined' && - typeof G_vmlCanvasManager !== 'undefined' && - G_vmlCanvasManager.initElement) { - - G_vmlCanvasManager.initElement(element); - } - if (typeof element.getContext === 'undefined') { - throw CANVAS_INIT_ERROR; - } - }, - - /** - * @method _initOptions - * @param {Object} options - */ - _initOptions: function (options) { - for (var prop in options) { - this[prop] = options[prop]; - } - - this.width = parseInt(this.lowerCanvasEl.width, 10) || 0; - this.height = parseInt(this.lowerCanvasEl.height, 10) || 0; - - if (!this.lowerCanvasEl.style) return; - - this.lowerCanvasEl.style.width = this.width + 'px'; - this.lowerCanvasEl.style.height = this.height + 'px'; - }, - - /** - * Creates a secondary canvas - * @method _createLowerCanvas - */ - _createLowerCanvas: function (canvasEl) { - this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); - this._initCanvasElement(this.lowerCanvasEl); - - fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); - - if (this.interactive) { - this._applyCanvasStyle(this.lowerCanvasEl); - } - - this.contextContainer = this.lowerCanvasEl.getContext('2d'); - }, - - /** - * Returns canvas width - * @method getWidth - * @return {Number} - */ - getWidth: function () { - return this.width; - }, - - /** - * Returns canvas height - * @method getHeight - * @return {Number} - */ - getHeight: function () { - return this.height; - }, - - /** - * Sets width of this canvas instance - * @method setWidth - * @param {Number} width value to set width to - * @return {fabric.Canvas} instance - * @chainable true - */ - setWidth: function (value) { - return this._setDimension('width', value); - }, - - /** - * Sets height of this canvas instance - * @method setHeight - * @param {Number} height value to set height to - * @return {fabric.Canvas} instance - * @chainable true - */ - setHeight: function (value) { - return this._setDimension('height', value); - }, - - /** - * Sets dimensions (width, height) of this canvas instance - * @method setDimensions - * @param {Object} dimensions - * @return {fabric.Canvas} thisArg - * @chainable - */ - setDimensions: function(dimensions) { - for (var prop in dimensions) { - this._setDimension(prop, dimensions[prop]); - } - return this; - }, - - /** - * Helper for setting width/height - * @private - * @method _setDimensions - * @param {String} prop property (width|height) - * @param {Number} value value to set property to - * @return {fabric.Canvas} instance - * @chainable true - */ - _setDimension: function (prop, value) { - this.lowerCanvasEl[prop] = value; - this.lowerCanvasEl.style[prop] = value + 'px'; - - if (this.upperCanvasEl) { - this.upperCanvasEl[prop] = value; - this.upperCanvasEl.style[prop] = value + 'px'; - } - - if (this.wrapperEl) { - this.wrapperEl.style[prop] = value + 'px'; - } - - this[prop] = value; - - this.calcOffset(); - this.renderAll(); - - return this; - }, - - /** - * Returns <canvas> element corresponding to this instance - * @method getElement - * @return {HTMLCanvasElement} - */ - getElement: function () { - return this.lowerCanvasEl; - }, - - // placeholder - getActiveObject: function() { - return null; - }, - - // placeholder - getActiveGroup: function() { - return null; - }, - - /** - * Given a context, renders an object on that context - * @param ctx {Object} context to render object on - * @param object {Object} object to render - * @private - */ - _draw: function (ctx, object) { - if (!object) return; - - if (this.controlsAboveOverlay) { - var hasBorders = object.hasBorders, hasCorners = object.hasCorners; - object.hasBorders = object.hasCorners = false; - object.render(ctx); - object.hasBorders = hasBorders; - object.hasCorners = hasCorners; - } - else { - object.render(ctx); - } - }, - - /** - * Adds objects to canvas, then renders canvas; - * Objects should be instances of (or inherit from) fabric.Object - * @method add - * @return {fabric.Canvas} thisArg - * @chainable - */ - add: function () { - this._objects.push.apply(this._objects, arguments); - for (var i = arguments.length; i--; ) { - this._initObject(arguments[i]); - } - this.renderOnAddition && this.renderAll(); - return this; - }, - - /** - * @private - * @method _initObject - */ - _initObject: function(obj) { - this.stateful && obj.setupState(); - obj.setCoords(); - obj.canvas = this; - this.fire('object:added', { target: obj }); - obj.fire('added'); - }, - - /** - * 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 - * @param nonSplicing {Boolean} when `true`, no splicing (shifting) of objects occurs - * @return {fabric.Canvas} instance - */ - insertAt: function (object, index, nonSplicing) { - if (nonSplicing) { - this._objects[index] = object; - } - else { - this._objects.splice(index, 0, object); - } - this._initObject(object); - this.renderOnAddition && this.renderAll(); - return this; - }, - - /** - * Returns an array of objects this instance has - * @method getObjects - * @return {Array} - */ - getObjects: function () { - return this._objects; - }, - - /** - * Clears specified context of canvas element - * @method clearContext - * @param context {Object} ctx context to clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clearContext: function(ctx) { - ctx.clearRect(0, 0, this.width, this.height); - return this; - }, - - /** - * Returns context of canvas where objects are drawn - * @method getContext - * @return {CanvasRenderingContext2D} - */ - getContext: function () { - return this.contextContainer; - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @method clear - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - this._objects.length = 0; - this.clearContext(this.contextContainer); - if (this.contextTop) { - this.clearContext(this.contextTop); - } - this.renderAll(); - return this; - }, - - /** - * Renders both the top canvas and the secondary container canvas. - * @method renderAll - * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas - * @return {fabric.Canvas} instance - * @chainable - */ - renderAll: function (allOnTop) { - - var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; - - if (this.contextTop) { - this.clearContext(this.contextTop); - } - - if (allOnTop === false || (typeof allOnTop === 'undefined')) { - this.clearContext(canvasToDrawOn); - } - - var activeGroup = this.getActiveGroup(), - startTime = new Date(); - - if (this.clipTo) { - this._clipCanvas(canvasToDrawOn); - } - - canvasToDrawOn.fillStyle = this.backgroundColor; - canvasToDrawOn.fillRect(0, 0, this.width, this.height); - - if (typeof this.backgroundImage === 'object') { - this._drawBackroundImage(canvasToDrawOn); - } - - for (var i = 0, length = this._objects.length; i < length; ++i) { - if (!activeGroup || - (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { - this._draw(canvasToDrawOn, this._objects[i]); - } - } - - if (this.clipTo) { - canvasToDrawOn.restore(); - } - - // delegate rendering to group selection (if one exists) - if (activeGroup) { - this._draw(this.contextTop, activeGroup); - } - - if (this.overlayImage) { - this.contextContainer.drawImage(this.overlayImage, 0, 0); - } - - if (this.controlsAboveOverlay) { - this.drawControls(this.contextContainer); - } - - if (this.onFpsUpdate) { - var elapsedTime = new Date() - startTime; - this.onFpsUpdate(~~(1000 / elapsedTime)); - } - - this.fire('after:render'); - - return this; - }, - - _clipCanvas: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.beginPath(); - this.clipTo(canvasToDrawOn); - canvasToDrawOn.clip(); - }, - - _drawBackroundImage: function(canvasToDrawOn) { - canvasToDrawOn.save(); - canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; - - if (this.backgroundImageStretch) { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); - } - else { - canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); - } - canvasToDrawOn.restore(); - }, - - /** - * Method to render only the top canvas. - * Also used to render the group selection box. - * @method renderTop - * @return {fabric.Canvas} thisArg - * @chainable - */ - renderTop: function () { - this.clearContext(this.contextTop || this.contextContainer); - - if (this.overlayImage) { - this.contextContainer.drawImage(this.overlayImage, 0, 0); - } - - // we render the top context - last object - if (this.selection && this._groupSelector) { - this._drawSelection(); - } - - // delegate rendering to group selection if one exists - // used for drawing selection borders/corners - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.render(this.contextTop); - } - - this.fire('after:render'); - - return this; - }, - - /** - * Draws objects' controls (borders/corners) - * @method drawControls - * @param {Object} ctx context to render controls on - */ - drawControls: function(ctx) { - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - ctx.save(); - fabric.Group.prototype.transform.call(activeGroup, ctx); - activeGroup.drawBorders(ctx).drawCorners(ctx); - ctx.restore(); - } - else { - for (var i = 0, len = this._objects.length; i < len; ++i) { - if (!this._objects[i].active) continue; - - ctx.save(); - fabric.Object.prototype.transform.call(this._objects[i], ctx); - this._objects[i].drawBorders(ctx).drawCorners(ctx); - ctx.restore(); - } - } - }, - - /** - * Exports canvas element to a dataurl image. - * @method toDataURL - * @param {String} format the format of the output image. Either "jpeg" or "png". - * @param {Number} quality quality level (0..1) - * @return {String} - */ - toDataURL: function (format, quality) { - var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; - - this.renderAll(true); - var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) - ? canvasEl.toDataURL('image/' + format, quality) - : canvasEl.toDataURL('image/' + format); - this.renderAll(); - return data; - }, - - /** - * Exports canvas element to a dataurl image (allowing to change image size via multiplier). - * @method toDataURLWithMultiplier - * @param {String} format (png|jpeg) - * @param {Number} multiplier - * @param {Number} quality (0..1) - * @return {String} - */ - toDataURLWithMultiplier: function (format, multiplier, quality) { - - var origWidth = this.getWidth(), - origHeight = this.getHeight(), - scaledWidth = origWidth * multiplier, - scaledHeight = origHeight * multiplier, - activeObject = this.getActiveObject(), - activeGroup = this.getActiveGroup(); - - this.setWidth(scaledWidth).setHeight(scaledHeight); - this.contextTop.scale(multiplier, multiplier); - - if (activeGroup) { - // not removing group due to complications with restoring it with correct state afterwords - this._tempRemoveBordersCornersFromGroup(activeGroup); - } - else if (activeObject) { - this.deactivateAll(); - } - - // restoring width, height for `renderAll` to draw - // background properly (while context is scaled) - this.width = origWidth; - this.height = origHeight; - - this.renderAll(true); - - var dataURL = this.toDataURL(format, quality); - - this.contextTop.scale(1 / multiplier, 1 / multiplier); - this.setWidth(origWidth).setHeight(origHeight); - - if (activeGroup) { - this._restoreBordersCornersOnGroup(activeGroup); - } - else if (activeObject) { - this.setActiveObject(activeObject); - } - - this.renderAll(); - - return dataURL; - }, - - _tempRemoveBordersCornersFromGroup: function(group) { - group.origHideCorners = group.hideCorners; - group.origBorderColor = group.borderColor; - - group.hideCorners = true; - group.borderColor = 'rgba(0,0,0,0)'; - - group.forEachObject(function(o) { - o.origBorderColor = o.borderColor; - o.borderColor = 'rgba(0,0,0,0)'; - }); - }, - _restoreBordersCornersOnGroup: function(group) { - group.hideCorners = group.origHideCorners; - group.borderColor = group.origBorderColor; - - group.forEachObject(function(o) { - o.borderColor = o.origBorderColor; - delete o.origBorderColor; - }); - }, - - /** - * Returns coordinates of a center of canvas. - * Returned value is an object with top and left properties - * @method getCenter - * @return {Object} object with "top" and "left" number values - */ - getCenter: function () { - return { - top: this.getHeight() / 2, - left: this.getWidth() / 2 - }; - }, - - /** - * Centers object horizontally. - * @method centerObjectH - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - */ - centerObjectH: function (object) { - object.set('left', this.getCenter().left); - this.renderAll(); - return this; - }, - - /** - * Centers object vertically. - * @method centerObjectH - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObjectV: function (object) { - object.set('top', this.getCenter().top); - this.renderAll(); - return this; - }, - - /** - * Centers object vertically and horizontally. - * @method centerObject - * @param {fabric.Object} object Object to center - * @return {fabric.Canvas} thisArg - * @chainable - */ - centerObject: function (object) { - return this.centerObjectH(object).centerObjectV(object); - }, - - /** - * Returs dataless JSON representation of canvas - * @method toDatalessJSON - * @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) { - var data = { - objects: this._objects.map(function (instance) { - // TODO (kangax): figure out how to clean this up - var originalValue; - if (!this.includeDefaultValues) { - originalValue = instance.includeDefaultValues; - instance.includeDefaultValues = false; - } - var object = instance[methodName](); - if (!this.includeDefaultValues) { - instance.includeDefaultValues = originalValue; - } - return object; - }, this), - background: this.backgroundColor - }; - if (this.backgroundImage) { - data.backgroundImage = this.backgroundImage.src; - data.backgroundImageOpacity = this.backgroundImageOpacity; - data.backgroundImageStretch = this.backgroundImageStretch; - } - return data; - }, - - /** - * Returns SVG representation of canvas - * @function - * @method toSVG - * @return {String} - */ - toSVG: function() { - var markup = [ - '', - '', - '', - 'Created with Fabric.js ', fabric.version, '', - fabric.createSVGFontFacesMarkup(this.getObjects()) - ]; - - if (this.backgroundImage) { - markup.push( - '' - ); - } - - for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { - markup.push(objects[i].toSVG()); - } - markup.push(''); - - return markup.join(''); - }, - - /** - * Returns true if canvas contains no objects - * @method isEmpty - * @return {Boolean} true if canvas is empty - */ - isEmpty: function () { - return this._objects.length === 0; - }, - - /** - * 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); - if (this.getActiveObject() === object) { - - // removing active object should fire "selection:cleared" events - this.fire('before:selection:cleared', { target: object }); - this.discardActiveObject(); - this.fire('selection:cleared'); - } - this.renderAll(); - return object; - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @param object {fabric.Object} Object to send to back - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendToBack: function (object) { - removeFromArray(this._objects, object); - this._objects.unshift(object); - return this.renderAll(); - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringToFront: function (object) { - removeFromArray(this._objects, object); - this._objects.push(object); - return this.renderAll(); - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - sendBackwards: function (object) { - var idx = this._objects.indexOf(object), - nextIntersectingIdx = idx; - - // if object is not on the bottom of stack - if (idx !== 0) { - - // traverse down the stack looking for the nearest intersecting object - for (var i=idx-1; i>=0; --i) { - if (object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i])) { - nextIntersectingIdx = i; - break; - } - } - removeFromArray(this._objects, object); - this._objects.splice(nextIntersectingIdx, 0, object); - } - return this.renderAll(); - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @param object {fabric.Object} Object to send - * @return {fabric.Canvas} thisArg - * @chainable - */ - bringForward: function (object) { - var objects = this.getObjects(), - idx = objects.indexOf(object), - nextIntersectingIdx = idx; - - - // if object is not on top of stack (last item in an array) - if (idx !== objects.length-1) { - - // traverse up the stack looking for the nearest intersecting object - for (var i = idx + 1, l = this._objects.length; i < l; ++i) { - if (object.intersectsWithObject(objects[i]) || object.isContainedWithinObject(this._objects[i])) { - nextIntersectingIdx = i; - break; - } - } - removeFromArray(objects, object); - objects.splice(nextIntersectingIdx, 0, object); - } - this.renderAll(); - }, - - /** - * Returns object at specified index - * @method item - * @param {Number} index - * @return {fabric.Object} - */ - item: function (index) { - return this.getObjects()[index]; - }, - - /** - * 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); - }, - - /** - * Iterates over all objects, invoking callback for each one of them - * @method forEachObject - * @return {fabric.Canvas} thisArg - */ - forEachObject: function(callback, context) { - var objects = this.getObjects(), - i = objects.length; - while (i--) { - callback.call(context, objects[i], i, objects); - } - return this; - }, - - /** - * Clears a canvas element and removes all event handlers. - * @method dispose - * @return {fabric.Canvas} thisArg - * @chainable - */ - dispose: function () { - this.clear(); - if (this.interactive) { - removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - removeListener(fabric.window, 'resize', this._onResize); - } - return this; - }, - - /** - * @private - * @method _resizeImageToFit - * @param {HTMLImageElement} imgEl - */ - _resizeImageToFit: function (imgEl) { - - var imageWidth = imgEl.width || imgEl.offsetWidth, - widthScaleFactor = this.getWidth() / imageWidth; - - // scale image down so that it has original dimensions when printed in large resolution - if (imageWidth) { - imgEl.width = imageWidth * widthScaleFactor; - } - } - }); - - /** - * Returns a string representation of an instance - * @method toString - * @return {String} string representation of an instance - */ - fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet - return '#'; - }; - - extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ { - - /** - * @static - * @property EMPTY_JSON - * @type String - */ - EMPTY_JSON: '{"objects": [], "background": "white"}', - - /** - * Takes element and transforms its data in such way that it becomes grayscale - * @static - * @method toGrayscale - * @param {HTMLCanvasElement} canvasEl - */ - toGrayscale: function (canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = imageData.width, - jLen = imageData.height, - index, average, i, j; - - for (i = 0; i < iLen; i++) { - for (j = 0; j < jLen; j++) { - - index = (i * 4) * jLen + (j * 4); - average = (data[index] + data[index + 1] + data[index + 2]) / 3; - - data[index] = average; - data[index + 1] = average; - data[index + 2] = average; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * Provides a way to check support of some of the canvas methods - * (either those of HTMLCanvasElement itself, or rendering context) - * - * @method supports - * @param methodName {String} Method to check support for; - * Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality" - * @return {Boolean | null} `true` if method is supported (or at least exists), - * `null` if canvas element or context can not be initialized - */ - supports: function (methodName) { - var el = fabric.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'; - - case 'toDataURLWithQuality': - try { - el.toDataURL('image/jpeg', 0); - return true; - } - catch (e) { } - return false; - - default: - return null; - } - } - }); - - /** - * Returs JSON representation of canvas - * @function - * @method toJSON - * @return {String} json string - */ - fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; - -})(); -(function() { - - var extend = fabric.util.object.extend, - getPointer = fabric.util.getPointer, - addListener = fabric.util.addListener, - removeListener = fabric.util.removeListener, - 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' - }, - - 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, - - STROKE_OFFSET = 0.5; - - /** - * @class fabric.Canvas - * @constructor - * @extends fabric.StaticCanvas - * @param {HTMLElement | String} el <canvas> element to initialize instance on - * @param {Object} [options] Options object - */ - fabric.Canvas = function(el, options) { - options || (options = { }); - - this._initStatic(el, options); - this._initInteractive(); - this._createCacheCanvas(); - - fabric.Canvas.activeInstance = this; - }; - - function ProtoProxy(){ } - ProtoProxy.prototype = fabric.StaticCanvas.prototype; - fabric.Canvas.prototype = new ProtoProxy(); - - var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { - - /** - * Indicates that canvas is interactive. This property should not be changed. - * @property - * @type Boolean - */ - interactive: true, - - /** - * Indicates whether group selection should be enabled - * @property - * @type Boolean - */ - selection: true, - - /** - * Color of selection - * @property - * @type String - */ - selectionColor: 'rgba(100, 100, 255, 0.3)', // blue - - /** - * Color of the border of selection (usually slightly darker than color of selection itself) - * @property - * @type String - */ - selectionBorderColor: 'rgba(255, 255, 255, 0.3)', - - /** - * Width of a line used in object/group selection - * @property - * @type Number - */ - selectionLineWidth: 1, - - /** - * Color of the line used in free drawing mode - * @property - * @type String - */ - freeDrawingColor: 'rgb(0, 0, 0)', - - /** - * Width of a line used in free drawing mode - * @property - * @type Number - */ - freeDrawingLineWidth: 1, - - /** - * Default cursor value used when hovering over an object on canvas - * @property - * @type String - */ - hoverCursor: 'move', - - /** - * Default cursor value used when moving an object on canvas - * @property - * @type String - */ - moveCursor: 'move', - - /** - * Default cursor value used for the entire canvas - * @property - * @type String - */ - defaultCursor: 'default', - - /** - * Cursor value used for rotation point - * @property - * @type String - */ - rotationCursor: 'crosshair', - - /** - * Default element class that's given to wrapper (div) element of canvas - * @property - * @type String - */ - containerClass: 'canvas-container', - - perPixelTargetFind: false, - - targetFindTolerance: 0, - - _initInteractive: function() { - this._currentTransform = null; - this._groupSelector = null; - this._freeDrawingXPoints = [ ]; - this._freeDrawingYPoints = [ ]; - this._initWrapperElement(); - this._createUpperCanvas(); - this._initEvents(); - this.calcOffset(); - }, - - /** - * Adds mouse listeners to canvas - * @method _initEvents - * @private - * See configuration documentation for more details. - */ - _initEvents: function () { - var _this = this; - - this._onMouseDown = function (e) { - _this.__onMouseDown(e); - - addListener(fabric.document, 'mouseup', _this._onMouseUp); - fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); - - addListener(fabric.document, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); - - removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); - }; - - this._onMouseUp = function (e) { - _this.__onMouseUp(e); - - removeListener(fabric.document, 'mouseup', _this._onMouseUp); - fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); - - removeListener(fabric.document, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); - - addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); - fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); - }; - - this._onMouseMove = function (e) { - e.preventDefault && e.preventDefault(); - _this.__onMouseMove(e); - }; - - this._onResize = function () { - _this.calcOffset(); - }; - - - addListener(fabric.window, 'resize', this._onResize); - - if (fabric.isTouchSupported) { - addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); - addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); - } - else { - addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); - addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); - } - }, - - /** - * 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) { - - var target; - - if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._finalizeDrawingPath(); - this.fire('mouse:up', { e: e }); - return; - } - - if (this._currentTransform) { - - var transform = this._currentTransform; - - target = transform.target; - if (target._scaling) { - target._scaling = false; - } - - // determine the new coords everytime the image changes its position - var i = this._objects.length; - while (i--) { - this._objects[i].setCoords(); - } - - // only fire :modified event if target coordinates were changed during mousedown-mouseup - if (this.stateful && target.hasStateChanged()) { - target.isMoving = false; - this.fire('object:modified', { target: target }); - target.fire('modified'); - } - } - - this._currentTransform = null; - - if (this._groupSelector) { - // group selection was completed, determine its bounds - this._findSelectedObjects(e); - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - activeGroup.setObjectsCoords(); - activeGroup.set('isMoving', false); - this._setCursor(this.defaultCursor); - } - - // clear selection - this._groupSelector = null; - this.renderAll(); - - this._setCursorFromEvent(e, target); - - // fix for FF - this._setCursor(''); - - var _this = this; - setTimeout(function () { - _this._setCursorFromEvent(e, target); - }, 50); - - this.fire('mouse:up', { target: target, e: e }); - target && target.fire('mouseup', { e: e }); - }, - - /** - * 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) { - - // accept only left clicks - var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; - if (!isLeftClick && !fabric.isTouchSupported) return; - - if (this.isDrawingMode) { - this._prepareForDrawing(e); - - // capture coordinates immediately; this allows to draw dots (when movement never occurs) - this._captureDrawingPath(e); - this.fire('mouse:down', { e: e }); - return; - } - - // ignore if some object is being transformed at this moment - if (this._currentTransform) return; - - var target = this.findTarget(e), - pointer = this.getPointer(e), - activeGroup = this.getActiveGroup(), - corner; - - if (this._shouldClearSelection(e)) { - - this._groupSelector = { - ex: pointer.x, - ey: pointer.y, - top: 0, - left: 0 - }; - - this.deactivateAllWithDispatch(); - } - else { - // determine if it's a drag or rotate case - // rotate and scale will happen at the same time - this.stateful && target.saveState(); - - if ((corner = target._findTargetCorner(e, this._offset))) { - this.onBeforeScaleRotate(target); - } - - this._setupCurrentTransform(e, target); - - var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()) && this.selection; - if (shouldHandleGroupLogic) { - this._handleGroupLogic(e, target); - } - else { - if (target !== this.getActiveGroup()) { - this.deactivateAll(); - } - this.setActiveObject(target, e); - } - } - // we must renderAll so that active image is placed on the top canvas - this.renderAll(); - - this.fire('mouse:down', { target: target, e: e }); - target && target.fire('mousedown', { e: e }); - }, - - /** - * 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) { - - var target; - - if (this.isDrawingMode) { - if (this._isCurrentlyDrawing) { - this._captureDrawingPath(e); - } - this.fire('mouse:move', { e: e }); - return; - } - - var groupSelector = this._groupSelector, pointer; - - // We initially clicked in an empty area, so we draw a box for multiple selection. - if (groupSelector !== null) { - pointer = getPointer(e); - - groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; - groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; - this.renderTop(); - } - else if (!this._currentTransform) { - - // alias style to elimintate unnecessary lookup - var style = this.upperCanvasEl.style; - - // Here we are hovering the canvas then we will determine - // what part of the pictures we are hovering to change the caret symbol. - // We won't do that while dragging or rotating in order to improve the - // performance. - target = this.findTarget(e); - - if (!target) { - // image/text was hovered-out from, we remove its borders - for (var i = this._objects.length; i--; ) { - if (this._objects[i] && !this._objects[i].active) { - this._objects[i].setActive(false); - } - } - style.cursor = this.defaultCursor; - } - else { - // set proper cursor - this._setCursorFromEvent(e, target); - if (target.isActive()) { - // display corners when hovering over an image - target.setCornersVisibility && target.setCornersVisibility(true); - } - } - } - else { - // object is being transformed (scaled/rotated/moved/etc.) - pointer = getPointer(e); - - var x = pointer.x, - y = pointer.y; - - this._currentTransform.target.isMoving = true; - - if (this._currentTransform.action === 'rotate') { - // rotate object only if shift key is not pressed - // and if it is not a group we are transforming - - if (!e.shiftKey) { - this._rotateObject(x, y); - - this.fire('object:rotating', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('rotating'); - } - if (!this._currentTransform.target.hasRotatingPoint) { - this._scaleObject(x, y); - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - } - else if (this._currentTransform.action === 'scale') { - this._scaleObject(x, y); - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else if (this._currentTransform.action === 'scaleX') { - this._scaleObject(x, y, 'x'); - - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else if (this._currentTransform.action === 'scaleY') { - this._scaleObject(x, y, 'y'); - - this.fire('object:scaling', { - target: this._currentTransform.target - }); - this._currentTransform.target.fire('scaling'); - } - else { - this._translateObject(x, y); - - this.fire('object:moving', { - target: this._currentTransform.target - }); - - this._setCursor(this.moveCursor); - - this._currentTransform.target.fire('moving'); - } - // only commit here. when we are actually moving the pictures - this.renderAll(); - } - this.fire('mouse:move', { target: target, e: e }); - target && target.fire('mousemove', { e: e }); - }, - - /** - * Applies one implementation of 'point inside polygon' algorithm - * @method containsPoint - * @param e { Event } event object - * @param target { fabric.Object } object to test against - * @return {Boolean} true if point contains within area of given object - */ - containsPoint: function (e, target) { - var pointer = this.getPointer(e), - xy = this._normalizePointer(target, pointer), - x = xy.x, - y = xy.y; - - // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html - // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html - - // we iterate through each object. If target found, return it. - var iLines = target._getImageLines(target.oCoords), - xpoints = target._findCrossPoints(x, y, iLines); - - // if xcount is odd then we clicked inside the object - // For the specific case of square images xcount === 1 in all true cases - if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { - return true; - } - return false; - }, - - /** - * @private - * @method _normalizePointer - */ - _normalizePointer: function (object, pointer) { - - var activeGroup = this.getActiveGroup(), - x = pointer.x, - y = pointer.y; - - var isObjectInGroup = ( - activeGroup && - object.type !== 'group' && - activeGroup.contains(object) - ); - - if (isObjectInGroup) { - x -= activeGroup.left; - y -= activeGroup.top; - } - return { x: x, y: y }; - }, - - _isTargetTransparent: function (target, x, y) { - var cacheContext = this.contextCache; - - var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; - target.hasBorders = target.transparentCorners = false; - - this._draw(cacheContext, target); - - target.hasBorders = hasBorders; - target.transparentCorners = transparentCorners; - - // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 - if (this.targetFindTolerance > 0) { - if (x > this.targetFindTolerance) { - x -= this.targetFindTolerance; - } - else { - x = 0; - } - if (y > this.targetFindTolerance) { - y -= this.targetFindTolerance; - } - else { - y = 0; - } - } - - var isTransparent = true; - var imageData = cacheContext.getImageData( - x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); - - // Split image data - for tolerance > 1, pixelDataSize = 4; - for (var i = 3; i < imageData.data.length; i += 4) { - var temp = imageData.data[i]; - isTransparent = temp <= 0; - if (isTransparent === false) break; //Stop if colour found - } - - imageData = null; - this.clearContext(cacheContext); - return isTransparent; - }, - - /** - * @private - * @method _shouldClearSelection - */ - _shouldClearSelection: function (e) { - var target = this.findTarget(e), - activeGroup = this.getActiveGroup(); - return ( - !target || ( - target && - activeGroup && - !activeGroup.contains(target) && - activeGroup !== target && - !e.shiftKey - ) - ); - }, - - /** - * @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' - : corner === 'mtr' - ? 'rotate' - : (target.hasRotatingPoint) - ? 'scale' - : '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 === this.getActiveGroup()) { - // if it's a group, find target again, this time skipping group - target = this.findTarget(e, true); - // if even object is not found, bail out - if (!target || target.isType('group')) { - return; - } - } - var activeGroup = this.getActiveGroup(); - if (activeGroup) { - if (activeGroup.contains(target)) { - activeGroup.removeWithUpdate(target); - target.setActive(false); - if (activeGroup.size() === 1) { - // remove group alltogether if after removal it only contains 1 object - this.discardActiveGroup(); - } - } - else { - activeGroup.addWithUpdate(target); - } - this.fire('selection:created', { target: activeGroup, e: e }); - activeGroup.setActive(true); - } - else { - // group does not exist - if (this._activeObject) { - // only if there's an active object - if (target !== this._activeObject) { - // and that object is not the actual target - var group = new fabric.Group([ this._activeObject, target ]); - this.setActiveGroup(group); - activeGroup = this.getActiveGroup(); - } - } - // activate target object in any case - target.setActive(true); - } - - if (activeGroup) { - activeGroup.saveCoords(); - } - }, - - /** - * @private - * @method _prepareForDrawing - */ - _prepareForDrawing: function(e) { - - this._isCurrentlyDrawing = true; - - this.discardActiveObject().renderAll(); - - var pointer = this.getPointer(e); - - this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; - - this._freeDrawingXPoints.push(pointer.x); - this._freeDrawingYPoints.push(pointer.y); - - this.contextTop.beginPath(); - this.contextTop.moveTo(pointer.x, pointer.y); - this.contextTop.strokeStyle = this.freeDrawingColor; - this.contextTop.lineWidth = this.freeDrawingLineWidth; - this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; - }, - - /** - * @private - * @method _captureDrawingPath - */ - _captureDrawingPath: function(e) { - var pointer = this.getPointer(e); - - this._freeDrawingXPoints.push(pointer.x); - this._freeDrawingYPoints.push(pointer.y); - - this.contextTop.lineTo(pointer.x, pointer.y); - this.contextTop.stroke(); - }, - - /** - * @private - * @method _finalizeDrawingPath - */ - _finalizeDrawingPath: function() { - - this.contextTop.closePath(); - - this._isCurrentlyDrawing = false; - - var minX = utilMin(this._freeDrawingXPoints), - minY = utilMin(this._freeDrawingYPoints), - maxX = utilMax(this._freeDrawingXPoints), - maxY = utilMax(this._freeDrawingYPoints), - path = [ ], - xPoints = this._freeDrawingXPoints, - yPoints = this._freeDrawingYPoints; - - path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); - - for (var i = 1, len = xPoints.length; i < len; i++) { - path.push('L ', xPoints[i] - minX, ' ', yPoints[i] - minY, ' '); - } - - // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, - // and instead fire something like "drawing:completed" event with path string - - path = path.join(''); - - if (path === "M 0 0 L 0 0 ") { - // do not create 0 width/height paths, as they are rendered inconsistently across browsers - // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing - this.renderAll(); - return; - } - - var p = new fabric.Path(path); - - 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(); - this.fire('path:created', { path: p }); - }, - - /** - * 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.lockMovementX || target.set('left', x - this._currentTransform.offsetX); - target.lockMovementY || 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.lockScalingX && target.lockScalingY) 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.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); - target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); - } - else if (by === 'x' && !target.lockUniScaling) { - target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); - } - else if (by === 'y' && !target.lockUniScaling) { - target.lockScalingY || 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._theta = (curAngle - lastAngle) + t.theta; - }, - - /** - * @method _setCursor - */ - _setCursor: function (value) { - this.upperCanvasEl.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.upperCanvasEl.style; - if (!target) { - s.cursor = this.defaultCursor; - return false; - } - else { - var activeGroup = this.getActiveGroup(); - // only show proper corner when group selection is not active - var corner = !!target._findTargetCorner - && (!activeGroup || !activeGroup.contains(target)) - && target._findTargetCorner(e, this._offset); - - if (!corner) { - s.cursor = this.hoverCursor; - } - else { - if (corner in cursorMap) { - s.cursor = cursorMap[corner]; - } else if (corner === 'mtr' && target.hasRotatingPoint) { - s.cursor = this.rotationCursor; - } else { - s.cursor = this.defaultCursor; - return false; - } - } - } - return true; - }, - - /** - * @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 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) continue; - - if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || - currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { - - if (this.selection && currentObject.selectable) { - currentObject.setActive(true); - group.push(currentObject); - } - } - } - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } - else if (group.length > 1) { - group = new fabric.Group(group); - this.setActiveGroup(group); - group.saveCoords(); - this.fire('selection:created', { target: group }); - } - - this.renderAll(); - }, - - /** - * Method that determines what object we are clicking on - * @method findTarget - * @param {Event} e mouse event - * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through - */ - findTarget: function (e, skipGroup) { - - var target, - pointer = this.getPointer(e); - - // first check current group (if one exists) - var activeGroup = this.getActiveGroup(); - - if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { - target = activeGroup; - return target; - } - - // then check all of the objects on canvas - // Cache all targets where their bounding box contains point. - var possibleTargets = []; - for (var i = this._objects.length; i--; ) { - if (this._objects[i] && this.containsPoint(e, this._objects[i])) { - if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { - possibleTargets[possibleTargets.length] = this._objects[i]; - } - else { - target = this._objects[i]; - this.relatedTarget = target; - break; - } - } - } - for (var j = 0, len = possibleTargets.length; j < len; j++) { - pointer = this.getPointer(e); - var isTransparent = this._isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); - if (!isTransparent) { - target = possibleTargets[j]; - this.relatedTarget = target; - break; - } - } - if (target && target.selectable) { - return target; - } - }, - - /** - * Returns pointer coordinates relative to canvas. - * @method getPointer - * @return {Object} object with "x" and "y" number values - */ - getPointer: function (e) { - var pointer = getPointer(e); - return { - x: pointer.x - this._offset.left, - y: pointer.y - this._offset.top - }; - }, - - /** - * @method _createUpperCanvas - * @param {HTMLElement|String} canvasEl Canvas element - * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized - */ - _createUpperCanvas: function () { - this.upperCanvasEl = this._createCanvasElement(); - this.upperCanvasEl.className = 'upper-canvas'; - - this.wrapperEl.appendChild(this.upperCanvasEl); - - this._applyCanvasStyle(this.upperCanvasEl); - this.contextTop = this.upperCanvasEl.getContext('2d'); - }, - - _createCacheCanvas: function () { - this.cacheCanvasEl = this._createCanvasElement(); - this.cacheCanvasEl.setAttribute('width', this.width); - this.cacheCanvasEl.setAttribute('height', this.height); - this.contextCache = this.cacheCanvasEl.getContext('2d'); - }, - - /** - * @private - * @method _initWrapperElement - * @param {Number} width - * @param {Number} height - */ - _initWrapperElement: function () { - this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { - 'class': this.containerClass - }); - fabric.util.setStyle(this.wrapperEl, { - width: this.getWidth() + 'px', - height: this.getHeight() + 'px', - position: 'relative' - }); - fabric.util.makeElementUnselectable(this.wrapperEl); - }, - - /** - * @private - * @method _applyCanvasStyle - * @param {Element} element - */ - _applyCanvasStyle: function (element) { - var width = this.getWidth() || element.width, - height = this.getHeight() || element.height; - - fabric.util.setStyle(element, { - position: 'absolute', - width: width + 'px', - height: height + 'px', - left: 0, - top: 0 - }); - element.width = width; - element.height = height; - fabric.util.makeElementUnselectable(element); - }, - - /** - * Returns context of canvas where object selection is drawn - * @method getSelectionContext - * @return {CanvasRenderingContext2D} - */ - getSelectionContext: function() { - return this.contextTop; - }, - - /** - * Returns <canvas> element on which object selection is drawn - * @method getSelectionElement - * @return {HTMLCanvasElement} - */ - getSelectionElement: function () { - return this.upperCanvasEl; - }, - - /** - * Sets given object as active - * @method setActiveObject - * @param object {fabric.Object} Object to set as an active one - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveObject: function (object, e) { - if (this._activeObject) { - this._activeObject.setActive(false); - } - this._activeObject = object; - object.setActive(true); - - this.renderAll(); - - this.fire('object:selected', { target: object, e: e }); - object.fire('selected', { e: e }); - return this; - }, - - /** - * Returns currently active object - * @method getActiveObject - * @return {fabric.Object} active object - */ - getActiveObject: function () { - return this._activeObject; - }, - - /** - * Discards currently active object - * @method discardActiveObject - * @return {fabric.Canvas} thisArg - * @chainable - */ - discardActiveObject: function () { - if (this._activeObject) { - this._activeObject.setActive(false); - } - this._activeObject = null; - return this; - }, - - /** - * Sets active group to a speicified one - * @method setActiveGroup - * @param {fabric.Group} group Group to set as a current one - * @return {fabric.Canvas} thisArg - * @chainable - */ - setActiveGroup: function (group) { - this._activeGroup = group; - group && group.setActive(true); - return this; - }, - - /** - * Returns currently active group - * @method getActiveGroup - * @return {fabric.Group} Current group - */ - getActiveGroup: function () { - return this._activeGroup; - }, - - /** - * Removes currently active group - * @method discardActiveGroup - * @return {fabric.Canvas} thisArg - */ - discardActiveGroup: function () { - var g = this.getActiveGroup(); - if (g) { - g.destroy(); - } - return this.setActiveGroup(null); - }, - - /** - * Deactivates all objects by calling their setActive(false) - * @method deactivateAll - * @return {fabric.Canvas} thisArg - */ - deactivateAll: function () { - var allObjects = this.getObjects(), - i = 0, - len = allObjects.length; - for ( ; i < len; i++) { - allObjects[i].setActive(false); - } - this.discardActiveGroup(); - this.discardActiveObject(); - return this; - }, - - /** - * Deactivates all objects and dispatches appropriate events - * @method deactivateAllWithDispatch - * @return {fabric.Canvas} thisArg - */ - deactivateAllWithDispatch: function () { - var activeObject = this.getActiveGroup() || this.getActiveObject(); - if (activeObject) { - this.fire('before:selection:cleared', { target: activeObject }); - } - this.deactivateAll(); - if (activeObject) { - this.fire('selection:cleared'); - } - return this; - } - }; - - fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; - extend(fabric.Canvas.prototype, InteractiveMethods); - - // iterating manually to workaround Opera's bug - // where "prototype" property is enumerable and overrides existing prototype - for (var prop in fabric.StaticCanvas) { - if (prop !== 'prototype') { - fabric.Canvas[prop] = fabric.StaticCanvas[prop]; - } - } - - if (fabric.isTouchSupported) { - fabric.Canvas.prototype._setCursorFromEvent = function() { }; - } - - /** - * @class fabric.Element - * @alias fabric.Canvas - * @deprecated - * @constructor - */ - fabric.Element = fabric.Canvas; -})(); -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - FX_DURATION: 500, - - /** - * Centers object horizontally with animation. - * @method fxCenterObjectH - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @return {fabric.Canvas} 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 with animation. - * @method fxCenterObjectV - * @param {fabric.Object} object Object to center - * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties - * @return {fabric.Canvas} 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; - }, - - /** - * Same as `fabric.Canvas#remove` but animated - * @method fxRemove - * @param {fabric.Object} object Object to remove - * @param {Function} callback Callback, invoked on effect completion - * @return {fabric.Canvas} thisArg - * @chainable - */ - fxRemove: function (object, callbacks) { - callbacks = callbacks || { }; - - var empty = function() { }, - onComplete = callbacks.onComplete || empty, - onChange = callbacks.onChange || empty, - _this = this; - - fabric.util.animate({ - startValue: object.get('opacity'), - endValue: 0, - duration: this.FX_DURATION, - onStart: function() { - object.setActive(false); - }, - onChange: function(value) { - object.set('opacity', value); - _this.renderAll(); - onChange(); - }, - onComplete: function () { - _this.remove(object); - onComplete(); - } - }); - - return this; - } -}); -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - /** - * Populates canvas with data from the specified dataless JSON - * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` - * @method loadFromDatalessJSON - * @param {String} json JSON string - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: fabric.Image) - * are initialized - * @return {fabric.Canvas} instance - * @chainable - */ - loadFromDatalessJSON: function (json, callback) { - - if (!json) { - return; - } - - // serialize if it wasn't already - var serialized = (typeof json === 'string') - ? JSON.parse(json) - : json; - - if (!serialized || (serialized && !serialized.objects)) return; - - this.clear(); - - // TODO: test this - this.backgroundColor = serialized.background; - this._enlivenDatalessObjects(serialized.objects, callback); - }, - - /** - * @method _enlivenDatalessObjects - * @param {Array} objects - * @param {Function} callback - */ - _enlivenDatalessObjects: function (objects, callback) { - - /** @ignore */ - function onObjectLoaded(object, index) { - _this.insertAt(object, index, true); - object.setCoords(); - if (++numLoadedObjects === numTotalObjects) { - callback && callback(); - } - } - - /** @ignore */ - function loadObject(obj, index) { - - var pathProp = obj.paths ? 'paths' : 'path'; - var path = obj[pathProp]; - - delete obj[pathProp]; - - if (typeof path !== 'string') { - if (obj.type === 'image') { - fabric[fabric.util.string.capitalize(obj.type)].fromObject(obj, function (o) { - onObjectLoaded(o, index); - }); - } - else { - var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(obj.type))]; - if (!klass || !klass.fromObject) return; - - // restore path - if (path) { - obj[pathProp] = path; - } - onObjectLoaded(klass.fromObject(obj), index); - } - } - else { - if (obj.type === 'image') { - fabric.util.loadImage(path, function (image) { - var oImg = new fabric.Image(image); - - oImg.setSourcePath(path); - - fabric.util.object.extend(oImg, obj); - oImg.setAngle(obj.angle); - - onObjectLoaded(oImg, index); - }); - } - else if (obj.type === 'text') { - - if (obj.useNative) { - onObjectLoaded(fabric.Text.fromObject(obj), index); - } - else { - obj.path = path; - var object = fabric.Text.fromObject(obj); - var onscriptload = function () { - // TODO (kangax): find out why Opera refuses to work without this timeout - if (Object.prototype.toString.call(fabric.window.opera) === '[object Opera]') { - setTimeout(function () { - onObjectLoaded(object, index); - }, 500); - } - else { - onObjectLoaded(object, index); - } - }; - - fabric.util.getScript(path, onscriptload); - } - } - else { - fabric.loadSVGFromURL(path, function (elements) { - var object = fabric.util.groupSVGElements(elements, obj, path); - - // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) - // skip this step if an object is a PathGroup, since we already passed it options object before - if (!(object instanceof fabric.PathGroup)) { - fabric.util.object.extend(object, obj); - if (typeof obj.angle !== 'undefined') { - object.setAngle(obj.angle); - } - } - - onObjectLoaded(object, index); - }); - } - } - } - - var _this = this, - numLoadedObjects = 0, - numTotalObjects = objects.length; - - if (numTotalObjects === 0 && callback) { - callback(); - } - - try { - objects.forEach(loadObject, this); - } - catch(e) { - fabric.log(e.message); - } - }, - - /** - * Populates canvas with data from the specified JSON - * JSON format must conform to the one of `fabric.Canvas#toJSON` - * @method loadFromJSON - * @param {String} json JSON string - * @param {Function} callback Callback, invoked when json is parsed - * and corresponding objects (e.g: fabric.Image) - * are initialized - * @return {fabric.Canvas} 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 (serialized.backgroundImage) { - _this.setBackgroundImage(serialized.backgroundImage, function() { - - _this.backgroundImageOpacity = serialized.backgroundImageOpacity; - _this.backgroundImageStretch = serialized.backgroundImageStretch; - - _this.renderAll(); - - callback && callback(); - }); - } - else { - callback && callback(); - } - }); - - return this; - }, - - /** - * @method _enlivenObjects - * @param {Array} objects - * @param {Function} callback - */ - _enlivenObjects: function (objects, callback) { - var _this = this; - fabric.util.enlivenObjects(objects, function(enlivenedObjects) { - enlivenedObjects.forEach(function(obj, index) { - _this.insertAt(obj, index, true); - }); - callback && callback(); - }); - }, - - /** - * @private - * @method _toDataURL - * @param {String} format - * @param {Function} callback - */ - _toDataURL: function (format, callback) { - this.clone(function (clone) { - callback(clone.toDataURL(format)); - }); - }, - - /** - * @private - * @method _toDataURLWithMultiplier - * @param {String} format - * @param {Number} multiplier - * @param {Function} callback - */ - _toDataURLWithMultiplier: function (format, multiplier, callback) { - this.clone(function (clone) { - callback(clone.toDataURLWithMultiplier(format, multiplier)); - }); - }, - - /** - * Clones canvas instance - * @method clone - * @param {Object} [callback] Receives cloned instance as a first argument - */ - clone: function (callback) { - var data = JSON.stringify(this); - this.cloneWithoutData(function(clone) { - clone.loadFromJSON(data, function() { - callback && callback(clone); - }); - }); - }, - - /** - * Clones canvas instance without cloning existing data. - * This essentially copies canvas dimensions, clipping properties, etc. - * but leaves data empty (so that you can populate it with your own) - * @method cloneWithoutData - * @param {Object} [callback] Receives cloned instance as a first argument - */ - cloneWithoutData: function(callback) { - var el = fabric.document.createElement('canvas'); - - el.width = this.getWidth(); - el.height = this.getHeight(); - - var clone = new fabric.Canvas(el); - clone.clipTo = this.clipTo; - if (this.backgroundImage) { - clone.setBackgroundImage(this.backgroundImage.src, function() { - clone.renderAll(); - callback && callback(clone); - }); - clone.backgroundImageOpacity = this.backgroundImageOpacity; - clone.backgroundImageStretch = this.backgroundImageStretch; - } - else { - callback && callback(clone); - } - } -}); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - toFixed = fabric.util.toFixed, - capitalize = fabric.util.string.capitalize, - getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians, - slice = Array.prototype.slice; - - if (fabric.Object) { - return; - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { - - /** - * Type of an object (rect, circle, path, etc) - * @property - * @type String - */ - type: 'object', - - /** - * @property - * @type Number - */ - top: 0, - - /** - * @property - * @type Number - */ - left: 0, - - /** - * @property - * @type Number - */ - width: 0, - - /** - * @property - * @type Number - */ - height: 0, - - /** - * @property - * @type Number - */ - scaleX: 1, - - /** - * @property - * @type Number - */ - scaleY: 1, - - /** - * @property - * @type Boolean - */ - flipX: false, - - /** - * @property - * @type Boolean - */ - flipY: false, - - /** - * @property - * @type Number - */ - opacity: 1, - - /** - * @property - * @type Number - */ - angle: 0, - - /** - * @property - * @type Number - */ - cornersize: 12, - - /** - * @property - * @type Boolean - */ - transparentCorners: true, - - /** - * @property - * @type Number - */ - padding: 0, - - /** - * @property - * @type String - */ - borderColor: 'rgba(102,153,255,0.75)', - - /** - * @property - * @type String - */ - cornerColor: 'rgba(102,153,255,0.5)', - - /** - * @property - * @type String - */ - fill: 'rgb(0,0,0)', - - /** - * @property - * @type String - */ - fillRule: 'source-over', - - /** - * @property - * @type String - */ - overlayFill: null, - - /** - * @property - * @type String - */ - stroke: null, - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type Array - */ - strokeDashArray: null, - - /** - * @property - * @type Number - */ - borderOpacityWhenMoving: 0.4, - - /** - * @property - * @type Number - */ - borderScaleFactor: 1, - - /** - * Transform matrix - * @property - * @type Array - */ - transformMatrix: null, - - /** - * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) - * @property - * @type Boolean - */ - selectable: true, - - /** - * When set to `false`, object's controls are not displayed and can not be used to manipulate object - * @property - * @type Boolean - */ - hasControls: true, - - /** - * When set to `false`, object's borders are not rendered - * @property - * @type Boolean - */ - hasBorders: true, - - /** - * When set to `false`, object's rotating point will not be visible or selectable - * @property - * @type Boolean - */ - hasRotatingPoint: false, - - /** - * Offset for object's rotating point (when enabled) - * @property - * @type Number - */ - rotatingPointOffset: 40, - - /** - * @private - * @property - * @type Number - */ - _theta: 0, - - perPixelTargetFind: false, - - includeDefaultValues: true, - - /** - * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); - * as well as for history (undo/redo) purposes - * @property - * @type Array - */ - stateProperties: ( - 'top left width height scaleX scaleY flipX flipY ' + - 'theta angle opacity cornersize fill overlayFill ' + - 'stroke strokeWidth strokeDashArray fillRule ' + - 'borderScaleFactor transformMatrix selectable' - ).split(' '), - - /** - * @method callSuper - * @param {String} methodName - */ - callSuper: function(methodName) { - var fn = this.constructor.superclass.prototype[methodName]; - return (arguments.length > 1) - ? fn.apply(this, slice.call(arguments, 1)) - : fn.call(this); - }, - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - */ - initialize: function(options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * @method initGradient - */ - _initGradient: function(options) { - if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { - this.set('fill', new fabric.Gradient(options.fill)); - } - }, - - /** - * @method setOptions - * @param {Object} [options] - */ - setOptions: function(options) { - var i = this.stateProperties.length, prop; - while (i--) { - prop = this.stateProperties[i]; - if (prop in options) { - this.set(prop, options[prop]); - } - } - this._initGradient(options); - }, - - /** - * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var object = { - type: this.type, - left: toFixed(this.left, NUM_FRACTION_DIGITS), - top: toFixed(this.top, NUM_FRACTION_DIGITS), - width: toFixed(this.width, NUM_FRACTION_DIGITS), - height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, - overlayFill: this.overlayFill, - stroke: this.stroke, - strokeWidth: this.strokeWidth, - strokeDashArray: this.strokeDashArray, - scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), - scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), - flipX: this.flipX, - flipY: this.flipY, - opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), - selectable: this.selectable, - hasControls: this.hasControls, - hasBorders: this.hasBorders, - hasRotatingPoint: this.hasRotatingPoint, - transparentCorners: this.transparentCorners, - perPixelTargetFind: this.perPixelTargetFind - }; - - if (!this.includeDefaultValues) { - object = this._removeDefaultValues(object); - } - - return object; - }, - - /** - * Returns (dataless) object representation of an instance - * @method toDatalessObject - */ - toDatalessObject: function() { - // will be overwritten by subclasses - return this.toObject(); - }, - - /** - * Returns styles-string for svg-export - * @method getSvgStyles - * @return {string} - */ - getSvgStyles: function() { - return [ - "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", - "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", - "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), - "fill: ", (this.fill ? this.fill : 'none'), "; ", - "opacity: ", (this.opacity ? this.opacity : '1'), ";" - ].join(""); - }, - - /** - * Returns transform-string for svg-export - * @method getSvgTransform - * @return {string} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - return [ - "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", - angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', - (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") - ].join(''); - }, - - /** - * @private - * @method _removeDefaultValues - */ - _removeDefaultValues: function(object) { - var defaultOptions = fabric.Object.prototype.options; - if (defaultOptions) { - 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 "#"; - }, - - /** - * Sets property to a given value - * @method set - * @param {String} name - * @param {Object|Function} value - * @return {fabric.Group} thisArg - * @chainable - */ - set: function(key, value) { - if (typeof key === 'object') { - for (var prop in key) { - this._set(prop, key[prop]); - } - } - else { - if (typeof value === 'function') { - this._set(key, value(this.get(key))); - } - else { - this._set(key, value); - } - } - return this; - }, - - _set: function(key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && - value < fabric.Object.MIN_SCALE_LIMIT; - - if (shouldConstrainValue) { - value = fabric.Object.MIN_SCALE_LIMIT; - } - if (key === 'angle') { - this.setAngle(value); - } - else { - this[key] = value; - } - }, - - /** - * 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) { - - // do not render if width or height are zeros - if (this.width === 0 || this.height === 0) return; - - ctx.save(); - - var m = this.transformMatrix; - if (m && !this.group) { - ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - if (!noTransform) { - this.transform(ctx); - } - - if (this.stroke || this.strokeDashArray) { - ctx.lineWidth = this.strokeWidth; - ctx.strokeStyle = this.stroke; - } - - if (this.overlayFill) { - ctx.fillStyle = this.overlayFill; - } - else if (this.fill) { - ctx.fillStyle = this.fill.toLiveGradient - ? this.fill.toLiveGradient(ctx) - : this.fill; - } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._render(ctx, noTransform); - - if (this.active && !noTransform) { - this.drawBorders(ctx); - 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; - this.setCoords(); - return this; - }, - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @method scaleToWidth - * @param value {Number} new width value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @method scaleToHeight - * @param value {Number} new height value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, - - /** - * 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() { - - var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, - padding = this.padding; - - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; - this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; - - //If width is negative, make postive. Fixes path selection issue - if(this.currentWidth < 0){ - this.currentWidth = Math.abs(this.currentWidth); - } - - this._hypotenuse = Math.sqrt( - Math.pow(this.currentWidth / 2, 2) + - Math.pow(this.currentHeight / 2, 2)); - - this._angle = Math.atan(this.currentHeight / this.currentWidth); - - // offset added for rotate and scale actions - 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) - }; - var mtr = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }; - - // debugging - - // setTimeout(function() { - // canvas.contextTop.fillStyle = 'green'; - // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); - // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); - // canvas.contextTop.fillRect(br.x, br.y, 3, 3); - // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); - // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); - // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); - // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); - // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); - // }, 50); - - // clockwise - this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; - - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this._setCornerCoords(); - - return this; - }, - - /** - * Returns width of an object's bounding rectangle - * @method getBoundingRectWidth - * @return {Number} width value - */ - getBoundingRectWidth: function() { - this.oCoords || this.setCoords(); - var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; - var minX = fabric.util.array.min(xCoords); - var maxX = fabric.util.array.max(xCoords); - return Math.abs(minX - maxX); - }, - - /** - * Returns height of an object's bounding rectangle - * @method getBoundingRectHeight - * @return {Number} height value - */ - getBoundingRectHeight: function() { - this.oCoords || this.setCoords(); - var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; - var minY = fabric.util.array.min(yCoords); - var maxY = fabric.util.array.max(yCoords); - return Math.abs(minY - maxY); - }, - - /** - * 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) { - if (!this.hasBorders) return; - - var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, - padding = this.padding, - padding2 = padding * 2, - strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; - - ctx.save(); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = this.borderColor; - - var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), - scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX), - ~~(h + padding2 + strokeWidth * this.scaleY) - ); - - if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { - - var rotateHeight = ( - this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) - ) / 2; - - ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); - ctx.closePath(); - ctx.stroke(); - } - - ctx.restore(); - return this; - }, - - _renderDashedStroke: function(ctx) { - - if (1 & this.strokeDashArray.length /* if odd number of items */) { - /* duplicate items */ - this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); - } - - var i = 0, - x = -this.width/2, y = -this.height/2, - _this = this, - padding = this.padding, - dashedArrayLength = this.strokeDashArray.length; - - ctx.save(); - ctx.beginPath(); - - function renderSide(xMultiplier, yMultiplier) { - - var lineLength = 0, - lengthDiff = 0, - sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; - - while (lineLength < sideLength) { - - var lengthOfSubPath = _this.strokeDashArray[i++]; - lineLength += lengthOfSubPath; - - if (lineLength > sideLength) { - lengthDiff = lineLength - sideLength; - } - - // track coords - if (xMultiplier) { - x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); - } - else { - y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); - } - - ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); - if (i >= dashedArrayLength) { - i = 0; - } - } - } - - renderSide(1, 0); - renderSide(0, 1); - renderSide(-1, 0); - renderSide(0, -1); - - ctx.stroke(); - ctx.closePath(); - ctx.restore(); - }, - - /** - * 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) { - if (!this.hasControls) return; - - var size = this.cornersize, - size2 = size / 2, - strokeWidth2 = this.strokeWidth / 2, - left = -(this.width / 2), - top = -(this.height / 2), - _left, - _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; - - ctx.save(); - - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = ctx.fillStyle = this.cornerColor; - - // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - if (!this.lockUniScaling) { - // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - } - - // middle-top-rotate - if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffsetX; - - _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_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(); - - /** @ignore */ - 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') - }; - - // normalize angle - this.set('angle', 0).set('flipX', false).set('flipY', false); - this.toDataURL(function(dataURL) { - i.src = dataURL; - }); - } - return this; - }, - - /** - * Converts an object into a data-url-like string - * @method toDataURL - * @return {String} string of data - */ - toDataURL: function(callback) { - var el = fabric.document.createElement('canvas'); - if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(el); - } - - el.width = this.getBoundingRectWidth(); - el.height = this.getBoundingRectHeight(); - - fabric.util.wrapElement(el, 'div'); - - var canvas = new fabric.Canvas(el); - canvas.backgroundColor = 'transparent'; - canvas.renderAll(); - - if (this.constructor.async) { - this.clone(proceed); - } - else { - proceed(this.clone()); - } - - function proceed(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; - - callback && callback(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; - }, - - /** - * @method setupState - */ - setupState: function() { - this.originalState = { }; - this.saveState(); - }, - - /** - * 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) { - // extracts coords - 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 of another object - * @method isContainedWithinObject - * @param {Object} other Object to test - * @return {Boolean} - */ - isContainedWithinObject: function(other) { - return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); - }, - - /** - * 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); - - 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) { - if (!this.hasControls || !this.active) return false; - - var pointer = getPointer(e), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, - xpoints, - lines; - - for (var i in this.oCoords) { - - if (i === 'mtr' && !this.hasRotatingPoint) { - continue; - } - - if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { - continue; - } - - lines = this._getImageLines(this.oCoords[i].corner, i); - - // debugging - - // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - - 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]; - // optimisation 1: line below dot. no cross - if ((iLine.o.y < ey) && (iLine.d.y < ey)) { - continue; - } - // optimisation 2: line above dot. no cross - if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { - continue; - } - // optimisation 3: vertical line case - if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { - xi = iLine.o.x; - yi = ey; - } - // calculate the intersection point - 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; - } - // dont count xi < ex cases - if (xi >= ex) { - xcount += 1; - } - // optimisation 4: specific for square images - 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) { - 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 = degreesToRadians(45 - this.getAngle()), - cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, - cosHalfOffset = cornerHypotenuse * Math.cos(theta), - sinHalfOffset = cornerHypotenuse * Math.sin(theta), - sinTh = Math.sin(this._theta), - cosTh = Math.cos(this._theta); - - coords.tl.corner = { - tl: { - x: coords.tl.x - sinHalfOffset, - y: coords.tl.y - cosHalfOffset - }, - tr: { - x: coords.tl.x + cosHalfOffset, - y: coords.tl.y - sinHalfOffset - }, - bl: { - x: coords.tl.x - cosHalfOffset, - y: coords.tl.y + sinHalfOffset - }, - br: { - x: coords.tl.x + sinHalfOffset, - y: coords.tl.y + cosHalfOffset - } - }; - - coords.tr.corner = { - tl: { - x: coords.tr.x - sinHalfOffset, - y: coords.tr.y - cosHalfOffset - }, - tr: { - x: coords.tr.x + cosHalfOffset, - y: coords.tr.y - sinHalfOffset - }, - br: { - x: coords.tr.x + sinHalfOffset, - y: coords.tr.y + cosHalfOffset - }, - bl: { - x: coords.tr.x - cosHalfOffset, - y: coords.tr.y + sinHalfOffset - } - }; - - coords.bl.corner = { - tl: { - x: coords.bl.x - sinHalfOffset, - y: coords.bl.y - cosHalfOffset - }, - bl: { - x: coords.bl.x - cosHalfOffset, - y: coords.bl.y + sinHalfOffset - }, - br: { - x: coords.bl.x + sinHalfOffset, - y: coords.bl.y + cosHalfOffset - }, - tr: { - x: coords.bl.x + cosHalfOffset, - y: coords.bl.y - sinHalfOffset - } - }; - - coords.br.corner = { - tr: { - x: coords.br.x + cosHalfOffset, - y: coords.br.y - sinHalfOffset - }, - bl: { - x: coords.br.x - cosHalfOffset, - y: coords.br.y + sinHalfOffset - }, - br: { - x: coords.br.x + sinHalfOffset, - y: coords.br.y + cosHalfOffset - }, - tl: { - x: coords.br.x - sinHalfOffset, - y: coords.br.y - cosHalfOffset - } - }; - - coords.ml.corner = { - tl: { - x: coords.ml.x - sinHalfOffset, - y: coords.ml.y - cosHalfOffset - }, - tr: { - x: coords.ml.x + cosHalfOffset, - y: coords.ml.y - sinHalfOffset - }, - bl: { - x: coords.ml.x - cosHalfOffset, - y: coords.ml.y + sinHalfOffset - }, - br: { - x: coords.ml.x + sinHalfOffset, - y: coords.ml.y + cosHalfOffset - } - }; - - coords.mt.corner = { - tl: { - x: coords.mt.x - sinHalfOffset, - y: coords.mt.y - cosHalfOffset - }, - tr: { - x: coords.mt.x + cosHalfOffset, - y: coords.mt.y - sinHalfOffset - }, - bl: { - x: coords.mt.x - cosHalfOffset, - y: coords.mt.y + sinHalfOffset - }, - br: { - x: coords.mt.x + sinHalfOffset, - y: coords.mt.y + cosHalfOffset - } - }; - - coords.mr.corner = { - tl: { - x: coords.mr.x - sinHalfOffset, - y: coords.mr.y - cosHalfOffset - }, - tr: { - x: coords.mr.x + cosHalfOffset, - y: coords.mr.y - sinHalfOffset - }, - bl: { - x: coords.mr.x - cosHalfOffset, - y: coords.mr.y + sinHalfOffset - }, - br: { - x: coords.mr.x + sinHalfOffset, - y: coords.mr.y + cosHalfOffset - } - }; - - coords.mb.corner = { - tl: { - x: coords.mb.x - sinHalfOffset, - y: coords.mb.y - cosHalfOffset - }, - tr: { - x: coords.mb.x + cosHalfOffset, - y: coords.mb.y - sinHalfOffset - }, - bl: { - x: coords.mb.x - cosHalfOffset, - y: coords.mb.y + sinHalfOffset - }, - br: { - x: coords.mb.x + sinHalfOffset, - y: coords.mb.y + cosHalfOffset - } - }; - - coords.mtr.corner = { - tl: { - x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) - }, - tr: { - x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - bl: { - x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - br: { - x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) - } - }; - }, - - /** - * 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; - }, - - /** - * Returns a JSON representation of an instance - * @method toJSON - * @return {String} json - */ - toJSON: function() { - // delegate, not alias - return this.toObject(); - }, - - /** - * @method setGradientFill - */ - setGradientFill: function(options) { - this.set('fill', fabric.Gradient.forObject(this, options)); - }, - - /** - * @method animate - * - * As object — multiple properties - * - * object.animate({ left: ..., top: ... }); - * object.animate({ left: ..., top: ... }, { duration: ... }); - * - * As string — one property - * - * object.animate('left', ...); - * object.animate('left', { duration: ... }); - * - */ - animate: function() { - if (arguments[0] && typeof arguments[0] === 'object') { - for (var prop in arguments[0]) { - this._animate(prop, arguments[0][prop], arguments[1]); - } - } - else { - this._animate.apply(this, arguments); - } - return this; - }, - - /** - * @private - * @method _animate - */ - _animate: function(property, to, options) { - var obj = this; - - options || (options = { }); - - if (!('from' in options)) { - options.from = this.get(property); - } - - if (/[+\-]/.test((to + '').charAt(0))) { - to = this.get(property) + parseFloat(to); - } - - fabric.util.animate({ - startValue: options.from, - endValue: to, - byValue: options.by, - easing: options.easing, - duration: options.duration, - onChange: function(value) { - obj.set(property, value); - options.onChange && options.onChange(); - }, - onComplete: function() { - obj.setCoords(); - options.onComplete && options.onComplete(); - } - }); - }, - - /** - * Centers object horizontally on canvas to which it was added last - * @method centerH - * @return {fabric.Object} thisArg - */ - centerH: function () { - this.canvas.centerObjectH(this); - return this; - }, - - /** - * Centers object vertically on canvas to which it was added last - * @method centerV - * @return {fabric.Object} thisArg - * @chainable - */ - centerV: function () { - this.canvas.centerObjectV(this); - return this; - }, - - /** - * Centers object vertically and horizontally on canvas to which is was added last - * @method center - * @return {fabric.Object} thisArg - * @chainable - */ - center: function () { - return this.centerH().centerV(); - }, - - /** - * Removes object from canvas to which it was added last - * @method remove - * @return {fabric.Object} thisArg - * @chainable - */ - remove: function() { - return this.canvas.remove(this); - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @return {fabric.Object} thisArg - * @chainable - */ - sendToBack: function() { - this.canvas.sendToBack(this); - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - this.canvas.bringToFront(this); - return this; - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function() { - this.canvas.sendBackwards(this); - return this; - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function() { - this.canvas.bringForward(this); - return this; - } - }); - - /** - * @alias rotate -> setAngle - */ - fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; - - var proto = fabric.Object.prototype; - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - - extend(fabric.Object.prototype, fabric.Observable); - - extend(fabric.Object, { - - /** - * @static - * @constant - * @type Number - */ - NUM_FRACTION_DIGITS: 2, - - /** - * @static - * @constant - * @type Number - */ - MIN_SCALE_LIMIT: 0.1 - - }); - -})(typeof exports !== 'undefined' ? exports : this); - -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 }; - - if (fabric.Line) { - fabric.warn('fabric.Line is already defined'); - return; - } - - /** - * @class Line - * @extends fabric.Object - */ - fabric.Line = fabric.util.createClass(fabric.Object, /** @scope fabric.Line.prototype */ { - - /** - * @property - * @type String - */ - type: 'line', - - /** - * Constructor - * @method initialize - * @param {Array} points Array of points - * @param {Object} [options] Options object - * @return {fabric.Line} 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._setWidthHeight(options); - }, - - /** - * @private - * @method _setWidthHeight - * @param {Object} options - */ - _setWidthHeight: function(options) { - options || (options = { }); - - this.set('width', (this.x2 - this.x1) || 1); - this.set('height', (this.y2 - this.y1) || 1); - - this.set('left', 'left' in options ? options.left : (this.x1 + this.width / 2)); - this.set('top', 'top' in options ? options.top : (this.y1 + this.height / 2)); - }, - - /** - * @private - * @method _set - * @param {String} key - * @param {Any} value - */ - _set: function(key, value) { - this[key] = value; - if (key in coordProps) { - this._setWidthHeight(); - } - return this; - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - ctx.beginPath(); - - if (this.group) { - ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); - } - - // move from center (of virtual box) to its left/top corner - ctx.moveTo(this.width === 1 ? 0 : (-this.width / 2), this.height === 1 ? 0 : (-this.height / 2)); - ctx.lineTo(this.width === 1 ? 0 : (this.width / 2), this.height === 1 ? 0 : (this.height / 2)); - - ctx.lineWidth = this.strokeWidth; - - // TODO: test this - // make sure setting "fill" changes color of a line - // (by copying fillStyle to strokeStyle, since line is stroked, not filled) - var origStrokeStyle = ctx.strokeStyle; - ctx.strokeStyle = ctx.fillStyle; - ctx.stroke(); - ctx.strokeStyle = origStrokeStyle; - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - }, - - /** - * Returns object representation of an instance - * @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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return [ - '' - ].join(''); - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Line.fromElement`) - * @static - * @see http://www.w3.org/TR/SVG/shapes.html#LineElement - */ - fabric.Line.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Line instance from an SVG element - * @static - * @method fabric.Line.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @return {fabric.Line} instance of fabric.Line - */ - fabric.Line.fromElement = function(element, options) { - var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); - var points = [ - parsedAttributes.x1 || 0, - parsedAttributes.y1 || 0, - parsedAttributes.x2 || 0, - parsedAttributes.y2 || 0 - ]; - return new fabric.Line(points, extend(parsedAttributes, options)); - }; - - /** - * Returns fabric.Line instance from an object representation - * @static - * @method fabric.Line.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Line} 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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - piBy2 = Math.PI * 2, - extend = fabric.util.object.extend; - - if (fabric.Circle) { - fabric.warn('fabric.Circle is already defined.'); - return; - } - - /** - * @class Circle - * @extends fabric.Object - */ - fabric.Circle = fabric.util.createClass(fabric.Object, /** @scope fabric.Circle.prototype */ { - - /** - * @property - * @type String - */ - type: 'circle', - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - * @return {fabric.Circle} thisArg - */ - initialize: function(options) { - options = options || { }; - - this.set('radius', options.radius || 0); - this.callSuper('initialize', options); - - var diameter = this.get('radius') * 2; - this.set('width', diameter).set('height', diameter); - }, - - /** - * 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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return (''); - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx, noTransform) { - ctx.beginPath(); - // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) - ctx.globalAlpha *= this.opacity; - 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 horizontal radius of an object (according to how an object is scaled) - * @method getRadiusX - * @return {Number} - */ - getRadiusX: function() { - return this.get('radius') * this.get('scaleX'); - }, - - /** - * Returns vertical radius of an object (according to how an object is scaled) - * @method getRadiusY - * @return {Number} - */ - getRadiusY: function() { - return this.get('radius') * this.get('scaleY'); - }, - - /** - * Sets radius of an object (and updates width accordingly) - * @method setRadius - * @return {Number} - */ - setRadius: function(value) { - this.radius = value; - this.set('width', value * 2).set('height', value * 2); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity of this instance - */ - complexity: function() { - return 1; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement - */ - fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns {@link fabric.Circle} instance from an SVG element - * @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 new 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)); - } - - /** - * Returns {@link fabric.Circle} instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Triangle) { - fabric.warn('fabric.Triangle is already defined'); - return; - } - - /** - * @class Triangle - * @extends fabric.Object - */ - fabric.Triangle = fabric.util.createClass(fabric.Object, /** @scope fabric.Triangle.prototype */ { - - /** - * @property - * @type String - */ - type: 'triangle', - - /** - * Constructor - * @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; - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - - var widthBy2 = this.width / 2, - heightBy2 = this.height / 2; - - var points = [ - -widthBy2 + " " + heightBy2, - "0 " + -heightBy2, - widthBy2 + " " + heightBy2 - ].join(","); - - return ''; - } - }); - - /** - * Returns fabric.Triangle instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global){ - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - piBy2 = Math.PI * 2, - extend = fabric.util.object.extend; - - if (fabric.Ellipse) { - fabric.warn('fabric.Ellipse is already defined.'); - return; - } - - /** - * @class Ellipse - * @extends fabric.Object - */ - fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @scope fabric.Ellipse.prototype */ { - - /** - * @property - * @type String - */ - type: 'ellipse', - - /** - * Constructor - * @method initialize - * @param {Object} [options] 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') - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return [ - '' - ].join(''); - }, - - /** - * Renders this instance on a given context - * @method render - * @param ctx {CanvasRenderingContext2D} context to render on - * @param noTransform {Boolean} context is not transformed when set to true - */ - render: function(ctx, noTransform) { - // do not use `get` for perf. reasons - 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.globalAlpha *= this.opacity; - if (this.transformMatrix && this.group) { - ctx.translate(this.cx, this.cy); - } - 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); - if (this.stroke) { - ctx.stroke(); - } - if (this.fill) { - ctx.fill(); - } - ctx.restore(); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) - * @static - * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement - */ - fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns {@link fabric.Ellipse} instance from an SVG element - * @static - * @method fabric.Ellipse.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] Options object - * @return {fabric.Ellipse} - */ - fabric.Ellipse.fromElement = function(element, options) { - options || (options = { }); - - var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); - var cx = parsedAttributes.left; - var cy = parsedAttributes.top; - - if ('left' in parsedAttributes) { - parsedAttributes.left -= (options.width / 2) || 0; - } - if ('top' in parsedAttributes) { - parsedAttributes.top -= (options.height / 2) || 0; - } - - var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); - - ellipse.cx = cx || 0; - ellipse.cy = cy || 0; - - return ellipse; - }; - - /** - * Returns fabric.Ellipse instance from an object representation - * @static - * @method fabric.Ellipse.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Ellipse} - */ - fabric.Ellipse.fromObject = function(object) { - return new fabric.Ellipse(object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }); - - if (fabric.Rect) { - console.warn('fabric.Rect is already defined'); - return; - } - - /** - * @class Rect - * @extends fabric.Object - */ - fabric.Rect = fabric.util.createClass(fabric.Object, /** @scope fabric.Rect.prototype */ { - - /** - * @property - * @type String - */ - type: 'rect', - - /** - * @property - * @type Number - */ - rx: 0, - - /** - * @property - * @type Number - */ - ry: 0, - - /** - * Constructor - * @method initialize - * @param options {Object} options object - * @return {Object} thisArg - */ - initialize: function(options) { - this._initStateProperties(); - this.callSuper('initialize', options); - this._initRxRy(); - }, - - /** - * Creates `stateProperties` list on an instance, and adds `fabric.Rect` -specific ones to it - * (such as "rx", "ry", etc.) - * @private - * @method _initStateProperties - */ - _initStateProperties: function() { - this.stateProperties = this.stateProperties.concat(['rx', 'ry']); - }, - - /** - * @private - * @method _initRxRy - */ - _initRxRy: function() { - if (this.rx && !this.ry) { - this.ry = this.rx; - } - else if (this.ry && !this.rx) { - this.rx = this.ry; - } - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx) { - var rx = this.rx || 0, - ry = this.ry || 0, - x = -this.width / 2, - y = -this.height / 2, - w = this.width, - h = this.height; - - ctx.beginPath(); - ctx.globalAlpha *= this.opacity; - - if (this.transformMatrix && this.group) { - ctx.translate( - this.width / 2 + this.x, - this.height / 2 + this.y); - } - if (!this.transformMatrix && this.group) { - ctx.translate( - -this.group.width / 2 + this.width / 2 + this.x, - -this.group.height / 2 + this.height / 2 + this.y); - } - - ctx.moveTo(x+rx, y); - ctx.lineTo(x+w-rx, y); - ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry); - ctx.lineTo(x+w, y+h-ry); - ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h); - ctx.lineTo(x+rx,y+h); - ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry); - ctx.lineTo(x,y+ry); - ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y); - ctx.closePath(); - - if (this.fill) { - ctx.fill(); - } - - if (this.strokeDashArray) { - this._renderDashedStroke(ctx); - } - else if (this.stroke) { - ctx.stroke(); - } - }, - - // since our coordinate system differs from that of SVG - _normalizeLeftTopProperties: function(parsedAttributes) { - if (parsedAttributes.left) { - this.set('left', parsedAttributes.left + this.getWidth() / 2); - } - this.set('x', parsedAttributes.left || 0); - if (parsedAttributes.top) { - this.set('top', parsedAttributes.top + this.getHeight() / 2); - } - this.set('y', parsedAttributes.top || 0); - return this; - }, - - /** - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} object representation of an instance - */ - toObject: function() { - return fabric.util.object.extend(this.callSuper('toObject'), { - rx: this.get('rx') || 0, - ry: this.get('ry') || 0 - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return ''; - } - }); - - // TODO (kangax): implement rounded rectangles (both parsing and rendering) - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) - * @static - */ - fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * @private - */ - function _setDefaultLeftTopValues(attributes) { - attributes.left = attributes.left || 0; - attributes.top = attributes.top || 0; - return attributes; - } - - /** - * Returns fabric.Rect instance from an SVG element - * @static - * @method fabric.Rect.fromElement - * @param element {SVGElement} element to parse - * @param options {Object} options object - * @return {fabric.Rect} 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 ? fabric.util.object.clone(options) : { }), parsedAttributes)); - rect._normalizeLeftTopProperties(parsedAttributes); - - return rect; - }; - - /** - * Returns fabric.Rect instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - toFixed = fabric.util.toFixed; - - if (fabric.Polyline) { - fabric.warn('fabric.Polyline is already defined'); - return; - } - - /** - * @class Polyline - * @extends fabric.Object - */ - fabric.Polyline = fabric.util.createClass(fabric.Object, /** @scope fabric.Polyline.prototype */ { - - /** - * @property - * @type String - */ - type: 'polyline', - - /** - * Constructor - * @method initialize - * @param {Array} points array of points - * @param {Object} [options] 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); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var points = []; - for (var i = 0, len = this.points.length; i < len; i++) { - points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); - } - - return [ - '' - ].join(''); - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - var point; - ctx.beginPath(); - ctx.moveTo(this.points[0].x, this.points[0].y); - 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(); - } - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return this.get('points').length; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polyline.fromElement`) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement - */ - fabric.Polyline.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Polyline instance from an SVG element - * @static - * @method fabric.Polyline.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} [options] 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, fabric.Polyline.ATTRIBUTE_NAMES); - - for (var i = 0, len = points.length; i < len; i++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - 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)); - }; - - /** - * Returns fabric.Polyline instance from an object representation - * @static - * @method fabric.Polyline.fromObject - * @param {Object} [object] Object to create an instance from - * @return {fabric.Polyline} - */ - fabric.Polyline.fromObject = function(object) { - var points = object.points; - return new fabric.Polyline(points, object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - min = fabric.util.array.min, - max = fabric.util.array.max, - toFixed = fabric.util.toFixed; - - if (fabric.Polygon) { - fabric.warn('fabric.Polygon is already defined'); - return; - } - - /** - * @class Polygon - * @extends fabric.Object - */ - fabric.Polygon = fabric.util.createClass(fabric.Object, /** @scope fabric.Polygon.prototype */ { - - /** - * @property - * @type String - */ - type: 'polygon', - - /** - * Constructor - * @method initialize - * @param {Array} points Array of points - * @param {Object} options Options object - * @return {fabric.Polygon} 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) || 1; - this.height = (maxY - minY) || 1; - - 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() - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var points = []; - for (var i = 0, len = this.points.length; i < len; i++) { - points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); - } - - return [ - '' - ].join(''); - }, - - /** - * @private - * @method _render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - _render: function(ctx) { - var point; - ctx.beginPath(); - ctx.moveTo(this.points[0].x, this.points[0].y); - 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; - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) - * @static - * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement - */ - fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); - - /** - * Returns fabric.Polygon instance from an SVG element - * @static - * @method fabric.Polygon.fromElement - * @param {SVGElement} element Element to parse - * @param {Object} options Options object - * @return {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++) { - // normalize coordinates, according to containing box (dimensions of which are passed via `options`) - points[i].x -= (options.width / 2) || 0; - points[i].y -= (options.height / 2) || 0; - } - - return new fabric.Polygon(points, extend(parsedAttributes, options)); - }; - - /** - * Returns fabric.Polygon instance from an object representation - * @static - * @method fabric.Polygon.fromObject - * @param {Object} object Object to create an instance from - * @return {fabric.Polygon} - */ - fabric.Polygon.fromObject = function(object) { - return new fabric.Polygon(object.points, object); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - var commandLengths = { - m: 2, - l: 2, - h: 1, - v: 1, - c: 6, - s: 4, - q: 4, - t: 2, - a: 7 - }; - - function drawArc(ctx, x, y, coords) { - var rx = coords[0]; - var ry = coords[1]; - var rot = coords[2]; - var large = coords[3]; - var sweep = coords[4]; - var ex = coords[5]; - var ey = coords[6]; - var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); - for (var i=0; i 1) { - pl = Math.sqrt(pl); - rx *= pl; - ry *= pl; - } - - var a00 = cos_th / rx; - var a01 = sin_th / rx; - var a10 = (-sin_th) / ry; - var a11 = (cos_th) / ry; - var x0 = a00 * ox + a01 * oy; - var y0 = a10 * ox + a11 * oy; - var x1 = a00 * x + a01 * y; - var y1 = a10 * x + a11 * y; - - var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); - var sfactor_sq = 1 / d - 0.25; - if (sfactor_sq < 0) sfactor_sq = 0; - var sfactor = Math.sqrt(sfactor_sq); - if (sweep === large) sfactor = -sfactor; - var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); - var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); - - var th0 = Math.atan2(y0-yc, x0-xc); - var th1 = Math.atan2(y1-yc, x1-xc); - - var th_arc = th1-th0; - if (th_arc < 0 && sweep === 1){ - th_arc += 2*Math.PI; - } else if (th_arc > 0 && sweep === 0) { - th_arc -= 2 * Math.PI; - } - - var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); - var result = []; - for (var i=0; i'; - }, - - /** - * Returns object representation of an instance - * @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; - }, - - /** - * Returns dataless object representation of an instance - * @method toDatalessObject - * @return {Object} - */ - toDatalessObject: function() { - var o = this.toObject(); - if (this.sourcePath) { - o.path = this.sourcePath; - } - delete o.sourcePath; - return o; - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var chunks = []; - for (var i = 0, len = this.path.length; i < len; i++) { - chunks.push(this.path[i].join(' ')); - } - var path = chunks.join(' '); - - return [ - '', - '', - '' - ].join(''); - }, - - /** - * Returns number representation of an instance complexity - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return this.path.length; - }, - - /** - * @private - * @method _parsePath - */ - _parsePath: function() { - var result = [ ], - currentPath, - chunks, - parsed; - - for (var i = 0, chunksParsed, len = this.path.length; i < len; i++) { - currentPath = this.path[i]; - chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/); - chunksParsed = [ currentPath.charAt(0) ]; - - for (var j = 0, jlen = chunks.length; j < jlen; j++) { - parsed = parseFloat(chunks[j]); - if (!isNaN(parsed)) { - chunksParsed.push(parsed); - } - } - - var command = chunksParsed[0].toLowerCase(), - commandLength = commandLengths[command]; - - if (chunksParsed.length - 1 > commandLength) { - for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) { - result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); - } - } - else { - result.push(chunksParsed); - } - } - - return result; - }, - - /** - * @method _parseDimensions - */ - _parseDimensions: function() { - var aX = [], - aY = [], - previousX, - previousY, - isLowerCase = false, - x, - y; - - 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]); - } - - // lowercased letter denotes relative position; - // transform to absolute - if (item[0] === item[0].toLowerCase()) { - isLowerCase = true; - } - - // last 2 items in an array of coordinates are the actualy x/y (except H/V); - // collect them - - // TODO (kangax): support relative h/v commands - - 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 = 0, - 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); - }; - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) - * @static - * @see http://www.w3.org/TR/SVG/paths.html#PathElement - */ - fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' '); - - /** - * Creates an instance of fabric.Path from an SVG 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, fabric.Path.ATTRIBUTE_NAMES); - return new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options)); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - invoke = fabric.util.array.invoke, - parentToObject = fabric.Object.prototype.toObject, - camelize = fabric.util.string.camelize, - capitalize = fabric.util.string.capitalize; - - if (fabric.PathGroup) { - fabric.warn('fabric.PathGroup is already defined'); - return; - } - - /** - * @class PathGroup - * @extends fabric.Path - */ - fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @scope fabric.PathGroup.prototype */ { - - /** - * @property - * @type String - */ - type: 'path-group', - - /** - * @property - * @type String - */ - fill: '', - - /** - * @property - * @type Boolean - */ - forceFillOverwrite: false, - - /** - * Constructor - * @method initialize - * @param {Array} paths - * @param {Object} [options] Options object - * @return {fabric.PathGroup} thisArg - */ - initialize: function(paths, options) { - - options = options || { }; - this.paths = paths || [ ]; - - for (var i = this.paths.length; i--; ) { - this.paths[i].group = this; - } - - this.setOptions(options); - this.setCoords(); - - if (options.sourcePath) { - this.setSourcePath(options.sourcePath); - } - }, - - /** - * Renders this group on a specified context - * @method render - * @param {CanvasRenderingContext2D} ctx Context to render this instance on - */ - render: function(ctx) { - 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(); - }, - - /** - * Sets certain property to a certain value - * @method _set - * @param {String} prop - * @param {Any} value - * @return {fabric.PathGroup} thisArg - */ - _set: function(prop, value) { - - if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) { - var i = this.paths.length; - while (i--) { - this.paths[i]._set(prop, value); - } - } - - return this.callSuper('_set', prop, value); - }, - - /** - * Returns object representation of this path group - * @method toObject - * @return {Object} object representation of an instance - */ - toObject: function() { - return extend(parentToObject.call(this), { - paths: invoke(this.getObjects(), 'toObject'), - sourcePath: this.sourcePath - }); - }, - - /** - * Returns dataless object representation of this path group - * @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 svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var objects = this.getObjects(); - var markup = [ - '' - ]; - - for (var i = 0, len = objects.length; i < len; i++) { - markup.push(objects[i].toSVG()); - } - markup.push(''); - - return markup.join(''); - }, - - /** - * Returns a string representation of this path group - * @method toString - * @return {String} string representation of an object - */ - toString: function() { - return '#'; - }, - - /** - * Returns true if all paths in this group are of same color - * @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; - }, - - /** - * Returns all paths in this path group - * @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; - } - - /** - * Creates fabric.Triangle instance from an object representation - * @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); - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global){ - - "use strict"; - - var fabric = global.fabric || (global.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; - } - - /** - * @class Group - * @extends fabric.Object - */ - fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { - - /** - * @property - * @type String - */ - type: 'group', - - /** - * Constructor - * @method initialized - * @param {Object} objects Group objects - * @param {Object} [options] Options object - * @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(); - - // group is active by default - 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(); - - // do not display corners of objects enclosed in a group - object.hideCorners = true; - }, this); - }, - - /** - * Returns string represenation of a group - * @method toString - * @return {String} - */ - toString: function() { - return '#'; - }, - - /** - * Returns an array of all objects in this group - * @method getObjects - * @return {Array} group objects - */ - getObjects: function() { - return this.objects; - }, - - /** - * Adds an object to a group; Then recalculates group's dimension, position. - * @method addWithUpdate - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - addWithUpdate: function(object) { - this._restoreObjectsState(); - this.objects.push(object); - this._calcBounds(); - this._updateObjectsCoords(); - return this; - }, - - /** - * Removes an object from a group; Then recalculates group's dimension, position. - * @method removeWithUpdate - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - removeWithUpdate: function(object) { - this._restoreObjectsState(); - removeFromArray(this.objects, object); - object.setActive(false); - this._calcBounds(); - this._updateObjectsCoords(); - return this; - }, - - /** - * Adds an object to a group - * @method add - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - add: function(object) { - this.objects.push(object); - return this; - }, - - /** - * Removes an object from a group - * @method remove - * @param {Object} object - * @return {fabric.Group} thisArg - * @chainable - */ - remove: function(object) { - removeFromArray(this.objects, object); - 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; - }, - - /** - * @private - */ - _set: function(key, value) { - if (key === 'fill' || key === 'opacity') { - var i = this.objects.length; - this[key] = value; - while (i--) { - this.objects[i].set(key, value); - } - } - else { - this[key] = value; - } - }, - - /** - * 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, 'toObject') - }); - }, - - /** - * Renders instance on a given context - * @method render - * @param {CanvasRenderingContext2D} ctx context to render instance on - */ - render: function(ctx, noTransform) { - ctx.save(); - this.transform(ctx); - - var groupScaleFactor = Math.max(this.scaleX, this.scaleY); - - for (var i = 0, len = this.objects.length; i < len; i++) { - - var object = this.objects[i]; - var originalScaleFactor = object.borderScaleFactor; - - object.borderScaleFactor = groupScaleFactor; - object.render(ctx); - object.borderScaleFactor = originalScaleFactor; - } - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - this.setCoords(); - }, - - /** - * Returns object from the group at the specified index - * @method item - * @param index {Number} index of item to get - * @return {fabric.Object} - */ - item: function(index) { - return this.getObjects()[index]; - }, - - /** - * Returns complexity of an instance - * @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 (original state is that which was before group was created). - * @private - * @method _restoreObjectsState - * @return {fabric.Group} thisArg - * @chainable - */ - _restoreObjectsState: function() { - this.objects.forEach(this._restoreObjectState, this); - return this; - }, - - /** - * Restores original state of a specified object in group - * @private - * @method _restoreObjectState - * @param {fabric.Object} object - * @return {fabric.Group} thisArg - */ - _restoreObjectState: function(object) { - - var groupLeft = this.get('left'), - groupTop = this.get('top'), - groupAngle = this.getAngle() * (Math.PI / 180), - 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; - }, - - /** - * Destroys a group (restoring state of its objects) - * @method destroy - * @return {fabric.Group} thisArg - * @chainable - */ - destroy: function() { - return this._restoreObjectsState(); - }, - - /** - * Saves coordinates of this instance (to be used together with `hasMoved`) - * @saveCoords - * @return {fabric.Group} thisArg - * @chainable - */ - saveCoords: function() { - this._originalLeft = this.get('left'); - this._originalTop = this.get('top'); - return this; - }, - - /** - * Checks whether this group was moved (since `saveCoords` was called last) - * @method hasMoved - * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) - */ - 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() { - this.forEachObject(function(object) { - object.setActive(); - }); - return this; - }, - - /** - * Executes given function for each object in this group - * @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 Context (aka thisObject) - * - * @return {fabric.Group} thisArg - * @chainable - */ - forEachObject: fabric.StaticCanvas.prototype.forEachObject, - - /** - * @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) || 0; - height = (maxY - minY) || 0; - - this.width = width; - this.height = height; - - this.left = (minX + width / 2) || 0; - this.top = (minY + height / 2) || 0; - }, - - /** - * Checks if point is contained within the group - * @method containsPoint - * @param {fabric.Point} 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; - }, - - /** - * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) - * @method toGrayscale - */ - toGrayscale: function() { - var i = this.objects.length; - while (i--) { - this.objects[i].toGrayscale(); - } - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - var objectsMarkup = [ ]; - for (var i = 0, len = this.objects.length; i < len; i++) { - objectsMarkup.push(this.objects[i].toSVG()); - } - - return ( - '' + - objectsMarkup.join('') + - ''); - } - }); - - /** - * Returns fabric.Group instance from an object representation - * @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, callback) { - fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { - delete object.objects; - callback && callback(new fabric.Group(enlivenedObjects, object)); - }); - }; - - fabric.Group.async = true; - -})(typeof exports !== 'undefined' ? exports : this); -(function(global) { - - "use strict"; - - var extend = fabric.util.object.extend; - - if (!global.fabric) { - global.fabric = { }; - } - - if (global.fabric.Image) { - fabric.warn('fabric.Image is already defined.'); - return; - } - - if (!fabric.Object) { - fabric.warn('fabric.Object is required for fabric.Image initialization'); - return; - } - - /** - * @class Image - * @extends fabric.Object - */ - fabric.Image = fabric.util.createClass(fabric.Object, /** @scope fabric.Image.prototype */ { - - /** - * @property - * @type Boolean - */ - active: false, - - /** - * @property - * @type Boolean - */ - bordervisibility: false, - - /** - * @property - * @type Boolean - */ - cornervisibility: false, - - /** - * @property - * @type String - */ - type: 'image', - - /** - * Constructor - * @param {HTMLImageElement | String} element Image element - * @param {Object} options optional - */ - initialize: function(element, options) { - options || (options = { }); - - this.callSuper('initialize', options); - this._initElement(element); - this._originalImage = this.getElement(); - this._initConfig(options); - - this.filters = [ ]; - - if (options.filters) { - this.filters = options.filters; - this.applyFilters(); - } - }, - - /** - * Returns image element which this instance if based on - * @method getElement - * @return {HTMLImageElement} image element - */ - getElement: function() { - return this._element; - }, - - /** - * Sets image element for this instance to a specified one - * @method setElement - * @param {HTMLImageElement} element - * @return {fabric.Image} thisArg - * @chainable - */ - setElement: function(element) { - this._element = element; - this._initConfig(); - return this; - }, - - /** - * Returns original size of an image - * @method getOriginalSize - * @return {Object} object with "width" and "height" properties - */ - getOriginalSize: function() { - var element = this.getElement(); - return { - width: element.width, - height: element.height - }; - }, - - /** - * Sets border visibility - * @method setBorderVisibility - * @param {Boolean} visible When true, border is set to be visible - */ - setBorderVisibility: function() { - this._resetWidthHeight(); - this._adjustWidthHeightToBorders(); - this.setCoords(); - }, - - /** - * Sets corner visibility - * @method setCornersVisibility - * @param {Boolean} visible When true, corners are set to be visible - */ - setCornersVisibility: function(visible) { - this.cornervisibility = !!visible; - }, - - /** - * Renders image on a specified context - * @method render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - render: function(ctx, noTransform) { - ctx.save(); - var m = this.transformMatrix; - this._resetWidthHeight(); - if (this.group) { - ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); - } - if (m) { - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - if (!noTransform) { - this.transform(ctx); - } - this._render(ctx); - if (this.active && !noTransform) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} Object representation of an instance - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - src: this._originalImage.src || this._originalImage._src, - filters: this.filters.concat() - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - return ''+ - ' element with actual transformation, then offsetting object to the top/left - // so that object's center aligns with container's left/top - 'transform="translate('+ (-this.width/2) + ' ' + (-this.height/2) + ')" ' + - 'width="' + this.width + '" ' + - 'height="' + this.height + '"' + '/>'+ - ''; - }, - - /** - * Returns source of an image - * @method getSrc - * @return {String} Source of an image - */ - getSrc: function() { - return this.getElement().src || this.getElement()._src; - }, - - /** - * Returns string representation of an instance - * @method toString - * @return {String} String representation of an instance - */ - toString: function() { - return '#'; - }, - - /** - * Returns a clone of an instance - * @mthod clone - * @param {Function} callback Callback is invoked with a clone as a first argument - */ - clone: function(callback) { - this.constructor.fromObject(this.toObject(), callback); - }, - - /** - * Applies filters assigned to this image (from "filters" array) - * @mthod applyFilters - * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated - */ - applyFilters: function(callback) { - - if (this.filters.length === 0) { - this.setElement(this._originalImage); - callback && callback(); - return; - } - - var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined', - imgEl = this._originalImage, - canvasEl = fabric.document.createElement('canvas'), - replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'), - _this = this; - - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - - canvasEl.width = imgEl.width; - canvasEl.height = imgEl.height; - - canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); - - this.filters.forEach(function(filter) { - filter && filter.applyTo(canvasEl); - }); - - /** @ignore */ - replacement.onload = function() { - _this._element = replacement; - callback && callback(); - replacement.onload = canvasEl = imgEl = null; - }; - replacement.width = imgEl.width; - replacement.height = imgEl.height; - - if (isLikelyNode) { - var base64str = canvasEl.toDataURL('image/png').replace(/data:image\/png;base64,/, ''); - replacement.src = new Buffer(base64str, 'base64'); - _this._element = replacement; - - // onload doesn't fire in node, so we invoke callback manually - callback && callback(); - } - else { - replacement.src = canvasEl.toDataURL('image/png'); - } - - return this; - }, - - /** - * @private - */ - _render: function(ctx) { - ctx.drawImage( - this.getElement(), - - this.width / 2, - -this.height / 2, - this.width, - this.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) { - options || (options = { }); - this.setOptions(options); - this._setBorder(); - this._setWidthHeight(options); - }, - - /** - * @method _initFilters - * @param {Object} object Object with filters property - */ - _initFilters: function(object) { - if (object.filters && object.filters.length) { - this.filters = object.filters.map(function(filterObj) { - return filterObj && fabric.Image.filters[filterObj.type].fromObject(filterObj); - }); - } - }, - - /** - * @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 = 'width' in options - ? options.width - : ((this.getElement().width || 0) + sidesBorderWidth); - - this.height = 'height' in options - ? options.height - : ((this.getElement().height || 0) + sidesBorderWidth); - }, - - /** - * Returns complexity of an instance - * @method complexity - * @return {Number} complexity - */ - complexity: function() { - return 1; - } - }); - - /** - * Default CSS class name for canvas - * @static - * @type String - */ - fabric.Image.CSS_CANVAS = "canvas-img"; - - fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; - - /** - * Creates an instance of fabric.Image from its object representation - * @static - * @method fromObject - * @param object {Object} - * @param callback {Function} optional - */ - fabric.Image.fromObject = function(object, callback) { - var img = fabric.document.createElement('img'), - src = object.src; - - if (object.width) { - img.width = object.width; - } - if (object.height) { - img.height = object.height; - } - - /** @ignore */ - img.onload = function() { - fabric.Image.prototype._initFilters.call(object, object); - - var instance = new fabric.Image(img, object); - callback && callback(instance); - img = img.onload = null; - }; - img.src = src; - }; - - /** - * Creates an instance of fabric.Image from an URL string - * @static - * @method fromURL - * @param {String} url URL to create an image from - * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) - * @param {Object} [imgOptions] Options object - */ - fabric.Image.fromURL = function(url, callback, imgOptions) { - var img = fabric.document.createElement('img'); - - /** @ignore */ - img.onload = function() { - if (callback) { - callback(new fabric.Image(img, imgOptions)); - } - img = img.onload = null; - }; - img.src = url; - }; - - /** - * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) - * @static - * @see http://www.w3.org/TR/SVG/struct.html#ImageElement - */ - fabric.Image.ATTRIBUTE_NAMES = 'x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href'.split(' '); - - /** - * Returns {@link fabric.Image} instance from an SVG element - * @static - * @method fabric.Image.fromElement - * @param {SVGElement} element Element to parse - * @param {Function} callback Callback to execute when fabric.Image object is created - * @param {Object} [options] Options object - * @return {fabric.Image} - */ - fabric.Image.fromElement = function(element, callback, options) { - options || (options = { }); - - var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); - - fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend(parsedAttributes, options)); - }; - - fabric.Image.async = true; - -})(typeof exports !== 'undefined' ? exports : this); - -fabric.util.object.extend(fabric.Object.prototype, { - - /** - * @method _getAngleValueForStraighten - * @return {Number} angle value - * @private - */ - _getAngleValueForStraighten: function() { - var angle = this.get('angle'); - - // TODO (kangax): can this be simplified? - - 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; - }, - - /** - * @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; - } -}); - -fabric.util.object.extend(fabric.StaticCanvas.prototype, { - - /** - * Straightens object, then rerenders canvas - * @method straightenObject - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - straightenObject: function (object) { - object.straighten(); - this.renderAll(); - return this; - }, - - /** - * Same as `fabric.Canvas#straightenObject`, but animated - * @method fxStraightenObject - * @param {fabric.Object} object Object to straighten - * @return {fabric.Canvas} thisArg - * @chainable - */ - fxStraightenObject: function (object) { - object.fxStraighten({ - onChange: this.renderAll.bind(this) - }); - return this; - } -}); -/** - * @namespace - */ -fabric.Image.filters = { }; - -/** - * @class fabric.Image.filters.Grayscale - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Grayscale = fabric.util.createClass( /** @scope fabric.Image.filters.Grayscale.prototype */ { - - /** - * @param {String} type - */ - type: "Grayscale", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Grayscale.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = imageData.width, - jLen = imageData.height, - index, average, i, j; - - for (i = 0; i < iLen; i++) { - for (j = 0; j < jLen; j++) { - - index = (i * 4) * jLen + (j * 4); - average = (data[index] + data[index + 1] + data[index + 2]) / 3; - - data[index] = average; - data[index + 1] = average; - data[index + 2] = average; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Grayscale.fromObject = function() { - return new fabric.Image.filters.Grayscale(); -}; - -/** - * @class fabric.Image.filters.RemoveWhite - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.RemoveWhite = fabric.util.createClass( /** @scope fabric.Image.filters.RemoveWhite.prototype */ { - - /** - * @param {String} type - */ - type: "RemoveWhite", - - /** - * @memberOf fabric.Image.filters.RemoveWhite.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.threshold = options.threshold || 30; - this.distance = options.distance || 20; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - distance = this.distance, - limit = 255 - threshold, - abs = Math.abs, - r, g, b; - - for (var i = 0, len = data.length; i < len; i += 4) { - - r = data[i]; - g = data[i+1]; - b = data[i+2]; - - if (r > limit && - g > limit && - b > limit && - abs(r-g) < distance && - abs(r-b) < distance && - abs(g-b) < distance) { - - data[i+3] = 1; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - threshold: this.threshold, - distance: this.distance - }; - } -}); - -fabric.Image.filters.RemoveWhite.fromObject = function(object) { - return new fabric.Image.filters.RemoveWhite(object); -}; - -/** - * @class fabric.Image.filters.Invert - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Invert = fabric.util.createClass( /** @scope fabric.Image.filters.Invert.prototype */ { - - /** - * @param {String} type - */ - type: "Invert", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Invert.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i; - - for (i = 0; i < iLen; i+=4) { - data[i] = 255 - data[i]; - data[i + 1] = 255 - data[i + 1]; - data[i + 2] = 255 - data[i + 2]; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Invert.fromObject = function() { - return new fabric.Image.filters.Invert(); -}; - -/** - * @class fabric.Image.filters.Sepia - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Sepia = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia.prototype */ { - - /** - * @param {String} type - */ - type: "Sepia", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, avg; - - for (i = 0; i < iLen; i+=4) { - avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; - data[i] = avg + 100; - data[i + 1] = avg + 50; - data[i + 2] = avg + 255; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Sepia.fromObject = function() { - return new fabric.Image.filters.Sepia(); -}; - -/** - * @class fabric.Image.filters.Sepia2 - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Sepia2 = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia2.prototype */ { - - /** - * @param {String} type - */ - type: "Sepia2", - - /** - * @method applyTo - * @memberOf fabric.Image.filters.Sepia.prototype - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, r, g, b; - - for (i = 0; i < iLen; i+=4) { - - r = data[i]; - g = data[i + 1]; - b = data[i + 2]; - - data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; - data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; - data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { type: this.type }; - } -}); - -fabric.Image.filters.Sepia2.fromObject = function() { - return new fabric.Image.filters.Sepia2(); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Brightness = fabric.util.createClass( /** @scope fabric.Image.filters.Brightness.prototype */ { - - /** - * @param {String} type - */ - type: "Brightness", - - /** - * @memberOf fabric.Image.filters.Brightness.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.brightness = options.brightness || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - brightness = this.brightness; - - for (var i = 0, len = data.length; i < len; i += 4) { - data[i] += brightness; - data[i + 1] += brightness; - data[i + 2] += brightness; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - brightness: this.brightness - }; - } -}); - -fabric.Image.filters.Brightness.fromObject = function(object) { - return new fabric.Image.filters.Brightness(object); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Noise = fabric.util.createClass( /** @scope fabric.Image.filters.Noise.prototype */ { - - /** - * @param {String} type - */ - type: "Noise", - - /** - * @memberOf fabric.Image.filters.Brightness.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.noise = options.noise || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - noise = this.noise, rand; - - for (var i = 0, len = data.length; i < len; i += 4) { - - rand = (0.5 - Math.random()) * noise; - - data[i] += rand; - data[i + 1] += rand; - data[i + 2] += rand; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - noise: this.noise - }; - } -}); - -fabric.Image.filters.Noise.fromObject = function(object) { - return new fabric.Image.filters.Noise(object); -}; - -/** - * @class fabric.Image.filters.Brightness - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.GradientTransparency = fabric.util.createClass( /** @scope fabric.Image.filters.GradientTransparency.prototype */ { - - /** - * @param {String} type - */ - type: "GradientTransparency", - - /** - * @memberOf fabric.Image.filters.GradientTransparency.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.threshold = options.threshold || 100; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - threshold = this.threshold, - total = data.length; - - for (var i = 0, len = data.length; i < len; i += 4) { - data[i + 3] = threshold + 255 * (total - i) / total; - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - threshold: this.threshold - }; - } -}); - -fabric.Image.filters.GradientTransparency.fromObject = function(object) { - return new fabric.Image.filters.GradientTransparency(object); -}; - -/** - * @class fabric.Image.filters.Tint - * @memberOf fabric.Image.filters - */ -fabric.Image.filters.Tint = fabric.util.createClass( /** @scope fabric.Image.filters.Tint.prototype */ { - - /** - * @param {String} type - */ - type: "Tint", - - /** - * @memberOf fabric.Image.filters.RemoveWhite.prototype - * @param {Object} [options] Options object - */ - initialize: function(options) { - options || (options = { }); - this.color = options.color || 0; - }, - - /** - * @method applyTo - * @param {Object} canvasEl Canvas element to apply filter to - */ - applyTo: function(canvasEl) { - - var context = canvasEl.getContext('2d'), - imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), - data = imageData.data, - iLen = data.length, i, a; - - var rgb = parseInt(this.color, 10).toString(16); - - var cr = parseInt('0x' + rgb.substr(0, 2), 16); - var cg = parseInt('0x' + rgb.substr(2, 2), 16); - var cb = parseInt('0x' + rgb.substr(4, 2), 16); - - for (i = 0; i < iLen; i+=4) { - - a = data[i+3]; - - if (a > 0){ - data[i] = cr; - data[i+1] = cg; - data[i+2] = cb; - } - } - - context.putImageData(imageData, 0, 0); - }, - - /** - * @method toJSON - * @return {String} json representation of filter - */ - toJSON: function() { - return { - type: this.type, - color: this.color - }; - } -}); - -fabric.Image.filters.Tint.fromObject = function(object) { - return new fabric.Image.filters.Tint(object); -}; -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - clone = fabric.util.object.clone, - toFixed = fabric.util.toFixed; - - if (fabric.Text) { - fabric.warn('fabric.Text is already defined'); - return; - } - if (!fabric.Object) { - fabric.warn('fabric.Text requires fabric.Object'); - return; - } - - /** - * @class Text - * @extends fabric.Object - */ - fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ { - - /** - * @property - * @type Number - */ - fontSize: 40, - - /** - * @property - * @type Number - */ - fontWeight: 100, - - /** - * @property - * @type String - */ - fontFamily: 'Times New Roman', - - /** - * @property - * @type String - */ - textDecoration: '', - - /** - * @property - * @type String | null - */ - textShadow: '', - - /** - * Determines text alignment. Possible values: "left", "center", or "right". - * @property - * @type String - */ - textAlign: 'left', - - /** - * @property - * @type String - */ - fontStyle: '', - - /** - * @property - * @type Number - */ - lineHeight: 1.3, - - /** - * @property - * @type String - */ - strokeStyle: '', - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type String - */ - backgroundColor: '', - - - /** - * @property - * @type String | null - */ - path: null, - - /** - * @property - * @type String - */ - type: 'text', - - /** - * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) - * @property - * @type Boolean - */ - useNative: true, - - /** - * Constructor - * @method initialize - * @param {String} text - * @param {Object} [options] - * @return {fabric.Text} thisArg - */ - initialize: function(text, options) { - this._initStateProperties(); - this.text = text; - this.setOptions(options || { }); - this._theta = this.angle * Math.PI / 180; - this._initDimensions(); - this.setCoords(); - }, - - /** - * Renders text object on offscreen canvas, so that it would get dimensions - * @private - * @method _initDimensions - */ - _initDimensions: function() { - var canvasEl = fabric.document.createElement('canvas'); - - if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(canvasEl); - } - - this._render(canvasEl.getContext('2d')); - }, - - /** - * Creates `stateProperties` list on an instance, and adds `fabric.Text` -specific ones to it - * (such as "fontFamily", "fontWeight", etc.) - * @private - * @method _initStateProperties - */ - _initStateProperties: function() { - this.stateProperties = this.stateProperties.concat(); - this.stateProperties.push( - 'fontFamily', - 'fontWeight', - 'fontSize', - 'path', - 'text', - 'textDecoration', - 'textShadow', - 'textAlign', - 'fontStyle', - 'lineHeight', - 'strokeStyle', - 'strokeWidth', - 'backgroundColor', - 'useNative' - ); - fabric.util.removeFromArray(this.stateProperties, 'width'); - }, - - /** - * Returns string representation of an instance - * @method toString - * @return {String} String representation of text object - */ - toString: function() { - return '#'; - }, - - /** - * @private - * @method _render - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _render: function(ctx) { - if (typeof Cufon === 'undefined' || this.useNative === true) { - this._renderViaNative(ctx); - } - else { - this._renderViaCufon(ctx); - } - }, - - /** - * @private - * @method _renderViaCufon - */ - _renderViaCufon: function(ctx) { - var o = Cufon.textOptions || (Cufon.textOptions = { }); - - // export options to be used by cufon.js - o.left = this.left; - o.top = this.top; - o.context = ctx; - o.color = this.fill; - - var el = this._initDummyElementForCufon(); - - // set "cursor" to top/left corner - this.transform(ctx); - - // draw text - Cufon.replaceElement(el, { - engine: 'canvas', - separate: 'none', - fontFamily: this.fontFamily, - fontWeight: this.fontWeight, - textDecoration: this.textDecoration, - textShadow: this.textShadow, - textAlign: this.textAlign, - fontStyle: this.fontStyle, - lineHeight: this.lineHeight, - strokeStyle: this.strokeStyle, - strokeWidth: this.strokeWidth, - backgroundColor: this.backgroundColor - }); - - // update width, height - this.width = o.width; - this.height = o.height; - - this._totalLineHeight = o.totalLineHeight; - this._fontAscent = o.fontAscent; - this._boundaries = o.boundaries; - this._shadowOffsets = o.shadowOffsets; - this._shadows = o.shadows || [ ]; - - el = null; - - // need to set coords _after_ the width/height was retreived from Cufon - this.setCoords(); - }, - - /** - * @private - * @method _render_native - * @param {CanvasRenderingContext2D} ctx Context to render on - */ - _renderViaNative: function(ctx) { - - this.transform(ctx); - this._setTextStyles(ctx); - - var textLines = this.text.split(/\r?\n/); - - this.width = this._getTextWidth(ctx, textLines); - this.height = this._getTextHeight(ctx, textLines); - - this._renderTextBackground(ctx, textLines); - - if (this.textAlign !== 'left') { - ctx.save(); - ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); - } - - this._setTextShadow(ctx); - this._renderTextFill(ctx, textLines); - this.textShadow && ctx.restore(); - - this._renderTextStroke(ctx, textLines); - if (this.textAlign !== 'left') { - ctx.restore(); - } - - this._renderTextDecoration(ctx, textLines); - this._setBoundaries(ctx, textLines); - this._totalLineHeight = 0; - - this.setCoords(); - }, - - /** - * @private - * @method _setBoundaries - */ - _setBoundaries: function(ctx, textLines) { - this._boundaries = [ ]; - - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = this._getLineLeftOffset(lineWidth); - - this._boundaries.push({ - height: this.fontSize, - width: lineWidth, - left: lineLeftOffset - }); - } - }, - - /** - * @private - * @method _setTextStyles - */ - _setTextStyles: function(ctx) { - ctx.fillStyle = this.fill; - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.strokeWidth; - ctx.textBaseline = 'top'; - ctx.textAlign = this.textAlign; - ctx.font = this._getFontDeclaration(); - }, - - /** - * @private - * @method _getTextHeight - */ - _getTextHeight: function(ctx, textLines) { - return this.fontSize * textLines.length * this.lineHeight; - }, - - /** - * @private - * @method _getTextWidth - */ - _getTextWidth: function(ctx, textLines) { - var maxWidth = ctx.measureText(textLines[0]).width; - - for (var i = 1, len = textLines.length; i < len; i++) { - var currentLineWidth = ctx.measureText(textLines[i]).width; - if (currentLineWidth > maxWidth) { - maxWidth = currentLineWidth; - } - } - return maxWidth; - }, - - /** - * @private - * @method _setTextShadow - */ - _setTextShadow: function(ctx) { - if (this.textShadow) { - - // "rgba(0,0,0,0.2) 2px 2px 10px" - // "rgb(0, 100, 0) 0 0 5px" - // "red 2px 2px 1px" - // "#f55 123 345 567" - var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; - - var shadowDeclaration = this.textShadow; - var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); - var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); - - ctx.save(); - ctx.shadowColor = shadowColor; - ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); - ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); - ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); - - this._shadows = [{ - blur: ctx.shadowBlur, - color: ctx.shadowColor, - offX: ctx.shadowOffsetX, - offY: ctx.shadowOffsetY - }]; - - this._shadowOffsets = [[ - parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) - ]]; - } - }, - - _renderTextFill: function(ctx, textLines) { - this._boundaries = [ ]; - for (var i = 0, len = textLines.length; i < len; i++) { - ctx.fillText( - textLines[i], - -this.width / 2, - (-this.height / 2) + (i * this.fontSize * this.lineHeight) - ); - } - }, - - /** - * @private - * @method _renderTextStroke - */ - _renderTextStroke: function(ctx, textLines) { - if (this.strokeStyle) { - for (var i = 0, len = textLines.length; i < len; i++) { - ctx.strokeText( - textLines[i], - -this.width / 2, - (-this.height / 2) + (i * this.fontSize * this.lineHeight) - ); - } - } - }, - - /** - * @private - * @_renderTextBackground - */ - _renderTextBackground: function(ctx, textLines) { - if (this.backgroundColor) { - ctx.save(); - ctx.fillStyle = this.backgroundColor; - - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = this._getLineLeftOffset(lineWidth); - - ctx.fillRect( - (-this.width / 2) + lineLeftOffset, - (-this.height / 2) + (i * this.fontSize * this.lineHeight), - lineWidth, - this.fontSize - ); - } - ctx.restore(); - } - }, - - /** - * @private - * @method _getLineLeftOffset - */ - _getLineLeftOffset: function(lineWidth) { - if (this.textAlign === 'center') { - return (this.width - lineWidth) / 2; - } - if (this.textAlign === 'right') { - return this.width - lineWidth; - } - return 0; - }, - - /** - * @private - * @method _renderTextDecoration - */ - _renderTextDecoration: function(ctx, textLines) { - - var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; - var _this = this; - - function renderLinesAtOffset(offset) { - for (var i = 0, len = textLines.length; i < len; i++) { - - var lineWidth = ctx.measureText(textLines[i]).width; - var lineLeftOffset = _this._getLineLeftOffset(lineWidth); - - ctx.fillRect( - (-_this.width / 2) + lineLeftOffset, - (offset + (i * _this.fontSize * _this.lineHeight)) - halfOfVerticalBox, - lineWidth, - 1); - } - } - - if (this.textDecoration.indexOf('underline') > -1) { - renderLinesAtOffset(this.fontSize); - } - if (this.textDecoration.indexOf('line-through') > -1) { - renderLinesAtOffset(this.fontSize / 2); - } - if (this.textDecoration.indexOf('overline') > -1) { - renderLinesAtOffset(0); - } - }, - - /** - * @private - * @method _getFontDeclaration - */ - _getFontDeclaration: function() { - return [ - this.fontStyle, - this.fontWeight, - this.fontSize + 'px', - (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) - ].join(' '); - }, - - /** - * @private - * @method _initDummyElement - */ - _initDummyElementForCufon: function() { - var el = fabric.document.createElement('pre'), - container = fabric.document.createElement('div'); - - // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent - container.appendChild(el); - - if (typeof G_vmlCanvasManager === 'undefined') { - el.innerHTML = this.text; - } - else { - // IE 7 & 8 drop newlines and white space on text nodes - // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html - // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp - el.innerText = this.text.replace(/\r?\n/gi, '\r'); - } - - el.style.fontSize = this.fontSize + 'px'; - el.style.letterSpacing = 'normal'; - - return el; - }, - - /** - * Renders text instance on a specified context - * @method render - * @param ctx {CanvasRenderingContext2D} context to render on - */ - render: function(ctx, noTransform) { - ctx.save(); - this._render(ctx); - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.hideCorners || this.drawCorners(ctx); - } - ctx.restore(); - }, - - /** - * Returns object representation of an instance - * @method toObject - * @return {Object} Object representation of text object - */ - toObject: function() { - return extend(this.callSuper('toObject'), { - text: this.text, - fontSize: this.fontSize, - fontWeight: this.fontWeight, - fontFamily: this.fontFamily, - fontStyle: this.fontStyle, - lineHeight: this.lineHeight, - textDecoration: this.textDecoration, - textShadow: this.textShadow, - textAlign: this.textAlign, - path: this.path, - strokeStyle: this.strokeStyle, - strokeWidth: this.strokeWidth, - backgroundColor: this.backgroundColor, - useNative: this.useNative - }); - }, - - /** - * Returns svg representation of an instance - * @method toSVG - * @return {string} svg representation of an instance - */ - toSVG: function() { - - var textLines = this.text.split(/\r?\n/), - lineTopOffset = this.useNative - ? this.fontSize * this.lineHeight - : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), - - textLeftOffset = -(this.width/2), - textTopOffset = this.useNative - ? this.fontSize - 1 - : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, - - textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), - shadowSpans = this._getSVGShadows(lineTopOffset, textLines); - - // move top offset by an ascent - textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); - - return [ - '', - textAndBg.textBgRects.join(''), - '', - shadowSpans.join(''), - textAndBg.textSpans.join(''), - '', - '' - ].join(''); - }, - - _getSVGShadows: function(lineTopOffset, textLines) { - var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; - - if (!this._shadows || !this._boundaries) { - return shadowSpans; - } - - for (j = 0, jlen = this._shadows.length; j < jlen; j++) { - for (i = 0, ilen = textLines.length; i < ilen; i++) { - if (textLines[i] !== '') { - var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; - shadowSpans.push( - '', - fabric.util.string.escapeXml(textLines[i]), - ''); - lineTopOffsetMultiplier = 1; - } else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - } - } - return shadowSpans; - }, - - _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { - var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; - - // text and background - for (i = 0, len = textLines.length; i < len; i++) { - if (textLines[i] !== '') { - lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; - textSpans.push( - ' elements since setting opacity on containing one doesn't work in Illustrator - this._getFillAttributes(this.fill), '>', - fabric.util.string.escapeXml(textLines[i]), - '' - ); - lineTopOffsetMultiplier = 1; - } else { - // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier - // prevents empty tspans - lineTopOffsetMultiplier++; - } - - if (!this.backgroundColor || !this._boundaries) continue; - - textBgRects.push( - ''); - } - return { - textSpans: textSpans, - textBgRects: textBgRects - }; - }, - - // Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values - // we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 - _getFillAttributes: function(value) { - var fillColor = value ? new fabric.Color(value) : ''; - if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { - return 'fill="' + value + '"'; - } - return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; - }, - - /** - * Sets "color" of an instance (alias of `set('fill', …)`) - * @method setColor - * @param {String} value - * @return {fabric.Text} thisArg - * @chainable - */ - setColor: function(value) { - this.set('fill', value); - return this; - }, - - /** - * Sets fontSize of an instance and updates its coordinates - * @method setFontsize - * @param {Number} value - * @return {fabric.Text} thisArg - * @chainable - */ - setFontsize: function(value) { - this.set('fontSize', value); - this._initDimensions(); - this.setCoords(); - return this; - }, - - /** - * Returns actual text value of an instance - * @method getText - * @return {String} - */ - getText: function() { - return this.text; - }, - - /** - * Sets text of an instance, and updates its coordinates - * @method setText - * @param {String} value - * @return {fabric.Text} thisArg - * @chainable - */ - setText: function(value) { - this.set('text', value); - this._initDimensions(); - this.setCoords(); - return this; - }, - - /** - * Sets specified property to a specified value - * @method set - * @param {String} name - * @param {Any} value - * @return {fabric.Text} thisArg - * @chainable - */ - _set: function(name, value) { - if (name === 'fontFamily' && this.path) { - this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); - } - this.callSuper('_set', name, value); - } - }); - - /** - * List of attribute names to account for when parsing SVG element (used by `fabric.Text.fromElement`) - * @static - */ - fabric.Text.ATTRIBUTE_NAMES = - ('x y fill fill-opacity opacity stroke stroke-width transform ' + - 'font-family font-style font-weight font-size text-decoration').split(' '); - - /** - * Returns fabric.Text instance from an object representation - * @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)); - }; - - /** - * Returns fabric.Text instance from an SVG element (not yet implemented) - * @static - * @method fabric.Text.fromElement - * @param element - * @param options - * @return {fabric.Text} an instance - */ - fabric.Text.fromElement = function(element, options) { - if (!element) { - return null; - } - - var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); - options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); - var text = new fabric.Text(element.textContent, options); - - return text; - }; - -})(typeof exports !== 'undefined' ? exports : this); -(function() { - - if (typeof document !== 'undefined' && typeof window !== 'undefined') { - return; - } - - var DOMParser = new require('xmldom').DOMParser, - URL = require('url'), - HTTP = require('http'), - - Canvas = require('canvas'), - Image = require('canvas').Image; - - function request(url, encoding, callback) { - var oURL = URL.parse(url), - client = HTTP.createClient(oURL.port, oURL.hostname), - req = client.request('GET', oURL.pathname, { 'host': oURL.hostname }); - - client.addListener('error', function(err) { - if (err.errno === process.ECONNREFUSED) { - fabric.log('ECONNREFUSED: connection refused to ' + client.host + ':' + client.port); - } - else { - fabric.log(err.message); - } - }); - - req.end(); - req.on('response', function (response) { - var body = ""; - if (encoding) { - response.setEncoding(encoding); - } - response.on('end', function () { - callback(body); - }); - response.on('data', function (chunk) { - if (response.statusCode === 200) { - body += chunk; - } - }); - }); - } - - fabric.util.loadImage = function(url, callback) { - request(url, 'binary', function(body) { - var img = new Image(); - img.src = new Buffer(body, 'binary'); - // preserving original url, which seems to be lost in node-canvas - img._src = url; - callback(img); - }); - }; - - fabric.loadSVGFromURL = function(url, callback) { - url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); - request(url, '', function(body) { - fabric.loadSVGFromString(body, callback); - }); - }; - - fabric.loadSVGFromString = function(string, callback) { - var doc = new DOMParser().parseFromString(string); - fabric.parseSVGDocument(doc.documentElement, function(results, options) { - callback(results, options); - }); - }; - - fabric.util.getScript = function(url, callback) { - request(url, '', function(body) { - eval(body); - callback && callback(); - }); - }; - - fabric.Image.fromObject = function(object, callback) { - fabric.util.loadImage(object.src, function(img) { - var oImg = new fabric.Image(img); - - oImg._initConfig(object); - oImg._initFilters(object); - callback(oImg); - }); - }; - - /** - * Only available when running fabric on node.js - * @method createCanvasForNode - * @param width Canvas width - * @param height Canvas height - * @return {Object} wrapped canvas instance - */ - fabric.createCanvasForNode = function(width, height) { - - var canvasEl = fabric.document.createElement('canvas'), - nodeCanvas = new Canvas(width || 600, height || 600); - - // jsdom doesn't create style on canvas element, so here be temp. workaround - canvasEl.style = { }; - - canvasEl.width = nodeCanvas.width; - canvasEl.height = nodeCanvas.height; - - var FabricCanvas = fabric.Canvas || fabric.StaticCanvas; - var fabricCanvas = new FabricCanvas(canvasEl); - fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); - fabricCanvas.nodeCanvas = nodeCanvas; - - return fabricCanvas; - }; - - fabric.StaticCanvas.prototype.createPNGStream = function() { - return this.nodeCanvas.createPNGStream(); - }; - - var origSetWidth = fabric.StaticCanvas.prototype.setWidth; - fabric.StaticCanvas.prototype.setWidth = function(width) { - origSetWidth.call(this); - this.nodeCanvas.width = width; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; - } - - var origSetHeight = fabric.StaticCanvas.prototype.setHeight; - fabric.StaticCanvas.prototype.setHeight = function(height) { - origSetHeight.call(this); - this.nodeCanvas.height = height; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; - } - -})(); +/* build: `node build.js modules=ALL` */ +/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: "0.9.14" }; + +if (typeof exports != 'undefined') { + exports.fabric = fabric; +} + +if (typeof document != 'undefined' && typeof window != 'undefined') { + fabric.document = document; + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + fabric.document = require("jsdom").jsdom(""); + fabric.window = fabric.document.createWindow(); +} + +/** + * True when in environment that supports touch events + * @property isTouchSupported + * @type boolean + */ +fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; + +/** + * True when in environment that's probably Node.js + * @property isLikelyNode + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +/*! + * Copyright (c) 2009 Simo Kinnunen. + * Licensed under the MIT license. + */ + +var Cufon = (function() { + + var api = function() { + return api.replace.apply(null, arguments); + }; + + var DOM = api.DOM = { + + ready: (function() { + + var complete = false, readyStatus = { loaded: 1, complete: 1 }; + + var queue = [], perform = function() { + if (complete) return; + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Gecko, Opera, WebKit r26101+ + + if (fabric.document.addEventListener) { + fabric.document.addEventListener('DOMContentLoaded', perform, false); + fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages + } + + // Old WebKit, Internet Explorer + + if (!fabric.window.opera && fabric.document.readyState) (function() { + readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); + })(); + + // Internet Explorer + + if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { + try { + fabric.document.body.doScroll('left'); + perform(); + } + catch (e) { + setTimeout(arguments.callee, 1); + } + })(); + + addEvent(fabric.window, 'load', perform); // Fallback + + return function(listener) { + if (!arguments.length) perform(); + else complete ? listener() : queue.push(listener); + }; + + })() + + }; + + var CSS = api.CSS = { + + Size: function(value, base) { + + this.value = parseFloat(value); + this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; + + this.convert = function(value) { + return value / base * this.value; + }; + + this.convertFrom = function(value) { + return value / this.value * base; + }; + + this.toString = function() { + return this.value + this.unit; + }; + + }, + + getStyle: function(el) { + return new Style(el.style); + /* + var view = document.defaultView; + if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); + if (el.currentStyle) return new Style(el.currentStyle); + return new Style(el.style); + */ + }, + + quotedList: cached(function(value) { + // doesn't work properly with empty quoted strings (""), but + // it's not worth the extra code. + var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; + while (match = re.exec(value)) list.push(match[3] || match[1]); + return list; + }), + + ready: (function() { + + var complete = false; + + var queue = [], perform = function() { + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Safari 2 does not include '); + + function getFontSizeInPixels(el, value) { + return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); + } + + // Original by Dead Edwards. + // Combined with getFontSizeInPixels it also works with relative units. + function getSizeInPixels(el, value) { + if (/px$/i.test(value)) return parseFloat(value); + var style = el.style.left, runtimeStyle = el.runtimeStyle.left; + el.runtimeStyle.left = el.currentStyle.left; + el.style.left = value; + var result = el.style.pixelLeft; + el.style.left = style; + el.runtimeStyle.left = runtimeStyle; + return result; + } + + return function(font, text, style, options, node, el, hasNext) { + var redraw = (text === null); + + if (redraw) text = node.alt; + + // @todo word-spacing, text-decoration + + var viewBox = font.viewBox; + + var size = style.computedFontSize || + (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); + + var letterSpacing = style.computedLSpacing; + + if (letterSpacing == undefined) { + letterSpacing = style.get('letterSpacing'); + style.computedLSpacing = letterSpacing = + (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); + } + + var wrapper, canvas; + + if (redraw) { + wrapper = node; + canvas = node.firstChild; + } + else { + wrapper = fabric.document.createElement('span'); + wrapper.className = 'cufon cufon-vml'; + wrapper.alt = text; + + canvas = fabric.document.createElement('span'); + canvas.className = 'cufon-vml-canvas'; + wrapper.appendChild(canvas); + + if (options.printable) { + var print = fabric.document.createElement('span'); + print.className = 'cufon-alt'; + print.appendChild(fabric.document.createTextNode(text)); + wrapper.appendChild(print); + } + + // ie6, for some reason, has trouble rendering the last VML element in the document. + // we can work around this by injecting a dummy element where needed. + // @todo find a better solution + if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); + } + + var wStyle = wrapper.style; + var cStyle = canvas.style; + + var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); + var roundingFactor = roundedHeight / height; + var minX = viewBox.minX, minY = viewBox.minY; + + cStyle.height = roundedHeight; + cStyle.top = Math.round(size.convert(minY - font.ascent)); + cStyle.left = Math.round(size.convert(minX)); + + wStyle.height = size.convert(font.height) + 'px'; + + var textDecoration = Cufon.getTextDecoration(options); + + var color = style.get('color'); + + var chars = Cufon.CSS.textTransform(text, style).split(''); + + var width = 0, offsetX = 0, advance = null; + + var glyph, shape, shadows = options.textShadow; + + // pre-calculate width + for (var i = 0, k = 0, l = chars.length; i < l; ++i) { + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; + } + + if (advance === null) return null; + + var fullWidth = -minX + width + (viewBox.width - advance); + + var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); + + var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; + var stretch = 'r' + coordSize + 'nsnf'; + + for (i = 0; i < l; ++i) { + + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (!glyph) continue; + + if (redraw) { + // some glyphs may be missing so we can't use i + shape = canvas.childNodes[k]; + if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow + } + else { + shape = fabric.document.createElement('cvml:shape'); + canvas.appendChild(shape); + } + + shape.stroked = 'f'; + shape.coordsize = coordSize; + shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; + shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; + shape.fillcolor = color; + + // it's important to not set top/left or IE8 will grind to a halt + var sStyle = shape.style; + sStyle.width = roundedShapeWidth; + sStyle.height = roundedHeight; + + if (shadows) { + // due to the limitations of the VML shadow element there + // can only be two visible shadows. opacity is shared + // for all shadows. + var shadow1 = shadows[0], shadow2 = shadows[1]; + var color1 = Cufon.CSS.color(shadow1.color), color2; + var shadow = fabric.document.createElement('cvml:shadow'); + shadow.on = 't'; + shadow.color = color1.color; + shadow.offset = shadow1.offX + ',' + shadow1.offY; + if (shadow2) { + color2 = Cufon.CSS.color(shadow2.color); + shadow.type = 'double'; + shadow.color2 = color2.color; + shadow.offset2 = shadow2.offX + ',' + shadow2.offY; + } + shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; + shape.appendChild(shadow); + } + + offsetX += ~~(glyph.w || font.w) + letterSpacing; + + ++k; + + } + + wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); + + return wrapper; + + }; + +})()); + +Cufon.getTextDecoration = function(options) { + return { + underline: options.textDecoration === 'underline', + overline: options.textDecoration === 'overline', + 'line-through': options.textDecoration === 'line-through' + }; +}; + +if (typeof exports != 'undefined') { + exports.Cufon = Cufon; +} + +/* + json2.js + 2011-10-19 + + 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) { + // Format integers to have at least two digits. + 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 is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + 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: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + 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, regexp: true */ + +/*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 +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +var JSON; +if (!JSON) { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + 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) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + 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) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + 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); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + 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, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); +/** + * Wrapper around `console.log` (when available) + * @method log + * @param {Any} Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @method warn + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; + +if (typeof console !== 'undefined') { + if (typeof console.log !== 'undefined' && console.log.apply) { + fabric.log = function() { + return console.log.apply(console, arguments); + }; + } + if (typeof console.warn !== 'undefined' && console.warn.apply) { + fabric.warn = function() { + return console.warn.apply(console, arguments); + }; + } +} + +/** + * @namespace + */ +fabric.Observable = { + + /** + * Observes specified event + * @method observe + * @depracated Since 0.8.34. Use `on` instead. + * @param {String} eventName + * @param {Function} handler + */ + observe: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = [ ]; + } + this.__eventListeners[eventName].push(handler); + } + }, + + /** + * Stops event observing for a particular event handler + * @method stopObserving + * @depracated Since 0.8.34. Use `off` instead. + * @param {String} eventName + * @param {Function} handler + */ + stopObserving: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + if (this.__eventListeners[eventName]) { + fabric.util.removeFromArray(this.__eventListeners[eventName], handler); + } + }, + + /** + * Fires event with an optional options object + * @method fire + * @param {String} eventName + * @param {Object} [options] + */ + fire: function(eventName, options) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) return; + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + // avoiding try/catch for perf. reasons + listenersForEvent[i](options || { }); + } + } +}; + +/** + * Alias for observe + * @method observe + * @memberOf fabric.Observable + */ +fabric.Observable.on = fabric.Observable.observe; + +/** + * Alias for stopObserving + * @method off + */ +fabric.Observable.off = fabric.Observable.stopObserving; +(function() { + + /** + * @namespace + */ + fabric.util = { }; + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @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; + } + + /** + * Returns random number between 2 specified ones. + * @static + * @method getRandomInt + * @memberOf fabric.util + * @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; + } + + var PiBy180 = Math.PI / 180; + + /** + * Transforms degrees to radians. + * @static + * @method degreesToRadians + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + function degreesToRadians(degrees) { + return degrees * PiBy180; + } + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @method toFixed + * @memberOf fabric.util + * @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 + * @memberOf fabric.util + * @return {Boolean} + */ + function falseFunction() { + return false; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @method animate + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + options || (options = { }); + + var start = +new Date(), + duration = options.duration || 500, + finish = start + duration, time, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100, + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick() { + time = +new Date(); + var currentTime = time > finish ? duration : (time - start); + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish || abort()) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(); + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback) { + fabric.window.setTimeout(callback, 1000 / 60); + }; + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * @method requestAnimFrame + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + var requestAnimFrame = function() { + return _requestAnimFrame.apply(fabric.window, arguments); + }; + + /** + * Loads image element from given url and passes it to a callback + * @method loadImage + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} context optional Context to invoke callback in + */ + function loadImage(url, callback, context) { + if (url) { + var img = new Image(); + /** @ignore */ + img.onload = function () { + callback && callback.call(context, img); + img = img.onload = null; + }; + img.src = url; + } + else { + callback && callback.call(context, url); + } + } + + function enlivenObjects(objects, callback) { + + function getKlass(type) { + return fabric[fabric.util.string.camelize(fabric.util.string.capitalize(type))]; + } + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + if (callback) { + callback(enlivenedObjects); + } + } + } + + var enlivenedObjects = [ ], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + objects.forEach(function (o, index) { + if (!o.type) { + return; + } + var klass = getKlass(o.type); + if (klass.async) { + klass.fromObject(o, function (o) { + enlivenedObjects[index] = o; + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + onLoaded(); + } + }); + } + + /** + * Groups SVG elements (usually those retrieved from SVG document) + * @static + * @memberOf fabric.util + * @method groupSVGElements + * @param {Array} elements + * @param {Object} options optional + * @return {String} path optional + */ + function groupSVGElements(elements, options, path) { + var object = elements.length > 1 + ? new fabric.PathGroup(elements, options) + : elements[0]; + + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + } + + fabric.util.removeFromArray = removeFromArray; + fabric.util.degreesToRadians = degreesToRadians; + fabric.util.toFixed = toFixed; + fabric.util.getRandomInt = getRandomInt; + fabric.util.falseFunction = falseFunction; + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.loadImage = loadImage; + fabric.util.enlivenObjects = enlivenObjects; + fabric.util.groupSVGElements = groupSVGElements; +})(); +(function() { + + var slice = Array.prototype.slice; + + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + var t = Object(this), len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + 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 array contains no values, no initial value to return + 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; + }; + } + + /** + * Invokes method on all items in a given array + * @method invoke + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + */ + 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; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @method max + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function max(array, byProperty) { + if (!array || array.length === 0) return undefined; + + 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; + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @method min + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function min(array, byProperty) { + if (!array || array.length === 0) return undefined; + + 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; + } + + /** @namespace */ + fabric.util.array = { + invoke: invoke, + min: min, + max: max + }; + +})(); +(function(){ + + /** + * Copies all enumerable properties of one object to another + * @memberOf fabric.util.object + * @method extend + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + */ + function extend(destination, source) { + // JScript DontEnum bug is not taken care of + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * @method clone + * @memberOf fabric.util.object + * @param {Object} object Object to clone + */ + function clone(object) { + return extend({ }, object); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + +})(); +(function() { + +if (!String.prototype.trim) { + /** + * Trims a string (removing whitespace from the beginning and the end) + * @method trim + * @see String#trim on MDN + */ + String.prototype.trim = function () { + // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now + return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); + }; +} + +/** + * Camelizes a string + * @memberOf fabric.util.string + * @method camelize + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ +function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); +} + +/** + * Capitalizes a string + * @memberOf fabric.util.string + * @method capitalize + * @param {String} string String to capitalize + * @return {String} Capitalized version of a string + */ +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +} + +function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + +/** @namespace */ +fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml +}; +}()); + +(function() { + + var slice = Array.prototype.slice, + apply = Function.prototype.apply, + Dummy = function() { }; + + if (!Function.prototype.bind) { + /** + * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) + * @see Function#bind on MDN + * @param {Object} thisArg Object to bind function to + * @param {Any[]} [...] Values to pass to a bound function + * @return {Function} + */ + Function.prototype.bind = function(thisArg) { + var fn = this, args = slice.call(arguments, 1), bound; + if (args.length) { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + bound = function() { + return apply.call(fn, this instanceof Dummy ? this : thisArg, arguments); + }; + } + Dummy.prototype = this.prototype; + bound.prototype = new Dummy(); + + return bound; + }; + } + +})(); +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }; + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + + /** @ignore */ + var addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && typeof klass.prototype[property] === 'function') { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + }; + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function Subclass() { } + + /** + * Helper for creation of "classes" + * @method createClass + * @memberOf fabric.util + */ + 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], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + return klass; + } + + fabric.util.createClass = createClass; +})(); +(function () { + + /* 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 fabric.document.documentElement.uniqueID !== 'undefined') { + return function (element) { + return element.uniqueID; + }; + } + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + /** @ignore */ + var getElement, setElement; + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + 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 || fabric.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 || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + + // DOM L0 branch + handlers = { }, + + addListener, removeListener; + + if (shouldUseAddListenerRemoveListener) { + /** @ignore */ + addListener = function (element, eventName, handler) { + element.addEventListener(eventName, handler, false); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + element.removeEventListener(eventName, handler, false); + }; + } + + else if (shouldUseAttachEventDetachEvent) { + /** @ignore */ + 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); + }; + /** @ignore */ + 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 { + /** @ignore */ + 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); + }; + /** @ignore */ + 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); + } + } + } + }; + } + + /** + * Adds an event listener to an element + * @mthod addListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = addListener; + + /** + * Removes an event listener from an element + * @mthod removeListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = removeListener; + + /** + * Cross-browser wrapper for getting event's coordinates + * @method getPointer + * @memberOf fabric.util + * @param {Event} event + */ + function getPointer(event) { + // TODO (kangax): this method needs fixing + return { x: pointerX(event), y: pointerY(event) }; + } + + var pointerX = function(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollLeft: 0 }; + + // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) + // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] + // need to investigate later + return event.pageX || ((typeof event.clientX !== 'unknown' ? event.clientX : 0) + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)); + }; + + var pointerY = function(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollTop: 0 }; + + return event.pageY || ((typeof event.clientY !== 'unknown' ? event.clientY : 0) + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)); + }; + + if (fabric.isTouchSupported) { + pointerX = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX; + }; + pointerY = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY; + }; + } + + fabric.util.getPointer = getPointer; + + fabric.util.object.extend(fabric.util, fabric.Observable); + +})(); +(function () { + + /** + * Cross-browser wrapper for setting element's style + * @method setStyle + * @memberOf fabric.util + * @param {HTMLElement} element + * @param {Object} styles + * @return {HTMLElement} Element that was passed as a first argument + */ + function setStyle(element, styles) { + var elementStyle = element.style; + if (!elementStyle) { + return element; + } + 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 = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, + + /** @ignore */ + setOpacity = function (element) { return element; }; + + if (supportsOpacity) { + /** @ignore */ + setOpacity = function(element, value) { + element.style.opacity = value; + return element; + }; + } + else if (supportsFilters) { + /** @ignore */ + 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() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @method getById + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @method toArray + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + var toArray = function(arrayLike) { + return _slice.call(arrayLike, 0); + }; + + var sliceCanConvertNodelists; + try { + sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch(err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @method makeElement + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.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; + } + + /** + * Adds class to an element + * @method addClass + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @method wrapElement + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + 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; + } + + /** + * Returns offset for a given element + * @method getElementOffset + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + // TODO (kangax): need to fix this method + 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 = fabric.document.documentElement.style; + + var selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @method makeElementUnselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + 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; + } + + /** + * Makes element selectable + * @method makeElementSelectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable === 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + (function() { + + /** + * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading + * @method getScript + * @memberOf fabric.util + * @param {String} url URL of a script to load + * @param {Function} callback Callback to execute when script is finished loading + */ + function getScript(url, callback) { + var headEl = fabric.document.getElementsByTagName("head")[0], + scriptEl = fabric.document.createElement('script'), + loading = true; + + scriptEl.type = 'text/javascript'; + scriptEl.setAttribute('runat', 'server'); + + /** @ignore */ + scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { + if (loading) { + if (typeof this.readyState === 'string' && + this.readyState !== 'loaded' && + this.readyState !== 'complete') return; + loading = false; + callback(e || fabric.window.event); + scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; + } + }; + scriptEl.src = url; + headEl.appendChild(scriptEl); + // causes issue in Opera + // headEl.removeChild(scriptEl); + } + + fabric.util.getScript = getScript; + })(); + + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.makeElement = makeElement; + fabric.util.addClass = addClass; + fabric.util.wrapElement = wrapElement; + fabric.util.getElementOffset = getElementOffset; + +})(); +(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() { } + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @method request + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + xhr = makeXHR(), + body; + + /** @ignore */ + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + onComplete(xhr); + xhr.onreadystatechange = emptyFn; + } + }; + + if (method === 'GET') { + body = null; + if (typeof options.parameters === 'string') { + url = addParamToUrl(url, options.parameters); + } + } + + xhr.open(method, url, true); + + if (method === 'POST' || method === 'PUT') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + xhr.send(body); + return xhr; + } + + fabric.util.request = request; +})(); +(function() { + + /** + * @method easeInQuad + * @memberOf fabric.util.ease + */ + function easeInQuad(t, b, c, d) { + return c*(t/=d)*t + b; + } + + /** + * @method easeOutQuad + * @memberOf fabric.util.ease + */ + function easeOutQuad(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } + + /** + * @method easeInOutQuad + * @memberOf fabric.util.ease + */ + function easeInOutQuad(t, b, c, d) { + t /= (d/2); + if (t < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + } + + /** + * @method easeInCubic + * @memberOf fabric.util.ease + */ + function easeInCubic(t, b, c, d) { + return c*(t/=d)*t*t + b; + } + + /** + * @method easeOutCubic + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c*((t=t/d-1)*t*t + 1) + b; + } + + /** + * @method easeInOutCubic + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t + b; + return c/2*((t-=2)*t*t + 2) + b; + } + + /** + * @method easeInQuart + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c*(t/=d)*t*t*t + b; + } + + /** + * @method easeOutQuart + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t=t/d-1)*t*t*t - 1) + b; + } + + /** + * @method easeInOutQuart + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t*t + b; + return -c/2 * ((t-=2)*t*t*t - 2) + b; + } + + /** + * @method easeInQuint + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c*(t/=d)*t*t*t*t + b; + } + + /** + * @method easeOutQuint + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c*((t=t/d-1)*t*t*t*t + 1) + b; + } + + /** + * @method easeInOutQuint + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + t /= d/2; + if (t < 1) return c/2*t*t*t*t*t + b; + return c/2*((t-=2)*t*t*t*t + 2) + b; + } + + /** + * @method easeInSine + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t/d * (Math.PI/2)) + c + b; + } + + /** + * @method easeOutSine + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t/d * (Math.PI/2)) + b; + } + + /** + * @method easeInOutSine + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; + } + + /** + * @method easeInExpo + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t===0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + } + + /** + * @method easeOutExpo + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + } + + /** + * @method easeInOutExpo + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t===0) return b; + if (t===d) return b+c; + t /= d/2; + if (t < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * @method easeInCirc + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; + } + + /** + * @method easeOutCirc + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t=t/d-1)*t) + b; + } + + /** + * @method easeInOutCirc + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + t /= d/2; + if (t < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; + return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; + } + + /** + * @method easeInElastic + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + } + + /** + * @method easeOutElastic + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d; + if (t===1) return b+c; + if (!p) p=d*0.3; + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + } + + /** + * @method easeInOutElastic + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t===0) return b; + t /= d/2; + if (t===2) return b+c; + if (!p) p=d*(0.3*1.5); + if (a < Math.abs(c)) { a=c; s=p/4; } + else s = p/(2*Math.PI) * Math.asin (c/a); + if (t < 1) return -0.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*0.5 + c + b; + } + + /** + * @method easeInBack + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + return c*(t/=d)*t*((s+1)*t - s) + b; + } + + /** + * @method easeOutBack + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; + } + + /** + * @method easeInOutBack + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s === undefined) s = 1.70158; + t /= d/2; + if (t < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; + return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; + } + + /** + * @method easeInBounce + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d-t, 0, c, d) + b; + } + + /** + * @method easeOutBounce + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t/=d) < (1/2.75)) { + return c*(7.5625*t*t) + b; + } else if (t < (2/2.75)) { + return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; + } else if (t < (2.5/2.75)) { + return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; + } else { + return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; + } + } + + /** + * @method easeInOutBounce + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d/2) return easeInBounce (t*2, 0, c, d) * 0.5 + b; + return easeOutBounce (t*2-d, 0, c, d) * 0.5 + c*0.5 + b; + } + + /** @namespace fabric.util.ease */ + fabric.util.ease = { + easeInQuad: easeInQuad, + easeOutQuad: easeOutQuad, + easeInOutQuad: easeInOutQuad, + easeInCubic: easeInCubic, + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +}()); +(function(global) { + + "use strict"; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.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', + 'text-decoration': 'textDecoration', + 'font-size': 'fontSize', + 'font-weight': 'fontWeight', + 'font-style': 'fontStyle', + 'font-family': 'fontFamily' + }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @method parseAttributes + * @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 there's a parent container (`g` node), parse its attributes recursively upwards + 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) { + // "normalize" attribute values + 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); + } + attr = normalizeAttr(attr); + memo[attr] = isNaN(parsed) ? value : parsed; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + + ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + return extend(parentAttributes, ownAttributes); + } + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @method 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]; + } + } + + // identity matrix + var iMatrix = [ + 1, // a + 0, // b + 0, // c + 1, // d + 0, // e + 0 // f + ], + + // == begin transform regexp + 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*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transform_list), + // == end transform regexp + + reTransform = new RegExp(transform); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(); + + // return if no argument was given or + // an argument does not match transform attribute regexp + 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; + }; + })(); + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @method parsePointsAttribute + * @param points {String} points attribute string + * @return {Array} array of points + */ + function parsePointsAttribute(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ], i, len; + + // points could look like "10,20 30,40" or "10 20 30 40" + if (asPairs) { + i = 0; + len = points.length; + for (; i < len; i++) { + var pair = points[i].split(','); + parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) }); + } + } + else { + i = 0; + len = points.length; + for (; i < len; i+=2) { + parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + } + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @method parseStyleAttribute + * @param {SVGElement} element 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) return oStyle; + + if (typeof style === 'string') { + style = style.replace(/;$/, '').split(';').forEach(function (current) { + var attr = current.split(':'); + oStyle[normalizeAttr(attr[0].trim().toLowerCase())] = attr[1].trim(); + }); + } + else { + for (var prop in style) { + if (typeof style[prop] === 'undefined') continue; + oStyle[normalizeAttr(prop.toLowerCase())] = style[prop]; + } + } + + return oStyle; + } + + function resolveGradients(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); + + if (/^url\(/.test(instanceFillValue)) { + + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); + + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + } + } + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @method parseElements + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} options Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function parseElements(elements, callback, options, reviver) { + var instances = new Array(elements.length), i = elements.length; + + function checkIfDone() { + if (--i === 0) { + instances = instances.filter(function(el) { + return el != null; + }); + resolveGradients(instances); + callback(instances); + } + } + + for (var index = 0, el, len = elements.length; index < len; index++) { + el = elements[index]; + var klass = fabric[capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + if (klass.async) { + klass.fromElement(el, (function(index, el) { + return function(obj) { + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + }; + })(index), options); + } + else { + var obj = klass.fromElement(el, options); + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + } + } + catch(e) { + fabric.log(e.message || e); + } + } + else { + checkIfDone(); + } + } + } + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @method getCSSRules + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + function getCSSRules(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim(); }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/); + rule = match[1]; + var declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + } + + function getGlobalStylesForElement(element) { + var nodeName = element.nodeName, + className = element.getAttribute('class'), + id = element.getAttribute('id'), + styles = { }; + + for (var rule in fabric.cssRules) { + var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || + (id && new RegExp('^#' + id).test(rule)) || + (new RegExp('^' + nodeName).test(rule)); + + if (ruleMatchesElement) { + for (var property in fabric.cssRules[rule]) { + styles[property] = fabric.cssRules[rule][property]; + } + } + } + + return styles; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @method parseSVGDocument + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + fabric.parseSVGDocument = (function() { + + var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // \d doesn't quite cut it (as we need to match an actual float number) + + // matches, e.g.: +14.56e-12, etc. + var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; + + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*' + + '$' + ); + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (nodeName.test(element.nodeName)) { + return true; + } + } + return false; + } + + return function(doc, callback, reviver) { + if (!doc) return; + + var startTime = new Date(), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + + if (descendants.length === 0) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes("//*[name(.)!='svg']"); + var arr = [ ]; + for (var i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + return reAllowedSVGTagNames.test(el.tagName) && + !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + 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); + } + + // values of width/height attributes overwrite those extracted from viewbox attribute + width = widthAttr ? parseFloat(widthAttr) : width; + height = heightAttr ? parseFloat(heightAttr) : height; + + var options = { + width: width, + height: height + }; + + fabric.gradientDefs = fabric.getGradientDefs(doc); + fabric.cssRules = getCSSRules(doc); + + // Precedence of rules: style > class > attribute + + fabric.parseElements(elements, function(instances) { + fabric.documentParsingTime = new Date() - startTime; + if (callback) { + callback(instances, options); + } + }, clone(options), reviver); + }; + })(); + + /** + * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) + * @property + * @namespace + */ + var svgCache = { + + /** + * @method has + * @param {String} name + * @param {Function} callback + */ + has: function (name, callback) { + callback(false); + }, + + /** + * @method get + * @param {String} url + * @param {Function} callback + */ + get: function () { + /* NOOP */ + }, + + /** + * @method set + * @param {String} url + * @param {Object} object + */ + set: function () { + /* NOOP */ + } + }; + + /** + * Takes url corresponding to an SVG document, and parses it into a set of fabric objects + * @method loadSVGFromURL + * @param {String} url + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromURL(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(//i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + } + + /** + * @method _enlivenCachedObject + */ + function _enlivenCachedObject(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 }); + } + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @method loadSVGFromString + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromString(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(//i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + } + + function createSVGFontFacesMarkup(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '', + '', + '' + ].join(''); + } + + return markup; + } + + extend(fabric, { + + parseAttributes: parseAttributes, + parseElements: parseElements, + parseStyleAttribute: parseStyleAttribute, + parsePointsAttribute: parsePointsAttribute, + getCSSRules: getCSSRules, + + loadSVGFromURL: loadSVGFromURL, + loadSVGFromString: loadSVGFromString, + + createSVGFontFacesMarkup: createSVGFontFacesMarkup + }); + +})(typeof exports !== 'undefined' ? exports : this); + +(function() { + + function getColorStopFromStyle(el) { + var style = el.getAttribute('style'); + + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length-1] === '') { + keyValuePairs.pop(); + } + + for (var i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + return value; + } + } + } + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Gradient = fabric.util.createClass(/** @scope fabric.Gradient.prototype */ { + + initialize: function(options) { + + options || (options = { }); + + this.x1 = options.x1 || 0; + this.y1 = options.y1 || 0; + this.x2 = options.x2 || 0; + this.y2 = options.y2 || 0; + + this.colorStops = options.colorStops; + }, + + toObject: function() { + return { + x1: this.x1, + x2: this.x2, + y1: this.y1, + y2: this.y2, + colorStops: this.colorStops + }; + }, + + toLiveGradient: function(ctx) { + var gradient = ctx.createLinearGradient( + this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); + + for (var position in this.colorStops) { + var colorValue = this.colorStops[position]; + gradient.addColorStop(parseFloat(position), colorValue); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /** + * @method fromElement + * @static + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + */ + fromElement: function(el, instance) { + + /** + * @example: + * + * + * + * + * + * + * OR + * + * + * + * + * + * + */ + + var colorStopEls = el.getElementsByTagName('stop'), + offset, + colorStops = { }, + coords = { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + + for (var i = colorStopEls.length; i--; ) { + el = colorStopEls[i]; + offset = el.getAttribute('offset'); + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); + } + + _convertPercentUnitsToValues(instance, coords); + + return new fabric.Gradient({ + x1: coords.x1, + y1: coords.y1, + x2: coords.x2, + y2: coords.y2, + colorStops: colorStops + }); + }, + + /** + * @method forObject + * @static + */ + forObject: function(obj, options) { + options || (options = { }); + _convertPercentUnitsToValues(obj, options); + return new fabric.Gradient(options); + } + }); + + function _convertPercentUnitsToValues(object, options) { + for (var prop in options) { + if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { + var percents = parseFloat(options[prop], 10); + if (prop === 'x1' || prop === 'x2') { + options[prop] = object.width * percents / 100; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = object.height * percents / 100; + } + } + // normalize rendering point (should be from top/left corner rather than center of the shape) + if (prop === 'x1' || prop === 'x2') { + options[prop] -= object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= object.height / 2; + } + } + } + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @method getGradientDefs + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + function getGradientDefs(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, i, + gradientDefs = { }; + + i = linearGradientEls.length; + for (; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + i = radialGradientEls.length; + for (; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + } + + fabric.getGradientDefs = getGradientDefs; + +})(); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Point) { + fabric.warn('fabric.Point is already defined'); + return; + } + + fabric.Point = Point; + + /** + * @name Point + * @memberOf fabric + * @constructor + * @param {Number} x + * @param {Number} y + * @return {fabric.Point} thisArg + */ + function Point(x, y) { + if (arguments.length > 0) { + this.init(x, y); + } + } + + Point.prototype = /** @scope fabric.Point.prototype */ { + + constructor: Point, + + /** + * @method init + * @param {Number} x + * @param {Number} y + */ + init: function (x, y) { + this.x = x; + this.y = y; + }, + + /** + * @method add + * @param {fabric.Point} that + * @return {fabric.Point} new Point instance with added values + */ + add: function (that) { + return new Point(this.x + that.x, this.y + that.y); + }, + + /** + * @method addEquals + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + addEquals: function (that) { + this.x += that.x; + this.y += that.y; + return this; + }, + + /** + * @method scalarAdd + * @param {Number} scalar + * @return {fabric.Point} new Point with added value + */ + scalarAdd: function (scalar) { + return new Point(this.x + scalar, this.y + scalar); + }, + + /** + * @method scalarAddEquals + * @param {Number} scalar + * @param {fabric.Point} thisArg + */ + scalarAddEquals: function (scalar) { + this.x += scalar; + this.y += scalar; + return this; + }, + + /** + * @method subtract + * @param {fabric.Point} that + * @return {fabric.Point} new Point object with subtracted values + */ + subtract: function (that) { + return new Point(this.x - that.x, this.y - that.y); + }, + + /** + * @method subtractEquals + * @param {fabric.Point} that + * @return {fabric.Point} thisArg + */ + 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; + } + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * @class Intersection + * @memberOf fabric + */ + function Intersection(status) { + if (arguments.length > 0) { + this.init(status); + } + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @scope fabric.Intersection.prototype */ { + + /** + * @method init + * @param {String} status + */ + init: function (status) { + this.status = status; + this.points = []; + }, + + /** + * @method appendPoint + * @param {String} status + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * @method appendPoints + * @param {String} status + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * @static + * @method intersectLineLine + */ + fabric.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; + }; + + /** + * @method intersectLinePolygon + */ + fabric.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; + }; + + /** + * @method intersectPolygonPolygon + */ + fabric.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; + }; + + /** + * @method intersectPolygonRectangle + */ + fabric.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; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class Color + * @memberOf fabric + * @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); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @scope fabric.Color.prototype */ { + + /** + * @private + * @method _tryParsingColor + */ + _tryParsingColor: function(color) { + var source = Color.sourceFromHex(color); + if (!source) { + source = Color.sourceFromRgb(color); + } + if (source) { + this.setSource(source); + } + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method getSource + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method setSource + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color represenation in RGB format + * @method toRgb + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color represenation in RGBA format + * @method toRgba + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color represenation in HEX format + * @method toHex + * @return {String} ex: FF5555 + */ + 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(); + }, + + /** + * Gets value of alpha channel for this color + * @method getAlpha + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @method setAlpha + * @param {Number} 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @method toGrayscale + * @return {fabric.Color} thisArg + */ + 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 {fabric.Color} thisArg + */ + 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 {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + 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[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgb(255, 100, 10, 0.5), rgb(1,1,1)) + * @static + * @field + */ + fabric.Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/; + + /** + * Regex matching color in HEX format (ex: #FF5555, 010155, aff) + * @static + * @field + */ + fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; + + /** + * Returns new color object, when given a color in RGB format + * @method fromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @method sourceFromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {Array} source + */ + fabric.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 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @method fromRgba + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @method fromHex + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @method sourceFromHex + * @param {String} color ex: FF5555 + * @return {Array} source + */ + fabric.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 + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @method fromSource + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function () { + + "use strict"; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + removeListener = fabric.util.removeListener, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * @class fabric.StaticCanvas + * @constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.StaticCanvas = function (el, options) { + options || (options = { }); + + this._initStatic(el, options); + fabric.StaticCanvas.activeInstance = this; + }; + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + + extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ { + + /** + * Background color of canvas instance + * @property + * @type String + */ + backgroundColor: 'rgba(0, 0, 0, 0)', + + /** + * Background image of canvas instance + * Should be set via `setBackgroundImage` + * @property + * @type String + */ + backgroundImage: '', + + /** + * Opacity of the background image of the canvas instance + * @property + * @type Float + */ + backgroundImageOpacity: 1.0, + + /** + * Indicatus whether the background image should be stretched to fit the + * dimensions of the canvas instance. + * @property + * @type Boolean + */ + backgroundImageStretch: true, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * @property + * @type Boolean + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @property + * @type Boolean + */ + stateful: true, + + /** + * Indicates whether fabric.Canvas#add should also re-render canvas. + * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once + * (followed by a manual rendering after addition) + */ + renderOnAddition: true, + + /** + * Function that determines clipping of entire canvas area + * Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ + * @property + * @type Function + */ + clipTo: null, + + /** + * Indicates whether object controls (borders/corners) are rendered above overlay image + * @property + * @type Boolean + */ + controlsAboveOverlay: false, + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @method onBeforeScaleRotate + * @param {fabric.Object} target Object that's about to be scaled/rotated + */ + onBeforeScaleRotate: function () { + /* NOOP */ + }, + + /** + * Callback; invoked on every redraw of canvas and is being passed a number indicating current fps + * @method onFpsUpdate + * @param {Number} fps + */ + onFpsUpdate: null, + + _initStatic: function(el, options) { + this._objects = []; + + this._createLowerCanvas(el); + this._initOptions(options); + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); + } + this.calcOffset(); + }, + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @method calcOffset + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets overlay image for this canvas + * @method setOverlayImage + * @param {String} url url of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @return {fabric.Canvas} thisArg + * @chainable + */ + setOverlayImage: function (url, callback) { // TODO (kangax): test callback + fabric.util.loadImage(url, function(img) { + this.overlayImage = img; + callback && callback(); + }, this); + return this; + }, + + /** + * Sets background image for this canvas + * @method setBackgroundImage + * @param {String} url url of an image to set background to + * @param {Function} callback callback to invoke when image is loaded and set as background + * @param {Object} options optional options to set for the background image + * @return {fabric.Canvas} thisArg + * @chainable + */ + setBackgroundImage: function (url, callback, options) { + return fabric.util.loadImage(url, function(img) { + this.backgroundImage = img; + if (options && ('backgroundImageOpacity' in options)) { + this.backgroundImageOpacity = options.backgroundImageOpacity; + } + if (options && ('backgroundImageStretch' in options)) { + this.backgroundImageStretch = options.backgroundImageStretch; + } + callback && callback(); + }, this); + }, + + /** + * @private + * @method _createCanvasElement + * @param {Element} element + */ + _createCanvasElement: function() { + var element = fabric.document.createElement('canvas'); + if (!element.style) { + element.style = { }; + } + if (!element) { + throw CANVAS_INIT_ERROR; + } + this._initCanvasElement(element); + return element; + }, + + _initCanvasElement: function(element) { + if (typeof element.getContext === 'undefined' && + typeof G_vmlCanvasManager !== 'undefined' && + G_vmlCanvasManager.initElement) { + + G_vmlCanvasManager.initElement(element); + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + }, + + /** + * @method _initOptions + * @param {Object} options + */ + _initOptions: function (options) { + for (var prop in options) { + this[prop] = options[prop]; + } + + this.width = parseInt(this.lowerCanvasEl.width, 10) || 0; + this.height = parseInt(this.lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) return; + + this.lowerCanvasEl.style.width = this.width + 'px'; + this.lowerCanvasEl.style.height = this.height + 'px'; + }, + + /** + * Creates a secondary canvas + * @method _createLowerCanvas + */ + _createLowerCanvas: function (canvasEl) { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + this._initCanvasElement(this.lowerCanvasEl); + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width + * @method getWidth + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height + * @method getHeight + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @method setWidth + * @param {Number} width value to set width to + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value) { + return this._setDimension('width', value); + }, + + /** + * Sets height of this canvas instance + * @method setHeight + * @param {Number} height value to set height to + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value) { + return this._setDimension('height', value); + }, + + /** + * Sets dimensions (width, height) of this canvas instance + * @method setDimensions + * @param {Object} dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function(dimensions) { + for (var prop in dimensions) { + this._setDimension(prop, dimensions[prop]); + } + return this; + }, + + /** + * Helper for setting width/height + * @private + * @method _setDimensions + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + this.lowerCanvasEl.style[prop] = value + 'px'; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + this.upperCanvasEl.style[prop] = value + 'px'; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value + 'px'; + } + + this[prop] = value; + + this.calcOffset(); + this.renderAll(); + + return this; + }, + + /** + * Returns <canvas> element corresponding to this instance + * @method getElement + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + // placeholder + getActiveObject: function() { + return null; + }, + + // placeholder + getActiveGroup: function() { + return null; + }, + + /** + * Given a context, renders an object on that context + * @param ctx {Object} context to render object on + * @param object {Object} object to render + * @private + */ + _draw: function (ctx, object) { + if (!object) return; + + if (this.controlsAboveOverlay) { + var hasBorders = object.hasBorders, hasCorners = object.hasCorners; + object.hasBorders = object.hasCorners = false; + object.render(ctx); + object.hasBorders = hasBorders; + object.hasCorners = hasCorners; + } + else { + object.render(ctx); + } + }, + + /** + * Adds objects to canvas, then renders canvas; + * Objects should be instances of (or inherit from) fabric.Object + * @method add + * @return {fabric.Canvas} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + for (var i = arguments.length; i--; ) { + this._initObject(arguments[i]); + } + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * @private + * @method _initObject + */ + _initObject: function(obj) { + this.stateful && obj.setupState(); + obj.setCoords(); + obj.canvas = this; + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * 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 + * @param nonSplicing {Boolean} when `true`, no splicing (shifting) of objects occurs + * @return {fabric.Canvas} instance + */ + insertAt: function (object, index, nonSplicing) { + if (nonSplicing) { + this._objects[index] = object; + } + else { + this._objects.splice(index, 0, object); + } + this._initObject(object); + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * Returns an array of objects this instance has + * @method getObjects + * @return {Array} + */ + getObjects: function () { + return this._objects; + }, + + /** + * Clears specified context of canvas element + * @method clearContext + * @param context {Object} ctx context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @method getContext + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @method clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this._objects.length = 0; + this.clearContext(this.contextContainer); + if (this.contextTop) { + this.clearContext(this.contextTop); + } + this.renderAll(); + return this; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @method renderAll + * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function (allOnTop) { + + var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + + if (this.contextTop) { + this.clearContext(this.contextTop); + } + + if (allOnTop === false || (typeof allOnTop === 'undefined')) { + this.clearContext(canvasToDrawOn); + } + + var activeGroup = this.getActiveGroup(), + startTime = new Date(); + + if (this.clipTo) { + this._clipCanvas(canvasToDrawOn); + } + + canvasToDrawOn.fillStyle = this.backgroundColor; + canvasToDrawOn.fillRect(0, 0, this.width, this.height); + + if (typeof this.backgroundImage === 'object') { + this._drawBackroundImage(canvasToDrawOn); + } + + for (var i = 0, length = this._objects.length; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(canvasToDrawOn, this._objects[i]); + } + } + + if (this.clipTo) { + canvasToDrawOn.restore(); + } + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + this._draw(this.contextTop, activeGroup); + } + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + if (this.controlsAboveOverlay) { + this.drawControls(this.contextContainer); + } + + if (this.onFpsUpdate) { + var elapsedTime = new Date() - startTime; + this.onFpsUpdate(~~(1000 / elapsedTime)); + } + + this.fire('after:render'); + + return this; + }, + + _clipCanvas: function(canvasToDrawOn) { + canvasToDrawOn.save(); + canvasToDrawOn.beginPath(); + this.clipTo(canvasToDrawOn); + canvasToDrawOn.clip(); + }, + + _drawBackroundImage: function(canvasToDrawOn) { + canvasToDrawOn.save(); + canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + + if (this.backgroundImageStretch) { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + } + else { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + } + canvasToDrawOn.restore(); + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @method renderTop + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + this.clearContext(this.contextTop || this.contextContainer); + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(); + } + + // delegate rendering to group selection if one exists + // used for drawing selection borders/corners + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.render(this.contextTop); + } + + this.fire('after:render'); + + return this; + }, + + /** + * Draws objects' controls (borders/corners) + * @method drawControls + * @param {Object} ctx context to render controls on + */ + drawControls: function(ctx) { + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + ctx.save(); + fabric.Group.prototype.transform.call(activeGroup, ctx); + activeGroup.drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + else { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i].active) continue; + + ctx.save(); + fabric.Object.prototype.transform.call(this._objects[i], ctx); + this._objects[i].drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + } + }, + + /** + * Exports canvas element to a dataurl image. + * @method toDataURL + * @param {String} format the format of the output image. Either "jpeg" or "png". + * @param {Number} quality quality level (0..1) + * @return {String} + */ + toDataURL: function (format, quality) { + var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; + + this.renderAll(true); + var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) + ? canvasEl.toDataURL('image/' + format, quality) + : canvasEl.toDataURL('image/' + format); + this.renderAll(); + return data; + }, + + /** + * Exports canvas element to a dataurl image (allowing to change image size via multiplier). + * @method toDataURLWithMultiplier + * @param {String} format (png|jpeg) + * @param {Number} multiplier + * @param {Number} quality (0..1) + * @return {String} + */ + toDataURLWithMultiplier: function (format, multiplier, quality) { + + var origWidth = this.getWidth(), + origHeight = this.getHeight(), + scaledWidth = origWidth * multiplier, + scaledHeight = origHeight * multiplier, + activeObject = this.getActiveObject(), + activeGroup = this.getActiveGroup(); + + this.setWidth(scaledWidth).setHeight(scaledHeight); + this.contextTop.scale(multiplier, multiplier); + + if (activeGroup) { + // not removing group due to complications with restoring it with correct state afterwords + this._tempRemoveBordersCornersFromGroup(activeGroup); + } + else if (activeObject) { + this.deactivateAll(); + } + + // restoring width, height for `renderAll` to draw + // background properly (while context is scaled) + this.width = origWidth; + this.height = origHeight; + + this.renderAll(true); + + var dataURL = this.toDataURL(format, quality); + + this.contextTop.scale(1 / multiplier, 1 / multiplier); + this.setWidth(origWidth).setHeight(origHeight); + + if (activeGroup) { + this._restoreBordersCornersOnGroup(activeGroup); + } + else if (activeObject) { + this.setActiveObject(activeObject); + } + + this.renderAll(); + + return dataURL; + }, + + _tempRemoveBordersCornersFromGroup: function(group) { + group.origHideCorners = group.hideCorners; + group.origBorderColor = group.borderColor; + + group.hideCorners = true; + group.borderColor = 'rgba(0,0,0,0)'; + + group.forEachObject(function(o) { + o.origBorderColor = o.borderColor; + o.borderColor = 'rgba(0,0,0,0)'; + }); + }, + _restoreBordersCornersOnGroup: function(group) { + group.hideCorners = group.origHideCorners; + group.borderColor = group.origBorderColor; + + group.forEachObject(function(o) { + o.borderColor = o.origBorderColor; + delete o.origBorderColor; + }); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @method getCenter + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.getHeight() / 2, + left: this.getWidth() / 2 + }; + }, + + /** + * Centers object horizontally. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + object.set('left', this.getCenter().left); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + object.set('top', this.getCenter().top); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically and horizontally. + * @method centerObject + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function (object) { + return this.centerObjectH(object).centerObjectV(object); + }, + + /** + * Returs dataless JSON representation of canvas + * @method toDatalessJSON + * @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) { + var data = { + objects: this._objects.map(function (instance) { + // TODO (kangax): figure out how to clean this up + var originalValue; + if (!this.includeDefaultValues) { + originalValue = instance.includeDefaultValues; + instance.includeDefaultValues = false; + } + var object = instance[methodName](); + if (!this.includeDefaultValues) { + instance.includeDefaultValues = originalValue; + } + return object; + }, this), + background: this.backgroundColor + }; + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.src; + data.backgroundImageOpacity = this.backgroundImageOpacity; + data.backgroundImageStretch = this.backgroundImageStretch; + } + return data; + }, + + /** + * Returns SVG representation of canvas + * @function + * @method toSVG + * @return {String} + */ + toSVG: function() { + var markup = [ + '', + '', + '', + 'Created with Fabric.js ', fabric.version, '', + fabric.createSVGFontFacesMarkup(this.getObjects()) + ]; + + if (this.backgroundImage) { + markup.push( + '' + ); + } + + for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push(''); + + return markup.join(''); + }, + + /** + * Returns true if canvas contains no objects + * @method isEmpty + * @return {Boolean} true if canvas is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * 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); + if (this.getActiveObject() === object) { + + // removing active object should fire "selection:cleared" events + this.fire('before:selection:cleared', { target: object }); + this.discardActiveObject(); + this.fire('selection:cleared'); + } + this.renderAll(); + return object; + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @param object {fabric.Object} Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + removeFromArray(this._objects, object); + this._objects.unshift(object); + return this.renderAll(); + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + removeFromArray(this._objects, object); + this._objects.push(object); + return this.renderAll(); + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object) { + var idx = this._objects.indexOf(object), + nextIntersectingIdx = idx; + + // if object is not on the bottom of stack + if (idx !== 0) { + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + if (object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(this._objects, object); + this._objects.splice(nextIntersectingIdx, 0, object); + } + return this.renderAll(); + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object) { + var objects = this.getObjects(), + idx = objects.indexOf(object), + nextIntersectingIdx = idx; + + + // if object is not on top of stack (last item in an array) + if (idx !== objects.length-1) { + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1, l = this._objects.length; i < l; ++i) { + if (object.intersectsWithObject(objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(objects, object); + objects.splice(nextIntersectingIdx, 0, object); + } + this.renderAll(); + }, + + /** + * Returns object at specified index + * @method item + * @param {Number} index + * @return {fabric.Object} + */ + item: function (index) { + return this.getObjects()[index]; + }, + + /** + * 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); + }, + + /** + * Iterates over all objects, invoking callback for each one of them + * @method forEachObject + * @return {fabric.Canvas} thisArg + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(), + i = objects.length; + while (i--) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Clears a canvas element and removes all event handlers. + * @method dispose + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + this.clear(); + if (this.interactive) { + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(fabric.window, 'resize', this._onResize); + } + return this; + }, + + /** + * @private + * @method _resizeImageToFit + * @param {HTMLImageElement} imgEl + */ + _resizeImageToFit: function (imgEl) { + + var imageWidth = imgEl.width || imgEl.offsetWidth, + widthScaleFactor = this.getWidth() / imageWidth; + + // scale image down so that it has original dimensions when printed in large resolution + if (imageWidth) { + imgEl.width = imageWidth * widthScaleFactor; + } + } + }); + + /** + * Returns a string representation of an instance + * @method toString + * @return {String} string representation of an instance + */ + fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet + return '#'; + }; + + extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ { + + /** + * @static + * @property EMPTY_JSON + * @type String + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Takes element and transforms its data in such way that it becomes grayscale + * @static + * @method toGrayscale + * @param {HTMLCanvasElement} canvasEl + */ + toGrayscale: function (canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @method supports + * @param methodName {String} Method to check support for; + * Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = fabric.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'; + + case 'toDataURLWithQuality': + try { + el.toDataURL('image/jpeg', 0); + return true; + } + catch (e) { } + return false; + + default: + return null; + } + } + }); + + /** + * Returs JSON representation of canvas + * @function + * @method toJSON + * @return {String} json string + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + +})(); +(function() { + + var extend = fabric.util.object.extend, + getPointer = fabric.util.getPointer, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + 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' + }, + + 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, + + STROKE_OFFSET = 0.5; + + /** + * @class fabric.Canvas + * @constructor + * @extends fabric.StaticCanvas + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.Canvas = function(el, options) { + options || (options = { }); + + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + + fabric.Canvas.activeInstance = this; + }; + + function ProtoProxy(){ } + ProtoProxy.prototype = fabric.StaticCanvas.prototype; + fabric.Canvas.prototype = new ProtoProxy(); + + var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @property + * @type Boolean + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @property + * @type Boolean + */ + selection: true, + + /** + * Color of selection + * @property + * @type String + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @property + * @type String + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @property + * @type Number + */ + selectionLineWidth: 1, + + /** + * Color of the line used in free drawing mode + * @property + * @type String + */ + freeDrawingColor: 'rgb(0, 0, 0)', + + /** + * Width of a line used in free drawing mode + * @property + * @type Number + */ + freeDrawingLineWidth: 1, + + /** + * Default cursor value used when hovering over an object on canvas + * @property + * @type String + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @property + * @type String + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @property + * @type String + */ + defaultCursor: 'default', + + /** + * Cursor value used for rotation point + * @property + * @type String + */ + rotationCursor: 'crosshair', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @property + * @type String + */ + containerClass: 'canvas-container', + + perPixelTargetFind: false, + + targetFindTolerance: 0, + + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._freeDrawingXPoints = [ ]; + this._freeDrawingYPoints = [ ]; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEvents(); + this.calcOffset(); + }, + + /** + * Adds mouse listeners to canvas + * @method _initEvents + * @private + * See configuration documentation for more details. + */ + _initEvents: function () { + var _this = this; + + this._onMouseDown = function (e) { + _this.__onMouseDown(e); + + addListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); + + addListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); + + removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseUp = function (e) { + _this.__onMouseUp(e); + + removeListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); + + removeListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); + + addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseMove = function (e) { + e.preventDefault && e.preventDefault(); + _this.__onMouseMove(e); + }; + + this._onResize = function () { + _this.calcOffset(); + }; + + + addListener(fabric.window, 'resize', this._onResize); + + if (fabric.isTouchSupported) { + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + } + else { + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + } + }, + + /** + * 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) { + + var target; + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._finalizeDrawingPath(); + this.fire('mouse:up', { e: e }); + return; + } + + if (this._currentTransform) { + + var transform = this._currentTransform; + + target = transform.target; + if (target._scaling) { + target._scaling = false; + } + + // determine the new coords everytime the image changes its position + var i = this._objects.length; + while (i--) { + this._objects[i].setCoords(); + } + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + target.isMoving = false; + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + } + + this._currentTransform = null; + + if (this._groupSelector) { + // group selection was completed, determine its bounds + this._findSelectedObjects(e); + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords(); + activeGroup.set('isMoving', false); + this._setCursor(this.defaultCursor); + } + + // clear selection + this._groupSelector = null; + this.renderAll(); + + this._setCursorFromEvent(e, target); + + // fix for FF + this._setCursor(''); + + var _this = this; + setTimeout(function () { + _this._setCursorFromEvent(e, target); + }, 50); + + this.fire('mouse:up', { target: target, e: e }); + target && target.fire('mouseup', { e: e }); + }, + + /** + * 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) { + + // accept only left clicks + var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1; + if (!isLeftClick && !fabric.isTouchSupported) return; + + if (this.isDrawingMode) { + this._prepareForDrawing(e); + + // capture coordinates immediately; this allows to draw dots (when movement never occurs) + this._captureDrawingPath(e); + this.fire('mouse:down', { e: e }); + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) return; + + var target = this.findTarget(e), + pointer = this.getPointer(e), + activeGroup = this.getActiveGroup(), + corner; + + if (this._shouldClearSelection(e)) { + + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + + this.deactivateAllWithDispatch(); + } + else { + // determine if it's a drag or rotate case + // rotate and scale will happen at the same time + this.stateful && target.saveState(); + + if ((corner = target._findTargetCorner(e, this._offset))) { + this.onBeforeScaleRotate(target); + } + + this._setupCurrentTransform(e, target); + + var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()) && this.selection; + if (shouldHandleGroupLogic) { + this._handleGroupLogic(e, target); + } + else { + if (target !== this.getActiveGroup()) { + this.deactivateAll(); + } + this.setActiveObject(target, e); + } + } + // we must renderAll so that active image is placed on the top canvas + this.renderAll(); + + this.fire('mouse:down', { target: target, e: e }); + target && target.fire('mousedown', { e: e }); + }, + + /** + * 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) { + + var target; + + if (this.isDrawingMode) { + if (this._isCurrentlyDrawing) { + this._captureDrawingPath(e); + } + this.fire('mouse:move', { e: e }); + return; + } + + var groupSelector = this._groupSelector, pointer; + + // We initially clicked in an empty area, so we draw a box for multiple selection. + if (groupSelector !== null) { + pointer = getPointer(e); + + groupSelector.left = pointer.x - this._offset.left - groupSelector.ex; + groupSelector.top = pointer.y - this._offset.top - groupSelector.ey; + this.renderTop(); + } + else if (!this._currentTransform) { + + // alias style to elimintate unnecessary lookup + var style = this.upperCanvasEl.style; + + // Here we are hovering the canvas then we will determine + // what part of the pictures we are hovering to change the caret symbol. + // We won't do that while dragging or rotating in order to improve the + // performance. + target = this.findTarget(e); + + if (!target) { + // image/text was hovered-out from, we remove its borders + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && !this._objects[i].active) { + this._objects[i].setActive(false); + } + } + style.cursor = this.defaultCursor; + } + else { + // set proper cursor + this._setCursorFromEvent(e, target); + if (target.isActive()) { + // display corners when hovering over an image + target.setCornersVisibility && target.setCornersVisibility(true); + } + } + } + else { + // object is being transformed (scaled/rotated/moved/etc.) + pointer = getPointer(e); + + var x = pointer.x, + y = pointer.y; + + this._currentTransform.target.isMoving = true; + + if (this._currentTransform.action === 'rotate') { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + + if (!e.shiftKey) { + this._rotateObject(x, y); + + this.fire('object:rotating', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('rotating'); + } + if (!this._currentTransform.target.hasRotatingPoint) { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + } + else if (this._currentTransform.action === 'scale') { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleX') { + this._scaleObject(x, y, 'x'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleY') { + this._scaleObject(x, y, 'y'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else { + this._translateObject(x, y); + + this.fire('object:moving', { + target: this._currentTransform.target + }); + + this._setCursor(this.moveCursor); + + this._currentTransform.target.fire('moving'); + } + // only commit here. when we are actually moving the pictures + this.renderAll(); + } + this.fire('mouse:move', { target: target, e: e }); + target && target.fire('mousemove', { e: e }); + }, + + /** + * Applies one implementation of 'point inside polygon' algorithm + * @method containsPoint + * @param e { Event } event object + * @param target { fabric.Object } object to test against + * @return {Boolean} true if point contains within area of given object + */ + containsPoint: function (e, target) { + var pointer = this.getPointer(e), + xy = this._normalizePointer(target, pointer), + x = xy.x, + y = xy.y; + + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + + // we iterate through each object. If target found, return it. + var iLines = target._getImageLines(target.oCoords), + xpoints = target._findCrossPoints(x, y, iLines); + + // if xcount is odd then we clicked inside the object + // For the specific case of square images xcount === 1 in all true cases + if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { + return true; + } + return false; + }, + + /** + * @private + * @method _normalizePointer + */ + _normalizePointer: function (object, pointer) { + + var activeGroup = this.getActiveGroup(), + x = pointer.x, + y = pointer.y; + + var isObjectInGroup = ( + activeGroup && + object.type !== 'group' && + activeGroup.contains(object) + ); + + if (isObjectInGroup) { + x -= activeGroup.left; + y -= activeGroup.top; + } + return { x: x, y: y }; + }, + + _isTargetTransparent: function (target, x, y) { + var cacheContext = this.contextCache; + + var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; + target.hasBorders = target.transparentCorners = false; + + this._draw(cacheContext, target); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + + // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 + if (this.targetFindTolerance > 0) { + if (x > this.targetFindTolerance) { + x -= this.targetFindTolerance; + } + else { + x = 0; + } + if (y > this.targetFindTolerance) { + y -= this.targetFindTolerance; + } + else { + y = 0; + } + } + + var isTransparent = true; + var imageData = cacheContext.getImageData( + x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3; i < imageData.data.length; i += 4) { + var temp = imageData.data[i]; + isTransparent = temp <= 0; + if (isTransparent === false) break; //Stop if colour found + } + + imageData = null; + this.clearContext(cacheContext); + return isTransparent; + }, + + /** + * @private + * @method _shouldClearSelection + */ + _shouldClearSelection: function (e) { + var target = this.findTarget(e), + activeGroup = this.getActiveGroup(); + return ( + !target || ( + target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey + ) + ); + }, + + /** + * @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' + : corner === 'mtr' + ? 'rotate' + : (target.hasRotatingPoint) + ? 'scale' + : '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 === this.getActiveGroup()) { + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + if (activeGroup.contains(target)) { + activeGroup.removeWithUpdate(target); + target.setActive(false); + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(); + } + } + else { + activeGroup.addWithUpdate(target); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.setActive(true); + } + else { + // group does not exist + if (this._activeObject) { + // only if there's an active object + if (target !== this._activeObject) { + // and that object is not the actual target + var group = new fabric.Group([ this._activeObject, target ]); + this.setActiveGroup(group); + activeGroup = this.getActiveGroup(); + } + } + // activate target object in any case + target.setActive(true); + } + + if (activeGroup) { + activeGroup.saveCoords(); + } + }, + + /** + * @private + * @method _prepareForDrawing + */ + _prepareForDrawing: function(e) { + + this._isCurrentlyDrawing = true; + + this.discardActiveObject().renderAll(); + + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.beginPath(); + this.contextTop.moveTo(pointer.x, pointer.y); + this.contextTop.strokeStyle = this.freeDrawingColor; + this.contextTop.lineWidth = this.freeDrawingLineWidth; + this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; + }, + + /** + * @private + * @method _captureDrawingPath + */ + _captureDrawingPath: function(e) { + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.lineTo(pointer.x, pointer.y); + this.contextTop.stroke(); + }, + + /** + * @private + * @method _finalizeDrawingPath + */ + _finalizeDrawingPath: function() { + + this.contextTop.closePath(); + + this._isCurrentlyDrawing = false; + + var minX = utilMin(this._freeDrawingXPoints), + minY = utilMin(this._freeDrawingYPoints), + maxX = utilMax(this._freeDrawingXPoints), + maxY = utilMax(this._freeDrawingYPoints), + path = [ ], + xPoints = this._freeDrawingXPoints, + yPoints = this._freeDrawingYPoints; + + path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' '); + + for (var i = 1, len = xPoints.length; i < len; i++) { + path.push('L ', xPoints[i] - minX, ' ', yPoints[i] - minY, ' '); + } + + // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, + // and instead fire something like "drawing:completed" event with path string + + path = path.join(''); + + if (path === "M 0 0 L 0 0 ") { + // do not create 0 width/height paths, as they are rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing + this.renderAll(); + return; + } + + var p = new fabric.Path(path); + + 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(); + this.fire('path:created', { path: p }); + }, + + /** + * 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.lockMovementX || target.set('left', x - this._currentTransform.offsetX); + target.lockMovementY || 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.lockScalingX && target.lockScalingY) 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.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); + } + else if (by === 'x' && !target.lockUniScaling) { + target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + } + else if (by === 'y' && !target.lockUniScaling) { + target.lockScalingY || 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._theta = (curAngle - lastAngle) + t.theta; + }, + + /** + * @method _setCursor + */ + _setCursor: function (value) { + this.upperCanvasEl.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.upperCanvasEl.style; + if (!target) { + s.cursor = this.defaultCursor; + return false; + } + else { + var activeGroup = this.getActiveGroup(); + // only show proper corner when group selection is not active + var corner = !!target._findTargetCorner + && (!activeGroup || !activeGroup.contains(target)) + && target._findTargetCorner(e, this._offset); + + if (!corner) { + s.cursor = this.hoverCursor; + } + else { + if (corner in cursorMap) { + s.cursor = cursorMap[corner]; + } else if (corner === 'mtr' && target.hasRotatingPoint) { + s.cursor = this.rotationCursor; + } else { + s.cursor = this.defaultCursor; + return false; + } + } + } + return true; + }, + + /** + * @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 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) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { + + if (this.selection && currentObject.selectable) { + currentObject.setActive(true); + group.push(currentObject); + } + } + } + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + group = new fabric.Group(group); + this.setActiveGroup(group); + group.saveCoords(); + this.fire('selection:created', { target: group }); + } + + this.renderAll(); + }, + + /** + * Method that determines what object we are clicking on + * @method findTarget + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through + */ + findTarget: function (e, skipGroup) { + + var target, + pointer = this.getPointer(e); + + // first check current group (if one exists) + var activeGroup = this.getActiveGroup(); + + if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { + target = activeGroup; + return target; + } + + // then check all of the objects on canvas + // Cache all targets where their bounding box contains point. + var possibleTargets = []; + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && this.containsPoint(e, this._objects[i])) { + if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { + possibleTargets[possibleTargets.length] = this._objects[i]; + } + else { + target = this._objects[i]; + this.relatedTarget = target; + break; + } + } + } + for (var j = 0, len = possibleTargets.length; j < len; j++) { + pointer = this.getPointer(e); + var isTransparent = this._isTargetTransparent(possibleTargets[j], pointer.x, pointer.y); + if (!isTransparent) { + target = possibleTargets[j]; + this.relatedTarget = target; + break; + } + } + if (target && target.selectable) { + return target; + } + }, + + /** + * Returns pointer coordinates relative to canvas. + * @method getPointer + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e) { + var pointer = getPointer(e); + return { + x: pointer.x - this._offset.left, + y: pointer.y - this._offset.top + }; + }, + + /** + * @method _createUpperCanvas + * @param {HTMLElement|String} canvasEl Canvas element + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + this.upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl.className = 'upper-canvas'; + + this.wrapperEl.appendChild(this.upperCanvasEl); + + this._applyCanvasStyle(this.upperCanvasEl); + this.contextTop = this.upperCanvasEl.getContext('2d'); + }, + + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + * @method _initWrapperElement + * @param {Number} width + * @param {Number} height + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.getWidth() + 'px', + height: this.getHeight() + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @method _applyCanvasStyle + * @param {Element} element + */ + _applyCanvasStyle: function (element) { + var width = this.getWidth() || element.width, + height = this.getHeight() || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0 + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Returns context of canvas where object selection is drawn + * @method getSelectionContext + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @method getSelectionElement + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Sets given object as active + * @method setActiveObject + * @param object {fabric.Object} Object to set as an active one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = object; + object.setActive(true); + + this.renderAll(); + + this.fire('object:selected', { target: object, e: e }); + object.fire('selected', { e: e }); + return this; + }, + + /** + * Returns currently active object + * @method getActiveObject + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Discards currently active object + * @method discardActiveObject + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function () { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = null; + return this; + }, + + /** + * Sets active group to a speicified one + * @method setActiveGroup + * @param {fabric.Group} group Group to set as a current one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveGroup: function (group) { + this._activeGroup = group; + group && group.setActive(true); + return this; + }, + + /** + * Returns currently active group + * @method getActiveGroup + * @return {fabric.Group} Current group + */ + getActiveGroup: function () { + return this._activeGroup; + }, + + /** + * Removes currently active group + * @method discardActiveGroup + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function () { + var g = this.getActiveGroup(); + if (g) { + g.destroy(); + } + return this.setActiveGroup(null); + }, + + /** + * Deactivates all objects by calling their setActive(false) + * @method deactivateAll + * @return {fabric.Canvas} thisArg + */ + deactivateAll: function () { + var allObjects = this.getObjects(), + i = 0, + len = allObjects.length; + for ( ; i < len; i++) { + allObjects[i].setActive(false); + } + this.discardActiveGroup(); + this.discardActiveObject(); + return this; + }, + + /** + * Deactivates all objects and dispatches appropriate events + * @method deactivateAllWithDispatch + * @return {fabric.Canvas} thisArg + */ + deactivateAllWithDispatch: function () { + var activeObject = this.getActiveGroup() || this.getActiveObject(); + if (activeObject) { + this.fire('before:selection:cleared', { target: activeObject }); + } + this.deactivateAll(); + if (activeObject) { + this.fire('selection:cleared'); + } + return this; + } + }; + + fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; + extend(fabric.Canvas.prototype, InteractiveMethods); + + // iterating manually to workaround Opera's bug + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } + + if (fabric.isTouchSupported) { + fabric.Canvas.prototype._setCursorFromEvent = function() { }; + } + + /** + * @class fabric.Element + * @alias fabric.Canvas + * @deprecated + * @constructor + */ + fabric.Element = fabric.Canvas; +})(); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @method fxCenterObjectH + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} 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 with animation. + * @method fxCenterObjectV + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} 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; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @method fxRemove + * @param {fabric.Object} object Object to remove + * @param {Function} callback Callback, invoked on effect completion + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.get('opacity'), + endValue: 0, + duration: this.FX_DURATION, + onStart: function() { + object.setActive(false); + }, + onChange: function(value) { + object.set('opacity', value); + _this.renderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Populates canvas with data from the specified dataless JSON + * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` + * @method loadFromDatalessJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} instance + * @chainable + */ + loadFromDatalessJSON: function (json, callback) { + + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : json; + + if (!serialized || (serialized && !serialized.objects)) return; + + this.clear(); + + // TODO: test this + this.backgroundColor = serialized.background; + this._enlivenDatalessObjects(serialized.objects, callback); + }, + + /** + * @method _enlivenDatalessObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenDatalessObjects: function (objects, callback) { + + /** @ignore */ + function onObjectLoaded(object, index) { + _this.insertAt(object, index, true); + object.setCoords(); + if (++numLoadedObjects === numTotalObjects) { + callback && callback(); + } + } + + /** @ignore */ + function loadObject(obj, index) { + + var pathProp = obj.paths ? 'paths' : 'path'; + var path = obj[pathProp]; + + delete obj[pathProp]; + + if (typeof path !== 'string') { + if (obj.type === 'image') { + fabric[fabric.util.string.capitalize(obj.type)].fromObject(obj, function (o) { + onObjectLoaded(o, index); + }); + } + else { + var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(obj.type))]; + if (!klass || !klass.fromObject) return; + + // restore path + if (path) { + obj[pathProp] = path; + } + onObjectLoaded(klass.fromObject(obj), index); + } + } + else { + if (obj.type === 'image') { + fabric.util.loadImage(path, function (image) { + var oImg = new fabric.Image(image); + + oImg.setSourcePath(path); + + fabric.util.object.extend(oImg, obj); + oImg.setAngle(obj.angle); + + onObjectLoaded(oImg, index); + }); + } + else if (obj.type === 'text') { + + if (obj.useNative) { + onObjectLoaded(fabric.Text.fromObject(obj), index); + } + else { + obj.path = path; + var object = fabric.Text.fromObject(obj); + var onscriptload = function () { + // TODO (kangax): find out why Opera refuses to work without this timeout + if (Object.prototype.toString.call(fabric.window.opera) === '[object Opera]') { + setTimeout(function () { + onObjectLoaded(object, index); + }, 500); + } + else { + onObjectLoaded(object, index); + } + }; + + fabric.util.getScript(path, onscriptload); + } + } + else { + fabric.loadSVGFromURL(path, function (elements) { + var object = fabric.util.groupSVGElements(elements, obj, path); + + // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) + // skip this step if an object is a PathGroup, since we already passed it options object before + if (!(object instanceof fabric.PathGroup)) { + fabric.util.object.extend(object, obj); + if (typeof obj.angle !== 'undefined') { + object.setAngle(obj.angle); + } + } + + onObjectLoaded(object, index); + }); + } + } + } + + var _this = this, + numLoadedObjects = 0, + numTotalObjects = objects.length; + + if (numTotalObjects === 0 && callback) { + callback(); + } + + try { + objects.forEach(loadObject, this); + } + catch(e) { + fabric.log(e.message); + } + }, + + /** + * Populates canvas with data from the specified JSON + * JSON format must conform to the one of `fabric.Canvas#toJSON` + * @method loadFromJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} 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 (serialized.backgroundImage) { + _this.setBackgroundImage(serialized.backgroundImage, function() { + + _this.backgroundImageOpacity = serialized.backgroundImageOpacity; + _this.backgroundImageStretch = serialized.backgroundImageStretch; + + _this.renderAll(); + + callback && callback(); + }); + } + else { + callback && callback(); + } + }); + + return this; + }, + + /** + * @method _enlivenObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenObjects: function (objects, callback) { + var _this = this; + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + enlivenedObjects.forEach(function(obj, index) { + _this.insertAt(obj, index, true); + }); + callback && callback(); + }); + }, + + /** + * @private + * @method _toDataURL + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @method _toDataURLWithMultiplier + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @method clone + * @param {Object} [callback] Receives cloned instance as a first argument + */ + clone: function (callback) { + var data = JSON.stringify(this); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @method cloneWithoutData + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.document.createElement('canvas'); + + el.width = this.getWidth(); + el.height = this.getHeight(); + + var clone = new fabric.Canvas(el); + clone.clipTo = this.clipTo; + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + slice = Array.prototype.slice; + + if (fabric.Object) { + return; + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc) + * @property + * @type String + */ + type: 'object', + + /** + * @property + * @type Number + */ + top: 0, + + /** + * @property + * @type Number + */ + left: 0, + + /** + * @property + * @type Number + */ + width: 0, + + /** + * @property + * @type Number + */ + height: 0, + + /** + * @property + * @type Number + */ + scaleX: 1, + + /** + * @property + * @type Number + */ + scaleY: 1, + + /** + * @property + * @type Boolean + */ + flipX: false, + + /** + * @property + * @type Boolean + */ + flipY: false, + + /** + * @property + * @type Number + */ + opacity: 1, + + /** + * @property + * @type Number + */ + angle: 0, + + /** + * @property + * @type Number + */ + cornersize: 12, + + /** + * @property + * @type Boolean + */ + transparentCorners: true, + + /** + * @property + * @type Number + */ + padding: 0, + + /** + * @property + * @type String + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * @property + * @type String + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * @property + * @type String + */ + fill: 'rgb(0,0,0)', + + /** + * @property + * @type String + */ + fillRule: 'source-over', + + /** + * @property + * @type String + */ + overlayFill: null, + + /** + * @property + * @type String + */ + stroke: null, + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type Array + */ + strokeDashArray: null, + + /** + * @property + * @type Number + */ + borderOpacityWhenMoving: 0.4, + + /** + * @property + * @type Number + */ + borderScaleFactor: 1, + + /** + * Transform matrix + * @property + * @type Array + */ + transformMatrix: null, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) + * @property + * @type Boolean + */ + selectable: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @property + * @type Boolean + */ + hasControls: true, + + /** + * When set to `false`, object's borders are not rendered + * @property + * @type Boolean + */ + hasBorders: true, + + /** + * When set to `false`, object's rotating point will not be visible or selectable + * @property + * @type Boolean + */ + hasRotatingPoint: false, + + /** + * Offset for object's rotating point (when enabled) + * @property + * @type Number + */ + rotatingPointOffset: 40, + + /** + * @private + * @property + * @type Number + */ + _theta: 0, + + perPixelTargetFind: false, + + includeDefaultValues: true, + + /** + * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); + * as well as for history (undo/redo) purposes + * @property + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY ' + + 'theta angle opacity cornersize fill overlayFill ' + + 'stroke strokeWidth strokeDashArray fillRule ' + + 'borderScaleFactor transformMatrix selectable' + ).split(' '), + + /** + * @method callSuper + * @param {String} methodName + */ + callSuper: function(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + }, + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * @method initGradient + */ + _initGradient: function(options) { + if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @method setOptions + * @param {Object} [options] + */ + setOptions: function(options) { + var i = this.stateProperties.length, prop; + while (i--) { + prop = this.stateProperties[i]; + if (prop in options) { + this.set(prop, options[prop]); + } + } + this._initGradient(options); + }, + + /** + * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + overlayFill: this.overlayFill, + stroke: this.stroke, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray, + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + selectable: this.selectable, + hasControls: this.hasControls, + hasBorders: this.hasBorders, + hasRotatingPoint: this.hasRotatingPoint, + transparentCorners: this.transparentCorners, + perPixelTargetFind: this.perPixelTargetFind + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @method toDatalessObject + */ + toDatalessObject: function() { + // will be overwritten by subclasses + return this.toObject(); + }, + + /** + * Returns styles-string for svg-export + * @method getSvgStyles + * @return {string} + */ + getSvgStyles: function() { + return [ + "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", + "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), + "fill: ", (this.fill ? this.fill : 'none'), "; ", + "opacity: ", (this.opacity ? this.opacity : '1'), ";" + ].join(""); + }, + + /** + * Returns transform-string for svg-export + * @method getSvgTransform + * @return {string} + */ + getSvgTransform: function() { + var angle = this.getAngle(); + return [ + "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", + angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', + (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") + ].join(''); + }, + + /** + * @private + * @method _removeDefaultValues + */ + _removeDefaultValues: function(object) { + var defaultOptions = fabric.Object.prototype.options; + if (defaultOptions) { + 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 "#"; + }, + + /** + * Sets property to a given value + * @method set + * @param {String} name + * @param {Object|Function} value + * @return {fabric.Group} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && + value < fabric.Object.MIN_SCALE_LIMIT; + + if (shouldConstrainValue) { + value = fabric.Object.MIN_SCALE_LIMIT; + } + if (key === 'angle') { + this.setAngle(value); + } + else { + this[key] = value; + } + }, + + /** + * 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) { + + // do not render if width or height are zeros + if (this.width === 0 || this.height === 0) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + if (!noTransform) { + this.transform(ctx); + } + + if (this.stroke || this.strokeDashArray) { + ctx.lineWidth = this.strokeWidth; + ctx.strokeStyle = this.stroke; + } + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : this.fill; + } + + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._render(ctx, noTransform); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + 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; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @method scaleToWidth + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @method scaleToHeight + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * 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() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; + this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + + this._hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + this._angle = Math.atan(this.currentHeight / this.currentWidth); + + // offset added for rotate and scale actions + 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) + }; + var mtr = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + // clockwise + this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords(); + + return this; + }, + + /** + * Returns width of an object's bounding rectangle + * @method getBoundingRectWidth + * @return {Number} width value + */ + getBoundingRectWidth: function() { + this.oCoords || this.setCoords(); + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + return Math.abs(minX - maxX); + }, + + /** + * Returns height of an object's bounding rectangle + * @method getBoundingRectHeight + * @return {Number} height value + */ + getBoundingRectHeight: function() { + this.oCoords || this.setCoords(); + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + return Math.abs(minY - maxY); + }, + + /** + * 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) { + if (!this.hasBorders) return; + + var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, + padding = this.padding, + padding2 = padding * 2, + strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), + scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX), + ~~(h + padding2 + strokeWidth * this.scaleY) + ); + + if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + _renderDashedStroke: function(ctx) { + + if (1 & this.strokeDashArray.length /* if odd number of items */) { + /* duplicate items */ + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + var i = 0, + x = -this.width/2, y = -this.height/2, + _this = this, + padding = this.padding, + dashedArrayLength = this.strokeDashArray.length; + + ctx.save(); + ctx.beginPath(); + + function renderSide(xMultiplier, yMultiplier) { + + var lineLength = 0, + lengthDiff = 0, + sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; + + while (lineLength < sideLength) { + + var lengthOfSubPath = _this.strokeDashArray[i++]; + lineLength += lengthOfSubPath; + + if (lineLength > sideLength) { + lengthDiff = lineLength - sideLength; + } + + // track coords + if (xMultiplier) { + x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); + } + else { + y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); + } + + ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); + if (i >= dashedArrayLength) { + i = 0; + } + } + } + + renderSide(1, 0); + renderSide(0, 1); + renderSide(-1, 0); + renderSide(0, -1); + + ctx.stroke(); + ctx.closePath(); + ctx.restore(); + }, + + /** + * 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) { + if (!this.hasControls) return; + + var size = this.cornersize, + size2 = size / 2, + strokeWidth2 = this.strokeWidth / 2, + left = -(this.width / 2), + top = -(this.height / 2), + _left, + _top, + sizeX = size / this.scaleX, + sizeY = size / this.scaleY, + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // top-right + _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + if (!this.lockUniScaling) { + // middle-top + _left = left + width/2 - scaleOffsetX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-bottom + _left = left + width/2 - scaleOffsetX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + + _left = left + width/2 - scaleOffsetX; + + _top = this.flipY ? + (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_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(); + + /** @ignore */ + 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') + }; + + // normalize angle + this.set('angle', 0).set('flipX', false).set('flipY', false); + this.toDataURL(function(dataURL) { + i.src = dataURL; + }); + } + return this; + }, + + /** + * Converts an object into a data-url-like string + * @method toDataURL + * @return {String} string of data + */ + toDataURL: function(callback) { + var el = fabric.document.createElement('canvas'); + if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(el); + } + + el.width = this.getBoundingRectWidth(); + el.height = this.getBoundingRectHeight(); + + fabric.util.wrapElement(el, 'div'); + + var canvas = new fabric.Canvas(el); + canvas.backgroundColor = 'transparent'; + canvas.renderAll(); + + if (this.constructor.async) { + this.clone(proceed); + } + else { + proceed(this.clone()); + } + + function proceed(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; + + callback && callback(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; + }, + + /** + * @method setupState + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + }, + + /** + * 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) { + // extracts coords + 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 of another object + * @method isContainedWithinObject + * @param {Object} other Object to test + * @return {Boolean} + */ + isContainedWithinObject: function(other) { + return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); + }, + + /** + * 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); + + 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) { + if (!this.hasControls || !this.active) return false; + + var pointer = getPointer(e), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xpoints, + lines; + + for (var i in this.oCoords) { + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner, i); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + 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]; + // optimisation 1: line below dot. no cross + if ((iLine.o.y < ey) && (iLine.d.y < ey)) { + continue; + } + // optimisation 2: line above dot. no cross + if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { + xi = iLine.o.x; + yi = ey; + } + // calculate the intersection point + 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; + } + // dont count xi < ex cases + if (xi >= ex) { + xcount += 1; + } + // optimisation 4: specific for square images + 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) { + 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 = degreesToRadians(45 - this.getAngle()), + cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, + cosHalfOffset = cornerHypotenuse * Math.cos(theta), + sinHalfOffset = cornerHypotenuse * Math.sin(theta), + sinTh = Math.sin(this._theta), + cosTh = Math.cos(this._theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + + /** + * 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; + }, + + /** + * Returns a JSON representation of an instance + * @method toJSON + * @return {String} json + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + }, + + /** + * @method setGradientFill + */ + setGradientFill: function(options) { + this.set('fill', fabric.Gradient.forObject(this, options)); + }, + + /** + * @method animate + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + for (var prop in arguments[0]) { + this._animate(prop, arguments[0][prop], arguments[1]); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @method _animate + */ + _animate: function(property, to, options) { + var obj = this; + + options || (options = { }); + + if (!('from' in options)) { + options.from = this.get(property); + } + + if (/[+\-]/.test((to + '').charAt(0))) { + to = this.get(property) + parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + onChange: function(value) { + obj.set(property, value); + options.onChange && options.onChange(); + }, + onComplete: function() { + obj.setCoords(); + options.onComplete && options.onComplete(); + } + }); + }, + + /** + * Centers object horizontally on canvas to which it was added last + * @method centerH + * @return {fabric.Object} thisArg + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last + * @method centerV + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * @method center + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + return this.centerH().centerV(); + }, + + /** + * Removes object from canvas to which it was added last + * @method remove + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + return this.canvas.remove(this); + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + this.canvas.sendToBack(this); + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + this.canvas.bringToFront(this); + return this; + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function() { + this.canvas.sendBackwards(this); + return this; + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function() { + this.canvas.bringForward(this); + return this; + } + }); + + /** + * @alias rotate -> setAngle + */ + fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + + var proto = fabric.Object.prototype; + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + + extend(fabric.Object.prototype, fabric.Observable); + + extend(fabric.Object, { + + /** + * @static + * @constant + * @type Number + */ + NUM_FRACTION_DIGITS: 2, + + /** + * @static + * @constant + * @type Number + */ + MIN_SCALE_LIMIT: 0.1 + + }); + +})(typeof exports !== 'undefined' ? exports : this); + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * @class Line + * @extends fabric.Object + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @scope fabric.Line.prototype */ { + + /** + * @property + * @type String + */ + type: 'line', + + /** + * Constructor + * @method initialize + * @param {Array} points Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} 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._setWidthHeight(options); + }, + + /** + * @private + * @method _setWidthHeight + * @param {Object} options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.set('width', (this.x2 - this.x1) || 1); + this.set('height', (this.y2 - this.y1) || 1); + + this.set('left', 'left' in options ? options.left : (this.x1 + this.width / 2)); + this.set('top', 'top' in options ? options.top : (this.y1 + this.height / 2)); + }, + + /** + * @private + * @method _set + * @param {String} key + * @param {Any} value + */ + _set: function(key, value) { + this[key] = value; + if (key in coordProps) { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + if (this.group) { + ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); + } + + // move from center (of virtual box) to its left/top corner + ctx.moveTo(this.width === 1 ? 0 : (-this.width / 2), this.height === 1 ? 0 : (-this.height / 2)); + ctx.lineTo(this.width === 1 ? 0 : (this.width / 2), this.height === 1 ? 0 : (this.height / 2)); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = ctx.fillStyle; + ctx.stroke(); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '' + ].join(''); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Line.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @method fabric.Line.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromElement = function(element, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); + var points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + return new fabric.Line(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Line instance from an object representation + * @static + * @method fabric.Line.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Line} 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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * @class Circle + * @extends fabric.Object + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @scope fabric.Circle.prototype */ { + + /** + * @property + * @type String + */ + type: 'circle', + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + * @return {fabric.Circle} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.set('radius', options.radius || 0); + this.callSuper('initialize', options); + + var diameter = this.get('radius') * 2; + this.set('width', diameter).set('height', diameter); + }, + + /** + * 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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return (''); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) + ctx.globalAlpha *= this.opacity; + 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 horizontal radius of an object (according to how an object is scaled) + * @method getRadiusX + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @method getRadiusY + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @method setRadius + * @return {Number} + */ + setRadius: function(value) { + this.radius = value; + this.set('width', value * 2).set('height', value * 2); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @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 new 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)); + } + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * @class Triangle + * @extends fabric.Object + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @scope fabric.Triangle.prototype */ { + + /** + * @property + * @type String + */ + type: 'triangle', + + /** + * Constructor + * @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; + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + var points = [ + -widthBy2 + " " + heightBy2, + "0 " + -heightBy2, + widthBy2 + " " + heightBy2 + ].join(","); + + return ''; + } + }); + + /** + * Returns fabric.Triangle instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * @class Ellipse + * @extends fabric.Object + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @scope fabric.Ellipse.prototype */ { + + /** + * @property + * @type String + */ + type: 'ellipse', + + /** + * Constructor + * @method initialize + * @param {Object} [options] 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') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '' + ].join(''); + }, + + /** + * Renders this instance on a given context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + * @param noTransform {Boolean} context is not transformed when set to true + */ + render: function(ctx, noTransform) { + // do not use `get` for perf. reasons + 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.globalAlpha *= this.opacity; + if (this.transformMatrix && this.group) { + ctx.translate(this.cx, this.cy); + } + 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); + if (this.stroke) { + ctx.stroke(); + } + if (this.fill) { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @method fabric.Ellipse.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + var cx = parsedAttributes.left; + var cy = parsedAttributes.top; + + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + + var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); + + ellipse.cx = cx || 0; + ellipse.cy = cy || 0; + + return ellipse; + }; + + /** + * Returns fabric.Ellipse instance from an object representation + * @static + * @method fabric.Ellipse.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object) { + return new fabric.Ellipse(object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Rect) { + console.warn('fabric.Rect is already defined'); + return; + } + + /** + * @class Rect + * @extends fabric.Object + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @scope fabric.Rect.prototype */ { + + /** + * @property + * @type String + */ + type: 'rect', + + /** + * @property + * @type Number + */ + rx: 0, + + /** + * @property + * @type Number + */ + ry: 0, + + /** + * Constructor + * @method initialize + * @param options {Object} options object + * @return {Object} thisArg + */ + initialize: function(options) { + this._initStateProperties(); + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Rect` -specific ones to it + * (such as "rx", "ry", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(['rx', 'ry']); + }, + + /** + * @private + * @method _initRxRy + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var rx = this.rx || 0, + ry = this.ry || 0, + x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.beginPath(); + ctx.globalAlpha *= this.opacity; + + if (this.transformMatrix && this.group) { + ctx.translate( + this.width / 2 + this.x, + this.height / 2 + this.y); + } + if (!this.transformMatrix && this.group) { + ctx.translate( + -this.group.width / 2 + this.width / 2 + this.x, + -this.group.height / 2 + this.height / 2 + this.y); + } + + ctx.moveTo(x+rx, y); + ctx.lineTo(x+w-rx, y); + ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry); + ctx.lineTo(x+w, y+h-ry); + ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h); + ctx.lineTo(x+rx,y+h); + ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry); + ctx.lineTo(x,y+ry); + ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y); + ctx.closePath(); + + if (this.fill) { + ctx.fill(); + } + + if (this.strokeDashArray) { + this._renderDashedStroke(ctx); + } + else if (this.stroke) { + ctx.stroke(); + } + }, + + // since our coordinate system differs from that of SVG + _normalizeLeftTopProperties: function(parsedAttributes) { + if (parsedAttributes.left) { + this.set('left', parsedAttributes.left + this.getWidth() / 2); + } + this.set('x', parsedAttributes.left || 0); + if (parsedAttributes.top) { + this.set('top', parsedAttributes.top + this.getHeight() / 2); + } + this.set('y', parsedAttributes.top || 0); + return this; + }, + + /** + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + rx: this.get('rx') || 0, + ry: this.get('ry') || 0 + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return ''; + } + }); + + // TODO (kangax): implement rounded rectangles (both parsing and rendering) + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + */ + fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * @private + */ + function _setDefaultLeftTopValues(attributes) { + attributes.left = attributes.left || 0; + attributes.top = attributes.top || 0; + return attributes; + } + + /** + * Returns fabric.Rect instance from an SVG element + * @static + * @method fabric.Rect.fromElement + * @param element {SVGElement} element to parse + * @param options {Object} options object + * @return {fabric.Rect} 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 ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect._normalizeLeftTopProperties(parsedAttributes); + + return rect; + }; + + /** + * Returns fabric.Rect instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * @class Polyline + * @extends fabric.Object + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @scope fabric.Polyline.prototype */ { + + /** + * @property + * @type String + */ + type: 'polyline', + + /** + * Constructor + * @method initialize + * @param {Array} points array of points + * @param {Object} [options] 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); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '' + ].join(''); + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + 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(); + } + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.get('points').length; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polyline.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @method fabric.Polyline.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] 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, fabric.Polyline.ATTRIBUTE_NAMES); + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + 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)); + }; + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @method fabric.Polyline.fromObject + * @param {Object} [object] Object to create an instance from + * @return {fabric.Polyline} + */ + fabric.Polyline.fromObject = function(object) { + var points = object.points; + return new fabric.Polyline(points, object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + /** + * @class Polygon + * @extends fabric.Object + */ + fabric.Polygon = fabric.util.createClass(fabric.Object, /** @scope fabric.Polygon.prototype */ { + + /** + * @property + * @type String + */ + type: 'polygon', + + /** + * Constructor + * @method initialize + * @param {Array} points Array of points + * @param {Object} options Options object + * @return {fabric.Polygon} 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) || 1; + this.height = (maxY - minY) || 1; + + 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() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '' + ].join(''); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + 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; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polygon instance from an SVG element + * @static + * @method fabric.Polygon.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} options Options object + * @return {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++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2) || 0; + points[i].y -= (options.height / 2) || 0; + } + + return new fabric.Polygon(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @method fabric.Polygon.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Polygon} + */ + fabric.Polygon.fromObject = function(object) { + return new fabric.Polygon(object.points, object); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + var commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }; + + function drawArc(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i 1) { + pl = Math.sqrt(pl); + rx *= pl; + ry *= pl; + } + + var a00 = cos_th / rx; + var a01 = sin_th / rx; + var a10 = (-sin_th) / ry; + var a11 = (cos_th) / ry; + var x0 = a00 * ox + a01 * oy; + var y0 = a10 * ox + a11 * oy; + var x1 = a00 * x + a01 * y; + var y1 = a10 * x + a11 * y; + + var d = (x1-x0) * (x1-x0) + (y1-y0) * (y1-y0); + var sfactor_sq = 1 / d - 0.25; + if (sfactor_sq < 0) sfactor_sq = 0; + var sfactor = Math.sqrt(sfactor_sq); + if (sweep === large) sfactor = -sfactor; + var xc = 0.5 * (x0 + x1) - sfactor * (y1-y0); + var yc = 0.5 * (y0 + y1) + sfactor * (x1-x0); + + var th0 = Math.atan2(y0-yc, x0-xc); + var th1 = Math.atan2(y1-yc, x1-xc); + + var th_arc = th1-th0; + if (th_arc < 0 && sweep === 1){ + th_arc += 2*Math.PI; + } else if (th_arc > 0 && sweep === 0) { + th_arc -= 2 * Math.PI; + } + + var segments = Math.ceil(Math.abs(th_arc / (Math.PI * 0.5 + 0.001))); + var result = []; + for (var i=0; i'; + }, + + /** + * Returns object representation of an instance + * @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; + }, + + /** + * Returns dataless object representation of an instance + * @method toDatalessObject + * @return {Object} + */ + toDatalessObject: function() { + var o = this.toObject(); + if (this.sourcePath) { + o.path = this.sourcePath; + } + delete o.sourcePath; + return o; + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var chunks = []; + for (var i = 0, len = this.path.length; i < len; i++) { + chunks.push(this.path[i].join(' ')); + } + var path = chunks.join(' '); + + return [ + '', + '', + '' + ].join(''); + }, + + /** + * Returns number representation of an instance complexity + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + * @method _parsePath + */ + _parsePath: function() { + var result = [ ], + currentPath, + chunks, + parsed; + + for (var i = 0, chunksParsed, len = this.path.length; i < len; i++) { + currentPath = this.path[i]; + chunks = currentPath.slice(1).trim().replace(/(\d)-/g, '$1###-').split(/\s|,|###/); + chunksParsed = [ currentPath.charAt(0) ]; + + for (var j = 0, jlen = chunks.length; j < jlen; j++) { + parsed = parseFloat(chunks[j]); + if (!isNaN(parsed)) { + chunksParsed.push(parsed); + } + } + + var command = chunksParsed[0].toLowerCase(), + commandLength = commandLengths[command]; + + if (chunksParsed.length - 1 > commandLength) { + for (var k = 1, klen = chunksParsed.length; k < klen; k += commandLength) { + result.push([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); + } + } + else { + result.push(chunksParsed); + } + } + + return result; + }, + + /** + * @method _parseDimensions + */ + _parseDimensions: function() { + var aX = [], + aY = [], + previousX, + previousY, + isLowerCase = false, + x, + y; + + 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]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + // last 2 items in an array of coordinates are the actualy x/y (except H/V); + // collect them + + // TODO (kangax): support relative h/v commands + + 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 = 0, + 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); + }; + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' '); + + /** + * Creates an instance of fabric.Path from an SVG 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, fabric.Path.ATTRIBUTE_NAMES); + return new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options)); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + invoke = fabric.util.array.invoke, + parentToObject = fabric.Object.prototype.toObject, + camelize = fabric.util.string.camelize, + capitalize = fabric.util.string.capitalize; + + if (fabric.PathGroup) { + fabric.warn('fabric.PathGroup is already defined'); + return; + } + + /** + * @class PathGroup + * @extends fabric.Path + */ + fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @scope fabric.PathGroup.prototype */ { + + /** + * @property + * @type String + */ + type: 'path-group', + + /** + * @property + * @type String + */ + fill: '', + + /** + * @property + * @type Boolean + */ + forceFillOverwrite: false, + + /** + * Constructor + * @method initialize + * @param {Array} paths + * @param {Object} [options] Options object + * @return {fabric.PathGroup} thisArg + */ + initialize: function(paths, options) { + + options = options || { }; + this.paths = paths || [ ]; + + for (var i = this.paths.length; i--; ) { + this.paths[i].group = this; + } + + this.setOptions(options); + this.setCoords(); + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * Renders this group on a specified context + * @method render + * @param {CanvasRenderingContext2D} ctx Context to render this instance on + */ + render: function(ctx) { + 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(); + }, + + /** + * Sets certain property to a certain value + * @method _set + * @param {String} prop + * @param {Any} value + * @return {fabric.PathGroup} thisArg + */ + _set: function(prop, value) { + + if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) { + var i = this.paths.length; + while (i--) { + this.paths[i]._set(prop, value); + } + } + + return this.callSuper('_set', prop, value); + }, + + /** + * Returns object representation of this path group + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(parentToObject.call(this), { + paths: invoke(this.getObjects(), 'toObject'), + sourcePath: this.sourcePath + }); + }, + + /** + * Returns dataless object representation of this path group + * @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 svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objects = this.getObjects(); + var markup = [ + '' + ]; + + for (var i = 0, len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push(''); + + return markup.join(''); + }, + + /** + * Returns a string representation of this path group + * @method toString + * @return {String} string representation of an object + */ + toString: function() { + return '#'; + }, + + /** + * Returns true if all paths in this group are of same color + * @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; + }, + + /** + * Returns all paths in this path group + * @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; + } + + /** + * Creates fabric.Triangle instance from an object representation + * @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); + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.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; + } + + /** + * @class Group + * @extends fabric.Object + */ + fabric.Group = fabric.util.createClass(fabric.Object, /** @scope fabric.Group.prototype */ { + + /** + * @property + * @type String + */ + type: 'group', + + /** + * Constructor + * @method initialized + * @param {Object} objects Group objects + * @param {Object} [options] Options object + * @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(); + + // group is active by default + 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(); + + // do not display corners of objects enclosed in a group + object.hideCorners = true; + }, this); + }, + + /** + * Returns string represenation of a group + * @method toString + * @return {String} + */ + toString: function() { + return '#'; + }, + + /** + * Returns an array of all objects in this group + * @method getObjects + * @return {Array} group objects + */ + getObjects: function() { + return this.objects; + }, + + /** + * Adds an object to a group; Then recalculates group's dimension, position. + * @method addWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + this._restoreObjectsState(); + this.objects.push(object); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @method removeWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + removeFromArray(this.objects, object); + object.setActive(false); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Adds an object to a group + * @method add + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + add: function(object) { + this.objects.push(object); + return this; + }, + + /** + * Removes an object from a group + * @method remove + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + remove: function(object) { + removeFromArray(this.objects, object); + 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; + }, + + /** + * @private + */ + _set: function(key, value) { + if (key === 'fill' || key === 'opacity') { + var i = this.objects.length; + this[key] = value; + while (i--) { + this.objects[i].set(key, value); + } + } + else { + this[key] = value; + } + }, + + /** + * 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, 'toObject') + }); + }, + + /** + * Renders instance on a given context + * @method render + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx, noTransform) { + ctx.save(); + this.transform(ctx); + + var groupScaleFactor = Math.max(this.scaleX, this.scaleY); + + for (var i = 0, len = this.objects.length; i < len; i++) { + + var object = this.objects[i]; + var originalScaleFactor = object.borderScaleFactor; + + object.borderScaleFactor = groupScaleFactor; + object.render(ctx); + object.borderScaleFactor = originalScaleFactor; + } + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + this.setCoords(); + }, + + /** + * Returns object from the group at the specified index + * @method item + * @param index {Number} index of item to get + * @return {fabric.Object} + */ + item: function(index) { + return this.getObjects()[index]; + }, + + /** + * Returns complexity of an instance + * @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 (original state is that which was before group was created). + * @private + * @method _restoreObjectsState + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this.objects.forEach(this._restoreObjectState, this); + return this; + }, + + /** + * Restores original state of a specified object in group + * @private + * @method _restoreObjectState + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + + var groupLeft = this.get('left'), + groupTop = this.get('top'), + groupAngle = this.getAngle() * (Math.PI / 180), + 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; + }, + + /** + * Destroys a group (restoring state of its objects) + * @method destroy + * @return {fabric.Group} thisArg + * @chainable + */ + destroy: function() { + return this._restoreObjectsState(); + }, + + /** + * Saves coordinates of this instance (to be used together with `hasMoved`) + * @saveCoords + * @return {fabric.Group} thisArg + * @chainable + */ + saveCoords: function() { + this._originalLeft = this.get('left'); + this._originalTop = this.get('top'); + return this; + }, + + /** + * Checks whether this group was moved (since `saveCoords` was called last) + * @method hasMoved + * @return {Boolean} true if an object was moved (since fabric.Group#saveCoords was called) + */ + 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() { + this.forEachObject(function(object) { + object.setActive(); + }); + return this; + }, + + /** + * Executes given function for each object in this group + * @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 Context (aka thisObject) + * + * @return {fabric.Group} thisArg + * @chainable + */ + forEachObject: fabric.StaticCanvas.prototype.forEachObject, + + /** + * @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) || 0; + height = (maxY - minY) || 0; + + this.width = width; + this.height = height; + + this.left = (minX + width / 2) || 0; + this.top = (minY + height / 2) || 0; + }, + + /** + * Checks if point is contained within the group + * @method containsPoint + * @param {fabric.Point} 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; + }, + + /** + * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) + * @method toGrayscale + */ + toGrayscale: function() { + var i = this.objects.length; + while (i--) { + this.objects[i].toGrayscale(); + } + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objectsMarkup = [ ]; + for (var i = 0, len = this.objects.length; i < len; i++) { + objectsMarkup.push(this.objects[i].toSVG()); + } + + return ( + '' + + objectsMarkup.join('') + + ''); + } + }); + + /** + * Returns fabric.Group instance from an object representation + * @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, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.Group(enlivenedObjects, object)); + }); + }; + + fabric.Group.async = true; + +})(typeof exports !== 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + } + + if (!fabric.Object) { + fabric.warn('fabric.Object is required for fabric.Image initialization'); + return; + } + + /** + * @class Image + * @extends fabric.Object + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @scope fabric.Image.prototype */ { + + /** + * @property + * @type Boolean + */ + active: false, + + /** + * @property + * @type Boolean + */ + bordervisibility: false, + + /** + * @property + * @type Boolean + */ + cornervisibility: false, + + /** + * @property + * @type String + */ + type: 'image', + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} options optional + */ + initialize: function(element, options) { + options || (options = { }); + + this.callSuper('initialize', options); + this._initElement(element); + this._originalImage = this.getElement(); + this._initConfig(options); + + this.filters = [ ]; + + if (options.filters) { + this.filters = options.filters; + this.applyFilters(); + } + }, + + /** + * Returns image element which this instance if based on + * @method getElement + * @return {HTMLImageElement} image element + */ + getElement: function() { + return this._element; + }, + + /** + * Sets image element for this instance to a specified one + * @method setElement + * @param {HTMLImageElement} element + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element) { + this._element = element; + this._initConfig(); + return this; + }, + + /** + * Returns original size of an image + * @method getOriginalSize + * @return {Object} object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.width, + height: element.height + }; + }, + + /** + * Sets border visibility + * @method setBorderVisibility + * @param {Boolean} visible When true, border is set to be visible + */ + setBorderVisibility: function() { + this._resetWidthHeight(); + this._adjustWidthHeightToBorders(); + this.setCoords(); + }, + + /** + * Sets corner visibility + * @method setCornersVisibility + * @param {Boolean} visible When true, corners are set to be visible + */ + setCornersVisibility: function(visible) { + this.cornervisibility = !!visible; + }, + + /** + * Renders image on a specified context + * @method render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + render: function(ctx, noTransform) { + ctx.save(); + var m = this.transformMatrix; + this._resetWidthHeight(); + if (this.group) { + ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); + } + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + this._render(ctx); + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + src: this._originalImage.src || this._originalImage._src, + filters: this.filters.concat() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return ''+ + ' element with actual transformation, then offsetting object to the top/left + // so that object's center aligns with container's left/top + 'transform="translate('+ (-this.width/2) + ' ' + (-this.height/2) + ')" ' + + 'width="' + this.width + '" ' + + 'height="' + this.height + '"' + '/>'+ + ''; + }, + + /** + * Returns source of an image + * @method getSrc + * @return {String} Source of an image + */ + getSrc: function() { + return this.getElement().src || this.getElement()._src; + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of an instance + */ + toString: function() { + return '#'; + }, + + /** + * Returns a clone of an instance + * @mthod clone + * @param {Function} callback Callback is invoked with a clone as a first argument + */ + clone: function(callback) { + this.constructor.fromObject(this.toObject(), callback); + }, + + /** + * Applies filters assigned to this image (from "filters" array) + * @mthod applyFilters + * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated + */ + applyFilters: function(callback) { + + if (this.filters.length === 0) { + this.setElement(this._originalImage); + callback && callback(); + return; + } + + var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined', + imgEl = this._originalImage, + canvasEl = fabric.document.createElement('canvas'), + replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'), + _this = this; + + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + canvasEl.width = imgEl.width; + canvasEl.height = imgEl.height; + + canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); + + this.filters.forEach(function(filter) { + filter && filter.applyTo(canvasEl); + }); + + /** @ignore */ + replacement.onload = function() { + _this._element = replacement; + callback && callback(); + replacement.onload = canvasEl = imgEl = null; + }; + replacement.width = imgEl.width; + replacement.height = imgEl.height; + + if (isLikelyNode) { + var base64str = canvasEl.toDataURL('image/png').replace(/data:image\/png;base64,/, ''); + replacement.src = new Buffer(base64str, 'base64'); + _this._element = replacement; + + // onload doesn't fire in node, so we invoke callback manually + callback && callback(); + } + else { + replacement.src = canvasEl.toDataURL('image/png'); + } + + return this; + }, + + /** + * @private + */ + _render: function(ctx) { + ctx.drawImage( + this.getElement(), + - this.width / 2, + -this.height / 2, + this.width, + this.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) { + options || (options = { }); + this.setOptions(options); + this._setBorder(); + this._setWidthHeight(options); + }, + + /** + * @method _initFilters + * @param {Object} object Object with filters property + */ + _initFilters: function(object) { + if (object.filters && object.filters.length) { + this.filters = object.filters.map(function(filterObj) { + return filterObj && fabric.Image.filters[filterObj.type].fromObject(filterObj); + }); + } + }, + + /** + * @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 = 'width' in options + ? options.width + : ((this.getElement().width || 0) + sidesBorderWidth); + + this.height = 'height' in options + ? options.height + : ((this.getElement().height || 0) + sidesBorderWidth); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + */ + fabric.Image.CSS_CANVAS = "canvas-img"; + + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @method fromObject + * @param object {Object} + * @param callback {Function} optional + */ + fabric.Image.fromObject = function(object, callback) { + var img = fabric.document.createElement('img'), + src = object.src; + + if (object.width) { + img.width = object.width; + } + if (object.height) { + img.height = object.height; + } + + /** @ignore */ + img.onload = function() { + fabric.Image.prototype._initFilters.call(object, object); + + var instance = new fabric.Image(img, object); + callback && callback(instance); + img = img.onload = null; + }; + img.src = src; + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @method fromURL + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + var img = fabric.document.createElement('img'); + + /** @ignore */ + img.onload = function() { + if (callback) { + callback(new fabric.Image(img, imgOptions)); + } + img = img.onload = null; + }; + img.src = url; + }; + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/struct.html#ImageElement + */ + fabric.Image.ATTRIBUTE_NAMES = 'x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href'.split(' '); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @method fabric.Image.fromElement + * @param {SVGElement} element Element to parse + * @param {Function} callback Callback to execute when fabric.Image object is created + * @param {Object} [options] Options object + * @return {fabric.Image} + */ + fabric.Image.fromElement = function(element, callback, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend(parsedAttributes, options)); + }; + + fabric.Image.async = true; + +})(typeof exports !== 'undefined' ? exports : this); + +fabric.util.object.extend(fabric.Object.prototype, { + + /** + * @method _getAngleValueForStraighten + * @return {Number} angle value + * @private + */ + _getAngleValueForStraighten: function() { + var angle = this.get('angle'); + + // TODO (kangax): can this be simplified? + + 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; + }, + + /** + * @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; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Straightens object, then rerenders canvas + * @method straightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.renderAll(); + return this; + }, + + /** + * Same as `fabric.Canvas#straightenObject`, but animated + * @method fxStraightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.renderAll.bind(this) + }); + return this; + } +}); +/** + * @namespace + */ +fabric.Image.filters = { }; + +/** + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Grayscale = fabric.util.createClass( /** @scope fabric.Image.filters.Grayscale.prototype */ { + + /** + * @param {String} type + */ + type: "Grayscale", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Grayscale.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Grayscale.fromObject = function() { + return new fabric.Image.filters.Grayscale(); +}; + +/** + * @class fabric.Image.filters.RemoveWhite + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.RemoveWhite = fabric.util.createClass( /** @scope fabric.Image.filters.RemoveWhite.prototype */ { + + /** + * @param {String} type + */ + type: "RemoveWhite", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 30; + this.distance = options.distance || 20; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + distance = this.distance, + limit = 255 - threshold, + abs = Math.abs, + r, g, b; + + for (var i = 0, len = data.length; i < len; i += 4) { + + r = data[i]; + g = data[i+1]; + b = data[i+2]; + + if (r > limit && + g > limit && + b > limit && + abs(r-g) < distance && + abs(r-b) < distance && + abs(g-b) < distance) { + + data[i+3] = 1; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold, + distance: this.distance + }; + } +}); + +fabric.Image.filters.RemoveWhite.fromObject = function(object) { + return new fabric.Image.filters.RemoveWhite(object); +}; + +/** + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Invert = fabric.util.createClass( /** @scope fabric.Image.filters.Invert.prototype */ { + + /** + * @param {String} type + */ + type: "Invert", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Invert.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i; + + for (i = 0; i < iLen; i+=4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Invert.fromObject = function() { + return new fabric.Image.filters.Invert(); +}; + +/** + * @class fabric.Image.filters.Sepia + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, avg; + + for (i = 0; i < iLen; i+=4) { + avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; + data[i] = avg + 100; + data[i + 1] = avg + 50; + data[i + 2] = avg + 255; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia.fromObject = function() { + return new fabric.Image.filters.Sepia(); +}; + +/** + * @class fabric.Image.filters.Sepia2 + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia2 = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia2.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia2", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, r, g, b; + + for (i = 0; i < iLen; i+=4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; + data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; + data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia2.fromObject = function() { + return new fabric.Image.filters.Sepia2(); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Brightness = fabric.util.createClass( /** @scope fabric.Image.filters.Brightness.prototype */ { + + /** + * @param {String} type + */ + type: "Brightness", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.brightness = options.brightness || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + brightness = this.brightness; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i] += brightness; + data[i + 1] += brightness; + data[i + 2] += brightness; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + brightness: this.brightness + }; + } +}); + +fabric.Image.filters.Brightness.fromObject = function(object) { + return new fabric.Image.filters.Brightness(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Noise = fabric.util.createClass( /** @scope fabric.Image.filters.Noise.prototype */ { + + /** + * @param {String} type + */ + type: "Noise", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.noise = options.noise || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + noise = this.noise, rand; + + for (var i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + noise: this.noise + }; + } +}); + +fabric.Image.filters.Noise.fromObject = function(object) { + return new fabric.Image.filters.Noise(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.GradientTransparency = fabric.util.createClass( /** @scope fabric.Image.filters.GradientTransparency.prototype */ { + + /** + * @param {String} type + */ + type: "GradientTransparency", + + /** + * @memberOf fabric.Image.filters.GradientTransparency.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + total = data.length; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = threshold + 255 * (total - i) / total; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold + }; + } +}); + +fabric.Image.filters.GradientTransparency.fromObject = function(object) { + return new fabric.Image.filters.GradientTransparency(object); +}; + +/** + * @class fabric.Image.filters.Tint + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Tint = fabric.util.createClass( /** @scope fabric.Image.filters.Tint.prototype */ { + + /** + * @param {String} type + */ + type: "Tint", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.color = options.color || 0; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, a; + + var rgb = parseInt(this.color, 10).toString(16); + + var cr = parseInt('0x' + rgb.substr(0, 2), 16); + var cg = parseInt('0x' + rgb.substr(2, 2), 16); + var cb = parseInt('0x' + rgb.substr(4, 2), 16); + + for (i = 0; i < iLen; i+=4) { + + a = data[i+3]; + + if (a > 0){ + data[i] = cr; + data[i+1] = cg; + data[i+2] = cb; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + color: this.color + }; + } +}); + +fabric.Image.filters.Tint.fromObject = function(object) { + return new fabric.Image.filters.Tint(object); +}; +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + if (!fabric.Object) { + fabric.warn('fabric.Text requires fabric.Object'); + return; + } + + /** + * @class Text + * @extends fabric.Object + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ { + + /** + * @property + * @type Number + */ + fontSize: 40, + + /** + * @property + * @type Number + */ + fontWeight: 100, + + /** + * @property + * @type String + */ + fontFamily: 'Times New Roman', + + /** + * @property + * @type String + */ + textDecoration: '', + + /** + * @property + * @type String | null + */ + textShadow: '', + + /** + * Determines text alignment. Possible values: "left", "center", or "right". + * @property + * @type String + */ + textAlign: 'left', + + /** + * @property + * @type String + */ + fontStyle: '', + + /** + * @property + * @type Number + */ + lineHeight: 1.3, + + /** + * @property + * @type String + */ + strokeStyle: '', + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type String + */ + backgroundColor: '', + + + /** + * @property + * @type String | null + */ + path: null, + + /** + * @property + * @type String + */ + type: 'text', + + /** + * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) + * @property + * @type Boolean + */ + useNative: true, + + /** + * Constructor + * @method initialize + * @param {String} text + * @param {Object} [options] + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this._initStateProperties(); + this.text = text; + this.setOptions(options || { }); + this._theta = this.angle * Math.PI / 180; + this._initDimensions(); + this.setCoords(); + }, + + /** + * Renders text object on offscreen canvas, so that it would get dimensions + * @private + * @method _initDimensions + */ + _initDimensions: function() { + var canvasEl = fabric.document.createElement('canvas'); + + if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + this._render(canvasEl.getContext('2d')); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Text` -specific ones to it + * (such as "fontFamily", "fontWeight", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(); + this.stateProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'path', + 'text', + 'textDecoration', + 'textShadow', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'strokeStyle', + 'strokeWidth', + 'backgroundColor', + 'useNative' + ); + fabric.util.removeFromArray(this.stateProperties, 'width'); + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of text object + */ + toString: function() { + return '#'; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (typeof Cufon === 'undefined' || this.useNative === true) { + this._renderViaNative(ctx); + } + else { + this._renderViaCufon(ctx); + } + }, + + /** + * @private + * @method _renderViaCufon + */ + _renderViaCufon: function(ctx) { + var o = Cufon.textOptions || (Cufon.textOptions = { }); + + // export options to be used by cufon.js + o.left = this.left; + o.top = this.top; + o.context = ctx; + o.color = this.fill; + + var el = this._initDummyElementForCufon(); + + // set "cursor" to top/left corner + this.transform(ctx); + + // draw text + Cufon.replaceElement(el, { + engine: 'canvas', + separate: 'none', + fontFamily: this.fontFamily, + fontWeight: this.fontWeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor + }); + + // update width, height + this.width = o.width; + this.height = o.height; + + this._totalLineHeight = o.totalLineHeight; + this._fontAscent = o.fontAscent; + this._boundaries = o.boundaries; + this._shadowOffsets = o.shadowOffsets; + this._shadows = o.shadows || [ ]; + + el = null; + + // need to set coords _after_ the width/height was retreived from Cufon + this.setCoords(); + }, + + /** + * @private + * @method _render_native + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderViaNative: function(ctx) { + + this.transform(ctx); + this._setTextStyles(ctx); + + var textLines = this.text.split(/\r?\n/); + + this.width = this._getTextWidth(ctx, textLines); + this.height = this._getTextHeight(ctx, textLines); + + this._renderTextBackground(ctx, textLines); + + if (this.textAlign !== 'left') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + + this._setTextShadow(ctx); + this._renderTextFill(ctx, textLines); + this.textShadow && ctx.restore(); + + this._renderTextStroke(ctx, textLines); + if (this.textAlign !== 'left') { + ctx.restore(); + } + + this._renderTextDecoration(ctx, textLines); + this._setBoundaries(ctx, textLines); + this._totalLineHeight = 0; + + this.setCoords(); + }, + + /** + * @private + * @method _setBoundaries + */ + _setBoundaries: function(ctx, textLines) { + this._boundaries = [ ]; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + this._boundaries.push({ + height: this.fontSize, + width: lineWidth, + left: lineLeftOffset + }); + } + }, + + /** + * @private + * @method _setTextStyles + */ + _setTextStyles: function(ctx) { + ctx.fillStyle = this.fill; + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.textBaseline = 'top'; + ctx.textAlign = this.textAlign; + ctx.font = this._getFontDeclaration(); + }, + + /** + * @private + * @method _getTextHeight + */ + _getTextHeight: function(ctx, textLines) { + return this.fontSize * textLines.length * this.lineHeight; + }, + + /** + * @private + * @method _getTextWidth + */ + _getTextWidth: function(ctx, textLines) { + var maxWidth = ctx.measureText(textLines[0]).width; + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = ctx.measureText(textLines[i]).width; + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @method _setTextShadow + */ + _setTextShadow: function(ctx) { + if (this.textShadow) { + + // "rgba(0,0,0,0.2) 2px 2px 10px" + // "rgb(0, 100, 0) 0 0 5px" + // "red 2px 2px 1px" + // "#f55 123 345 567" + var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; + + var shadowDeclaration = this.textShadow; + var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); + var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); + + ctx.save(); + ctx.shadowColor = shadowColor; + ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); + ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); + ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); + + this._shadows = [{ + blur: ctx.shadowBlur, + color: ctx.shadowColor, + offX: ctx.shadowOffsetX, + offY: ctx.shadowOffsetY + }]; + + this._shadowOffsets = [[ + parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) + ]]; + } + }, + + _renderTextFill: function(ctx, textLines) { + this._boundaries = [ ]; + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.fillText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + }, + + /** + * @private + * @method _renderTextStroke + */ + _renderTextStroke: function(ctx, textLines) { + if (this.strokeStyle) { + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.strokeText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + } + }, + + /** + * @private + * @_renderTextBackground + */ + _renderTextBackground: function(ctx, textLines) { + if (this.backgroundColor) { + ctx.save(); + ctx.fillStyle = this.backgroundColor; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-this.width / 2) + lineLeftOffset, + (-this.height / 2) + (i * this.fontSize * this.lineHeight), + lineWidth, + this.fontSize + ); + } + ctx.restore(); + } + }, + + /** + * @private + * @method _getLineLeftOffset + */ + _getLineLeftOffset: function(lineWidth) { + if (this.textAlign === 'center') { + return (this.width - lineWidth) / 2; + } + if (this.textAlign === 'right') { + return this.width - lineWidth; + } + return 0; + }, + + /** + * @private + * @method _renderTextDecoration + */ + _renderTextDecoration: function(ctx, textLines) { + + var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; + var _this = this; + + function renderLinesAtOffset(offset) { + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = _this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-_this.width / 2) + lineLeftOffset, + (offset + (i * _this.fontSize * _this.lineHeight)) - halfOfVerticalBox, + lineWidth, + 1); + } + } + + if (this.textDecoration.indexOf('underline') > -1) { + renderLinesAtOffset(this.fontSize); + } + if (this.textDecoration.indexOf('line-through') > -1) { + renderLinesAtOffset(this.fontSize / 2); + } + if (this.textDecoration.indexOf('overline') > -1) { + renderLinesAtOffset(0); + } + }, + + /** + * @private + * @method _getFontDeclaration + */ + _getFontDeclaration: function() { + return [ + this.fontStyle, + this.fontWeight, + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + + /** + * @private + * @method _initDummyElement + */ + _initDummyElementForCufon: function() { + var el = fabric.document.createElement('pre'), + container = fabric.document.createElement('div'); + + // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent + container.appendChild(el); + + if (typeof G_vmlCanvasManager === 'undefined') { + el.innerHTML = this.text; + } + else { + // IE 7 & 8 drop newlines and white space on text nodes + // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html + // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp + el.innerText = this.text.replace(/\r?\n/gi, '\r'); + } + + el.style.fontSize = this.fontSize + 'px'; + el.style.letterSpacing = 'normal'; + + return el; + }, + + /** + * Renders text instance on a specified context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + render: function(ctx, noTransform) { + ctx.save(); + this._render(ctx); + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of text object + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + text: this.text, + fontSize: this.fontSize, + fontWeight: this.fontWeight, + fontFamily: this.fontFamily, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + path: this.path, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor, + useNative: this.useNative + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var textLines = this.text.split(/\r?\n/), + lineTopOffset = this.useNative + ? this.fontSize * this.lineHeight + : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), + + textLeftOffset = -(this.width/2), + textTopOffset = this.useNative + ? this.fontSize - 1 + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + + textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), + shadowSpans = this._getSVGShadows(lineTopOffset, textLines); + + // move top offset by an ascent + textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + return [ + '', + textAndBg.textBgRects.join(''), + '', + shadowSpans.join(''), + textAndBg.textSpans.join(''), + '', + '' + ].join(''); + }, + + _getSVGShadows: function(lineTopOffset, textLines) { + var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; + + if (!this._shadows || !this._boundaries) { + return shadowSpans; + } + + for (j = 0, jlen = this._shadows.length; j < jlen; j++) { + for (i = 0, ilen = textLines.length; i < ilen; i++) { + if (textLines[i] !== '') { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; + shadowSpans.push( + '', + fabric.util.string.escapeXml(textLines[i]), + ''); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + } + } + return shadowSpans; + }, + + _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { + var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; + + // text and background + for (i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; + textSpans.push( + ' elements since setting opacity on containing one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLines[i]), + '' + ); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.backgroundColor || !this._boundaries) continue; + + textBgRects.push( + ''); + } + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + // Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + // we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + _getFillAttributes: function(value) { + var fillColor = value ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @method setColor + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setColor: function(value) { + this.set('fill', value); + return this; + }, + + /** + * Sets fontSize of an instance and updates its coordinates + * @method setFontsize + * @param {Number} value + * @return {fabric.Text} thisArg + * @chainable + */ + setFontsize: function(value) { + this.set('fontSize', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Returns actual text value of an instance + * @method getText + * @return {String} + */ + getText: function() { + return this.text; + }, + + /** + * Sets text of an instance, and updates its coordinates + * @method setText + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setText: function(value) { + this.set('text', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Sets specified property to a specified value + * @method set + * @param {String} name + * @param {Any} value + * @return {fabric.Text} thisArg + * @chainable + */ + _set: function(name, value) { + if (name === 'fontFamily' && this.path) { + this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); + } + this.callSuper('_set', name, value); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Text.fromElement`) + * @static + */ + fabric.Text.ATTRIBUTE_NAMES = + ('x y fill fill-opacity opacity stroke stroke-width transform ' + + 'font-family font-style font-weight font-size text-decoration').split(' '); + + /** + * Returns fabric.Text instance from an object representation + * @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)); + }; + + /** + * Returns fabric.Text instance from an SVG element (not yet implemented) + * @static + * @method fabric.Text.fromElement + * @param element + * @param options + * @return {fabric.Text} an instance + */ + fabric.Text.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); + options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); + var text = new fabric.Text(element.textContent, options); + + return text; + }; + +})(typeof exports !== 'undefined' ? exports : this); +(function() { + + if (typeof document !== 'undefined' && typeof window !== 'undefined') { + return; + } + + var DOMParser = new require('xmldom').DOMParser, + URL = require('url'), + HTTP = require('http'), + + Canvas = require('canvas'), + Image = require('canvas').Image; + + function request(url, encoding, callback) { + var oURL = URL.parse(url), + client = HTTP.createClient(oURL.port, oURL.hostname), + req = client.request('GET', oURL.pathname, { 'host': oURL.hostname }); + + client.addListener('error', function(err) { + if (err.errno === process.ECONNREFUSED) { + fabric.log('ECONNREFUSED: connection refused to ' + client.host + ':' + client.port); + } + else { + fabric.log(err.message); + } + }); + + req.end(); + req.on('response', function (response) { + var body = ""; + if (encoding) { + response.setEncoding(encoding); + } + response.on('end', function () { + callback(body); + }); + response.on('data', function (chunk) { + if (response.statusCode === 200) { + body += chunk; + } + }); + }); + } + + fabric.util.loadImage = function(url, callback) { + request(url, 'binary', function(body) { + var img = new Image(); + img.src = new Buffer(body, 'binary'); + // preserving original url, which seems to be lost in node-canvas + img._src = url; + callback(img); + }); + }; + + fabric.loadSVGFromURL = function(url, callback) { + url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); + request(url, '', function(body) { + fabric.loadSVGFromString(body, callback); + }); + }; + + fabric.loadSVGFromString = function(string, callback) { + var doc = new DOMParser().parseFromString(string); + fabric.parseSVGDocument(doc.documentElement, function(results, options) { + callback(results, options); + }); + }; + + fabric.util.getScript = function(url, callback) { + request(url, '', function(body) { + eval(body); + callback && callback(); + }); + }; + + fabric.Image.fromObject = function(object, callback) { + fabric.util.loadImage(object.src, function(img) { + var oImg = new fabric.Image(img); + + oImg._initConfig(object); + oImg._initFilters(object); + callback(oImg); + }); + }; + + /** + * Only available when running fabric on node.js + * @method createCanvasForNode + * @param width Canvas width + * @param height Canvas height + * @return {Object} wrapped canvas instance + */ + fabric.createCanvasForNode = function(width, height) { + + var canvasEl = fabric.document.createElement('canvas'), + nodeCanvas = new Canvas(width || 600, height || 600); + + // jsdom doesn't create style on canvas element, so here be temp. workaround + canvasEl.style = { }; + + canvasEl.width = nodeCanvas.width; + canvasEl.height = nodeCanvas.height; + + var FabricCanvas = fabric.Canvas || fabric.StaticCanvas; + var fabricCanvas = new FabricCanvas(canvasEl); + fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); + fabricCanvas.nodeCanvas = nodeCanvas; + + return fabricCanvas; + }; + + fabric.StaticCanvas.prototype.createPNGStream = function() { + return this.nodeCanvas.createPNGStream(); + }; + + var origSetWidth = fabric.StaticCanvas.prototype.setWidth; + fabric.StaticCanvas.prototype.setWidth = function(width) { + origSetWidth.call(this); + this.nodeCanvas.width = width; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; + } + + var origSetHeight = fabric.StaticCanvas.prototype.setHeight; + fabric.StaticCanvas.prototype.setHeight = function(height) { + origSetHeight.call(this); + this.nodeCanvas.height = height; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; + } + +})(); diff --git a/src/object.class.js b/src/object.class.js index c825aa4e..99d22d75 100644 --- a/src/object.class.js +++ b/src/object.class.js @@ -1,1734 +1,1729 @@ -(function(global) { - - "use strict"; - - var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - toFixed = fabric.util.toFixed, - capitalize = fabric.util.string.capitalize, - getPointer = fabric.util.getPointer, - degreesToRadians = fabric.util.degreesToRadians, - slice = Array.prototype.slice; - - if (fabric.Object) { - return; - } - - /** - * @class Object - * @memberOf fabric - */ - fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { - - /** - * Type of an object (rect, circle, path, etc) - * @property - * @type String - */ - type: 'object', - - /** - * @property - * @type Number - */ - top: 0, - - /** - * @property - * @type Number - */ - left: 0, - - /** - * @property - * @type Number - */ - width: 0, - - /** - * @property - * @type Number - */ - height: 0, - - /** - * @property - * @type Number - */ - scaleX: 1, - - /** - * @property - * @type Number - */ - scaleY: 1, - - /** - * @property - * @type Boolean - */ - flipX: false, - - /** - * @property - * @type Boolean - */ - flipY: false, - - /** - * @property - * @type Number - */ - opacity: 1, - - /** - * @property - * @type Number - */ - angle: 0, - - /** - * @property - * @type Number - */ - cornersize: 12, - - /** - * @property - * @type Boolean - */ - transparentCorners: true, - - /** - * @property - * @type Number - */ - padding: 0, - - /** - * @property - * @type String - */ - borderColor: 'rgba(102,153,255,0.75)', - - /** - * @property - * @type String - */ - cornerColor: 'rgba(102,153,255,0.5)', - - /** - * @property - * @type String - */ - fill: 'rgb(0,0,0)', - - /** - * @property - * @type String - */ - fillRule: 'source-over', - - /** - * @property - * @type String - */ - overlayFill: null, - - /** - * @property - * @type String - */ - stroke: null, - - /** - * @property - * @type Number - */ - strokeWidth: 1, - - /** - * @property - * @type Array - */ - strokeDashArray: null, - - /** - * @property - * @type Number - */ - borderOpacityWhenMoving: 0.4, - - /** - * @property - * @type Number - */ - borderScaleFactor: 1, - - /** - * Transform matrix - * @property - * @type Array - */ - transformMatrix: null, - - /** - * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) - * @property - * @type Boolean - */ - selectable: true, - - /** - * When set to `false`, object's controls are not displayed and can not be used to manipulate object - * @property - * @type Boolean - */ - hasControls: true, - - /** - * When set to `false`, object's borders are not rendered - * @property - * @type Boolean - */ - hasBorders: true, - - /** - * When set to `false`, object's rotating point will not be visible or selectable - * @property - * @type Boolean - */ - hasRotatingPoint: false, - - /** - * Offset for object's rotating point (when enabled) - * @property - * @type Number - */ - rotatingPointOffset: 40, - - /** - * @private - * @property - * @type Number - */ - _theta: 0, - - perPixelTargetFind: false, - - includeDefaultValues: true, - - /** - * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); - * as well as for history (undo/redo) purposes - * @property - * @type Array - */ - stateProperties: ( - 'top left width height scaleX scaleY flipX flipY ' + - 'theta angle opacity cornersize fill overlayFill ' + - 'stroke strokeWidth strokeDashArray fillRule ' + - 'borderScaleFactor transformMatrix selectable' - ).split(' '), - - /** - * @method callSuper - * @param {String} methodName - */ - callSuper: function(methodName) { - var fn = this.constructor.superclass.prototype[methodName]; - return (arguments.length > 1) - ? fn.apply(this, slice.call(arguments, 1)) - : fn.call(this); - }, - - /** - * Constructor - * @method initialize - * @param {Object} [options] Options object - */ - initialize: function(options) { - if (options) { - this.setOptions(options); - } - }, - - /** - * @method initGradient - */ - _initGradient: function(options) { - if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { - this.set('fill', new fabric.Gradient(options.fill)); - } - }, - - /** - * @method setOptions - * @param {Object} [options] - */ - setOptions: function(options) { - var i = this.stateProperties.length, prop; - while (i--) { - prop = this.stateProperties[i]; - if (prop in options) { - this.set(prop, options[prop]); - } - } - this._initGradient(options); - }, - - /** - * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; - - var object = { - type: this.type, - left: toFixed(this.left, NUM_FRACTION_DIGITS), - top: toFixed(this.top, NUM_FRACTION_DIGITS), - width: toFixed(this.width, NUM_FRACTION_DIGITS), - height: toFixed(this.height, NUM_FRACTION_DIGITS), - fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, - overlayFill: this.overlayFill, - stroke: this.stroke, - strokeWidth: this.strokeWidth, - strokeDashArray: this.strokeDashArray, - scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), - scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), - angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), - flipX: this.flipX, - flipY: this.flipY, - opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), - selectable: this.selectable, - hasControls: this.hasControls, - hasBorders: this.hasBorders, - hasRotatingPoint: this.hasRotatingPoint, - transparentCorners: this.transparentCorners, - perPixelTargetFind: this.perPixelTargetFind - }; - - if (!this.includeDefaultValues) { - object = this._removeDefaultValues(object); - } - - return object; - }, - - /** - * Returns (dataless) object representation of an instance - * @method toDatalessObject - */ - toDatalessObject: function() { - // will be overwritten by subclasses - return this.toObject(); - }, - - /** - * Returns styles-string for svg-export - * @method getSvgStyles - * @return {string} - */ - getSvgStyles: function() { - return [ - "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", - "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", - "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), - "fill: ", (this.fill ? this.fill : 'none'), "; ", - "opacity: ", (this.opacity ? this.opacity : '1'), ";" - ].join(""); - }, - - /** - * Returns transform-string for svg-export - * @method getSvgTransform - * @return {string} - */ - getSvgTransform: function() { - var angle = this.getAngle(); - return [ - "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", - angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', - (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") - ].join(''); - }, - - /** - * @private - * @method _removeDefaultValues - */ - _removeDefaultValues: function(object) { - var defaultOptions = fabric.Object.prototype.options; - if (defaultOptions) { - 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 "#"; - }, - - /** - * Sets property to a given value - * @method set - * @param {String} name - * @param {Object|Function} value - * @return {fabric.Group} thisArg - * @chainable - */ - set: function(key, value) { - if (typeof key === 'object') { - for (var prop in key) { - this._set(prop, key[prop]); - } - } - else { - if (typeof value === 'function') { - this._set(key, value(this.get(key))); - } - else { - this._set(key, value); - } - } - return this; - }, - - _set: function(key, value) { - var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && - value < fabric.Object.MIN_SCALE_LIMIT; - - if (shouldConstrainValue) { - value = fabric.Object.MIN_SCALE_LIMIT; - } - if (key === 'angle') { - this.setAngle(value); - } - else { - this[key] = value; - } - }, - - /** - * 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) { - - // do not render if width or height are zeros - if (this.width === 0 || this.height === 0) return; - - ctx.save(); - - var m = this.transformMatrix; - if (m && !this.group) { - ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - if (!noTransform) { - this.transform(ctx); - } - - if (this.stroke || this.strokeDashArray) { - ctx.lineWidth = this.strokeWidth; - ctx.strokeStyle = this.stroke; - } - - if (this.overlayFill) { - ctx.fillStyle = this.overlayFill; - } - else if (this.fill) { - ctx.fillStyle = this.fill.toLiveGradient - ? this.fill.toLiveGradient(ctx) - : this.fill; - } - - if (m && this.group) { - ctx.translate(-this.group.width/2, -this.group.height/2); - ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); - } - - this._render(ctx, noTransform); - - if (this.active && !noTransform) { - this.drawBorders(ctx); - 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; - this.setCoords(); - return this; - }, - - /** - * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) - * @method scaleToWidth - * @param value {Number} new width value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToWidth: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); - return this.scale(value / this.width / boundingRectFactor); - }, - - /** - * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) - * @method scaleToHeight - * @param value {Number} new height value - * @return {fabric.Object} thisArg - * @chainable - */ - scaleToHeight: function(value) { - // adjust to bounding rect factor so that rotated shapes would fit as well - var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); - return this.scale(value / this.height / boundingRectFactor); - }, - - /** - * 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() { - - var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, - padding = this.padding; - - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + toFixed = fabric.util.toFixed, + capitalize = fabric.util.string.capitalize, + getPointer = fabric.util.getPointer, + degreesToRadians = fabric.util.degreesToRadians, + slice = Array.prototype.slice; + + if (fabric.Object) { + return; + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ { + + /** + * Type of an object (rect, circle, path, etc) + * @property + * @type String + */ + type: 'object', + + /** + * @property + * @type Number + */ + top: 0, + + /** + * @property + * @type Number + */ + left: 0, + + /** + * @property + * @type Number + */ + width: 0, + + /** + * @property + * @type Number + */ + height: 0, + + /** + * @property + * @type Number + */ + scaleX: 1, + + /** + * @property + * @type Number + */ + scaleY: 1, + + /** + * @property + * @type Boolean + */ + flipX: false, + + /** + * @property + * @type Boolean + */ + flipY: false, + + /** + * @property + * @type Number + */ + opacity: 1, + + /** + * @property + * @type Number + */ + angle: 0, + + /** + * @property + * @type Number + */ + cornersize: 12, + + /** + * @property + * @type Boolean + */ + transparentCorners: true, + + /** + * @property + * @type Number + */ + padding: 0, + + /** + * @property + * @type String + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * @property + * @type String + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * @property + * @type String + */ + fill: 'rgb(0,0,0)', + + /** + * @property + * @type String + */ + fillRule: 'source-over', + + /** + * @property + * @type String + */ + overlayFill: null, + + /** + * @property + * @type String + */ + stroke: null, + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type Array + */ + strokeDashArray: null, + + /** + * @property + * @type Number + */ + borderOpacityWhenMoving: 0.4, + + /** + * @property + * @type Number + */ + borderScaleFactor: 1, + + /** + * Transform matrix + * @property + * @type Array + */ + transformMatrix: null, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) + * @property + * @type Boolean + */ + selectable: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @property + * @type Boolean + */ + hasControls: true, + + /** + * When set to `false`, object's borders are not rendered + * @property + * @type Boolean + */ + hasBorders: true, + + /** + * When set to `false`, object's rotating point will not be visible or selectable + * @property + * @type Boolean + */ + hasRotatingPoint: false, + + /** + * Offset for object's rotating point (when enabled) + * @property + * @type Number + */ + rotatingPointOffset: 40, + + /** + * @private + * @property + * @type Number + */ + _theta: 0, + + perPixelTargetFind: false, + + includeDefaultValues: true, + + /** + * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); + * as well as for history (undo/redo) purposes + * @property + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY ' + + 'theta angle opacity cornersize fill overlayFill ' + + 'stroke strokeWidth strokeDashArray fillRule ' + + 'borderScaleFactor transformMatrix selectable' + ).split(' '), + + /** + * @method callSuper + * @param {String} methodName + */ + callSuper: function(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + }, + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + } + }, + + /** + * @method initGradient + */ + _initGradient: function(options) { + if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @method setOptions + * @param {Object} [options] + */ + setOptions: function(options) { + var i = this.stateProperties.length, prop; + while (i--) { + prop = this.stateProperties[i]; + if (prop in options) { + this.set(prop, options[prop]); + } + } + this._initGradient(options); + }, + + /** + * @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 NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + overlayFill: this.overlayFill, + stroke: this.stroke, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray, + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + selectable: this.selectable, + hasControls: this.hasControls, + hasBorders: this.hasBorders, + hasRotatingPoint: this.hasRotatingPoint, + transparentCorners: this.transparentCorners, + perPixelTargetFind: this.perPixelTargetFind + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @method toDatalessObject + */ + toDatalessObject: function() { + // will be overwritten by subclasses + return this.toObject(); + }, + + /** + * Returns styles-string for svg-export + * @method getSvgStyles + * @return {string} + */ + getSvgStyles: function() { + return [ + "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", + "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), + "fill: ", (this.fill ? this.fill : 'none'), "; ", + "opacity: ", (this.opacity ? this.opacity : '1'), ";" + ].join(""); + }, + + /** + * Returns transform-string for svg-export + * @method getSvgTransform + * @return {string} + */ + getSvgTransform: function() { + var angle = this.getAngle(); + return [ + "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", + angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', + (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") + ].join(''); + }, + + /** + * @private + * @method _removeDefaultValues + */ + _removeDefaultValues: function(object) { + var defaultOptions = fabric.Object.prototype.options; + if (defaultOptions) { + 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 "#"; + }, + + /** + * Sets property to a given value + * @method set + * @param {String} name + * @param {Object|Function} value + * @return {fabric.Group} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && + value < fabric.Object.MIN_SCALE_LIMIT; + + if (shouldConstrainValue) { + value = fabric.Object.MIN_SCALE_LIMIT; + } + if (key === 'angle') { + this.setAngle(value); + } + else { + this[key] = value; + } + }, + + /** + * 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) { + + // do not render if width or height are zeros + if (this.width === 0 || this.height === 0) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + if (!noTransform) { + this.transform(ctx); + } + + if (this.stroke || this.strokeDashArray) { + ctx.lineWidth = this.strokeWidth; + ctx.strokeStyle = this.stroke; + } + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : this.fill; + } + + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._render(ctx, noTransform); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + 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; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @method scaleToWidth + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @method scaleToHeight + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * 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() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; - //If width is negative, make postive. Fixes path selection issue - if(this.currentWidth < 0){ - this.currentWidth = Math.abs(this.currentWidth); - } - - this._hypotenuse = Math.sqrt( - Math.pow(this.currentWidth / 2, 2) + - Math.pow(this.currentHeight / 2, 2)); - - this._angle = Math.atan(this.currentHeight / this.currentWidth); - - // offset added for rotate and scale actions - 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) - }; - var mtr = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }; - - // debugging - - // setTimeout(function() { - // canvas.contextTop.fillStyle = 'green'; - // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); - // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); - // canvas.contextTop.fillRect(br.x, br.y, 3, 3); - // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); - // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); - // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); - // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); - // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); - // }, 50); - - // clockwise - this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; - - // set coordinates of the draggable boxes in the corners used to scale/rotate the image - this._setCornerCoords(); - - return this; - }, - - /** - * Returns width of an object's bounding rectangle - * @method getBoundingRectWidth - * @return {Number} width value - */ - getBoundingRectWidth: function() { - this.oCoords || this.setCoords(); - var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; - var minX = fabric.util.array.min(xCoords); - var maxX = fabric.util.array.max(xCoords); - return Math.abs(minX - maxX); - }, - - /** - * Returns height of an object's bounding rectangle - * @method getBoundingRectHeight - * @return {Number} height value - */ - getBoundingRectHeight: function() { - this.oCoords || this.setCoords(); - var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; - var minY = fabric.util.array.min(yCoords); - var maxY = fabric.util.array.max(yCoords); - return Math.abs(minY - maxY); - }, - - /** - * 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) { - if (!this.hasBorders) return; - - var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, - padding = this.padding, - padding2 = padding * 2, - strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; - - ctx.save(); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = this.borderColor; - - var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), - scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper - ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, - ~~(w + padding2 + strokeWidth * this.scaleX), - ~~(h + padding2 + strokeWidth * this.scaleY) - ); - - if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { - - var rotateHeight = ( - this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) - ) / 2; - - ctx.beginPath(); - ctx.moveTo(0, rotateHeight); - ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); - ctx.closePath(); - ctx.stroke(); - } - - ctx.restore(); - return this; - }, - - _renderDashedStroke: function(ctx) { - - if (1 & this.strokeDashArray.length /* if odd number of items */) { - /* duplicate items */ - this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); - } - - var i = 0, - x = -this.width/2, y = -this.height/2, - _this = this, - padding = this.padding, - dashedArrayLength = this.strokeDashArray.length; - - ctx.save(); - ctx.beginPath(); - - function renderSide(xMultiplier, yMultiplier) { - - var lineLength = 0, - lengthDiff = 0, - sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; - - while (lineLength < sideLength) { - - var lengthOfSubPath = _this.strokeDashArray[i++]; - lineLength += lengthOfSubPath; - - if (lineLength > sideLength) { - lengthDiff = lineLength - sideLength; - } - - // track coords - if (xMultiplier) { - x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); - } - else { - y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); - } - - ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); - if (i >= dashedArrayLength) { - i = 0; - } - } - } - - renderSide(1, 0); - renderSide(0, 1); - renderSide(-1, 0); - renderSide(0, -1); - - ctx.stroke(); - ctx.closePath(); - ctx.restore(); - }, - - /** - * 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) { - if (!this.hasControls) return; - - var size = this.cornersize, - size2 = size / 2, - strokeWidth2 = this.strokeWidth / 2, - left = -(this.width / 2), - top = -(this.height / 2), - _left, - _top, - sizeX = size / this.scaleX, - sizeY = size / this.scaleY, - paddingX = this.padding / this.scaleX, - paddingY = this.padding / this.scaleY, - scaleOffsetY = size2 / this.scaleY, - scaleOffsetX = size2 / this.scaleX, - scaleOffsetSizeX = (size2 - size) / this.scaleX, - scaleOffsetSizeY = (size2 - size) / this.scaleY, - height = this.height, - width = this.width, - methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; - - ctx.save(); - - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); - - ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; - ctx.strokeStyle = ctx.fillStyle = this.cornerColor; - - // top-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // top-right - _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // bottom-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - if (!this.lockUniScaling) { - // middle-top - _left = left + width/2 - scaleOffsetX; - _top = top - scaleOffsetY - strokeWidth2 - paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-bottom - _left = left + width/2 - scaleOffsetX; - _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-right - _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - - // middle-left - _left = left - scaleOffsetX - strokeWidth2 - paddingX; - _top = top + height/2 - scaleOffsetY; - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_left, _top, sizeX, sizeY); - } - - // middle-top-rotate - if (this.hasRotatingPoint) { - - _left = left + width/2 - scaleOffsetX; - - _top = this.flipY ? - (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); - - ctx.clearRect(_left, _top, sizeX, sizeY); - ctx[methodName](_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(); - - /** @ignore */ - 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') - }; - - // normalize angle - this.set('angle', 0).set('flipX', false).set('flipY', false); - this.toDataURL(function(dataURL) { - i.src = dataURL; - }); - } - return this; - }, - - /** - * Converts an object into a data-url-like string - * @method toDataURL - * @return {String} string of data - */ - toDataURL: function(callback) { - var el = fabric.document.createElement('canvas'); - if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { - G_vmlCanvasManager.initElement(el); - } - - el.width = this.getBoundingRectWidth(); - el.height = this.getBoundingRectHeight(); - - fabric.util.wrapElement(el, 'div'); - - var canvas = new fabric.Canvas(el); - canvas.backgroundColor = 'transparent'; - canvas.renderAll(); - - if (this.constructor.async) { - this.clone(proceed); - } - else { - proceed(this.clone()); - } - - function proceed(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; - - callback && callback(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; - }, - - /** - * @method setupState - */ - setupState: function() { - this.originalState = { }; - this.saveState(); - }, - - /** - * 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) { - // extracts coords - 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 of another object - * @method isContainedWithinObject - * @param {Object} other Object to test - * @return {Boolean} - */ - isContainedWithinObject: function(other) { - return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); - }, - - /** - * 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); - - 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) { - if (!this.hasControls || !this.active) return false; - - var pointer = getPointer(e), - ex = pointer.x - offset.left, - ey = pointer.y - offset.top, - xpoints, - lines; - - for (var i in this.oCoords) { - - if (i === 'mtr' && !this.hasRotatingPoint) { - continue; - } - - if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { - continue; - } - - lines = this._getImageLines(this.oCoords[i].corner, i); - - // debugging - - // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); - - // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); - // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); - - 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]; - // optimisation 1: line below dot. no cross - if ((iLine.o.y < ey) && (iLine.d.y < ey)) { - continue; - } - // optimisation 2: line above dot. no cross - if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { - continue; - } - // optimisation 3: vertical line case - if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { - xi = iLine.o.x; - yi = ey; - } - // calculate the intersection point - 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; - } - // dont count xi < ex cases - if (xi >= ex) { - xcount += 1; - } - // optimisation 4: specific for square images - 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) { - 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 = degreesToRadians(45 - this.getAngle()), - cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, - cosHalfOffset = cornerHypotenuse * Math.cos(theta), - sinHalfOffset = cornerHypotenuse * Math.sin(theta), - sinTh = Math.sin(this._theta), - cosTh = Math.cos(this._theta); - - coords.tl.corner = { - tl: { - x: coords.tl.x - sinHalfOffset, - y: coords.tl.y - cosHalfOffset - }, - tr: { - x: coords.tl.x + cosHalfOffset, - y: coords.tl.y - sinHalfOffset - }, - bl: { - x: coords.tl.x - cosHalfOffset, - y: coords.tl.y + sinHalfOffset - }, - br: { - x: coords.tl.x + sinHalfOffset, - y: coords.tl.y + cosHalfOffset - } - }; - - coords.tr.corner = { - tl: { - x: coords.tr.x - sinHalfOffset, - y: coords.tr.y - cosHalfOffset - }, - tr: { - x: coords.tr.x + cosHalfOffset, - y: coords.tr.y - sinHalfOffset - }, - br: { - x: coords.tr.x + sinHalfOffset, - y: coords.tr.y + cosHalfOffset - }, - bl: { - x: coords.tr.x - cosHalfOffset, - y: coords.tr.y + sinHalfOffset - } - }; - - coords.bl.corner = { - tl: { - x: coords.bl.x - sinHalfOffset, - y: coords.bl.y - cosHalfOffset - }, - bl: { - x: coords.bl.x - cosHalfOffset, - y: coords.bl.y + sinHalfOffset - }, - br: { - x: coords.bl.x + sinHalfOffset, - y: coords.bl.y + cosHalfOffset - }, - tr: { - x: coords.bl.x + cosHalfOffset, - y: coords.bl.y - sinHalfOffset - } - }; - - coords.br.corner = { - tr: { - x: coords.br.x + cosHalfOffset, - y: coords.br.y - sinHalfOffset - }, - bl: { - x: coords.br.x - cosHalfOffset, - y: coords.br.y + sinHalfOffset - }, - br: { - x: coords.br.x + sinHalfOffset, - y: coords.br.y + cosHalfOffset - }, - tl: { - x: coords.br.x - sinHalfOffset, - y: coords.br.y - cosHalfOffset - } - }; - - coords.ml.corner = { - tl: { - x: coords.ml.x - sinHalfOffset, - y: coords.ml.y - cosHalfOffset - }, - tr: { - x: coords.ml.x + cosHalfOffset, - y: coords.ml.y - sinHalfOffset - }, - bl: { - x: coords.ml.x - cosHalfOffset, - y: coords.ml.y + sinHalfOffset - }, - br: { - x: coords.ml.x + sinHalfOffset, - y: coords.ml.y + cosHalfOffset - } - }; - - coords.mt.corner = { - tl: { - x: coords.mt.x - sinHalfOffset, - y: coords.mt.y - cosHalfOffset - }, - tr: { - x: coords.mt.x + cosHalfOffset, - y: coords.mt.y - sinHalfOffset - }, - bl: { - x: coords.mt.x - cosHalfOffset, - y: coords.mt.y + sinHalfOffset - }, - br: { - x: coords.mt.x + sinHalfOffset, - y: coords.mt.y + cosHalfOffset - } - }; - - coords.mr.corner = { - tl: { - x: coords.mr.x - sinHalfOffset, - y: coords.mr.y - cosHalfOffset - }, - tr: { - x: coords.mr.x + cosHalfOffset, - y: coords.mr.y - sinHalfOffset - }, - bl: { - x: coords.mr.x - cosHalfOffset, - y: coords.mr.y + sinHalfOffset - }, - br: { - x: coords.mr.x + sinHalfOffset, - y: coords.mr.y + cosHalfOffset - } - }; - - coords.mb.corner = { - tl: { - x: coords.mb.x - sinHalfOffset, - y: coords.mb.y - cosHalfOffset - }, - tr: { - x: coords.mb.x + cosHalfOffset, - y: coords.mb.y - sinHalfOffset - }, - bl: { - x: coords.mb.x - cosHalfOffset, - y: coords.mb.y + sinHalfOffset - }, - br: { - x: coords.mb.x + sinHalfOffset, - y: coords.mb.y + cosHalfOffset - } - }; - - coords.mtr.corner = { - tl: { - x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) - }, - tr: { - x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - bl: { - x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) - }, - br: { - x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), - y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) - } - }; - }, - - /** - * 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; - }, - - /** - * Returns a JSON representation of an instance - * @method toJSON - * @return {String} json - */ - toJSON: function() { - // delegate, not alias - return this.toObject(); - }, - - /** - * @method setGradientFill - */ - setGradientFill: function(options) { - this.set('fill', fabric.Gradient.forObject(this, options)); - }, - - /** - * @method animate - * - * As object — multiple properties - * - * object.animate({ left: ..., top: ... }); - * object.animate({ left: ..., top: ... }, { duration: ... }); - * - * As string — one property - * - * object.animate('left', ...); - * object.animate('left', { duration: ... }); - * - */ - animate: function() { - if (arguments[0] && typeof arguments[0] === 'object') { - for (var prop in arguments[0]) { - this._animate(prop, arguments[0][prop], arguments[1]); - } - } - else { - this._animate.apply(this, arguments); - } - return this; - }, - - /** - * @private - * @method _animate - */ - _animate: function(property, to, options) { - var obj = this; - - options || (options = { }); - - if (!('from' in options)) { - options.from = this.get(property); - } - - if (/[+\-]/.test((to + '').charAt(0))) { - to = this.get(property) + parseFloat(to); - } - - fabric.util.animate({ - startValue: options.from, - endValue: to, - byValue: options.by, - easing: options.easing, - duration: options.duration, - onChange: function(value) { - obj.set(property, value); - options.onChange && options.onChange(); - }, - onComplete: function() { - obj.setCoords(); - options.onComplete && options.onComplete(); - } - }); - }, - - /** - * Centers object horizontally on canvas to which it was added last - * @method centerH - * @return {fabric.Object} thisArg - */ - centerH: function () { - this.canvas.centerObjectH(this); - return this; - }, - - /** - * Centers object vertically on canvas to which it was added last - * @method centerV - * @return {fabric.Object} thisArg - * @chainable - */ - centerV: function () { - this.canvas.centerObjectV(this); - return this; - }, - - /** - * Centers object vertically and horizontally on canvas to which is was added last - * @method center - * @return {fabric.Object} thisArg - * @chainable - */ - center: function () { - return this.centerH().centerV(); - }, - - /** - * Removes object from canvas to which it was added last - * @method remove - * @return {fabric.Object} thisArg - * @chainable - */ - remove: function() { - return this.canvas.remove(this); - }, - - /** - * Moves an object to the bottom of the stack of drawn objects - * @method sendToBack - * @return {fabric.Object} thisArg - * @chainable - */ - sendToBack: function() { - this.canvas.sendToBack(this); - return this; - }, - - /** - * Moves an object to the top of the stack of drawn objects - * @method bringToFront - * @return {fabric.Object} thisArg - * @chainable - */ - bringToFront: function() { - this.canvas.bringToFront(this); - return this; - }, - - /** - * Moves an object one level down in stack of drawn objects - * @method sendBackwards - * @return {fabric.Object} thisArg - * @chainable - */ - sendBackwards: function() { - this.canvas.sendBackwards(this); - return this; - }, - - /** - * Moves an object one level up in stack of drawn objects - * @method bringForward - * @return {fabric.Object} thisArg - * @chainable - */ - bringForward: function() { - this.canvas.bringForward(this); - return this; - } - }); - - /** - * @alias rotate -> setAngle - */ - fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; - - var proto = fabric.Object.prototype; - for (var i = proto.stateProperties.length; i--; ) { - - var propName = proto.stateProperties[i], - capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), - setterName = 'set' + capitalizedPropName, - getterName = 'get' + capitalizedPropName; - - // using `new Function` for better introspection - if (!proto[getterName]) { - proto[getterName] = (function(property) { - return new Function('return this.get("' + property + '")'); - })(propName); - } - if (!proto[setterName]) { - proto[setterName] = (function(property) { - return new Function('value', 'return this.set("' + property + '", value)'); - })(propName); - } - } - - extend(fabric.Object.prototype, fabric.Observable); - - extend(fabric.Object, { - - /** - * @static - * @constant - * @type Number - */ - NUM_FRACTION_DIGITS: 2, - - /** - * @static - * @constant - * @type Number - */ - MIN_SCALE_LIMIT: 0.1 - - }); - -})(typeof exports !== 'undefined' ? exports : this); + this._hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + this._angle = Math.atan(this.currentHeight / this.currentWidth); + + // offset added for rotate and scale actions + 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) + }; + var mtr = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + // clockwise + this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords(); + + return this; + }, + + /** + * Returns width of an object's bounding rectangle + * @method getBoundingRectWidth + * @return {Number} width value + */ + getBoundingRectWidth: function() { + this.oCoords || this.setCoords(); + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + return Math.abs(minX - maxX); + }, + + /** + * Returns height of an object's bounding rectangle + * @method getBoundingRectHeight + * @return {Number} height value + */ + getBoundingRectHeight: function() { + this.oCoords || this.setCoords(); + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + return Math.abs(minY - maxY); + }, + + /** + * 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) { + if (!this.hasBorders) return; + + var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, + padding = this.padding, + padding2 = padding * 2, + strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), + scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? 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 - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX), + ~~(h + padding2 + strokeWidth * this.scaleY) + ); + + if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + _renderDashedStroke: function(ctx) { + + if (1 & this.strokeDashArray.length /* if odd number of items */) { + /* duplicate items */ + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + var i = 0, + x = -this.width/2, y = -this.height/2, + _this = this, + padding = this.padding, + dashedArrayLength = this.strokeDashArray.length; + + ctx.save(); + ctx.beginPath(); + + function renderSide(xMultiplier, yMultiplier) { + + var lineLength = 0, + lengthDiff = 0, + sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; + + while (lineLength < sideLength) { + + var lengthOfSubPath = _this.strokeDashArray[i++]; + lineLength += lengthOfSubPath; + + if (lineLength > sideLength) { + lengthDiff = lineLength - sideLength; + } + + // track coords + if (xMultiplier) { + x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); + } + else { + y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); + } + + ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); + if (i >= dashedArrayLength) { + i = 0; + } + } + } + + renderSide(1, 0); + renderSide(0, 1); + renderSide(-1, 0); + renderSide(0, -1); + + ctx.stroke(); + ctx.closePath(); + ctx.restore(); + }, + + /** + * 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) { + if (!this.hasControls) return; + + var size = this.cornersize, + size2 = size / 2, + strokeWidth2 = this.strokeWidth / 2, + left = -(this.width / 2), + top = -(this.height / 2), + _left, + _top, + sizeX = size / this.scaleX, + sizeY = size / this.scaleY, + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // top-right + _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + if (!this.lockUniScaling) { + // middle-top + _left = left + width/2 - scaleOffsetX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-bottom + _left = left + width/2 - scaleOffsetX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + + _left = left + width/2 - scaleOffsetX; + + _top = this.flipY ? + (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_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(); + + /** @ignore */ + 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') + }; + + // normalize angle + this.set('angle', 0).set('flipX', false).set('flipY', false); + this.toDataURL(function(dataURL) { + i.src = dataURL; + }); + } + return this; + }, + + /** + * Converts an object into a data-url-like string + * @method toDataURL + * @return {String} string of data + */ + toDataURL: function(callback) { + var el = fabric.document.createElement('canvas'); + if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(el); + } + + el.width = this.getBoundingRectWidth(); + el.height = this.getBoundingRectHeight(); + + fabric.util.wrapElement(el, 'div'); + + var canvas = new fabric.Canvas(el); + canvas.backgroundColor = 'transparent'; + canvas.renderAll(); + + if (this.constructor.async) { + this.clone(proceed); + } + else { + proceed(this.clone()); + } + + function proceed(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; + + callback && callback(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; + }, + + /** + * @method setupState + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + }, + + /** + * 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) { + // extracts coords + 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 of another object + * @method isContainedWithinObject + * @param {Object} other Object to test + * @return {Boolean} + */ + isContainedWithinObject: function(other) { + return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); + }, + + /** + * 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); + + 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) { + if (!this.hasControls || !this.active) return false; + + var pointer = getPointer(e), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xpoints, + lines; + + for (var i in this.oCoords) { + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner, i); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + 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]; + // optimisation 1: line below dot. no cross + if ((iLine.o.y < ey) && (iLine.d.y < ey)) { + continue; + } + // optimisation 2: line above dot. no cross + if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) { + xi = iLine.o.x; + yi = ey; + } + // calculate the intersection point + 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; + } + // dont count xi < ex cases + if (xi >= ex) { + xcount += 1; + } + // optimisation 4: specific for square images + 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) { + 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 = degreesToRadians(45 - this.getAngle()), + cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2, + cosHalfOffset = cornerHypotenuse * Math.cos(theta), + sinHalfOffset = cornerHypotenuse * Math.sin(theta), + sinTh = Math.sin(this._theta), + cosTh = Math.cos(this._theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + + /** + * 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; + }, + + /** + * Returns a JSON representation of an instance + * @method toJSON + * @return {String} json + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + }, + + /** + * @method setGradientFill + */ + setGradientFill: function(options) { + this.set('fill', fabric.Gradient.forObject(this, options)); + }, + + /** + * @method animate + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] === 'object') { + for (var prop in arguments[0]) { + this._animate(prop, arguments[0][prop], arguments[1]); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @method _animate + */ + _animate: function(property, to, options) { + var obj = this; + + options || (options = { }); + + if (!('from' in options)) { + options.from = this.get(property); + } + + if (/[+\-]/.test((to + '').charAt(0))) { + to = this.get(property) + parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + onChange: function(value) { + obj.set(property, value); + options.onChange && options.onChange(); + }, + onComplete: function() { + obj.setCoords(); + options.onComplete && options.onComplete(); + } + }); + }, + + /** + * Centers object horizontally on canvas to which it was added last + * @method centerH + * @return {fabric.Object} thisArg + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last + * @method centerV + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * @method center + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + return this.centerH().centerV(); + }, + + /** + * Removes object from canvas to which it was added last + * @method remove + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + return this.canvas.remove(this); + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + this.canvas.sendToBack(this); + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + this.canvas.bringToFront(this); + return this; + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function() { + this.canvas.sendBackwards(this); + return this; + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function() { + this.canvas.bringForward(this); + return this; + } + }); + + /** + * @alias rotate -> setAngle + */ + fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + + var proto = fabric.Object.prototype; + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + + extend(fabric.Object.prototype, fabric.Observable); + + extend(fabric.Object, { + + /** + * @static + * @constant + * @type Number + */ + NUM_FRACTION_DIGITS: 2, + + /** + * @static + * @constant + * @type Number + */ + MIN_SCALE_LIMIT: 0.1 + + }); + +})(typeof exports !== 'undefined' ? exports : this); From f11f83b0db92634428be438b2ccf9eee14376ed3 Mon Sep 17 00:00:00 2001 From: Steve Pemberton Date: Sun, 14 Oct 2012 18:30:08 +0100 Subject: [PATCH 3/3] Fix for negative width on objects --- dist/all.js | 5 +++++ dist/all.min.js.gz | Bin 42880 -> 0 bytes src/object.class.js | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/dist/all.js b/dist/all.js index e17e8ce2..b21c0304 100644 --- a/dist/all.js +++ b/dist/all.js @@ -8265,6 +8265,11 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, { this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + //If width is negative, make postive. Fixes path selection issue + if(this.currentWidth < 0){ + this.currentWidth = Math.abs(this.currentWidth); + } + this._hypotenuse = Math.sqrt( Math.pow(this.currentWidth / 2, 2) + Math.pow(this.currentHeight / 2, 2)); diff --git a/dist/all.min.js.gz b/dist/all.min.js.gz index a46582abbedbc1d1f819ef231598c8562514a1d9..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 42880 zcmV(#K;*w4iwFo140=!i17U1zE^TRUE^2cCq&jPN+Q^dM=T~5Du17LLV9(6m4RXiJ z!{A4pWX8_Scx7@=0}aRsiAVx0K=-%bs_xbcaFTQOgN?dXRb5?O?^gAs)S-Rf9vII- zspZWh-yToG(pjzcKb5oE>6u>oIYGvc4fOmZ?!{G&@d8Al02C5g+Ld8-~lbqhD0}XZOZRy^gd8J$1br&vk4Fq~`511RUVF~g5 zQaE9yyX%LNBc>5VC{SxL@?4^h7VZGl&Q-My&Q3{g!e`;acS}>lXLE-Rm_g@rCsN&o z~(aCiW;)HM6B^+9u{ozk4!tIJ{iK!+Sdjnalh= zHQj?y71&7&jvVJWw~F4-`383M=>Y1Sj#)7ARv8M~)KL-5n=_vtAMY~4Wp^KfMrxb9 z1e@0DkiCnBs?`G)KG-wnEkZRt8Dl^lhcW%Mrp21d*?67IJ~P5J?!(Ao_Y=m#B1@qk z?a2`>4@UO`-*X&Qao8vX0|fTs0SyNewWV#o$)^@`K0UP{EL}m30dZ=kRLGQ#7FUs@ zjFQ9LaX8U7xF&Zm>kH6~dSuKO0j?j56}Y-Nps}s{d$PH=*Q}n>Tp0MmoRFRmQ+`>Lt55lPrp2Go8|JQ(CLmbv;$msczoOe znhqTCdvC@377duHL44rM1ZbHMdCTQ)e8|9q)O-`;cn4lnJe9lPSrS9}o&$;Zt*^~F z1;uQq)+SD|E$vJXL;wvk9c$=oA&}^mP%Pso$a+p)2>*No=++~v(SxvDdlD?Hi@(6 z>&hw2m+?iMXlhfe)@B3Gr?^tKWLuZ63VvFIn_*&qC@rDE8y>Mik64|DSZ|php@OkW zIPty3c%tyRI8~^Kv`uhw)CbXFs&Rar9~mzEdjQ2Iaj^`^{+!Qv7WWZhY;fSa4t^gvEXErr_ zd3g5|3PB+6cZ4Wga8R!2f;pyM3q>L}2hN~PHZdMEshPrARMr0i7d@&~-uB8{bk@D{ zlWLq?JZW;IZPm(!^)xhkW$DYctzNaqslRFnf7Q~2YJi&u9&})!ndrUASA{h*RY702 zX&YQ11V`dk+p-jIH2NFx;pY*4`p&{vde*vJ?)3ohtOrJv5l(eX(>bZUKvI+1#GnOY zrFD|Dsnsw3xKE!{+^eRemlk`}p0ui7FbWWC1gQxf*kP29d#z-71t_UXxlvX$C36T7 z@PsGz;8QmMjhgKQ)8ss@4Y&j555R%dmC*Z)1N3H!?x15!=2I`H#8#B4UP@ijFuEoI zPw9d40Yi{0mcC}v;952U5r8o8M?}i86U`?9Oo<0Exq|UZJoaH_%m{=4uE7;WmVz-e zr~tneHdA`&5KT#aW;>w2mIBFiSP?^|SM=3#35H%J1r0Q`EAV(|@f?BwZDJn;p@qoCR| zo;}-D!SMnES&op1nFa$aBP-eI$*JtnhHM{3&SgMy3zZzvavnQQHIAhv4mupN@%oPa zbpfhuhZL$VMHjzyMZQpE$_AxFA-zM-D-;1vJ~sfeng%1GF+I|d_+S|s{-xd#TtJos zxx{w6wj%h&2SX?kJ_%0nN%De$^{8F+pdGRi81-LkHNEdS!}@>MYO&P;VyVPBTvCAD zhWQj92hsy?PNBkk`?ijV|2VCI+FhYnZs`rgY8E4TlXTv}Vi+Ylg#1>6nA;&Zk;JDi z#wXyjn^|Qy;ozvX-FDA;VD0i!s{!M84a@+$@s)6(U0(fk5DJnl{)`YE5ayK;K#G#t z)Qx_f>_=^0s5kFA$H&+2I@-m}`E~u~-F02PImfAUoPr@tYZn#RZRQZ$))m+p`0rFl z=Aa|#prN*Okvp|8*AJ=f2B`?MDNGD0ba z!?`$jGB0T{!L7Gu<~H0+Po-%L79)EIb}64oxg5z273{E?h##?2_hhxur11xhIk!5+ zqAOh=Ob3Sr%|qlv0GjO5m{RX&FF!S@vHzmLmsPH8ojfV zHQ8-3!H(8qM+nzMlSn~$0fkmc@>6J~ahfrwtIini~t~1pyPeIiP8R*3g=wHWJxT3eBC_L0c68fg_o`+$4nZ8A^|{)yx;+qA7b0j~K6BCOfDc6|GI^!(tpdm=ZGgm1McXO zah?}Q9amO26?sGF+7yA1oBBGpuxMR1jUf$b?s}>FujR~XQFlp$JL=Lg-fa}Fg)Yfu zR*gRS)v>NTxk67a@uow`u_L1A44AmgIKTvfSJ_!hL(&rmjA2fQWd~*k9$J9#sE@=0 z?Aa7bC8>c#b_#|(IznE+M}0n;%h3{YSpKj=Co#c)A=wDQi8c+733YMVk;RAlhSvFc z>2tj1!#O_IF)JB_P|g>7-WZzK4uf!AoPPd&!F({n13g>u3O$;(hc@({z2js24-Orm z_95Q9IY{))o5KWOf!+a5{SIIBUT(GFI0v!6ha`!QG(B;Sb?KV&eIns0MPJq>8xBzC z=d2GVfHi=;FsLg4F$Hs7$hRnGhEhPBT6-Ez?}=0yG_*Q{)9o~lTbPvKX9p5+r$h#laDg-$A9J$ZT^ zFtb9@DFPm<_*WQ0po?TrgVzfaWNd@~IS9n~7zdcR!D;NQm`e=wpOv4>>oWY8tMe`A z7S{Pj)?}=P(!L?D5iI96mbsX4O@o|10lfxX8lN+c7xWs2JMNrB`O`i27 zPJFwSi#M_r(T`#p%0jk-%5&eOqz49vz;fMl~HRW@e7cPfviLg z#U0!;q-|^7U*FMEjwv&o9fgUwoFgr_aP*2mqk=%AR|Q&tOH}t+L>OR49A!XO@Qiv9 zkO@3NfchxY-~E&wSmdZdzVzX3`p}AUt^H}7Jz>{}-(&9>jH(QHI8yZ8UKu3zDd??YyS&Zbcf0}K<{-+L9MR+BJ)g!a^6(Dt^@-A?gK3LTDBEpy(wC3i<;; zVQf?tgXbR=A*c;vSFoY)hV@$fzcV(pX(<34=0eEw@O}(-u_+=+vDJmp%;HeU(45Oo zMV)se>?JetZqtUurb79(%*-ARVIyUAe4!|H#S|S-yplbgznGko=*8h!(aK6(V4@^P z)Cp65Dl>0gNx1M{4l~9Zi}ji*iPuaF)*}Z($OJfIZVW8Zi=voHa^riOih=WQL(+cW zIg1b=aSKwwJQ>&#>KvRa*kZj_ITkLt?gjA0XTPO7Rk^Or9S@0!2h<;{r(S*2r+v5kP4LE??4faj38xVZVMfh(R0 zqBG)HXFZt*;w6q5tZbBk!!K;`GD2A@b!JU_WHYNPeYUot&|UaT_TVWxty^CC-~XOr zKi%aIr#)TFoFy~A;0(d1UvY{*{nqQnCcg$V`4WC^2|S;aHc5#SLA{7K%i&>?o8YBr z2eDpqSOdok*e4oJIARfwDRn+7CGw1)EkrbCNxk{3^D#|*Kg$ZMzUu0;EFi)W=+DGW z0^;=iIXeYLTp(l4ES8a;=?v+{yZsN^hmzngsPt}hIuPhC;ROUHQNn9p5!z4gNlc+T zeL^HoubtTNF2e9}K*+l-vCD9k@q;`va*G>v#QA zPn2JDoQuGPdp*^?xG}(E)!~%2#4u$HX1rIxx140xcjQ!mk0RRHAsrvsBFKr3IV*6M zSbJ{n%4L)ud$-0sl*R8wG6?DbYZXi0CMuPff`-jPAxB=vB%v!nOfW-1j^+sybTSmizbV!ql=^uKV2 z^Jkf;SK{uh(uN$VOs@FtnkZuRIkM}*$&(v%PHu8d3Xxk4S0xXjcb4Z$zeJ{tI$-eK@o0)(7B zA7%#l5S)kPIRg%I8;2zYo0@Dg!g*q-yAt9!)lM?ljKAt{+KGCH9ld?;CS>z`;Ej}r z8l2o|<<0>BhnFRH2i}BUNS*g7U!;SY2d;xo;$%&>H zhTaU=fqe)Je(poEzdSk)7;d+7j6*FkQk64Elk{6uibQ`7u#R!9A?~zO`JLPz0PN!d zQ{QGXd?b!$b%3ElDAK_d{juYUR>HM&4-YBF$*nFPPvNl_)1eX2C4T*YPmu02^;W;- zBVHBZ)udW!RaEt~dV3=EU8|mHWmWLP%SH?KBl;0Nj3~eg%6Pl2X?o=va9`7a>g^AZ zdhq5*iSvYd@fSa44mMzxl75Ya-vA7zpf`ciGqZi=J~?r88x#fJguXWwpUe^)LHkga zx|n1MIfzV6t?5p>C=8h#3T#z0?|peBpbQU%53jIIvw&nGy&}PoCO6lx43qKYT-sl;^Cv|z>%G#h)fI_e*+;7@`C%LcV3MUD1$?fSi&BU*w zgiGO5C%5TM-VT|8xS~Bor+RjPQuPdIrI!cPL2jq{Xz!du`U>IRNyEL9g?s8kyyu8B zk~taBpn}86J0{_+UPxpm-%D0bMSetWlE}w=kl{S|DS2V9bnn>VsJ^grvyYjSEYinz(zE%a=6G#H#E06#e zh4gR#&CGs>pjX@TJV$P_pR=gKPN*4tCBGwNTnDRtPDAdW3oiZC6d=rwtT(uNoIH+jW&=0S!w;U zUfvfRRExyoia9;z5@fi-{_7AIUaFLBlEELY7-T(wtBH7_w$nTgCj%_J&o*Q_)1 zt-SMLt4+0?Qu}HrY*g&GQfcN4>aFIN=2p4CN%?~Mlit&Icbe5sTDG&Ok)G{zJvaqj zaBDT3W@ce*@b;nt9c`-?E;8DWMi-Fyy>32bK zp1?+~vvBHIh4Kn9qpn~JW3mY(r6dxxpdi%plB}d=-Xs{S#m%4J)=8I$r+~Q4X0PlGbr}7R8GQuFYhVrVIJMl?PL z8g1vJNwf&^#v&PIX`aTL#wcC1F`tXQ!w-%I%C_NbvO?R(ke|1KBPPKvNizn0%-w&5 zQ53htC@q{F6KZe(A3p2dnOC+UsD$<}Fpawl#ta7?3IB#-AsZ3lY`)L03q}aH!^|+(d z_+_>GOaGn!%)j=(^Z)Dr#Fx7chJMlT3ejz zLG^M$%^BLw1zkEId|It3f$bFp~~$-Xl>uHjiI!C z%ofI5%+%z%52V`C|J_i1#+=fAWWQg;^z;LJz%Q;m;SaAnkLAhkcX%x6VyA{K_N7wA zb{?N-VRgYDApebQQYXKZ;AySv>a5P_+LKpZPY-*q&KhKZ@v96m-^2g4HpcAz%)igB z4eOTDE53k|zrz1t*efZKy}0@X1hN<2VGpZ>IJ&&*y}*OZ%ae1<-aABfwq}3*IpnVm+~s$E^!%SqbibC$>AVh-N?p#qKNc5! zoy@5}syoYk89OvQ7~02;<@Vlox`ag{1KJxQ6(h~(y-6}#c&IL4?&K5fliONtD__0i@e;6Fa&G>69-zR~5EO z6gLlb!(*P%FD(^6ymbVAXpa1(^#>*Kq2w zYvla|s|_67>z5UI2!@Mt)XP%F_bEu=l+!LZ2P+o{xRrS?V0xAz&wC>;r}N96r-S91 zr_=NYX*!RB+J&2L1U0e!p(T{q5z~!!PLf%l`Im zt2JuSpBR7d{+nLz(#uyZdie@o;wZ-7IL0LS6Av!New2@b6$(h$X}|CE_t@3`GVbrc zj^W{(__C$G(P?>Zl5_P)h7tEiPud6EQ zn`;ZrCc_c+f`M9`IGc@__E_zX5Y(+xcO9H{ryexX&HG!}Q15TtmZ$CiAk2`% zcH;}L@ai;T1rNUJlO3cXvH6p99^zd!1Sj!l{?6xO4Bnp(8R8y}cz8bdd9WB>z6Vul zFQ-Hm+KCszwT|MAICh34c6+(tb<6_91jeERc&w}g>HHELhsnp`P>xp!PdBr7;qfN0 z+7<}s6V3~8&#)!&t}39GC|;&3aLwP^4z2?ZuQ=q)?X*QRkRcnfD)G_+Lv37}1$-g` z0o5!3^L56_z9VJ;9cQ5UNh}vh07ARRTSJWXcqg~+XIb!e0C*GEKsd66xQe1)f_9KA zk&x8!Zjb1*mkbz^N+K|eH}(m>9TO$g-4X^HMsMJ>0djZ%KCI&yWGQ~7-51HQh~g8{ zSlxj-PR}WqL?LV7bf7T9Vp`3J5@fVeq$Q|B;8TqeiR1kvv#1D^lZ{5C0IkP)!d3~ zLBHxshRhT5b=w#T2Jgz!Yl>m@Fw0|x zpo|LBuuFDwyO`>I^E#N%jAd9v+JnvH6E+R+A7K}QNG4FHw=ESJWud3=j?a1+VW7TS zG8uVB;82YD+*X5lk)hJ?8P@l`B`-kVkU~NN<$I)1XJn6%BEo3zpPoBDaoeZMseknP z1HXMJ`Z>mH)4dzF6#H(s-9stpVi+&8&EUB0wHn_9jre}!(cth!f&S*se;iL&ae9AX z`DH&F+;NVmtUuBpY!8b3acm!b z4{POj`aQ3XWq`uH!<0pUVApY-I>L?dc)XI7Bqqj^4Z1Y%Nbl1ETY^8DSzf$dZi2nZ zGDPEeWw*d1p*UR6Q!9Q;p#mZX+0xF?tOSRH9JYTu>iqd< zWX4L!MUcXBGM-=27F@co&ge?A)XRg=W-eK5_25yMX#YjP&6B0swRz)HwtE%F0Tv6VdppK;q z(b&ZJ6vjpR)L+V5HK_^+O1!nTwMOUWg7K#Q;RzjGggz?i+N#zd&4Yo=XjLtBQ%2; zmcF@bTvNt)b}A=CCH4MzboUtAMO-_s<9fn`B%LH%vvFHnF3Y4tbUuqm|hzPoSgq;|{SGACMwb^)2tzJw8711Q6{^nN^U8;cGO z>;*PatEorV)6{e;gW;qmo`iiOy|?SG+HTqa>dD=W`_^Ch*Z#^QT7t~uagCun32`EP zFz-UXDVK|4hU#rkxYH_17Z=zu%rxx!_m|0nX66xI*#taVg|4IFEZIE2Nzq06?ndR( zo3)moD<lA+8%-H1ll4mVCo83op&^NQt}hb3+H4x|>qCfS$R; z3(e99P&G%}ab@0$e5En)k>?F`SHkP2UPp;(LC?^0?yhCrh&A3k%wTC=Fm-lbHDS$u zh_QY2fz03Xtzt)(0LxWp!%|lylW^1w^0&(oS~O$WmbefP4XC!Ry~Db<9kA;TKvxJ) zE=sX$c$8mQo(X_SU>mh4i`cK1U7R?97xe3ToTb|pp}n2F&}byw!q(GJoYJQZ3|TU;{4Vdtw6> zL`2cvd~_cXq;l^b9nSFV+0xs|CDls!!L2N0VyxUmIhLx}N0$R4_XG$Ey=9ExsF>8M z)$osddC~ItV}qWtq?_D>q2LvvvOo|Fb9A&RNB~Ukv8W7U%pkSZP1?%*6*Y{SX!1gN zs}o-VsiAa*?rylbO=`7CFGhXWSg$qexC^ZpRkJ15fz#O%8G5!R{#Xm*F{~#Mmk$lJ zpTI7j6J~5)I~-R6H3p!@0;sVDD(;O3 zXj!M@KZk5Q@W&D|jle1hK_brHp2A4Ue;;GD+U;{FE6GwSf}IMfbi$|UB02>WQ~wD# zKL{oV%!av=43qf=6;RdbXC~gCecIH|n3!(lZbbq!Ir)GKe(sxj#fJzXi^A=w0>M1p zTI%=-sRW{r=$=%rNpSXruPLyQz;sBKyL1+{hiGMN zk1+ajJ19;B3WHKcexz5P5a|vLdcAPKBKVPgjm+5`DI^SCIBuL zQ*_)og#yugYUs}r5TPrR&wZdmM=QsVN9^yn2K5V6QR)np{uyOhtfIdL5b6qzy4xhZ zs_N2o^^Lo6(+EQDdKeVUGs(wn6t&AzvuxCASLy%~_!wR`nT}xPsaObFT$f z$v44rga@7>`PH;TKSJ-QQLu5#J$Rl}k$zgkrUrPZ$H_M{_1cMzEZlO2`Wi`GoN^#v zx~k_UvL}E~V^!2L+SU)!YJ$P6X|qS`fr_;MKB37|;S%*;?UR%tdq}W{DK$%v#=@YC zT}GNPIU8oTQl=ioOao?PWx?e4bb$D`7)}jienlBMD0kkI$0p(nK~oBHL%StWou0w> zRwpm;86Kd~J0c+#)99PiBqriKUmDgFeo*ol5vHcfkOhsMb#1V4qoeHW$^|tgoTaK- z5?OXbB9W<@MPYtvAwZHc^q}$sVkF~CSzKn%mKlF;N*~X#=641iiSP5+L3s(&8j;oW zQH;$An04@VNH0+?Ttk3P0)w??2Fs00EC*x5P;{yVF366(!j!jwJhZ$u=knGN$sXmZ z8TVH=@u`Pk<5{P(s4z1O>SANL1dOd!oqM=`OM?#2iwI*^SfzK>3q{3Jy7hB^&32}n zNDjyT{A%6Faim-MIlHX4>*WJ(%z*9N?JB#;k&6+>?Q*tEub19u7aq5@Zr^(VC^ zkRdwzz-RaH<5uf!^yIs;@~XT*WGX3Bb)=q8Ik8oi{_{^+{&;lFSL~2K{rRU;UjvQ` zwJb*d=w!`aqY=eepmitfUN$H?Bb}A|8L9&F0n2!sYGNnu+E*11lVBOnqpY3z*BSZ< zhu@2Y85J``rkPn!4-~H!a$1Powddz$#k2^ZZfa;Ve|_4{j-P;F9JLz$_SOSF=nWIE z>g2KoPRiLv(%JSZ?`)C2HodJlk`Slw&Vj;bZOPnmRcJ+%IDDrZaNhA9uC26Fy;^Nt z{s!Gmt9r03+z1pM#vYmqBmGv{`}xwIzrZlWG5!qwsc=y@_F+&-ksaQmHU zMTLq1nst;KZ*KJm%Z9(2~^v7 z9g)tL;H}rmPd@>$_P0%&;_VS_{mkD<0t}%M|C(%*y$#SO{t{G@B6GUmB9bLts&(&L z<^Vg=MX6ftx+xeB^{*I5*q9&=ATVSZz>aQ)+eDN<+Npj zF+{L#RRM|3Lgh2g%7s;6W8-lLlYh;@*#bB_0w)GWMK0gsP$aD4RdM--a4P+^Ivc>| zM(9a0zyl!e8YxcLh&S{t$U^%`@Y8V^N-`|z<~gCNOvCkR<*L=`0Hj!i zEw2vyif-(9TpMp*12TD0845|5`O<%WEHBg5$xvQb4CQs@P+nU@p=e8M{bCG624XTl z-&+0zFUW3se0s-OH|e{)QC_R5M-VOTH#r4siQ}S^{=LRt>&9N&L^_T(zwpb;Y}Gkc zsmQ?mMvnYOiT1(p&I@uO$d`$fw8#4Eh`042Xlyt)C6<4tC@({{)|u0B>WQ4*M0svF z;<;8U{wf#j|(-ga~$ba3mSLX%;SO2{1B9s%cW zi-pO~L5rA_JO%RP6)lZeKPZUN{Zh+PEp)g(YiJ{NvOEAYhVi+q3dJ@G}pp{4Cs zAd_Ol+Godjx*nIHn&@MK+Ma{cYTSrt(9hVqUS6x+wiD2y^l9)m-NI&+G~cD^f?d)n zRa|+Oh6hx2Pwuf+^Beew@tj?UhV4rDoVq)?bJ>E0fm{}4MJ6A#&nJK>Mj*1#3;8{b z>DjdzG8bRPs5l;R;;S~v8b_z8)>Mkat9b$Ce8A%eZ=ZxvxjmJUEtK^|fA@w~?%~oZjw!vNBH=soR#KTekJz zx2kZHl$}i8e%O~6iv$W)018I|tmBShmhqvegum)Y;ruP2dR<`56l$bIUhg#k$|uhedkZo%Syr}(tYO)M@|wlo(#;?GyV<_ zP@0MNd*gc^j)9co;x`Mi?7&=mEaj9fKPlbw{=gX~VwJ?~D(*3s5+NkLU~>e;vQTo@ zk+96jMnBR3I*cUi`XlwUYH9S(U27=Uia*Q(caDf9OBkJ>$;9%I-AIHK+Een{LUB@El5OurNUO0sNxyWdjDqK@&d;;h z0n>Icj5!?`d2xaHnmSByG?s^(XiVj6KIV#Lx4TA3Os5Rajr@YNVEn~4nW z{`R?>)zJE=80Fl;&4I8eiM#_R153*RY!sbs^4kj+KN`s|NwiV7(e2V-LOaVjm;SQ$ z{yP77^7-?X<84#o9;TI?t?x$VA1t=_cCWa*C<4vr6QQ%}dRu+&*9v8oh`a`)cq1Gk zp*p)KzN<@l2-3Dfnt@*3#%??9PY!sL0H6*AoKniy_~GF$n)k-xfqaO6=B*!pc32PK zwAOG;{kji3HHiKoo<27(`L`^48vX(ju-o_2OSZVH0umpw5~vO!-T12$1ctVPdZPh> zb#@Ez=x1i56${Ay5o+^vd59o|{p#}ivph*%{ehJFxEKizoj}E+sq2v6xBGeiGmr3r z-&Oi;Qa3KF^Q+Y=k6vK%izzS=od<>(vJKF{@ikI1NI$0gNd$u1=O#>z9kUY*y(eSx zW!+jKsC(I3?$}hhkHh5^pj*qAJrj7UfdRs;7s}KZwf&N&QYdF@L_(p16xYq?YXTuU z={9hFc&4IuRllY|y0xHIsH5&8LffbpR&}AaJ1T2dm)EwI56T>)-2wAwD{EJ`o7%Hh zeY3S#XAHl&#wlH_DxHnPsS^^%t}RM0`{p@yq#LS z4#jgPfWXZgDDr>IN4)d?4`~I z&Wo?pxm(K10U7Vk$xa^1a%yB+<<>8aNQ0vSRZUShTW1sFBQ@URGq(=J&e(5U-Z%+L zd%qS-@BS$R)d;s&&22;-Hmf_TUJPm%!7Wdxr*y>8V`V0fU0>+cz zwD1c*HQ=KwICga^)Va}i$vrF7l)51_BF@x$9w>WyYPj_*I-$xM$O@ zI6&(!wl#NVHn#PHjpc(6bIQA#>q2|8CE*hJhZ>EP%wb;KdzbwYmhbAsZ$tr%d}sIO z9nH*BwLsPFK=gIlyOQ!LZB{3^K>&>$&h&iU8jP#&$4JY@3E$8p)?8vGt$r(FtYC79 zImPv(MiiiP0vGsOe&>0z2}bXpTKixMIM&TxQlsR;n7;+4)L4;HP1sv&N8n5CzFR7k z_kMlnnfv^&d++{HeuvvImX~)CwSBj|6BcWYRTThG)gnH`imUt`2+Dh93&X$O8aQ?p z`A4kGKYtOW%+T|P_HMY3Vyiky0(Z)yzi6N;hTLLKzHa872nfqw2~In~BmO2gJaBquaF8*5m4UyAqR^w$jk_UX`F@7zdFbDPaD7Yk>2V!?-@~K;9h; z_BhAk1S}u%Aj~Fk>P~)iByYp%#*pl5tOV(Mi#yxL_S$#w7T96xp}p~)`FP?m`8)NU zX)Ek6W^j;1r;mC0$L7gNG0jIU?M#r4%{m z_jGvCteumN8gun6Wo|B%E5=o&Uau@m(G6Q*VDYW+#H&6IWHN;>UFB)o%%QNwB7Qu2 zizeH4b@@26V1cFx0XzZX(K?v8Yc_Gm?#g2VAu(MeTX~+k z@|d`Dw)VC=DF>`I0?xWb$4^T55*Rxf4m;0o~jm{qw%(iiZ9*ffsccpZr>R?$3mikwnBr!I4T;W;=k zE=;nJ^$xR2A_8PJXizK*9?_zS-UZMbPCnN>xDh^bZuV}-Ck}dIR~g=c=dd#ZzPXv@ z7$nZJPYnM;#s|u9XTxaqmxpOLH!l9TPHvHNM2ZK14;t|AX8}(+;B<^Pcqx#O>45m? zh*$I3JJ4u83VIjA51F?LoG(;0Og_vx2Sni~2V;Ilt5=_*F6`krIoIZU zO`Ao?`6edVc~F%$99(1W0(QuYOy>0wUpOy3E$y9eFO<@O;b-cToT?T<9!L0-CsY#j zM88&dBG>p)#mQvgmyEArJH84oF+A|^!KJVkDtl;$YXnSH(TA^uBSyt^$HxiwyJrOr zrXlJ0^F$7fGFU8=88|N-|F_hPza4*>$lwFV@y~)h`eQpI?V<{Ta2MkRnA8PNs4X^{Y{}+glUc$!b`EfcDtcwPKd&rNgOG1z ztH#m_=Dgtgi;rvH*tOQMJfZ%dl~ih?YXuqibQd99$5L@3;TTRx$_cEfb1AU>Tz*Z* zZ@Kp@%v-UAR8D=|vSqNaH(wR%9ea4Gl~D{much^^U`hMBum`Jfb2-^LXe*X*TCtJc zxRGE!#vo|4sLKQkZ38#28DQUuJn5W51(YQJbmkY!WnJ9MP9O5n#L+Z3s3 zila>{mMV75=>roVKPwQpMAQfrOsMY)Ul!BRja$$*2;3rKA!g+}wovw;Jg&uF7iC(I znvSB*3>p1McxMVPNDFlVLLNEfN8&_Dh6TxAfq4(Vfa70{9ZBYE1)YUb(AfkFH5~P3 zOg)>=Z}^SQ9Q}dB79@T4uJeTmXDL<54DaQfsm{~Leog9fMhL@@Q;fh(4E3)+;uP|JYp znS2W6{8+*KNLL>aGZbVQn7|L}>nE%y%ow1FH4En!99YD6IFLivh6lpJO%_dFubbI} zo9s=2`~Isu0F2PZY2MhqA{$1SgHdrh&~TY#4Y0#oapmK%Ed{*1;0 zpSDNQ(_}o_bjSDfi;no8qS2^7nMF@iwDqEtKKjpjB7dGg6~vDI?LxkY_7LkjDY)iH zHssR(_on>h7(&Ym#J!EQ=wD|tE>1r~E05OL4e#!?dAv4l)oh;}@g?nyo#V|s4)-(b zeE%UZJjB)H-HT9canE+FGFgXny=M6`s0C+xTz87JLtUp($Tlzpo4e<|4&Ka`-8W4U zz2(Go8BvF+b{a@rmHPf^rH)hWq)~4MHTbiw1t;R3pqAKQE_FZfF1l3P1IXDcIE<1h zAPgKRcK-yM{tWH^wtVuF!#sdn%YDwi{Rqxtb54Ylf{AyO$m7>W9;+sDcvumd#AKBE zk^daCLlP{3&9pL4doa`9^0bd;+Aoy0eF%;Y z48ico^B(}NYNk~^|6_$8+8?PU{sZIn_CIbbwj2TD1R7X3Qrt7IqUU!I@^?Zn$B%ia zxx`&h>a|?~6v&I{HO?4~`48AP=WuEV7G9t$%&%}ErE8Hd4RuQk)&G3V&=Y~c#4 zFa}lhjWuM;X0e2=>8No5;>R4VNf(9-h?{#{*=g?($A=+mpaZbNv!-daM%`Hy^NgGa zzHV)TTfF;O!4&3qnllDZJoK%SSFznaOLYY>J#w`n-vPu9(@H}A5=DB?l*0_cc^b_o zs7U$O`A0s)<9e{@=0~W}S-Mu4j4q6385mCJB@M8n%X660Yc6R84t2c)@2HXo*{+Sz z@EGy>=<@LM-&eLHK0_85Hsr z0;(k~{Fgx@5vX&e(=~<9R3Ej{hZ&;ywqdo54XdRZR*U~&B&7|O2r|_8qR-<+x>R*>e0UI_g^`Tis>7F z=2TwccZ+G;e}(Zo-v|gq%$zqKz{^N7c{O zhQolt%_cxQGIAw7auf!7CFM@s9(#=4{QD8J_5;7mQZBgtK2IgvwV6(y;c|X>eNFp| zA2J*Xz)2j0e|-Arx6K6MJLH~mg=tKs9&1Ol^VW2@nD}>FG;NEAYO*Z3M5<4YvC=gN z+ip*~g6ZwPDd`$kaw+GJVPmKc`*bnkkQv5!;%X-k9c>)bQFjYbFl=;EP9b3s>AMm( zfH!qJg7iXuLo=S_MeIGf5Dg47SGl!YMg)e72lr9?iqKnX=skuYPlcodOB&wmQV_x) zwC|wz4Zd&m`?`$zWu1@j8~sjIyoSoo(DRsjfe9&AXLCQ7*0m*H&}Qqhcr3GxNZpj! zMx<`aOr!K;^tMRjBP>v|D>S7@GCdMq@)1T%YbpUJMm?lkomg@sY*xCYP%e{v)D8n)k8+sH{DEH108Uo4YL3!+6)#!*~Ll z<$y(a_s|QF5#89dSeG0)OBRtE@vM!=0-hyGl`+dH6^lPR$h|~S$}j43aALl6X|7ZT zLA)rpID&~fFuy7eyn3)Yx)6rn|PTaMz|qh6r71 znid@(5QjO{FPQB~f3imJVLF{eq~j*wI&>I4l~LCm(RTPG$c6>zTO~0N&_;o83~+uH z5%6@Oy$g32d8=D|p-*3LX_Qy`{Yx zUWFLygvc4`31UJNjHmhWvQJMvx;IiPBH8+`(gAFy@I4y zKzh8)PHF{3tq`XceNCYxXuui=oDpVr8+t>Aj?$PQitHN&_9b{)VVu)y798S^SSvt9vU>(j^*2To$tWB z2m>+7I(m?6t@kJ!&=uKVf4{tL0vPrgE#E1l)>syhZvek)O%ejM=+iY5((Oi^2|%t^rGaCW zh={{ez`aiXN5?i1%JVj8>7A?<+sSVCPSIb=ETSUb zgLcJEF!#CG&L%LSdEa6tnKYr(GkUOM+~Rcyr#Ku}C&1xr0H>@*0;^e4D7Rn};#Ka; z7d#%}f73yEhoM6OZW+z`fjfkGHp0p3g7gbd8!r!O{0Y-|E}Ujvn2q^f3=$?Kq*;*N zz$h=v7)eLdTQ?_;Gu= zlJ|3`3G8Yo2Mp0fA0Ntan~zMlD|(qOv{aYtxpgO}gUz0AXu4>2qVlZU%o++p`P&3EK? z*d6Bgcje!~vI6beEHh!LO~^o!o+QKcxQ!xLw3XhrDEr^@=|+6}BQjzmq-WXz>B-#s;w7vt zim%@7y%~{2@#G}Ju}L4n$m6}4_o<7{+PB=9Rjjsnw8ZOX{<=$W`Zq^L!cZJi_JDv1 zLX^XRq>0NAffOy}VB48O{hVCbC^ktx3mYZ*P;Qc!DUPu9T^L)>prnVl3ODp!9Ca^= zny-jGx^FfDomM)yPa6Rr4_v3{-6#@yKDYh|p-4GaBn;dz zStRs)byA9X*3U%=uld$HalMMJy>>C(XgqWl@jeD0?c@V?ZxB^tgH2!A@nhY5T%Wc! zq(2*HKqr9{HWKx*-|YY!&?Yq3fzCxktbf5%8J|ie9Zs<5;oTf;IvuD^yfTf5;lHq% zsMLp1t3CX)*N>gp_>PdM7Vy6-YtvP=>892u3c6U6FbJQXO66a1?1;!s9dBPeg`Rg% z^vmzwB`Vxu_%DiA7;QC1t<9hPc~94u``&s@L5r2&vbKjFzxjSJ^n}mn28t)LRm5Ny zMM`J|OU?p{cVaTy2)$G3?7!uKSVArm@+gvHShih?#gTPO`L5@|*%V*8CF;6qd*}X0Jl&wOoM88e`QIGQS0<^D zz^M2Xo&JvMQl78O6?h8r5tBcSaU{nFP~kVI)dzHS5Y0!Mf}M%g1utw}?6q#UjFq~37PD7F73K$}=Nnc>ht$frm-bx+ZOoQfh+ zwth$TA$po;X@6M5&^`mF_>ooo$SZy|+&`q&u##W5_^XYQbDNUwmIH;a@OaJ1B%YT)++-IO)6z%!hzC9g(NK6Z1 zdt99I^IE7jNh#zz37!emZW`UZ&*?6tpdG)7G|SnQYrKV}s!|^}20@Jk4tBJaq>} zZ$ILJk)fhAh>|{+Spb86ew9mn6}7LqF_*>Z7q5(#&WLP&-J=7vi#Ak=LX(Y^=Z0Fx zF?##yc0A&?erQf$H9YOVy}fJ+5=v`D%kB9vpv^7=GD{%mCxL}CGe;3Yf+2eQ$6$7} zbo8okIG*ecl$td_%xM)Oe%;?%iHoROxMw+q#=J18VG5QEZZl1h16mj#kLsaZr)^sl@$2&s+)@!fl z%Y?d%R|zcP95baw0P134Rlkqzb*0UKs;qM!zcXG-0M$`ZXXBs>r$`?9cb1lJVc{XB zTt4S&ua_)Skz075O|x2b()|`UsznZ3lIqsD zq61x|jL6oO8#ND_Wn0n>Db9m&qfP5cK_sm5m!o2)f% z!s+&|B&6`EXzgpbVAHMUvJh4g=zDuTq%)T@a<7Ua9{fC?!s^WFi=iDsB|c=mBt`W? zZWmStkf69NZmehh0!rr7V113h^<(Kwx13*=*05w%ne5%pgZl zd=jNNL}?gX!wqvAG#E_gV9c2N?Uk{}X8W4z3~R$}Ir}B)EFudicgMz`r-A7xs0aR# zgR8F{vs9HNXaMZH3Ww=Yb9qe-Sav!xVp9Y0LVWTwrd?)m#M`P3!Y|2K4$!L&8n$!9 z43FR^j=jMJ9z9N;uX3NE!I&`8&PNUdfiSXB7C$*Ks`wbzK!)@5PE=!fGH#U19CzW# z2v%<-cTQj1;nm*WlMyXA?9)8-bEC)}gbZWv6xeM1wL=&@UjXMHM$%cn)q=ObVif86 zIqbQAjH=iMuRx{>zUM`c;3LYyHqK>s!38s8GBOJLwOtwnUN`(g;s5+kcvP#^4tt7^ zmq4|*z~F;)!vY`Pn`7pIr+M?1L3^v)PbNP#7Xw8x@LD6A)}!CZN9LT9qW1cuX{$M+ zX9WK!+w8Bt7Wo`3HtB7MFXh;B52Ra+9a@01ZXGEJr!WAQ4^g*463KQH!_q(X__5Wo zL1dqBN;8hpfREbdMto2u!)W6^sX5?^q#U^5lD^4@?T;Te`|xMr{IIz1Aq7;D=SOMr zL#`9UT)O*Gk$V8S4{6CH%?91b%SSXDBLBX%v5uP*j3V31hia(0Q!YzN)P+ZbtE%GX zv<;g->};%Hcieu9Ldz7bUUr`qne70)XrfV=rEiuvFQ^X7A&;s<+WU+GJGJM>&Rogz zoWsM-!$5bBZ;$`EY>p`0SVTeIWlk87UxLG+j03CZ=M*z5eQS=`$q%fuoOCUs=UPmR zww5|(#Jh&5{-RQtB@OXiXnZgc!*~k+`gcMSkCFBC=^j{l2W9(7RMpHb9OQ;~cdGQX z9ZtlAUE8SJx#*5(@9AAa?zG%te8b?5bR4dprq4TFI)H!*XoWVmm}u_S95;V zf%$51KDWf0IDik$SHmgIac7(Yv3f|M|3v{ZI_%*^O~-*9Lz z9yoLMR>7M@W)&WpM$4IkU$@gmtNl_YW5Xh<^Da{MIhKqV>B_&UhsT2AV0m{Mt(Qcb z8sY(d>k}jBDvDjEs;b;?7*#R(G1-XS_AhyDlk?R~(~$ZlEf)&MoT)*klkf}P5eXAV z-TC|7k&Z#Dw$X_ZaK_w4TT8ZHj1Px_ll> zmsbu0Ks7Q&)q~VFqC8Vcy)gU+$lruEwJhkhCa)jf10VEhyou zcc+OSkI`9eP&$3FFZQ*AgVOxAU;63im*0PPUJ5FQhrd2OKRkT(@l^?WC>_^oN43A# zo*f>(eixLWS}<5FX7$6vySuyU-BSQP{lky%53$(c``70cs8i8(ppipp{HRv#EIMra zk9`GVAI3PD$2n~sBijXqs4ps2UyYK8+q)Y7LRrrwap%yOD|La^klC!f;l7<8MMF^Vj3qV*Qv@(sEOU3@y@14ruLoGU=Y- zQ!`XBO2;In#{6H4^e*a}2dxud?8|$ZTup$AOgnCaw!ApoNU<2Phwnm@&c`V^bAMzy z?s674_-$a+|L4de$+gai(;gn6#xGV zUj<7PKX4x3a+dkcG{vkPaT8+f1R}qY-%Xzmmy_*C|l05kVlU_km)Ywu7A*5I52S~y?P8?1V z0A`{<2CD3P`u1O&xfEdy04cWpqUAw{@F3nuRc|Rl|A5RwbJYng zv6D0$h%*N!_{)hhc3~~eP@2Qq=b;=#_0oMw5#J?Q#5rcX)#aeHc@Y#SEsFcsKm7FZ z;uCQw`}gwcVII^=msg@>={-S8aEIFV!Dc_-B;Z<|yX%bN106g_r1eH7gP7)PNoZUv zk+7HopWA_K3^gk=Z?r!QF+*D^F{W493BcCL$*3_wY>f$HNEn~oG{%@Pi^Ivmv*_Ta z9FE~{qaTOU%h^>K{wW{vpX00O@M!~60bX&s{Bev=ndqBqX^pqX@?D|CYB_IYU!a<` zqcYT^%1#4zM+ay0$-r84_Z43~!uy-jkh)-YS?O!NFMX}sTNc#S4YG8rbgnya_LHvV zSANxj5p8X)XA3Or=tFss6-*9i-F^UDqmeuM9%WC)iE$~;$^;$e`RewJce$u|IpW6( z@wLlD_mm3pg-X;nzM2#n=XFz0LAu%LXzl3!Hr45Ie7{KR!K7PJfUpIhbL(>ipLhCm z0iP4|2`o%teJvo92pJx z&SNo)RY)}eAj&NPzy{@5Ks3<+rr`if15W26g~R>u5T^Mm-S+6IzL1&!9am2X!Y;Op7>GM!@$ zM(_Bqn<8(lN|c|0pg0dWLz#h|8s~#%k_Mu!Uh7C7urA?lMJERWsEePHRd@7}n4vd7 zH%OKBg--Z59l;V~h^dSg$;b6)Lq0hQm9KBf>8*N5aP-Tq0={S{wRDJVa?QGEaLuZp>^w*^gqcE!l9F3u zNG#pb&oh0alKIE!vOV~~7`EHl+skQUfkgmr`fU{T#>Xi2h{iAOF~6C9m~J6fckVQ& znc_}2?oRwXE82&jXSfUfKfVh&O%&}yQEy+oAYZZztv_cMa+=wB7ckA)2ZY`jv}+;7 z%zJx&3W`n4W?=(R&os+ii^ChZS9WK{r(ul4{7DyaX*6%ZXzAh$*feXgvTPc^DgH0< zgs9SQPNjio$|bU4SmzSR{y+ibB&eL9GdLfuWv&AAK%UFT?*j;EcZm)HWw>KVBJf9h4s)t!OO z=x|!Mqc~m}yl+vg#)7d}$mok}XnolQKN+f-q3j)9w#(~?t3-rGDBt)kqC_dR7U6kY zPU^LW@8N`V!&|Y;_7%v`1b2*pAp;%KPnQduy|^oEZzG=9icY866~V`JiCb+IGAG!& zNq8O2UAj|87li`S66)^su1a6%`=EYTZ7mmz=>$KB)lB#Ho)iKi@1{66Q*Ai_Pz*xx zW}3=O7=@yInlq^H+|0Y2$upFJsom{#IW0Tb+gLURv>`1X$6QkfO)_nmSaAA;0Xv`O z12kzpNAa0a|GLwvmhMry@2={r>PmFi@MkUX3BJZJ?lp-n9Gw&7nO`OI8F9e2a}#r| zZ%HFcDUM^DO7s%{$;^#a5W+hJTM6fARnQ#6PP}>g_%3JX-bzv|&jy32yDCZg|JlVL z6AP*8lzG#LBsA6x2M%+t76^b*XT%Hp{j?9P9Ja63R&Jlf`CCw+q9Z$UM{q0d^0Zuv zPL#Y*w}Ow>EiSTE%szM4mHNSX_K{cWHXl8)o!uiCD)$;}GJ|Hs>0SdHfj9?He5SRB zMWvv9_n+e%yjycp(uhARw}$eRP-0kXM&>T;z$mVrdL>D$OWoL`$<*TWX#IYhe$>I*et7U0?lz2aoE-p zIUt;_44!5<2C`9fufQ0aG=zRh=1D6-;TT=%oOD(x+eklca^o0m!@h|#+?+=KVoSQi z;CfQ5Duk7^CIstM2u2z8P~Uc~d`8*wmZjn>%Ap^ayGJyvF4opTYT-`NB)DAptpVWk}&&o2d^5nobElk6CCpL|DBP4*}#7*8o3Bo!T^-F_Ixk`n&1@pz=Nk?8FBPgEk+U>)3l~g#u^x7L7an1xSCRqG8liv4*Wcq2rwAMTddo%bD?g7BAso z-Wv``n`59jUas?J+q4v^k%ehWDbm_ya!c5JEL$nv!LGXXu$i2i3yl(UWXI@35wc&t zC@K)$zpTVXnS5nm#x=xNXIt7hd#sH!XhShEh{c%#xN{$5VX;NwOb(9WC>}kQZ|r54 z@zcigB&D}pMPe<>@iX4e^O$okmsdcuou98yfm1VjK1>_hZJg6nZoP zoHGEfskjBIY{?fNhLdyKml-Z>9z#*Mfa6i{@W8*)AUaiOvo3QNI=7{BFM_(vOoLM$ zEj!GicTQErY*ngAR-^HQU=&!N3m!;)!6pj7@%M%Lz7RZ>-|Dx>JP?$SNf(d=>;i%d z0c`Irv%QceRm{08X`xe@#h|-r!tYP?LvWP9g$(4Oy5AIG9#noukzRbC+d4uhpVRdA z5-)#T%xz}Q98@AB528PT_eDVnK+{mdQWNWOW61^c7Z5>KN1M)2xMF7$qaUn%p z4qHMER8crg%7aIWFSd07WnYroO!58MY1YQYmX09nxEhq40!l{NUuq44nG%xPAnN5=w-I7SX>#S7<*Faq$q!7fx^g)lNUQBk%<2=q#uJ)gmGc`u)F*#FSKQrgnId9J5y!3y-^pJf;Whb=C&B#)m9scso z`9n*K1AKh>=qWennIfyYXKJ=b(|On4w7g@>6cW>7O3^es#R#xjPVR-uoyUVMb<@0D zUH1%Kpf0jUTUHmJ3r~IHOzLg_PzuE2vUXLu69N4Ej6ZMjry*Qc@sU`^3q`Z6PS%YT zDwL@vzNaQuriqnmVsjRsX+3a+uxE&8e<;=9f8W!;iX+TwPH&iIvY;C6#~Htwj7Cfc zE#)Q`To0AK)0F^lj#S6ew!*qAhx5u^ba)(D_EWdzFs*6ub)2@ZQErOQ!oNK{%wGhJMMB zu`6+QYS+|4>l|syqF7unCG(475qZNAX3)NFqlTlfKTb<~O&_==K7x*|l4Ia-O|Ig*u-6>%veUe)-zKGZyDrp5hg2bnkZ}D>%sn$a$Ju#_Z+JLl0d4d!bnWb{y8B1Mz?(HTi!X~d*vDW&}yRu?%`pTwer!q=DE&twz85RJ^Iva0d|D!j}xqA&A#&X?G=%|aH~*!leP%T3NJ^hOw% zWy?#Ke6G$cxt8KfK1YqmG-CiyF6FDcBO-@}QMdr-Q9+qA`B;2;rBB0Mf$CBIv+CVd zQAb`eYm>7|yULNMDV|d(x}RUF;atUj1tlN#(-nP-w5nX__)M9QX@gRXu)dD~IH^{GQ zvHU#mDPOGX^PCs_jrIp?X&Yy_Ob6>L0;$;Tt4k!bQGFYO)y4s3DF3Zo)_B|GkVfZ& zWYo!X^(lGUe8;YNMGf@NBVDpCU9(04S#I0p?XnI|ae^c!Zmx9T%4>kXcc`Nrw=&@( zz0hS|RN|a=a%P)VKhI28?~P;nfvLsbd_~YnZUOEEwCzFN1`kq|-wPU!F%-;B>ORH2 zlmg2uD2?>U6RY6KXIz+Y=4?7=FZTgmH2{x+*X6i*t?apeo|p^g`-6p5@{e2HRt|O{ zu^b?>qHOVIz=s+i=JC+;S6~iYwjO-5HJ^@P-vcEMccz~57weC)GT8>11nDKZq1_Xu zJI#E~4r4wTdT$rkm~gk;YL7U%b*>W*&5g~YBAIeFEvv4REd$Gt5nXQ;eyG=c zN&C5y@Ht7sZKO{nWQONu^btS4*|CzLt}jxyYyP<_LozAf+kdW8;neMea&ckz_Off6 zzY0ZOiZji6Ymlk;__In1oE1|2>(I54_Xs-Dg@fK~T|&PjBOW#u*hn^~hGw8p>5CI! zJdkk6S0ZC}FXwS)n+pfmfV)URbjPJB($bVmW*z^>T~DBoDapV(YM9x!@?1~20jCP= z1^L>t(=xc%^O zzmf6soaiqY{1EX4TC^{^4-XyDa3o*~ zoUBf)<5Ns@mT8j>B{AEYj8Hv0hTSb&y4*+4sW#YeA}eO*U|*(|BP}OCklvki7w(t; zyM;s1_qKII(ZSa(8wu6_>{Wa3Im`LqUM}f@$X>GA*7f;-O=M>2g7~e=sCxXOrR+&v zfUuRjkefdM0x&!e!kv8Qc1_$5WM7+(NV^p)rw2{aktDzyM5kD}N}*7~W>jwy;h&S1Ek$jIWANqc_N<1RJ4Q@8BA>za?eQ4mYVw}chvXQlC$TVjo$PP>vXX% zF~B)B!b6e`fVL4V)&M-fzMxG~y1s8K2d{wRuu>%5ni#@xH0_6->KLFEy7=2{gaXYz zNbcl=WC!?u5$Gj?*mevcp=TDhTIZp?9atwA^gH9Xu$>o9FH^3l8^@_;rk3)knW>>X zYG!K4j7HnR!=1goPN;9T{PIZ`Z;3pN$s3X(M7(8fnt66GfUi0rmLoA#pEXFSr;rki zroFex(X_OYqrnK!GvJ^-`SPxSO7MsX)UT&+x&p}xhnO32V8 zVydznhfFf9)9t6-E^zDbo1LUNnTttGF>Nh8O5u-jV>(rjj8O@yG5HQavFq-!Ze4|c zaZG)sFt?`UxHY9@eWbLjk3XaQrvjY=)vAAe9tE<|;zC#kU;dSUAo7_`Despss(>R+S5UDc zP~pyR6e+G#S3>$93*jT~6sLWe|;1nT`MMVf4p zKq7J5B|Ix7Iw{h1Z;4L{|G6m9388;4O4NCYPDDwbkdiqUCES;iwF@O)=90DcC2!?R zYA;Kf6)t(}TQW-GdnX7l1Tq*kv zKW5vDqv&*Ig?tkeCUbQ;fic8qCJYD#K~o{8odLnkPK?5U3N)NVDw^oq6hMRXq9H4h z?u(9w%AZ!qCm_9K8Jk$`R8@PC=`;+mSRfEjm;$nPgd;H{tJ_Q*F6NEYfpJffTqluA zpCLEsGbGI|dU-?aT8cu^nKh_X%r4U_?b*X#3+Q`e?wPwX*@#3_6@sX$wMON$AsK1l zFLLFdu&$KMnPr&c8$!V|KwU6{1pWXE%>q?S113Y= zff@SoX9<1I*?+>9j9;CtS8oTSSw-R}PZC!YprmZs#*ORepnM%_VQNw2c`l)mY_iduy`eJM z5hj*{hlhv6c9^iYH#^Z={ljNY1<`{7JHNj&%Bj<`5`F6KHfPBIz~ zPH{=e+7mnVpg>(tNd-o<<|A&5(gzD6;M&W_d?|dOxCN3movBE9)+%^(JKZ@?oI!()7CSnRs;3j0(1NIV-_aKvy5wpDG69o|rd%d%q}!gkH-)rbDrwkAN_|;Raqb<(Vjrj8xHB-|C5om1R-@jza`^9y@*F)OLWr|N zGw;JB(!Kdp-rAjxu$GY!7Ir8P2K9*C9}^qd1#{k&lN`0Hi31t^z+uQ_9UT3wc1WJM zfBKHGpj$^{9kx444x#*kEH8}^v4Rf)^?v!}&pLD8UmRs=z?w3gkish|6lSX|1{)SU zEUzQ`RF=;Uj-3j;;IYkixm7Mql~059Y`W-9mh-N0Z5}Ge=3!c~AuFBcfJ6J_f;Z=2=m-jj1`sY~xuDYt^EEg0) zH#B|&GIJ$cNaYBMTgVK-Yhkgag~Q{mZ9ukaB`IDBDpj}^)CIRO+p-#JBn9hXjpQY^ zl5QPI`q)V3e{eR8tQkzi_lr^e6|mIxJB)O2j`0mr3}djs=miM|Etp}ff|u}_*5}F@ zh<2Bkj~wyyY@M0eIx{mr1!`U8jz2-<(NKamZ)Qn$W;Yd-ELt>+ zna|pa5^dx(&NFyg$WkbG0}_=8z|sfD%3|*^Jk(e#YI>a&ZYr;eT(9V@;t1RD30gZE zt@K!MX!G7D>1kTGzR~>KDRJSIxWEw_qWb92>c*1b<5-B&SfQ_C%O$R%gA@X=DHSyX zGTZx~{|PJMEs(91MP&|m*&b%OR(&q0z)fC(3tiy0oRsHIRZP9{QZFJy-um)4#lP70 z*OC^uN%Awl{z^H@cs9w3Lco`MShz^fo1#Gt{Yvl-~20XD`c>XHe9nsIc zXQN6{PK2%5zC6^*1AV?Ww7RF0z}x`xSKZDB7Q=bslB2ymMohjGXvvG4i8hwjJ4;3Y zoXgrtqe57TxMC5{$Kp5BAlpC9sv)DXy6wX{QYOaU{N41ka^@IrE;0|^x$-v7) zjOKC5djf4(cOCZL{=N{eoI~IgJuk|-5U=AK=lhNG{Tsk|Kq_~U zla^-EE|dds`5+@*(D19x!jhjjBK_0AKt$M3WeXoP&eRb-bHK3*cH{_H9q@hj^$6z7 zK`>{&fWvyyZ@i6Xl{b~7@~ZJNURGYiuXCtQ3Sk7?X}8d6@!93#%6vh!^5@DM0MB1u z(ezhdl~ZV9_a4i0)#H~`)rZ{o7eZ|w9#`Ic#pG4BKQsqupq+K$yWMX1u9xrLxY zoK9GTjO}N&T%~Kvrj4;$zBFL&5m$sBqPbuo`bD5vueoUg5R6ntpqesnhDo|`LunW-LVH_gsDBnG z8&8U!qotUd2l!p$EIyf8Cd^vv;tz*iB=M)K)t4hCY|MNOzdYeyqmwm{p0PEw;=bhPuZ7YLat06^ zfpo67N%A(fQ2q=PJBH&7O%|Yp3^9wtjg@KX-d>9K#T(%Tp(zqe`tN`9>9`waA5*CC zWpbq)Oe!y+)CIkxU@{6&RgjDfd;I82qmq64ZJU=Rl+aQ?8R_S8mfxw{ZP|^VY46xPF5WP8DU<5z#jLv8+qi}N=Or*L^_4CJ&oDUgzxe>tcM3%*+92!Vt(Dm({RZ?X=G9IT>%lEYUHA4T!x~ZA+Z{@hj zw?}uGUP%T71GOqgS-#_Zn6BVw`Z+tzhpl-+ui9Mx6E_iSA8F~v_mS^4X*uc=OCeeO zwOC$5ic2Uuj5){6?0n^YCSrnuN8f*fXfHQWqoklm{HYrWp%(!+yUc_ z64b*G(tu2hZ_4X~I~>6s_H{om$4B^uaN*eaX6GeD8IrSuIrrcm&LJ|i_4{c(!JFT~ zKj}uKNHbVDDdn^vr!`$Hrel-Ul3Ak-sZm-Pvb7QUX!L~o{tKc9I%aVzeD=JeP_k5~ zK%{4$?~V_=2Rb9&An6{!pY%Y#Y#|Mgc5=tDKmun+>8OQ_EzPG0U!H8Z0N9O@rWKrsvf3oO(T{rsq`lY-gET z(7YK`Y6g|kps?pwcFrv`=a!sv%g#!4mOZhSnOKV_rpMbdE7jtaqMofetAb`?t<+u8 zmKl93^?EjQZpk?>>{=$P!mB;b&UtL+Joff^Y(_u!_IYfUW9%)**eu7`?DKe$?RjB( zUU)q(OwS9i=Y{Ec;q|;QJughp*%LN4i!=64*w}2u*xQJ)*@&^X5o5CvP2<1l>f%N&_rp>jdBb3qUW3!3K(Htdp7^D29j4Ft^hEi3lW%7ve zzTZOT*p)Vs7lxIiZ=(se8jjAj$}QHVhT$l!G1AJTBFl8!7q zFHqXx<+66Ar6P7>t}bbUgpOTGyLFP@G7@3u0=sK{A8D0Sr;crnqYHZr`zbRCU+h^=xEMAi17nVv1e z8b(=3?FwbGl72IqHWwjqE1hCDA>5g94w8!2TIICXqT8IKihbT(A)5Z8jXdGKKP*Wmrlr2=?{y2S;PIEjU z$`?bl$(CD{_1 zJL<>70xPpdpPa*Dp+UCLW&cWajHOfGGSo&JzP7h<~G!_wDmSSYjXW=zb9GN1G@du-fwQvPTW}c4#oT#XBDer{TG2 zSd^%MZhtchv#TgDY2Hn5$*pU{6cP71c4zft;VH1xYtCA<36|N%)iLw2t0NA?W{N52uUu{|%dIoz*vA?FLB!cT?UFP}nisB)%I0zV zQ0t`{KVS5(s)%-tvO`%Felr3sX6?a{{a z+bXb;z$@L0yY{<(!})?dtp%WS&>fposP_oLdz5cID|4l%X6x3e!a*budKfsoLPNQ= zowCLh04`RUV=QB*z9*gORU*6`~Bc9+Al>k1kj^bcnc>V1MrG2r#_OjL{Yjy0^ zIySYuthLFaxEFS=Hkk}%k>K!!Z4!uHTL|=3&pv*9|J}=-Q9fZwZYl)oHyl8x(Z(K^bdOqhXK8I|je44EXJfqQSDnqYXR+ zu2ed`BT)D{O@RSpCZq&F@L%b_N=?-qLZ7sIvrN)%r_`KuO28zRCeuZ!)rHD^c70Dr z#*l-9=zhS4I>7sAY6O4>6;reyl|lm^ETkzCEMOke*lD2nCRAKaj_iv-0+7|uP*+M_P9*vUOJbRO5`ePK7EVj!< z>T=QidxY#{+o4Gj_U+>?@{fBi-;{BMKtcuu7t)pG2LT3X z`#^Bz#(L$rJ=i~JCr{{r&AVcUuF~f3ZXx;9h&(R~u44dOV!`8*HiGRnN+FE>{}G9_ z8Qp4 zP6ooN8va_xL;NMzd$94AAcxC#aDi(353tgY$&Xne{GJr=Q8{@EXvzWgnDvNQ7>n#8 z7EZ~bCcwNJg1K5QsPE{o?U4DweU1pX^T-_?IvWz-uimdi6)MYX=P^~{e7AhCI_>5n zyd=zmAoNN#kA3BZ_(@kvn=f4}4oW&uNLQ|JlXR_%u2fNJ*Oaa~^sUd=Du?y5Fp7B+ zRRcddo(12vZ^75KEv5Q4HPzUiU)~9L_I%*!$az=785bw6`L*ZE+Ab6&d;Jv)l{16r zDZ02*9*S#R3q=W+yA_#ovut)!3e&JmW!bK{7W8j9QGeaKkkYRt#kI?QA$zcYIc?~a zYzthA`%r)Fx@tUmH3hXS^nAZqu34}Hmic~Qo3aK%CT(H`r#}!J|Eor|nPE%`Hj9S`V zGiAG=KGz-o7m<|+4>FV`VzOf5a{mOubqR^ea#%S!a2`OAy;(as8b4wdk)maH2$a6| z($}2MFD)vIox(t}d|*-O()G)q&Y&>@%MtFE)~*}hKaUd9vX&;hkSBv4eFhtJgkp7& z)H&)%RGUvdbya);i-PtZRALVdUo{VZd_9`AzlQf;?h0oG`bksug7txo1s#t0ea+w|7CL4(tD?R z?*@)r?FzEoT5cqIn(h5x;H!&h!ET0bvmeLhtZG+^;4%s8$Wj$5Sj9f9VjowrYv*3} zuMAnyg_gULa{J=vkFEI8pbUAwBRMRF-|ALnVfdc`tgZG7_*SXY(sv=R(HC*vbJ@Pd zLO=$gjt$&V_aZkqMc4CzBwAila|M{8 zo52iS1;+sAQ028Fl=&yIs? zhX1FiW+=D4F?3MP@GDTwbfLa;Pi(d)JF0B zxPB}~@E^v!xrP7Q^>4(y{!HA!e+x)&*8gffrC*0{#7ps6oT*sUXK)9kJCL)>qbq5I ze|C9%CEanJT|VWXui@vjE9rsr?DF}QboX|4`OTFyLr=mxaVzHHM!XWQ#R7XWd$Npc zjpa$I_GF3s!R|PFw!KGsxx9)RZ{RY2PP(ZS1IYq!#JRMKZ^5w=GyG?oiL!*iLJZBE zlklVXAbu3@#6QGO;;r~Wd=K|8o_b+k_^}>Ng5Ea#kj?<#9)$9N5*^m^f&SGPai*No zY#RGbNcu6>&Io^u8t=^4J1%|}H=EFI`@KbPxyRv6<)7l)$}gO=v~!@Pe~LFM z>mpuOZn6K1liJ?iPZa6k{-PWoE6>GRc6jkZ7JzA7RICCU_L9oSqnnIEeHbnd)zP9; zP^;EzM^VHbe`n+P-{1oMcJi|Eo4PrR__qAp!NuWA=|knV9R5~5iYVo~%ipfz%U{Gl z(K4M5|F5FiypkY|&UyTu<;bN6S9cDV_FSM%`%=6rydpFEipuxUxK0 z$pgNWiJu{RB@YYj%z5Q?Bt6H5e>;2{8T05jxD1)^L-Vbc=B}(S+v}+E($sjZwt8N+ z(#kIntnyUit=-1Ca+_+^l4;+@Z_8Hu=gRHIO~q>ST)WJ`MTsx}dGKBN9|yPM4E}zG zzyCb=7QR#X{uaJd_OLQ~NQNNYXfUL|`fmSjswi^Bje|e_@mV z(pHqVCSDLpKQL{+i*qN`E}_1wKr&W1{+rWz9T*Y}35(uH=tp>XIsQBv+_vX8i{{nk ze_J01hyBEIa5tlkxAybKe*NU>Vw_AqENJtHu0pKH73Ph?3XJR&P_fYb#xmZ#Zepnf zgHPY709HdPywJBbWgZ}tmikrb$<4z9u63WO3agCGO-3?+h{gJ){Fi)N$PCH*rfZ%!o7cQzc#l1^Np9yMh0{Mc1JM;cIFfT58jcHyIa~8sgtV#h0<3sUn?j0ST1L zx6%E%$-BJ0YRvBv-0gER!fiL_-Cz{dUzg)^BY@ z_!ayBD)@#{fL{KVQl4QO4jmg}{?Qe+hkcaah#U9-`bB((zjxTl>-aN_;Lh&soQL+D zhejRAZgM-3U3eYj&0?-*@l?&?88&`=@VasXtsft}s(hPi-ZbdXMbCj3;z%t7EN0HE zK49mv>o4lqonL0*ZerOrb!>HblRsO3sBg2c(7SBXfBMtOY3FN4zEC4SQX@YmpF+sJ zX&C>XHy;;Qhln2#0!8;QGn|yBP0u7cMC*NC>bYF%G*jkAm(kk+U~eAZG}8FnFIu7O z`dah)$ZK}QYj&J>1ior7nnC?Ke5;PUxWq1K)dg;l!0i!up9J2f@@v|Xzh0H0EbyKL zHNKHh#o&olf-#bHut&s0jI{7^~h7lum8l>eeoNvR(y$q}ffg;AbT zM=)q>fJmAwGr}BsU19T$bemlzXP!6Fy%zrZ$26J50r}_*8mXf?`D8Bjh?G}1`mkdz zrE>Ixq|=pdqoum49fb)tE44K)cL^&;)Oi@NYJ!nJqEH%4z*=5X?v2sB!S;BU z_Lb8vS_%d`%NwSQFlbMuVRxM)?_b!mfRsqf3}u*}0DwmEz=EayhCEEKO!!LWRDWTT zRM5!%lYVJ8+5C%r5u9Mt_!yw{WrQJZg{?1RE7ZvsfDR_lzv{)n=-2HF->MoTTl#j? zVm~6?8W`|l-+fNlLDp`T7s!6iW%#>u4!Z+H{;gQs0#9)x?y|eIJh@}>=Vyz4UtTqC zG{2&kbNu zMwfG zcILOi>0KRDqCFE zHe{gMyatlwoaQIm!ko948|UqX3x*7f(IL93nt3hjY|*Bp#1sSB@BVUT4Ked(?x{csM=ef@)y)Y$3AJtJ}K*%HS=+|QFGwp21p9h1o|Mg#9c zYL1Li%F&vt&YTDnAO^-=6b6ZQCS#T?NKS-@vQM*p%Uag`tay=fwn`|i7F18RppUa{ zF<*sLvICB&9>CuzAZw&azjrp=d&4;=zd}|8N)&*V~8aUzc555 zV~9+~5Sff2G8sc;GKMDPs3|=$0M-&~&LQhK3~qK}EKypbWyUrNPL=_PpNGQJlFVQd~AoZjMOZCt{<=iR5G_ zA~}&a!|y=(+b*Dpjq^S|^afw4%K+%uV!9N_$!(xw zTYxY{S)raU8-DSsPsOz%Itj5+UA>E@Nx;o+5(Ah# zBXOY6vjj-jaWibo^M`@uqXzzt`-pg~C^wjBz|h#nr@2&EqL!!=$}8@WFIM$%h#_4D?glK*bCU6Olx< z2PieCp8{w?PS*wsjm)?;$+gL2>FWYs#}a6efGQFr^^H<2Qnn+{zH-IBC$p^UO!`MO zu^Q=Qw%)o%v$dgRgEguQWP)C;p-o>fE2o0T>;kdrsML$VHG<&BL4`aZ8`11g+>D=T zxh<_+V+luOj%-ebjA__yFcuno@tJXe8{d(y7>SZPb1Iy@#vmI?=#PvH7AQLyf9g4N zaVlj;={j_Eu5reOz#u^_W^yRbp`T*HRc4dp$zdp_ zs8cJM6_3FfGFdo@9}k*nBh&l^9i0pquUgETTFMbCLXP9>oJhGjY4HS7B@+XK%quab zr-A8XPXoi9IGbU#8CqfSe5MhIyG3_gxYG_@H@Mo-LdKl#EHQ?pe>qk-e_+8nI_k1u z5SkvgAH0iC+%bp^A9{xN?eRG|;atS9lunm?XkI!i0vCfkj-s zQjr)1(m`}!qb;M9W-??qSII*1MIC9fv zWF;|l+t}8uY{RK?k?SsQJ2EDOA!qKO5Vw-E_nj`v`3jGCKH}myaaqEKtJtJR=F~LY zGC+p-r^p@Opovjkiv!>}y{YhaR(}#s*d02hs7vy~S4ggHqshqmn+;@q1dRItYj!po z`Xz6ZpCTvJSrB z&nOPBD>E|6SjIrCOz5YP-ozuvBx~8wJVG4K$|Gsdp_qj@oRpW+mP1oAWPVcjc~l!z zv)UoXaT-{uQu{C>&m|d9KP&!;7x0i%$jiba4XvjmRJlrqEKxL-wkNUck{9L{(yC+JW zJG!?NyOG#apDOsT{BoWe^NzXSTM0 z!|6xdGX*POO_%DxJ5dYAJF zEoaKA7{01h+|G{J!b`iM#Jps$Q^bRT*NY>k0RT>A#Ov*3Te)vi2XSkf(IUlEr<;mk;bf~|1UT-?Kv3I`WCr{d0mJ&US-goTxuXg_XSK$=A z7z3Rza16DBM<{ylYU^eRw6^i#t&IgwLn5a=c{kTYE4SVyB)zBJ7y-Hi zUtvL2huX6gKIt$ptrMHzum+@tImaquB0?iU2^g4Cc)9HL{J8Ntl-gY~=}hlpCsWy* zyL||aP9F4z|?Khjn*?rX^ektW?L1tXp;fLzg|fBWfnJYweQ zhvo!Y_NE!#XG>IV?Ak?7pnOF8{I{XJdq9Hp^JjM*2FK7W0TY@^*e6e>edQof)^Sm9 zR-p@(Fz}g=4g@-dK}NAxolZv6W+%3e>cAk(p4wzxt{eABt$x$!d6Vd6Cc$u-4fwit zTzdi(hZNo3DE4(Aw@x{~V^*WpoOi!@CJse_e!1|*ke+ZC|D^I7vS6Jz=? ztrFS1Vo@0@{Gi)kjm^A@;x82wMhb}duG8~;H@B??vMx&&@>xL1rnI5sm+S93Pz=tB zmEP=hnEcLepACqs=N~?NIzM~&%h?At0;Uh$+IFkwI(8C0bw_tXfT%qHV3RGN`NH1Im5n>bD72$)?7dpESS4-R zIasA&7WQ3Wt*j3Ez+)Xug!gv6u#I^=IM?W&upR4u7$dbA+;6gL0-)bn5EaX5w*sdFw)y1r zhgi6K8Cb)+f(~2g_L72~*yX=g2vsO?a-}dZOOG(Y~3cK*Be11G*C**EKB> zC&w5eMzQ&#a(w)JZ?Ab0SB{=O*N~uc^tYNe0)s-3$Az9*W&Z9IdB!EI0(Q2{wfk_Y zs}5i^1{^tKP~&`hWJ(HT|C+jF>;RI$PG$A`SR$t?55ltnwDW72UVu z7AXvI~UwdgYdBSOT;!vj9*=SqtA+pv>-hQe%1F$#?=8_3V=;B!z9#XtdGMMvhUNGf zOh3N999}iX_y^8NV@f~R>c076u(vnHf2R1)ENoWB(MgAYP35mK{em+I)t+9(Ko@u2 zqt*Vhqb2k#>;9zWZ0pY4i;tqU`?-g)`S@wHntg6pKE!z7QfQ{Bkv(ci>?q0U8=%nC}2K<9iL4M=r36Z3h+l!rkcax*PrMZb-Q9?gs$%>eB<5|{Onw;dZ|ylh;XiK7VY>P03vmcFVNgv_2?CV-8yWGw*jV z_C}!5EnIW!_InVx9QWj1R=?ueQv`(whTe{6Om9hR zOr1YSum7)|2t%Is%sF`Uc;Kc3vD`xReGv94?db3i?|%{9yp>nWtozpP^H>MrH<`V{i0IORH-a6GbZBYDY1Ct7c~q{L54GuOld1Zq`&cx zWa|r$BrQVStVi)t#LfQNqlb0rsC_|@Asq+EHI(ecIWd(AA`*RsI+9HSl_Z_;mjRulO9SAO&Ys7Rl0n&V`&#w#|4e%`^tzMqbplR)5|c-3uB zrI0h=ln;YuXL_f;&PGWeve-<5IzrVR%tgmv@}&<5Utu{C!(7W zz)jNO1IuW|(k--ItLA9ud1bP}lOET1A^j9va`flvr}R8ExU78Y-x0l2t&^9_rvK(l z1|l%$RepRRO;+iSSe}Z&J*hH*532m2bE=xaLkPdk355^qeCn@XS(72~jwht(b4c%n zc^1)O-VBA(vfs@pmLR}Z2X#1oqfmyQ<^4dE>@_smCQ)mg<;Ac<4FEn}=S!&Ks%}X= zI6Z&JC3|Nl#^f(alXG=|NiUZg#!Ni|m6#Ek4q{5Dxyf4!;5(B>*nhw_5!4y^QyP#x zgjAH9VJKie*+6LzB$=SJEJhN8TK9T6969M0`* z-o|(ELDhW7+!hX?nIZ@qRLzG{@S$b=vDX6_j|H4>r;N#+lfwemq{~AP-lcJ_K=!n0 zMI(d`QjxY7*{2sbRr|?HPk)~rIxDFkHn9PY#M;xLr{u~yWu!8bDI`aXc`x;zalZCM zsSXz6BeoVo1wB?P&jcdYr4iWRUUbvo)b$u%w{+}kmNSNr`fQuUF~dW*MApE~McCWx z`8L5;H5DHB@fjH%Z@&$5AMKSRohx;w>ZI8C_Wpm!8(4S*Y z#SL+8h&6uEch$+&A{;38|G=C63(^ zFGtJN&VI_-E&{l`Z$YuTT}N`k&d|iP*ZZyRjSdqpyu^#mRW2I3ywoG{TY*7|g3Jrd zyt*%5u(g7Fho zJWwwFrw-K_tHF>Sz}A|!F4;r&-F$!tJ`D7V{`=uOkAR{)`vy6qoV6J~vNob8oC48i zU|~>(f$ks*fsRiuD0pj84V7MWc)ZE2l(JQ`1EXafOYbcgo7<;u;5cpa=q1cPJhpsD z8W(20aB7a`7&dQYqQs6*Mvbv?wc-Np!R6T5ya^oXoSnOl8hv}`hGyF)BAk}P@xjpS z($G1+l|)>NqiEwSoSD9Mc|%NLN|CYA`Hl7mWcK#42C!}g!vZb)gZwt6z2DYW$N{B2 zikdn}9>UIY+Ls=ylVP>&v6O?^DiC42n#{jXuDhf4yJ@F;8t&(E*ZYyr(nXdlawQbX zVK9C1P=AYfXqI-RR!C8aPI5~Ku{oQ;Nj@JWqfRJE+9LT;?8%S%u+o!m`t8RbzBkyJ znR=N;0!YJnXcHf1J^G0VeuwM1K}fqsO)$BhadveZs?4anfIIyGRi7vQIN-qbfztZx zXU$fL;OaS^khDaL#b^D;oCzsvcDrF`FWU5WH0Dh&=#1;E^X zbgbUizAHy{FLqWXOdP&K!{ACTb15t#04xe#;Im2RWgiIWzDOeF=|g7o7SG99Wuu3l ztGrpA?l#eS?dt+A447~0zb7XhI_h$8g2@>8z&rk@B>k-P=p+6;*bg!QI@tdm;BXzA zaS_hxKGj9vb+HZD2}Gf5TfYt-Tvf?2bk@0f)%Xg_dUvj+vdQ;ry4Ue^AHORdoeN+!PbjN@Blxe-y&6XWl$ul>kQE=LsO zAW;67L%&anTC5aw8GD+ONMh-FtubXm$^OY=4hvRV;ifdha9r+5S7a`ScX@qVu@g znC&1@AErJXCj4iFy3ll<)W%Qkji2Ui{O+6H%oJ4H4$A~P-_P0kF)v^K)}97yfK;7z zP`s}8FIRSzWC86jJ%8_5=FCNn1NHo}(s+Xkzc@L>nZ4Low&(X6K-T-h!fEwu#jrRN z&pd_o;~sfEXjj>h2N{gd31;&l7e!-9Q?=AKt4uff+b-8sC2_G%NcXjknB;v z)&$iex=Mx~Dp@2>Nz+y(k95&-oyclc4lhpusOKmEalw#f_@V>3(Z;Twv34ch5V|m? z9kkbF31QaWwJ-9Q3gKU_QC-+O&Cwacn@Z7SA}P?e2H&T_lg7nlVO^5stNp^;FZfC@ zu+=$>uQFo!czN~X4?p3Wq%>shZDlncb*5w3HJvZe=vwD5(-EMnZ$Ezg$)pVy3-krc z!iG5GxJ9tru>yxKMs$V+fc*}k3(=jlryaml0#7<(2uU~MSl~b2PAnOT_6YS6V;qv! zlA`#EQz{4xDd^W=I$unh<8CCNqnK+|RiDAz*B=8ehu0FziF>S3ck1E|SWX&d#gU!VW@?%n&>Z+`yp`jxh(A5HsVVCU3J zfJ`S{wJ<%P2|A_4v=o$O|2U#DP!H-G=c&A!2)Xf)sU~})v#|&0&uSb4IrEN&*fLvuTBT9%(kk8_ zEGH~bH(f58i{<=$+CdGK<614c$A-!=6@^K{|7?v@G~JgT{ZylPz|6U_k6qA8Ce3th z4S)v80);Uc5lTlJs6W%UFIHKS*%L#-?ZZ|I7JK+zZ>T`*ud-tW;sq0xgk44 zxoK26dIDI)o`i&~1iIf$Q@= z@EeE8&9hJ$?ZI^lll-PR`J|%rC7o{-keWs~>^C%8ISx5={(5-Gv8ty!J-ZH$-EJgQ z{i@?F`;^*h=j_85k+2z$ucqnNv=>VMRHTJnQg2;>pJ?8RpWeNNYuIg$bIibq)U*cX z)j%^aI)Q1@{~#Rz#oPN-eu5qu+FZBhoUrKL?#hb&)$m8khk$q9rrHsUIoqnQ+?Fxy zG}Mi?5>n6S(TY-aZO3w!-(Y#sobI+Qhf=EJ{=R-ezHDMh=0=VGAI4~wwfQ;%0FY{0 A`v3p{ diff --git a/src/object.class.js b/src/object.class.js index 99d22d75..a35b9222 100644 --- a/src/object.class.js +++ b/src/object.class.js @@ -645,6 +645,11 @@ this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + //If width is negative, make postive. Fixes path selection issue + if(this.currentWidth < 0){ + this.currentWidth = Math.abs(this.currentWidth); + } + this._hypotenuse = Math.sqrt( Math.pow(this.currentWidth / 2, 2) + Math.pow(this.currentHeight / 2, 2));