mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-18 15:00:32 +00:00
10664 lines
280 KiB
JavaScript
10664 lines
280 KiB
JavaScript
/*! Fabric.js Copyright 2008-2011, Bitsonnet (Juriy Zaytsev, Maxim Chernyak) */
|
|
|
|
var fabric = fabric || { version: "0.4.13" };
|
|
|
|
if (typeof exports != 'undefined') {
|
|
exports.fabric = fabric;
|
|
}
|
|
|
|
/*!
|
|
* 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());
|
|
};
|
|
|
|
|
|
if (document.addEventListener) {
|
|
document.addEventListener('DOMContentLoaded', perform, false);
|
|
window.addEventListener('pageshow', perform, false); // For cached Gecko pages
|
|
}
|
|
|
|
|
|
if (!window.opera && document.readyState) (function() {
|
|
readyStatus[document.readyState] ? perform() : setTimeout(arguments.callee, 10);
|
|
})();
|
|
|
|
|
|
if (document.readyState && document.createStyleSheet) (function() {
|
|
try {
|
|
document.body.doScroll('left');
|
|
perform();
|
|
}
|
|
catch (e) {
|
|
setTimeout(arguments.callee, 1);
|
|
}
|
|
})();
|
|
|
|
addEvent(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) {
|
|
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());
|
|
};
|
|
|
|
|
|
var styleElements = Object.prototype.propertyIsEnumerable ? elementsByTagName('style') : { length: 0 };
|
|
var linkElements = elementsByTagName('link');
|
|
|
|
DOM.ready(function() {
|
|
var linkStyles = 0, link;
|
|
for (var i = 0, l = linkElements.length; link = linkElements[i], i < l; ++i) {
|
|
if (!link.disabled && link.rel.toLowerCase() == 'stylesheet') ++linkStyles;
|
|
}
|
|
if (document.styleSheets.length >= styleElements.length + linkStyles) perform();
|
|
else setTimeout(arguments.callee, 10);
|
|
});
|
|
|
|
return function(listener) {
|
|
if (complete) listener();
|
|
else queue.push(listener);
|
|
};
|
|
|
|
})(),
|
|
|
|
supports: function(property, value) {
|
|
var checker = document.createElement('span').style;
|
|
if (checker[property] === undefined) return false;
|
|
checker[property] = value;
|
|
return checker[property] === value;
|
|
},
|
|
|
|
textAlign: function(word, style, position, wordCount) {
|
|
if (style.get('textAlign') == 'right') {
|
|
if (position > 0) word = ' ' + word;
|
|
}
|
|
else if (position < wordCount - 1) word += ' ';
|
|
return word;
|
|
},
|
|
|
|
textDecoration: function(el, style) {
|
|
if (!style) style = this.getStyle(el);
|
|
var types = {
|
|
underline: null,
|
|
overline: null,
|
|
'line-through': null
|
|
};
|
|
for (var search = el; search.parentNode && search.parentNode.nodeType == 1; ) {
|
|
var foundAll = true;
|
|
for (var type in types) {
|
|
if (types[type]) continue;
|
|
if (style.get('textDecoration').indexOf(type) != -1) types[type] = style.get('color');
|
|
foundAll = false;
|
|
}
|
|
if (foundAll) break; // this is rather unlikely to happen
|
|
style = this.getStyle(search = search.parentNode);
|
|
}
|
|
return types;
|
|
},
|
|
|
|
textShadow: cached(function(value) {
|
|
if (value == 'none') return null;
|
|
var shadows = [], currentShadow = {}, result, offCount = 0;
|
|
var re = /(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)|(-?[\d.]+[a-z%]*)|,/ig;
|
|
while (result = re.exec(value)) {
|
|
if (result[0] == ',') {
|
|
shadows.push(currentShadow);
|
|
currentShadow = {}, offCount = 0;
|
|
}
|
|
else if (result[1]) {
|
|
currentShadow.color = result[1];
|
|
}
|
|
else {
|
|
currentShadow[[ 'offX', 'offY', 'blur' ][offCount++]] = result[2];
|
|
}
|
|
}
|
|
shadows.push(currentShadow);
|
|
return shadows;
|
|
}),
|
|
|
|
color: cached(function(value) {
|
|
var parsed = {};
|
|
parsed.color = value.replace(/^rgba\((.*?),\s*([\d.]+)\)/, function($0, $1, $2) {
|
|
parsed.opacity = parseFloat($2);
|
|
return 'rgb(' + $1 + ')';
|
|
});
|
|
return parsed;
|
|
}),
|
|
|
|
textTransform: function(text, style) {
|
|
return text[{
|
|
uppercase: 'toUpperCase',
|
|
lowercase: 'toLowerCase'
|
|
}[style.get('textTransform')] || 'toString']();
|
|
}
|
|
|
|
};
|
|
|
|
function Font(data) {
|
|
|
|
var face = this.face = data.face;
|
|
this.glyphs = data.glyphs;
|
|
this.w = data.w;
|
|
this.baseSize = parseInt(face['units-per-em'], 10);
|
|
|
|
this.family = face['font-family'].toLowerCase();
|
|
this.weight = face['font-weight'];
|
|
this.style = face['font-style'] || 'normal';
|
|
|
|
this.viewBox = (function () {
|
|
var parts = face.bbox.split(/\s+/);
|
|
var box = {
|
|
minX: parseInt(parts[0], 10),
|
|
minY: parseInt(parts[1], 10),
|
|
maxX: parseInt(parts[2], 10),
|
|
maxY: parseInt(parts[3], 10)
|
|
};
|
|
box.width = box.maxX - box.minX,
|
|
box.height = box.maxY - box.minY;
|
|
box.toString = function() {
|
|
return [ this.minX, this.minY, this.width, this.height ].join(' ');
|
|
};
|
|
return box;
|
|
})();
|
|
|
|
this.ascent = -parseInt(face.ascent, 10);
|
|
this.descent = -parseInt(face.descent, 10);
|
|
|
|
this.height = -this.ascent + this.descent;
|
|
|
|
}
|
|
|
|
function FontFamily() {
|
|
|
|
var styles = {}, mapping = {
|
|
oblique: 'italic',
|
|
italic: 'oblique'
|
|
};
|
|
|
|
this.add = function(font) {
|
|
(styles[font.style] || (styles[font.style] = {}))[font.weight] = font;
|
|
};
|
|
|
|
this.get = function(style, weight) {
|
|
var weights = styles[style] || styles[mapping[style]]
|
|
|| styles.normal || styles.italic || styles.oblique;
|
|
if (!weights) return null;
|
|
weight = {
|
|
normal: 400,
|
|
bold: 700
|
|
}[weight] || parseInt(weight, 10);
|
|
if (weights[weight]) return weights[weight];
|
|
var up = {
|
|
1: 1,
|
|
99: 0
|
|
}[weight % 100], alts = [], min, max;
|
|
if (up === undefined) up = weight > 400;
|
|
if (weight == 500) weight = 400;
|
|
for (var alt in weights) {
|
|
alt = parseInt(alt, 10);
|
|
if (!min || alt < min) min = alt;
|
|
if (!max || alt > max) max = alt;
|
|
alts.push(alt);
|
|
}
|
|
if (weight < min) weight = min;
|
|
if (weight > max) weight = max;
|
|
alts.sort(function(a, b) {
|
|
return (up
|
|
? (a > weight && b > weight) ? a < b : a > b
|
|
: (a < weight && b < weight) ? a > b : a < b) ? -1 : 1;
|
|
});
|
|
return weights[alts[0]];
|
|
};
|
|
|
|
}
|
|
|
|
function HoverHandler() {
|
|
|
|
function contains(node, anotherNode) {
|
|
if (node.contains) return node.contains(anotherNode);
|
|
return node.compareDocumentPosition(anotherNode) & 16;
|
|
}
|
|
|
|
function onOverOut(e) {
|
|
var related = e.relatedTarget;
|
|
if (!related || contains(this, related)) return;
|
|
trigger(this);
|
|
}
|
|
|
|
function onEnterLeave(e) {
|
|
trigger(this);
|
|
}
|
|
|
|
function trigger(el) {
|
|
setTimeout(function() {
|
|
api.replace(el, sharedStorage.get(el).options, true);
|
|
}, 10);
|
|
}
|
|
|
|
this.attach = function(el) {
|
|
if (el.onmouseenter === undefined) {
|
|
addEvent(el, 'mouseover', onOverOut);
|
|
addEvent(el, 'mouseout', onOverOut);
|
|
}
|
|
else {
|
|
addEvent(el, 'mouseenter', onEnterLeave);
|
|
addEvent(el, 'mouseleave', onEnterLeave);
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
function Storage() {
|
|
|
|
var map = {}, at = 0;
|
|
|
|
function identify(el) {
|
|
return el.cufid || (el.cufid = ++at);
|
|
}
|
|
|
|
this.get = function(el) {
|
|
var id = identify(el);
|
|
return map[id] || (map[id] = {});
|
|
};
|
|
|
|
}
|
|
|
|
function Style(style) {
|
|
|
|
var custom = {}, sizes = {};
|
|
|
|
this.get = function(property) {
|
|
return custom[property] != undefined ? custom[property] : style[property];
|
|
};
|
|
|
|
this.getSize = function(property, base) {
|
|
return sizes[property] || (sizes[property] = new CSS.Size(this.get(property), base));
|
|
};
|
|
|
|
this.extend = function(styles) {
|
|
for (var property in styles) custom[property] = styles[property];
|
|
return this;
|
|
};
|
|
|
|
}
|
|
|
|
function addEvent(el, type, listener) {
|
|
if (el.addEventListener) {
|
|
el.addEventListener(type, listener, false);
|
|
}
|
|
else if (el.attachEvent) {
|
|
el.attachEvent('on' + type, function() {
|
|
return listener.call(el, window.event);
|
|
});
|
|
}
|
|
}
|
|
|
|
function attach(el, options) {
|
|
var storage = sharedStorage.get(el);
|
|
if (storage.options) return el;
|
|
if (options.hover && options.hoverables[el.nodeName.toLowerCase()]) {
|
|
hoverHandler.attach(el);
|
|
}
|
|
storage.options = options;
|
|
return el;
|
|
}
|
|
|
|
function cached(fun) {
|
|
var cache = {};
|
|
return function(key) {
|
|
if (!cache.hasOwnProperty(key)) cache[key] = fun.apply(null, arguments);
|
|
return cache[key];
|
|
};
|
|
}
|
|
|
|
function getFont(el, style) {
|
|
if (!style) style = CSS.getStyle(el);
|
|
var families = CSS.quotedList(style.get('fontFamily').toLowerCase()), family;
|
|
for (var i = 0, l = families.length; i < l; ++i) {
|
|
family = families[i];
|
|
if (fonts[family]) return fonts[family].get(style.get('fontStyle'), style.get('fontWeight'));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function elementsByTagName(query) {
|
|
return document.getElementsByTagName(query);
|
|
}
|
|
|
|
function merge() {
|
|
var merged = {}, key;
|
|
for (var i = 0, l = arguments.length; i < l; ++i) {
|
|
for (key in arguments[i]) merged[key] = arguments[i][key];
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
function process(font, text, style, options, node, el) {
|
|
|
|
var separate = options.separate;
|
|
if (separate == 'none') return engines[options.engine].apply(null, arguments);
|
|
var fragment = document.createDocumentFragment(), processed;
|
|
var parts = text.split(separators[separate]), needsAligning = (separate == 'words');
|
|
if (needsAligning && HAS_BROKEN_REGEXP) {
|
|
if (/^\s/.test(text)) parts.unshift('');
|
|
if (/\s$/.test(text)) parts.push('');
|
|
}
|
|
for (var i = 0, l = parts.length; i < l; ++i) {
|
|
processed = engines[options.engine](font,
|
|
needsAligning ? CSS.textAlign(parts[i], style, i, l) : parts[i],
|
|
style, options, node, el, i < l - 1);
|
|
if (processed) fragment.appendChild(processed);
|
|
}
|
|
return fragment;
|
|
}
|
|
|
|
function replaceElement(el, options) {
|
|
var font, style, nextNode, redraw;
|
|
for (var node = attach(el, options).firstChild; node; node = nextNode) {
|
|
nextNode = node.nextSibling;
|
|
redraw = false;
|
|
if (node.nodeType == 1) {
|
|
if (!node.firstChild) continue;
|
|
if (!/cufon/.test(node.className)) {
|
|
arguments.callee(node, options);
|
|
continue;
|
|
}
|
|
else redraw = true;
|
|
}
|
|
if (!style) style = CSS.getStyle(el).extend(options);
|
|
if (!font) font = getFont(el, style);
|
|
|
|
if (!font) continue;
|
|
if (redraw) {
|
|
engines[options.engine](font, null, style, options, node, el);
|
|
continue;
|
|
}
|
|
var text = node.data;
|
|
if (text === '') continue;
|
|
var processed = process(font, text, style, options, node, el);
|
|
if (processed) node.parentNode.replaceChild(processed, node);
|
|
else node.parentNode.removeChild(node);
|
|
}
|
|
}
|
|
|
|
var HAS_BROKEN_REGEXP = ' '.split(/\s+/).length == 0;
|
|
|
|
var sharedStorage = new Storage();
|
|
var hoverHandler = new HoverHandler();
|
|
var replaceHistory = [];
|
|
|
|
var engines = {}, fonts = {}, defaultOptions = {
|
|
enableTextDecoration: false,
|
|
engine: null,
|
|
hover: false,
|
|
hoverables: {
|
|
a: true
|
|
},
|
|
printable: true,
|
|
selector: (
|
|
window.Sizzle
|
|
|| (window.jQuery && function(query) { return jQuery(query); }) // avoid noConflict issues
|
|
|| (window.dojo && dojo.query)
|
|
|| (window.$$ && function(query) { return $$(query); })
|
|
|| (window.$ && function(query) { return $(query); })
|
|
|| (document.querySelectorAll && function(query) { return document.querySelectorAll(query); })
|
|
|| elementsByTagName
|
|
),
|
|
separate: 'words', // 'none' and 'characters' are also accepted
|
|
textShadow: 'none'
|
|
};
|
|
|
|
var separators = {
|
|
words: /\s+/,
|
|
characters: ''
|
|
};
|
|
|
|
api.now = function() {
|
|
DOM.ready();
|
|
return api;
|
|
};
|
|
|
|
api.refresh = function() {
|
|
var currentHistory = replaceHistory.splice(0, replaceHistory.length);
|
|
for (var i = 0, l = currentHistory.length; i < l; ++i) {
|
|
api.replace.apply(null, currentHistory[i]);
|
|
}
|
|
return api;
|
|
};
|
|
|
|
api.registerEngine = function(id, engine) {
|
|
if (!engine) return api;
|
|
engines[id] = engine;
|
|
return api.set('engine', id);
|
|
};
|
|
|
|
api.registerFont = function(data) {
|
|
var font = new Font(data), family = font.family;
|
|
if (!fonts[family]) fonts[family] = new FontFamily();
|
|
fonts[family].add(font);
|
|
return api.set('fontFamily', '"' + family + '"');
|
|
};
|
|
|
|
api.replace = function(elements, options, ignoreHistory) {
|
|
options = merge(defaultOptions, options);
|
|
if (!options.engine) return api; // there's no browser support so we'll just stop here
|
|
if (typeof options.textShadow == 'string' && options.textShadow)
|
|
options.textShadow = CSS.textShadow(options.textShadow);
|
|
if (!ignoreHistory) replaceHistory.push(arguments);
|
|
if (elements.nodeType || typeof elements == 'string') elements = [ elements ];
|
|
CSS.ready(function() {
|
|
for (var i = 0, l = elements.length; i < l; ++i) {
|
|
var el = elements[i];
|
|
if (typeof el == 'string') api.replace(options.selector(el), options, true);
|
|
else replaceElement(el, options);
|
|
}
|
|
});
|
|
return api;
|
|
};
|
|
|
|
api.replaceElement = function(el, options) {
|
|
options = merge(defaultOptions, options);
|
|
if (typeof options.textShadow == 'string' && options.textShadow)
|
|
options.textShadow = CSS.textShadow(options.textShadow);
|
|
return replaceElement(el, options);
|
|
};
|
|
|
|
api.engines = engines;
|
|
api.fonts = fonts;
|
|
api.getOptions = function() {
|
|
return merge(defaultOptions);
|
|
}
|
|
|
|
api.set = function(option, value) {
|
|
defaultOptions[option] = value;
|
|
return api;
|
|
};
|
|
|
|
return api;
|
|
|
|
})();
|
|
|
|
Cufon.registerEngine('canvas', (function() {
|
|
|
|
|
|
var check = document.createElement('canvas');
|
|
if (!check || !check.getContext || !check.getContext.apply) return;
|
|
check = null;
|
|
|
|
var HAS_INLINE_BLOCK = Cufon.CSS.supports('display', 'inline-block');
|
|
|
|
var HAS_BROKEN_LINEHEIGHT = !HAS_INLINE_BLOCK && (document.compatMode == 'BackCompat' || /frameset|transitional/i.test(document.doctype.publicId));
|
|
|
|
var styleSheet = document.createElement('style');
|
|
styleSheet.type = 'text/css';
|
|
styleSheet.appendChild(document.createTextNode(
|
|
'.cufon-canvas{text-indent:0}' +
|
|
'@media screen,projection{' +
|
|
'.cufon-canvas{display:inline;display:inline-block;position:relative;vertical-align:middle' +
|
|
(HAS_BROKEN_LINEHEIGHT
|
|
? ''
|
|
: ';font-size:1px;line-height:1px') +
|
|
'}.cufon-canvas .cufon-alt{display:-moz-inline-box;display:inline-block;width:0;height:0;overflow:hidden}' +
|
|
(HAS_INLINE_BLOCK
|
|
? '.cufon-canvas canvas{position:relative}'
|
|
: '.cufon-canvas canvas{position:absolute}') +
|
|
'}' +
|
|
'@media print{' +
|
|
'.cufon-canvas{padding:0 !important}' +
|
|
'.cufon-canvas canvas{display:none}' +
|
|
'.cufon-canvas .cufon-alt{display:inline}' +
|
|
'}'
|
|
));
|
|
document.getElementsByTagName('head')[0].appendChild(styleSheet);
|
|
|
|
function generateFromVML(path, context) {
|
|
var atX = 0, atY = 0;
|
|
var code = [], re = /([mrvxe])([^a-z]*)/g, match;
|
|
generate: for (var i = 0; match = re.exec(path); ++i) {
|
|
var c = match[2].split(',');
|
|
switch (match[1]) {
|
|
case 'v':
|
|
code[i] = { m: 'bezierCurveTo', a: [ atX + ~~c[0], atY + ~~c[1], atX + ~~c[2], atY + ~~c[3], atX += ~~c[4], atY += ~~c[5] ] };
|
|
break;
|
|
case 'r':
|
|
code[i] = { m: 'lineTo', a: [ atX += ~~c[0], atY += ~~c[1] ] };
|
|
break;
|
|
case 'm':
|
|
code[i] = { m: 'moveTo', a: [ atX = ~~c[0], atY = ~~c[1] ] };
|
|
break;
|
|
case 'x':
|
|
code[i] = { m: 'closePath' };
|
|
break;
|
|
case 'e':
|
|
break generate;
|
|
}
|
|
context[code[i].m].apply(context, code[i].a);
|
|
}
|
|
return code;
|
|
}
|
|
|
|
function interpret(code, context) {
|
|
for (var i = 0, l = code.length; i < l; ++i) {
|
|
var line = code[i];
|
|
context[line.m].apply(context, line.a);
|
|
}
|
|
}
|
|
|
|
return function(font, text, style, options, node, el) {
|
|
|
|
var redraw = (text === null);
|
|
|
|
var viewBox = font.viewBox;
|
|
|
|
var size = style.getSize('fontSize', font.baseSize);
|
|
|
|
var letterSpacing = style.get('letterSpacing');
|
|
letterSpacing = (letterSpacing == 'normal') ? 0 : size.convertFrom(parseInt(letterSpacing, 10));
|
|
|
|
var expandTop = 0, expandRight = 0, expandBottom = 0, expandLeft = 0;
|
|
var shadows = options.textShadow, shadowOffsets = [];
|
|
if (shadows) {
|
|
for (var i = 0, l = shadows.length; i < l; ++i) {
|
|
var shadow = shadows[i];
|
|
var x = size.convertFrom(parseFloat(shadow.offX));
|
|
var y = size.convertFrom(parseFloat(shadow.offY));
|
|
shadowOffsets[i] = [ x, y ];
|
|
if (y < expandTop) expandTop = y;
|
|
if (x > expandRight) expandRight = x;
|
|
if (y > expandBottom) expandBottom = y;
|
|
if (x < expandLeft) expandLeft = x;
|
|
}
|
|
}
|
|
|
|
var chars = Cufon.CSS.textTransform(redraw ? node.alt : text, style).split('');
|
|
|
|
var width = 0, lastWidth = null;
|
|
|
|
var maxWidth = 0, lines = 1, lineWidths = [ ];
|
|
for (var i = 0, l = chars.length; i < l; ++i) {
|
|
if (chars[i] === '\n') {
|
|
lines++;
|
|
if (width > maxWidth) {
|
|
maxWidth = width;
|
|
}
|
|
lineWidths.push(width);
|
|
width = 0;
|
|
continue;
|
|
}
|
|
var glyph = font.glyphs[chars[i]] || font.missingGlyph;
|
|
if (!glyph) continue;
|
|
width += lastWidth = Number(glyph.w || font.w) + letterSpacing;
|
|
}
|
|
lineWidths.push(width);
|
|
|
|
width = Math.max(maxWidth, width);
|
|
|
|
var lineOffsets = [ ];
|
|
for (var i = lineWidths.length; i--; ) {
|
|
lineOffsets[i] = width - lineWidths[i];
|
|
}
|
|
|
|
if (lastWidth === null) return null; // there's nothing to render
|
|
|
|
expandRight += (viewBox.width - lastWidth);
|
|
expandLeft += viewBox.minX;
|
|
|
|
var wrapper, canvas;
|
|
|
|
if (redraw) {
|
|
wrapper = node;
|
|
canvas = node.firstChild;
|
|
}
|
|
else {
|
|
wrapper = document.createElement('span');
|
|
wrapper.className = 'cufon cufon-canvas';
|
|
wrapper.alt = text;
|
|
|
|
canvas = document.createElement('canvas');
|
|
wrapper.appendChild(canvas);
|
|
|
|
if (options.printable) {
|
|
var print = document.createElement('span');
|
|
print.className = 'cufon-alt';
|
|
print.appendChild(document.createTextNode(text));
|
|
wrapper.appendChild(print);
|
|
}
|
|
}
|
|
|
|
var wStyle = wrapper.style;
|
|
var cStyle = canvas.style || { };
|
|
|
|
var height = size.convert(viewBox.height - expandTop + expandBottom);
|
|
var roundedHeight = Math.ceil(height);
|
|
var roundingFactor = roundedHeight / height;
|
|
|
|
canvas.width = Math.ceil(size.convert(width + expandRight - expandLeft) * roundingFactor);
|
|
canvas.height = roundedHeight;
|
|
|
|
expandTop += viewBox.minY;
|
|
|
|
cStyle.top = Math.round(size.convert(expandTop - font.ascent)) + 'px';
|
|
cStyle.left = Math.round(size.convert(expandLeft)) + 'px';
|
|
|
|
var _width = Math.ceil(size.convert(width * roundingFactor));
|
|
var wrapperWidth = _width + 'px';
|
|
var _height = size.convert(font.height);
|
|
var totalLineHeight = (options.lineHeight - 1) * size.convert(-font.ascent / 5) * (lines - 1);
|
|
|
|
Cufon.textOptions.width = _width;
|
|
Cufon.textOptions.height = (_height * lines) + totalLineHeight;
|
|
Cufon.textOptions.lines = lines;
|
|
|
|
if (HAS_INLINE_BLOCK) {
|
|
wStyle.width = wrapperWidth;
|
|
wStyle.height = _height + 'px';
|
|
}
|
|
else {
|
|
wStyle.paddingLeft = wrapperWidth;
|
|
wStyle.paddingBottom = (_height - 1) + 'px';
|
|
}
|
|
|
|
var g = Cufon.textOptions.context || canvas.getContext('2d'),
|
|
scale = roundedHeight / viewBox.height;
|
|
|
|
g.save();
|
|
g.scale(scale, scale);
|
|
|
|
g.translate(
|
|
-expandLeft - ((1/scale * canvas.width) / 2) + (Cufon.fonts[font.family].offsetLeft || 0),
|
|
-expandTop - (Cufon.textOptions.height / scale) / 2
|
|
);
|
|
|
|
g.lineWidth = font.face['underline-thickness'];
|
|
|
|
g.save();
|
|
|
|
function line(y, color) {
|
|
g.strokeStyle = color;
|
|
|
|
g.beginPath();
|
|
|
|
g.moveTo(0, y);
|
|
g.lineTo(width, y);
|
|
|
|
g.stroke();
|
|
}
|
|
|
|
var textDecoration = options.enableTextDecoration ? Cufon.CSS.textDecoration(el, style) : {},
|
|
isItalic = options.fontStyle === 'italic';
|
|
|
|
function renderBackground() {
|
|
g.save();
|
|
|
|
g.fillStyle = options.backgroundColor;
|
|
|
|
var left = 0, lineNum = 0;
|
|
|
|
if (options.textAlign === 'right') {
|
|
g.translate(lineOffsets[lineNum], 0);
|
|
}
|
|
else if (options.textAlign === 'center') {
|
|
g.translate(lineOffsets[lineNum] / 2, 0);
|
|
}
|
|
|
|
for (var i = 0, l = chars.length; i < l; ++i) {
|
|
if (chars[i] === '\n') {
|
|
|
|
lineNum++;
|
|
|
|
var topOffset = -font.ascent - ((font.ascent / 5) * options.lineHeight);
|
|
|
|
if (options.textAlign === 'right') {
|
|
g.translate(-width, topOffset);
|
|
g.translate(lineOffsets[lineNum], 0);
|
|
}
|
|
else if (options.textAlign === 'center') {
|
|
g.translate(-left - (lineOffsets[lineNum - 1] / 2), topOffset);
|
|
g.translate(lineOffsets[lineNum] / 2, 0);
|
|
}
|
|
else {
|
|
g.translate(-left, topOffset);
|
|
}
|
|
|
|
left = 0;
|
|
|
|
continue;
|
|
}
|
|
var glyph = font.glyphs[chars[i]] || font.missingGlyph;
|
|
if (!glyph) continue;
|
|
|
|
var charWidth = Number(glyph.w || font.w) + letterSpacing;
|
|
|
|
g.save();
|
|
g.translate(0, font.ascent);
|
|
g.fillRect(0, 0, charWidth + 10, -font.ascent + font.descent);
|
|
g.restore();
|
|
|
|
g.translate(charWidth, 0);
|
|
left += charWidth;
|
|
}
|
|
g.restore();
|
|
}
|
|
|
|
function renderText() {
|
|
g.fillStyle = Cufon.textOptions.color || style.get('color');
|
|
|
|
var left = 0, lineNum = 0;
|
|
|
|
if (options.textAlign === 'right') {
|
|
g.translate(lineOffsets[lineNum], 0);
|
|
}
|
|
else if (options.textAlign === 'center') {
|
|
g.translate(lineOffsets[lineNum] / 2, 0);
|
|
}
|
|
|
|
for (var i = 0, l = chars.length; i < l; ++i) {
|
|
if (chars[i] === '\n') {
|
|
|
|
lineNum++;
|
|
|
|
var topOffset = -font.ascent - ((font.ascent / 5) * options.lineHeight);
|
|
|
|
if (options.textAlign === 'right') {
|
|
g.translate(-width, topOffset);
|
|
g.translate(lineOffsets[lineNum], 0);
|
|
}
|
|
else if (options.textAlign === 'center') {
|
|
g.translate(-left - (lineOffsets[lineNum - 1] / 2), topOffset);
|
|
g.translate(lineOffsets[lineNum] / 2, 0);
|
|
}
|
|
else {
|
|
g.translate(-left, topOffset);
|
|
}
|
|
|
|
left = 0;
|
|
|
|
continue;
|
|
}
|
|
var glyph = font.glyphs[chars[i]] || font.missingGlyph;
|
|
if (!glyph) continue;
|
|
|
|
var charWidth = Number(glyph.w || font.w) + letterSpacing;
|
|
|
|
if (textDecoration) {
|
|
g.save();
|
|
g.strokeStyle = g.fillStyle;
|
|
g.beginPath();
|
|
if (textDecoration.underline) {
|
|
g.moveTo(0, -font.face['underline-position']);
|
|
g.lineTo(charWidth, -font.face['underline-position']);
|
|
}
|
|
if (textDecoration.overline) {
|
|
g.moveTo(0, font.ascent);
|
|
g.lineTo(charWidth, font.ascent);
|
|
}
|
|
if (textDecoration['line-through']) {
|
|
g.moveTo(0, -font.descent);
|
|
g.lineTo(charWidth, -font.descent);
|
|
}
|
|
g.stroke();
|
|
g.restore();
|
|
}
|
|
|
|
if (isItalic) {
|
|
g.save();
|
|
g.transform(1, 0, -0.25, 1, 0, 0);
|
|
}
|
|
|
|
g.beginPath();
|
|
if (glyph.d) {
|
|
if (glyph.code) interpret(glyph.code, g);
|
|
else glyph.code = generateFromVML('m' + glyph.d, g);
|
|
}
|
|
|
|
g.fill();
|
|
|
|
if (options.strokeStyle) {
|
|
g.closePath();
|
|
g.save();
|
|
g.lineWidth = options.strokeWidth;
|
|
g.strokeStyle = options.strokeStyle;
|
|
g.stroke();
|
|
g.restore();
|
|
}
|
|
|
|
if (isItalic) {
|
|
g.restore();
|
|
}
|
|
|
|
g.translate(charWidth, 0);
|
|
left += charWidth;
|
|
}
|
|
}
|
|
|
|
if (shadows) {
|
|
for (var i = 0, l = shadows.length; i < l; ++i) {
|
|
var shadow = shadows[i];
|
|
g.save();
|
|
g.fillStyle = shadow.color;
|
|
g.translate.apply(g, shadowOffsets[i]);
|
|
renderText();
|
|
g.restore();
|
|
}
|
|
}
|
|
|
|
g.save();
|
|
if (options.backgroundColor) {
|
|
renderBackground();
|
|
}
|
|
renderText();
|
|
g.restore();
|
|
g.restore();
|
|
g.restore();
|
|
|
|
return wrapper;
|
|
|
|
};
|
|
|
|
})());
|
|
|
|
Cufon.registerEngine('vml', (function() {
|
|
|
|
if (!document.namespaces) return;
|
|
|
|
var canvasEl = document.createElement('canvas');
|
|
if (canvasEl && canvasEl.getContext && canvasEl.getContext.apply) return;
|
|
|
|
if (document.namespaces.cvml == null) {
|
|
document.namespaces.add('cvml', 'urn:schemas-microsoft-com:vml');
|
|
}
|
|
|
|
var check = document.createElement('cvml:shape');
|
|
check.style.behavior = 'url(#default#VML)';
|
|
if (!check.coordsize) return; // VML isn't supported
|
|
check = null;
|
|
|
|
document.write('<style type="text/css">' +
|
|
'.cufon-vml-canvas{text-indent:0}' +
|
|
'@media screen{' +
|
|
'cvml\\:shape,cvml\\:shadow{behavior:url(#default#VML);display:block;antialias:true;position:absolute}' +
|
|
'.cufon-vml-canvas{position:absolute;text-align:left}' +
|
|
'.cufon-vml{display:inline-block;position:relative;vertical-align:middle}' +
|
|
'.cufon-vml .cufon-alt{position:absolute;left:-10000in;font-size:1px}' +
|
|
'a .cufon-vml{cursor:pointer}' +
|
|
'}' +
|
|
'@media print{' +
|
|
'.cufon-vml *{display:none}' +
|
|
'.cufon-vml .cufon-alt{display:inline}' +
|
|
'}' +
|
|
'</style>');
|
|
|
|
function getFontSizeInPixels(el, value) {
|
|
return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value);
|
|
}
|
|
|
|
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;
|
|
|
|
|
|
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 = document.createElement('span');
|
|
wrapper.className = 'cufon cufon-vml';
|
|
wrapper.alt = text;
|
|
|
|
canvas = document.createElement('span');
|
|
canvas.className = 'cufon-vml-canvas';
|
|
wrapper.appendChild(canvas);
|
|
|
|
if (options.printable) {
|
|
var print = document.createElement('span');
|
|
print.className = 'cufon-alt';
|
|
print.appendChild(document.createTextNode(text));
|
|
wrapper.appendChild(print);
|
|
}
|
|
|
|
if (!hasNext) wrapper.appendChild(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 = options.enableTextDecoration ? Cufon.CSS.textDecoration(el, style) : {};
|
|
|
|
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;
|
|
|
|
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) {
|
|
shape = canvas.childNodes[k];
|
|
if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow
|
|
}
|
|
else {
|
|
shape = 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;
|
|
|
|
var sStyle = shape.style;
|
|
sStyle.width = roundedShapeWidth;
|
|
sStyle.height = roundedHeight;
|
|
|
|
if (shadows) {
|
|
var shadow1 = shadows[0], shadow2 = shadows[1];
|
|
var color1 = Cufon.CSS.color(shadow1.color), color2;
|
|
var shadow = 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;
|
|
|
|
};
|
|
|
|
})());
|
|
|
|
if (typeof exports != 'undefined') {
|
|
exports.Cufon = Cufon;
|
|
}
|
|
/*
|
|
http://www.JSON.org/json2.js
|
|
2010-03-20
|
|
|
|
Public Domain.
|
|
|
|
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
|
|
|
See http://www.JSON.org/js.html
|
|
|
|
|
|
This code should be minified before deployment.
|
|
See http://javascript.crockford.com/jsmin.html
|
|
|
|
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
|
NOT CONTROL.
|
|
|
|
|
|
This file creates a global JSON object containing two methods: stringify
|
|
and parse.
|
|
|
|
JSON.stringify(value, replacer, space)
|
|
value any JavaScript value, usually an object or array.
|
|
|
|
replacer an optional parameter that determines how object
|
|
values are stringified for objects. It can be a
|
|
function or an array of strings.
|
|
|
|
space an optional parameter that specifies the indentation
|
|
of nested structures. If it is omitted, the text will
|
|
be packed without extra whitespace. If it is a number,
|
|
it will specify the number of spaces to indent at each
|
|
level. If it is a string (such as '\t' or ' '),
|
|
it contains the characters used to indent at each level.
|
|
|
|
This method produces a JSON text from a JavaScript value.
|
|
|
|
When an object value is found, if the object contains a toJSON
|
|
method, its toJSON method will be called and the result will be
|
|
stringified. A toJSON method does not serialize: it returns the
|
|
value represented by the name/value pair that should be serialized,
|
|
or undefined if nothing should be serialized. The toJSON method
|
|
will be passed the key associated with the value, and this will be
|
|
bound to the value
|
|
|
|
For example, this would serialize Dates as ISO strings.
|
|
|
|
Date.prototype.toJSON = function (key) {
|
|
function f(n) {
|
|
return n < 10 ? '0' + n : n;
|
|
}
|
|
|
|
return this.getUTCFullYear() + '-' +
|
|
f(this.getUTCMonth() + 1) + '-' +
|
|
f(this.getUTCDate()) + 'T' +
|
|
f(this.getUTCHours()) + ':' +
|
|
f(this.getUTCMinutes()) + ':' +
|
|
f(this.getUTCSeconds()) + 'Z';
|
|
};
|
|
|
|
You can provide an optional replacer method. It will be passed the
|
|
key and value of each member, with this bound to the containing
|
|
object. The value that is returned from your method will be
|
|
serialized. If your method returns undefined, then the member will
|
|
be excluded from the serialization.
|
|
|
|
If the replacer parameter is an array of strings, then it will be
|
|
used to select the members to be serialized. It filters the results
|
|
such that only members with keys listed in the replacer array are
|
|
stringified.
|
|
|
|
Values that do not have JSON representations, such as undefined or
|
|
functions, will not be serialized. Such values in objects will be
|
|
dropped; in arrays they will be replaced with null. You can use
|
|
a replacer function to replace those with JSON values.
|
|
JSON.stringify(undefined) returns undefined.
|
|
|
|
The optional space parameter produces a stringification of the
|
|
value that is filled with line breaks and indentation to make it
|
|
easier to read.
|
|
|
|
If the space parameter is a non-empty string, then that string will
|
|
be used for indentation. If the space parameter is a number, then
|
|
the indentation will be that many spaces.
|
|
|
|
Example:
|
|
|
|
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
|
|
|
|
|
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
|
|
|
text = JSON.stringify([new Date()], function (key, value) {
|
|
return this[key] instanceof Date ?
|
|
'Date(' + this[key] + ')' : value;
|
|
});
|
|
|
|
|
|
JSON.parse(text, reviver)
|
|
This method parses a JSON text to produce an object or array.
|
|
It can throw a SyntaxError exception.
|
|
|
|
The optional reviver parameter is a function that can filter and
|
|
transform the results. It receives each of the keys and values,
|
|
and its return value is used instead of the original value.
|
|
If it returns what it received, then the structure is not modified.
|
|
If it returns undefined then the member is deleted.
|
|
|
|
Example:
|
|
|
|
|
|
myData = JSON.parse(text, function (key, value) {
|
|
var a;
|
|
if (typeof value === 'string') {
|
|
a =
|
|
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
|
if (a) {
|
|
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
|
+a[5], +a[6]));
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
|
|
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
|
var d;
|
|
if (typeof value === 'string' &&
|
|
value.slice(0, 5) === 'Date(' &&
|
|
value.slice(-1) === ')') {
|
|
d = new Date(value.slice(5, -1));
|
|
if (d) {
|
|
return d;
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
|
|
|
|
This is a reference implementation. You are free to copy, modify, or
|
|
redistribute.
|
|
*/
|
|
|
|
/*jslint evil: true, strict: false */
|
|
|
|
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
|
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
|
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
|
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
|
test, toJSON, toString, valueOf
|
|
*/
|
|
|
|
|
|
|
|
if (!this.JSON) {
|
|
this.JSON = {};
|
|
}
|
|
|
|
(function () {
|
|
|
|
function f(n) {
|
|
return n < 10 ? '0' + n : n;
|
|
}
|
|
|
|
if (typeof Date.prototype.toJSON !== 'function') {
|
|
|
|
Date.prototype.toJSON = function (key) {
|
|
|
|
return isFinite(this.valueOf()) ?
|
|
this.getUTCFullYear() + '-' +
|
|
f(this.getUTCMonth() + 1) + '-' +
|
|
f(this.getUTCDate()) + 'T' +
|
|
f(this.getUTCHours()) + ':' +
|
|
f(this.getUTCMinutes()) + ':' +
|
|
f(this.getUTCSeconds()) + 'Z' : null;
|
|
};
|
|
|
|
String.prototype.toJSON =
|
|
Number.prototype.toJSON =
|
|
Boolean.prototype.toJSON = function (key) {
|
|
return this.valueOf();
|
|
};
|
|
}
|
|
|
|
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
|
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
|
gap,
|
|
indent,
|
|
meta = { // table of character substitutions
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\f': '\\f',
|
|
'\r': '\\r',
|
|
'"' : '\\"',
|
|
'\\': '\\\\'
|
|
},
|
|
rep;
|
|
|
|
|
|
function quote(string) {
|
|
|
|
|
|
escapable.lastIndex = 0;
|
|
return escapable.test(string) ?
|
|
'"' + string.replace(escapable, function (a) {
|
|
var c = meta[a];
|
|
return typeof c === 'string' ? c :
|
|
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
}) + '"' :
|
|
'"' + string + '"';
|
|
}
|
|
|
|
|
|
function str(key, holder) {
|
|
|
|
|
|
var i, // The loop counter.
|
|
k, // The member key.
|
|
v, // The member value.
|
|
length,
|
|
mind = gap,
|
|
partial,
|
|
value = holder[key];
|
|
|
|
|
|
if (value && typeof value === 'object' &&
|
|
typeof value.toJSON === 'function') {
|
|
value = value.toJSON(key);
|
|
}
|
|
|
|
|
|
if (typeof rep === 'function') {
|
|
value = rep.call(holder, key, value);
|
|
}
|
|
|
|
|
|
switch (typeof value) {
|
|
case 'string':
|
|
return quote(value);
|
|
|
|
case 'number':
|
|
|
|
|
|
return isFinite(value) ? String(value) : 'null';
|
|
|
|
case 'boolean':
|
|
case 'null':
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
case 'object':
|
|
|
|
|
|
if (!value) {
|
|
return 'null';
|
|
}
|
|
|
|
|
|
gap += indent;
|
|
partial = [];
|
|
|
|
|
|
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
|
|
|
|
|
length = value.length;
|
|
for (i = 0; i < length; i += 1) {
|
|
partial[i] = str(i, value) || 'null';
|
|
}
|
|
|
|
|
|
v = partial.length === 0 ? '[]' :
|
|
gap ? '[\n' + gap +
|
|
partial.join(',\n' + gap) + '\n' +
|
|
mind + ']' :
|
|
'[' + partial.join(',') + ']';
|
|
gap = mind;
|
|
return v;
|
|
}
|
|
|
|
|
|
if (rep && typeof rep === 'object') {
|
|
length = rep.length;
|
|
for (i = 0; i < length; i += 1) {
|
|
k = rep[i];
|
|
if (typeof k === 'string') {
|
|
v = str(k, value);
|
|
if (v) {
|
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
|
|
|
|
for (k in value) {
|
|
if (Object.hasOwnProperty.call(value, k)) {
|
|
v = str(k, value);
|
|
if (v) {
|
|
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
v = partial.length === 0 ? '{}' :
|
|
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
|
|
mind + '}' : '{' + partial.join(',') + '}';
|
|
gap = mind;
|
|
return v;
|
|
}
|
|
}
|
|
|
|
|
|
if (typeof JSON.stringify !== 'function') {
|
|
JSON.stringify = function (value, replacer, space) {
|
|
|
|
|
|
var i;
|
|
gap = '';
|
|
indent = '';
|
|
|
|
|
|
if (typeof space === 'number') {
|
|
for (i = 0; i < space; i += 1) {
|
|
indent += ' ';
|
|
}
|
|
|
|
|
|
} else if (typeof space === 'string') {
|
|
indent = space;
|
|
}
|
|
|
|
|
|
rep = replacer;
|
|
if (replacer && typeof replacer !== 'function' &&
|
|
(typeof replacer !== 'object' ||
|
|
typeof replacer.length !== 'number')) {
|
|
throw new Error('JSON.stringify');
|
|
}
|
|
|
|
|
|
return str('', {'': value});
|
|
};
|
|
}
|
|
|
|
|
|
|
|
if (typeof JSON.parse !== 'function') {
|
|
JSON.parse = function (text, reviver) {
|
|
|
|
|
|
var j;
|
|
|
|
function walk(holder, key) {
|
|
|
|
|
|
var k, v, value = holder[key];
|
|
if (value && typeof value === 'object') {
|
|
for (k in value) {
|
|
if (Object.hasOwnProperty.call(value, k)) {
|
|
v = walk(value, k);
|
|
if (v !== undefined) {
|
|
value[k] = v;
|
|
} else {
|
|
delete value[k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reviver.call(holder, key, value);
|
|
}
|
|
|
|
|
|
|
|
text = String(text);
|
|
cx.lastIndex = 0;
|
|
if (cx.test(text)) {
|
|
text = text.replace(cx, function (a) {
|
|
return '\\u' +
|
|
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
if (/^[\],:{}\s]*$/.
|
|
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
|
|
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
|
|
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
|
|
|
|
|
j = eval('(' + text + ')');
|
|
|
|
|
|
return typeof reviver === 'function' ?
|
|
walk({'': j}, '') : j;
|
|
}
|
|
|
|
|
|
throw new SyntaxError('JSON.parse');
|
|
};
|
|
}
|
|
}());
|
|
|
|
/**
|
|
* 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);
|
|
};
|
|
}
|
|
}
|
|
(function (global) {
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
slice = Array.prototype.slice,
|
|
apply = Function.prototype.apply;
|
|
|
|
/** @namespace */
|
|
fabric.util = { };
|
|
|
|
fabric.Observable = {
|
|
|
|
/**
|
|
* @mthod observe
|
|
* @param {String} eventName
|
|
* @param {Function} handler
|
|
*/
|
|
observe: function(eventName, handler) {
|
|
if (!this.__eventListeners) {
|
|
this.__eventListeners = { };
|
|
}
|
|
if (arguments.length === 1) {
|
|
for (var prop in eventName) {
|
|
this.observe(prop, eventName[prop]);
|
|
}
|
|
}
|
|
else {
|
|
if (!this.__eventListeners[eventName]) {
|
|
this.__eventListeners[eventName] = [ ];
|
|
}
|
|
this.__eventListeners[eventName].push(handler);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @mthod stopObserving
|
|
* @memberOf fabric.util
|
|
* @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 memo object
|
|
* @mthod fire
|
|
* @memberOf fabric.util
|
|
* @param {String} eventName
|
|
* @param {Object} [memo]
|
|
*/
|
|
fire: function(eventName, memo) {
|
|
if (!this.__eventListeners) {
|
|
this.__eventListeners = { }
|
|
}
|
|
var listenersForEvent = this.__eventListeners[eventName];
|
|
if (!listenersForEvent) return;
|
|
for (var i = 0, len = listenersForEvent.length; i < len; i++) {
|
|
listenersForEvent[i]({ memo: memo });
|
|
}
|
|
}
|
|
};
|
|
(function() {
|
|
|
|
/**
|
|
* 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 {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, pos,
|
|
onChange = options.onChange || function() { },
|
|
abort = options.abort || function() { return false; },
|
|
easing = options.easing || function(pos) { return (-Math.cos(pos * Math.PI) / 2) + 0.5; },
|
|
startValue = 'startValue' in options ? options.startValue : 0,
|
|
endValue = 'endValue' in options ? options.endValue : 100,
|
|
isReversed = startValue > endValue;
|
|
|
|
options.onStart && options.onStart();
|
|
|
|
var interval = setInterval(function() {
|
|
time = +new Date();
|
|
pos = time > finish ? 1 : (time - start) / duration;
|
|
onChange(isReversed
|
|
? (startValue - (startValue - endValue) * easing(pos))
|
|
: (startValue + (endValue - startValue) * easing(pos)));
|
|
if (time > finish || abort()) {
|
|
clearInterval(interval);
|
|
options.onComplete && options.onComplete();
|
|
}
|
|
}, 10);
|
|
|
|
return interval;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 (url, callback) {
|
|
/* NOOP */
|
|
},
|
|
|
|
/**
|
|
* @method set
|
|
* @param {String} url
|
|
* @param {Object} object
|
|
*/
|
|
set: function (url, object) {
|
|
/* 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
|
|
*/
|
|
function loadSVGFromURL(url, callback) {
|
|
|
|
url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').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) return;
|
|
|
|
var doc = xml.documentElement;
|
|
if (!doc) return;
|
|
|
|
fabric.parseSVGDocument(doc, function (results, options) {
|
|
svgCache.set(url, {
|
|
objects: fabric.util.array.invoke(results, 'toObject'),
|
|
options: options
|
|
});
|
|
callback(results, options);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
function loadSVGFromString(string, callback) {
|
|
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 (window.ActiveXObject) {
|
|
var doc = new ActiveXObject('Microsoft.XMLDOM');
|
|
if (doc && doc.loadXML) {
|
|
doc.async = 'false';
|
|
doc.loadXML(string);
|
|
}
|
|
}
|
|
|
|
fabric.parseSVGDocument(doc.documentElement, function (results, options) {
|
|
callback(results, options);
|
|
});
|
|
}
|
|
|
|
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.loadSVGFromURL = loadSVGFromURL;
|
|
fabric.loadSVGFromString = loadSVGFromString;
|
|
})();
|
|
|
|
if (!Array.prototype.indexOf) {
|
|
Array.prototype.indexOf = function(value, from) {
|
|
var len = this.length >>> 0;
|
|
from = Number(from) || 0;
|
|
from = Math[from < 0 ? 'ceil' : 'floor'](from);
|
|
if (from < 0) {
|
|
from += len;
|
|
}
|
|
for (; from < len; from++) {
|
|
if (from in this && this[from] === value) {
|
|
return from;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.forEach) {
|
|
Array.prototype.forEach = function(fn, context) {
|
|
for (var i = 0, len = this.length >>> 0; i < len; i++) {
|
|
if (i in this) {
|
|
fn.call(context, this[i], i, this);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.map) {
|
|
Array.prototype.map = function(fn, context) {
|
|
var result = [ ];
|
|
for (var i = 0, len = this.length >>> 0; i < len; i++) {
|
|
if (i in this) {
|
|
result[i] = fn.call(context, this[i], i, this);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.every) {
|
|
Array.prototype.every = function(fn, context) {
|
|
for (var i = 0, len = this.length >>> 0; i < len; i++) {
|
|
if (i in this && !fn.call(context, this[i], i, this)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.some) {
|
|
Array.prototype.some = function(fn, context) {
|
|
for (var i = 0, len = this.length >>> 0; i < len; i++) {
|
|
if (i in this && fn.call(context, this[i], i, this)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.filter) {
|
|
Array.prototype.filter = function(fn, context) {
|
|
var result = [ ], val;
|
|
for (var i = 0, len = this.length >>> 0; i < len; i++) {
|
|
if (i in this) {
|
|
val = this[i]; // in case fn mutates this
|
|
if (fn.call(context, val, i, this)) {
|
|
result.push(val);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
if (!Array.prototype.reduce) {
|
|
Array.prototype.reduce = function(fn /*, initial*/) {
|
|
var len = this.length >>> 0,
|
|
i = 0,
|
|
rv;
|
|
|
|
if (arguments.length > 1) {
|
|
rv = arguments[1];
|
|
}
|
|
else {
|
|
do {
|
|
if (i in this) {
|
|
rv = this[i++];
|
|
break;
|
|
}
|
|
if (++i >= len) {
|
|
throw new TypeError();
|
|
}
|
|
}
|
|
while (true);
|
|
}
|
|
for (; i < len; i++) {
|
|
if (i in this) {
|
|
rv = fn.call(null, rv, this[i], i, this);
|
|
}
|
|
}
|
|
return rv;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
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) {
|
|
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
|
|
};
|
|
/**
|
|
* 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) {
|
|
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
|
|
};
|
|
if (!String.prototype.trim) {
|
|
/**
|
|
* Trims a string (removing whitespace from the beginning and the end)
|
|
* @method trim
|
|
* @see <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/Trim">String#trim on MDN</a>
|
|
*/
|
|
String.prototype.trim = function () {
|
|
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();
|
|
}
|
|
|
|
/** @namespace */
|
|
fabric.util.string = {
|
|
camelize: camelize,
|
|
capitalize: capitalize
|
|
};
|
|
if (!Function.prototype.bind) {
|
|
/**
|
|
* Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming)
|
|
* @see <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind">Function#bind on MDN</a>
|
|
* @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);
|
|
return args.length
|
|
? function() { return apply.call(fn, thisArg, args.concat(slice.call(arguments))); }
|
|
: function() { return apply.call(fn, thisArg, arguments) };
|
|
};
|
|
}
|
|
|
|
|
|
(function() {
|
|
|
|
var IS_DONTENUM_BUGGY = (function(){
|
|
for (var p in { toString: 1 }) {
|
|
if (p === 'toString') return false;
|
|
}
|
|
return true;
|
|
})();
|
|
|
|
var addMethods;
|
|
if (IS_DONTENUM_BUGGY) {
|
|
/** @ignore */
|
|
addMethods = function(klass, source) {
|
|
if (source.toString !== Object.prototype.toString) {
|
|
klass.prototype.toString = source.toString;
|
|
}
|
|
if (source.valueOf !== Object.prototype.valueOf) {
|
|
klass.prototype.valueOf = source.valueOf;
|
|
}
|
|
for (var property in source) {
|
|
klass.prototype[property] = source[property];
|
|
}
|
|
};
|
|
}
|
|
else {
|
|
/** @ignore */
|
|
addMethods = function(klass, source) {
|
|
for (var property in source) {
|
|
klass.prototype[property] = source[property];
|
|
}
|
|
};
|
|
}
|
|
|
|
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]);
|
|
}
|
|
if (!klass.prototype.initialize) {
|
|
klass.prototype.initialize = emptyFunction;
|
|
}
|
|
klass.prototype.constructor = klass;
|
|
return klass;
|
|
}
|
|
|
|
fabric.util.createClass = createClass;
|
|
})();
|
|
|
|
(function (global) {
|
|
|
|
/* EVENT HANDLING */
|
|
|
|
function areHostMethods(object) {
|
|
var methodNames = Array.prototype.slice.call(arguments, 1),
|
|
t, i, len = methodNames.length;
|
|
for (i = 0; i < len; i++) {
|
|
t = typeof object[methodNames[i]];
|
|
if (!(/^(?:function|object|unknown)$/).test(t)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
var getUniqueId = (function () {
|
|
if (typeof document.documentElement.uniqueID !== 'undefined') {
|
|
return function (element) {
|
|
return element.uniqueID;
|
|
};
|
|
}
|
|
var uid = 0;
|
|
return function (element) {
|
|
return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++);
|
|
};
|
|
})();
|
|
|
|
/** @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 || window.event);
|
|
};
|
|
}
|
|
|
|
function createDispatcher(uid, eventName) {
|
|
return function (e) {
|
|
if (handlers[uid] && handlers[uid][eventName]) {
|
|
var handlersForEvent = handlers[uid][eventName];
|
|
for (var i = 0, len = handlersForEvent.length; i < len; i++) {
|
|
handlersForEvent[i].call(this, e || window.event);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
var shouldUseAddListenerRemoveListener = (
|
|
areHostMethods(document.documentElement, 'addEventListener', 'removeEventListener') &&
|
|
areHostMethods(window, 'addEventListener', 'removeEventListener')),
|
|
|
|
shouldUseAttachEventDetachEvent = (
|
|
areHostMethods(document.documentElement, 'attachEvent', 'detachEvent') &&
|
|
areHostMethods(window, 'attachEvent', 'detachEvent')),
|
|
|
|
listeners = { },
|
|
|
|
handlers = { },
|
|
|
|
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) {
|
|
return { x: pointerX(event), y: pointerY(event) };
|
|
}
|
|
|
|
function pointerX(event) {
|
|
var docElement = document.documentElement,
|
|
body = document.body || { scrollLeft: 0 };
|
|
|
|
return event.pageX || ((typeof event.clientX != 'unknown' ? event.clientX : 0) +
|
|
(docElement.scrollLeft || body.scrollLeft) -
|
|
(docElement.clientLeft || 0));
|
|
}
|
|
|
|
function pointerY(event) {
|
|
var docElement = document.documentElement,
|
|
body = document.body || { scrollTop: 0 };
|
|
|
|
return event.pageY || ((typeof event.clientY != 'unknown' ? event.clientY : 0) +
|
|
(docElement.scrollTop || body.scrollTop) -
|
|
(docElement.clientTop || 0));
|
|
}
|
|
|
|
fabric.util.getPointer = getPointer;
|
|
|
|
fabric.util.object.extend(fabric.util, fabric.Observable);
|
|
|
|
})(this);
|
|
(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, match;
|
|
if (typeof styles === 'string') {
|
|
element.style.cssText += ';' + styles;
|
|
return styles.indexOf('opacity') > -1
|
|
? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1])
|
|
: element;
|
|
}
|
|
for (var property in styles) {
|
|
if (property === 'opacity') {
|
|
setOpacity(element, styles[property]);
|
|
}
|
|
else {
|
|
var normalizedProperty = (property === 'float' || property === 'cssFloat')
|
|
? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat')
|
|
: property;
|
|
elementStyle[normalizedProperty] = styles[property];
|
|
}
|
|
}
|
|
return element;
|
|
}
|
|
|
|
var parseEl = document.createElement('div'),
|
|
supportsOpacity = typeof parseEl.style.opacity === 'string',
|
|
supportsFilters = typeof parseEl.style.filter === 'string',
|
|
view = document.defaultView,
|
|
supportsGCS = view && typeof view.getComputedStyle !== 'undefined',
|
|
reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,
|
|
|
|
/** @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;
|
|
|
|
})();
|
|
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' ? 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}
|
|
*/
|
|
function toArray(arrayLike) {
|
|
return _slice.call(arrayLike, 0);
|
|
}
|
|
|
|
try {
|
|
var sliceCanConvertNodelists = toArray(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 = 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) {
|
|
var valueT = 0, valueL = 0;
|
|
do {
|
|
valueT += element.offsetTop || 0;
|
|
valueL += element.offsetLeft || 0;
|
|
element = element.offsetParent;
|
|
}
|
|
while (element);
|
|
return ({ left: valueL, top: valueT });
|
|
}
|
|
|
|
(function () {
|
|
var style = document.documentElement.style;
|
|
|
|
var selectProp = 'userSelect' in style
|
|
? 'userSelect'
|
|
: 'MozUserSelect' in style
|
|
? 'MozUserSelect'
|
|
: 'WebkitUserSelect' in style
|
|
? 'WebkitUserSelect'
|
|
: 'KhtmlUserSelect' in style
|
|
? 'KhtmlUserSelect'
|
|
: '';
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
fabric.util.makeElementUnselectable = makeElementUnselectable;
|
|
})();
|
|
|
|
(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 = document.getElementsByTagName("head")[0],
|
|
scriptEl = 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 || window.event);
|
|
scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null;
|
|
}
|
|
};
|
|
scriptEl.src = url;
|
|
headEl.appendChild(scriptEl);
|
|
}
|
|
|
|
function getScriptJaxer(url, callback) {
|
|
Jaxer.load(url);
|
|
callback();
|
|
}
|
|
|
|
fabric.util.getScript = getScript;
|
|
|
|
var Jaxer = global.Jaxer;
|
|
if (Jaxer && Jaxer.load) {
|
|
fabric.util.getScript = getScriptJaxer;
|
|
}
|
|
})();
|
|
|
|
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() { },
|
|
request = makeXHR(),
|
|
body;
|
|
|
|
/** @ignore */
|
|
request.onreadystatechange = function() {
|
|
if (request.readyState === 4) {
|
|
onComplete(request);
|
|
request.onreadystatechange = emptyFn;
|
|
}
|
|
};
|
|
|
|
if (method === 'GET') {
|
|
body = null;
|
|
if (typeof options.parameters == 'string') {
|
|
url = addParamToUrl(url, options.parameters);
|
|
}
|
|
}
|
|
|
|
request.open(method, url, true);
|
|
|
|
if (method === 'POST' || method === 'PUT') {
|
|
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
|
}
|
|
|
|
request.send(body);
|
|
return request;
|
|
};
|
|
|
|
fabric.util.request = request;
|
|
})();
|
|
|
|
})(this);
|
|
(function(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'
|
|
};
|
|
|
|
/**
|
|
* 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 (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) {
|
|
parentAttributes = fabric.parseAttributes(element.parentNode, attributes);
|
|
}
|
|
|
|
var ownAttributes = attributes.reduce(function(memo, attr) {
|
|
value = element.getAttribute(attr);
|
|
parsed = parseFloat(value);
|
|
if (value) {
|
|
if ((attr === 'fill' || attr === 'stroke') && value === 'none') {
|
|
value = '';
|
|
}
|
|
if (attr === 'fill-rule') {
|
|
value = (value === 'evenodd') ? 'destination-over' : value;
|
|
}
|
|
if (attr === 'transform') {
|
|
value = fabric.parseTransformAttribute(value);
|
|
}
|
|
if (attr in attributesMap) {
|
|
attr = attributesMap[attr];
|
|
}
|
|
memo[attr] = isNaN(parsed) ? value : parsed;
|
|
}
|
|
return memo;
|
|
}, { });
|
|
|
|
|
|
ownAttributes = extend(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];
|
|
}
|
|
}
|
|
|
|
var iMatrix = [
|
|
1, // a
|
|
0, // b
|
|
0, // c
|
|
1, // d
|
|
0, // e
|
|
0 // f
|
|
],
|
|
|
|
number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)',
|
|
comma_wsp = '(?:\\s+,?\\s*|,\\s*)',
|
|
|
|
skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))',
|
|
skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))',
|
|
rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))',
|
|
scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
|
|
translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))',
|
|
|
|
matrix = '(?:(matrix)\\s*\\(\\s*' +
|
|
'(' + number + ')' + comma_wsp +
|
|
'(' + number + ')' + comma_wsp +
|
|
'(' + number + ')' + comma_wsp +
|
|
'(' + number + ')' + comma_wsp +
|
|
'(' + number + ')' + comma_wsp +
|
|
'(' + number + ')' +
|
|
'\\s*\\))',
|
|
|
|
transform = '(?:' +
|
|
matrix + '|' +
|
|
translate + '|' +
|
|
scale + '|' +
|
|
rotate + '|' +
|
|
skewX + '|' +
|
|
skewY +
|
|
')',
|
|
|
|
transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')',
|
|
|
|
transform_list = '^\\s*(?:' + transforms + '?)\\s*$',
|
|
|
|
reTransformList = new RegExp(transform_list),
|
|
|
|
reTransform = new RegExp(transform);
|
|
|
|
return function(attributeValue) {
|
|
|
|
var matrix = iMatrix.concat();
|
|
|
|
if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) {
|
|
return matrix;
|
|
}
|
|
|
|
attributeValue.replace(reTransform, function(match) {
|
|
|
|
var m = new RegExp(transform).exec(match).filter(function (match) {
|
|
return (match !== '' && match != null);
|
|
}),
|
|
operation = m[1],
|
|
args = m.slice(2).map(parseFloat);
|
|
|
|
switch(operation) {
|
|
case 'translate':
|
|
translateMatrix(matrix, args);
|
|
break;
|
|
case 'rotate':
|
|
rotateMatrix(matrix, args);
|
|
break;
|
|
case 'scale':
|
|
scaleMatrix(matrix, args);
|
|
break;
|
|
case 'skewX':
|
|
skewXMatrix(matrix, args);
|
|
break;
|
|
case 'skewY':
|
|
skewYMatrix(matrix, args);
|
|
break;
|
|
case 'matrix':
|
|
matrix = args;
|
|
break;
|
|
}
|
|
})
|
|
return matrix;
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* 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) {
|
|
|
|
if (!points) return null;
|
|
|
|
points = points.trim();
|
|
var asPairs = points.indexOf(',') > -1;
|
|
|
|
points = points.split(/\s+/);
|
|
var parsedPoints = [ ];
|
|
|
|
if (asPairs) {
|
|
for (var i = 0, len = points.length; i < len; i++) {
|
|
var pair = points[i].split(',');
|
|
parsedPoints.push({ x: parseFloat(pair[0]), y: parseFloat(pair[1]) });
|
|
}
|
|
}
|
|
else {
|
|
for (var i = 0, len = points.length; i < len; i+=2) {
|
|
parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i+1]) });
|
|
}
|
|
}
|
|
|
|
if (parsedPoints.length % 2 !== 0) {
|
|
}
|
|
|
|
return parsedPoints;
|
|
};
|
|
|
|
/**
|
|
* 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) {
|
|
if (typeof style == 'string') {
|
|
style = style.replace(/;$/, '').split(';');
|
|
oStyle = style.reduce(function(memo, current) {
|
|
var attr = current.split(':'),
|
|
key = attr[0].trim(),
|
|
value = attr[1].trim();
|
|
memo[key] = value;
|
|
return memo;
|
|
}, { });
|
|
}
|
|
else {
|
|
for (var prop in style) {
|
|
if (typeof style[prop] !== 'undefined') {
|
|
oStyle[prop] = style[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return oStyle;
|
|
};
|
|
|
|
function resolveGradients(instances) {
|
|
var activeInstance = fabric.Canvas.activeInstance,
|
|
ctx = activeInstance ? activeInstance.getContext() : null;
|
|
|
|
if (!ctx) return;
|
|
|
|
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], ctx, 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
|
|
*/
|
|
function parseElements(elements, callback, options) {
|
|
var instances = 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.fromElement.async) {
|
|
klass.fromElement(el, (function(index) {
|
|
return function(obj) {
|
|
instances.splice(index, 0, obj);
|
|
checkIfDone();
|
|
};
|
|
})(index), options);
|
|
}
|
|
else {
|
|
instances.splice(index, 0, klass.fromElement(el, options));
|
|
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;
|
|
|
|
for (var i = 0, len = styles.length; i < len; i++) {
|
|
var styleContents = styles[0].textContent;
|
|
|
|
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],
|
|
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).
|
|
*/
|
|
fabric.parseSVGDocument = (function() {
|
|
|
|
var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image)$/;
|
|
|
|
|
|
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) {
|
|
if (!doc) return;
|
|
|
|
var startTime = new Date(),
|
|
descendants = fabric.util.toArray(doc.getElementsByTagName('*'));
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
|
|
fabric.parseElements(elements, function(instances) {
|
|
fabric.documentParsingTime = new Date() - startTime;
|
|
if (callback) {
|
|
callback(instances, options);
|
|
}
|
|
}, clone(options));
|
|
};
|
|
})();
|
|
|
|
extend(fabric, {
|
|
parseAttributes: parseAttributes,
|
|
parseElements: parseElements,
|
|
parseStyleAttribute: parseStyleAttribute,
|
|
parsePointsAttribute: parsePointsAttribute,
|
|
getCSSRules: getCSSRules
|
|
});
|
|
|
|
})(typeof exports != 'undefined' ? exports : this);
|
|
(function() {
|
|
|
|
function getColorStopFromStyle(el) {
|
|
var style = el.getAttribute('style');
|
|
|
|
if (style) {
|
|
var keyValuePairs = style.split(/\s*;\s*/);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @namespace */
|
|
|
|
fabric.Gradient = {
|
|
|
|
/**
|
|
* @method create
|
|
* @static
|
|
*/
|
|
create: function(ctx, options) {
|
|
options || (options = { });
|
|
|
|
var x1 = options.x1 || 0,
|
|
y1 = options.y1 || 0,
|
|
x2 = options.x2 || ctx.canvas.width,
|
|
y2 = options.y2 || 0,
|
|
colorStops = options.colorStops;
|
|
|
|
var gradient = ctx.createLinearGradient(x1, y1, x2, y2);
|
|
|
|
for (var position in colorStops) {
|
|
var colorValue = colorStops[position];
|
|
gradient.addColorStop(parseFloat(position), colorValue);
|
|
}
|
|
return gradient;
|
|
},
|
|
|
|
/**
|
|
* @method fromElement
|
|
* @static
|
|
* @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement
|
|
*/
|
|
fromElement: function(el, ctx, instance) {
|
|
|
|
/**
|
|
* @example:
|
|
*
|
|
* <linearGradient id="grad1">
|
|
* <stop offset="0%" stop-color="white"/>
|
|
* <stop offset="100%" stop-color="black"/>
|
|
* </linearGradient>
|
|
*
|
|
* OR
|
|
*
|
|
* <linearGradient id="grad1">
|
|
* <stop offset="0%" style="stop-color:rgb(255,255,255)"/>
|
|
* <stop offset="100%" style="stop-color:rgb(0,0,0)"/>
|
|
* </linearGradient>
|
|
*
|
|
*/
|
|
|
|
var colorStopEls = el.getElementsByTagName('stop'),
|
|
el,
|
|
offset,
|
|
colorStops = { },
|
|
colorStopFromStyle;
|
|
|
|
for (var i = colorStopEls.length; i--; ) {
|
|
el = colorStopEls[i];
|
|
offset = parseInt(el.getAttribute('offset'), 10) / 100;
|
|
colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color');
|
|
}
|
|
|
|
var coords = {
|
|
x1: el.getAttribute('x1') || 0,
|
|
y1: el.getAttribute('y1') || 0,
|
|
x2: el.getAttribute('x2') || '100%',
|
|
y2: el.getAttribute('y2') || 0
|
|
};
|
|
|
|
_convertPercentUnitsToValues(instance, coords);
|
|
|
|
return fabric.Gradient.create(ctx, {
|
|
x1: coords.x1,
|
|
y1: coords.y1,
|
|
x2: coords.x2,
|
|
y2: coords.y2,
|
|
colorStops: colorStops
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @method forObject
|
|
* @static
|
|
*/
|
|
forObject: function(obj, ctx, options) {
|
|
options || (options = { });
|
|
|
|
_convertPercentUnitsToValues(obj, options);
|
|
|
|
var gradient = fabric.Gradient.create(ctx, {
|
|
x1: options.x1 - (obj.width / 2),
|
|
y1: options.y1 - (obj.height / 2),
|
|
x2: options.x2 - (obj.width / 2),
|
|
y2: options.y2 - (obj.height / 2),
|
|
colorStops: options.colorStops
|
|
});
|
|
|
|
return gradient;
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
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,
|
|
gradientDefs = { };
|
|
|
|
for (var i = linearGradientEls.length; i--; ) {
|
|
el = linearGradientEls[i];
|
|
gradientDefs[el.id] = el;
|
|
}
|
|
|
|
for (var i = radialGradientEls.length; i--; ) {
|
|
el = radialGradientEls[i];
|
|
gradientDefs[el.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[4] = 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 (global) {
|
|
|
|
"use strict";
|
|
|
|
if (fabric.Canvas) {
|
|
fabric.warn('fabric.Canvas is already defined.');
|
|
return;
|
|
}
|
|
|
|
var extend = fabric.util.object.extend,
|
|
capitalize = fabric.util.string.capitalize,
|
|
camelize = fabric.util.string.camelize,
|
|
getPointer = fabric.util.getPointer,
|
|
getElementOffset = fabric.util.getElementOffset,
|
|
removeFromArray = fabric.util.removeFromArray,
|
|
addListener = fabric.util.addListener,
|
|
removeListener = fabric.util.removeListener,
|
|
|
|
utilMin = fabric.util.array.min,
|
|
utilMax = fabric.util.array.max,
|
|
|
|
sqrt = Math.sqrt,
|
|
pow = Math.pow,
|
|
atan2 = Math.atan2,
|
|
abs = Math.abs,
|
|
min = Math.min,
|
|
max = Math.max,
|
|
|
|
CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'),
|
|
STROKE_OFFSET = 0.5,
|
|
|
|
cursorMap = {
|
|
'tr': 'ne-resize',
|
|
'br': 'se-resize',
|
|
'bl': 'sw-resize',
|
|
'tl': 'nw-resize',
|
|
'ml': 'w-resize',
|
|
'mt': 'n-resize',
|
|
'mr': 'e-resize',
|
|
'mb': 's-resize'
|
|
};
|
|
|
|
/**
|
|
* @class fabric.Canvas
|
|
* @constructor
|
|
* @param {HTMLElement | String} el <canvas> element to initialize instance on
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
fabric.Canvas = function (el, options) {
|
|
|
|
options || (options = { });
|
|
|
|
/**
|
|
* The object literal containing mouse position if clicked in an empty area (no image)
|
|
* @property _groupSelector
|
|
* @type object
|
|
*/
|
|
this._groupSelector = null;
|
|
|
|
/**
|
|
* The array literal containing all objects on canvas
|
|
* @property _objects
|
|
* @type array
|
|
*/
|
|
this._objects = [];
|
|
|
|
/**
|
|
* The element that references the canvas interface implementation
|
|
* @property _context
|
|
* @type object
|
|
*/
|
|
this._context = null;
|
|
|
|
/**
|
|
* The object literal containing the current x,y params of the transformation
|
|
* @property _currentTransform
|
|
* @type object
|
|
*/
|
|
this._currentTransform = null;
|
|
|
|
/**
|
|
* References instance of fabric.Group - when multiple objects are selected
|
|
* @property _activeGroup
|
|
* @type object
|
|
*/
|
|
this._activeGroup = null;
|
|
|
|
/**
|
|
* X coordinates of a path, captured during free drawing
|
|
*/
|
|
this._freeDrawingXPoints = [ ];
|
|
|
|
/**
|
|
* Y coordinates of a path, captured during free drawing
|
|
*/
|
|
this._freeDrawingYPoints = [ ];
|
|
|
|
this._createUpperCanvas(el);
|
|
this._initOptions(options);
|
|
this._initWrapperElement();
|
|
this._createLowerCanvas();
|
|
|
|
this._initEvents();
|
|
|
|
if (options.overlayImage) {
|
|
this.setOverlayImage(options.overlayImage);
|
|
}
|
|
|
|
this.calcOffset();
|
|
|
|
fabric.Canvas.activeInstance = this;
|
|
};
|
|
|
|
extend(fabric.Canvas.prototype, fabric.Observable);
|
|
|
|
extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ {
|
|
|
|
/**
|
|
* Background color of this canvas instance
|
|
* @property
|
|
* @type String
|
|
*/
|
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
|
|
/**
|
|
* Indicates whether object 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 selection
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
selectionLineWidth: 1,
|
|
|
|
/**
|
|
* Color of the line used in free drawing mode
|
|
* @property
|
|
* @type String
|
|
*/
|
|
freeDrawingColor: 'rgb(0, 0, 0)',
|
|
|
|
/**
|
|
* Width of a line used in free drawing mode
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
freeDrawingLineWidth: 1,
|
|
|
|
/**
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
includeDefaultValues: true,
|
|
|
|
/**
|
|
* Indicates whether images loaded via `fabric.Canvas#loadImageFromUrl` should be cached
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
shouldCacheImages: false,
|
|
|
|
/**
|
|
* 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,
|
|
|
|
/**
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
CANVAS_WIDTH: 600,
|
|
|
|
/**
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
CANVAS_HEIGHT: 600,
|
|
|
|
/**
|
|
* @constant
|
|
* @type String
|
|
*/
|
|
CONTAINER_CLASS: 'canvas-container',
|
|
|
|
/**
|
|
* @constant
|
|
* @type String
|
|
*/
|
|
HOVER_CURSOR: 'move',
|
|
|
|
/**
|
|
* Callback; invoked right before object is about to be scaled/rotated
|
|
* @method onBeforeScaleRotate
|
|
* @param {fabric.Object} target Object that's about to be scaled/rotated
|
|
*/
|
|
onBeforeScaleRotate: function (target) {
|
|
/* NOOP */
|
|
},
|
|
|
|
/**
|
|
* Callback; invoked on every redraw of canvas and is being passed a number indicating current fps
|
|
* @method onFpsUpdate
|
|
* @param {Number} fps
|
|
*/
|
|
onFpsUpdate: null,
|
|
|
|
/**
|
|
* 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.upperCanvasEl);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets overlay image for this canvas
|
|
* @method setOverlayImage
|
|
* @param {String} url url of an image to set background to
|
|
* @param {Function} callback callback to invoke when image is loaded and set as an overlay one
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
setOverlayImage: function (url, callback) { // TODO (kangax): test callback
|
|
if (url) {
|
|
var _this = this, img = new Image();
|
|
|
|
/** @ignore */
|
|
img.onload = function () {
|
|
_this.overlayImage = img;
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
img = img.onload = null;
|
|
};
|
|
img.src = url;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _initWrapperElement
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
_initWrapperElement: function () {
|
|
this.wrapperEl = fabric.util.wrapElement(this.upperCanvasEl, 'div', {
|
|
'class': this.CONTAINER_CLASS
|
|
});
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _createCanvasElement
|
|
* @param {Element} element
|
|
*/
|
|
_createCanvasElement: function() {
|
|
var element = 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.upperCanvasEl.width, 10) || 0;
|
|
this.height = parseInt(this.upperCanvasEl.height, 10) || 0;
|
|
|
|
this.upperCanvasEl.style.width = this.width + 'px';
|
|
this.upperCanvasEl.style.height = this.height + 'px';
|
|
},
|
|
|
|
/**
|
|
* Adds mouse listeners to canvas
|
|
* @method _initEvents
|
|
* @private
|
|
* See configuration documentation for more details.
|
|
*/
|
|
_initEvents: function () {
|
|
|
|
var _this = this;
|
|
|
|
this._onMouseDown = function (e) {
|
|
_this.__onMouseDown(e);
|
|
addListener(document, 'mouseup', _this._onMouseUp);
|
|
};
|
|
this._onMouseUp = function (e) {
|
|
_this.__onMouseUp(e);
|
|
removeListener(document, 'mouseup', _this._onMouseUp);
|
|
};
|
|
this._onMouseMove = function (e) { _this.__onMouseMove(e); };
|
|
this._onResize = function (e) { _this.calcOffset() };
|
|
|
|
addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
|
|
addListener(document, 'mousemove', this._onMouseMove);
|
|
addListener(window, 'resize', this._onResize);
|
|
},
|
|
|
|
/**
|
|
* @method _createUpperCanvas
|
|
* @param {HTMLElement|String} canvasEl Canvas element
|
|
* @throws {CANVAS_INIT_ERROR} If canvas can not be initialized
|
|
*/
|
|
_createUpperCanvas: function (canvasEl) {
|
|
this.upperCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement();
|
|
this._initCanvasElement(this.upperCanvasEl);
|
|
|
|
fabric.util.addClass(this.upperCanvasEl, 'upper-canvas');
|
|
this._applyCanvasStyle(this.upperCanvasEl);
|
|
|
|
this.contextTop = this.upperCanvasEl.getContext('2d');
|
|
},
|
|
|
|
/**
|
|
* Creates a secondary canvas
|
|
* @method _createLowerCanvas
|
|
*/
|
|
_createLowerCanvas: function () {
|
|
this.lowerCanvasEl = this._createCanvasElement();
|
|
this.lowerCanvasEl.className = 'lower-canvas';
|
|
|
|
this.wrapperEl.insertBefore(this.lowerCanvasEl, this.upperCanvasEl);
|
|
|
|
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';
|
|
|
|
this.upperCanvasEl[prop] = value;
|
|
this.upperCanvasEl.style[prop] = value + 'px';
|
|
|
|
this.wrapperEl.style[prop] = value + 'px';
|
|
|
|
this[prop] = value;
|
|
|
|
this.calcOffset();
|
|
this.renderAll();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is released on canvas.
|
|
* The method resets the currentTransform parameters, store the image corner
|
|
* position in the image object and render the canvas on top.
|
|
* @method __onMouseUp
|
|
* @param {Event} e Event object fired on mouseup
|
|
*
|
|
*/
|
|
__onMouseUp: function (e) {
|
|
|
|
if (this.isDrawingMode && this._isCurrentlyDrawing) {
|
|
this._finalizeDrawingPath();
|
|
return;
|
|
}
|
|
|
|
if (this._currentTransform) {
|
|
|
|
var transform = this._currentTransform,
|
|
target = transform.target;
|
|
|
|
if (target._scaling) {
|
|
target._scaling = false;
|
|
}
|
|
|
|
var i = this._objects.length;
|
|
while (i--) {
|
|
this._objects[i].setCoords();
|
|
}
|
|
|
|
if (this.stateful && target.hasStateChanged()) {
|
|
target.isMoving = false;
|
|
this.fire('object:modified', { target: target });
|
|
}
|
|
}
|
|
|
|
this._currentTransform = null;
|
|
|
|
if (this._groupSelector) {
|
|
this._findSelectedObjects(e);
|
|
}
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
activeGroup.setObjectsCoords();
|
|
activeGroup.set('isMoving', false);
|
|
this._setCursor('default');
|
|
}
|
|
|
|
this._groupSelector = null;
|
|
this.renderAll();
|
|
|
|
this._setCursorFromEvent(e, target);
|
|
|
|
this._setCursor('');
|
|
|
|
var _this = this;
|
|
setTimeout(function () {
|
|
_this._setCursorFromEvent(e, target);
|
|
}, 50);
|
|
|
|
this.fire('mouse:up', { target: target, e: e });
|
|
},
|
|
|
|
_shouldClearSelection: function (e) {
|
|
var target = this.findTarget(e),
|
|
activeGroup = this.getActiveGroup();
|
|
return (
|
|
!target || (
|
|
target &&
|
|
activeGroup &&
|
|
!activeGroup.contains(target) &&
|
|
activeGroup !== target &&
|
|
!e.shiftKey
|
|
)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is clic ked on canvas.
|
|
* The method inits the currentTransform parameters and renders all the
|
|
* canvas so the current image can be placed on the top canvas and the rest
|
|
* in on the container one.
|
|
* @method __onMouseDown
|
|
* @param e {Event} Event object fired on mousedown
|
|
*
|
|
*/
|
|
__onMouseDown: function (e) {
|
|
|
|
if (e.which !== 1) return;
|
|
|
|
if (this.isDrawingMode) {
|
|
this._prepareForDrawing(e);
|
|
|
|
this._captureDrawingPath(e);
|
|
|
|
return;
|
|
}
|
|
|
|
if (this._currentTransform) return;
|
|
|
|
var target = this.findTarget(e),
|
|
pointer = this.getPointer(e),
|
|
activeGroup = this.getActiveGroup(),
|
|
corner;
|
|
|
|
if (this._shouldClearSelection(e)) {
|
|
|
|
this._groupSelector = {
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
top: 0,
|
|
left: 0
|
|
};
|
|
|
|
this.deactivateAllWithDispatch();
|
|
}
|
|
else {
|
|
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());
|
|
if (shouldHandleGroupLogic) {
|
|
this._handleGroupLogic(e, target);
|
|
}
|
|
else {
|
|
if (target !== this.getActiveGroup()) {
|
|
this.deactivateAll();
|
|
}
|
|
this.setActiveObject(target);
|
|
}
|
|
}
|
|
this.renderAll();
|
|
|
|
this.fire('mouse:down', { target: target, e: e });
|
|
},
|
|
|
|
/**
|
|
* Returns <canvas> element corresponding to this instance
|
|
* @method getElement
|
|
* @return {HTMLCanvasElement}
|
|
*/
|
|
getElement: function () {
|
|
return this.upperCanvasEl;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _setupCurrentTransform
|
|
*/
|
|
_setupCurrentTransform: function (e, target) {
|
|
var action = 'drag',
|
|
corner,
|
|
pointer = getPointer(e);
|
|
|
|
if (corner = target._findTargetCorner(e, this._offset)) {
|
|
action = (corner === 'ml' || corner === 'mr')
|
|
? 'scaleX'
|
|
: (corner === 'mt' || corner === 'mb')
|
|
? 'scaleY'
|
|
: 'rotate';
|
|
}
|
|
|
|
this._currentTransform = {
|
|
target: target,
|
|
action: action,
|
|
scaleX: target.scaleX,
|
|
scaleY: target.scaleY,
|
|
offsetX: pointer.x - target.left,
|
|
offsetY: pointer.y - target.top,
|
|
ex: pointer.x,
|
|
ey: pointer.y,
|
|
left: target.left,
|
|
top: target.top,
|
|
theta: target.theta,
|
|
width: target.width * target.scaleX
|
|
};
|
|
|
|
this._currentTransform.original = {
|
|
left: target.left,
|
|
top: target.top
|
|
};
|
|
},
|
|
|
|
_handleGroupLogic: function (e, target) {
|
|
if (target.isType('group')) {
|
|
target = this.findTarget(e, true);
|
|
if (!target || target.isType('group')) {
|
|
return;
|
|
}
|
|
}
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
if (activeGroup.contains(target)) {
|
|
activeGroup.remove(target);
|
|
target.setActive(false);
|
|
if (activeGroup.size() === 1) {
|
|
this.discardActiveGroup();
|
|
}
|
|
}
|
|
else {
|
|
activeGroup.add(target);
|
|
}
|
|
this.fire('selection:created', { target: activeGroup });
|
|
activeGroup.setActive(true);
|
|
}
|
|
else {
|
|
if (this._activeObject) {
|
|
if (target !== this._activeObject) {
|
|
var group = new fabric.Group([ this._activeObject,target ]);
|
|
this.setActiveGroup(group);
|
|
activeGroup = this.getActiveGroup();
|
|
}
|
|
}
|
|
target.setActive(true);
|
|
}
|
|
|
|
if (activeGroup) {
|
|
activeGroup.saveCoords();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @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),
|
|
ctx = this.contextTop,
|
|
path = [ ],
|
|
xPoint,
|
|
yPoint,
|
|
xPoints = this._freeDrawingXPoints,
|
|
yPoints = this._freeDrawingYPoints;
|
|
|
|
path.push('M ', xPoints[0] - minX, ' ', yPoints[0] - minY, ' ');
|
|
|
|
for (var i = 1; xPoint = xPoints[i], yPoint = yPoints[i]; i++) {
|
|
path.push('L ', xPoint - minX, ' ', yPoint - minY, ' ');
|
|
}
|
|
|
|
|
|
path = path.join('');
|
|
|
|
if (path === "M 0 0 L 0 0 ") {
|
|
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 });
|
|
},
|
|
|
|
/**
|
|
* Method that defines the actions when mouse is hovering the canvas.
|
|
* The currentTransform parameter will definde whether the user is rotating/scaling/translating
|
|
* an image or neither of them (only hovering). A group selection is also possible and would cancel
|
|
* all any other type of action.
|
|
* In case of an image transformation only the top canvas will be rendered.
|
|
* @method __onMouseMove
|
|
* @param e {Event} Event object fired on mousemove
|
|
*
|
|
*/
|
|
__onMouseMove: function (e) {
|
|
|
|
if (this.isDrawingMode) {
|
|
if (this._isCurrentlyDrawing) {
|
|
this._captureDrawingPath(e);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var groupSelector = this._groupSelector;
|
|
|
|
if (groupSelector !== null) {
|
|
var pointer = getPointer(e);
|
|
groupSelector.left = pointer.x - this._offset.left - groupSelector.ex;
|
|
groupSelector.top = pointer.y - this._offset.top - groupSelector.ey;
|
|
this.renderTop();
|
|
}
|
|
else if (!this._currentTransform) {
|
|
|
|
var style = this.upperCanvasEl.style;
|
|
|
|
var target = this.findTarget(e);
|
|
|
|
if (!target) {
|
|
for (var i = this._objects.length; i--; ) {
|
|
if (!this._objects[i].active) {
|
|
this._objects[i].setActive(false);
|
|
}
|
|
}
|
|
style.cursor = 'default';
|
|
}
|
|
else {
|
|
this._setCursorFromEvent(e, target);
|
|
if (target.isActive()) {
|
|
target.setCornersVisibility && target.setCornersVisibility(true);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
var pointer = getPointer(e),
|
|
x = pointer.x,
|
|
y = pointer.y;
|
|
|
|
this._currentTransform.target.isMoving = true;
|
|
|
|
if (this._currentTransform.action === 'rotate') {
|
|
|
|
if (!e.shiftKey) {
|
|
this._rotateObject(x, y);
|
|
}
|
|
this._scaleObject(x, y);
|
|
}
|
|
else if (this._currentTransform.action === 'scaleX') {
|
|
this._scaleObject(x, y, 'x');
|
|
}
|
|
else if (this._currentTransform.action === 'scaleY') {
|
|
this._scaleObject(x, y, 'y');
|
|
}
|
|
else {
|
|
this._translateObject(x, y);
|
|
|
|
this.fire('object:moving', {
|
|
target: this._currentTransform.target
|
|
});
|
|
}
|
|
this.renderAll();
|
|
}
|
|
this.fire('mouse:move', { target: target, e: e });
|
|
},
|
|
|
|
/**
|
|
* 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.set('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 = 'default';
|
|
return false;
|
|
}
|
|
else {
|
|
var activeGroup = this.getActiveGroup();
|
|
var corner = !!target._findTargetCorner
|
|
&& (!activeGroup || !activeGroup.contains(target))
|
|
&& target._findTargetCorner(e, this._offset);
|
|
|
|
if (!corner) {
|
|
s.cursor = this.HOVER_CURSOR;
|
|
}
|
|
else {
|
|
if (corner in cursorMap) {
|
|
s.cursor = cursorMap[corner];
|
|
}
|
|
else {
|
|
s.cursor = 'default';
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Given a context, renders an object on that context
|
|
* @param ctx {Object} context to render object on
|
|
* @param object {Object} object to render
|
|
* @private
|
|
*/
|
|
_draw: function (ctx, object) {
|
|
object && object.render(ctx);
|
|
},
|
|
|
|
/**
|
|
* @method _drawSelection
|
|
* @private
|
|
*/
|
|
_drawSelection: function () {
|
|
var groupSelector = this._groupSelector,
|
|
left = groupSelector.left,
|
|
top = groupSelector.top,
|
|
aleft = abs(left),
|
|
atop = abs(top);
|
|
|
|
this.contextTop.fillStyle = this.selectionColor;
|
|
|
|
this.contextTop.fillRect(
|
|
groupSelector.ex - ((left > 0) ? 0 : -left),
|
|
groupSelector.ey - ((top > 0) ? 0 : -top),
|
|
aleft,
|
|
atop
|
|
);
|
|
|
|
this.contextTop.lineWidth = this.selectionLineWidth;
|
|
this.contextTop.strokeStyle = this.selectionBorderColor;
|
|
|
|
this.contextTop.strokeRect(
|
|
groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft),
|
|
groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop),
|
|
aleft,
|
|
atop
|
|
);
|
|
},
|
|
|
|
_findSelectedObjects: function (e) {
|
|
var target,
|
|
targetRegion,
|
|
group = [ ],
|
|
x1 = this._groupSelector.ex,
|
|
y1 = this._groupSelector.ey,
|
|
x2 = x1 + this._groupSelector.left,
|
|
y2 = y1 + this._groupSelector.top,
|
|
currentObject,
|
|
selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)),
|
|
selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2));
|
|
|
|
for (var i = 0, len = this._objects.length; i < len; ++i) {
|
|
currentObject = this._objects[i];
|
|
|
|
if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) ||
|
|
currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) {
|
|
|
|
if (this.selection && currentObject.selectable) {
|
|
currentObject.setActive(true);
|
|
group.push(currentObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (group.length === 1) {
|
|
this.setActiveObject(group[0]);
|
|
this.fire('object:selected', {
|
|
target: group[0]
|
|
});
|
|
}
|
|
else if (group.length > 1) {
|
|
var group = new fabric.Group(group);
|
|
this.setActiveGroup(group);
|
|
group.saveCoords();
|
|
this.fire('selection:created', { target: group });
|
|
}
|
|
|
|
this.renderAll();
|
|
},
|
|
|
|
/**
|
|
* 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.stateful && arguments[i].setupState();
|
|
arguments[i].setCoords();
|
|
}
|
|
this.renderOnAddition && this.renderAll();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Inserts an object to canvas at specified index and renders canvas.
|
|
* An object should be an instance of (or inherit from) fabric.Object
|
|
* @method insertAt
|
|
* @param object {Object} Object to insert
|
|
* @param index {Number} index to insert object at
|
|
* @return {fabric.Canvas} instance
|
|
*/
|
|
insertAt: function (object, index) {
|
|
this._objects.splice(index, 0, object);
|
|
this.stateful && object.setupState();
|
|
object.setCoords();
|
|
this.renderAll();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns an array of objects this instance has
|
|
* @method getObjects
|
|
* @return {Array}
|
|
*/
|
|
getObjects: function () {
|
|
return this._objects;
|
|
},
|
|
|
|
/**
|
|
* Returns topmost canvas context
|
|
* @method getContext
|
|
* @return {CanvasRenderingContext2D}
|
|
*/
|
|
getContext: function () {
|
|
return this.contextTop;
|
|
},
|
|
|
|
/**
|
|
* Clears specified context of canvas element
|
|
* @method clearContext
|
|
* @param context {Object} ctx context to clear
|
|
* @return {fabric.Canvas} thisArg
|
|
* @chainable
|
|
*/
|
|
clearContext: function(ctx) {
|
|
ctx.clearRect(0, 0, this.width, this.height);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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.contextTop);
|
|
this.clearContext(this.contextContainer);
|
|
this.renderAll();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Renders both the top canvas and the secondary container canvas.
|
|
* @method renderAll
|
|
* @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas
|
|
* @return {fabric.Canvas} instance
|
|
* @chainable
|
|
*/
|
|
renderAll: function (allOnTop) {
|
|
|
|
var containerCanvas = this[allOnTop ? 'contextTop' : 'contextContainer'];
|
|
|
|
this.clearContext(this.contextTop);
|
|
|
|
if (!allOnTop) {
|
|
this.clearContext(containerCanvas);
|
|
}
|
|
|
|
var length = this._objects.length,
|
|
activeGroup = this.getActiveGroup(),
|
|
startTime = new Date();
|
|
|
|
if (this.clipTo) {
|
|
containerCanvas.save();
|
|
containerCanvas.beginPath();
|
|
this.clipTo(containerCanvas);
|
|
containerCanvas.clip();
|
|
}
|
|
|
|
containerCanvas.fillStyle = this.backgroundColor;
|
|
containerCanvas.fillRect(0, 0, this.width, this.height);
|
|
|
|
if (length) {
|
|
for (var i = 0; i < length; ++i) {
|
|
if (!activeGroup ||
|
|
(activeGroup &&
|
|
!activeGroup.contains(this._objects[i]))) {
|
|
this._draw(containerCanvas, this._objects[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.clipTo) {
|
|
containerCanvas.restore();
|
|
}
|
|
|
|
if (activeGroup) {
|
|
this._draw(this.contextTop, activeGroup);
|
|
}
|
|
|
|
if (this.overlayImage) {
|
|
this.contextTop.drawImage(this.overlayImage, 0, 0);
|
|
}
|
|
|
|
if (this.onFpsUpdate) {
|
|
var elapsedTime = new Date() - startTime;
|
|
this.onFpsUpdate(~~(1000 / elapsedTime));
|
|
}
|
|
|
|
this.fire('after:render');
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
if (this.overlayImage) {
|
|
this.contextTop.drawImage(this.overlayImage, 0, 0);
|
|
}
|
|
|
|
if (this.selection && this._groupSelector) {
|
|
this._drawSelection();
|
|
}
|
|
|
|
var activeGroup = this.getActiveGroup();
|
|
if (activeGroup) {
|
|
activeGroup.render(this.contextTop);
|
|
}
|
|
|
|
this.fire('after:render');
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Applies one implementation of 'point inside polygon' algorithm
|
|
* @method containsPoint
|
|
* @param e { Event } event object
|
|
* @param target { fabric.Object } object to test against
|
|
* @return {Boolean} true if point contains within area of given object
|
|
*/
|
|
containsPoint: function (e, target) {
|
|
var pointer = this.getPointer(e),
|
|
xy = this._normalizePointer(target, pointer),
|
|
x = xy.x,
|
|
y = xy.y;
|
|
|
|
|
|
var iLines = target._getImageLines(target.oCoords),
|
|
xpoints = target._findCrossPoints(x, y, iLines);
|
|
|
|
if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _normalizePointer
|
|
*/
|
|
_normalizePointer: function (object, pointer) {
|
|
|
|
var activeGroup = this.getActiveGroup(),
|
|
x = pointer.x,
|
|
y = pointer.y;
|
|
|
|
var isObjectInGroup = (
|
|
activeGroup &&
|
|
object.type !== 'group' &&
|
|
activeGroup.contains(object)
|
|
);
|
|
|
|
if (isObjectInGroup) {
|
|
x -= activeGroup.left;
|
|
y -= activeGroup.top;
|
|
}
|
|
return { x: x, y: y };
|
|
},
|
|
|
|
/**
|
|
* Method that determines what object we are clicking on
|
|
* @method findTarget
|
|
* @param {Event} e mouse event
|
|
* @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through
|
|
*/
|
|
findTarget: function (e, skipGroup) {
|
|
|
|
var target,
|
|
pointer = this.getPointer(e);
|
|
|
|
var activeGroup = this.getActiveGroup();
|
|
|
|
if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) {
|
|
target = activeGroup;
|
|
return target;
|
|
}
|
|
|
|
for (var i = this._objects.length; i--; ) {
|
|
if (this.containsPoint(e, this._objects[i])) {
|
|
target = this._objects[i];
|
|
this.relatedTarget = target;
|
|
break;
|
|
}
|
|
}
|
|
if (this.selection && target && target.selectable) {
|
|
return target;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Exports canvas element to a dataurl image.
|
|
* @method toDataURL
|
|
* @param {String} format the format of the output image. Either "jpeg" or "png".
|
|
* @return {String}
|
|
*/
|
|
toDataURL: function (format) {
|
|
this.renderAll(true);
|
|
var data = this.upperCanvasEl.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
|
|
* @return {String}
|
|
*/
|
|
toDataURLWithMultiplier: function (format, multiplier) {
|
|
|
|
var origWidth = this.getWidth(),
|
|
origHeight = this.getHeight(),
|
|
scaledWidth = origWidth * multiplier,
|
|
scaledHeight = origHeight * multiplier,
|
|
activeObject = this.getActiveObject();
|
|
|
|
this.setWidth(scaledWidth).setHeight(scaledHeight);
|
|
this.contextTop.scale(multiplier, multiplier);
|
|
|
|
if (activeObject) {
|
|
this.deactivateAll().renderAll();
|
|
}
|
|
var dataURL = this.toDataURL(format);
|
|
|
|
this.contextTop.scale( 1 / multiplier, 1 / multiplier);
|
|
this.setWidth(origWidth).setHeight(origHeight);
|
|
|
|
if (activeObject) {
|
|
this.setActiveObject(activeObject);
|
|
}
|
|
this.renderAll();
|
|
|
|
return dataURL;
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* Returs dataless JSON representation of canvas
|
|
* @method toDatalessJSON
|
|
* @return {String} json string
|
|
*/
|
|
toDatalessJSON: function () {
|
|
return this.toDatalessObject();
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of canvas
|
|
* @method toObject
|
|
* @return {Object}
|
|
*/
|
|
toObject: function () {
|
|
return this._toObjectMethod('toObject');
|
|
},
|
|
|
|
/**
|
|
* Returns dataless object representation of canvas
|
|
* @method toDatalessObject
|
|
* @return {Object}
|
|
*/
|
|
toDatalessObject: function () {
|
|
return this._toObjectMethod('toDatalessObject');
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _toObjectMethod
|
|
*/
|
|
_toObjectMethod: function (methodName) {
|
|
return {
|
|
objects: this._objects.map(function (instance){
|
|
if (!this.includeDefaultValues) {
|
|
var originalValue = instance.includeDefaultValues;
|
|
instance.includeDefaultValues = false;
|
|
}
|
|
var object = instance[methodName]();
|
|
if (!this.includeDefaultValues) {
|
|
instance.includeDefaultValues = originalValue;
|
|
}
|
|
return object;
|
|
}, this),
|
|
background: this.backgroundColor
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns true if canvas contains no objects
|
|
* @method isEmpty
|
|
* @return {Boolean} true if canvas is empty
|
|
*/
|
|
isEmpty: function () {
|
|
return this._objects.length === 0;
|
|
},
|
|
|
|
/**
|
|
* Loads an image from URL, creates an instance of fabric.Image and passes it to a callback
|
|
* @function
|
|
* @method loadImageFromURL
|
|
* @param url {String} url of image to load
|
|
* @param callback {Function} calback, invoked when image is loaded
|
|
*/
|
|
loadImageFromURL: (function () {
|
|
var imgCache = { };
|
|
|
|
return function (url, callback) {
|
|
|
|
var _this = this;
|
|
|
|
function checkIfLoaded() {
|
|
var imgEl = document.getElementById(imgCache[url]);
|
|
if (imgEl.width && imgEl.height) {
|
|
callback(new fabric.Image(imgEl));
|
|
}
|
|
else {
|
|
setTimeout(checkIfLoaded, 50);
|
|
}
|
|
}
|
|
|
|
if (imgCache[url]) {
|
|
checkIfLoaded();
|
|
}
|
|
else {
|
|
var imgEl = new Image();
|
|
|
|
/** @ignore */
|
|
imgEl.onload = function () {
|
|
imgEl.onload = null;
|
|
|
|
|
|
setTimeout(function() {
|
|
if (imgEl.width && imgEl.height) {
|
|
callback(new fabric.Image(imgEl));
|
|
}
|
|
}, 0);
|
|
};
|
|
|
|
imgEl.className = 'canvas-img-clone';
|
|
imgEl.style.cssText = 'position:absolute;left:-9999px;top:-9999px;';
|
|
imgEl.src = url;
|
|
|
|
if (this.shouldCacheImages) {
|
|
imgCache[url] = Element.identify(imgEl);
|
|
}
|
|
|
|
document.body.appendChild(imgEl);
|
|
}
|
|
}
|
|
})(),
|
|
|
|
/**
|
|
* 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) {
|
|
this.discardActiveObject();
|
|
}
|
|
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 (idx !== 0) {
|
|
|
|
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 sendForward
|
|
* @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 (idx !== objects.length-1) {
|
|
|
|
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();
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
if (this._activeObject) {
|
|
this._activeObject.setActive(false);
|
|
}
|
|
this._activeObject = object;
|
|
object.setActive(true);
|
|
|
|
this.renderAll();
|
|
|
|
this.fire('object:selected', { target: object });
|
|
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;
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* Returns object at specified index
|
|
* @method item
|
|
* @param {Number} index
|
|
* @return {fabric.Object}
|
|
*/
|
|
item: function (index) {
|
|
return this.getObjects()[index];
|
|
},
|
|
|
|
/**
|
|
* Deactivates all objects by calling their setActive(false)
|
|
* @method deactivateAll
|
|
* @return {fabric.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;
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
|
|
removeListener(document, 'mousemove', this._onMouseMove);
|
|
removeListener(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;
|
|
|
|
if (imageWidth) {
|
|
imgEl.width = imageWidth * widthScaleFactor;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returns a string representation of an instance
|
|
* @method toString
|
|
* @return {String} string representation of an instance
|
|
*/
|
|
fabric.Canvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet
|
|
return '#<fabric.Canvas (' + this.complexity() + '): '+
|
|
'{ objects: ' + this.getObjects().length + ' }>';
|
|
};
|
|
|
|
extend(fabric.Canvas, /** @scope fabric.Canvas */ {
|
|
|
|
/**
|
|
* @static
|
|
* @property EMPTY_JSON
|
|
* @type String
|
|
*/
|
|
EMPTY_JSON: '{"objects": [], "background": "white"}',
|
|
|
|
/**
|
|
* Takes <canvas> element and transforms its data in such way that it becomes grayscale
|
|
* @static
|
|
* @method toGrayscale
|
|
* @param {HTMLCanvasElement} canvasEl
|
|
*/
|
|
toGrayscale: function (canvasEl) {
|
|
var context = canvasEl.getContext('2d'),
|
|
imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height),
|
|
data = imageData.data,
|
|
iLen = imageData.width,
|
|
jLen = imageData.height,
|
|
index, average, i, j;
|
|
|
|
for (i = 0; i < iLen; i++) {
|
|
for (j = 0; j < jLen; j++) {
|
|
|
|
index = (i * 4) * jLen + (j * 4);
|
|
average = (data[index] + data[index + 1] + data[index + 2]) / 3;
|
|
|
|
data[index] = average;
|
|
data[index + 1] = average;
|
|
data[index + 2] = average;
|
|
}
|
|
}
|
|
|
|
context.putImageData(imageData, 0, 0);
|
|
},
|
|
|
|
/**
|
|
* Provides a way to check support of some of the canvas methods
|
|
* (either those of HTMLCanvasElement itself, or rendering context)
|
|
*
|
|
* @method supports
|
|
* @param methodName {String} Method to check support for;
|
|
* Could be one of "getImageData" or "toDataURL"
|
|
* @return {Boolean | null} `true` if method is supported (or at least exists),
|
|
* `null` if canvas element or context can not be initialized
|
|
*/
|
|
supports: function (methodName) {
|
|
var el = document.createElement('canvas');
|
|
|
|
if (typeof G_vmlCanvasManager !== 'undefined') {
|
|
G_vmlCanvasManager.initElement(el);
|
|
}
|
|
if (!el || !el.getContext) {
|
|
return null;
|
|
}
|
|
|
|
var ctx = el.getContext('2d');
|
|
if (!ctx) {
|
|
return null;
|
|
}
|
|
|
|
switch (methodName) {
|
|
|
|
case 'getImageData':
|
|
return typeof ctx.getImageData !== 'undefined';
|
|
|
|
case 'toDataURL':
|
|
return typeof el.toDataURL !== 'undefined';
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Returs JSON representation of canvas
|
|
* @function
|
|
* @method toJSON
|
|
* @return {String} json string
|
|
*/
|
|
fabric.Canvas.prototype.toJSON = fabric.Canvas.prototype.toObject;
|
|
|
|
/**
|
|
* @class fabric.Element
|
|
* @alias fabric.Canvas
|
|
* @deprecated
|
|
* @constructor
|
|
*/
|
|
fabric.Element = fabric.Canvas;
|
|
|
|
})(typeof exports != 'undefined' ? exports : this);
|
|
fabric.util.object.extend(fabric.Canvas.prototype, {
|
|
|
|
/**
|
|
* 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#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;
|
|
},
|
|
|
|
/**
|
|
* 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, callback) {
|
|
var _this = this;
|
|
object.fxRemove({
|
|
onChange: this.renderAll.bind(this),
|
|
onComplete: function () {
|
|
_this.remove(object);
|
|
if (typeof callback === 'function') {
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
});
|
|
fabric.util.object.extend(fabric.Canvas.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;
|
|
}
|
|
|
|
var serialized = (typeof json === 'string')
|
|
? JSON.parse(json)
|
|
: json;
|
|
|
|
if (!serialized || (serialized && !serialized.objects)) return;
|
|
|
|
this.clear();
|
|
|
|
this.backgroundColor = serialized.background;
|
|
this._enlivenDatalessObjects(serialized.objects, callback);
|
|
},
|
|
|
|
/**
|
|
* @method _enlivenDatalessObjects
|
|
* @param {Array} objects
|
|
* @param {Function} callback
|
|
*/
|
|
_enlivenDatalessObjects: function (objects, callback) {
|
|
|
|
/** @ignore */
|
|
function onObjectLoaded(object, index) {
|
|
_this.insertAt(object, index);
|
|
object.setCoords();
|
|
if (++numLoadedObjects === numTotalObjects) {
|
|
callback && callback();
|
|
}
|
|
}
|
|
|
|
var _this = this,
|
|
numLoadedObjects = 0,
|
|
numTotalObjects = objects.length;
|
|
|
|
if (numTotalObjects === 0 && callback) {
|
|
callback();
|
|
}
|
|
|
|
try {
|
|
objects.forEach(function (obj, index) {
|
|
|
|
var pathProp = obj.paths ? 'paths' : 'path';
|
|
var path = obj[pathProp];
|
|
|
|
delete obj[pathProp];
|
|
|
|
if (typeof path !== 'string') {
|
|
switch (obj.type) {
|
|
case 'image':
|
|
case 'text':
|
|
fabric[fabric.util.string.capitalize(obj.type)].fromObject(obj, function (o) {
|
|
onObjectLoaded(o, index);
|
|
});
|
|
break;
|
|
default:
|
|
var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(obj.type))];
|
|
if (klass && klass.fromObject) {
|
|
if (path) {
|
|
obj[pathProp] = path;
|
|
}
|
|
onObjectLoaded(klass.fromObject(obj), index);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
if (obj.type === 'image') {
|
|
_this.loadImageFromURL(path, function (image) {
|
|
image.setSourcePath(path);
|
|
|
|
fabric.util.object.extend(image, obj);
|
|
image.setAngle(obj.angle);
|
|
|
|
onObjectLoaded(image, index);
|
|
});
|
|
}
|
|
else if (obj.type === 'text') {
|
|
|
|
obj.path = path;
|
|
var object = fabric.Text.fromObject(obj);
|
|
var onscriptload = function () {
|
|
if (Object.prototype.toString.call(window.opera) === '[object Opera]') {
|
|
setTimeout(function () {
|
|
onObjectLoaded(object, index);
|
|
}, 500);
|
|
}
|
|
else {
|
|
onObjectLoaded(object, index);
|
|
}
|
|
}
|
|
|
|
fabric.util.getScript(path, onscriptload);
|
|
}
|
|
else {
|
|
fabric.loadSVGFromURL(path, function (elements, options) {
|
|
if (elements.length > 1) {
|
|
var object = new fabric.PathGroup(elements, obj);
|
|
}
|
|
else {
|
|
var object = elements[0];
|
|
}
|
|
object.setSourcePath(path);
|
|
|
|
if (!(object instanceof fabric.PathGroup)) {
|
|
fabric.util.object.extend(object, obj);
|
|
if (typeof obj.angle !== 'undefined') {
|
|
object.setAngle(obj.angle);
|
|
}
|
|
}
|
|
|
|
onObjectLoaded(object, index);
|
|
});
|
|
}
|
|
}
|
|
}, 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 (callback) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method _enlivenObjects
|
|
* @param {Array} objects
|
|
* @param {Function} callback
|
|
*/
|
|
_enlivenObjects: function (objects, callback) {
|
|
var numLoadedImages = 0,
|
|
numTotalImages = objects.filter(function (o) {
|
|
return o.type === 'image';
|
|
}).length;
|
|
|
|
var _this = this;
|
|
|
|
objects.forEach(function (o, index) {
|
|
if (!o.type) {
|
|
return;
|
|
}
|
|
switch (o.type) {
|
|
case 'image':
|
|
case 'font':
|
|
fabric[fabric.util.string.capitalize(o.type)].fromObject(o, function (o) {
|
|
_this.insertAt(o, index);
|
|
if (++numLoadedImages === numTotalImages) {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
default:
|
|
var klass = fabric[fabric.util.string.camelize(fabric.util.string.capitalize(o.type))];
|
|
if (klass && klass.fromObject) {
|
|
_this.insertAt(klass.fromObject(o), index);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
if (numTotalImages === 0 && 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] Expects `onBeforeClone` and `onAfterClone` functions
|
|
* @return {fabric.Canvas} Clone of this instance
|
|
*/
|
|
clone: function (callback) {
|
|
var el = document.createElement('canvas');
|
|
|
|
el.width = this.getWidth();
|
|
el.height = this.getHeight();
|
|
|
|
var clone = this.__clone || (this.__clone = new fabric.Canvas(el));
|
|
clone.clipTo = this.clipTo;
|
|
|
|
return clone.loadFromJSON(JSON.stringify(this.toJSON()), function () {
|
|
if (callback) {
|
|
callback(clone);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
(function(global) {
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
extend = fabric.util.object.extend,
|
|
clone = fabric.util.object.clone,
|
|
toFixed = fabric.util.toFixed,
|
|
capitalize = fabric.util.string.capitalize,
|
|
getPointer = fabric.util.getPointer,
|
|
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 */ {
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
type: 'object',
|
|
|
|
/**
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
includeDefaultValues: true,
|
|
|
|
/**
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
NUM_FRACTION_DIGITS: 2,
|
|
|
|
/**
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
FX_DURATION: 500,
|
|
|
|
/**
|
|
* @constant
|
|
* @type Number
|
|
*/
|
|
MIN_SCALE_LIMIT: 0.1,
|
|
|
|
/**
|
|
* 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 fillRule borderScaleFactor transformMatrix ' +
|
|
'selectable').split(' '),
|
|
|
|
top: 0,
|
|
left: 0,
|
|
width: 0,
|
|
height: 0,
|
|
scaleX: 1,
|
|
scaleY: 1,
|
|
flipX: false,
|
|
flipY: false,
|
|
theta: 0,
|
|
opacity: 1,
|
|
angle: 0,
|
|
cornersize: 12,
|
|
padding: 0,
|
|
borderColor: 'rgba(102,153,255,0.75)',
|
|
cornerColor: 'rgba(102,153,255,0.5)',
|
|
fill: 'rgb(0,0,0)',
|
|
fillRule: 'source-over',
|
|
overlayFill: null,
|
|
stroke: null,
|
|
strokeWidth: 1,
|
|
borderOpacityWhenMoving: 0.4,
|
|
borderScaleFactor: 1,
|
|
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,
|
|
|
|
/**
|
|
* @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) {
|
|
options && this.setOptions(options);
|
|
},
|
|
|
|
/**
|
|
* @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]);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @method transform
|
|
* @param {CanvasRenderingContext2D} ctx Context
|
|
*/
|
|
transform: function(ctx) {
|
|
ctx.globalAlpha = this.opacity;
|
|
ctx.translate(this.left, this.top);
|
|
ctx.rotate(this.theta);
|
|
ctx.scale(
|
|
this.scaleX * (this.flipX ? -1 : 1),
|
|
this.scaleY * (this.flipY ? -1 : 1)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Returns an object representation of an instance
|
|
* @method toObject
|
|
* @return {Object}
|
|
*/
|
|
toObject: function() {
|
|
|
|
var object = {
|
|
type: this.type,
|
|
left: toFixed(this.left, this.NUM_FRACTION_DIGITS),
|
|
top: toFixed(this.top, this.NUM_FRACTION_DIGITS),
|
|
width: toFixed(this.width, this.NUM_FRACTION_DIGITS),
|
|
height: toFixed(this.height, this.NUM_FRACTION_DIGITS),
|
|
fill: this.fill,
|
|
overlayFill: this.overlayFill,
|
|
stroke: this.stroke,
|
|
strokeWidth: this.strokeWidth,
|
|
scaleX: toFixed(this.scaleX, this.NUM_FRACTION_DIGITS),
|
|
scaleY: toFixed(this.scaleY, this.NUM_FRACTION_DIGITS),
|
|
angle: toFixed(this.getAngle(), this.NUM_FRACTION_DIGITS),
|
|
flipX: this.flipX,
|
|
flipY: this.flipY,
|
|
opacity: toFixed(this.opacity, this.NUM_FRACTION_DIGITS),
|
|
selectable: this.selectable
|
|
};
|
|
|
|
if (!this.includeDefaultValues) {
|
|
object = this._removeDefaultValues(object);
|
|
}
|
|
|
|
return object;
|
|
},
|
|
|
|
/**
|
|
* Returns (dataless) object representation of an instance
|
|
* @method toDatalessObject
|
|
*/
|
|
toDatalessObject: function() {
|
|
return this.toObject();
|
|
},
|
|
|
|
/**
|
|
* @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 "#<fabric." + capitalize(this.type) + ">";
|
|
},
|
|
|
|
/**
|
|
* Basic setter
|
|
* @param {Any} property
|
|
* @param {Any} value
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
set: function(property, value) {
|
|
var shouldConstrainValue = (property === 'scaleX' || property === 'scaleY') && value < this.MIN_SCALE_LIMIT;
|
|
if (shouldConstrainValue) {
|
|
value = this.MIN_SCALE_LIMIT;
|
|
}
|
|
if (typeof property == 'object') {
|
|
for (var prop in property) {
|
|
this.set(prop, property[prop]);
|
|
}
|
|
}
|
|
else {
|
|
if (property === 'angle') {
|
|
this.setAngle(value);
|
|
}
|
|
else {
|
|
this[property] = value;
|
|
}
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Toggles specified property from `true` to `false` or from `false` to `true`
|
|
* @method toggle
|
|
* @param {String} property property to toggle
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
toggle: function(property) {
|
|
var value = this.get(property);
|
|
if (typeof value === 'boolean') {
|
|
this.set(property, !value);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method setSourcePath
|
|
* @param {String} value
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
setSourcePath: function(value) {
|
|
this.sourcePath = value;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Basic getter
|
|
* @method get
|
|
* @param {Any} property
|
|
* @return {Any} value of a property
|
|
*/
|
|
get: function(property) {
|
|
return (property === 'angle')
|
|
? this.getAngle()
|
|
: this[property];
|
|
},
|
|
|
|
/**
|
|
* @method render
|
|
* @param {CanvasRenderingContext2D} ctx context to render on
|
|
* @param {Boolean} noTransform
|
|
*/
|
|
render: function(ctx, noTransform) {
|
|
|
|
if (this.width === 0 || this.height === 0) return;
|
|
|
|
ctx.save();
|
|
|
|
var m = this.transformMatrix;
|
|
if (m) {
|
|
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
if (!noTransform) {
|
|
this.transform(ctx);
|
|
}
|
|
|
|
if (this.stroke) {
|
|
ctx.lineWidth = this.strokeWidth;
|
|
ctx.strokeStyle = this.stroke;
|
|
}
|
|
|
|
if (this.overlayFill) {
|
|
ctx.fillStyle = this.overlayFill;
|
|
}
|
|
else if (this.fill) {
|
|
ctx.fillStyle = this.fill;
|
|
}
|
|
|
|
if (this.group) {
|
|
ctx.translate(
|
|
-this.group.width / 2 + this.width / 2,
|
|
-this.group.height / 2 + this.height / 2
|
|
);
|
|
}
|
|
this._render(ctx, noTransform);
|
|
|
|
if (this.active && !noTransform) {
|
|
this.drawBorders(ctx);
|
|
this.hideCorners || this.drawCorners(ctx);
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Returns width of an object
|
|
* @method getWidth
|
|
* @return {Number} width value
|
|
*/
|
|
getWidth: function() {
|
|
return this.width * this.scaleX;
|
|
},
|
|
|
|
/**
|
|
* Returns height of an object
|
|
* @method getHeight
|
|
* @return {Number} height value
|
|
*/
|
|
getHeight: function() {
|
|
return this.height * this.scaleY;
|
|
},
|
|
|
|
/**
|
|
* Scales an object (equally by x and y)
|
|
* @method scale
|
|
* @param value {Number} scale factor
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
scale: function(value) {
|
|
this.scaleX = value;
|
|
this.scaleY = value;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Scales an object to a given width (scaling by x/y equally)
|
|
* @method scaleToWidth
|
|
* @param value {Number} new width value
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
scaleToWidth: function(value) {
|
|
return this.scale(value / this.width);
|
|
},
|
|
|
|
/**
|
|
* Scales an object to a given height (scaling by x/y equally)
|
|
* @method scaleToHeight
|
|
* @param value {Number} new height value
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
scaleToHeight: function(value) {
|
|
return this.scale(value / this.height);
|
|
},
|
|
|
|
/**
|
|
* Sets object opacity
|
|
* @method setOpacity
|
|
* @param value {Number} value 0-1
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
setOpacity: function(value) {
|
|
this.set('opacity', value);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns object's angle value
|
|
* @method getAngle
|
|
* @return {Number} angle value
|
|
*/
|
|
getAngle: function() {
|
|
return this.theta * 180 / Math.PI;
|
|
},
|
|
|
|
/**
|
|
* Sets object's angle
|
|
* @method setAngle
|
|
* @param value {Number} angle value
|
|
* @return {Object} thisArg
|
|
*/
|
|
setAngle: function(value) {
|
|
this.theta = value / 180 * Math.PI;
|
|
this.angle = value;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Sets corner position coordinates based on current angle, width and height.
|
|
* @method setCoords
|
|
* return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
setCoords: function() {
|
|
|
|
this.currentWidth = this.width * this.scaleX;
|
|
this.currentHeight = this.height * this.scaleY;
|
|
|
|
this._hypotenuse = Math.sqrt(
|
|
Math.pow(this.currentWidth / 2, 2) +
|
|
Math.pow(this.currentHeight / 2, 2));
|
|
|
|
this._angle = Math.atan(this.currentHeight / this.currentWidth);
|
|
|
|
var offsetX = Math.cos(this._angle + this.theta) * this._hypotenuse,
|
|
offsetY = Math.sin(this._angle + this.theta) * this._hypotenuse,
|
|
theta = this.theta,
|
|
sinTh = Math.sin(theta),
|
|
cosTh = Math.cos(theta);
|
|
|
|
var tl = {
|
|
x: this.left - offsetX,
|
|
y: this.top - offsetY
|
|
};
|
|
var tr = {
|
|
x: tl.x + (this.currentWidth * cosTh),
|
|
y: tl.y + (this.currentWidth * sinTh)
|
|
};
|
|
var br = {
|
|
x: tr.x - (this.currentHeight * sinTh),
|
|
y: tr.y + (this.currentHeight * cosTh)
|
|
};
|
|
var bl = {
|
|
x: tl.x - (this.currentHeight * sinTh),
|
|
y: tl.y + (this.currentHeight * cosTh)
|
|
};
|
|
var ml = {
|
|
x: tl.x - (this.currentHeight/2 * sinTh),
|
|
y: tl.y + (this.currentHeight/2 * cosTh)
|
|
};
|
|
var mt = {
|
|
x: tl.x + (this.currentWidth/2 * cosTh),
|
|
y: tl.y + (this.currentWidth/2 * sinTh)
|
|
};
|
|
var mr = {
|
|
x: tr.x - (this.currentHeight/2 * sinTh),
|
|
y: tr.y + (this.currentHeight/2 * cosTh)
|
|
}
|
|
var mb = {
|
|
x: bl.x + (this.currentWidth/2 * cosTh),
|
|
y: bl.y + (this.currentWidth/2 * sinTh)
|
|
}
|
|
|
|
|
|
|
|
this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb };
|
|
|
|
this._setCornerCoords();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Draws borders of an object's bounding box.
|
|
* Requires public properties: width, height
|
|
* Requires public options: padding, borderColor
|
|
* @method drawBorders
|
|
* @param {CanvasRenderingContext2D} ctx Context to draw on
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
drawBorders: function(ctx) {
|
|
if (!this.hasBorders) return;
|
|
|
|
var padding = this.padding,
|
|
padding2 = padding * 2;
|
|
|
|
ctx.save();
|
|
|
|
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
ctx.strokeStyle = this.borderColor;
|
|
|
|
var scaleX = 1 / (this.scaleX < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleX),
|
|
scaleY = 1 / (this.scaleY < this.MIN_SCALE_LIMIT ? this.MIN_SCALE_LIMIT : this.scaleY);
|
|
|
|
ctx.lineWidth = 1 / this.borderScaleFactor;
|
|
|
|
ctx.scale(scaleX, scaleY);
|
|
|
|
var w = this.getWidth(),
|
|
h = this.getHeight();
|
|
|
|
ctx.strokeRect(
|
|
~~(-(w / 2) - padding) + 0.5, // offset needed to make lines look sharper
|
|
~~(-(h / 2) - padding) + 0.5,
|
|
~~(w + padding2),
|
|
~~(h + padding2)
|
|
);
|
|
|
|
ctx.restore();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Draws corners of an object's bounding box.
|
|
* Requires public properties: width, height, scaleX, scaleY
|
|
* Requires public options: cornersize, padding
|
|
* @method drawCorners
|
|
* @param {CanvasRenderingContext2D} ctx Context to draw on
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
drawCorners: function(ctx) {
|
|
if (!this.hasControls) return;
|
|
|
|
var size = this.cornersize,
|
|
size2 = size / 2,
|
|
padding = this.padding,
|
|
left = -(this.width / 2),
|
|
top = -(this.height / 2),
|
|
_left,
|
|
_top,
|
|
sizeX = size / this.scaleX,
|
|
sizeY = size / this.scaleY,
|
|
scaleOffsetY = (padding + size2) / this.scaleY,
|
|
scaleOffsetX = (padding + size2) / this.scaleX,
|
|
scaleOffsetSizeX = (padding + size2 - size) / this.scaleX,
|
|
scaleOffsetSizeY = (padding + size2 - size) / this.scaleY,
|
|
height = this.height;
|
|
|
|
ctx.save();
|
|
|
|
ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
|
|
ctx.fillStyle = this.cornerColor;
|
|
|
|
_left = left - scaleOffsetX;
|
|
_top = top - scaleOffsetY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left + this.width - scaleOffsetX;
|
|
_top = top - scaleOffsetY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left - scaleOffsetX;
|
|
_top = top + height + scaleOffsetSizeY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left + this.width + scaleOffsetSizeX;
|
|
_top = top + height + scaleOffsetSizeY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left + this.width/2 - scaleOffsetX;
|
|
_top = top - scaleOffsetY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left + this.width/2 - scaleOffsetX;
|
|
_top = top + height + scaleOffsetSizeY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left + this.width + scaleOffsetSizeX;
|
|
_top = top + height/2 - scaleOffsetY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
_left = left - scaleOffsetX;
|
|
_top = top + height/2 - scaleOffsetY;
|
|
ctx.fillRect(_left, _top, sizeX, sizeY);
|
|
|
|
ctx.restore();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Clones an instance
|
|
* @method clone
|
|
* @param {Object} options object
|
|
* @return {fabric.Object} clone of an instance
|
|
*/
|
|
clone: function(options) {
|
|
if (this.constructor.fromObject) {
|
|
return this.constructor.fromObject(this.toObject(), options);
|
|
}
|
|
return new fabric.Object(this.toObject());
|
|
},
|
|
|
|
/**
|
|
* Creates an instance of fabric.Image out of an object
|
|
* @method cloneAsImage
|
|
* @param callback {Function} callback, invoked with an instance as a first argument
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
cloneAsImage: function(callback) {
|
|
if (fabric.Image) {
|
|
var i = new Image();
|
|
|
|
/** @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')
|
|
};
|
|
|
|
this.set('angle', 0).set('flipX', false).set('flipY', false);
|
|
i.src = this.toDataURL();
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Converts an object into a data-url-like string
|
|
* @method toDataURL
|
|
* @return {String} string of data
|
|
*/
|
|
toDataURL: function() {
|
|
var el = document.createElement('canvas');
|
|
|
|
el.width = this.getWidth();
|
|
el.height = this.getHeight();
|
|
|
|
fabric.util.wrapElement(el, 'div');
|
|
|
|
var canvas = new fabric.Canvas(el);
|
|
canvas.backgroundColor = 'transparent';
|
|
canvas.renderAll();
|
|
|
|
var clone = this.clone();
|
|
clone.left = el.width / 2;
|
|
clone.top = el.height / 2;
|
|
|
|
clone.setActive(false);
|
|
|
|
canvas.add(clone);
|
|
var data = canvas.toDataURL('png');
|
|
|
|
canvas.dispose();
|
|
canvas = clone = null;
|
|
return data;
|
|
},
|
|
|
|
/**
|
|
* @method hasStateChanged
|
|
* @return {Boolean} true if instance' state has changed
|
|
*/
|
|
hasStateChanged: function() {
|
|
return this.stateProperties.some(function(prop) {
|
|
return this[prop] !== this.originalState[prop];
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* @method saveState
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
saveState: function() {
|
|
this.stateProperties.forEach(function(prop) {
|
|
this.originalState[prop] = this.get(prop);
|
|
}, this);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @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) {
|
|
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),
|
|
br = new fabric.Point(oCoords.br.x, oCoords.br.y);
|
|
|
|
return tl.x > selectionTL.x
|
|
&& tr.x < selectionBR.x
|
|
&& tl.y > selectionTL.y
|
|
&& bl.y < selectionBR.y;
|
|
},
|
|
|
|
/**
|
|
* @method isType
|
|
* @param type {String} type to check against
|
|
* @return {Boolean} true if specified type is identical to the type of instance
|
|
*/
|
|
isType: function(type) {
|
|
return this.type === type;
|
|
},
|
|
|
|
/**
|
|
* Determines which one of the four corners has been clicked
|
|
* @method _findTargetCorner
|
|
* @private
|
|
* @param e {Event} event object
|
|
* @param offset {Object} canvas offset
|
|
* @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
|
|
*/
|
|
_findTargetCorner: function(e, offset) {
|
|
if (!this.hasControls) return false;
|
|
|
|
var pointer = getPointer(e),
|
|
ex = pointer.x - offset.left,
|
|
ey = pointer.y - offset.top,
|
|
xpoints,
|
|
lines;
|
|
|
|
for (var i in this.oCoords) {
|
|
lines = this._getImageLines(this.oCoords[i].corner, i);
|
|
|
|
xpoints = this._findCrossPoints(ex, ey, lines);
|
|
if (xpoints % 2 == 1 && xpoints != 0) {
|
|
this.__corner = i;
|
|
return i;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Helper method to determine how many cross points are between the 4 image edges
|
|
* and the horizontal line determined by the position of our mouse when clicked on canvas
|
|
* @method _findCrossPoints
|
|
* @private
|
|
* @param ex {Number} x coordinate of the mouse
|
|
* @param ey {Number} y coordinate of the mouse
|
|
* @param oCoords {Object} Coordinates of the image being evaluated
|
|
*/
|
|
_findCrossPoints: function(ex, ey, oCoords) {
|
|
var b1, b2, a1, a2, xi, yi,
|
|
xcount = 0,
|
|
iLine;
|
|
|
|
for (var lineKey in oCoords) {
|
|
iLine = oCoords[lineKey];
|
|
if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
|
|
continue;
|
|
}
|
|
if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
|
|
continue;
|
|
}
|
|
if ((iLine.o.x == iLine.d.x) && (iLine.o.x >= ex)) {
|
|
xi = iLine.o.x;
|
|
yi = ey;
|
|
}
|
|
else {
|
|
b1 = 0;
|
|
b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
|
|
a1 = ey-b1*ex;
|
|
a2 = iLine.o.y-b2*iLine.o.x;
|
|
|
|
xi = - (a1-a2)/(b1-b2);
|
|
yi = a1+b1*xi;
|
|
}
|
|
if (xi >= ex) {
|
|
xcount += 1;
|
|
}
|
|
if (xcount == 2) {
|
|
break;
|
|
}
|
|
}
|
|
return xcount;
|
|
},
|
|
|
|
/**
|
|
* Method that returns an object with the image lines in it given the coordinates of the corners
|
|
* @method _getImageLines
|
|
* @private
|
|
* @param oCoords {Object} coordinates of the image corners
|
|
*/
|
|
_getImageLines: function(oCoords, i) {
|
|
return {
|
|
topline: {
|
|
o: oCoords.tl,
|
|
d: oCoords.tr
|
|
},
|
|
rightline: {
|
|
o: oCoords.tr,
|
|
d: oCoords.br
|
|
},
|
|
bottomline: {
|
|
o: oCoords.br,
|
|
d: oCoords.bl
|
|
},
|
|
leftline: {
|
|
o: oCoords.bl,
|
|
d: oCoords.tl
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the coordinates of the draggable boxes in the corners of
|
|
* the image used to scale/rotate it.
|
|
* @method _setCornerCoords
|
|
* @private
|
|
*/
|
|
_setCornerCoords: function() {
|
|
var coords = this.oCoords,
|
|
theta = degreesToRadians(45 - this.getAngle()),
|
|
cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornersize, 2)) / 2,
|
|
cosHalfOffset = cornerHypotenuse * Math.cos(theta),
|
|
sinHalfOffset = cornerHypotenuse * Math.sin(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
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Makes object's color grayscale
|
|
* @method toGrayscale
|
|
* @return {fabric.Object} thisArg
|
|
*/
|
|
toGrayscale: function() {
|
|
var fillValue = this.get('fill');
|
|
if (fillValue) {
|
|
this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb());
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method complexity
|
|
* @return {Number}
|
|
*/
|
|
complexity: function() {
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* @method getCenter
|
|
* @return {Object} object with `x`, `y` properties corresponding to path center coordinates
|
|
*/
|
|
getCenter: function() {
|
|
return {
|
|
x: this.get('left') + this.width / 2,
|
|
y: this.get('top') + this.height / 2
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @method straighten
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
straighten: function() {
|
|
var angle = this._getAngleValueForStraighten();
|
|
this.setAngle(angle);
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method fxStraighten
|
|
* @param {Object} callbacks
|
|
* - onComplete: invoked on completion
|
|
* - onChange: invoked on every step of animation
|
|
*
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
fxStraighten: function(callbacks) {
|
|
callbacks = callbacks || { };
|
|
|
|
var empty = function() { },
|
|
onComplete = callbacks.onComplete || empty,
|
|
onChange = callbacks.onChange || empty,
|
|
_this = this;
|
|
|
|
fabric.util.animate({
|
|
startValue: this.get('angle'),
|
|
endValue: this._getAngleValueForStraighten(),
|
|
duration: this.FX_DURATION,
|
|
onChange: function(value) {
|
|
_this.setAngle(value);
|
|
onChange();
|
|
},
|
|
onComplete: function() {
|
|
_this.setCoords();
|
|
onComplete();
|
|
},
|
|
onStart: function() {
|
|
_this.setActive(false);
|
|
}
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method fxRemove
|
|
* @param {Object} callbacks
|
|
* @return {fabric.Object} thisArg
|
|
* @chainable
|
|
*/
|
|
fxRemove: function(callbacks) {
|
|
callbacks || (callbacks = { });
|
|
|
|
var empty = function() { },
|
|
onComplete = callbacks.onComplete || empty,
|
|
onChange = callbacks.onChange || empty,
|
|
_this = this;
|
|
|
|
fabric.util.animate({
|
|
startValue: this.get('opacity'),
|
|
endValue: 0,
|
|
duration: this.FX_DURATION,
|
|
onChange: function(value) {
|
|
_this.set('opacity', value);
|
|
onChange();
|
|
},
|
|
onComplete: onComplete,
|
|
onStart: function() {
|
|
_this.setActive(false);
|
|
}
|
|
});
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @method _getAngleValueForStraighten
|
|
* @return {Number} angle value
|
|
* @private
|
|
*/
|
|
_getAngleValueForStraighten: function() {
|
|
var angle = this.get('angle');
|
|
|
|
|
|
if (angle > -225 && angle <= -135) { return -180; }
|
|
else if (angle > -135 && angle <= -45) { return -90; }
|
|
else if (angle > -45 && angle <= 45) { return 0; }
|
|
else if (angle > 45 && angle <= 135) { return 90; }
|
|
else if (angle > 135 && angle <= 225 ) { return 180; }
|
|
else if (angle > 225 && angle <= 315) { return 270; }
|
|
else if (angle > 315) { return 360; }
|
|
|
|
return 0;
|
|
},
|
|
|
|
/**
|
|
* Returns a JSON representation of an instance
|
|
* @method toJSON
|
|
* @return {String} json
|
|
*/
|
|
toJSON: function() {
|
|
return this.toObject();
|
|
},
|
|
|
|
setGradientFill: function(ctx, options) {
|
|
this.set('fill', fabric.Gradient.forObject(this, ctx, options));
|
|
},
|
|
|
|
animate: function(property, to, options) {
|
|
var obj = this;
|
|
|
|
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,
|
|
duration: options.duration,
|
|
onChange: function(value) {
|
|
obj.set(property, value);
|
|
options.onChange && options.onChange();
|
|
},
|
|
onComplete: function() {
|
|
obj.setCoords();
|
|
options.onComplete && options.onComplete();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
})(typeof exports != 'undefined' ? exports : this);
|
|
|
|
(function(global) {
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
extend = fabric.util.object.extend;
|
|
|
|
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.set('width', (this.x2 - this.x1) || 1);
|
|
this.set('height', (this.y2 - this.y1) || 1);
|
|
this.set('left', this.x1 + this.width / 2);
|
|
this.set('top', this.y1 + this.height / 2);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(-this.width / 2, -this.height / 2);
|
|
ctx.lineTo(this.width / 2, this.height / 2);
|
|
|
|
ctx.lineWidth = this.strokeWidth;
|
|
|
|
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')
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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 radiusBy2ByScale = this.get('radius') * 2 * this.get('scaleX');
|
|
this.set('width', radiusBy2ByScale).set('height', radiusBy2ByScale);
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @method toObject
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function() {
|
|
return extend(this.callSuper('toObject'), {
|
|
radius: this.get('radius')
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
* @param ctx {CanvasRenderingContext2D} context to render on
|
|
*/
|
|
_render: function(ctx, noTransform) {
|
|
ctx.beginPath();
|
|
ctx.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');
|
|
},
|
|
|
|
/**
|
|
* 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 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 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')
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
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;
|
|
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);
|
|
if ('left' in parsedAttributes) {
|
|
parsedAttributes.left -= (options.width / 2) || 0;
|
|
}
|
|
if ('top' in parsedAttributes) {
|
|
parsedAttributes.top -= (options.height / 2) || 0;
|
|
}
|
|
return new fabric.Ellipse(extend(parsedAttributes, options));
|
|
};
|
|
|
|
/**
|
|
* 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 Object
|
|
*/
|
|
options: {
|
|
rx: 0,
|
|
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.group) {
|
|
ctx.translate(this.x, this.y);
|
|
}
|
|
|
|
ctx.moveTo(x+rx, y);
|
|
ctx.lineTo(x+w-rx, y);
|
|
ctx.bezierCurveTo(x+w, y, x+w, y+ry, x+w, y+ry);
|
|
ctx.lineTo(x+w, y+h-ry);
|
|
ctx.bezierCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h);
|
|
ctx.lineTo(x+rx,y+h);
|
|
ctx.bezierCurveTo(x,y+h,x,y+h-ry,x,y+h-ry);
|
|
ctx.lineTo(x,y+ry);
|
|
ctx.bezierCurveTo(x,y,x+rx,y,x+rx,y);
|
|
ctx.closePath();
|
|
|
|
if (this.fill) {
|
|
ctx.fill();
|
|
}
|
|
if (this.stroke) {
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
_normalizeLeftTopProperties: function(parsedAttributes) {
|
|
if (parsedAttributes.left) {
|
|
this.set('left', parsedAttributes.left + this.getWidth() / 2);
|
|
}
|
|
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;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* 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 = { });
|
|
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
var point;
|
|
ctx.beginPath();
|
|
for (var i = 0, len = this.points.length; i < len; i++) {
|
|
point = this.points[i];
|
|
ctx.lineTo(point.x, point.y);
|
|
}
|
|
if (this.fill) {
|
|
ctx.fill();
|
|
}
|
|
if (this.stroke) {
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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++) {
|
|
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;
|
|
|
|
if (fabric.Polygon) {
|
|
fabric.warn('fabric.Polygon is already defined');
|
|
return;
|
|
}
|
|
|
|
function byX(p) { return p.x; }
|
|
function byY(p) { return p.y; }
|
|
|
|
/**
|
|
* @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;
|
|
this.height = maxY - minY;
|
|
this.minX = minX;
|
|
this.minY = minY;
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @method toObject
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function() {
|
|
return extend(this.callSuper('toObject'), {
|
|
points: this.points.concat()
|
|
});
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
* @param ctx {CanvasRenderingContext2D} context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
var point;
|
|
ctx.beginPath();
|
|
for (var i = 0, len = this.points.length; i < len; i++) {
|
|
point = this.points[i];
|
|
ctx.lineTo(point.x, point.y);
|
|
}
|
|
if (this.fill) {
|
|
ctx.fill();
|
|
}
|
|
if (this.stroke) {
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns complexity of an instance
|
|
* @method complexity
|
|
* @return {Number} complexity of this instance
|
|
*/
|
|
complexity: function() {
|
|
return this.points.length;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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++) {
|
|
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<segs.length; i++) {
|
|
var bez = segmentToBezier.apply(this, segs[i]);
|
|
ctx.bezierCurveTo.apply(ctx, bez);
|
|
}
|
|
}
|
|
|
|
var arcToSegmentsCache = { },
|
|
segmentToBezierCache = { },
|
|
_join = Array.prototype.join,
|
|
argsString;
|
|
|
|
function arcToSegments(x, y, rx, ry, large, sweep, rotateX, ox, oy) {
|
|
argsString = _join.call(arguments);
|
|
if (arcToSegmentsCache[argsString]) {
|
|
return arcToSegmentsCache[argsString];
|
|
}
|
|
|
|
var th = rotateX * (Math.PI/180);
|
|
var sin_th = Math.sin(th);
|
|
var cos_th = Math.cos(th);
|
|
rx = Math.abs(rx);
|
|
ry = Math.abs(ry);
|
|
var px = cos_th * (ox - x) * 0.5 + sin_th * (oy - y) * 0.5;
|
|
var py = cos_th * (oy - y) * 0.5 - sin_th * (ox - x) * 0.5;
|
|
var pl = (px*px) / (rx*rx) + (py*py) / (ry*ry);
|
|
if (pl > 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<segments; i++) {
|
|
var th2 = th0 + i * th_arc / segments;
|
|
var th3 = th0 + (i+1) * th_arc / segments;
|
|
result[i] = [xc, yc, th2, th3, rx, ry, sin_th, cos_th];
|
|
}
|
|
|
|
return (arcToSegmentsCache[argsString] = result);
|
|
}
|
|
|
|
function segmentToBezier(cx, cy, th0, th1, rx, ry, sin_th, cos_th) {
|
|
argsString = _join.call(arguments);
|
|
if (segmentToBezierCache[argsString]) {
|
|
return segmentToBezierCache[argsString];
|
|
}
|
|
|
|
var a00 = cos_th * rx;
|
|
var a01 = -sin_th * ry;
|
|
var a10 = sin_th * rx;
|
|
var a11 = cos_th * ry;
|
|
|
|
var th_half = 0.5 * (th1 - th0);
|
|
var t = (8/3) * Math.sin(th_half * 0.5) * Math.sin(th_half * 0.5) / Math.sin(th_half);
|
|
var x1 = cx + Math.cos(th0) - t * Math.sin(th0);
|
|
var y1 = cy + Math.sin(th0) + t * Math.cos(th0);
|
|
var x3 = cx + Math.cos(th1);
|
|
var y3 = cy + Math.sin(th1);
|
|
var x2 = x3 + t * Math.sin(th1);
|
|
var y2 = y3 - t * Math.cos(th1);
|
|
|
|
return (segmentToBezierCache[argsString] = [
|
|
a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
|
|
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
|
|
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3
|
|
]);
|
|
}
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
min = fabric.util.array.min,
|
|
max = fabric.util.array.max,
|
|
extend = fabric.util.object.extend,
|
|
_toString = Object.prototype.toString;
|
|
|
|
if (fabric.Path) {
|
|
fabric.warn('fabric.Path is already defined');
|
|
return;
|
|
}
|
|
if (!fabric.Object) {
|
|
fabric.warn('fabric.Path requires fabric.Object');
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function getX(item) {
|
|
if (item[0] === 'H') {
|
|
return item[1];
|
|
}
|
|
return item[item.length - 2];
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
function getY(item) {
|
|
if (item[0] === 'V') {
|
|
return item[1];
|
|
}
|
|
return item[item.length - 1];
|
|
}
|
|
|
|
/**
|
|
* @class Path
|
|
* @extends fabric.Object
|
|
*/
|
|
fabric.Path = fabric.util.createClass(fabric.Object, /** @scope fabric.Path.prototype */ {
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
type: 'path',
|
|
|
|
/**
|
|
* Constructor
|
|
* @method initialize
|
|
* @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens)
|
|
* @param {Object} [options] Options object
|
|
*/
|
|
initialize: function(path, options) {
|
|
options = options || { };
|
|
|
|
this.setOptions(options);
|
|
|
|
if (!path) {
|
|
throw Error('`path` argument is required');
|
|
}
|
|
|
|
var fromArray = _toString.call(path) === '[object Array]';
|
|
|
|
this.path = fromArray
|
|
? path
|
|
: path.match && path.match(/[a-zA-Z][^a-zA-Z]*/g);
|
|
|
|
if (!this.path) return;
|
|
|
|
if (!fromArray) {
|
|
this._initializeFromArray(options);
|
|
}
|
|
|
|
if (options.sourcePath) {
|
|
this.setSourcePath(options.sourcePath);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _initializeFromArray
|
|
*/
|
|
_initializeFromArray: function(options) {
|
|
var isWidthSet = 'width' in options,
|
|
isHeightSet = 'height' in options;
|
|
|
|
this.path = this._parsePath();
|
|
|
|
if (!isWidthSet || !isHeightSet) {
|
|
extend(this, this._parseDimensions());
|
|
if (isWidthSet) {
|
|
this.width = options.width;
|
|
}
|
|
if (isHeightSet) {
|
|
this.height = options.height;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
*/
|
|
_render: function(ctx) {
|
|
var current, // current instruction
|
|
x = 0, // current x
|
|
y = 0, // current y
|
|
controlX = 0, // current control point x
|
|
controlY = 0, // current control point y
|
|
tempX,
|
|
tempY,
|
|
l = -(this.width / 2),
|
|
t = -(this.height / 2);
|
|
|
|
for (var i = 0, len = this.path.length; i < len; ++i) {
|
|
|
|
current = this.path[i];
|
|
|
|
switch (current[0]) { // first letter
|
|
|
|
case 'l': // lineto, relative
|
|
x += current[1];
|
|
y += current[2];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'L': // lineto, absolute
|
|
x = current[1];
|
|
y = current[2];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'h': // horizontal lineto, relative
|
|
x += current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'H': // horizontal lineto, absolute
|
|
x = current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'v': // vertical lineto, relative
|
|
y += current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'V': // verical lineto, absolute
|
|
y = current[1];
|
|
ctx.lineTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'm': // moveTo, relative
|
|
x += current[1];
|
|
y += current[2];
|
|
ctx.moveTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'M': // moveTo, absolute
|
|
x = current[1];
|
|
y = current[2];
|
|
ctx.moveTo(x + l, y + t);
|
|
break;
|
|
|
|
case 'c': // bezierCurveTo, relative
|
|
tempX = x + current[5];
|
|
tempY = y + current[6];
|
|
controlX = x + current[3];
|
|
controlY = y + current[4];
|
|
ctx.bezierCurveTo(
|
|
x + current[1] + l, // x1
|
|
y + current[2] + t, // y1
|
|
controlX + l, // x2
|
|
controlY + t, // y2
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
x = tempX;
|
|
y = tempY;
|
|
break;
|
|
|
|
case 'C': // bezierCurveTo, absolute
|
|
x = current[5];
|
|
y = current[6];
|
|
controlX = current[3];
|
|
controlY = current[4];
|
|
ctx.bezierCurveTo(
|
|
current[1] + l,
|
|
current[2] + t,
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 's': // shorthand cubic bezierCurveTo, relative
|
|
tempX = x + current[3];
|
|
tempY = y + current[4];
|
|
controlX = 2 * x - controlX;
|
|
controlY = 2 * y - controlY;
|
|
ctx.bezierCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + current[1] + l,
|
|
y + current[2] + t,
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
x = tempX;
|
|
y = tempY;
|
|
break;
|
|
|
|
case 'S': // shorthand cubic bezierCurveTo, absolute
|
|
tempX = current[3];
|
|
tempY = current[4];
|
|
controlX = 2*x - controlX;
|
|
controlY = 2*y - controlY;
|
|
ctx.bezierCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
current[1] + l,
|
|
current[2] + t,
|
|
tempX + l,
|
|
tempY + t
|
|
);
|
|
x = tempX;
|
|
y = tempY;
|
|
break;
|
|
|
|
case 'q': // quadraticCurveTo, relative
|
|
x += current[3];
|
|
y += current[4];
|
|
ctx.quadraticCurveTo(
|
|
current[1] + l,
|
|
current[2] + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'Q': // quadraticCurveTo, absolute
|
|
x = current[3];
|
|
y = current[4];
|
|
controlX = current[1];
|
|
controlY = current[2];
|
|
ctx.quadraticCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'T':
|
|
tempX = x;
|
|
tempY = y;
|
|
x = current[1];
|
|
y = current[2];
|
|
controlX = -controlX + 2 * tempX;
|
|
controlY = -controlY + 2 * tempY;
|
|
ctx.quadraticCurveTo(
|
|
controlX + l,
|
|
controlY + t,
|
|
x + l,
|
|
y + t
|
|
);
|
|
break;
|
|
|
|
case 'a':
|
|
drawArc(ctx, x + l, y + t, [
|
|
current[1],
|
|
current[2],
|
|
current[3],
|
|
current[4],
|
|
current[5],
|
|
current[6] + x + l,
|
|
current[7] + y + t
|
|
]);
|
|
x += current[6];
|
|
y += current[7];
|
|
break;
|
|
|
|
case 'A':
|
|
drawArc(ctx, x + l, y + t, [
|
|
current[1],
|
|
current[2],
|
|
current[3],
|
|
current[4],
|
|
current[5],
|
|
current[6] + l,
|
|
current[7] + t
|
|
]);
|
|
x = current[6];
|
|
y = current[7];
|
|
break;
|
|
|
|
case 'z':
|
|
case 'Z':
|
|
ctx.closePath();
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Renders path on a specified context
|
|
* @method render
|
|
* @param {CanvasRenderingContext2D} ctx context to render path on
|
|
* @param {Boolean} noTransform When true, context is not transformed
|
|
*/
|
|
render: function(ctx, noTransform) {
|
|
ctx.save();
|
|
var m = this.transformMatrix;
|
|
if (m) {
|
|
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
if (!noTransform) {
|
|
this.transform(ctx);
|
|
}
|
|
|
|
if (this.overlayFill) {
|
|
ctx.fillStyle = this.overlayFill;
|
|
}
|
|
else if (this.fill) {
|
|
ctx.fillStyle = this.fill;
|
|
}
|
|
|
|
if (this.stroke) {
|
|
ctx.strokeStyle = this.stroke;
|
|
}
|
|
ctx.beginPath();
|
|
|
|
this._render(ctx);
|
|
|
|
if (this.fill) {
|
|
ctx.fill();
|
|
}
|
|
if (this.stroke) {
|
|
ctx.strokeStyle = this.stroke;
|
|
ctx.lineWidth = this.strokeWidth;
|
|
ctx.lineCap = ctx.lineJoin = 'round';
|
|
ctx.stroke();
|
|
}
|
|
if (!noTransform && this.active) {
|
|
this.drawBorders(ctx);
|
|
this.hideCorners || this.drawCorners(ctx);
|
|
}
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* Returns string representation of an instance
|
|
* @method toString
|
|
* @return {String} string representation of an instance
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Path (' + this.complexity() +
|
|
'): { "top": ' + this.top + ', "left": ' + this.left + ' }>';
|
|
},
|
|
|
|
/**
|
|
* 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 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, j, 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([command].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]);
|
|
}
|
|
|
|
if (item[0] === item[0].toLowerCase()) {
|
|
isLowerCase = true;
|
|
}
|
|
|
|
|
|
|
|
x = isLowerCase
|
|
? previousX + getX(item)
|
|
: item[0] === 'V'
|
|
? previousX
|
|
: getX(item);
|
|
|
|
y = isLowerCase
|
|
? previousY + getY(item)
|
|
: item[0] === 'H'
|
|
? previousY
|
|
: getY(item);
|
|
|
|
var val = parseInt(x, 10);
|
|
if (!isNaN(val)) aX.push(val);
|
|
|
|
val = parseInt(y, 10);
|
|
if (!isNaN(val)) aY.push(val);
|
|
|
|
}, this);
|
|
|
|
var minX = min(aX),
|
|
minY = min(aY),
|
|
deltaX = 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 <path> element
|
|
* @static
|
|
* @method fabric.Path.fromElement
|
|
* @param {SVGElement} element to parse
|
|
* @param {Object} options object
|
|
* @return {fabric.Path} Instance of fabric.Path
|
|
*/
|
|
fabric.Path.fromElement = function(element, options) {
|
|
var parsedAttributes = fabric.parseAttributes(element, 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,
|
|
parentSet = fabric.Object.prototype.set,
|
|
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 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);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _initProperties
|
|
*/
|
|
|
|
/**
|
|
* Renders this group on a specified context
|
|
* @method render
|
|
* @param {CanvasRenderingContext2D} ctx Context to render this instance on
|
|
*/
|
|
render: function(ctx) {
|
|
if (this.stub) {
|
|
ctx.save();
|
|
|
|
this.transform(ctx);
|
|
this.stub.render(ctx, false /* no transform */);
|
|
if (this.active) {
|
|
this.drawBorders(ctx);
|
|
this.drawCorners(ctx);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
else {
|
|
ctx.save();
|
|
|
|
var m = this.transformMatrix;
|
|
if (m) {
|
|
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
this.transform(ctx);
|
|
for (var i = 0, l = this.paths.length; i < l; ++i) {
|
|
this.paths[i].render(ctx, true);
|
|
}
|
|
if (this.active) {
|
|
this.drawBorders(ctx);
|
|
this.hideCorners || this.drawCorners(ctx);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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') && this.isSameColor()) {
|
|
this[prop] = value;
|
|
var i = this.paths.length;
|
|
while (i--) {
|
|
this.paths[i].set(prop, value);
|
|
}
|
|
}
|
|
else {
|
|
parentSet.call(this, prop, value);
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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(), 'clone'),
|
|
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 a string representation of this path group
|
|
* @method toString
|
|
* @return {String} string representation of an object
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.PathGroup (' + this.complexity() +
|
|
'): { top: ' + this.top + ', left: ' + this.left + ' }>';
|
|
},
|
|
|
|
/**
|
|
* 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();
|
|
|
|
this.setCoords(true);
|
|
this.saveCoords();
|
|
|
|
this.activateAllObjects();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _updateObjectsCoords
|
|
*/
|
|
_updateObjectsCoords: function() {
|
|
var groupDeltaX = this.left,
|
|
groupDeltaY = this.top;
|
|
|
|
this.forEachObject(function(object) {
|
|
|
|
var objectLeft = object.get('left'),
|
|
objectTop = object.get('top');
|
|
|
|
object.set('originalLeft', objectLeft);
|
|
object.set('originalTop', objectTop);
|
|
|
|
object.set('left', objectLeft - groupDeltaX);
|
|
object.set('top', objectTop - groupDeltaY);
|
|
|
|
object.setCoords();
|
|
|
|
object.hideCorners = true;
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Returns string represenation of a group
|
|
* @method toString
|
|
* @return {String}
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Group: (' + this.complexity() + ')>';
|
|
},
|
|
|
|
/**
|
|
* 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 add
|
|
* @param {Object} object
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
add: function(object) {
|
|
this._restoreObjectsState();
|
|
this.objects.push(object);
|
|
object.setActive(true);
|
|
this._calcBounds();
|
|
this._updateObjectsCoords();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Removes an object from a group; Then recalculates group's dimension, position.
|
|
* @param {Object} object
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
remove: function(object) {
|
|
this._restoreObjectsState();
|
|
removeFromArray(this.objects, object);
|
|
object.setActive(false);
|
|
this._calcBounds();
|
|
this._updateObjectsCoords();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns a size of a group (i.e: length of an array containing its objects)
|
|
* @return {Number} Group size
|
|
*/
|
|
size: function() {
|
|
return this.getObjects().length;
|
|
},
|
|
|
|
/**
|
|
* Sets property to a given value
|
|
* @method set
|
|
* @param {String} name
|
|
* @param {Object|Function} value
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
set: function(name, value) {
|
|
if (typeof value == 'function') {
|
|
this.set(name, value(this[name]));
|
|
}
|
|
else {
|
|
if (name === 'fill' || name === 'opacity') {
|
|
var i = this.objects.length;
|
|
this[name] = value;
|
|
while (i--) {
|
|
this.objects[i].set(name, value);
|
|
}
|
|
}
|
|
else {
|
|
this[name] = value;
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Returns true if a group contains an object
|
|
* @method contains
|
|
* @param {Object} object Object to check against
|
|
* @return {Boolean} `true` if group contains an object
|
|
*/
|
|
contains: function(object) {
|
|
return this.objects.indexOf(object) > -1;
|
|
},
|
|
|
|
/**
|
|
* Returns object representation of an instance
|
|
* @method toObject
|
|
* @return {Object} object representation of an instance
|
|
*/
|
|
toObject: function() {
|
|
return extend(this.callSuper('toObject'), {
|
|
objects: invoke(this.objects, 'clone')
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Renders instance on a given context
|
|
* @method render
|
|
* @param {CanvasRenderingContext2D} ctx context to render instance on
|
|
*/
|
|
render: function(ctx) {
|
|
ctx.save();
|
|
this.transform(ctx);
|
|
|
|
var groupScaleFactor = Math.max(this.scaleX, this.scaleY);
|
|
|
|
for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) {
|
|
var originalScaleFactor = object.borderScaleFactor;
|
|
object.borderScaleFactor = groupScaleFactor;
|
|
object.render(ctx);
|
|
object.borderScaleFactor = originalScaleFactor;
|
|
}
|
|
this.hideBorders || this.drawBorders(ctx);
|
|
this.hideCorners || this.drawCorners(ctx);
|
|
ctx.restore();
|
|
this.setCoords();
|
|
},
|
|
|
|
/**
|
|
* 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),
|
|
objectLeft = object.get('originalLeft'),
|
|
objectTop = object.get('originalTop'),
|
|
rotatedTop = Math.cos(groupAngle) * object.get('top') + Math.sin(groupAngle) * object.get('left'),
|
|
rotatedLeft = -Math.sin(groupAngle) * object.get('top') + Math.cos(groupAngle) * object.get('left');
|
|
|
|
object.setAngle(object.getAngle() + this.getAngle());
|
|
|
|
object.set('left', groupLeft + rotatedLeft * this.get('scaleX'));
|
|
object.set('top', groupTop + rotatedTop * this.get('scaleY'));
|
|
|
|
object.set('scaleX', object.get('scaleX') * this.get('scaleX'));
|
|
object.set('scaleY', object.get('scaleY') * this.get('scaleY'));
|
|
|
|
object.setCoords();
|
|
object.hideCorners = false;
|
|
object.setActive(false);
|
|
object.setCoords();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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() {
|
|
return this.setActive(true);
|
|
},
|
|
|
|
/**
|
|
* Activates (makes active) all group objects
|
|
* @method setActive
|
|
* @param {Boolean} value `true` to activate object, `false` otherwise
|
|
* @return {fabric.Group} thisArg
|
|
* @chainable
|
|
*/
|
|
setActive: function(value) {
|
|
this.forEachObject(function(object) {
|
|
object.setActive(value);
|
|
});
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* 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.Canvas.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;
|
|
height = maxY - minY;
|
|
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
this.left = minX + width / 2;
|
|
this.top = minY + height / 2;
|
|
},
|
|
|
|
/**
|
|
* 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 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) {
|
|
return new fabric.Group(object.objects, object);
|
|
};
|
|
|
|
})(typeof exports != 'undefined' ? exports : this);
|
|
|
|
(function(global) {
|
|
|
|
"use strict";
|
|
|
|
var fabric = global.fabric || (global.fabric = { }),
|
|
extend = fabric.util.object.extend,
|
|
clone = fabric.util.object.clone;
|
|
|
|
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: 20,
|
|
|
|
/**
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
fontWeight: 100,
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
fontFamily: 'Times_New_Roman',
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
textDecoration: '',
|
|
|
|
/**
|
|
* @property
|
|
* @type String | null
|
|
*/
|
|
textShadow: null,
|
|
|
|
/**
|
|
* Determines text alignment. Possible values: "left", "center", or "right".
|
|
* @property
|
|
* @type String
|
|
*/
|
|
textAlign: 'left',
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
fontStyle: '',
|
|
|
|
/**
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
lineHeight: 1.6,
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
strokeStyle: '',
|
|
|
|
/**
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
strokeWidth: 1,
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
backgroundColor: '',
|
|
|
|
|
|
/**
|
|
* @property
|
|
* @type String | null
|
|
*/
|
|
path: null,
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
type: 'text',
|
|
|
|
/**
|
|
* 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.width = this.getWidth();
|
|
this.setCoords();
|
|
},
|
|
|
|
/**
|
|
* 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',
|
|
'path',
|
|
'text',
|
|
'textDecoration',
|
|
'textShadow',
|
|
'textAlign',
|
|
'fontStyle',
|
|
'lineHeight',
|
|
'strokeStyle',
|
|
'strokeWidth',
|
|
'backgroundColor'
|
|
);
|
|
fabric.util.removeFromArray(this.stateProperties, 'width');
|
|
},
|
|
|
|
/**
|
|
* Returns string representation of an instance
|
|
* @method toString
|
|
* @return {String} String representation of text object
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Text (' + this.complexity() +
|
|
'): { "text": "' + this.text + '", "fontFamily": "' + this.fontFamily + '" }>';
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
* @method _render
|
|
* @param {CanvasRenderingContext2D} ctx Context to render on
|
|
*/
|
|
_render: function(ctx) {
|
|
var o = Cufon.textOptions || (Cufon.textOptions = { });
|
|
|
|
o.left = this.left;
|
|
o.top = this.top;
|
|
o.context = ctx;
|
|
o.color = this.fill;
|
|
|
|
var el = this._initDummyElement();
|
|
|
|
this.transform(ctx);
|
|
|
|
Cufon.replaceElement(el, {
|
|
separate: 'none',
|
|
fontFamily: this.fontFamily,
|
|
enableTextDecoration: true,
|
|
textDecoration: this.textDecoration,
|
|
textShadow: this.textShadow,
|
|
textAlign: this.textAlign,
|
|
fontStyle: this.fontStyle,
|
|
lineHeight: this.lineHeight,
|
|
strokeStyle: this.strokeStyle,
|
|
strokeWidth: this.strokeWidth,
|
|
backgroundColor: this.backgroundColor
|
|
});
|
|
|
|
this.width = o.width;
|
|
this.height = o.height;
|
|
|
|
this.setCoords();
|
|
},
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @method _initDummyElement
|
|
*/
|
|
_initDummyElement: function() {
|
|
var el = document.createElement('div'),
|
|
container = document.createElement('div');
|
|
|
|
container.appendChild(el);
|
|
el.innerHTML = this.text;
|
|
|
|
el.style.fontSize = '40px';
|
|
el.style.fontWeight = '400';
|
|
el.style.letterSpacing = 'normal';
|
|
el.style.color = '#000000';
|
|
el.style.fontWeight = '600';
|
|
el.style.fontFamily = 'Verdana';
|
|
|
|
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
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 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.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.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) {
|
|
this[name] = value;
|
|
if (name === 'fontFamily' && this.path) {
|
|
this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3');
|
|
}
|
|
return this;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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 (<b>not yet implemented</b>)
|
|
* @static
|
|
* @method fabric.Text.fromElement
|
|
* @return {fabric.Text} an instance
|
|
*/
|
|
fabric.Text.fromElement = function(element) {
|
|
};
|
|
|
|
})(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 Number
|
|
*/
|
|
maxwidth: null,
|
|
|
|
/**
|
|
* @property
|
|
* @type Number
|
|
*/
|
|
maxheight: null,
|
|
|
|
/**
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
active: false,
|
|
|
|
/**
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
bordervisibility: false,
|
|
|
|
/**
|
|
* @property
|
|
* @type Boolean
|
|
*/
|
|
cornervisibility: false,
|
|
|
|
/**
|
|
* @property
|
|
* @type String
|
|
*/
|
|
type: 'image',
|
|
|
|
__isGrayscaled: false,
|
|
|
|
/**
|
|
* Constructor
|
|
* @param {HTMLImageElement | String} element Image element
|
|
* @param {Object} options optional
|
|
*/
|
|
initialize: function(element, options) {
|
|
this.callSuper('initialize', options);
|
|
this._initElement(element);
|
|
this._initConfig(options || { });
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Resizes an image depending on whether maxwidth and maxheight are set up;
|
|
* Width and height have to mantain the same proportion in the final image as it was in the initial one.
|
|
* @method getNormalizedSize
|
|
* @param {Object} oImg
|
|
* @param {Number} maxwidth maximum width of the image (in px)
|
|
* @param {Number} maxheight maximum height of the image (in px)
|
|
*/
|
|
getNormalizedSize: function(oImg, maxwidth, maxheight) {
|
|
if (maxheight && maxwidth && (oImg.width > oImg.height && (oImg.width / oImg.height) < (maxwidth / maxheight))) {
|
|
normalizedWidth = ~~((oImg.width * maxheight) / oImg.height);
|
|
normalizedHeight = maxheight;
|
|
}
|
|
else if (maxheight && ((oImg.height == oImg.width) || (oImg.height > oImg.width) || (oImg.height > maxheight))) {
|
|
normalizedWidth = ~~((oImg.width * maxheight) / oImg.height);
|
|
normalizedHeight = maxheight;
|
|
}
|
|
else if (maxwidth && (maxwidth < oImg.width)) {
|
|
normalizedHeight = ~~((oImg.height * maxwidth) / oImg.width);
|
|
normalizedWidth = maxwidth;
|
|
}
|
|
else {
|
|
normalizedWidth = oImg.width;
|
|
normalizedHeight = oImg.height;
|
|
}
|
|
|
|
return {
|
|
width: normalizedWidth,
|
|
height: normalizedHeight
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 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(visible) {
|
|
this._resetWidthHeight();
|
|
this._adjustWidthHeightToBorders(showBorder);
|
|
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();
|
|
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.getSrc()
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns source of an image
|
|
* @method getSrc
|
|
* @return {String} Source of an image
|
|
*/
|
|
getSrc: function() {
|
|
return this.getElement().src;
|
|
},
|
|
|
|
/**
|
|
* Returns string representation of an instance
|
|
* @method toString
|
|
* @return {String} String representation of an instance
|
|
*/
|
|
toString: function() {
|
|
return '#<fabric.Image: { src: "' + this.getSrc() + '" }>';
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* Makes image grayscale
|
|
* @mthod toGrayscale
|
|
* @param {Function} callback
|
|
*/
|
|
toGrayscale: function(callback) {
|
|
|
|
if (this.__isGrayscaled) {
|
|
return;
|
|
}
|
|
|
|
var imgEl = this.getElement(),
|
|
canvasEl = document.createElement('canvas'),
|
|
replacement = document.createElement('img'),
|
|
_this = this;
|
|
|
|
canvasEl.width = imgEl.width;
|
|
canvasEl.height = imgEl.height;
|
|
|
|
canvasEl.getContext('2d').drawImage(imgEl, 0, 0);
|
|
fabric.Canvas.toGrayscale(canvasEl);
|
|
|
|
/** @ignore */
|
|
replacement.onload = function() {
|
|
_this.setElement(replacement);
|
|
callback && callback();
|
|
replacement.onload = canvasEl = imgEl = imageData = null;
|
|
};
|
|
replacement.width = imgEl.width;
|
|
replacement.height = imgEl.height;
|
|
|
|
replacement.src = canvasEl.toDataURL('image/png');
|
|
|
|
this.__isGrayscaled = true;
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_render: function(ctx) {
|
|
var originalImgSize = this.getOriginalSize();
|
|
ctx.drawImage(
|
|
this.getElement(),
|
|
- originalImgSize.width / 2,
|
|
- originalImgSize.height / 2,
|
|
originalImgSize.width,
|
|
originalImgSize.height
|
|
);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_adjustWidthHeightToBorders: function(showBorder) {
|
|
if (showBorder) {
|
|
this.currentBorder = this.borderwidth;
|
|
this.width += (2 * this.currentBorder);
|
|
this.height += (2 * this.currentBorder);
|
|
}
|
|
else {
|
|
this.currentBorder = 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_resetWidthHeight: function() {
|
|
var element = this.getElement();
|
|
|
|
this.set('width', element.width);
|
|
this.set('height', element.height);
|
|
},
|
|
|
|
/**
|
|
* The Image class's initialization method. This method is automatically
|
|
* called by the constructor.
|
|
* @method _initElement
|
|
* @param {HTMLImageElement|String} el The element representing the image
|
|
*/
|
|
_initElement: function(element) {
|
|
this.setElement(fabric.util.getById(element));
|
|
fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS);
|
|
},
|
|
|
|
/**
|
|
* @method _initConfig
|
|
* @param {Object} options Options object
|
|
*/
|
|
_initConfig: function(options) {
|
|
this.setOptions(options);
|
|
this._setBorder();
|
|
this._setWidthHeight(options);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setBorder: function() {
|
|
if (this.bordervisibility) {
|
|
this.currentBorder = this.borderwidth;
|
|
}
|
|
else {
|
|
this.currentBorder = 0;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_setWidthHeight: function(options) {
|
|
var sidesBorderWidth = 2 * this.currentBorder;
|
|
this.width = (this.getElement().width || 0) + sidesBorderWidth;
|
|
this.height = (this.getElement().height || 0) + sidesBorderWidth;
|
|
},
|
|
|
|
/**
|
|
* 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";
|
|
|
|
/**
|
|
* 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 = document.createElement('img'),
|
|
src = object.src;
|
|
|
|
if (object.width) {
|
|
img.width = object.width;
|
|
}
|
|
if (object.height) {
|
|
img.height = object.height;
|
|
}
|
|
|
|
/** @ignore */
|
|
img.onload = function() {
|
|
if (callback) {
|
|
callback(new fabric.Image(img, object));
|
|
}
|
|
img = img.onload = null;
|
|
};
|
|
img.src = src;
|
|
};
|
|
|
|
/**
|
|
* Creates an instance of fabric.Image from an URL string
|
|
* @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 = 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.fromElement.async = true;
|
|
|
|
})(typeof exports != 'undefined' ? exports : this);
|