diff options
| author | Bobby <[email protected]> | 2013-06-13 01:41:58 -0500 |
|---|---|---|
| committer | Imtiyaz S. Momin <[email protected]> | 2013-06-13 01:41:58 -0500 |
| commit | 3a02e993ef59cfdde447e2bd95dca34142a776b1 (patch) | |
| tree | f55740990a700f28f08ef0269e850036f66f66e4 /js | |
| download | TShirtDesigner-3a02e993ef59cfdde447e2bd95dca34142a776b1.tar.xz TShirtDesigner-3a02e993ef59cfdde447e2bd95dca34142a776b1.zip | |
Committing local files
Diffstat (limited to 'js')
| -rw-r--r-- | js/bootstrap.js | 2027 | ||||
| -rw-r--r-- | js/bootstrap.min.js | 6 | ||||
| -rw-r--r-- | js/caseEditor.js | 344 | ||||
| -rw-r--r-- | js/excanvas.js | 924 | ||||
| -rw-r--r-- | js/fabric.js | 13962 | ||||
| -rw-r--r-- | js/jquery.miniColors.min.js | 9 | ||||
| -rw-r--r-- | js/tshirtEditor.js | 355 |
7 files changed, 17627 insertions, 0 deletions
diff --git a/js/bootstrap.js b/js/bootstrap.js new file mode 100644 index 0000000..f73fcb8 --- /dev/null +++ b/js/bootstrap.js @@ -0,0 +1,2027 @@ +/* =================================================== + * bootstrap-transition.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + $(function () { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-alert.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT DATA-API + * ============== */ + + $(function () { + $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-button.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON DATA-API + * =============== */ + + $(function () { + $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + }) + +}(window.jQuery);/* ========================================================== + * bootstrap-carousel.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.options = options + this.options.slide && this.slide(this.options.slide) + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , to: function (pos) { + var $active = this.$element.find('.item.active') + , children = $active.parent().children() + , activePos = children.index($active) + , that = this + + if (pos > (children.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activePos == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle() + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e = $.Event('slide', { + relatedTarget: $next[0] + }) + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + if ($next.hasClass('active')) return + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL DATA-API + * ================= */ + + $(function () { + $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) + $target.carousel(options) + e.preventDefault() + }) + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-collapse.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSIBLE PLUGIN DEFINITION + * ============================== */ + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = typeof option == 'object' && option + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSIBLE DATA-API + * ==================== */ + + $(function () { + $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + }) + +}(window.jQuery);/* ============================================================ + * bootstrap-dropdown.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + $this.focus() + } + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) return $this.click() + + $items = $('[role=menu] li:not(.divider) a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + getParent($(toggle)) + .removeClass('open') + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + $parent.length || ($parent = $this.parent()) + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(function () { + $('html') + .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) + $('body') + .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + }) + +}(window.jQuery);/* ========================================================= + * bootstrap-modal.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + $('body').addClass('modal-open') + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element + .show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + .focus() + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : + that.$element.trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + $('body').removeClass('modal-open') + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function (that) { + this.$element + .hide() + .trigger('hidden') + + this.backdrop() + } + + , removeBackdrop: function () { + this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + if (this.options.backdrop != 'static') { + this.$backdrop.click($.proxy(this.hide, this)) + } + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + doAnimate ? + this.$backdrop.one($.support.transition.end, callback) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) : + this.removeBackdrop() + + } else if (callback) { + callback() + } + } + } + + + /* MODAL PLUGIN DEFINITION + * ======================= */ + + $.fn.modal = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('modal') + , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option]() + else if (options.show) data.show() + }) + } + + $.fn.modal.defaults = { + backdrop: true + , keyboard: true + , show: true + } + + $.fn.modal.Constructor = Modal + + + /* MODAL DATA-API + * ============== */ + + $(function () { + $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) { + var $this = $(this) + , href = $this.attr('href') + , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + , option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + e.preventDefault() + + $target + .modal(option) + .one('hide', function () { + $this.focus() + }) + }) + }) + +}(window.jQuery);/* =========================================================== + * bootstrap-tooltip.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + if (this.options.trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (this.options.trigger != 'manual') { + eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , inside + , pos + , actualWidth + , actualHeight + , placement + , tp + + if (this.hasContent() && this.enabled) { + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + inside = /in/.test(placement) + + $tip + .remove() + .css({ top: 0, left: 0, display: 'block' }) + .appendTo(inside ? this.$element : document.body) + + pos = this.getPosition(inside) + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (inside ? placement.split(' ')[1] : placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + $tip + .css(tp) + .addClass(placement) + .addClass('in') + } + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).remove() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.remove() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.remove() + + return this + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function (inside) { + return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { + width: this.$element[0].offsetWidth + , height: this.$element[0].offsetHeight + }) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function () { + this[this.tip().hasClass('in') ? 'hide' : 'show']() + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + , trigger: 'hover' + , title: '' + , delay: 0 + , html: true + } + +}(window.jQuery); +/* =========================================================== + * bootstrap-popover.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#popovers + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* POPOVER PUBLIC CLASS DEFINITION + * =============================== */ + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + + /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js + ========================================== */ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { + + constructor: Popover + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + , content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content > *')[this.options.html ? 'html' : 'text'](content) + + $tip.removeClass('fade top bottom left right in') + } + + , hasContent: function () { + return this.getTitle() || this.getContent() + } + + , getContent: function () { + var content + , $e = this.$element + , o = this.options + + content = $e.attr('data-content') + || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) + + return content + } + + , tip: function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + } + return this.$tip + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + }) + + + /* POPOVER PLUGIN DEFINITION + * ======================= */ + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('popover') + , options = typeof option == 'object' && option + if (!data) $this.data('popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { + placement: 'right' + , trigger: 'click' + , content: '' + , template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>' + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-scrollspy.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#scrollspy + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* SCROLLSPY CLASS DEFINITION + * ========================== */ + + function ScrollSpy(element, options) { + var process = $.proxy(this.process, this) + , $element = $(element).is('body') ? $(window) : $(element) + , href + this.options = $.extend({}, $.fn.scrollspy.defaults, options) + this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) + this.selector = (this.options.target + || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + || '') + ' .nav li > a' + this.$body = $('body') + this.refresh() + this.process() + } + + ScrollSpy.prototype = { + + constructor: ScrollSpy + + , refresh: function () { + var self = this + , $targets + + this.offsets = $([]) + this.targets = $([]) + + $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + , href = $el.data('target') || $el.attr('href') + , $href = /^#\w/.test(href) && $(href) + return ( $href + && $href.length + && [[ $href.position().top, href ]] ) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } + + , process: function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + , maxScroll = scrollHeight - this.$scrollElement.height() + , offsets = this.offsets + , targets = this.targets + , activeTarget = this.activeTarget + , i + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) + && this.activate ( i ) + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } + + , activate: function (target) { + var active + , selector + + this.activeTarget = target + + $(this.selector) + .parent('.active') + .removeClass('active') + + selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + active = $(selector) + .parent('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active.closest('li.dropdown').addClass('active') + } + + active.trigger('activate') + } + + } + + + /* SCROLLSPY PLUGIN DEFINITION + * =========================== */ + + $.fn.scrollspy = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('scrollspy') + , options = typeof option == 'object' && option + if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.scrollspy.Constructor = ScrollSpy + + $.fn.scrollspy.defaults = { + offset: 10 + } + + + /* SCROLLSPY DATA-API + * ================== */ + + $(window).on('load', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + $spy.scrollspy($spy.data()) + }) + }) + +}(window.jQuery);/* ======================================================== + * bootstrap-tab.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#tabs + * ======================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TAB CLASS DEFINITION + * ==================== */ + + var Tab = function (element) { + this.element = $(element) + } + + Tab.prototype = { + + constructor: Tab + + , show: function () { + var $this = this.element + , $ul = $this.closest('ul:not(.dropdown-menu)') + , selector = $this.attr('data-target') + , previous + , $target + , e + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + if ( $this.parent('li').hasClass('active') ) return + + previous = $ul.find('.active a').last()[0] + + e = $.Event('show', { + relatedTarget: previous + }) + + $this.trigger(e) + + if (e.isDefaultPrevented()) return + + $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown' + , relatedTarget: previous + }) + }) + } + + , activate: function ( element, container, callback) { + var $active = container.find('> .active') + , transition = callback + && $.support.transition + && $active.hasClass('fade') + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + + element.addClass('active') + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if ( element.parent('.dropdown-menu') ) { + element.closest('li.dropdown').addClass('active') + } + + callback && callback() + } + + transition ? + $active.one($.support.transition.end, next) : + next() + + $active.removeClass('in') + } + } + + + /* TAB PLUGIN DEFINITION + * ===================== */ + + $.fn.tab = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tab') + if (!data) $this.data('tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tab.Constructor = Tab + + + /* TAB DATA-API + * ============ */ + + $(function () { + $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + e.preventDefault() + $(this).tab('show') + }) + }) + +}(window.jQuery);/* ============================================================= + * bootstrap-typeahead.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.$menu = $(this.options.menu).appendTo('body') + this.source = this.options.source + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.offset(), { + height: this.$element[0].offsetHeight + }) + + this.$menu.css({ + top: pos.top + pos.height + , left: pos.left + }) + + this.$menu.show() + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if ($.browser.chrome || $.browser.webkit || $.browser.msie) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , blur: function (e) { + var that = this + setTimeout(function () { that.hide() }, 150) + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + } + + , mouseenter: function (e) { + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '<ul class="typeahead dropdown-menu"></ul>' + , item: '<li><a href="#"></a></li>' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(function () { + $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + e.preventDefault() + $this.typeahead($this.data()) + }) + }) + +}(window.jQuery); +/* ========================================================== + * bootstrap-affix.js v2.1.1 + * http://twitter.github.com/bootstrap/javascript.html#affix + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* AFFIX CLASS DEFINITION + * ====================== */ + + var Affix = function (element, options) { + this.options = $.extend({}, $.fn.affix.defaults, options) + this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) + this.$element = $(element) + this.checkPosition() + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + , scrollTop = this.$window.scrollTop() + , position = this.$element.offset() + , offset = this.options.offset + , offsetBottom = offset.bottom + , offsetTop = offset.top + , reset = 'affix affix-top affix-bottom' + , affix + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top() + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + + affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? + false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? + 'bottom' : offsetTop != null && scrollTop <= offsetTop ? + 'top' : false + + if (this.affixed === affix) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? position.top - scrollTop : null + + this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) + } + + + /* AFFIX PLUGIN DEFINITION + * ======================= */ + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('affix') + , options = typeof option == 'object' && option + if (!data) $this.data('affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + $.fn.affix.defaults = { + offset: 0 + } + + + /* AFFIX DATA-API + * ============== */ + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + , data = $spy.data() + + data.offset = data.offset || {} + + data.offsetBottom && (data.offset.bottom = data.offsetBottom) + data.offsetTop && (data.offset.top = data.offsetTop) + + $spy.affix(data) + }) + }) + + +}(window.jQuery);
\ No newline at end of file diff --git a/js/bootstrap.min.js b/js/bootstrap.min.js new file mode 100644 index 0000000..0e33fb1 --- /dev/null +++ b/js/bootstrap.min.js @@ -0,0 +1,6 @@ +/*! +* Bootstrap.js by @fat & @mdo +* Copyright 2012 Twitter, Inc. +* http://www.apache.org/licenses/LICENSE-2.0.txt +*/ +!function(e){e(function(){"use strict";e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()},e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e(function(){e("body").on("click.alert.data-api",t,n.prototype.close)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")},e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e(function(){e("body").on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=n,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},to:function(t){var n=this.$element.find(".item.active"),r=n.parent().children(),i=r.index(n),s=this;if(t>r.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){s.to(t)}):i==t?this.pause().cycle():this.slide(t>i?"next":"prev",e(r[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f=e.Event("slide",{relatedTarget:i[0]});this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u]();if(i.hasClass("active"))return;if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}},e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e(function(){e("body").on("click.carousel.data-api","[data-slide]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=!i.data("modal")&&e.extend({},i.data(),n.data());i.carousel(s),t.preventDefault()})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning)return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning)return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=typeof n=="object"&&n;i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e(function(){e("body").on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})})}(window.jQuery),!function(e){"use strict";function r(){i(e(t)).removeClass("open")}function i(t){var n=t.attr("data-target"),r;return n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=e(n),r.length||(r=t.parent()),r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||(s.toggleClass("open"),n.focus()),!1},keydown:function(t){var n,r,s,o,u,a;if(!/(38|40|27)/.test(t.keyCode))return;n=e(this),t.preventDefault(),t.stopPropagation();if(n.is(".disabled, :disabled"))return;o=i(n),u=o.hasClass("open");if(!u||u&&t.keyCode==27)return n.click();r=e("[role=menu] li:not(.divider) a",o);if(!r.length)return;a=r.index(r.filter(":focus")),t.keyCode==38&&a>0&&a--,t.keyCode==40&&a<r.length-1&&a++,~a||(a=0),r.eq(a).focus()}},e.fn.dropdown=function(t){return this.each(function(){var r=e(this),i=r.data("dropdown");i||r.data("dropdown",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.dropdown.Constructor=n,e(function(){e("html").on("click.dropdown.data-api touchstart.dropdown.data-api",r),e("body").on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",t,n.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",t+", [role=menu]",n.prototype.keydown)})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=n,this.$element=e(t).delegate('[data-dismiss="modal"]',"click.dismiss.modal",e.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};t.prototype={constructor:t,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var t=this,n=e.Event("show");this.$element.trigger(n);if(this.isShown||n.isDefaultPrevented())return;e("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var n=e.support.transition&&t.$element.hasClass("fade");t.$element.parent().length||t.$element.appendTo(document.body),t.$element.show(),n&&t.$element[0].offsetWidth,t.$element.addClass("in").attr("aria-hidden",!1).focus(),t.enforceFocus(),n?t.$element.one(e.support.transition.end,function(){t.$element.trigger("shown")}):t.$element.trigger("shown")})},hide:function(t){t&&t.preventDefault();var n=this;t=e.Event("hide"),this.$element.trigger(t);if(!this.isShown||t.isDefaultPrevented())return;this.isShown=!1,e("body").removeClass("modal-open"),this.escape(),e(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),e.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var t=this;e(document).on("focusin.modal",function(e){t.$element[0]!==e.target&&!t.$element.has(e.target).length&&t.$element.focus()})},escape:function(){var e=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(t){t.which==27&&e.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var t=this,n=setTimeout(function(){t.$element.off(e.support.transition.end),t.hideModal()},500);this.$element.one(e.support.transition.end,function(){clearTimeout(n),t.hideModal()})},hideModal:function(e){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(t){var n=this,r=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var i=e.support.transition&&r;this.$backdrop=e('<div class="modal-backdrop '+r+'" />').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,e.proxy(this.removeBackdrop,this)):this.removeBackdrop()):t&&t()}},e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e(function(){e("body").on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):this.options.trigger!="manual"&&(i=this.options.trigger=="hover"?"mouseenter":"focus",s=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this))),this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,t,this.$element.data()),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);if(!n.options.delay||!n.options.delay.show)return n.show();clearTimeout(this.timeout),n.hoverState="in",this.timeout=setTimeout(function(){n.hoverState=="in"&&n.show()},n.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var e,t,n,r,i,s,o;if(this.hasContent()&&this.enabled){e=this.tip(),this.setContent(),this.options.animation&&e.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,t=/in/.test(s),e.remove().css({top:0,left:0,display:"block"}).appendTo(t?this.$element:document.body),n=this.getPosition(t),r=e[0].offsetWidth,i=e[0].offsetHeight;switch(t?s.split(" ")[1]:s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}e.css(o).addClass(s).addClass("in")}},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function r(){var t=setTimeout(function(){n.off(e.support.transition.end).remove()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.remove()})}var t=this,n=this.tip();return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?r():n.remove(),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(t){return e.extend({},t?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content > *")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-content")||(typeof n.content=="function"?n.content.call(t[0]):n.content),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var t=e(this),n=t.data("target")||t.attr("href"),r=/^#\w/.test(n)&&e(n);return r&&r.length&&[[r.position().top,n]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}},e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active a").last()[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}},e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e(function(){e("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=e(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:t.top+t.height,left:t.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(n=e.isFunction(this.source)?this.source(this.query,e.proxy(this.process,this)):this.source,n?this.process(n):this)},process:function(t){var n=this;return t=e.grep(t,function(e){return n.matcher(e)}),t=this.sorter(t),t.length?this.render(t.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(e){return~e.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(e){var t=[],n=[],r=[],i;while(i=e.shift())i.toLowerCase().indexOf(this.query.toLowerCase())?~i.indexOf(this.query)?n.push(i):r.push(i):t.push(i);return t.concat(n,r)},highlighter:function(e){var t=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return e.replace(new RegExp("("+t+")","ig"),function(e,t){return"<strong>"+t+"</strong>"})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))},e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery);
\ No newline at end of file diff --git a/js/caseEditor.js b/js/caseEditor.js new file mode 100644 index 0000000..5e4c46a --- /dev/null +++ b/js/caseEditor.js @@ -0,0 +1,344 @@ +var canvas; +var tshirts = new Array(); //prototype: [{style:'x',color:'white',front:'a',back:'b',price:{tshirt:'12.95',frontPrint:'4.99',backPrint:'4.99',total:'22.47'}}] +var a; +var b; +var line1; +var line2; +var line3; +var line4; + $(document).ready(function() { + //setup front side canvas + canvas = new fabric.Canvas('tcanvas', { + hoverCursor: 'pointer', + selection: true, + selectionBorderColor:'blue' + }); + canvas.on({ + 'object:moving': function(e) { + e.target.opacity = 0.5; + }, + 'object:modified': function(e) { + e.target.opacity = 1; + }, + 'object:selected':onObjectSelected, + 'selection:cleared':onSelectedCleared + }); + // piggyback on `canvas.findTarget`, to fire "object:over" and "object:out" events + canvas.findTarget = (function(originalFn) { + return function() { + var target = originalFn.apply(this, arguments); + if (target) { + if (this._hoveredTarget !== target) { + canvas.fire('object:over', { target: target }); + if (this._hoveredTarget) { + canvas.fire('object:out', { target: this._hoveredTarget }); + } + this._hoveredTarget = target; + } + } + else if (this._hoveredTarget) { + canvas.fire('object:out', { target: this._hoveredTarget }); + this._hoveredTarget = null; + } + return target; + }; + })(canvas.findTarget); + + canvas.on('object:over', function(e) { + //e.target.setFill('red'); + //canvas.renderAll(); + }); + + canvas.on('object:out', function(e) { + //e.target.setFill('green'); + //canvas.renderAll(); + }); + + document.getElementById('add-text').onclick = function() { + var text = $("#text-string").val(); + var textSample = new fabric.Text(text, { + left: fabric.util.getRandomInt(0, 200), + top: fabric.util.getRandomInt(0, 400), + fontFamily: 'helvetica', + angle: 0, + fill: '#000000', + scaleX: 0.5, + scaleY: 0.5, + fontWeight: '', + hasRotatingPoint:true + }); + canvas.add(textSample); + canvas.item(canvas.item.length-1).hasRotatingPoint = true; + $("#texteditor").css('display', 'block'); + $("#imageeditor").css('display', 'block'); + }; + $("#text-string").keyup(function(){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.text = this.value; + canvas.renderAll(); + } + }); + + $("#phoneTypes").change(function(e){ + debugger; + if($(this).val() == "1"){ + $("#phoneDiv").css('height','590'); + $("#phone")[0].src = "img/phones/iphone5A.png"; + //$("#borderMask")[0].src = "img/phones/iphone5Mask.png"; + line1 = new fabric.Line([0,0,225,0], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line2 = new fabric.Line([224,0,225,450], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line3 = new fabric.Line([0,0,0,450], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line4 = new fabric.Line([0,450,225,449], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + } + else if ($(this).val() == "2"){ + $("#phoneDiv").css('height','540'); + $("#phone")[0].src = "img/phones/iPhone4A.png"; + //$("#borderMask")[0].src = "img/phones/iphone4Mask.png"; + line1 = new fabric.Line([0,20,220,20], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line2 = new fabric.Line([220,20,220,420], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line3 = new fabric.Line([0,20,0,420], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line4 = new fabric.Line([0,420,220,420], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + } + else if ($(this).val() == "3"){ + $("#phoneDiv").css('height','535'); + $("#phone")[0].src = "img/phones/GalaxyS3A.png"; + //$("#borderMask")[0].src = "img/phones/GalaxyS3Mask.png"; + line1 = new fabric.Line([0,30,225,30], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line2 = new fabric.Line([224,30,225,400], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line3 = new fabric.Line([0,30,0,400], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line4 = new fabric.Line([0,400,225,400], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + } + }); + + line1 = new fabric.Line([0,0,225,0], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line2 = new fabric.Line([224,0,225,450], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line3 = new fabric.Line([0,0,0,450], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line4 = new fabric.Line([0,450,225,449], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + + $(".img-polaroid").click(function(e){ + var el = e.target; + var design = $(this).attr("src"); + $('#phoneDiv').css({ + 'backgroundImage': 'url(' + design +')', + 'backgroundRepeat': 'no-repeat', + 'backgroundPosition': 'top center', + 'background-size': '100% 100%' + + }); + // document.getElementById("phoneDiv").style.backgroundImage="url("+ design +")"; + }); + document.getElementById('remove-selected').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + canvas.remove(activeObject); + $("#text-string").val(""); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + canvas.remove(object); + }); + } + }; + document.getElementById('bring-to-front').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + activeObject.bringToFront(); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + object.bringToFront(); + }); + } + }; + document.getElementById('send-to-back').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + activeObject.sendToBack(); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + object.sendToBack(); + }); + } + }; + $("#text-bold").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontWeight = (activeObject.fontWeight == 'bold' ? '' : 'bold'); + canvas.renderAll(); + } + }); + $("#text-italic").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontStyle = (activeObject.fontStyle == 'italic' ? '' : 'italic'); + canvas.renderAll(); + } + }); + $("#text-strike").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textDecoration = (activeObject.textDecoration == 'line-through' ? '' : 'line-through'); + canvas.renderAll(); + } + }); + $("#text-underline").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textDecoration = (activeObject.textDecoration == 'underline' ? '' : 'underline'); + canvas.renderAll(); + } + }); + $("#text-left").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'left'; + canvas.renderAll(); + } + }); + $("#text-center").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'center'; + canvas.renderAll(); + } + }); + $("#text-right").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'right'; + canvas.renderAll(); + } + }); + $("#font-family").change(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontFamily = this.value; + canvas.renderAll(); + } + }); + $('#text-bgcolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.backgroundColor = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + $('#text-fontcolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fill = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + + $('#text-strokecolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.strokeStyle = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + + //canvas.add(new fabric.fabric.Object({hasBorders:true,hasControls:false,hasRotatingPoint:false,selectable:false,type:'rect'})); + $("#drawingArea").hover( + function() { + canvas.add(line1); + canvas.add(line2); + canvas.add(line3); + canvas.add(line4); + canvas.renderAll(); + }, + function() { + canvas.remove(line1); + canvas.remove(line2); + canvas.remove(line3); + canvas.remove(line4); + canvas.renderAll(); + } + ); + + $('.color-preview').click(function(){ + var color = $(this).css("background-color"); + document.getElementById("phoneDiv").style.backgroundColor = color; + }); + + $(".clearfix button,a").tooltip(); + });//doc ready + + + function getRandomNum(min, max) { + return Math.random() * (max - min) + min; + } + + function onObjectSelected(e) { + var selectedObject = e.target; + $("#text-string").val(""); + selectedObject.hasRotatingPoint = true + if (selectedObject && selectedObject.type === 'text') { + //display text editor + $("#texteditor").css('display', 'block'); + $("#text-string").val(selectedObject.getText()); + $('#text-fontcolor').miniColors('value',selectedObject.fill); + $('#text-strokecolor').miniColors('value',selectedObject.strokeStyle); + $("#imageeditor").css('display', 'block'); + } + else if (selectedObject && selectedObject.type === 'image'){ + //display image editor + $("#texteditor").css('display', 'none'); + $("#imageeditor").css('display', 'block'); + } + } + function onSelectedCleared(e){ + $("#texteditor").css('display', 'none'); + $("#text-string").val(""); + $("#imageeditor").css('display', 'none'); + } + function setFont(font){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontFamily = font; + canvas.renderAll(); + } + } + function removeWhite(){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'image') { + activeObject.filters[2] = new fabric.Image.filters.RemoveWhite({hreshold: 100, distance: 10});//0-255, 0-255 + activeObject.applyFilters(canvas.renderAll.bind(canvas)); + } + }
\ No newline at end of file diff --git a/js/excanvas.js b/js/excanvas.js new file mode 100644 index 0000000..367764b --- /dev/null +++ b/js/excanvas.js @@ -0,0 +1,924 @@ +// Copyright 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +// Known Issues: +// +// * Patterns are not implemented. +// * Radial gradient are not implemented. The VML version of these look very +// different from the canvas one. +// * Clipping paths are not implemented. +// * Coordsize. The width and height attribute have higher priority than the +// width and height style values which isn't correct. +// * Painting mode isn't implemented. +// * Canvas width/height should is using content-box by default. IE in +// Quirks mode will draw the canvas using border-box. Either change your +// doctype to HTML5 +// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) +// or use Box Sizing Behavior from WebFX +// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) +// * Non uniform scaling does not correctly scale strokes. +// * Optimize. There is always room for speed improvements. + +// Only add this code if we do not already have a canvas implementation +if (!document.createElement('canvas').getContext) { + +(function() { + + // alias some functions to make (compiled) code shorter + var m = Math; + var mr = m.round; + var ms = m.sin; + var mc = m.cos; + var abs = m.abs; + var sqrt = m.sqrt; + + // this is used for sub pixel precision + var Z = 10; + var Z2 = Z / 2; + + /** + * This funtion is assigned to the <canvas> elements as element.getContext(). + * @this {HTMLElement} + * @return {CanvasRenderingContext2D_} + */ + function getContext() { + return this.context_ || + (this.context_ = new CanvasRenderingContext2D_(this)); + } + + var slice = Array.prototype.slice; + + /** + * Binds a function to an object. The returned function will always use the + * passed in {@code obj} as {@code this}. + * + * Example: + * + * g = bind(f, obj, a, b) + * g(c, d) // will do f.call(obj, a, b, c, d) + * + * @param {Function} f The function to bind the object to + * @param {Object} obj The object that should act as this when the function + * is called + * @param {*} var_args Rest arguments that will be used as the initial + * arguments when the function is called + * @return {Function} A new function that has bound this + */ + function bind(f, obj, var_args) { + var a = slice.call(arguments, 2); + return function() { + return f.apply(obj, a.concat(slice.call(arguments))); + }; + } + + var G_vmlCanvasManager_ = { + init: function(opt_doc) { + if (/MSIE/.test(navigator.userAgent) && !window.opera) { + var doc = opt_doc || document; + // Create a dummy element so that IE will allow canvas elements to be + // recognized. + doc.createElement('canvas'); + doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); + } + }, + + init_: function(doc) { + // create xmlns + if (!doc.namespaces['g_vml_']) { + doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml', + '#default#VML'); + + } + if (!doc.namespaces['g_o_']) { + doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office', + '#default#VML'); + } + + // Setup default CSS. Only add one style sheet per document + if (!doc.styleSheets['ex_canvas_']) { + var ss = doc.createStyleSheet(); + ss.owningElement.id = 'ex_canvas_'; + ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + + // default size is 300x150 in Gecko and Opera + 'text-align:left;width:300px;height:150px}' + + 'g_vml_\\:*{behavior:url(#default#VML)}' + + 'g_o_\\:*{behavior:url(#default#VML)}'; + + } + + // find all canvas elements + var els = doc.getElementsByTagName('canvas'); + for (var i = 0; i < els.length; i++) { + this.initElement(els[i]); + } + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function(el) { + if (!el.getContext) { + + el.getContext = getContext; + + // Remove fallback content. There is no way to hide text nodes so we + // just remove all childNodes. We could hide all elements and remove + // text nodes but who really cares about the fallback content. + el.innerHTML = ''; + + // do not use inline function because that will leak memory + el.attachEvent('onpropertychange', onPropertyChange); + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + 'px'; + } else { + el.width = el.clientWidth; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + 'px'; + } else { + el.height = el.clientHeight; + } + //el.getContext().setCoordsize_() + } + return el; + } + }; + + function onPropertyChange(e) { + var el = e.srcElement; + + switch (e.propertyName) { + case 'width': + el.style.width = el.attributes.width.nodeValue + 'px'; + el.getContext().clearRect(); + break; + case 'height': + el.style.height = el.attributes.height.nodeValue + 'px'; + el.getContext().clearRect(); + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var dec2hex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + o2.globalAlpha = o1.globalAlpha; + o2.arcScaleX_ = o1.arcScaleX_; + o2.arcScaleY_ = o1.arcScaleY_; + o2.lineScale_ = o1.lineScale_; + } + + function processStyle(styleString) { + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.substring(0, 3) == 'rgb') { + var start = styleString.indexOf('(', 3); + var end = styleString.indexOf(')', start + 1); + var guts = styleString.substring(start + 1, end).split(','); + + str = '#'; + for (var i = 0; i < 3; i++) { + str += dec2hex[Number(guts[i])]; + } + + if (guts.length == 4 && styleString.substr(3, 1) == 'a') { + alpha = guts[3]; + } + } else { + str = styleString; + } + + return {color: str, alpha: alpha}; + } + + function processLineCap(lineCap) { + switch (lineCap) { + case 'butt': + return 'flat'; + case 'round': + return 'round'; + case 'square': + default: + return 'square'; + } + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} surfaceElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(surfaceElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = '#000'; + this.fillStyle = '#000'; + + this.lineWidth = 1; + this.lineJoin = 'miter'; + this.lineCap = 'butt'; + this.miterLimit = Z * 1; + this.globalAlpha = 1; + this.canvas = surfaceElement; + + var el = surfaceElement.ownerDocument.createElement('div'); + el.style.width = surfaceElement.clientWidth + 'px'; + el.style.height = surfaceElement.clientHeight + 'px'; + el.style.overflow = 'hidden'; + el.style.position = 'absolute'; + surfaceElement.appendChild(el); + + this.element_ = el; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + this.lineScale_ = 1; + } + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + this.element_.innerHTML = ''; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.lineTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); + + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + var p = this.getCoords_(aX, aY); + var cp1 = this.getCoords_(aCP1x, aCP1y); + var cp2 = this.getCoords_(aCP2x, aCP2y); + bezierCurveTo(this, cp1, cp2, p); + }; + + // Helper function that takes the already fixed cordinates. + function bezierCurveTo(self, cp1, cp2, p) { + self.currentPath_.push({ + type: 'bezierCurveTo', + cp1x: cp1.x, + cp1y: cp1.y, + cp2x: cp2.x, + cp2y: cp2.y, + x: p.x, + y: p.y + }); + self.currentX_ = p.x; + self.currentY_ = p.y; + } + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // the following is lifted almost directly from + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes + + var cp = this.getCoords_(aCPx, aCPy); + var p = this.getCoords_(aX, aY); + + var cp1 = { + x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), + y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) + }; + var cp2 = { + x: cp1.x + (p.x - this.currentX_) / 3.0, + y: cp1.y + (p.y - this.currentY_) / 3.0 + }; + + bezierCurveTo(this, cp1, cp2, p); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + aRadius *= Z; + var arcType = aClockwise ? 'at' : 'wa'; + + var xStart = aX + mc(aStartAngle) * aRadius - Z2; + var yStart = aY + ms(aStartAngle) * aRadius - Z2; + + var xEnd = aX + mc(aEndAngle) * aRadius - Z2; + var yEnd = aY + ms(aEndAngle) * aRadius - Z2; + + // IE won't render arches drawn counter clockwise if xStart == xEnd. + if (xStart == xEnd && !aClockwise) { + xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something + // that can be represented in binary + } + + var p = this.getCoords_(aX, aY); + var pStart = this.getCoords_(xStart, yStart); + var pEnd = this.getCoords_(xEnd, yEnd); + + this.currentPath_.push({type: arcType, + x: p.x, + y: p.y, + radius: aRadius, + xStart: pStart.x, + yStart: pStart.y, + xEnd: pEnd.x, + yEnd: pEnd.y}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_('gradient'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, aR0, + aX1, aY1, aR1) { + var gradient = new CanvasGradient_('gradientradial'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.r0_ = aR0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + gradient.r1_ = aR1; + return gradient; + }; + + contextPrototype.drawImage = function(image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + + // to find the original width we overide the width and height + var oldRuntimeWidth = image.runtimeStyle.width; + var oldRuntimeHeight = image.runtimeStyle.height; + image.runtimeStyle.width = 'auto'; + image.runtimeStyle.height = 'auto'; + + // get the original size + var w = image.width; + var h = image.height; + + // and remove overides + image.runtimeStyle.width = oldRuntimeWidth; + image.runtimeStyle.height = oldRuntimeHeight; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw Error('Invalid number of arguments'); + } + + var d = this.getCoords_(dx, dy); + + var w2 = sw / 2; + var h2 = sh / 2; + + var vmlStr = []; + + var W = 10; + var H = 10; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' <g_vml_:group', + ' coordsize="', Z * W, ',', Z * H, '"', + ' coordorigin="0,0"' , + ' style="width:', W, 'px;height:', H, 'px;position:absolute;'); + + // If filters are necessary (rotation exists), create them + // filters are bog-slow, so only create them if abbsolutely necessary + // The following check doesn't account for skews (which don't exist + // in the canvas spec (yet) anyway. + + if (this.m_[0][0] != 1 || this.m_[0][1]) { + var filter = []; + + // Note the 12/21 reversal + filter.push('M11=', this.m_[0][0], ',', + 'M12=', this.m_[1][0], ',', + 'M21=', this.m_[0][1], ',', + 'M22=', this.m_[1][1], ',', + 'Dx=', mr(d.x / Z), ',', + 'Dy=', mr(d.y / Z), ''); + + // Bounding box calculation (need to minimize displayed area so that + // filters don't waste time on unused pixels. + var max = d; + var c2 = this.getCoords_(dx + dw, dy); + var c3 = this.getCoords_(dx, dy + dh); + var c4 = this.getCoords_(dx + dw, dy + dh); + + max.x = m.max(max.x, c2.x, c3.x, c4.x); + max.y = m.max(max.y, c2.y, c3.y, c4.y); + + vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z), + 'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(', + filter.join(''), ", sizingmethod='clip');") + } else { + vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;'); + } + + vmlStr.push(' ">' , + '<g_vml_:image src="', image.src, '"', + ' style="width:', Z * dw, 'px;', + ' height:', Z * dh, 'px;"', + ' cropleft="', sx / w, '"', + ' croptop="', sy / h, '"', + ' cropright="', (w - sx - sw) / w, '"', + ' cropbottom="', (h - sy - sh) / h, '"', + ' />', + '</g_vml_:group>'); + + this.element_.insertAdjacentHTML('BeforeEnd', + vmlStr.join('')); + }; + + contextPrototype.stroke = function(aFill) { + var lineStr = []; + var lineOpen = false; + var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); + var color = a.color; + var opacity = a.alpha * this.globalAlpha; + + var W = 10; + var H = 10; + + lineStr.push('<g_vml_:shape', + ' filled="', !!aFill, '"', + ' style="position:absolute;width:', W, 'px;height:', H, 'px;"', + ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"', + ' stroked="', !aFill, '"', + ' path="'); + + var newSeq = false; + var min = {x: null, y: null}; + var max = {x: null, y: null}; + + for (var i = 0; i < this.currentPath_.length; i++) { + var p = this.currentPath_[i]; + var c; + + switch (p.type) { + case 'moveTo': + c = p; + lineStr.push(' m ', mr(p.x), ',', mr(p.y)); + break; + case 'lineTo': + lineStr.push(' l ', mr(p.x), ',', mr(p.y)); + break; + case 'close': + lineStr.push(' x '); + p = null; + break; + case 'bezierCurveTo': + lineStr.push(' c ', + mr(p.cp1x), ',', mr(p.cp1y), ',', + mr(p.cp2x), ',', mr(p.cp2y), ',', + mr(p.x), ',', mr(p.y)); + break; + case 'at': + case 'wa': + lineStr.push(' ', p.type, ' ', + mr(p.x - this.arcScaleX_ * p.radius), ',', + mr(p.y - this.arcScaleY_ * p.radius), ' ', + mr(p.x + this.arcScaleX_ * p.radius), ',', + mr(p.y + this.arcScaleY_ * p.radius), ' ', + mr(p.xStart), ',', mr(p.yStart), ' ', + mr(p.xEnd), ',', mr(p.yEnd)); + break; + } + + + // TODO: Following is broken for curves due to + // move to proper paths. + + // Figure out dimensions so we can do gradient fills + // properly + if (p) { + if (min.x == null || p.x < min.x) { + min.x = p.x; + } + if (max.x == null || p.x > max.x) { + max.x = p.x; + } + if (min.y == null || p.y < min.y) { + min.y = p.y; + } + if (max.y == null || p.y > max.y) { + max.y = p.y; + } + } + } + lineStr.push(' ">'); + + if (!aFill) { + var lineWidth = this.lineScale_ * this.lineWidth; + + // VML cannot correctly render a line if the width is less than 1px. + // In that case, we dilute the color to make the line look thinner. + if (lineWidth < 1) { + opacity *= lineWidth; + } + + lineStr.push( + '<g_vml_:stroke', + ' opacity="', opacity, '"', + ' joinstyle="', this.lineJoin, '"', + ' miterlimit="', this.miterLimit, '"', + ' endcap="', processLineCap(this.lineCap), '"', + ' weight="', lineWidth, 'px"', + ' color="', color, '" />' + ); + } else if (typeof this.fillStyle == 'object') { + var fillStyle = this.fillStyle; + var angle = 0; + var focus = {x: 0, y: 0}; + + // additional offset + var shift = 0; + // scale factor for offset + var expansion = 1; + + if (fillStyle.type_ == 'gradient') { + var x0 = fillStyle.x0_ / this.arcScaleX_; + var y0 = fillStyle.y0_ / this.arcScaleY_; + var x1 = fillStyle.x1_ / this.arcScaleX_; + var y1 = fillStyle.y1_ / this.arcScaleY_; + var p0 = this.getCoords_(x0, y0); + var p1 = this.getCoords_(x1, y1); + var dx = p1.x - p0.x; + var dy = p1.y - p0.y; + angle = Math.atan2(dx, dy) * 180 / Math.PI; + + // The angle should be a non-negative number. + if (angle < 0) { + angle += 360; + } + + // Very small angles produce an unexpected result because they are + // converted to a scientific notation string. + if (angle < 1e-6) { + angle = 0; + } + } else { + var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_); + var width = max.x - min.x; + var height = max.y - min.y; + focus = { + x: (p0.x - min.x) / width, + y: (p0.y - min.y) / height + }; + + width /= this.arcScaleX_ * Z; + height /= this.arcScaleY_ * Z; + var dimension = m.max(width, height); + shift = 2 * fillStyle.r0_ / dimension; + expansion = 2 * fillStyle.r1_ / dimension - shift; + } + + // We need to sort the color stops in ascending order by offset, + // otherwise IE won't interpret it correctly. + var stops = fillStyle.colors_; + stops.sort(function(cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + var length = stops.length; + var color1 = stops[0].color; + var color2 = stops[length - 1].color; + var opacity1 = stops[0].alpha * this.globalAlpha; + var opacity2 = stops[length - 1].alpha * this.globalAlpha; + + var colors = []; + for (var i = 0; i < length; i++) { + var stop = stops[i]; + colors.push(stop.offset * expansion + shift + ' ' + stop.color); + } + + // When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"', + ' method="none" focus="100%"', + ' color="', color1, '"', + ' color2="', color2, '"', + ' colors="', colors.join(','), '"', + ' opacity="', opacity2, '"', + ' g_o_:opacity2="', opacity1, '"', + ' angle="', angle, '"', + ' focusposition="', focus.x, ',', focus.y, '" />'); + } else { + lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, + '" />'); + } + + lineStr.push('</g_vml_:shape>'); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + }; + + contextPrototype.fill = function() { + this.stroke(true); + } + + contextPrototype.closePath = function() { + this.currentPath_.push({type: 'close'}); + }; + + /** + * @private + */ + contextPrototype.getCoords_ = function(aX, aY) { + var m = this.m_; + return { + x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, + y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 + } + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + }; + + function matrixIsFinite(m) { + for (var j = 0; j < 3; j++) { + for (var k = 0; k < 2; k++) { + if (!isFinite(m[j][k]) || isNaN(m[j][k])) { + return false; + } + } + } + return true; + } + + function setM(ctx, m, updateLineScale) { + if (!matrixIsFinite(m)) { + return; + } + ctx.m_ = m; + + if (updateLineScale) { + // Get the line scale. + // Determinant of this.m_ means how much the area is enlarged by the + // transformation. So its square root can be used as a scale factor + // for width. + var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; + ctx.lineScale_ = sqrt(abs(det)); + } + } + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.scale = function(aX, aY) { + this.arcScaleX_ *= aX; + this.arcScaleY_ *= aY; + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { + var m1 = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { + var m = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, m, true); + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function() { + return new CanvasPattern_; + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.x0_ = 0; + this.y0_ = 0; + this.r0_ = 0; + this.x1_ = 0; + this.y1_ = 0; + this.r1_ = 0; + this.colors_ = []; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: aOffset, + color: aColor.color, + alpha: aColor.alpha}); + }; + + function CanvasPattern_() {} + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + +})(); + +} // if diff --git a/js/fabric.js b/js/fabric.js new file mode 100644 index 0000000..e438f56 --- /dev/null +++ b/js/fabric.js @@ -0,0 +1,13962 @@ +/* build: `node build.js modules=ALL` */ +/*! Fabric.js Copyright 2008-2012, Printio (Juriy Zaytsev, Maxim Chernyak) */ + +var fabric = fabric || { version: "0.9.13" }; + +if (typeof exports != 'undefined') { + exports.fabric = fabric; +} + +if (typeof document != 'undefined' && typeof window != 'undefined') { + fabric.document = document; + fabric.window = window; +} +else { + // assume we're running under node.js when document/window are not present + fabric.document = require("jsdom").jsdom("<!DOCTYPE html><html><head></head><body></body></html>"); + fabric.window = fabric.document.createWindow(); +} + +/** + * True when in environment that supports touch events + * @property isTouchSupported + * @type boolean + */ +fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; + +/** + * True when in environment that's probably Node.js + * @property isLikelyNode + * @type boolean + */ +fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; +/*! + * Copyright (c) 2009 Simo Kinnunen. + * Licensed under the MIT license. + */ + +var Cufon = (function() { + + var api = function() { + return api.replace.apply(null, arguments); + }; + + var DOM = api.DOM = { + + ready: (function() { + + var complete = false, readyStatus = { loaded: 1, complete: 1 }; + + var queue = [], perform = function() { + if (complete) return; + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Gecko, Opera, WebKit r26101+ + + if (fabric.document.addEventListener) { + fabric.document.addEventListener('DOMContentLoaded', perform, false); + fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages + } + + // Old WebKit, Internet Explorer + + if (!fabric.window.opera && fabric.document.readyState) (function() { + readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); + })(); + + // Internet Explorer + + if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { + try { + fabric.document.body.doScroll('left'); + perform(); + } + catch (e) { + setTimeout(arguments.callee, 1); + } + })(); + + addEvent(fabric.window, 'load', perform); // Fallback + + return function(listener) { + if (!arguments.length) perform(); + else complete ? listener() : queue.push(listener); + }; + + })() + + }; + + var CSS = api.CSS = { + + Size: function(value, base) { + + this.value = parseFloat(value); + this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; + + this.convert = function(value) { + return value / base * this.value; + }; + + this.convertFrom = function(value) { + return value / this.value * base; + }; + + this.toString = function() { + return this.value + this.unit; + }; + + }, + + getStyle: function(el) { + return new Style(el.style); + /* + var view = document.defaultView; + if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); + if (el.currentStyle) return new Style(el.currentStyle); + return new Style(el.style); + */ + }, + + quotedList: cached(function(value) { + // doesn't work properly with empty quoted strings (""), but + // it's not worth the extra code. + var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; + while (match = re.exec(value)) list.push(match[3] || match[1]); + return list; + }), + + ready: (function() { + + var complete = false; + + var queue = [], perform = function() { + complete = true; + for (var fn; fn = queue.shift(); fn()); + }; + + // Safari 2 does not include <style> elements in document.styleSheets. + // Safari 2 also does not support Object.prototype.propertyIsEnumerable. + + var styleElements = Object.prototype.propertyIsEnumerable ? elementsByTagName('style') : { length: 0 }; + var linkElements = elementsByTagName('link'); + + DOM.ready(function() { + // These checks are actually only needed for WebKit-based browsers, but don't really hurt other browsers. + var linkStyles = 0, link; + for (var i = 0, l = linkElements.length; link = linkElements[i], i < l; ++i) { + // WebKit does not load alternate stylesheets. + if (!link.disabled && link.rel.toLowerCase() == 'stylesheet') ++linkStyles; + } + if (fabric.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 = fabric.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; + // we don't have to worry about "bolder" and "lighter" + // because IE's currentStyle returns a numeric value for it, + // and other browsers use the computed value anyway + weight = { + normal: 400, + bold: 700 + }[weight] || parseInt(weight, 10); + if (weights[weight]) return weights[weight]; + // http://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight + // Gecko uses x99/x01 for lighter/bolder + 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) { + // A timeout is needed so that the event can actually "happen" + // before replace is triggered. This ensures that styles are up + // to date. + 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, fabric.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 fabric.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 = fabric.document.createDocumentFragment(), processed; + var parts = text.split(separators[separate]), needsAligning = (separate == 'words'); + if (needsAligning && HAS_BROKEN_REGEXP) { + // @todo figure out a better way to do this + 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; + //for some reason, the carriage return is not stripped by IE but "\n" is, so let's keep \r as a new line marker... + if (typeof G_vmlCanvasManager != 'undefined') { + text = text.replace(/\r/g, "\n"); + } + 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 = { + engine: null, + //fontScale: 1, + //fontScaling: false, + hover: false, + hoverables: { + a: true + }, + printable: true, + //rotation: 0, + //selectable: false, + selector: ( + fabric.window.Sizzle + || (fabric.window.jQuery && function(query) { return jQuery(query); }) // avoid noConflict issues + || (fabric.window.dojo && dojo.query) + || (fabric.window.$$ && function(query) { return $$(query); }) + || (fabric.window.$ && function(query) { return $(query); }) + || (fabric.document.querySelectorAll && function(query) { return fabric.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() { + + // Safari 2 doesn't support .apply() on native methods + var HAS_INLINE_BLOCK = Cufon.CSS.supports('display', 'inline-block'); + + // Firefox 2 w/ non-strict doctype (almost standards mode) + var HAS_BROKEN_LINEHEIGHT = !HAS_INLINE_BLOCK && (fabric.document.compatMode == 'BackCompat' || /frameset|transitional/i.test(fabric.document.doctype.publicId)); + + var styleSheet = fabric.document.createElement('style'); + styleSheet.type = 'text/css'; + + var textNode = fabric.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}' + + '}' + ) + + try { + styleSheet.appendChild(textNode); + } catch(e) { + //IE8- can't do this... + styleSheet.setAttribute("type", "text/css"); + styleSheet.styleSheet.cssText = textNode.data; + } + fabric.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', a: [] }; + 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 = []; + + Cufon.textOptions.shadowOffsets = [ ]; + Cufon.textOptions.shadows = null; + + if (shadows) { + Cufon.textOptions.shadows = 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 = fabric.document.createElement('span'); + wrapper.className = 'cufon cufon-canvas'; + wrapper.alt = text; + + canvas = fabric.document.createElement('canvas'); + wrapper.appendChild(canvas); + + if (options.printable) { + var print = fabric.document.createElement('span'); + print.className = 'cufon-alt'; + print.appendChild(fabric.document.createTextNode(text)); + wrapper.appendChild(print); + } + } + + 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; + Cufon.textOptions.totalLineHeight = totalLineHeight; + + 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; + + Cufon.textOptions.fontAscent = font.ascent * scale; + Cufon.textOptions.boundaries = null; + + for (var offsets = Cufon.textOptions.shadowOffsets, i = shadowOffsets.length; i--; ) { + offsets[i] = [ shadowOffsets[i][0] * scale, shadowOffsets[i][1] * scale ]; + } + + g.save(); + g.scale(scale, scale); + + g.translate( + // we're at the center of an object and need to jump to the top left corner + // where first character is to be drawn + -expandLeft - ((1/scale * canvas.width) / 2) + (Cufon.fonts[font.family].offsetLeft || 0), + -expandTop - ((Cufon.textOptions.height / scale) / 2) + (Cufon.fonts[font.family].offsetTop || 0) + ); + + 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 = Cufon.getTextDecoration(options), + isItalic = options.fontStyle === 'italic'; + + function renderBackground() { + g.save(); + + g.fillStyle = options.backgroundColor; + + var left = 0, lineNum = 0, boundaries = [{ left: 0 }]; + + if (options.textAlign === 'right') { + g.translate(lineOffsets[lineNum], 0); + boundaries[0].left = lineOffsets[lineNum] * scale; + } + else if (options.textAlign === 'center') { + g.translate(lineOffsets[lineNum] / 2, 0); + boundaries[0].left = lineOffsets[lineNum] / 2 * scale; + } + + for (var i = 0, l = chars.length; i < l; ++i) { + if (chars[i] === '\n') { + + lineNum++; + + var topOffset = -font.ascent - ((font.ascent / 5) * options.lineHeight); + var boundary = boundaries[boundaries.length - 1]; + var nextBoundary = { left: 0 }; + + boundary.width = left * scale; + boundary.height = (-font.ascent + font.descent) * scale; + + if (options.textAlign === 'right') { + g.translate(-width, topOffset); + g.translate(lineOffsets[lineNum], 0); + nextBoundary.left = lineOffsets[lineNum] * scale; + } + else if (options.textAlign === 'center') { + // offset to the start of text in previous line AND half of its offset + // (essentially moving caret to the left edge of bounding box) + g.translate(-left - (lineOffsets[lineNum - 1] / 2), topOffset); + g.translate(lineOffsets[lineNum] / 2, 0); + nextBoundary.left = lineOffsets[lineNum] / 2 * scale; + } + else { + g.translate(-left, topOffset); + } + + /* push next boundary (for the next line) */ + boundaries.push(nextBoundary); + + left = 0; + + continue; + } + var glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (!glyph) continue; + + var charWidth = Number(glyph.w || font.w) + letterSpacing; + + // only draw background when there's some kind of value + if (options.backgroundColor) { + 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; + + if (i == l-1) { + boundaries[boundaries.length - 1].width = left * scale; + boundaries[boundaries.length - 1].height = (-font.ascent + font.descent) * scale; + } + } + g.restore(); + + Cufon.textOptions.boundaries = boundaries; + } + + function renderText(color) { + g.fillStyle = color || 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') { + // offset to the start of text in previous line AND half of its offset + // (essentially moving caret to the left edge of bounding box) + 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; + + // add 2x more thickness — closer to SVG rendering + g.lineWidth += g.lineWidth; + + g.beginPath(); + if (textDecoration.underline) { + g.moveTo(0, -font.face['underline-position'] + 0.5); + g.lineTo(charWidth, -font.face['underline-position'] + 0.5); + } + if (textDecoration.overline) { + g.moveTo(0, font.ascent + 0.5); + g.lineTo(charWidth, font.ascent + 0.5); + } + if (textDecoration['line-through']) { + g.moveTo(0, -font.descent + 0.5); + g.lineTo(charWidth, -font.descent + 0.5); + } + 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; + } + } + + g.save(); + renderBackground(); + if (shadows) { + for (var i = 0, l = shadows.length; i < l; ++i) { + var shadow = shadows[i]; + g.save(); + g.translate.apply(g, shadowOffsets[i]); + renderText(shadow.color); + g.restore(); + } + } + renderText(); + g.restore(); + g.restore(); + g.restore(); + + return wrapper; + + }; + +})()); + +Cufon.registerEngine('vml', (function() { + + if (!fabric.document.namespaces) return; + + var canvasEl = fabric.document.createElement('canvas'); + if (canvasEl && canvasEl.getContext && canvasEl.getContext.apply) return; + + if (fabric.document.namespaces.cvml == null) { + fabric.document.namespaces.add('cvml', 'urn:schemas-microsoft-com:vml'); + } + + var check = fabric.document.createElement('cvml:shape'); + check.style.behavior = 'url(#default#VML)'; + if (!check.coordsize) return; // VML isn't supported + check = null; + + fabric.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); + } + + // Original by Dead Edwards. + // Combined with getFontSizeInPixels it also works with relative units. + function getSizeInPixels(el, value) { + if (/px$/i.test(value)) return parseFloat(value); + var style = el.style.left, runtimeStyle = el.runtimeStyle.left; + el.runtimeStyle.left = el.currentStyle.left; + el.style.left = value; + var result = el.style.pixelLeft; + el.style.left = style; + el.runtimeStyle.left = runtimeStyle; + return result; + } + + return function(font, text, style, options, node, el, hasNext) { + var redraw = (text === null); + + if (redraw) text = node.alt; + + // @todo word-spacing, text-decoration + + var viewBox = font.viewBox; + + var size = style.computedFontSize || + (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); + + var letterSpacing = style.computedLSpacing; + + if (letterSpacing == undefined) { + letterSpacing = style.get('letterSpacing'); + style.computedLSpacing = letterSpacing = + (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); + } + + var wrapper, canvas; + + if (redraw) { + wrapper = node; + canvas = node.firstChild; + } + else { + wrapper = fabric.document.createElement('span'); + wrapper.className = 'cufon cufon-vml'; + wrapper.alt = text; + + canvas = fabric.document.createElement('span'); + canvas.className = 'cufon-vml-canvas'; + wrapper.appendChild(canvas); + + if (options.printable) { + var print = fabric.document.createElement('span'); + print.className = 'cufon-alt'; + print.appendChild(fabric.document.createTextNode(text)); + wrapper.appendChild(print); + } + + // ie6, for some reason, has trouble rendering the last VML element in the document. + // we can work around this by injecting a dummy element where needed. + // @todo find a better solution + if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); + } + + var wStyle = wrapper.style; + var cStyle = canvas.style; + + var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); + var roundingFactor = roundedHeight / height; + var minX = viewBox.minX, minY = viewBox.minY; + + cStyle.height = roundedHeight; + cStyle.top = Math.round(size.convert(minY - font.ascent)); + cStyle.left = Math.round(size.convert(minX)); + + wStyle.height = size.convert(font.height) + 'px'; + + var textDecoration = Cufon.getTextDecoration(options); + + var color = style.get('color'); + + var chars = Cufon.CSS.textTransform(text, style).split(''); + + var width = 0, offsetX = 0, advance = null; + + var glyph, shape, shadows = options.textShadow; + + // pre-calculate width + for (var i = 0, k = 0, l = chars.length; i < l; ++i) { + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; + } + + if (advance === null) return null; + + var fullWidth = -minX + width + (viewBox.width - advance); + + var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); + + var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; + var stretch = 'r' + coordSize + 'nsnf'; + + for (i = 0; i < l; ++i) { + + glyph = font.glyphs[chars[i]] || font.missingGlyph; + if (!glyph) continue; + + if (redraw) { + // some glyphs may be missing so we can't use i + shape = canvas.childNodes[k]; + if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow + } + else { + shape = fabric.document.createElement('cvml:shape'); + canvas.appendChild(shape); + } + + shape.stroked = 'f'; + shape.coordsize = coordSize; + shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; + shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; + shape.fillcolor = color; + + // it's important to not set top/left or IE8 will grind to a halt + var sStyle = shape.style; + sStyle.width = roundedShapeWidth; + sStyle.height = roundedHeight; + + if (shadows) { + // due to the limitations of the VML shadow element there + // can only be two visible shadows. opacity is shared + // for all shadows. + var shadow1 = shadows[0], shadow2 = shadows[1]; + var color1 = Cufon.CSS.color(shadow1.color), color2; + var shadow = fabric.document.createElement('cvml:shadow'); + shadow.on = 't'; + shadow.color = color1.color; + shadow.offset = shadow1.offX + ',' + shadow1.offY; + if (shadow2) { + color2 = Cufon.CSS.color(shadow2.color); + shadow.type = 'double'; + shadow.color2 = color2.color; + shadow.offset2 = shadow2.offX + ',' + shadow2.offY; + } + shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; + shape.appendChild(shadow); + } + + offsetX += ~~(glyph.w || font.w) + letterSpacing; + + ++k; + + } + + wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); + + return wrapper; + + }; + +})()); + +Cufon.getTextDecoration = function(options) { + return { + underline: options.textDecoration === 'underline', + overline: options.textDecoration === 'overline', + 'line-through': options.textDecoration === 'line-through' + }; +}; + +if (typeof exports != 'undefined') { + exports.Cufon = Cufon; +} + +/* + json2.js + 2011-10-19 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, regexp: true */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +var JSON; +if (!JSON) { + JSON = {}; +} + +(function () { + 'use strict'; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) + ? this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' + : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' + ? c + : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? '[]' + : gap + ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' + : '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? '{}' + : gap + ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' + : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' + ? walk({'': j}, '') + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); +/** + * Wrapper around `console.log` (when available) + * @method log + * @param {Any} Values to log + */ +fabric.log = function() { }; + +/** + * Wrapper around `console.warn` (when available) + * @method warn + * @param {Any} Values to log as a warning + */ +fabric.warn = function() { }; + +if (typeof console !== 'undefined') { + if (typeof console.log !== 'undefined' && console.log.apply) { + fabric.log = function() { + return console.log.apply(console, arguments); + }; + } + if (typeof console.warn !== 'undefined' && console.warn.apply) { + fabric.warn = function() { + return console.warn.apply(console, arguments); + }; + } +} + +/** + * @namespace + */ +fabric.Observable = { + + /** + * Observes specified event + * @method observe + * @depracated Since 0.8.34. Use `on` instead. + * @param {String} eventName + * @param {Function} handler + */ + observe: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + // one object with key/value pairs was passed + if (arguments.length === 1) { + for (var prop in eventName) { + this.on(prop, eventName[prop]); + } + } + else { + if (!this.__eventListeners[eventName]) { + this.__eventListeners[eventName] = [ ]; + } + this.__eventListeners[eventName].push(handler); + } + }, + + /** + * Stops event observing for a particular event handler + * @method stopObserving + * @depracated Since 0.8.34. Use `off` instead. + * @param {String} eventName + * @param {Function} handler + */ + stopObserving: function(eventName, handler) { + if (!this.__eventListeners) { + this.__eventListeners = { }; + } + if (this.__eventListeners[eventName]) { + fabric.util.removeFromArray(this.__eventListeners[eventName], handler); + } + }, + + /** + * Fires event with an optional options object + * @method fire + * @param {String} eventName + * @param {Object} [options] + */ + fire: function(eventName, options) { + if (!this.__eventListeners) { + this.__eventListeners = { } + } + var listenersForEvent = this.__eventListeners[eventName]; + if (!listenersForEvent) return; + for (var i = 0, len = listenersForEvent.length; i < len; i++) { + // avoiding try/catch for perf. reasons + listenersForEvent[i](options || { }); + } + } +}; + +/** + * Alias for observe + * @method observe + * @memberOf fabric.Observable + */ +fabric.Observable.on = fabric.Observable.observe; + +/** + * Alias for stopObserving + * @method off + */ +fabric.Observable.off = fabric.Observable.stopObserving; +(function() { + + /** + * @namespace + */ + fabric.util = { }; + + /** + * Removes value from an array. + * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` + * @static + * @memberOf fabric.util + * @method removeFromArray + * @param {Array} array + * @param {Any} value + * @return {Array} original array + */ + function removeFromArray(array, value) { + var idx = array.indexOf(value); + if (idx !== -1) { + array.splice(idx, 1); + } + return array; + }; + + /** + * Returns random number between 2 specified ones. + * @static + * @method getRandomInt + * @memberOf fabric.util + * @param {Number} min lower limit + * @param {Number} max upper limit + * @return {Number} random value (between min and max) + */ + function getRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + var PiBy180 = Math.PI / 180; + + /** + * Transforms degrees to radians. + * @static + * @method degreesToRadians + * @memberOf fabric.util + * @param {Number} degrees value in degrees + * @return {Number} value in radians + */ + function degreesToRadians(degrees) { + return degrees * PiBy180; + } + + /** + * A wrapper around Number#toFixed, which contrary to native method returns number, not string. + * @static + * @method toFixed + * @memberOf fabric.util + * @param {Number | String} number number to operate on + * @param {Number} fractionDigits number of fraction digits to "leave" + * @return {Number} + */ + function toFixed(number, fractionDigits) { + return parseFloat(Number(number).toFixed(fractionDigits)); + } + + /** + * Function which always returns `false`. + * @static + * @method falseFunction + * @memberOf fabric.util + * @return {Boolean} + */ + function falseFunction() { + return false; + } + + /** + * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. + * @method animate + * @memberOf fabric.util + * @param {Object} [options] Animation options + * @param {Function} [options.onChange] Callback; invoked on every value change + * @param {Function} [options.onComplete] Callback; invoked when value change is completed + * @param {Number} [options.startValue=0] Starting value + * @param {Number} [options.endValue=100] Ending value + * @param {Number} [options.byValue=100] Value to modify the property by + * @param {Function} [options.easing] Easing function + * @param {Number} [options.duration=500] Duration of change + */ + function animate(options) { + + options || (options = { }); + + var start = +new Date(), + duration = options.duration || 500, + finish = start + duration, time, pos, + onChange = options.onChange || function() { }, + abort = options.abort || function() { return false; }, + easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t/d * (Math.PI/2)) + c + b;}, + startValue = 'startValue' in options ? options.startValue : 0, + endValue = 'endValue' in options ? options.endValue : 100; + byValue = options.byValue || endValue - startValue; + + options.onStart && options.onStart(); + + (function tick() { + time = +new Date(); + currentTime = time > finish ? duration : (time - start); + onChange(easing(currentTime, startValue, byValue, duration)); + if (time > finish || abort()) { + options.onComplete && options.onComplete(); + return; + } + requestAnimFrame(tick); + })(); + } + + var _requestAnimFrame = fabric.window.requestAnimationFrame || + fabric.window.webkitRequestAnimationFrame || + fabric.window.mozRequestAnimationFrame || + fabric.window.oRequestAnimationFrame || + fabric.window.msRequestAnimationFrame || + function(callback, element) { + fabric.window.setTimeout(callback, 1000 / 60); + }; + /** + * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * @method requestAnimFrame + * @memberOf fabric.util + * @param {Function} callback Callback to invoke + * @param {DOMElement} element optional Element to associate with animation + */ + var requestAnimFrame = function() { + return _requestAnimFrame.apply(fabric.window, arguments); + }; + + /** + * Loads image element from given url and passes it to a callback + * @method loadImage + * @memberOf fabric.util + * @param {String} url URL representing an image + * @param {Function} callback Callback; invoked with loaded image + * @param {Any} context optional Context to invoke callback in + */ + function loadImage(url, callback, context) { + if (url) { + var img = new Image(); + /** @ignore */ + img.onload = function () { + callback && callback.call(context, img); + img = img.onload = null; + }; + img.src = url; + } + else { + callback && callback.call(context, url); + } + } + + function enlivenObjects(objects, callback) { + + function getKlass(type) { + return fabric[fabric.util.string.camelize(fabric.util.string.capitalize(type))]; + } + + function onLoaded() { + if (++numLoadedObjects === numTotalObjects) { + if (callback) { + callback(enlivenedObjects); + } + } + } + + var enlivenedObjects = [ ], + numLoadedObjects = 0, + numTotalObjects = objects.length; + + objects.forEach(function (o, index) { + if (!o.type) { + return; + } + var klass = getKlass(o.type); + if (klass.async) { + klass.fromObject(o, function (o) { + enlivenedObjects[index] = o; + onLoaded(); + }); + } + else { + enlivenedObjects[index] = klass.fromObject(o); + onLoaded(); + } + }); + } + + function groupSVGElements(elements, options, path) { + var object = elements.length > 1 + ? new fabric.PathGroup(elements, options) + : elements[0]; + + if (typeof path !== 'undefined') { + object.setSourcePath(path); + } + return object; + } + + fabric.util.removeFromArray = removeFromArray; + fabric.util.degreesToRadians = degreesToRadians; + fabric.util.toFixed = toFixed; + fabric.util.getRandomInt = getRandomInt; + fabric.util.falseFunction = falseFunction; + fabric.util.animate = animate; + fabric.util.requestAnimFrame = requestAnimFrame; + fabric.util.loadImage = loadImage; + fabric.util.enlivenObjects = enlivenObjects; + fabric.util.groupSVGElements = groupSVGElements; +})(); +(function() { + + var slice = Array.prototype.slice; + + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + var t = Object(this), len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } + else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } + } + + if (!Array.prototype.forEach) { + Array.prototype.forEach = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + fn.call(context, this[i], i, this); + } + } + }; + } + + if (!Array.prototype.map) { + Array.prototype.map = function(fn, context) { + var result = [ ]; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + result[i] = fn.call(context, this[i], i, this); + } + } + return result; + }; + } + + if (!Array.prototype.every) { + Array.prototype.every = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && !fn.call(context, this[i], i, this)) { + return false; + } + } + return true; + }; + } + + if (!Array.prototype.some) { + Array.prototype.some = function(fn, context) { + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this && fn.call(context, this[i], i, this)) { + return true; + } + } + return false; + }; + } + + if (!Array.prototype.filter) { + Array.prototype.filter = function(fn, context) { + var result = [ ], val; + for (var i = 0, len = this.length >>> 0; i < len; i++) { + if (i in this) { + val = this[i]; // in case fn mutates this + if (fn.call(context, val, i, this)) { + result.push(val); + } + } + } + return result; + }; + } + + if (!Array.prototype.reduce) { + Array.prototype.reduce = function(fn /*, initial*/) { + var len = this.length >>> 0, + i = 0, + rv; + + if (arguments.length > 1) { + rv = arguments[1]; + } + else { + do { + if (i in this) { + rv = this[i++]; + break; + } + // if array contains no values, no initial value to return + if (++i >= len) { + throw new TypeError(); + } + } + while (true); + } + for (; i < len; i++) { + if (i in this) { + rv = fn.call(null, rv, this[i], i, this); + } + } + return rv; + }; + } + + /** + * Invokes method on all items in a given array + * @method invoke + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} method Name of a method to invoke + */ + function invoke(array, method) { + var args = slice.call(arguments, 2), result = [ ]; + for (var i = 0, len = array.length; i < len; i++) { + result[i] = args.length ? array[i][method].apply(array[i], args) : array[i][method].call(array[i]); + } + return result; + } + + /** + * Finds maximum value in array (not necessarily "first" one) + * @method max + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function max(array, byProperty) { + if (!array || array.length === 0) return undefined; + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + if (byProperty) { + while (i--) { + if (array[i][byProperty] >= result) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (array[i] >= result) { + result = array[i]; + } + } + } + return result; + } + + /** + * Finds minimum value in array (not necessarily "first" one) + * @method min + * @memberOf fabric.util.array + * @param {Array} array Array to iterate over + * @param {String} byProperty + */ + function min(array, byProperty) { + if (!array || array.length === 0) return undefined; + + var i = array.length - 1, + result = byProperty ? array[i][byProperty] : array[i]; + + if (byProperty) { + while (i--) { + if (array[i][byProperty] < result) { + result = array[i][byProperty]; + } + } + } + else { + while (i--) { + if (array[i] < result) { + result = array[i]; + } + } + } + return result; + } + + /** @namespace */ + fabric.util.array = { + invoke: invoke, + min: min, + max: max + }; + +})(); +(function(){ + + /** + * Copies all enumerable properties of one object to another + * @memberOf fabric.util.object + * @method extend + * @param {Object} destination Where to copy to + * @param {Object} source Where to copy from + */ + function extend(destination, source) { + // JScript DontEnum bug is not taken care of + for (var property in source) { + destination[property] = source[property]; + } + return destination; + } + + /** + * Creates an empty object and copies all enumerable properties of another object to it + * @method clone + * @memberOf fabric.util.object + * @param {Object} object Object to clone + */ + function clone(object) { + return extend({ }, object); + } + + /** @namespace fabric.util.object */ + fabric.util.object = { + extend: extend, + clone: clone + }; + +})(); +(function() { + +if (!String.prototype.trim) { + /** + * Trims a string (removing whitespace from the beginning and the end) + * @method trim + * @see <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/Trim">String#trim on MDN</a> + */ + String.prototype.trim = function () { + // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now + return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); + }; +} + +/** + * Camelizes a string + * @memberOf fabric.util.string + * @method camelize + * @param {String} string String to camelize + * @return {String} Camelized version of a string + */ +function camelize(string) { + return string.replace(/-+(.)?/g, function(match, character) { + return character ? character.toUpperCase() : ''; + }); +} + +/** + * Capitalizes a string + * @memberOf fabric.util.string + * @method capitalize + * @param {String} string String to capitalize + * @return {String} Capitalized version of a string + */ +function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +} + +function escapeXml(string) { + return string.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +/** @namespace */ +fabric.util.string = { + camelize: camelize, + capitalize: capitalize, + escapeXml: escapeXml +}; +}()); + +(function() { + + var slice = Array.prototype.slice, + apply = Function.prototype.apply, + dummy = function() { }; + + if (!Function.prototype.bind) { + /** + * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) + * @see <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), bound; + if (args.length) { + bound = function() { + return apply.call(fn, this instanceof dummy ? this : thisArg, args.concat(slice.call(arguments))); + }; + } + else { + bound = function() { + return apply.call(fn, this instanceof dummy ? this : thisArg, arguments); + }; + } + dummy.prototype = this.prototype; + bound.prototype = new dummy; + + return bound; + }; + } + +})(); +(function() { + + var slice = Array.prototype.slice, emptyFunction = function() { }; + + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; + } + return true; + })(); + + /** @ignore */ + var addMethods = function(klass, source, parent) { + for (var property in source) { + + if (property in klass.prototype && typeof klass.prototype[property] == 'function') { + + klass.prototype[property] = (function(property) { + return function() { + + var superclass = this.constructor.superclass; + this.constructor.superclass = parent; + var returnValue = source[property].apply(this, arguments); + this.constructor.superclass = superclass; + + if (property !== 'initialize') { + return returnValue; + } + } + })(property); + } + else { + klass.prototype[property] = source[property]; + } + + if (IS_DONTENUM_BUGGY) { + if (source.toString !== Object.prototype.toString) { + klass.prototype.toString = source.toString; + } + if (source.valueOf !== Object.prototype.valueOf) { + klass.prototype.valueOf = source.valueOf; + } + } + } + }; + + function subclass() { }; + + /** + * Helper for creation of "classes" + * @method createClass + * @memberOf fabric.util + */ + function createClass() { + var parent = null, + properties = slice.call(arguments, 0); + + if (typeof properties[0] === 'function') { + parent = properties.shift(); + } + function klass() { + this.initialize.apply(this, arguments); + } + + klass.superclass = parent; + klass.subclasses = [ ]; + + if (parent) { + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + for (var i = 0, length = properties.length; i < length; i++) { + addMethods(klass, properties[i], parent); + } + if (!klass.prototype.initialize) { + klass.prototype.initialize = emptyFunction; + } + klass.prototype.constructor = klass; + return klass; + } + + fabric.util.createClass = createClass; +})(); +(function (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 fabric.document.documentElement.uniqueID !== 'undefined') { + return function (element) { + return element.uniqueID; + }; + } + var uid = 0; + return function (element) { + return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); + }; + })(); + + /** @ignore */ + var getElement, setElement; + + (function () { + var elements = { }; + /** @ignore */ + getElement = function (uid) { + return elements[uid]; + }; + /** @ignore */ + setElement = function (uid, element) { + elements[uid] = element; + }; + })(); + + function createListener(uid, handler) { + return { + handler: handler, + wrappedHandler: createWrappedHandler(uid, handler) + }; + } + + function createWrappedHandler(uid, handler) { + return function (e) { + handler.call(getElement(uid), e || fabric.window.event); + }; + } + + function createDispatcher(uid, eventName) { + return function (e) { + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + handlersForEvent[i].call(this, e || fabric.window.event); + } + } + }; + } + + var shouldUseAddListenerRemoveListener = ( + areHostMethods(fabric.document.documentElement, 'addEventListener', 'removeEventListener') && + areHostMethods(fabric.window, 'addEventListener', 'removeEventListener')), + + shouldUseAttachEventDetachEvent = ( + areHostMethods(fabric.document.documentElement, 'attachEvent', 'detachEvent') && + areHostMethods(fabric.window, 'attachEvent', 'detachEvent')), + + // IE branch + listeners = { }, + + // DOM L0 branch + handlers = { }, + + addListener, removeListener; + + if (shouldUseAddListenerRemoveListener) { + /** @ignore */ + addListener = function (element, eventName, handler) { + element.addEventListener(eventName, handler, false); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + element.removeEventListener(eventName, handler, false); + }; + } + + else if (shouldUseAttachEventDetachEvent) { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + setElement(uid, element); + if (!listeners[uid]) { + listeners[uid] = { }; + } + if (!listeners[uid][eventName]) { + listeners[uid][eventName] = [ ]; + + } + var listener = createListener(uid, handler); + listeners[uid][eventName].push(listener); + element.attachEvent('on' + eventName, listener.wrappedHandler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element), listener; + if (listeners[uid] && listeners[uid][eventName]) { + for (var i = 0, len = listeners[uid][eventName].length; i < len; i++) { + listener = listeners[uid][eventName][i]; + if (listener && listener.handler === handler) { + element.detachEvent('on' + eventName, listener.wrappedHandler); + listeners[uid][eventName][i] = null; + } + } + } + }; + } + else { + /** @ignore */ + addListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (!handlers[uid]) { + handlers[uid] = { }; + } + if (!handlers[uid][eventName]) { + handlers[uid][eventName] = [ ]; + var existingHandler = element['on' + eventName]; + if (existingHandler) { + handlers[uid][eventName].push(existingHandler); + } + element['on' + eventName] = createDispatcher(uid, eventName); + } + handlers[uid][eventName].push(handler); + }; + /** @ignore */ + removeListener = function (element, eventName, handler) { + var uid = getUniqueId(element); + if (handlers[uid] && handlers[uid][eventName]) { + var handlersForEvent = handlers[uid][eventName]; + for (var i = 0, len = handlersForEvent.length; i < len; i++) { + if (handlersForEvent[i] === handler) { + handlersForEvent.splice(i, 1); + } + } + } + }; + } + + /** + * Adds an event listener to an element + * @mthod addListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.addListener = addListener; + + /** + * Removes an event listener from an element + * @mthod removeListener + * @memberOf fabric.util + * @function + * @param {HTMLElement} element + * @param {String} eventName + * @param {Function} handler + */ + fabric.util.removeListener = removeListener; + + /** + * Cross-browser wrapper for getting event's coordinates + * @method getPointer + * @memberOf fabric.util + * @param {Event} event + */ + function getPointer(event) { + // TODO (kangax): this method needs fixing + return { x: pointerX(event), y: pointerY(event) }; + } + + function pointerX(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollLeft: 0 }; + + // looks like in IE (<9) clientX at certain point (apparently when mouseup fires on VML element) + // is represented as COM object, with all the consequences, like "unknown" type and error on [[Get]] + // need to investigate later + return event.pageX || ((typeof event.clientX != 'unknown' ? event.clientX : 0) + + (docElement.scrollLeft || body.scrollLeft) - + (docElement.clientLeft || 0)); + } + + function pointerY(event) { + var docElement = fabric.document.documentElement, + body = fabric.document.body || { scrollTop: 0 }; + + return event.pageY || ((typeof event.clientY != 'unknown' ? event.clientY : 0) + + (docElement.scrollTop || body.scrollTop) - + (docElement.clientTop || 0)); + } + + if (fabric.isTouchSupported) { + pointerX = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageX || event.clientX; + }; + pointerY = function(event) { + return event.touches && event.touches[0] && event.touches[0].pageY || event.clientY; + }; + } + + fabric.util.getPointer = getPointer; + + fabric.util.object.extend(fabric.util, fabric.Observable); + +})(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 (!elementStyle) { + return element; + } + if (typeof styles === 'string') { + element.style.cssText += ';' + styles; + return styles.indexOf('opacity') > -1 + ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) + : element; + } + for (var property in styles) { + if (property === 'opacity') { + setOpacity(element, styles[property]); + } + else { + var normalizedProperty = (property === 'float' || property === 'cssFloat') + ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') + : property; + elementStyle[normalizedProperty] = styles[property]; + } + } + return element; + } + + var parseEl = fabric.document.createElement('div'), + supportsOpacity = typeof parseEl.style.opacity === 'string', + supportsFilters = typeof parseEl.style.filter === 'string', + view = fabric.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; + +})(); +(function() { + + var _slice = Array.prototype.slice; + + /** + * Takes id and returns an element with that id (if one exists in a document) + * @method getById + * @memberOf fabric.util + * @param {String|HTMLElement} id + * @return {HTMLElement|null} + */ + function getById(id) { + return typeof id === 'string' ? fabric.document.getElementById(id) : id; + } + + /** + * Converts an array-like object (e.g. arguments or NodeList) to an array + * @method toArray + * @memberOf fabric.util + * @param {Object} arrayLike + * @return {Array} + */ + function toArray(arrayLike) { + return _slice.call(arrayLike, 0); + } + + try { + var sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; + } + catch(err) { } + + if (!sliceCanConvertNodelists) { + toArray = function(arrayLike) { + var arr = new Array(arrayLike.length), i = arrayLike.length; + while (i--) { + arr[i] = arrayLike[i]; + } + return arr; + }; + } + + /** + * Creates specified element with specified attributes + * @method makeElement + * @memberOf fabric.util + * @param {String} tagName Type of an element to create + * @param {Object} [attributes] Attributes to set on an element + * @return {HTMLElement} Newly created element + */ + function makeElement(tagName, attributes) { + var el = fabric.document.createElement(tagName); + for (var prop in attributes) { + if (prop === 'class') { + el.className = attributes[prop]; + } + else if (prop === 'for') { + el.htmlFor = attributes[prop]; + } + else { + el.setAttribute(prop, attributes[prop]); + } + } + return el; + } + + /** + * Adds class to an element + * @method addClass + * @memberOf fabric.util + * @param {HTMLElement} element Element to add class to + * @param {String} className Class to add to an element + */ + function addClass(element, className) { + if ((' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { + element.className += (element.className ? ' ' : '') + className; + } + } + + /** + * Wraps element with another element + * @method wrapElement + * @memberOf fabric.util + * @param {HTMLElement} element Element to wrap + * @param {HTMLElement|String} wrapper Element to wrap with + * @param {Object} [attributes] Attributes to set on a wrapper + * @return {HTMLElement} wrapper + */ + function wrapElement(element, wrapper, attributes) { + if (typeof wrapper === 'string') { + wrapper = makeElement(wrapper, attributes); + } + if (element.parentNode) { + element.parentNode.replaceChild(wrapper, element); + } + wrapper.appendChild(element); + return wrapper; + } + + /** + * Returns offset for a given element + * @method getElementOffset + * @function + * @memberOf fabric.util + * @param {HTMLElement} element Element to get offset for + * @return {Object} Object with "left" and "top" properties + */ + function getElementOffset(element) { + // TODO (kangax): need to fix this method + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } + while (element); + return ({ left: valueL, top: valueT }); + } + + (function () { + var style = fabric.document.documentElement.style; + + var selectProp = 'userSelect' in style + ? 'userSelect' + : 'MozUserSelect' in style + ? 'MozUserSelect' + : 'WebkitUserSelect' in style + ? 'WebkitUserSelect' + : 'KhtmlUserSelect' in style + ? 'KhtmlUserSelect' + : ''; + + /** + * Makes element unselectable + * @method makeElementUnselectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make unselectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementUnselectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = fabric.util.falseFunction; + } + if (selectProp) { + element.style[selectProp] = 'none'; + } + else if (typeof element.unselectable == 'string') { + element.unselectable = 'on'; + } + return element; + } + + /** + * Makes element selectable + * @method makeElementSelectable + * @memberOf fabric.util + * @param {HTMLElement} element Element to make selectable + * @return {HTMLElement} Element that was passed in + */ + function makeElementSelectable(element) { + if (typeof element.onselectstart !== 'undefined') { + element.onselectstart = null; + } + if (selectProp) { + element.style[selectProp] = ''; + } + else if (typeof element.unselectable == 'string') { + element.unselectable = ''; + } + return element; + } + + fabric.util.makeElementUnselectable = makeElementUnselectable; + fabric.util.makeElementSelectable = makeElementSelectable; + })(); + + (function() { + + /** + * Inserts a script element with a given url into a document; invokes callback, when that script is finished loading + * @method getScript + * @memberOf fabric.util + * @param {String} url URL of a script to load + * @param {Function} callback Callback to execute when script is finished loading + */ + function getScript(url, callback) { + var headEl = fabric.document.getElementsByTagName("head")[0], + scriptEl = fabric.document.createElement('script'), + loading = true; + + scriptEl.type = 'text/javascript'; + scriptEl.setAttribute('runat', 'server'); + + /** @ignore */ + scriptEl.onload = /** @ignore */ scriptEl.onreadystatechange = function(e) { + if (loading) { + if (typeof this.readyState == 'string' && + this.readyState !== 'loaded' && + this.readyState !== 'complete') return; + loading = false; + callback(e || fabric.window.event); + scriptEl = scriptEl.onload = scriptEl.onreadystatechange = null; + } + }; + scriptEl.src = url; + headEl.appendChild(scriptEl); + // causes issue in Opera + // headEl.removeChild(scriptEl); + } + + fabric.util.getScript = getScript; + })(); + + fabric.util.getById = getById; + fabric.util.toArray = toArray; + fabric.util.makeElement = makeElement; + fabric.util.addClass = addClass; + fabric.util.wrapElement = wrapElement; + fabric.util.getElementOffset = getElementOffset; + +})(); +(function(){ + + function addParamToUrl(url, param) { + return url + (/\?/.test(url) ? '&' : '?') + param; + } + + var makeXHR = (function() { + var factories = [ + function() { return new ActiveXObject("Microsoft.XMLHTTP"); }, + function() { return new ActiveXObject("Msxml2.XMLHTTP"); }, + function() { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }, + function() { return new XMLHttpRequest(); } + ]; + for (var i = factories.length; i--; ) { + try { + var req = factories[i](); + if (req) { + return factories[i]; + } + } + catch (err) { } + } + })(); + + function emptyFn() { }; + + /** + * Cross-browser abstraction for sending XMLHttpRequest + * @method request + * @memberOf fabric.util + * @param {String} url URL to send XMLHttpRequest to + * @param {Object} [options] Options object + * @param {String} [options.method="GET"] + * @param {Function} options.onComplete Callback to invoke when request is completed + * @return {XMLHttpRequest} request + */ + function request(url, options) { + + options || (options = { }); + + var method = options.method ? options.method.toUpperCase() : 'GET', + onComplete = options.onComplete || function() { }, + 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; +})(); +(function() { + + /** + * @method easeInQuad + * @memberOf fabric.util.ease + */ + function easeInQuad(t, b, c, d) { + return c*(t/=d)*t + b; + } + + /** + * @method easeOutQuad + * @memberOf fabric.util.ease + */ + function easeOutQuad(t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } + + /** + * @method easeInOutQuad + * @memberOf fabric.util.ease + */ + function easeInOutQuad(t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + } + + /** + * @method easeInCubic + * @memberOf fabric.util.ease + */ + function easeInCubic(t, b, c, d) { + return c*(t/=d)*t*t + b; + } + + /** + * @method easeOutCubic + * @memberOf fabric.util.ease + */ + function easeOutCubic(t, b, c, d) { + return c*((t=t/d-1)*t*t + 1) + b; + } + + /** + * @method easeInOutCubic + * @memberOf fabric.util.ease + */ + function easeInOutCubic(t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t + b; + return c/2*((t-=2)*t*t + 2) + b; + } + + /** + * @method easeInQuart + * @memberOf fabric.util.ease + */ + function easeInQuart(t, b, c, d) { + return c*(t/=d)*t*t*t + b; + } + + /** + * @method easeOutQuart + * @memberOf fabric.util.ease + */ + function easeOutQuart(t, b, c, d) { + return -c * ((t=t/d-1)*t*t*t - 1) + b; + } + + /** + * @method easeInOutQuart + * @memberOf fabric.util.ease + */ + function easeInOutQuart(t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t*t + b; + return -c/2 * ((t-=2)*t*t*t - 2) + b; + } + + /** + * @method easeInQuint + * @memberOf fabric.util.ease + */ + function easeInQuint(t, b, c, d) { + return c*(t/=d)*t*t*t*t + b; + } + + /** + * @method easeOutQuint + * @memberOf fabric.util.ease + */ + function easeOutQuint(t, b, c, d) { + return c*((t=t/d-1)*t*t*t*t + 1) + b; + } + + /** + * @method easeInOutQuint + * @memberOf fabric.util.ease + */ + function easeInOutQuint(t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; + return c/2*((t-=2)*t*t*t*t + 2) + b; + } + + /** + * @method easeInSine + * @memberOf fabric.util.ease + */ + function easeInSine(t, b, c, d) { + return -c * Math.cos(t/d * (Math.PI/2)) + c + b; + } + + /** + * @method easeOutSine + * @memberOf fabric.util.ease + */ + function easeOutSine(t, b, c, d) { + return c * Math.sin(t/d * (Math.PI/2)) + b; + } + + /** + * @method easeInOutSine + * @memberOf fabric.util.ease + */ + function easeInOutSine(t, b, c, d) { + return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; + } + + /** + * @method easeInExpo + * @memberOf fabric.util.ease + */ + function easeInExpo(t, b, c, d) { + return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + } + + /** + * @method easeOutExpo + * @memberOf fabric.util.ease + */ + function easeOutExpo(t, b, c, d) { + return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + } + + /** + * @method easeInOutExpo + * @memberOf fabric.util.ease + */ + function easeInOutExpo(t, b, c, d) { + if (t==0) return b; + if (t==d) return b+c; + if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + } + + /** + * @method easeInCirc + * @memberOf fabric.util.ease + */ + function easeInCirc(t, b, c, d) { + return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; + } + + /** + * @method easeOutCirc + * @memberOf fabric.util.ease + */ + function easeOutCirc(t, b, c, d) { + return c * Math.sqrt(1 - (t=t/d-1)*t) + b; + } + + /** + * @method easeInOutCirc + * @memberOf fabric.util.ease + */ + function easeInOutCirc(t, b, c, d) { + if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; + return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; + } + + /** + * @method easeInElastic + * @memberOf fabric.util.ease + */ + function easeInElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + } + + /** + * @method easeOutElastic + * @memberOf fabric.util.ease + */ + function easeOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; + } + + /** + * @method easeInOutElastic + * @memberOf fabric.util.ease + */ + function easeInOutElastic(t, b, c, d) { + var s=1.70158;var p=0;var a=c; + if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); + if (a < Math.abs(c)) { a=c; var s=p/4; } + else var s = p/(2*Math.PI) * Math.asin (c/a); + if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; + return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; + } + + /** + * @method easeInBack + * @memberOf fabric.util.ease + */ + function easeInBack(t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c*(t/=d)*t*((s+1)*t - s) + b; + } + + /** + * @method easeOutBack + * @memberOf fabric.util.ease + */ + function easeOutBack(t, b, c, d, s) { + if (s == undefined) s = 1.70158; + return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; + } + + /** + * @method easeInOutBack + * @memberOf fabric.util.ease + */ + function easeInOutBack(t, b, c, d, s) { + if (s == undefined) s = 1.70158; + if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; + return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; + } + + /** + * @method easeInBounce + * @memberOf fabric.util.ease + */ + function easeInBounce(t, b, c, d) { + return c - easeOutBounce (d-t, 0, c, d) + b; + } + + /** + * @method easeOutBounce + * @memberOf fabric.util.ease + */ + function easeOutBounce(t, b, c, d) { + if ((t/=d) < (1/2.75)) { + return c*(7.5625*t*t) + b; + } else if (t < (2/2.75)) { + return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; + } else if (t < (2.5/2.75)) { + return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; + } else { + return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; + } + } + + /** + * @method easeInOutBounce + * @memberOf fabric.util.ease + */ + function easeInOutBounce(t, b, c, d) { + if (t < d/2) return easeInBounce (t*2, 0, c, d) * .5 + b; + return easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b; + } + + /** @namespace fabric.util.ease */ + fabric.util.ease = { + easeInQuad: easeInQuad, + easeOutQuad: easeOutQuad, + easeInOutQuad: easeInOutQuad, + easeInCubic: easeInCubic, + easeOutCubic: easeOutCubic, + easeInOutCubic: easeInOutCubic, + easeInQuart: easeInQuart, + easeOutQuart: easeOutQuart, + easeInOutQuart: easeInOutQuart, + easeInQuint: easeInQuint, + easeOutQuint: easeOutQuint, + easeInOutQuint: easeInOutQuint, + easeInSine: easeInSine, + easeOutSine: easeOutSine, + easeInOutSine: easeInOutSine, + easeInExpo: easeInExpo, + easeOutExpo: easeOutExpo, + easeInOutExpo: easeInOutExpo, + easeInCirc: easeInCirc, + easeOutCirc: easeOutCirc, + easeInOutCirc: easeInOutCirc, + easeInElastic: easeInElastic, + easeOutElastic: easeOutElastic, + easeInOutElastic: easeInOutElastic, + easeInBack: easeInBack, + easeOutBack: easeOutBack, + easeInOutBack: easeInOutBack, + easeInBounce: easeInBounce, + easeOutBounce: easeOutBounce, + easeInOutBounce: easeInOutBounce + }; + +}()); +(function(global) { + + "use strict"; + + /** + * @name fabric + * @namespace + */ + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + capitalize = fabric.util.string.capitalize, + clone = fabric.util.object.clone; + + var attributesMap = { + 'cx': 'left', + 'x': 'left', + 'cy': 'top', + 'y': 'top', + 'r': 'radius', + 'fill-opacity': 'opacity', + 'fill-rule': 'fillRule', + 'stroke-width': 'strokeWidth', + 'transform': 'transformMatrix', + 'text-decoration': 'textDecoration', + 'font-size': 'fontSize', + 'font-weight': 'fontWeight', + 'font-style': 'fontStyle', + 'font-family': 'fontFamily' + }; + + function normalizeAttr(attr) { + // transform attribute names + if (attr in attributesMap) { + return attributesMap[attr]; + } + return attr; + } + + /** + * Returns an object of attributes' name/value, given element and an array of attribute names; + * Parses parent "g" nodes recursively upwards. + * @static + * @memberOf fabric + * @method parseAttributes + * @param {DOMElement} element Element to parse + * @param {Array} attributes Array of attributes to parse + * @return {Object} object containing parsed attributes' names/values + */ + function parseAttributes(element, attributes) { + + if (!element) { + return; + } + + var value, + parsed, + parentAttributes = { }; + + // if there's a parent container (`g` node), parse its attributes recursively upwards + if (element.parentNode && /^g$/i.test(element.parentNode.nodeName)) { + parentAttributes = fabric.parseAttributes(element.parentNode, attributes); + } + + var ownAttributes = attributes.reduce(function(memo, attr) { + value = element.getAttribute(attr); + parsed = parseFloat(value); + if (value) { + // "normalize" attribute values + if ((attr === 'fill' || attr === 'stroke') && value === 'none') { + value = ''; + } + if (attr === 'fill-rule') { + value = (value === 'evenodd') ? 'destination-over' : value; + } + if (attr === 'transform') { + value = fabric.parseTransformAttribute(value); + } + attr = normalizeAttr(attr); + memo[attr] = isNaN(parsed) ? value : parsed; + } + return memo; + }, { }); + + // add values parsed from style, which take precedence over attributes + // (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes) + + ownAttributes = extend(ownAttributes, extend(getGlobalStylesForElement(element), fabric.parseStyleAttribute(element))); + return extend(parentAttributes, ownAttributes); + }; + + /** + * Parses "transform" attribute, returning an array of values + * @static + * @function + * @memberOf fabric + * @method parseTransformAttribute + * @param attributeValue {String} string containing attribute value + * @return {Array} array of 6 elements representing transformation matrix + */ + fabric.parseTransformAttribute = (function() { + function rotateMatrix(matrix, args) { + var angle = args[0]; + + matrix[0] = Math.cos(angle); + matrix[1] = Math.sin(angle); + matrix[2] = -Math.sin(angle); + matrix[3] = Math.cos(angle); + } + + function scaleMatrix(matrix, args) { + var multiplierX = args[0], + multiplierY = (args.length === 2) ? args[1] : args[0]; + + matrix[0] = multiplierX; + matrix[3] = multiplierY; + } + + function skewXMatrix(matrix, args) { + matrix[2] = args[0]; + } + + function skewYMatrix(matrix, args) { + matrix[1] = args[0]; + } + + function translateMatrix(matrix, args) { + matrix[4] = args[0]; + if (args.length === 2) { + matrix[5] = args[1]; + } + } + + // identity matrix + var iMatrix = [ + 1, // a + 0, // b + 0, // c + 1, // d + 0, // e + 0 // f + ], + + // == begin transform regexp + number = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)', + comma_wsp = '(?:\\s+,?\\s*|,\\s*)', + + skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', + skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', + rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + ')' + comma_wsp + '(' + number + '))?\\s*\\))', + scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', + translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + comma_wsp + '(' + number + '))?\\s*\\))', + + matrix = '(?:(matrix)\\s*\\(\\s*' + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + comma_wsp + + '(' + number + ')' + + '\\s*\\))', + + transform = '(?:' + + matrix + '|' + + translate + '|' + + scale + '|' + + rotate + '|' + + skewX + '|' + + skewY + + ')', + + transforms = '(?:' + transform + '(?:' + comma_wsp + transform + ')*' + ')', + + transform_list = '^\\s*(?:' + transforms + '?)\\s*$', + + // http://www.w3.org/TR/SVG/coords.html#TransformAttribute + reTransformList = new RegExp(transform_list), + // == end transform regexp + + reTransform = new RegExp(transform); + + return function(attributeValue) { + + // start with identity matrix + var matrix = iMatrix.concat(); + + // return if no argument was given or + // an argument does not match transform attribute regexp + if (!attributeValue || (attributeValue && !reTransformList.test(attributeValue))) { + return matrix; + } + + attributeValue.replace(reTransform, function(match) { + + var m = new RegExp(transform).exec(match).filter(function (match) { + return (match !== '' && match != null); + }), + operation = m[1], + args = m.slice(2).map(parseFloat); + + switch(operation) { + case 'translate': + translateMatrix(matrix, args); + break; + case 'rotate': + rotateMatrix(matrix, args); + break; + case 'scale': + scaleMatrix(matrix, args); + break; + case 'skewX': + skewXMatrix(matrix, args); + break; + case 'skewY': + skewYMatrix(matrix, args); + break; + case 'matrix': + matrix = args; + break; + } + }) + return matrix; + } + })(); + + /** + * Parses "points" attribute, returning an array of values + * @static + * @memberOf fabric + * @method parsePointsAttribute + * @param points {String} points attribute string + * @return {Array} array of points + */ + function parsePointsAttribute(points) { + + // points attribute is required and must not be empty + if (!points) return null; + + points = points.trim(); + var asPairs = points.indexOf(',') > -1; + + points = points.split(/\s+/); + var parsedPoints = [ ]; + + // points could look like "10,20 30,40" or "10 20 30 40" + 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]) }); + } + } + + // odd number of points is an error + if (parsedPoints.length % 2 !== 0) { + // return null; + } + + return parsedPoints; + }; + + /** + * Parses "style" attribute, retuning an object with values + * @static + * @memberOf fabric + * @method parseStyleAttribute + * @param {SVGElement} element Element to parse + * @return {Object} Objects with values parsed from style attribute of an element + */ + function parseStyleAttribute(element) { + var oStyle = { }, + style = element.getAttribute('style'); + if (style) { + if (typeof style == 'string') { + style = style.replace(/;$/, '').split(';').forEach(function (current) { + var attr = current.split(':'); + oStyle[normalizeAttr(attr[0].trim().toLowerCase())] = attr[1].trim(); + }); + } else { + for (var prop in style) { + if (typeof style[prop] !== 'undefined') { + oStyle[normalizeAttr(prop.toLowerCase())] = style[prop]; + } + } + } + } + return oStyle; + }; + + function resolveGradients(instances) { + for (var i = instances.length; i--; ) { + var instanceFillValue = instances[i].get('fill'); + + if (/^url\(/.test(instanceFillValue)) { + + var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); + + if (fabric.gradientDefs[gradientId]) { + instances[i].set('fill', + fabric.Gradient.fromElement(fabric.gradientDefs[gradientId], instances[i])); + } + } + } + } + + /** + * Transforms an array of svg elements to corresponding fabric.* instances + * @static + * @memberOf fabric + * @method parseElements + * @param {Array} elements Array of elements to parse + * @param {Function} callback Being passed an array of fabric instances (transformed from SVG elements) + * @param {Object} options Options object + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function parseElements(elements, callback, options, reviver) { + var instances = Array(elements.length), i = elements.length; + + function checkIfDone() { + if (--i === 0) { + instances = instances.filter(function(el) { + return el != null; + }); + resolveGradients(instances); + callback(instances); + } + } + + for (var index = 0, el, len = elements.length; index < len; index++) { + el = elements[index]; + var klass = fabric[capitalize(el.tagName)]; + if (klass && klass.fromElement) { + try { + if (klass.async) { + klass.fromElement(el, (function(index, el) { + return function(obj) { + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + }; + })(index), options); + } + else { + var obj = klass.fromElement(el, options); + reviver && reviver(el, obj); + instances.splice(index, 0, obj); + checkIfDone(); + } + } + catch(e) { + fabric.log(e.message || e); + } + } + else { + checkIfDone(); + } + } + }; + + /** + * Returns CSS rules for a given SVG document + * @static + * @function + * @memberOf fabric + * @method getCSSRules + * @param {SVGDocument} doc SVG document to parse + * @return {Object} CSS rules of this document + */ + function getCSSRules(doc) { + var styles = doc.getElementsByTagName('style'), + allRules = { }, + rules; + + // very crude parsing of style contents + for (var i = 0, len = styles.length; i < len; i++) { + var styleContents = styles[0].textContent; + + // remove comments + styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); + + rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g); + rules = rules.map(function(rule) { return rule.trim() }); + + rules.forEach(function(rule) { + var match = rule.match(/([\s\S]*?)\s*\{([^}]*)\}/), + rule = match[1], + declaration = match[2].trim(), + propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); + + if (!allRules[rule]) { + allRules[rule] = { }; + } + + for (var i = 0, len = propertyValuePairs.length; i < len; i++) { + var pair = propertyValuePairs[i].split(/\s*:\s*/), + property = pair[0], + value = pair[1]; + + allRules[rule][property] = value; + } + }); + } + + return allRules; + } + + function getGlobalStylesForElement(element) { + var nodeName = element.nodeName, + className = element.getAttribute('class'), + id = element.getAttribute('id'), + styles = { }; + + for (var rule in fabric.cssRules) { + var ruleMatchesElement = (className && new RegExp('^\\.' + className).test(rule)) || + (id && new RegExp('^#' + id).test(rule)) || + (new RegExp('^' + nodeName).test(rule)); + + if (ruleMatchesElement) { + for (var property in fabric.cssRules[rule]) { + styles[property] = fabric.cssRules[rule][property]; + } + } + } + + return styles; + } + + /** + * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback + * @static + * @function + * @memberOf fabric + * @method parseSVGDocument + * @param {SVGDocument} doc SVG document to parse + * @param {Function} callback Callback to call when parsing is finished; It's being passed an array of elements (parsed from a document). + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + fabric.parseSVGDocument = (function() { + + var reAllowedSVGTagNames = /^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/; + + // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute + // \d doesn't quite cut it (as we need to match an actual float number) + + // matches, e.g.: +14.56e-12, etc. + var reNum = '(?:[-+]?\\d+(?:\\.\\d+)?(?:e[-+]?\\d+)?)'; + + var reViewBoxAttrValue = new RegExp( + '^' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*,?' + + '\\s*(' + reNum + '+)\\s*' + + '$' + ); + + function hasAncestorWithNodeName(element, nodeName) { + while (element && (element = element.parentNode)) { + if (nodeName.test(element.nodeName)) { + return true; + } + } + return false; + } + + return function(doc, callback, reviver) { + if (!doc) return; + + var startTime = new Date(), + descendants = fabric.util.toArray(doc.getElementsByTagName('*')); + + if (descendants.length === 0) { + // we're likely in node, where "o3-xml" library fails to gEBTN("*") + // https://github.com/ajaxorg/node-o3-xml/issues/21 + descendants = doc.selectNodes("//*[name(.)!='svg']"); + var arr = [ ]; + for (var i = 0, len = descendants.length; i < len; i++) { + arr[i] = descendants[i]; + } + descendants = arr; + } + + var elements = descendants.filter(function(el) { + return reAllowedSVGTagNames.test(el.tagName) && + !hasAncestorWithNodeName(el, /^(?:pattern|defs)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement + }); + + if (!elements || (elements && !elements.length)) return; + + var viewBoxAttr = doc.getAttribute('viewBox'), + widthAttr = doc.getAttribute('width'), + heightAttr = doc.getAttribute('height'), + width = null, + height = null, + minX, + minY; + + if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) { + minX = parseInt(viewBoxAttr[1], 10); + minY = parseInt(viewBoxAttr[2], 10); + width = parseInt(viewBoxAttr[3], 10); + height = parseInt(viewBoxAttr[4], 10); + } + + // values of width/height attributes overwrite those extracted from viewbox attribute + width = widthAttr ? parseFloat(widthAttr) : width; + height = heightAttr ? parseFloat(heightAttr) : height; + + var options = { + width: width, + height: height + }; + + fabric.gradientDefs = fabric.getGradientDefs(doc); + fabric.cssRules = getCSSRules(doc); + + // Precedence of rules: style > class > attribute + + fabric.parseElements(elements, function(instances) { + fabric.documentParsingTime = new Date() - startTime; + if (callback) { + callback(instances, options); + } + }, clone(options), reviver); + }; + })(); + + /** + * Used for caching SVG documents (loaded via `fabric.Canvas#loadSVGFromURL`) + * @property + * @namespace + */ + var svgCache = { + + /** + * @method has + * @param {String} name + * @param {Function} callback + */ + has: function (name, callback) { + callback(false); + }, + + /** + * @method get + * @param {String} url + * @param {Function} callback + */ + get: function (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 + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromURL(url, callback, reviver) { + + url = url.replace(/^\n\s*/, '').trim(); + + svgCache.has(url, function (hasUrl) { + if (hasUrl) { + svgCache.get(url, function (value) { + var enlivedRecord = _enlivenCachedObject(value); + callback(enlivedRecord.objects, enlivedRecord.options); + }); + } + else { + new fabric.util.request(url, { + method: 'get', + onComplete: onComplete + }); + } + }); + + function onComplete(r) { + + var xml = r.responseXML; + if (!xml.documentElement && fabric.window.ActiveXObject && r.responseText) { + xml = new ActiveXObject('Microsoft.XMLDOM'); + xml.async = 'false'; + //IE chokes on DOCTYPE + xml.loadXML(r.responseText.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,'')); + } + if (!xml.documentElement) return; + + fabric.parseSVGDocument(xml.documentElement, function (results, options) { + svgCache.set(url, { + objects: fabric.util.array.invoke(results, 'toObject'), + options: options + }); + callback(results, options); + }, reviver); + } + } + + /** + * @method _enlivenCachedObject + */ + function _enlivenCachedObject(cachedObject) { + + var objects = cachedObject.objects, + options = cachedObject.options; + + objects = objects.map(function (o) { + return fabric[capitalize(o.type)].fromObject(o); + }); + + return ({ objects: objects, options: options }); + } + + /** + * Takes string corresponding to an SVG document, and parses it into a set of fabric objects + * @method loadSVGFromString + * @param {String} string + * @param {Function} callback + * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. + */ + function loadSVGFromString(string, callback, reviver) { + string = string.trim(); + var doc; + if (typeof DOMParser !== 'undefined') { + var parser = new DOMParser(); + if (parser && parser.parseFromString) { + doc = parser.parseFromString(string, 'text/xml'); + } + } + else if (fabric.window.ActiveXObject) { + var doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + //IE chokes on DOCTYPE + doc.loadXML(string.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,'')); + } + + fabric.parseSVGDocument(doc.documentElement, function (results, options) { + callback(results, options); + }, reviver); + } + + function createSVGFontFacesMarkup(objects) { + var markup = ''; + + for (var i = 0, len = objects.length; i < len; i++) { + if (objects[i].type !== 'text' || !objects[i].path) continue; + + markup += [ + '@font-face {', + 'font-family: ', objects[i].fontFamily, '; ', + 'src: url(\'', objects[i].path, '\')', + '}' + ].join(''); + } + + if (markup) { + markup = [ + '<defs>', + '<style type="text/css">', + '<![CDATA[', + markup, + ']]>', + '</style>', + '</defs>' + ].join(''); + } + + return markup; + } + + extend(fabric, { + + parseAttributes: parseAttributes, + parseElements: parseElements, + parseStyleAttribute: parseStyleAttribute, + parsePointsAttribute: parsePointsAttribute, + getCSSRules: getCSSRules, + + loadSVGFromURL: loadSVGFromURL, + loadSVGFromString: loadSVGFromString, + + createSVGFontFacesMarkup: createSVGFontFacesMarkup + }); + +})(typeof exports != 'undefined' ? exports : this); + +(function() { + + function getColorStopFromStyle(el) { + var style = el.getAttribute('style'); + + if (style) { + var keyValuePairs = style.split(/\s*;\s*/); + + if (keyValuePairs[keyValuePairs.length-1] === '') { + keyValuePairs.pop(); + } + + for (var i = keyValuePairs.length; i--; ) { + + var split = keyValuePairs[i].split(/\s*:\s*/), + key = split[0].trim(), + value = split[1].trim(); + + if (key === 'stop-color') { + return value; + } + } + } + } + + /** + * @class Object + * @memberOf fabric + */ + fabric.Gradient = fabric.util.createClass(/** @scope fabric.Gradient.prototype */ { + + initialize: function(options) { + + options || (options = { }); + + this.x1 = options.x1 || 0; + this.y1 = options.y1 || 0; + this.x2 = options.x2 || 0; + this.y2 = options.y2 || 0; + + this.colorStops = options.colorStops; + }, + + toObject: function() { + return { + x1: this.x1, + x2: this.x2, + y1: this.y1, + y2: this.y2, + colorStops: this.colorStops + }; + }, + + toLiveGradient: function(ctx) { + var gradient = ctx.createLinearGradient( + this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2); + + for (var position in this.colorStops) { + var colorValue = this.colorStops[position]; + gradient.addColorStop(parseFloat(position), colorValue); + } + + return gradient; + } + }); + + fabric.util.object.extend(fabric.Gradient, { + + /** + * @method fromElement + * @static + * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement + */ + fromElement: function(el, instance) { + + /** + * @example: + * + * <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, + coords = { + x1: el.getAttribute('x1') || 0, + y1: el.getAttribute('y1') || 0, + x2: el.getAttribute('x2') || '100%', + y2: el.getAttribute('y2') || 0 + }; + + for (var i = colorStopEls.length; i--; ) { + el = colorStopEls[i]; + offset = el.getAttribute('offset'); + + // convert percents to absolute values + offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); + colorStops[offset] = getColorStopFromStyle(el) || el.getAttribute('stop-color'); + } + + _convertPercentUnitsToValues(instance, coords); + + return new fabric.Gradient({ + x1: coords.x1, + y1: coords.y1, + x2: coords.x2, + y2: coords.y2, + colorStops: colorStops + }); + }, + + /** + * @method forObject + * @static + */ + forObject: function(obj, options) { + options || (options = { }); + _convertPercentUnitsToValues(obj, options); + return new fabric.Gradient(options); + } + }); + + function _convertPercentUnitsToValues(object, options) { + for (var prop in options) { + if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { + var percents = parseFloat(options[prop], 10); + if (prop === 'x1' || prop === 'x2') { + options[prop] = object.width * percents / 100; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] = object.height * percents / 100; + } + } + // normalize rendering point (should be from top/left corner rather than center of the shape) + if (prop === 'x1' || prop === 'x2') { + options[prop] -= object.width / 2; + } + else if (prop === 'y1' || prop === 'y2') { + options[prop] -= object.height / 2; + } + } + } + + /** + * Parses an SVG document, returning all of the gradient declarations found in it + * @static + * @function + * @memberOf fabric + * @method getGradientDefs + * @param {SVGDocument} doc SVG document to parse + * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element + */ + function getGradientDefs(doc) { + var linearGradientEls = doc.getElementsByTagName('linearGradient'), + radialGradientEls = doc.getElementsByTagName('radialGradient'), + el, + gradientDefs = { }; + + for (var i = linearGradientEls.length; i--; ) { + el = linearGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + for (var i = radialGradientEls.length; i--; ) { + el = radialGradientEls[i]; + gradientDefs[el.getAttribute('id')] = el; + } + + return gradientDefs; + } + + fabric.getGradientDefs = getGradientDefs; + +})(); +(function(global) { + + "use strict"; + + /* Adaptation of work of Kevin Lindsey ([email protected]) */ + + 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 ([email protected]) */ + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Intersection) { + fabric.warn('fabric.Intersection is already defined'); + return; + } + + /** + * @class Intersection + * @memberOf fabric + */ + function Intersection(status) { + if (arguments.length > 0) { + this.init(status); + } + } + + fabric.Intersection = Intersection; + + fabric.Intersection.prototype = /** @scope fabric.Intersection.prototype */ { + + /** + * @method init + * @param {String} status + */ + init: function (status) { + this.status = status; + this.points = []; + }, + + /** + * @method appendPoint + * @param {String} status + */ + appendPoint: function (point) { + this.points.push(point); + }, + + /** + * @method appendPoints + * @param {String} status + */ + appendPoints: function (points) { + this.points = this.points.concat(points); + } + }; + + /** + * @static + * @method intersectLineLine + */ + fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { + var result, + ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), + ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), + u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); + if (u_b != 0) { + var ua = ua_t / u_b, + ub = ub_t / u_b; + if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) { + result = new Intersection("Intersection"); + result.points.push(new fabric.Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y))); + } + else { + result = new Intersection("No Intersection"); + } + } + else { + if (ua_t == 0 || ub_t == 0) { + result = new Intersection("Coincident"); + } + else { + result = new Intersection("Parallel"); + } + } + return result; + }; + + /** + * @method intersectLinePolygon + */ + fabric.Intersection.intersectLinePolygon = function(a1,a2,points){ + var result = new Intersection("No Intersection"), + length = points.length; + + for (var i = 0; i < length; i++) { + var b1 = points[i], + b2 = points[(i+1) % length], + inter = Intersection.intersectLineLine(a1, a2, b1, b2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * @method intersectPolygonPolygon + */ + fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { + var result = new Intersection("No Intersection"), + length = points1.length; + + for (var i = 0; i < length; i++) { + var a1 = points1[i], + a2 = points1[(i+1) % length], + inter = Intersection.intersectLinePolygon(a1, a2, points2); + + result.appendPoints(inter.points); + } + if (result.points.length > 0) { + result.status = "Intersection"; + } + return result; + }; + + /** + * @method intersectPolygonRectangle + */ + fabric.Intersection.intersectPolygonRectangle = function (points, r1, r2) { + var min = r1.min(r2), + max = r1.max(r2), + topRight = new fabric.Point(max.x, min.y), + bottomLeft = new fabric.Point(min.x, max.y), + inter1 = Intersection.intersectLinePolygon(min, topRight, points), + inter2 = Intersection.intersectLinePolygon(topRight, max, points), + inter3 = Intersection.intersectLinePolygon(max, bottomLeft, points), + inter4 = Intersection.intersectLinePolygon(bottomLeft, min, points), + result = new Intersection("No Intersection"); + + result.appendPoints(inter1.points); + result.appendPoints(inter2.points); + result.appendPoints(inter3.points); + result.appendPoints(inter4.points); + if (result.points.length > 0) { + result.status="Intersection"; + } + return result; + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Color) { + fabric.warn('fabric.Color is already defined.'); + return; + } + + /** + * The purpose of {@link fabric.Color} is to abstract and encapsulate common color operations; + * {@link fabric.Color} is a constructor and creates instances of {@link fabric.Color} objects. + * + * @class Color + * @memberOf fabric + * @param {String} color (optional) in hex or rgb(a) format + */ + function Color(color) { + if (!color) { + this.setSource([0, 0, 0, 1]); + } + else { + this._tryParsingColor(color); + } + } + + fabric.Color = Color; + + fabric.Color.prototype = /** @scope fabric.Color.prototype */ { + + /** + * @private + * @method _tryParsingColor + */ + _tryParsingColor: function(color) { + var source = Color.sourceFromHex(color); + if (!source) { + source = Color.sourceFromRgb(color); + } + if (source) { + this.setSource(source); + } + }, + + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method getSource + * @return {Array} + */ + getSource: function() { + return this._source; + }, + + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @method setSource + * @param {Array} source + */ + setSource: function(source) { + this._source = source; + }, + + /** + * Returns color represenation in RGB format + * @method toRgb + * @return {String} ex: rgb(0-255,0-255,0-255) + */ + toRgb: function() { + var source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + }, + + /** + * Returns color represenation in RGBA format + * @method toRgba + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) + */ + toRgba: function() { + var source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + }, + + /** + * Returns color represenation in HEX format + * @method toHex + * @return {String} ex: FF5555 + */ + toHex: function() { + var source = this.getSource(); + + var r = source[0].toString(16); + r = (r.length == 1) ? ('0' + r) : r; + + var g = source[1].toString(16); + g = (g.length == 1) ? ('0' + g) : g; + + var b = source[2].toString(16); + b = (b.length == 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + }, + + /** + * Gets value of alpha channel for this color + * @method getAlpha + * @return {Number} 0-1 + */ + getAlpha: function() { + return this.getSource()[3]; + }, + + /** + * Sets value of alpha channel for this color + * @method setAlpha + * @param {Number} 0-1 + * @return {fabric.Color} thisArg + */ + setAlpha: function(alpha) { + var source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + }, + + /** + * Transforms color to its grayscale representation + * @method toGrayscale + * @return {fabric.Color} thisArg + */ + toGrayscale: function() { + var source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Transforms color to its black and white representation + * @method toGrayscale + * @return {fabric.Color} thisArg + */ + toBlackWhite: function(threshold) { + var source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3], + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + }, + + /** + * Overlays color with another color + * @method overlayWith + * @param {String|fabric.Color} otherColor + * @return {fabric.Color} thisArg + */ + overlayWith: function(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); + } + + var result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(); + + for (var i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); + } + + result[3] = alpha; + this.setSource(result); + return this; + } + }; + + /** + * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgb(255, 100, 10, 0.5), rgb(1,1,1)) + * @static + * @field + */ + fabric.Color.reRGBa = /^rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*(\d+(?:\.\d+)?))?\)$/; + + /** + * Regex matching color in HEX format (ex: #FF5555, 010155, aff) + * @static + * @field + */ + fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; + + /** + * Returns new color object, when given a color in RGB format + * @method fromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {fabric.Color} + */ + fabric.Color.fromRgb = function(color) { + return Color.fromSource(Color.sourceFromRgb(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format + * @method sourceFromRgb + * @param {String} color ex: rgb(0-255,0-255,0-255) + * @return {Array} source + */ + fabric.Color.sourceFromRgb = function(color) { + var match = color.match(Color.reRGBa); + if (match) { + return [ + parseInt(match[1], 10), + parseInt(match[2], 10), + parseInt(match[3], 10), + match[4] ? parseFloat(match[4]) : 1 + ]; + } + }; + + /** + * Returns new color object, when given a color in RGBA format + * @static + * @function + * @method fromRgba + * @param {String} color + * @return {fabric.Color} + */ + fabric.Color.fromRgba = Color.fromRgb; + + /** + * Returns new color object, when given a color in HEX format + * @static + * @method fromHex + * @return {fabric.Color} + */ + fabric.Color.fromHex = function(color) { + return Color.fromSource(Color.sourceFromHex(color)); + }; + + /** + * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HEX format + * @static + * @method sourceFromHex + * @param {String} color ex: FF5555 + * @return {Array} source + */ + fabric.Color.sourceFromHex = function(color) { + if (color.match(Color.reHex)) { + var value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6); + + return [ + parseInt(r, 16), + parseInt(g, 16), + parseInt(b, 16), + 1 + ]; + } + }; + + /** + * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) + * @static + * @method fromSource + * @return {fabric.Color} + */ + fabric.Color.fromSource = function(source) { + var oColor = new Color(); + oColor.setSource(source); + return oColor; + }; + +})(typeof exports != 'undefined' ? exports : this); +(function (global) { + + "use strict"; + + if (fabric.StaticCanvas) { + fabric.warn('fabric.StaticCanvas is already defined.'); + return; + } + + // aliases for faster resolution + var extend = fabric.util.object.extend, + getElementOffset = fabric.util.getElementOffset, + removeFromArray = fabric.util.removeFromArray, + removeListener = fabric.util.removeListener, + + CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); + + /** + * @class fabric.StaticCanvas + * @constructor + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.StaticCanvas = function (el, options) { + options || (options = { }); + + this._initStatic(el, options); + fabric.StaticCanvas.activeInstance = this; + }; + + extend(fabric.StaticCanvas.prototype, fabric.Observable); + + extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ { + + /** + * Background color of canvas instance + * @property + * @type String + */ + backgroundColor: 'rgba(0, 0, 0, 0)', + + /** + * Background image of canvas instance + * Should be set via `setBackgroundImage` + * @property + * @type String + */ + backgroundImage: '', + + /** + * Opacity of the background image of the canvas instance + * @property + * @type Float + */ + backgroundImageOpacity: 1.0, + + /** + * Indicatus whether the background image should be stretched to fit the + * dimensions of the canvas instance. + * @property + * @type Boolean + */ + backgroundImageStretch: true, + + /** + * Indicates whether toObject/toDatalessObject should include default values + * @property + * @type Boolean + */ + includeDefaultValues: true, + + /** + * Indicates whether objects' state should be saved + * @property + * @type Boolean + */ + stateful: true, + + /** + * Indicates whether fabric.Canvas#add should also re-render canvas. + * Disabling this option could give a great performance boost when adding a lot of objects to canvas at once + * (followed by a manual rendering after addition) + */ + renderOnAddition: true, + + /** + * Function that determines clipping of entire canvas area + * Being passed context as first argument. See clipping canvas area in https://github.com/kangax/fabric.js/wiki/FAQ + * @property + * @type Function + */ + clipTo: null, + + /** + * Indicates whether object controls (borders/corners) are rendered above overlay image + * @property + * @type Boolean + */ + controlsAboveOverlay: false, + + /** + * Callback; invoked right before object is about to be scaled/rotated + * @method onBeforeScaleRotate + * @param {fabric.Object} target Object that's about to be scaled/rotated + */ + onBeforeScaleRotate: function (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, + + _initStatic: function(el, options) { + this._objects = []; + + this._createLowerCanvas(el); + this._initOptions(options); + + if (options.overlayImage) { + this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); + } + if (options.backgroundImage) { + this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); + } + this.calcOffset(); + }, + + /** + * Calculates canvas element offset relative to the document + * This method is also attached as "resize" event handler of window + * @method calcOffset + * @return {fabric.Canvas} instance + * @chainable + */ + calcOffset: function () { + this._offset = getElementOffset(this.lowerCanvasEl); + return this; + }, + + /** + * Sets overlay image for this canvas + * @method setOverlayImage + * @param {String} url url of an image to set overlay to + * @param {Function} callback callback to invoke when image is loaded and set as an overlay + * @return {fabric.Canvas} thisArg + * @chainable + */ + setOverlayImage: function (url, callback) { // TODO (kangax): test callback + fabric.util.loadImage(url, function(img) { + this.overlayImage = img; + callback && callback(); + }, this); + return this; + }, + + /** + * Sets background image for this canvas + * @method setBackgroundImage + * @param {String} url url of an image to set background to + * @param {Function} callback callback to invoke when image is loaded and set as background + * @param {Object} options optional options to set for the background image + * @return {fabric.Canvas} thisArg + * @chainable + */ + setBackgroundImage: function (url, callback, options) { + return fabric.util.loadImage(url, function(img) { + this.backgroundImage = img; + if (options && ('backgroundImageOpacity' in options)) { + this.backgroundImageOpacity = options.backgroundImageOpacity; + } + if (options && ('backgroundImageStretch' in options)) { + this.backgroundImageStretch = options.backgroundImageStretch; + } + callback && callback(); + }, this); + }, + + /** + * @private + * @method _createCanvasElement + * @param {Element} element + */ + _createCanvasElement: function() { + var element = fabric.document.createElement('canvas'); + if (!element.style) { + element.style = { }; + } + if (!element) { + throw CANVAS_INIT_ERROR; + } + this._initCanvasElement(element); + return element; + }, + + _initCanvasElement: function(element) { + if (typeof element.getContext === 'undefined' && + typeof G_vmlCanvasManager !== 'undefined' && + G_vmlCanvasManager.initElement) { + + G_vmlCanvasManager.initElement(element); + } + if (typeof element.getContext === 'undefined') { + throw CANVAS_INIT_ERROR; + } + }, + + /** + * @method _initOptions + * @param {Object} options + */ + _initOptions: function (options) { + for (var prop in options) { + this[prop] = options[prop]; + } + + this.width = parseInt(this.lowerCanvasEl.width, 10) || 0; + this.height = parseInt(this.lowerCanvasEl.height, 10) || 0; + + if (!this.lowerCanvasEl.style) return; + + this.lowerCanvasEl.style.width = this.width + 'px'; + this.lowerCanvasEl.style.height = this.height + 'px'; + }, + + /** + * Creates a secondary canvas + * @method _createLowerCanvas + */ + _createLowerCanvas: function (canvasEl) { + this.lowerCanvasEl = fabric.util.getById(canvasEl) || this._createCanvasElement(); + this._initCanvasElement(this.lowerCanvasEl); + + fabric.util.addClass(this.lowerCanvasEl, 'lower-canvas'); + + if (this.interactive) { + this._applyCanvasStyle(this.lowerCanvasEl); + } + + this.contextContainer = this.lowerCanvasEl.getContext('2d'); + }, + + /** + * Returns canvas width + * @method getWidth + * @return {Number} + */ + getWidth: function () { + return this.width; + }, + + /** + * Returns canvas height + * @method getHeight + * @return {Number} + */ + getHeight: function () { + return this.height; + }, + + /** + * Sets width of this canvas instance + * @method setWidth + * @param {Number} width value to set width to + * @return {fabric.Canvas} instance + * @chainable true + */ + setWidth: function (value) { + return this._setDimension('width', value); + }, + + /** + * Sets height of this canvas instance + * @method setHeight + * @param {Number} height value to set height to + * @return {fabric.Canvas} instance + * @chainable true + */ + setHeight: function (value) { + return this._setDimension('height', value); + }, + + /** + * Sets dimensions (width, height) of this canvas instance + * @method setDimensions + * @param {Object} dimensions + * @return {fabric.Canvas} thisArg + * @chainable + */ + setDimensions: function(dimensions) { + for (var prop in dimensions) { + this._setDimension(prop, dimensions[prop]); + } + return this; + }, + + /** + * Helper for setting width/height + * @private + * @method _setDimensions + * @param {String} prop property (width|height) + * @param {Number} value value to set property to + * @return {fabric.Canvas} instance + * @chainable true + */ + _setDimension: function (prop, value) { + this.lowerCanvasEl[prop] = value; + this.lowerCanvasEl.style[prop] = value + 'px'; + + if (this.upperCanvasEl) { + this.upperCanvasEl[prop] = value; + this.upperCanvasEl.style[prop] = value + 'px'; + } + + if (this.wrapperEl) { + this.wrapperEl.style[prop] = value + 'px'; + } + + this[prop] = value; + + this.calcOffset(); + this.renderAll(); + + return this; + }, + + /** + * Returns <canvas> element corresponding to this instance + * @method getElement + * @return {HTMLCanvasElement} + */ + getElement: function () { + return this.lowerCanvasEl; + }, + + // placeholder + getActiveObject: function() { + return null; + }, + + // placeholder + getActiveGroup: function() { + return null; + }, + + /** + * Given a context, renders an object on that context + * @param ctx {Object} context to render object on + * @param object {Object} object to render + * @private + */ + _draw: function (ctx, object) { + if (!object) return; + + if (this.controlsAboveOverlay) { + var hasBorders = object.hasBorders, hasCorners = object.hasCorners; + object.hasBorders = object.hasCorners = false; + object.render(ctx); + object.hasBorders = hasBorders; + object.hasCorners = hasCorners; + } + else { + object.render(ctx); + } + }, + + /** + * Adds objects to canvas, then renders canvas; + * Objects should be instances of (or inherit from) fabric.Object + * @method add + * @return {fabric.Canvas} thisArg + * @chainable + */ + add: function () { + this._objects.push.apply(this._objects, arguments); + for (var i = arguments.length; i--; ) { + this._initObject(arguments[i]); + } + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * @private + * @method _initObject + */ + _initObject: function(obj) { + this.stateful && obj.setupState(); + obj.setCoords(); + obj.canvas = this; + this.fire('object:added', { target: obj }); + obj.fire('added'); + }, + + /** + * Inserts an object to canvas at specified index and renders canvas. + * An object should be an instance of (or inherit from) fabric.Object + * @method insertAt + * @param object {Object} Object to insert + * @param index {Number} index to insert object at + * @param nonSplicing {Boolean} when `true`, no splicing (shifting) of objects occurs + * @return {fabric.Canvas} instance + */ + insertAt: function (object, index, nonSplicing) { + if (nonSplicing) { + this._objects[index] = object; + } + else { + this._objects.splice(index, 0, object); + } + this._initObject(object); + this.renderOnAddition && this.renderAll(); + return this; + }, + + /** + * Returns an array of objects this instance has + * @method getObjects + * @return {Array} + */ + getObjects: function () { + return this._objects; + }, + + /** + * Clears specified context of canvas element + * @method clearContext + * @param context {Object} ctx context to clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clearContext: function(ctx) { + ctx.clearRect(0, 0, this.width, this.height); + return this; + }, + + /** + * Returns context of canvas where objects are drawn + * @method getContext + * @return {CanvasRenderingContext2D} + */ + getContext: function () { + return this.contextContainer; + }, + + /** + * Clears all contexts (background, main, top) of an instance + * @method clear + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this._objects.length = 0; + this.clearContext(this.contextContainer); + if (this.contextTop) { + this.clearContext(this.contextTop); + } + this.renderAll(); + return this; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @method renderAll + * @param allOnTop {Boolean} optional Whether we want to force all images to be rendered on the top canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function (allOnTop) { + + var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer']; + + if (this.contextTop) { + this.clearContext(this.contextTop); + } + + if (allOnTop === false || (typeof allOnTop === 'undefined')) { + this.clearContext(canvasToDrawOn); + } + + var length = this._objects.length, + activeGroup = this.getActiveGroup(), + startTime = new Date(); + + if (this.clipTo) { + canvasToDrawOn.save(); + canvasToDrawOn.beginPath(); + this.clipTo(canvasToDrawOn); + canvasToDrawOn.clip(); + } + + canvasToDrawOn.fillStyle = this.backgroundColor; + canvasToDrawOn.fillRect(0, 0, this.width, this.height); + + if (typeof this.backgroundImage == 'object') { + canvasToDrawOn.save(); + canvasToDrawOn.globalAlpha = this.backgroundImageOpacity; + + if (this.backgroundImageStretch) { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0, this.width, this.height); + } + else { + canvasToDrawOn.drawImage(this.backgroundImage, 0, 0); + } + canvasToDrawOn.restore(); + } + + if (length) { + for (var i = 0; i < length; ++i) { + if (!activeGroup || + (activeGroup && this._objects[i] && !activeGroup.contains(this._objects[i]))) { + this._draw(canvasToDrawOn, this._objects[i]); + } + } + } + + if (this.clipTo) { + canvasToDrawOn.restore(); + } + + // delegate rendering to group selection (if one exists) + if (activeGroup) { + this._draw(this.contextTop, activeGroup); + } + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + if (this.controlsAboveOverlay) { + this.drawControls(this.contextContainer); + } + + if (this.onFpsUpdate) { + var elapsedTime = new Date() - startTime; + this.onFpsUpdate(~~(1000 / elapsedTime)); + } + + this.fire('after:render'); + + return this; + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @method renderTop + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + this.clearContext(this.contextTop || this.contextContainer); + + if (this.overlayImage) { + this.contextContainer.drawImage(this.overlayImage, 0, 0); + } + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(); + } + + // delegate rendering to group selection if one exists + // used for drawing selection borders/corners + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.render(this.contextTop); + } + + this.fire('after:render'); + + return this; + }, + + /** + * Draws objects' controls (borders/corners) + * @method drawControls + * @param {Object} ctx context to render controls on + */ + drawControls: function(ctx) { + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + ctx.save(); + fabric.Group.prototype.transform.call(activeGroup, ctx); + activeGroup.drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + else { + for (var i = 0, len = this._objects.length; i < len; ++i) { + if (!this._objects[i].active) continue; + + ctx.save(); + fabric.Object.prototype.transform.call(this._objects[i], ctx); + this._objects[i].drawBorders(ctx).drawCorners(ctx); + ctx.restore(); + } + } + }, + + /** + * Exports canvas element to a dataurl image. + * @method toDataURL + * @param {String} format the format of the output image. Either "jpeg" or "png". + * @param {Number} quality quality level (0..1) + * @return {String} + */ + toDataURL: function (format, quality) { + var canvasEl = this.upperCanvasEl || this.lowerCanvasEl; + + this.renderAll(true); + var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) + ? canvasEl.toDataURL('image/' + format, quality) + : canvasEl.toDataURL('image/' + format); + this.renderAll(); + return data; + }, + + /** + * Exports canvas element to a dataurl image (allowing to change image size via multiplier). + * @method toDataURLWithMultiplier + * @param {String} format (png|jpeg) + * @param {Number} multiplier + * @param {Number} quality (0..1) + * @return {String} + */ + toDataURLWithMultiplier: function (format, multiplier, quality) { + + var origWidth = this.getWidth(), + origHeight = this.getHeight(), + scaledWidth = origWidth * multiplier, + scaledHeight = origHeight * multiplier, + activeObject = this.getActiveObject(), + activeGroup = this.getActiveGroup(); + + this.setWidth(scaledWidth).setHeight(scaledHeight); + this.contextTop.scale(multiplier, multiplier); + + if (activeGroup) { + // not removing group due to complications with restoring it with correct state afterwords + this._tempRemoveBordersCornersFromGroup(activeGroup); + } + else if (activeObject) { + this.deactivateAll(); + } + + // restoring width, height for `renderAll` to draw + // background properly (while context is scaled) + this.width = origWidth; + this.height = origHeight; + + this.renderAll(true); + + var dataURL = this.toDataURL(format, quality); + + this.contextTop.scale(1 / multiplier, 1 / multiplier); + this.setWidth(origWidth).setHeight(origHeight); + + if (activeGroup) { + this._restoreBordersCornersOnGroup(activeGroup); + } + else if (activeObject) { + this.setActiveObject(activeObject); + } + + this.renderAll(); + + return dataURL; + }, + + _tempRemoveBordersCornersFromGroup: function(group) { + group.origHideCorners = group.hideCorners; + group.origBorderColor = group.borderColor; + + group.hideCorners = true; + group.borderColor = 'rgba(0,0,0,0)'; + + group.forEachObject(function(o) { + o.origBorderColor = o.borderColor; + o.borderColor = 'rgba(0,0,0,0)'; + }); + }, + _restoreBordersCornersOnGroup: function(group) { + group.hideCorners = group.origHideCorners; + group.borderColor = group.origBorderColor; + + group.forEachObject(function(o) { + o.borderColor = o.origBorderColor; + delete o.origBorderColor; + }); + }, + + /** + * Returns coordinates of a center of canvas. + * Returned value is an object with top and left properties + * @method getCenter + * @return {Object} object with "top" and "left" number values + */ + getCenter: function () { + return { + top: this.getHeight() / 2, + left: this.getWidth() / 2 + }; + }, + + /** + * Centers object horizontally. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + */ + centerObjectH: function (object) { + object.set('left', this.getCenter().left); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically. + * @method centerObjectH + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObjectV: function (object) { + object.set('top', this.getCenter().top); + this.renderAll(); + return this; + }, + + /** + * Centers object vertically and horizontally. + * @method centerObject + * @param {fabric.Object} object Object to center + * @return {fabric.Canvas} thisArg + * @chainable + */ + centerObject: function (object) { + return this.centerObjectH(object).centerObjectV(object); + }, + + /** + * Returs dataless JSON representation of canvas + * @method toDatalessJSON + * @return {String} json string + */ + toDatalessJSON: function () { + return this.toDatalessObject(); + }, + + /** + * Returns object representation of canvas + * @method toObject + * @return {Object} + */ + toObject: function () { + return this._toObjectMethod('toObject'); + }, + + /** + * Returns dataless object representation of canvas + * @method toDatalessObject + * @return {Object} + */ + toDatalessObject: function () { + return this._toObjectMethod('toDatalessObject'); + }, + + /** + * @private + * @method _toObjectMethod + */ + _toObjectMethod: function (methodName) { + var data = { + objects: this._objects.map(function (instance) { + // TODO (kangax): figure out how to clean this up + 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 + }; + if (this.backgroundImage) { + data.backgroundImage = this.backgroundImage.src; + data.backgroundImageOpacity = this.backgroundImageOpacity; + data.backgroundImageStretch = this.backgroundImageStretch; + } + return data; + }, + + /** + * Returns SVG representation of canvas + * @function + * @method toSVG + * @return {String} + */ + toSVG: function() { + var markup = [ + '<?xml version="1.0" standalone="no" ?>', + '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" ', + '"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">', + '<svg ', + 'xmlns="http://www.w3.org/2000/svg" ', + 'xmlns:xlink="http://www.w3.org/1999/xlink" ', + 'version="1.1" ', + 'width="', this.width, '" ', + 'height="', this.height, '" ', + 'xml:space="preserve">', + '<desc>Created with Fabric.js ', fabric.version, '</desc>', + fabric.createSVGFontFacesMarkup(this.getObjects()) + ]; + + if (this.backgroundImage) { + markup.push( + '<image x="0" y="0" ', + 'width="', this.width, + '" height="', this.height, + '" preserveAspectRatio="', (this.backgroundImageStretch ? 'none' : 'defer'), + '" xlink:href="', this.backgroundImage.src, + '" style="opacity:', this.backgroundImageOpacity, + '"></image>' + ); + } + + for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push('</svg>'); + + return markup.join(''); + }, + + /** + * Returns true if canvas contains no objects + * @method isEmpty + * @return {Boolean} true if canvas is empty + */ + isEmpty: function () { + return this._objects.length === 0; + }, + + /** + * Removes an object from canvas and returns it + * @method remove + * @param object {Object} Object to remove + * @return {Object} removed object + */ + remove: function (object) { + removeFromArray(this._objects, object); + if (this.getActiveObject() === object) { + + // removing active object should fire "selection:cleared" events + this.fire('before:selection:cleared', { target: object }); + this.discardActiveObject(); + this.fire('selection:cleared'); + } + this.renderAll(); + return object; + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @param object {fabric.Object} Object to send to back + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendToBack: function (object) { + removeFromArray(this._objects, object); + this._objects.unshift(object); + return this.renderAll(); + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringToFront: function (object) { + removeFromArray(this._objects, object); + this._objects.push(object); + return this.renderAll(); + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + sendBackwards: function (object) { + var idx = this._objects.indexOf(object), + nextIntersectingIdx = idx; + + // if object is not on the bottom of stack + if (idx !== 0) { + + // traverse down the stack looking for the nearest intersecting object + for (var i=idx-1; i>=0; --i) { + if (object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(this._objects, object); + this._objects.splice(nextIntersectingIdx, 0, object); + } + return this.renderAll(); + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @param object {fabric.Object} Object to send + * @return {fabric.Canvas} thisArg + * @chainable + */ + bringForward: function (object) { + var objects = this.getObjects(), + idx = objects.indexOf(object), + nextIntersectingIdx = idx; + + + // if object is not on top of stack (last item in an array) + if (idx !== objects.length-1) { + + // traverse up the stack looking for the nearest intersecting object + for (var i = idx + 1, l = this._objects.length; i < l; ++i) { + if (object.intersectsWithObject(objects[i]) || object.isContainedWithinObject(this._objects[i])) { + nextIntersectingIdx = i; + break; + } + } + removeFromArray(objects, object); + objects.splice(nextIntersectingIdx, 0, object); + } + this.renderAll(); + }, + + /** + * Returns object at specified index + * @method item + * @param {Number} index + * @return {fabric.Object} + */ + item: function (index) { + return this.getObjects()[index]; + }, + + /** + * Returns number representation of an instance complexity + * @method complexity + * @return {Number} complexity + */ + complexity: function () { + return this.getObjects().reduce(function (memo, current) { + memo += current.complexity ? current.complexity() : 0; + return memo; + }, 0); + }, + + /** + * Iterates over all objects, invoking callback for each one of them + * @method forEachObject + * @return {fabric.Canvas} thisArg + */ + forEachObject: function(callback, context) { + var objects = this.getObjects(), + i = objects.length; + while (i--) { + callback.call(context, objects[i], i, objects); + } + return this; + }, + + /** + * Clears a canvas element and removes all event handlers. + * @method dispose + * @return {fabric.Canvas} thisArg + * @chainable + */ + dispose: function () { + this.clear(); + if (this.interactive) { + removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + removeListener(fabric.window, 'resize', this._onResize); + } + return this; + }, + + /** + * @private + * @method _resizeImageToFit + * @param {HTMLImageElement} imgEl + */ + _resizeImageToFit: function (imgEl) { + + var imageWidth = imgEl.width || imgEl.offsetWidth, + widthScaleFactor = this.getWidth() / imageWidth; + + // scale image down so that it has original dimensions when printed in large resolution + if (imageWidth) { + imgEl.width = imageWidth * widthScaleFactor; + } + } + }); + + /** + * Returns a string representation of an instance + * @method toString + * @return {String} string representation of an instance + */ + fabric.StaticCanvas.prototype.toString = function () { // Assign explicitly since `extend` doesn't take care of DontEnum bug yet + return '#<fabric.Canvas (' + this.complexity() + '): '+ + '{ objects: ' + this.getObjects().length + ' }>'; + }; + + extend(fabric.StaticCanvas, /** @scope fabric.StaticCanvas */ { + + /** + * @static + * @property EMPTY_JSON + * @type String + */ + EMPTY_JSON: '{"objects": [], "background": "white"}', + + /** + * Takes <canvas> element and transforms its data in such way that it becomes grayscale + * @static + * @method toGrayscale + * @param {HTMLCanvasElement} canvasEl + */ + toGrayscale: function (canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * Provides a way to check support of some of the canvas methods + * (either those of HTMLCanvasElement itself, or rendering context) + * + * @method supports + * @param methodName {String} Method to check support for; + * Could be one of "getImageData", "toDataURL" or "toDataURLWithQuality" + * @return {Boolean | null} `true` if method is supported (or at least exists), + * `null` if canvas element or context can not be initialized + */ + supports: function (methodName) { + var el = fabric.document.createElement('canvas'); + + if (typeof G_vmlCanvasManager !== 'undefined') { + G_vmlCanvasManager.initElement(el); + } + if (!el || !el.getContext) { + return null; + } + + var ctx = el.getContext('2d'); + if (!ctx) { + return null; + } + + switch (methodName) { + + case 'getImageData': + return typeof ctx.getImageData !== 'undefined'; + + case 'toDataURL': + return typeof el.toDataURL !== 'undefined'; + + case 'toDataURLWithQuality': + try { + el.toDataURL('image/jpeg', 0); + return true; + } catch (e) { + return false; + } + + default: + return null; + } + } + }); + + /** + * Returs JSON representation of canvas + * @function + * @method toJSON + * @return {String} json string + */ + fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; + +})(typeof exports != 'undefined' ? exports : this); + +(function() { + + var extend = fabric.util.object.extend, + getPointer = fabric.util.getPointer, + addListener = fabric.util.addListener, + removeListener = fabric.util.removeListener, + cursorMap = { + 'tr': 'ne-resize', + 'br': 'se-resize', + 'bl': 'sw-resize', + 'tl': 'nw-resize', + 'ml': 'w-resize', + 'mt': 'n-resize', + 'mr': 'e-resize', + 'mb': 's-resize' + }, + + utilMin = fabric.util.array.min, + utilMax = fabric.util.array.max, + + sqrt = Math.sqrt, + pow = Math.pow, + atan2 = Math.atan2, + abs = Math.abs, + min = Math.min, + max = Math.max, + + STROKE_OFFSET = 0.5; + + /** + * @class fabric.Canvas + * @constructor + * @extends fabric.StaticCanvas + * @param {HTMLElement | String} el <canvas> element to initialize instance on + * @param {Object} [options] Options object + */ + fabric.Canvas = function(el, options) { + options || (options = { }); + + this._initStatic(el, options); + this._initInteractive(); + this._createCacheCanvas(); + + fabric.Canvas.activeInstance = this; + }; + + function ProtoProxy(){ } + ProtoProxy.prototype = fabric.StaticCanvas.prototype; + fabric.Canvas.prototype = new ProtoProxy; + + var InteractiveMethods = /** @scope fabric.Canvas.prototype */ { + + /** + * Indicates that canvas is interactive. This property should not be changed. + * @property + * @type Boolean + */ + interactive: true, + + /** + * Indicates whether group selection should be enabled + * @property + * @type Boolean + */ + selection: true, + + /** + * Color of selection + * @property + * @type String + */ + selectionColor: 'rgba(100, 100, 255, 0.3)', // blue + + /** + * Color of the border of selection (usually slightly darker than color of selection itself) + * @property + * @type String + */ + selectionBorderColor: 'rgba(255, 255, 255, 0.3)', + + /** + * Width of a line used in object/group selection + * @property + * @type Number + */ + selectionLineWidth: 1, + + /** + * Color of the line used in free drawing mode + * @property + * @type String + */ + freeDrawingColor: 'rgb(0, 0, 0)', + + /** + * Width of a line used in free drawing mode + * @property + * @type Number + */ + freeDrawingLineWidth: 1, + + /** + * Default cursor value used when hovering over an object on canvas + * @property + * @type String + */ + hoverCursor: 'move', + + /** + * Default cursor value used when moving an object on canvas + * @property + * @type String + */ + moveCursor: 'move', + + /** + * Default cursor value used for the entire canvas + * @property + * @type String + */ + defaultCursor: 'default', + + /** + * Cursor value used for rotation point + * @property + * @type String + */ + rotationCursor: 'crosshair', + + /** + * Default element class that's given to wrapper (div) element of canvas + * @property + * @type String + */ + containerClass: 'canvas-container', + + perPixelTargetFind: false, + + targetFindTolerance: 0, + + _initInteractive: function() { + this._currentTransform = null; + this._groupSelector = null; + this._freeDrawingXPoints = [ ]; + this._freeDrawingYPoints = [ ]; + this._initWrapperElement(); + this._createUpperCanvas(); + this._initEvents(); + this.calcOffset(); + }, + + /** + * Adds mouse listeners to canvas + * @method _initEvents + * @private + * See configuration documentation for more details. + */ + _initEvents: function () { + var _this = this; + + this._onMouseDown = function (e) { + _this.__onMouseDown(e); + + addListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp); + + addListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove); + + removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseUp = function (e) { + _this.__onMouseUp(e); + + removeListener(fabric.document, 'mouseup', _this._onMouseUp); + fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp); + + removeListener(fabric.document, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove); + + addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove); + fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove); + }; + + this._onMouseMove = function (e) { + e.preventDefault && e.preventDefault(); + _this.__onMouseMove(e); + }; + + this._onResize = function (e) { + _this.calcOffset(); + }; + + + addListener(fabric.window, 'resize', this._onResize); + + if (fabric.isTouchSupported) { + addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); + addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); + } + else { + addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); + addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); + } + }, + + /** + * Method that defines the actions when mouse is released on canvas. + * The method resets the currentTransform parameters, store the image corner + * position in the image object and render the canvas on top. + * @method __onMouseUp + * @param {Event} e Event object fired on mouseup + * + */ + __onMouseUp: function (e) { + + if (this.isDrawingMode && this._isCurrentlyDrawing) { + this._finalizeDrawingPath(); + this.fire('mouse:up', { e: e }); + return; + } + + if (this._currentTransform) { + + var transform = this._currentTransform, + target = transform.target; + + if (target._scaling) { + target._scaling = false; + } + + // determine the new coords everytime the image changes its position + var i = this._objects.length; + while (i--) { + this._objects[i].setCoords(); + } + + // only fire :modified event if target coordinates were changed during mousedown-mouseup + if (this.stateful && target.hasStateChanged()) { + target.isMoving = false; + this.fire('object:modified', { target: target }); + target.fire('modified'); + } + } + + this._currentTransform = null; + + if (this._groupSelector) { + // group selection was completed, determine its bounds + this._findSelectedObjects(e); + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + activeGroup.setObjectsCoords(); + activeGroup.set('isMoving', false); + this._setCursor(this.defaultCursor); + } + + // clear selection + this._groupSelector = null; + this.renderAll(); + + this._setCursorFromEvent(e, target); + + // fix for FF + this._setCursor(''); + + var _this = this; + setTimeout(function () { + _this._setCursorFromEvent(e, target); + }, 50); + + this.fire('mouse:up', { target: target, e: e }); + target && target.fire('mouseup', { e: e }) + }, + + /** + * Method that defines the actions when mouse is clic ked on canvas. + * The method inits the currentTransform parameters and renders all the + * canvas so the current image can be placed on the top canvas and the rest + * in on the container one. + * @method __onMouseDown + * @param e {Event} Event object fired on mousedown + * + */ + __onMouseDown: function (e) { + + // accept only left clicks + var isLeftClick = 'which' in e ? e.which == 1 : e.button == 1; + if (!isLeftClick && !fabric.isTouchSupported) return; + + if (this.isDrawingMode) { + this._prepareForDrawing(e); + + // capture coordinates immediately; this allows to draw dots (when movement never occurs) + this._captureDrawingPath(e); + this.fire('mouse:down', { e: e }); + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) return; + + var target = this.findTarget(e), + pointer = this.getPointer(e), + activeGroup = this.getActiveGroup(), + corner; + + if (this._shouldClearSelection(e)) { + + this._groupSelector = { + ex: pointer.x, + ey: pointer.y, + top: 0, + left: 0 + }; + + this.deactivateAllWithDispatch(); + } + else { + // determine if it's a drag or rotate case + // rotate and scale will happen at the same time + this.stateful && target.saveState(); + + if (corner = target._findTargetCorner(e, this._offset)) { + this.onBeforeScaleRotate(target); + } + + this._setupCurrentTransform(e, target); + + var shouldHandleGroupLogic = e.shiftKey && (activeGroup || this.getActiveObject()) && this.selection; + if (shouldHandleGroupLogic) { + this._handleGroupLogic(e, target); + } + else { + if (target !== this.getActiveGroup()) { + this.deactivateAll(); + } + this.setActiveObject(target, e); + } + } + // we must renderAll so that active image is placed on the top canvas + this.renderAll(); + + this.fire('mouse:down', { target: target, e: e }); + target && target.fire('mousedown', { e: e }); + }, + + /** + * Method that defines the actions when mouse is hovering the canvas. + * The currentTransform parameter will definde whether the user is rotating/scaling/translating + * an image or neither of them (only hovering). A group selection is also possible and would cancel + * all any other type of action. + * In case of an image transformation only the top canvas will be rendered. + * @method __onMouseMove + * @param e {Event} Event object fired on mousemove + * + */ + __onMouseMove: function (e) { + + if (this.isDrawingMode) { + if (this._isCurrentlyDrawing) { + this._captureDrawingPath(e); + } + this.fire('mouse:move', { e: e }); + return; + } + + var groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection. + 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) { + + // alias style to elimintate unnecessary lookup + var style = this.upperCanvasEl.style; + + // Here we are hovering the canvas then we will determine + // what part of the pictures we are hovering to change the caret symbol. + // We won't do that while dragging or rotating in order to improve the + // performance. + var target = this.findTarget(e); + + if (!target) { + // image/text was hovered-out from, we remove its borders + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && !this._objects[i].active) { + this._objects[i].setActive(false); + } + } + style.cursor = this.defaultCursor; + } + else { + // set proper cursor + this._setCursorFromEvent(e, target); + if (target.isActive()) { + // display corners when hovering over an image + target.setCornersVisibility && target.setCornersVisibility(true); + } + } + } + else { + // object is being transformed (scaled/rotated/moved/etc.) + var pointer = getPointer(e), + x = pointer.x, + y = pointer.y; + + this._currentTransform.target.isMoving = true; + + if (this._currentTransform.action === 'rotate') { + // rotate object only if shift key is not pressed + // and if it is not a group we are transforming + + if (!e.shiftKey) { + this._rotateObject(x, y); + + this.fire('object:rotating', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('rotating'); + } + if (!this._currentTransform.target.hasRotatingPoint) { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + } + else if (this._currentTransform.action === 'scale') { + this._scaleObject(x, y); + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleX') { + this._scaleObject(x, y, 'x'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else if (this._currentTransform.action === 'scaleY') { + this._scaleObject(x, y, 'y'); + + this.fire('object:scaling', { + target: this._currentTransform.target + }); + this._currentTransform.target.fire('scaling'); + } + else { + this._translateObject(x, y); + + this.fire('object:moving', { + target: this._currentTransform.target + }); + + this._setCursor(this.moveCursor); + + this._currentTransform.target.fire('moving'); + } + // only commit here. when we are actually moving the pictures + this.renderAll(); + } + this.fire('mouse:move', { target: target, e: e }); + target && target.fire('mousemove', { e: e }); + }, + + /** + * Applies one implementation of 'point inside polygon' algorithm + * @method containsPoint + * @param e { Event } event object + * @param target { fabric.Object } object to test against + * @return {Boolean} true if point contains within area of given object + */ + containsPoint: function (e, target) { + var pointer = this.getPointer(e), + xy = this._normalizePointer(target, pointer), + x = xy.x, + y = xy.y; + + // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html + // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html + + // we iterate through each object. If target found, return it. + var iLines = target._getImageLines(target.oCoords), + xpoints = target._findCrossPoints(x, y, iLines); + + // if xcount is odd then we clicked inside the object + // For the specific case of square images xcount === 1 in all true cases + if ((xpoints && xpoints % 2 === 1) || target._findTargetCorner(e, this._offset)) { + return true; + } + return false; + }, + + /** + * @private + * @method _normalizePointer + */ + _normalizePointer: function (object, pointer) { + + var activeGroup = this.getActiveGroup(), + x = pointer.x, + y = pointer.y; + + var isObjectInGroup = ( + activeGroup && + object.type !== 'group' && + activeGroup.contains(object) + ); + + if (isObjectInGroup) { + x -= activeGroup.left; + y -= activeGroup.top; + } + return { x: x, y: y }; + }, + + _isTargetTransparent: function (target, x, y) { + var cacheContext = this.contextCache; + + var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; + target.hasBorders = target.transparentCorners = false; + + this._draw(cacheContext, target); + + target.hasBorders = hasBorders; + target.transparentCorners = transparentCorners; + + // If tolerance is > 0 adjust start coords to take into account. If moves off Canvas fix to 0 + if (this.targetFindTolerance > 0) { + if (x > this.targetFindTolerance) { + x -= this.targetFindTolerance; + } + else { + x = 0; + } + if (y > this.targetFindTolerance) { + y -= this.targetFindTolerance; + } + else { + y = 0; + } + } + + var isTransparent = true; + var imageData = cacheContext.getImageData( + x, y, (this.targetFindTolerance * 2) || 1, (this.targetFindTolerance * 2) || 1); + + // Split image data - for tolerance > 1, pixelDataSize = 4; + for (var i = 3; i < imageData.data.length; i += 4) { + var temp = imageData.data[i]; + isTransparent = temp <= 0; + if (isTransparent === false) break; //Stop if colour found + } + + imageData = null; + this.clearContext(cacheContext); + return isTransparent; + }, + + /** + * @private + * @method _shouldClearSelection + */ + _shouldClearSelection: function (e) { + var target = this.findTarget(e), + activeGroup = this.getActiveGroup(); + return ( + !target || ( + target && + activeGroup && + !activeGroup.contains(target) && + activeGroup !== target && + !e.shiftKey + ) + ); + }, + + /** + * @private + * @method _setupCurrentTransform + */ + _setupCurrentTransform: function (e, target) { + var action = 'drag', + corner, + pointer = getPointer(e); + + if (corner = target._findTargetCorner(e, this._offset)) { + action = (corner === 'ml' || corner === 'mr') + ? 'scaleX' + : (corner === 'mt' || corner === 'mb') + ? 'scaleY' + : corner === 'mtr' + ? 'rotate' + : (target.hasRotatingPoint) + ? 'scale' + : 'rotate'; + } + + this._currentTransform = { + target: target, + action: action, + scaleX: target.scaleX, + scaleY: target.scaleY, + offsetX: pointer.x - target.left, + offsetY: pointer.y - target.top, + ex: pointer.x, + ey: pointer.y, + left: target.left, + top: target.top, + theta: target._theta, + width: target.width * target.scaleX + }; + + this._currentTransform.original = { + left: target.left, + top: target.top + }; + }, + + _handleGroupLogic: function (e, target) { + if (target === this.getActiveGroup()) { + // if it's a group, find target again, this time skipping group + target = this.findTarget(e, true); + // if even object is not found, bail out + if (!target || target.isType('group')) { + return; + } + } + var activeGroup = this.getActiveGroup(); + if (activeGroup) { + if (activeGroup.contains(target)) { + activeGroup.removeWithUpdate(target); + target.setActive(false); + if (activeGroup.size() === 1) { + // remove group alltogether if after removal it only contains 1 object + this.discardActiveGroup(); + } + } + else { + activeGroup.addWithUpdate(target); + } + this.fire('selection:created', { target: activeGroup, e: e }); + activeGroup.setActive(true); + } + else { + // group does not exist + if (this._activeObject) { + // only if there's an active object + if (target !== this._activeObject) { + // and that object is not the actual target + var group = new fabric.Group([ this._activeObject, target ]); + this.setActiveGroup(group); + activeGroup = this.getActiveGroup(); + } + } + // activate target object in any case + target.setActive(true); + } + + if (activeGroup) { + activeGroup.saveCoords(); + } + }, + + /** + * @private + * @method _prepareForDrawing + */ + _prepareForDrawing: function(e) { + + this._isCurrentlyDrawing = true; + + this.discardActiveObject().renderAll(); + + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.length = this._freeDrawingYPoints.length = 0; + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.beginPath(); + this.contextTop.moveTo(pointer.x, pointer.y); + this.contextTop.strokeStyle = this.freeDrawingColor; + this.contextTop.lineWidth = this.freeDrawingLineWidth; + this.contextTop.lineCap = this.contextTop.lineJoin = 'round'; + }, + + /** + * @private + * @method _captureDrawingPath + */ + _captureDrawingPath: function(e) { + var pointer = this.getPointer(e); + + this._freeDrawingXPoints.push(pointer.x); + this._freeDrawingYPoints.push(pointer.y); + + this.contextTop.lineTo(pointer.x, pointer.y); + this.contextTop.stroke(); + }, + + /** + * @private + * @method _finalizeDrawingPath + */ + _finalizeDrawingPath: function() { + + this.contextTop.closePath(); + + this._isCurrentlyDrawing = false; + + var minX = utilMin(this._freeDrawingXPoints), + minY = utilMin(this._freeDrawingYPoints), + maxX = utilMax(this._freeDrawingXPoints), + maxY = utilMax(this._freeDrawingYPoints), + 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, ' '); + } + + // TODO (kangax): maybe remove Path creation from here, to decouple fabric.Canvas from fabric.Path, + // and instead fire something like "drawing:completed" event with path string + + path = path.join(''); + + if (path === "M 0 0 L 0 0 ") { + // do not create 0 width/height paths, as they are rendered inconsistently across browsers + // Firefox 4, for example, renders a dot, whereas Chrome 10 renders nothing + this.renderAll(); + return; + } + + var p = new fabric.Path(path); + + p.fill = null; + p.stroke = this.freeDrawingColor; + p.strokeWidth = this.freeDrawingLineWidth; + this.add(p); + p.set("left", minX + (maxX - minX) / 2).set("top", minY + (maxY - minY) / 2).setCoords(); + this.renderAll(); + this.fire('path:created', { path: p }); + }, + + /** + * Translates object by "setting" its left/top + * @method _translateObject + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _translateObject: function (x, y) { + var target = this._currentTransform.target; + target.lockMovementX || target.set('left', x - this._currentTransform.offsetX); + target.lockMovementY || target.set('top', y - this._currentTransform.offsetY); + }, + + /** + * Scales object by invoking its scaleX/scaleY methods + * @method _scaleObject + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + * @param by {String} Either 'x' or 'y' - specifies dimension constraint by which to scale an object. + * When not provided, an object is scaled by both dimensions equally + */ + _scaleObject: function (x, y, by) { + var t = this._currentTransform, + offset = this._offset, + target = t.target; + + if (target.lockScalingX && target.lockScalingY) return; + + var lastLen = sqrt(pow(t.ey - t.top - offset.top, 2) + pow(t.ex - t.left - offset.left, 2)), + curLen = sqrt(pow(y - t.top - offset.top, 2) + pow(x - t.left - offset.left, 2)); + + target._scaling = true; + + if (!by) { + target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); + } + else if (by === 'x' && !target.lockUniScaling) { + target.lockScalingX || target.set('scaleX', t.scaleX * curLen/lastLen); + } + else if (by === 'y' && !target.lockUniScaling) { + target.lockScalingY || target.set('scaleY', t.scaleY * curLen/lastLen); + } + }, + + /** + * Rotates object by invoking its rotate method + * @method _rotateObject + * @param x {Number} pointer's x coordinate + * @param y {Number} pointer's y coordinate + */ + _rotateObject: function (x, y) { + + var t = this._currentTransform, + o = this._offset; + + if (t.target.lockRotation) return; + + var lastAngle = atan2(t.ey - t.top - o.top, t.ex - t.left - o.left), + curAngle = atan2(y - t.top - o.top, x - t.left - o.left); + + t.target._theta = (curAngle - lastAngle) + t.theta; + }, + + /** + * @method _setCursor + */ + _setCursor: function (value) { + this.upperCanvasEl.style.cursor = value; + }, + + /** + * Sets the cursor depending on where the canvas is being hovered. + * Note: very buggy in Opera + * @method _setCursorFromEvent + * @param e {Event} Event object + * @param target {Object} Object that the mouse is hovering, if so. + */ + _setCursorFromEvent: function (e, target) { + var s = this.upperCanvasEl.style; + if (!target) { + s.cursor = this.defaultCursor; + return false; + } + else { + var activeGroup = this.getActiveGroup(); + // only show proper corner when group selection is not active + var corner = !!target._findTargetCorner + && (!activeGroup || !activeGroup.contains(target)) + && target._findTargetCorner(e, this._offset); + + if (!corner) { + s.cursor = this.hoverCursor; + } + else { + if (corner in cursorMap) { + s.cursor = cursorMap[corner]; + } else if (corner === 'mtr' && target.hasRotatingPoint) { + s.cursor = this.rotationCursor; + } else { + s.cursor = this.defaultCursor; + return false; + } + } + } + return true; + }, + + /** + * @method _drawSelection + * @private + */ + _drawSelection: function () { + var groupSelector = this._groupSelector, + left = groupSelector.left, + top = groupSelector.top, + aleft = abs(left), + atop = abs(top); + + this.contextTop.fillStyle = this.selectionColor; + + this.contextTop.fillRect( + groupSelector.ex - ((left > 0) ? 0 : -left), + groupSelector.ey - ((top > 0) ? 0 : -top), + aleft, + atop + ); + + this.contextTop.lineWidth = this.selectionLineWidth; + this.contextTop.strokeStyle = this.selectionBorderColor; + + this.contextTop.strokeRect( + groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), + groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), + aleft, + atop + ); + }, + + _findSelectedObjects: function (e) { + var 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) continue; + + if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || + currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) { + + if (this.selection && currentObject.selectable) { + currentObject.setActive(true); + group.push(currentObject); + } + } + } + + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } + else if (group.length > 1) { + var group = new fabric.Group(group); + this.setActiveGroup(group); + group.saveCoords(); + this.fire('selection:created', { target: group }); + } + + this.renderAll(); + }, + + /** + * Method that determines what object we are clicking on + * @method findTarget + * @param {Event} e mouse event + * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through + */ + findTarget: function (e, skipGroup) { + + var target, + pointer = this.getPointer(e); + + // first check current group (if one exists) + var activeGroup = this.getActiveGroup(); + + if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { + target = activeGroup; + return target; + } + + // then check all of the objects on canvas + // Cache all targets where their bounding box contains point. + var possibleTargets = []; + for (var i = this._objects.length; i--; ) { + if (this._objects[i] && this.containsPoint(e, this._objects[i])) { + if (this.perPixelTargetFind || this._objects[i].perPixelTargetFind) { + possibleTargets[possibleTargets.length] = this._objects[i]; + } + else { + target = this._objects[i]; + this.relatedTarget = target; + break; + } + } + } + for (var i = 0, len = possibleTargets.length; i < len; i++) { + var pointer = this.getPointer(e); + var isTransparent = this._isTargetTransparent(possibleTargets[i], pointer.x, pointer.y); + if (!isTransparent) { + target = possibleTargets[i]; + this.relatedTarget = target; + break; + } + } + if (target && target.selectable) { + return target; + } + }, + + /** + * Returns pointer coordinates relative to canvas. + * @method getPointer + * @return {Object} object with "x" and "y" number values + */ + getPointer: function (e) { + var pointer = getPointer(e); + return { + x: pointer.x - this._offset.left, + y: pointer.y - this._offset.top + }; + }, + + /** + * @method _createUpperCanvas + * @param {HTMLElement|String} canvasEl Canvas element + * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized + */ + _createUpperCanvas: function () { + this.upperCanvasEl = this._createCanvasElement(); + this.upperCanvasEl.className = 'upper-canvas'; + + this.wrapperEl.appendChild(this.upperCanvasEl); + + this._applyCanvasStyle(this.upperCanvasEl); + this.contextTop = this.upperCanvasEl.getContext('2d'); + }, + + _createCacheCanvas: function () { + this.cacheCanvasEl = this._createCanvasElement(); + this.cacheCanvasEl.setAttribute('width', this.width); + this.cacheCanvasEl.setAttribute('height', this.height); + this.contextCache = this.cacheCanvasEl.getContext('2d'); + }, + + /** + * @private + * @method _initWrapperElement + * @param {Number} width + * @param {Number} height + */ + _initWrapperElement: function () { + this.wrapperEl = fabric.util.wrapElement(this.lowerCanvasEl, 'div', { + 'class': this.containerClass + }); + fabric.util.setStyle(this.wrapperEl, { + width: this.getWidth() + 'px', + height: this.getHeight() + 'px', + position: 'relative' + }); + fabric.util.makeElementUnselectable(this.wrapperEl); + }, + + /** + * @private + * @method _applyCanvasStyle + * @param {Element} element + */ + _applyCanvasStyle: function (element) { + var width = this.getWidth() || element.width, + height = this.getHeight() || element.height; + + fabric.util.setStyle(element, { + position: 'absolute', + width: width + 'px', + height: height + 'px', + left: 0, + top: 0 + }); + element.width = width; + element.height = height; + fabric.util.makeElementUnselectable(element); + }, + + /** + * Returns context of canvas where object selection is drawn + * @method getSelectionContext + * @return {CanvasRenderingContext2D} + */ + getSelectionContext: function() { + return this.contextTop; + }, + + /** + * Returns <canvas> element on which object selection is drawn + * @method getSelectionElement + * @return {HTMLCanvasElement} + */ + getSelectionElement: function () { + return this.upperCanvasEl; + }, + + /** + * Sets given object as active + * @method setActiveObject + * @param object {fabric.Object} Object to set as an active one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveObject: function (object, e) { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = object; + object.setActive(true); + + this.renderAll(); + + this.fire('object:selected', { target: object, e: e }); + object.fire('selected', { e: e }); + return this; + }, + + /** + * Returns currently active object + * @method getActiveObject + * @return {fabric.Object} active object + */ + getActiveObject: function () { + return this._activeObject; + }, + + /** + * Discards currently active object + * @method discardActiveObject + * @return {fabric.Canvas} thisArg + * @chainable + */ + discardActiveObject: function () { + if (this._activeObject) { + this._activeObject.setActive(false); + } + this._activeObject = null; + return this; + }, + + /** + * Sets active group to a speicified one + * @method setActiveGroup + * @param {fabric.Group} group Group to set as a current one + * @return {fabric.Canvas} thisArg + * @chainable + */ + setActiveGroup: function (group) { + this._activeGroup = group; + group && group.setActive(true); + return this; + }, + + /** + * Returns currently active group + * @method getActiveGroup + * @return {fabric.Group} Current group + */ + getActiveGroup: function () { + return this._activeGroup; + }, + + /** + * Removes currently active group + * @method discardActiveGroup + * @return {fabric.Canvas} thisArg + */ + discardActiveGroup: function () { + var g = this.getActiveGroup(); + if (g) { + g.destroy(); + } + return this.setActiveGroup(null); + }, + + /** + * Deactivates all objects by calling their setActive(false) + * @method deactivateAll + * @return {fabric.Canvas} thisArg + */ + deactivateAll: function () { + var allObjects = this.getObjects(), + i = 0, + len = allObjects.length; + for ( ; i < len; i++) { + allObjects[i].setActive(false); + } + this.discardActiveGroup(); + this.discardActiveObject(); + return this; + }, + + /** + * Deactivates all objects and dispatches appropriate events + * @method deactivateAllWithDispatch + * @return {fabric.Canvas} thisArg + */ + deactivateAllWithDispatch: function () { + var activeObject = this.getActiveGroup() || this.getActiveObject(); + if (activeObject) { + this.fire('before:selection:cleared', { target: activeObject }); + } + this.deactivateAll(); + if (activeObject) { + this.fire('selection:cleared'); + } + return this; + } + }; + + fabric.Canvas.prototype.toString = fabric.StaticCanvas.prototype.toString; + extend(fabric.Canvas.prototype, InteractiveMethods); + + // iterating manually to workaround Opera's bug + // where "prototype" property is enumerable and overrides existing prototype + for (var prop in fabric.StaticCanvas) { + if (prop !== 'prototype') { + fabric.Canvas[prop] = fabric.StaticCanvas[prop]; + } + } + + if (fabric.isTouchSupported) { + fabric.Canvas.prototype._setCursorFromEvent = function() { }; + } + + /** + * @class fabric.Element + * @alias fabric.Canvas + * @deprecated + * @constructor + */ + fabric.Element = fabric.Canvas; +})(); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + FX_DURATION: 500, + + /** + * Centers object horizontally with animation. + * @method fxCenterObjectH + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectH: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.get('left'), + endValue: this.getCenter().left, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('left', value); + _this.renderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Centers object vertically with animation. + * @method fxCenterObjectV + * @param {fabric.Object} object Object to center + * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxCenterObjectV: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.get('top'), + endValue: this.getCenter().top, + duration: this.FX_DURATION, + onChange: function(value) { + object.set('top', value); + _this.renderAll(); + onChange(); + }, + onComplete: function() { + object.setCoords(); + onComplete(); + } + }); + + return this; + }, + + /** + * Same as `fabric.Canvas#remove` but animated + * @method fxRemove + * @param {fabric.Object} object Object to remove + * @param {Function} callback Callback, invoked on effect completion + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxRemove: function (object, callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: object.get('opacity'), + endValue: 0, + duration: this.FX_DURATION, + onStart: function() { + object.setActive(false); + }, + onChange: function(value) { + object.set('opacity', value); + _this.renderAll(); + onChange(); + }, + onComplete: function () { + _this.remove(object); + onComplete(); + } + }); + + return this; + } +}); +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Populates canvas with data from the specified dataless JSON + * JSON format must conform to the one of `fabric.Canvas#toDatalessJSON` + * @method loadFromDatalessJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} instance + * @chainable + */ + loadFromDatalessJSON: function (json, callback) { + + if (!json) { + return; + } + + // serialize if it wasn't already + var serialized = (typeof json === 'string') + ? JSON.parse(json) + : json; + + if (!serialized || (serialized && !serialized.objects)) return; + + this.clear(); + + // TODO: test this + this.backgroundColor = serialized.background; + this._enlivenDatalessObjects(serialized.objects, callback); + }, + + /** + * @method _enlivenDatalessObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenDatalessObjects: function (objects, callback) { + + /** @ignore */ + function onObjectLoaded(object, index) { + _this.insertAt(object, index, true); + object.setCoords(); + if (++numLoadedObjects === numTotalObjects) { + callback && callback(); + } + } + + 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': + 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) { + // restore path + if (path) { + obj[pathProp] = path; + } + onObjectLoaded(klass.fromObject(obj), index); + } + break; + } + } + else { + if (obj.type === 'image') { + fabric.util.loadImage(path, function (image) { + var oImg = new fabric.Image(image); + + oImg.setSourcePath(path); + + fabric.util.object.extend(oImg, obj); + oImg.setAngle(obj.angle); + + onObjectLoaded(oImg, index); + }); + } + else if (obj.type === 'text') { + + if (obj.useNative) { + onObjectLoaded(fabric.Text.fromObject(obj), index); + } + else { + obj.path = path; + var object = fabric.Text.fromObject(obj); + var onscriptload = function () { + // TODO (kangax): find out why Opera refuses to work without this timeout + if (Object.prototype.toString.call(fabric.window.opera) === '[object Opera]') { + setTimeout(function () { + onObjectLoaded(object, index); + }, 500); + } + else { + onObjectLoaded(object, index); + } + } + + fabric.util.getScript(path, onscriptload); + } + } + else { + fabric.loadSVGFromURL(path, function (elements, options) { + var object = fabric.util.groupSVGElements(elements, obj, path); + + // copy parameters from serialied json to object (left, top, scaleX, scaleY, etc.) + // skip this step if an object is a PathGroup, since we already passed it options object before + if (!(object instanceof fabric.PathGroup)) { + fabric.util.object.extend(object, obj); + if (typeof obj.angle !== 'undefined') { + object.setAngle(obj.angle); + } + } + + onObjectLoaded(object, index); + }); + } + } + }, this); + } + catch(e) { + fabric.log(e.message); + } + }, + + /** + * Populates canvas with data from the specified JSON + * JSON format must conform to the one of `fabric.Canvas#toJSON` + * @method loadFromJSON + * @param {String} json JSON string + * @param {Function} callback Callback, invoked when json is parsed + * and corresponding objects (e.g: fabric.Image) + * are initialized + * @return {fabric.Canvas} instance + * @chainable + */ + loadFromJSON: function (json, callback) { + if (!json) return; + + var serialized = JSON.parse(json); + if (!serialized || (serialized && !serialized.objects)) return; + + this.clear(); + var _this = this; + this._enlivenObjects(serialized.objects, function () { + _this.backgroundColor = serialized.background; + + if (serialized.backgroundImage) { + _this.setBackgroundImage(serialized.backgroundImage, function() { + + _this.backgroundImageOpacity = serialized.backgroundImageOpacity; + _this.backgroundImageStretch = serialized.backgroundImageStretch; + + _this.renderAll(); + + callback && callback(); + }); + } + else { + callback && callback(); + } + }); + + return this; + }, + + /** + * @method _enlivenObjects + * @param {Array} objects + * @param {Function} callback + */ + _enlivenObjects: function (objects, callback) { + var _this = this; + fabric.util.enlivenObjects(objects, function(enlivenedObjects) { + enlivenedObjects.forEach(function(obj, index) { + _this.insertAt(obj, index, true); + }); + callback && callback(); + }); + }, + + /** + * @private + * @method _toDataURL + * @param {String} format + * @param {Function} callback + */ + _toDataURL: function (format, callback) { + this.clone(function (clone) { + callback(clone.toDataURL(format)); + }); + }, + + /** + * @private + * @method _toDataURLWithMultiplier + * @param {String} format + * @param {Number} multiplier + * @param {Function} callback + */ + _toDataURLWithMultiplier: function (format, multiplier, callback) { + this.clone(function (clone) { + callback(clone.toDataURLWithMultiplier(format, multiplier)); + }); + }, + + /** + * Clones canvas instance + * @method clone + * @param {Object} [callback] Receives cloned instance as a first argument + */ + clone: function (callback) { + var data = JSON.stringify(this); + this.cloneWithoutData(function(clone) { + clone.loadFromJSON(data, function() { + callback && callback(clone); + }); + }); + }, + + /** + * Clones canvas instance without cloning existing data. + * This essentially copies canvas dimensions, clipping properties, etc. + * but leaves data empty (so that you can populate it with your own) + * @method cloneWithoutData + * @param {Object} [callback] Receives cloned instance as a first argument + */ + cloneWithoutData: function(callback) { + var el = fabric.document.createElement('canvas'); + + el.width = this.getWidth(); + el.height = this.getHeight(); + + var clone = new fabric.Canvas(el); + clone.clipTo = this.clipTo; + if (this.backgroundImage) { + clone.setBackgroundImage(this.backgroundImage.src, function() { + clone.renderAll(); + callback && callback(clone); + }); + clone.backgroundImageOpacity = this.backgroundImageOpacity; + clone.backgroundImageStretch = this.backgroundImageStretch; + } + else { + callback && callback(clone); + } + } +}); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + 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 */ { + + /** + * Type of an object (rect, circle, path, etc) + * @property + * @type String + */ + type: 'object', + + /** + * @property + * @type Number + */ + top: 0, + + /** + * @property + * @type Number + */ + left: 0, + + /** + * @property + * @type Number + */ + width: 0, + + /** + * @property + * @type Number + */ + height: 0, + + /** + * @property + * @type Number + */ + scaleX: 1, + + /** + * @property + * @type Number + */ + scaleY: 1, + + /** + * @property + * @type Boolean + */ + flipX: false, + + /** + * @property + * @type Boolean + */ + flipY: false, + + /** + * @property + * @type Number + */ + opacity: 1, + + /** + * @property + * @type Number + */ + angle: 0, + + /** + * @property + * @type Number + */ + cornersize: 12, + + /** + * @property + * @type Boolean + */ + transparentCorners: true, + + /** + * @property + * @type Number + */ + padding: 0, + + /** + * @property + * @type String + */ + borderColor: 'rgba(102,153,255,0.75)', + + /** + * @property + * @type String + */ + cornerColor: 'rgba(102,153,255,0.5)', + + /** + * @property + * @type String + */ + fill: 'rgb(0,0,0)', + + /** + * @property + * @type String + */ + fillRule: 'source-over', + + /** + * @property + * @type String + */ + overlayFill: null, + + /** + * @property + * @type String + */ + stroke: null, + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type Array + */ + strokeDashArray: null, + + /** + * @property + * @type Number + */ + borderOpacityWhenMoving: 0.4, + + /** + * @property + * @type Number + */ + borderScaleFactor: 1, + + /** + * Transform matrix + * @property + * @type Array + */ + transformMatrix: null, + + /** + * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection) + * @property + * @type Boolean + */ + selectable: true, + + /** + * When set to `false`, object's controls are not displayed and can not be used to manipulate object + * @property + * @type Boolean + */ + hasControls: true, + + /** + * When set to `false`, object's borders are not rendered + * @property + * @type Boolean + */ + hasBorders: true, + + /** + * When set to `false`, object's rotating point will not be visible or selectable + * @property + * @type Boolean + */ + hasRotatingPoint: false, + + /** + * Offset for object's rotating point (when enabled) + * @property + * @type Number + */ + rotatingPointOffset: 40, + + /** + * @private + * @property + * @type Number + */ + _theta: 0, + + perPixelTargetFind: false, + + includeDefaultValues: true, + + /** + * List of properties to consider when checking if state of an object is changed (fabric.Object#hasStateChanged); + * as well as for history (undo/redo) purposes + * @property + * @type Array + */ + stateProperties: ( + 'top left width height scaleX scaleY flipX flipY ' + + 'theta angle opacity cornersize fill overlayFill ' + + 'stroke strokeWidth strokeDashArray fillRule ' + + 'borderScaleFactor transformMatrix selectable' + ).split(' '), + + /** + * @method callSuper + * @param {String} methodName + */ + callSuper: function(methodName) { + var fn = this.constructor.superclass.prototype[methodName]; + return (arguments.length > 1) + ? fn.apply(this, slice.call(arguments, 1)) + : fn.call(this); + }, + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + */ + initialize: function(options) { + if (options) { + this.setOptions(options); + this._initGradient(options); + } + }, + + /** + * @method initGradient + */ + _initGradient: function(options) { + if (options.fill && typeof options.fill == 'object' && !(options.fill instanceof fabric.Gradient)) { + this.set('fill', new fabric.Gradient(options.fill)); + } + }, + + /** + * @method setOptions + * @param {Object} [options] + */ + setOptions: function(options) { + var i = this.stateProperties.length, prop; + while (i--) { + prop = this.stateProperties[i]; + if (prop in options) { + this.set(prop, options[prop]); + } + } + }, + + /** + * @method transform + * @param {CanvasRenderingContext2D} ctx Context + */ + transform: function(ctx) { + ctx.globalAlpha = this.opacity; + ctx.translate(this.left, this.top); + ctx.rotate(this._theta); + ctx.scale( + this.scaleX * (this.flipX ? -1 : 1), + this.scaleY * (this.flipY ? -1 : 1) + ); + }, + + /** + * Returns an object representation of an instance + * @method toObject + * @return {Object} + */ + toObject: function() { + + var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS; + + var object = { + type: this.type, + left: toFixed(this.left, NUM_FRACTION_DIGITS), + top: toFixed(this.top, NUM_FRACTION_DIGITS), + width: toFixed(this.width, NUM_FRACTION_DIGITS), + height: toFixed(this.height, NUM_FRACTION_DIGITS), + fill: (this.fill && this.fill.toObject) ? this.fill.toObject() : this.fill, + overlayFill: this.overlayFill, + stroke: this.stroke, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray, + scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS), + scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS), + angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS), + flipX: this.flipX, + flipY: this.flipY, + opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS), + selectable: this.selectable, + hasControls: this.hasControls, + hasBorders: this.hasBorders, + hasRotatingPoint: this.hasRotatingPoint, + transparentCorners: this.transparentCorners, + perPixelTargetFind: this.perPixelTargetFind + }; + + if (!this.includeDefaultValues) { + object = this._removeDefaultValues(object); + } + + return object; + }, + + /** + * Returns (dataless) object representation of an instance + * @method toDatalessObject + */ + toDatalessObject: function() { + // will be overwritten by subclasses + return this.toObject(); + }, + + /** + * Returns styles-string for svg-export + * @method getSvgStyles + * @return {string} + */ + getSvgStyles: function() { + return [ + "stroke: ", (this.stroke ? this.stroke : 'none'), "; ", + "stroke-width: ", (this.strokeWidth ? this.strokeWidth : '0'), "; ", + "stroke-dasharray: ", (this.strokeDashArray ? this.strokeDashArray.join(' ') : "; "), + "fill: ", (this.fill ? this.fill : 'none'), "; ", + "opacity: ", (this.opacity ? this.opacity : '1'), ";" + ].join(""); + }, + + /** + * Returns transform-string for svg-export + * @method getSvgTransform + * @return {string} + */ + getSvgTransform: function() { + var angle = this.getAngle(); + return [ + "translate(", toFixed(this.left, 2), " ", toFixed(this.top, 2), ")", + angle !== 0 ? (" rotate(" + toFixed(angle, 2) + ")") : '', + (this.scaleX === 1 && this.scaleY === 1) ? '' : (" scale(" + toFixed(this.scaleX, 2) + " " + toFixed(this.scaleY, 2) + ")") + ].join(''); + }, + + /** + * @private + * @method _removeDefaultValues + */ + _removeDefaultValues: function(object) { + var defaultOptions = fabric.Object.prototype.options; + if (defaultOptions) { + this.stateProperties.forEach(function(prop) { + if (object[prop] === defaultOptions[prop]) { + delete object[prop]; + } + }); + } + return object; + }, + + /** + * Returns true if an object is in its active state + * @return {Boolean} true if an object is in its active state + */ + isActive: function() { + return !!this.active; + }, + + /** + * Sets state of an object - `true` makes it active, `false` - inactive + * @param {Boolean} active + * @return {fabric.Object} thisArg + * @chainable + */ + setActive: function(active) { + this.active = !!active; + return this; + }, + + /** + * Returns a string representation of an instance + * @return {String} + */ + toString: function() { + return "#<fabric." + capitalize(this.type) + ">"; + }, + + /** + * Sets property to a given value + * @method set + * @param {String} name + * @param {Object|Function} value + * @return {fabric.Group} thisArg + * @chainable + */ + set: function(key, value) { + if (typeof key === 'object') { + for (var prop in key) { + this._set(prop, key[prop]); + } + } + else { + if (typeof value === 'function') { + this._set(key, value(this.get(key))); + } + else { + this._set(key, value); + } + } + return this; + }, + + _set: function(key, value) { + var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY') && + value < fabric.Object.MIN_SCALE_LIMIT; + + if (shouldConstrainValue) { + value = fabric.Object.MIN_SCALE_LIMIT; + } + if (key === 'angle') { + this.setAngle(value); + } + else { + this[key] = value; + } + }, + + /** + * Toggles specified property from `true` to `false` or from `false` to `true` + * @method toggle + * @param {String} property property to toggle + * @return {fabric.Object} thisArg + * @chainable + */ + toggle: function(property) { + var value = this.get(property); + if (typeof value === 'boolean') { + this.set(property, !value); + } + return this; + }, + + /** + * @method setSourcePath + * @param {String} value + * @return {fabric.Object} thisArg + * @chainable + */ + setSourcePath: function(value) { + this.sourcePath = value; + return this; + }, + + /** + * Basic getter + * @method get + * @param {Any} property + * @return {Any} value of a property + */ + get: function(property) { + return (property === 'angle') + ? this.getAngle() + : this[property]; + }, + + /** + * @method render + * @param {CanvasRenderingContext2D} ctx context to render on + * @param {Boolean} noTransform + */ + render: function(ctx, noTransform) { + + // do not render if width or height are zeros + if (this.width === 0 || this.height === 0) return; + + ctx.save(); + + var m = this.transformMatrix; + if (m && !this.group) { + ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + if (!noTransform) { + this.transform(ctx); + } + + if (this.stroke || this.strokeDashArray) { + ctx.lineWidth = this.strokeWidth; + ctx.strokeStyle = this.stroke; + } + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : this.fill; + } + + if (m && this.group) { + ctx.translate(-this.group.width/2, -this.group.height/2); + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this._render(ctx, noTransform); + + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns width of an object + * @method getWidth + * @return {Number} width value + */ + getWidth: function() { + return this.width * this.scaleX; + }, + + /** + * Returns height of an object + * @method getHeight + * @return {Number} height value + */ + getHeight: function() { + return this.height * this.scaleY; + }, + + /** + * Scales an object (equally by x and y) + * @method scale + * @param value {Number} scale factor + * @return {fabric.Object} thisArg + * @chainable + */ + scale: function(value) { + this.scaleX = value; + this.scaleY = value; + this.setCoords(); + return this; + }, + + /** + * Scales an object to a given width, with respect to bounding box (scaling by x/y equally) + * @method scaleToWidth + * @param value {Number} new width value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToWidth: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth(); + return this.scale(value / this.width / boundingRectFactor); + }, + + /** + * Scales an object to a given height, with respect to bounding box (scaling by x/y equally) + * @method scaleToHeight + * @param value {Number} new height value + * @return {fabric.Object} thisArg + * @chainable + */ + scaleToHeight: function(value) { + // adjust to bounding rect factor so that rotated shapes would fit as well + var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight(); + return this.scale(value / this.height / boundingRectFactor); + }, + + /** + * Sets object opacity + * @method setOpacity + * @param value {Number} value 0-1 + * @return {fabric.Object} thisArg + * @chainable + */ + setOpacity: function(value) { + this.set('opacity', value); + return this; + }, + + /** + * Returns object's angle value + * @method getAngle + * @return {Number} angle value + */ + getAngle: function() { + return this._theta * 180 / Math.PI; + }, + + /** + * Sets object's angle + * @method setAngle + * @param value {Number} angle value + * @return {Object} thisArg + */ + setAngle: function(value) { + this._theta = value / 180 * Math.PI; + this.angle = value; + return this; + }, + + /** + * Sets corner position coordinates based on current angle, width and height. + * @method setCoords + * return {fabric.Object} thisArg + * @chainable + */ + setCoords: function() { + + var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, + padding = this.padding; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; + this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + + this._hypotenuse = Math.sqrt( + Math.pow(this.currentWidth / 2, 2) + + Math.pow(this.currentHeight / 2, 2)); + + this._angle = Math.atan(this.currentHeight / this.currentWidth); + + // offset added for rotate and scale actions + var offsetX = Math.cos(this._angle + this._theta) * this._hypotenuse, + offsetY = Math.sin(this._angle + this._theta) * this._hypotenuse, + theta = this._theta, + sinTh = Math.sin(theta), + cosTh = Math.cos(theta); + + var tl = { + x: this.left - offsetX, + y: this.top - offsetY + }; + var tr = { + x: tl.x + (this.currentWidth * cosTh), + y: tl.y + (this.currentWidth * sinTh) + }; + var br = { + x: tr.x - (this.currentHeight * sinTh), + y: tr.y + (this.currentHeight * cosTh) + }; + var bl = { + x: tl.x - (this.currentHeight * sinTh), + y: tl.y + (this.currentHeight * cosTh) + }; + var ml = { + x: tl.x - (this.currentHeight/2 * sinTh), + y: tl.y + (this.currentHeight/2 * cosTh) + }; + var mt = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + var mr = { + x: tr.x - (this.currentHeight/2 * sinTh), + y: tr.y + (this.currentHeight/2 * cosTh) + }; + var mb = { + x: bl.x + (this.currentWidth/2 * cosTh), + y: bl.y + (this.currentWidth/2 * sinTh) + }; + var mtr = { + x: tl.x + (this.currentWidth/2 * cosTh), + y: tl.y + (this.currentWidth/2 * sinTh) + }; + + // debugging + + // setTimeout(function() { + // canvas.contextTop.fillStyle = 'green'; + // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3); + // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3); + // canvas.contextTop.fillRect(br.x, br.y, 3, 3); + // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3); + // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3); + // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3); + // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3); + // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3); + // }, 50); + + // clockwise + this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr }; + + // set coordinates of the draggable boxes in the corners used to scale/rotate the image + this._setCornerCoords(); + + return this; + }, + + /** + * Returns width of an object's bounding rectangle + * @method getBoundingRectWidth + * @return {Number} width value + */ + getBoundingRectWidth: function() { + this.oCoords || this.setCoords(); + var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x]; + var minX = fabric.util.array.min(xCoords); + var maxX = fabric.util.array.max(xCoords); + return Math.abs(minX - maxX); + }, + + /** + * Returns height of an object's bounding rectangle + * @method getBoundingRectHeight + * @return {Number} height value + */ + getBoundingRectHeight: function() { + this.oCoords || this.setCoords(); + var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y]; + var minY = fabric.util.array.min(yCoords); + var maxY = fabric.util.array.max(yCoords); + return Math.abs(minY - maxY); + }, + + /** + * Draws borders of an object's bounding box. + * Requires public properties: width, height + * Requires public options: padding, borderColor + * @method drawBorders + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawBorders: function(ctx) { + if (!this.hasBorders) return; + + var MIN_SCALE_LIMIT = fabric.Object.MIN_SCALE_LIMIT, + padding = this.padding, + padding2 = padding * 2, + strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0; + + ctx.save(); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = this.borderColor; + + var scaleX = 1 / (this.scaleX < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleX), + scaleY = 1 / (this.scaleY < MIN_SCALE_LIMIT ? MIN_SCALE_LIMIT : this.scaleY); + + ctx.lineWidth = 1 / this.borderScaleFactor; + + ctx.scale(scaleX, scaleY); + + var w = this.getWidth(), + h = this.getHeight(); + + ctx.strokeRect( + ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5, + ~~(w + padding2 + strokeWidth * this.scaleX), + ~~(h + padding2 + strokeWidth * this.scaleY) + ); + + if (this.hasRotatingPoint && !this.lockRotation && this.hasControls) { + + var rotateHeight = ( + this.flipY + ? h + (strokeWidth * this.scaleY) + (padding * 2) + : -h - (strokeWidth * this.scaleY) - (padding * 2) + ) / 2; + + var rotateWidth = (-w/2); + + ctx.beginPath(); + ctx.moveTo(0, rotateHeight); + ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset)); + ctx.closePath(); + ctx.stroke(); + } + + ctx.restore(); + return this; + }, + + _renderDashedStroke: function(ctx) { + + if (1 & this.strokeDashArray.length /* if odd number of items */) { + /* duplicate items */ + this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); + } + + var i = 0, + x = -this.width/2, y = -this.height/2, + _this = this, + padding = this.padding, + width = this.getWidth(), + height = this.getHeight(), + dashedArrayLength = this.strokeDashArray.length; + + ctx.save(); + ctx.beginPath(); + + function renderSide(xMultiplier, yMultiplier) { + + var lineLength = 0, + sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2; + + while (lineLength < sideLength) { + + var lengthOfSubPath = _this.strokeDashArray[i++]; + lineLength += lengthOfSubPath; + + if (lineLength > sideLength) { + var lengthDiff = lineLength - sideLength; + } + + // track coords + if (xMultiplier) { + x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0); + } + else { + y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0); + } + + ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y); + if (i >= dashedArrayLength) { + i = 0; + } + } + } + + renderSide(1, 0); + renderSide(0, 1); + renderSide(-1, 0); + renderSide(0, -1); + + ctx.stroke(); + ctx.closePath(); + ctx.restore(); + }, + + /** + * Draws corners of an object's bounding box. + * Requires public properties: width, height, scaleX, scaleY + * Requires public options: cornersize, padding + * @method drawCorners + * @param {CanvasRenderingContext2D} ctx Context to draw on + * @return {fabric.Object} thisArg + * @chainable + */ + drawCorners: function(ctx) { + if (!this.hasControls) return; + + var size = this.cornersize, + size2 = size / 2, + strokeWidth2 = this.strokeWidth / 2, + left = -(this.width / 2), + top = -(this.height / 2), + _left, + _top, + sizeX = size / this.scaleX, + sizeY = size / this.scaleY, + paddingX = this.padding / this.scaleX, + paddingY = this.padding / this.scaleY, + scaleOffsetY = size2 / this.scaleY, + scaleOffsetX = size2 / this.scaleX, + scaleOffsetSizeX = (size2 - size) / this.scaleX, + scaleOffsetSizeY = (size2 - size) / this.scaleY, + height = this.height, + width = this.width, + methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; + + ctx.save(); + + ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + + ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; + ctx.strokeStyle = ctx.fillStyle = this.cornerColor; + + // top-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // top-right + _left = left + width - scaleOffsetX + strokeWidth2 + paddingX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // bottom-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + if (!this.lockUniScaling) { + // middle-top + _left = left + width/2 - scaleOffsetX; + _top = top - scaleOffsetY - strokeWidth2 - paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-bottom + _left = left + width/2 - scaleOffsetX; + _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-right + _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + + // middle-left + _left = left - scaleOffsetX - strokeWidth2 - paddingX; + _top = top + height/2 - scaleOffsetY; + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + // middle-top-rotate + if (this.hasRotatingPoint) { + + _left = left + width/2 - scaleOffsetX; + + _top = this.flipY ? + (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY) + : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY); + + ctx.clearRect(_left, _top, sizeX, sizeY); + ctx[methodName](_left, _top, sizeX, sizeY); + } + + ctx.restore(); + + return this; + }, + + /** + * Clones an instance + * @method clone + * @param {Object} options object + * @return {fabric.Object} clone of an instance + */ + clone: function(options) { + if (this.constructor.fromObject) { + return this.constructor.fromObject(this.toObject(), options); + } + return new fabric.Object(this.toObject()); + }, + + /** + * Creates an instance of fabric.Image out of an object + * @method cloneAsImage + * @param callback {Function} callback, invoked with an instance as a first argument + * @return {fabric.Object} thisArg + * @chainable + */ + cloneAsImage: function(callback) { + if (fabric.Image) { + var i = new Image(); + + /** @ignore */ + i.onload = function() { + if (callback) { + callback(new fabric.Image(i), orig); + } + i = i.onload = null; + }; + + var orig = { + angle: this.get('angle'), + flipX: this.get('flipX'), + flipY: this.get('flipY') + }; + + // normalize angle + this.set('angle', 0).set('flipX', false).set('flipY', false); + this.toDataURL(function(dataURL) { + i.src = dataURL; + }); + } + return this; + }, + + /** + * Converts an object into a data-url-like string + * @method toDataURL + * @return {String} string of data + */ + toDataURL: function(callback) { + var el = fabric.document.createElement('canvas'); + if (!el.getContext && typeof G_vmlCanvasManager != 'undefined') { + G_vmlCanvasManager.initElement(el); + } + + el.width = this.getBoundingRectWidth(); + el.height = this.getBoundingRectHeight(); + + fabric.util.wrapElement(el, 'div'); + + var canvas = new fabric.Canvas(el); + canvas.backgroundColor = 'transparent'; + canvas.renderAll(); + + if (this.constructor.async) { + this.clone(proceed); + } + else { + proceed(this.clone()); + } + + function proceed(clone) { + clone.left = el.width / 2; + clone.top = el.height / 2; + + clone.setActive(false); + + canvas.add(clone); + var data = canvas.toDataURL('png'); + + canvas.dispose(); + canvas = clone = null; + + callback && callback(data); + } + }, + + /** + * @method hasStateChanged + * @return {Boolean} true if instance' state has changed + */ + hasStateChanged: function() { + return this.stateProperties.some(function(prop) { + return this[prop] !== this.originalState[prop]; + }, this); + }, + + /** + * @method saveState + * @return {fabric.Object} thisArg + * @chainable + */ + saveState: function() { + this.stateProperties.forEach(function(prop) { + this.originalState[prop] = this.get(prop); + }, this); + return this; + }, + + /** + * @method setupState + */ + setupState: function() { + this.originalState = { }; + this.saveState(); + }, + + /** + * Returns true if object intersects with an area formed by 2 points + * @method intersectsWithRect + * @param {Object} selectionTL + * @param {Object} selectionBR + * @return {Boolean} + */ + intersectsWithRect: function(selectionTL, selectionBR) { + var oCoords = this.oCoords, + tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y), + tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y), + bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y), + br = new fabric.Point(oCoords.br.x, oCoords.br.y); + + var intersection = fabric.Intersection.intersectPolygonRectangle( + [tl, tr, br, bl], + selectionTL, + selectionBR + ); + return (intersection.status === 'Intersection'); + }, + + /** + * Returns true if object intersects with another object + * @method intersectsWithObject + * @param {Object} other Object to test + * @return {Boolean} + */ + intersectsWithObject: function(other) { + // extracts coords + function getCoords(oCoords) { + return { + tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y), + tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y), + bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y), + br: new fabric.Point(oCoords.br.x, oCoords.br.y) + } + } + var thisCoords = getCoords(this.oCoords), + otherCoords = getCoords(other.oCoords); + + var intersection = fabric.Intersection.intersectPolygonPolygon( + [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl], + [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl] + ); + + return (intersection.status === 'Intersection'); + }, + + /** + * Returns true if object is fully contained within area of another object + * @method isContainedWithinObject + * @param {Object} other Object to test + * @return {Boolean} + */ + isContainedWithinObject: function(other) { + return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br); + }, + + /** + * Returns true if object is fully contained within area formed by 2 points + * @method isContainedWithinRect + * @param {Object} selectionTL + * @param {Object} selectionBR + * @return {Boolean} + */ + isContainedWithinRect: function(selectionTL, selectionBR) { + var oCoords = this.oCoords, + tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y), + tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y), + bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y), + 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 || !this.active) return false; + + var pointer = getPointer(e), + ex = pointer.x - offset.left, + ey = pointer.y - offset.top, + xpoints, + lines; + + for (var i in this.oCoords) { + + if (i === 'mtr' && !this.hasRotatingPoint) { + continue; + } + + if (this.lockUniScaling && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { + continue; + } + + lines = this._getImageLines(this.oCoords[i].corner, i); + + // debugging + + // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2); + + // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); + // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); + + xpoints = this._findCrossPoints(ex, ey, lines); + if (xpoints % 2 == 1 && xpoints != 0) { + this.__corner = i; + return i; + } + } + return false; + }, + + /** + * Helper method to determine how many cross points are between the 4 image edges + * and the horizontal line determined by the position of our mouse when clicked on canvas + * @method _findCrossPoints + * @private + * @param ex {Number} x coordinate of the mouse + * @param ey {Number} y coordinate of the mouse + * @param oCoords {Object} Coordinates of the image being evaluated + */ + _findCrossPoints: function(ex, ey, oCoords) { + var b1, b2, a1, a2, xi, yi, + xcount = 0, + iLine; + + for (var lineKey in oCoords) { + iLine = oCoords[lineKey]; + // optimisation 1: line below dot. no cross + if ((iLine.o.y < ey) && (iLine.d.y < ey)) { + continue; + } + // optimisation 2: line above dot. no cross + if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) { + continue; + } + // optimisation 3: vertical line case + if ((iLine.o.x == iLine.d.x) && (iLine.o.x >= ex)) { + xi = iLine.o.x; + yi = ey; + } + // calculate the intersection point + else { + b1 = 0; + b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x); + a1 = ey-b1*ex; + a2 = iLine.o.y-b2*iLine.o.x; + + xi = - (a1-a2)/(b1-b2); + yi = a1+b1*xi; + } + // dont count xi < ex cases + if (xi >= ex) { + xcount += 1; + } + // optimisation 4: specific for square images + if (xcount == 2) { + break; + } + } + return xcount; + }, + + /** + * Method that returns an object with the image lines in it given the coordinates of the corners + * @method _getImageLines + * @private + * @param oCoords {Object} coordinates of the image corners + */ + _getImageLines: function(oCoords, 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), + sinTh = Math.sin(this._theta), + cosTh = Math.cos(this._theta); + + coords.tl.corner = { + tl: { + x: coords.tl.x - sinHalfOffset, + y: coords.tl.y - cosHalfOffset + }, + tr: { + x: coords.tl.x + cosHalfOffset, + y: coords.tl.y - sinHalfOffset + }, + bl: { + x: coords.tl.x - cosHalfOffset, + y: coords.tl.y + sinHalfOffset + }, + br: { + x: coords.tl.x + sinHalfOffset, + y: coords.tl.y + cosHalfOffset + } + }; + + coords.tr.corner = { + tl: { + x: coords.tr.x - sinHalfOffset, + y: coords.tr.y - cosHalfOffset + }, + tr: { + x: coords.tr.x + cosHalfOffset, + y: coords.tr.y - sinHalfOffset + }, + br: { + x: coords.tr.x + sinHalfOffset, + y: coords.tr.y + cosHalfOffset + }, + bl: { + x: coords.tr.x - cosHalfOffset, + y: coords.tr.y + sinHalfOffset + } + }; + + coords.bl.corner = { + tl: { + x: coords.bl.x - sinHalfOffset, + y: coords.bl.y - cosHalfOffset + }, + bl: { + x: coords.bl.x - cosHalfOffset, + y: coords.bl.y + sinHalfOffset + }, + br: { + x: coords.bl.x + sinHalfOffset, + y: coords.bl.y + cosHalfOffset + }, + tr: { + x: coords.bl.x + cosHalfOffset, + y: coords.bl.y - sinHalfOffset + } + }; + + coords.br.corner = { + tr: { + x: coords.br.x + cosHalfOffset, + y: coords.br.y - sinHalfOffset + }, + bl: { + x: coords.br.x - cosHalfOffset, + y: coords.br.y + sinHalfOffset + }, + br: { + x: coords.br.x + sinHalfOffset, + y: coords.br.y + cosHalfOffset + }, + tl: { + x: coords.br.x - sinHalfOffset, + y: coords.br.y - cosHalfOffset + } + }; + + coords.ml.corner = { + tl: { + x: coords.ml.x - sinHalfOffset, + y: coords.ml.y - cosHalfOffset + }, + tr: { + x: coords.ml.x + cosHalfOffset, + y: coords.ml.y - sinHalfOffset + }, + bl: { + x: coords.ml.x - cosHalfOffset, + y: coords.ml.y + sinHalfOffset + }, + br: { + x: coords.ml.x + sinHalfOffset, + y: coords.ml.y + cosHalfOffset + } + }; + + coords.mt.corner = { + tl: { + x: coords.mt.x - sinHalfOffset, + y: coords.mt.y - cosHalfOffset + }, + tr: { + x: coords.mt.x + cosHalfOffset, + y: coords.mt.y - sinHalfOffset + }, + bl: { + x: coords.mt.x - cosHalfOffset, + y: coords.mt.y + sinHalfOffset + }, + br: { + x: coords.mt.x + sinHalfOffset, + y: coords.mt.y + cosHalfOffset + } + }; + + coords.mr.corner = { + tl: { + x: coords.mr.x - sinHalfOffset, + y: coords.mr.y - cosHalfOffset + }, + tr: { + x: coords.mr.x + cosHalfOffset, + y: coords.mr.y - sinHalfOffset + }, + bl: { + x: coords.mr.x - cosHalfOffset, + y: coords.mr.y + sinHalfOffset + }, + br: { + x: coords.mr.x + sinHalfOffset, + y: coords.mr.y + cosHalfOffset + } + }; + + coords.mb.corner = { + tl: { + x: coords.mb.x - sinHalfOffset, + y: coords.mb.y - cosHalfOffset + }, + tr: { + x: coords.mb.x + cosHalfOffset, + y: coords.mb.y - sinHalfOffset + }, + bl: { + x: coords.mb.x - cosHalfOffset, + y: coords.mb.y + sinHalfOffset + }, + br: { + x: coords.mb.x + sinHalfOffset, + y: coords.mb.y + cosHalfOffset + } + }; + + coords.mtr.corner = { + tl: { + x: coords.mtr.x - sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - cosHalfOffset - (cosTh * this.rotatingPointOffset) + }, + tr: { + x: coords.mtr.x + cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y - sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + bl: { + x: coords.mtr.x - cosHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + sinHalfOffset - (cosTh * this.rotatingPointOffset) + }, + br: { + x: coords.mtr.x + sinHalfOffset + (sinTh * this.rotatingPointOffset), + y: coords.mtr.y + cosHalfOffset - (cosTh * this.rotatingPointOffset) + } + }; + }, + + /** + * Makes object's color grayscale + * @method toGrayscale + * @return {fabric.Object} thisArg + */ + toGrayscale: function() { + var fillValue = this.get('fill'); + if (fillValue) { + this.set('overlayFill', new fabric.Color(fillValue).toGrayscale().toRgb()); + } + return this; + }, + + /** + * @method complexity + * @return {Number} + */ + complexity: function() { + return 0; + }, + + /** + * Returns a JSON representation of an instance + * @method toJSON + * @return {String} json + */ + toJSON: function() { + // delegate, not alias + return this.toObject(); + }, + + /** + * @method setGradientFill + */ + setGradientFill: function(options) { + this.set('fill', fabric.Gradient.forObject(this, options)); + }, + + /** + * @method animate + * + * As object — multiple properties + * + * object.animate({ left: ..., top: ... }); + * object.animate({ left: ..., top: ... }, { duration: ... }); + * + * As string — one property + * + * object.animate('left', ...); + * object.animate('left', { duration: ... }); + * + */ + animate: function() { + if (arguments[0] && typeof arguments[0] == 'object') { + for (var prop in arguments[0]) { + this._animate(prop, arguments[0][prop], arguments[1]); + } + } + else { + this._animate.apply(this, arguments); + } + return this; + }, + + /** + * @private + * @method _animate + */ + _animate: function(property, to, options) { + var obj = this; + + options || (options = { }); + + if (!('from' in options)) { + options.from = this.get(property); + } + + if (/[+-]/.test((to + '').charAt(0))) { + to = this.get(property) + parseFloat(to); + } + + fabric.util.animate({ + startValue: options.from, + endValue: to, + byValue: options.by, + easing: options.easing, + duration: options.duration, + onChange: function(value) { + obj.set(property, value); + options.onChange && options.onChange(); + }, + onComplete: function() { + obj.setCoords(); + options.onComplete && options.onComplete(); + } + }); + }, + + /** + * Centers object horizontally on canvas to which it was added last + * @method centerH + * @return {fabric.Object} thisArg + */ + centerH: function () { + this.canvas.centerObjectH(this); + return this; + }, + + /** + * Centers object vertically on canvas to which it was added last + * @method centerV + * @return {fabric.Object} thisArg + * @chainable + */ + centerV: function () { + this.canvas.centerObjectV(this); + return this; + }, + + /** + * Centers object vertically and horizontally on canvas to which is was added last + * @method center + * @return {fabric.Object} thisArg + * @chainable + */ + center: function () { + return this.centerH().centerV(); + }, + + /** + * Removes object from canvas to which it was added last + * @method remove + * @return {fabric.Object} thisArg + * @chainable + */ + remove: function() { + return this.canvas.remove(this); + }, + + /** + * Moves an object to the bottom of the stack of drawn objects + * @method sendToBack + * @return {fabric.Object} thisArg + * @chainable + */ + sendToBack: function() { + this.canvas.sendToBack(this); + return this; + }, + + /** + * Moves an object to the top of the stack of drawn objects + * @method bringToFront + * @return {fabric.Object} thisArg + * @chainable + */ + bringToFront: function() { + this.canvas.bringToFront(this); + return this; + }, + + /** + * Moves an object one level down in stack of drawn objects + * @method sendBackwards + * @return {fabric.Object} thisArg + * @chainable + */ + sendBackwards: function() { + this.canvas.sendBackwards(this); + return this; + }, + + /** + * Moves an object one level up in stack of drawn objects + * @method bringForward + * @return {fabric.Object} thisArg + * @chainable + */ + bringForward: function() { + this.canvas.bringForward(this); + return this; + } + }); + + /** + * @alias rotate -> setAngle + */ + fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; + + var proto = fabric.Object.prototype; + for (var i = proto.stateProperties.length; i--; ) { + + var propName = proto.stateProperties[i], + capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), + setterName = 'set' + capitalizedPropName, + getterName = 'get' + capitalizedPropName; + + // using `new Function` for better introspection + if (!proto[getterName]) { + proto[getterName] = (function(property) { + return new Function('return this.get("' + property + '")'); + })(propName); + } + if (!proto[setterName]) { + proto[setterName] = (function(property) { + return new Function('value', 'return this.set("' + property + '", value)'); + })(propName); + } + } + + extend(fabric.Object.prototype, fabric.Observable); + + extend(fabric.Object, { + + /** + * @static + * @constant + * @type Number + */ + NUM_FRACTION_DIGITS: 2, + + /** + * @static + * @constant + * @type Number + */ + MIN_SCALE_LIMIT: 0.1 + + }); + +})(typeof exports != 'undefined' ? exports : this); + +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + parentSet = fabric.Object.prototype.set, + coordProps = { 'x1': 1, 'x2': 1, 'y1': 1, 'y2': 1 }; + + if (fabric.Line) { + fabric.warn('fabric.Line is already defined'); + return; + } + + /** + * @class Line + * @extends fabric.Object + */ + fabric.Line = fabric.util.createClass(fabric.Object, /** @scope fabric.Line.prototype */ { + + /** + * @property + * @type String + */ + type: 'line', + + /** + * Constructor + * @method initialize + * @param {Array} points Array of points + * @param {Object} [options] Options object + * @return {fabric.Line} thisArg + */ + initialize: function(points, options) { + if (!points) { + points = [0, 0, 0, 0]; + } + + this.callSuper('initialize', options); + + this.set('x1', points[0]); + this.set('y1', points[1]); + this.set('x2', points[2]); + this.set('y2', points[3]); + + this._setWidthHeight(options); + }, + + /** + * @private + * @method _setWidthHeight + * @param {Object} options + */ + _setWidthHeight: function(options) { + options || (options = { }); + + this.set('width', (this.x2 - this.x1) || 1); + this.set('height', (this.y2 - this.y1) || 1); + + this.set('left', 'left' in options ? options.left : (this.x1 + this.width / 2)); + this.set('top', 'top' in options ? options.top : (this.y1 + this.height / 2)); + }, + + /** + * @private + * @method _set + * @param {String} key + * @param {Any} value + */ + _set: function(key, value) { + this[key] = value; + if (key in coordProps) { + this._setWidthHeight(); + } + return this; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + ctx.beginPath(); + + if (this.group) { + ctx.translate(-this.group.width/2 + this.left, -this.group.height / 2 + this.top); + } + + // move from center (of virtual box) to its left/top corner + ctx.moveTo(this.width === 1 ? 0 : (-this.width / 2), this.height === 1 ? 0 : (-this.height / 2)); + ctx.lineTo(this.width === 1 ? 0 : (this.width / 2), this.height === 1 ? 0 : (this.height / 2)); + + ctx.lineWidth = this.strokeWidth; + + // TODO: test this + // make sure setting "fill" changes color of a line + // (by copying fillStyle to strokeStyle, since line is stroked, not filled) + var origStrokeStyle = ctx.strokeStyle; + ctx.strokeStyle = ctx.fillStyle; + ctx.stroke(); + ctx.strokeStyle = origStrokeStyle; + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @methd toObject + * @return {Object} + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + x1: this.get('x1'), + y1: this.get('y1'), + x2: this.get('x2'), + y2: this.get('y2') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '<line ', + 'x1="', this.get('x1'), '" ', + 'y1="', this.get('y1'), '" ', + 'x2="', this.get('x2'), '" ', + 'y2="', this.get('y2'), '" ', + 'style="', this.getSvgStyles(), '" ', + '/>' + ].join(''); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Line.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#LineElement + */ + fabric.Line.ATTRIBUTE_NAMES = 'x1 y1 x2 y2 stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Line instance from an SVG element + * @static + * @method fabric.Line.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromElement = function(element, options) { + var parsedAttributes = fabric.parseAttributes(element, fabric.Line.ATTRIBUTE_NAMES); + var points = [ + parsedAttributes.x1 || 0, + parsedAttributes.y1 || 0, + parsedAttributes.x2 || 0, + parsedAttributes.y2 || 0 + ]; + return new fabric.Line(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Line instance from an object representation + * @static + * @method fabric.Line.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Line} instance of fabric.Line + */ + fabric.Line.fromObject = function(object) { + var points = [object.x1, object.y1, object.x2, object.y2]; + return new fabric.Line(points, object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Circle) { + fabric.warn('fabric.Circle is already defined.'); + return; + } + + /** + * @class Circle + * @extends fabric.Object + */ + fabric.Circle = fabric.util.createClass(fabric.Object, /** @scope fabric.Circle.prototype */ { + + /** + * @property + * @type String + */ + type: 'circle', + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + * @return {fabric.Circle} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.set('radius', options.radius || 0); + this.callSuper('initialize', options); + + var diameter = this.get('radius') * 2; + this.set('width', diameter).set('height', diameter); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + radius: this.get('radius') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return ('<circle ' + + 'cx="0" cy="0" ' + + 'r="' + this.radius + '" ' + + 'style="' + this.getSvgStyles() + '" ' + + 'transform="' + this.getSvgTransform() + '" ' + + '/>'); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + // multiply by currently set alpha (the one that was set by path group where this object is contained, for example) + ctx.globalAlpha *= this.opacity; + ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.radius, 0, piBy2, false); + ctx.closePath(); + if (this.fill) { + ctx.fill(); + } + if (this.stroke) { + ctx.stroke(); + } + }, + + /** + * Returns horizontal radius of an object (according to how an object is scaled) + * @method getRadiusX + * @return {Number} + */ + getRadiusX: function() { + return this.get('radius') * this.get('scaleX'); + }, + + /** + * Returns vertical radius of an object (according to how an object is scaled) + * @method getRadiusY + * @return {Number} + */ + getRadiusY: function() { + return this.get('radius') * this.get('scaleY'); + }, + + /** + * Sets radius of an object (and updates width accordingly) + * @method setRadius + * @return {Number} + */ + setRadius: function(value) { + this.radius = value; + this.set('width', value * 2).set('height', value * 2); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement + */ + fabric.Circle.ATTRIBUTE_NAMES = 'cx cy r fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Circle} instance from an SVG element + * @static + * @method fabric.Circle.fromElement + * @param element {SVGElement} element to parse + * @param options {Object} options object + * @throws {Error} If value of `r` attribute is missing or invalid + * @return {Object} instance of fabric.Circle + */ + fabric.Circle.fromElement = function(element, options) { + options || (options = { }); + var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); + if (!isValidRadius(parsedAttributes)) { + throw Error('value of `r` attribute is required and can not be negative'); + } + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + return new fabric.Circle(extend(parsedAttributes, options)); + }; + + /** + * @private + */ + function isValidRadius(attributes) { + return (('radius' in attributes) && (attributes.radius > 0)); + } + + /** + * Returns {@link fabric.Circle} instance from an object representation + * @static + * @method fabric.Circle.fromObject + * @param {Object} object Object to create an instance from + * @return {Object} Instance of fabric.Circle + */ + fabric.Circle.fromObject = function(object) { + return new fabric.Circle(object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Triangle) { + fabric.warn('fabric.Triangle is already defined'); + return; + } + + /** + * @class Triangle + * @extends fabric.Object + */ + fabric.Triangle = fabric.util.createClass(fabric.Object, /** @scope fabric.Triangle.prototype */ { + + /** + * @property + * @type String + */ + type: 'triangle', + + /** + * Constructor + * @method initialize + * @param options {Object} options object + * @return {Object} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.callSuper('initialize', options); + + this.set('width', options.width || 100) + .set('height', options.height || 100); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} Context to render on + */ + _render: function(ctx) { + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + ctx.beginPath(); + ctx.moveTo(-widthBy2, heightBy2); + ctx.lineTo(0, -heightBy2); + ctx.lineTo(widthBy2, heightBy2); + ctx.closePath(); + + if (this.fill) { + ctx.fill(); + } + if (this.stroke) { + ctx.stroke(); + } + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return 1; + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var widthBy2 = this.width / 2, + heightBy2 = this.height / 2; + + var points = [ + -widthBy2 + " " + heightBy2, + "0 " + -heightBy2, + widthBy2 + " " + heightBy2 + ].join(","); + + return '<polygon ' + + 'points="' + points + '" ' + + 'style="' + this.getSvgStyles() + '" ' + + 'transform="' + this.getSvgTransform() + '" ' + + '/>'; + } + }); + + /** + * Returns fabric.Triangle instance from an object representation + * @static + * @method Canvas.Trangle.fromObject + * @param object {Object} object to create an instance from + * @return {Object} instance of Canvas.Triangle + */ + fabric.Triangle.fromObject = function(object) { + return new fabric.Triangle(object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global){ + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + piBy2 = Math.PI * 2, + extend = fabric.util.object.extend; + + if (fabric.Ellipse) { + fabric.warn('fabric.Ellipse is already defined.'); + return; + } + + /** + * @class Ellipse + * @extends fabric.Object + */ + fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @scope fabric.Ellipse.prototype */ { + + /** + * @property + * @type String + */ + type: 'ellipse', + + /** + * Constructor + * @method initialize + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(options) { + options = options || { }; + + this.callSuper('initialize', options); + + this.set('rx', options.rx || 0); + this.set('ry', options.ry || 0); + + this.set('width', this.get('rx') * 2); + this.set('height', this.get('ry') * 2); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + rx: this.get('rx'), + ry: this.get('ry') + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return [ + '<ellipse ', + 'rx="', this.get('rx'), '" ', + 'ry="', this.get('ry'), '" ', + 'style="', this.getSvgStyles(), '" ', + 'transform="', this.getSvgTransform(), '" ', + '/>' + ].join(''); + }, + + /** + * Renders this instance on a given context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + * @param noTransform {Boolean} context is not transformed when set to true + */ + render: function(ctx, noTransform) { + // do not use `get` for perf. reasons + if (this.rx === 0 || this.ry === 0) return; + return this.callSuper('render', ctx, noTransform); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx, noTransform) { + ctx.beginPath(); + ctx.save(); + ctx.globalAlpha *= this.opacity; + if (this.transformMatrix && this.group) { + ctx.translate(this.cx, this.cy); + } + ctx.transform(1, 0, 0, this.ry/this.rx, 0, 0); + ctx.arc(noTransform ? this.left : 0, noTransform ? this.top : 0, this.rx, 0, piBy2, false); + if (this.stroke) { + ctx.stroke(); + } + if (this.fill) { + ctx.fill(); + } + ctx.restore(); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement + */ + fabric.Ellipse.ATTRIBUTE_NAMES = 'cx cy rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns {@link fabric.Ellipse} instance from an SVG element + * @static + * @method fabric.Ellipse.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromElement = function(element, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Ellipse.ATTRIBUTE_NAMES); + var cx = parsedAttributes.left; + var cy = parsedAttributes.top; + + if ('left' in parsedAttributes) { + parsedAttributes.left -= (options.width / 2) || 0; + } + if ('top' in parsedAttributes) { + parsedAttributes.top -= (options.height / 2) || 0; + } + + var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); + + ellipse.cx = cx || 0; + ellipse.cy = cy || 0; + + return ellipse; + }; + + /** + * Returns fabric.Ellipse instance from an object representation + * @static + * @method fabric.Ellipse.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Ellipse} + */ + fabric.Ellipse.fromObject = function(object) { + return new fabric.Ellipse(object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }); + + if (fabric.Rect) { + console.warn('fabric.Rect is already defined'); + return; + } + + /** + * @class Rect + * @extends fabric.Object + */ + fabric.Rect = fabric.util.createClass(fabric.Object, /** @scope fabric.Rect.prototype */ { + + /** + * @property + * @type String + */ + type: 'rect', + + /** + * @property + * @type Number + */ + rx: 0, + + /** + * @property + * @type Number + */ + ry: 0, + + /** + * Constructor + * @method initialize + * @param options {Object} options object + * @return {Object} thisArg + */ + initialize: function(options) { + this._initStateProperties(); + this.callSuper('initialize', options); + this._initRxRy(); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Rect` -specific ones to it + * (such as "rx", "ry", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(['rx', 'ry']); + }, + + /** + * @private + * @method _initRxRy + */ + _initRxRy: function() { + if (this.rx && !this.ry) { + this.ry = this.rx; + } + else if (this.ry && !this.rx) { + this.rx = this.ry; + } + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var rx = this.rx || 0, + ry = this.ry || 0, + x = -this.width / 2, + y = -this.height / 2, + w = this.width, + h = this.height; + + ctx.beginPath(); + ctx.globalAlpha *= this.opacity; + + if (this.transformMatrix && this.group) { + ctx.translate( + this.width / 2 + this.x, + this.height / 2 + this.y); + } + if (!this.transformMatrix && this.group) { + ctx.translate( + -this.group.width / 2 + this.width / 2 + this.x, + -this.group.height / 2 + this.height / 2 + this.y); + } + + ctx.moveTo(x+rx, y); + ctx.lineTo(x+w-rx, y); + ctx.quadraticCurveTo(x+w, y, x+w, y+ry, x+w, y+ry); + ctx.lineTo(x+w, y+h-ry); + ctx.quadraticCurveTo(x+w,y+h,x+w-rx,y+h,x+w-rx,y+h); + ctx.lineTo(x+rx,y+h); + ctx.quadraticCurveTo(x,y+h,x,y+h-ry,x,y+h-ry); + ctx.lineTo(x,y+ry); + ctx.quadraticCurveTo(x,y,x+rx,y,x+rx,y); + ctx.closePath(); + + if (this.fill) { + ctx.fill(); + } + + if (this.strokeDashArray) { + this._renderDashedStroke(ctx); + } + else if (this.stroke) { + ctx.stroke(); + } + }, + + // since our coordinate system differs from that of SVG + _normalizeLeftTopProperties: function(parsedAttributes) { + if (parsedAttributes.left) { + this.set('left', parsedAttributes.left + this.getWidth() / 2); + } + this.set('x', parsedAttributes.left || 0); + if (parsedAttributes.top) { + this.set('top', parsedAttributes.top + this.getHeight() / 2); + } + this.set('y', parsedAttributes.top || 0); + return this; + }, + + /** + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return fabric.util.object.extend(this.callSuper('toObject'), { + rx: this.get('rx') || 0, + ry: this.get('ry') || 0 + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return '<rect ' + + 'x="' + (-1 * this.width / 2) + '" y="' + (-1 * this.height / 2) + '" ' + + 'rx="' + this.get('rx') + '" ry="' + this.get('ry') + '" ' + + 'width="' + this.width + '" height="' + this.height + '" ' + + 'style="' + this.getSvgStyles() + '" ' + + 'transform="' + this.getSvgTransform() + '" ' + + '/>'; + } + }); + + // TODO (kangax): implement rounded rectangles (both parsing and rendering) + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) + * @static + */ + fabric.Rect.ATTRIBUTE_NAMES = 'x y width height rx ry fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * @private + */ + function _setDefaultLeftTopValues(attributes) { + attributes.left = attributes.left || 0; + attributes.top = attributes.top || 0; + return attributes; + } + + /** + * Returns fabric.Rect instance from an SVG element + * @static + * @method fabric.Rect.fromElement + * @param element {SVGElement} element to parse + * @param options {Object} options object + * @return {fabric.Rect} instance of fabric.Rect + */ + fabric.Rect.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); + parsedAttributes = _setDefaultLeftTopValues(parsedAttributes); + + var rect = new fabric.Rect(fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); + rect._normalizeLeftTopProperties(parsedAttributes); + + return rect; + }; + + /** + * Returns fabric.Rect instance from an object representation + * @static + * @method fabric.Rect.fromObject + * @param object {Object} object to create an instance from + * @return {Object} instance of fabric.Rect + */ + fabric.Rect.fromObject = function(object) { + return new fabric.Rect(object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + toFixed = fabric.util.toFixed; + + if (fabric.Polyline) { + fabric.warn('fabric.Polyline is already defined'); + return; + } + + /** + * @class Polyline + * @extends fabric.Object + */ + fabric.Polyline = fabric.util.createClass(fabric.Object, /** @scope fabric.Polyline.prototype */ { + + /** + * @property + * @type String + */ + type: 'polyline', + + /** + * Constructor + * @method initialize + * @param {Array} points array of points + * @param {Object} [options] Options object + * @return {Object} thisArg + */ + initialize: function(points, options) { + options = options || { }; + this.set('points', points); + this.callSuper('initialize', options); + this._calcDimensions(); + }, + + /** + * @private + * @method _calcDimensions + */ + _calcDimensions: function() { + return fabric.Polygon.prototype._calcDimensions.call(this); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of an instance + */ + toObject: function() { + return fabric.Polygon.prototype.toObject.call(this); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '<polyline ', + 'points="', points.join(''), '" ', + 'style="', this.getSvgStyles(), '" ', + 'transform="', this.getSvgTransform(), '" ', + '/>' + ].join(''); + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + for (var i = 0, len = this.points.length; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x, point.y); + } + if (this.fill) { + ctx.fill(); + } + if (this.stroke) { + ctx.stroke(); + } + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.get('points').length; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polyline.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement + */ + fabric.Polyline.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polyline instance from an SVG element + * @static + * @method fabric.Polyline.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} [options] Options object + * @return {Object} instance of fabric.Polyline + */ + fabric.Polyline.fromElement = function(element, options) { + if (!element) { + return null; + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric.Polyline.ATTRIBUTE_NAMES); + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2) || 0; + points[i].y -= (options.height / 2) || 0; + } + + return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Polyline instance from an object representation + * @static + * @method fabric.Polyline.fromObject + * @param {Object} [object] Object to create an instance from + * @return {fabric.Polyline} + */ + fabric.Polyline.fromObject = function(object) { + var points = object.points; + return new fabric.Polyline(points, object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; + + if (fabric.Polygon) { + fabric.warn('fabric.Polygon is already defined'); + return; + } + + 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) || 1; + this.height = (maxY - minY) || 1; + + this.minX = minX; + this.minY = minY; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + points: this.points.concat() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var points = []; + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + + return [ + '<polygon ', + 'points="', points.join(''), '" ', + 'style="', this.getSvgStyles(), '" ', + 'transform="', this.getSvgTransform(), '" ', + '/>' + ].join(''); + }, + + /** + * @private + * @method _render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + _render: function(ctx) { + var point; + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + for (var i = 0, len = this.points.length; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x, point.y); + } + if (this.fill) { + ctx.fill(); + } + if (this.stroke) { + ctx.closePath(); + ctx.stroke(); + } + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity of this instance + */ + complexity: function() { + return this.points.length; + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) + * @static + * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement + */ + fabric.Polygon.ATTRIBUTE_NAMES = 'fill fill-opacity opacity stroke stroke-width transform'.split(' '); + + /** + * Returns fabric.Polygon instance from an SVG element + * @static + * @method fabric.Polygon.fromElement + * @param {SVGElement} element Element to parse + * @param {Object} options Options object + * @return {fabric.Polygon} + */ + fabric.Polygon.fromElement = function(element, options) { + if (!element) { + return null; + } + options || (options = { }); + + var points = fabric.parsePointsAttribute(element.getAttribute('points')), + parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); + + for (var i = 0, len = points.length; i < len; i++) { + // normalize coordinates, according to containing box (dimensions of which are passed via `options`) + points[i].x -= (options.width / 2) || 0; + points[i].y -= (options.height / 2) || 0; + } + + return new fabric.Polygon(points, extend(parsedAttributes, options)); + }; + + /** + * Returns fabric.Polygon instance from an object representation + * @static + * @method fabric.Polygon.fromObject + * @param {Object} object Object to create an instance from + * @return {fabric.Polygon} + */ + fabric.Polygon.fromObject = function(object) { + return new fabric.Polygon(object.points, object); + }; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + var commandLengths = { + m: 2, + l: 2, + h: 1, + v: 1, + c: 6, + s: 4, + q: 4, + t: 2, + a: 7 + }; + + function drawArc(ctx, x, y, coords) { + var rx = coords[0]; + var ry = coords[1]; + var rot = coords[2]; + var large = coords[3]; + var sweep = coords[4]; + var ex = coords[5]; + var ey = coords[6]; + var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); + for (var i=0; i<segs.length; i++) { + var bez = segmentToBezier.apply(this, segs[i]); + ctx.bezierCurveTo.apply(ctx, bez); + } + } + + var arcToSegmentsCache = { }, + segmentToBezierCache = { }, + _join = Array.prototype.join, + argsString; + + // Copied from Inkscape svgtopdf, thanks! + 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 + // one of commands (m,M,l,L,q,Q,c,C,etc.) followed by non-command characters (i.e. command values) + : path.match && path.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi); + + if (!this.path) return; + + if (!fromArray) { + this._initializeFromString(options); + } + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * @private + * @method _initializeFromString + */ + _initializeFromString: 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 + previous = null, + x = 0, // current x + y = 0, // current y + controlX = 0, // current control point x + controlY = 0, // current control point y + tempX, + tempY, + tempControlX, + tempControlY, + 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 + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + // calculate reflection of previous control points + 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 + ); + // set control point to 2nd one of this command + // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + controlX = x + current[1]; + controlY = y + current[2]; + + x = tempX; + y = tempY; + break; + + case 'S': // shorthand cubic bezierCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + // calculate reflection of previous control points + 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; + + // set control point to 2nd one of this command + // "... the first control point is assumed to be the reflection of the second control point on the previous command relative to the current point." + controlX = current[1]; + controlY = current[2]; + + break; + + case 'q': // quadraticCurveTo, relative + // transform to absolute x,y + tempX = x + current[3]; + tempY = y + current[4]; + + controlX = x + current[1]; + controlY = y + current[2]; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'Q': // quadraticCurveTo, absolute + tempX = current[3]; + tempY = current[4]; + + ctx.quadraticCurveTo( + current[1] + l, + current[2] + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + controlX = current[1]; + controlY = current[2]; + break; + + case 't': // shorthand quadraticCurveTo, relative + + // transform to absolute x,y + tempX = x + current[1]; + tempY = y + current[2]; + + + if (previous[0].match(/[QqTt]/) === null) { + // If there is no previous command or if the previous command was not a Q, q, T or t, + // assume the control point is coincident with the current point + controlX = x; + controlY = y; + } + else if (previous[0] === 't') { + // calculate reflection of previous control points for t + controlX = 2 * x - tempControlX; + controlY = 2 * y - tempControlY; + } + else if (previous[0] === 'q') { + // calculate reflection of previous control points for q + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + } + + tempControlX = controlX; + tempControlY = controlY; + + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + controlX = x + current[1]; + controlY = y + current[2]; + break; + + case 'T': + tempX = current[1]; + tempY = current[2]; + + // calculate reflection of previous control points + controlX = 2 * x - controlX; + controlY = 2 * y - controlY; + ctx.quadraticCurveTo( + controlX + l, + controlY + t, + tempX + l, + tempY + t + ); + x = tempX; + y = tempY; + break; + + case 'a': + // TODO: optimize this + 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': + // TODO: optimize this + 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; + } + previous = current; + } + }, + + /** + * 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); + } + // ctx.globalCompositeOperation = this.fillRule; + + if (this.overlayFill) { + ctx.fillStyle = this.overlayFill; + } + else if (this.fill) { + ctx.fillStyle = this.fill.toLiveGradient + ? this.fill.toLiveGradient(ctx) + : 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 svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var chunks = []; + for (var i = 0, len = this.path.length; i < len; i++) { + chunks.push(this.path[i].join(' ')); + } + var path = chunks.join(' '); + + return [ + '<g transform="', this.getSvgTransform(), '">', + '<path ', + 'width="', this.width, '" height="', this.height, '" ', + 'd="', path, '" ', + 'style="', this.getSvgStyles(), '" ', + 'transform="translate(', (-this.width / 2), ' ', (-this.height/2), ')" />', + '</g>' + ].join(''); + }, + + /** + * Returns number representation of an instance complexity + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.path.length; + }, + + /** + * @private + * @method _parsePath + */ + _parsePath: function() { + var result = [ ], + currentPath, + chunks, + parsed; + + for (var i = 0, 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([ chunksParsed[0] ].concat(chunksParsed.slice(k, k + commandLength))); + } + } + else { + result.push(chunksParsed); + } + } + + return result; + }, + + /** + * @method _parseDimensions + */ + _parseDimensions: function() { + var aX = [], + aY = [], + previousX, + previousY, + isLowerCase = false, + x, + y; + + this.path.forEach(function(item, i) { + if (item[0] !== 'H') { + previousX = (i === 0) ? getX(item) : getX(this.path[i-1]); + } + if (item[0] !== 'V') { + previousY = (i === 0) ? getY(item) : getY(this.path[i-1]); + } + + // lowercased letter denotes relative position; + // transform to absolute + if (item[0] === item[0].toLowerCase()) { + isLowerCase = true; + } + + // last 2 items in an array of coordinates are the actualy x/y (except H/V); + // collect them + + // TODO (kangax): support relative h/v commands + + x = isLowerCase + ? previousX + getX(item) + : item[0] === 'V' + ? previousX + : getX(item); + + y = isLowerCase + ? previousY + getY(item) + : item[0] === 'H' + ? previousY + : getY(item); + + var val = parseInt(x, 10); + if (!isNaN(val)) aX.push(val); + + val = parseInt(y, 10); + if (!isNaN(val)) aY.push(val); + + }, this); + + var minX = min(aX), + minY = min(aY), + deltaX = 0, + deltaY = 0; + + var o = { + top: minY - deltaY, + left: minX - deltaX, + bottom: max(aY) - deltaY, + right: max(aX) - deltaX + }; + + o.width = o.right - o.left; + o.height = o.bottom - o.top; + + return o; + } + }); + + /** + * Creates an instance of fabric.Path from an object + * @static + * @method fabric.Path.fromObject + * @return {fabric.Path} Instance of fabric.Path + */ + fabric.Path.fromObject = function(object) { + return new fabric.Path(object.path, object); + }; + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) + * @static + * @see http://www.w3.org/TR/SVG/paths.html#PathElement + */ + fabric.Path.ATTRIBUTE_NAMES = 'd fill fill-opacity opacity fill-rule stroke stroke-width transform'.split(' '); + + /** + * Creates an instance of fabric.Path from an SVG <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 String + */ + fill: '', + + /** + * @property + * @type Boolean + */ + forceFillOverwrite: false, + + /** + * Constructor + * @method initialize + * @param {Array} paths + * @param {Object} [options] Options object + * @return {fabric.PathGroup} thisArg + */ + initialize: function(paths, options) { + + options = options || { }; + this.paths = paths || [ ]; + + for (var i = this.paths.length; i--; ) { + this.paths[i].group = this; + } + + this.setOptions(options); + this.setCoords(); + + if (options.sourcePath) { + this.setSourcePath(options.sourcePath); + } + }, + + /** + * Renders this group on a specified context + * @method render + * @param {CanvasRenderingContext2D} ctx Context to render this instance on + */ + render: function(ctx) { + ctx.save(); + + var m = this.transformMatrix; + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + + this.transform(ctx); + for (var i = 0, l = this.paths.length; i < l; ++i) { + this.paths[i].render(ctx, true); + } + if (this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Sets certain property to a certain value + * @method _set + * @param {String} prop + * @param {Any} value + * @return {fabric.PathGroup} thisArg + */ + _set: function(prop, value) { + + if ((prop === 'fill' || prop === 'overlayFill') && value && this.isSameColor()) { + var i = this.paths.length; + while (i--) { + this.paths[i]._set(prop, value); + } + } + + return this.callSuper('_set', prop, value); + }, + + /** + * Returns object representation of this path group + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(parentToObject.call(this), { + paths: invoke(this.getObjects(), '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 svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objects = this.getObjects(); + var markup = [ + '<g ', + 'width="', this.width, '" ', + 'height="', this.height, '" ', + 'style="', this.getSvgStyles(), '" ', + 'transform="', this.getSvgTransform(), '" ', + '>' + ]; + + for (var i = 0, len = objects.length; i < len; i++) { + markup.push(objects[i].toSVG()); + } + markup.push('</g>'); + + return markup.join(''); + }, + + /** + * 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(); + + // group is active by default + this.setCoords(true); + this.saveCoords(); + + //this.activateAllObjects(); + }, + + /** + * @private + * @method _updateObjectsCoords + */ + _updateObjectsCoords: function() { + var groupDeltaX = this.left, + groupDeltaY = this.top; + + this.forEachObject(function(object) { + + var objectLeft = object.get('left'), + objectTop = object.get('top'); + + object.set('originalLeft', objectLeft); + object.set('originalTop', objectTop); + + object.set('left', objectLeft - groupDeltaX); + object.set('top', objectTop - groupDeltaY); + + object.setCoords(); + + // do not display corners of objects enclosed in a group + object.hideCorners = true; + }, this); + }, + + /** + * Returns string represenation of a group + * @method toString + * @return {String} + */ + toString: function() { + return '#<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 addWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + addWithUpdate: function(object) { + this._restoreObjectsState(); + this.objects.push(object); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Removes an object from a group; Then recalculates group's dimension, position. + * @method removeWithUpdate + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + removeWithUpdate: function(object) { + this._restoreObjectsState(); + removeFromArray(this.objects, object); + object.setActive(false); + this._calcBounds(); + this._updateObjectsCoords(); + return this; + }, + + /** + * Adds an object to a group + * @method add + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + add: function(object) { + this.objects.push(object); + return this; + }, + + /** + * Removes an object from a group + * @method remove + * @param {Object} object + * @return {fabric.Group} thisArg + * @chainable + */ + remove: function(object) { + removeFromArray(this.objects, object); + return this; + }, + + /** + * Returns a size of a group (i.e: length of an array containing its objects) + * @return {Number} Group size + */ + size: function() { + return this.getObjects().length; + }, + + /** + * @private + */ + _set: function(key, value) { + if (key === 'fill' || key === 'opacity') { + var i = this.objects.length; + this[key] = value; + while (i--) { + this.objects[i].set(key, value); + } + } + else { + this[key] = value; + } + }, + + /** + * Returns true if a group contains an object + * @method contains + * @param {Object} object Object to check against + * @return {Boolean} `true` if group contains an object + */ + contains: function(object) { + return this.objects.indexOf(object) > -1; + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + objects: invoke(this.objects, 'toObject') + }); + }, + + /** + * Renders instance on a given context + * @method render + * @param {CanvasRenderingContext2D} ctx context to render instance on + */ + render: function(ctx, noTransform) { + ctx.save(); + this.transform(ctx); + + var groupScaleFactor = Math.max(this.scaleX, this.scaleY); + + for (var i = 0, len = this.objects.length, object; object = this.objects[i]; i++) { + var originalScaleFactor = object.borderScaleFactor; + object.borderScaleFactor = groupScaleFactor; + object.render(ctx); + object.borderScaleFactor = originalScaleFactor; + } + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + this.setCoords(); + }, + + /** + * Returns object from the group at the specified index + * @method item + * @param index {Number} index of item to get + * @return {fabric.Object} + */ + item: function(index) { + return this.getObjects()[index]; + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return this.getObjects().reduce(function(total, object) { + total += (typeof object.complexity == 'function') ? object.complexity() : 0; + return total; + }, 0); + }, + + /** + * Retores original state of each of group objects (original state is that which was before group was created). + * @private + * @method _restoreObjectsState + * @return {fabric.Group} thisArg + * @chainable + */ + _restoreObjectsState: function() { + this.objects.forEach(this._restoreObjectState, this); + return this; + }, + + /** + * Restores original state of a specified object in group + * @private + * @method _restoreObjectState + * @param {fabric.Object} object + * @return {fabric.Group} thisArg + */ + _restoreObjectState: function(object) { + + var groupLeft = this.get('left'), + groupTop = this.get('top'), + groupAngle = this.getAngle() * (Math.PI / 180), + 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() { + this.forEachObject(function(object) { + object.setActive(); + }); + return this; + }, + + /** + * Executes given function for each object in this group + * @method forEachObject + * @param {Function} callback + * Callback invoked with current object as first argument, + * index - as second and an array of all objects - as third. + * Iteration happens in reverse order (for performance reasons). + * Callback is invoked in a context of Global Object (e.g. `window`) + * when no `context` argument is given + * + * @param {Object} context Context (aka thisObject) + * + * @return {fabric.Group} thisArg + * @chainable + */ + forEachObject: fabric.StaticCanvas.prototype.forEachObject, + + /** + * @private + * @method _setOpacityIfSame + */ + _setOpacityIfSame: function() { + var objects = this.getObjects(), + firstValue = objects[0] ? objects[0].get('opacity') : 1; + + var isSameOpacity = objects.every(function(o) { + return o.get('opacity') === firstValue; + }); + + if (isSameOpacity) { + this.opacity = firstValue; + } + }, + + /** + * @private + * @method _calcBounds + */ + _calcBounds: function() { + var aX = [], + aY = [], + minX, minY, maxX, maxY, o, width, height, + i = 0, + len = this.objects.length; + + for (; i < len; ++i) { + o = this.objects[i]; + o.setCoords(); + for (var prop in o.oCoords) { + aX.push(o.oCoords[prop].x); + aY.push(o.oCoords[prop].y); + } + }; + + minX = min(aX); + maxX = max(aX); + minY = min(aY); + maxY = max(aY); + + width = (maxX - minX) || 0; + height = (maxY - minY) || 0; + + this.width = width; + this.height = height; + + this.left = (minX + width / 2) || 0; + this.top = (minY + height / 2) || 0; + }, + + /** + * Checks if point is contained within the group + * @method containsPoint + * @param {fabric.Point} point point with `x` and `y` properties + * @return {Boolean} true if point is contained within group + */ + containsPoint: function(point) { + + var halfWidth = this.get('width') / 2, + halfHeight = this.get('height') / 2, + centerX = this.get('left'), + centerY = this.get('top'); + + return centerX - halfWidth < point.x && + centerX + halfWidth > point.x && + centerY - halfHeight < point.y && + centerY + halfHeight > point.y; + }, + + /** + * Makes all of this group's objects grayscale (i.e. calling `toGrayscale` on them) + * @method toGrayscale + */ + toGrayscale: function() { + var i = this.objects.length; + while (i--) { + this.objects[i].toGrayscale(); + } + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + var objectsMarkup = [ ]; + for (var i = 0, len = this.objects.length; i < len; i++) { + objectsMarkup.push(this.objects[i].toSVG()); + } + + return ( + '<g transform="' + this.getSvgTransform() + '">' + + objectsMarkup.join('') + + '</g>'); + } + }); + + /** + * Returns fabric.Group instance from an object representation + * @static + * @method fabric.Group.fromObject + * @param object {Object} object to create a group from + * @param options {Object} options object + * @return {fabric.Group} an instance of fabric.Group + */ + fabric.Group.fromObject = function(object, callback) { + fabric.util.enlivenObjects(object.objects, function(enlivenedObjects) { + delete object.objects; + callback && callback(new fabric.Group(enlivenedObjects, object)); + }); + }; + + fabric.Group.async = true; + +})(typeof exports != 'undefined' ? exports : this); +(function(global) { + + "use strict"; + + var extend = fabric.util.object.extend; + + if (!global.fabric) { + global.fabric = { }; + } + + if (global.fabric.Image) { + fabric.warn('fabric.Image is already defined.'); + return; + }; + + if (!fabric.Object) { + fabric.warn('fabric.Object is required for fabric.Image initialization'); + return; + } + + /** + * @class Image + * @extends fabric.Object + */ + fabric.Image = fabric.util.createClass(fabric.Object, /** @scope fabric.Image.prototype */ { + + /** + * @property + * @type Boolean + */ + active: false, + + /** + * @property + * @type Boolean + */ + bordervisibility: false, + + /** + * @property + * @type Boolean + */ + cornervisibility: false, + + /** + * @property + * @type String + */ + type: 'image', + + /** + * Constructor + * @param {HTMLImageElement | String} element Image element + * @param {Object} options optional + */ + initialize: function(element, options) { + options || (options = { }); + + this.callSuper('initialize', options); + this._initElement(element); + this._originalImage = this.getElement(); + this._initConfig(options); + + this.filters = [ ]; + + if (options.filters) { + this.filters = options.filters; + this.applyFilters(); + } + }, + + /** + * Returns image element which this instance if based on + * @method getElement + * @return {HTMLImageElement} image element + */ + getElement: function() { + return this._element; + }, + + /** + * Sets image element for this instance to a specified one + * @method setElement + * @param {HTMLImageElement} element + * @return {fabric.Image} thisArg + * @chainable + */ + setElement: function(element) { + this._element = element; + this._initConfig(); + return this; + }, + + /** + * Returns original size of an image + * @method getOriginalSize + * @return {Object} object with "width" and "height" properties + */ + getOriginalSize: function() { + var element = this.getElement(); + return { + width: element.width, + height: element.height + }; + }, + + /** + * Sets border visibility + * @method setBorderVisibility + * @param {Boolean} visible When true, border is set to be visible + */ + setBorderVisibility: function(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(); + var m = this.transformMatrix; + this._resetWidthHeight(); + if (this.group) { + ctx.translate(-this.group.width/2 + this.width/2, -this.group.height/2 + this.height/2); + } + if (m) { + ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); + } + if (!noTransform) { + this.transform(ctx); + } + this._render(ctx); + if (this.active && !noTransform) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of an instance + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + src: this._originalImage.src || this._originalImage._src, + filters: this.filters.concat() + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + return '<g transform="' + this.getSvgTransform() + '">'+ + '<image xlink:href="' + this.getSvgSrc() + '" '+ + 'style="' + this.getSvgStyles() + '" ' + + // we're essentially moving origin of transformation from top/left corner to the center of the shape + // by wrapping it in container <g> element with actual transformation, then offsetting object to the top/left + // so that object's center aligns with container's left/top + 'transform="translate('+ (-this.width/2) + ' ' + (-this.height/2) + ')" ' + + 'width="' + this.width + '" ' + + 'height="' + this.height + '"' + '/>'+ + '</g>'; + }, + + /** + * Returns source of an image + * @method getSrc + * @return {String} Source of an image + */ + getSrc: function() { + return this.getElement().src || this.getElement()._src; + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of an instance + */ + toString: function() { + return '#<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); + }, + + /** + * Applies filters assigned to this image (from "filters" array) + * @mthod applyFilters + * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated + */ + applyFilters: function(callback) { + + if (this.filters.length === 0) { + this.setElement(this._originalImage); + callback && callback(); + return; + } + + var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined', + imgEl = this._originalImage, + canvasEl = fabric.document.createElement('canvas'), + replacement = isLikelyNode ? new (require('canvas').Image) : fabric.document.createElement('img'), + _this = this; + + if (!canvasEl.getContext && typeof G_vmlCanvasManager != 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + canvasEl.width = imgEl.width; + canvasEl.height = imgEl.height; + + canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); + + this.filters.forEach(function(filter) { + filter && filter.applyTo(canvasEl); + }); + + /** @ignore */ + replacement.onload = function() { + _this._element = replacement; + callback && callback(); + replacement.onload = canvasEl = imgEl = null; + }; + replacement.width = imgEl.width; + replacement.height = imgEl.height; + + if (isLikelyNode) { + var base64str = canvasEl.toDataURL('image/png').replace(/data:image\/png;base64,/, ''); + replacement.src = new Buffer(base64str, 'base64'); + _this._element = replacement; + + // onload doesn't fire in node, so we invoke callback manually + callback && callback(); + } + else { + replacement.src = canvasEl.toDataURL('image/png'); + } + + return this; + }, + + /** + * @private + */ + _render: function(ctx) { + ctx.drawImage( + this.getElement(), + - this.width / 2, + -this.height / 2, + this.width, + this.height + ); + }, + + /** + * @private + */ + _adjustWidthHeightToBorders: function(showBorder) { + if (showBorder) { + this.currentBorder = this.borderwidth; + this.width += (2 * this.currentBorder); + this.height += (2 * this.currentBorder); + } + else { + this.currentBorder = 0; + } + }, + + /** + * @private + */ + _resetWidthHeight: function() { + var element = this.getElement(); + + this.set('width', element.width); + this.set('height', element.height); + }, + + /** + * The Image class's initialization method. This method is automatically + * called by the constructor. + * @method _initElement + * @param {HTMLImageElement|String} el The element representing the image + */ + _initElement: function(element) { + this.setElement(fabric.util.getById(element)); + fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); + }, + + /** + * @method _initConfig + * @param {Object} options Options object + */ + _initConfig: function(options) { + options || (options = { }); + this.setOptions(options); + this._setBorder(); + this._setWidthHeight(options); + }, + + /** + * @method _initFilters + * @param {Object} object Object with filters property + */ + _initFilters: function(object) { + if (object.filters && object.filters.length) { + this.filters = object.filters.map(function(filterObj) { + return filterObj && fabric.Image.filters[filterObj.type].fromObject(filterObj); + }); + } + }, + + /** + * @private + */ + _setBorder: function() { + if (this.bordervisibility) { + this.currentBorder = this.borderwidth; + } + else { + this.currentBorder = 0; + } + }, + + /** + * @private + */ + _setWidthHeight: function(options) { + var sidesBorderWidth = 2 * this.currentBorder; + + this.width = 'width' in options + ? options.width + : ((this.getElement().width || 0) + sidesBorderWidth); + + this.height = 'height' in options + ? options.height + : ((this.getElement().height || 0) + sidesBorderWidth); + }, + + /** + * Returns complexity of an instance + * @method complexity + * @return {Number} complexity + */ + complexity: function() { + return 1; + } + }); + + /** + * Default CSS class name for canvas + * @static + * @type String + */ + fabric.Image.CSS_CANVAS = "canvas-img"; + + fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; + + /** + * Creates an instance of fabric.Image from its object representation + * @static + * @method fromObject + * @param object {Object} + * @param callback {Function} optional + */ + fabric.Image.fromObject = function(object, callback) { + var img = fabric.document.createElement('img'), + src = object.src; + + if (object.width) { + img.width = object.width; + } + if (object.height) { + img.height = object.height; + } + + /** @ignore */ + img.onload = function() { + fabric.Image.prototype._initFilters.call(object, object); + + var instance = new fabric.Image(img, object); + callback && callback(instance); + img = img.onload = null; + }; + img.src = src; + }; + + /** + * Creates an instance of fabric.Image from an URL string + * @static + * @method fromURL + * @param {String} url URL to create an image from + * @param {Function} [callback] Callback to invoke when image is created (newly created image is passed as a first argument) + * @param {Object} [imgOptions] Options object + */ + fabric.Image.fromURL = function(url, callback, imgOptions) { + var img = fabric.document.createElement('img'); + + /** @ignore */ + img.onload = function() { + if (callback) { + callback(new fabric.Image(img, imgOptions)); + } + img = img.onload = null; + }; + img.src = url; + }; + + /** + * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) + * @static + * @see http://www.w3.org/TR/SVG/struct.html#ImageElement + */ + fabric.Image.ATTRIBUTE_NAMES = 'x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href'.split(' '); + + /** + * Returns {@link fabric.Image} instance from an SVG element + * @static + * @method fabric.Image.fromElement + * @param {SVGElement} element Element to parse + * @param {Function} callback Callback to execute when fabric.Image object is created + * @param {Object} [options] Options object + * @return {fabric.Image} + */ + fabric.Image.fromElement = function(element, callback, options) { + options || (options = { }); + + var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES); + + fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend(parsedAttributes, options)); + }; + + fabric.Image.async = true; + +})(typeof exports != 'undefined' ? exports : this); + +fabric.util.object.extend(fabric.Object.prototype, { + + /** + * @method _getAngleValueForStraighten + * @return {Number} angle value + * @private + */ + _getAngleValueForStraighten: function() { + var angle = this.get('angle'); + + // TODO (kangax): can this be simplified? + + if (angle > -225 && angle <= -135) { return -180; } + else if (angle > -135 && angle <= -45) { return -90; } + else if (angle > -45 && angle <= 45) { return 0; } + else if (angle > 45 && angle <= 135) { return 90; } + else if (angle > 135 && angle <= 225 ) { return 180; } + else if (angle > 225 && angle <= 315) { return 270; } + else if (angle > 315) { return 360; } + + return 0; + }, + + /** + * @method straighten + * @return {fabric.Object} thisArg + * @chainable + */ + straighten: function() { + var angle = this._getAngleValueForStraighten(); + this.setAngle(angle); + return this; + }, + + /** + * @method fxStraighten + * @param {Object} callbacks + * - onComplete: invoked on completion + * - onChange: invoked on every step of animation + * + * @return {fabric.Object} thisArg + * @chainable + */ + fxStraighten: function(callbacks) { + callbacks = callbacks || { }; + + var empty = function() { }, + onComplete = callbacks.onComplete || empty, + onChange = callbacks.onChange || empty, + _this = this; + + fabric.util.animate({ + startValue: this.get('angle'), + endValue: this._getAngleValueForStraighten(), + duration: this.FX_DURATION, + onChange: function(value) { + _this.setAngle(value); + onChange(); + }, + onComplete: function() { + _this.setCoords(); + onComplete(); + }, + onStart: function() { + _this.setActive(false); + } + }); + + return this; + } +}); + +fabric.util.object.extend(fabric.StaticCanvas.prototype, { + + /** + * Straightens object, then rerenders canvas + * @method straightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + straightenObject: function (object) { + object.straighten(); + this.renderAll(); + return this; + }, + + /** + * Same as `fabric.Canvas#straightenObject`, but animated + * @method fxStraightenObject + * @param {fabric.Object} object Object to straighten + * @return {fabric.Canvas} thisArg + * @chainable + */ + fxStraightenObject: function (object) { + object.fxStraighten({ + onChange: this.renderAll.bind(this) + }); + return this; + } +}); +/** + * @namespace + */ +fabric.Image.filters = { }; + +/** + * @class fabric.Image.filters.Grayscale + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Grayscale = fabric.util.createClass( /** @scope fabric.Image.filters.Grayscale.prototype */ { + + /** + * @param {String} type + */ + type: "Grayscale", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Grayscale.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = imageData.width, + jLen = imageData.height, + index, average, i, j; + + for (i = 0; i < iLen; i++) { + for (j = 0; j < jLen; j++) { + + index = (i * 4) * jLen + (j * 4); + average = (data[index] + data[index + 1] + data[index + 2]) / 3; + + data[index] = average; + data[index + 1] = average; + data[index + 2] = average; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Grayscale.fromObject = function() { + return new fabric.Image.filters.Grayscale(); +}; + +/** + * @class fabric.Image.filters.RemoveWhite + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.RemoveWhite = fabric.util.createClass( /** @scope fabric.Image.filters.RemoveWhite.prototype */ { + + /** + * @param {String} type + */ + type: "RemoveWhite", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 30; + this.distance = options.distance || 20; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + distance = this.distance, + limit = 255 - threshold, + abs = Math.abs, + r, g, b; + + for (var i = 0, len = data.length; i < len; i += 4) { + + r = data[i]; + g = data[i+1]; + b = data[i+2]; + + if (r > limit && + g > limit && + b > limit && + abs(r-g) < distance && + abs(r-b) < distance && + abs(g-b) < distance) { + + data[i+3] = 1; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold, + distance: this.distance + }; + } +}); + +fabric.Image.filters.RemoveWhite.fromObject = function(object) { + return new fabric.Image.filters.RemoveWhite(object); +}; + +/** + * @class fabric.Image.filters.Invert + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Invert = fabric.util.createClass( /** @scope fabric.Image.filters.Invert.prototype */ { + + /** + * @param {String} type + */ + type: "Invert", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Invert.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i; + + for (i = 0; i < iLen; i+=4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Invert.fromObject = function() { + return new fabric.Image.filters.Invert(); +}; + +/** + * @class fabric.Image.filters.Sepia + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, avg; + + for (i = 0; i < iLen; i+=4) { + avg = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; + data[i] = avg + 100; + data[i + 1] = avg + 50; + data[i + 2] = avg + 255; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia.fromObject = function() { + return new fabric.Image.filters.Sepia(); +}; + +/** + * @class fabric.Image.filters.Sepia2 + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Sepia2 = fabric.util.createClass( /** @scope fabric.Image.filters.Sepia2.prototype */ { + + /** + * @param {String} type + */ + type: "Sepia2", + + /** + * @method applyTo + * @memberOf fabric.Image.filters.Sepia.prototype + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, r, g, b; + + for (i = 0; i < iLen; i+=4) { + + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + + data[i] = (r * 0.393 + g * 0.769 + b * 0.189 ) / 1.351; + data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168 ) / 1.203; + data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131 ) / 2.140; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { type: this.type }; + } +}); + +fabric.Image.filters.Sepia2.fromObject = function() { + return new fabric.Image.filters.Sepia2(); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Brightness = fabric.util.createClass( /** @scope fabric.Image.filters.Brightness.prototype */ { + + /** + * @param {String} type + */ + type: "Brightness", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.brightness = options.brightness || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + brightness = this.brightness; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i] += brightness; + data[i + 1] += brightness; + data[i + 2] += brightness; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + brightness: this.brightness + }; + } +}); + +fabric.Image.filters.Brightness.fromObject = function(object) { + return new fabric.Image.filters.Brightness(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Noise = fabric.util.createClass( /** @scope fabric.Image.filters.Noise.prototype */ { + + /** + * @param {String} type + */ + type: "Noise", + + /** + * @memberOf fabric.Image.filters.Brightness.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.noise = options.noise || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + noise = this.noise, rand; + + for (var i = 0, len = data.length; i < len; i += 4) { + + rand = (0.5 - Math.random()) * noise; + + data[i] += rand; + data[i + 1] += rand; + data[i + 2] += rand; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + noise: this.noise + }; + } +}); + +fabric.Image.filters.Noise.fromObject = function(object) { + return new fabric.Image.filters.Noise(object); +}; + +/** + * @class fabric.Image.filters.Brightness + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.GradientTransparency = fabric.util.createClass( /** @scope fabric.Image.filters.GradientTransparency.prototype */ { + + /** + * @param {String} type + */ + type: "GradientTransparency", + + /** + * @memberOf fabric.Image.filters.GradientTransparency.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.threshold = options.threshold || 100; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + threshold = this.threshold, + total = data.length; + + for (var i = 0, len = data.length; i < len; i += 4) { + data[i + 3] = threshold + 255 * (total - i) / total; + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + threshold: this.threshold + }; + } +}); + +fabric.Image.filters.GradientTransparency.fromObject = function(object) { + return new fabric.Image.filters.GradientTransparency(object); +}; + +/** + * @class fabric.Image.filters.Tint + * @memberOf fabric.Image.filters + */ +fabric.Image.filters.Tint = fabric.util.createClass( /** @scope fabric.Image.filters.Tint.prototype */ { + + /** + * @param {String} type + */ + type: "Tint", + + /** + * @memberOf fabric.Image.filters.RemoveWhite.prototype + * @param {Object} [options] Options object + */ + initialize: function(options) { + options || (options = { }); + this.color = options.color || 0; + }, + + /** + * @method applyTo + * @param {Object} canvasEl Canvas element to apply filter to + */ + applyTo: function(canvasEl) { + + var context = canvasEl.getContext('2d'), + imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), + data = imageData.data, + iLen = data.length, i, + r, g, b, a; + + var rgb = parseInt(this.color).toString(16); + var cr = parseInt('0x'+rgb.substr(0, 2)); + var cg = parseInt('0x'+rgb.substr(2, 2)); + var cb = parseInt('0x'+rgb.substr(4, 2)); + + for (i = 0; i < iLen; i+=4) { + + a = data[i+3]; + + if (a > 0){ + data[i] = cr; + data[i+1] = cg; + data[i+2] = cb; + } + } + + context.putImageData(imageData, 0, 0); + }, + + /** + * @method toJSON + * @return {String} json representation of filter + */ + toJSON: function() { + return { + type: this.type, + color: this.color + }; + } +}); + +fabric.Image.filters.Tint.fromObject = function(object) { + return new fabric.Image.filters.Tint(object); +}; +(function(global) { + + "use strict"; + + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + clone = fabric.util.object.clone, + toFixed = fabric.util.toFixed; + + if (fabric.Text) { + fabric.warn('fabric.Text is already defined'); + return; + } + if (!fabric.Object) { + fabric.warn('fabric.Text requires fabric.Object'); + return; + } + + /** + * @class Text + * @extends fabric.Object + */ + fabric.Text = fabric.util.createClass(fabric.Object, /** @scope fabric.Text.prototype */ { + + /** + * @property + * @type Number + */ + fontSize: 40, + + /** + * @property + * @type Number + */ + fontWeight: 100, + + /** + * @property + * @type String + */ + fontFamily: 'Times New Roman', + + /** + * @property + * @type String + */ + textDecoration: '', + + /** + * @property + * @type String | null + */ + textShadow: '', + + /** + * Determines text alignment. Possible values: "left", "center", or "right". + * @property + * @type String + */ + textAlign: 'left', + + /** + * @property + * @type String + */ + fontStyle: '', + + /** + * @property + * @type Number + */ + lineHeight: 1.3, + + /** + * @property + * @type String + */ + strokeStyle: '', + + /** + * @property + * @type Number + */ + strokeWidth: 1, + + /** + * @property + * @type String + */ + backgroundColor: '', + + + /** + * @property + * @type String | null + */ + path: null, + + /** + * @property + * @type String + */ + type: 'text', + + /** + * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) + * @property + * @type Boolean + */ + useNative: true, + + /** + * Constructor + * @method initialize + * @param {String} text + * @param {Object} [options] + * @return {fabric.Text} thisArg + */ + initialize: function(text, options) { + this._initStateProperties(); + this.text = text; + this.setOptions(options || { }); + this._theta = this.angle * Math.PI / 180; + this._initDimensions(); + this.setCoords(); + }, + + /** + * Renders text object on offscreen canvas, so that it would get dimensions + * @private + * @method _initDimensions + */ + _initDimensions: function() { + var canvasEl = fabric.document.createElement('canvas'); + + if (!canvasEl.getContext && typeof G_vmlCanvasManager != 'undefined') { + G_vmlCanvasManager.initElement(canvasEl); + } + + this._render(canvasEl.getContext('2d')); + }, + + /** + * Creates `stateProperties` list on an instance, and adds `fabric.Text` -specific ones to it + * (such as "fontFamily", "fontWeight", etc.) + * @private + * @method _initStateProperties + */ + _initStateProperties: function() { + this.stateProperties = this.stateProperties.concat(); + this.stateProperties.push( + 'fontFamily', + 'fontWeight', + 'fontSize', + 'path', + 'text', + 'textDecoration', + 'textShadow', + 'textAlign', + 'fontStyle', + 'lineHeight', + 'strokeStyle', + 'strokeWidth', + 'backgroundColor', + 'useNative' + ); + fabric.util.removeFromArray(this.stateProperties, 'width'); + }, + + /** + * Returns string representation of an instance + * @method toString + * @return {String} String representation of text object + */ + toString: function() { + return '#<fabric.Text (' + this.complexity() + + '): { "text": "' + this.text + '", "fontFamily": "' + this.fontFamily + '" }>'; + }, + + /** + * @private + * @method _render + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _render: function(ctx) { + if (typeof Cufon === 'undefined' || this.useNative === true) { + this._renderViaNative(ctx); + } + else { + this._renderViaCufon(ctx); + } + }, + + /** + * @private + * @method _renderViaCufon + */ + _renderViaCufon: function(ctx) { + var o = Cufon.textOptions || (Cufon.textOptions = { }); + + // export options to be used by cufon.js + o.left = this.left; + o.top = this.top; + o.context = ctx; + o.color = this.fill; + + var el = this._initDummyElementForCufon(); + + // set "cursor" to top/left corner + this.transform(ctx); + + // draw text + Cufon.replaceElement(el, { + engine: 'canvas', + separate: 'none', + fontFamily: this.fontFamily, + fontWeight: this.fontWeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor + }); + + // update width, height + this.width = o.width; + this.height = o.height; + + this._totalLineHeight = o.totalLineHeight; + this._fontAscent = o.fontAscent; + this._boundaries = o.boundaries; + this._shadowOffsets = o.shadowOffsets; + this._shadows = o.shadows || [ ]; + + el = null; + + // need to set coords _after_ the width/height was retreived from Cufon + this.setCoords(); + }, + + /** + * @private + * @method _render_native + * @param {CanvasRenderingContext2D} ctx Context to render on + */ + _renderViaNative: function(ctx) { + + this.transform(ctx); + this._setTextStyles(ctx); + + var textLines = this.text.split(/\r?\n/); + + this.width = this._getTextWidth(ctx, textLines); + this.height = this._getTextHeight(ctx, textLines); + + this._renderTextBackground(ctx, textLines); + + if (this.textAlign !== 'left') { + ctx.save(); + ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); + } + + this._setTextShadow(ctx); + this._renderTextFill(ctx, textLines); + this.textShadow && ctx.restore(); + + this._renderTextStroke(ctx, textLines); + if (this.textAlign !== 'left') { + ctx.restore(); + } + + this._renderTextDecoration(ctx, textLines); + this._setBoundaries(ctx, textLines); + this._totalLineHeight = 0; + + this.setCoords(); + }, + + /** + * @private + * @method _setBoundaries + */ + _setBoundaries: function(ctx, textLines) { + this._boundaries = [ ]; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + this._boundaries.push({ + height: this.fontSize, + width: lineWidth, + left: lineLeftOffset + }); + } + }, + + /** + * @private + * @method _setTextStyles + */ + _setTextStyles: function(ctx) { + ctx.fillStyle = this.fill; + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.strokeWidth; + ctx.textBaseline = 'top'; + ctx.textAlign = this.textAlign; + ctx.font = this._getFontDeclaration(); + }, + + /** + * @private + * @method _getTextHeight + */ + _getTextHeight: function(ctx, textLines) { + return this.fontSize * textLines.length * this.lineHeight; + }, + + /** + * @private + * @method _getTextWidth + */ + _getTextWidth: function(ctx, textLines) { + var maxWidth = ctx.measureText(textLines[0]).width; + + for (var i = 1, len = textLines.length; i < len; i++) { + var currentLineWidth = ctx.measureText(textLines[i]).width; + if (currentLineWidth > maxWidth) { + maxWidth = currentLineWidth; + } + } + return maxWidth; + }, + + /** + * @private + * @method _setTextShadow + */ + _setTextShadow: function(ctx) { + if (this.textShadow) { + + // "rgba(0,0,0,0.2) 2px 2px 10px" + // "rgb(0, 100, 0) 0 0 5px" + // "red 2px 2px 1px" + // "#f55 123 345 567" + var reOffsetsAndBlur = /\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/; + + var shadowDeclaration = this.textShadow; + var offsetsAndBlur = reOffsetsAndBlur.exec(this.textShadow); + var shadowColor = shadowDeclaration.replace(reOffsetsAndBlur, ''); + + ctx.save(); + ctx.shadowColor = shadowColor; + ctx.shadowOffsetX = parseInt(offsetsAndBlur[1], 10); + ctx.shadowOffsetY = parseInt(offsetsAndBlur[2], 10); + ctx.shadowBlur = parseInt(offsetsAndBlur[3], 10); + + this._shadows = [{ + blur: ctx.shadowBlur, + color: ctx.shadowColor, + offX: ctx.shadowOffsetX, + offY: ctx.shadowOffsetY + }]; + + this._shadowOffsets = [[ + parseInt(ctx.shadowOffsetX, 10), parseInt(ctx.shadowOffsetY, 10) + ]]; + } + }, + + _renderTextFill: function(ctx, textLines) { + this._boundaries = [ ]; + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.fillText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + }, + + /** + * @private + * @method _renderTextStroke + */ + _renderTextStroke: function(ctx, textLines) { + if (this.strokeStyle) { + for (var i = 0, len = textLines.length; i < len; i++) { + ctx.strokeText( + textLines[i], + -this.width / 2, + (-this.height / 2) + (i * this.fontSize * this.lineHeight) + ); + } + } + }, + + /** + * @private + * @_renderTextBackground + */ + _renderTextBackground: function(ctx, textLines) { + if (this.backgroundColor) { + ctx.save(); + ctx.fillStyle = this.backgroundColor; + + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-this.width / 2) + lineLeftOffset, + (-this.height / 2) + (i * this.fontSize * this.lineHeight), + lineWidth, + this.fontSize + ); + } + ctx.restore(); + } + }, + + /** + * @private + * @method _getLineLeftOffset + */ + _getLineLeftOffset: function(lineWidth) { + if (this.textAlign === 'center') { + return (this.width - lineWidth) / 2; + } + if (this.textAlign === 'right') { + return this.width - lineWidth; + } + return 0; + }, + + /** + * @private + * @method _renderTextDecoration + */ + _renderTextDecoration: function(ctx, textLines) { + + var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2; + + function renderLinesAtOffset(offset) { + for (var i = 0, len = textLines.length; i < len; i++) { + + var lineWidth = ctx.measureText(textLines[i]).width; + var lineLeftOffset = this._getLineLeftOffset(lineWidth); + + ctx.fillRect( + (-this.width / 2) + lineLeftOffset, + (offset + (i * this.fontSize * this.lineHeight)) - halfOfVerticalBox, + lineWidth, + 1); + } + } + + if (this.textDecoration.indexOf('underline') > -1) { + renderLinesAtOffset.call(this, this.fontSize); + } + if (this.textDecoration.indexOf('line-through') > -1) { + renderLinesAtOffset.call(this, this.fontSize / 2); + } + if (this.textDecoration.indexOf('overline') > -1) { + renderLinesAtOffset.call(this, 0); + } + }, + + /** + * @private + * @method _getFontDeclaration + */ + _getFontDeclaration: function() { + return [ + this.fontStyle, + this.fontWeight, + this.fontSize + 'px', + (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) + ].join(' '); + }, + + /** + * @private + * @method _initDummyElement + */ + _initDummyElementForCufon: function() { + var el = fabric.document.createElement('pre'), + container = fabric.document.createElement('div'); + + // Cufon doesn't play nice with textDecoration=underline if element doesn't have a parent + container.appendChild(el); + + if (typeof G_vmlCanvasManager == 'undefined') { + el.innerHTML = this.text; + } + else { + // IE 7 & 8 drop newlines and white space on text nodes + // see: http://web.student.tuwien.ac.at/~e0226430/innerHtmlQuirk.html + // see: http://www.w3schools.com/dom/dom_mozilla_vs_ie.asp + el.innerText = this.text.replace(/\r?\n/gi, '\r'); + } + + el.style.fontSize = this.fontSize + 'px'; + el.style.letterSpacing = 'normal'; + + return el; + }, + + /** + * Renders text instance on a specified context + * @method render + * @param ctx {CanvasRenderingContext2D} context to render on + */ + render: function(ctx, noTransform) { + ctx.save(); + this._render(ctx); + if (!noTransform && this.active) { + this.drawBorders(ctx); + this.hideCorners || this.drawCorners(ctx); + } + ctx.restore(); + }, + + /** + * Returns object representation of an instance + * @method toObject + * @return {Object} Object representation of text object + */ + toObject: function() { + return extend(this.callSuper('toObject'), { + text: this.text, + fontSize: this.fontSize, + fontWeight: this.fontWeight, + fontFamily: this.fontFamily, + fontStyle: this.fontStyle, + lineHeight: this.lineHeight, + textDecoration: this.textDecoration, + textShadow: this.textShadow, + textAlign: this.textAlign, + path: this.path, + strokeStyle: this.strokeStyle, + strokeWidth: this.strokeWidth, + backgroundColor: this.backgroundColor, + useNative: this.useNative + }); + }, + + /** + * Returns svg representation of an instance + * @method toSVG + * @return {string} svg representation of an instance + */ + toSVG: function() { + + var textLines = this.text.split(/\r?\n/), + lineTopOffset = this.useNative + ? this.fontSize * this.lineHeight + : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), + + textLeftOffset = -(this.width/2), + textTopOffset = this.useNative + ? this.fontSize - 1 + : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight, + + textAndBg = this._getSVGTextAndBg(lineTopOffset, textLeftOffset, textLines), + shadowSpans = this._getSVGShadows(lineTopOffset, textLines); + + // move top offset by an ascent + textTopOffset += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); + + return [ + '<g transform="', this.getSvgTransform(), '">', + textAndBg.textBgRects.join(''), + '<text ', + (this.fontFamily ? 'font-family="\'' + this.fontFamily + '\'" ': ''), + (this.fontSize ? 'font-size="' + this.fontSize + '" ': ''), + (this.fontStyle ? 'font-style="' + this.fontStyle + '" ': ''), + (this.fontWeight ? 'font-weight="' + this.fontWeight + '" ': ''), + (this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ': ''), + 'style="', this.getSvgStyles(), '" ', + /* svg starts from left/bottom corner so we normalize height */ + 'transform="translate(', toFixed(textLeftOffset, 2), ' ', toFixed(textTopOffset, 2), ')">', + shadowSpans.join(''), + textAndBg.textSpans.join(''), + '</text>', + '</g>' + ].join(''); + }, + + _getSVGShadows: function(lineTopOffset, textLines) { + var shadowSpans = [], j, i, jlen, ilen, lineTopOffsetMultiplier = 1; + + if (!this._shadows || !this._boundaries) { + return shadowSpans; + } + + for (j = 0, jlen = this._shadows.length; j < jlen; j++) { + for (i = 0, ilen = textLines.length; i < ilen; i++) { + if (textLines[i] !== '') { + var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; + shadowSpans.push( + '<tspan x="', + toFixed((lineLeftOffset + lineTopOffsetMultiplier) + this._shadowOffsets[j][0], 2), + ((i === 0 || this.useNative) ? '" y' : '" dy'), '="', + toFixed(this.useNative + ? ((lineTopOffset * i) - this.height / 2 + this._shadowOffsets[j][1]) + : (lineTopOffset + (i === 0 ? this._shadowOffsets[j][1] : 0)), 2), + '" ', + this._getFillAttributes(this._shadows[j].color), '>', + fabric.util.string.escapeXml(textLines[i]), + '</tspan>'); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + } + } + return shadowSpans; + }, + + _getSVGTextAndBg: function(lineTopOffset, textLeftOffset, textLines) { + var textSpans = [ ], textBgRects = [ ], i, lineLeftOffset, len, lineTopOffsetMultiplier = 1; + + // text and background + for (i = 0, len = textLines.length; i < len; i++) { + if (textLines[i] !== '') { + lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; + textSpans.push( + '<tspan x="', + lineLeftOffset, '" ', + (i === 0 || this.useNative ? 'y' : 'dy'), '="', + toFixed(this.useNative ? ((lineTopOffset * i) - this.height / 2) : (lineTopOffset * lineTopOffsetMultiplier), 2) , '" ', + // doing this on <tspan> elements since setting opacity on containing <text> one doesn't work in Illustrator + this._getFillAttributes(this.fill), '>', + fabric.util.string.escapeXml(textLines[i]), + '</tspan>' + ); + lineTopOffsetMultiplier = 1; + } else { + // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier + // prevents empty tspans + lineTopOffsetMultiplier++; + } + + if (!this.backgroundColor || !this._boundaries) continue; + + textBgRects.push( + '<rect ', + this._getFillAttributes(this.backgroundColor), + ' x="', + toFixed(textLeftOffset + this._boundaries[i].left, 2), + '" y="', + /* an offset that seems to straighten things out */ + toFixed((lineTopOffset * i) - this.height / 2, 2), + '" width="', + toFixed(this._boundaries[i].width, 2), + '" height="', + toFixed(this._boundaries[i].height, 2), + '"></rect>'); + } + return { + textSpans: textSpans, + textBgRects: textBgRects + }; + }, + + // Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values + // we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1 + _getFillAttributes: function(value) { + var fillColor = value ? new fabric.Color(value) : ''; + if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { + return 'fill="' + value + '"'; + } + return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; + }, + + /** + * Sets "color" of an instance (alias of `set('fill', …)`) + * @method setColor + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setColor: function(value) { + this.set('fill', value); + return this; + }, + + /** + * Sets fontSize of an instance and updates its coordinates + * @method setFontsize + * @param {Number} value + * @return {fabric.Text} thisArg + * @chainable + */ + setFontsize: function(value) { + this.set('fontSize', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Returns actual text value of an instance + * @method getText + * @return {String} + */ + getText: function() { + return this.text; + }, + + /** + * Sets text of an instance, and updates its coordinates + * @method setText + * @param {String} value + * @return {fabric.Text} thisArg + * @chainable + */ + setText: function(value) { + this.set('text', value); + this._initDimensions(); + this.setCoords(); + return this; + }, + + /** + * Sets specified property to a specified value + * @method set + * @param {String} name + * @param {Any} value + * @return {fabric.Text} thisArg + * @chainable + */ + _set: function(name, value) { + if (name === 'fontFamily' && this.path) { + this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); + } + this.callSuper('_set', name, value); + } + }); + + /** + * List of attribute names to account for when parsing SVG element (used by `fabric.Text.fromElement`) + * @static + */ + fabric.Text.ATTRIBUTE_NAMES = + ('x y fill fill-opacity opacity stroke stroke-width transform ' + + 'font-family font-style font-weight font-size text-decoration').split(' '); + + /** + * Returns fabric.Text instance from an object representation + * @static + * @method fromObject + * @param {Object} object to create an instance from + * @return {fabric.Text} an instance + */ + fabric.Text.fromObject = function(object) { + return new fabric.Text(object.text, clone(object)); + }; + + /** + * Returns fabric.Text instance from an SVG element (<b>not yet implemented</b>) + * @static + * @method fabric.Text.fromElement + * @param element + * @param options + * @return {fabric.Text} an instance + */ + fabric.Text.fromElement = function(element, options) { + if (!element) { + return null; + } + + var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); + var options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); + var text = new fabric.Text(element.textContent, options); + + return text; + }; + +})(typeof exports != 'undefined' ? exports : this); +(function() { + + if (typeof document != 'undefined' && typeof window != 'undefined') { + return; + } + + var DOMParser = new require('xmldom').DOMParser, + URL = require('url'), + HTTP = require('http'), + + Canvas = require('canvas'), + Image = require('canvas').Image; + + function request(url, encoding, callback) { + var oURL = URL.parse(url), + client = HTTP.createClient(oURL.port, oURL.hostname), + request = client.request('GET', oURL.pathname, { 'host': oURL.hostname }); + + client.addListener('error', function(err) { + if (err.errno === process.ECONNREFUSED) { + fabric.log('ECONNREFUSED: connection refused to ' + client.host + ':' + client.port); + } + else { + fabric.log(err.message); + } + }); + + request.end(); + request.on('response', function (response) { + var body = ""; + if (encoding) { + response.setEncoding(encoding); + } + response.on('end', function () { + callback(body); + }); + response.on('data', function (chunk) { + if (response.statusCode == 200) { + body += chunk; + } + }); + }); + } + + fabric.util.loadImage = function(url, callback) { + request(url, 'binary', function(body) { + var img = new Image(); + img.src = new Buffer(body, 'binary'); + // preserving original url, which seems to be lost in node-canvas + img._src = url; + callback(img); + }); + }; + + fabric.loadSVGFromURL = function(url, callback) { + url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); + request(url, '', function(body) { + fabric.loadSVGFromString(body, callback); + }); + }; + + fabric.loadSVGFromString = function(string, callback) { + var doc = new DOMParser().parseFromString(string); + fabric.parseSVGDocument(doc.documentElement, function(results, options) { + callback(results, options); + }); + }; + + fabric.util.getScript = function(url, callback) { + request(url, '', function(body) { + eval(body); + callback && callback(); + }); + }; + + fabric.Image.fromObject = function(object, callback) { + fabric.util.loadImage(object.src, function(img) { + var oImg = new fabric.Image(img); + + oImg._initConfig(object); + oImg._initFilters(object); + callback(oImg); + }); + }; + + /** + * Only available when running fabric on node.js + * @method createCanvasForNode + * @param width Canvas width + * @param height Canvas height + * @return {Object} wrapped canvas instance + */ + fabric.createCanvasForNode = function(width, height) { + + var canvasEl = fabric.document.createElement('canvas'), + nodeCanvas = new Canvas(width || 600, height || 600); + + // jsdom doesn't create style on canvas element, so here be temp. workaround + canvasEl.style = { }; + + canvasEl.width = nodeCanvas.width; + canvasEl.height = nodeCanvas.height; + + var canvas = fabric.Canvas || fabric.StaticCanvas; + var fabricCanvas = new canvas(canvasEl); + fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); + fabricCanvas.nodeCanvas = nodeCanvas; + + return fabricCanvas; + }; + + fabric.StaticCanvas.prototype.createPNGStream = function() { + return this.nodeCanvas.createPNGStream(); + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.createPNGStream + } + + var origSetWidth = fabric.StaticCanvas.prototype.setWidth; + fabric.StaticCanvas.prototype.setWidth = function(width) { + origSetWidth.call(this); + this.nodeCanvas.width = width; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth; + } + + var origSetHeight = fabric.StaticCanvas.prototype.setHeight; + fabric.StaticCanvas.prototype.setHeight = function(height) { + origSetHeight.call(this); + this.nodeCanvas.height = height; + return this; + }; + if (fabric.Canvas) { + fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; + } + +})(); diff --git a/js/jquery.miniColors.min.js b/js/jquery.miniColors.min.js new file mode 100644 index 0000000..1d33464 --- /dev/null +++ b/js/jquery.miniColors.min.js @@ -0,0 +1,9 @@ +/* + * jQuery miniColors: A small color selector + * + * Copyright 2012 Cory LaViska for A Beautiful Site, LLC. (http://www.abeautifulsite.net/) + * + * Dual licensed under the MIT or GPL Version 2 licenses + * +*/ +if(jQuery)(function($){$.extend($.fn,{miniColors:function(o,data){var create=function(input,o,data){var color=expandHex(input.val())||'ffffff',hsb=hex2hsb(color),rgb=hsb2rgb(hsb),alpha=parseFloat(input.attr('data-opacity')).toFixed(2);if(alpha>1)alpha=1;if(alpha<0)alpha=0;var trigger=$('<a class="miniColors-trigger" style="background-color: #'+color+'" href="#"></a>');trigger.insertAfter(input);trigger.wrap('<span class="miniColors-triggerWrap"></span>');if(o.opacity){trigger.css('backgroundColor','rgba('+rgb.r+', '+rgb.g+', '+rgb.b+', '+alpha+')')}input.addClass('miniColors').data('original-maxlength',input.attr('maxlength')||null).data('original-autocomplete',input.attr('autocomplete')||null).data('letterCase',o.letterCase==='uppercase'?'uppercase':'lowercase').data('opacity',o.opacity?true:false).data('alpha',alpha).data('trigger',trigger).data('hsb',hsb).data('change',o.change?o.change:null).data('close',o.close?o.close:null).data('open',o.open?o.open:null).attr('maxlength',7).attr('autocomplete','off').val('#'+convertCase(color,o.letterCase));if(o.readonly||input.prop('readonly'))input.prop('readonly',true);if(o.disabled||input.prop('disabled'))disable(input);trigger.on('click.miniColors',function(event){event.preventDefault();if(input.val()==='')input.val('#');show(input)});input.on('focus.miniColors',function(event){if(input.val()==='')input.val('#');show(input)});input.on('blur.miniColors',function(event){var hex=expandHex(hsb2hex(input.data('hsb')));input.val(hex?'#'+convertCase(hex,input.data('letterCase')):'')});input.on('keydown.miniColors',function(event){if(event.keyCode===9)hide(input)});input.on('keyup.miniColors',function(event){setColorFromInput(input)});input.on('paste.miniColors',function(event){setTimeout(function(){setColorFromInput(input)},5)})};var destroy=function(input){hide();input=$(input);input.data('trigger').parent().remove();input.attr('autocomplete',input.data('original-autocomplete')).attr('maxlength',input.data('original-maxlength')).removeData().removeClass('miniColors').off('.miniColors');$(document).off('.miniColors')};var enable=function(input){input.prop('disabled',false).data('trigger').parent().removeClass('disabled')};var disable=function(input){hide(input);input.prop('disabled',true).data('trigger').parent().addClass('disabled')};var show=function(input){if(input.prop('disabled'))return false;hide();var selector=$('<div class="miniColors-selector"></div>');selector.append('<div class="miniColors-hues"><div class="miniColors-huePicker"></div></div>').append('<div class="miniColors-colors" style="background-color: #FFF;"><div class="miniColors-colorPicker"><div class="miniColors-colorPicker-inner"></div></div>').css('display','none').addClass(input.attr('class'));if(input.data('opacity')){selector.addClass('opacity').prepend('<div class="miniColors-opacity"><div class="miniColors-opacityPicker"></div></div>')}var hsb=input.data('hsb');selector.find('.miniColors-colors').css('backgroundColor','#'+hsb2hex({h:hsb.h,s:100,b:100})).end().find('.miniColors-opacity').css('backgroundColor','#'+hsb2hex({h:hsb.h,s:hsb.s,b:hsb.b})).end();var colorPosition=input.data('colorPosition');if(!colorPosition)colorPosition=getColorPositionFromHSB(hsb);selector.find('.miniColors-colorPicker').css('top',colorPosition.y+'px').css('left',colorPosition.x+'px');var huePosition=input.data('huePosition');if(!huePosition)huePosition=getHuePositionFromHSB(hsb);selector.find('.miniColors-huePicker').css('top',huePosition+'px');var opacityPosition=input.data('opacityPosition');if(!opacityPosition)opacityPosition=getOpacityPositionFromAlpha(input.attr('data-opacity'));selector.find('.miniColors-opacityPicker').css('top',opacityPosition+'px');input.data('selector',selector).data('huePicker',selector.find('.miniColors-huePicker')).data('opacityPicker',selector.find('.miniColors-opacityPicker')).data('colorPicker',selector.find('.miniColors-colorPicker')).data('mousebutton',0);$('BODY').append(selector);var trigger=input.data('trigger'),hidden=!input.is(':visible'),top=hidden?trigger.offset().top+trigger.outerHeight():input.offset().top+input.outerHeight(),left=hidden?trigger.offset().left:input.offset().left,selectorWidth=selector.outerWidth(),selectorHeight=selector.outerHeight(),triggerWidth=trigger.outerWidth(),triggerHeight=trigger.outerHeight(),windowHeight=$(window).height(),windowWidth=$(window).width(),scrollTop=$(window).scrollTop(),scrollLeft=$(window).scrollLeft();if((top+selectorHeight)>windowHeight+scrollTop)top=top-selectorHeight-triggerHeight;if((left+selectorWidth)>windowWidth+scrollLeft)left=left-selectorWidth+triggerWidth;selector.css({top:top,left:left}).fadeIn(100);selector.on('selectstart',function(){return false});if(!$.browser.msie||($.browser.msie&&$.browser.version>=9)){$(window).on('resize.miniColors',function(event){hide(input)})}$(document).on('mousedown.miniColors touchstart.miniColors',function(event){input.data('mousebutton',1);var testSubject=$(event.target).parents().andSelf();if(testSubject.hasClass('miniColors-colors')){event.preventDefault();input.data('moving','colors');moveColor(input,event)}if(testSubject.hasClass('miniColors-hues')){event.preventDefault();input.data('moving','hues');moveHue(input,event)}if(testSubject.hasClass('miniColors-opacity')){event.preventDefault();input.data('moving','opacity');moveOpacity(input,event)}if(testSubject.hasClass('miniColors-selector')){event.preventDefault();return}if(testSubject.hasClass('miniColors'))return;hide(input)}).on('mouseup.miniColors touchend.miniColors',function(event){event.preventDefault();input.data('mousebutton',0).removeData('moving')}).on('mousemove.miniColors touchmove.miniColors',function(event){event.preventDefault();if(input.data('mousebutton')===1){if(input.data('moving')==='colors')moveColor(input,event);if(input.data('moving')==='hues')moveHue(input,event);if(input.data('moving')==='opacity')moveOpacity(input,event)}});if(input.data('open')){input.data('open').call(input.get(0),'#'+hsb2hex(hsb),$.extend(hsb2rgb(hsb),{a:parseFloat(input.attr('data-opacity'))}))}};var hide=function(input){if(!input)input=$('.miniColors');input.each(function(){var selector=$(this).data('selector');$(this).removeData('selector');$(selector).fadeOut(100,function(){if(input.data('close')){var hsb=input.data('hsb'),hex=hsb2hex(hsb);input.data('close').call(input.get(0),'#'+hex,$.extend(hsb2rgb(hsb),{a:parseFloat(input.attr('data-opacity'))}))}$(this).remove()})});$(document).off('.miniColors')};var moveColor=function(input,event){var colorPicker=input.data('colorPicker');colorPicker.hide();var position={x:event.pageX,y:event.pageY};if(event.originalEvent.changedTouches){position.x=event.originalEvent.changedTouches[0].pageX;position.y=event.originalEvent.changedTouches[0].pageY}position.x=position.x-input.data('selector').find('.miniColors-colors').offset().left-6;position.y=position.y-input.data('selector').find('.miniColors-colors').offset().top-6;if(position.x<=-5)position.x=-5;if(position.x>=144)position.x=144;if(position.y<=-5)position.y=-5;if(position.y>=144)position.y=144;input.data('colorPosition',position);colorPicker.css('left',position.x).css('top',position.y).show();var s=Math.round((position.x+5)*0.67);if(s<0)s=0;if(s>100)s=100;var b=100-Math.round((position.y+5)*0.67);if(b<0)b=0;if(b>100)b=100;var hsb=input.data('hsb');hsb.s=s;hsb.b=b;setColor(input,hsb,true)};var moveHue=function(input,event){var huePicker=input.data('huePicker');huePicker.hide();var position=event.pageY;if(event.originalEvent.changedTouches){position=event.originalEvent.changedTouches[0].pageY}position=position-input.data('selector').find('.miniColors-colors').offset().top-1;if(position<=-1)position=-1;if(position>=149)position=149;input.data('huePosition',position);huePicker.css('top',position).show();var h=Math.round((150-position-1)*2.4);if(h<0)h=0;if(h>360)h=360;var hsb=input.data('hsb');hsb.h=h;setColor(input,hsb,true)};var moveOpacity=function(input,event){var opacityPicker=input.data('opacityPicker');opacityPicker.hide();var position=event.pageY;if(event.originalEvent.changedTouches){position=event.originalEvent.changedTouches[0].pageY}position=position-input.data('selector').find('.miniColors-colors').offset().top-1;if(position<=-1)position=-1;if(position>=149)position=149;input.data('opacityPosition',position);opacityPicker.css('top',position).show();var alpha=parseFloat((150-position-1)/150).toFixed(2);if(alpha<0)alpha=0;if(alpha>1)alpha=1;input.data('alpha',alpha).attr('data-opacity',alpha);setColor(input,input.data('hsb'),true)};var setColor=function(input,hsb,updateInput){input.data('hsb',hsb);var hex=hsb2hex(hsb),selector=$(input.data('selector'));if(updateInput)input.val('#'+convertCase(hex,input.data('letterCase')));selector.find('.miniColors-colors').css('backgroundColor','#'+hsb2hex({h:hsb.h,s:100,b:100})).end().find('.miniColors-opacity').css('backgroundColor','#'+hex).end();var rgb=hsb2rgb(hsb);input.data('trigger').css('backgroundColor','#'+hex);if(input.data('opacity')){input.data('trigger').css('backgroundColor','rgba('+rgb.r+', '+rgb.g+', '+rgb.b+', '+input.attr('data-opacity')+')')}if(input.data('change')){if((hex+','+input.attr('data-opacity'))===input.data('lastChange'))return;input.data('change').call(input.get(0),'#'+hex,$.extend(hsb2rgb(hsb),{a:parseFloat(input.attr('data-opacity'))}));input.data('lastChange',hex+','+input.attr('data-opacity'))}};var setColorFromInput=function(input){input.val('#'+cleanHex(input.val()));var hex=expandHex(input.val());if(!hex)return false;var hsb=hex2hsb(hex);var colorPosition=getColorPositionFromHSB(hsb);var colorPicker=$(input.data('colorPicker'));colorPicker.css('top',colorPosition.y+'px').css('left',colorPosition.x+'px');input.data('colorPosition',colorPosition);var huePosition=getHuePositionFromHSB(hsb);var huePicker=$(input.data('huePicker'));huePicker.css('top',huePosition+'px');input.data('huePosition',huePosition);var opacityPosition=getOpacityPositionFromAlpha(input.attr('data-opacity'));var opacityPicker=$(input.data('opacityPicker'));opacityPicker.css('top',opacityPosition+'px');input.data('opacityPosition',opacityPosition);setColor(input,hsb);return true};var convertCase=function(string,letterCase){if(letterCase==='uppercase'){return string.toUpperCase()}else{return string.toLowerCase()}};var getColorPositionFromHSB=function(hsb){var x=Math.ceil(hsb.s/0.67);if(x<0)x=0;if(x>150)x=150;var y=150-Math.ceil(hsb.b/0.67);if(y<0)y=0;if(y>150)y=150;return{x:x-5,y:y-5}};var getHuePositionFromHSB=function(hsb){var y=150-(hsb.h/2.4);if(y<0)h=0;if(y>150)h=150;return y};var getOpacityPositionFromAlpha=function(alpha){var y=150*alpha;if(y<0)y=0;if(y>150)y=150;return 150-y};var cleanHex=function(hex){return hex.replace(/[^A-F0-9]/ig,'')};var expandHex=function(hex){hex=cleanHex(hex);if(!hex)return null;if(hex.length===3)hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];return hex.length===6?hex:null};var hsb2rgb=function(hsb){var rgb={};var h=Math.round(hsb.h);var s=Math.round(hsb.s*255/100);var v=Math.round(hsb.b*255/100);if(s===0){rgb.r=rgb.g=rgb.b=v}else{var t1=v;var t2=(255-s)*v/255;var t3=(t1-t2)*(h%60)/60;if(h===360)h=0;if(h<60){rgb.r=t1;rgb.b=t2;rgb.g=t2+t3}else if(h<120){rgb.g=t1;rgb.b=t2;rgb.r=t1-t3}else if(h<180){rgb.g=t1;rgb.r=t2;rgb.b=t2+t3}else if(h<240){rgb.b=t1;rgb.r=t2;rgb.g=t1-t3}else if(h<300){rgb.b=t1;rgb.g=t2;rgb.r=t2+t3}else if(h<360){rgb.r=t1;rgb.g=t2;rgb.b=t1-t3}else{rgb.r=0;rgb.g=0;rgb.b=0}}return{r:Math.round(rgb.r),g:Math.round(rgb.g),b:Math.round(rgb.b)}};var rgb2hex=function(rgb){var hex=[rgb.r.toString(16),rgb.g.toString(16),rgb.b.toString(16)];$.each(hex,function(nr,val){if(val.length===1)hex[nr]='0'+val});return hex.join('')};var hex2rgb=function(hex){hex=parseInt(((hex.indexOf('#')>-1)?hex.substring(1):hex),16);return{r:hex>>16,g:(hex&0x00FF00)>>8,b:(hex&0x0000FF)}};var rgb2hsb=function(rgb){var hsb={h:0,s:0,b:0};var min=Math.min(rgb.r,rgb.g,rgb.b);var max=Math.max(rgb.r,rgb.g,rgb.b);var delta=max-min;hsb.b=max;hsb.s=max!==0?255*delta/max:0;if(hsb.s!==0){if(rgb.r===max){hsb.h=(rgb.g-rgb.b)/delta}else if(rgb.g===max){hsb.h=2+(rgb.b-rgb.r)/delta}else{hsb.h=4+(rgb.r-rgb.g)/delta}}else{hsb.h=-1}hsb.h*=60;if(hsb.h<0){hsb.h+=360}hsb.s*=100/255;hsb.b*=100/255;return hsb};var hex2hsb=function(hex){var hsb=rgb2hsb(hex2rgb(hex));if(hsb.s===0)hsb.h=360;return hsb};var hsb2hex=function(hsb){return rgb2hex(hsb2rgb(hsb))};switch(o){case'readonly':$(this).each(function(){if(!$(this).hasClass('miniColors'))return;$(this).prop('readonly',data)});return $(this);case'disabled':$(this).each(function(){if(!$(this).hasClass('miniColors'))return;if(data){disable($(this))}else{enable($(this))}});return $(this);case'value':if(data===undefined){if(!$(this).hasClass('miniColors'))return;var input=$(this),hex=expandHex(input.val());return hex?'#'+convertCase(hex,input.data('letterCase')):null}$(this).each(function(){if(!$(this).hasClass('miniColors'))return;$(this).val(data);setColorFromInput($(this))});return $(this);case'opacity':if(data===undefined){if(!$(this).hasClass('miniColors'))return;if($(this).data('opacity')){return parseFloat($(this).attr('data-opacity'))}else{return null}}$(this).each(function(){if(!$(this).hasClass('miniColors'))return;if(data<0)data=0;if(data>1)data=1;$(this).attr('data-opacity',data).data('alpha',data);setColorFromInput($(this))});return $(this);case'destroy':$(this).each(function(){if(!$(this).hasClass('miniColors'))return;destroy($(this))});return $(this);default:if(!o)o={};$(this).each(function(){if($(this)[0].tagName.toLowerCase()!=='input')return;if($(this).data('trigger'))return;create($(this),o,data)});return $(this)}}})})(jQuery);
\ No newline at end of file diff --git a/js/tshirtEditor.js b/js/tshirtEditor.js new file mode 100644 index 0000000..1b3f8c0 --- /dev/null +++ b/js/tshirtEditor.js @@ -0,0 +1,355 @@ +var canvas; +var tshirts = new Array(); //prototype: [{style:'x',color:'white',front:'a',back:'b',price:{tshirt:'12.95',frontPrint:'4.99',backPrint:'4.99',total:'22.47'}}] +var a; +var b; +var line1; +var line2; +var line3; +var line4; + $(document).ready(function() { + //setup front side canvas + canvas = new fabric.Canvas('tcanvas', { + hoverCursor: 'pointer', + selection: true, + selectionBorderColor:'blue' + }); + canvas.on({ + 'object:moving': function(e) { + e.target.opacity = 0.5; + }, + 'object:modified': function(e) { + e.target.opacity = 1; + }, + 'object:selected':onObjectSelected, + 'selection:cleared':onSelectedCleared + }); + // piggyback on `canvas.findTarget`, to fire "object:over" and "object:out" events + canvas.findTarget = (function(originalFn) { + return function() { + var target = originalFn.apply(this, arguments); + if (target) { + if (this._hoveredTarget !== target) { + canvas.fire('object:over', { target: target }); + if (this._hoveredTarget) { + canvas.fire('object:out', { target: this._hoveredTarget }); + } + this._hoveredTarget = target; + } + } + else if (this._hoveredTarget) { + canvas.fire('object:out', { target: this._hoveredTarget }); + this._hoveredTarget = null; + } + return target; + }; + })(canvas.findTarget); + + canvas.on('object:over', function(e) { + //e.target.setFill('red'); + //canvas.renderAll(); + }); + + canvas.on('object:out', function(e) { + //e.target.setFill('green'); + //canvas.renderAll(); + }); + + document.getElementById('add-text').onclick = function() { + var text = $("#text-string").val(); + var textSample = new fabric.Text(text, { + left: fabric.util.getRandomInt(0, 200), + top: fabric.util.getRandomInt(0, 400), + fontFamily: 'helvetica', + angle: 0, + fill: '#000000', + scaleX: 0.5, + scaleY: 0.5, + fontWeight: '', + hasRotatingPoint:true + }); + canvas.add(textSample); + canvas.item(canvas.item.length-1).hasRotatingPoint = true; + $("#texteditor").css('display', 'block'); + $("#imageeditor").css('display', 'block'); + }; + $("#text-string").keyup(function(){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.text = this.value; + canvas.renderAll(); + } + }); + $(".img-polaroid").click(function(e){ + var el = e.target; + /*temp code*/ + var offset = 50; + var left = fabric.util.getRandomInt(0 + offset, 200 - offset); + var top = fabric.util.getRandomInt(0 + offset, 400 - offset); + var angle = fabric.util.getRandomInt(-20, 40); + var width = fabric.util.getRandomInt(30, 50); + var opacity = (function(min, max){ return Math.random() * (max - min) + min; })(0.5, 1); + + fabric.Image.fromURL(el.src, function(image) { + image.set({ + left: left, + top: top, + angle: 0, + padding: 10, + cornersize: 10, + hasRotatingPoint:true + }); + //image.scale(getRandomNum(0.1, 0.25)).setCoords(); + canvas.add(image); + }); + }); + document.getElementById('remove-selected').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + canvas.remove(activeObject); + $("#text-string").val(""); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + canvas.remove(object); + }); + } + }; + document.getElementById('bring-to-front').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + activeObject.bringToFront(); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + object.bringToFront(); + }); + } + }; + document.getElementById('send-to-back').onclick = function() { + var activeObject = canvas.getActiveObject(), + activeGroup = canvas.getActiveGroup(); + if (activeObject) { + activeObject.sendToBack(); + } + else if (activeGroup) { + var objectsInGroup = activeGroup.getObjects(); + canvas.discardActiveGroup(); + objectsInGroup.forEach(function(object) { + object.sendToBack(); + }); + } + }; + $("#text-bold").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontWeight = (activeObject.fontWeight == 'bold' ? '' : 'bold'); + canvas.renderAll(); + } + }); + $("#text-italic").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontStyle = (activeObject.fontStyle == 'italic' ? '' : 'italic'); + canvas.renderAll(); + } + }); + $("#text-strike").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textDecoration = (activeObject.textDecoration == 'line-through' ? '' : 'line-through'); + canvas.renderAll(); + } + }); + $("#text-underline").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textDecoration = (activeObject.textDecoration == 'underline' ? '' : 'underline'); + canvas.renderAll(); + } + }); + $("#text-left").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'left'; + canvas.renderAll(); + } + }); + $("#text-center").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'center'; + canvas.renderAll(); + } + }); + $("#text-right").click(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.textAlign = 'right'; + canvas.renderAll(); + } + }); + $("#font-family").change(function() { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontFamily = this.value; + canvas.renderAll(); + } + }); + $('#text-bgcolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.backgroundColor = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + $('#text-fontcolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fill = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + + $('#text-strokecolor').miniColors({ + change: function(hex, rgb) { + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.strokeStyle = this.value; + canvas.renderAll(); + } + }, + open: function(hex, rgb) { + // + }, + close: function(hex, rgb) { + // + } + }); + + //canvas.add(new fabric.fabric.Object({hasBorders:true,hasControls:false,hasRotatingPoint:false,selectable:false,type:'rect'})); + $("#drawingArea").hover( + function() { + canvas.add(line1); + canvas.add(line2); + canvas.add(line3); + canvas.add(line4); + canvas.renderAll(); + }, + function() { + canvas.remove(line1); + canvas.remove(line2); + canvas.remove(line3); + canvas.remove(line4); + canvas.renderAll(); + } + ); + + $('.color-preview').click(function(){ + var color = $(this).css("background-color"); + document.getElementById("shirtDiv").style.backgroundColor = color; + }); + + $('#flip').click( + function() { + if ($(this).attr("data-original-title") == "Show Back View") { + $(this).attr('data-original-title', 'Show Front View'); + $("#tshirtFacing").attr("src","img/crew_back.png"); + a = JSON.stringify(canvas); + canvas.clear(); + try + { + var json = JSON.parse(b); + canvas.loadFromJSON(b); + } + catch(e) + {} + + } else { + $(this).attr('data-original-title', 'Show Back View'); + $("#tshirtFacing").attr("src","img/crew_front.png"); + b = JSON.stringify(canvas); + canvas.clear(); + try + { + var json = JSON.parse(a); + canvas.loadFromJSON(a); + } + catch(e) + {} + } + canvas.renderAll(); + setTimeout(function() { + canvas.calcOffset(); + },200); + }); + $(".clearfix button,a").tooltip(); + line1 = new fabric.Line([0,0,200,0], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line2 = new fabric.Line([199,0,200,399], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line3 = new fabric.Line([0,0,0,400], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + line4 = new fabric.Line([0,400,200,399], {"stroke":"#000000", "strokeWidth":1,hasBorders:false,hasControls:false,hasRotatingPoint:false,selectable:false}); + });//doc ready + + + function getRandomNum(min, max) { + return Math.random() * (max - min) + min; + } + + function onObjectSelected(e) { + var selectedObject = e.target; + $("#text-string").val(""); + selectedObject.hasRotatingPoint = true + if (selectedObject && selectedObject.type === 'text') { + //display text editor + $("#texteditor").css('display', 'block'); + $("#text-string").val(selectedObject.getText()); + $('#text-fontcolor').miniColors('value',selectedObject.fill); + $('#text-strokecolor').miniColors('value',selectedObject.strokeStyle); + $("#imageeditor").css('display', 'block'); + } + else if (selectedObject && selectedObject.type === 'image'){ + //display image editor + $("#texteditor").css('display', 'none'); + $("#imageeditor").css('display', 'block'); + } + } + function onSelectedCleared(e){ + $("#texteditor").css('display', 'none'); + $("#text-string").val(""); + $("#imageeditor").css('display', 'none'); + } + function setFont(font){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'text') { + activeObject.fontFamily = font; + canvas.renderAll(); + } + } + function removeWhite(){ + var activeObject = canvas.getActiveObject(); + if (activeObject && activeObject.type === 'image') { + activeObject.filters[2] = new fabric.Image.filters.RemoveWhite({hreshold: 100, distance: 10});//0-255, 0-255 + activeObject.applyFilters(canvas.renderAll.bind(canvas)); + } + }
\ No newline at end of file |
