// source --> https://jsp.co.id/wp-content/plugins/woocommerce/assets/js/frontend/add-to-cart.min.js jQuery(function(t){if("undefined"==typeof wc_add_to_cart_params)return!1;var a=function(){t(document.body).on("click",".add_to_cart_button",this.onAddToCart).on("click",".remove_from_cart_button",this.onRemoveFromCart).on("added_to_cart",this.updateButton).on("added_to_cart",this.updateCartPage).on("added_to_cart removed_from_cart",this.updateFragments)};a.prototype.onAddToCart=function(a){var o=t(this);if(o.is(".ajax_add_to_cart")){if(!o.attr("data-product_id"))return!0;a.preventDefault(),o.removeClass("added"),o.addClass("loading");var r={};t.each(o.data(),function(t,a){r[t]=a}),t(document.body).trigger("adding_to_cart",[o,r]),t.post(wc_add_to_cart_params.wc_ajax_url.toString().replace("%%endpoint%%","add_to_cart"),r,function(a){a&&(a.error&&a.product_url?window.location=a.product_url:"yes"!==wc_add_to_cart_params.cart_redirect_after_add?t(document.body).trigger("added_to_cart",[a.fragments,a.cart_hash,o]):window.location=wc_add_to_cart_params.cart_url)})}},a.prototype.onRemoveFromCart=function(a){var o=t(this),r=o.closest(".woocommerce-mini-cart-item");a.preventDefault(),r.block({message:null,overlayCSS:{opacity:.6}}),t.post(wc_add_to_cart_params.wc_ajax_url.toString().replace("%%endpoint%%","remove_from_cart"),{cart_item_key:o.data("cart_item_key")},function(a){a&&a.fragments?t(document.body).trigger("removed_from_cart",[a.fragments,a.cart_hash]):window.location=o.attr("href")}).fail(function(){window.location=o.attr("href")})},a.prototype.updateButton=function(a,o,r,e){(e=void 0!==e&&e)&&(e.removeClass("loading"),e.addClass("added"),wc_add_to_cart_params.is_cart||0!==e.parent().find(".added_to_cart").length||e.after(' '+wc_add_to_cart_params.i18n_view_cart+""),t(document.body).trigger("wc_cart_button_updated",[e]))},a.prototype.updateCartPage=function(){var a=window.location.toString().replace("add-to-cart","added-to-cart");t(".shop_table.cart").load(a+" .shop_table.cart:eq(0) > *",function(){t(".shop_table.cart").stop(!0).css("opacity","1").unblock(),t(document.body).trigger("cart_page_refreshed")}),t(".cart_totals").load(a+" .cart_totals:eq(0) > *",function(){t(".cart_totals").stop(!0).css("opacity","1").unblock(),t(document.body).trigger("cart_totals_refreshed")})},a.prototype.updateFragments=function(a,o){o&&(t.each(o,function(a){t(a).addClass("updating").fadeTo("400","0.6").block({message:null,overlayCSS:{opacity:.6}})}),t.each(o,function(a,o){t(a).replaceWith(o),t(a).stop(!0).css("opacity","1").unblock()}),t(document.body).trigger("wc_fragments_loaded"))},new a}); // source --> https://jsp.co.id/wp-includes/js/underscore.min.js // Underscore.js 1.8.3 // http://underscorejs.org // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); // source --> https://jsp.co.id/wp-content/plugins/learnpress-certificates/assets/js/fabric.js /* build: `node build.js modules=ALL exclude=gestures,cufon,json minifier=uglifyjs` */ /*! Fabric.js Copyright 2008-2014, Printio (Juriy Zaytsev, Maxim Chernyak) */ var fabric = fabric || { version: "1.4.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(""); if (fabric.document.createWindow) { fabric.window = fabric.document.createWindow(); } else { fabric.window = fabric.document.parentWindow; } } /** * True when in environment that supports touch events * @type boolean */ fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; /** * True when in environment that's probably Node.js * @type boolean */ fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; /** * Attributes parsed from all SVG elements * @type array */ fabric.SHARED_ATTRIBUTES = [ "display", "transform", "fill", "fill-opacity", "fill-rule", "opacity", "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width" ]; /** * Pixel per Inch as a default value set to 96. Can be changed for more realistic conversion. */ fabric.DPI = 96; fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)'; (function() { /** * @private * @param {String} eventName * @param {Function} handler */ function _removeEventListener(eventName, handler) { if (!this.__eventListeners[eventName]) { return; } if (handler) { fabric.util.removeFromArray(this.__eventListeners[eventName], handler); } else { this.__eventListeners[eventName].length = 0; } } /** * Observes specified event * @deprecated `observe` deprecated since 0.8.34 (use `on` instead) * @memberOf fabric.Observable * @alias on * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function that receives a notification when an event of the specified type occurs * @return {Self} thisArg * @chainable */ function observe(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); } return this; } /** * Stops event observing for a particular event handler. Calling this method * without arguments removes all handlers for all events * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) * @memberOf fabric.Observable * @alias off * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function to be deleted from EventListeners * @return {Self} thisArg * @chainable */ function stopObserving(eventName, handler) { if (!this.__eventListeners) { return; } // remove all key/value pairs (event name -> event handler) if (arguments.length === 0) { this.__eventListeners = { }; } // one object with key/value pairs was passed else if (arguments.length === 1 && typeof arguments[0] === 'object') { for (var prop in eventName) { _removeEventListener.call(this, prop, eventName[prop]); } } else { _removeEventListener.call(this, eventName, handler); } return this; } /** * Fires event with an optional options object * @deprecated `fire` deprecated since 1.0.7 (use `trigger` instead) * @memberOf fabric.Observable * @alias trigger * @param {String} eventName Event name to fire * @param {Object} [options] Options object * @return {Self} thisArg * @chainable */ function fire(eventName, options) { if (!this.__eventListeners) { return; } 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].call(this, options || { }); } return this; } /** * @namespace fabric.Observable * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#events} * @see {@link http://fabricjs.com/events/|Events demo} */ fabric.Observable = { observe: observe, stopObserving: stopObserving, fire: fire, on: observe, off: stopObserving, trigger: fire }; })(); /** * @namespace fabric.Collection */ fabric.Collection = { /** * Adds objects to collection, then renders canvas (if `renderOnAddRemove` is not `false`) * Objects should be instances of (or inherit from) fabric.Object * @param {...fabric.Object} object Zero or more fabric instances * @return {Self} thisArg */ add: function () { this._objects.push.apply(this._objects, arguments); for (var i = 0, length = arguments.length; i < length; i++) { this._onObjectAdded(arguments[i]); } this.renderOnAddRemove && this.renderAll(); return this; }, /** * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) * An object should be an instance of (or inherit from) fabric.Object * @param {Object} object Object to insert * @param {Number} index Index to insert object at * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs * @return {Self} thisArg * @chainable */ insertAt: function (object, index, nonSplicing) { var objects = this.getObjects(); if (nonSplicing) { objects[index] = object; } else { objects.splice(index, 0, object); } this._onObjectAdded(object); this.renderOnAddRemove && this.renderAll(); return this; }, /** * Removes objects from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) * @param {...fabric.Object} object Zero or more fabric instances * @return {Self} thisArg * @chainable */ remove: function() { var objects = this.getObjects(), index; for (var i = 0, length = arguments.length; i < length; i++) { index = objects.indexOf(arguments[i]); // only call onObjectRemoved if an object was actually removed if (index !== -1) { objects.splice(index, 1); this._onObjectRemoved(arguments[i]); } } this.renderOnAddRemove && this.renderAll(); return this; }, /** * Executes given function for each object in this group * @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 {Self} thisArg */ forEachObject: function(callback, context) { var objects = this.getObjects(), i = objects.length; while (i--) { callback.call(context, objects[i], i, objects); } return this; }, /** * Returns an array of children objects of this instance * Type parameter introduced in 1.3.10 * @param {String} [type] When specified, only objects of this type are returned * @return {Array} */ getObjects: function(type) { if (typeof type === 'undefined') { return this._objects; } return this._objects.filter(function(o) { return o.type === type; }); }, /** * Returns object at specified index * @param {Number} index * @return {Self} thisArg */ item: function (index) { return this.getObjects()[index]; }, /** * Returns true if collection contains no objects * @return {Boolean} true if collection is empty */ isEmpty: function () { return this.getObjects().length === 0; }, /** * Returns a size of a collection (i.e: length of an array containing its objects) * @return {Number} Collection size */ size: function() { return this.getObjects().length; }, /** * Returns true if collection contains an object * @param {Object} object Object to check against * @return {Boolean} `true` if collection contains an object */ contains: function(object) { return this.getObjects().indexOf(object) > -1; }, /** * Returns number representation of a collection complexity * @return {Number} complexity */ complexity: function () { return this.getObjects().reduce(function (memo, current) { memo += current.complexity ? current.complexity() : 0; return memo; }, 0); } }; (function(global) { var sqrt = Math.sqrt, atan2 = Math.atan2, PiBy180 = Math.PI / 180; /** * @namespace fabric.util */ 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 * @param {Array} array * @param {Any} value * @return {Array} original array */ removeFromArray: function(array, value) { var idx = array.indexOf(value); if (idx !== -1) { array.splice(idx, 1); } return array; }, /** * Returns random number between 2 specified ones. * @static * @memberOf fabric.util * @param {Number} min lower limit * @param {Number} max upper limit * @return {Number} random value (between min and max) */ getRandomInt: function(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }, /** * Transforms degrees to radians. * @static * @memberOf fabric.util * @param {Number} degrees value in degrees * @return {Number} value in radians */ degreesToRadians: function(degrees) { return degrees * PiBy180; }, /** * Transforms radians to degrees. * @static * @memberOf fabric.util * @param {Number} radians value in radians * @return {Number} value in degrees */ radiansToDegrees: function(radians) { return radians / PiBy180; }, /** * Rotates `point` around `origin` with `radians` * @static * @memberOf fabric.util * @param {fabric.Point} point The point to rotate * @param {fabric.Point} origin The origin of the rotation * @param {Number} radians The radians of the angle for the rotation * @return {fabric.Point} The new rotated point */ rotatePoint: function(point, origin, radians) { var sin = Math.sin(radians), cos = Math.cos(radians); point.subtractEquals(origin); var rx = point.x * cos - point.y * sin, ry = point.x * sin + point.y * cos; return new fabric.Point(rx, ry).addEquals(origin); }, /** * Apply transform t to point p * @static * @memberOf fabric.util * @param {fabric.Point} p The point to transform * @param {Array} t The transform * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied * @return {fabric.Point} The transformed point */ transformPoint: function(p, t, ignoreOffset) { if (ignoreOffset) { return new fabric.Point( t[0] * p.x + t[1] * p.y, t[2] * p.x + t[3] * p.y ); } return new fabric.Point( t[0] * p.x + t[1] * p.y + t[4], t[2] * p.x + t[3] * p.y + t[5] ); }, /** * Invert transformation t * @static * @memberOf fabric.util * @param {Array} t The transform * @return {Array} The inverted transform */ invertTransform: function(t) { var r = t.slice(), a = 1 / (t[0] * t[3] - t[1] * t[2]); r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; var o = fabric.util.transformPoint({ x: t[4], y: t[5] }, r); r[4] = -o.x; r[5] = -o.y; return r; }, /** * A wrapper around Number#toFixed, which contrary to native method returns number, not string. * @static * @memberOf fabric.util * @param {Number|String} number number to operate on * @param {Number} fractionDigits number of fraction digits to "leave" * @return {Number} */ toFixed: function(number, fractionDigits) { return parseFloat(Number(number).toFixed(fractionDigits)); }, /** * Converts from attribute value to pixel value if applicable. * Returns converted pixels or original value not converted. * @param {Number|String} value number to operate on * @return {Number|String} */ parseUnit: function(value, fontSize) { var unit = /\D{0,2}$/.exec(value), number = parseFloat(value); if (!fontSize) { fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; } switch (unit[0]) { case 'mm': return number * fabric.DPI / 25.4; case 'cm': return number * fabric.DPI / 2.54; case 'in': return number * fabric.DPI; case 'pt': return number * fabric.DPI / 72; // or * 4 / 3 case 'pc': return number * fabric.DPI / 72 * 12; // or * 16 case 'em': return number * fontSize; default: return number; } }, /** * Function which always returns `false`. * @static * @memberOf fabric.util * @return {Boolean} */ falseFunction: function() { return false; }, /** * Returns klass "Class" object of given namespace * @memberOf fabric.util * @param {String} type Type of object (eg. 'circle') * @param {String} namespace Namespace to get klass "Class" object from * @return {Object} klass "Class" */ getKlass: function(type, namespace) { // capitalize first letter only type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); return fabric.util.resolveNamespace(namespace)[type]; }, /** * Returns object of given namespace * @memberOf fabric.util * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' * @return {Object} Object for given namespace (default fabric) */ resolveNamespace: function(namespace) { if (!namespace) { return fabric; } var parts = namespace.split('.'), len = parts.length, obj = global || fabric.window; for (var i = 0; i < len; ++i) { obj = obj[parts[i]]; } return obj; }, /** * Loads image element from given url and passes it to a callback * @memberOf fabric.util * @param {String} url URL representing an image * @param {Function} callback Callback; invoked with loaded image * @param {Any} [context] Context to invoke callback in * @param {Object} [crossOrigin] crossOrigin value to set image element to */ loadImage: function(url, callback, context, crossOrigin) { if (!url) { callback && callback.call(context, url); return; } var img = fabric.util.createImage(); /** @ignore */ img.onload = function () { callback && callback.call(context, img); img = img.onload = img.onerror = null; }; /** @ignore */ img.onerror = function() { fabric.log('Error loading ' + img.src); callback && callback.call(context, null, true); img = img.onload = img.onerror = null; }; // data-urls appear to be buggy with crossOrigin // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767 // see https://code.google.com/p/chromium/issues/detail?id=315152 // https://bugzilla.mozilla.org/show_bug.cgi?id=935069 if (url.indexOf('data') !== 0 && typeof crossOrigin !== 'undefined') { img.crossOrigin = crossOrigin; } img.src = url; }, /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util * @param {Array} objects Objects to enliven * @param {Function} callback Callback to invoke when all objects are created * @param {String} namespace Namespace to get klass "Class" object from * @param {Function} reviver Method for further parsing of object elements, * called after each fabric object created. */ enlivenObjects: function(objects, callback, namespace, reviver) { objects = objects || [ ]; function onLoaded() { if (++numLoadedObjects === numTotalObjects) { callback && callback(enlivenedObjects); } } var enlivenedObjects = [ ], numLoadedObjects = 0, numTotalObjects = objects.length; if (!numTotalObjects) { callback && callback(enlivenedObjects); return; } objects.forEach(function (o, index) { // if sparse array if (!o || !o.type) { onLoaded(); return; } var klass = fabric.util.getKlass(o.type, namespace); if (klass.async) { klass.fromObject(o, function (obj, error) { if (!error) { enlivenedObjects[index] = obj; reviver && reviver(o, enlivenedObjects[index]); } onLoaded(); }); } else { enlivenedObjects[index] = klass.fromObject(o); reviver && reviver(o, enlivenedObjects[index]); onLoaded(); } }); }, /** * Groups SVG elements (usually those retrieved from SVG document) * @static * @memberOf fabric.util * @param {Array} elements SVG elements to group * @param {Object} [options] Options object * @return {fabric.Object|fabric.PathGroup} */ groupSVGElements: function(elements, options, path) { var object; object = new fabric.PathGroup(elements, options); if (typeof path !== 'undefined') { object.setSourcePath(path); } return object; }, /** * Populates an object with properties of another object * @static * @memberOf fabric.util * @param {Object} source Source object * @param {Object} destination Destination object * @return {Array} properties Propertie names to include */ populateWithProperties: function(source, destination, properties) { if (properties && Object.prototype.toString.call(properties) === '[object Array]') { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; } } } }, /** * Draws a dashed line between two points * * This method is used to draw dashed line around selection area. * See dotted stroke in canvas * * @param {CanvasRenderingContext2D} ctx context * @param {Number} x start x coordinate * @param {Number} y start y coordinate * @param {Number} x2 end x coordinate * @param {Number} y2 end y coordinate * @param {Array} da dash array pattern */ drawDashedLine: function(ctx, x, y, x2, y2, da) { var dx = x2 - x, dy = y2 - y, len = sqrt(dx * dx + dy * dy), rot = atan2(dy, dx), dc = da.length, di = 0, draw = true; ctx.save(); ctx.translate(x, y); ctx.moveTo(0, 0); ctx.rotate(rot); x = 0; while (len > x) { x += da[di++ % dc]; if (x > len) { x = len; } ctx[draw ? 'lineTo' : 'moveTo'](x, 0); draw = !draw; } ctx.restore(); }, /** * Creates canvas element and initializes it via excanvas if necessary * @static * @memberOf fabric.util * @param {CanvasElement} [canvasEl] optional canvas element to initialize; * when not given, element is created implicitly * @return {CanvasElement} initialized canvas element */ createCanvasElement: function(canvasEl) { canvasEl || (canvasEl = fabric.document.createElement('canvas')); //jscs:disable requireCamelCaseOrUpperCaseIdentifiers if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { G_vmlCanvasManager.initElement(canvasEl); } //jscs:enable requireCamelCaseOrUpperCaseIdentifiers return canvasEl; }, /** * Creates image element (works on client and node) * @static * @memberOf fabric.util * @return {HTMLImageElement} HTML image element */ createImage: function() { return fabric.isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'); }, /** * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array * @static * @memberOf fabric.util * @param {Object} klass "Class" to create accessors for */ createAccessors: function(klass) { var proto = klass.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); } } }, /** * @static * @memberOf fabric.util * @param {fabric.Object} receiver Object implementing `clipTo` method * @param {CanvasRenderingContext2D} ctx Context to clip */ clipContext: function(receiver, ctx) { ctx.save(); ctx.beginPath(); receiver.clipTo(ctx); ctx.clip(); }, /** * Multiply matrix A by matrix B to nest transformations * @static * @memberOf fabric.util * @param {Array} matrixA First transformMatrix * @param {Array} matrixB Second transformMatrix * @return {Array} The product of the two transform matrices */ multiplyTransformMatrices: function(matrixA, matrixB) { // Matrix multiply matrixA * matrixB var a = [ [matrixA[0], matrixA[2], matrixA[4]], [matrixA[1], matrixA[3], matrixA[5]], [0, 0, 1 ] ], b = [ [matrixB[0], matrixB[2], matrixB[4]], [matrixB[1], matrixB[3], matrixB[5]], [0, 0, 1 ] ], result = []; for (var r = 0; r < 3; r++) { result[r] = []; for (var c = 0; c < 3; c++) { var sum = 0; for (var k = 0; k < 3; k++) { sum += a[r][k] * b[k][c]; } result[r][c] = sum; } } return [ result[0][0], result[1][0], result[0][1], result[1][1], result[0][2], result[1][2] ]; }, /** * Returns string representation of function body * @param {Function} fn Function to get body of * @return {String} Function body */ getFunctionBody: function(fn) { return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; }, /** * Returns true if context has transparent pixel * at specified location (taking tolerance into account) * @param {CanvasRenderingContext2D} ctx context * @param {Number} x x coordinate * @param {Number} y y coordinate * @param {Number} tolerance Tolerance */ isTransparent: function(ctx, x, y, tolerance) { // If tolerance is > 0 adjust start coords to take into account. // If moves off Canvas fix to 0 if (tolerance > 0) { if (x > tolerance) { x -= tolerance; } else { x = 0; } if (y > tolerance) { y -= tolerance; } else { y = 0; } } var _isTransparent = true, imageData = ctx.getImageData(x, y, (tolerance * 2) || 1, (tolerance * 2) || 1); // Split image data - for tolerance > 1, pixelDataSize = 4; for (var i = 3, l = imageData.data.length; i < l; i += 4) { var temp = imageData.data[i]; _isTransparent = temp <= 0; if (_isTransparent === false) { break; // Stop if colour found } } imageData = null; return _isTransparent; } }; })(typeof exports !== 'undefined' ? exports : this); (function() { var arcToSegmentsCache = { }, segmentToBezierCache = { }, boundsOfCurveCache = { }, _join = Array.prototype.join; /* Adapted from http://dxr.mozilla.org/mozilla-central/source/content/svg/content/src/nsSVGPathDataParser.cpp * by Andrea Bogazzi code is under MPL. if you don't have a copy of the license you can take it here * http://mozilla.org/MPL/2.0/ */ function arcToSegments(toX, toY, rx, ry, large, sweep, rotateX) { var argsString = _join.call(arguments); if (arcToSegmentsCache[argsString]) { return arcToSegmentsCache[argsString]; } var PI = Math.PI, th = rotateX * PI / 180, sinTh = Math.sin(th), cosTh = Math.cos(th), fromX = 0, fromY = 0; rx = Math.abs(rx); ry = Math.abs(ry); var px = -cosTh * toX * 0.5 - sinTh * toY * 0.5, py = -cosTh * toY * 0.5 + sinTh * toX * 0.5, rx2 = rx * rx, ry2 = ry * ry, py2 = py * py, px2 = px * px, pl = rx2 * ry2 - rx2 * py2 - ry2 * px2, root = 0; if (pl < 0) { var s = Math.sqrt(1 - pl/(rx2 * ry2)); rx *= s; ry *= s; } else { root = (large === sweep ? -1.0 : 1.0) * Math.sqrt( pl /(rx2 * py2 + ry2 * px2)); } var cx = root * rx * py / ry, cy = -root * ry * px / rx, cx1 = cosTh * cx - sinTh * cy + toX * 0.5, cy1 = sinTh * cx + cosTh * cy + toY * 0.5, mTheta = calcVectorAngle(1, 0, (px - cx) / rx, (py - cy) / ry), dtheta = calcVectorAngle((px - cx) / rx, (py - cy) / ry, (-px - cx) / rx, (-py - cy) / ry); if (sweep === 0 && dtheta > 0) { dtheta -= 2 * PI; } else if (sweep === 1 && dtheta < 0) { dtheta += 2 * PI; } // Convert into cubic bezier segments <= 90deg var segments = Math.ceil(Math.abs(dtheta / PI * 2)), result = [], mDelta = dtheta / segments, mT = 8 / 3 * Math.sin(mDelta / 4) * Math.sin(mDelta / 4) / Math.sin(mDelta / 2), th3 = mTheta + mDelta; for (var i = 0; i < segments; i++) { result[i] = segmentToBezier(mTheta, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY); fromX = result[i][4]; fromY = result[i][5]; mTheta = th3; th3 += mDelta; } arcToSegmentsCache[argsString] = result; return result; } function segmentToBezier(th2, th3, cosTh, sinTh, rx, ry, cx1, cy1, mT, fromX, fromY) { var argsString2 = _join.call(arguments); if (segmentToBezierCache[argsString2]) { return segmentToBezierCache[argsString2]; } var costh2 = Math.cos(th2), sinth2 = Math.sin(th2), costh3 = Math.cos(th3), sinth3 = Math.sin(th3), toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1, toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1, cp1X = fromX + mT * ( - cosTh * rx * sinth2 - sinTh * ry * costh2), cp1Y = fromY + mT * ( - sinTh * rx * sinth2 + cosTh * ry * costh2), cp2X = toX + mT * ( cosTh * rx * sinth3 + sinTh * ry * costh3), cp2Y = toY + mT * ( sinTh * rx * sinth3 - cosTh * ry * costh3); segmentToBezierCache[argsString2] = [ cp1X, cp1Y, cp2X, cp2Y, toX, toY ]; return segmentToBezierCache[argsString2]; } /* * Private */ function calcVectorAngle(ux, uy, vx, vy) { var ta = Math.atan2(uy, ux), tb = Math.atan2(vy, vx); if (tb >= ta) { return tb - ta; } else { return 2 * Math.PI - (ta - tb); } } /** * Draws arc * @param {CanvasRenderingContext2D} ctx * @param {Number} fx * @param {Number} fy * @param {Array} coords */ fabric.util.drawArc = function(ctx, fx, fy, coords) { var rx = coords[0], ry = coords[1], rot = coords[2], large = coords[3], sweep = coords[4], tx = coords[5], ty = coords[6], segs = [[ ], [ ], [ ], [ ]], segsNorm = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); for (var i = 0, len = segsNorm.length; i < len; i++) { segs[i][0] = segsNorm[i][0] + fx; segs[i][1] = segsNorm[i][1] + fy; segs[i][2] = segsNorm[i][2] + fx; segs[i][3] = segsNorm[i][3] + fy; segs[i][4] = segsNorm[i][4] + fx; segs[i][5] = segsNorm[i][5] + fy; ctx.bezierCurveTo.apply(ctx, segs[i]); } }; /** * Calculate bounding box of a elliptic-arc * @param {Number} fx start point of arc * @param {Number} fy * @param {Number} rx horizontal radius * @param {Number} ry vertical radius * @param {Number} rot angle of horizontal axe * @param {Number} large 1 or 0, whatever the arc is the big or the small on the 2 points * @param {Number} sweep 1 or 0, 1 clockwise or counterclockwise direction * @param {Number} tx end point of arc * @param {Number} ty */ fabric.util.getBoundsOfArc = function(fx, fy, rx, ry, rot, large, sweep, tx, ty) { var fromX = 0, fromY = 0, bound = [ ], bounds = [ ], segs = arcToSegments(tx - fx, ty - fy, rx, ry, large, sweep, rot); for (var i = 0, len = segs.length; i < len; i++) { bound = getBoundsOfCurve(fromX, fromY, segs[i][0], segs[i][1], segs[i][2], segs[i][3], segs[i][4], segs[i][5]); bound[0].x += fx; bound[0].y += fy; bound[1].x += fx; bound[1].y += fy; bounds.push(bound[0]); bounds.push(bound[1]); fromX = segs[i][4]; fromY = segs[i][5]; } return bounds; }; /** * Calculate bounding box of a beziercurve * @param {Number} x0 starting point * @param {Number} y0 * @param {Number} x1 first control point * @param {Number} y1 * @param {Number} x2 secondo control point * @param {Number} y2 * @param {Number} x3 end of beizer * @param {Number} y3 */ // taken from http://jsbin.com/ivomiq/56/edit no credits available for that. function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3) { var argsString = _join.call(arguments); if (boundsOfCurveCache[argsString]) { return boundsOfCurveCache[argsString]; } var sqrt = Math.sqrt, min = Math.min, max = Math.max, abs = Math.abs, tvalues = [ ], bounds = [[ ], [ ]], a, b, c, t, t1, t2, b2ac, sqrtb2ac; b = 6 * x0 - 12 * x1 + 6 * x2; a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3; c = 3 * x1 - 3 * x0; for (var i = 0; i < 2; ++i) { if (i > 0) { b = 6 * y0 - 12 * y1 + 6 * y2; a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3; c = 3 * y1 - 3 * y0; } if (abs(a) < 1e-12) { if (abs(b) < 1e-12) { continue; } t = -c / b; if (0 < t && t < 1) { tvalues.push(t); } continue; } b2ac = b * b - 4 * c * a; if (b2ac < 0) { continue; } sqrtb2ac = sqrt(b2ac); t1 = (-b + sqrtb2ac) / (2 * a); if (0 < t1 && t1 < 1) { tvalues.push(t1); } t2 = (-b - sqrtb2ac) / (2 * a); if (0 < t2 && t2 < 1) { tvalues.push(t2); } } var x, y, j = tvalues.length, jlen = j, mt; while (j--) { t = tvalues[j]; mt = 1 - t; x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3); bounds[0][j] = x; y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3); bounds[1][j] = y; } bounds[0][jlen] = x0; bounds[1][jlen] = y0; bounds[0][jlen + 1] = x3; bounds[1][jlen + 1] = y3; var result = [ { x: min.apply(null, bounds[0]), y: min.apply(null, bounds[1]) }, { x: max.apply(null, bounds[0]), y: max.apply(null, bounds[1]) } ]; boundsOfCurveCache[argsString] = result; return result; } fabric.util.getBoundsOfCurve = getBoundsOfCurve; })(); (function() { var slice = Array.prototype.slice; /* _ES5_COMPAT_START_ */ if (!Array.prototype.indexOf) { /** * Finds index of an element in an array * @param {Any} searchElement * @param {Number} [fromIndex] * @return {Number} */ 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 !== Number.POSITIVE_INFINITY && n !== Number.NEGATIVE_INFINITY) { 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) { /** * Iterates an array, invoking callback for each element * @param {Function} fn Callback to invoke for each element * @param {Object} [context] Context to invoke callback in * @return {Array} */ 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) { /** * Returns a result of iterating over an array, invoking callback for each element * @param {Function} fn Callback to invoke for each element * @param {Object} [context] Context to invoke callback in * @return {Array} */ 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) { /** * Returns true if a callback returns truthy value for all elements in an array * @param {Function} fn Callback to invoke for each element * @param {Object} [context] Context to invoke callback in * @return {Boolean} */ 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) { /** * Returns true if a callback returns truthy value for at least one element in an array * @param {Function} fn Callback to invoke for each element * @param {Object} [context] Context to invoke callback in * @return {Boolean} */ 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) { /** * Returns the result of iterating over elements in an array * @param {Function} fn Callback to invoke for each element * @param {Object} [context] Context to invoke callback in * @return {Array} */ 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) { /** * Returns "folded" (reduced) result of iterating over elements in an array * @param {Function} fn Callback to invoke for each element * @param {Object} [initial] Object to use as the first argument to the first call of the callback * @return {Any} */ 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; }; } /* _ES5_COMPAT_END_ */ /** * Invokes method on all items in a given array * @memberOf fabric.util.array * @param {Array} array Array to iterate over * @param {String} method Name of a method to invoke * @return {Array} */ 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) * @memberOf fabric.util.array * @param {Array} array Array to iterate over * @param {String} byProperty * @return {Any} */ function max(array, byProperty) { return find(array, byProperty, function(value1, value2) { return value1 >= value2; }); } /** * Finds minimum value in array (not necessarily "first" one) * @memberOf fabric.util.array * @param {Array} array Array to iterate over * @param {String} byProperty * @return {Any} */ function min(array, byProperty) { return find(array, byProperty, function(value1, value2) { return value1 < value2; }); } /** * @private */ function find(array, byProperty, condition) { if (!array || array.length === 0) { return; } var i = array.length - 1, result = byProperty ? array[i][byProperty] : array[i]; if (byProperty) { while (i--) { if (condition(array[i][byProperty], result)) { result = array[i][byProperty]; } } } else { while (i--) { if (condition(array[i], result)) { result = array[i]; } } } return result; } /** * @namespace fabric.util.array */ fabric.util.array = { invoke: invoke, min: min, max: max }; })(); (function() { /** * Copies all enumerable properties of one object to another * @memberOf fabric.util.object * @param {Object} destination Where to copy to * @param {Object} source Where to copy from * @return {Object} */ 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 * @memberOf fabric.util.object * @param {Object} object Object to clone * @return {Object} */ function clone(object) { return extend({ }, object); } /** @namespace fabric.util.object */ fabric.util.object = { extend: extend, clone: clone }; })(); (function() { /* _ES5_COMPAT_START_ */ if (!String.prototype.trim) { /** * Trims a string (removing whitespace from the beginning and the end) * @function external:String#trim * @see String#trim on MDN */ String.prototype.trim = function () { // this trim is not fully ES3 or ES5 compliant, but it should cover most cases for now return this.replace(/^[\s\xA0]+/, '').replace(/[\s\xA0]+$/, ''); }; } /* _ES5_COMPAT_END_ */ /** * Camelizes a string * @memberOf fabric.util.string * @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 * @param {String} string String to capitalize * @param {Boolean} [firstLetterOnly] If true only first letter is capitalized * and other letters stay untouched, if false first letter is capitalized * and other letters are converted to lowercase. * @return {String} Capitalized version of a string */ function capitalize(string, firstLetterOnly) { return string.charAt(0).toUpperCase() + (firstLetterOnly ? string.slice(1) : string.slice(1).toLowerCase()); } /** * Escapes XML in a string * @memberOf fabric.util.string * @param {String} string String to escape * @return {String} Escaped version of a string */ function escapeXml(string) { return string.replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } /** * String utilities * @namespace fabric.util.string */ fabric.util.string = { camelize: camelize, capitalize: capitalize, escapeXml: escapeXml }; }()); /* _ES5_COMPAT_START_ */ (function() { var slice = Array.prototype.slice, apply = Function.prototype.apply, Dummy = function() { }; if (!Function.prototype.bind) { /** * Cross-browser approximation of ES5 Function.prototype.bind (not fully spec conforming) * @see Function#bind on MDN * @param {Object} thisArg Object to bind function to * @param {Any[]} [...] Values to pass to a bound function * @return {Function} */ Function.prototype.bind = function(thisArg) { var _this = this, args = slice.call(arguments, 1), bound; if (args.length) { bound = function() { return apply.call(_this, this instanceof Dummy ? this : thisArg, args.concat(slice.call(arguments))); }; } else { /** @ignore */ bound = function() { return apply.call(_this, this instanceof Dummy ? this : thisArg, arguments); }; } Dummy.prototype = this.prototype; bound.prototype = new Dummy(); return bound; }; } })(); /* _ES5_COMPAT_END_ */ (function() { var slice = Array.prototype.slice, emptyFunction = function() { }, IS_DONTENUM_BUGGY = (function() { for (var p in { toString: 1 }) { if (p === 'toString') { return false; } } return true; })(), /** @ignore */ addMethods = function(klass, source, parent) { for (var property in source) { if (property in klass.prototype && typeof klass.prototype[property] === 'function' && (source[property] + '').indexOf('callSuper') > -1) { 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() { } function callSuper(methodName) { var fn = this.constructor.superclass.prototype[methodName]; return (arguments.length > 1) ? fn.apply(this, slice.call(arguments, 1)) : fn.call(this); } /** * Helper for creation of "classes". * @memberOf fabric.util * @param {Function} [parent] optional "Class" to inherit from * @param {Object} [properties] Properties shared by all instances of this class * (be careful modifying objects defined here as this would affect all instances) */ 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; klass.prototype.callSuper = callSuper; return klass; } fabric.util.createClass = createClass; })(); (function () { var unknown = 'unknown'; /* 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; } /** @ignore */ var getElement, setElement, getUniqueId = (function () { var uid = 0; return function (element) { return element.__uniqueID || (element.__uniqueID = 'uniqueID__' + uid++); }; })(); (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 * @function * @memberOf fabric.util * @param {HTMLElement} element * @param {String} eventName * @param {Function} handler */ fabric.util.addListener = addListener; /** * Removes an event listener from an element * @function * @memberOf fabric.util * @param {HTMLElement} element * @param {String} eventName * @param {Function} handler */ fabric.util.removeListener = removeListener; /** * Cross-browser wrapper for getting event's coordinates * @memberOf fabric.util * @param {Event} event Event object * @param {HTMLCanvasElement} upperCanvasEl <canvas> element on which object selection is drawn */ function getPointer(event, upperCanvasEl) { event || (event = fabric.window.event); var element = event.target || (typeof event.srcElement !== unknown ? event.srcElement : null), scroll = fabric.util.getScrollLeftTop(element, upperCanvasEl); return { x: pointerX(event) + scroll.left, y: pointerY(event) + scroll.top }; } var pointerX = function(event) { // 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 (typeof event.clientX !== unknown ? event.clientX : 0); }, pointerY = function(event) { return (typeof event.clientY !== unknown ? event.clientY : 0); }; function _getPointer(event, pageProp, clientProp) { var touchProp = event.type === 'touchend' ? 'changedTouches' : 'touches'; return (event[touchProp] && event[touchProp][0] ? (event[touchProp][0][pageProp] - (event[touchProp][0][pageProp] - event[touchProp][0][clientProp])) || event[clientProp] : event[clientProp]); } if (fabric.isTouchSupported) { pointerX = function(event) { return _getPointer(event, 'pageX', 'clientX'); }; pointerY = function(event) { return _getPointer(event, 'pageY', 'clientY'); }; } fabric.util.getPointer = getPointer; fabric.util.object.extend(fabric.util, fabric.Observable); })(); (function () { /** * Cross-browser wrapper for setting element's style * @memberOf fabric.util * @param {HTMLElement} element * @param {Object} styles * @return {HTMLElement} Element that was passed as a first argument */ function setStyle(element, styles) { var elementStyle = element.style; if (!elementStyle) { return element; } if (typeof styles === 'string') { element.style.cssText += ';' + styles; return styles.indexOf('opacity') > -1 ? setOpacity(element, styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; } for (var property in styles) { if (property === 'opacity') { setOpacity(element, styles[property]); } else { var normalizedProperty = (property === 'float' || property === 'cssFloat') ? (typeof elementStyle.styleFloat === 'undefined' ? 'cssFloat' : 'styleFloat') : property; elementStyle[normalizedProperty] = styles[property]; } } return element; } var parseEl = fabric.document.createElement('div'), supportsOpacity = typeof parseEl.style.opacity === 'string', supportsFilters = typeof parseEl.style.filter === 'string', reOpacity = /alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/, /** @ignore */ setOpacity = function (element) { return element; }; if (supportsOpacity) { /** @ignore */ setOpacity = function(element, value) { element.style.opacity = value; return element; }; } else if (supportsFilters) { /** @ignore */ setOpacity = function(element, value) { var es = element.style; if (element.currentStyle && !element.currentStyle.hasLayout) { es.zoom = 1; } if (reOpacity.test(es.filter)) { value = value >= 0.9999 ? '' : ('alpha(opacity=' + (value * 100) + ')'); es.filter = es.filter.replace(reOpacity, value); } else { es.filter += ' alpha(opacity=' + (value * 100) + ')'; } return element; }; } fabric.util.setStyle = setStyle; })(); (function() { var _slice = Array.prototype.slice; /** * Takes id and returns an element with that id (if one exists in a document) * @memberOf fabric.util * @param {String|HTMLElement} id * @return {HTMLElement|null} */ function getById(id) { return typeof id === 'string' ? fabric.document.getElementById(id) : id; } var sliceCanConvertNodelists, /** * Converts an array-like object (e.g. arguments or NodeList) to an array * @memberOf fabric.util * @param {Object} arrayLike * @return {Array} */ toArray = function(arrayLike) { return _slice.call(arrayLike, 0); }; try { sliceCanConvertNodelists = toArray(fabric.document.childNodes) instanceof Array; } catch (err) { } if (!sliceCanConvertNodelists) { toArray = function(arrayLike) { var arr = new Array(arrayLike.length), i = arrayLike.length; while (i--) { arr[i] = arrayLike[i]; } return arr; }; } /** * Creates specified element with specified attributes * @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 * @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 && (' ' + element.className + ' ').indexOf(' ' + className + ' ') === -1) { element.className += (element.className ? ' ' : '') + className; } } /** * Wraps element with another element * @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 element scroll offsets * @memberOf fabric.util * @param {HTMLElement} element Element to operate on * @param {HTMLElement} upperCanvasEl Upper canvas element * @return {Object} Object with left/top values */ function getScrollLeftTop(element, upperCanvasEl) { var firstFixedAncestor, origElement, left = 0, top = 0, docElement = fabric.document.documentElement, body = fabric.document.body || { scrollLeft: 0, scrollTop: 0 }; origElement = element; while (element && element.parentNode && !firstFixedAncestor) { element = element.parentNode; if (element.nodeType === 1 && fabric.util.getElementStyle(element, 'position') === 'fixed') { firstFixedAncestor = element; } if (element.nodeType === 1 && origElement !== upperCanvasEl && fabric.util.getElementStyle(element, 'position') === 'absolute') { left = 0; top = 0; } else if (element === fabric.document) { left = body.scrollLeft || docElement.scrollLeft || 0; top = body.scrollTop || docElement.scrollTop || 0; } else { left += element.scrollLeft || 0; top += element.scrollTop || 0; } } return { left: left, top: top }; } /** * Returns offset for a given element * @function * @memberOf fabric.util * @param {HTMLElement} element Element to get offset for * @return {Object} Object with "left" and "top" properties */ function getElementOffset(element) { var docElem, doc = element && element.ownerDocument, box = { left: 0, top: 0 }, offset = { left: 0, top: 0 }, scrollLeftTop, offsetAttributes = { borderLeftWidth: 'left', borderTopWidth: 'top', paddingLeft: 'left', paddingTop: 'top' }; if (!doc) { return { left: 0, top: 0 }; } for (var attr in offsetAttributes) { offset[offsetAttributes[attr]] += parseInt(getElementStyle(element, attr), 10) || 0; } docElem = doc.documentElement; if ( typeof element.getBoundingClientRect !== 'undefined' ) { box = element.getBoundingClientRect(); } scrollLeftTop = fabric.util.getScrollLeftTop(element, null); return { left: box.left + scrollLeftTop.left - (docElem.clientLeft || 0) + offset.left, top: box.top + scrollLeftTop.top - (docElem.clientTop || 0) + offset.top }; } /** * Returns style attribute value of a given element * @memberOf fabric.util * @param {HTMLElement} element Element to get style attribute for * @param {String} attr Style attribute to get for element * @return {String} Style attribute value of the given element. */ var getElementStyle; if (fabric.document.defaultView && fabric.document.defaultView.getComputedStyle) { getElementStyle = function(element, attr) { var style = fabric.document.defaultView.getComputedStyle(element, null); return style ? style[attr] : undefined; }; } else { getElementStyle = function(element, attr) { var value = element.style[attr]; if (!value && element.currentStyle) { value = element.currentStyle[attr]; } return value; }; } (function () { var style = fabric.document.documentElement.style, selectProp = 'userSelect' in style ? 'userSelect' : 'MozUserSelect' in style ? 'MozUserSelect' : 'WebkitUserSelect' in style ? 'WebkitUserSelect' : 'KhtmlUserSelect' in style ? 'KhtmlUserSelect' : ''; /** * Makes element unselectable * @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 * @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 * @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; /** @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.getScrollLeftTop = getScrollLeftTop; fabric.util.getElementOffset = getElementOffset; fabric.util.getElementStyle = getElementStyle; })(); (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 * @memberOf fabric.util * @param {String} url URL to send XMLHttpRequest to * @param {Object} [options] Options object * @param {String} [options.method="GET"] * @param {Function} options.onComplete Callback to invoke when request is completed * @return {XMLHttpRequest} request */ function request(url, options) { options || (options = { }); var method = options.method ? options.method.toUpperCase() : 'GET', onComplete = options.onComplete || function() { }, xhr = makeXHR(), body; /** @ignore */ xhr.onreadystatechange = function() { if (xhr.readyState === 4) { onComplete(xhr); xhr.onreadystatechange = emptyFn; } }; if (method === 'GET') { body = null; if (typeof options.parameters === 'string') { url = addParamToUrl(url, options.parameters); } } xhr.open(method, url, true); if (method === 'POST' || method === 'PUT') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } xhr.send(body); return xhr; } fabric.util.request = request; })(); /** * Wrapper around `console.log` (when available) * @param {Any} [values] Values to log */ fabric.log = function() { }; /** * Wrapper around `console.warn` (when available) * @param {Any} [values] Values to log as a warning */ fabric.warn = function() { }; if (typeof console !== 'undefined') { ['log', 'warn'].forEach(function(methodName) { if (typeof console[methodName] !== 'undefined' && console[methodName].apply) { fabric[methodName] = function() { return console[methodName].apply(console, arguments); }; } }); } (function() { /** * Changes value from one to another within certain period of time, invoking callbacks as value is being changed. * @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 (in ms) */ function animate(options) { requestAnimFrame(function(timestamp) { options || (options = { }); var start = timestamp || +new Date(), duration = options.duration || 500, finish = start + duration, time, onChange = options.onChange || function() { }, abort = options.abort || function() { return false; }, easing = options.easing || function(t, b, c, d) {return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;}, startValue = 'startValue' in options ? options.startValue : 0, endValue = 'endValue' in options ? options.endValue : 100, byValue = options.byValue || endValue - startValue; options.onStart && options.onStart(); (function tick(ticktime) { time = ticktime || +new Date(); var currentTime = time > finish ? duration : (time - start); if (abort()) { options.onComplete && options.onComplete(); return; } onChange(easing(currentTime, startValue, byValue, duration)); if (time > finish) { options.onComplete && options.onComplete(); return; } requestAnimFrame(tick); })(start); }); } var _requestAnimFrame = fabric.window.requestAnimationFrame || fabric.window.webkitRequestAnimationFrame || fabric.window.mozRequestAnimationFrame || fabric.window.oRequestAnimationFrame || fabric.window.msRequestAnimationFrame || function(callback) { fabric.window.setTimeout(callback, 1000 / 60); }; /** * requestAnimationFrame polyfill based on http://paulirish.com/2011/requestanimationframe-for-smart-animating/ * In order to get a precise start time, `requestAnimFrame` should be called as an entry into the method * @memberOf fabric.util * @param {Function} callback Callback to invoke * @param {DOMElement} element optional Element to associate with animation */ function requestAnimFrame() { return _requestAnimFrame.apply(fabric.window, arguments); } fabric.util.animate = animate; fabric.util.requestAnimFrame = requestAnimFrame; })(); (function() { function normalize(a, c, p, s) { if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = p / (2 * Math.PI) * Math.asin(c / a); } return { a: a, c: c, p: p, s: s }; } function elastic(opts, t, d) { return opts.a * Math.pow(2, 10 * (t -= 1)) * Math.sin( (t * d - opts.s) * (2 * Math.PI) / opts.p ); } /** * Cubic easing out * @memberOf fabric.util.ease */ function easeOutCubic(t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; } /** * Cubic easing in and out * @memberOf fabric.util.ease */ function easeInOutCubic(t, b, c, d) { t /= d/2; if (t < 1) { return c / 2 * t * t * t + b; } return c / 2 * ((t -= 2) * t * t + 2) + b; } /** * Quartic easing in * @memberOf fabric.util.ease */ function easeInQuart(t, b, c, d) { return c * (t /= d) * t * t * t + b; } /** * Quartic easing out * @memberOf fabric.util.ease */ function easeOutQuart(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; } /** * Quartic easing in and out * @memberOf fabric.util.ease */ function easeInOutQuart(t, b, c, d) { t /= d / 2; if (t < 1) { return c / 2 * t * t * t * t + b; } return -c / 2 * ((t -= 2) * t * t * t - 2) + b; } /** * Quintic easing in * @memberOf fabric.util.ease */ function easeInQuint(t, b, c, d) { return c * (t /= d) * t * t * t * t + b; } /** * Quintic easing out * @memberOf fabric.util.ease */ function easeOutQuint(t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; } /** * Quintic easing in and out * @memberOf fabric.util.ease */ function easeInOutQuint(t, b, c, d) { t /= d / 2; if (t < 1) { return c / 2 * t * t * t * t * t + b; } return c / 2 * ((t -= 2) * t * t * t * t + 2) + b; } /** * Sinusoidal easing in * @memberOf fabric.util.ease */ function easeInSine(t, b, c, d) { return -c * Math.cos(t / d * (Math.PI / 2)) + c + b; } /** * Sinusoidal easing out * @memberOf fabric.util.ease */ function easeOutSine(t, b, c, d) { return c * Math.sin(t / d * (Math.PI / 2)) + b; } /** * Sinusoidal easing in and out * @memberOf fabric.util.ease */ function easeInOutSine(t, b, c, d) { return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b; } /** * Exponential easing in * @memberOf fabric.util.ease */ function easeInExpo(t, b, c, d) { return (t === 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; } /** * Exponential easing out * @memberOf fabric.util.ease */ function easeOutExpo(t, b, c, d) { return (t === d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; } /** * Exponential easing in and out * @memberOf fabric.util.ease */ function easeInOutExpo(t, b, c, d) { if (t === 0) { return b; } if (t === d) { return b + c; } t /= d / 2; if (t < 1) { return c / 2 * Math.pow(2, 10 * (t - 1)) + b; } return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b; } /** * Circular easing in * @memberOf fabric.util.ease */ function easeInCirc(t, b, c, d) { return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; } /** * Circular easing out * @memberOf fabric.util.ease */ function easeOutCirc(t, b, c, d) { return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; } /** * Circular easing in and out * @memberOf fabric.util.ease */ function easeInOutCirc(t, b, c, d) { t /= d / 2; if (t < 1) { return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b; } return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; } /** * Elastic easing in * @memberOf fabric.util.ease */ function easeInElastic(t, b, c, d) { var s = 1.70158, p = 0, a = c; if (t === 0) { return b; } t /= d; if (t === 1) { return b + c; } if (!p) { p = d * 0.3; } var opts = normalize(a, c, p, s); return -elastic(opts, t, d) + b; } /** * Elastic easing out * @memberOf fabric.util.ease */ function easeOutElastic(t, b, c, d) { var s = 1.70158, p = 0, a = c; if (t === 0) { return b; } t /= d; if (t === 1) { return b + c; } if (!p) { p = d * 0.3; } var opts = normalize(a, c, p, s); return opts.a * Math.pow(2, -10 * t) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) + opts.c + b; } /** * Elastic easing in and out * @memberOf fabric.util.ease */ function easeInOutElastic(t, b, c, d) { var s = 1.70158, p = 0, a = c; if (t === 0) { return b; } t /= d / 2; if (t === 2) { return b + c; } if (!p) { p = d * (0.3 * 1.5); } var opts = normalize(a, c, p, s); if (t < 1) { return -0.5 * elastic(opts, t, d) + b; } return opts.a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - opts.s) * (2 * Math.PI) / opts.p ) * 0.5 + opts.c + b; } /** * Backwards easing in * @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; } /** * Backwards easing out * @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; } /** * Backwards easing in and out * @memberOf fabric.util.ease */ function easeInOutBack(t, b, c, d, s) { if (s === undefined) { s = 1.70158; } t /= d / 2; if (t < 1) { return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b; } return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b; } /** * Bouncing easing in * @memberOf fabric.util.ease */ function easeInBounce(t, b, c, d) { return c - easeOutBounce (d - t, 0, c, d) + b; } /** * Bouncing easing out * @memberOf fabric.util.ease */ function easeOutBounce(t, b, c, d) { if ((t /= d) < (1 / 2.75)) { return c * (7.5625 * t * t) + b; } else if (t < (2/2.75)) { return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b; } else if (t < (2.5/2.75)) { return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b; } else { return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b; } } /** * Bouncing easing in and out * @memberOf fabric.util.ease */ function easeInOutBounce(t, b, c, d) { if (t < d / 2) { return easeInBounce (t * 2, 0, c, d) * 0.5 + b; } return easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; } /** * Easing functions * See Easing Equations by Robert Penner * @namespace fabric.util.ease */ fabric.util.ease = { /** * Quadratic easing in * @memberOf fabric.util.ease */ easeInQuad: function(t, b, c, d) { return c * (t /= d) * t + b; }, /** * Quadratic easing out * @memberOf fabric.util.ease */ easeOutQuad: function(t, b, c, d) { return -c * (t /= d) * (t - 2) + b; }, /** * Quadratic easing in and out * @memberOf fabric.util.ease */ easeInOutQuad: function(t, b, c, d) { t /= (d / 2); if (t < 1) { return c / 2 * t * t + b; } return -c / 2 * ((--t) * (t - 2) - 1) + b; }, /** * Cubic easing in * @memberOf fabric.util.ease */ easeInCubic: function(t, b, c, d) { return c * (t /= d) * t * t + b; }, 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, toFixed = fabric.util.toFixed, parseUnit = fabric.util.parseUnit, multiplyTransformMatrices = fabric.util.multiplyTransformMatrices, attributesMap = { cx: 'left', x: 'left', r: 'radius', cy: 'top', y: 'top', display: 'visible', visibility: 'visible', transform: 'transformMatrix', 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule', 'font-family': 'fontFamily', 'font-size': 'fontSize', 'font-style': 'fontStyle', 'font-weight': 'fontWeight', 'stroke-dasharray': 'strokeDashArray', 'stroke-linecap': 'strokeLineCap', 'stroke-linejoin': 'strokeLineJoin', 'stroke-miterlimit': 'strokeMiterLimit', 'stroke-opacity': 'strokeOpacity', 'stroke-width': 'strokeWidth', 'text-decoration': 'textDecoration', 'text-anchor': 'originX' }, colorAttributes = { stroke: 'strokeOpacity', fill: 'fillOpacity' }; fabric.cssRules = { }; fabric.gradientDefs = { }; function normalizeAttr(attr) { // transform attribute names if (attr in attributesMap) { return attributesMap[attr]; } return attr; } function normalizeValue(attr, value, parentAttributes, fontSize) { var isArray = Object.prototype.toString.call(value) === '[object Array]', parsed; if ((attr === 'fill' || attr === 'stroke') && value === 'none') { value = ''; } else if (attr === 'strokeDashArray') { value = value.replace(/,/g, ' ').split(/\s+/).map(function(n) { return parseFloat(n); }); } else if (attr === 'transformMatrix') { if (parentAttributes && parentAttributes.transformMatrix) { value = multiplyTransformMatrices( parentAttributes.transformMatrix, fabric.parseTransformAttribute(value)); } else { value = fabric.parseTransformAttribute(value); } } else if (attr === 'visible') { value = (value === 'none' || value === 'hidden') ? false : true; // display=none on parent element always takes precedence over child element if (parentAttributes && parentAttributes.visible === false) { value = false; } } else if (attr === 'originX' /* text-anchor */) { value = value === 'start' ? 'left' : value === 'end' ? 'right' : 'center'; } else { parsed = isArray ? value.map(parseUnit) : parseUnit(value, fontSize); } return (!isArray && isNaN(parsed) ? value : parsed); } /** * @private * @param {Object} attributes Array of attributes to parse */ function _setStrokeFillOpacity(attributes) { for (var attr in colorAttributes) { if (!attributes[attr] || typeof attributes[colorAttributes[attr]] === 'undefined') { continue; } if (attributes[attr].indexOf('url(') === 0) { continue; } var color = new fabric.Color(attributes[attr]); attributes[attr] = color.setAlpha(toFixed(color.getAlpha() * attributes[colorAttributes[attr]], 2)).toRgba(); } return attributes; } /** * Parses "transform" attribute, returning an array of values * @static * @function * @memberOf fabric * @param {String} attributeValue 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] = Math.tan(fabric.util.degreesToRadians(args[0])); } function skewYMatrix(matrix, args) { matrix[1] = Math.tan(fabric.util.degreesToRadians(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 = fabric.reNum, commaWsp = '(?:\\s+,?\\s*|,\\s*)', skewX = '(?:(skewX)\\s*\\(\\s*(' + number + ')\\s*\\))', skewY = '(?:(skewY)\\s*\\(\\s*(' + number + ')\\s*\\))', rotate = '(?:(rotate)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + '))?\\s*\\))', scale = '(?:(scale)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + '))?\\s*\\))', translate = '(?:(translate)\\s*\\(\\s*(' + number + ')(?:' + commaWsp + '(' + number + '))?\\s*\\))', matrix = '(?:(matrix)\\s*\\(\\s*' + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + commaWsp + '(' + number + ')' + '\\s*\\))', transform = '(?:' + matrix + '|' + translate + '|' + scale + '|' + rotate + '|' + skewX + '|' + skewY + ')', transforms = '(?:' + transform + '(?:' + commaWsp + transform + ')*' + ')', transformList = '^\\s*(?:' + transforms + '?)\\s*$', // http://www.w3.org/TR/SVG/coords.html#TransformAttribute reTransformList = new RegExp(transformList), // == end transform regexp reTransform = new RegExp(transform, 'g'); return function(attributeValue) { // start with identity matrix var matrix = iMatrix.concat(), matrices = [ ]; // 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': args[0] = fabric.util.degreesToRadians(args[0]); 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; } // snapshot current matrix into matrices array matrices.push(matrix.concat()); // reset matrix = iMatrix.concat(); }); var combinedMatrix = matrices[0]; while (matrices.length > 1) { matrices.shift(); combinedMatrix = fabric.util.multiplyTransformMatrices(combinedMatrix, matrices[0]); } return combinedMatrix; }; })(); /** * @private */ function parseStyleString(style, oStyle) { var attr, value; style.replace(/;$/, '').split(';').forEach(function (chunk) { var pair = chunk.split(':'); attr = normalizeAttr(pair[0].trim().toLowerCase()); value = normalizeValue(attr, pair[1].trim()); oStyle[attr] = value; }); } /** * @private */ function parseStyleObject(style, oStyle) { var attr, value; for (var prop in style) { if (typeof style[prop] === 'undefined') { continue; } attr = normalizeAttr(prop.toLowerCase()); value = normalizeValue(attr, style[prop]); oStyle[attr] = value; } } /** * @private */ function getGlobalStylesForElement(element, svgUid) { var styles = { }; for (var rule in fabric.cssRules[svgUid]) { if (elementMatchesRule(element, rule.split(' '))) { for (var property in fabric.cssRules[svgUid][rule]) { styles[property] = fabric.cssRules[svgUid][rule][property]; } } } return styles; } /** * @private */ function elementMatchesRule(element, selectors) { var firstMatching, parentMatching = true; //start from rightmost selector. firstMatching = selectorMatches(element, selectors.pop()); if (firstMatching && selectors.length) { parentMatching = doesSomeParentMatch(element, selectors); } return firstMatching && parentMatching && (selectors.length === 0); } function doesSomeParentMatch(element, selectors) { var selector, parentMatching = true; while (element.parentNode && element.parentNode.nodeType === 1 && selectors.length) { if (parentMatching) { selector = selectors.pop(); } element = element.parentNode; parentMatching = selectorMatches(element, selector); } return selectors.length === 0; } /** * @private */ function selectorMatches(element, selector) { var nodeName = element.nodeName, classNames = element.getAttribute('class'), id = element.getAttribute('id'), matcher; // i check if a selector matches slicing away part from it. // if i get empty string i should match matcher = new RegExp('^' + nodeName, 'i'); selector = selector.replace(matcher, ''); if (id && selector.length) { matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); selector = selector.replace(matcher, ''); } if (classNames && selector.length) { classNames = classNames.split(' '); for (var i = classNames.length; i--;) { matcher = new RegExp('\\.' + classNames[i] + '(?![a-zA-Z\\-]+)', 'i'); selector = selector.replace(matcher, ''); } } return selector.length === 0; } /** * @private */ function parseUseDirectives(doc) { var nodelist = doc.getElementsByTagName('use'); while (nodelist.length) { var el = nodelist[0], xlink = el.getAttribute('xlink:href').substr(1), x = el.getAttribute('x') || 0, y = el.getAttribute('y') || 0, el2 = doc.getElementById(xlink).cloneNode(true), currentTrans = (el2.getAttribute('transform') || '') + ' translate(' + x + ', ' + y + ')', parentNode; for (var j = 0, attrs = el.attributes, l = attrs.length; j < l; j++) { var attr = attrs.item(j); if (attr.nodeName === 'x' || attr.nodeName === 'y' || attr.nodeName === 'xlink:href') { continue; } if (attr.nodeName === 'transform') { currentTrans = attr.nodeValue + ' ' + currentTrans; } else { el2.setAttribute(attr.nodeName, attr.nodeValue); } } el2.setAttribute('transform', currentTrans); el2.setAttribute('instantiated_by_use', '1'); el2.removeAttribute('id'); parentNode = el.parentNode; parentNode.replaceChild(el2, el); } } /** * Add a element that envelop all child elements and makes the viewbox transformMatrix descend on all elements */ function addVBTransform(element, widthAttr, heightAttr) { // http://www.w3.org/TR/SVG/coords.html#ViewBoxAttribute // matches, e.g.: +14.56e-12, etc. var reViewBoxAttrValue = new RegExp( '^' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*,?' + '\\s*(' + fabric.reNum + '+)\\s*' + '$' ), viewBoxAttr = element.getAttribute('viewBox'), scaleX = 1, scaleY = 1, minX = 0, minY = 0, viewBoxWidth, viewBoxHeight, matrix, el; if (viewBoxAttr && (viewBoxAttr = viewBoxAttr.match(reViewBoxAttrValue))) { minX = -parseFloat(viewBoxAttr[1]), minY = -parseFloat(viewBoxAttr[2]), viewBoxWidth = parseFloat(viewBoxAttr[3]), viewBoxHeight = parseFloat(viewBoxAttr[4]); } else { return; } if (widthAttr && widthAttr !== viewBoxWidth) { scaleX = widthAttr / viewBoxWidth; } if (heightAttr && heightAttr !== viewBoxHeight) { scaleY = heightAttr / viewBoxHeight; } // default is to preserve aspect ratio // preserveAspectRatio attribute to be implemented scaleY = scaleX = (scaleX > scaleY ? scaleY : scaleX); if (!(scaleX !== 1 || scaleY !== 1 || minX !== 0 || minY !== 0)) { return; } matrix = 'matrix(' + scaleX + ' 0' + ' 0 ' + scaleY + ' ' + (minX * scaleX) + ' ' + (minY * scaleY) + ')'; if (element.tagName === 'svg') { el = element.ownerDocument.createElement('g'); while (element.firstChild != null) { el.appendChild(element.firstChild); } element.appendChild(el); } else { el = element; matrix += el.getAttribute('transform'); } el.setAttribute('transform', matrix); } /** * Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback * @static * @function * @memberOf fabric * @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)$/, reViewBoxTagNames = /^(symbol|image|marker|pattern|view)$/; function hasAncestorWithNodeName(element, nodeName) { while (element && (element = element.parentNode)) { if (nodeName.test(element.nodeName) && !element.getAttribute('instantiated_by_use')) { return true; } } return false; } return function(doc, callback, reviver) { if (!doc) { return; } parseUseDirectives(doc); var startTime = new Date(), svgUid = fabric.Object.__uid++, /* http://www.w3.org/TR/SVG/struct.html#SVGElementWidthAttribute * as per spec, width and height attributes are to be considered * 100% if no value is specified. */ widthAttr = parseUnit(doc.getAttribute('width') || '100%'), heightAttr = parseUnit(doc.getAttribute('height') || '100%'); addVBTransform(doc, widthAttr, heightAttr); var descendants = fabric.util.toArray(doc.getElementsByTagName('*')); if (descendants.length === 0 && fabric.isLikelyNode) { // 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) { reViewBoxTagNames.test(el.tagName) && addVBTransform(el, 0, 0); return reAllowedSVGTagNames.test(el.tagName) && !hasAncestorWithNodeName(el, /^(?:pattern|defs|symbol)$/); // http://www.w3.org/TR/SVG/struct.html#DefsElement }); if (!elements || (elements && !elements.length)) { callback && callback([], {}); return; } var options = { width: widthAttr, height: heightAttr, widthAttr: widthAttr, heightAttr: heightAttr, svgUid: svgUid }; fabric.gradientDefs[svgUid] = fabric.getGradientDefs(doc); fabric.cssRules[svgUid] = fabric.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`) * @namespace */ var svgCache = { /** * @param {String} name * @param {Function} callback */ has: function (name, callback) { callback(false); }, get: function () { /* NOOP */ }, set: function () { /* NOOP */ } }; /** * @private */ 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 }); } /** * @private */ function _createSVGPattern(markup, canvas, property) { if (canvas[property] && canvas[property].toSVG) { markup.push( '', '' ); } } extend(fabric, { /** * Parses a short font declaration, building adding its properties to a style object * @static * @function * @memberOf fabric * @param {String} value font declaration * @param {Object} oStyle definition */ parseFontDeclaration: function(value, oStyle) { var fontDeclaration = '(normal|italic)?\\s*(normal|small-caps)?\\s*' + '(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*(' + fabric.reNum + '(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|' + fabric.reNum + '))?\\s+(.*)', match = value.match(fontDeclaration); if (!match) { return; } var fontStyle = match[1], // font variant is not used // fontVariant = match[2], fontWeight = match[3], fontSize = match[4], lineHeight = match[5], fontFamily = match[6]; if (fontStyle) { oStyle.fontStyle = fontStyle; } if (fontWeight) { oStyle.fontWeight = isNaN(parseFloat(fontWeight)) ? fontWeight : parseFloat(fontWeight); } if (fontSize) { oStyle.fontSize = parseUnit(fontSize); } if (fontFamily) { oStyle.fontFamily = fontFamily; } if (lineHeight) { oStyle.lineHeight = lineHeight === 'normal' ? 1 : lineHeight; } }, /** * Parses an SVG document, returning all of the gradient declarations found in it * @static * @function * @memberOf fabric * @param {SVGDocument} doc SVG document to parse * @return {Object} Gradient definitions; key corresponds to element id, value -- to gradient definition element */ getGradientDefs: function(doc) { var linearGradientEls = doc.getElementsByTagName('linearGradient'), radialGradientEls = doc.getElementsByTagName('radialGradient'), el, i, j = 0, id, xlink, elList = [ ], gradientDefs = { }, idsToXlinkMap = { }; elList.length = linearGradientEls.length + radialGradientEls.length; i = linearGradientEls.length; while (i--) { elList[j++] = linearGradientEls[i]; } i = radialGradientEls.length; while (i--) { elList[j++] = radialGradientEls[i]; } while (j--) { el = elList[j]; xlink = el.getAttribute('xlink:href'); id = el.getAttribute('id'); if (xlink) { idsToXlinkMap[id] = xlink.substr(1); } gradientDefs[id] = el; } for (id in idsToXlinkMap) { var el2 = gradientDefs[idsToXlinkMap[id]].cloneNode(true); el = gradientDefs[id]; while (el2.firstChild) { el.appendChild(el2.firstChild); } } return gradientDefs; }, /** * Returns an object of attributes' name/value, given element and an array of attribute names; * Parses parent "g" nodes recursively upwards. * @static * @memberOf fabric * @param {DOMElement} element Element to parse * @param {Array} attributes Array of attributes to parse * @return {Object} object containing parsed attributes' names/values */ parseAttributes: function(element, attributes, svgUid) { if (!element) { return; } var value, parentAttributes = { }, fontSize; if (typeof svgUid === 'undefined') { svgUid = element.getAttribute('svgUid'); } // if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards if (element.parentNode && /^symbol|[g|a]$/i.test(element.parentNode.nodeName)) { parentAttributes = fabric.parseAttributes(element.parentNode, attributes, svgUid); } fontSize = (parentAttributes && parentAttributes.fontSize ) || element.getAttribute('font-size') || fabric.Text.DEFAULT_SVG_FONT_SIZE; var ownAttributes = attributes.reduce(function(memo, attr) { value = element.getAttribute(attr); if (value) { attr = normalizeAttr(attr); value = normalizeValue(attr, value, parentAttributes, fontSize); memo[attr] = value; } 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, svgUid), fabric.parseStyleAttribute(element))); if (ownAttributes.font) { fabric.parseFontDeclaration(ownAttributes.font, ownAttributes); } return _setStrokeFillOpacity(extend(parentAttributes, ownAttributes)); }, /** * Transforms an array of svg elements to corresponding fabric.* instances * @static * @memberOf fabric * @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. */ parseElements: function(elements, callback, options, reviver) { new fabric.ElementsParser(elements, callback, options, reviver).parse(); }, /** * Parses "style" attribute, retuning an object with values * @static * @memberOf fabric * @param {SVGElement} element Element to parse * @return {Object} Objects with values parsed from style attribute of an element */ parseStyleAttribute: function(element) { var oStyle = { }, style = element.getAttribute('style'); if (!style) { return oStyle; } if (typeof style === 'string') { parseStyleString(style, oStyle); } else { parseStyleObject(style, oStyle); } return oStyle; }, /** * Parses "points" attribute, returning an array of values * @static * @memberOf fabric * @param {String} points points attribute string * @return {Array} array of points */ parsePointsAttribute: function(points) { // points attribute is required and must not be empty if (!points) { return null; } // replace commas with whitespace and remove bookending whitespace points = points.replace(/,/g, ' ').trim(); points = points.split(/\s+/); var parsedPoints = [ ], i, len; i = 0; len = points.length; for (; i < len; i+=2) { parsedPoints.push({ x: parseFloat(points[i]), y: parseFloat(points[i + 1]) }); } // odd number of points is an error // if (parsedPoints.length % 2 !== 0) { // return null; // } return parsedPoints; }, /** * Returns CSS rules for a given SVG document * @static * @function * @memberOf fabric * @param {SVGDocument} doc SVG document to parse * @return {Object} CSS rules of this document */ getCSSRules: function(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[i].textContent; // remove comments styleContents = styleContents.replace(/\/\*[\s\S]*?\*\//g, ''); if (styleContents.trim() === '') { continue; } 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*\{([^}]*)\}/), ruleObj = { }, declaration = match[2].trim(), propertyValuePairs = declaration.replace(/;$/, '').split(/\s*;\s*/); for (var i = 0, len = propertyValuePairs.length; i < len; i++) { var pair = propertyValuePairs[i].split(/\s*:\s*/), property = normalizeAttr(pair[0]), value = normalizeValue(property, pair[1], pair[0]); ruleObj[property] = value; } rule = match[1]; rule.split(',').forEach(function(_rule) { _rule = _rule.replace(/^svg/i, '').trim(); if (_rule === '') { return; } allRules[_rule] = fabric.util.object.clone(ruleObj); }); }); } return allRules; }, /** * Takes url corresponding to an SVG document, and parses it into a set of fabric objects. Note that SVG is fetched via XMLHttpRequest, so it needs to conform to SOP (Same Origin Policy) * @memberof fabric * @param {String} url * @param {Function} callback * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. */ loadSVGFromURL: function(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 && !xml.documentElement && fabric.window.ActiveXObject && r.responseText) { xml = new ActiveXObject('Microsoft.XMLDOM'); xml.async = 'false'; //IE chokes on DOCTYPE xml.loadXML(r.responseText.replace(//i, '')); } if (!xml || !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); } }, /** * Takes string corresponding to an SVG document, and parses it into a set of fabric objects * @memberof fabric * @param {String} string * @param {Function} callback * @param {Function} [reviver] Method for further parsing of SVG elements, called after each fabric object created. */ loadSVGFromString: function(string, callback, reviver) { string = string.trim(); var doc; if (typeof DOMParser !== 'undefined') { var parser = new DOMParser(); if (parser && parser.parseFromString) { doc = parser.parseFromString(string, 'text/xml'); } } else if (fabric.window.ActiveXObject) { doc = new ActiveXObject('Microsoft.XMLDOM'); doc.async = 'false'; // IE chokes on DOCTYPE doc.loadXML(string.replace(//i, '')); } fabric.parseSVGDocument(doc.documentElement, function (results, options) { callback(results, options); }, reviver); }, /** * Creates markup containing SVG font faces * @param {Array} objects Array of fabric objects * @return {String} */ createSVGFontFacesMarkup: function(objects) { var markup = ''; for (var i = 0, len = objects.length; i < len; i++) { if (objects[i].type !== 'text' || !objects[i].path) { continue; } markup += [ //jscs:disable validateIndentation '@font-face {', 'font-family: ', objects[i].fontFamily, '; ', 'src: url(\'', objects[i].path, '\')', '}' //jscs:enable validateIndentation ].join(''); } if (markup) { markup = [ //jscs:disable validateIndentation '' //jscs:enable validateIndentation ].join(''); } return markup; }, /** * Creates markup containing SVG referenced elements like patterns, gradients etc. * @param {fabric.Canvas} canvas instance of fabric.Canvas * @return {String} */ createSVGRefElementsMarkup: function(canvas) { var markup = [ ]; _createSVGPattern(markup, canvas, 'backgroundColor'); _createSVGPattern(markup, canvas, 'overlayColor'); return markup.join(''); } }); })(typeof exports !== 'undefined' ? exports : this); fabric.ElementsParser = function(elements, callback, options, reviver) { this.elements = elements; this.callback = callback; this.options = options; this.reviver = reviver; this.svgUid = (options && options.svgUid) || 0; }; fabric.ElementsParser.prototype.parse = function() { this.instances = new Array(this.elements.length); this.numElements = this.elements.length; this.createObjects(); }; fabric.ElementsParser.prototype.createObjects = function() { for (var i = 0, len = this.elements.length; i < len; i++) { this.elements[i].setAttribute('svgUid', this.svgUid); (function(_this, i) { setTimeout(function() { _this.createObject(_this.elements[i], i); }, 0); })(this, i); } }; fabric.ElementsParser.prototype.createObject = function(el, index) { var klass = fabric[fabric.util.string.capitalize(el.tagName)]; if (klass && klass.fromElement) { try { this._createObject(klass, el, index); } catch (err) { fabric.log(err); } } else { this.checkIfDone(); } }; fabric.ElementsParser.prototype._createObject = function(klass, el, index) { if (klass.async) { klass.fromElement(el, this.createCallback(index, el), this.options); } else { var obj = klass.fromElement(el, this.options); this.resolveGradient(obj, 'fill'); this.resolveGradient(obj, 'stroke'); this.reviver && this.reviver(el, obj); this.instances[index] = obj; this.checkIfDone(); } }; fabric.ElementsParser.prototype.createCallback = function(index, el) { var _this = this; return function(obj) { _this.resolveGradient(obj, 'fill'); _this.resolveGradient(obj, 'stroke'); _this.reviver && _this.reviver(el, obj); _this.instances[index] = obj; _this.checkIfDone(); }; }; fabric.ElementsParser.prototype.resolveGradient = function(obj, property) { var instanceFillValue = obj.get(property); if (!(/^url\(/).test(instanceFillValue)) { return; } var gradientId = instanceFillValue.slice(5, instanceFillValue.length - 1); if (fabric.gradientDefs[this.svgUid][gradientId]) { obj.set(property, fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][gradientId], obj)); } }; fabric.ElementsParser.prototype.checkIfDone = function() { if (--this.numElements === 0) { this.instances = this.instances.filter(function(el) { return el != null; }); this.callback(this.instances); } }; (function(global) { 'use strict'; /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ var fabric = global.fabric || (global.fabric = { }); if (fabric.Point) { fabric.warn('fabric.Point is already defined'); return; } fabric.Point = Point; /** * Point class * @class fabric.Point * @memberOf fabric * @constructor * @param {Number} x * @param {Number} y * @return {fabric.Point} thisArg */ function Point(x, y) { this.x = x; this.y = y; } Point.prototype = /** @lends fabric.Point.prototype */ { constructor: Point, /** * Adds another point to this one and returns another one * @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); }, /** * Adds another point to this one * @param {fabric.Point} that * @return {fabric.Point} thisArg */ addEquals: function (that) { this.x += that.x; this.y += that.y; return this; }, /** * Adds value to this point and returns a new one * @param {Number} scalar * @return {fabric.Point} new Point with added value */ scalarAdd: function (scalar) { return new Point(this.x + scalar, this.y + scalar); }, /** * Adds value to this point * @param {Number} scalar * @return {fabric.Point} thisArg */ scalarAddEquals: function (scalar) { this.x += scalar; this.y += scalar; return this; }, /** * Subtracts another point from this point and returns a new one * @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); }, /** * Subtracts another point from this point * @param {fabric.Point} that * @return {fabric.Point} thisArg */ subtractEquals: function (that) { this.x -= that.x; this.y -= that.y; return this; }, /** * Subtracts value from this point and returns a new one * @param {Number} scalar * @return {fabric.Point} */ scalarSubtract: function (scalar) { return new Point(this.x - scalar, this.y - scalar); }, /** * Subtracts value from this point * @param {Number} scalar * @return {fabric.Point} thisArg */ scalarSubtractEquals: function (scalar) { this.x -= scalar; this.y -= scalar; return this; }, /** * Miltiplies this point by a value and returns a new one * @param {Number} scalar * @return {fabric.Point} */ multiply: function (scalar) { return new Point(this.x * scalar, this.y * scalar); }, /** * Miltiplies this point by a value * @param {Number} scalar * @return {fabric.Point} thisArg */ multiplyEquals: function (scalar) { this.x *= scalar; this.y *= scalar; return this; }, /** * Divides this point by a value and returns a new one * @param {Number} scalar * @return {fabric.Point} */ divide: function (scalar) { return new Point(this.x / scalar, this.y / scalar); }, /** * Divides this point by a value * @param {Number} scalar * @return {fabric.Point} thisArg */ divideEquals: function (scalar) { this.x /= scalar; this.y /= scalar; return this; }, /** * Returns true if this point is equal to another one * @param {fabric.Point} that * @return {Boolean} */ eq: function (that) { return (this.x === that.x && this.y === that.y); }, /** * Returns true if this point is less than another one * @param {fabric.Point} that * @return {Boolean} */ lt: function (that) { return (this.x < that.x && this.y < that.y); }, /** * Returns true if this point is less than or equal to another one * @param {fabric.Point} that * @return {Boolean} */ lte: function (that) { return (this.x <= that.x && this.y <= that.y); }, /** * Returns true if this point is greater another one * @param {fabric.Point} that * @return {Boolean} */ gt: function (that) { return (this.x > that.x && this.y > that.y); }, /** * Returns true if this point is greater than or equal to another one * @param {fabric.Point} that * @return {Boolean} */ gte: function (that) { return (this.x >= that.x && this.y >= that.y); }, /** * Returns new point which is the result of linear interpolation with this one and another one * @param {fabric.Point} that * @param {Number} t * @return {fabric.Point} */ lerp: function (that, t) { return new Point(this.x + (that.x - this.x) * t, this.y + (that.y - this.y) * t); }, /** * Returns distance from this point and another one * @param {fabric.Point} that * @return {Number} */ distanceFrom: function (that) { var dx = this.x - that.x, dy = this.y - that.y; return Math.sqrt(dx * dx + dy * dy); }, /** * Returns the point between this point and another one * @param {fabric.Point} that * @return {fabric.Point} */ midPointFrom: function (that) { return new Point(this.x + (that.x - this.x)/2, this.y + (that.y - this.y)/2); }, /** * Returns a new point which is the min of this and another one * @param {fabric.Point} that * @return {fabric.Point} */ min: function (that) { return new Point(Math.min(this.x, that.x), Math.min(this.y, that.y)); }, /** * Returns a new point which is the max of this and another one * @param {fabric.Point} that * @return {fabric.Point} */ max: function (that) { return new Point(Math.max(this.x, that.x), Math.max(this.y, that.y)); }, /** * Returns string representation of this point * @return {String} */ toString: function () { return this.x + ',' + this.y; }, /** * Sets x/y of this point * @param {Number} x * @param {Number} y */ setXY: function (x, y) { this.x = x; this.y = y; }, /** * Sets x/y of this point from another point * @param {fabric.Point} that */ setFromPoint: function (that) { this.x = that.x; this.y = that.y; }, /** * Swaps x/y of this point and another point * @param {fabric.Point} that */ swap: function (that) { var x = this.x, y = this.y; this.x = that.x; this.y = that.y; that.x = x; that.y = y; } }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; /* Adaptation of work of Kevin Lindsey (kevin@kevlindev.com) */ var fabric = global.fabric || (global.fabric = { }); if (fabric.Intersection) { fabric.warn('fabric.Intersection is already defined'); return; } /** * Intersection class * @class fabric.Intersection * @memberOf fabric * @constructor */ function Intersection(status) { this.status = status; this.points = []; } fabric.Intersection = Intersection; fabric.Intersection.prototype = /** @lends fabric.Intersection.prototype */ { /** * Appends a point to intersection * @param {fabric.Point} point */ appendPoint: function (point) { this.points.push(point); }, /** * Appends points to intersection * @param {Array} points */ appendPoints: function (points) { this.points = this.points.concat(points); } }; /** * Checks if one line intersects another * @static * @param {fabric.Point} a1 * @param {fabric.Point} a2 * @param {fabric.Point} b1 * @param {fabric.Point} b2 * @return {fabric.Intersection} */ fabric.Intersection.intersectLineLine = function (a1, a2, b1, b2) { var result, uaT = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x), ubT = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x), uB = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y); if (uB !== 0) { var ua = uaT / uB, ub = ubT / uB; 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(); } } else { if (uaT === 0 || ubT === 0) { result = new Intersection('Coincident'); } else { result = new Intersection('Parallel'); } } return result; }; /** * Checks if line intersects polygon * @static * @param {fabric.Point} a1 * @param {fabric.Point} a2 * @param {Array} points * @return {fabric.Intersection} */ fabric.Intersection.intersectLinePolygon = function(a1, a2, points) { var result = new 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; }; /** * Checks if polygon intersects another polygon * @static * @param {Array} points1 * @param {Array} points2 * @return {fabric.Intersection} */ fabric.Intersection.intersectPolygonPolygon = function (points1, points2) { var result = new 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; }; /** * Checks if polygon intersects rectangle * @static * @param {Array} points * @param {Number} r1 * @param {Number} r2 * @return {fabric.Intersection} */ 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(); 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; } /** * Color class * 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 fabric.Color * @param {String} color optional in hex or rgb(a) format * @return {fabric.Color} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} */ function Color(color) { if (!color) { this.setSource([0, 0, 0, 1]); } else { this._tryParsingColor(color); } } fabric.Color = Color; fabric.Color.prototype = /** @lends fabric.Color.prototype */ { /** * @private * @param {String|Array} color Color value to parse */ _tryParsingColor: function(color) { var source; if (color in Color.colorNameMap) { color = Color.colorNameMap[color]; } if (color === 'transparent') { this.setSource([255, 255, 255, 0]); return; } source = Color.sourceFromHex(color); if (!source) { source = Color.sourceFromRgb(color); } if (!source) { source = Color.sourceFromHsl(color); } if (source) { this.setSource(source); } }, /** * Adapted from https://github.com/mjijackson * @private * @param {Number} r Red color value * @param {Number} g Green color value * @param {Number} b Blue color value * @return {Array} Hsl color */ _rgbToHsl: function(r, g, b) { r /= 255, g /= 255, b /= 255; var h, s, l, max = fabric.util.array.max([r, g, b]), min = fabric.util.array.min([r, g, b]); l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [ Math.round(h * 360), Math.round(s * 100), Math.round(l * 100) ]; }, /** * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) * @return {Array} */ getSource: function() { return this._source; }, /** * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) * @param {Array} source */ setSource: function(source) { this._source = source; }, /** * Returns color represenation in RGB format * @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 * @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 HSL format * @return {String} ex: hsl(0-360,0%-100%,0%-100%) */ toHsl: function() { var source = this.getSource(), hsl = this._rgbToHsl(source[0], source[1], source[2]); return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; }, /** * Returns color represenation in HSLA format * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) */ toHsla: function() { var source = this.getSource(), hsl = this._rgbToHsl(source[0], source[1], source[2]); return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; }, /** * Returns color represenation in HEX format * @return {String} ex: FF5555 */ toHex: function() { var source = this.getSource(), r, g, b; r = source[0].toString(16); r = (r.length === 1) ? ('0' + r) : r; g = source[1].toString(16); g = (g.length === 1) ? ('0' + g) : g; 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 * @return {Number} 0-1 */ getAlpha: function() { return this.getSource()[3]; }, /** * Sets value of alpha channel for this color * @param {Number} alpha Alpha value 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 * @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 * @param {Number} threshold * @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 * @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), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) * @static * @field * @memberOf fabric.Color */ fabric.Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/; /** * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) * @static * @field * @memberOf fabric.Color */ fabric.Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/; /** * Regex matching color in HEX format (ex: #FF5555, 010155, aff) * @static * @field * @memberOf fabric.Color */ fabric.Color.reHex = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i; /** * Map of the 17 basic color names with HEX code * @static * @field * @memberOf fabric.Color * @see: http://www.w3.org/TR/CSS2/syndata.html#color-units */ fabric.Color.colorNameMap = { aqua: '#00FFFF', black: '#000000', blue: '#0000FF', fuchsia: '#FF00FF', gray: '#808080', green: '#008000', lime: '#00FF00', maroon: '#800000', navy: '#000080', olive: '#808000', orange: '#FFA500', purple: '#800080', red: '#FF0000', silver: '#C0C0C0', teal: '#008080', white: '#FFFFFF', yellow: '#FFFF00' }; /** * @private * @param {Number} p * @param {Number} q * @param {Number} t * @return {Number} */ function hue2rgb(p, q, t) { if (t < 0) { t += 1; } if (t > 1) { t -= 1; } if (t < 1/6) { return p + (q - p) * 6 * t; } if (t < 1/2) { return q; } if (t < 2/3) { return p + (q - p) * (2/3 - t) * 6; } return p; } /** * Returns new color object, when given a color in RGB format * @memberOf fabric.Color * @param {String} color Color value 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 * @memberOf fabric.Color * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) * @return {Array} source */ fabric.Color.sourceFromRgb = function(color) { var match = color.match(Color.reRGBa); if (match) { var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); return [ parseInt(r, 10), parseInt(g, 10), parseInt(b, 10), match[4] ? parseFloat(match[4]) : 1 ]; } }; /** * Returns new color object, when given a color in RGBA format * @static * @function * @memberOf fabric.Color * @param {String} color * @return {fabric.Color} */ fabric.Color.fromRgba = Color.fromRgb; /** * Returns new color object, when given a color in HSL format * @param {String} color Color value ex: hsl(0-260,0%-100%,0%-100%) * @memberOf fabric.Color * @return {fabric.Color} */ fabric.Color.fromHsl = function(color) { return Color.fromSource(Color.sourceFromHsl(color)); }; /** * Returns array represenatation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. * Adapted from https://github.com/mjijackson * @memberOf fabric.Color * @param {String} color Color value ex: hsl(0-360,0%-100%,0%-100%) or hsla(0-360,0%-100%,0%-100%, 0-1) * @return {Array} source * @see http://http://www.w3.org/TR/css3-color/#hsl-color */ fabric.Color.sourceFromHsl = function(color) { var match = color.match(Color.reHSLa); if (!match) { return; } var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), r, g, b; if (s === 0) { r = g = b = l; } else { var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, p = l * 2 - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return [ Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), match[4] ? parseFloat(match[4]) : 1 ]; }; /** * Returns new color object, when given a color in HSLA format * @static * @function * @memberOf fabric.Color * @param {String} color * @return {fabric.Color} */ fabric.Color.fromHsla = Color.fromHsl; /** * Returns new color object, when given a color in HEX format * @static * @memberOf fabric.Color * @param {String} color Color value ex: FF5555 * @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 * @memberOf fabric.Color * @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 * @memberOf fabric.Color * @param {Array} source * @return {fabric.Color} */ fabric.Color.fromSource = function(source) { var oColor = new Color(); oColor.setSource(source); return oColor; }; })(typeof exports !== 'undefined' ? exports : this); (function() { /* _FROM_SVG_START_ */ function getColorStop(el) { var style = el.getAttribute('style'), offset = el.getAttribute('offset'), color, colorAlpha, opacity; // convert percents to absolute values offset = parseFloat(offset) / (/%$/.test(offset) ? 100 : 1); offset = offset < 0 ? 0 : offset > 1 ? 1 : offset; 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') { color = value; } else if (key === 'stop-opacity') { opacity = value; } } } if (!color) { color = el.getAttribute('stop-color') || 'rgb(0,0,0)'; } if (!opacity) { opacity = el.getAttribute('stop-opacity'); } color = new fabric.Color(color); colorAlpha = color.getAlpha(); opacity = isNaN(parseFloat(opacity)) ? 1 : parseFloat(opacity); opacity *= colorAlpha; return { offset: offset, color: color.toRgb(), opacity: opacity }; } function getLinearCoords(el) { return { x1: el.getAttribute('x1') || 0, y1: el.getAttribute('y1') || 0, x2: el.getAttribute('x2') || '100%', y2: el.getAttribute('y2') || 0 }; } function getRadialCoords(el) { return { x1: el.getAttribute('fx') || el.getAttribute('cx') || '50%', y1: el.getAttribute('fy') || el.getAttribute('cy') || '50%', r1: 0, x2: el.getAttribute('cx') || '50%', y2: el.getAttribute('cy') || '50%', r2: el.getAttribute('r') || '50%' }; } /* _FROM_SVG_END_ */ /** * Gradient class * @class fabric.Gradient * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#gradients} * @see {@link fabric.Gradient#initialize} for constructor definition */ fabric.Gradient = fabric.util.createClass(/** @lends fabric.Gradient.prototype */ { /** * Horizontal offset for aligning gradients coming from SVG when outside pathgroups * @type Number * @default 0 */ offsetX: 0, /** * Vertical offset for aligning gradients coming from SVG when outside pathgroups * @type Number * @default 0 */ offsetY: 0, /** * Constructor * @param {Object} [options] Options object with type, coords, gradientUnits and colorStops * @return {fabric.Gradient} thisArg */ initialize: function(options) { options || (options = { }); var coords = { }; this.id = fabric.Object.__uid++; this.type = options.type || 'linear'; coords = { x1: options.coords.x1 || 0, y1: options.coords.y1 || 0, x2: options.coords.x2 || 0, y2: options.coords.y2 || 0 }; if (this.type === 'radial') { coords.r1 = options.coords.r1 || 0; coords.r2 = options.coords.r2 || 0; } this.coords = coords; this.colorStops = options.colorStops.slice(); if (options.gradientTransform) { this.gradientTransform = options.gradientTransform; } this.offsetX = options.offsetX || this.offsetX; this.offsetY = options.offsetY || this.offsetY; }, /** * Adds another colorStop * @param {Object} colorStop Object with offset and color * @return {fabric.Gradient} thisArg */ addColorStop: function(colorStop) { for (var position in colorStop) { var color = new fabric.Color(colorStop[position]); this.colorStops.push({ offset: position, color: color.toRgb(), opacity: color.getAlpha() }); } return this; }, /** * Returns object representation of a gradient * @return {Object} */ toObject: function() { return { type: this.type, coords: this.coords, colorStops: this.colorStops, offsetX: this.offsetX, offsetY: this.offsetY }; }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an gradient * @param {Object} object Object to create a gradient for * @param {Boolean} normalize Whether coords should be normalized * @return {String} SVG representation of an gradient (linear/radial) */ toSVG: function(object) { var coords = fabric.util.object.clone(this.coords), markup, commonAttributes; // colorStops must be sorted ascending this.colorStops.sort(function(a, b) { return a.offset - b.offset; }); if (!(object.group && object.group.type === 'path-group')) { for (var prop in coords) { if (prop === 'x1' || prop === 'x2' || prop === 'r2') { coords[prop] += this.offsetX - object.width / 2; } else if (prop === 'y1' || prop === 'y2') { coords[prop] += this.offsetY - object.height / 2; } } } commonAttributes = 'id="SVGID_' + this.id + '" gradientUnits="userSpaceOnUse"'; if (this.gradientTransform) { commonAttributes += ' gradientTransform="matrix(' + this.gradientTransform.join(' ') + ')" '; } if (this.type === 'linear') { markup = [ //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ]; } else if (this.type === 'radial') { markup = [ //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ]; } for (var i = 0; i < this.colorStops.length; i++) { markup.push( //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ); } markup.push((this.type === 'linear' ? '\n' : '\n')); return markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns an instance of CanvasGradient * @param {CanvasRenderingContext2D} ctx Context to render on * @return {CanvasGradient} */ toLive: function(ctx, object) { var gradient, coords = fabric.util.object.clone(this.coords); if (!this.type) { return; } if (object.group && object.group.type === 'path-group') { for (var prop in coords) { if (prop === 'x1' || prop === 'x2') { coords[prop] += -this.offsetX + object.width / 2; } else if (prop === 'y1' || prop === 'y2') { coords[prop] += -this.offsetY + object.height / 2; } } } if (this.type === 'linear') { gradient = ctx.createLinearGradient( coords.x1, coords.y1, coords.x2, coords.y2); } else if (this.type === 'radial') { gradient = ctx.createRadialGradient( coords.x1, coords.y1, coords.r1, coords.x2, coords.y2, coords.r2); } for (var i = 0, len = this.colorStops.length; i < len; i++) { var color = this.colorStops[i].color, opacity = this.colorStops[i].opacity, offset = this.colorStops[i].offset; if (typeof opacity !== 'undefined') { color = new fabric.Color(color).setAlpha(opacity).toRgba(); } gradient.addColorStop(parseFloat(offset), color); } return gradient; } }); fabric.util.object.extend(fabric.Gradient, { /* _FROM_SVG_START_ */ /** * Returns {@link fabric.Gradient} instance from an SVG element * @static * @memberof fabric.Gradient * @param {SVGGradientElement} el SVG gradient element * @param {fabric.Object} instance * @return {fabric.Gradient} Gradient instance * @see http://www.w3.org/TR/SVG/pservers.html#LinearGradientElement * @see http://www.w3.org/TR/SVG/pservers.html#RadialGradientElement */ fromElement: function(el, instance) { /** * @example: * * * * * * * OR * * * * * * * OR * * * * * * * * OR * * * * * * * */ var colorStopEls = el.getElementsByTagName('stop'), type = (el.nodeName === 'linearGradient' ? 'linear' : 'radial'), gradientUnits = el.getAttribute('gradientUnits') || 'objectBoundingBox', gradientTransform = el.getAttribute('gradientTransform'), colorStops = [], coords = { }, ellipseMatrix; if (type === 'linear') { coords = getLinearCoords(el); } else if (type === 'radial') { coords = getRadialCoords(el); } for (var i = colorStopEls.length; i--; ) { colorStops.push(getColorStop(colorStopEls[i])); } ellipseMatrix = _convertPercentUnitsToValues(instance, coords, gradientUnits); var gradient = new fabric.Gradient({ type: type, coords: coords, colorStops: colorStops, offsetX: -instance.left, offsetY: -instance.top }); if (gradientTransform || ellipseMatrix !== '') { gradient.gradientTransform = fabric.parseTransformAttribute((gradientTransform || '') + ellipseMatrix); } return gradient; }, /* _FROM_SVG_END_ */ /** * Returns {@link fabric.Gradient} instance from its object representation * @static * @memberof fabric.Gradient * @param {Object} obj * @param {Object} [options] Options object */ forObject: function(obj, options) { options || (options = { }); _convertPercentUnitsToValues(obj, options.coords, 'userSpaceOnUse'); return new fabric.Gradient(options); } }); /** * @private */ function _convertPercentUnitsToValues(object, options, gradientUnits) { var propValue, addFactor = 0, multFactor = 1, ellipseMatrix = ''; for (var prop in options) { propValue = parseFloat(options[prop], 10); if (typeof options[prop] === 'string' && /^\d+%$/.test(options[prop])) { multFactor = 0.01; } else { multFactor = 1; } if (prop === 'x1' || prop === 'x2' || prop === 'r2') { multFactor *= gradientUnits === 'objectBoundingBox' ? object.width : 1; addFactor = gradientUnits === 'objectBoundingBox' ? object.left || 0 : 0; } else if (prop === 'y1' || prop === 'y2') { multFactor *= gradientUnits === 'objectBoundingBox' ? object.height : 1; addFactor = gradientUnits === 'objectBoundingBox' ? object.top || 0 : 0; } options[prop] = propValue * multFactor + addFactor; } if (object.type === 'ellipse' && options.r2 !== null && gradientUnits === 'objectBoundingBox' && object.rx !== object.ry) { var scaleFactor = object.ry/object.rx; ellipseMatrix = ' scale(1, ' + scaleFactor + ')'; if (options.y1) { options.y1 /= scaleFactor; } if (options.y2) { options.y2 /= scaleFactor; } } return ellipseMatrix; } })(); /** * Pattern class * @class fabric.Pattern * @see {@link http://fabricjs.com/patterns/|Pattern demo} * @see {@link http://fabricjs.com/dynamic-patterns/|DynamicPattern demo} * @see {@link fabric.Pattern#initialize} for constructor definition */ fabric.Pattern = fabric.util.createClass(/** @lends fabric.Pattern.prototype */ { /** * Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) * @type String * @default */ repeat: 'repeat', /** * Pattern horizontal offset from object's left/top corner * @type Number * @default */ offsetX: 0, /** * Pattern vertical offset from object's left/top corner * @type Number * @default */ offsetY: 0, /** * Constructor * @param {Object} [options] Options object * @return {fabric.Pattern} thisArg */ initialize: function(options) { options || (options = { }); this.id = fabric.Object.__uid++; if (options.source) { if (typeof options.source === 'string') { // function string if (typeof fabric.util.getFunctionBody(options.source) !== 'undefined') { this.source = new Function(fabric.util.getFunctionBody(options.source)); } else { // img src string var _this = this; this.source = fabric.util.createImage(); fabric.util.loadImage(options.source, function(img) { _this.source = img; }); } } else { // img element this.source = options.source; } } if (options.repeat) { this.repeat = options.repeat; } if (options.offsetX) { this.offsetX = options.offsetX; } if (options.offsetY) { this.offsetY = options.offsetY; } }, /** * Returns object representation of a pattern * @return {Object} Object representation of a pattern instance */ toObject: function() { var source; // callback if (typeof this.source === 'function') { source = String(this.source); } // element else if (typeof this.source.src === 'string') { source = this.source.src; } return { source: source, repeat: this.repeat, offsetX: this.offsetX, offsetY: this.offsetY }; }, /* _TO_SVG_START_ */ /** * Returns SVG representation of a pattern * @param {fabric.Object} object * @return {String} SVG representation of a pattern */ toSVG: function(object) { var patternSource = typeof this.source === 'function' ? this.source() : this.source, patternWidth = patternSource.width / object.getWidth(), patternHeight = patternSource.height / object.getHeight(), patternOffsetX = this.offsetX / object.getWidth(), patternOffsetY = this.offsetY / object.getHeight(), patternImgSrc = ''; if (this.repeat === 'repeat-x' || this.repeat === 'no-repeat') { patternHeight = 1; } if (this.repeat === 'repeat-y' || this.repeat === 'no-repeat') { patternWidth = 1; } if (patternSource.src) { patternImgSrc = patternSource.src; } else if (patternSource.toDataURL) { patternImgSrc = patternSource.toDataURL(); } return '\n' + '\n' + '\n'; }, /* _TO_SVG_END_ */ /** * Returns an instance of CanvasPattern * @param {CanvasRenderingContext2D} ctx Context to create pattern * @return {CanvasPattern} */ toLive: function(ctx) { var source = typeof this.source === 'function' ? this.source() : this.source; // if the image failed to load, return, and allow rest to continue loading if (!source) { return ''; } // if an image if (typeof source.src !== 'undefined') { if (!source.complete) { return ''; } if (source.naturalWidth === 0 || source.naturalHeight === 0) { return ''; } } return ctx.createPattern(source, this.repeat); } }); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); if (fabric.Shadow) { fabric.warn('fabric.Shadow is already defined.'); return; } /** * Shadow class * @class fabric.Shadow * @see {@link http://fabricjs.com/shadows/|Shadow demo} * @see {@link fabric.Shadow#initialize} for constructor definition */ fabric.Shadow = fabric.util.createClass(/** @lends fabric.Shadow.prototype */ { /** * Shadow color * @type String * @default */ color: 'rgb(0,0,0)', /** * Shadow blur * @type Number */ blur: 0, /** * Shadow horizontal offset * @type Number * @default */ offsetX: 0, /** * Shadow vertical offset * @type Number * @default */ offsetY: 0, /** * Whether the shadow should affect stroke operations * @type Boolean * @default */ affectStroke: false, /** * Indicates whether toObject should include default values * @type Boolean * @default */ includeDefaultValues: true, /** * Constructor * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetX properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px, "2px 2px 10px rgba(0,0,0,0.2)") * @return {fabric.Shadow} thisArg */ initialize: function(options) { if (typeof options === 'string') { options = this._parseShadow(options); } for (var prop in options) { this[prop] = options[prop]; } this.id = fabric.Object.__uid++; }, /** * @private * @param {String} shadow Shadow value to parse * @return {Object} Shadow object with color, offsetX, offsetY and blur */ _parseShadow: function(shadow) { var shadowStr = shadow.trim(), offsetsAndBlur = fabric.Shadow.reOffsetsAndBlur.exec(shadowStr) || [ ], color = shadowStr.replace(fabric.Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)'; return { color: color.trim(), offsetX: parseInt(offsetsAndBlur[1], 10) || 0, offsetY: parseInt(offsetsAndBlur[2], 10) || 0, blur: parseInt(offsetsAndBlur[3], 10) || 0 }; }, /** * Returns a string representation of an instance * @see http://www.w3.org/TR/css-text-decor-3/#text-shadow * @return {String} Returns CSS3 text-shadow declaration */ toString: function() { return [this.offsetX, this.offsetY, this.blur, this.color].join('px '); }, /* _TO_SVG_START_ */ /** * Returns SVG representation of a shadow * @param {fabric.Object} object * @return {String} SVG representation of a shadow */ toSVG: function(object) { var mode = 'SourceAlpha', fBoxX = 40, fBoxY = 40; if (object && (object.fill === this.color || object.stroke === this.color)) { mode = 'SourceGraphic'; } if (object.width && object.height) { //http://www.w3.org/TR/SVG/filters.html#FilterEffectsRegion // we add some extra space to filter box to contain the blur ( 20 ) fBoxX = Math.abs(this.offsetX / object.getWidth()) * 100 + 20; fBoxY = Math.abs(this.offsetY / object.getHeight()) * 100 + 20; } return ( '\n' + '\t\n' + '\t\n' + '\t\n' + '\t\t\n' + '\t\t\n' + '\t\n' + '\n'); }, /* _TO_SVG_END_ */ /** * Returns object representation of a shadow * @return {Object} Object representation of a shadow instance */ toObject: function() { if (this.includeDefaultValues) { return { color: this.color, blur: this.blur, offsetX: this.offsetX, offsetY: this.offsetY }; } var obj = { }, proto = fabric.Shadow.prototype; if (this.color !== proto.color) { obj.color = this.color; } if (this.blur !== proto.blur) { obj.blur = this.blur; } if (this.offsetX !== proto.offsetX) { obj.offsetX = this.offsetX; } if (this.offsetY !== proto.offsetY) { obj.offsetY = this.offsetY; } return obj; } }); /** * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") * @static * @field * @memberOf fabric.Shadow */ fabric.Shadow.reOffsetsAndBlur = /(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/; })(typeof exports !== 'undefined' ? exports : this); (function () { 'use strict'; if (fabric.StaticCanvas) { fabric.warn('fabric.StaticCanvas is already defined.'); return; } // aliases for faster resolution var extend = fabric.util.object.extend, getElementOffset = fabric.util.getElementOffset, removeFromArray = fabric.util.removeFromArray, CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); /** * Static canvas class * @class fabric.StaticCanvas * @mixes fabric.Collection * @mixes fabric.Observable * @see {@link http://fabricjs.com/static_canvas/|StaticCanvas demo} * @see {@link fabric.StaticCanvas#initialize} for constructor definition * @fires before:render * @fires after:render * @fires canvas:cleared * @fires object:added * @fires object:removed */ fabric.StaticCanvas = fabric.util.createClass(/** @lends fabric.StaticCanvas.prototype */ { /** * Constructor * @param {HTMLElement | String} el <canvas> element to initialize instance on * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(el, options) { options || (options = { }); this._initStatic(el, options); fabric.StaticCanvas.activeInstance = this; }, /** * Background color of canvas instance. * Should be set via {@link fabric.StaticCanvas#setBackgroundColor}. * @type {(String|fabric.Pattern)} * @default */ backgroundColor: '', /** * Background image of canvas instance. * Should be set via {@link fabric.StaticCanvas#setBackgroundImage}. * Backwards incompatibility note: The "backgroundImageOpacity" * and "backgroundImageStretch" properties are deprecated since 1.3.9. * Use {@link fabric.Image#opacity}, {@link fabric.Image#width} and {@link fabric.Image#height}. * @type fabric.Image * @default */ backgroundImage: null, /** * Overlay color of canvas instance. * Should be set via {@link fabric.StaticCanvas#setOverlayColor} * @since 1.3.9 * @type {(String|fabric.Pattern)} * @default */ overlayColor: '', /** * Overlay image of canvas instance. * Should be set via {@link fabric.StaticCanvas#setOverlayImage}. * Backwards incompatibility note: The "overlayImageLeft" * and "overlayImageTop" properties are deprecated since 1.3.9. * Use {@link fabric.Image#left} and {@link fabric.Image#top}. * @type fabric.Image * @default */ overlayImage: null, /** * Indicates whether toObject/toDatalessObject should include default values * @type Boolean * @default */ includeDefaultValues: true, /** * Indicates whether objects' state should be saved * @type Boolean * @default */ stateful: true, /** * Indicates whether {@link fabric.Collection.add}, {@link fabric.Collection.insertAt} and {@link fabric.Collection.remove} should also re-render canvas. * Disabling this option could give a great performance boost when adding/removing a lot of objects to/from canvas at once * (followed by a manual rendering after addition/deletion) * @type Boolean * @default */ renderOnAddRemove: true, /** * Function that determines clipping of entire canvas area * Being passed context as first argument. See clipping canvas area in {@link https://github.com/kangax/fabric.js/wiki/FAQ} * @type Function * @default */ clipTo: null, /** * Indicates whether object controls (borders/controls) are rendered above overlay image * @type Boolean * @default */ controlsAboveOverlay: false, /** * Indicates whether the browser can be scrolled when using a touchscreen and dragging on the canvas * @type Boolean * @default */ allowTouchScrolling: false, /** * Indicates whether this canvas will use image smoothing, this is on by default in browsers * @type Boolean * @default */ imageSmoothingEnabled: true, /** * Indicates whether objects should remain in current stack position when selected. When false objects are brought to top and rendered as part of the selection group * @type Boolean * @default */ preserveObjectStacking: false, /** * The transformation (in the format of Canvas transform) which focuses the viewport * @type Array * @default */ viewportTransform: [1, 0, 0, 1, 0, 0], /** * Callback; invoked right before object is about to be scaled/rotated */ onBeforeScaleRotate: function () { /* NOOP */ }, /** * @private * @param {HTMLElement | String} el <canvas> element to initialize instance on * @param {Object} [options] Options object */ _initStatic: function(el, options) { this._objects = []; this._createLowerCanvas(el); this._initOptions(options); this._setImageSmoothing(); if (options.overlayImage) { this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); } if (options.backgroundImage) { this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); } if (options.backgroundColor) { this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); } if (options.overlayColor) { this.setOverlayColor(options.overlayColor, 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 * @return {fabric.Canvas} instance * @chainable */ calcOffset: function () { this._offset = getElementOffset(this.lowerCanvasEl); return this; }, /** * Sets {@link fabric.StaticCanvas#overlayImage|overlay image} for this canvas * @param {(fabric.Image|String)} image fabric.Image instance or URL of an image to set overlay to * @param {Function} callback callback to invoke when image is loaded and set as an overlay * @param {Object} [options] Optional options to set for the {@link fabric.Image|overlay image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/MnzHT/|jsFiddle demo} * @example Normal overlayImage with left/top = 0 * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { * // Needed to position overlayImage at 0/0 * originX: 'left', * originY: 'top' * }); * @example overlayImage with different properties * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { * opacity: 0.5, * angle: 45, * left: 400, * top: 400, * originX: 'left', * originY: 'top' * }); * @example Stretched overlayImage #1 - width/height correspond to canvas width/height * fabric.Image.fromURL('http://fabricjs.com/assets/jail_cell_bars.png', function(img) { * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); * canvas.setOverlayImage(img, canvas.renderAll.bind(canvas)); * }); * @example Stretched overlayImage #2 - width/height correspond to canvas width/height * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { * width: canvas.width, * height: canvas.height, * // Needed to position overlayImage at 0/0 * originX: 'left', * originY: 'top' * }); * @example overlayImage loaded from cross-origin * canvas.setOverlayImage('http://fabricjs.com/assets/jail_cell_bars.png', canvas.renderAll.bind(canvas), { * opacity: 0.5, * angle: 45, * left: 400, * top: 400, * originX: 'left', * originY: 'top', * crossOrigin: 'anonymous' * }); */ setOverlayImage: function (image, callback, options) { return this.__setBgOverlayImage('overlayImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#backgroundImage|background image} for this canvas * @param {(fabric.Image|String)} image fabric.Image instance or 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 {@link fabric.Image|background image}. * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/YH9yD/|jsFiddle demo} * @example Normal backgroundImage with left/top = 0 * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { * // Needed to position backgroundImage at 0/0 * originX: 'left', * originY: 'top' * }); * @example backgroundImage with different properties * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { * opacity: 0.5, * angle: 45, * left: 400, * top: 400, * originX: 'left', * originY: 'top' * }); * @example Stretched backgroundImage #1 - width/height correspond to canvas width/height * fabric.Image.fromURL('http://fabricjs.com/assets/honey_im_subtle.png', function(img) { * img.set({width: canvas.width, height: canvas.height, originX: 'left', originY: 'top'}); * canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas)); * }); * @example Stretched backgroundImage #2 - width/height correspond to canvas width/height * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { * width: canvas.width, * height: canvas.height, * // Needed to position backgroundImage at 0/0 * originX: 'left', * originY: 'top' * }); * @example backgroundImage loaded from cross-origin * canvas.setBackgroundImage('http://fabricjs.com/assets/honey_im_subtle.png', canvas.renderAll.bind(canvas), { * opacity: 0.5, * angle: 45, * left: 400, * top: 400, * originX: 'left', * originY: 'top', * crossOrigin: 'anonymous' * }); */ setBackgroundImage: function (image, callback, options) { return this.__setBgOverlayImage('backgroundImage', image, callback, options); }, /** * Sets {@link fabric.StaticCanvas#overlayColor|background color} for this canvas * @param {(String|fabric.Pattern)} overlayColor Color or pattern to set background color to * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/pB55h/|jsFiddle demo} * @example Normal overlayColor - color value * canvas.setOverlayColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); * @example fabric.Pattern used as overlayColor * canvas.setOverlayColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); * @example fabric.Pattern used as overlayColor with repeat and offset * canvas.setOverlayColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png', * repeat: 'repeat', * offsetX: 200, * offsetY: 100 * }, canvas.renderAll.bind(canvas)); */ setOverlayColor: function(overlayColor, callback) { return this.__setBgOverlayColor('overlayColor', overlayColor, callback); }, /** * Sets {@link fabric.StaticCanvas#backgroundColor|background color} for this canvas * @param {(String|fabric.Pattern)} backgroundColor Color or pattern to set background color to * @param {Function} callback Callback to invoke when background color is set * @return {fabric.Canvas} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/hXzvk/|jsFiddle demo} * @example Normal backgroundColor - color value * canvas.setBackgroundColor('rgba(255, 73, 64, 0.6)', canvas.renderAll.bind(canvas)); * @example fabric.Pattern used as backgroundColor * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png' * }, canvas.renderAll.bind(canvas)); * @example fabric.Pattern used as backgroundColor with repeat and offset * canvas.setBackgroundColor({ * source: 'http://fabricjs.com/assets/escheresque_ste.png', * repeat: 'repeat', * offsetX: 200, * offsetY: 100 * }, canvas.renderAll.bind(canvas)); */ setBackgroundColor: function(backgroundColor, callback) { return this.__setBgOverlayColor('backgroundColor', backgroundColor, callback); }, /** * @private * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-imagesmoothingenabled|WhatWG Canvas Standard} */ _setImageSmoothing: function() { var ctx = this.getContext(); ctx.imageSmoothingEnabled = this.imageSmoothingEnabled; ctx.webkitImageSmoothingEnabled = this.imageSmoothingEnabled; ctx.mozImageSmoothingEnabled = this.imageSmoothingEnabled; ctx.msImageSmoothingEnabled = this.imageSmoothingEnabled; ctx.oImageSmoothingEnabled = this.imageSmoothingEnabled; }, /** * @private * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundImage|backgroundImage} * or {@link fabric.StaticCanvas#overlayImage|overlayImage}) * @param {(fabric.Image|String|null)} image fabric.Image instance, URL of an image or null to set background or overlay to * @param {Function} callback Callback to invoke when image is loaded and set as background or overlay * @param {Object} [options] Optional options to set for the {@link fabric.Image|image}. */ __setBgOverlayImage: function(property, image, callback, options) { if (typeof image === 'string') { fabric.util.loadImage(image, function(img) { this[property] = new fabric.Image(img, options); callback && callback(); }, this, options && options.crossOrigin); } else { this[property] = image; callback && callback(); } return this; }, /** * @private * @param {String} property Property to set ({@link fabric.StaticCanvas#backgroundColor|backgroundColor} * or {@link fabric.StaticCanvas#overlayColor|overlayColor}) * @param {(Object|String|null)} color Object with pattern information, color value or null * @param {Function} [callback] Callback is invoked when color is set */ __setBgOverlayColor: function(property, color, callback) { if (color && color.source) { var _this = this; fabric.util.loadImage(color.source, function(img) { _this[property] = new fabric.Pattern({ source: img, repeat: color.repeat, offsetX: color.offsetX, offsetY: color.offsetY }); callback && callback(); }); } else { this[property] = color; callback && callback(); } return this; }, /** * @private */ _createCanvasElement: function() { var element = fabric.document.createElement('canvas'); if (!element.style) { element.style = { }; } if (!element) { throw CANVAS_INIT_ERROR; } this._initCanvasElement(element); return element; }, /** * @private * @param {HTMLElement} element */ _initCanvasElement: function(element) { fabric.util.createCanvasElement(element); if (typeof element.getContext === 'undefined') { throw CANVAS_INIT_ERROR; } }, /** * @private * @param {Object} [options] Options object */ _initOptions: function (options) { for (var prop in options) { this[prop] = options[prop]; } this.width = this.width || parseInt(this.lowerCanvasEl.width, 10) || 0; this.height = this.height || parseInt(this.lowerCanvasEl.height, 10) || 0; if (!this.lowerCanvasEl.style) { return; } this.lowerCanvasEl.width = this.width; this.lowerCanvasEl.height = this.height; this.lowerCanvasEl.style.width = this.width + 'px'; this.lowerCanvasEl.style.height = this.height + 'px'; this.viewportTransform = this.viewportTransform.slice(); }, /** * Creates a bottom canvas * @private * @param {HTMLElement} [canvasEl] */ _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 (in px) * @return {Number} */ getWidth: function () { return this.width; }, /** * Returns canvas height (in px) * @return {Number} */ getHeight: function () { return this.height; }, /** * Sets width of this canvas instance * @param {Number|String} value Value to set width to * @param {Object} [options] Options object * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions * @return {fabric.Canvas} instance * @chainable true */ setWidth: function (value, options) { return this.setDimensions({ width: value }, options); }, /** * Sets height of this canvas instance * @param {Number|String} value Value to set height to * @param {Object} [options] Options object * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions * @return {fabric.Canvas} instance * @chainable true */ setHeight: function (value, options) { return this.setDimensions({ height: value }, options); }, /** * Sets dimensions (width, height) of this canvas instance. when options.cssOnly flag active you should also supply the unit of measure (px/%/em) * @param {Object} dimensions Object with width/height properties * @param {Number|String} [dimensions.width] Width of canvas element * @param {Number|String} [dimensions.height] Height of canvas element * @param {Object} [options] Options object * @param {Boolean} [options.backstoreOnly=false] Set the given dimensions only as canvas backstore dimensions * @param {Boolean} [options.cssOnly=false] Set the given dimensions only as css dimensions * @return {fabric.Canvas} thisArg * @chainable */ setDimensions: function (dimensions, options) { var cssValue; options = options || {}; for (var prop in dimensions) { cssValue = dimensions[prop]; if (!options.cssOnly) { this._setBackstoreDimension(prop, dimensions[prop]); cssValue += 'px'; } if (!options.backstoreOnly) { this._setCssDimension(prop, cssValue); } } if (!options.cssOnly) { this.renderAll(); } this.calcOffset(); return this; }, /** * Helper for setting width/height * @private * @param {String} prop property (width|height) * @param {Number} value value to set property to * @return {fabric.Canvas} instance * @chainable true */ _setBackstoreDimension: function (prop, value) { this.lowerCanvasEl[prop] = value; if (this.upperCanvasEl) { this.upperCanvasEl[prop] = value; } if (this.cacheCanvasEl) { this.cacheCanvasEl[prop] = value; } this[prop] = value; return this; }, /** * Helper for setting css width/height * @private * @param {String} prop property (width|height) * @param {String} value value to set property to * @return {fabric.Canvas} instance * @chainable true */ _setCssDimension: function (prop, value) { this.lowerCanvasEl.style[prop] = value; if (this.upperCanvasEl) { this.upperCanvasEl.style[prop] = value; } if (this.wrapperEl) { this.wrapperEl.style[prop] = value; } return this; }, /** * Returns canvas zoom level * @return {Number} */ getZoom: function () { return Math.sqrt(this.viewportTransform[0] * this.viewportTransform[3]); }, /** * Sets viewport transform of this canvas instance * @param {Array} vpt the transform in the form of context.transform * @return {fabric.Canvas} instance * @chainable true */ setViewportTransform: function (vpt) { this.viewportTransform = vpt; this.renderAll(); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].setCoords(); } return this; }, /** * Sets zoom level of this canvas instance, zoom centered around point * @param {fabric.Point} point to zoom with respect to * @param {Number} value to set zoom to, less than 1 zooms out * @return {fabric.Canvas} instance * @chainable true */ zoomToPoint: function (point, value) { // TODO: just change the scale, preserve other transformations var before = point; point = fabric.util.transformPoint(point, fabric.util.invertTransform(this.viewportTransform)); this.viewportTransform[0] = value; this.viewportTransform[3] = value; var after = fabric.util.transformPoint(point, this.viewportTransform); this.viewportTransform[4] += before.x - after.x; this.viewportTransform[5] += before.y - after.y; this.renderAll(); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].setCoords(); } return this; }, /** * Sets zoom level of this canvas instance * @param {Number} value to set zoom to, less than 1 zooms out * @return {fabric.Canvas} instance * @chainable true */ setZoom: function (value) { this.zoomToPoint(new fabric.Point(0, 0), value); return this; }, /** * Pan viewport so as to place point at top left corner of canvas * @param {fabric.Point} point to move to * @return {fabric.Canvas} instance * @chainable true */ absolutePan: function (point) { this.viewportTransform[4] = -point.x; this.viewportTransform[5] = -point.y; this.renderAll(); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].setCoords(); } return this; }, /** * Pans viewpoint relatively * @param {fabric.Point} point (position vector) to move by * @return {fabric.Canvas} instance * @chainable true */ relativePan: function (point) { return this.absolutePan(new fabric.Point( -point.x - this.viewportTransform[4], -point.y - this.viewportTransform[5] )); }, /** * Returns <canvas> element corresponding to this instance * @return {HTMLCanvasElement} */ getElement: function () { return this.lowerCanvasEl; }, /** * Returns currently selected object, if any * @return {fabric.Object} */ getActiveObject: function() { return null; }, /** * Returns currently selected group of object, if any * @return {fabric.Group} */ getActiveGroup: function() { return null; }, /** * Given a context, renders an object on that context * @param {CanvasRenderingContext2D} ctx Context to render object on * @param {fabric.Object} object Object to render * @private */ _draw: function (ctx, object) { if (!object) { return; } ctx.save(); var v = this.viewportTransform; ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); if (this._shouldRenderObject(object)) { object.render(ctx); } ctx.restore(); if (!this.controlsAboveOverlay) { object._renderControls(ctx); } }, _shouldRenderObject: function(object) { if (!object) { return false; } return (object !== this.getActiveGroup() || !this.preserveObjectStacking); }, /** * @private * @param {fabric.Object} obj Object that was added */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); obj.canvas = this; obj.setCoords(); this.fire('object:added', { target: obj }); obj.fire('added'); }, /** * @private * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { // removing active object should fire "selection:cleared" events if (this.getActiveObject() === obj) { this.fire('before:selection:cleared', { target: obj }); this._discardActiveObject(); this.fire('selection:cleared'); } this.fire('object:removed', { target: obj }); obj.fire('removed'); }, /** * Clears specified context of canvas element * @param {CanvasRenderingContext2D} 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 * @return {CanvasRenderingContext2D} */ getContext: function () { return this.contextContainer; }, /** * Clears all contexts (background, main, top) of an instance * @return {fabric.Canvas} thisArg * @chainable */ clear: function () { this._objects.length = 0; if (this.discardActiveGroup) { this.discardActiveGroup(); } if (this.discardActiveObject) { this.discardActiveObject(); } this.clearContext(this.contextContainer); if (this.contextTop) { this.clearContext(this.contextTop); } this.fire('canvas:cleared'); this.renderAll(); return this; }, /** * Renders both the top canvas and the secondary container canvas. * @param {Boolean} [allOnTop] 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'], activeGroup = this.getActiveGroup(); if (this.contextTop && this.selection && !this._groupSelector) { this.clearContext(this.contextTop); } if (!allOnTop) { this.clearContext(canvasToDrawOn); } this.fire('before:render'); if (this.clipTo) { fabric.util.clipContext(this, canvasToDrawOn); } this._renderBackground(canvasToDrawOn); this._renderObjects(canvasToDrawOn, activeGroup); this._renderActiveGroup(canvasToDrawOn, activeGroup); if (this.clipTo) { canvasToDrawOn.restore(); } this._renderOverlay(canvasToDrawOn); if (this.controlsAboveOverlay && this.interactive) { this.drawControls(canvasToDrawOn); } this.fire('after:render'); return this; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {fabric.Group} activeGroup */ _renderObjects: function(ctx, activeGroup) { var i, length; // fast path if (!activeGroup || this.preserveObjectStacking) { for (i = 0, length = this._objects.length; i < length; ++i) { this._draw(ctx, this._objects[i]); } } else { for (i = 0, length = this._objects.length; i < length; ++i) { if (this._objects[i] && !activeGroup.contains(this._objects[i])) { this._draw(ctx, this._objects[i]); } } } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {fabric.Group} activeGroup */ _renderActiveGroup: function(ctx, activeGroup) { // delegate rendering to group selection (if one exists) if (activeGroup) { //Store objects in group preserving order, then replace var sortedObjects = []; this.forEachObject(function (object) { if (activeGroup.contains(object)) { sortedObjects.push(object); } }); activeGroup._set('objects', sortedObjects); this._draw(ctx, activeGroup); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderBackground: function(ctx) { if (this.backgroundColor) { ctx.fillStyle = this.backgroundColor.toLive ? this.backgroundColor.toLive(ctx) : this.backgroundColor; ctx.fillRect( this.backgroundColor.offsetX || 0, this.backgroundColor.offsetY || 0, this.width, this.height); } if (this.backgroundImage) { this._draw(ctx, this.backgroundImage); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderOverlay: function(ctx) { if (this.overlayColor) { ctx.fillStyle = this.overlayColor.toLive ? this.overlayColor.toLive(ctx) : this.overlayColor; ctx.fillRect( this.overlayColor.offsetX || 0, this.overlayColor.offsetY || 0, this.width, this.height); } if (this.overlayImage) { this._draw(ctx, this.overlayImage); } }, /** * Method to render only the top canvas. * Also used to render the group selection box. * @return {fabric.Canvas} thisArg * @chainable */ renderTop: function () { var ctx = this.contextTop || this.contextContainer; this.clearContext(ctx); // 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/controls var activeGroup = this.getActiveGroup(); if (activeGroup) { activeGroup.render(ctx); } this._renderOverlay(ctx); this.fire('after:render'); return this; }, /** * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties * @return {Object} object with "top" and "left" number values */ getCenter: function () { return { top: this.getHeight() / 2, left: this.getWidth() / 2 }; }, /** * Centers object horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. * @param {fabric.Object} object Object to center horizontally * @return {fabric.Canvas} thisArg */ centerObjectH: function (object) { this._centerObject(object, new fabric.Point(this.getCenter().left, object.getCenterPoint().y)); this.renderAll(); return this; }, /** * Centers object vertically. * You might need to call `setCoords` on an object after centering, to update controls area. * @param {fabric.Object} object Object to center vertically * @return {fabric.Canvas} thisArg * @chainable */ centerObjectV: function (object) { this._centerObject(object, new fabric.Point(object.getCenterPoint().x, this.getCenter().top)); this.renderAll(); return this; }, /** * Centers object vertically and horizontally. * You might need to call `setCoords` on an object after centering, to update controls area. * @param {fabric.Object} object Object to center vertically and horizontally * @return {fabric.Canvas} thisArg * @chainable */ centerObject: function(object) { var center = this.getCenter(); this._centerObject(object, new fabric.Point(center.left, center.top)); this.renderAll(); return this; }, /** * @private * @param {fabric.Object} object Object to center * @param {fabric.Point} center Center point * @return {fabric.Canvas} thisArg * @chainable */ _centerObject: function(object, center) { object.setPositionByOrigin(center, 'center', 'center'); return this; }, /** * Returs dataless JSON representation of canvas * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {String} json string */ toDatalessJSON: function (propertiesToInclude) { return this.toDatalessObject(propertiesToInclude); }, /** * Returns object representation of canvas * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function (propertiesToInclude) { return this._toObjectMethod('toObject', propertiesToInclude); }, /** * Returns dataless object representation of canvas * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toDatalessObject: function (propertiesToInclude) { return this._toObjectMethod('toDatalessObject', propertiesToInclude); }, /** * @private */ _toObjectMethod: function (methodName, propertiesToInclude) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this.discardActiveGroup(); } var data = { objects: this._toObjects(methodName, propertiesToInclude) }; extend(data, this.__serializeBgOverlay()); fabric.util.populateWithProperties(this, data, propertiesToInclude); if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects(), { originX: 'center', originY: 'center' })); activeGroup.forEachObject(function(o) { o.set('active', true); }); if (this._currentTransform) { this._currentTransform.target = this.getActiveGroup(); } } return data; }, /** * @private */ _toObjects: function(methodName, propertiesToInclude) { return this.getObjects().map(function(instance) { return this._toObject(instance, methodName, propertiesToInclude); }, this); }, /** * @private */ _toObject: function(instance, methodName, propertiesToInclude) { var originalValue; if (!this.includeDefaultValues) { originalValue = instance.includeDefaultValues; instance.includeDefaultValues = false; } var object = instance[methodName](propertiesToInclude); if (!this.includeDefaultValues) { instance.includeDefaultValues = originalValue; } return object; }, /** * @private */ __serializeBgOverlay: function() { var data = { background: (this.backgroundColor && this.backgroundColor.toObject) ? this.backgroundColor.toObject() : this.backgroundColor }; if (this.overlayColor) { data.overlay = this.overlayColor.toObject ? this.overlayColor.toObject() : this.overlayColor; } if (this.backgroundImage) { data.backgroundImage = this.backgroundImage.toObject(); } if (this.overlayImage) { data.overlayImage = this.overlayImage.toObject(); } return data; }, /* _TO_SVG_START_ */ /** * When true, getSvgTransform() will apply the StaticCanvas.viewportTransform to the SVG transformation. When true, * a zoomed canvas will then produce zoomed SVG output. * @type Boolean * @default */ svgViewportTransformation: true, /** * Returns SVG representation of canvas * @function * @param {Object} [options] Options object for SVG output * @param {Boolean} [options.suppressPreamble=false] If true xml tag is not included * @param {Object} [options.viewBox] SVG viewbox object * @param {Number} [options.viewBox.x] x-cooridnate of viewbox * @param {Number} [options.viewBox.y] y-coordinate of viewbox * @param {Number} [options.viewBox.width] Width of viewbox * @param {Number} [options.viewBox.height] Height of viewbox * @param {String} [options.encoding=UTF-8] Encoding of SVG output * @param {Function} [reviver] Method for further parsing of svg elements, called after each fabric object converted into svg representation. * @return {String} SVG string * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#serialization} * @see {@link http://jsfiddle.net/fabricjs/jQ3ZZ/|jsFiddle demo} * @example Normal SVG output * var svg = canvas.toSVG(); * @example SVG output without preamble (without <?xml ../>) * var svg = canvas.toSVG({suppressPreamble: true}); * @example SVG output with viewBox attribute * var svg = canvas.toSVG({ * viewBox: { * x: 100, * y: 100, * width: 200, * height: 300 * } * }); * @example SVG output with different encoding (default: UTF-8) * var svg = canvas.toSVG({encoding: 'ISO-8859-1'}); * @example Modify SVG output with reviver function * var svg = canvas.toSVG(null, function(svg) { * return svg.replace('stroke-dasharray: ; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; ', ''); * }); */ toSVG: function(options, reviver) { options || (options = { }); var markup = []; this._setSVGPreamble(markup, options); this._setSVGHeader(markup, options); this._setSVGBgOverlayColor(markup, 'backgroundColor'); this._setSVGBgOverlayImage(markup, 'backgroundImage'); this._setSVGObjects(markup, reviver); this._setSVGBgOverlayColor(markup, 'overlayColor'); this._setSVGBgOverlayImage(markup, 'overlayImage'); markup.push(''); return markup.join(''); }, /** * @private */ _setSVGPreamble: function(markup, options) { if (!options.suppressPreamble) { markup.push( '', '\n' ); } }, /** * @private */ _setSVGHeader: function(markup, options) { var width, height, vpt; if (options.viewBox) { width = options.viewBox.width; height = options.viewBox.height; } else { width = this.width; height = this.height; if (!this.svgViewportTransformation) { vpt = this.viewportTransform; width /= vpt[0]; height /= vpt[3]; } } markup.push( '', 'Created with Fabric.js ', fabric.version, '', '', fabric.createSVGFontFacesMarkup(this.getObjects()), fabric.createSVGRefElementsMarkup(this), '' ); }, /** * @private */ _setSVGObjects: function(markup, reviver) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this.discardActiveGroup(); } for (var i = 0, objects = this.getObjects(), len = objects.length; i < len; i++) { markup.push(objects[i].toSVG(reviver)); } if (activeGroup) { this.setActiveGroup(new fabric.Group(activeGroup.getObjects())); activeGroup.forEachObject(function(o) { o.set('active', true); }); } }, /** * @private */ _setSVGBgOverlayImage: function(markup, property) { if (this[property] && this[property].toSVG) { markup.push(this[property].toSVG()); } }, /** * @private */ _setSVGBgOverlayColor: function(markup, property) { if (this[property] && this[property].source) { markup.push( '' ); } else if (this[property] && property === 'overlayColor') { markup.push( '' ); } }, /* _TO_SVG_END_ */ /** * Moves an object to the bottom of the stack of drawn objects * @param {fabric.Object} 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 && this.renderAll(); }, /** * Moves an object to the top of the stack of drawn objects * @param {fabric.Object} object Object to send * @return {fabric.Canvas} thisArg * @chainable */ bringToFront: function (object) { removeFromArray(this._objects, object); this._objects.push(object); return this.renderAll && this.renderAll(); }, /** * Moves an object down in stack of drawn objects * @param {fabric.Object} object Object to send * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object * @return {fabric.Canvas} thisArg * @chainable */ sendBackwards: function (object, intersecting) { var idx = this._objects.indexOf(object); // if object is not on the bottom of stack if (idx !== 0) { var newIdx = this._findNewLowerIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); this.renderAll && this.renderAll(); } return this; }, /** * @private */ _findNewLowerIndex: function(object, idx, intersecting) { var newIdx; if (intersecting) { newIdx = idx; // traverse down the stack looking for the nearest intersecting object for (var i = idx - 1; i >= 0; --i) { var isIntersecting = object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i]) || this._objects[i].isContainedWithinObject(object); if (isIntersecting) { newIdx = i; break; } } } else { newIdx = idx - 1; } return newIdx; }, /** * Moves an object up in stack of drawn objects * @param {fabric.Object} object Object to send * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object * @return {fabric.Canvas} thisArg * @chainable */ bringForward: function (object, intersecting) { var idx = this._objects.indexOf(object); // if object is not on top of stack (last item in an array) if (idx !== this._objects.length - 1) { var newIdx = this._findNewUpperIndex(object, idx, intersecting); removeFromArray(this._objects, object); this._objects.splice(newIdx, 0, object); this.renderAll && this.renderAll(); } return this; }, /** * @private */ _findNewUpperIndex: function(object, idx, intersecting) { var newIdx; if (intersecting) { newIdx = idx; // traverse up the stack looking for the nearest intersecting object for (var i = idx + 1; i < this._objects.length; ++i) { var isIntersecting = object.intersectsWithObject(this._objects[i]) || object.isContainedWithinObject(this._objects[i]) || this._objects[i].isContainedWithinObject(object); if (isIntersecting) { newIdx = i; break; } } } else { newIdx = idx + 1; } return newIdx; }, /** * Moves an object to specified level in stack of drawn objects * @param {fabric.Object} object Object to send * @param {Number} index Position to move to * @return {fabric.Canvas} thisArg * @chainable */ moveTo: function (object, index) { removeFromArray(this._objects, object); this._objects.splice(index, 0, object); return this.renderAll && this.renderAll(); }, /** * Clears a canvas element and removes all event listeners * @return {fabric.Canvas} thisArg * @chainable */ dispose: function () { this.clear(); this.interactive && this.removeListeners(); return this; }, /** * Returns a string representation of an instance * @return {String} string representation of an instance */ toString: function () { return '#'; } }); extend(fabric.StaticCanvas.prototype, fabric.Observable); extend(fabric.StaticCanvas.prototype, fabric.Collection); extend(fabric.StaticCanvas.prototype, fabric.DataURLExporter); extend(fabric.StaticCanvas, /** @lends fabric.StaticCanvas */ { /** * @static * @type String * @default */ EMPTY_JSON: '{"objects": [], "background": "white"}', /** * Provides a way to check support of some of the canvas methods * (either those of HTMLCanvasElement itself, or rendering context) * * @param {String} methodName Method to check support for; * Could be one of "getImageData", "toDataURL", "toDataURLWithQuality" or "setLineDash" * @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.util.createCanvasElement(); 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 'setLineDash': return typeof ctx.setLineDash !== '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; } } }); /** * Returns JSON representation of canvas * @function * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {String} JSON string * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#serialization} * @see {@link http://jsfiddle.net/fabricjs/pec86/|jsFiddle demo} * @example JSON without additional properties * var json = canvas.toJSON(); * @example JSON with additional properties included * var json = canvas.toJSON(['lockMovementX', 'lockMovementY', 'lockRotation', 'lockScalingX', 'lockScalingY', 'lockUniScaling']); * @example JSON without default values * canvas.includeDefaultValues = false; * var json = canvas.toJSON(); */ fabric.StaticCanvas.prototype.toJSON = fabric.StaticCanvas.prototype.toObject; })(); /** * BaseBrush class * @class fabric.BaseBrush * @see {@link http://fabricjs.com/freedrawing/|Freedrawing demo} */ fabric.BaseBrush = fabric.util.createClass(/** @lends fabric.BaseBrush.prototype */ { /** * Color of a brush * @type String * @default */ color: 'rgb(0, 0, 0)', /** * Width of a brush * @type Number * @default */ width: 1, /** * Shadow object representing shadow of this shape. * Backwards incompatibility note: This property replaces "shadowColor" (String), "shadowOffsetX" (Number), * "shadowOffsetY" (Number) and "shadowBlur" (Number) since v1.2.12 * @type fabric.Shadow * @default */ shadow: null, /** * Line endings style of a brush (one of "butt", "round", "square") * @type String * @default */ strokeLineCap: 'round', /** * Corner style of a brush (one of "bevil", "round", "miter") * @type String * @default */ strokeLineJoin: 'round', /** * Sets shadow of an object * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") * @return {fabric.Object} thisArg * @chainable */ setShadow: function(options) { this.shadow = new fabric.Shadow(options); return this; }, /** * Sets brush styles * @private */ _setBrushStyles: function() { var ctx = this.canvas.contextTop; ctx.strokeStyle = this.color; ctx.lineWidth = this.width; ctx.lineCap = this.strokeLineCap; ctx.lineJoin = this.strokeLineJoin; }, /** * Sets brush shadow styles * @private */ _setShadow: function() { if (!this.shadow) { return; } var ctx = this.canvas.contextTop; ctx.shadowColor = this.shadow.color; ctx.shadowBlur = this.shadow.blur; ctx.shadowOffsetX = this.shadow.offsetX; ctx.shadowOffsetY = this.shadow.offsetY; }, /** * Removes brush shadow styles * @private */ _resetShadow: function() { var ctx = this.canvas.contextTop; ctx.shadowColor = ''; ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; } }); (function() { /** * PencilBrush class * @class fabric.PencilBrush * @extends fabric.BaseBrush */ fabric.PencilBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.PencilBrush.prototype */ { /** * Constructor * @param {fabric.Canvas} canvas * @return {fabric.PencilBrush} Instance of a pencil brush */ initialize: function(canvas) { this.canvas = canvas; this._points = [ ]; }, /** * Inovoked on mouse down * @param {Object} pointer */ onMouseDown: function(pointer) { this._prepareForDrawing(pointer); // capture coordinates immediately // this allows to draw dots (when movement never occurs) this._captureDrawingPath(pointer); this._render(); }, /** * Inovoked on mouse move * @param {Object} pointer */ onMouseMove: function(pointer) { this._captureDrawingPath(pointer); // redraw curve // clear top canvas this.canvas.clearContext(this.canvas.contextTop); this._render(); }, /** * Invoked on mouse up */ onMouseUp: function() { this._finalizeAndAddPath(); }, /** * @private * @param {Object} pointer Actual mouse position related to the canvas. */ _prepareForDrawing: function(pointer) { var p = new fabric.Point(pointer.x, pointer.y); this._reset(); this._addPoint(p); this.canvas.contextTop.moveTo(p.x, p.y); }, /** * @private * @param {fabric.Point} point Point to be added to points array */ _addPoint: function(point) { this._points.push(point); }, /** * Clear points array and set contextTop canvas style. * @private */ _reset: function() { this._points.length = 0; this._setBrushStyles(); this._setShadow(); }, /** * @private * @param {Object} pointer Actual mouse position related to the canvas. */ _captureDrawingPath: function(pointer) { var pointerPoint = new fabric.Point(pointer.x, pointer.y); this._addPoint(pointerPoint); }, /** * Draw a smooth path on the topCanvas using quadraticCurveTo * @private */ _render: function() { var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform, p1 = this._points[0], p2 = this._points[1]; ctx.save(); ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); ctx.beginPath(); //if we only have 2 points in the path and they are the same //it means that the user only clicked the canvas without moving the mouse //then we should be drawing a dot. A path isn't drawn between two identical dots //that's why we set them apart a bit if (this._points.length === 2 && p1.x === p2.x && p1.y === p2.y) { p1.x -= 0.5; p2.x += 0.5; } ctx.moveTo(p1.x, p1.y); for (var i = 1, len = this._points.length; i < len; i++) { // we pick the point between pi + 1 & pi + 2 as the // end point and p1 as our control point. var midPoint = p1.midPointFrom(p2); ctx.quadraticCurveTo(p1.x, p1.y, midPoint.x, midPoint.y); p1 = this._points[i]; p2 = this._points[i + 1]; } // Draw last line as a straight line while // we wait for the next point to be able to calculate // the bezier control point ctx.lineTo(p1.x, p1.y); ctx.stroke(); ctx.restore(); }, /** * Converts points to SVG path * @param {Array} points Array of points * @param {Number} minX * @param {Number} minY * @return {String} SVG path */ convertPointsToSVGPath: function(points) { var path = [], p1 = new fabric.Point(points[0].x, points[0].y), p2 = new fabric.Point(points[1].x, points[1].y); path.push('M ', points[0].x, ' ', points[0].y, ' '); for (var i = 1, len = points.length; i < len; i++) { var midPoint = p1.midPointFrom(p2); // p1 is our bezier control point // midpoint is our endpoint // start point is p(i-1) value. path.push('Q ', p1.x, ' ', p1.y, ' ', midPoint.x, ' ', midPoint.y, ' '); p1 = new fabric.Point(points[i].x, points[i].y); if ((i + 1) < points.length) { p2 = new fabric.Point(points[i + 1].x, points[i + 1].y); } } path.push('L ', p1.x, ' ', p1.y, ' '); return path; }, /** * Creates fabric.Path object to add on canvas * @param {String} pathData Path data * @return {fabric.Path} Path to add on canvas */ createPath: function(pathData) { var path = new fabric.Path(pathData, { fill: null, stroke: this.color, strokeWidth: this.width, strokeLineCap: this.strokeLineCap, strokeLineJoin: this.strokeLineJoin, originX: 'center', originY: 'center' }); if (this.shadow) { this.shadow.affectStroke = true; path.setShadow(this.shadow); } return path; }, /** * On mouseup after drawing the path on contextTop canvas * we use the points captured to create an new fabric path object * and add it to the fabric canvas. */ _finalizeAndAddPath: function() { var ctx = this.canvas.contextTop; ctx.closePath(); var pathData = this.convertPointsToSVGPath(this._points).join(''); if (pathData === 'M 0 0 Q 0 0 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.canvas.renderAll(); return; } var path = this.createPath(pathData); this.canvas.add(path); path.setCoords(); this.canvas.clearContext(this.canvas.contextTop); this._resetShadow(); this.canvas.renderAll(); // fire event 'path' created this.canvas.fire('path:created', { path: path }); } }); })(); /** * CircleBrush class * @class fabric.CircleBrush */ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric.CircleBrush.prototype */ { /** * Width of a brush * @type Number * @default */ width: 10, /** * Constructor * @param {fabric.Canvas} canvas * @return {fabric.CircleBrush} Instance of a circle brush */ initialize: function(canvas) { this.canvas = canvas; this.points = [ ]; }, /** * Invoked inside on mouse down and mouse move * @param {Object} pointer */ drawDot: function(pointer) { var point = this.addPoint(pointer), ctx = this.canvas.contextTop, v = this.canvas.viewportTransform; ctx.save(); ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); ctx.fillStyle = point.fill; ctx.beginPath(); ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); ctx.closePath(); ctx.fill(); ctx.restore(); }, /** * Invoked on mouse down */ onMouseDown: function(pointer) { this.points.length = 0; this.canvas.clearContext(this.canvas.contextTop); this._setShadow(); this.drawDot(pointer); }, /** * Invoked on mouse move * @param {Object} pointer */ onMouseMove: function(pointer) { this.drawDot(pointer); }, /** * Invoked on mouse up */ onMouseUp: function() { var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; this.canvas.renderOnAddRemove = false; var circles = [ ]; for (var i = 0, len = this.points.length; i < len; i++) { var point = this.points[i], circle = new fabric.Circle({ radius: point.radius, left: point.x, top: point.y, originX: 'center', originY: 'center', fill: point.fill }); this.shadow && circle.setShadow(this.shadow); circles.push(circle); } var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); this.canvas.clearContext(this.canvas.contextTop); this._resetShadow(); this.canvas.renderOnAddRemove = originalRenderOnAddRemove; this.canvas.renderAll(); }, /** * @param {Object} pointer * @return {fabric.Point} Just added pointer point */ addPoint: function(pointer) { var pointerPoint = new fabric.Point(pointer.x, pointer.y), circleRadius = fabric.util.getRandomInt( Math.max(0, this.width - 20), this.width + 20) / 2, circleColor = new fabric.Color(this.color) .setAlpha(fabric.util.getRandomInt(0, 100) / 100) .toRgba(); pointerPoint.radius = circleRadius; pointerPoint.fill = circleColor; this.points.push(pointerPoint); return pointerPoint; } }); /** * SprayBrush class * @class fabric.SprayBrush */ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric.SprayBrush.prototype */ { /** * Width of a spray * @type Number * @default */ width: 10, /** * Density of a spray (number of dots per chunk) * @type Number * @default */ density: 20, /** * Width of spray dots * @type Number * @default */ dotWidth: 1, /** * Width variance of spray dots * @type Number * @default */ dotWidthVariance: 1, /** * Whether opacity of a dot should be random * @type Boolean * @default */ randomOpacity: false, /** * Whether overlapping dots (rectangles) should be removed (for performance reasons) * @type Boolean * @default */ optimizeOverlapping: true, /** * Constructor * @param {fabric.Canvas} canvas * @return {fabric.SprayBrush} Instance of a spray brush */ initialize: function(canvas) { this.canvas = canvas; this.sprayChunks = [ ]; }, /** * Invoked on mouse down * @param {Object} pointer */ onMouseDown: function(pointer) { this.sprayChunks.length = 0; this.canvas.clearContext(this.canvas.contextTop); this._setShadow(); this.addSprayChunk(pointer); this.render(); }, /** * Invoked on mouse move * @param {Object} pointer */ onMouseMove: function(pointer) { this.addSprayChunk(pointer); this.render(); }, /** * Invoked on mouse up */ onMouseUp: function() { var originalRenderOnAddRemove = this.canvas.renderOnAddRemove; this.canvas.renderOnAddRemove = false; var rects = [ ]; for (var i = 0, ilen = this.sprayChunks.length; i < ilen; i++) { var sprayChunk = this.sprayChunks[i]; for (var j = 0, jlen = sprayChunk.length; j < jlen; j++) { var rect = new fabric.Rect({ width: sprayChunk[j].width, height: sprayChunk[j].width, left: sprayChunk[j].x + 1, top: sprayChunk[j].y + 1, originX: 'center', originY: 'center', fill: this.color }); this.shadow && rect.setShadow(this.shadow); rects.push(rect); } } if (this.optimizeOverlapping) { rects = this._getOptimizedRects(rects); } var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); this.canvas.clearContext(this.canvas.contextTop); this._resetShadow(); this.canvas.renderOnAddRemove = originalRenderOnAddRemove; this.canvas.renderAll(); }, /** * @private * @param {Array} rects */ _getOptimizedRects: function(rects) { // avoid creating duplicate rects at the same coordinates var uniqueRects = { }, key; for (var i = 0, len = rects.length; i < len; i++) { key = rects[i].left + '' + rects[i].top; if (!uniqueRects[key]) { uniqueRects[key] = rects[i]; } } var uniqueRectsArray = [ ]; for (key in uniqueRects) { uniqueRectsArray.push(uniqueRects[key]); } return uniqueRectsArray; }, /** * Renders brush */ render: function() { var ctx = this.canvas.contextTop; ctx.fillStyle = this.color; var v = this.canvas.viewportTransform; ctx.save(); ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { var point = this.sprayChunkPoints[i]; if (typeof point.opacity !== 'undefined') { ctx.globalAlpha = point.opacity; } ctx.fillRect(point.x, point.y, point.width, point.width); } ctx.restore(); }, /** * @param {Object} pointer */ addSprayChunk: function(pointer) { this.sprayChunkPoints = [ ]; var x, y, width, radius = this.width / 2; for (var i = 0; i < this.density; i++) { x = fabric.util.getRandomInt(pointer.x - radius, pointer.x + radius); y = fabric.util.getRandomInt(pointer.y - radius, pointer.y + radius); if (this.dotWidthVariance) { width = fabric.util.getRandomInt( // bottom clamp width to 1 Math.max(1, this.dotWidth - this.dotWidthVariance), this.dotWidth + this.dotWidthVariance); } else { width = this.dotWidth; } var point = new fabric.Point(x, y); point.width = width; if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; } this.sprayChunkPoints.push(point); } this.sprayChunks.push(this.sprayChunkPoints); } }); /** * PatternBrush class * @class fabric.PatternBrush * @extends fabric.BaseBrush */ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fabric.PatternBrush.prototype */ { getPatternSrc: function() { var dotWidth = 20, dotDistance = 5, patternCanvas = fabric.document.createElement('canvas'), patternCtx = patternCanvas.getContext('2d'); patternCanvas.width = patternCanvas.height = dotWidth + dotDistance; patternCtx.fillStyle = this.color; patternCtx.beginPath(); patternCtx.arc(dotWidth / 2, dotWidth / 2, dotWidth / 2, 0, Math.PI * 2, false); patternCtx.closePath(); patternCtx.fill(); return patternCanvas; }, getPatternSrcFunction: function() { return String(this.getPatternSrc).replace('this.color', '"' + this.color + '"'); }, /** * Creates "pattern" instance property */ getPattern: function() { return this.canvas.contextTop.createPattern(this.source || this.getPatternSrc(), 'repeat'); }, /** * Sets brush styles */ _setBrushStyles: function() { this.callSuper('_setBrushStyles'); this.canvas.contextTop.strokeStyle = this.getPattern(); }, /** * Creates path */ createPath: function(pathData) { var path = this.callSuper('createPath', pathData); path.stroke = new fabric.Pattern({ source: this.source || this.getPatternSrcFunction() }); return path; } }); (function() { var getPointer = fabric.util.getPointer, degreesToRadians = fabric.util.degreesToRadians, radiansToDegrees = fabric.util.radiansToDegrees, atan2 = Math.atan2, abs = Math.abs, STROKE_OFFSET = 0.5; /** * Canvas class * @class fabric.Canvas * @extends fabric.StaticCanvas * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#canvas} * @see {@link fabric.Canvas#initialize} for constructor definition * * @fires object:modified * @fires object:rotating * @fires object:scaling * @fires object:moving * @fires object:selected * * @fires before:selection:cleared * @fires selection:cleared * @fires selection:created * * @fires path:created * @fires mouse:down * @fires mouse:move * @fires mouse:up * @fires mouse:over * @fires mouse:out * */ fabric.Canvas = fabric.util.createClass(fabric.StaticCanvas, /** @lends fabric.Canvas.prototype */ { /** * Constructor * @param {HTMLElement | String} el <canvas> element to initialize instance on * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(el, options) { options || (options = { }); this._initStatic(el, options); this._initInteractive(); this._createCacheCanvas(); fabric.Canvas.activeInstance = this; }, /** * When true, objects can be transformed by one side (unproportionally) * @type Boolean * @default */ uniScaleTransform: false, /** * When true, objects use center point as the origin of scale transformation. * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredScaling: false, /** * When true, objects use center point as the origin of rotate transformation. * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredRotation: false, /** * Indicates that canvas is interactive. This property should not be changed. * @type Boolean * @default */ interactive: true, /** * Indicates whether group selection should be enabled * @type Boolean * @default */ selection: true, /** * Color of selection * @type String * @default */ selectionColor: 'rgba(100, 100, 255, 0.3)', // blue /** * Default dash array pattern * If not empty the selection border is dashed * @type Array */ selectionDashArray: [ ], /** * Color of the border of selection (usually slightly darker than color of selection itself) * @type String * @default */ selectionBorderColor: 'rgba(255, 255, 255, 0.3)', /** * Width of a line used in object/group selection * @type Number * @default */ selectionLineWidth: 1, /** * Default cursor value used when hovering over an object on canvas * @type String * @default */ hoverCursor: 'move', /** * Default cursor value used when moving an object on canvas * @type String * @default */ moveCursor: 'move', /** * Default cursor value used for the entire canvas * @type String * @default */ defaultCursor: 'default', /** * Cursor value used during free drawing * @type String * @default */ freeDrawingCursor: 'crosshair', /** * Cursor value used for rotation point * @type String * @default */ rotationCursor: 'crosshair', /** * Default element class that's given to wrapper (div) element of canvas * @type String * @default */ containerClass: 'canvas-container', /** * When true, object detection happens on per-pixel basis rather than on per-bounding-box * @type Boolean * @default */ perPixelTargetFind: false, /** * Number of pixels around target pixel to tolerate (consider active) during object detection * @type Number * @default */ targetFindTolerance: 0, /** * When true, target detection is skipped when hovering over canvas. This can be used to improve performance. * @type Boolean * @default */ skipTargetFind: false, /** * @private */ _initInteractive: function() { this._currentTransform = null; this._groupSelector = null; this._initWrapperElement(); this._createUpperCanvas(); this._initEventListeners(); this.freeDrawingBrush = fabric.PencilBrush && new fabric.PencilBrush(this); this.calcOffset(); }, /** * Resets the current transform to its original values and chooses the type of resizing based on the event * @private * @param {Event} e Event object fired on mousemove */ _resetCurrentTransform: function(e) { var t = this._currentTransform; t.target.set({ scaleX: t.original.scaleX, scaleY: t.original.scaleY, left: t.original.left, top: t.original.top }); if (this._shouldCenterTransform(e, t.target)) { if (t.action === 'rotate') { this._setOriginToCenter(t.target); } else { if (t.originX !== 'center') { if (t.originX === 'right') { t.mouseXSign = -1; } else { t.mouseXSign = 1; } } if (t.originY !== 'center') { if (t.originY === 'bottom') { t.mouseYSign = -1; } else { t.mouseYSign = 1; } } t.originX = 'center'; t.originY = 'center'; } } else { t.originX = t.original.originX; t.originY = t.original.originY; } }, /** * Checks if point is contained within an area of given object * @param {Event} e Event object * @param {fabric.Object} target Object to test against * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { var pointer = this.getPointer(e, true), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html // http://idav.ucdavis.edu/~okreylos/TAship/Spring2000/PointInPolygon.html return (target.containsPoint(xy) || target._findTargetCorner(pointer)); }, /** * @private */ _normalizePointer: function (object, pointer) { var activeGroup = this.getActiveGroup(), x = pointer.x, y = pointer.y, isObjectInGroup = ( activeGroup && object.type !== 'group' && activeGroup.contains(object)), lt; if (isObjectInGroup) { lt = new fabric.Point(activeGroup.left, activeGroup.top); lt = fabric.util.transformPoint(lt, this.viewportTransform, true); x -= lt.x; y -= lt.y; } return { x: x, y: y }; }, /** * Returns true if object is transparent at a certain location * @param {fabric.Object} target Object to check * @param {Number} x Left coordinate * @param {Number} y Top coordinate * @return {Boolean} */ isTargetTransparent: function (target, x, y) { var hasBorders = target.hasBorders, transparentCorners = target.transparentCorners; target.hasBorders = target.transparentCorners = false; this._draw(this.contextCache, target); target.hasBorders = hasBorders; target.transparentCorners = transparentCorners; var isTransparent = fabric.util.isTransparent( this.contextCache, x, y, this.targetFindTolerance); this.clearContext(this.contextCache); return isTransparent; }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _shouldClearSelection: function (e, target) { var activeGroup = this.getActiveGroup(), activeObject = this.getActiveObject(); return ( !target || (target && activeGroup && !activeGroup.contains(target) && activeGroup !== target && !e.shiftKey) || (target && !target.evented) || (target && !target.selectable && activeObject && activeObject !== target) ); }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _shouldCenterTransform: function (e, target) { if (!target) { return; } var t = this._currentTransform, centerTransform; if (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY') { centerTransform = this.centeredScaling || target.centeredScaling; } else if (t.action === 'rotate') { centerTransform = this.centeredRotation || target.centeredRotation; } return centerTransform ? !e.altKey : e.altKey; }, /** * @private */ _getOriginFromCorner: function(target, corner) { var origin = { x: target.originX, y: target.originY }; if (corner === 'ml' || corner === 'tl' || corner === 'bl') { origin.x = 'right'; } else if (corner === 'mr' || corner === 'tr' || corner === 'br') { origin.x = 'left'; } if (corner === 'tl' || corner === 'mt' || corner === 'tr') { origin.y = 'bottom'; } else if (corner === 'bl' || corner === 'mb' || corner === 'br') { origin.y = 'top'; } return origin; }, /** * @private */ _getActionFromCorner: function(target, corner) { var action = 'drag'; if (corner) { action = (corner === 'ml' || corner === 'mr') ? 'scaleX' : (corner === 'mt' || corner === 'mb') ? 'scaleY' : corner === 'mtr' ? 'rotate' : 'scale'; } return action; }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _setupCurrentTransform: function (e, target) { if (!target) { return; } var pointer = this.getPointer(e), corner = target._findTargetCorner(this.getPointer(e, true)), action = this._getActionFromCorner(target, corner), origin = this._getOriginFromCorner(target, corner); this._currentTransform = { target: target, action: action, scaleX: target.scaleX, scaleY: target.scaleY, offsetX: pointer.x - target.left, offsetY: pointer.y - target.top, originX: origin.x, originY: origin.y, ex: pointer.x, ey: pointer.y, left: target.left, top: target.top, theta: degreesToRadians(target.angle), width: target.width * target.scaleX, mouseXSign: 1, mouseYSign: 1 }; this._currentTransform.original = { left: target.left, top: target.top, scaleX: target.scaleX, scaleY: target.scaleY, originX: origin.x, originY: origin.y }; this._resetCurrentTransform(e); }, /** * Translates object by "setting" its left/top * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate */ _translateObject: function (x, y) { var target = this._currentTransform.target; if (!target.get('lockMovementX')) { target.set('left', x - this._currentTransform.offsetX); } if (!target.get('lockMovementY')) { target.set('top', y - this._currentTransform.offsetY); } }, /** * Scales object by invoking its scaleX/scaleY methods * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate * @param {String} by 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, target = t.target, lockScalingX = target.get('lockScalingX'), lockScalingY = target.get('lockScalingY'), lockScalingFlip = target.get('lockScalingFlip'); if (lockScalingX && lockScalingY) { return; } // Get the constraint point var constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY), localMouse = target.toLocalPoint(new fabric.Point(x, y), t.originX, t.originY); this._setLocalMouse(localMouse, t); // Actually scale the object this._setObjectScale(localMouse, t, lockScalingX, lockScalingY, by, lockScalingFlip); // Make sure the constraints apply target.setPositionByOrigin(constraintPosition, t.originX, t.originY); }, /** * @private */ _setObjectScale: function(localMouse, transform, lockScalingX, lockScalingY, by, lockScalingFlip) { var target = transform.target, forbidScalingX = false, forbidScalingY = false, strokeWidth = target.stroke ? target.strokeWidth : 0; transform.newScaleX = localMouse.x / (target.width + strokeWidth / 2); transform.newScaleY = localMouse.y / (target.height + strokeWidth / 2); if (lockScalingFlip && transform.newScaleX <= 0 && transform.newScaleX < target.scaleX) { forbidScalingX = true; } if (lockScalingFlip && transform.newScaleY <= 0 && transform.newScaleY < target.scaleY) { forbidScalingY = true; } if (by === 'equally' && !lockScalingX && !lockScalingY) { forbidScalingX || forbidScalingY || this._scaleObjectEqually(localMouse, target, transform); } else if (!by) { forbidScalingX || lockScalingX || target.set('scaleX', transform.newScaleX); forbidScalingY || lockScalingY || target.set('scaleY', transform.newScaleY); } else if (by === 'x' && !target.get('lockUniScaling')) { forbidScalingX || lockScalingX || target.set('scaleX', transform.newScaleX); } else if (by === 'y' && !target.get('lockUniScaling')) { forbidScalingY || lockScalingY || target.set('scaleY', transform.newScaleY); } forbidScalingX || forbidScalingY || this._flipObject(transform, by); }, /** * @private */ _scaleObjectEqually: function(localMouse, target, transform) { var dist = localMouse.y + localMouse.x, strokeWidth = target.stroke ? target.strokeWidth : 0, lastDist = (target.height + (strokeWidth / 2)) * transform.original.scaleY + (target.width + (strokeWidth / 2)) * transform.original.scaleX; // We use transform.scaleX/Y instead of target.scaleX/Y // because the object may have a min scale and we'll loose the proportions transform.newScaleX = transform.original.scaleX * dist / lastDist; transform.newScaleY = transform.original.scaleY * dist / lastDist; target.set('scaleX', transform.newScaleX); target.set('scaleY', transform.newScaleY); }, /** * @private */ _flipObject: function(transform, by) { if (transform.newScaleX < 0 && by !== 'y') { if (transform.originX === 'left') { transform.originX = 'right'; } else if (transform.originX === 'right') { transform.originX = 'left'; } } if (transform.newScaleY < 0 && by !== 'x') { if (transform.originY === 'top') { transform.originY = 'bottom'; } else if (transform.originY === 'bottom') { transform.originY = 'top'; } } }, /** * @private */ _setLocalMouse: function(localMouse, t) { var target = t.target; if (t.originX === 'right') { localMouse.x *= -1; } else if (t.originX === 'center') { localMouse.x *= t.mouseXSign * 2; if (localMouse.x < 0) { t.mouseXSign = -t.mouseXSign; } } if (t.originY === 'bottom') { localMouse.y *= -1; } else if (t.originY === 'center') { localMouse.y *= t.mouseYSign * 2; if (localMouse.y < 0) { t.mouseYSign = -t.mouseYSign; } } // adjust the mouse coordinates when dealing with padding if (abs(localMouse.x) > target.padding) { if (localMouse.x < 0) { localMouse.x += target.padding; } else { localMouse.x -= target.padding; } } else { // mouse is within the padding, set to 0 localMouse.x = 0; } if (abs(localMouse.y) > target.padding) { if (localMouse.y < 0) { localMouse.y += target.padding; } else { localMouse.y -= target.padding; } } else { localMouse.y = 0; } }, /** * Rotates object by invoking its rotate method * @private * @param {Number} x pointer's x coordinate * @param {Number} y pointer's y coordinate */ _rotateObject: function (x, y) { var t = this._currentTransform; if (t.target.get('lockRotation')) { return; } var lastAngle = atan2(t.ey - t.top, t.ex - t.left), curAngle = atan2(y - t.top, x - t.left), angle = radiansToDegrees(curAngle - lastAngle + t.theta); // normalize angle to positive value if (angle < 0) { angle = 360 + angle; } t.target.angle = angle % 360; }, /** * Set the cursor type of the canvas element * @param {String} value Cursor type of the canvas element. * @see http://www.w3.org/TR/css3-ui/#cursor */ setCursor: function (value) { this.upperCanvasEl.style.cursor = value; }, /** * @private */ _resetObjectTransform: function (target) { target.scaleX = 1; target.scaleY = 1; target.setAngle(0); }, /** * @private */ _drawSelection: function () { var ctx = this.contextTop, groupSelector = this._groupSelector, left = groupSelector.left, top = groupSelector.top, aleft = abs(left), atop = abs(top); ctx.fillStyle = this.selectionColor; ctx.fillRect( groupSelector.ex - ((left > 0) ? 0 : -left), groupSelector.ey - ((top > 0) ? 0 : -top), aleft, atop ); ctx.lineWidth = this.selectionLineWidth; ctx.strokeStyle = this.selectionBorderColor; // selection border if (this.selectionDashArray.length > 1) { var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0: aleft), py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0: atop); ctx.beginPath(); fabric.util.drawDashedLine(ctx, px, py, px + aleft, py, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px, py + atop - 1, px + aleft, py + atop - 1, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px, py, px, py + atop, this.selectionDashArray); fabric.util.drawDashedLine(ctx, px + aleft - 1, py, px + aleft - 1, py + atop, this.selectionDashArray); ctx.closePath(); ctx.stroke(); } else { ctx.strokeRect( groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0 : aleft), groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0 : atop), aleft, atop ); } }, /** * @private */ _isLastRenderedObject: function(e) { return ( this.controlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay.visible && this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(this.getPointer(e, true))); }, /** * Method that determines what object we are clicking on * @param {Event} e mouse event * @param {Boolean} skipGroup when true, group is skipped and only objects are traversed through */ findTarget: function (e, skipGroup) { if (this.skipTargetFind) { return; } if (this._isLastRenderedObject(e)) { return this.lastRenderedObjectWithControlsAboveOverlay; } // first check current group (if one exists) var activeGroup = this.getActiveGroup(); if (activeGroup && !skipGroup && this.containsPoint(e, activeGroup)) { return activeGroup; } var target = this._searchPossibleTargets(e); this._fireOverOutEvents(target); return target; }, /** * @private */ _fireOverOutEvents: function(target) { if (target) { if (this._hoveredTarget !== target) { this.fire('mouse:over', { target: target }); target.fire('mouseover'); if (this._hoveredTarget) { this.fire('mouse:out', { target: this._hoveredTarget }); this._hoveredTarget.fire('mouseout'); } this._hoveredTarget = target; } } else if (this._hoveredTarget) { this.fire('mouse:out', { target: this._hoveredTarget }); this._hoveredTarget.fire('mouseout'); this._hoveredTarget = null; } }, /** * @private */ _checkTarget: function(e, obj, pointer) { if (obj && obj.visible && obj.evented && this.containsPoint(e, obj)){ if ((this.perPixelTargetFind || obj.perPixelTargetFind) && !obj.isEditing) { var isTransparent = this.isTargetTransparent(obj, pointer.x, pointer.y); if (!isTransparent) { return true; } } else { return true; } } }, /** * @private */ _searchPossibleTargets: function(e) { // Cache all targets where their bounding box contains point. var target, pointer = this.getPointer(e, true), i = this._objects.length; while (i--) { if (this._checkTarget(e, this._objects[i], pointer)){ this.relatedTarget = this._objects[i]; target = this._objects[i]; break; } } return target; }, /** * Returns pointer coordinates relative to canvas. * @param {Event} e * @return {Object} object with "x" and "y" number values */ getPointer: function (e, ignoreZoom, upperCanvasEl) { if (!upperCanvasEl) { upperCanvasEl = this.upperCanvasEl; } var pointer = getPointer(e, upperCanvasEl), bounds = upperCanvasEl.getBoundingClientRect(), boundsWidth = bounds.width || 0, boundsHeight = bounds.height || 0, cssScale; if (!boundsWidth || !boundsHeight ) { if ('top' in bounds && 'bottom' in bounds) { boundsHeight = Math.abs( bounds.top - bounds.bottom ); } if ('right' in bounds && 'left' in bounds) { boundsWidth = Math.abs( bounds.right - bounds.left ); } } this.calcOffset(); pointer.x = pointer.x - this._offset.left; pointer.y = pointer.y - this._offset.top; if (!ignoreZoom) { pointer = fabric.util.transformPoint( pointer, fabric.util.invertTransform(this.viewportTransform) ); } if (boundsWidth === 0 || boundsHeight === 0) { // If bounds are not available (i.e. not visible), do not apply scale. cssScale = { width: 1, height: 1 }; } else { cssScale = { width: upperCanvasEl.width / boundsWidth, height: upperCanvasEl.height / boundsHeight }; } return { x: pointer.x * cssScale.width, y: pointer.y * cssScale.height }; }, /** * @private * @throws {CANVAS_INIT_ERROR} If canvas can not be initialized */ _createUpperCanvas: function () { var lowerCanvasClass = this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/, ''); this.upperCanvasEl = this._createCanvasElement(); fabric.util.addClass(this.upperCanvasEl, 'upper-canvas ' + lowerCanvasClass); this.wrapperEl.appendChild(this.upperCanvasEl); this._copyCanvasStyle(this.lowerCanvasEl, this.upperCanvasEl); this._applyCanvasStyle(this.upperCanvasEl); this.contextTop = this.upperCanvasEl.getContext('2d'); }, /** * @private */ _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 */ _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 * @param {HTMLElement} element canvas element to apply styles on */ _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); }, /** * Copys the the entire inline style from one element (fromEl) to another (toEl) * @private * @param {Element} fromEl Element style is copied from * @param {Element} toEl Element copied style is applied to */ _copyCanvasStyle: function (fromEl, toEl) { toEl.style.cssText = fromEl.style.cssText; }, /** * Returns context of canvas where object selection is drawn * @return {CanvasRenderingContext2D} */ getSelectionContext: function() { return this.contextTop; }, /** * Returns <canvas> element on which object selection is drawn * @return {HTMLCanvasElement} */ getSelectionElement: function () { return this.upperCanvasEl; }, /** * @private * @param {Object} object */ _setActiveObject: function(object) { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = object; object.set('active', true); }, /** * Sets given object as the only active object on canvas * @param {fabric.Object} object Object to set as an active one * @param {Event} [e] Event (passed along when firing "object:selected") * @return {fabric.Canvas} thisArg * @chainable */ setActiveObject: function (object, e) { this._setActiveObject(object); this.renderAll(); this.fire('object:selected', { target: object, e: e }); object.fire('selected', { e: e }); return this; }, /** * Returns currently active object * @return {fabric.Object} active object */ getActiveObject: function () { return this._activeObject; }, /** * @private */ _discardActiveObject: function() { if (this._activeObject) { this._activeObject.set('active', false); } this._activeObject = null; }, /** * Discards currently active object * @return {fabric.Canvas} thisArg * @chainable */ discardActiveObject: function (e) { this._discardActiveObject(); this.renderAll(); this.fire('selection:cleared', { e: e }); return this; }, /** * @private * @param {fabric.Group} group */ _setActiveGroup: function(group) { this._activeGroup = group; if (group) { group.set('active', true); } }, /** * Sets active group to a speicified one * @param {fabric.Group} group Group to set as a current one * @return {fabric.Canvas} thisArg * @chainable */ setActiveGroup: function (group, e) { this._setActiveGroup(group); if (group) { this.fire('object:selected', { target: group, e: e }); group.fire('selected', { e: e }); } return this; }, /** * Returns currently active group * @return {fabric.Group} Current group */ getActiveGroup: function () { return this._activeGroup; }, /** * @private */ _discardActiveGroup: function() { var g = this.getActiveGroup(); if (g) { g.destroy(); } this.setActiveGroup(null); }, /** * Discards currently active group * @return {fabric.Canvas} thisArg */ discardActiveGroup: function (e) { this._discardActiveGroup(); this.fire('selection:cleared', { e: e }); return this; }, /** * Deactivates all objects on canvas, removing any active group or object * @return {fabric.Canvas} thisArg */ deactivateAll: function () { var allObjects = this.getObjects(), i = 0, len = allObjects.length; for ( ; i < len; i++) { allObjects[i].set('active', false); } this._discardActiveGroup(); this._discardActiveObject(); return this; }, /** * Deactivates all objects and dispatches appropriate events * @return {fabric.Canvas} thisArg */ deactivateAllWithDispatch: function (e) { var activeObject = this.getActiveGroup() || this.getActiveObject(); if (activeObject) { this.fire('before:selection:cleared', { target: activeObject, e: e }); } this.deactivateAll(); if (activeObject) { this.fire('selection:cleared', { e: e }); } return this; }, /** * Draws objects' controls (borders/controls) * @param {CanvasRenderingContext2D} ctx Context to render controls on */ drawControls: function(ctx) { var activeGroup = this.getActiveGroup(); if (activeGroup) { this._drawGroupControls(ctx, activeGroup); } else { this._drawObjectsControls(ctx); } }, /** * @private */ _drawGroupControls: function(ctx, activeGroup) { activeGroup._renderControls(ctx); }, /** * @private */ _drawObjectsControls: function(ctx) { for (var i = 0, len = this._objects.length; i < len; ++i) { if (!this._objects[i] || !this._objects[i].active) { continue; } this._objects[i]._renderControls(ctx); this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; } } }); // copying static properties manually to work around 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) { /** @ignore */ fabric.Canvas.prototype._setCursorFromEvent = function() { }; } /** * @class fabric.Element * @alias fabric.Canvas * @deprecated Use {@link fabric.Canvas} instead. * @constructor */ fabric.Element = fabric.Canvas; })(); (function() { var cursorOffset = { mt: 0, // n tr: 1, // ne mr: 2, // e br: 3, // se mb: 4, // s bl: 5, // sw ml: 6, // w tl: 7 // nw }, addListener = fabric.util.addListener, removeListener = fabric.util.removeListener; fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { /** * Map of cursor style values for each of the object controls * @private */ cursorMap: [ 'n-resize', 'ne-resize', 'e-resize', 'se-resize', 's-resize', 'sw-resize', 'w-resize', 'nw-resize' ], /** * Adds mouse listeners to canvas * @private */ _initEventListeners: function () { this._bindEvents(); addListener(fabric.window, 'resize', this._onResize); // mouse events addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); addListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); // touch events addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); if (typeof Event !== 'undefined' && 'add' in Event) { Event.add(this.upperCanvasEl, 'gesture', this._onGesture); Event.add(this.upperCanvasEl, 'drag', this._onDrag); Event.add(this.upperCanvasEl, 'orientation', this._onOrientationChange); Event.add(this.upperCanvasEl, 'shake', this._onShake); Event.add(this.upperCanvasEl, 'longpress', this._onLongPress); } }, /** * @private */ _bindEvents: function() { this._onMouseDown = this._onMouseDown.bind(this); this._onMouseMove = this._onMouseMove.bind(this); this._onMouseUp = this._onMouseUp.bind(this); this._onResize = this._onResize.bind(this); this._onGesture = this._onGesture.bind(this); this._onDrag = this._onDrag.bind(this); this._onShake = this._onShake.bind(this); this._onLongPress = this._onLongPress.bind(this); this._onOrientationChange = this._onOrientationChange.bind(this); this._onMouseWheel = this._onMouseWheel.bind(this); }, /** * Removes all event listeners */ removeListeners: function() { removeListener(fabric.window, 'resize', this._onResize); removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); removeListener(this.upperCanvasEl, 'mousewheel', this._onMouseWheel); removeListener(this.upperCanvasEl, 'touchstart', this._onMouseDown); removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); if (typeof Event !== 'undefined' && 'remove' in Event) { Event.remove(this.upperCanvasEl, 'gesture', this._onGesture); Event.remove(this.upperCanvasEl, 'drag', this._onDrag); Event.remove(this.upperCanvasEl, 'orientation', this._onOrientationChange); Event.remove(this.upperCanvasEl, 'shake', this._onShake); Event.remove(this.upperCanvasEl, 'longpress', this._onLongPress); } }, /** * @private * @param {Event} [e] Event object fired on Event.js gesture * @param {Event} [self] Inner Event object */ _onGesture: function(e, self) { this.__onTransformGesture && this.__onTransformGesture(e, self); }, /** * @private * @param {Event} [e] Event object fired on Event.js drag * @param {Event} [self] Inner Event object */ _onDrag: function(e, self) { this.__onDrag && this.__onDrag(e, self); }, /** * @private * @param {Event} [e] Event object fired on Event.js wheel event * @param {Event} [self] Inner Event object */ _onMouseWheel: function(e, self) { this.__onMouseWheel && this.__onMouseWheel(e, self); }, /** * @private * @param {Event} [e] Event object fired on Event.js orientation change * @param {Event} [self] Inner Event object */ _onOrientationChange: function(e, self) { this.__onOrientationChange && this.__onOrientationChange(e, self); }, /** * @private * @param {Event} [e] Event object fired on Event.js shake * @param {Event} [self] Inner Event object */ _onShake: function(e, self) { this.__onShake && this.__onShake(e, self); }, /** * @private * @param {Event} [e] Event object fired on Event.js shake * @param {Event} [self] Inner Event object */ _onLongPress: function(e, self) { this.__onLongPress && this.__onLongPress(e, self); }, /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDown: function (e) { this.__onMouseDown(e); addListener(fabric.document, 'touchend', this._onMouseUp); addListener(fabric.document, 'touchmove', this._onMouseMove); removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); if (e.type === 'touchstart') { // Unbind mousedown to prevent double triggers from touch devices removeListener(this.upperCanvasEl, 'mousedown', this._onMouseDown); } else { addListener(fabric.document, 'mouseup', this._onMouseUp); addListener(fabric.document, 'mousemove', this._onMouseMove); } }, /** * @private * @param {Event} e Event object fired on mouseup */ _onMouseUp: function (e) { this.__onMouseUp(e); removeListener(fabric.document, 'mouseup', this._onMouseUp); removeListener(fabric.document, 'touchend', this._onMouseUp); removeListener(fabric.document, 'mousemove', this._onMouseMove); removeListener(fabric.document, 'touchmove', this._onMouseMove); addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove); addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove); if (e.type === 'touchend') { // Wait 400ms before rebinding mousedown to prevent double triggers // from touch devices var _this = this; setTimeout(function() { addListener(_this.upperCanvasEl, 'mousedown', _this._onMouseDown); }, 400); } }, /** * @private * @param {Event} e Event object fired on mousemove */ _onMouseMove: function (e) { !this.allowTouchScrolling && e.preventDefault && e.preventDefault(); this.__onMouseMove(e); }, /** * @private */ _onResize: function () { this.calcOffset(); }, /** * Decides whether the canvas should be redrawn in mouseup and mousedown events. * @private * @param {Object} target * @param {Object} pointer */ _shouldRender: function(target, pointer) { var activeObject = this.getActiveGroup() || this.getActiveObject(); return !!( (target && ( target.isMoving || target !== activeObject)) || (!target && !!activeObject) || (!target && !activeObject && !this._groupSelector) || (pointer && this._previousPointer && this.selection && ( pointer.x !== this._previousPointer.x || pointer.y !== this._previousPointer.y)) ); }, /** * 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. * @private * @param {Event} e Event object fired on mouseup */ __onMouseUp: function (e) { var target; if (this.isDrawingMode && this._isCurrentlyDrawing) { this._onMouseUpInDrawingMode(e); return; } if (this._currentTransform) { this._finalizeCurrentTransform(); target = this._currentTransform.target; } else { target = this.findTarget(e, true); } var shouldRender = this._shouldRender(target, this.getPointer(e)); this._maybeGroupObjects(e); if (target) { target.isMoving = false; } shouldRender && this.renderAll(); this._handleCursorAndEvent(e, target); }, _handleCursorAndEvent: function(e, target) { this._setCursorFromEvent(e, target); // TODO: why are we doing this? var _this = this; setTimeout(function () { _this._setCursorFromEvent(e, target); }, 50); this.fire('mouse:up', { target: target, e: e }); target && target.fire('mouseup', { e: e }); }, /** * @private */ _finalizeCurrentTransform: function() { var transform = this._currentTransform, target = transform.target; if (target._scaling) { target._scaling = false; } target.setCoords(); // only fire :modified event if target coordinates were changed during mousedown-mouseup if (this.stateful && target.hasStateChanged()) { this.fire('object:modified', { target: target }); target.fire('modified'); } this._restoreOriginXY(target); }, /** * @private * @param {Object} target Object to restore */ _restoreOriginXY: function(target) { if (this._previousOriginX && this._previousOriginY) { var originPoint = target.translateToOriginPoint( target.getCenterPoint(), this._previousOriginX, this._previousOriginY); target.originX = this._previousOriginX; target.originY = this._previousOriginY; target.left = originPoint.x; target.top = originPoint.y; this._previousOriginX = null; this._previousOriginY = null; } }, /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDownInDrawingMode: function(e) { this._isCurrentlyDrawing = true; this.discardActiveObject(e).renderAll(); if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } var ivt = fabric.util.invertTransform(this.viewportTransform), pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); this.freeDrawingBrush.onMouseDown(pointer); this.fire('mouse:down', { e: e }); var target = this.findTarget(e); if (typeof target !== 'undefined') { target.fire('mousedown', { e: e, target: target }); } }, /** * @private * @param {Event} e Event object fired on mousemove */ _onMouseMoveInDrawingMode: function(e) { if (this._isCurrentlyDrawing) { var ivt = fabric.util.invertTransform(this.viewportTransform), pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); this.freeDrawingBrush.onMouseMove(pointer); } this.setCursor(this.freeDrawingCursor); this.fire('mouse:move', { e: e }); var target = this.findTarget(e); if (typeof target !== 'undefined') { target.fire('mousemove', { e: e, target: target }); } }, /** * @private * @param {Event} e Event object fired on mouseup */ _onMouseUpInDrawingMode: function(e) { this._isCurrentlyDrawing = false; if (this.clipTo) { this.contextTop.restore(); } this.freeDrawingBrush.onMouseUp(); this.fire('mouse:up', { e: e }); var target = this.findTarget(e); if (typeof target !== 'undefined') { target.fire('mouseup', { e: e, target: target }); } }, /** * 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. * @private * @param {Event} e 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._onMouseDownInDrawingMode(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, true); // save pointer for check in __onMouseUp event this._previousPointer = pointer; var shouldRender = this._shouldRender(target, pointer), shouldGroup = this._shouldGroup(e, target); if (this._shouldClearSelection(e, target)) { this._clearSelection(e, target, pointer); } else if (shouldGroup) { this._handleGrouping(e, target); target = this.getActiveGroup(); } if (target && target.selectable && !shouldGroup) { this._beforeTransform(e, target); this._setupCurrentTransform(e, target); } // we must renderAll so that active image is placed on the top canvas shouldRender && this.renderAll(); this.fire('mouse:down', { target: target, e: e }); target && target.fire('mousedown', { e: e }); }, /** * @private */ _beforeTransform: function(e, target) { var corner; this.stateful && target.saveState(); // determine if it's a drag or rotate case if ((corner = target._findTargetCorner(this.getPointer(e)))) { this.onBeforeScaleRotate(target); } if (target !== this.getActiveGroup() && target !== this.getActiveObject()) { this.deactivateAll(); this.setActiveObject(target, e); } }, /** * @private */ _clearSelection: function(e, target, pointer) { this.deactivateAllWithDispatch(e); if (target && target.selectable) { this.setActiveObject(target, e); } else if (this.selection) { this._groupSelector = { ex: pointer.x, ey: pointer.y, top: 0, left: 0 }; } }, /** * @private * @param {Object} target Object for that origin is set to center */ _setOriginToCenter: function(target) { this._previousOriginX = this._currentTransform.target.originX; this._previousOriginY = this._currentTransform.target.originY; var center = target.getCenterPoint(); target.originX = 'center'; target.originY = 'center'; target.left = center.x; target.top = center.y; this._currentTransform.left = target.left; this._currentTransform.top = target.top; }, /** * @private * @param {Object} target Object for that center is set to origin */ _setCenterToOrigin: function(target) { var originPoint = target.translateToOriginPoint( target.getCenterPoint(), this._previousOriginX, this._previousOriginY); target.originX = this._previousOriginX; target.originY = this._previousOriginY; target.left = originPoint.x; target.top = originPoint.y; this._previousOriginX = null; this._previousOriginY = null; }, /** * 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. * @private * @param {Event} e Event object fired on mousemove */ __onMouseMove: function (e) { var target, pointer; if (this.isDrawingMode) { this._onMouseMoveInDrawingMode(e); return; } if (typeof e.touches !== 'undefined' && e.touches.length > 1) { return; } var groupSelector = this._groupSelector; // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { pointer = this.getPointer(e, true); groupSelector.left = pointer.x - groupSelector.ex; groupSelector.top = pointer.y - groupSelector.ey; this.renderTop(); } else if (!this._currentTransform) { target = this.findTarget(e); if (!target || target && !target.selectable) { this.setCursor(this.defaultCursor); } else { this._setCursorFromEvent(e, target); } } else { this._transformObject(e); } this.fire('mouse:move', { target: target, e: e }); target && target.fire('mousemove', { e: e }); }, /** * @private * @param {Event} e Event fired on mousemove */ _transformObject: function(e) { var pointer = this.getPointer(e), transform = this._currentTransform; transform.reset = false, transform.target.isMoving = true; this._beforeScaleTransform(e, transform); this._performTransformAction(e, transform, pointer); this.renderAll(); }, /** * @private */ _performTransformAction: function(e, transform, pointer) { var x = pointer.x, y = pointer.y, target = transform.target, action = transform.action; if (action === 'rotate') { this._rotateObject(x, y); this._fire('rotating', target, e); } else if (action === 'scale') { this._onScale(e, transform, x, y); this._fire('scaling', target, e); } else if (action === 'scaleX') { this._scaleObject(x, y, 'x'); this._fire('scaling', target, e); } else if (action === 'scaleY') { this._scaleObject(x, y, 'y'); this._fire('scaling', target, e); } else { this._translateObject(x, y); this._fire('moving', target, e); this.setCursor(this.moveCursor); } }, /** * @private */ _fire: function(eventName, target, e) { this.fire('object:' + eventName, { target: target, e: e }); target.fire(eventName, { e: e }); }, /** * @private */ _beforeScaleTransform: function(e, transform) { if (transform.action === 'scale' || transform.action === 'scaleX' || transform.action === 'scaleY') { var centerTransform = this._shouldCenterTransform(e, transform.target); // Switch from a normal resize to center-based if ((centerTransform && (transform.originX !== 'center' || transform.originY !== 'center')) || // Switch from center-based resize to normal one (!centerTransform && transform.originX === 'center' && transform.originY === 'center') ) { this._resetCurrentTransform(e); transform.reset = true; } } }, /** * @private */ _onScale: function(e, transform, x, y) { // rotate object only if shift key is not pressed // and if it is not a group we are transforming if ((e.shiftKey || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { transform.currentAction = 'scale'; this._scaleObject(x, y); } else { // Switch from a normal resize to proportional if (!transform.reset && transform.currentAction === 'scale') { this._resetCurrentTransform(e, transform.target); } transform.currentAction = 'scaleEqually'; this._scaleObject(x, y, 'equally'); } }, /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera * @param {Event} e Event object * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent: function (e, target) { if (!target || !target.selectable) { this.setCursor(this.defaultCursor); return false; } else { var activeGroup = this.getActiveGroup(), // only show proper corner when group selection is not active corner = target._findTargetCorner && (!activeGroup || !activeGroup.contains(target)) && target._findTargetCorner(this.getPointer(e, true)); if (!corner) { this.setCursor(target.hoverCursor || this.hoverCursor); } else { this._setCornerCursor(corner, target); } } return true; }, /** * @private */ _setCornerCursor: function(corner, target) { if (corner in cursorOffset) { this.setCursor(this._getRotatedCornerCursor(corner, target)); } else if (corner === 'mtr' && target.hasRotatingPoint) { this.setCursor(this.rotationCursor); } else { this.setCursor(this.defaultCursor); return false; } }, /** * @private */ _getRotatedCornerCursor: function(corner, target) { var n = Math.round((target.getAngle() % 360) / 45); if (n < 0) { n += 8; // full circle ahead } n += cursorOffset[corner]; // normalize n to be from 0 to 7 n %= 8; return this.cursorMap[n]; } }); })(); (function() { var min = Math.min, max = Math.max; fabric.util.object.extend(fabric.Canvas.prototype, /** @lends fabric.Canvas.prototype */ { /** * @private * @param {Event} e Event object * @param {fabric.Object} target * @return {Boolean} */ _shouldGroup: function(e, target) { var activeObject = this.getActiveObject(); return e.shiftKey && (this.getActiveGroup() || (activeObject && activeObject !== target)) && this.selection; }, /** * @private * @param {Event} e Event object * @param {fabric.Object} target */ _handleGrouping: 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; } } if (this.getActiveGroup()) { this._updateActiveGroup(target, e); } else { this._createActiveGroup(target, e); } if (this._activeGroup) { this._activeGroup.saveCoords(); } }, /** * @private */ _updateActiveGroup: function(target, e) { var activeGroup = this.getActiveGroup(); if (activeGroup.contains(target)) { activeGroup.removeWithUpdate(target); this._resetObjectTransform(activeGroup); target.set('active', false); if (activeGroup.size() === 1) { // remove group alltogether if after removal it only contains 1 object this.discardActiveGroup(e); // activate last remaining object this.setActiveObject(activeGroup.item(0)); return; } } else { activeGroup.addWithUpdate(target); this._resetObjectTransform(activeGroup); } this.fire('selection:created', { target: activeGroup, e: e }); activeGroup.set('active', true); }, /** * @private */ _createActiveGroup: function(target, e) { if (this._activeObject && target !== this._activeObject) { var group = this._createGroup(target); group.addWithUpdate(); this.setActiveGroup(group); this._activeObject = null; this.fire('selection:created', { target: group, e: e }); } target.set('active', true); }, /** * @private * @param {Object} target */ _createGroup: function(target) { var objects = this.getObjects(), isActiveLower = objects.indexOf(this._activeObject) < objects.indexOf(target), groupObjects = isActiveLower ? [ this._activeObject, target ] : [ target, this._activeObject ]; return new fabric.Group(groupObjects, { canvas: this }); }, /** * @private * @param {Event} e mouse event */ _groupSelectedObjects: function (e) { var group = this._collectObjects(); // do not create group for 1 element only if (group.length === 1) { this.setActiveObject(group[0], e); } else if (group.length > 1) { group = new fabric.Group(group.reverse(), { canvas: this }); group.addWithUpdate(); this.setActiveGroup(group, e); group.saveCoords(); this.fire('selection:created', { target: group }); this.renderAll(); } }, /** * @private */ _collectObjects: function() { var group = [ ], currentObject, x1 = this._groupSelector.ex, y1 = this._groupSelector.ey, x2 = x1 + this._groupSelector.left, y2 = y1 + this._groupSelector.top, selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)), selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)), isClick = x1 === x2 && y1 === y2; for (var i = this._objects.length; i--; ) { currentObject = this._objects[i]; if (!currentObject || !currentObject.selectable || !currentObject.visible) { continue; } if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2) || currentObject.containsPoint(selectionX1Y1) || currentObject.containsPoint(selectionX2Y2) ) { currentObject.set('active', true); group.push(currentObject); // only add one object if it's a click if (isClick) { break; } } } return group; }, /** * @private */ _maybeGroupObjects: function(e) { if (this.selection && this._groupSelector) { this._groupSelectedObjects(e); } var activeGroup = this.getActiveGroup(); if (activeGroup) { activeGroup.setObjectsCoords().setCoords(); activeGroup.isMoving = false; this.setCursor(this.defaultCursor); } // clear selection and current transformation this._groupSelector = null; this._currentTransform = null; } }); })(); fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately * @param {Object} [options] Options object * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format * @see {@link http://jsfiddle.net/fabricjs/NfZVb/|jsFiddle demo} * @example Generate jpeg dataURL with lower quality * var dataURL = canvas.toDataURL({ * format: 'jpeg', * quality: 0.8 * }); * @example Generate cropped png dataURL (clipping of canvas) * var dataURL = canvas.toDataURL({ * format: 'png', * left: 100, * top: 100, * width: 200, * height: 200 * }); * @example Generate double scaled png dataURL * var dataURL = canvas.toDataURL({ * format: 'png', * multiplier: 2 * }); */ toDataURL: function (options) { options || (options = { }); var format = options.format || 'png', quality = options.quality || 1, multiplier = options.multiplier || 1, cropping = { left: options.left, top: options.top, width: options.width, height: options.height }; if (multiplier !== 1) { return this.__toDataURLWithMultiplier(format, quality, cropping, multiplier); } else { return this.__toDataURL(format, quality, cropping); } }, /** * @private */ __toDataURL: function(format, quality, cropping) { this.renderAll(true); var canvasEl = this.upperCanvasEl || this.lowerCanvasEl, croppedCanvasEl = this.__getCroppedCanvas(canvasEl, cropping); // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 if (format === 'jpg') { format = 'jpeg'; } var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) ? (croppedCanvasEl || canvasEl).toDataURL('image/' + format, quality) : (croppedCanvasEl || canvasEl).toDataURL('image/' + format); this.contextTop && this.clearContext(this.contextTop); this.renderAll(); if (croppedCanvasEl) { croppedCanvasEl = null; } return data; }, /** * @private */ __getCroppedCanvas: function(canvasEl, cropping) { var croppedCanvasEl, croppedCtx, shouldCrop = 'left' in cropping || 'top' in cropping || 'width' in cropping || 'height' in cropping; if (shouldCrop) { croppedCanvasEl = fabric.util.createCanvasElement(); croppedCtx = croppedCanvasEl.getContext('2d'); croppedCanvasEl.width = cropping.width || this.width; croppedCanvasEl.height = cropping.height || this.height; croppedCtx.drawImage(canvasEl, -cropping.left || 0, -cropping.top || 0); } return croppedCanvasEl; }, /** * @private */ __toDataURLWithMultiplier: function(format, quality, cropping, multiplier) { var origWidth = this.getWidth(), origHeight = this.getHeight(), scaledWidth = origWidth * multiplier, scaledHeight = origHeight * multiplier, activeObject = this.getActiveObject(), activeGroup = this.getActiveGroup(), ctx = this.contextTop || this.contextContainer; if (multiplier > 1) { this.setWidth(scaledWidth).setHeight(scaledHeight); } ctx.scale(multiplier, multiplier); if (cropping.left) { cropping.left *= multiplier; } if (cropping.top) { cropping.top *= multiplier; } if (cropping.width) { cropping.width *= multiplier; } else if (multiplier < 1) { cropping.width = scaledWidth; } if (cropping.height) { cropping.height *= multiplier; } else if (multiplier < 1) { cropping.height = scaledHeight; } if (activeGroup) { // not removing group due to complications with restoring it with correct state afterwords this._tempRemoveBordersControlsFromGroup(activeGroup); } else if (activeObject && this.deactivateAll) { this.deactivateAll(); } this.renderAll(true); var data = this.__toDataURL(format, quality, cropping); // restoring width, height for `renderAll` to draw // background properly (while context is scaled) this.width = origWidth; this.height = origHeight; ctx.scale(1 / multiplier, 1 / multiplier); this.setWidth(origWidth).setHeight(origHeight); if (activeGroup) { this._restoreBordersControlsOnGroup(activeGroup); } else if (activeObject && this.setActiveObject) { this.setActiveObject(activeObject); } this.contextTop && this.clearContext(this.contextTop); this.renderAll(); return data; }, /** * Exports canvas element to a dataurl image (allowing to change image size via multiplier). * @deprecated since 1.0.13 * @param {String} format (png|jpeg) * @param {Number} multiplier * @param {Number} quality (0..1) * @return {String} */ toDataURLWithMultiplier: function (format, multiplier, quality) { return this.toDataURL({ format: format, multiplier: multiplier, quality: quality }); }, /** * @private */ _tempRemoveBordersControlsFromGroup: function(group) { group.origHasControls = group.hasControls; group.origBorderColor = group.borderColor; group.hasControls = true; group.borderColor = 'rgba(0,0,0,0)'; group.forEachObject(function(o) { o.origBorderColor = o.borderColor; o.borderColor = 'rgba(0,0,0,0)'; }); }, /** * @private */ _restoreBordersControlsOnGroup: function(group) { group.hideControls = group.origHideControls; group.borderColor = group.origBorderColor; group.forEachObject(function(o) { o.borderColor = o.origBorderColor; delete o.origBorderColor; }); } }); fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** * Populates canvas with data from the specified dataless JSON. * JSON format must conform to the one of {@link fabric.Canvas#toDatalessJSON} * @deprecated since 1.2.2 * @param {String|Object} json JSON string or object * @param {Function} callback Callback, invoked when json is parsed * and corresponding objects (e.g: {@link fabric.Image}) * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. * @return {fabric.Canvas} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#deserialization} */ loadFromDatalessJSON: function (json, callback, reviver) { return this.loadFromJSON(json, callback, reviver); }, /** * Populates canvas with data from the specified JSON. * JSON format must conform to the one of {@link fabric.Canvas#toJSON} * @param {String|Object} json JSON string or object * @param {Function} callback Callback, invoked when json is parsed * and corresponding objects (e.g: {@link fabric.Image}) * are initialized * @param {Function} [reviver] Method for further parsing of JSON elements, called after each fabric object created. * @return {fabric.Canvas} instance * @chainable * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#deserialization} * @see {@link http://jsfiddle.net/fabricjs/fmgXt/|jsFiddle demo} * @example loadFromJSON * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas)); * @example loadFromJSON with reviver * canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), function(o, object) { * // `o` = json object * // `object` = fabric.Object instance * // ... do some stuff ... * }); */ loadFromJSON: function (json, callback, reviver) { if (!json) { return; } // serialize if it wasn't already var serialized = (typeof json === 'string') ? JSON.parse(json) : json; this.clear(); var _this = this; this._enlivenObjects(serialized.objects, function () { _this._setBgOverlay(serialized, callback); }, reviver); return this; }, /** * @private * @param {Object} serialized Object with background and overlay information * @param {Function} callback Invoked after all background and overlay images/patterns loaded */ _setBgOverlay: function(serialized, callback) { var _this = this, loaded = { backgroundColor: false, overlayColor: false, backgroundImage: false, overlayImage: false }; if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background && !serialized.overlay) { callback && callback(); return; } var cbIfLoaded = function () { if (loaded.backgroundImage && loaded.overlayImage && loaded.backgroundColor && loaded.overlayColor) { _this.renderAll(); callback && callback(); } }; this.__setBgOverlay('backgroundImage', serialized.backgroundImage, loaded, cbIfLoaded); this.__setBgOverlay('overlayImage', serialized.overlayImage, loaded, cbIfLoaded); this.__setBgOverlay('backgroundColor', serialized.background, loaded, cbIfLoaded); this.__setBgOverlay('overlayColor', serialized.overlay, loaded, cbIfLoaded); cbIfLoaded(); }, /** * @private * @param {String} property Property to set (backgroundImage, overlayImage, backgroundColor, overlayColor) * @param {(Object|String)} value Value to set * @param {Object} loaded Set loaded property to true if property is set * @param {Object} callback Callback function to invoke after property is set */ __setBgOverlay: function(property, value, loaded, callback) { var _this = this; if (!value) { loaded[property] = true; return; } if (property === 'backgroundImage' || property === 'overlayImage') { fabric.Image.fromObject(value, function(img) { _this[property] = img; loaded[property] = true; callback && callback(); }); } else { this['set' + fabric.util.string.capitalize(property, true)](value, function() { loaded[property] = true; callback && callback(); }); } }, /** * @private * @param {Array} objects * @param {Function} callback * @param {Function} [reviver] */ _enlivenObjects: function (objects, callback, reviver) { var _this = this; if (!objects || objects.length === 0) { callback && callback(); return; } var renderOnAddRemove = this.renderOnAddRemove; this.renderOnAddRemove = false; fabric.util.enlivenObjects(objects, function(enlivenedObjects) { enlivenedObjects.forEach(function(obj, index) { _this.insertAt(obj, index, true); }); _this.renderOnAddRemove = renderOnAddRemove; callback && callback(); }, null, reviver); }, /** * @private * @param {String} format * @param {Function} callback */ _toDataURL: function (format, callback) { this.clone(function (clone) { callback(clone.toDataURL(format)); }); }, /** * @private * @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 * @param {Object} [callback] Receives cloned instance as a first argument * @param {Array} [properties] Array of properties to include in the cloned canvas and children */ clone: function (callback, properties) { var data = JSON.stringify(this.toJSON(properties)); 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) * @param {Object} [callback] Receives cloned instance as a first argument */ cloneWithoutData: function(callback) { var el = fabric.document.createElement('canvas'); el.width = this.getWidth(); el.height = this.getHeight(); var clone = new fabric.Canvas(el); clone.clipTo = this.clipTo; if (this.backgroundImage) { clone.setBackgroundImage(this.backgroundImage.src, function() { clone.renderAll(); callback && callback(clone); }); clone.backgroundImageOpacity = this.backgroundImageOpacity; clone.backgroundImageStretch = this.backgroundImageStretch; } else { callback && callback(clone); } } }); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, toFixed = fabric.util.toFixed, capitalize = fabric.util.string.capitalize, degreesToRadians = fabric.util.degreesToRadians, supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); if (fabric.Object) { return; } /** * Root object class from which all 2d shape classes inherit from * @class fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#objects} * @see {@link fabric.Object#initialize} for constructor definition * * @fires added * @fires removed * * @fires selected * @fires modified * @fires rotating * @fires scaling * @fires moving * * @fires mousedown * @fires mouseup */ fabric.Object = fabric.util.createClass(/** @lends fabric.Object.prototype */ { /** * Retrieves object's {@link fabric.Object#clipTo|clipping function} * @method getClipTo * @memberOf fabric.Object.prototype * @return {Function} */ /** * Sets object's {@link fabric.Object#clipTo|clipping function} * @method setClipTo * @memberOf fabric.Object.prototype * @param {Function} clipTo Clipping function * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#transformMatrix|transformMatrix} * @method getTransformMatrix * @memberOf fabric.Object.prototype * @return {Array} transformMatrix */ /** * Sets object's {@link fabric.Object#transformMatrix|transformMatrix} * @method setTransformMatrix * @memberOf fabric.Object.prototype * @param {Array} transformMatrix * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#visible|visible} state * @method getVisible * @memberOf fabric.Object.prototype * @return {Boolean} True if visible */ /** * Sets object's {@link fabric.Object#visible|visible} state * @method setVisible * @memberOf fabric.Object.prototype * @param {Boolean} value visible value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#shadow|shadow} * @method getShadow * @memberOf fabric.Object.prototype * @return {Object} Shadow instance */ /** * Retrieves object's {@link fabric.Object#stroke|stroke} * @method getStroke * @memberOf fabric.Object.prototype * @return {String} stroke value */ /** * Sets object's {@link fabric.Object#stroke|stroke} * @method setStroke * @memberOf fabric.Object.prototype * @param {String} value stroke value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#strokeWidth|strokeWidth} * @method getStrokeWidth * @memberOf fabric.Object.prototype * @return {Number} strokeWidth value */ /** * Sets object's {@link fabric.Object#strokeWidth|strokeWidth} * @method setStrokeWidth * @memberOf fabric.Object.prototype * @param {Number} value strokeWidth value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#originX|originX} * @method getOriginX * @memberOf fabric.Object.prototype * @return {String} originX value */ /** * Sets object's {@link fabric.Object#originX|originX} * @method setOriginX * @memberOf fabric.Object.prototype * @param {String} value originX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#originY|originY} * @method getOriginY * @memberOf fabric.Object.prototype * @return {String} originY value */ /** * Sets object's {@link fabric.Object#originY|originY} * @method setOriginY * @memberOf fabric.Object.prototype * @param {String} value originY value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#fill|fill} * @method getFill * @memberOf fabric.Object.prototype * @return {String} Fill value */ /** * Sets object's {@link fabric.Object#fill|fill} * @method setFill * @memberOf fabric.Object.prototype * @param {String} value Fill value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#opacity|opacity} * @method getOpacity * @memberOf fabric.Object.prototype * @return {Number} Opacity value (0-1) */ /** * Sets object's {@link fabric.Object#opacity|opacity} * @method setOpacity * @memberOf fabric.Object.prototype * @param {Number} value Opacity value (0-1) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#angle|angle} (in degrees) * @method getAngle * @memberOf fabric.Object.prototype * @return {Number} */ /** * Sets object's {@link fabric.Object#angle|angle} * @method setAngle * @memberOf fabric.Object.prototype * @param {Number} value Angle value (in degrees) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#top|top position} * @method getTop * @memberOf fabric.Object.prototype * @return {Number} Top value (in pixels) */ /** * Sets object's {@link fabric.Object#top|top position} * @method setTop * @memberOf fabric.Object.prototype * @param {Number} value Top value (in pixels) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#left|left position} * @method getLeft * @memberOf fabric.Object.prototype * @return {Number} Left value (in pixels) */ /** * Sets object's {@link fabric.Object#left|left position} * @method setLeft * @memberOf fabric.Object.prototype * @param {Number} value Left value (in pixels) * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#scaleX|scaleX} value * @method getScaleX * @memberOf fabric.Object.prototype * @return {Number} scaleX value */ /** * Sets object's {@link fabric.Object#scaleX|scaleX} value * @method setScaleX * @memberOf fabric.Object.prototype * @param {Number} value scaleX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#scaleY|scaleY} value * @method getScaleY * @memberOf fabric.Object.prototype * @return {Number} scaleY value */ /** * Sets object's {@link fabric.Object#scaleY|scaleY} value * @method setScaleY * @memberOf fabric.Object.prototype * @param {Number} value scaleY value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#flipX|flipX} value * @method getFlipX * @memberOf fabric.Object.prototype * @return {Boolean} flipX value */ /** * Sets object's {@link fabric.Object#flipX|flipX} value * @method setFlipX * @memberOf fabric.Object.prototype * @param {Boolean} value flipX value * @return {fabric.Object} thisArg * @chainable */ /** * Retrieves object's {@link fabric.Object#flipY|flipY} value * @method getFlipY * @memberOf fabric.Object.prototype * @return {Boolean} flipY value */ /** * Sets object's {@link fabric.Object#flipY|flipY} value * @method setFlipY * @memberOf fabric.Object.prototype * @param {Boolean} value flipY value * @return {fabric.Object} thisArg * @chainable */ /** * Type of an object (rect, circle, path, etc.) * @type String * @default */ type: 'object', /** * Horizontal origin of transformation of an object (one of "left", "right", "center") * @type String * @default */ originX: 'left', /** * Vertical origin of transformation of an object (one of "top", "bottom", "center") * @type String * @default */ originY: 'top', /** * Top position of an object. Note that by default it's relative to object center. You can change this by setting originY={top/center/bottom} * @type Number * @default */ top: 0, /** * Left position of an object. Note that by default it's relative to object center. You can change this by setting originX={left/center/right} * @type Number * @default */ left: 0, /** * Object width * @type Number * @default */ width: 0, /** * Object height * @type Number * @default */ height: 0, /** * Object scale factor (horizontal) * @type Number * @default */ scaleX: 1, /** * Object scale factor (vertical) * @type Number * @default */ scaleY: 1, /** * When true, an object is rendered as flipped horizontally * @type Boolean * @default */ flipX: false, /** * When true, an object is rendered as flipped vertically * @type Boolean * @default */ flipY: false, /** * Opacity of an object * @type Number * @default */ opacity: 1, /** * Angle of rotation of an object (in degrees) * @type Number * @default */ angle: 0, /** * Size of object's controlling corners (in pixels) * @type Number * @default */ cornerSize: 12, /** * When true, object's controlling corners are rendered as transparent inside (i.e. stroke instead of fill) * @type Boolean * @default */ transparentCorners: true, /** * Default cursor value used when hovering over this object on canvas * @type String * @default */ hoverCursor: null, /** * Padding between object and its controlling borders (in pixels) * @type Number * @default */ padding: 0, /** * Color of controlling borders of an object (when it's active) * @type String * @default */ borderColor: 'rgba(102,153,255,0.75)', /** * Color of controlling corners of an object (when it's active) * @type String * @default */ cornerColor: 'rgba(102,153,255,0.5)', /** * When true, this object will use center point as the origin of transformation * when being scaled via the controls. * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredScaling: false, /** * When true, this object will use center point as the origin of transformation * when being rotated via the controls. * Backwards incompatibility note: This property replaces "centerTransform" (Boolean). * @since 1.3.4 * @type Boolean * @default */ centeredRotation: true, /** * Color of object's fill * @type String * @default */ fill: 'rgb(0,0,0)', /** * Fill rule used to fill an object * accepted values are nonzero, evenodd * Backwards incompatibility note: This property was used for setting globalCompositeOperation until v1.4.12 (use `fabric.Object#globalCompositeOperation` instead) * @type String * @default */ fillRule: 'nonzero', /** * Composite rule used for canvas globalCompositeOperation * @type String * @default */ globalCompositeOperation: 'source-over', /** * Background color of an object. Only works with text objects at the moment. * @type String * @default */ backgroundColor: '', /** * When defined, an object is rendered via stroke and this property specifies its color * @type String * @default */ stroke: null, /** * Width of a stroke used to render this object * @type Number * @default */ strokeWidth: 1, /** * Array specifying dash pattern of an object's stroke (stroke must be defined) * @type Array */ strokeDashArray: null, /** * Line endings style of an object's stroke (one of "butt", "round", "square") * @type String * @default */ strokeLineCap: 'butt', /** * Corner style of an object's stroke (one of "bevil", "round", "miter") * @type String * @default */ strokeLineJoin: 'miter', /** * Maximum miter length (used for strokeLineJoin = "miter") of an object's stroke * @type Number * @default */ strokeMiterLimit: 10, /** * Shadow object representing shadow of this shape * @type fabric.Shadow * @default */ shadow: null, /** * Opacity of object's controlling borders when object is active and moving * @type Number * @default */ borderOpacityWhenMoving: 0.4, /** * Scale factor of object's controlling borders * @type Number * @default */ borderScaleFactor: 1, /** * Transform matrix (similar to SVG's transform matrix) * @type Array */ transformMatrix: null, /** * Minimum allowed scale value of an object * @type Number * @default */ minScaleLimit: 0.01, /** * When set to `false`, an object can not be selected for modification (using either point-click-based or group-based selection). * But events still fire on it. * @type Boolean * @default */ selectable: true, /** * When set to `false`, an object can not be a target of events. All events propagate through it. Introduced in v1.3.4 * @type Boolean * @default */ evented: true, /** * When set to `false`, an object is not rendered on canvas * @type Boolean * @default */ visible: true, /** * When set to `false`, object's controls are not displayed and can not be used to manipulate object * @type Boolean * @default */ hasControls: true, /** * When set to `false`, object's controlling borders are not rendered * @type Boolean * @default */ hasBorders: true, /** * When set to `false`, object's controlling rotating point will not be visible or selectable * @type Boolean * @default */ hasRotatingPoint: true, /** * Offset for object's controlling rotating point (when enabled via `hasRotatingPoint`) * @type Number * @default */ rotatingPointOffset: 40, /** * When set to `true`, objects are "found" on canvas on per-pixel basis rather than according to bounding box * @type Boolean * @default */ perPixelTargetFind: false, /** * When `false`, default object's values are not included in its serialization * @type Boolean * @default */ includeDefaultValues: true, /** * Function that determines clipping of an object (context is passed as a first argument) * Note that context origin is at the object's center point (not left/top corner) * @type Function */ clipTo: null, /** * When `true`, object horizontal movement is locked * @type Boolean * @default */ lockMovementX: false, /** * When `true`, object vertical movement is locked * @type Boolean * @default */ lockMovementY: false, /** * When `true`, object rotation is locked * @type Boolean * @default */ lockRotation: false, /** * When `true`, object horizontal scaling is locked * @type Boolean * @default */ lockScalingX: false, /** * When `true`, object vertical scaling is locked * @type Boolean * @default */ lockScalingY: false, /** * When `true`, object non-uniform scaling is locked * @type Boolean * @default */ lockUniScaling: false, /** * When `true`, object cannot be flipped by scaling into negative values * @type Boolean * @default */ lockScalingFlip: false, /** * 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 * @type Array */ stateProperties: ( 'top left width height scaleX scaleY flipX flipY originX originY transformMatrix ' + 'stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit ' + 'angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor' ).split(' '), /** * Constructor * @param {Object} [options] Options object */ initialize: function(options) { if (options) { this.setOptions(options); } }, /** * @private * @param {Object} [options] Options object */ _initGradient: function(options) { if (options.fill && options.fill.colorStops && !(options.fill instanceof fabric.Gradient)) { this.set('fill', new fabric.Gradient(options.fill)); } }, /** * @private * @param {Object} [options] Options object */ _initPattern: function(options) { if (options.fill && options.fill.source && !(options.fill instanceof fabric.Pattern)) { this.set('fill', new fabric.Pattern(options.fill)); } if (options.stroke && options.stroke.source && !(options.stroke instanceof fabric.Pattern)) { this.set('stroke', new fabric.Pattern(options.stroke)); } }, /** * @private * @param {Object} [options] Options object */ _initClipping: function(options) { if (!options.clipTo || typeof options.clipTo !== 'string') { return; } var functionBody = fabric.util.getFunctionBody(options.clipTo); if (typeof functionBody !== 'undefined') { this.clipTo = new Function('ctx', functionBody); } }, /** * Sets object's properties from options * @param {Object} [options] Options object */ setOptions: function(options) { for (var prop in options) { this.set(prop, options[prop]); } this._initGradient(options); this._initPattern(options); this._initClipping(options); }, /** * Transforms context when rendering an object * @param {CanvasRenderingContext2D} ctx Context * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node */ transform: function(ctx, fromLeft) { if (this.group) { this.group.transform(ctx, fromLeft); } var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); ctx.translate(center.x, center.y); ctx.rotate(degreesToRadians(this.angle)); ctx.scale( this.scaleX * (this.flipX ? -1 : 1), this.scaleY * (this.flipY ? -1 : 1) ); }, /** * Returns an object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, object = { type: this.type, originX: this.originX, originY: this.originY, 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, stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke, strokeWidth: toFixed(this.strokeWidth, NUM_FRACTION_DIGITS), strokeDashArray: this.strokeDashArray, strokeLineCap: this.strokeLineCap, strokeLineJoin: this.strokeLineJoin, strokeMiterLimit: toFixed(this.strokeMiterLimit, NUM_FRACTION_DIGITS), 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), shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow, visible: this.visible, clipTo: this.clipTo && String(this.clipTo), backgroundColor: this.backgroundColor, fillRule: this.fillRule, globalCompositeOperation: this.globalCompositeOperation }; if (!this.includeDefaultValues) { object = this._removeDefaultValues(object); } fabric.util.populateWithProperties(this, object, propertiesToInclude); return object; }, /** * Returns (dataless) object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toDatalessObject: function(propertiesToInclude) { // will be overwritten by subclasses return this.toObject(propertiesToInclude); }, /** * @private * @param {Object} object */ _removeDefaultValues: function(object) { var prototype = fabric.util.getKlass(object.type).prototype, stateProperties = prototype.stateProperties; stateProperties.forEach(function(prop) { if (object[prop] === prototype[prop]) { delete object[prop]; } }); return object; }, /** * Returns a string representation of an instance * @return {String} */ toString: function() { return '#'; }, /** * Basic getter * @param {String} property Property name * @return {Any} value of a property */ get: function(property) { return this[property]; }, /** * @private */ _setObject: function(obj) { for (var prop in obj) { this._set(prop, obj[prop]); } }, /** * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`. * @param {String|Object} key Property name or object (if object, iterate over the object properties) * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one) * @return {fabric.Object} thisArg * @chainable */ set: function(key, value) { if (typeof key === 'object') { this._setObject(key); } else { if (typeof value === 'function' && key !== 'clipTo') { this._set(key, value(this.get(key))); } else { this._set(key, value); } } return this; }, /** * @private * @param {String} key * @param {Any} value * @return {fabric.Object} thisArg */ _set: function(key, value) { var shouldConstrainValue = (key === 'scaleX' || key === 'scaleY'); if (shouldConstrainValue) { value = this._constrainScale(value); } if (key === 'scaleX' && value < 0) { this.flipX = !this.flipX; value *= -1; } else if (key === 'scaleY' && value < 0) { this.flipY = !this.flipY; value *= -1; } else if (key === 'width' || key === 'height') { this.minScaleLimit = toFixed(Math.min(0.1, 1/Math.max(this.width, this.height)), 2); } else if (key === 'shadow' && value && !(value instanceof fabric.Shadow)) { value = new fabric.Shadow(value); } this[key] = value; return this; }, /** * Toggles specified property from `true` to `false` or from `false` to `true` * @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; }, /** * Sets sourcePath of an object * @param {String} value Value to set sourcePath to * @return {fabric.Object} thisArg * @chainable */ setSourcePath: function(value) { this.sourcePath = value; return this; }, /** * Retrieves viewportTransform from Object's canvas if possible * @method getViewportTransform * @memberOf fabric.Object.prototype * @return {Boolean} flipY value // TODO */ getViewportTransform: function() { if (this.canvas && this.canvas.viewportTransform) { return this.canvas.viewportTransform; } return [1, 0, 0, 1, 0, 0]; }, /** * Renders an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ render: function(ctx, noTransform) { // do not render if width/height are zeros or object is not visible if ((this.width === 0 && this.height === 0) || !this.visible) { return; } ctx.save(); //setup fill rule for current object this._setupCompositeOperation(ctx); if (!noTransform) { this.transform(ctx); } this._setStrokeStyles(ctx); this._setFillStyles(ctx); if (this.group && this.group.type === 'path-group') { ctx.translate(-this.group.width/2, -this.group.height/2); } if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } this._setOpacity(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); this._restoreCompositeOperation(ctx); ctx.restore(); }, /* @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setOpacity: function(ctx) { if (this.group) { this.group._setOpacity(ctx); } ctx.globalAlpha *= this.opacity; }, _setStrokeStyles: function(ctx) { if (this.stroke) { ctx.lineWidth = this.strokeWidth; ctx.lineCap = this.strokeLineCap; ctx.lineJoin = this.strokeLineJoin; ctx.miterLimit = this.strokeMiterLimit; ctx.strokeStyle = this.stroke.toLive ? this.stroke.toLive(ctx, this) : this.stroke; } }, _setFillStyles: function(ctx) { if (this.fill) { ctx.fillStyle = this.fill.toLive ? this.fill.toLive(ctx, this) : this.fill; } }, /** * Renders controls and borders for the object * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ _renderControls: function(ctx, noTransform) { var vpt = this.getViewportTransform(); ctx.save(); if (this.active && !noTransform) { var center; if (this.group) { center = fabric.util.transformPoint(this.group.getCenterPoint(), vpt); ctx.translate(center.x, center.y); ctx.rotate(degreesToRadians(this.group.angle)); } center = fabric.util.transformPoint(this.getCenterPoint(), vpt, null != this.group); if (this.group) { center.x *= this.group.scaleX; center.y *= this.group.scaleY; } ctx.translate(center.x, center.y); ctx.rotate(degreesToRadians(this.angle)); this.drawBorders(ctx); this.drawControls(ctx); } ctx.restore(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setShadow: function(ctx) { if (!this.shadow) { return; } var mult = (this.canvas && this.canvas._currentMultiplier) || 1; ctx.shadowColor = this.shadow.color; ctx.shadowBlur = this.shadow.blur * mult * (this.scaleX + this.scaleY) / 2; ctx.shadowOffsetX = this.shadow.offsetX * mult * this.scaleX; ctx.shadowOffsetY = this.shadow.offsetY * mult * this.scaleY; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _removeShadow: function(ctx) { if (!this.shadow) { return; } ctx.shadowColor = ''; ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderFill: function(ctx) { if (!this.fill) { return; } ctx.save(); if (this.fill.gradientTransform) { var g = this.fill.gradientTransform; ctx.transform.apply(ctx, g); } if (this.fill.toLive) { ctx.translate( -this.width / 2 + this.fill.offsetX || 0, -this.height / 2 + this.fill.offsetY || 0); } if (this.fillRule === 'evenodd') { ctx.fill('evenodd'); } else { ctx.fill(); } ctx.restore(); if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderStroke: function(ctx) { if (!this.stroke || this.strokeWidth === 0) { return; } ctx.save(); if (this.strokeDashArray) { // Spec requires the concatenation of two copies the dash list when the number of elements is odd if (1 & this.strokeDashArray.length) { this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); } if (supportsLineDash) { ctx.setLineDash(this.strokeDashArray); this._stroke && this._stroke(ctx); } else { this._renderDashedStroke && this._renderDashedStroke(ctx); } ctx.stroke(); } else { if (this.stroke.gradientTransform) { var g = this.stroke.gradientTransform; ctx.transform.apply(ctx, g); } this._stroke ? this._stroke(ctx) : ctx.stroke(); } this._removeShadow(ctx); ctx.restore(); }, /** * Clones an instance * @param {Function} callback Callback is invoked with a clone as a first argument * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {fabric.Object} clone of an instance */ clone: function(callback, propertiesToInclude) { if (this.constructor.fromObject) { return this.constructor.fromObject(this.toObject(propertiesToInclude), callback); } return new fabric.Object(this.toObject(propertiesToInclude)); }, /** * Creates an instance of fabric.Image out of an object * @param {Function} callback callback, invoked with an instance as a first argument * @return {fabric.Object} thisArg */ cloneAsImage: function(callback) { var dataUrl = this.toDataURL(); fabric.util.loadImage(dataUrl, function(img) { if (callback) { callback(new fabric.Image(img)); } }); return this; }, /** * Converts an object into a data-url-like string * @param {Object} options Options object * @param {String} [options.format=png] The format of the output image. Either "jpeg" or "png" * @param {Number} [options.quality=1] Quality level (0..1). Only used for jpeg. * @param {Number} [options.multiplier=1] Multiplier to scale by * @param {Number} [options.left] Cropping left offset. Introduced in v1.2.14 * @param {Number} [options.top] Cropping top offset. Introduced in v1.2.14 * @param {Number} [options.width] Cropping width. Introduced in v1.2.14 * @param {Number} [options.height] Cropping height. Introduced in v1.2.14 * @return {String} Returns a data: URL containing a representation of the object in the format specified by options.format */ toDataURL: function(options) { options || (options = { }); var el = fabric.util.createCanvasElement(), boundingRect = this.getBoundingRect(); el.width = boundingRect.width; el.height = boundingRect.height; fabric.util.wrapElement(el, 'div'); var canvas = new fabric.Canvas(el); // to avoid common confusion https://github.com/kangax/fabric.js/issues/806 if (options.format === 'jpg') { options.format = 'jpeg'; } if (options.format === 'jpeg') { canvas.backgroundColor = '#fff'; } var origParams = { active: this.get('active'), left: this.getLeft(), top: this.getTop() }; this.set('active', false); this.setPositionByOrigin(new fabric.Point(el.width / 2, el.height / 2), 'center', 'center'); var originalCanvas = this.canvas; canvas.add(this); var data = canvas.toDataURL(options); this.set(origParams).setCoords(); this.canvas = originalCanvas; canvas.dispose(); canvas = null; return data; }, /** * Returns true if specified type is identical to the type of an instance * @param {String} type Type to check against * @return {Boolean} */ isType: function(type) { return this.type === type; }, /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return 0; }, /** * Returns a JSON representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} JSON */ toJSON: function(propertiesToInclude) { // delegate, not alias return this.toObject(propertiesToInclude); }, /** * Sets gradient (fill or stroke) of an object * Backwards incompatibility note: This method was named "setGradientFill" until v1.1.0 * @param {String} property Property name 'stroke' or 'fill' * @param {Object} [options] Options object * @param {String} [options.type] Type of gradient 'radial' or 'linear' * @param {Number} [options.x1=0] x-coordinate of start point * @param {Number} [options.y1=0] y-coordinate of start point * @param {Number} [options.x2=0] x-coordinate of end point * @param {Number} [options.y2=0] y-coordinate of end point * @param {Number} [options.r1=0] Radius of start point (only for radial gradients) * @param {Number} [options.r2=0] Radius of end point (only for radial gradients) * @param {Object} [options.colorStops] Color stops object eg. {0: 'ff0000', 1: '000000'} * @return {fabric.Object} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/58y8b/|jsFiddle demo} * @example Set linear gradient * object.setGradient('fill', { * type: 'linear', * x1: -object.width / 2, * y1: 0, * x2: object.width / 2, * y2: 0, * colorStops: { * 0: 'red', * 0.5: '#005555', * 1: 'rgba(0,0,255,0.5)' * } * }); * canvas.renderAll(); * @example Set radial gradient * object.setGradient('fill', { * type: 'radial', * x1: 0, * y1: 0, * x2: 0, * y2: 0, * r1: object.width / 2, * r2: 10, * colorStops: { * 0: 'red', * 0.5: '#005555', * 1: 'rgba(0,0,255,0.5)' * } * }); * canvas.renderAll(); */ setGradient: function(property, options) { options || (options = { }); var gradient = { colorStops: [] }; gradient.type = options.type || (options.r1 || options.r2 ? 'radial' : 'linear'); gradient.coords = { x1: options.x1, y1: options.y1, x2: options.x2, y2: options.y2 }; if (options.r1 || options.r2) { gradient.coords.r1 = options.r1; gradient.coords.r2 = options.r2; } for (var position in options.colorStops) { var color = new fabric.Color(options.colorStops[position]); gradient.colorStops.push({ offset: position, color: color.toRgb(), opacity: color.getAlpha() }); } return this.set(property, fabric.Gradient.forObject(this, gradient)); }, /** * Sets pattern fill of an object * @param {Object} options Options object * @param {(String|HTMLImageElement)} options.source Pattern source * @param {String} [options.repeat=repeat] Repeat property of a pattern (one of repeat, repeat-x, repeat-y or no-repeat) * @param {Number} [options.offsetX=0] Pattern horizontal offset from object's left/top corner * @param {Number} [options.offsetY=0] Pattern vertical offset from object's left/top corner * @return {fabric.Object} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/QT3pa/|jsFiddle demo} * @example Set pattern * fabric.util.loadImage('http://fabricjs.com/assets/escheresque_ste.png', function(img) { * object.setPatternFill({ * source: img, * repeat: 'repeat' * }); * canvas.renderAll(); * }); */ setPatternFill: function(options) { return this.set('fill', new fabric.Pattern(options)); }, /** * Sets {@link fabric.Object#shadow|shadow} of an object * @param {Object|String} [options] Options object or string (e.g. "2px 2px 10px rgba(0,0,0,0.2)") * @param {String} [options.color=rgb(0,0,0)] Shadow color * @param {Number} [options.blur=0] Shadow blur * @param {Number} [options.offsetX=0] Shadow horizontal offset * @param {Number} [options.offsetY=0] Shadow vertical offset * @return {fabric.Object} thisArg * @chainable * @see {@link http://jsfiddle.net/fabricjs/7gvJG/|jsFiddle demo} * @example Set shadow with string notation * object.setShadow('2px 2px 10px rgba(0,0,0,0.2)'); * canvas.renderAll(); * @example Set shadow with object notation * object.setShadow({ * color: 'red', * blur: 10, * offsetX: 20, * offsetY: 20 * }); * canvas.renderAll(); */ setShadow: function(options) { return this.set('shadow', options ? new fabric.Shadow(options) : null); }, /** * Sets "color" of an instance (alias of `set('fill', …)`) * @param {String} color Color value * @return {fabric.Object} thisArg * @chainable */ setColor: function(color) { this.set('fill', color); return this; }, /** * Sets "angle" of an instance * @param {Number} angle Angle value * @return {fabric.Object} thisArg * @chainable */ setAngle: function(angle) { var shouldCenterOrigin = (this.originX !== 'center' || this.originY !== 'center') && this.centeredRotation; if (shouldCenterOrigin) { this._setOriginToCenter(); } this.set('angle', angle); if (shouldCenterOrigin) { this._resetOrigin(); } return this; }, /** * Centers object horizontally on canvas to which it was added last. * You might need to call `setCoords` on an object after centering, to update controls area. * @return {fabric.Object} thisArg * @chainable */ centerH: function () { this.canvas.centerObjectH(this); return this; }, /** * Centers object vertically on canvas to which it was added last. * You might need to call `setCoords` on an object after centering, to update controls area. * @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 * You might need to call `setCoords` on an object after centering, to update controls area. * @return {fabric.Object} thisArg * @chainable */ center: function () { this.canvas.centerObject(this); return this; }, /** * Removes object from canvas to which it was added last * @return {fabric.Object} thisArg * @chainable */ remove: function() { this.canvas.remove(this); return this; }, /** * Returns coordinates of a pointer relative to an object * @param {Event} e Event to operate upon * @param {Object} [pointer] Pointer to operate upon (instead of event) * @return {Object} Coordinates of a pointer (x, y) */ getLocalPointer: function(e, pointer) { pointer = pointer || this.canvas.getPointer(e); var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top'); return { x: pointer.x - objectLeftTop.x, y: pointer.y - objectLeftTop.y }; }, /** * Sets canvas globalCompositeOperation for specific object * custom composition operation for the particular object can be specifed using globalCompositeOperation property * @param {CanvasRenderingContext2D} ctx Rendering canvas context */ _setupCompositeOperation: function (ctx) { if (this.globalCompositeOperation) { this._prevGlobalCompositeOperation = ctx.globalCompositeOperation; ctx.globalCompositeOperation = this.globalCompositeOperation; } }, /** * Restores previously saved canvas globalCompositeOperation after obeject rendering * @param {CanvasRenderingContext2D} ctx Rendering canvas context */ _restoreCompositeOperation: function (ctx) { if (this.globalCompositeOperation && this._prevGlobalCompositeOperation) { ctx.globalCompositeOperation = this._prevGlobalCompositeOperation; } } }); fabric.util.createAccessors(fabric.Object); /** * Alias for {@link fabric.Object.prototype.setAngle} * @alias rotate -> setAngle * @memberof fabric.Object */ fabric.Object.prototype.rotate = fabric.Object.prototype.setAngle; extend(fabric.Object.prototype, fabric.Observable); /** * Defines the number of fraction digits to use when serializing object values. * You can use it to increase/decrease precision of such values like left, top, scaleX, scaleY, etc. * @static * @memberof fabric.Object * @constant * @type Number */ fabric.Object.NUM_FRACTION_DIGITS = 2; /** * Unique id used internally when creating SVG elements * @static * @memberof fabric.Object * @type Number */ fabric.Object.__uid = 0; })(typeof exports !== 'undefined' ? exports : this); (function() { var degreesToRadians = fabric.util.degreesToRadians; fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Translates the coordinates from origin to center coordinates (based on the object's dimensions) * @param {fabric.Point} point The point which corresponds to the originX and originY params * @param {String} originX Horizontal origin: 'left', 'center' or 'right' * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToCenterPoint: function(point, originX, originY) { var cx = point.x, cy = point.y, strokeWidth = this.stroke ? this.strokeWidth : 0; if (originX === 'left') { cx = point.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } else if (originX === 'right') { cx = point.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } if (originY === 'top') { cy = point.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } else if (originY === 'bottom') { cy = point.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the reverse rotation to the point (it's already scaled properly) return fabric.util.rotatePoint(new fabric.Point(cx, cy), point, degreesToRadians(this.angle)); }, /** * Translates the coordinates from center to origin coordinates (based on the object's dimensions) * @param {fabric.Point} center The point which corresponds to center of the object * @param {String} originX Horizontal origin: 'left', 'center' or 'right' * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ translateToOriginPoint: function(center, originX, originY) { var x = center.x, y = center.y, strokeWidth = this.stroke ? this.strokeWidth : 0; // Get the point coordinates if (originX === 'left') { x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } else if (originX === 'right') { x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } if (originY === 'top') { y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } else if (originY === 'bottom') { y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } // Apply the rotation to the point (it's already scaled properly) return fabric.util.rotatePoint(new fabric.Point(x, y), center, degreesToRadians(this.angle)); }, /** * Returns the real center coordinates of the object * @return {fabric.Point} */ getCenterPoint: function() { var leftTop = new fabric.Point(this.left, this.top); return this.translateToCenterPoint(leftTop, this.originX, this.originY); }, /** * Returns the coordinates of the object based on center coordinates * @param {fabric.Point} point The point which corresponds to the originX and originY params * @return {fabric.Point} */ // getOriginPoint: function(center) { // return this.translateToOriginPoint(center, this.originX, this.originY); // }, /** * Returns the coordinates of the object as if it has a different origin * @param {String} originX Horizontal origin: 'left', 'center' or 'right' * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ getPointByOrigin: function(originX, originY) { var center = this.getCenterPoint(); return this.translateToOriginPoint(center, originX, originY); }, /** * Returns the point in local coordinates * @param {fabric.Point} point The point relative to the global coordinate system * @param {String} originX Horizontal origin: 'left', 'center' or 'right' * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {fabric.Point} */ toLocalPoint: function(point, originX, originY) { var center = this.getCenterPoint(), strokeWidth = this.stroke ? this.strokeWidth : 0, x, y; if (originX && originY) { if (originX === 'left') { x = center.x - (this.getWidth() + strokeWidth * this.scaleX) / 2; } else if (originX === 'right') { x = center.x + (this.getWidth() + strokeWidth * this.scaleX) / 2; } else { x = center.x; } if (originY === 'top') { y = center.y - (this.getHeight() + strokeWidth * this.scaleY) / 2; } else if (originY === 'bottom') { y = center.y + (this.getHeight() + strokeWidth * this.scaleY) / 2; } else { y = center.y; } } else { x = this.left; y = this.top; } return fabric.util.rotatePoint(new fabric.Point(point.x, point.y), center, -degreesToRadians(this.angle)) .subtractEquals(new fabric.Point(x, y)); }, /** * Returns the point in global coordinates * @param {fabric.Point} The point relative to the local coordinate system * @return {fabric.Point} */ // toGlobalPoint: function(point) { // return fabric.util.rotatePoint(point, this.getCenterPoint(), degreesToRadians(this.angle)).addEquals(new fabric.Point(this.left, this.top)); // }, /** * Sets the position of the object taking into consideration the object's origin * @param {fabric.Point} pos The new position of the object * @param {String} originX Horizontal origin: 'left', 'center' or 'right' * @param {String} originY Vertical origin: 'top', 'center' or 'bottom' * @return {void} */ setPositionByOrigin: function(pos, originX, originY) { var center = this.translateToCenterPoint(pos, originX, originY), position = this.translateToOriginPoint(center, this.originX, this.originY); this.set('left', position.x); this.set('top', position.y); }, /** * @param {String} to One of 'left', 'center', 'right' */ adjustPosition: function(to) { var angle = degreesToRadians(this.angle), hypotHalf = this.getWidth() / 2, xHalf = Math.cos(angle) * hypotHalf, yHalf = Math.sin(angle) * hypotHalf, hypotFull = this.getWidth(), xFull = Math.cos(angle) * hypotFull, yFull = Math.sin(angle) * hypotFull; if (this.originX === 'center' && to === 'left' || this.originX === 'right' && to === 'center') { // move half left this.left -= xHalf; this.top -= yHalf; } else if (this.originX === 'left' && to === 'center' || this.originX === 'center' && to === 'right') { // move half right this.left += xHalf; this.top += yHalf; } else if (this.originX === 'left' && to === 'right') { // move full right this.left += xFull; this.top += yFull; } else if (this.originX === 'right' && to === 'left') { // move full left this.left -= xFull; this.top -= yFull; } this.setCoords(); this.originX = to; }, /** * Sets the origin/position of the object to it's center point * @private * @return {void} */ _setOriginToCenter: function() { this._originalOriginX = this.originX; this._originalOriginY = this.originY; var center = this.getCenterPoint(); this.originX = 'center'; this.originY = 'center'; this.left = center.x; this.top = center.y; }, /** * Resets the origin/position of the object to it's original origin * @private * @return {void} */ _resetOrigin: function() { var originPoint = this.translateToOriginPoint( this.getCenterPoint(), this._originalOriginX, this._originalOriginY); this.originX = this._originalOriginX; this.originY = this._originalOriginY; this.left = originPoint.x; this.top = originPoint.y; this._originalOriginX = null; this._originalOriginY = null; }, /** * @private */ _getLeftTopCoords: function() { return this.translateToOriginPoint(this.getCenterPoint(), 'left', 'center'); } }); })(); (function() { var degreesToRadians = fabric.util.degreesToRadians; fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Object containing coordinates of object's controls * @type Object * @default */ oCoords: null, /** * Checks if object intersects with an area formed by 2 points * @param {Object} pointTL top-left point of area * @param {Object} pointBR bottom-right point of area * @return {Boolean} true if object intersects with an area formed by 2 points */ intersectsWithRect: function(pointTL, pointBR) { 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), intersection = fabric.Intersection.intersectPolygonRectangle( [tl, tr, br, bl], pointTL, pointBR ); return intersection.status === 'Intersection'; }, /** * Checks if object intersects with another object * @param {Object} other Object to test * @return {Boolean} true if object intersects with another object */ 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), intersection = fabric.Intersection.intersectPolygonPolygon( [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl], [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl] ); return intersection.status === 'Intersection'; }, /** * Checks if object is fully contained within area of another object * @param {Object} other Object to test * @return {Boolean} true if object is fully contained within area of another object */ isContainedWithinObject: function(other) { var boundingRect = other.getBoundingRect(), point1 = new fabric.Point(boundingRect.left, boundingRect.top), point2 = new fabric.Point(boundingRect.left + boundingRect.width, boundingRect.top + boundingRect.height); return this.isContainedWithinRect(point1, point2); }, /** * Checks if object is fully contained within area formed by 2 points * @param {Object} pointTL top-left point of area * @param {Object} pointBR bottom-right point of area * @return {Boolean} true if object is fully contained within area formed by 2 points */ isContainedWithinRect: function(pointTL, pointBR) { var boundingRect = this.getBoundingRect(); return ( boundingRect.left >= pointTL.x && boundingRect.left + boundingRect.width <= pointBR.x && boundingRect.top >= pointTL.y && boundingRect.top + boundingRect.height <= pointBR.y ); }, /** * Checks if point is inside the object * @param {fabric.Point} point Point to check against * @return {Boolean} true if point is inside the object */ containsPoint: function(point) { var lines = this._getImageLines(this.oCoords), xPoints = this._findCrossPoints(point, lines); // if xPoints is odd then point is inside the object return (xPoints !== 0 && xPoints % 2 === 1); }, /** * Method that returns an object with the object edges in it, given the coordinates of the corners * @private * @param {Object} oCoords Coordinates of the object corners */ _getImageLines: function(oCoords) { return { topline: { o: oCoords.tl, d: oCoords.tr }, rightline: { o: oCoords.tr, d: oCoords.br }, bottomline: { o: oCoords.br, d: oCoords.bl }, leftline: { o: oCoords.bl, d: oCoords.tl } }; }, /** * Helper method to determine how many cross points are between the 4 object edges * and the horizontal line determined by a point on canvas * @private * @param {fabric.Point} point Point to check * @param {Object} oCoords Coordinates of the object being evaluated */ _findCrossPoints: function(point, oCoords) { var b1, b2, a1, a2, xi, yi, xcount = 0, iLine; for (var lineKey in oCoords) { iLine = oCoords[lineKey]; // optimisation 1: line below point. no cross if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { continue; } // optimisation 2: line above point. no cross if ((iLine.o.y >= point.y) && (iLine.d.y >= point.y)) { continue; } // optimisation 3: vertical line case if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= point.x)) { xi = iLine.o.x; yi = point.y; } // calculate the intersection point else { b1 = 0; b2 = (iLine.d.y - iLine.o.y) / (iLine.d.x - iLine.o.x); a1 = point.y - b1 * point.x; a2 = iLine.o.y - b2 * iLine.o.x; xi = - (a1 - a2) / (b1 - b2); yi = a1 + b1 * xi; } // dont count xi < point.x cases if (xi >= point.x) { xcount += 1; } // optimisation 4: specific for square images if (xcount === 2) { break; } } return xcount; }, /** * Returns width of an object's bounding rectangle * @deprecated since 1.0.4 * @return {Number} width value */ getBoundingRectWidth: function() { return this.getBoundingRect().width; }, /** * Returns height of an object's bounding rectangle * @deprecated since 1.0.4 * @return {Number} height value */ getBoundingRectHeight: function() { return this.getBoundingRect().height; }, /** * Returns coordinates of object's bounding rectangle (left, top, width, height) * @return {Object} Object with left, top, width, height properties */ getBoundingRect: function() { this.oCoords || this.setCoords(); var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x], minX = fabric.util.array.min(xCoords), maxX = fabric.util.array.max(xCoords), width = Math.abs(minX - maxX), yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y], minY = fabric.util.array.min(yCoords), maxY = fabric.util.array.max(yCoords), height = Math.abs(minY - maxY); return { left: minX, top: minY, width: width, height: height }; }, /** * Returns width of an object * @return {Number} width value */ getWidth: function() { return this.width * this.scaleX; }, /** * Returns height of an object * @return {Number} height value */ getHeight: function() { return this.height * this.scaleY; }, /** * Makes sure the scale is valid and modifies it if necessary * @private * @param {Number} value * @return {Number} */ _constrainScale: function(value) { if (Math.abs(value) < this.minScaleLimit) { if (value < 0) { return -this.minScaleLimit; } else { return this.minScaleLimit; } } return value; }, /** * Scales an object (equally by x and y) * @param {Number} value Scale factor * @return {fabric.Object} thisArg * @chainable */ scale: function(value) { value = this._constrainScale(value); if (value < 0) { this.flipX = !this.flipX; this.flipY = !this.flipY; value *= -1; } 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) * @param {Number} value 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) * @param {Number} value 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 corner position coordinates based on current angle, width and height * @return {fabric.Object} thisArg * @chainable */ setCoords: function() { var strokeWidth = this.strokeWidth, theta = degreesToRadians(this.angle), vpt = this.getViewportTransform(), f = function (p) { return fabric.util.transformPoint(p, vpt); }, w = this.width, h = this.height, capped = this.strokeLineCap === 'round' || this.strokeLineCap === 'square', vLine = this.type === 'line' && this.width === 0, hLine = this.type === 'line' && this.height === 0, sLine = vLine || hLine, strokeW = (capped && hLine) || !sLine, strokeH = (capped && vLine) || !sLine; if (vLine) { w = strokeWidth; } else if (hLine) { h = strokeWidth; } if (strokeW) { w += w > 0 ? strokeWidth : -strokeWidth; } if (strokeH) { h += h > 0 ? strokeWidth : -strokeWidth; } this.currentWidth = w * this.scaleX; this.currentHeight = h * this.scaleY; // If width is negative, make postive. Fixes path selection issue if (this.currentWidth < 0) { this.currentWidth = Math.abs(this.currentWidth); } var _hypotenuse = Math.sqrt( Math.pow(this.currentWidth / 2, 2) + Math.pow(this.currentHeight / 2, 2)), _angle = Math.atan( isFinite(this.currentHeight / this.currentWidth) ? this.currentHeight / this.currentWidth : 0), // offset added for rotate and scale actions offsetX = Math.cos(_angle + theta) * _hypotenuse, offsetY = Math.sin(_angle + theta) * _hypotenuse, sinTh = Math.sin(theta), cosTh = Math.cos(theta), coords = this.getCenterPoint(), wh = new fabric.Point(this.currentWidth, this.currentHeight), _tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY), _tr = new fabric.Point(_tl.x + (wh.x * cosTh), _tl.y + (wh.x * sinTh)), _bl = new fabric.Point(_tl.x - (wh.y * sinTh), _tl.y + (wh.y * cosTh)), _mt = new fabric.Point(_tl.x + (wh.x/2 * cosTh), _tl.y + (wh.x/2 * sinTh)), tl = f(_tl), tr = f(_tr), br = f(new fabric.Point(_tr.x - (wh.y * sinTh), _tr.y + (wh.y * cosTh))), bl = f(_bl), ml = f(new fabric.Point(_tl.x - (wh.y/2 * sinTh), _tl.y + (wh.y/2 * cosTh))), mt = f(_mt), mr = f(new fabric.Point(_tr.x - (wh.y/2 * sinTh), _tr.y + (wh.y/2 * cosTh))), mb = f(new fabric.Point(_bl.x + (wh.x/2 * cosTh), _bl.y + (wh.x/2 * sinTh))), mtr = f(new fabric.Point(_mt.x, _mt.y)), // padding padX = Math.cos(_angle + theta) * this.padding * Math.sqrt(2), padY = Math.sin(_angle + theta) * this.padding * Math.sqrt(2); tl = tl.add(new fabric.Point(-padX, -padY)); tr = tr.add(new fabric.Point(padY, -padX)); br = br.add(new fabric.Point(padX, padY)); bl = bl.add(new fabric.Point(-padY, padX)); ml = ml.add(new fabric.Point((-padX - padY) / 2, (-padY + padX) / 2)); mt = mt.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); mr = mr.add(new fabric.Point((padY + padX) / 2, (padY - padX) / 2)); mb = mb.add(new fabric.Point((padX - padY) / 2, (padX + padY) / 2)); mtr = mtr.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); // 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); this.oCoords = { // corners tl: tl, tr: tr, br: br, bl: bl, // middle ml: ml, mt: mt, mr: mr, mb: mb, // rotating point mtr: mtr }; // set coordinates of the draggable boxes in the corners used to scale/rotate the image this._setCornerCoords && this._setCornerCoords(); return this; } }); })(); fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Moves an object to the bottom of the stack of drawn objects * @return {fabric.Object} thisArg * @chainable */ sendToBack: function() { if (this.group) { fabric.StaticCanvas.prototype.sendToBack.call(this.group, this); } else { this.canvas.sendToBack(this); } return this; }, /** * Moves an object to the top of the stack of drawn objects * @return {fabric.Object} thisArg * @chainable */ bringToFront: function() { if (this.group) { fabric.StaticCanvas.prototype.bringToFront.call(this.group, this); } else { this.canvas.bringToFront(this); } return this; }, /** * Moves an object down in stack of drawn objects * @param {Boolean} [intersecting] If `true`, send object behind next lower intersecting object * @return {fabric.Object} thisArg * @chainable */ sendBackwards: function(intersecting) { if (this.group) { fabric.StaticCanvas.prototype.sendBackwards.call(this.group, this, intersecting); } else { this.canvas.sendBackwards(this, intersecting); } return this; }, /** * Moves an object up in stack of drawn objects * @param {Boolean} [intersecting] If `true`, send object in front of next upper intersecting object * @return {fabric.Object} thisArg * @chainable */ bringForward: function(intersecting) { if (this.group) { fabric.StaticCanvas.prototype.bringForward.call(this.group, this, intersecting); } else { this.canvas.bringForward(this, intersecting); } return this; }, /** * Moves an object to specified level in stack of drawn objects * @param {Number} index New position of object * @return {fabric.Object} thisArg * @chainable */ moveTo: function(index) { if (this.group) { fabric.StaticCanvas.prototype.moveTo.call(this.group, this, index); } else { this.canvas.moveTo(this, index); } return this; } }); /* _TO_SVG_START_ */ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Returns styles-string for svg-export * @return {String} */ getSvgStyles: function() { var fill = this.fill ? (this.fill.toLive ? 'url(#SVGID_' + this.fill.id + ')' : this.fill) : 'none', fillRule = this.fillRule, stroke = this.stroke ? (this.stroke.toLive ? 'url(#SVGID_' + this.stroke.id + ')' : this.stroke) : 'none', strokeWidth = this.strokeWidth ? this.strokeWidth : '0', strokeDashArray = this.strokeDashArray ? this.strokeDashArray.join(' ') : '', strokeLineCap = this.strokeLineCap ? this.strokeLineCap : 'butt', strokeLineJoin = this.strokeLineJoin ? this.strokeLineJoin : 'miter', strokeMiterLimit = this.strokeMiterLimit ? this.strokeMiterLimit : '4', opacity = typeof this.opacity !== 'undefined' ? this.opacity : '1', visibility = this.visible ? '' : ' visibility: hidden;', filter = this.shadow && this.type !== 'text' ? 'filter: url(#SVGID_' + this.shadow.id + ');' : ''; return [ 'stroke: ', stroke, '; ', 'stroke-width: ', strokeWidth, '; ', 'stroke-dasharray: ', strokeDashArray, '; ', 'stroke-linecap: ', strokeLineCap, '; ', 'stroke-linejoin: ', strokeLineJoin, '; ', 'stroke-miterlimit: ', strokeMiterLimit, '; ', 'fill: ', fill, '; ', 'fill-rule: ', fillRule, '; ', 'opacity: ', opacity, ';', filter, visibility ].join(''); }, /** * Returns transform-string for svg-export * @return {String} */ getSvgTransform: function() { if (this.group && this.group.type === 'path-group') { return ''; } var toFixed = fabric.util.toFixed, angle = this.getAngle(), vpt = !this.canvas || this.canvas.svgViewportTransformation ? this.getViewportTransform() : [1, 0, 0, 1, 0, 0], center = fabric.util.transformPoint(this.getCenterPoint(), vpt), NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, translatePart = this.type === 'path-group' ? '' : 'translate(' + toFixed(center.x, NUM_FRACTION_DIGITS) + ' ' + toFixed(center.y, NUM_FRACTION_DIGITS) + ')', anglePart = angle !== 0 ? (' rotate(' + toFixed(angle, NUM_FRACTION_DIGITS) + ')') : '', scalePart = (this.scaleX === 1 && this.scaleY === 1 && vpt[0] === 1 && vpt[3] === 1) ? '' : (' scale(' + toFixed(this.scaleX * vpt[0], NUM_FRACTION_DIGITS) + ' ' + toFixed(this.scaleY * vpt[3], NUM_FRACTION_DIGITS) + ')'), addTranslateX = this.type === 'path-group' ? this.width * vpt[0] : 0, flipXPart = this.flipX ? ' matrix(-1 0 0 1 ' + addTranslateX + ' 0) ' : '', addTranslateY = this.type === 'path-group' ? this.height * vpt[3] : 0, flipYPart = this.flipY ? ' matrix(1 0 0 -1 0 ' + addTranslateY + ')' : ''; return [ translatePart, anglePart, scalePart, flipXPart, flipYPart ].join(''); }, /** * Returns transform-string for svg-export from the transform matrix of single elements * @return {String} */ getSvgTransformMatrix: function() { return this.transformMatrix ? ' matrix(' + this.transformMatrix.join(' ') + ')' : ''; }, /** * @private */ _createBaseSVGMarkup: function() { var markup = [ ]; if (this.fill && this.fill.toLive) { markup.push(this.fill.toSVG(this, false)); } if (this.stroke && this.stroke.toLive) { markup.push(this.stroke.toSVG(this, false)); } if (this.shadow) { markup.push(this.shadow.toSVG(this)); } return markup; } }); /* _TO_SVG_END_ */ /* Depends on `stateProperties` */ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Returns true if object state (one of its state properties) was changed * @return {Boolean} true if instance' state has changed since `{@link fabric.Object#saveState}` was called */ hasStateChanged: function() { return this.stateProperties.some(function(prop) { return this.get(prop) !== this.originalState[prop]; }, this); }, /** * Saves state of an object * @param {Object} [options] Object with additional `stateProperties` array to include when saving state * @return {fabric.Object} thisArg */ saveState: function(options) { this.stateProperties.forEach(function(prop) { this.originalState[prop] = this.get(prop); }, this); if (options && options.stateProperties) { options.stateProperties.forEach(function(prop) { this.originalState[prop] = this.get(prop); }, this); } return this; }, /** * Setups state of an object * @return {fabric.Object} thisArg */ setupState: function() { this.originalState = { }; this.saveState(); return this; } }); (function() { var degreesToRadians = fabric.util.degreesToRadians, //jscs:disable requireCamelCaseOrUpperCaseIdentifiers isVML = function() { return typeof G_vmlCanvasManager !== 'undefined'; }; //jscs:enable requireCamelCaseOrUpperCaseIdentifiers fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * The object interactivity controls. * @private */ _controlsVisibility: null, /** * Determines which corner has been clicked * @private * @param {Object} pointer The pointer indicating the mouse position * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ _findTargetCorner: function(pointer) { if (!this.hasControls || !this.active) { return false; } var ex = pointer.x, ey = pointer.y, xPoints, lines; for (var i in this.oCoords) { if (!this.isControlVisible(i)) { continue; } if (i === 'mtr' && !this.hasRotatingPoint) { continue; } if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) { continue; } lines = this._getImageLines(this.oCoords[i].corner); // 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({ x: ex, y: ey }, lines); if (xPoints !== 0 && xPoints % 2 === 1) { this.__corner = i; return i; } } return false; }, /** * Sets the coordinates of the draggable boxes in the corners of * the image used to scale/rotate it. * @private */ _setCornerCoords: function() { var coords = this.oCoords, theta = degreesToRadians(this.angle), newTheta = degreesToRadians(45 - this.angle), cornerHypotenuse = Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, cosHalfOffset = cornerHypotenuse * Math.cos(newTheta), sinHalfOffset = cornerHypotenuse * Math.sin(newTheta), sinTh = Math.sin(theta), cosTh = Math.cos(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) } }; }, /** * Draws borders of an object's bounding box. * Requires public properties: width, height * Requires public options: padding, borderColor * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg * @chainable */ drawBorders: function(ctx) { if (!this.hasBorders) { return this; } var padding = this.padding, padding2 = padding * 2, vpt = this.getViewportTransform(); ctx.save(); ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = this.borderColor; var scaleX = 1 / this._constrainScale(this.scaleX), scaleY = 1 / this._constrainScale(this.scaleY); ctx.lineWidth = 1 / this.borderScaleFactor; var w = this.getWidth(), h = this.getHeight(), strokeWidth = this.strokeWidth, capped = this.strokeLineCap === 'round' || this.strokeLineCap === 'square', vLine = this.type === 'line' && this.width === 0, hLine = this.type === 'line' && this.height === 0, sLine = vLine || hLine, strokeW = (capped && hLine) || !sLine, strokeH = (capped && vLine) || !sLine; if (vLine) { w = strokeWidth / scaleX; } else if (hLine) { h = strokeWidth / scaleY; } if (strokeW) { w += strokeWidth / scaleX; } if (strokeH) { h += strokeWidth / scaleY; } var wh = fabric.util.transformPoint(new fabric.Point(w, h), vpt, true), width = wh.x, height = wh.y; if (this.group) { width = width * this.group.scaleX; height = height * this.group.scaleY; } ctx.strokeRect( ~~(-(width / 2) - padding) - 0.5, // offset needed to make lines look sharper ~~(-(height / 2) - padding) - 0.5, ~~(width + padding2) + 1, // double offset needed to make lines look sharper ~~(height + padding2) + 1 ); if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( -height - (padding * 2)) / 2; ctx.beginPath(); ctx.moveTo(0, rotateHeight); ctx.lineTo(0, rotateHeight - this.rotatingPointOffset); ctx.closePath(); ctx.stroke(); } ctx.restore(); return this; }, /** * Draws corners of an object's bounding box. * Requires public properties: width, height * Requires public options: cornerSize, padding * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg * @chainable */ drawControls: function(ctx) { if (!this.hasControls) { return this; } var size = this.cornerSize, size2 = size / 2, vpt = this.getViewportTransform(), strokeWidth = this.strokeWidth, w = this.width, h = this.height, capped = this.strokeLineCap === 'round' || this.strokeLineCap === 'square', vLine = this.type === 'line' && this.width === 0, hLine = this.type === 'line' && this.height === 0, sLine = vLine || hLine, strokeW = (capped && hLine) || !sLine, strokeH = (capped && vLine) || !sLine; if (vLine) { w = strokeWidth; } else if (hLine) { h = strokeWidth; } if (strokeW) { w += strokeWidth; } if (strokeH) { h += strokeWidth; } w *= this.scaleX; h *= this.scaleY; var wh = fabric.util.transformPoint(new fabric.Point(w, h), vpt, true), width = wh.x, height = wh.y, left = -(width / 2), top = -(height / 2), padding = this.padding, scaleOffset = size2, scaleOffsetSize = size2 - size, methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; ctx.save(); ctx.lineWidth = 1; ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left this._drawControl('tl', ctx, methodName, left - scaleOffset - padding, top - scaleOffset - padding); // top-right this._drawControl('tr', ctx, methodName, left + width - scaleOffset + padding, top - scaleOffset - padding); // bottom-left this._drawControl('bl', ctx, methodName, left - scaleOffset - padding, top + height + scaleOffsetSize + padding); // bottom-right this._drawControl('br', ctx, methodName, left + width + scaleOffsetSize + padding, top + height + scaleOffsetSize + padding); if (!this.get('lockUniScaling')) { // middle-top this._drawControl('mt', ctx, methodName, left + width/2 - scaleOffset, top - scaleOffset - padding); // middle-bottom this._drawControl('mb', ctx, methodName, left + width/2 - scaleOffset, top + height + scaleOffsetSize + padding); // middle-right this._drawControl('mr', ctx, methodName, left + width + scaleOffsetSize + padding, top + height/2 - scaleOffset); // middle-left this._drawControl('ml', ctx, methodName, left - scaleOffset - padding, top + height/2 - scaleOffset); } // middle-top-rotate if (this.hasRotatingPoint) { this._drawControl('mtr', ctx, methodName, left + width/2 - scaleOffset, top - this.rotatingPointOffset - this.cornerSize/2 - padding); } ctx.restore(); return this; }, /** * @private */ _drawControl: function(control, ctx, methodName, left, top) { var size = this.cornerSize; if (this.isControlVisible(control)) { isVML() || this.transparentCorners || ctx.clearRect(left, top, size, size); ctx[methodName](left, top, size, size); } }, /** * Returns true if the specified control is visible, false otherwise. * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. * @returns {Boolean} true if the specified control is visible, false otherwise */ isControlVisible: function(controlName) { return this._getControlsVisibility()[controlName]; }, /** * Sets the visibility of the specified control. * @param {String} controlName The name of the control. Possible values are 'tl', 'tr', 'br', 'bl', 'ml', 'mt', 'mr', 'mb', 'mtr'. * @param {Boolean} visible true to set the specified control visible, false otherwise * @return {fabric.Object} thisArg * @chainable */ setControlVisible: function(controlName, visible) { this._getControlsVisibility()[controlName] = visible; return this; }, /** * Sets the visibility state of object controls. * @param {Object} [options] Options object * @param {Boolean} [options.bl] true to enable the bottom-left control, false to disable it * @param {Boolean} [options.br] true to enable the bottom-right control, false to disable it * @param {Boolean} [options.mb] true to enable the middle-bottom control, false to disable it * @param {Boolean} [options.ml] true to enable the middle-left control, false to disable it * @param {Boolean} [options.mr] true to enable the middle-right control, false to disable it * @param {Boolean} [options.mt] true to enable the middle-top control, false to disable it * @param {Boolean} [options.tl] true to enable the top-left control, false to disable it * @param {Boolean} [options.tr] true to enable the top-right control, false to disable it * @param {Boolean} [options.mtr] true to enable the middle-top-rotate control, false to disable it * @return {fabric.Object} thisArg * @chainable */ setControlsVisibility: function(options) { options || (options = { }); for (var p in options) { this.setControlVisible(p, options[p]); } return this; }, /** * Returns the instance of the control visibility set for this object. * @private * @returns {Object} */ _getControlsVisibility: function() { if (!this._controlsVisibility) { this._controlsVisibility = { tl: true, tr: true, br: true, bl: true, ml: true, mt: true, mr: true, mb: true, mtr: true }; } return this._controlsVisibility; } }); })(); fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** * Animation duration (in ms) for fx* methods * @type Number * @default */ FX_DURATION: 500, /** * Centers object horizontally with animation. * @param {fabric.Object} object Object to center * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties * @param {Function} [callbacks.onComplete] Invoked on completion * @param {Function} [callbacks.onChange] Invoked on every step of animation * @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. * @param {fabric.Object} object Object to center * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties * @param {Function} [callbacks.onComplete] Invoked on completion * @param {Function} [callbacks.onChange] Invoked on every step of animation * @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 * @param {fabric.Object} object Object to remove * @param {Object} [callbacks] Callbacks object with optional "onComplete" and/or "onChange" properties * @param {Function} [callbacks.onComplete] Invoked on completion * @param {Function} [callbacks.onChange] Invoked on every step of animation * @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.set('active', false); }, onChange: function(value) { object.set('opacity', value); _this.renderAll(); onChange(); }, onComplete: function () { _this.remove(object); onComplete(); } }); return this; } }); fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * Animates object's properties * @param {String|Object} property Property to animate (if string) or properties to animate (if object) * @param {Number|Object} value Value to animate property to (if string was given first) or options object * @return {fabric.Object} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#animation} * @chainable * * 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') { var propsToAnimate = [ ], prop, skipCallbacks; for (prop in arguments[0]) { propsToAnimate.push(prop); } for (var i = 0, len = propsToAnimate.length; i < len; i++) { prop = propsToAnimate[i]; skipCallbacks = i !== len - 1; this._animate(prop, arguments[0][prop], arguments[1], skipCallbacks); } } else { this._animate.apply(this, arguments); } return this; }, /** * @private * @param {String} property Property to animate * @param {String} to Value to animate to * @param {Object} [options] Options object * @param {Boolean} [skipCallbacks] When true, callbacks like onchange and oncomplete are not invoked */ _animate: function(property, to, options, skipCallbacks) { var _this = this, propPair; to = to.toString(); if (!options) { options = { }; } else { options = fabric.util.object.clone(options); } if (~property.indexOf('.')) { propPair = property.split('.'); } var currentValue = propPair ? this.get(propPair[0])[propPair[1]] : this.get(property); if (!('from' in options)) { options.from = currentValue; } if (~to.indexOf('=')) { to = currentValue + parseFloat(to.replace('=', '')); } else { to = parseFloat(to); } fabric.util.animate({ startValue: options.from, endValue: to, byValue: options.by, easing: options.easing, duration: options.duration, abort: options.abort && function() { return options.abort.call(_this); }, onChange: function(value) { if (propPair) { _this[propPair[0]][propPair[1]] = value; } else { _this.set(property, value); } if (skipCallbacks) { return; } options.onChange && options.onChange(); }, onComplete: function() { if (skipCallbacks) { return; } _this.setCoords(); options.onComplete && options.onComplete(); } }); } }); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, coordProps = { x1: 1, x2: 1, y1: 1, y2: 1 }, supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); if (fabric.Line) { fabric.warn('fabric.Line is already defined'); return; } /** * Line class * @class fabric.Line * @extends fabric.Object * @see {@link fabric.Line#initialize} for constructor definition */ fabric.Line = fabric.util.createClass(fabric.Object, /** @lends fabric.Line.prototype */ { /** * Type of an object * @type String * @default */ type: 'line', /** * x value or first line edge * @type Number * @default */ x1: 0, /** * y value or first line edge * @type Number * @default */ y1: 0, /** * x value or second line edge * @type Number * @default */ x2: 0, /** * y value or second line edge * @type Number * @default */ y2: 0, /** * Constructor * @param {Array} [points] Array of points * @param {Object} [options] Options object * @return {fabric.Line} thisArg */ initialize: function(points, options) { options = 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 * @param {Object} [options] Options */ _setWidthHeight: function(options) { options || (options = { }); this.width = Math.abs(this.x2 - this.x1); this.height = Math.abs(this.y2 - this.y1); this.left = 'left' in options ? options.left : this._getLeftToOriginX(); this.top = 'top' in options ? options.top : this._getTopToOriginY(); }, /** * @private * @param {String} key * @param {Any} value */ _set: function(key, value) { this.callSuper('_set', key, value); if (typeof coordProps[key] !== 'undefined') { this._setWidthHeight(); } return this; }, /** * @private * @return {Number} leftToOriginX Distance from left edge of canvas to originX of Line. */ _getLeftToOriginX: makeEdgeToOriginGetter( { // property names origin: 'originX', axis1: 'x1', axis2: 'x2', dimension: 'width' }, { // possible values of origin nearest: 'left', center: 'center', farthest: 'right' } ), /** * @private * @return {Number} topToOriginY Distance from top edge of canvas to originY of Line. */ _getTopToOriginY: makeEdgeToOriginGetter( { // property names origin: 'originY', axis1: 'y1', axis2: 'y2', dimension: 'height' }, { // possible values of origin nearest: 'top', center: 'center', farthest: 'bottom' } ), /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx, noTransform) { ctx.beginPath(); if (noTransform) { // Line coords are distances from left-top of canvas to origin of line. // To render line in a path-group, we need to translate them to // distances from center of path-group to center of line. var cp = this.getCenterPoint(); ctx.translate( cp.x - this.strokeWidth / 2, cp.y - this.strokeWidth / 2 ); } if (!this.strokeDashArray || this.strokeDashArray && supportsLineDash) { // move from center (of virtual box) to its left/top corner // we can't assume x1, y1 is top left and x2, y2 is bottom right var p = this.calcLinePoints(); ctx.moveTo(p.x1, p.y1); ctx.lineTo(p.x2, p.y2); } 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 = this.stroke || ctx.fillStyle; this.stroke && this._renderStroke(ctx); ctx.strokeStyle = origStrokeStyle; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var p = this.calcLinePoints(); ctx.beginPath(); fabric.util.drawDashedLine(ctx, p.x1, p.y1, p.x2, p.y2, this.strokeDashArray); ctx.closePath(); }, /** * Returns object representation of an instance * @methd toObject * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), this.calcLinePoints()); }, /** * @private * Recalculate line points from width and height. */ calcLinePoints: function() { var xMult = this.x1 <= this.x2 ? -1 : 1, yMult = this.y1 <= this.y2 ? -1 : 1, x1 = (xMult * this.width * 0.5), y1 = (yMult * this.height * 0.5), x2 = (xMult * this.width * -0.5), y2 = (yMult * this.height * -0.5); return { x1: x1, x2: x2, y1: y1, y2: y2 }; }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), p = { x1: this.x1, x2: this.x2, y1: this.y1, y2: this.y2 }; if (!(this.group && this.group.type === 'path-group')) { p = this.calcLinePoints(); } markup.push( '\n' ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns complexity of an instance * @return {Number} complexity */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Line.fromElement}) * @static * @memberOf fabric.Line * @see http://www.w3.org/TR/SVG/shapes.html#LineElement */ fabric.Line.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x1 y1 x2 y2'.split(' ')); /** * Returns fabric.Line instance from an SVG element * @static * @memberOf fabric.Line * @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), points = [ parsedAttributes.x1 || 0, parsedAttributes.y1 || 0, parsedAttributes.x2 || 0, parsedAttributes.y2 || 0 ]; return new fabric.Line(points, extend(parsedAttributes, options)); }; /* _FROM_SVG_END_ */ /** * Returns fabric.Line instance from an object representation * @static * @memberOf fabric.Line * @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); }; /** * Produces a function that calculates distance from canvas edge to Line origin. */ function makeEdgeToOriginGetter(propertyNames, originValues) { var origin = propertyNames.origin, axis1 = propertyNames.axis1, axis2 = propertyNames.axis2, dimension = propertyNames.dimension, nearest = originValues.nearest, center = originValues.center, farthest = originValues.farthest; return function() { switch (this.get(origin)) { case nearest: return Math.min(this.get(axis1), this.get(axis2)); case center: return Math.min(this.get(axis1), this.get(axis2)) + (0.5 * this.get(dimension)); case farthest: return Math.max(this.get(axis1), this.get(axis2)); } }; } })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), pi = Math.PI, extend = fabric.util.object.extend; if (fabric.Circle) { fabric.warn('fabric.Circle is already defined.'); return; } /** * Circle class * @class fabric.Circle * @extends fabric.Object * @see {@link fabric.Circle#initialize} for constructor definition */ fabric.Circle = fabric.util.createClass(fabric.Object, /** @lends fabric.Circle.prototype */ { /** * Type of an object * @type String * @default */ type: 'circle', /** * Radius of this circle * @type Number * @default */ radius: 0, /** * Start angle of the circle, moving clockwise * @type Number * @default 0 */ startAngle: 0, /** * End angle of the circle * @type Number * @default 2Pi */ endAngle: pi * 2, /** * Constructor * @param {Object} [options] Options object * @return {fabric.Circle} thisArg */ initialize: function(options) { options = options || { }; this.callSuper('initialize', options); this.set('radius', options.radius || 0); this.startAngle = options.startAngle || this.startAngle; this.endAngle = options.endAngle || this.endAngle; }, /** * @private * @param {String} key * @param {Any} value * @return {fabric.Circle} thisArg */ _set: function(key, value) { this.callSuper('_set', key, value); if (key === 'radius') { this.setRadius(value); } return this; }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { radius: this.get('radius'), startAngle: this.startAngle, endAngle: this.endAngle }); }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), x = 0, y = 0, angle = (this.endAngle - this.startAngle) % ( 2 * pi); if (angle === 0) { if (this.group && this.group.type === 'path-group') { x = this.left + this.radius; y = this.top + this.radius; } markup.push( '\n' ); } else { var startX = Math.cos(this.startAngle) * this.radius, startY = Math.sin(this.startAngle) * this.radius, endX = Math.cos(this.endAngle) * this.radius, endY = Math.sin(this.endAngle) * this.radius, largeFlag = angle > pi ? '1' : '0'; markup.push( '\n' ); } return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ _render: function(ctx, noTransform) { ctx.beginPath(); ctx.arc(noTransform ? this.left + this.radius : 0, noTransform ? this.top + this.radius : 0, this.radius, this.startAngle, this.endAngle, false); this._renderFill(ctx); this._renderStroke(ctx); }, /** * Returns horizontal radius of an object (according to how an object is scaled) * @return {Number} */ getRadiusX: function() { return this.get('radius') * this.get('scaleX'); }, /** * Returns vertical radius of an object (according to how an object is scaled) * @return {Number} */ getRadiusY: function() { return this.get('radius') * this.get('scaleY'); }, /** * Sets radius of an object (and updates width accordingly) * @return {Number} */ setRadius: function(value) { this.radius = value; this.set('width', value * 2).set('height', value * 2); }, /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Circle.fromElement}) * @static * @memberOf fabric.Circle * @see: http://www.w3.org/TR/SVG/shapes.html#CircleElement */ fabric.Circle.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy r'.split(' ')); /** * Returns {@link fabric.Circle} instance from an SVG element * @static * @memberOf fabric.Circle * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @throws {Error} If value of `r` attribute is missing or invalid * @return {fabric.Circle} Instance of fabric.Circle */ fabric.Circle.fromElement = function(element, options) { options || (options = { }); var parsedAttributes = fabric.parseAttributes(element, fabric.Circle.ATTRIBUTE_NAMES); if (!isValidRadius(parsedAttributes)) { throw new Error('value of `r` attribute is required and can not be negative'); } parsedAttributes.left = parsedAttributes.left || 0; parsedAttributes.top = parsedAttributes.top || 0; var obj = new fabric.Circle(extend(parsedAttributes, options)); obj.left -= obj.radius; obj.top -= obj.radius; return obj; }; /** * @private */ function isValidRadius(attributes) { return (('radius' in attributes) && (attributes.radius > 0)); } /* _FROM_SVG_END_ */ /** * Returns {@link fabric.Circle} instance from an object representation * @static * @memberOf fabric.Circle * @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; } /** * Triangle class * @class fabric.Triangle * @extends fabric.Object * @return {fabric.Triangle} thisArg * @see {@link fabric.Triangle#initialize} for constructor definition */ fabric.Triangle = fabric.util.createClass(fabric.Object, /** @lends fabric.Triangle.prototype */ { /** * Type of an object * @type String * @default */ type: 'triangle', /** * Constructor * @param {Object} [options] 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 * @param {CanvasRenderingContext2D} ctx 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(); this._renderFill(ctx); this._renderStroke(ctx); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var widthBy2 = this.width / 2, heightBy2 = this.height / 2; ctx.beginPath(); fabric.util.drawDashedLine(ctx, -widthBy2, heightBy2, 0, -heightBy2, this.strokeDashArray); fabric.util.drawDashedLine(ctx, 0, -heightBy2, widthBy2, heightBy2, this.strokeDashArray); fabric.util.drawDashedLine(ctx, widthBy2, heightBy2, -widthBy2, heightBy2, this.strokeDashArray); ctx.closePath(); }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), widthBy2 = this.width / 2, heightBy2 = this.height / 2, points = [ -widthBy2 + ' ' + heightBy2, '0 ' + -heightBy2, widthBy2 + ' ' + heightBy2 ] .join(','); markup.push( '' ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return 1; } }); /** * Returns fabric.Triangle instance from an object representation * @static * @memberOf fabric.Triangle * @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; } /** * Ellipse class * @class fabric.Ellipse * @extends fabric.Object * @return {fabric.Ellipse} thisArg * @see {@link fabric.Ellipse#initialize} for constructor definition */ fabric.Ellipse = fabric.util.createClass(fabric.Object, /** @lends fabric.Ellipse.prototype */ { /** * Type of an object * @type String * @default */ type: 'ellipse', /** * Horizontal radius * @type Number * @default */ rx: 0, /** * Vertical radius * @type Number * @default */ ry: 0, /** * Constructor * @param {Object} [options] Options object * @return {fabric.Ellipse} thisArg */ initialize: function(options) { options = options || { }; this.callSuper('initialize', options); this.set('rx', options.rx || 0); this.set('ry', options.ry || 0); }, /** * @private * @param {String} key * @param {Any} value * @return {fabric.Ellipse} thisArg */ _set: function(key, value) { this.callSuper('_set', key, value); switch (key) { case 'rx': this.rx = value; this.set('width', value * 2); break; case 'ry': this.ry = value; this.set('height', value * 2); break; } return this; }, /** * Returns horizontal radius of an object (according to how an object is scaled) * @return {Number} */ getRx: function() { return this.get('rx') * this.get('scaleX'); }, /** * Returns Vertical radius of an object (according to how an object is scaled) * @return {Number} */ getRy: function() { return this.get('ry') * this.get('scaleY'); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { rx: this.get('rx'), ry: this.get('ry') }); }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), x = 0, y = 0; if (this.group && this.group.type === 'path-group') { x = this.left + this.rx; y = this.top + this.ry; } markup.push( '\n' ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ _render: function(ctx, noTransform) { ctx.beginPath(); ctx.save(); ctx.transform(1, 0, 0, this.ry/this.rx, 0, 0); ctx.arc( noTransform ? this.left + this.rx : 0, noTransform ? (this.top + this.ry) * this.rx/this.ry : 0, this.rx, 0, piBy2, false); ctx.restore(); this._renderFill(ctx); this._renderStroke(ctx); }, /** * Returns complexity of an instance * @return {Number} complexity */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Ellipse.fromElement}) * @static * @memberOf fabric.Ellipse * @see http://www.w3.org/TR/SVG/shapes.html#EllipseElement */ fabric.Ellipse.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('cx cy rx ry'.split(' ')); /** * Returns {@link fabric.Ellipse} instance from an SVG element * @static * @memberOf fabric.Ellipse * @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); parsedAttributes.left = parsedAttributes.left || 0; parsedAttributes.top = parsedAttributes.top || 0; var ellipse = new fabric.Ellipse(extend(parsedAttributes, options)); ellipse.top -= ellipse.ry; ellipse.left -= ellipse.rx; return ellipse; }; /* _FROM_SVG_END_ */ /** * Returns {@link fabric.Ellipse} instance from an object representation * @static * @memberOf fabric.Ellipse * @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 = { }), extend = fabric.util.object.extend; if (fabric.Rect) { console.warn('fabric.Rect is already defined'); return; } var stateProperties = fabric.Object.prototype.stateProperties.concat(); stateProperties.push('rx', 'ry', 'x', 'y'); /** * Rectangle class * @class fabric.Rect * @extends fabric.Object * @return {fabric.Rect} thisArg * @see {@link fabric.Rect#initialize} for constructor definition */ fabric.Rect = fabric.util.createClass(fabric.Object, /** @lends fabric.Rect.prototype */ { /** * List of properties to consider when checking if state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ stateProperties: stateProperties, /** * Type of an object * @type String * @default */ type: 'rect', /** * Horizontal border radius * @type Number * @default */ rx: 0, /** * Vertical border radius * @type Number * @default */ ry: 0, /** * Used to specify dash pattern for stroke on this object * @type Array */ strokeDashArray: null, /** * Constructor * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(options) { options = options || { }; this.callSuper('initialize', options); this._initRxRy(); }, /** * Initializes rx/ry attributes * @private */ _initRxRy: function() { if (this.rx && !this.ry) { this.ry = this.rx; } else if (this.ry && !this.rx) { this.rx = this.ry; } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx, noTransform) { // optimize 1x1 case (used in spray brush) if (this.width === 1 && this.height === 1) { ctx.fillRect(0, 0, 1, 1); return; } var rx = this.rx ? Math.min(this.rx, this.width / 2) : 0, ry = this.ry ? Math.min(this.ry, this.height / 2) : 0, w = this.width, h = this.height, x = noTransform ? this.left : -this.width / 2, y = noTransform ? this.top : -this.height / 2, isRounded = rx !== 0 || ry !== 0, k = 1 - 0.5522847498 /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */; ctx.beginPath(); ctx.moveTo(x + rx, y); ctx.lineTo(x + w - rx, y); isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); ctx.lineTo(x + w, y + h - ry); isRounded && ctx.bezierCurveTo(x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h); ctx.lineTo(x + rx, y + h); isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); ctx.lineTo(x, y + ry); isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); ctx.closePath(); this._renderFill(ctx); this._renderStroke(ctx); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var x = -this.width / 2, y = -this.height / 2, w = this.width, h = this.height; ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); ctx.closePath(); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { var object = extend(this.callSuper('toObject', propertiesToInclude), { rx: this.get('rx') || 0, ry: this.get('ry') || 0 }); if (!this.includeDefaultValues) { this._removeDefaultValues(object); } return object; }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = this._createBaseSVGMarkup(), x = this.left, y = this.top; if (!(this.group && this.group.type === 'path-group')) { x = -this.width / 2; y = -this.height / 2; } markup.push( '\n'); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns complexity of an instance * @return {Number} complexity */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by `fabric.Rect.fromElement`) * @static * @memberOf fabric.Rect * @see: http://www.w3.org/TR/SVG/shapes.html#RectElement */ fabric.Rect.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y rx ry width height'.split(' ')); /** * Returns {@link fabric.Rect} instance from an SVG element * @static * @memberOf fabric.Rect * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @return {fabric.Rect} Instance of fabric.Rect */ fabric.Rect.fromElement = function(element, options) { if (!element) { return null; } options = options || { }; var parsedAttributes = fabric.parseAttributes(element, fabric.Rect.ATTRIBUTE_NAMES); parsedAttributes.left = parsedAttributes.left || 0; parsedAttributes.top = parsedAttributes.top || 0; return new fabric.Rect(extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); }; /* _FROM_SVG_END_ */ /** * Returns {@link fabric.Rect} instance from an object representation * @static * @memberOf fabric.Rect * @param {Object} object Object to create an instance from * @return {Object} instance of fabric.Rect */ fabric.Rect.fromObject = function(object) { return new fabric.Rect(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); return; } /** * Polyline class * @class fabric.Polyline * @extends fabric.Object * @see {@link fabric.Polyline#initialize} for constructor definition */ fabric.Polyline = fabric.util.createClass(fabric.Object, /** @lends fabric.Polyline.prototype */ { /** * Type of an object * @type String * @default */ type: 'polyline', /** * Points array * @type Array * @default */ points: null, /** * Minimum X from points values, necessary to offset points * @type Number * @default */ minX: 0, /** * Minimum Y from points values, necessary to offset points * @type Number * @default */ minY: 0, /** * Constructor * @param {Array} points Array of points (where each point is an object with x and y) * @param {Object} [options] Options object * @param {Boolean} [skipOffset] Whether points offsetting should be skipped * @return {fabric.Polyline} thisArg * @example * var poly = new fabric.Polyline([ * { x: 10, y: 10 }, * { x: 50, y: 30 }, * { x: 40, y: 70 }, * { x: 60, y: 50 }, * { x: 100, y: 150 }, * { x: 40, y: 100 } * ], { * stroke: 'red', * left: 100, * top: 100 * }); */ initialize: function(points, options) { return fabric.Polygon.prototype.initialize.call(this, points, options); }, /** * @private */ _calcDimensions: function() { return fabric.Polygon.prototype._calcDimensions.call(this); }, /** * @private */ _applyPointOffset: function() { return fabric.Polygon.prototype._applyPointOffset.call(this); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { return fabric.Polygon.prototype.toObject.call(this, propertiesToInclude); }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { return fabric.Polygon.prototype.toSVG.call(this, reviver); }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { fabric.Polygon.prototype.commonRender.call(this, ctx); this._renderFill(ctx); this._renderStroke(ctx); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var p1, p2; ctx.beginPath(); for (var i = 0, len = this.points.length; i < len; i++) { p1 = this.points[i]; p2 = this.points[i + 1] || p1; fabric.util.drawDashedLine(ctx, p1.x, p1.y, p2.x, p2.y, this.strokeDashArray); } }, /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return this.get('points').length; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Polyline.fromElement}) * @static * @memberOf fabric.Polyline * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement */ fabric.Polyline.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); /** * Returns fabric.Polyline instance from an SVG element * @static * @memberOf fabric.Polyline * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @return {fabric.Polyline} 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); if (points === null) { return null; } return new fabric.Polyline(points, fabric.util.object.extend(parsedAttributes, options)); }; /* _FROM_SVG_END_ */ /** * Returns fabric.Polyline instance from an object representation * @static * @memberOf fabric.Polyline * @param {Object} object Object to create an instance from * @return {fabric.Polyline} Instance of fabric.Polyline */ fabric.Polyline.fromObject = function(object) { var points = object.points; return new fabric.Polyline(points, object, true); }; })(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; } /** * Polygon class * @class fabric.Polygon * @extends fabric.Object * @see {@link fabric.Polygon#initialize} for constructor definition */ fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { /** * Type of an object * @type String * @default */ type: 'polygon', /** * Points array * @type Array * @default */ points: null, /** * Minimum X from points values, necessary to offset points * @type Number * @default */ minX: 0, /** * Minimum Y from points values, necessary to offset points * @type Number * @default */ minY: 0, /** * Constructor * @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(); if (!('top' in options)) { this.top = this.minY; } if (!('left' in options)) { this.left = this.minX; } }, /** * @private */ _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; }, /** * @private */ _applyPointOffset: function() { // change points to offset polygon into a bounding box // executed one time this.points.forEach(function(p) { p.x -= (this.minX + this.width / 2); p.y -= (this.minY + this.height / 2); }, this); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { points: this.points.concat() }); }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var points = [], markup = this._createBaseSVGMarkup(); 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), ' '); } markup.push( '<', this.type, ' ', 'points="', points.join(''), '" style="', this.getSvgStyles(), '" transform="', this.getSvgTransform(), ' ', this.getSvgTransformMatrix(), '"/>\n' ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { this.commonRender(ctx); this._renderFill(ctx); if (this.stroke || this.strokeDashArray) { ctx.closePath(); this._renderStroke(ctx); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ commonRender: function(ctx) { var point; ctx.beginPath(); if (this._applyPointOffset) { if (!(this.group && this.group.type === 'path-group')) { this._applyPointOffset(); } this._applyPointOffset = null; } 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); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { fabric.Polyline.prototype._renderDashedStroke.call(this, ctx); ctx.closePath(); }, /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return this.points.length; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by `fabric.Polygon.fromElement`) * @static * @memberOf fabric.Polygon * @see: http://www.w3.org/TR/SVG/shapes.html#PolygonElement */ fabric.Polygon.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(); /** * Returns {@link fabric.Polygon} instance from an SVG element * @static * @memberOf fabric.Polygon * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @return {fabric.Polygon} Instance of fabric.Polygon */ fabric.Polygon.fromElement = function(element, options) { if (!element) { return null; } options || (options = { }); var points = fabric.parsePointsAttribute(element.getAttribute('points')), parsedAttributes = fabric.parseAttributes(element, fabric.Polygon.ATTRIBUTE_NAMES); if (points === null) { return null; } return new fabric.Polygon(points, extend(parsedAttributes, options)); }; /* _FROM_SVG_END_ */ /** * Returns fabric.Polygon instance from an object representation * @static * @memberOf fabric.Polygon * @param {Object} object Object to create an instance from * @return {fabric.Polygon} Instance of fabric.Polygon */ fabric.Polygon.fromObject = function(object) { return new fabric.Polygon(object.points, object, true); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { '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, drawArc = fabric.util.drawArc, commandLengths = { m: 2, l: 2, h: 1, v: 1, c: 6, s: 4, q: 4, t: 2, a: 7 }, repeatedCommands = { m: 'l', M: 'L' }; if (fabric.Path) { fabric.warn('fabric.Path is already defined'); return; } /** * Path class * @class fabric.Path * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} * @see {@link fabric.Path#initialize} for constructor definition */ fabric.Path = fabric.util.createClass(fabric.Object, /** @lends fabric.Path.prototype */ { /** * Type of an object * @type String * @default */ type: 'path', /** * Array of path points * @type Array * @default */ path: null, /** * Minimum X from points values, necessary to offset points * @type Number * @default */ minX: 0, /** * Minimum Y from points values, necessary to offset points * @type Number * @default */ minY: 0, /** * Constructor * @param {Array|String} path Path data (sequence of coordinates and corresponding "command" tokens) * @param {Object} [options] Options object * @return {fabric.Path} thisArg */ initialize: function(path, options) { options = options || { }; this.setOptions(options); if (!path) { throw new 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.path = this._parsePath(); } var calcDim = this._parseDimensions(); this.minX = calcDim.left; this.minY = calcDim.top; this.width = calcDim.width; this.height = calcDim.height; calcDim.left += this.originX === 'center' ? this.width / 2 : this.originX === 'right' ? this.width : 0; calcDim.top += this.originY === 'center' ? this.height / 2 : this.originY === 'bottom' ? this.height : 0; this.top = this.top || calcDim.top; this.left = this.left || calcDim.left; this.pathOffset = this.pathOffset || { x: this.minX + this.width / 2, y: this.minY + this.height / 2 }; if (options.sourcePath) { this.setSourcePath(options.sourcePath); } }, /** * @private * @param {CanvasRenderingContext2D} ctx context to render path on */ _render: function(ctx) { var current, // current instruction previous = null, subpathStartX = 0, subpathStartY = 0, 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.pathOffset.x, t = -this.pathOffset.y; if (this.group && this.group.type === 'path-group') { l = 0; t = 0; } ctx.beginPath(); 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]; subpathStartX = x; subpathStartY = y; ctx.moveTo(x + l, y + t); break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; subpathStartX = x; subpathStartY = y; 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 = controlX ? (2 * x - controlX) : x; controlY = controlY ? (2 * y - controlY) : y; 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': x = subpathStartX; y = subpathStartY; ctx.closePath(); break; } previous = current; } this._renderFill(ctx); this._renderStroke(ctx); }, /** * Renders path on a specified context * @param {CanvasRenderingContext2D} ctx context to render path on * @param {Boolean} [noTransform] When true, context is not transformed */ render: function(ctx, noTransform) { // do not render if width/height are zeros or object is not visible if (!this.visible) { return; } ctx.save(); this._setupCompositeOperation(ctx); if (!noTransform) { this.transform(ctx); } this._setStrokeStyles(ctx); this._setFillStyles(ctx); if (this.group && this.group.type === 'path-group') { ctx.translate(-this.group.width / 2, -this.group.height / 2); } if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } this._setOpacity(ctx); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); this._restoreCompositeOperation(ctx); ctx.restore(); }, /** * Returns string representation of an instance * @return {String} string representation of an instance */ toString: function() { return '#'; }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { var o = extend(this.callSuper('toObject', propertiesToInclude), { path: this.path.map(function(item) { return item.slice() }), pathOffset: this.pathOffset }); if (this.sourcePath) { o.sourcePath = this.sourcePath; } if (this.transformMatrix) { o.transformMatrix = this.transformMatrix; } return o; }, /** * Returns dataless object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toDatalessObject: function(propertiesToInclude) { var o = this.toObject(propertiesToInclude); if (this.sourcePath) { o.path = this.sourcePath; } delete o.sourcePath; return o; }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var chunks = [], markup = this._createBaseSVGMarkup(), addTransform = ''; for (var i = 0, len = this.path.length; i < len; i++) { chunks.push(this.path[i].join(' ')); } var path = chunks.join(' '); if (!(this.group && this.group.type === 'path-group')) { addTransform = 'translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ')'; } markup.push( //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns number representation of an instance complexity * @return {Number} complexity of this instance */ complexity: function() { return this.path.length; }, /** * @private */ _parsePath: function() { var result = [ ], coords = [ ], currentPath, parsed, re = /([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/ig, match, coordsStr; for (var i = 0, coordsParsed, len = this.path.length; i < len; i++) { currentPath = this.path[i]; coordsStr = currentPath.slice(1).trim(); coords.length = 0; while ((match = re.exec(coordsStr))) { coords.push(match[0]); } coordsParsed = [ currentPath.charAt(0) ]; for (var j = 0, jlen = coords.length; j < jlen; j++) { parsed = parseFloat(coords[j]); if (!isNaN(parsed)) { coordsParsed.push(parsed); } } var command = coordsParsed[0], commandLength = commandLengths[command.toLowerCase()], repeatedCommand = repeatedCommands[command] || command; if (coordsParsed.length - 1 > commandLength) { for (var k = 1, klen = coordsParsed.length; k < klen; k += commandLength) { result.push([ command ].concat(coordsParsed.slice(k, k + commandLength))); command = repeatedCommand; } } else { result.push(coordsParsed); } } return result; }, /** * @private */ _parseDimensions: function() { var aX = [], aY = [], current, // current instruction previous = null, subpathStartX = 0, subpathStartY = 0, x = 0, // current x y = 0, // current y controlX = 0, // current control point x controlY = 0, // current control point y tempX, tempY, tempControlX, tempControlY, bounds; 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]; bounds = [ ]; break; case 'L': // lineto, absolute x = current[1]; y = current[2]; bounds = [ ]; break; case 'h': // horizontal lineto, relative x += current[1]; bounds = [ ]; break; case 'H': // horizontal lineto, absolute x = current[1]; bounds = [ ]; break; case 'v': // vertical lineto, relative y += current[1]; bounds = [ ]; break; case 'V': // verical lineto, absolute y = current[1]; bounds = [ ]; break; case 'm': // moveTo, relative x += current[1]; y += current[2]; subpathStartX = x; subpathStartY = y; bounds = [ ]; break; case 'M': // moveTo, absolute x = current[1]; y = current[2]; subpathStartX = x; subpathStartY = y; bounds = [ ]; break; case 'c': // bezierCurveTo, relative tempX = x + current[5]; tempY = y + current[6]; controlX = x + current[3]; controlY = y + current[4]; bounds = fabric.util.getBoundsOfCurve(x, y, x + current[1], // x1 y + current[2], // y1 controlX, // x2 controlY, // y2 tempX, tempY ); x = tempX; y = tempY; break; case 'C': // bezierCurveTo, absolute x = current[5]; y = current[6]; controlX = current[3]; controlY = current[4]; bounds = fabric.util.getBoundsOfCurve(x, y, current[1], current[2], controlX, controlY, x, y ); 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 = controlX ? (2 * x - controlX) : x; controlY = controlY ? (2 * y - controlY) : y; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, x + current[1], y + current[2], tempX, 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 = 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; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, current[1], current[2], tempX, tempY ); 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]; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); x = tempX; y = tempY; break; case 'Q': // quadraticCurveTo, absolute controlX = current[1]; controlY = current[2]; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, current[3], current[4] ); x = current[3]; y = current[4]; 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; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); 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; bounds = fabric.util.getBoundsOfCurve(x, y, controlX, controlY, controlX, controlY, tempX, tempY ); x = tempX; y = tempY; break; case 'a': // TODO: optimize this bounds = fabric.util.getBoundsOfArc(x, y, current[1], current[2], current[3], current[4], current[5], current[6] + x, current[7] + y ); x += current[6]; y += current[7]; break; case 'A': // TODO: optimize this bounds = fabric.util.getBoundsOfArc(x, y, current[1], current[2], current[3], current[4], current[5], current[6], current[7] ); x = current[6]; y = current[7]; break; case 'z': case 'Z': x = subpathStartX; y = subpathStartY; break; } previous = current; bounds.forEach(function (point) { aX.push(point.x); aY.push(point.y); }); aX.push(x); aY.push(y); } var minX = min(aX), minY = min(aY), maxX = max(aX), maxY = max(aY), deltaX = maxX - minX, deltaY = maxY - minY, o = { left: minX, top: minY, width: deltaX, height: deltaY }; return o; } }); /** * Creates an instance of fabric.Path from an object * @static * @memberOf fabric.Path * @param {Object} object * @param {Function} callback Callback to invoke when an fabric.Path instance is created */ fabric.Path.fromObject = function(object, callback) { if (typeof object.path === 'string') { fabric.loadSVGFromURL(object.path, function (elements) { var path = elements[0], pathUrl = object.path; delete object.path; fabric.util.object.extend(path, object); path.setSourcePath(pathUrl); callback(path); }); } else { callback(new fabric.Path(object.path, object)); } }; /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by `fabric.Path.fromElement`) * @static * @memberOf fabric.Path * @see http://www.w3.org/TR/SVG/paths.html#PathElement */ fabric.Path.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(['d']); /** * Creates an instance of fabric.Path from an SVG element * @static * @memberOf fabric.Path * @param {SVGElement} element to parse * @param {Function} callback Callback to invoke when an fabric.Path instance is created * @param {Object} [options] Options object */ fabric.Path.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Path.ATTRIBUTE_NAMES); callback && callback(new fabric.Path(parsedAttributes.d, extend(parsedAttributes, options))); }; /* _FROM_SVG_END_ */ /** * Indicates that instances of this type are async * @static * @memberOf fabric.Path * @type Boolean * @default */ fabric.Path.async = true; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, invoke = fabric.util.array.invoke, parentToObject = fabric.Object.prototype.toObject; if (fabric.PathGroup) { fabric.warn('fabric.PathGroup is already defined'); return; } /** * Path group class * @class fabric.PathGroup * @extends fabric.Path * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#path_and_pathgroup} * @see {@link fabric.PathGroup#initialize} for constructor definition */ fabric.PathGroup = fabric.util.createClass(fabric.Path, /** @lends fabric.PathGroup.prototype */ { /** * Type of an object * @type String * @default */ type: 'path-group', /** * Fill value * @type String * @default */ fill: '', /** * Constructor * @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); if (options.widthAttr) { this.scaleX = options.widthAttr / options.width; } if (options.heightAttr) { this.scaleY = options.heightAttr / options.height; } this.setCoords(); if (options.sourcePath) { this.setSourcePath(options.sourcePath); } }, /** * Renders this group on a specified context * @param {CanvasRenderingContext2D} ctx Context to render this instance on */ render: function(ctx) { // do not render if object is not visible if (!this.visible) { return; } 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); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); for (var i = 0, l = this.paths.length; i < l; ++i) { this.paths[i].render(ctx, true); } this.clipTo && ctx.restore(); this._removeShadow(ctx); ctx.restore(); }, /** * Sets certain property to a certain value * @param {String} prop * @param {Any} value * @return {fabric.PathGroup} thisArg */ _set: function(prop, value) { if (prop === 'fill' && 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 * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { var o = extend(parentToObject.call(this, propertiesToInclude), { paths: invoke(this.getObjects(), 'toObject', propertiesToInclude) }); if (this.sourcePath) { o.sourcePath = this.sourcePath; } return o; }, /** * Returns dataless object representation of this path group * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} dataless object representation of an instance */ toDatalessObject: function(propertiesToInclude) { var o = this.toObject(propertiesToInclude); if (this.sourcePath) { o.paths = this.sourcePath; } return o; }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var objects = this.getObjects(), translatePart = 'translate(' + this.left + ' ' + this.top + ')', markup = [ //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ]; for (var i = 0, len = objects.length; i < len; i++) { markup.push(objects[i].toSVG(reviver)); } markup.push('\n'); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns a string representation of this path group * @return {String} string representation of an object */ toString: function() { return '#'; }, /** * Returns true if all paths in this group are of same color * @return {Boolean} true if all paths are of the same color (`fill`) */ isSameColor: function() { var firstPathFill = (this.getObjects()[0].get('fill') || '').toLowerCase(); return this.getObjects().every(function(path) { return (path.get('fill') || '').toLowerCase() === firstPathFill; }); }, /** * Returns number representation of object's complexity * @return {Number} complexity */ complexity: function() { return this.paths.reduce(function(total, path) { return total + ((path && path.complexity) ? path.complexity() : 0); }, 0); }, /** * Returns all paths in this path group * @return {Array} array of path objects included in this path group */ getObjects: function() { return this.paths; } }); /** * Creates fabric.PathGroup instance from an object representation * @static * @memberOf fabric.PathGroup * @param {Object} object Object to create an instance from * @param {Function} callback Callback to invoke when an fabric.PathGroup instance is created */ fabric.PathGroup.fromObject = function(object, callback) { if (typeof object.paths === 'string') { fabric.loadSVGFromURL(object.paths, function (elements) { var pathUrl = object.paths; delete object.paths; var pathGroup = fabric.util.groupSVGElements(elements, object, pathUrl); callback(pathGroup); }); } else { fabric.util.enlivenObjects(object.paths, function(enlivenedObjects) { delete object.paths; callback(new fabric.PathGroup(enlivenedObjects, object)); }); } }; /** * Indicates that instances of this type are async * @static * @memberOf fabric.PathGroup * @type Boolean * @default */ fabric.PathGroup.async = true; })(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; if (fabric.Group) { return; } // lock-related properties, for use in fabric.Group#get // to enable locking behavior on group // when one of its objects has lock-related properties set var _lockProperties = { lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockUniScaling: true }; /** * Group class * @class fabric.Group * @extends fabric.Object * @mixes fabric.Collection * @tutorial {@link http://fabricjs.com/fabric-intro-part-3/#groups} * @see {@link fabric.Group#initialize} for constructor definition */ fabric.Group = fabric.util.createClass(fabric.Object, fabric.Collection, /** @lends fabric.Group.prototype */ { /** * Type of an object * @type String * @default */ type: 'group', /** * Constructor * @param {Object} objects Group objects * @param {Object} [options] Options object * @return {Object} thisArg */ initialize: function(objects, options) { options = options || { }; this._objects = objects || []; for (var i = this._objects.length; i--; ) { this._objects[i].group = this; } this.originalState = { }; this.callSuper('initialize'); if (options.originX) { this.originX = options.originX; } if (options.originY) { this.originY = options.originY; } this._calcBounds(); this._updateObjectsCoords(); if (options) { extend(this, options); } this.setCoords(); this.saveCoords(); }, /** * @private */ _updateObjectsCoords: function() { this.forEachObject(this._updateObjectCoords, this); }, /** * @private */ _updateObjectCoords: function(object) { var objectLeft = object.getLeft(), objectTop = object.getTop(), center = this.getCenterPoint(); object.set({ originalLeft: objectLeft, originalTop: objectTop, left: objectLeft - center.x, top: objectTop - center.y }); object.setCoords(); // do not display corners of objects enclosed in a group object.__origHasControls = object.hasControls; object.hasControls = false; }, /** * Returns string represenation of a group * @return {String} */ toString: function() { return '#'; }, /** * Adds an object to a group; Then recalculates group's dimension, position. * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ addWithUpdate: function(object) { this._restoreObjectsState(); if (object) { this._objects.push(object); object.group = this; } // since _restoreObjectsState set objects inactive this.forEachObject(this._setObjectActive, this); this._calcBounds(); this._updateObjectsCoords(); return this; }, /** * @private */ _setObjectActive: function(object) { object.set('active', true); object.group = this; }, /** * Removes an object from a group; Then recalculates group's dimension, position. * @param {Object} object * @return {fabric.Group} thisArg * @chainable */ removeWithUpdate: function(object) { this._moveFlippedObject(object); this._restoreObjectsState(); // since _restoreObjectsState set objects inactive this.forEachObject(this._setObjectActive, this); this.remove(object); this._calcBounds(); this._updateObjectsCoords(); return this; }, /** * @private */ _onObjectAdded: function(object) { object.group = this; }, /** * @private */ _onObjectRemoved: function(object) { delete object.group; object.set('active', false); }, /** * Properties that are delegated to group objects when reading/writing * @param {Object} delegatedProperties */ delegatedProperties: { fill: true, opacity: true, fontFamily: true, fontWeight: true, fontSize: true, fontStyle: true, lineHeight: true, textDecoration: true, textAlign: true, backgroundColor: true }, /** * @private */ _set: function(key, value) { if (key in this.delegatedProperties) { var i = this._objects.length; this[key] = value; while (i--) { this._objects[i].set(key, value); } } else { this[key] = value; } }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { objects: invoke(this._objects, 'toObject', propertiesToInclude) }); }, /** * Renders instance on a given context * @param {CanvasRenderingContext2D} ctx context to render instance on */ render: function(ctx) { // do not render if object is not visible if (!this.visible) { return; } ctx.save(); this.clipTo && fabric.util.clipContext(this, ctx); // the array is now sorted in order of highest first, so start from end for (var i = 0, len = this._objects.length; i < len; i++) { this._renderObject(this._objects[i], ctx); } this.clipTo && ctx.restore(); ctx.restore(); }, /** * Renders controls and borders for the object * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} [noTransform] When true, context is not transformed */ _renderControls: function(ctx, noTransform) { this.callSuper('_renderControls', ctx, noTransform); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i]._renderControls(ctx); } }, /** * @private */ _renderObject: function(object, ctx) { var originalHasRotatingPoint = object.hasRotatingPoint; // do not render if object is not visible if (!object.visible) { return; } object.hasRotatingPoint = false; object.render(ctx); object.hasRotatingPoint = originalHasRotatingPoint; }, /** * Retores original state of each of group objects (original state is that which was before group was created). * @private * @return {fabric.Group} thisArg * @chainable */ _restoreObjectsState: function() { this._objects.forEach(this._restoreObjectState, this); return this; }, /** * Moves a flipped object to the position where it's displayed * @private * @param {fabric.Object} object * @return {fabric.Group} thisArg */ _moveFlippedObject: function(object) { var oldOriginX = object.get('originX'), oldOriginY = object.get('originY'), center = object.getCenterPoint(); object.set({ originX: 'center', originY: 'center', left: center.x, top: center.y }); this._toggleFlipping(object); var newOrigin = object.getPointByOrigin(oldOriginX, oldOriginY); object.set({ originX: oldOriginX, originY: oldOriginY, left: newOrigin.x, top: newOrigin.y }); return this; }, /** * @private */ _toggleFlipping: function(object) { if (this.flipX) { object.toggle('flipX'); object.set('left', -object.get('left')); object.setAngle(-object.getAngle()); } if (this.flipY) { object.toggle('flipY'); object.set('top', -object.get('top')); object.setAngle(-object.getAngle()); } }, /** * Restores original state of a specified object in group * @private * @param {fabric.Object} object * @return {fabric.Group} thisArg */ _restoreObjectState: function(object) { this._setObjectPosition(object); object.setCoords(); object.hasControls = object.__origHasControls; delete object.__origHasControls; object.set('active', false); object.setCoords(); delete object.group; return this; }, /** * @private */ _setObjectPosition: function(object) { var center = this.getCenterPoint(), rotated = this._getRotatedLeftTop(object); object.set({ angle: object.getAngle() + this.getAngle(), left: center.x + rotated.left, top: center.y + rotated.top, scaleX: object.get('scaleX') * this.get('scaleX'), scaleY: object.get('scaleY') * this.get('scaleY') }); }, /** * @private */ _getRotatedLeftTop: function(object) { var groupAngle = this.getAngle() * (Math.PI / 180); return { left: (-Math.sin(groupAngle) * object.getTop() * this.get('scaleY') + Math.cos(groupAngle) * object.getLeft() * this.get('scaleX')), top: (Math.cos(groupAngle) * object.getTop() * this.get('scaleY') + Math.sin(groupAngle) * object.getLeft() * this.get('scaleX')) }; }, /** * Destroys a group (restoring state of its objects) * @return {fabric.Group} thisArg * @chainable */ destroy: function() { this._objects.forEach(this._moveFlippedObject, this); 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) * @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 * @return {fabric.Group} thisArg * @chainable */ setObjectsCoords: function() { this.forEachObject(function(object) { object.setCoords(); }); return this; }, /** * @private */ _calcBounds: function(onlyWidthHeight) { var aX = [], aY = [], o; for (var i = 0, len = this._objects.length; 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); } } this.set(this._getBounds(aX, aY, onlyWidthHeight)); }, /** * @private */ _getBounds: function(aX, aY, onlyWidthHeight) { var ivt = fabric.util.invertTransform(this.getViewportTransform()), minXY = fabric.util.transformPoint(new fabric.Point(min(aX), min(aY)), ivt), maxXY = fabric.util.transformPoint(new fabric.Point(max(aX), max(aY)), ivt), obj = { width: (maxXY.x - minXY.x) || 0, height: (maxXY.y - minXY.y) || 0 }; if (!onlyWidthHeight) { obj.left = minXY.x || 0; obj.top = minXY.y || 0; if (this.originX === 'center') { obj.left += obj.width / 2; } if (this.originX === 'right') { obj.left += obj.width; } if (this.originY === 'center') { obj.top += obj.height / 2; } if (this.originY === 'bottom') { obj.top += obj.height; } } return obj; }, /* _TO_SVG_START_ */ /** * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = [ //jscs:disable validateIndentation '\n' //jscs:enable validateIndentation ]; for (var i = 0, len = this._objects.length; i < len; i++) { markup.push(this._objects[i].toSVG(reviver)); } markup.push('\n'); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns requested property * @param {String} prop Property to get * @return {Any} */ get: function(prop) { if (prop in _lockProperties) { if (this[prop]) { return this[prop]; } else { for (var i = 0, len = this._objects.length; i < len; i++) { if (this._objects[i][prop]) { return true; } } return false; } } else { if (prop in this.delegatedProperties) { return this._objects[0] && this._objects[0].get(prop); } return this[prop]; } } }); /** * Returns {@link fabric.Group} instance from an object representation * @static * @memberOf fabric.Group * @param {Object} object Object to create a group from * @param {Function} [callback] Callback to invoke when an group instance is created * @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)); }); }; /** * Indicates that instances of this type are async * @static * @memberOf fabric.Group * @type Boolean * @default */ 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; } /** * Image class * @class fabric.Image * @extends fabric.Object * @tutorial {@link http://fabricjs.com/fabric-intro-part-1/#images} * @see {@link fabric.Image#initialize} for constructor definition */ fabric.Image = fabric.util.createClass(fabric.Object, /** @lends fabric.Image.prototype */ { /** * Type of an object * @type String * @default */ type: 'image', /** * crossOrigin value (one of "", "anonymous", "allow-credentials") * @see https://developer.mozilla.org/en-US/docs/HTML/CORS_settings_attributes * @type String * @default */ crossOrigin: '', /** * AlignX value, part of preserveAspectRatio (one of "none", "mid", "min", "max") * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute * This parameter defines how the picture is aligned to its viewport when image element width differs from image width. * @type String * @default */ alignX: 'none', /** * AlignY value, part of preserveAspectRatio (one of "none", "mid", "min", "max") * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute * This parameter defines how the picture is aligned to its viewport when image element height differs from image height. * @type String * @default */ alignY: 'none', /** * meetOrSlice value, part of preserveAspectRatio (one of "meet", "slice"). * if meet the image is always fully visibile, if slice the viewport is always filled with image. * @see http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute * @type String * @default */ meetOrSlice: 'meet', /** * private * contains last value of scaleX to detect * if the Image got resized after the last Render * @type Number */ _lastScaleX: 1, /** * private * contains last value of scaleY to detect * if the Image got resized after the last Render * @type Number */ _lastScaleY: 1, /** * Constructor * @param {HTMLImageElement | String} element Image element * @param {Object} [options] Options object * @return {fabric.Image} thisArg */ initialize: function(element, options) { options || (options = { }); this.filters = [ ]; this.resizeFilters = [ ]; this.callSuper('initialize', options); this._initElement(element, options); this._initConfig(options); if (options.filters) { this.filters = options.filters; this.applyFilters(); } }, /** * Returns image element which this instance if based on * @return {HTMLImageElement} Image element */ getElement: function() { return this._element; }, /** * Sets image element for this instance to a specified one. * If filters defined they are applied to new image. * You might need to call `canvas.renderAll` and `object.setCoords` after replacing, to render new image and update controls area. * @param {HTMLImageElement} element * @param {Function} [callback] Callback is invoked when all filters have been applied and new image is generated * @param {Object} [options] Options object * @return {fabric.Image} thisArg * @chainable */ setElement: function(element, callback, options) { this._element = element; this._originalElement = element; this._initConfig(options); if (this.filters.length !== 0) { this.applyFilters(callback); } else if (callback) { callback(); } return this; }, /** * Sets crossOrigin value (on an instance and corresponding image element) * @return {fabric.Image} thisArg * @chainable */ setCrossOrigin: function(value) { this.crossOrigin = value; this._element.crossOrigin = value; return this; }, /** * Returns original size of an image * @return {Object} Object with "width" and "height" properties */ getOriginalSize: function() { var element = this.getElement(); return { width: element.width, height: element.height }; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _stroke: function(ctx) { ctx.save(); this._setStrokeStyles(ctx); ctx.beginPath(); ctx.strokeRect(-this.width / 2, -this.height / 2, this.width, this.height); ctx.closePath(); ctx.restore(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { var x = -this.width / 2, y = -this.height / 2, w = this.width, h = this.height; ctx.save(); this._setStrokeStyles(ctx); ctx.beginPath(); fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y, x + w, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x + w, y + h, x, y + h, this.strokeDashArray); fabric.util.drawDashedLine(ctx, x, y + h, x, y, this.strokeDashArray); ctx.closePath(); ctx.restore(); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { return extend(this.callSuper('toObject', propertiesToInclude), { src: this._originalElement.src || this._originalElement._src, filters: this.filters.map(function(filterObj) { return filterObj && filterObj.toObject(); }), crossOrigin: this.crossOrigin, alignX: this.alignX, alignY: this.alignY, meetOrSlice: this.meetOrSlice }); }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = [], x = -this.width / 2, y = -this.height / 2, preserveAspectRatio = 'none'; if (this.group && this.group.type === 'path-group') { x = this.left; y = this.top; } if (this.alignX !== 'none' && this.alignY !== 'none') { preserveAspectRatio = 'x' + this.alignX + 'Y' + this.alignY + ' ' + this.meetOrSlice; } markup.push( '\n', '\n' ); if (this.stroke || this.strokeDashArray) { var origFill = this.fill; this.fill = null; markup.push( '\n' ); this.fill = origFill; } markup.push('\n'); return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ /** * Returns source of an image * @return {String} Source of an image */ getSrc: function() { if (this.getElement()) { return this.getElement().src || this.getElement()._src; } }, /** * Sets source of an image * @param {String} src Source string (URL) * @param {Function} [callback] Callback is invoked when image has been loaded (and all filters have been applied) * @param {Object} [options] Options object * @return {fabric.Image} thisArg * @chainable */ setSrc: function(src, callback, options) { fabric.util.loadImage(src, function(img) { return this.setElement(img, callback, options); }, this, options && options.crossOrigin); }, /** * Returns string representation of an instance * @return {String} String representation of an instance */ toString: function() { return '#'; }, /** * Returns a clone of an instance * @param {Function} callback Callback is invoked with a clone as a first argument * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output */ clone: function(callback, propertiesToInclude) { this.constructor.fromObject(this.toObject(propertiesToInclude), callback); }, /** * Applies filters assigned to this image (from "filters" array) * @method applyFilters * @param {Function} callback Callback is invoked when all filters have been applied and new image is generated * @return {fabric.Image} thisArg * @chainable */ applyFilters: function(callback, filters, imgElement, forResizing) { filters = filters || this.filters; imgElement = imgElement || this._originalElement; if (!imgElement) { return; } var imgEl = imgElement, canvasEl = fabric.util.createCanvasElement(), replacement = fabric.util.createImage(), _this = this; canvasEl.width = imgEl.width; canvasEl.height = imgEl.height; canvasEl.getContext('2d').drawImage(imgEl, 0, 0, imgEl.width, imgEl.height); if (filters.length === 0) { this._element = imgElement; callback && callback(); return canvasEl; } filters.forEach(function(filter) { filter && filter.applyTo(canvasEl, filter.scaleX || _this.scaleX, filter.scaleY || _this.scaleY); if (!forResizing && filter.type === 'Resize') { _this.width *= filter.scaleX; _this.height *= filter.scaleY; } }); /** @ignore */ replacement.width = canvasEl.width; replacement.height = canvasEl.height; if (fabric.isLikelyNode) { replacement.src = canvasEl.toBuffer(undefined, fabric.Image.pngCompression); // onload doesn't fire in some node versions, so we invoke callback manually _this._element = replacement; !forResizing && (_this._filteredEl = replacement); callback && callback(); } else { replacement.onload = function() { _this._element = replacement; !forResizing && (_this._filteredEl = replacement); callback && callback(); replacement.onload = canvasEl = imgEl = null; }; replacement.src = canvasEl.toDataURL('image/png'); } return canvasEl; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx, noTransform) { var x, y, imageMargins = this._findMargins(), elementToDraw; x = (noTransform ? this.left : -this.width / 2); y = (noTransform ? this.top : -this.height / 2); if (this.meetOrSlice === 'slice') { ctx.beginPath(); ctx.rect(x, y, this.width, this.height); ctx.clip(); } if (this.isMoving === false && this.resizeFilters.length && this._needsResize()) { this._lastScaleX = this.scaleX; this._lastScaleY = this.scaleY; elementToDraw = this.applyFilters(null, this.resizeFilters, this._filteredEl || this._originalElement, false); } else { elementToDraw = this._element; } elementToDraw && ctx.drawImage(elementToDraw, x + imageMargins.marginX, y + imageMargins.marginY, imageMargins.width, imageMargins.height ); this._renderStroke(ctx); }, /** * @private, needed to check if image needs resize */ _needsResize: function() { return (this.scaleX !== this._lastScaleX || this.scaleY !== this._lastScaleY); }, /** * @private */ _findMargins: function() { var width = this.width, height = this.height, scales, scale, marginX = 0, marginY = 0; if (this.alignX !== 'none' || this.alignY !== 'none') { scales = [this.width / this._element.width, this.height / this._element.height]; scale = this.meetOrSlice === 'meet' ? Math.min.apply(null, scales) : Math.max.apply(null, scales); width = this._element.width * scale; height = this._element.height * scale; if (this.alignX === 'Mid') { marginX = (this.width - width) / 2; } if (this.alignX === 'Max') { marginX = this.width - width; } if (this.alignY === 'Mid') { marginY = (this.height - height) / 2; } if (this.alignY === 'Max') { marginY = this.height - height; } } return { width: width, height: height, marginX: marginX, marginY: marginY }; }, /** * @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. * @private * @param {HTMLImageElement|String} element The element representing the image */ _initElement: function(element) { this.setElement(fabric.util.getById(element)); fabric.util.addClass(this.getElement(), fabric.Image.CSS_CANVAS); }, /** * @private * @param {Object} [options] Options object */ _initConfig: function(options) { options || (options = { }); this.setOptions(options); this._setWidthHeight(options); if (this._element && this.crossOrigin) { this._element.crossOrigin = this.crossOrigin; } }, /** * @private * @param {Object} object Object with filters property * @param {Function} callback Callback to invoke when all fabric.Image.filters instances are created */ _initFilters: function(object, callback) { if (object.filters && object.filters.length) { fabric.util.enlivenObjects(object.filters, function(enlivenedObjects) { callback && callback(enlivenedObjects); }, 'fabric.Image.filters'); } else { callback && callback(); } }, /** * @private * @param {Object} [options] Object with width/height properties */ _setWidthHeight: function(options) { this.width = 'width' in options ? options.width : (this.getElement() ? this.getElement().width || 0 : 0); this.height = 'height' in options ? options.height : (this.getElement() ? this.getElement().height || 0 : 0); }, /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity: function() { return 1; } }); /** * Default CSS class name for canvas * @static * @type String * @default */ fabric.Image.CSS_CANVAS = 'canvas-img'; /** * Alias for getSrc * @static */ fabric.Image.prototype.getSvgSrc = fabric.Image.prototype.getSrc; /** * Creates an instance of fabric.Image from its object representation * @static * @param {Object} object Object to create an instance from * @param {Function} [callback] Callback to invoke when an image instance is created */ fabric.Image.fromObject = function(object, callback) { fabric.util.loadImage(object.src, function(img) { fabric.Image.prototype._initFilters.call(object, object, function(filters) { object.filters = filters || [ ]; var instance = new fabric.Image(img, object); callback && callback(instance); }); }, null, object.crossOrigin); }; /** * Creates an instance of fabric.Image from an URL string * @static * @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) { fabric.util.loadImage(url, function(img) { callback(new fabric.Image(img, imgOptions)); }, null, imgOptions && imgOptions.crossOrigin); }; /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Image.fromElement}) * @static * @see {@link http://www.w3.org/TR/SVG/struct.html#ImageElement} */ fabric.Image.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat('x y width height preserveAspectRatio xlink:href'.split(' ')); /** * Returns {@link fabric.Image} instance from an SVG element * @static * @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} Instance of fabric.Image */ fabric.Image.fromElement = function(element, callback, options) { var parsedAttributes = fabric.parseAttributes(element, fabric.Image.ATTRIBUTE_NAMES), align = 'xMidYMid', meetOrSlice = 'meet', alignX, alignY, aspectRatioAttrs; if (parsedAttributes.preserveAspectRatio) { aspectRatioAttrs = parsedAttributes.preserveAspectRatio.split(' '); } if (aspectRatioAttrs && aspectRatioAttrs.length) { meetOrSlice = aspectRatioAttrs.pop(); if (meetOrSlice !== 'meet' && meetOrSlice !== 'slice') { align = meetOrSlice; meetOrSlice = 'meet'; } else if (aspectRatioAttrs.length) { align = aspectRatioAttrs.pop(); } } //divide align in alignX and alignY alignX = align !== 'none' ? align.slice(1, 4) : 'none'; alignY = align !== 'none' ? align.slice(5, 8) : 'none'; parsedAttributes.alignX = alignX; parsedAttributes.alignY = alignY; parsedAttributes.meetOrSlice = meetOrSlice; fabric.Image.fromURL(parsedAttributes['xlink:href'], callback, extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes)); }; /* _FROM_SVG_END_ */ /** * Indicates that instances of this type are async * @static * @type Boolean * @default */ fabric.Image.async = true; /** * Indicates compression level used when generating PNG under Node (in applyFilters). Any of 0-9 * @static * @type Number * @default */ fabric.Image.pngCompression = 1; })(typeof exports !== 'undefined' ? exports : this); fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** * @private * @return {Number} angle value */ _getAngleValueForStraighten: function() { var angle = this.getAngle() % 360; if (angle > 0) { return Math.round((angle - 1) / 90) * 90; } return Math.round(angle / 90) * 90; }, /** * Straightens an object (rotating it from current angle to one of 0, 90, 180, 270, etc. depending on which is closer) * @return {fabric.Object} thisArg * @chainable */ straighten: function() { this.setAngle(this._getAngleValueForStraighten()); return this; }, /** * Same as {@link fabric.Object.prototype.straighten} but with animation * @param {Object} callbacks Object with callback functions * @param {Function} [callbacks.onComplete] Invoked on completion * @param {Function} [callbacks.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.set('active', false); } }); return this; } }); fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.StaticCanvas.prototype */ { /** * Straightens object, then rerenders canvas * @param {fabric.Object} object Object to straighten * @return {fabric.Canvas} thisArg * @chainable */ straightenObject: function (object) { object.straighten(); this.renderAll(); return this; }, /** * Same as {@link fabric.Canvas.prototype.straightenObject}, but animated * @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 * @memberOf fabric.Image * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#image_filters} * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} */ fabric.Image.filters = fabric.Image.filters || { }; /** * Root filter class from which all filter classes inherit from * @class fabric.Image.filters.BaseFilter * @memberOf fabric.Image.filters */ fabric.Image.filters.BaseFilter = fabric.util.createClass(/** @lends fabric.Image.filters.BaseFilter.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'BaseFilter', /** * Constructor * @param {Object} [options] Options object */ initialize: function(options) { if (options) { this.setOptions(options); } }, /** * Sets filter's properties from options * @param {Object} [options] Options object */ setOptions: function(options) { for (var prop in options) { this[prop] = options[prop]; } }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return { type: this.type }; }, /** * Returns a JSON representation of an instance * @return {Object} JSON */ toJSON: function() { // delegate, not alias return this.toObject(); } }); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Brightness filter class * @class fabric.Image.filters.Brightness * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Brightness#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Brightness({ * brightness: 200 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Brightness = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Brightness.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Brightness', /** * Constructor * @memberOf fabric.Image.filters.Brightness.prototype * @param {Object} [options] Options object * @param {Number} [options.brightness=0] Value to brighten the image up (0..255) */ initialize: function(options) { options = options || { }; this.brightness = options.brightness || 0; }, /** * Applies filter to canvas element * @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); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { brightness: this.brightness }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Brightness} Instance of fabric.Image.filters.Brightness */ fabric.Image.filters.Brightness.fromObject = function(object) { return new fabric.Image.filters.Brightness(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Adapted from html5rocks article * @class fabric.Image.filters.Convolute * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Convolute#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example Sharpen filter * var filter = new fabric.Image.filters.Convolute({ * matrix: [ 0, -1, 0, * -1, 5, -1, * 0, -1, 0 ] * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); * @example Blur filter * var filter = new fabric.Image.filters.Convolute({ * matrix: [ 1/9, 1/9, 1/9, * 1/9, 1/9, 1/9, * 1/9, 1/9, 1/9 ] * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); * @example Emboss filter * var filter = new fabric.Image.filters.Convolute({ * matrix: [ 1, 1, 1, * 1, 0.7, -1, * -1, -1, -1 ] * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); * @example Emboss filter with opaqueness * var filter = new fabric.Image.filters.Convolute({ * opaque: true, * matrix: [ 1, 1, 1, * 1, 0.7, -1, * -1, -1, -1 ] * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Convolute = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Convolute.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Convolute', /** * Constructor * @memberOf fabric.Image.filters.Convolute.prototype * @param {Object} [options] Options object * @param {Boolean} [options.opaque=false] Opaque value (true/false) * @param {Array} [options.matrix] Filter matrix */ initialize: function(options) { options = options || { }; this.opaque = options.opaque; this.matrix = options.matrix || [ 0, 0, 0, 0, 1, 0, 0, 0, 0 ]; var canvasEl = fabric.util.createCanvasElement(); this.tmpCtx = canvasEl.getContext('2d'); }, /** * @private */ _createImageData: function(w, h) { return this.tmpCtx.createImageData(w, h); }, /** * Applies filter to canvas element * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl) { var weights = this.matrix, context = canvasEl.getContext('2d'), pixels = context.getImageData(0, 0, canvasEl.width, canvasEl.height), side = Math.round(Math.sqrt(weights.length)), halfSide = Math.floor(side/2), src = pixels.data, sw = pixels.width, sh = pixels.height, // pad output by the convolution matrix w = sw, h = sh, output = this._createImageData(w, h), dst = output.data, // go through the destination image pixels alphaFac = this.opaque ? 1 : 0; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var sy = y, sx = x, dstOff = (y * w + x) * 4, // calculate the weighed sum of the source image pixels that // fall under the convolution matrix r = 0, g = 0, b = 0, a = 0; for (var cy = 0; cy < side; cy++) { for (var cx = 0; cx < side; cx++) { var scy = sy + cy - halfSide, scx = sx + cx - halfSide; /* jshint maxdepth:5 */ if (scy < 0 || scy > sh || scx < 0 || scx > sw) { continue; } var srcOff = (scy * sw + scx) * 4, wt = weights[cy * side + cx]; r += src[srcOff] * wt; g += src[srcOff + 1] * wt; b += src[srcOff + 2] * wt; a += src[srcOff + 3] * wt; } } dst[dstOff] = r; dst[dstOff + 1] = g; dst[dstOff + 2] = b; dst[dstOff + 3] = a + alphaFac * (255 - a); } } context.putImageData(output, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { opaque: this.opaque, matrix: this.matrix }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Convolute} Instance of fabric.Image.filters.Convolute */ fabric.Image.filters.Convolute.fromObject = function(object) { return new fabric.Image.filters.Convolute(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * GradientTransparency filter class * @class fabric.Image.filters.GradientTransparency * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.GradientTransparency#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.GradientTransparency({ * threshold: 200 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.GradientTransparency = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.GradientTransparency.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'GradientTransparency', /** * Constructor * @memberOf fabric.Image.filters.GradientTransparency.prototype * @param {Object} [options] Options object * @param {Number} [options.threshold=100] Threshold value */ initialize: function(options) { options = options || { }; this.threshold = options.threshold || 100; }, /** * Applies filter to canvas element * @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); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { threshold: this.threshold }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.GradientTransparency} Instance of fabric.Image.filters.GradientTransparency */ fabric.Image.filters.GradientTransparency.fromObject = function(object) { return new fabric.Image.filters.GradientTransparency(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); /** * Grayscale image filter class * @class fabric.Image.filters.Grayscale * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Grayscale(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Grayscale = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Grayscale.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Grayscale', /** * Applies filter to canvas element * @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, len = imageData.width * imageData.height * 4, index = 0, average; while (index < len) { average = (data[index] + data[index + 1] + data[index + 2]) / 3; data[index] = average; data[index + 1] = average; data[index + 2] = average; index += 4; } context.putImageData(imageData, 0, 0); } }); /** * Returns filter instance from an object representation * @static * @return {fabric.Image.filters.Grayscale} Instance of fabric.Image.filters.Grayscale */ fabric.Image.filters.Grayscale.fromObject = function() { return new fabric.Image.filters.Grayscale(); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); /** * Invert filter class * @class fabric.Image.filters.Invert * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Invert(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Invert = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Invert.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Invert', /** * Applies filter to canvas element * @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); } }); /** * Returns filter instance from an object representation * @static * @return {fabric.Image.filters.Invert} Instance of fabric.Image.filters.Invert */ fabric.Image.filters.Invert.fromObject = function() { return new fabric.Image.filters.Invert(); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Mask filter class * See http://resources.aleph-1.com/mask/ * @class fabric.Image.filters.Mask * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Mask#initialize} for constructor definition */ fabric.Image.filters.Mask = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Mask.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Mask', /** * Constructor * @memberOf fabric.Image.filters.Mask.prototype * @param {Object} [options] Options object * @param {fabric.Image} [options.mask] Mask image object * @param {Number} [options.channel=0] Rgb channel (0, 1, 2 or 3) */ initialize: function(options) { options = options || { }; this.mask = options.mask; this.channel = [ 0, 1, 2, 3 ].indexOf(options.channel) > -1 ? options.channel : 0; }, /** * Applies filter to canvas element * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl) { if (!this.mask) { return; } var context = canvasEl.getContext('2d'), imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), data = imageData.data, maskEl = this.mask.getElement(), maskCanvasEl = fabric.util.createCanvasElement(), channel = this.channel, i, iLen = imageData.width * imageData.height * 4; maskCanvasEl.width = maskEl.width; maskCanvasEl.height = maskEl.height; maskCanvasEl.getContext('2d').drawImage(maskEl, 0, 0, maskEl.width, maskEl.height); var maskImageData = maskCanvasEl.getContext('2d').getImageData(0, 0, maskEl.width, maskEl.height), maskData = maskImageData.data; for (i = 0; i < iLen; i += 4) { data[i + 3] = maskData[i + channel]; } context.putImageData(imageData, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { mask: this.mask.toObject(), channel: this.channel }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @param {Function} [callback] Callback to invoke when a mask filter instance is created */ fabric.Image.filters.Mask.fromObject = function(object, callback) { fabric.util.loadImage(object.mask.src, function(img) { object.mask = new fabric.Image(img, object.mask); callback && callback(new fabric.Image.filters.Mask(object)); }); }; /** * Indicates that instances of this type are async * @static * @type Boolean * @default */ fabric.Image.filters.Mask.async = true; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Noise filter class * @class fabric.Image.filters.Noise * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Noise#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Noise({ * noise: 700 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Noise = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Noise.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Noise', /** * Constructor * @memberOf fabric.Image.filters.Noise.prototype * @param {Object} [options] Options object * @param {Number} [options.noise=0] Noise value */ initialize: function(options) { options = options || { }; this.noise = options.noise || 0; }, /** * Applies filter to canvas element * @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); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { noise: this.noise }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Noise} Instance of fabric.Image.filters.Noise */ fabric.Image.filters.Noise.fromObject = function(object) { return new fabric.Image.filters.Noise(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Pixelate filter class * @class fabric.Image.filters.Pixelate * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Pixelate#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Pixelate({ * blocksize: 8 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Pixelate = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Pixelate.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Pixelate', /** * Constructor * @memberOf fabric.Image.filters.Pixelate.prototype * @param {Object} [options] Options object * @param {Number} [options.blocksize=4] Blocksize for pixelate */ initialize: function(options) { options = options || { }; this.blocksize = options.blocksize || 4; }, /** * Applies filter to canvas element * @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.height, jLen = imageData.width, index, i, j, r, g, b, a; for (i = 0; i < iLen; i += this.blocksize) { for (j = 0; j < jLen; j += this.blocksize) { index = (i * 4) * jLen + (j * 4); r = data[index]; g = data[index + 1]; b = data[index + 2]; a = data[index + 3]; /* blocksize: 4 [1,x,x,x,1] [x,x,x,x,1] [x,x,x,x,1] [x,x,x,x,1] [1,1,1,1,1] */ for (var _i = i, _ilen = i + this.blocksize; _i < _ilen; _i++) { for (var _j = j, _jlen = j + this.blocksize; _j < _jlen; _j++) { index = (_i * 4) * jLen + (_j * 4); data[index] = r; data[index + 1] = g; data[index + 2] = b; data[index + 3] = a; } } } } context.putImageData(imageData, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { blocksize: this.blocksize }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Pixelate} Instance of fabric.Image.filters.Pixelate */ fabric.Image.filters.Pixelate.fromObject = function(object) { return new fabric.Image.filters.Pixelate(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Remove white filter class * @class fabric.Image.filters.RemoveWhite * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.RemoveWhite#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.RemoveWhite({ * threshold: 40, * distance: 140 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.RemoveWhite = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.RemoveWhite.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'RemoveWhite', /** * Constructor * @memberOf fabric.Image.filters.RemoveWhite.prototype * @param {Object} [options] Options object * @param {Number} [options.threshold=30] Threshold value * @param {Number} [options.distance=20] Distance value */ initialize: function(options) { options = options || { }; this.threshold = options.threshold || 30; this.distance = options.distance || 20; }, /** * Applies filter to canvas element * @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); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { threshold: this.threshold, distance: this.distance }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.RemoveWhite} Instance of fabric.Image.filters.RemoveWhite */ fabric.Image.filters.RemoveWhite.fromObject = function(object) { return new fabric.Image.filters.RemoveWhite(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); /** * Sepia filter class * @class fabric.Image.filters.Sepia * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Sepia(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Sepia', /** * Applies filter to canvas element * @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); } }); /** * Returns filter instance from an object representation * @static * @return {fabric.Image.filters.Sepia} Instance of fabric.Image.filters.Sepia */ fabric.Image.filters.Sepia.fromObject = function() { return new fabric.Image.filters.Sepia(); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }); /** * Sepia2 filter class * @class fabric.Image.filters.Sepia2 * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Sepia2(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Sepia2 = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Sepia2.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Sepia2', /** * Applies filter to canvas element * @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); } }); /** * Returns filter instance from an object representation * @static * @return {fabric.Image.filters.Sepia2} Instance of fabric.Image.filters.Sepia2 */ fabric.Image.filters.Sepia2.fromObject = function() { return new fabric.Image.filters.Sepia2(); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Tint filter class * Adapted from https://github.com/mezzoblue/PaintbrushJS * @class fabric.Image.filters.Tint * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link fabric.Image.filters.Tint#initialize} for constructor definition * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example Tint filter with hex color and opacity * var filter = new fabric.Image.filters.Tint({ * color: '#3513B0', * opacity: 0.5 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); * @example Tint filter with rgba color * var filter = new fabric.Image.filters.Tint({ * color: 'rgba(53, 21, 176, 0.5)' * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Tint = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Tint.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Tint', /** * Constructor * @memberOf fabric.Image.filters.Tint.prototype * @param {Object} [options] Options object * @param {String} [options.color=#000000] Color to tint the image with * @param {Number} [options.opacity] Opacity value that controls the tint effect's transparency (0..1) */ initialize: function(options) { options = options || { }; this.color = options.color || '#000000'; this.opacity = typeof options.opacity !== 'undefined' ? options.opacity : new fabric.Color(this.color).getAlpha(); }, /** * Applies filter to canvas element * @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, tintR, tintG, tintB, r, g, b, alpha1, source; source = new fabric.Color(this.color).getSource(); tintR = source[0] * this.opacity; tintG = source[1] * this.opacity; tintB = source[2] * this.opacity; alpha1 = 1 - this.opacity; for (i = 0; i < iLen; i+=4) { r = data[i]; g = data[i + 1]; b = data[i + 2]; // alpha compositing data[i] = tintR + r * alpha1; data[i + 1] = tintG + g * alpha1; data[i + 2] = tintB + b * alpha1; } context.putImageData(imageData, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { color: this.color, opacity: this.opacity }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Tint} Instance of fabric.Image.filters.Tint */ fabric.Image.filters.Tint.fromObject = function(object) { return new fabric.Image.filters.Tint(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend; /** * Multiply filter class * Adapted from http://www.laurenscorijn.com/articles/colormath-basics * @class fabric.Image.filters.Multiply * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @example Multiply filter with hex color * var filter = new fabric.Image.filters.Multiply({ * color: '#F0F' * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); * @example Multiply filter with rgb color * var filter = new fabric.Image.filters.Multiply({ * color: 'rgb(53, 21, 176)' * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Multiply = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Multiply.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Multiply', /** * Constructor * @memberOf fabric.Image.filters.Multiply.prototype * @param {Object} [options] Options object * @param {String} [options.color=#000000] Color to multiply the image pixels with */ initialize: function(options) { options = options || { }; this.color = options.color || '#000000'; }, /** * Applies filter to canvas element * @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, source; source = new fabric.Color(this.color).getSource(); for (i = 0; i < iLen; i+=4) { data[i] *= source[0] / 255; data[i + 1] *= source[1] / 255; data[i + 2] *= source[2] / 255; } context.putImageData(imageData, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return extend(this.callSuper('toObject'), { color: this.color }); } }); /** * Returns filter instance from an object representation * @static * @param {Object} object Object to create an instance from * @return {fabric.Image.filters.Multiply} Instance of fabric.Image.filters.Multiply */ fabric.Image.filters.Multiply.fromObject = function(object) { return new fabric.Image.filters.Multiply(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric; /** * Color Blend filter class * @class fabric.Image.filter.Blend * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @example * var filter = new fabric.Image.filters.Blend({ * color: '#000', * mode: 'multiply' * }); * * var filter = new fabric.Image.filters.Blend({ * image: fabricImageObject, * mode: 'multiply', * alpha: 0.5 * }); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Blend = fabric.util.createClass({ type: 'Blend', initialize: function(options) { options = options || {}; this.color = options.color || '#000'; this.image = options.image || false; this.mode = options.mode || 'multiply'; this.alpha = options.alpha || 1; }, applyTo: function(canvasEl) { var context = canvasEl.getContext('2d'), imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height), data = imageData.data, tr, tg, tb, r, g, b, source, isImage = false; if (this.image) { // Blend images isImage = true; var _el = fabric.util.createCanvasElement(); _el.width = this.image.width; _el.height = this.image.height; var tmpCanvas = new fabric.StaticCanvas(_el); tmpCanvas.add(this.image); var context2 = tmpCanvas.getContext('2d'); source = context2.getImageData(0, 0, tmpCanvas.width, tmpCanvas.height).data; } else { // Blend color source = new fabric.Color(this.color).getSource(); tr = source[0] * this.alpha; tg = source[1] * this.alpha; tb = source[2] * this.alpha; } for (var i = 0, len = data.length; i < len; i += 4) { r = data[i]; g = data[i + 1]; b = data[i + 2]; if (isImage) { tr = source[i] * this.alpha; tg = source[i + 1] * this.alpha; tb = source[i + 2] * this.alpha; } switch (this.mode) { case 'multiply': data[i] = r * tr / 255; data[i + 1] = g * tg / 255; data[i + 2] = b * tb / 255; break; case 'screen': data[i] = 1 - (1 - r) * (1 - tr); data[i + 1] = 1 - (1 - g) * (1 - tg); data[i + 2] = 1 - (1 - b) * (1 - tb); break; case 'add': data[i] = Math.min(255, r + tr); data[i + 1] = Math.min(255, g + tg); data[i + 2] = Math.min(255, b + tb); break; case 'diff': case 'difference': data[i] = Math.abs(r - tr); data[i + 1] = Math.abs(g - tg); data[i + 2] = Math.abs(b - tb); break; case 'subtract': var _r = r - tr, _g = g - tg, _b = b - tb; data[i] = (_r < 0) ? 0 : _r; data[i + 1] = (_g < 0) ? 0 : _g; data[i + 2] = (_b < 0) ? 0 : _b; break; case 'darken': data[i] = Math.min(r, tr); data[i + 1] = Math.min(g, tg); data[i + 2] = Math.min(b, tb); break; case 'lighten': data[i] = Math.max(r, tr); data[i + 1] = Math.max(g, tg); data[i + 2] = Math.max(b, tb); break; } } context.putImageData(imageData, 0, 0); }, /** * Returns object representation of an instance * @return {Object} Object representation of an instance */ toObject: function() { return { color: this.color, image: this.image, mode: this.mode, alpha: this.alpha }; } }); fabric.Image.filters.Blend.fromObject = function(object) { return new fabric.Image.filters.Blend(object); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), pow = Math.pow, floor = Math.floor, sqrt = Math.sqrt, abs = Math.abs, max = Math.max, round = Math.round, sin = Math.sin, ceil = Math.ceil; /** * Resize image filter class * @class fabric.Image.filters.Resize * @memberOf fabric.Image.filters * @extends fabric.Image.filters.BaseFilter * @see {@link http://fabricjs.com/image-filters/|ImageFilters demo} * @example * var filter = new fabric.Image.filters.Resize(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ fabric.Image.filters.Resize = fabric.util.createClass(fabric.Image.filters.BaseFilter, /** @lends fabric.Image.filters.Resize.prototype */ { /** * Filter type * @param {String} type * @default */ type: 'Resize', /** * Resize type * @param {String} resizeType * @default */ resizeType: 'hermite', /** * Scale factor for resizing, x axis * @param {Number} scaleX * @default */ scaleX: 0, /** * Scale factor for resizing, y axis * @param {Number} scaleY * @default */ scaleY: 0, /** * LanczosLobes parameter for lanczos filter * @param {Number} lanczosLobes * @default */ lanczosLobes: 3, /** * Applies filter to canvas element * @memberOf fabric.Image.filters.Resize.prototype * @param {Object} canvasEl Canvas element to apply filter to */ applyTo: function(canvasEl, scaleX, scaleY) { this.rcpScaleX = 1 / scaleX; this.rcpScaleY = 1 / scaleY; var oW = canvasEl.width, oH = canvasEl.height, dW = round(oW * scaleX), dH = round(oH * scaleY), imageData; if (this.resizeType === 'sliceHack') { imageData = this.sliceByTwo(canvasEl, oW, oH, dW, dH); } if (this.resizeType === 'hermite') { imageData = this.hermiteFastResize(canvasEl, oW, oH, dW, dH); } if (this.resizeType === 'bilinear') { imageData = this.bilinearFiltering(canvasEl, oW, oH, dW, dH); } if (this.resizeType === 'lanczos') { imageData = this.lanczosResize(canvasEl, oW, oH, dW, dH); } canvasEl.width = dW; canvasEl.height = dH; canvasEl.getContext('2d').putImageData(imageData, 0, 0); }, sliceByTwo: function(canvasEl, width, height, newWidth, newHeight) { var context = canvasEl.getContext('2d'), imageData, multW = 0.5, multH = 0.5, signW = 1, signH = 1, doneW = false, doneH = false, stepW = width, stepH = height, tmpCanvas = fabric.util.createCanvasElement(), tmpCtx = tmpCanvas.getContext('2d'); newWidth = floor(newWidth); newHeight = floor(newHeight); tmpCanvas.width = max(newWidth, width); tmpCanvas.height = max(newHeight, height); if (newWidth > width) { multW = 2; signW = -1; } if (newHeight > height) { multH = 2; signH = -1; } imageData = context.getImageData(0, 0, width, height); canvasEl.width = max(newWidth, width); canvasEl.height = max(newHeight, height); context.putImageData(imageData, 0, 0); while (!doneW || !doneH) { width = stepW; height = stepH; if (newWidth * signW < floor(stepW * multW * signW)) { stepW = floor(stepW * multW); } else { stepW = newWidth; doneW = true; } if (newHeight * signH < floor(stepH * multH * signH)) { stepH = floor(stepH * multH); } else { stepH = newHeight; doneH = true; } imageData = context.getImageData(0, 0, width, height); tmpCtx.putImageData(imageData, 0, 0); context.clearRect(0, 0, stepW, stepH); context.drawImage(tmpCanvas, 0, 0, width, height, 0, 0, stepW, stepH); } return context.getImageData(0, 0, newWidth, newHeight); }, lanczosResize: function(canvasEl, oW, oH, dW, dH) { function lanczosCreate(lobes) { return function(x) { if (x > lobes) { return 0; } x *= Math.PI; if (abs(x) < 1e-16) { return 1; } var xx = x / lobes; return sin(x) * sin(xx) / x / xx; }; } function process(u) { var v, i, weight, idx, a, red, green, blue, alpha, fX, fY; center.x = (u + 0.5) * ratioX; icenter.x = floor(center.x); for (v = 0; v < dH; v++) { center.y = (v + 0.5) * ratioY; icenter.y = floor(center.y); a = 0, red = 0, green = 0, blue = 0, alpha = 0; for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { if (i < 0 || i >= oW) { continue; } fX = floor(1000 * abs(i - center.x)); if (!cacheLanc[fX]) { cacheLanc[fX] = { }; } for (var j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { if (j < 0 || j >= oH) { continue; } fY = floor(1000 * abs(j - center.y)); if (!cacheLanc[fX][fY]) { cacheLanc[fX][fY] = lanczos(sqrt(pow(fX * rcpRatioX, 2) + pow(fY * rcpRatioY, 2)) / 1000); } weight = cacheLanc[fX][fY]; if (weight > 0) { idx = (j * oW + i) * 4; a += weight; red += weight * srcData[idx]; green += weight * srcData[idx + 1]; blue += weight * srcData[idx + 2]; alpha += weight * srcData[idx + 3]; } } } idx = (v * dW + u) * 4; destData[idx] = red / a; destData[idx + 1] = green / a; destData[idx + 2] = blue / a; destData[idx + 3] = alpha / a; } if (++u < dW) { return process(u); } else { return destImg; } } var context = canvasEl.getContext('2d'), srcImg = context.getImageData(0, 0, oW, oH), destImg = context.getImageData(0, 0, dW, dH), srcData = srcImg.data, destData = destImg.data, lanczos = lanczosCreate(this.lanczosLobes), ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, range2X = ceil(ratioX * this.lanczosLobes / 2), range2Y = ceil(ratioY * this.lanczosLobes / 2), cacheLanc = { }, center = { }, icenter = { }; return process(0); }, bilinearFiltering: function(canvasEl, w, h, w2, h2) { var a, b, c, d, x, y, i, j, xDiff, yDiff, chnl, color, offset = 0, origPix, ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, context = canvasEl.getContext('2d'), w4 = 4 * (w - 1), img = context.getImageData(0, 0, w, h), pixels = img.data, destImage = context.getImageData(0, 0, w2, h2), destPixels = destImage.data; for (i = 0; i < h2; i++) { for (j = 0; j < w2; j++) { x = floor(ratioX * j); y = floor(ratioY * i); xDiff = ratioX * j - x; yDiff = ratioY * i - y; origPix = 4 * (y * w + x); for (chnl = 0; chnl < 4; chnl++) { a = pixels[origPix + chnl]; b = pixels[origPix + 4 + chnl]; c = pixels[origPix + w4 + chnl]; d = pixels[origPix + w4 + 4 + chnl]; color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; destPixels[offset++] = color; } } } return destImage; }, hermiteFastResize: function(canvasEl, oW, oH, dW, dH) { var ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, ratioWHalf = ceil(ratioW / 2), ratioHHalf = ceil(ratioH / 2), context = canvasEl.getContext('2d'), img = context.getImageData(0, 0, oW, oH), data = img.data, img2 = context.getImageData(0, 0, dW, dH), data2 = img2.data; for (var j = 0; j < dH; j++) { for (var i = 0; i < dW; i++) { var x2 = (i + j * dW) * 4, weight = 0, weights = 0, weightsAlpha = 0, gxR = 0, gxG = 0, gxB = 0, gxA = 0, centerY = (j + 0.5) * ratioH; for (var yy = floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { var dy = abs(centerY - (yy + 0.5)) / ratioHHalf, centerX = (i + 0.5) * ratioW, w0 = dy * dy; for (var xx = floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { var dx = abs(centerX - (xx + 0.5)) / ratioWHalf, w = sqrt(w0 + dx * dx); /*jshint maxdepth:5 */ if (w > 1 && w < -1) { continue; } //hermite filter weight = 2 * w * w * w - 3 * w * w + 1; if (weight > 0) { dx = 4 * (xx + yy * oW); //alpha gxA += weight * data[dx + 3]; weightsAlpha += weight; //colors /*jshint maxdepth:6 */ if (data[dx + 3] < 255) { weight = weight * data[dx + 3] / 250; } /*jshint maxdepth:5 */ gxR += weight * data[dx]; gxG += weight * data[dx + 1]; gxB += weight * data[dx + 2]; weights += weight; } /*jshint maxdepth:4 */ } } data2[x2] = gxR / weights; data2[x2 + 1] = gxG / weights; data2[x2 + 2] = gxB / weights; data2[x2 + 3] = gxA / weightsAlpha; } } return img2; } }); /** * Returns filter instance from an object representation * @static * @return {fabric.Image.filters.Resize} Instance of fabric.Image.filters.Resize */ fabric.Image.filters.Resize.fromObject = function() { return new fabric.Image.filters.Resize(); }; })(typeof exports !== 'undefined' ? exports : this); (function(global) { 'use strict'; var fabric = global.fabric || (global.fabric = { }), extend = fabric.util.object.extend, clone = fabric.util.object.clone, toFixed = fabric.util.toFixed, supportsLineDash = fabric.StaticCanvas.supports('setLineDash'); if (fabric.Text) { fabric.warn('fabric.Text is already defined'); return; } var stateProperties = fabric.Object.prototype.stateProperties.concat(); stateProperties.push( 'fontFamily', 'fontWeight', 'fontSize', 'text', 'textDecoration', 'textAlign', 'fontStyle', 'lineHeight', 'textBackgroundColor', 'useNative', 'path' ); /** * Text class * @class fabric.Text * @extends fabric.Object * @return {fabric.Text} thisArg * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#text} * @see {@link fabric.Text#initialize} for constructor definition */ fabric.Text = fabric.util.createClass(fabric.Object, /** @lends fabric.Text.prototype */ { /** * Properties which when set cause object to change dimensions * @type Object * @private */ _dimensionAffectingProps: { fontSize: true, fontWeight: true, fontFamily: true, textDecoration: true, fontStyle: true, lineHeight: true, stroke: true, strokeWidth: true, text: true }, /** * @private */ _reNewline: /\r?\n/, /** * Retrieves object's fontSize * @method getFontSize * @memberOf fabric.Text.prototype * @return {String} Font size (in pixels) */ /** * Sets object's fontSize * @method setFontSize * @memberOf fabric.Text.prototype * @param {Number} fontSize Font size (in pixels) * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontWeight * @method getFontWeight * @memberOf fabric.Text.prototype * @return {(String|Number)} Font weight */ /** * Sets object's fontWeight * @method setFontWeight * @memberOf fabric.Text.prototype * @param {(Number|String)} fontWeight Font weight * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontFamily * @method getFontFamily * @memberOf fabric.Text.prototype * @return {String} Font family */ /** * Sets object's fontFamily * @method setFontFamily * @memberOf fabric.Text.prototype * @param {String} fontFamily Font family * @return {fabric.Text} * @chainable */ /** * Retrieves object's text * @method getText * @memberOf fabric.Text.prototype * @return {String} text */ /** * Sets object's text * @method setText * @memberOf fabric.Text.prototype * @param {String} text Text * @return {fabric.Text} * @chainable */ /** * Retrieves object's textDecoration * @method getTextDecoration * @memberOf fabric.Text.prototype * @return {String} Text decoration */ /** * Sets object's textDecoration * @method setTextDecoration * @memberOf fabric.Text.prototype * @param {String} textDecoration Text decoration * @return {fabric.Text} * @chainable */ /** * Retrieves object's fontStyle * @method getFontStyle * @memberOf fabric.Text.prototype * @return {String} Font style */ /** * Sets object's fontStyle * @method setFontStyle * @memberOf fabric.Text.prototype * @param {String} fontStyle Font style * @return {fabric.Text} * @chainable */ /** * Retrieves object's lineHeight * @method getLineHeight * @memberOf fabric.Text.prototype * @return {Number} Line height */ /** * Sets object's lineHeight * @method setLineHeight * @memberOf fabric.Text.prototype * @param {Number} lineHeight Line height * @return {fabric.Text} * @chainable */ /** * Retrieves object's textAlign * @method getTextAlign * @memberOf fabric.Text.prototype * @return {String} Text alignment */ /** * Sets object's textAlign * @method setTextAlign * @memberOf fabric.Text.prototype * @param {String} textAlign Text alignment * @return {fabric.Text} * @chainable */ /** * Retrieves object's textBackgroundColor * @method getTextBackgroundColor * @memberOf fabric.Text.prototype * @return {String} Text background color */ /** * Sets object's textBackgroundColor * @method setTextBackgroundColor * @memberOf fabric.Text.prototype * @param {String} textBackgroundColor Text background color * @return {fabric.Text} * @chainable */ /** * Type of an object * @type String * @default */ type: 'text', /** * Font size (in pixels) * @type Number * @default */ fontSize: 40, /** * Font weight (e.g. bold, normal, 400, 600, 800) * @type {(Number|String)} * @default */ fontWeight: 'normal', /** * Font family * @type String * @default */ fontFamily: 'Times New Roman', /** * Text decoration Possible values: "", "underline", "overline" or "line-through". * @type String * @default */ textDecoration: '', /** * Text alignment. Possible values: "left", "center", or "right". * @type String * @default */ textAlign: 'left', /** * Font style . Possible values: "", "normal", "italic" or "oblique". * @type String * @default */ fontStyle: '', /** * Line height * @type Number * @default */ lineHeight: 1.3, /** * Background color of text lines * @type String * @default */ textBackgroundColor: '', /** * URL of a font file, when using Cufon * @type String | null * @default */ path: null, /** * Indicates whether canvas native text methods should be used to render text (otherwise, Cufon is used) * @type Boolean * @default */ useNative: true, /** * List of properties to consider when checking if * state of an object is changed ({@link fabric.Object#hasStateChanged}) * as well as for history (undo/redo) purposes * @type Array */ stateProperties: stateProperties, /** * When defined, an object is rendered via stroke and this property specifies its color. * Backwards incompatibility note: This property was named "strokeStyle" until v1.1.6 * @type String * @default */ stroke: null, /** * Shadow object representing shadow of this shape. * Backwards incompatibility note: This property was named "textShadow" (String) until v1.2.11 * @type fabric.Shadow * @default */ shadow: null, /** * Constructor * @param {String} text Text string * @param {Object} [options] Options object * @return {fabric.Text} thisArg */ initialize: function(text, options) { options = options || { }; this.text = text; this.__skipDimension = true; this.setOptions(options); this.__skipDimension = false; this._initDimensions(); }, /** * Renders text object on offscreen canvas, so that it would get dimensions * @private */ _initDimensions: function() { if (this.__skipDimension) { return; } var canvasEl = fabric.util.createCanvasElement(); this._render(canvasEl.getContext('2d')); }, /** * Returns string representation of an instance * @return {String} String representation of text object */ toString: function() { return '#'; }, /** * @private * @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 * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderViaNative: function(ctx) { var textLines = this.text.split(this._reNewline); this._setTextStyles(ctx); this.width = this._getTextWidth(ctx, textLines); this.height = this._getTextHeight(ctx, textLines); this.clipTo && fabric.util.clipContext(this, ctx); this._renderTextBackground(ctx, textLines); this._translateForTextAlign(ctx); this._renderText(ctx, textLines); if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.restore(); } this._renderTextDecoration(ctx, textLines); this.clipTo && ctx.restore(); this._setBoundaries(ctx, textLines); this._totalLineHeight = 0; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderText: function(ctx, textLines) { ctx.save(); this._setOpacity(ctx); this._setShadow(ctx); this._setupCompositeOperation(ctx); this._renderTextFill(ctx, textLines); this._renderTextStroke(ctx, textLines); this._restoreCompositeOperation(ctx); this._removeShadow(ctx); ctx.restore(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _translateForTextAlign: function(ctx) { if (this.textAlign !== 'left' && this.textAlign !== 'justify') { ctx.save(); ctx.translate(this.textAlign === 'center' ? (this.width / 2) : this.width, 0); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _setBoundaries: function(ctx, textLines) { this._boundaries = [ ]; for (var i = 0, len = textLines.length; i < len; i++) { var lineWidth = this._getLineWidth(ctx, textLines[i]), lineLeftOffset = this._getLineLeftOffset(lineWidth); this._boundaries.push({ height: this.fontSize * this.lineHeight, width: lineWidth, left: lineLeftOffset }); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _setTextStyles: function(ctx) { this._setFillStyles(ctx); this._setStrokeStyles(ctx); ctx.textBaseline = 'alphabetic'; if (!this.skipTextAlign) { ctx.textAlign = this.textAlign; } ctx.font = this._getFontDeclaration(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines * @return {Number} Height of fabric.Text object */ _getTextHeight: function(ctx, textLines) { return this.fontSize * textLines.length * this.lineHeight; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines * @return {Number} Maximum width of fabric.Text object */ _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 * @param {String} method Method name ("fillText" or "strokeText") * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} chars Chars to render * @param {Number} left Left position of text * @param {Number} top Top position of text */ _renderChars: function(method, ctx, chars, left, top) { ctx[method](chars, left, top); }, /** * @private * @param {String} method Method name ("fillText" or "strokeText") * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Text to render * @param {Number} left Left position of text * @param {Number} top Top position of text * @param {Number} lineIndex Index of a line in a text */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // lift the line by quarter of fontSize top -= this.fontSize / 4; // short-circuit if (this.textAlign !== 'justify') { this._renderChars(method, ctx, line, left, top, lineIndex); return; } var lineWidth = ctx.measureText(line).width, totalWidth = this.width; if (totalWidth > lineWidth) { // stretch the line var words = line.split(/\s+/), wordsWidth = ctx.measureText(line.replace(/\s+/g, '')).width, widthDiff = totalWidth - wordsWidth, numSpaces = words.length - 1, spaceWidth = widthDiff / numSpaces, leftOffset = 0; for (var i = 0, len = words.length; i < len; i++) { this._renderChars(method, ctx, words[i], left + leftOffset, top, lineIndex); leftOffset += ctx.measureText(words[i]).width + spaceWidth; } } else { this._renderChars(method, ctx, line, left, top, lineIndex); } }, /** * @private * @return {Number} Left offset */ _getLeftOffset: function() { return -this.width / 2; }, /** * @private * @return {Number} Top offset */ _getTopOffset: function() { return -this.height / 2; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextFill: function(ctx, textLines) { if (!this.fill && !this._skipFillStrokeCheck) { return; } this._boundaries = [ ]; var lineHeights = 0; for (var i = 0, len = textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; this._renderTextLine( 'fillText', ctx, textLines[i], this._getLeftOffset(), this._getTopOffset() + lineHeights, i ); } if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextStroke: function(ctx, textLines) { if ((!this.stroke || this.strokeWidth === 0) && !this._skipFillStrokeCheck) { return; } var lineHeights = 0; ctx.save(); if (this.strokeDashArray) { // Spec requires the concatenation of two copies the dash list when the number of elements is odd if (1 & this.strokeDashArray.length) { this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray); } supportsLineDash && ctx.setLineDash(this.strokeDashArray); } ctx.beginPath(); for (var i = 0, len = textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i, textLines); lineHeights += heightOfLine; this._renderTextLine( 'strokeText', ctx, textLines[i], this._getLeftOffset(), this._getTopOffset() + lineHeights, i ); } ctx.closePath(); ctx.restore(); }, _getHeightOfLine: function() { return this.fontSize * this.lineHeight; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextBackground: function(ctx, textLines) { this._renderTextBoxBackground(ctx); this._renderTextLinesBackground(ctx, textLines); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderTextBoxBackground: function(ctx) { if (!this.backgroundColor) { return; } ctx.save(); ctx.fillStyle = this.backgroundColor; ctx.fillRect( this._getLeftOffset(), this._getTopOffset(), this.width, this.height ); ctx.restore(); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextLinesBackground: function(ctx, textLines) { if (!this.textBackgroundColor) { return; } ctx.save(); ctx.fillStyle = this.textBackgroundColor; for (var i = 0, len = textLines.length; i < len; i++) { if (textLines[i] !== '') { var lineWidth = this._getLineWidth(ctx, textLines[i]), lineLeftOffset = this._getLineLeftOffset(lineWidth); ctx.fillRect( this._getLeftOffset() + lineLeftOffset, this._getTopOffset() + (i * this.fontSize * this.lineHeight), lineWidth, this.fontSize * this.lineHeight ); } } ctx.restore(); }, /** * @private * @param {Number} lineWidth Width of text line * @return {Number} Line left offset */ _getLineLeftOffset: function(lineWidth) { if (this.textAlign === 'center') { return (this.width - lineWidth) / 2; } if (this.textAlign === 'right') { return this.width - lineWidth; } return 0; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Text line * @return {Number} Line width */ _getLineWidth: function(ctx, line) { return this.textAlign === 'justify' ? this.width : ctx.measureText(line).width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextDecoration: function(ctx, textLines) { if (!this.textDecoration) { return; } // var halfOfVerticalBox = this.originY === 'top' ? 0 : this._getTextHeight(ctx, textLines) / 2; var halfOfVerticalBox = this._getTextHeight(ctx, textLines) / 2, _this = this; /** @ignore */ function renderLinesAtOffset(offset) { for (var i = 0, len = textLines.length; i < len; i++) { var lineWidth = _this._getLineWidth(ctx, textLines[i]), lineLeftOffset = _this._getLineLeftOffset(lineWidth); ctx.fillRect( _this._getLeftOffset() + lineLeftOffset, ~~((offset + (i * _this._getHeightOfLine(ctx, i, textLines))) - halfOfVerticalBox), lineWidth, 1); } } if (this.textDecoration.indexOf('underline') > -1) { renderLinesAtOffset(this.fontSize * this.lineHeight); } if (this.textDecoration.indexOf('line-through') > -1) { renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize / 2); } if (this.textDecoration.indexOf('overline') > -1) { renderLinesAtOffset(this.fontSize * this.lineHeight - this.fontSize); } }, /** * @private */ _getFontDeclaration: function() { return [ // node-canvas needs "weight style", while browsers need "style weight" (fabric.isLikelyNode ? this.fontWeight : this.fontStyle), (fabric.isLikelyNode ? this.fontStyle : this.fontWeight), this.fontSize + 'px', (fabric.isLikelyNode ? ('"' + this.fontFamily + '"') : this.fontFamily) ].join(' '); }, /** * Renders text instance on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on */ render: function(ctx, noTransform) { // do not render if object is not visible if (!this.visible) { return; } ctx.save(); if (!noTransform) { this.transform(ctx); } var isInPathGroup = this.group && this.group.type === 'path-group'; if (isInPathGroup) { ctx.translate(-this.group.width/2, -this.group.height/2); } if (this.transformMatrix) { ctx.transform.apply(ctx, this.transformMatrix); } if (isInPathGroup) { ctx.translate(this.left, this.top); } this._render(ctx); ctx.restore(); }, /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { var object = extend(this.callSuper('toObject', propertiesToInclude), { text: this.text, fontSize: this.fontSize, fontWeight: this.fontWeight, fontFamily: this.fontFamily, fontStyle: this.fontStyle, lineHeight: this.lineHeight, textDecoration: this.textDecoration, textAlign: this.textAlign, path: this.path, textBackgroundColor: this.textBackgroundColor, useNative: this.useNative }); if (!this.includeDefaultValues) { this._removeDefaultValues(object); } return object; }, /* _TO_SVG_START_ */ /** * Returns SVG representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { var markup = [ ], textLines = this.text.split(this._reNewline), offsets = this._getSVGLeftTopOffsets(textLines), textAndBg = this._getSVGTextAndBg(offsets.lineTop, offsets.textLeft, textLines), shadowSpans = this._getSVGShadows(offsets.lineTop, textLines); // move top offset by an ascent offsets.textTop += (this._fontAscent ? ((this._fontAscent / 5) * this.lineHeight) : 0); this._wrapSVGTextAndBg(markup, textAndBg, shadowSpans, offsets); return reviver ? reviver(markup.join('')) : markup.join(''); }, /** * @private */ _getSVGLeftTopOffsets: function(textLines) { var lineTop = this.useNative ? this.fontSize * this.lineHeight : (-this._fontAscent - ((this._fontAscent / 5) * this.lineHeight)), textLeft = -(this.width/2), textTop = this.useNative ? (this.fontSize * this.lineHeight - 0.25 * this.fontSize) // to lift by 1 / 4 of font height. : (this.height/2) - (textLines.length * this.fontSize) - this._totalLineHeight; return { textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0), textTop: textTop + (this.group && this.group.type === 'path-group' ? this.top : 0), lineTop: lineTop }; }, /** * @private */ _wrapSVGTextAndBg: function(markup, textAndBg, shadowSpans, offsets) { markup.push( '\n', textAndBg.textBgRects.join(''), '', shadowSpans.join(''), textAndBg.textSpans.join(''), '\n', '\n' ); }, /** * @private * @param {Number} lineHeight * @param {Array} textLines Array of all text lines * @return {Array} */ _getSVGShadows: function(lineHeight, textLines) { var shadowSpans = [], i, len, lineTopOffsetMultiplier = 1; if (!this.shadow || !this._boundaries) { return shadowSpans; } for (i = 0, len = textLines.length; i < len; i++) { if (textLines[i] !== '') { var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? this._boundaries[i].left : 0; shadowSpans.push( '', fabric.util.string.escapeXml(textLines[i]), ''); lineTopOffsetMultiplier = 1; } else { // in some environments (e.g. IE 7 & 8) empty tspans are completely ignored, using a lineTopOffsetMultiplier // prevents empty tspans lineTopOffsetMultiplier++; } } return shadowSpans; }, /** * @private * @param {Number} lineHeight * @param {Number} textLeftOffset Text left offset * @param {Array} textLines Array of all text lines * @return {Object} */ _getSVGTextAndBg: function(lineHeight, textLeftOffset, textLines) { var textSpans = [ ], textBgRects = [ ], lineTopOffsetMultiplier = 1; // bounding-box background this._setSVGBg(textBgRects); // text and text-background for (var i = 0, len = textLines.length; i < len; i++) { if (textLines[i] !== '') { this._setSVGTextLineText(textLines[i], i, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); 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.textBackgroundColor || !this._boundaries) { continue; } this._setSVGTextLineBg(textBgRects, i, textLeftOffset, lineHeight); } return { textSpans: textSpans, textBgRects: textBgRects }; }, _setSVGTextLineText: function(textLine, i, textSpans, lineHeight, lineTopOffsetMultiplier) { var lineLeftOffset = (this._boundaries && this._boundaries[i]) ? toFixed(this._boundaries[i].left, 2) : 0; textSpans.push( ' elements since setting opacity // on containing one doesn't work in Illustrator this._getFillAttributes(this.fill), '>', fabric.util.string.escapeXml(textLine), '' ); }, _setSVGTextLineBg: function(textBgRects, i, textLeftOffset, lineHeight) { textBgRects.push( '\n'); }, _setSVGBg: function(textBgRects) { if (this.backgroundColor && this._boundaries) { textBgRects.push( ''); } }, /** * 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 * * @private * @param {Any} value * @return {String} */ _getFillAttributes: function(value) { var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : ''; if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) { return 'fill="' + value + '"'; } return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"'; }, /* _TO_SVG_END_ */ /** * Sets specified property to a specified value * @param {String} key * @param {Any} value * @return {fabric.Text} thisArg * @chainable */ _set: function(key, value) { if (key === 'fontFamily' && this.path) { this.path = this.path.replace(/(.*?)([^\/]*)(\.font\.js)/, '$1' + value + '$3'); } this.callSuper('_set', key, value); if (key in this._dimensionAffectingProps) { this._initDimensions(); this.setCoords(); } }, /** * Returns complexity of an instance * @return {Number} complexity */ complexity: function() { return 1; } }); /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement}) * @static * @memberOf fabric.Text * @see: http://www.w3.org/TR/SVG/text.html#TextElement */ fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat( 'x y dx dy font-family font-style font-weight font-size text-decoration text-anchor'.split(' ')); /** * Default SVG font size * @static * @memberOf fabric.Text */ fabric.Text.DEFAULT_SVG_FONT_SIZE = 16; /** * Returns fabric.Text instance from an SVG element (not yet implemented) * @static * @memberOf fabric.Text * @param {SVGElement} element Element to parse * @param {Object} [options] Options object * @return {fabric.Text} Instance of fabric.Text */ fabric.Text.fromElement = function(element, options) { if (!element) { return null; } var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES); options = fabric.util.object.extend((options ? fabric.util.object.clone(options) : { }), parsedAttributes); options.top = options.top || 0; options.left = options.left || 0; if ('dx' in parsedAttributes) { options.left += parsedAttributes.dx; } if ('dy' in parsedAttributes) { options.top += parsedAttributes.dy; } if (!('fontSize' in options)) { options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE; } if (!options.originX) { options.originX = 'left'; } options.top += options.fontSize / 4; var text = new fabric.Text(element.textContent, options), /* Adjust positioning: x/y attributes in SVG correspond to the bottom-left corner of text bounding box top/left properties in Fabric correspond to center point of text bounding box */ offX = 0; if (text.originX === 'left') { offX = text.getWidth() / 2; } if (text.originX === 'right') { offX = -text.getWidth() / 2; } text.set({ left: text.getLeft() + offX, top: text.getTop() - text.getHeight() / 2 }); return text; }; /* _FROM_SVG_END_ */ /** * Returns fabric.Text instance from an object representation * @static * @memberOf fabric.Text * @param {Object} object Object to create an instance from * @return {fabric.Text} Instance of fabric.Text */ fabric.Text.fromObject = function(object) { return new fabric.Text(object.text, clone(object)); }; fabric.util.createAccessors(fabric.Text); })(typeof exports !== 'undefined' ? exports : this); (function() { var clone = fabric.util.object.clone; /** * IText class (introduced in v1.4) Events are also fired with "text:" * prefix when observing canvas. * @class fabric.IText * @extends fabric.Text * @mixes fabric.Observable * * @fires changed * @fires selection:changed * @fires editing:entered * @fires editing:exited * * @return {fabric.IText} thisArg * @see {@link fabric.IText#initialize} for constructor definition * *

Supported key combinations:

*
    *   Move cursor:                    left, right, up, down
    *   Select character:               shift + left, shift + right
    *   Select text vertically:         shift + up, shift + down
    *   Move cursor by word:            alt + left, alt + right
    *   Select words:                   shift + alt + left, shift + alt + right
    *   Move cursor to line start/end:  cmd + left, cmd + right
    *   Select till start/end of line:  cmd + shift + left, cmd + shift + right
    *   Jump to start/end of text:      cmd + up, cmd + down
    *   Select till start/end of text:  cmd + shift + up, cmd + shift + down
    *   Delete character:               backspace
    *   Delete word:                    alt + backspace
    *   Delete line:                    cmd + backspace
    *   Forward delete:                 delete
    *   Copy text:                      ctrl/cmd + c
    *   Paste text:                     ctrl/cmd + v
    *   Cut text:                       ctrl/cmd + x
    *   Select entire text:             ctrl/cmd + a
    * 
* *

Supported mouse/touch combination

*
    *   Position cursor:                click/touch
    *   Create selection:               click/touch & drag
    *   Create selection:               click & shift + click
    *   Select word:                    double click
    *   Select line:                    triple click
    * 
*/ fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ { /** * Type of an object * @type String * @default */ type: 'i-text', /** * Index where text selection starts (or where cursor is when there is no selection) * @type Nubmer * @default */ selectionStart: 0, /** * Index where text selection ends * @type Nubmer * @default */ selectionEnd: 0, /** * Color of text selection * @type String * @default */ selectionColor: 'rgba(17,119,255,0.3)', /** * Indicates whether text is in editing mode * @type Boolean * @default */ isEditing: false, /** * Indicates whether a text can be edited * @type Boolean * @default */ editable: true, /** * Border color of text object while it's in editing mode * @type String * @default */ editingBorderColor: 'rgba(102,153,255,0.25)', /** * Width of cursor (in px) * @type Number * @default */ cursorWidth: 2, /** * Color of default cursor (when not overwritten by character style) * @type String * @default */ cursorColor: '#333', /** * Delay between cursor blink (in ms) * @type Number * @default */ cursorDelay: 1000, /** * Duration of cursor fadein (in ms) * @type Number * @default */ cursorDuration: 600, /** * Object containing character styles * (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line) * @type Object * @default */ styles: null, /** * Indicates whether internal text char widths can be cached * @type Boolean * @default */ caching: true, /** * @private * @type Boolean * @default */ _skipFillStrokeCheck: true, /** * @private */ _reSpace: /\s|\n/, /** * @private */ _fontSizeFraction: 4, /** * @private */ _currentCursorOpacity: 0, /** * @private */ _selectionDirection: null, /** * @private */ _abortCursorAnimation: false, /** * @private */ _charWidthsCache: { }, /** * Constructor * @param {String} text Text string * @param {Object} [options] Options object * @return {fabric.IText} thisArg */ initialize: function(text, options) { this.styles = options ? (options.styles || { }) : { }; this.callSuper('initialize', text, options); this.initBehavior(); fabric.IText.instances.push(this); // caching this.__lineWidths = { }; this.__lineHeights = { }; this.__lineOffsets = { }; }, /** * Returns true if object has no styling */ isEmptyStyles: function() { if (!this.styles) { return true; } var obj = this.styles; for (var p1 in obj) { for (var p2 in obj[p1]) { /*jshint unused:false */ for (var p3 in obj[p1][p2]) { return false; } } } return true; }, /** * Sets selection start (left boundary of a selection) * @param {Number} index Index to set selection start to */ setSelectionStart: function(index) { if (this.selectionStart !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); } this.selectionStart = index; this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index); }, /** * Sets selection end (right boundary of a selection) * @param {Number} index Index to set selection end to */ setSelectionEnd: function(index) { if (this.selectionEnd !== index) { this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); } this.selectionEnd = index; this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index); }, /** * Gets style of a current selection/cursor (at the start position) * @param {Number} [startIndex] Start index to get styles at * @param {Number} [endIndex] End index to get styles at * @return {Object} styles Style object at a specified (or current) index */ getSelectionStyles: function(startIndex, endIndex) { if (arguments.length === 2) { var styles = [ ]; for (var i = startIndex; i < endIndex; i++) { styles.push(this.getSelectionStyles(i)); } return styles; } var loc = this.get2DCursorLocation(startIndex); if (this.styles[loc.lineIndex]) { return this.styles[loc.lineIndex][loc.charIndex] || { }; } return { }; }, /** * Sets style of a current selection * @param {Object} [styles] Styles object * @return {fabric.IText} thisArg * @chainable */ setSelectionStyles: function(styles) { if (this.selectionStart === this.selectionEnd) { this._extendStyles(this.selectionStart, styles); } else { for (var i = this.selectionStart; i < this.selectionEnd; i++) { this._extendStyles(i, styles); } } return this; }, /** * @private */ _extendStyles: function(index, styles) { var loc = this.get2DCursorLocation(index); if (!this.styles[loc.lineIndex]) { this.styles[loc.lineIndex] = { }; } if (!this.styles[loc.lineIndex][loc.charIndex]) { this.styles[loc.lineIndex][loc.charIndex] = { }; } fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render: function(ctx) { this.callSuper('_render', ctx); this.ctx = ctx; this.isEditing && this.renderCursorOrSelection(); }, /** * Renders cursor or selection (depending on what exists) */ renderCursorOrSelection: function() { if (!this.active) { return; } var chars = this.text.split(''), boundaries; if (this.selectionStart === this.selectionEnd) { boundaries = this._getCursorBoundaries(chars, 'cursor'); this.renderCursor(boundaries); } else { boundaries = this._getCursorBoundaries(chars, 'selection'); this.renderSelection(chars, boundaries); } }, /** * Returns 2d representation (lineIndex and charIndex) of cursor (or selection start) * @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used. */ get2DCursorLocation: function(selectionStart) { if (typeof selectionStart === 'undefined') { selectionStart = this.selectionStart; } var textBeforeCursor = this.text.slice(0, selectionStart), linesBeforeCursor = textBeforeCursor.split(this._reNewline); return { lineIndex: linesBeforeCursor.length - 1, charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length }; }, /** * Returns complete style of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {Object} Character style */ getCurrentCharStyle: function(lineIndex, charIndex) { var style = this.styles[lineIndex] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)]; return { fontSize: style && style.fontSize || this.fontSize, fill: style && style.fill || this.fill, textBackgroundColor: style && style.textBackgroundColor || this.textBackgroundColor, textDecoration: style && style.textDecoration || this.textDecoration, fontFamily: style && style.fontFamily || this.fontFamily, fontWeight: style && style.fontWeight || this.fontWeight, fontStyle: style && style.fontStyle || this.fontStyle, stroke: style && style.stroke || this.stroke, strokeWidth: style && style.strokeWidth || this.strokeWidth }; }, /** * Returns fontSize of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {Number} Character font size */ getCurrentCharFontSize: function(lineIndex, charIndex) { return ( this.styles[lineIndex] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize; }, /** * Returns color (fill) of char at the current cursor * @param {Number} lineIndex Line index * @param {Number} charIndex Char index * @return {String} Character color (fill) */ getCurrentCharColor: function(lineIndex, charIndex) { return ( this.styles[lineIndex] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] && this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor; }, /** * Returns cursor boundaries (left, top, leftOffset, topOffset) * @private * @param {Array} chars Array of characters * @param {String} typeOfBoundaries */ _getCursorBoundaries: function(chars, typeOfBoundaries) { var cursorLocation = this.get2DCursorLocation(), textLines = this.text.split(this._reNewline), // left/top are left/top of entire text box // leftOffset/topOffset are offset from that left/top point of a text box left = Math.round(this._getLeftOffset()), top = this._getTopOffset(), offsets = this._getCursorBoundariesOffsets( chars, typeOfBoundaries, cursorLocation, textLines); return { left: left, top: top, leftOffset: offsets.left + offsets.lineLeft, topOffset: offsets.top }; }, /** * @private */ _getCursorBoundariesOffsets: function(chars, typeOfBoundaries, cursorLocation, textLines) { var lineLeftOffset = 0, lineIndex = 0, charIndex = 0, leftOffset = 0, topOffset = typeOfBoundaries === 'cursor' // selection starts at the very top of the line, // whereas cursor starts at the padding created by line height ? (this._getHeightOfLine(this.ctx, 0) - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)) : 0; for (var i = 0; i < this.selectionStart; i++) { if (chars[i] === '\n') { leftOffset = 0; var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0); topOffset += this._getCachedLineHeight(index); lineIndex++; charIndex = 0; } else { leftOffset += this._getWidthOfChar(this.ctx, chars[i], lineIndex, charIndex); charIndex++; } lineLeftOffset = this._getCachedLineOffset(lineIndex, textLines); } this._clearCache(); return { top: topOffset, left: leftOffset, lineLeft: lineLeftOffset }; }, /** * @private */ _clearCache: function() { this.__lineWidths = { }; this.__lineHeights = { }; this.__lineOffsets = { }; }, /** * @private */ _getCachedLineHeight: function(index) { return this.__lineHeights[index] || (this.__lineHeights[index] = this._getHeightOfLine(this.ctx, index)); }, /** * @private */ _getCachedLineWidth: function(lineIndex, textLines) { return this.__lineWidths[lineIndex] || (this.__lineWidths[lineIndex] = this._getWidthOfLine(this.ctx, lineIndex, textLines)); }, /** * @private */ _getCachedLineOffset: function(lineIndex, textLines) { var widthOfLine = this._getCachedLineWidth(lineIndex, textLines); return this.__lineOffsets[lineIndex] || (this.__lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine)); }, /** * Renders cursor * @param {Object} boundaries */ renderCursor: function(boundaries) { var ctx = this.ctx; ctx.save(); var cursorLocation = this.get2DCursorLocation(), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex, charHeight = this.getCurrentCharFontSize(lineIndex, charIndex), leftOffset = (lineIndex === 0 && charIndex === 0) ? this._getCachedLineOffset(lineIndex, this.text.split(this._reNewline)) : boundaries.leftOffset; ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex); ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity; ctx.fillRect( boundaries.left + leftOffset, boundaries.top + boundaries.topOffset, this.cursorWidth / this.scaleX, charHeight); ctx.restore(); }, /** * Renders text selection * @param {Array} chars Array of characters * @param {Object} boundaries Object with left/top/leftOffset/topOffset */ renderSelection: function(chars, boundaries) { var ctx = this.ctx; ctx.save(); ctx.fillStyle = this.selectionColor; var start = this.get2DCursorLocation(this.selectionStart), end = this.get2DCursorLocation(this.selectionEnd), startLine = start.lineIndex, endLine = end.lineIndex, textLines = this.text.split(this._reNewline); for (var i = startLine; i <= endLine; i++) { var lineOffset = this._getCachedLineOffset(i, textLines) || 0, lineHeight = this._getCachedLineHeight(i), boxWidth = 0; if (i === startLine) { for (var j = 0, len = textLines[i].length; j < len; j++) { if (j >= start.charIndex && (i !== endLine || j < end.charIndex)) { boxWidth += this._getWidthOfChar(ctx, textLines[i][j], i, j); } if (j < start.charIndex) { lineOffset += this._getWidthOfChar(ctx, textLines[i][j], i, j); } } } else if (i > startLine && i < endLine) { boxWidth += this._getCachedLineWidth(i, textLines) || 5; } else if (i === endLine) { for (var j2 = 0, j2len = end.charIndex; j2 < j2len; j2++) { boxWidth += this._getWidthOfChar(ctx, textLines[i][j2], i, j2); } } ctx.fillRect( boundaries.left + lineOffset, boundaries.top + boundaries.topOffset, boxWidth, lineHeight); boundaries.topOffset += lineHeight; } ctx.restore(); }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderChars: function(method, ctx, line, left, top, lineIndex) { if (this.isEmptyStyles()) { return this._renderCharsFast(method, ctx, line, left, top); } this.skipTextAlign = true; // set proper box offset left -= this.textAlign === 'center' ? (this.width / 2) : (this.textAlign === 'right') ? this.width : 0; // set proper line offset var textLines = this.text.split(this._reNewline), lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines), lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines), lineLeftOffset = this._getLineLeftOffset(lineWidth), chars = line.split(''), prevStyle, charsToRender = ''; left += lineLeftOffset || 0; ctx.save(); for (var i = 0, len = chars.length; i <= len; i++) { prevStyle = prevStyle || this.getCurrentCharStyle(lineIndex, i); var thisStyle = this.getCurrentCharStyle(lineIndex, i + 1); if (this._hasStyleChanged(prevStyle, thisStyle) || i === len) { this._renderChar(method, ctx, lineIndex, i - 1, charsToRender, left, top, lineHeight); charsToRender = ''; prevStyle = thisStyle; } charsToRender += chars[i]; } ctx.restore(); }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line Content of the line * @param {Number} left Left coordinate * @param {Number} top Top coordinate */ _renderCharsFast: function(method, ctx, line, left, top) { this.skipTextAlign = false; if (method === 'fillText' && this.fill) { this.callSuper('_renderChars', method, ctx, line, left, top); } if (method === 'strokeText' && this.stroke) { this.callSuper('_renderChars', method, ctx, line, left, top); } }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex * @param {Number} i * @param {String} _char * @param {Number} left Left coordinate * @param {Number} top Top coordinate * @param {Number} lineHeight Height of the line */ _renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) { var decl, charWidth, charHeight; if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) { var shouldStroke = decl.stroke || this.stroke, shouldFill = decl.fill || this.fill; ctx.save(); charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl); charHeight = this._getHeightOfChar(ctx, _char, lineIndex, i); if (shouldFill) { ctx.fillText(_char, left, top); } if (shouldStroke) { ctx.strokeText(_char, left, top); } this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight, charHeight); ctx.restore(); ctx.translate(charWidth, 0); } else { if (method === 'strokeText' && this.stroke) { ctx[method](_char, left, top); } if (method === 'fillText' && this.fill) { ctx[method](_char, left, top); } charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i); this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight); ctx.translate(ctx.measureText(_char).width, 0); } }, /** * @private * @param {Object} prevStyle * @param {Object} thisStyle */ _hasStyleChanged: function(prevStyle, thisStyle) { return (prevStyle.fill !== thisStyle.fill || prevStyle.fontSize !== thisStyle.fontSize || prevStyle.textBackgroundColor !== thisStyle.textBackgroundColor || prevStyle.textDecoration !== thisStyle.textDecoration || prevStyle.fontFamily !== thisStyle.fontFamily || prevStyle.fontWeight !== thisStyle.fontWeight || prevStyle.fontStyle !== thisStyle.fontStyle || prevStyle.stroke !== thisStyle.stroke || prevStyle.strokeWidth !== thisStyle.strokeWidth ); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight, charHeight) { var textDecoration = styleDeclaration ? (styleDeclaration.textDecoration || this.textDecoration) : this.textDecoration, fontSize = (styleDeclaration ? styleDeclaration.fontSize : null) || this.fontSize; if (!textDecoration) { return; } if (textDecoration.indexOf('underline') > -1) { this._renderCharDecorationAtOffset( ctx, left, top + (this.fontSize / this._fontSizeFraction), charWidth, 0, this.fontSize / 20 ); } if (textDecoration.indexOf('line-through') > -1) { this._renderCharDecorationAtOffset( ctx, left, top + (this.fontSize / this._fontSizeFraction), charWidth, charHeight / 2, fontSize / 20 ); } if (textDecoration.indexOf('overline') > -1) { this._renderCharDecorationAtOffset( ctx, left, top, charWidth, lineHeight - (this.fontSize / this._fontSizeFraction), this.fontSize / 20 ); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset, thickness) { ctx.fillRect(left, top - offset, charWidth, thickness); }, /** * @private * @param {String} method * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} line */ _renderTextLine: function(method, ctx, line, left, top, lineIndex) { // to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine top += this.fontSize / 4; this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines */ _renderTextDecoration: function(ctx, textLines) { if (this.isEmptyStyles()) { return this.callSuper('_renderTextDecoration', ctx, textLines); } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _renderTextLinesBackground: function(ctx, textLines) { if (!this.textBackgroundColor && !this.styles) { return; } ctx.save(); if (this.textBackgroundColor) { ctx.fillStyle = this.textBackgroundColor; } var lineHeights = 0, fractionOfFontSize = this.fontSize / this._fontSizeFraction; for (var i = 0, len = textLines.length; i < len; i++) { var heightOfLine = this._getHeightOfLine(ctx, i, textLines); if (textLines[i] === '') { lineHeights += heightOfLine; continue; } var lineWidth = this._getWidthOfLine(ctx, i, textLines), lineLeftOffset = this._getLineLeftOffset(lineWidth); if (this.textBackgroundColor) { ctx.fillStyle = this.textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset, this._getTopOffset() + lineHeights + fractionOfFontSize, lineWidth, heightOfLine ); } if (this.styles[i]) { for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) { var _char = textLines[i][j]; ctx.fillStyle = this.styles[i][j].textBackgroundColor; ctx.fillRect( this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines), this._getTopOffset() + lineHeights + fractionOfFontSize, this._getWidthOfChar(ctx, _char, i, j, textLines) + 1, heightOfLine ); } } } lineHeights += heightOfLine; } ctx.restore(); }, /** * @private */ _getCacheProp: function(_char, styleDeclaration) { return _char + styleDeclaration.fontFamily + styleDeclaration.fontSize + styleDeclaration.fontWeight + styleDeclaration.fontStyle + styleDeclaration.shadow; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {String} _char * @param {Number} lineIndex * @param {Number} charIndex * @param {Object} [decl] */ _applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) { var styleDeclaration = decl || (this.styles[lineIndex] && this.styles[lineIndex][charIndex]); if (styleDeclaration) { // cloning so that original style object is not polluted with following font declarations styleDeclaration = clone(styleDeclaration); } else { styleDeclaration = { }; } this._applyFontStyles(styleDeclaration); var cacheProp = this._getCacheProp(_char, styleDeclaration); // short-circuit if no styles if (this.isEmptyStyles() && this._charWidthsCache[cacheProp] && this.caching) { return this._charWidthsCache[cacheProp]; } if (typeof styleDeclaration.shadow === 'string') { styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow); } var fill = styleDeclaration.fill || this.fill; ctx.fillStyle = fill.toLive ? fill.toLive(ctx) : fill; if (styleDeclaration.stroke) { ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive) ? styleDeclaration.stroke.toLive(ctx) : styleDeclaration.stroke; } ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth; ctx.font = this._getFontDeclaration.call(styleDeclaration); this._setShadow.call(styleDeclaration, ctx); if (!this.caching) { return ctx.measureText(_char).width; } if (!this._charWidthsCache[cacheProp]) { this._charWidthsCache[cacheProp] = ctx.measureText(_char).width; } return this._charWidthsCache[cacheProp]; }, /** * @private * @param {Object} styleDeclaration */ _applyFontStyles: function(styleDeclaration) { if (!styleDeclaration.fontFamily) { styleDeclaration.fontFamily = this.fontFamily; } if (!styleDeclaration.fontSize) { styleDeclaration.fontSize = this.fontSize; } if (!styleDeclaration.fontWeight) { styleDeclaration.fontWeight = this.fontWeight; } if (!styleDeclaration.fontStyle) { styleDeclaration.fontStyle = this.fontStyle; } }, /** * @private * @param {Number} lineIndex * @param {Number} charIndex */ _getStyleDeclaration: function(lineIndex, charIndex) { return (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) ? clone(this.styles[lineIndex][charIndex]) : { }; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfChar: function(ctx, _char, lineIndex, charIndex) { if (this.textAlign === 'justify' && /\s/.test(_char)) { return this._getWidthOfSpace(ctx, lineIndex); } var styleDeclaration = this._getStyleDeclaration(lineIndex, charIndex); this._applyFontStyles(styleDeclaration); var cacheProp = this._getCacheProp(_char, styleDeclaration); if (this._charWidthsCache[cacheProp] && this.caching) { return this._charWidthsCache[cacheProp]; } else if (ctx) { ctx.save(); var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex); ctx.restore(); return width; } }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfChar: function(ctx, _char, lineIndex, charIndex) { if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) { return this.styles[lineIndex][charIndex].fontSize || this.fontSize; } return this.fontSize; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) { lines = lines || this.text.split(this._reNewline); var _char = lines[lineIndex].split('')[charIndex]; return this._getWidthOfChar(ctx, _char, lineIndex, charIndex); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) { lines = lines || this.text.split(this._reNewline); var _char = lines[lineIndex].split('')[charIndex]; return this._getHeightOfChar(ctx, _char, lineIndex, charIndex); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) { var width = 0; for (var i = 0; i < charIndex; i++) { width += this._getWidthOfCharAt(ctx, lineIndex, i, lines); } return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getWidthOfLine: function(ctx, lineIndex, textLines) { // if (!this.styles[lineIndex]) { // return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]); // } return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines); }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} lineIndex */ _getWidthOfSpace: function (ctx, lineIndex) { var lines = this.text.split(this._reNewline), line = lines[lineIndex], words = line.split(/\s+/), wordsWidth = this._getWidthOfWords(ctx, line, lineIndex), widthDiff = this.width - wordsWidth, numSpaces = words.length - 1, width = widthDiff / numSpaces; return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Number} line * @param {Number} lineIndex */ _getWidthOfWords: function (ctx, line, lineIndex) { var width = 0; for (var charIndex = 0; charIndex < line.length; charIndex++) { var _char = line[charIndex]; if (!_char.match(/\s/)) { width += this._getWidthOfChar(ctx, _char, lineIndex, charIndex); } } return width; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getTextWidth: function(ctx, textLines) { if (this.isEmptyStyles()) { return this.callSuper('_getTextWidth', ctx, textLines); } var maxWidth = this._getWidthOfLine(ctx, 0, textLines); for (var i = 1, len = textLines.length; i < len; i++) { var currentLineWidth = this._getWidthOfLine(ctx, i, textLines); if (currentLineWidth > maxWidth) { maxWidth = currentLineWidth; } } return maxWidth; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _getHeightOfLine: function(ctx, lineIndex, textLines) { textLines = textLines || this.text.split(this._reNewline); var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0), line = textLines[lineIndex], chars = line.split(''); for (var i = 1, len = chars.length; i < len; i++) { var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i); if (currentCharHeight > maxHeight) { maxHeight = currentCharHeight; } } return maxHeight * this.lineHeight; }, /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Array} textLines Array of all text lines */ _getTextHeight: function(ctx, textLines) { var height = 0; for (var i = 0, len = textLines.length; i < len; i++) { height += this._getHeightOfLine(ctx, i, textLines); } return height; }, /** * @private */ _getTopOffset: function() { var topOffset = fabric.Text.prototype._getTopOffset.call(this); return topOffset - (this.fontSize / this._fontSizeFraction); }, /** * This method is overwritten to account for different top offset * @private */ _renderTextBoxBackground: function(ctx) { if (!this.backgroundColor) { return; } ctx.save(); ctx.fillStyle = this.backgroundColor; ctx.fillRect( this._getLeftOffset(), this._getTopOffset() + (this.fontSize / this._fontSizeFraction), this.width, this.height ); ctx.restore(); }, /** * Returns object representation of an instance * @method toObject * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output * @return {Object} object representation of an instance */ toObject: function(propertiesToInclude) { return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { styles: clone(this.styles) }); } }); /** * Returns fabric.IText instance from an object representation * @static * @memberOf fabric.IText * @param {Object} object Object to create an instance from * @return {fabric.IText} instance of fabric.IText */ fabric.IText.fromObject = function(object) { return new fabric.IText(object.text, clone(object)); }; /** * Contains all fabric.IText objects that have been created * @static * @memberof fabric.IText * @type Array */ fabric.IText.instances = [ ]; })(); (function() { var clone = fabric.util.object.clone; fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { /** * Initializes all the interactive behavior of IText */ initBehavior: function() { this.initAddedHandler(); this.initCursorSelectionHandlers(); this.initDoubleClickSimulation(); }, /** * Initializes "selected" event handler */ initSelectedHandler: function() { this.on('selected', function() { var _this = this; setTimeout(function() { _this.selected = true; }, 100); }); }, /** * Initializes "added" event handler */ initAddedHandler: function() { this.on('added', function() { if (this.canvas && !this.canvas._hasITextHandlers) { this.canvas._hasITextHandlers = true; this._initCanvasHandlers(); } }); }, /** * @private */ _initCanvasHandlers: function() { this.canvas.on('selection:cleared', function() { fabric.IText.prototype.exitEditingOnOthers.call(); }); this.canvas.on('mouse:up', function() { fabric.IText.instances.forEach(function(obj) { obj.__isMousedown = false; }); }); this.canvas.on('object:selected', function(options) { fabric.IText.prototype.exitEditingOnOthers.call(options.target); }); }, /** * @private */ _tick: function() { if (this._abortCursorAnimation) { return; } var _this = this; this.animate('_currentCursorOpacity', 1, { duration: this.cursorDuration, onComplete: function() { _this._onTickComplete(); }, onChange: function() { _this.canvas && _this.canvas.renderAll(); }, abort: function() { return _this._abortCursorAnimation; } }); }, /** * @private */ _onTickComplete: function() { if (this._abortCursorAnimation) { return; } var _this = this; if (this._cursorTimeout1) { clearTimeout(this._cursorTimeout1); } this._cursorTimeout1 = setTimeout(function() { _this.animate('_currentCursorOpacity', 0, { duration: this.cursorDuration / 2, onComplete: function() { _this._tick(); }, onChange: function() { _this.canvas && _this.canvas.renderAll(); }, abort: function() { return _this._abortCursorAnimation; } }); }, 100); }, /** * Initializes delayed cursor */ initDelayedCursor: function(restart) { var _this = this, delay = restart ? 0 : this.cursorDelay; if (restart) { this._abortCursorAnimation = true; clearTimeout(this._cursorTimeout1); this._currentCursorOpacity = 1; this.canvas && this.canvas.renderAll(); } if (this._cursorTimeout2) { clearTimeout(this._cursorTimeout2); } this._cursorTimeout2 = setTimeout(function() { _this._abortCursorAnimation = false; _this._tick(); }, delay); }, /** * Aborts cursor animation and clears all timeouts */ abortCursorAnimation: function() { this._abortCursorAnimation = true; clearTimeout(this._cursorTimeout1); clearTimeout(this._cursorTimeout2); this._currentCursorOpacity = 0; this.canvas && this.canvas.renderAll(); var _this = this; setTimeout(function() { _this._abortCursorAnimation = false; }, 10); }, /** * Selects entire text */ selectAll: function() { this.selectionStart = 0; this.selectionEnd = this.text.length; this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Returns selected text * @return {String} */ getSelectedText: function() { return this.text.slice(this.selectionStart, this.selectionEnd); }, /** * Find new selection index representing start of current word according to current selection index * @param {Number} startFrom Surrent selection index * @return {Number} New selection index */ findWordBoundaryLeft: function(startFrom) { var offset = 0, index = startFrom - 1; // remove space before cursor first if (this._reSpace.test(this.text.charAt(index))) { while (this._reSpace.test(this.text.charAt(index))) { offset++; index--; } } while (/\S/.test(this.text.charAt(index)) && index > -1) { offset++; index--; } return startFrom - offset; }, /** * Find new selection index representing end of current word according to current selection index * @param {Number} startFrom Current selection index * @return {Number} New selection index */ findWordBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; // remove space after cursor first if (this._reSpace.test(this.text.charAt(index))) { while (this._reSpace.test(this.text.charAt(index))) { offset++; index++; } } while (/\S/.test(this.text.charAt(index)) && index < this.text.length) { offset++; index++; } return startFrom + offset; }, /** * Find new selection index representing start of current line according to current selection index * @param {Number} startFrom Current selection index * @return {Number} New selection index */ findLineBoundaryLeft: function(startFrom) { var offset = 0, index = startFrom - 1; while (!/\n/.test(this.text.charAt(index)) && index > -1) { offset++; index--; } return startFrom - offset; }, /** * Find new selection index representing end of current line according to current selection index * @param {Number} startFrom Current selection index * @return {Number} New selection index */ findLineBoundaryRight: function(startFrom) { var offset = 0, index = startFrom; while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) { offset++; index++; } return startFrom + offset; }, /** * Returns number of newlines in selected text * @return {Number} Number of newlines in selected text */ getNumNewLinesInSelectedText: function() { var selectedText = this.getSelectedText(), numNewLines = 0; for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) { if (chars[i] === '\n') { numNewLines++; } } return numNewLines; }, /** * Finds index corresponding to beginning or end of a word * @param {Number} selectionStart Index of a character * @param {Number} direction: 1 or -1 * @return {Number} Index of the beginning or end of a word */ searchWordBoundary: function(selectionStart, direction) { var index = this._reSpace.test(this.text.charAt(selectionStart)) ? selectionStart - 1 : selectionStart, _char = this.text.charAt(index), reNonWord = /[ \n\.,;!\?\-]/; while (!reNonWord.test(_char) && index > 0 && index < this.text.length) { index += direction; _char = this.text.charAt(index); } if (reNonWord.test(_char) && _char !== '\n') { index += direction === 1 ? 0 : 1; } return index; }, /** * Selects a word based on the index * @param {Number} selectionStart Index of a character */ selectWord: function(selectionStart) { var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */ newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */ this.setSelectionStart(newSelectionStart); this.setSelectionEnd(newSelectionEnd); this.initDelayedCursor(true); }, /** * Selects a line based on the index * @param {Number} selectionStart Index of a character */ selectLine: function(selectionStart) { var newSelectionStart = this.findLineBoundaryLeft(selectionStart), newSelectionEnd = this.findLineBoundaryRight(selectionStart); this.setSelectionStart(newSelectionStart); this.setSelectionEnd(newSelectionEnd); this.initDelayedCursor(true); }, /** * Enters editing state * @return {fabric.IText} thisArg * @chainable */ enterEditing: function() { if (this.isEditing || !this.editable) { return; } this.exitEditingOnOthers(); this.isEditing = true; this.initHiddenTextarea(); this._updateTextarea(); this._saveEditingProps(); this._setEditingProps(); this._tick(); this.canvas && this.canvas.renderAll(); this.fire('editing:entered'); this.canvas && this.canvas.fire('text:editing:entered', { target: this }); return this; }, exitEditingOnOthers: function() { fabric.IText.instances.forEach(function(obj) { obj.selected = false; if (obj.isEditing) { obj.exitEditing(); } }, this); }, /** * @private */ _setEditingProps: function() { this.hoverCursor = 'text'; if (this.canvas) { this.canvas.defaultCursor = this.canvas.moveCursor = 'text'; } this.borderColor = this.editingBorderColor; this.hasControls = this.selectable = false; this.lockMovementX = this.lockMovementY = true; }, /** * @private */ _updateTextarea: function() { if (!this.hiddenTextarea) { return; } this.hiddenTextarea.value = this.text; this.hiddenTextarea.selectionStart = this.selectionStart; }, /** * @private */ _saveEditingProps: function() { this._savedProps = { hasControls: this.hasControls, borderColor: this.borderColor, lockMovementX: this.lockMovementX, lockMovementY: this.lockMovementY, hoverCursor: this.hoverCursor, defaultCursor: this.canvas && this.canvas.defaultCursor, moveCursor: this.canvas && this.canvas.moveCursor }; }, /** * @private */ _restoreEditingProps: function() { if (!this._savedProps) { return; } this.hoverCursor = this._savedProps.overCursor; this.hasControls = this._savedProps.hasControls; this.borderColor = this._savedProps.borderColor; this.lockMovementX = this._savedProps.lockMovementX; this.lockMovementY = this._savedProps.lockMovementY; if (this.canvas) { this.canvas.defaultCursor = this._savedProps.defaultCursor; this.canvas.moveCursor = this._savedProps.moveCursor; } }, /** * Exits from editing state * @return {fabric.IText} thisArg * @chainable */ exitEditing: function() { this.selected = false; this.isEditing = false; this.selectable = true; this.selectionEnd = this.selectionStart; this.hiddenTextarea && this.canvas && this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea); this.hiddenTextarea = null; this.abortCursorAnimation(); this._restoreEditingProps(); this._currentCursorOpacity = 0; this.fire('editing:exited'); this.canvas && this.canvas.fire('text:editing:exited', { target: this }); return this; }, /** * @private */ _removeExtraneousStyles: function() { var textLines = this.text.split(this._reNewline); for (var prop in this.styles) { if (!textLines[prop]) { delete this.styles[prop]; } } }, /** * @private */ _removeCharsFromTo: function(start, end) { var i = end; while (i !== start) { var prevIndex = this.get2DCursorLocation(i).charIndex; i--; var index = this.get2DCursorLocation(i).charIndex, isNewline = index > prevIndex; if (isNewline) { this.removeStyleObject(isNewline, i + 1); } else { this.removeStyleObject(this.get2DCursorLocation(i).charIndex === 0, i); } } this.text = this.text.slice(0, start) + this.text.slice(end); }, /** * Inserts a character where cursor is (replacing selection if one exists) * @param {String} _chars Characters to insert */ insertChars: function(_chars) { var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n'; this.text = this.text.slice(0, this.selectionStart) + _chars + this.text.slice(this.selectionEnd); if (this.selectionStart === this.selectionEnd) { this.insertStyleObjects(_chars, isEndOfLine, this.copiedStyles); } // else if (this.selectionEnd - this.selectionStart > 1) { // TODO: replace styles properly // console.log('replacing MORE than 1 char'); // } this.selectionStart += _chars.length; this.selectionEnd = this.selectionStart; if (this.canvas) { // TODO: double renderAll gets rid of text box shift happenning sometimes // need to find out what exactly causes it and fix it this.canvas.renderAll().renderAll(); } this.setCoords(); this.fire('changed'); this.canvas && this.canvas.fire('text:changed', { target: this }); }, /** * Inserts new style object * @param {Number} lineIndex Index of a line * @param {Number} charIndex Index of a char * @param {Boolean} isEndOfLine True if it's end of line */ insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) { this.shiftLineStyles(lineIndex, +1); if (!this.styles[lineIndex + 1]) { this.styles[lineIndex + 1] = { }; } var currentCharStyle = this.styles[lineIndex][charIndex - 1], newLineStyles = { }; // if there's nothing after cursor, // we clone current char style onto the next (otherwise empty) line if (isEndOfLine) { newLineStyles[0] = clone(currentCharStyle); this.styles[lineIndex + 1] = newLineStyles; } // otherwise we clone styles of all chars // after cursor onto the next line, from the beginning else { for (var index in this.styles[lineIndex]) { if (parseInt(index, 10) >= charIndex) { newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index]; // remove lines from the previous line since they're on a new line now delete this.styles[lineIndex][index]; } } this.styles[lineIndex + 1] = newLineStyles; } }, /** * Inserts style object for a given line/char index * @param {Number} lineIndex Index of a line * @param {Number} charIndex Index of a char * @param {Object} [style] Style object to insert, if given */ insertCharStyleObject: function(lineIndex, charIndex, style) { var currentLineStyles = this.styles[lineIndex], currentLineStylesCloned = clone(currentLineStyles); if (charIndex === 0 && !style) { charIndex = 1; } // shift all char styles by 1 forward // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4 for (var index in currentLineStylesCloned) { var numericIndex = parseInt(index, 10); if (numericIndex >= charIndex) { currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex]; //delete currentLineStyles[index]; } } this.styles[lineIndex][charIndex] = style || clone(currentLineStyles[charIndex - 1]); }, /** * Inserts style object(s) * @param {String} _chars Characters at the location where style is inserted * @param {Boolean} isEndOfLine True if it's end of line * @param {Array} [styles] Styles to insert */ insertStyleObjects: function(_chars, isEndOfLine, styles) { // removed shortcircuit over isEmptyStyles var cursorLocation = this.get2DCursorLocation(), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex; if (!this.styles[lineIndex]) { this.styles[lineIndex] = { }; } if (_chars === '\n') { this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine); } else { if (styles) { this._insertStyles(styles); } else { // TODO: support multiple style insertion if _chars.length > 1 this.insertCharStyleObject(lineIndex, charIndex); } } }, /** * @private */ _insertStyles: function(styles) { for (var i = 0, len = styles.length; i < len; i++) { var cursorLocation = this.get2DCursorLocation(this.selectionStart + i), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex; this.insertCharStyleObject(lineIndex, charIndex, styles[i]); } }, /** * Shifts line styles up or down * @param {Number} lineIndex Index of a line * @param {Number} offset Can be -1 or +1 */ shiftLineStyles: function(lineIndex, offset) { // shift all line styles by 1 upward var clonedStyles = clone(this.styles); for (var line in this.styles) { var numericLine = parseInt(line, 10); if (numericLine > lineIndex) { this.styles[numericLine + offset] = clonedStyles[numericLine]; } } }, /** * Removes style object * @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line * @param {Number} [index] Optional index. When not given, current selectionStart is used. */ removeStyleObject: function(isBeginningOfLine, index) { var cursorLocation = this.get2DCursorLocation(index), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex; if (isBeginningOfLine) { var textLines = this.text.split(this._reNewline), textOnPreviousLine = textLines[lineIndex - 1], newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0; if (!this.styles[lineIndex - 1]) { this.styles[lineIndex - 1] = { }; } for (charIndex in this.styles[lineIndex]) { this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine] = this.styles[lineIndex][charIndex]; } this.shiftLineStyles(lineIndex, -1); } else { var currentLineStyles = this.styles[lineIndex]; if (currentLineStyles) { var offset = this.selectionStart === this.selectionEnd ? -1 : 0; delete currentLineStyles[charIndex + offset]; // console.log('deleting', lineIndex, charIndex + offset); } var currentLineStylesCloned = clone(currentLineStyles); // shift all styles by 1 backwards for (var i in currentLineStylesCloned) { var numericIndex = parseInt(i, 10); if (numericIndex >= charIndex && numericIndex !== 0) { currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex]; delete currentLineStyles[numericIndex]; } } } }, /** * Inserts new line */ insertNewline: function() { this.insertChars('\n'); } }); })(); fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { /** * Initializes "dbclick" event handler */ initDoubleClickSimulation: function() { // for double click this.__lastClickTime = +new Date(); // for triple click this.__lastLastClickTime = +new Date(); this.__lastPointer = { }; this.on('mousedown', this.onMouseDown.bind(this)); }, onMouseDown: function(options) { this.__newClickTime = +new Date(); var newPointer = this.canvas.getPointer(options.e); if (this.isTripleClick(newPointer)) { this.fire('tripleclick', options); this._stopEvent(options.e); } else if (this.isDoubleClick(newPointer)) { this.fire('dblclick', options); this._stopEvent(options.e); } this.__lastLastClickTime = this.__lastClickTime; this.__lastClickTime = this.__newClickTime; this.__lastPointer = newPointer; this.__lastIsEditing = this.isEditing; this.__lastSelected = this.selected; }, isDoubleClick: function(newPointer) { return this.__newClickTime - this.__lastClickTime < 500 && this.__lastPointer.x === newPointer.x && this.__lastPointer.y === newPointer.y && this.__lastIsEditing; }, isTripleClick: function(newPointer) { return this.__newClickTime - this.__lastClickTime < 500 && this.__lastClickTime - this.__lastLastClickTime < 500 && this.__lastPointer.x === newPointer.x && this.__lastPointer.y === newPointer.y; }, /** * @private */ _stopEvent: function(e) { e.preventDefault && e.preventDefault(); e.stopPropagation && e.stopPropagation(); }, /** * Initializes event handlers related to cursor or selection */ initCursorSelectionHandlers: function() { this.initSelectedHandler(); this.initMousedownHandler(); this.initMousemoveHandler(); this.initMouseupHandler(); this.initClicks(); }, /** * Initializes double and triple click event handlers */ initClicks: function() { this.on('dblclick', function(options) { this.selectWord(this.getSelectionStartFromPointer(options.e)); }); this.on('tripleclick', function(options) { this.selectLine(this.getSelectionStartFromPointer(options.e)); }); }, /** * Initializes "mousedown" event handler */ initMousedownHandler: function() { this.on('mousedown', function(options) { var pointer = this.canvas.getPointer(options.e); this.__mousedownX = pointer.x; this.__mousedownY = pointer.y; this.__isMousedown = true; if (this.hiddenTextarea && this.canvas) { this.canvas.wrapperEl.appendChild(this.hiddenTextarea); } if (this.selected) { this.setCursorByClick(options.e); } if (this.isEditing) { this.__selectionStartOnMouseDown = this.selectionStart; this.initDelayedCursor(true); } }); }, /** * Initializes "mousemove" event handler */ initMousemoveHandler: function() { this.on('mousemove', function(options) { if (!this.__isMousedown || !this.isEditing) { return; } var newSelectionStart = this.getSelectionStartFromPointer(options.e); if (newSelectionStart >= this.__selectionStartOnMouseDown) { this.setSelectionStart(this.__selectionStartOnMouseDown); this.setSelectionEnd(newSelectionStart); } else { this.setSelectionStart(newSelectionStart); this.setSelectionEnd(this.__selectionStartOnMouseDown); } }); }, /** * @private */ _isObjectMoved: function(e) { var pointer = this.canvas.getPointer(e); return this.__mousedownX !== pointer.x || this.__mousedownY !== pointer.y; }, /** * Initializes "mouseup" event handler */ initMouseupHandler: function() { this.on('mouseup', function(options) { this.__isMousedown = false; if (this._isObjectMoved(options.e)) { return; } if (this.__lastSelected) { this.enterEditing(); this.initDelayedCursor(true); } this.selected = true; }); }, /** * Changes cursor location in a text depending on passed pointer (x/y) object * @param {Event} e Event object */ setCursorByClick: function(e) { var newSelectionStart = this.getSelectionStartFromPointer(e); if (e.shiftKey) { if (newSelectionStart < this.selectionStart) { this.setSelectionEnd(this.selectionStart); this.setSelectionStart(newSelectionStart); } else { this.setSelectionEnd(newSelectionStart); } } else { this.setSelectionStart(newSelectionStart); this.setSelectionEnd(newSelectionStart); } }, /** * @private * @param {Event} e Event object * @return {Object} Coordinates of a pointer (x, y) */ _getLocalRotatedPointer: function(e) { var pointer = this.canvas.getPointer(e), pClicked = new fabric.Point(pointer.x, pointer.y), pLeftTop = new fabric.Point(this.left, this.top), rotated = fabric.util.rotatePoint( pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle)); return this.getLocalPointer(e, rotated); }, /** * Returns index of a character corresponding to where an object was clicked * @param {Event} e Event object * @return {Number} Index of a character */ getSelectionStartFromPointer: function(e) { var mouseOffset = this._getLocalRotatedPointer(e), textLines = this.text.split(this._reNewline), prevWidth = 0, width = 0, height = 0, charIndex = 0, newSelectionStart; for (var i = 0, len = textLines.length; i < len; i++) { height += this._getHeightOfLine(this.ctx, i) * this.scaleY; var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines), lineLeftOffset = this._getLineLeftOffset(widthOfLine); width = lineLeftOffset * this.scaleX; if (this.flipX) { // when oject is horizontally flipped we reverse chars textLines[i] = textLines[i].split('').reverse().join(''); } for (var j = 0, jlen = textLines[i].length; j < jlen; j++) { var _char = textLines[i][j]; prevWidth = width; width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) * this.scaleX; if (height <= mouseOffset.y || width <= mouseOffset.x) { charIndex++; continue; } return this._getNewSelectionStartFromOffset( mouseOffset, prevWidth, width, charIndex + i, jlen); } if (mouseOffset.y < height) { return this._getNewSelectionStartFromOffset( mouseOffset, prevWidth, width, charIndex + i, jlen); } } // clicked somewhere after all chars, so set at the end if (typeof newSelectionStart === 'undefined') { return this.text.length; } }, /** * @private */ _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) { var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth, distanceBtwNextCharAndCursor = width - mouseOffset.x, offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1, newSelectionStart = index + offset; // if object is horizontally flipped, mirror cursor location from the end if (this.flipX) { newSelectionStart = jlen - newSelectionStart; } if (newSelectionStart > this.text.length) { newSelectionStart = this.text.length; } return newSelectionStart; } }); fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { /** * Initializes hidden textarea (needed to bring up keyboard in iOS) */ initHiddenTextarea: function() { this.hiddenTextarea = fabric.document.createElement('textarea'); this.hiddenTextarea.setAttribute('autocapitalize', 'off'); this.hiddenTextarea.style.cssText = 'position: fixed; bottom: 20px; left: 0px; opacity: 0;' + ' width: 0px; height: 0px; z-index: -999;'; fabric.document.body.appendChild(this.hiddenTextarea); fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'keypress', this.onKeyPress.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this)); fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this)); if (!this._clickHandlerInitialized && this.canvas) { fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this)); this._clickHandlerInitialized = true; } }, /** * @private */ _keysMap: { 8: 'removeChars', 9: 'exitEditing', 27: 'exitEditing', 13: 'insertNewline', 33: 'moveCursorUp', 34: 'moveCursorDown', 35: 'moveCursorRight', 36: 'moveCursorLeft', 37: 'moveCursorLeft', 38: 'moveCursorUp', 39: 'moveCursorRight', 40: 'moveCursorDown', 46: 'forwardDelete' }, /** * @private */ _ctrlKeysMap: { 65: 'selectAll', 88: 'cut' }, onClick: function() { // No need to trigger click event here, focus is enough to have the keyboard appear on Android this.hiddenTextarea && this.hiddenTextarea.focus(); }, /** * Handles keyup event * @param {Event} e Event object */ onKeyDown: function(e) { if (!this.isEditing) { return; } if (e.keyCode in this._keysMap) { this[this._keysMap[e.keyCode]](e); } else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) { this[this._ctrlKeysMap[e.keyCode]](e); } else { return; } e.stopImmediatePropagation(); e.preventDefault(); this.canvas && this.canvas.renderAll(); }, /** * Forward delete */ forwardDelete: function(e) { if (this.selectionStart === this.selectionEnd) { this.moveCursorRight(e); } this.removeChars(e); }, /** * Copies selected text * @param {Event} e Event object */ copy: function(e) { var selectedText = this.getSelectedText(), clipboardData = this._getClipboardData(e); // Check for backward compatibility with old browsers if (clipboardData) { clipboardData.setData('text', selectedText); } this.copiedText = selectedText; this.copiedStyles = this.getSelectionStyles( this.selectionStart, this.selectionEnd); }, /** * Pastes text * @param {Event} e Event object */ paste: function(e) { var copiedText = null, clipboardData = this._getClipboardData(e); // Check for backward compatibility with old browsers if (clipboardData) { copiedText = clipboardData.getData('text'); } else { copiedText = this.copiedText; } if (copiedText) { this.insertChars(copiedText); } }, /** * Cuts text * @param {Event} e Event object */ cut: function(e) { if (this.selectionStart === this.selectionEnd) { return; } this.copy(); this.removeChars(e); }, /** * @private * @param {Event} e Event object * @return {Object} Clipboard data object */ _getClipboardData: function(e) { return e && (e.clipboardData || fabric.window.clipboardData); }, /** * Handles keypress event * @param {Event} e Event object */ onKeyPress: function(e) { if (!this.isEditing || e.metaKey || e.ctrlKey) { return; } if (e.which !== 0) { this.insertChars(String.fromCharCode(e.which)); } e.stopPropagation(); }, /** * Gets start offset of a selection * @param {Event} e Event object * @param {Boolean} isRight * @return {Number} */ getDownCursorOffset: function(e, isRight) { var selectionProp = isRight ? this.selectionEnd : this.selectionStart, textLines = this.text.split(this._reNewline), _char, lineLeftOffset, textBeforeCursor = this.text.slice(0, selectionProp), textAfterCursor = this.text.slice(selectionProp), textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1], textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '', cursorLocation = this.get2DCursorLocation(selectionProp); // if on last line, down cursor goes to end of line if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey || e.keyCode === 34) { // move to the end of a text return this.text.length - selectionProp; } var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines); lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor); var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset, lineIndex = cursorLocation.lineIndex; for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { _char = textOnSameLineBeforeCursor[i]; widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); } var indexOnNextLine = this._getIndexOnNextLine( cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines); return textOnSameLineAfterCursor.length + 1 + indexOnNextLine; }, /** * @private */ _getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) { var lineIndex = cursorLocation.lineIndex + 1, widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines), lineLeftOffset = this._getLineLeftOffset(widthOfNextLine), widthOfCharsOnNextLine = lineLeftOffset, indexOnNextLine = 0, foundMatch; for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) { var _char = textOnNextLine[j], widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); widthOfCharsOnNextLine += widthOfChar; if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) { foundMatch = true; var leftEdge = widthOfCharsOnNextLine - widthOfChar, rightEdge = widthOfCharsOnNextLine, offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor), offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j; break; } } // reached end if (!foundMatch) { indexOnNextLine = textOnNextLine.length; } return indexOnNextLine; }, /** * Moves cursor down * @param {Event} e Event object */ moveCursorDown: function(e) { this.abortCursorAnimation(); this._currentCursorOpacity = 1; var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right'); if (e.shiftKey) { this.moveCursorDownWithShift(offset); } else { this.moveCursorDownWithoutShift(offset); } this.initDelayedCursor(); }, /** * Moves cursor down without keeping selection * @param {Number} offset */ moveCursorDownWithoutShift: function(offset) { this._selectionDirection = 'right'; this.selectionStart += offset; if (this.selectionStart > this.text.length) { this.selectionStart = this.text.length; } this.selectionEnd = this.selectionStart; this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * private */ swapSelectionPoints: function() { var swapSel = this.selectionEnd; this.selectionEnd = this.selectionStart; this.selectionStart = swapSel; }, /** * Moves cursor down while keeping selection * @param {Number} offset */ moveCursorDownWithShift: function(offset) { if (this.selectionEnd === this.selectionStart) { this._selectionDirection = 'right'; } var prop = this._selectionDirection === 'right' ? 'selectionEnd' : 'selectionStart'; this[prop] += offset; if (this.selectionEnd < this.selectionStart && this._selectionDirection === 'left') { this.swapSelectionPoints(); this._selectionDirection = 'right'; } if (this.selectionEnd > this.text.length) { this.selectionEnd = this.text.length; } this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * @param {Event} e Event object * @param {Boolean} isRight * @return {Number} */ getUpCursorOffset: function(e, isRight) { var selectionProp = isRight ? this.selectionEnd : this.selectionStart, cursorLocation = this.get2DCursorLocation(selectionProp); // if on first line, up cursor goes to start of line if (cursorLocation.lineIndex === 0 || e.metaKey || e.keyCode === 33) { return selectionProp; } var textBeforeCursor = this.text.slice(0, selectionProp), textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1), textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '', textLines = this.text.split(this._reNewline), _char, widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines), lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor), widthOfCharsOnSameLineBeforeCursor = lineLeftOffset, lineIndex = cursorLocation.lineIndex; for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) { _char = textOnSameLineBeforeCursor[i]; widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i); } var indexOnPrevLine = this._getIndexOnPrevLine( cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines); return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length; }, /** * @private */ _getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) { var lineIndex = cursorLocation.lineIndex - 1, widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines), lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine), widthOfCharsOnPreviousLine = lineLeftOffset, indexOnPrevLine = 0, foundMatch; for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) { var _char = textOnPreviousLine[j], widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j); widthOfCharsOnPreviousLine += widthOfChar; if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) { foundMatch = true; var leftEdge = widthOfCharsOnPreviousLine - widthOfChar, rightEdge = widthOfCharsOnPreviousLine, offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor), offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor); indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1); break; } } // reached end if (!foundMatch) { indexOnPrevLine = textOnPreviousLine.length - 1; } return indexOnPrevLine; }, /** * Moves cursor up * @param {Event} e Event object */ moveCursorUp: function(e) { this.abortCursorAnimation(); this._currentCursorOpacity = 1; var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right'); if (e.shiftKey) { this.moveCursorUpWithShift(offset); } else { this.moveCursorUpWithoutShift(offset); } this.initDelayedCursor(); }, /** * Moves cursor up with shift * @param {Number} offset */ moveCursorUpWithShift: function(offset) { if (this.selectionEnd === this.selectionStart) { this._selectionDirection = 'left'; } var prop = this._selectionDirection === 'right' ? 'selectionEnd' : 'selectionStart'; this[prop] -= offset; if (this.selectionEnd < this.selectionStart && this._selectionDirection === 'right') { this.swapSelectionPoints(); this._selectionDirection = 'left'; } if (this.selectionStart < 0) { this.selectionStart = 0; } this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Moves cursor up without shift * @param {Number} offset */ moveCursorUpWithoutShift: function(offset) { if (this.selectionStart === this.selectionEnd) { this.selectionStart -= offset; } if (this.selectionStart < 0) { this.selectionStart = 0; } this.selectionEnd = this.selectionStart; this._selectionDirection = 'left'; this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Moves cursor left * @param {Event} e Event object */ moveCursorLeft: function(e) { if (this.selectionStart === 0 && this.selectionEnd === 0) { return; } this.abortCursorAnimation(); this._currentCursorOpacity = 1; if (e.shiftKey) { this.moveCursorLeftWithShift(e); } else { this.moveCursorLeftWithoutShift(e); } this.initDelayedCursor(); }, /** * @private */ _move: function(e, prop, direction) { if (e.altKey) { this[prop] = this['findWordBoundary' + direction](this[prop]); } else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36 ) { this[prop] = this['findLineBoundary' + direction](this[prop]); } else { this[prop] += (direction === 'Left' ? -1 : 1); } }, /** * @private */ _moveLeft: function(e, prop) { this._move(e, prop, 'Left'); }, /** * @private */ _moveRight: function(e, prop) { this._move(e, prop, 'Right'); }, /** * Moves cursor left without keeping selection * @param {Event} e */ moveCursorLeftWithoutShift: function(e) { this._selectionDirection = 'left'; // only move cursor when there is no selection, // otherwise we discard it, and leave cursor on same place if (this.selectionEnd === this.selectionStart) { this._moveLeft(e, 'selectionStart'); } this.selectionEnd = this.selectionStart; this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Moves cursor left while keeping selection * @param {Event} e */ moveCursorLeftWithShift: function(e) { if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) { this._moveLeft(e, 'selectionEnd'); } else { this._selectionDirection = 'left'; this._moveLeft(e, 'selectionStart'); // increase selection by one if it's a newline if (this.text.charAt(this.selectionStart) === '\n') { this.selectionStart--; } if (this.selectionStart < 0) { this.selectionStart = 0; } } this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Moves cursor right * @param {Event} e Event object */ moveCursorRight: function(e) { if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) { return; } this.abortCursorAnimation(); this._currentCursorOpacity = 1; if (e.shiftKey) { this.moveCursorRightWithShift(e); } else { this.moveCursorRightWithoutShift(e); } this.initDelayedCursor(); }, /** * Moves cursor right while keeping selection * @param {Event} e */ moveCursorRightWithShift: function(e) { if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) { this._moveRight(e, 'selectionStart'); } else { this._selectionDirection = 'right'; this._moveRight(e, 'selectionEnd'); // increase selection by one if it's a newline if (this.text.charAt(this.selectionEnd - 1) === '\n') { this.selectionEnd++; } if (this.selectionEnd > this.text.length) { this.selectionEnd = this.text.length; } } this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Moves cursor right without keeping selection * @param {Event} e Event object */ moveCursorRightWithoutShift: function(e) { this._selectionDirection = 'right'; if (this.selectionStart === this.selectionEnd) { this._moveRight(e, 'selectionStart'); this.selectionEnd = this.selectionStart; } else { this.selectionEnd += this.getNumNewLinesInSelectedText(); if (this.selectionEnd > this.text.length) { this.selectionEnd = this.text.length; } this.selectionStart = this.selectionEnd; } this.fire('selection:changed'); this.canvas && this.canvas.fire('text:selection:changed', { target: this }); }, /** * Inserts a character where cursor is (replacing selection if one exists) * @param {Event} e Event object */ removeChars: function(e) { if (this.selectionStart === this.selectionEnd) { this._removeCharsNearCursor(e); } else { this._removeCharsFromTo(this.selectionStart, this.selectionEnd); } this.selectionEnd = this.selectionStart; this._removeExtraneousStyles(); if (this.canvas) { // TODO: double renderAll gets rid of text box shift happenning sometimes // need to find out what exactly causes it and fix it this.canvas.renderAll().renderAll(); } this.setCoords(); this.fire('changed'); this.canvas && this.canvas.fire('text:changed', { target: this }); }, /** * @private * @param {Event} e Event object */ _removeCharsNearCursor: function(e) { if (this.selectionStart !== 0) { if (e.metaKey) { // remove all till the start of current line var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart); this._removeCharsFromTo(leftLineBoundary, this.selectionStart); this.selectionStart = leftLineBoundary; } else if (e.altKey) { // remove all till the start of current word var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart); this._removeCharsFromTo(leftWordBoundary, this.selectionStart); this.selectionStart = leftWordBoundary; } else { var isBeginningOfLine = this.text.slice(this.selectionStart - 1, this.selectionStart) === '\n'; this.removeStyleObject(isBeginningOfLine); this.selectionStart--; this.text = this.text.slice(0, this.selectionStart) + this.text.slice(this.selectionStart + 1); } } } }); /* _TO_SVG_START_ */ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ { /** * @private */ _setSVGTextLineText: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { if (!this.styles[lineIndex]) { this.callSuper('_setSVGTextLineText', textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier); } else { this._setSVGTextLineChars( textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects); } }, /** * @private */ _setSVGTextLineChars: function(textLine, lineIndex, textSpans, lineHeight, lineTopOffsetMultiplier, textBgRects) { var yProp = lineIndex === 0 || this.useNative ? 'y' : 'dy', chars = textLine.split(''), charOffset = 0, lineLeftOffset = this._getSVGLineLeftOffset(lineIndex), lineTopOffset = this._getSVGLineTopOffset(lineIndex), heightOfLine = this._getHeightOfLine(this.ctx, lineIndex); for (var i = 0, len = chars.length; i < len; i++) { var styleDecl = this.styles[lineIndex][i] || { }; textSpans.push( this._createTextCharSpan( chars[i], styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset)); var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i); if (styleDecl.textBackgroundColor) { textBgRects.push( this._createTextCharBg( styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset)); } charOffset += charWidth; } }, /** * @private */ _getSVGLineLeftOffset: function(lineIndex) { return (this._boundaries && this._boundaries[lineIndex]) ? fabric.util.toFixed(this._boundaries[lineIndex].left, 2) : 0; }, /** * @private */ _getSVGLineTopOffset: function(lineIndex) { var lineTopOffset = 0; for (var j = 0; j <= lineIndex; j++) { lineTopOffset += this._getHeightOfLine(this.ctx, j); } return lineTopOffset - this.height / 2; }, /** * @private */ _createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) { return [ //jscs:disable validateIndentation '' //jscs:enable validateIndentation ].join(''); }, /** * @private */ _createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, yProp, charOffset) { var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({ visible: true, fill: this.fill, stroke: this.stroke, type: 'text' }, styleDecl)); return [ //jscs:disable validateIndentation '', fabric.util.string.escapeXml(_char), '' //jscs:enable validateIndentation ].join(''); } }); /* _TO_SVG_END_ */ (function() { if (typeof document !== 'undefined' && typeof window !== 'undefined') { return; } var DOMParser = require('xmldom').DOMParser, URL = require('url'), HTTP = require('http'), HTTPS = require('https'), Canvas = require('canvas'), Image = require('canvas').Image; /** @private */ function request(url, encoding, callback) { var oURL = URL.parse(url); // detect if http or https is used if ( !oURL.port ) { oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80; } // assign request handler based on protocol var reqHandler = ( oURL.port === 443 ) ? HTTPS : HTTP, req = reqHandler.request({ hostname: oURL.hostname, port: oURL.port, path: oURL.path, method: 'GET' }, 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; } }); }); req.on('error', function(err) { if (err.errno === process.ECONNREFUSED) { fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port); } else { fabric.log(err.message); } }); req.end(); } /** @private */ function requestFs(path, callback) { var fs = require('fs'); fs.readFile(path, function (err, data) { if (err) { fabric.log(err); throw err; } else { callback(data); } }); } fabric.util.loadImage = function(url, callback, context) { function createImageAndCallBack(data) { img.src = new Buffer(data, 'binary'); // preserving original url, which seems to be lost in node-canvas img._src = url; callback && callback.call(context, img); } var img = new Image(); if (url && (url instanceof Buffer || url.indexOf('data') === 0)) { img.src = img._src = url; callback && callback.call(context, img); } else if (url && url.indexOf('http') !== 0) { requestFs(url, createImageAndCallBack); } else if (url) { request(url, 'binary', createImageAndCallBack); } else { callback && callback.call(context, url); } }; fabric.loadSVGFromURL = function(url, callback, reviver) { url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); if (url.indexOf('http') !== 0) { requestFs(url, function(body) { fabric.loadSVGFromString(body.toString(), callback, reviver); }); } else { request(url, '', function(body) { fabric.loadSVGFromString(body, callback, reviver); }); } }; fabric.loadSVGFromString = function(string, callback, reviver) { var doc = new DOMParser().parseFromString(string); fabric.parseSVGDocument(doc.documentElement, function(results, options) { callback && callback(results, options); }, reviver); }; 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, function(filters) { oImg.filters = filters || [ ]; callback && callback(oImg); }); }); }; /** * Only available when running fabric on node.js * @param {Number} width Canvas width * @param {Number} height Canvas height * @param {Object} [options] Options to pass to FabricCanvas. * @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas. * @return {Object} wrapped canvas instance */ fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) { nodeCanvasOptions = nodeCanvasOptions || options; var canvasEl = fabric.document.createElement('canvas'), nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions); // jsdom doesn't create style on canvas element, so here be temp. workaround canvasEl.style = { }; canvasEl.width = nodeCanvas.width; canvasEl.height = nodeCanvas.height; var FabricCanvas = fabric.Canvas || fabric.StaticCanvas, fabricCanvas = new FabricCanvas(canvasEl, options); fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); fabricCanvas.nodeCanvas = nodeCanvas; fabricCanvas.Font = Canvas.Font; return fabricCanvas; }; /** @ignore */ fabric.StaticCanvas.prototype.createPNGStream = function() { return this.nodeCanvas.createPNGStream(); }; fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { return this.nodeCanvas.createJPEGStream(opts); }; var origSetWidth = fabric.StaticCanvas.prototype.setWidth; fabric.StaticCanvas.prototype.setWidth = function(width, options) { origSetWidth.call(this, width, options); 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, options) { origSetHeight.call(this, height, options); this.nodeCanvas.height = height; return this; }; if (fabric.Canvas) { fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight; } })(); // source --> https://jsp.co.id/wp-content/plugins/learnpress-certificates/assets/js/download.js //download.js v4.2, by dandavis; 2008-2016. [CCBY2] see http://danml.com/download.html for tests/usage // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling. // v4 adds AMD/UMD, commonJS, and plain browser support // v4.1 adds url download capability via solo URL argument (same domain/CORS only) // v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors // https://github.com/rndme/download (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals (root is window) root.downloadjs = factory(); } }(this, function () { return function download(data, strFileName, strMimeType) { var self = window, // this script is only for browsers anyway... defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads mimeType = strMimeType || defaultMime, payload = data, url = !strFileName && !strMimeType && payload, anchor = document.createElement("a"), toString = function(a){return String(a);}, myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString), fileName = strFileName || "download", blob, reader; myBlob= myBlob.call ? myBlob.bind(self) : Blob ; if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback payload=[payload, mimeType]; mimeType=payload[0]; payload=payload[1]; } if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument fileName = url.split("/").pop().split("?")[0]; anchor.href = url; // assign href prop to temp anchor if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path: var ajax=new XMLHttpRequest(); ajax.open( "GET", url, true); ajax.responseType = 'blob'; ajax.onload= function(e){ download(e.target.response, fileName, defaultMime); }; setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return: return ajax; } // end if valid url? } // end if url? //go ahead and download dataURLs right away if(/^data\:[\w+\-]+\/[\w+\-]+[,;]/.test(payload)){ if(payload.length > (1024*1024*1.999) && myBlob !== toString ){ payload=dataUrlToBlob(payload); mimeType=payload.type || defaultMime; }else{ return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: navigator.msSaveBlob(dataUrlToBlob(payload), fileName) : saver(payload) ; // everyone else can save dataURLs un-processed } }//end if dataURL passed? blob = payload instanceof myBlob ? payload : new myBlob([payload], {type: mimeType}) ; function dataUrlToBlob(strUrl) { var parts= strUrl.split(/[:;,]/), type= parts[1], decoder= parts[2] == "base64" ? atob : decodeURIComponent, binData= decoder( parts.pop() ), mx= binData.length, i= 0, uiArr= new Uint8Array(mx); for(i;i https://jsp.co.id/wp-content/plugins/learnpress-certificates/assets/js/certificates.js?r=1591260841.4827 ;(function ($) { var _ = { now : Date.now || function () { return new Date().getTime(); }, debounce: function (func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function () { var last = _.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function () { context = this; args = arguments; timestamp = _.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; } }; window.LP_Certificate = function (el, options) { var self = this, viewport = { width : 0, height : 0, templateWidth : 0, templateHeight: 0, ratio : 1 }, $el = $(el), $background = $el.find('.cert-template'), $canvas = null; ajaxData = new FormData(); function init() { initCanvas(); $(document).on('click', '[data-cert="' + $el.attr('id') + '"]', download); self.$canvas = $canvas; self.$background = $background; } function dataURItoBlob(dataURI) { // convert base64 to raw binary data held in a string // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this var byteString = atob(dataURI.split(',')[1]); // separate out the mime component var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0] // write the bytes of the string to an ArrayBuffer var ab = new ArrayBuffer(byteString.length); // create a view into the buffer var ia = new Uint8Array(ab); // set the bytes of the buffer to the correct values for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } // write the ArrayBuffer to a blob, and you're done var blob = new Blob([ab], {type: mimeString}); return blob; } function download() { var name = 'certificate', type = $(this).data('type'), args = { format : type === 'jpg' ? 'jpeg' : 'png', multiplier: 1 / $canvas.getZoom() } if (args.type === 'jpeg') { args.quality = 1; } var data_url = $canvas.toDataURL(); var imageBlob = dataURItoBlob(data_url); var ajaxData = new FormData(); ajaxData.append('files', imageBlob,options.name); downloadjs($canvas.toDataURL(args), options.name); return false; } function createPreview() { var name = 'certificate', type = 'png', args = { format : type === 'jpg' ? 'jpeg' : 'png', multiplier: 1 / $canvas.getZoom() } if (args.type === 'jpeg') { args.quality = 1 } var data = $canvas.toDataURL(args), $img = $el.siblings('.certificate-result'); if ($img.length === 0) { $img = $('').css({'max-width': '100%'}).insertBefore($el); } $img.attr('src', data) } function initCanvas() { if (!$canvas) { $canvas = $el.find('canvas'); $canvas = new fabric.Canvas($canvas.get(0)); $canvas.selection = false; $.each(options.layers, function (i, layer) { if (!layer.type) return; addLayer(layer, {setActive: false}); }); $background.on('load', function () { var tester = new Image(); tester.src = $background.attr('src'); viewport = { width : $background.width(), height : $background.height(), templateWidth : tester.width, templateHeight: tester.height, ratio : $background.width() / tester.width }; fabric.Image.fromURL(tester.src, function (img) { $canvas.backgroundImage = img; updateView(); createPreview(); $(document).triggerHandler('learn-press/certificates/loaded'); }); }).trigger('load'); } $(window).on('resize.update-certificate-view', _.debounce(updateView, 300)).trigger('resize'); } function addLayer($layer, args) { args = $.extend({ setActive: true }, args || {}); if ($.isPlainObject($layer)) { $layer = createLayer($layer); } try { if ($layer) { $canvas.add($layer); if (args.setActive) { $canvas.setActiveObject($layer); } $canvas.renderAll(); } } catch (e) { console.log('Error:' + e) } return $layer; } function htmlDecode(input) { var e = document.createElement('div'); e.innerHTML = input; return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue; } function createLayer(args) { args.text = htmlDecode(args.text) || ''; var defaults = $.extend({ fontSize : 24, left : 0, top : 0, lineHeight: 1, originX : 'center', originY : 'center', fontFamily: 'Helvetica', fieldType : 'custom' }, args), text = args.text || ''; try { var $object = new fabric.Text('' + text + 'xx', defaults), that = this; var is_url = /^(https?|s?ftp):\/\//i.test(args.text); if( args.fieldType == 'verified-link' && args.qr_size > 0 && is_url ){ var qr_code = new Image(); defaults.type = 'image'; defaults.height = defaults.qr_size; defaults.width = defaults.qr_size; qr_code.crossOrigin = "Anonymous"; qr_code.src = args.text; $object = new fabric.Image(qr_code, defaults); } $object.set({ borderColor : '#AAA', cornerColor : '#666', cornerSize : 7, transparentCorners: true, padding : 0 }); $object.set({ hasControls: false }); $object.selectable = false; $.each(defaults, function (k, v) { setLayerProp($object, k, v); }); var $_object = $(document).triggerHandler('learn_press_certificate_layer_obj', [$object, args]); if (typeof $_object === 'object') { $object = $_object; } } catch (e) { console.log(text) } return $object; } function getMaxWidth() { return $el.width(); } function calculate() { viewport = $.extend(viewport, { width : $background.width(), height: $background.height(), ratio : $background.width() / viewport.templateWidth }); } function updateView() { calculate(); $canvas.setHeight(viewport.height); $canvas.setWidth(viewport.width); $canvas.setZoom(viewport.ratio); $canvas.calcOffset(); $canvas.renderAll(); fitImage(); } function fitImage() { var $preview = $el.siblings('.certificate-result'), scrWidth = $el.parent().width(), scrHeight = $(window).height() - (60 + parseInt($el.parent().position().top)), maxWidth = viewport.width, maxHeight = viewport.height; var scale = Math.min( scrWidth / maxWidth, scrHeight / maxHeight ); maxWidth = maxWidth * scale; if (maxWidth) { $preview.css({ //maxWidth: maxWidth * scale }); } } function setLayerProp($layer, prop, value) { var options = {}; switch (prop) { case 'textAlign': //$layer.originX = value; break; case 'color': //$layer.set('fill', value); options['fill'] = value; break; case 'scaleX': case 'scaleY': if (value < 0) { if (prop === 'scaleX') { $layer.flipX = true; } else { $layer.flipY = true; } } else { if (prop === 'scaleX') { $layer.flipX = false; } else { $layer.flipY = false; } } options[prop] = (Math.abs(value)); break; case 'top': case 'left': case 'angle': options[prop] = parseInt(value); break; default: options[prop] = value; } $.each(options, function (k, v) { $layer.set(k, v); }) $layer.setCoords(); } init(); } LP_Certificate.loadJs = function (src, id) { var d = document, s = 'script', js, fjs = d.getElementsByTagName(s)[0]; if (id && d.getElementById(id)) return; js = d.createElement(s); id && (js.id = id); js.src = src; fjs.parentNode.insertBefore(js, fjs); } $(document).ready(function () { var $html = $('html, body'), $popup = $('#certificate-popup'), docOverflow = ''; if (!$popup.length) { return; } function close() { $popup.fadeOut(function () { $html.css('overflow', docOverflow); $popup.find('.learn-press-message').remove(); }); } function open() { $html.css('overflow', 'hidden'); $popup.fadeIn(); } docOverflow = $html.css('overflow'); $(document).on('learn-press/certificates/loaded', function () { $popup.addClass('ready').hide(); $(this) .off('keyup.close-certificate-popup') .on('keyup.close-certificate-popup', function (e) { if (e.keyCode === 27) { close(); } }) .off('clic.close-certificate-popup') .on('click.close-certificate-popup', '.close-popup', function (e) { close(); e.preventDefault(); }); $('form[name="certificate-form-button"]').on('submit', function () { open(); return false; }); if (window.auto_show_certificate === true) { open(); } else { $popup.find('.learn-press-message').remove(); } }) }) })(jQuery); // source --> https://jsp.co.id/wp-content/plugins/learnpress-woo-payment/assets/script.js ;jQuery(function ($) { function get_cart_option() { return LP_WooCommerce_Payment.woocommerce_cart_option; } $('form.purchase-course').submit(function (event) { event.preventDefault(); var $form = $(this), $button = $('button.purchase-button, button.button-add-to-cart , button.button-purchase-course', this), $view_cart = $('.view-cart-button', this), $clicked = $form.find('input:focus, button:focus'), addToCart = $clicked.hasClass('button-add-to-cart'), errorHandler = function () { $button.removeClass('loading'); $('body, html').css('overflow', 'visible'); }; $button.removeClass('added').addClass('loading'); $form.find('#learn-press-wc-message, input[name="purchase-course"]').remove(); var ajax_url = ''; if ($form.find('input[name="course_url"]').length) { ajax_url = $form.find('input[name="course_url"]').val(); var concat_sign = '&'; if (/\?/.test(ajax_url) == false) { concat_sign = '?'; } ajax_url += concat_sign + 'r=' + Math.random(); } else { ajax_url = window.location.href.addQueryVar('r', Math.random());// Do not cache this page } $.ajax({ url: ajax_url,// Do not cache this page data: $(this).serialize(), error: errorHandler, dataType: 'text', success: function (response) { response = LP.parseJSON(response); if (response.added_to_cart == 'yes') { var $form = $('form.purchase-course input[name="add-to-cart"][value="'+response.course_id+'"]').parent("form"); if (response.message && !response.single_purchase) { var $message = $(response.message).addClass('woocommerce-message'); $form.parent('.lp-course-buttons').prepend($('
').append($message)); } if ( response.redirect ) { LP.reload( response.redirect ); } else { $form.hide(); } $('body, html').css('overflow', 'visible'); $(document.body).trigger('wc_fragment_refresh'); } else { errorHandler(); } } }); return false; }); var x = $('#learn-press-checkout') .on('learn_press_checkout_place_order', function () { var $form = $(this), chosen = $('input[type="radio"]:checked', $form); $form.find('input[name="woocommerce_chosen_method"]').remove(); if (chosen.val() == 'woocommerce') { $form.append(''); } }); }); // source --> https://jsp.co.id/wp-content/plugins/js_composer/assets/js/vendors/woocommerce-add-to-cart.js window.jQuery( document ).ready( function ( $ ) { $( 'body' ).on( 'adding_to_cart', function ( event, $button, data ) { $button && $button.hasClass( 'vc_gitem-link' ) && $button .addClass( 'vc-gitem-add-to-cart-loading-btn' ) .parents( '.vc_grid-item-mini' ) .addClass( 'vc-woocommerce-add-to-cart-loading' ) .append( $( '
' ) ); } ).on( 'added_to_cart', function ( event, fragments, cart_hash, $button ) { if ( 'undefined' === typeof($button) ) { $button = $( '.vc-gitem-add-to-cart-loading-btn' ); } $button && $button.hasClass( 'vc_gitem-link' ) && $button .removeClass( 'vc-gitem-add-to-cart-loading-btn' ) .parents( '.vc_grid-item-mini' ) .removeClass( 'vc-woocommerce-add-to-cart-loading' ) .find( '.vc_wc-load-add-to-loader-wrapper' ).remove(); } ); } ); // source --> https://jsp.co.id/wp-content/plugins/learnpress-coming-soon-courses/assets/js/jquery.mb-coming-soon.min.js (function(e){var t;t=function(t,n,r,i,s,o,u){this.$el=e(n);this.gmt=o;this.showText=u;this.end=t;this.active=false;this.interval=1e3;this.speed=i;if(jQuery.isFunction(s))this.callBack=s;else this.callBack=null;this.localization={days:"days",hours:"hours",minutes:"minutes",seconds:"seconds"};e.extend(this.localization,this.localization,r)};t.prototype={getCounterNumbers:function(){var e={days:{tens:0,units:0,hundreds:0},hours:{tens:0,units:0},minutes:{tens:0,units:0},seconds:{tens:0,units:0}},t=1e3*60*60*24,n=1e3*60*60,r=1e3*60,i=1e3,s=0;var o=new Date;var u=o.getTimezoneOffset()/60+this.gmt;var a=this.end.getTime()-o.getTime()-u*60*6e4;if(a<=0)return e;var f=Math.min(Math.floor(a/t),999);s=a%t;e.days.hundreds=Math.floor(f/100);var l=f%100;e.days.tens=Math.floor(l/10);e.days.units=l%10;var c=Math.floor(s/n);s=s%n;e.hours.tens=Math.floor(c/10);e.hours.units=c%10;var h=Math.floor(s/r);s=s%r;e.minutes.tens=Math.floor(h/10);e.minutes.units=h%10;var p=Math.floor(s/1e3);e.seconds.tens=Math.floor(p/10);e.seconds.units=p%10;return e},updatePart:function(t){var n=this.getCounterNumbers();var r=e("."+t,this.$el);if(t=="days"){this.setDayHundreds(n.days.hundreds>0);if(r.find(".number.hundreds.show").html()!=n[t].hundreds){var i=e(".n1.hundreds",r);var s=e(".n2.hundreds",r);this.scrollNumber(i,s,n[t].hundreds)}}if(r.find(".number.tens.show").html()!=n[t].tens){var i=e(".n1.tens",r);var s=e(".n2.tens",r);this.scrollNumber(i,s,n[t].tens)}if(r.find(".number.units.show").html()!=n[t].units){var i=e(".n1.units",r);var s=e(".n2.units",r);this.scrollNumber(i,s,n[t].units)}},timeOut:function(){var e=new Date;var t=e.getTimezoneOffset()/60+this.gmt;var n=this.end.getTime()-e.getTime()-t*60*6e4;if(n<=0)return true;return false},setDayHundreds:function(t){if(t)e(".counter.days",this.$el).addClass("with-hundreds");else e(".counter.days",this.$el).removeClass("with-hundreds")},updateCounter:function(){this.updatePart("days");this.updatePart("hours");this.updatePart("minutes");this.updatePart("seconds");if(this.timeOut()){this.active=false;if(this.callBack)this.callBack(this)}},localize:function(t){if(e.isPlainObject(t))e.extend(this.localization,this.localization,t);e(".days",this.$el).siblings(".counter-caption").text(this.localization.days);e(".hours",this.$el).siblings(".counter-caption").text(this.localization.hours);e(".minutes",this.$el).siblings(".counter-caption").text(this.localization.minutes);e(".seconds",this.$el).siblings(".counter-caption").text(this.localization.seconds)},start:function(e){if(e)this.interval=e;var t=this.interval;this.active=true;var n=this;setTimeout(function(){n.updateCounter();if(n.active)n.start()},t)},stop:function(){this.active=false},scrollNumber:function(e,t,n){if(e.hasClass("show")){t.removeClass("hidden-down").css("top","-100%").text(n).stop().animate({top:0},this.speed,function(){t.addClass("show")});e.stop().animate({top:"100%"},this.speed,function(){e.removeClass("show").addClass("hidden-down")})}else{e.removeClass("hidden-down").css("top","-100%").text(n).stop().animate({top:0},this.speed,function(){e.addClass("show")});t.stop().animate({top:"100%"},this.speed,function(){t.removeClass("show").addClass("hidden-down")})}}};jQuery.fn.mbComingsoon=function(n){var r={interval:1e3,callBack:null,localization:{days:"days",hours:"hours",minutes:"minutes",seconds:"seconds"},speed:500,gmt:0,showText:1};var i={};var s='
'+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+'
days
'+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+'
hours
'+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+'
minutes
'+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+'
seconds
'+"
"+"
";var o='
'+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+"
"+'
'+'
'+'
0
'+'
0
'+'
0
'+'
0
'+"
"+"
"+"
";return this.each(function(){var u=e(this);var a=u.data("mbComingsoon");if(!a){if(n instanceof Date)i.expiryDate=n;else if(e.isPlainObject(n))e.extend(i,r,n);else if(typeof n=="string")i.expiryDate=new Date(n);if(!i.expiryDate)throw new Error("Expiry date is required!");a=new t(i.expiryDate,u,i.localization,i.speed,i.callBack,i.gmt,i.showText);if(i.showText){u.html(s)}else{u.html(o)}a.localize();a.start()}else if(n=="start")a.start();else if(n=="stop")a.stop();else if(e.isPlainObject(n)){if(n.expiryDate instanceof Date)a.end=n.expiryDate;if(e.isNumeric(n.interval))a.interval=n.interval;if(e.isNumeric(n.gmt))a.gmt=n.gmt;if(e.isNumeric(n.showText))a.showText=n.showText;if(e.isFunction(n.callBack))a.callBack=n.callBack;if(e.isPlainObject(n.localization))this.localize(n.localization)}})}})(jQuery)/** * Created by Tu on 12/1/2016. */; // source --> https://jsp.co.id/wp-content/plugins/learnpress-coming-soon-courses/assets/js/coming-soon-course.js (function ($) { $.fn.lp_course_countdown = function () { var countdowns = this; for (var i = 0; i < countdowns.length; i++) { var _countdown = $(countdowns[i]); var speed = _countdown.attr('data-speed'); var remain = parseInt(_countdown.attr('data-timestamp-remain'))*1000; var gmt = _countdown.attr( 'data-gmt' ); var time = _countdown.attr('data-time'); var showtext = _countdown.attr('data-showtext'); var current_time = new Date(); var expiryTime = current_time.getTime()+remain; var expiryDate = new Date(expiryTime); var gmt = -expiryDate.getTimezoneOffset() / 60; var options = { expiryDate : expiryDate, speed : speed ? speed : 500, gmt : parseFloat(gmt), showText : (showtext=='yes') ? 1 : 0, localization: { days: lp_coming_soon_translation.days, hours: lp_coming_soon_translation.hours, minutes: lp_coming_soon_translation.minutes, seconds: lp_coming_soon_translation.seconds } }; _countdown.mbComingsoon(options); } }; $(document).ready(function () { $('.learnpress-course-coming-soon').lp_course_countdown(); if (typeof tinymce !== 'undefined') { setTimeout(function () { var $editor_cm = tinymce.get('_lp_coming_soon_msg'); if ($editor_cm) { $editor_cm.on('Change KeyUp', function (e, b) { $('#' + this.settings.id).val(this.getContent()) }); } }, 1000); } }); })(jQuery); // source --> https://jsp.co.id/wp-content/plugins/learnpress/assets/js/vendor/plugins.all.js?nocache=1591258106.684 /*! * Vue.js v2.6.10 * (c) 2014-2019 Evan You * Released under the MIT License. */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory()); }(this, function () { 'use strict'; /* */ var emptyObject = Object.freeze({}); // These helpers produce better VM code in JS engines due to their // explicitness and function inlining. function isUndef (v) { return v === undefined || v === null } function isDef (v) { return v !== undefined && v !== null } function isTrue (v) { return v === true } function isFalse (v) { return v === false } /** * Check if value is primitive. */ function isPrimitive (value) { return ( typeof value === 'string' || typeof value === 'number' || // $flow-disable-line typeof value === 'symbol' || typeof value === 'boolean' ) } /** * Quick object check - this is primarily used to tell * Objects from primitive values when we know the value * is a JSON-compliant type. */ function isObject (obj) { return obj !== null && typeof obj === 'object' } /** * Get the raw type string of a value, e.g., [object Object]. */ var _toString = Object.prototype.toString; function toRawType (value) { return _toString.call(value).slice(8, -1) } /** * Strict object type check. Only returns true * for plain JavaScript objects. */ function isPlainObject (obj) { return _toString.call(obj) === '[object Object]' } function isRegExp (v) { return _toString.call(v) === '[object RegExp]' } /** * Check if val is a valid array index. */ function isValidArrayIndex (val) { var n = parseFloat(String(val)); return n >= 0 && Math.floor(n) === n && isFinite(val) } function isPromise (val) { return ( isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function' ) } /** * Convert a value to a string that is actually rendered. */ function toString (val) { return val == null ? '' : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ? JSON.stringify(val, null, 2) : String(val) } /** * Convert an input value to a number for persistence. * If the conversion fails, return original string. */ function toNumber (val) { var n = parseFloat(val); return isNaN(n) ? val : n } /** * Make a map and return a function for checking if a key * is in that map. */ function makeMap ( str, expectsLowerCase ) { var map = Object.create(null); var list = str.split(','); for (var i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; } } /** * Check if a tag is a built-in tag. */ var isBuiltInTag = makeMap('slot,component', true); /** * Check if an attribute is a reserved attribute. */ var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); /** * Remove an item from an array. */ function remove (arr, item) { if (arr.length) { var index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1) } } } /** * Check whether an object has the property. */ var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } /** * Create a cached version of a pure function. */ function cached (fn) { var cache = Object.create(null); return (function cachedFn (str) { var hit = cache[str]; return hit || (cache[str] = fn(str)) }) } /** * Camelize a hyphen-delimited string. */ var camelizeRE = /-(\w)/g; var camelize = cached(function (str) { return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) }); /** * Capitalize a string. */ var capitalize = cached(function (str) { return str.charAt(0).toUpperCase() + str.slice(1) }); /** * Hyphenate a camelCase string. */ var hyphenateRE = /\B([A-Z])/g; var hyphenate = cached(function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase() }); /** * Simple bind polyfill for environments that do not support it, * e.g., PhantomJS 1.x. Technically, we don't need this anymore * since native bind is now performant enough in most browsers. * But removing it would mean breaking code that was able to run in * PhantomJS 1.x, so this must be kept for backward compatibility. */ /* istanbul ignore next */ function polyfillBind (fn, ctx) { function boundFn (a) { var l = arguments.length; return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } boundFn._length = fn.length; return boundFn } function nativeBind (fn, ctx) { return fn.bind(ctx) } var bind = Function.prototype.bind ? nativeBind : polyfillBind; /** * Convert an Array-like object to a real Array. */ function toArray (list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret } /** * Mix properties into target object. */ function extend (to, _from) { for (var key in _from) { to[key] = _from[key]; } return to } /** * Merge an Array of Objects into a single Object. */ function toObject (arr) { var res = {}; for (var i = 0; i < arr.length; i++) { if (arr[i]) { extend(res, arr[i]); } } return res } /* eslint-disable no-unused-vars */ /** * Perform no operation. * Stubbing args to make Flow happy without leaving useless transpiled code * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). */ function noop (a, b, c) {} /** * Always return false. */ var no = function (a, b, c) { return false; }; /* eslint-enable no-unused-vars */ /** * Return the same value. */ var identity = function (_) { return _; }; /** * Generate a string containing static keys from compiler modules. */ function genStaticKeys (modules) { return modules.reduce(function (keys, m) { return keys.concat(m.staticKeys || []) }, []).join(',') } /** * Check if two values are loosely equal - that is, * if they are plain objects, do they have the same shape? */ function looseEqual (a, b) { if (a === b) { return true } var isObjectA = isObject(a); var isObjectB = isObject(b); if (isObjectA && isObjectB) { try { var isArrayA = Array.isArray(a); var isArrayB = Array.isArray(b); if (isArrayA && isArrayB) { return a.length === b.length && a.every(function (e, i) { return looseEqual(e, b[i]) }) } else if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() } else if (!isArrayA && !isArrayB) { var keysA = Object.keys(a); var keysB = Object.keys(b); return keysA.length === keysB.length && keysA.every(function (key) { return looseEqual(a[key], b[key]) }) } else { /* istanbul ignore next */ return false } } catch (e) { /* istanbul ignore next */ return false } } else if (!isObjectA && !isObjectB) { return String(a) === String(b) } else { return false } } /** * Return the first index at which a loosely equal value can be * found in the array (if value is a plain object, the array must * contain an object of the same shape), or -1 if it is not present. */ function looseIndexOf (arr, val) { for (var i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) { return i } } return -1 } /** * Ensure a function is called only once. */ function once (fn) { var called = false; return function () { if (!called) { called = true; fn.apply(this, arguments); } } } var SSR_ATTR = 'data-server-rendered'; var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch' ]; /* */ var config = ({ /** * Option merge strategies (used in core/util/options) */ // $flow-disable-line optionMergeStrategies: Object.create(null), /** * Whether to suppress warnings. */ silent: false, /** * Show production mode tip message on boot? */ productionTip: "development" !== 'production', /** * Whether to enable devtools */ devtools: "development" !== 'production', /** * Whether to record perf */ performance: false, /** * Error handler for watcher errors */ errorHandler: null, /** * Warn handler for watcher warns */ warnHandler: null, /** * Ignore certain custom elements */ ignoredElements: [], /** * Custom user key aliases for v-on */ // $flow-disable-line keyCodes: Object.create(null), /** * Check if a tag is reserved so that it cannot be registered as a * component. This is platform-dependent and may be overwritten. */ isReservedTag: no, /** * Check if an attribute is reserved so that it cannot be used as a component * prop. This is platform-dependent and may be overwritten. */ isReservedAttr: no, /** * Check if a tag is an unknown element. * Platform-dependent. */ isUnknownElement: no, /** * Get the namespace of an element */ getTagNamespace: noop, /** * Parse the real tag name for the specific platform. */ parsePlatformTagName: identity, /** * Check if an attribute must be bound using property, e.g. value * Platform-dependent. */ mustUseProp: no, /** * Perform updates asynchronously. Intended to be used by Vue Test Utils * This will significantly reduce performance if set to false. */ async: true, /** * Exposed for legacy reasons */ _lifecycleHooks: LIFECYCLE_HOOKS }); /* */ /** * unicode letters used for parsing html tags, component names and property paths. * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname * skipping \u10000-\uEFFFF due to it freezing up PhantomJS */ var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; /** * Check if a string starts with $ or _ */ function isReserved (str) { var c = (str + '').charCodeAt(0); return c === 0x24 || c === 0x5F } /** * Define a property. */ function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); } /** * Parse simple path. */ var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]")); function parsePath (path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { for (var i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]]; } return obj } } /* */ // can we use __proto__? var hasProto = '__proto__' in {}; // Browser environment sniffing var inBrowser = typeof window !== 'undefined'; var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); var UA = inBrowser && window.navigator.userAgent.toLowerCase(); var isIE = UA && /msie|trident/.test(UA); var isIE9 = UA && UA.indexOf('msie 9.0') > 0; var isEdge = UA && UA.indexOf('edge/') > 0; var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; var isPhantomJS = UA && /phantomjs/.test(UA); var isFF = UA && UA.match(/firefox\/(\d+)/); // Firefox has a "watch" function on Object.prototype... var nativeWatch = ({}).watch; var supportsPassive = false; if (inBrowser) { try { var opts = {}; Object.defineProperty(opts, 'passive', ({ get: function get () { /* istanbul ignore next */ supportsPassive = true; } })); // https://github.com/facebook/flow/issues/285 window.addEventListener('test-passive', null, opts); } catch (e) {} } // this needs to be lazy-evaled because vue may be required before // vue-server-renderer can set VUE_ENV var _isServer; var isServerRendering = function () { if (_isServer === undefined) { /* istanbul ignore if */ if (!inBrowser && !inWeex && typeof global !== 'undefined') { // detect presence of vue-server-renderer and avoid // Webpack shimming the process _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; } else { _isServer = false; } } return _isServer }; // detect devtools var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; /* istanbul ignore next */ function isNative (Ctor) { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } var hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) && typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); var _Set; /* istanbul ignore if */ // $flow-disable-line if (typeof Set !== 'undefined' && isNative(Set)) { // use native Set when available. _Set = Set; } else { // a non-standard Set polyfill that only works with primitive keys. _Set = /*@__PURE__*/(function () { function Set () { this.set = Object.create(null); } Set.prototype.has = function has (key) { return this.set[key] === true }; Set.prototype.add = function add (key) { this.set[key] = true; }; Set.prototype.clear = function clear () { this.set = Object.create(null); }; return Set; }()); } /* */ var warn = noop; var tip = noop; var generateComponentTrace = (noop); // work around flow check var formatComponentName = (noop); { var hasConsole = typeof console !== 'undefined'; var classifyRE = /(?:^|[-_])(\w)/g; var classify = function (str) { return str .replace(classifyRE, function (c) { return c.toUpperCase(); }) .replace(/[-_]/g, ''); }; warn = function (msg, vm) { var trace = vm ? generateComponentTrace(vm) : ''; if (config.warnHandler) { config.warnHandler.call(null, msg, vm, trace); } else if (hasConsole && (!config.silent)) { console.error(("[Vue warn]: " + msg + trace)); } }; tip = function (msg, vm) { if (hasConsole && (!config.silent)) { console.warn("[Vue tip]: " + msg + ( vm ? generateComponentTrace(vm) : '' )); } }; formatComponentName = function (vm, includeFile) { if (vm.$root === vm) { return '' } var options = typeof vm === 'function' && vm.cid != null ? vm.options : vm._isVue ? vm.$options || vm.constructor.options : vm; var name = options.name || options._componentTag; var file = options.__file; if (!name && file) { var match = file.match(/([^/\\]+)\.vue$/); name = match && match[1]; } return ( (name ? ("<" + (classify(name)) + ">") : "") + (file && includeFile !== false ? (" at " + file) : '') ) }; var repeat = function (str, n) { var res = ''; while (n) { if (n % 2 === 1) { res += str; } if (n > 1) { str += str; } n >>= 1; } return res }; generateComponentTrace = function (vm) { if (vm._isVue && vm.$parent) { var tree = []; var currentRecursiveSequence = 0; while (vm) { if (tree.length > 0) { var last = tree[tree.length - 1]; if (last.constructor === vm.constructor) { currentRecursiveSequence++; vm = vm.$parent; continue } else if (currentRecursiveSequence > 0) { tree[tree.length - 1] = [last, currentRecursiveSequence]; currentRecursiveSequence = 0; } } tree.push(vm); vm = vm.$parent; } return '\n\nfound in\n\n' + tree .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") : formatComponentName(vm))); }) .join('\n') } else { return ("\n\n(found in " + (formatComponentName(vm)) + ")") } }; } /* */ var uid = 0; /** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null; var targetStack = []; function pushTarget (target) { targetStack.push(target); Dep.target = target; } function popTarget () { targetStack.pop(); Dep.target = targetStack[targetStack.length - 1]; } /* */ var VNode = function VNode ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag; this.data = data; this.children = children; this.text = text; this.elm = elm; this.ns = undefined; this.context = context; this.fnContext = undefined; this.fnOptions = undefined; this.fnScopeId = undefined; this.key = data && data.key; this.componentOptions = componentOptions; this.componentInstance = undefined; this.parent = undefined; this.raw = false; this.isStatic = false; this.isRootInsert = true; this.isComment = false; this.isCloned = false; this.isOnce = false; this.asyncFactory = asyncFactory; this.asyncMeta = undefined; this.isAsyncPlaceholder = false; }; var prototypeAccessors = { child: { configurable: true } }; // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ prototypeAccessors.child.get = function () { return this.componentInstance }; Object.defineProperties( VNode.prototype, prototypeAccessors ); var createEmptyVNode = function (text) { if ( text === void 0 ) text = ''; var node = new VNode(); node.text = text; node.isComment = true; return node }; function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val)) } // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. function cloneVNode (vnode) { var cloned = new VNode( vnode.tag, vnode.data, // #7975 // clone children array to avoid mutating original in case of cloning // a child. vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.asyncMeta = vnode.asyncMeta; cloned.isCloned = true; return cloned } /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); }); /* */ var arrayKeys = Object.getOwnPropertyNames(arrayMethods); /** * In some cases we may want to disable observation inside a component's * update computation. */ var shouldObserve = true; function toggleObserving (value) { shouldObserve = value; } /** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }; /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } }; // helpers /** * Augment a target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src) { /* eslint-disable no-proto */ target.__proto__ = src; /* eslint-enable no-proto */ } /** * Augment a target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; def(target, key, src[key]); } } /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } /** * Define a reactive property on an Object. */ function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } /** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ function set (target, key, val) { if (isUndef(target) || isPrimitive(target) ) { warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (key in target && !(key in Object.prototype)) { target[key] = val; return val } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val } if (!ob) { target[key] = val; return val } defineReactive$$1(ob.value, key, val); ob.dep.notify(); return val } /** * Delete a property and trigger change if necessary. */ function del (target, key) { if (isUndef(target) || isPrimitive(target) ) { warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); } if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1); return } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ); return } if (!hasOwn(target, key)) { return } delete target[key]; if (!ob) { return } ob.dep.notify(); } /** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray (value) { for (var e = (void 0), i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } } /* */ /** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ var strats = config.optionMergeStrategies; /** * Options with restrictions */ { strats.el = strats.propsData = function (parent, child, vm, key) { if (!vm) { warn( "option \"" + key + "\" can only be used during instance " + 'creation with the `new` keyword.' ); } return defaultStrat(parent, child) }; } /** * Helper that recursively merges two data objects together. */ function mergeData (to, from) { if (!from) { return to } var key, toVal, fromVal; var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); for (var i = 0; i < keys.length; i++) { key = keys[i]; // in case the object is already observed... if (key === '__ob__') { continue } toVal = to[key]; fromVal = from[key]; if (!hasOwn(to, key)) { set(to, key, fromVal); } else if ( toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal) ) { mergeData(toVal, fromVal); } } return to } /** * Data */ function mergeDataOrFn ( parentVal, childVal, vm ) { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn () { // instance merge var instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal; var defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal; if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } } strats.data = function ( parentVal, childVal, vm ) { if (!vm) { if (childVal && typeof childVal !== 'function') { warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ); return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) }; /** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal, childVal ) { var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal; return res ? dedupeHooks(res) : res } function dedupeHooks (hooks) { var res = []; for (var i = 0; i < hooks.length; i++) { if (res.indexOf(hooks[i]) === -1) { res.push(hooks[i]); } } return res } LIFECYCLE_HOOKS.forEach(function (hook) { strats[hook] = mergeHook; }); /** * Assets * * When a vm is present (instance creation), we need to do * a three-way merge between constructor options, instance * options and parent options. */ function mergeAssets ( parentVal, childVal, vm, key ) { var res = Object.create(parentVal || null); if (childVal) { assertObjectType(key, childVal, vm); return extend(res, childVal) } else { return res } } ASSET_TYPES.forEach(function (type) { strats[type + 's'] = mergeAssets; }); /** * Watchers. * * Watchers hashes should not overwrite one * another, so we merge them as arrays. */ strats.watch = function ( parentVal, childVal, vm, key ) { // work around Firefox's Object.prototype.watch... if (parentVal === nativeWatch) { parentVal = undefined; } if (childVal === nativeWatch) { childVal = undefined; } /* istanbul ignore if */ if (!childVal) { return Object.create(parentVal || null) } { assertObjectType(key, childVal, vm); } if (!parentVal) { return childVal } var ret = {}; extend(ret, parentVal); for (var key$1 in childVal) { var parent = ret[key$1]; var child = childVal[key$1]; if (parent && !Array.isArray(parent)) { parent = [parent]; } ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; } return ret }; /** * Other object hashes. */ strats.props = strats.methods = strats.inject = strats.computed = function ( parentVal, childVal, vm, key ) { if (childVal && "development" !== 'production') { assertObjectType(key, childVal, vm); } if (!parentVal) { return childVal } var ret = Object.create(null); extend(ret, parentVal); if (childVal) { extend(ret, childVal); } return ret }; strats.provide = mergeDataOrFn; /** * Default strategy. */ var defaultStrat = function (parentVal, childVal) { return childVal === undefined ? parentVal : childVal }; /** * Validate component names */ function checkComponents (options) { for (var key in options.components) { validateComponentName(key); } } function validateComponentName (name) { if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'should conform to valid custom element name in html5 specification.' ); } if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ); } } /** * Ensure all props option syntax are normalized into the * Object-based format. */ function normalizeProps (options, vm) { var props = options.props; if (!props) { return } var res = {}; var i, val, name; if (Array.isArray(props)) { i = props.length; while (i--) { val = props[i]; if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; } else { warn('props must be strings when using array syntax.'); } } } else if (isPlainObject(props)) { for (var key in props) { val = props[key]; name = camelize(key); res[name] = isPlainObject(val) ? val : { type: val }; } } else { warn( "Invalid value for option \"props\": expected an Array or an Object, " + "but got " + (toRawType(props)) + ".", vm ); } options.props = res; } /** * Normalize all injections into Object-based format */ function normalizeInject (options, vm) { var inject = options.inject; if (!inject) { return } var normalized = options.inject = {}; if (Array.isArray(inject)) { for (var i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] }; } } else if (isPlainObject(inject)) { for (var key in inject) { var val = inject[key]; normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; } } else { warn( "Invalid value for option \"inject\": expected an Array or an Object, " + "but got " + (toRawType(inject)) + ".", vm ); } } /** * Normalize raw function directives into object format. */ function normalizeDirectives (options) { var dirs = options.directives; if (dirs) { for (var key in dirs) { var def$$1 = dirs[key]; if (typeof def$$1 === 'function') { dirs[key] = { bind: def$$1, update: def$$1 }; } } } } function assertObjectType (name, value, vm) { if (!isPlainObject(value)) { warn( "Invalid value for option \"" + name + "\": expected an Object, " + "but got " + (toRawType(value)) + ".", vm ); } } /** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ function mergeOptions ( parent, child, vm ) { { checkComponents(child); } if (typeof child === 'function') { child = child.options; } normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm); } if (child.mixins) { for (var i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } } var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField (key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options } /** * Resolve an asset. * This function is used because child instances need access * to assets defined in its ancestor chain. */ function resolveAsset ( options, type, id, warnMissing ) { /* istanbul ignore if */ if (typeof id !== 'string') { return } var assets = options[type]; // check local registration variations first if (hasOwn(assets, id)) { return assets[id] } var camelizedId = camelize(id); if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } var PascalCaseId = capitalize(camelizedId); if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } // fallback to prototype chain var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; if (warnMissing && !res) { warn( 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, options ); } return res } /* */ function validateProp ( key, propOptions, propsData, vm ) { var prop = propOptions[key]; var absent = !hasOwn(propsData, key); var value = propsData[key]; // boolean casting var booleanIndex = getTypeIndex(Boolean, prop.type); if (booleanIndex > -1) { if (absent && !hasOwn(prop, 'default')) { value = false; } else if (value === '' || value === hyphenate(key)) { // only cast empty string / same name to boolean if // boolean has higher priority var stringIndex = getTypeIndex(String, prop.type); if (stringIndex < 0 || booleanIndex < stringIndex) { value = true; } } } // check default value if (value === undefined) { value = getPropDefaultValue(vm, prop, key); // since the default value is a fresh copy, // make sure to observe it. var prevShouldObserve = shouldObserve; toggleObserving(true); observe(value); toggleObserving(prevShouldObserve); } { assertProp(prop, key, value, vm, absent); } return value } /** * Get the default value of a prop. */ function getPropDefaultValue (vm, prop, key) { // no default, return undefined if (!hasOwn(prop, 'default')) { return undefined } var def = prop.default; // warn against non-factory defaults for Object & Array if (isObject(def)) { warn( 'Invalid default value for prop "' + key + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm ); } // the raw prop value was also undefined from previous render, // return previous default value to avoid unnecessary watcher trigger if (vm && vm.$options.propsData && vm.$options.propsData[key] === undefined && vm._props[key] !== undefined ) { return vm._props[key] } // call factory function for non-Function types // a value is Function if its prototype is function even across different execution context return typeof def === 'function' && getType(prop.type) !== 'Function' ? def.call(vm) : def } /** * Assert whether a prop is valid. */ function assertProp ( prop, name, value, vm, absent ) { if (prop.required && absent) { warn( 'Missing required prop: "' + name + '"', vm ); return } if (value == null && !prop.required) { return } var type = prop.type; var valid = !type || type === true; var expectedTypes = []; if (type) { if (!Array.isArray(type)) { type = [type]; } for (var i = 0; i < type.length && !valid; i++) { var assertedType = assertType(value, type[i]); expectedTypes.push(assertedType.expectedType || ''); valid = assertedType.valid; } } if (!valid) { warn( getInvalidTypeMessage(name, value, expectedTypes), vm ); return } var validator = prop.validator; if (validator) { if (!validator(value)) { warn( 'Invalid prop: custom validator check failed for prop "' + name + '".', vm ); } } } var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; function assertType (value, type) { var valid; var expectedType = getType(type); if (simpleCheckRE.test(expectedType)) { var t = typeof value; valid = t === expectedType.toLowerCase(); // for primitive wrapper objects if (!valid && t === 'object') { valid = value instanceof type; } } else if (expectedType === 'Object') { valid = isPlainObject(value); } else if (expectedType === 'Array') { valid = Array.isArray(value); } else { valid = value instanceof type; } return { valid: valid, expectedType: expectedType } } /** * Use function string name to check built-in types, * because a simple equality check will fail when running * across different vms / iframes. */ function getType (fn) { var match = fn && fn.toString().match(/^\s*function (\w+)/); return match ? match[1] : '' } function isSameType (a, b) { return getType(a) === getType(b) } function getTypeIndex (type, expectedTypes) { if (!Array.isArray(expectedTypes)) { return isSameType(expectedTypes, type) ? 0 : -1 } for (var i = 0, len = expectedTypes.length; i < len; i++) { if (isSameType(expectedTypes[i], type)) { return i } } return -1 } function getInvalidTypeMessage (name, value, expectedTypes) { var message = "Invalid prop: type check failed for prop \"" + name + "\"." + " Expected " + (expectedTypes.map(capitalize).join(', ')); var expectedType = expectedTypes[0]; var receivedType = toRawType(value); var expectedValue = styleValue(value, expectedType); var receivedValue = styleValue(value, receivedType); // check if we need to specify expected value if (expectedTypes.length === 1 && isExplicable(expectedType) && !isBoolean(expectedType, receivedType)) { message += " with value " + expectedValue; } message += ", got " + receivedType + " "; // check if we need to specify received value if (isExplicable(receivedType)) { message += "with value " + receivedValue + "."; } return message } function styleValue (value, type) { if (type === 'String') { return ("\"" + value + "\"") } else if (type === 'Number') { return ("" + (Number(value))) } else { return ("" + value) } } function isExplicable (value) { var explicitTypes = ['string', 'number', 'boolean']; return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) } function isBoolean () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) } /* */ function handleError (err, vm, info) { // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. // See: https://github.com/vuejs/vuex/issues/1505 pushTarget(); try { if (vm) { var cur = vm; while ((cur = cur.$parent)) { var hooks = cur.$options.errorCaptured; if (hooks) { for (var i = 0; i < hooks.length; i++) { try { var capture = hooks[i].call(cur, err, vm, info) === false; if (capture) { return } } catch (e) { globalHandleError(e, cur, 'errorCaptured hook'); } } } } } globalHandleError(err, vm, info); } finally { popTarget(); } } function invokeWithErrorHandling ( handler, context, args, vm, info ) { var res; try { res = args ? handler.apply(context, args) : handler.call(context); if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); }); // issue #9511 // avoid catch triggering multiple times when nested calls res._handled = true; } } catch (e) { handleError(e, vm, info); } return res } function globalHandleError (err, vm, info) { if (config.errorHandler) { try { return config.errorHandler.call(null, err, vm, info) } catch (e) { // if the user intentionally throws the original error in the handler, // do not log it twice if (e !== err) { logError(e, null, 'config.errorHandler'); } } } logError(err, vm, info); } function logError (err, vm, info) { { warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); } /* istanbul ignore else */ if ((inBrowser || inWeex) && typeof console !== 'undefined') { console.error(err); } else { throw err } } /* */ var isUsingMicroTask = false; var callbacks = []; var pending = false; function flushCallbacks () { pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } // Here we have async deferring wrappers using microtasks. // In 2.5 we used (macro) tasks (in combination with microtasks). // However, it has subtle problems when state is changed right before repaint // (e.g. #6813, out-in transitions). // Also, using (macro) tasks in event handler would cause some weird behaviors // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). // So we now use microtasks everywhere, again. // A major drawback of this tradeoff is that there are some scenarios // where microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690, which have workarounds) // or even between bubbling of the same event (#6566). var timerFunc; // The nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function () { p.then(flushCallbacks); // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) { setTimeout(noop); } }; isUsingMicroTask = true; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; isUsingMicroTask = true; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Techinically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = function () { setImmediate(flushCallbacks); }; } else { // Fallback to setTimeout. timerFunc = function () { setTimeout(flushCallbacks, 0); }; } function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } } /* */ var mark; var measure; { var perf = inBrowser && window.performance; /* istanbul ignore if */ if ( perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures ) { mark = function (tag) { return perf.mark(tag); }; measure = function (name, startTag, endTag) { perf.measure(name, startTag, endTag); perf.clearMarks(startTag); perf.clearMarks(endTag); // perf.clearMeasures(name) }; } } /* not type checking this file because flow doesn't play well with Proxy */ var initProxy; { var allowedGlobals = makeMap( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 'require' // for Webpack/Browserify ); var warnNonPresent = function (target, key) { warn( "Property or method \"" + key + "\" is not defined on the instance but " + 'referenced during render. Make sure that this property is reactive, ' + 'either in the data option, or for class-based components, by ' + 'initializing the property. ' + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target ); }; var warnReservedPrefix = function (target, key) { warn( "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + 'prevent conflicts with Vue internals' + 'See: https://vuejs.org/v2/api/#data', target ); }; var hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy); if (hasProxy) { var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); config.keyCodes = new Proxy(config.keyCodes, { set: function set (target, key, value) { if (isBuiltInModifier(key)) { warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); return false } else { target[key] = value; return true } } }); } var hasHandler = { has: function has (target, key) { var has = key in target; var isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); if (!has && !isAllowed) { if (key in target.$data) { warnReservedPrefix(target, key); } else { warnNonPresent(target, key); } } return has || !isAllowed } }; var getHandler = { get: function get (target, key) { if (typeof key === 'string' && !(key in target)) { if (key in target.$data) { warnReservedPrefix(target, key); } else { warnNonPresent(target, key); } } return target[key] } }; initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; } }; } /* */ var seenObjects = new _Set(); /** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */ function traverse (val) { _traverse(val, seenObjects); seenObjects.clear(); } function _traverse (val, seen) { var i, keys; var isA = Array.isArray(val); if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { var depId = val.__ob__.dep.id; if (seen.has(depId)) { return } seen.add(depId); } if (isA) { i = val.length; while (i--) { _traverse(val[i], seen); } } else { keys = Object.keys(val); i = keys.length; while (i--) { _traverse(val[keys[i]], seen); } } } /* */ var normalizeEvent = cached(function (name) { var passive = name.charAt(0) === '&'; name = passive ? name.slice(1) : name; var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first name = once$$1 ? name.slice(1) : name; var capture = name.charAt(0) === '!'; name = capture ? name.slice(1) : name; return { name: name, once: once$$1, capture: capture, passive: passive } }); function createFnInvoker (fns, vm) { function invoker () { var arguments$1 = arguments; var fns = invoker.fns; if (Array.isArray(fns)) { var cloned = fns.slice(); for (var i = 0; i < cloned.length; i++) { invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); } } else { // return handler return value for single handlers return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler") } } invoker.fns = fns; return invoker } function updateListeners ( on, oldOn, add, remove$$1, createOnceHandler, vm ) { var name, def$$1, cur, old, event; for (name in on) { def$$1 = cur = on[name]; old = oldOn[name]; event = normalizeEvent(name); if (isUndef(cur)) { warn( "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), vm ); } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm); } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture); } add(event.name, cur, event.capture, event.passive, event.params); } else if (cur !== old) { old.fns = cur; on[name] = old; } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove$$1(event.name, oldOn[name], event.capture); } } } /* */ function mergeVNodeHook (def, hookKey, hook) { if (def instanceof VNode) { def = def.data.hook || (def.data.hook = {}); } var invoker; var oldHook = def[hookKey]; function wrappedHook () { hook.apply(this, arguments); // important: remove merged hook to ensure it's called only once // and prevent memory leak remove(invoker.fns, wrappedHook); } if (isUndef(oldHook)) { // no existing hook invoker = createFnInvoker([wrappedHook]); } else { /* istanbul ignore if */ if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { // already a merged invoker invoker = oldHook; invoker.fns.push(wrappedHook); } else { // existing plain hook invoker = createFnInvoker([oldHook, wrappedHook]); } } invoker.merged = true; def[hookKey] = invoker; } /* */ function extractPropsFromVNodeData ( data, Ctor, tag ) { // we are only extracting raw values here. // validation and default values are handled in the child // component itself. var propOptions = Ctor.options.props; if (isUndef(propOptions)) { return } var res = {}; var attrs = data.attrs; var props = data.props; if (isDef(attrs) || isDef(props)) { for (var key in propOptions) { var altKey = hyphenate(key); { var keyInLowerCase = key.toLowerCase(); if ( key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase) ) { tip( "Prop \"" + keyInLowerCase + "\" is passed to component " + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + " \"" + key + "\". " + "Note that HTML attributes are case-insensitive and camelCased " + "props need to use their kebab-case equivalents when using in-DOM " + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." ); } } checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false); } } return res } function checkProp ( res, hash, key, altKey, preserve ) { if (isDef(hash)) { if (hasOwn(hash, key)) { res[key] = hash[key]; if (!preserve) { delete hash[key]; } return true } else if (hasOwn(hash, altKey)) { res[key] = hash[altKey]; if (!preserve) { delete hash[altKey]; } return true } } return false } /* */ // The template compiler attempts to minimize the need for normalization by // statically analyzing the template at compile time. // // For plain HTML markup, normalization can be completely skipped because the // generated render function is guaranteed to return Array. There are // two cases where extra normalization is needed: // 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. function simpleNormalizeChildren (children) { for (var i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g.