aboutsummaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authorBobby <[email protected]>2013-06-13 01:41:58 -0500
committerImtiyaz S. Momin <[email protected]>2013-06-13 01:41:58 -0500
commit3a02e993ef59cfdde447e2bd95dca34142a776b1 (patch)
treef55740990a700f28f08ef0269e850036f66f66e4 /js
downloadTShirtDesigner-3a02e993ef59cfdde447e2bd95dca34142a776b1.tar.xz
TShirtDesigner-3a02e993ef59cfdde447e2bd95dca34142a776b1.zip
Committing local files
Diffstat (limited to 'js')
-rw-r--r--js/bootstrap.js2027
-rw-r--r--js/bootstrap.min.js6
-rw-r--r--js/caseEditor.js344
-rw-r--r--js/excanvas.js924
-rw-r--r--js/fabric.js13962
-rw-r--r--js/jquery.miniColors.min.js9
-rw-r--r--js/tshirtEditor.js355
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 '&nbsp;'),
+ 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, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
+
+/** @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 &lt;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 &lt;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 &lt;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 &lt;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', &hellip;)`)
+ * @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