1&&(g=Math.sqrt(g),n*=g,i*=g);var y=d/n,b=p/n,w=-p/i,E=d/i,S=y*l+b*c,x=w*l+E*c,T=y*e+b*t,N=w*e+E*t,C=(T-S)*(T-S)+(N-x)*(N-x),k=1/C-.25;k<0&&(k=0);var L=Math.sqrt(k);a===u&&(L=-L);var A=.5*(S+T)-L*(N-x),O=.5*(x+N)+L*(T-S),M=Math.atan2(x-O,S-A),_=Math.atan2(N-O,T-A),D=_-M;D<0&&a===1?D+=2*Math.PI:D>0&&a===0&&(D-=2*Math.PI);var P=Math.ceil(Math.abs(D/(Math.PI*.5+.001))),H=[];for(var B=0;B"},toObject:function(e){var t=h(this.callSuper("toObject",e),{path:this.path});return this.sourcePath&&(t.sourcePath=this.sourcePath),this.transformMatrix&&(t.transformMatrix=this.transformMatrix),t},toDatalessObject:function(e){var t=this.toObject(e);return this.sourcePath&&(t.path=this.sourcePath),delete t.sourcePath,t},toSVG:function(){var e=[];for(var t=0,n=this.path.length;t',"',""].join("")},complexity:function(){return this.path.length},_parsePath:function(){var e=[],n,r,i;for(var s=0,o,u=this.path.length;sc)for(var h=1,p=o.length;h"];for(var n=0,r=e.length;n"),t.join("")},toString:function(){return"#"},isSameColor:function(){var e=this.getObjects()[0].get("fill");return this.getObjects().every(function(t){return t.get("fill")===e})},complexity:function(){return this.paths.reduce(function(e,t){return e+(t&&t.complexity?t.complexity():0)},0)},toGrayscale:function(){var e=this.paths.length;while(e--)this.paths[e].toGrayscale();return this},getObjects:function(){return this.paths}}),t.PathGroup.fromObject=function(e){var n=u(e.paths);return new t.PathGroup(n,e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.min,i=t.util.array.max,s=t.util.array.invoke,o=t.util.removeFromArray;if(t.Group)return;var u={lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0};t.Group=t.util.createClass(t.Object,{type:"group",initialize:function(e,t){t=t||{},this.objects=e||[],this.originalState={},this.callSuper("initialize"),this._calcBounds(),this._updateObjectsCoords(),t&&n(this,t),this._setOpacityIfSame(),this.setCoords(!0),this.saveCoords()},_updateObjectsCoords:function(){var e=this.left,t=this.top;this.forEachObject(function(n){var r=n.get("left"),i=n.get("top");n.set("originalLeft",r),n.set("originalTop",i),n.set("left",r-e),n.set("top",i-t),n.setCoords(),n.hideCorners=!0},this)},toString:function(){return"#"},getObjects:function(){return this.objects},addWithUpdate:function(e){return this._restoreObjectsState(),this.objects.push(e),this._calcBounds(),this._updateObjectsCoords(),this},removeWithUpdate:function(e){return this._restoreObjectsState(),o(this.objects,e),e.setActive(!1),this._calcBounds(),this._updateObjectsCoords(),this},add:function(e){return this.objects.push(e),this},remove:function(e){return o(this.objects,e),this},size:function(){return this.getObjects().length},delegatedProperties:{fill:!0,opacity:!0,fontFamily:!0,fontWeight:!0,lineHeight:!0,textDecoration:!0,textShadow:!0,backgroundColor:!0},_set:function(e,t){if(e in this.delegatedProperties){var n=this.objects.length;this[e]=t;while(n--)this.objects[n].set(e,t)}else this[e]=t},contains:function(e){return this.objects.indexOf(e)>-1},toObject:function(e){return n(this.callSuper("toObject",e),{objects:s(this.objects,"toObject",e)})},render:function(e,t){e.save(),this.transform(e);var n=Math.max(this.scaleX,this.scaleY);for(var r=this.objects.length;r>0;r--){var i=this.objects[r-1],s=i.borderScaleFactor,o=i.hasRotatingPoint;i.borderScaleFactor=n,i.hasRotatingPoint=!1,i.render(e),i.borderScaleFactor=s,i.hasRotatingPoint=o}!t&&this.active&&(this.drawBorders(e),this.hideCorners||this.drawCorners(e)),e.restore(),this.setCoords()},item:function(e){return this.getObjects()[e]},complexity:function(){return this.getObjects().reduce(function(e,t){return e+=typeof t.complexity=="function"?t.complexity():0,e},0)},_restoreObjectsState:function(){return this.objects.forEach(this._restoreObjectState,this),this},_restoreObjectState:function(e){var t=this.get("left"),n=this.get("top"),r=this.getAngle()*(Math.PI/180),i=Math.cos(r)*e.get("top")+Math.sin(r)*e.get("left"),s=-Math.sin(r)*e.get("top")+Math.cos(r)*e.get("left");return e.setAngle(e.getAngle()+this.getAngle()),e.set("left",t+s*this.get("scaleX")),e.set("top",n+i*this.get("scaleY")),e.set("scaleX",e.get("scaleX")*this.get("scaleX")),e.set("scaleY",e.get("scaleY")*this.get("scaleY")),e.setCoords(),e.hideCorners=!1,e.setActive(!1),e.setCoords(),this},destroy:function(){return this._restoreObjectsState()},saveCoords:function(){return this._originalLeft=this.get("left"),this._originalTop=this.get("top"),this},hasMoved:function(){return this._originalLeft!==this.get("left")||this._originalTop!==this.get("top")},setObjectsCoords:function(){return this.forEachObject(function(e){e.setCoords()}),this},activateAllObjects:function(){return this.forEachObject(function(e){e.setActive()}),this},forEachObject:t.StaticCanvas.prototype.forEachObject,_setOpacityIfSame:function(){var e=this.getObjects(),t=e[0]?e[0].get("opacity"):1,n=e.every(function(e){return e.get("opacity")===t});n&&(this.opacity=t)},_calcBounds:function(){var e=[],t=[],n,s,o,u,a,f,l,c=0,h=this.objects.length;for(;ce.x&&i-ne.y},toGrayscale:function(){var e=this.objects.length;while(e--)this.objects[e].toGrayscale();return this},toSVG:function(){var e=[];for(var t=0,n=this.objects.length;t'+e.join("")+""},get:function(e){if(e in u){if(this[e])return this[e];for(var t=0,n=this.objects.length;t'+'"+""},getSrc:function(){return this.getElement().src||this.getElement()._src},toString:function(){return'#'},clone:function(e,t){this.constructor.fromObject(this.toObject(t),e)},applyFilters:function(e){if(this.filters.length===0){this.setElement(this._originalImage),e&&e();return}var t=typeof Buffer!="undefined"&&typeof window=="undefined",n=this._originalImage,r=fabric.util.createCanvasElement(),i=t?new(require("canvas").Image):fabric.document.createElement("img"),s=this;r.width=n.width,r.height=n.height,r.getContext("2d").drawImage(n,0,0,n.width,n.height),this.filters.forEach(function(e){e&&e.applyTo(r)}),i.onload=function(){s._element=i,e&&e(),i.onload=r=n=null},i.width=n.width,i.height=n.height;if(t){var o=r.toDataURL("image/png").substring(22);i.src=new Buffer(o,"base64"),s._element=i,e&&e()}else i.src=r.toDataURL("image/png");return this},_render:function(e){e.drawImage(this._element,-this.width/2,-this.height/2,this.width,this.height)},_resetWidthHeight:function(){var e=this.getElement();this.set("width",e.width),this.set("height",e.height)},_initElement:function(e){this.setElement(fabric.util.getById(e)),fabric.util.addClass(this.getElement(),fabric.Image.CSS_CANVAS)},_initConfig:function(e){e||(e={}),this.setOptions(e),this._setWidthHeight(e)},_initFilters:function(e){e.filters&&e.filters.length&&(this.filters=e.filters.map(function(e){return e&&fabric.Image.filters[e.type].fromObject(e)}))},_setWidthHeight:function(e){this.width="width"in e?e.width:this.getElement().width||0,this.height="height"in e?e.height:this.getElement().height||0},complexity:function(){return 1}}),fabric.Image.CSS_CANVAS="canvas-img",fabric.Image.prototype.getSvgSrc=fabric.Image.prototype.getSrc,fabric.Image.fromObject=function(e,t){var n=fabric.document.createElement("img"),r=e.src;e.width&&(n.width=e.width),e.height&&(n.height=e.height),n.onload=function(){fabric.Image.prototype._initFilters.call(e,e);var r=new fabric.Image(n,e);t&&t(r),n=n.onload=n.onerror=null},n.onerror=function(){fabric.log("Error loading "+n.src),t&&t(null,!0),n=n.onload=n.onerror=null},n.src=r},fabric.Image.fromURL=function(e,t,n){var r=fabric.document.createElement("img");r.onload=function(){t&&t(new fabric.Image(r,n)),r=r.onload=null},r.src=e},fabric.Image.ATTRIBUTE_NAMES="x y width height fill fill-opacity opacity stroke stroke-width transform xlink:href".split(" "),fabric.Image.fromElement=function(e,n,r){r||(r={});var i=fabric.parseAttributes(e,fabric.Image.ATTRIBUTE_NAMES);fabric.Image.fromURL(i["xlink:href"],n,t(i,r))},fabric.Image.async=!0}(typeof exports!="undefined"?exports:this),fabric.util.object.extend(fabric.Object.prototype,{_getAngleValueForStraighten:function(){var e=this.getAngle()%360;return e>0?Math.round((e-1)/90)*90:Math.round(e/90)*90},straighten:function(){return this.setAngle(this._getAngleValueForStraighten()),this},fxStraighten:function(e){e=e||{};var t=function(){},n=e.onComplete||t,r=e.onChange||t,i=this;return fabric.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(e){i.setAngle(e),r()},onComplete:function(){i.setCoords(),n()},onStart:function(){i.setActive(!1)}}),this}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(e){return e.straighten(),this.renderAll(),this},fxStraightenObject:function(e){return e.fxStraighten({onChange:this.renderAll.bind(this)}),this}}),fabric.Image.filters={},fabric.Image.filters.Grayscale=fabric.util.createClass({type:"Grayscale",applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=n.width,s=n.height,o,u,a,f;for(a=0;ao&&f>o&&l>o&&u(a-f)0&&(r[s]=a,r[s+1]=f,r[s+2]=l);t.putImageData(n,0,0)},toJSON:function(){return{type:this.type,color:this.color}}}),fabric.Image.filters.Tint.fromObject=function(e){return new fabric.Image.filters.Tint(e)},fabric.Image.filters.Convolute=fabric.util.createClass({type:"Convolute",initialize:function(e){e||(e={}),this.opaque=e.opaque,this.matrix=e.matrix||[0,0,0,0,1,0,0,0,0];var t=fabric.util.createCanvasElement();this.tmpCtx=t.getContext("2d")},_createImageData:function(e,t){return this.tmpCtx.createImageData(e,t)},applyTo:function(e){var t=this.matrix,n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=Math.round(Math.sqrt(t.length)),s=Math.floor(i/2),o=r.data,u=r.width,a=r.height,f=u,l=a,c=this._createImageData(f,l),h=c.data,p=this.opaque?1:0;for(var d=0;d=0&&N=0&&C'},_render:function(e){typeof Cufon=="undefined"||this.useNative===!0?this._renderViaNative(e):this._renderViaCufon(e)},_renderViaCufon:function(e){var t=Cufon.textOptions||(Cufon.textOptions={});t.left=this.left,t.top=this.top,t.context=e,t.color=this.fill;var n=this._initDummyElementForCufon();this.transform(e),Cufon.replaceElement(n,{engine:"canvas",separate:"none",fontFamily:this.fontFamily,fontWeight:this.fontWeight,textDecoration:this.textDecoration,textShadow:this.textShadow,textAlign:this.textAlign,fontStyle:this.fontStyle,lineHeight:this.lineHeight,strokeStyle:this.strokeStyle,strokeWidth:this.strokeWidth,backgroundColor:this.backgroundColor,textBackgroundColor:this.textBackgroundColor}),this.width=t.width,this.height=t.height,this._totalLineHeight=t.totalLineHeight,this._fontAscent=t.fontAscent,this._boundaries=t.boundaries,this._shadowOffsets=t.shadowOffsets,this._shadows=t.shadows||[],n=null,this.setCoords()},_renderViaNative:function(e){this.transform(e),this._setTextStyles(e);var t=this.text.split(/\r?\n/);this.width=this._getTextWidth(e,t),this.height=this._getTextHeight(e,t),this._renderTextBackground(e,t),this.textAlign!=="left"&&this.textAlign!=="justify"&&(e.save(),e.translate(this.textAlign==="center"?this.width/2:this.width,0)),this._setTextShadow(e),this._renderTextFill(e,t),this.textShadow&&e.restore(),this._renderTextStroke(e,t),this.textAlign!=="left"&&this.textAlign!=="justify"&&e.restore(),this._renderTextDecoration(e,t),this._setBoundaries(e,t),this._totalLineHeight=0,this.setCoords()},_setBoundaries:function(e,t){this._boundaries=[];for(var n=0,r=t.length;nn&&(n=s)}return n},_setTextShadow:function(e){if(this.textShadow){var t=/\s+(-?\d+)(?:px)?\s+(-?\d+)(?:px)?\s+(\d+)(?:px)?\s*/,n=this.textShadow,r=t.exec(this.textShadow),i=n.replace(t,"");e.save(),e.shadowColor=i,e.shadowOffsetX=parseInt(r[1],10),e.shadowOffsetY=parseInt(r[2],10),e.shadowBlur=parseInt(r[3],10),this._shadows=[{blur:e.shadowBlur,color:e.shadowColor,offX:e.shadowOffsetX,offY:e.shadowOffsetY}],this._shadowOffsets=[[parseInt(e.shadowOffsetX,10),parseInt(e.shadowOffsetY,10)]]}},_drawTextLine:function(e,t,n,r,i){if(this.textAlign!=="justify"){t[e](n,r,i);return}var s=t.measureText(n).width,o=this.width;if(o>s){var u=n.split(/\s+/),a=t.measureText(n.replace(/\s+/g,"")).width,f=o-a,l=u.length-1,c=f/l,h=0;for(var p=0,d=u.length;p-1&&i(this.fontSize),this.textDecoration.indexOf("line-through")>-1&&i(this.fontSize/2),this.textDecoration.indexOf("overline")>-1&&i(0)},_getFontDeclaration:function(){return[this.fontStyle,this.fontWeight,this.fontSize+"px",t.isLikelyNode?'"'+this.fontFamily+'"':this.fontFamily].join(" ")},_initDummyElementForCufon:function(){var e=t.document.createElement("pre"),n=t.document.createElement("div");return n.appendChild(e),typeof G_vmlCanvasManager=="undefined"?e.innerHTML=this.text:e.innerText=this.text.replace(/\r?\n/gi,"\r"),e.style.fontSize=this.fontSize+"px",e.style.letterSpacing="normal",e},render:function(e,t){e.save(),this._render(e),!t&&this.active&&(this.drawBorders(e),this.hideCorners||this.drawCorners(e)),e.restore()},toObject:function(e){return n(this.callSuper("toObject",e),{text:this.text,fontSize:this.fontSize,fontWeight:this.fontWeight,fontFamily:this.fontFamily,fontStyle:this.fontStyle,lineHeight:this.lineHeight,textDecoration:this.textDecoration,textShadow:this.textShadow,textAlign:this.textAlign,path:this.path,strokeStyle:this.strokeStyle,strokeWidth:this.strokeWidth,backgroundColor:this.backgroundColor,textBackgroundColor:this.textBackgroundColor,useNative:this.useNative})},toSVG:function(){var e=this.text.split(/\r?\n/),t=this.useNative?this.fontSize*this.lineHeight:-this._fontAscent-this._fontAscent/5*this.lineHeight,n=-(this.width/2),r=this.useNative?this.fontSize-1:this.height/2-e.length*this.fontSize-this._totalLineHeight,s=this._getSVGTextAndBg(t,n,e),o=this._getSVGShadows(t,e);return r+=this._fontAscent?this._fontAscent/5*this.lineHeight:0,['',s.textBgRects.join(""),"',o.join(""),s.textSpans.join(""),"",""].join("")},_getSVGShadows:function(e,n){var r=[],s,o,u,a,f=1;if(!this._shadows||!this._boundaries)return r;for(s=0,u=this._shadows.length;s",t.util.string.escapeXml(n[o]),""),f=1}else f++;return r},_getSVGTextAndBg:function(e,n,r){var s=[],o=[],u,a,f,l=1;this.backgroundColor&&this._boundaries&&o.push("');for(u=0,f=r.length;u",t.util.string.escapeXml(r[u]),""),l=1):l++;if(!this.textBackgroundColor||!this._boundaries)continue;o.push("')}return{textSpans:s,textBgRects:o}},_getFillAttributes:function(e){var n=e?new t.Color(e):"";return!n||!n.getSource()||n.getAlpha()===1?'fill="'+e+'"':'opacity="'+n.getAlpha()+'" fill="'+n.setAlpha(1).toRgb()+'"'},setColor:function(e){return this.set("fill",e),this},setFontsize:function(e){return this.set("fontSize",e),this._initDimensions(),this.setCoords(),this},getText:function(){return this.text},setText:function(e){return this.set("text",e),this._initDimensions(),this.setCoords(),this},_set:function(e,t){e==="fontFamily"&&this.path&&(this.path=this.path.replace(/(.*?)([^\/]*)(\.font\.js)/,"$1"+t+"$3")),this.callSuper("_set",e,t)}}),t.Text.ATTRIBUTE_NAMES="x y fill fill-opacity opacity stroke stroke-width transform font-family font-style font-weight font-size text-decoration".split(" "),t.Text.fromObject=function(e){return new t.Text(e.text,r(e))},t.Text.fromElement=function(e,n){if(!e)return null;var r=t.parseAttributes(e,t.Text.ATTRIBUTE_NAMES);n=t.util.object.extend(n?t.util.object.clone(n):{},r);var i=new t.Text(e.textContent,n);return i.set({left:i.getLeft()+i.getWidth()/2,top:i.getTop()-i.getHeight()/2}),i}}(typeof exports!="undefined"?exports:this),function(){function request(e,t,n){var r=URL.parse(e),i=HTTP.createClient(r.port,r.hostname),s=i.request("GET",r.pathname,{host:r.hostname});i.addListener("error",function(e){e.errno===process.ECONNREFUSED?fabric.log("ECONNREFUSED: connection refused to "+i.host+":"+i.port):fabric.log(e.message)}),s.end(),s.on("response",function(e){var r="";t&&e.setEncoding(t),e.on("end",function(){n(r)}),e.on("data",function(t){e.statusCode===200&&(r+=t)})})}if(typeof document!="undefined"&&typeof window!="undefined")return;var DOMParser=(new require("xmldom")).DOMParser,URL=require("url"),HTTP=require("http"),Canvas=require("canvas"),Image=require("canvas").Image;fabric.util.loadImage=function(e,t,n){var r=new Image;e&&e.indexOf("data")===0?(r.src=r._src=e,t&&t.call(n,r)):e&&request(e,"binary",function(i){r.src=new Buffer(i,"binary"),r._src=e,t&&t.call(n,r)})},fabric.loadSVGFromURL=function(e,t){e=e.replace(/^\n\s*/,"").replace(/\?.*$/,"").trim(),request(e,"",function(e){fabric.loadSVGFromString(e,t)})},fabric.loadSVGFromString=function(e,t){var n=(new DOMParser).parseFromString(e);fabric.parseSVGDocument(n.documentElement,function(e,n){t(e,n)})},fabric.util.getScript=function(url,callback){request(url,"",function(body){eval(body),callback&&callback()})},fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){var r=new fabric.Image(n);r._initConfig(e),r._initFilters(e),t(r)})},fabric.createCanvasForNode=function(e,t){var n=fabric.document.createElement("canvas"),r=new Canvas(e||600,t||600);n.style={},n.width=r.width,n.height=r.height;var i=fabric.Canvas||fabric.StaticCanvas,s=new i(n);return s.contextContainer=r.getContext("2d"),s.nodeCanvas=r,s},fabric.StaticCanvas.prototype.createPNGStream=function(){return this.nodeCanvas.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(e){return this.nodeCanvas.createJPEGStream(e)};var origSetWidth=fabric.StaticCanvas.prototype.setWidth;fabric.StaticCanvas.prototype.setWidth=function(e){return origSetWidth.call(this),this.nodeCanvas.width=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setWidth=fabric.StaticCanvas.prototype.setWidth);var origSetHeight=fabric.StaticCanvas.prototype.setHeight;fabric.StaticCanvas.prototype.setHeight=function(e){return origSetHeight.call(this),this.nodeCanvas.height=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setHeight=fabric.StaticCanvas.prototype.setHeight)}();
+>>>>>>> master
diff --git a/lib/aligning_guidelines.js b/lib/aligning_guidelines.js
index a4dad1de..289240b9 100644
--- a/lib/aligning_guidelines.js
+++ b/lib/aligning_guidelines.js
@@ -6,7 +6,6 @@
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
- canvasHeight = canvas.getHeight(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
@@ -57,11 +56,16 @@ function initAligningGuidelines(canvas) {
var activeObject = e.target,
canvasObjects = canvas.getObjects(),
- activeObjectLeft = activeObject.get('left'),
- activeObjectTop = activeObject.get('top'),
- activeObjectHeight = activeObject.getHeight(),
- activeObjectWidth = activeObject.getWidth(),
- noneInTheRange = true;
+ activeObjectCenter = activeObject.getCenterPoint(),
+ activeObjectLeft = activeObjectCenter.x,
+ activeObjectTop = activeObjectCenter.y,
+ activeObjectHeight = activeObject.getBoundingRectHeight(),
+ activeObjectWidth = activeObject.getBoundingRectWidth(),
+ horizontalInTheRange = false,
+ verticalInTheRange = false,
+ transform = canvas._currentTransform;
+
+ if (!transform) return;
// It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
// but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
@@ -70,14 +74,15 @@ function initAligningGuidelines(canvas) {
if (canvasObjects[i] === activeObject) continue;
- var objectLeft = canvasObjects[i].get('left'),
- objectTop = canvasObjects[i].get('top'),
- objectHeight = canvasObjects[i].getHeight(),
- objectWidth = canvasObjects[i].getWidth();
+ var objectCenter = canvasObjects[i].getCenterPoint(),
+ objectLeft = objectCenter.x,
+ objectTop = objectCenter.y,
+ objectHeight = canvasObjects[i].getBoundingRectHeight(),
+ objectWidth = canvasObjects[i].getBoundingRectWidth();
// snap by the horizontal center line
if (isInRange(objectLeft, activeObjectLeft)) {
- noneInTheRange = false;
+ verticalInTheRange = true;
verticalLines.push({
x: objectLeft,
y1: (objectTop < activeObjectTop)
@@ -87,12 +92,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
- activeObject.set('left', objectLeft);
+ activeObject.setPositionByOrigin(new fabric.Point(objectLeft, activeObjectTop), transform.originX, transform.originY);
}
// snap by the left edge
if (isInRange(objectLeft - objectWidth / 2, activeObjectLeft - activeObjectWidth / 2)) {
- noneInTheRange = false;
+ verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1: (objectTop < activeObjectTop)
@@ -102,12 +107,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
- activeObject.set('left', objectLeft - objectWidth / 2 + activeObjectWidth / 2);
+ activeObject.setPositionByOrigin(new fabric.Point(objectLeft - objectWidth / 2 + activeObjectWidth / 2, activeObjectTop), transform.originX, transform.originY);
}
// snap by the right edge
if (isInRange(objectLeft + objectWidth / 2, activeObjectLeft + activeObjectWidth / 2)) {
- noneInTheRange = false;
+ verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1: (objectTop < activeObjectTop)
@@ -117,12 +122,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectTop + activeObjectHeight / 2 + aligningLineOffset)
: (activeObjectTop - activeObjectHeight / 2 - aligningLineOffset)
});
- activeObject.set('left', objectLeft + objectWidth / 2 - activeObjectWidth / 2);
+ activeObject.setPositionByOrigin(new fabric.Point(objectLeft + objectWidth / 2 - activeObjectWidth / 2, activeObjectTop), transform.originX, transform.originY);
}
// snap by the vertical center line
if (isInRange(objectTop, activeObjectTop)) {
- noneInTheRange = false;
+ horizontalInTheRange = true;
horizontalLines.push({
y: objectTop,
x1: (objectLeft < activeObjectLeft)
@@ -132,12 +137,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
- activeObject.set('top', objectTop);
+ activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop), transform.originX, transform.originY);
}
// snap by the top edge
if (isInRange(objectTop - objectHeight / 2, activeObjectTop - activeObjectHeight / 2)) {
- noneInTheRange = false;
+ horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1: (objectLeft < activeObjectLeft)
@@ -147,12 +152,12 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
- activeObject.set('top', objectTop - objectHeight / 2 + activeObjectHeight / 2);
+ activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop - objectHeight / 2 + activeObjectHeight / 2), transform.originX, transform.originY);
}
// snap by the bottom edge
if (isInRange(objectTop + objectHeight / 2, activeObjectTop + activeObjectHeight / 2)) {
- noneInTheRange = false;
+ horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1: (objectLeft < activeObjectLeft)
@@ -162,13 +167,21 @@ function initAligningGuidelines(canvas) {
? (activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset)
: (activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset)
});
- activeObject.set('top', objectTop + objectHeight / 2 - activeObjectHeight / 2);
+ activeObject.setPositionByOrigin(new fabric.Point(activeObjectLeft, objectTop + objectHeight / 2 - activeObjectHeight / 2), transform.originX, transform.originY);
}
}
- if (noneInTheRange) {
- verticalLines.length = horizontalLines.length = 0;
+ if (!horizontalInTheRange) {
+ horizontalLines.length = 0;
}
+
+ if (!verticalInTheRange) {
+ verticalLines.length = 0;
+ }
+ });
+
+ canvas.on('before:render', function() {
+ canvas.clearContext(canvas.contextTop);
});
canvas.on('after:render', function() {
@@ -184,4 +197,4 @@ function initAligningGuidelines(canvas) {
verticalLines.length = horizontalLines.length = 0;
canvas.renderAll();
});
-}
\ No newline at end of file
+}
diff --git a/lib/centering_guidelines.js b/lib/centering_guidelines.js
index c23f7bbe..91d6f965 100644
--- a/lib/centering_guidelines.js
+++ b/lib/centering_guidelines.js
@@ -48,19 +48,24 @@ function initCenteringGuidelines(canvas) {
isInHorizontalCenter;
canvas.on('object:moving', function(e) {
- object = e.target;
+ var object = e.target,
+ objectCenter = object.getCenterPoint(),
+ transform = canvas._currentTransform;
- isInVerticalCenter = object.get('left') in canvasWidthCenterMap,
- isInHorizontalCenter = object.get('top') in canvasHeightCenterMap;
+ if (!transform) return;
- if (isInHorizontalCenter) {
- object.set('top', canvasHeightCenter);
- }
- if (isInVerticalCenter) {
- object.set('left', canvasWidthCenter);
+ isInVerticalCenter = objectCenter.x in canvasWidthCenterMap,
+ isInHorizontalCenter = objectCenter.y in canvasHeightCenterMap;
+
+ if (isInHorizontalCenter || isInVerticalCenter) {
+ object.setPositionByOrigin(new fabric.Point((isInVerticalCenter ? canvasWidthCenter : objectCenter.x), (isInHorizontalCenter ? canvasHeightCenter : objectCenter.y)), transform.originX, transform.originY);
}
});
+ canvas.on('before:render', function() {
+ canvas.clearContext(canvas.contextTop);
+ });
+
canvas.on('after:render', function() {
if (isInVerticalCenter) {
showVerticalCenterLine();
diff --git a/lib/yuicompressor-2.4.2.jar b/lib/yuicompressor-2.4.2.jar
deleted file mode 100644
index c29470bd..00000000
Binary files a/lib/yuicompressor-2.4.2.jar and /dev/null differ
diff --git a/package.json b/package.json
index 0549acd8..80affc06 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "fabric",
"description": "Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.",
- "version": "1.0.0",
+ "version": "1.0.6",
"author": "Juriy Zaytsev ",
"keywords": ["canvas", "graphic", "graphics", "SVG", "node-canvas", "parser", "HTML5", "object model"],
"repository": "git://github.com/kangax/fabric.js",
@@ -10,19 +10,19 @@
"url": "http://github.com/kangax/fabric.js/raw/master/LICENSE"
}],
"scripts": {
- "build": "node build.js modules=ALL exclude=json,cufon",
+ "build": "node build.js modules=ALL exclude=json,cufon,gestures",
"test": "node test.js"
},
"dependencies": {
- "canvas": "~0.13.0",
+ "canvas": "~1.0.0",
"jsdom": ">=0.2.3",
"xmldom": ">=0.1.7"
},
"devDependencies": {
"qunit": "0.5.x",
"jshint": "0.9.x",
- "uglify-js": "1.3.x"
+ "uglify-js": ">=2.0.0"
},
- "engines": { "node": ">=0.4.0 && <0.9.0" },
+ "engines": { "node": ">=0.4.0 && <1.0.0" },
"main": "./dist/all.js"
}
\ No newline at end of file
diff --git a/src/canvas.class.js b/src/canvas.class.js
index e7d1ab29..ef2dd065 100644
--- a/src/canvas.class.js
+++ b/src/canvas.class.js
@@ -2,22 +2,8 @@
var extend = fabric.util.object.extend,
getPointer = fabric.util.getPointer,
- addListener = fabric.util.addListener,
- removeListener = fabric.util.removeListener,
degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees,
- cursorMap = {
- 'tr': 'ne-resize',
- 'br': 'se-resize',
- 'bl': 'sw-resize',
- 'tl': 'nw-resize',
- 'ml': 'w-resize',
- 'mt': 'n-resize',
- 'mr': 'e-resize',
- 'mb': 's-resize'
- },
-
- sqrt = Math.sqrt,
atan2 = Math.atan2,
abs = Math.abs,
min = Math.min,
@@ -178,418 +164,6 @@
this.calcOffset();
},
- /**
- * Adds mouse listeners to canvas
- * @method _initEvents
- * @private
- * See configuration documentation for more details.
- */
- _initEvents: function () {
- var _this = this;
-
- this._onMouseDown = function (e) {
- _this.__onMouseDown(e);
-
- addListener(fabric.document, 'mouseup', _this._onMouseUp);
- fabric.isTouchSupported && addListener(fabric.document, 'touchend', _this._onMouseUp);
-
- addListener(fabric.document, 'mousemove', _this._onMouseMove);
- fabric.isTouchSupported && addListener(fabric.document, 'touchmove', _this._onMouseMove);
-
- removeListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
- fabric.isTouchSupported && removeListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
- };
-
- this._onMouseUp = function (e) {
- _this.__onMouseUp(e);
-
- removeListener(fabric.document, 'mouseup', _this._onMouseUp);
- fabric.isTouchSupported && removeListener(fabric.document, 'touchend', _this._onMouseUp);
-
- removeListener(fabric.document, 'mousemove', _this._onMouseMove);
- fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', _this._onMouseMove);
-
- addListener(_this.upperCanvasEl, 'mousemove', _this._onMouseMove);
- fabric.isTouchSupported && addListener(_this.upperCanvasEl, 'touchmove', _this._onMouseMove);
- };
-
- this._onMouseMove = function (e) {
- e.preventDefault && e.preventDefault();
- _this.__onMouseMove(e);
- };
-
- this._onResize = function () {
- _this.calcOffset();
- };
-
-
- addListener(fabric.window, 'resize', this._onResize);
-
- if (fabric.isTouchSupported) {
- addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
- addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
-
- if (typeof Event !== 'undefined' && 'add' in Event) {
- Event.add(this.upperCanvasEl, 'gesture', function(e, s) {
- _this.__onTransformGesture(e, s);
- });
- }
- }
- else {
- addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
- addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
- }
- },
-
- /**
- * Method that defines the actions when mouse is released on canvas.
- * The method resets the currentTransform parameters, store the image corner
- * position in the image object and render the canvas on top.
- * @method __onMouseUp
- * @param {Event} e Event object fired on mouseup
- *
- */
- __onMouseUp: function (e) {
-
- var target;
-
- if (this.isDrawingMode && this._isCurrentlyDrawing) {
- this._isCurrentlyDrawing = false;
- this.freeDrawingBrush.onMouseUp();
- this.fire('mouse:up', { e: e });
- return;
- }
-
- if (this._currentTransform) {
-
- var transform = this._currentTransform;
-
- target = transform.target;
- if (target._scaling) {
- target._scaling = false;
- }
-
- // determine the new coords everytime the image changes its position
- var i = this._objects.length;
- while (i--) {
- this._objects[i].setCoords();
- }
-
- target.isMoving = false;
-
- // 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');
- }
-
- if (this._previousOriginX) {
- this._adjustPosition(this._currentTransform.target, this._previousOriginX);
- this._previousOriginX = null;
- }
- }
-
- this._currentTransform = null;
-
- if (this._groupSelector) {
- // group selection was completed, determine its bounds
- this._findSelectedObjects(e);
- }
- var activeGroup = this.getActiveGroup();
- if (activeGroup) {
- activeGroup.setObjectsCoords();
- activeGroup.set('isMoving', false);
- this._setCursor(this.defaultCursor);
- }
-
- // clear selection
- this._groupSelector = null;
- this.renderAll();
-
- this._setCursorFromEvent(e, target);
-
- // fix for FF
- this._setCursor('');
-
- var _this = this;
- setTimeout(function () {
- _this._setCursorFromEvent(e, target);
- }, 50);
-
- this.fire('mouse:up', { target: target, e: e });
- target && target.fire('mouseup', { e: e });
- },
-
- /**
- * Method that defines the actions when mouse is clic ked on canvas.
- * The method inits the currentTransform parameters and renders all the
- * canvas so the current image can be placed on the top canvas and the rest
- * in on the container one.
- * @method __onMouseDown
- * @param e {Event} Event object fired on mousedown
- *
- */
- __onMouseDown: function (e) {
-
- var pointer;
-
- // accept only left clicks
- var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
- if (!isLeftClick && !fabric.isTouchSupported) return;
-
- if (this.isDrawingMode) {
- pointer = this.getPointer(e);
-
- this._isCurrentlyDrawing = true;
- this.discardActiveObject().renderAll();
-
- this.freeDrawingBrush.onMouseDown(pointer);
- this.fire('mouse:down', { e: e });
- return;
- }
-
- // ignore if some object is being transformed at this moment
- if (this._currentTransform) return;
-
- var target = this.findTarget(e), corner;
- pointer = this.getPointer(e);
-
- if (this._shouldClearSelection(e)) {
- this._groupSelector = {
- ex: pointer.x,
- ey: pointer.y,
- top: 0,
- left: 0
- };
- this.deactivateAllWithDispatch();
- }
- else {
- // determine if it's a drag or rotate case
- this.stateful && target.saveState();
-
- if ((corner = target._findTargetCorner(e, this._offset))) {
- this.onBeforeScaleRotate(target);
- }
-
- if (this._shouldHandleGroupLogic(e, target)) {
- this._handleGroupLogic(e, target);
- target = this.getActiveGroup();
- }
- else {
- if (target !== this.getActiveGroup()) {
- this.deactivateAll();
- }
- this.setActiveObject(target, e);
- }
-
- this._setupCurrentTransform(e, target);
- }
- // we must renderAll so that active image is placed on the top canvas
- this.renderAll();
-
- this.fire('mouse:down', { target: target, e: e });
- target && target.fire('mousedown', { e: e });
-
- // center origin when rotating
- if (corner === 'mtr') {
- this._previousOriginX = this._currentTransform.target.originX;
- this._adjustPosition(this._currentTransform.target, 'center');
- this._currentTransform.left = this._currentTransform.target.left;
- this._currentTransform.top = this._currentTransform.target.top;
- }
- },
-
- /**
- * @method _shouldHandleGroupLogic
- * @param e {Event}
- * @param target {fabric.Object}
- * @return {Boolean}
- */
- _shouldHandleGroupLogic: function(e, target) {
- var activeObject = this.getActiveObject();
- return e.shiftKey &&
- (this.getActiveGroup() || (activeObject && activeObject !== target))
- && this.selection;
- },
-
- /**
- * Method that defines the actions when mouse is hovering the canvas.
- * The currentTransform parameter will definde whether the user is rotating/scaling/translating
- * an image or neither of them (only hovering). A group selection is also possible and would cancel
- * all any other type of action.
- * In case of an image transformation only the top canvas will be rendered.
- * @method __onMouseMove
- * @param e {Event} Event object fired on mousemove
- *
- */
- __onMouseMove: function (e) {
-
- var target, pointer;
-
- if (this.isDrawingMode) {
- if (this._isCurrentlyDrawing) {
- pointer = this.getPointer(e);
- this.freeDrawingBrush.onMouseMove(pointer);
- }
- this.upperCanvasEl.style.cursor = this.freeDrawingCursor;
- this.fire('mouse:move', { e: e });
- return;
- }
-
- var groupSelector = this._groupSelector;
-
- // We initially clicked in an empty area, so we draw a box for multiple selection.
- if (groupSelector !== null) {
- pointer = getPointer(e);
-
- groupSelector.left = pointer.x - this._offset.left - groupSelector.ex;
- groupSelector.top = pointer.y - this._offset.top - groupSelector.ey;
- this.renderTop();
- }
- else if (!this._currentTransform) {
-
- // alias style to elimintate unnecessary lookup
- var style = this.upperCanvasEl.style;
-
- // Here we are hovering the canvas then we will determine
- // what part of the pictures we are hovering to change the caret symbol.
- // We won't do that while dragging or rotating in order to improve the
- // performance.
- target = this.findTarget(e);
-
- if (!target) {
- // image/text was hovered-out from, we remove its borders
- for (var i = this._objects.length; i--; ) {
- if (this._objects[i] && !this._objects[i].active) {
- this._objects[i].setActive(false);
- }
- }
- style.cursor = this.defaultCursor;
- }
- else {
- // set proper cursor
- this._setCursorFromEvent(e, target);
- }
- }
- else {
- // object is being transformed (scaled/rotated/moved/etc.)
- pointer = getPointer(e);
-
- var x = pointer.x,
- y = pointer.y;
-
- this._currentTransform.target.isMoving = true;
-
- var t = this._currentTransform, reset = false;
- if (
- (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY')
- &&
- (
- // Switch from a normal resize to center-based
- (e.altKey && (t.originX !== 'center' || t.originY !== 'center'))
- ||
- // Switch from center-based resize to normal one
- (!e.altKey && t.originX === 'center' && t.originY === 'center')
- )
- ) {
- this._resetCurrentTransform(e);
- reset = true;
- }
-
- if (this._currentTransform.action === 'rotate') {
- this._rotateObject(x, y);
-
- this.fire('object:rotating', {
- target: this._currentTransform.target
- });
- this._currentTransform.target.fire('rotating');
- }
- else if (this._currentTransform.action === 'scale') {
- // rotate object only if shift key is not pressed
- // and if it is not a group we are transforming
-
- // TODO
- /*if (!e.shiftKey) {
- this._rotateObject(x, y);
-
- this.fire('object:rotating', {
- target: this._currentTransform.target,
- e: e
- });
- this._currentTransform.target.fire('rotating');
- }*/
-
- // if (!this._currentTransform.target.hasRotatingPoint) {
- // this._scaleObject(x, y);
- // this.fire('object:scaling', {
- // target: this._currentTransform.target
- // });
- // this._currentTransform.target.fire('scaling');
- // }
-
- if (e.shiftKey || this.uniScaleTransform) {
- this._currentTransform.currentAction = 'scale';
- this._scaleObject(x, y);
- }
- else {
- if (!reset && t.currentAction === 'scale') {
- // Switch from a normal resize to proportional
- this._resetCurrentTransform(e);
- }
-
- this._currentTransform.currentAction = 'scaleEqually';
- this._scaleObject(x, y, 'equally');
- }
-
- this.fire('object:scaling', {
- target: this._currentTransform.target,
- e: e
- });
- }
- // else if (this._currentTransform.action === 'scale') {
- // this._scaleObject(x, y);
- // this.fire('object:scaling', {
- // target: this._currentTransform.target
- // });
- // this._currentTransform.target.fire('scaling');
- // }
- else if (this._currentTransform.action === 'scaleX') {
- this._scaleObject(x, y, 'x');
-
- this.fire('object:scaling', {
- target: this._currentTransform.target,
- e: e
- });
- this._currentTransform.target.fire('scaling', { e: e });
- }
- else if (this._currentTransform.action === 'scaleY') {
- this._scaleObject(x, y, 'y');
-
- this.fire('object:scaling', {
- target: this._currentTransform.target,
- e: e
- });
- this._currentTransform.target.fire('scaling', { e: e });
- }
- else {
- this._translateObject(x, y);
-
- this.fire('object:moving', {
- target: this._currentTransform.target,
- e: e
- });
-
- this._setCursor(this.moveCursor);
-
- this._currentTransform.target.fire('moving', { e: e });
- }
- // only commit here. when we are actually moving the pictures
- this.renderAll();
- }
- this.fire('mouse:move', { target: target, e: e });
- target && target.fire('mousemove', { e: e });
- },
-
/**
* Resets the current transform to its original values and chooses the type of resizing based on the event
* @method _resetCurrentTransform
@@ -597,6 +171,7 @@
*/
_resetCurrentTransform: function(e) {
var t = this._currentTransform;
+
t.target.set('scaleX', t.original.scaleX);
t.target.set('scaleY', t.original.scaleY);
t.target.set('left', t.original.left);
@@ -752,7 +327,7 @@
_setupCurrentTransform: function (e, target) {
var action = 'drag',
corner,
- pointer = getPointer(e);
+ pointer = getPointer(e, target.canvas.upperCanvasEl);
corner = target._findTargetCorner(e, this._offset);
if (corner) {
@@ -818,6 +393,19 @@
this._resetCurrentTransform(e);
},
+ /**
+ * @method _shouldHandleGroupLogic
+ * @param e {Event}
+ * @param target {fabric.Object}
+ * @return {Boolean}
+ */
+ _shouldHandleGroupLogic: function(e, target) {
+ var activeObject = this.getActiveObject();
+ return e.shiftKey &&
+ (this.getActiveGroup() || (activeObject && activeObject !== target))
+ && this.selection;
+ },
+
/**
* @private
* @method _handleGroupLogic
@@ -1019,43 +607,6 @@
target.setAngle(0);
},
- /**
- * Sets the cursor depending on where the canvas is being hovered.
- * Note: very buggy in Opera
- * @method _setCursorFromEvent
- * @param e {Event} Event object
- * @param target {Object} Object that the mouse is hovering, if so.
- */
- _setCursorFromEvent: function (e, target) {
- var s = this.upperCanvasEl.style;
- if (!target) {
- s.cursor = this.defaultCursor;
- return false;
- }
- else {
- var activeGroup = this.getActiveGroup();
- // only show proper corner when group selection is not active
- var corner = !!target._findTargetCorner
- && (!activeGroup || !activeGroup.contains(target))
- && target._findTargetCorner(e, this._offset);
-
- if (!corner) {
- s.cursor = this.hoverCursor;
- }
- else {
- if (corner in cursorMap) {
- s.cursor = cursorMap[corner];
- } else if (corner === 'mtr' && target.hasRotatingPoint) {
- s.cursor = this.rotationCursor;
- } else {
- s.cursor = this.defaultCursor;
- return false;
- }
- }
- }
- return true;
- },
-
/**
* @method _drawSelection
* @private
@@ -1082,13 +633,17 @@
// selection border
if (this.selectionDashArray.length > 1) {
+
var px = groupSelector.ex + STROKE_OFFSET - ((left > 0) ? 0: aleft);
var py = groupSelector.ey + STROKE_OFFSET - ((top > 0) ? 0: atop);
+
ctx.beginPath();
- this.drawDashedLine(ctx, px, py, px+aleft, py, this.selectionDashArray);
- this.drawDashedLine(ctx, px, py+atop-1, px+aleft, py+atop-1, this.selectionDashArray);
- this.drawDashedLine(ctx, px, py, px, py+atop, this.selectionDashArray);
- this.drawDashedLine(ctx, px+aleft-1, py, px+aleft-1, py+atop, this.selectionDashArray);
+
+ 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();
}
@@ -1102,47 +657,6 @@
}
},
- /**
- * Draws a dashed line between two points
- *
- * This method is used to draw dashed line around selection area.
- * See dotted stroke in canvas
- *
- * @method drawDashedLine
- * @param ctx {Canvas} context
- * @param x {Number} start x coordinate
- * @param y {Number} start y coordinate
- * @param x2 {Number} end x coordinate
- * @param y2 {Number} end y coordinate
- * @param da {Array} 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();
- },
-
/**
* @private
* @method _findSelectedObjects
@@ -1199,7 +713,8 @@
if (this.controlsAboveOverlay &&
this.lastRenderedObjectWithControlsAboveOverlay &&
- this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay)) {
+ this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) &&
+ this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(e, this._offset)) {
target = this.lastRenderedObjectWithControlsAboveOverlay;
return target;
}
@@ -1247,7 +762,7 @@
* @return {Object} object with "x" and "y" number values
*/
getPointer: function (e) {
- var pointer = getPointer(e);
+ var pointer = getPointer(e, this.upperCanvasEl);
return {
x: pointer.x - this._offset.left,
y: pointer.y - this._offset.top
@@ -1452,51 +967,6 @@
this.fire('selection:cleared');
}
return this;
- },
-
- /**
- * @private
- * @method _adjustPosition
- * @param obj
- * @param {String} to One of left, center, right
- */
- _adjustPosition: function(obj, to) {
-
- var angle = fabric.util.degreesToRadians(obj.angle);
-
- var hypotHalf = obj.getWidth() / 2;
- var xHalf = Math.cos(angle) * hypotHalf;
- var yHalf = Math.sin(angle) * hypotHalf;
-
- var hypotFull = obj.getWidth();
- var xFull = Math.cos(angle) * hypotFull;
- var yFull = Math.sin(angle) * hypotFull;
-
- if (obj.originX === 'center' && to === 'left' ||
- obj.originX === 'right' && to === 'center') {
- // move half left
- obj.left -= xHalf;
- obj.top -= yHalf;
- }
- else if (obj.originX === 'left' && to === 'center' ||
- obj.originX === 'center' && to === 'right') {
- // move half right
- obj.left += xHalf;
- obj.top += yHalf;
- }
- else if (obj.originX === 'left' && to === 'right') {
- // move full right
- obj.left += xFull;
- obj.top += yFull;
- }
- else if (obj.originX === 'right' && to === 'left') {
- // move full left
- obj.left -= xFull;
- obj.top -= yFull;
- }
-
- obj.setCoords();
- obj.originX = to;
}
};
@@ -1522,4 +992,4 @@
* @constructor
*/
fabric.Element = fabric.Canvas;
-})();
\ No newline at end of file
+})();
diff --git a/src/canvas.animation.js b/src/canvas_animation.mixin.js
similarity index 96%
rename from src/canvas.animation.js
rename to src/canvas_animation.mixin.js
index 7707ad11..e6a32247 100644
--- a/src/canvas.animation.js
+++ b/src/canvas_animation.mixin.js
@@ -1,4 +1,4 @@
-fabric.util.object.extend(fabric.StaticCanvas.prototype, {
+fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ {
/**
* Animation duration (in ms) for fx* methods
diff --git a/src/canvas_events.mixin.js b/src/canvas_events.mixin.js
new file mode 100644
index 00000000..710d83b8
--- /dev/null
+++ b/src/canvas_events.mixin.js
@@ -0,0 +1,475 @@
+(function(){
+
+ var cursorMap = {
+ 'tr': 'ne-resize',
+ 'br': 'se-resize',
+ 'bl': 'sw-resize',
+ 'tl': 'nw-resize',
+ 'ml': 'w-resize',
+ 'mt': 'n-resize',
+ 'mr': 'e-resize',
+ 'mb': 's-resize'
+ },
+ addListener = fabric.util.addListener,
+ removeListener = fabric.util.removeListener,
+ getPointer = fabric.util.getPointer;
+
+ fabric.util.object.extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ {
+
+ /**
+ * Adds mouse listeners to canvas
+ * @method _initEvents
+ * @private
+ * See configuration documentation for more details.
+ */
+ _initEvents: function () {
+ var _this = this;
+
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+ this._onMouseUp = this._onMouseUp.bind(this);
+ this._onResize = this._onResize.bind(this);
+
+ addListener(fabric.window, 'resize', this._onResize);
+
+ if (fabric.isTouchSupported) {
+ addListener(this.upperCanvasEl, 'touchstart', this._onMouseDown);
+ addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
+
+ if (typeof Event !== 'undefined' && 'add' in Event) {
+ Event.add(this.upperCanvasEl, 'gesture', function(e, s) {
+ _this.__onTransformGesture(e, s);
+ });
+ }
+ }
+ else {
+ addListener(this.upperCanvasEl, 'mousedown', this._onMouseDown);
+ addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
+ }
+ },
+
+ /**
+ * @method _onMouseDown
+ * @private
+ */
+ _onMouseDown: function (e) {
+ this.__onMouseDown(e);
+
+ !fabric.isTouchSupported && addListener(fabric.document, 'mouseup', this._onMouseUp);
+ fabric.isTouchSupported && addListener(fabric.document, 'touchend', this._onMouseUp);
+
+ !fabric.isTouchSupported && addListener(fabric.document, 'mousemove', this._onMouseMove);
+ fabric.isTouchSupported && addListener(fabric.document, 'touchmove', this._onMouseMove);
+
+ !fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
+ fabric.isTouchSupported && removeListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
+ },
+
+ /**
+ * @method _onMouseUp
+ * @private
+ */
+ _onMouseUp: function (e) {
+ this.__onMouseUp(e);
+
+ !fabric.isTouchSupported && removeListener(fabric.document, 'mouseup', this._onMouseUp);
+ fabric.isTouchSupported && removeListener(fabric.document, 'touchend', this._onMouseUp);
+
+ !fabric.isTouchSupported && removeListener(fabric.document, 'mousemove', this._onMouseMove);
+ fabric.isTouchSupported && removeListener(fabric.document, 'touchmove', this._onMouseMove);
+
+ !fabric.isTouchSupported && addListener(this.upperCanvasEl, 'mousemove', this._onMouseMove);
+ fabric.isTouchSupported && addListener(this.upperCanvasEl, 'touchmove', this._onMouseMove);
+ },
+
+ /**
+ * @method _onMouseMove
+ * @private
+ */
+ _onMouseMove: function (e) {
+ e.preventDefault && e.preventDefault();
+ this.__onMouseMove(e);
+ },
+
+ /**
+ * @method _onResize
+ * @private
+ */
+ _onResize: function () {
+ this.calcOffset();
+ },
+
+ /**
+ * Method that defines the actions when mouse is released on canvas.
+ * The method resets the currentTransform parameters, store the image corner
+ * position in the image object and render the canvas on top.
+ * @method __onMouseUp
+ * @param {Event} e Event object fired on mouseup
+ *
+ */
+ __onMouseUp: function (e) {
+
+ var target;
+
+ if (this.isDrawingMode && this._isCurrentlyDrawing) {
+ this._isCurrentlyDrawing = false;
+ this.freeDrawingBrush.onMouseUp();
+ this.fire('mouse:up', { e: e });
+ return;
+ }
+
+ if (this._currentTransform) {
+
+ var transform = this._currentTransform;
+
+ target = transform.target;
+ if (target._scaling) {
+ target._scaling = false;
+ }
+
+ // determine the new coords everytime the image changes its position
+ var i = this._objects.length;
+ while (i--) {
+ this._objects[i].setCoords();
+ }
+
+ target.isMoving = false;
+
+ // 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');
+ }
+
+ if (this._previousOriginX) {
+ this._currentTransform.target.adjustPosition(this._previousOriginX);
+ this._previousOriginX = null;
+ }
+ }
+
+ this._currentTransform = null;
+
+ if (this._groupSelector) {
+ // group selection was completed, determine its bounds
+ this._findSelectedObjects(e);
+ }
+ var activeGroup = this.getActiveGroup();
+ if (activeGroup) {
+ activeGroup.setObjectsCoords();
+ activeGroup.set('isMoving', false);
+ this._setCursor(this.defaultCursor);
+ }
+
+ // clear selection
+ this._groupSelector = null;
+ this.renderAll();
+
+ this._setCursorFromEvent(e, target);
+
+ // fix for FF
+ this._setCursor('');
+
+ var _this = this;
+ setTimeout(function () {
+ _this._setCursorFromEvent(e, target);
+ }, 50);
+
+ this.fire('mouse:up', { target: target, e: e });
+ target && target.fire('mouseup', { e: e });
+ },
+
+ /**
+ * Method that defines the actions when mouse is clic ked on canvas.
+ * The method inits the currentTransform parameters and renders all the
+ * canvas so the current image can be placed on the top canvas and the rest
+ * in on the container one.
+ * @method __onMouseDown
+ * @param e {Event} Event object fired on mousedown
+ *
+ */
+ __onMouseDown: function (e) {
+
+ var pointer;
+
+ // accept only left clicks
+ var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
+ if (!isLeftClick && !fabric.isTouchSupported) return;
+
+ if (this.isDrawingMode) {
+ pointer = this.getPointer(e);
+ this._isCurrentlyDrawing = true;
+ this.discardActiveObject().renderAll();
+ this.freeDrawingBrush.onMouseDown(pointer);
+ this.fire('mouse:down', { e: e });
+ return;
+ }
+
+ // ignore if some object is being transformed at this moment
+ if (this._currentTransform) return;
+
+ var target = this.findTarget(e), corner;
+ pointer = this.getPointer(e);
+
+ if (this._shouldClearSelection(e)) {
+ this._groupSelector = {
+ ex: pointer.x,
+ ey: pointer.y,
+ top: 0,
+ left: 0
+ };
+ this.deactivateAllWithDispatch();
+ }
+ else {
+ // determine if it's a drag or rotate case
+ this.stateful && target.saveState();
+
+ if ((corner = target._findTargetCorner(e, this._offset))) {
+ this.onBeforeScaleRotate(target);
+ }
+
+ if (this._shouldHandleGroupLogic(e, target)) {
+ this._handleGroupLogic(e, target);
+ target = this.getActiveGroup();
+ }
+ else {
+ if (target !== this.getActiveGroup()) {
+ this.deactivateAll();
+ }
+ this.setActiveObject(target, e);
+ }
+
+ this._setupCurrentTransform(e, target);
+ }
+ // we must renderAll so that active image is placed on the top canvas
+ this.renderAll();
+
+ this.fire('mouse:down', { target: target, e: e });
+ target && target.fire('mousedown', { e: e });
+
+ // center origin when rotating
+ if (corner === 'mtr') {
+ this._previousOriginX = this._currentTransform.target.originX;
+ this._currentTransform.target.adjustPosition('center');
+ this._currentTransform.left = this._currentTransform.target.left;
+ this._currentTransform.top = this._currentTransform.target.top;
+ }
+ },
+
+ /**
+ * Method that defines the actions when mouse is hovering the canvas.
+ * The currentTransform parameter will definde whether the user is rotating/scaling/translating
+ * an image or neither of them (only hovering). A group selection is also possible and would cancel
+ * all any other type of action.
+ * In case of an image transformation only the top canvas will be rendered.
+ * @method __onMouseMove
+ * @param e {Event} Event object fired on mousemove
+ *
+ */
+ __onMouseMove: function (e) {
+
+ var target, pointer;
+
+ if (this.isDrawingMode) {
+ if (this._isCurrentlyDrawing) {
+ pointer = this.getPointer(e);
+ this.freeDrawingBrush.onMouseMove(pointer);
+ }
+ this.upperCanvasEl.style.cursor = this.freeDrawingCursor;
+ this.fire('mouse:move', { e: e });
+ return;
+ }
+
+ var groupSelector = this._groupSelector;
+
+ // We initially clicked in an empty area, so we draw a box for multiple selection.
+ if (groupSelector !== null) {
+ pointer = getPointer(e, this.upperCanvasEl);
+
+ groupSelector.left = pointer.x - this._offset.left - groupSelector.ex;
+ groupSelector.top = pointer.y - this._offset.top - groupSelector.ey;
+ this.renderTop();
+ }
+ else if (!this._currentTransform) {
+
+ // alias style to elimintate unnecessary lookup
+ var style = this.upperCanvasEl.style;
+
+ // Here we are hovering the canvas then we will determine
+ // what part of the pictures we are hovering to change the caret symbol.
+ // We won't do that while dragging or rotating in order to improve the
+ // performance.
+ target = this.findTarget(e);
+
+ if (!target) {
+ // image/text was hovered-out from, we remove its borders
+ for (var i = this._objects.length; i--; ) {
+ if (this._objects[i] && !this._objects[i].active) {
+ this._objects[i].setActive(false);
+ }
+ }
+ style.cursor = this.defaultCursor;
+ }
+ else {
+ // set proper cursor
+ this._setCursorFromEvent(e, target);
+ }
+ }
+ else {
+ // object is being transformed (scaled/rotated/moved/etc.)
+ pointer = getPointer(e, this.upperCanvasEl);
+
+ var x = pointer.x,
+ y = pointer.y;
+
+ this._currentTransform.target.isMoving = true;
+
+ var t = this._currentTransform, reset = false;
+ if (
+ (t.action === 'scale' || t.action === 'scaleX' || t.action === 'scaleY')
+ &&
+ (
+ // Switch from a normal resize to center-based
+ (e.altKey && (t.originX !== 'center' || t.originY !== 'center'))
+ ||
+ // Switch from center-based resize to normal one
+ (!e.altKey && t.originX === 'center' && t.originY === 'center')
+ )
+ ) {
+ this._resetCurrentTransform(e);
+ reset = true;
+ }
+
+ if (this._currentTransform.action === 'rotate') {
+ this._rotateObject(x, y);
+
+ this.fire('object:rotating', {
+ target: this._currentTransform.target,
+ e: e
+ });
+ this._currentTransform.target.fire('rotating');
+ }
+ else if (this._currentTransform.action === 'scale') {
+ // rotate object only if shift key is not pressed
+ // and if it is not a group we are transforming
+
+ // TODO
+ /*if (!e.shiftKey) {
+ this._rotateObject(x, y);
+
+ this.fire('object:rotating', {
+ target: this._currentTransform.target,
+ e: e
+ });
+ this._currentTransform.target.fire('rotating');
+ }*/
+
+ // if (!this._currentTransform.target.hasRotatingPoint) {
+ // this._scaleObject(x, y);
+ // this.fire('object:scaling', {
+ // target: this._currentTransform.target
+ // });
+ // this._currentTransform.target.fire('scaling');
+ // }
+
+ if (e.shiftKey || this.uniScaleTransform) {
+ this._currentTransform.currentAction = 'scale';
+ this._scaleObject(x, y);
+ }
+ else {
+ if (!reset && t.currentAction === 'scale') {
+ // Switch from a normal resize to proportional
+ this._resetCurrentTransform(e);
+ }
+
+ this._currentTransform.currentAction = 'scaleEqually';
+ this._scaleObject(x, y, 'equally');
+ }
+
+ this.fire('object:scaling', {
+ target: this._currentTransform.target,
+ e: e
+ });
+ }
+ // else if (this._currentTransform.action === 'scale') {
+ // this._scaleObject(x, y);
+ // this.fire('object:scaling', {
+ // target: this._currentTransform.target
+ // });
+ // this._currentTransform.target.fire('scaling');
+ // }
+ else if (this._currentTransform.action === 'scaleX') {
+ this._scaleObject(x, y, 'x');
+
+ this.fire('object:scaling', {
+ target: this._currentTransform.target,
+ e: e
+ });
+ this._currentTransform.target.fire('scaling', { e: e });
+ }
+ else if (this._currentTransform.action === 'scaleY') {
+ this._scaleObject(x, y, 'y');
+
+ this.fire('object:scaling', {
+ target: this._currentTransform.target,
+ e: e
+ });
+ this._currentTransform.target.fire('scaling', { e: e });
+ }
+ else {
+ this._translateObject(x, y);
+
+ this.fire('object:moving', {
+ target: this._currentTransform.target,
+ e: e
+ });
+
+ this._setCursor(this.moveCursor);
+
+ this._currentTransform.target.fire('moving', { e: e });
+ }
+ // only commit here. when we are actually moving the pictures
+ this.renderAll();
+ }
+ this.fire('mouse:move', { target: target, e: e });
+ target && target.fire('mousemove', { e: e });
+ },
+ /**
+ * Sets the cursor depending on where the canvas is being hovered.
+ * Note: very buggy in Opera
+ * @method _setCursorFromEvent
+ * @param e {Event} Event object
+ * @param target {Object} Object that the mouse is hovering, if so.
+ */
+ _setCursorFromEvent: function (e, target) {
+ var s = this.upperCanvasEl.style;
+ if (!target) {
+ s.cursor = this.defaultCursor;
+ return false;
+ }
+ else {
+ var activeGroup = this.getActiveGroup();
+ // only show proper corner when group selection is not active
+ var corner = target._findTargetCorner
+ && (!activeGroup || !activeGroup.contains(target))
+ && target._findTargetCorner(e, this._offset);
+
+ if (!corner) {
+ s.cursor = this.hoverCursor;
+ }
+ else {
+ if (corner in cursorMap) {
+ s.cursor = cursorMap[corner];
+ }
+ else if (corner === 'mtr' && target.hasRotatingPoint) {
+ s.cursor = this.rotationCursor;
+ }
+ else {
+ s.cursor = this.defaultCursor;
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ });
+})();
diff --git a/src/canvas.gestures.js b/src/canvas_gestures.mixin.js
similarity index 96%
rename from src/canvas.gestures.js
rename to src/canvas_gestures.mixin.js
index a7728bdf..56c997f0 100644
--- a/src/canvas.gestures.js
+++ b/src/canvas_gestures.mixin.js
@@ -3,7 +3,7 @@
var degreesToRadians = fabric.util.degreesToRadians,
radiansToDegrees = fabric.util.radiansToDegrees;
- fabric.util.object.extend(fabric.Canvas.prototype, {
+ fabric.util.object.extend(fabric.Canvas.prototype, /** @scope fabric.Canvas.prototype */ {
/**
* Method that defines actions when an Event.js gesture is detected on an object. Currently only supports
diff --git a/src/canvas.serialization.js b/src/canvas_serialization.mixin.js
similarity index 79%
rename from src/canvas.serialization.js
rename to src/canvas_serialization.mixin.js
index d88ec78a..3f224c1b 100644
--- a/src/canvas.serialization.js
+++ b/src/canvas_serialization.mixin.js
@@ -1,4 +1,4 @@
-fabric.util.object.extend(fabric.StaticCanvas.prototype, {
+fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @scope fabric.StaticCanvas.prototype */ {
/**
* Populates canvas with data from the specified dataless JSON
@@ -13,9 +13,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
*/
loadFromDatalessJSON: function (json, callback) {
- if (!json) {
- return;
- }
+ if (!json) return;
// serialize if it wasn't already
var serialized = (typeof json === 'string')
@@ -26,9 +24,10 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
this.clear();
- // TODO: test this
- this.backgroundColor = serialized.background;
- this._enlivenDatalessObjects(serialized.objects, callback);
+ var _this = this;
+ this._enlivenDatalessObjects(serialized.objects, function() {
+ _this._setBgOverlayImages(serialized, callback);
+ });
},
/**
@@ -37,6 +36,9 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
* @param {Function} callback
*/
_enlivenDatalessObjects: function (objects, callback) {
+ var _this = this,
+ numLoadedObjects = 0,
+ numTotalObjects = objects.length;
/** @ignore */
function onObjectLoaded(object, index) {
@@ -128,10 +130,6 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
}
}
- var _this = this,
- numLoadedObjects = 0,
- numTotalObjects = objects.length;
-
if (numTotalObjects === 0 && callback) {
callback();
}
@@ -140,7 +138,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
objects.forEach(loadObject, this);
}
catch(e) {
- fabric.log(e.message);
+ fabric.log(e);
}
},
@@ -163,55 +161,72 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, {
? JSON.parse(json)
: json;
- if (!serialized || (serialized && !serialized.objects)) return;
-
- this.clear();
-
var _this = this;
this._enlivenObjects(serialized.objects, function () {
- _this.backgroundColor = serialized.background;
- var backgroundImageLoaded, overlayImageLoaded;
-
- if (serialized.backgroundImage) {
- _this.setBackgroundImage(serialized.backgroundImage, function() {
-
- _this.backgroundImageOpacity = serialized.backgroundImageOpacity;
- _this.backgroundImageStretch = serialized.backgroundImageStretch;
-
- _this.renderAll();
- backgroundImageLoaded = true;
-
- callback && overlayImageLoaded && callback();
- });
- }
- else {
- backgroundImageLoaded = true;
- }
-
- if (serialized.overlayImage) {
- _this.setOverlayImage(serialized.overlayImage, function() {
-
- _this.overlayImageLeft = serialized.overlayImageLeft || 0;
- _this.overlayImageTop = serialized.overlayImageTop || 0;
-
- _this.renderAll();
- overlayImageLoaded = true;
-
- callback && backgroundImageLoaded && callback();
- });
- }
- else {
- overlayImageLoaded = true;
- }
-
- if (!serialized.backgroundImage && !serialized.overlayImage) {
- callback && callback();
- }
+ _this._setBgOverlayImages(serialized, callback);
});
return this;
},
+ _setBgOverlayImages: function(serialized, callback) {
+
+ var _this = this,
+ backgroundPatternLoaded,
+ backgroundImageLoaded,
+ overlayImageLoaded;
+
+ if (serialized.backgroundImage) {
+ this.setBackgroundImage(serialized.backgroundImage, function() {
+
+ _this.backgroundImageOpacity = serialized.backgroundImageOpacity;
+ _this.backgroundImageStretch = serialized.backgroundImageStretch;
+
+ _this.renderAll();
+
+ backgroundImageLoaded = true;
+
+ callback && overlayImageLoaded && backgroundPatternLoaded && callback();
+ });
+ }
+ else {
+ backgroundImageLoaded = true;
+ }
+
+ if (serialized.overlayImage) {
+ this.setOverlayImage(serialized.overlayImage, function() {
+
+ _this.overlayImageLeft = serialized.overlayImageLeft || 0;
+ _this.overlayImageTop = serialized.overlayImageTop || 0;
+
+ _this.renderAll();
+ overlayImageLoaded = true;
+
+ callback && backgroundImageLoaded && backgroundPatternLoaded && callback();
+ });
+ }
+ else {
+ overlayImageLoaded = true;
+ }
+
+ if (serialized.background) {
+ this.setBackgroundColor(serialized.background, function() {
+
+ _this.renderAll();
+ backgroundPatternLoaded = true;
+
+ callback && overlayImageLoaded && backgroundImageLoaded && callback();
+ });
+ }
+ else {
+ backgroundPatternLoaded = true;
+ }
+
+ if (!serialized.backgroundImage && !serialized.overlayImage && !serialized.background) {
+ callback && callback();
+ }
+ },
+
/**
* @method _enlivenObjects
* @param {Array} objects
diff --git a/src/circle.class.js b/src/circle.class.js
index 4b11e8e7..6de5fcc2 100644
--- a/src/circle.class.js
+++ b/src/circle.class.js
@@ -81,6 +81,7 @@
if (this.fill) {
ctx.fill();
}
+ this._removeShadow(ctx);
if (this.stroke) {
ctx.stroke();
}
diff --git a/src/ellipse.class.js b/src/ellipse.class.js
index d1204613..8d1eda6f 100644
--- a/src/ellipse.class.js
+++ b/src/ellipse.class.js
@@ -101,6 +101,7 @@
if (this.stroke) {
ctx.stroke();
}
+ this._removeShadow(ctx);
if (this.fill) {
ctx.fill();
}
diff --git a/src/gradient.class.js b/src/gradient.class.js
index 490070fc..9a4bf77f 100644
--- a/src/gradient.class.js
+++ b/src/gradient.class.js
@@ -65,11 +65,11 @@
/**
* Returns an instance of CanvasGradient
- * @method toLiveGradient
+ * @method toLive
* @param ctx
* @return {CanvasGradient}
*/
- toLiveGradient: function(ctx) {
+ toLive: function(ctx) {
var gradient = ctx.createLinearGradient(
this.x1, this.y1, this.x2 || ctx.canvas.width, this.y2);
diff --git a/src/image.class.js b/src/image.class.js
index 092a1c0f..a905ee5e 100644
--- a/src/image.class.js
+++ b/src/image.class.js
@@ -102,7 +102,11 @@
if (!noTransform) {
this.transform(ctx);
}
+
+ this._setShadow(ctx);
this._render(ctx);
+ this._removeShadow(ctx);
+
if (this.active && !noTransform) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
@@ -186,14 +190,10 @@
var isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined',
imgEl = this._originalImage,
- canvasEl = fabric.document.createElement('canvas'),
+ canvasEl = fabric.util.createCanvasElement(),
replacement = isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'),
_this = this;
- if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') {
- G_vmlCanvasManager.initElement(canvasEl);
- }
-
canvasEl.width = imgEl.width;
canvasEl.height = imgEl.height;
diff --git a/src/image_filters.js b/src/image_filters.js
index babf03fb..64bd7017 100644
--- a/src/image_filters.js
+++ b/src/image_filters.js
@@ -620,7 +620,8 @@ fabric.Image.filters.Convolute = fabric.util.createClass(/** @scope fabric.Image
0, 1, 0,
0, 0, 0 ];
- this.tmpCtx = fabric.document.createElement('canvas').getContext('2d');
+ var canvasEl = fabric.util.createCanvasElement();
+ this.tmpCtx = canvasEl.getContext('2d');
},
/**
diff --git a/src/node.js b/src/node.js
index 9b94e0fd..0cfaa7c9 100644
--- a/src/node.js
+++ b/src/node.js
@@ -121,6 +121,10 @@
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) {
origSetWidth.call(this);
@@ -141,4 +145,4 @@
fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight;
}
-})();
\ No newline at end of file
+})();
diff --git a/src/object.class.js b/src/object.class.js
index dc65f95a..8e334d73 100644
--- a/src/object.class.js
+++ b/src/object.class.js
@@ -6,7 +6,6 @@
extend = fabric.util.object.extend,
toFixed = fabric.util.toFixed,
capitalize = fabric.util.string.capitalize,
- getPointer = fabric.util.getPointer,
degreesToRadians = fabric.util.degreesToRadians;
if (fabric.Object) {
@@ -32,11 +31,11 @@
fabric.Object = fabric.util.createClass(/** @scope fabric.Object.prototype */ {
/**
- * Type of an object (rect, circle, path, etc)
+ * Type of an object (rect, circle, path, etc.)
* @property
* @type String
*/
- type: 'object',
+ type: 'object',
/**
* Horizontal origin of transformation of an object (one of "left", "right", "center")
@@ -199,6 +198,13 @@
*/
strokeDashArray: null,
+ /**
+ * Shadow object representing shadow of this shape
+ * @property
+ * @type fabric.Shadow
+ */
+ shadow: null,
+
/**
* Border opacity when object is active and moving
* @property
@@ -286,7 +292,7 @@
'top left width height scaleX scaleY flipX flipY ' +
'angle opacity cornerSize fill overlayFill originX originY ' +
'stroke strokeWidth strokeDashArray fillRule ' +
- 'borderScaleFactor transformMatrix selectable'
+ 'borderScaleFactor transformMatrix selectable shadow'
).split(' '),
/**
@@ -305,11 +311,34 @@
* @method _initGradient
*/
_initGradient: function(options) {
- if (options.fill && typeof options.fill === 'object' && !(options.fill instanceof fabric.Gradient)) {
+ if (options.fill && options.fill.colorStops && !(options.fill instanceof fabric.Gradient)) {
this.set('fill', new fabric.Gradient(options.fill));
}
},
+ /**
+ * @private
+ * @method _initPattern
+ */
+ _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
+ * @method _initShadow
+ */
+ _initShadow: function(options) {
+ if (options.shadow && !(options.shadow instanceof fabric.Shadow)) {
+ this.setShadow(options.shadow);
+ }
+ },
+
/**
* Sets object's properties from options
* @method setOptions
@@ -320,6 +349,8 @@
this.set(prop, options[prop]);
}
this._initGradient(options);
+ this._initPattern(options);
+ this._initShadow(options);
},
/**
@@ -350,30 +381,31 @@
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
var 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,
- overlayFill: this.overlayFill,
- stroke: this.stroke,
- strokeWidth: this.strokeWidth,
- strokeDashArray: this.strokeDashArray,
- scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
- scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
- angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS),
- flipX: this.flipX,
- flipY: this.flipY,
- opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS),
- selectable: this.selectable,
- hasControls: this.hasControls,
- hasBorders: this.hasBorders,
- hasRotatingPoint: this.hasRotatingPoint,
+ 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,
+ overlayFill: this.overlayFill,
+ stroke: (this.stroke && this.stroke.toObject) ? this.stroke.toObject() : this.stroke,
+ strokeWidth: this.strokeWidth,
+ strokeDashArray: this.strokeDashArray,
+ scaleX: toFixed(this.scaleX, NUM_FRACTION_DIGITS),
+ scaleY: toFixed(this.scaleY, NUM_FRACTION_DIGITS),
+ angle: toFixed(this.getAngle(), NUM_FRACTION_DIGITS),
+ flipX: this.flipX,
+ flipY: this.flipY,
+ opacity: toFixed(this.opacity, NUM_FRACTION_DIGITS),
+ selectable: this.selectable,
+ hasControls: this.hasControls,
+ hasBorders: this.hasBorders,
+ hasRotatingPoint: this.hasRotatingPoint,
transparentCorners: this.transparentCorners,
- perPixelTargetFind: this.perPixelTargetFind
+ perPixelTargetFind: this.perPixelTargetFind,
+ shadow: (this.shadow && this.shadow.toObject) ? this.shadow.toObject() : this.shadow
};
if (!this.includeDefaultValues) {
@@ -489,21 +521,13 @@
},
/**
- * Makes sure the scale is valid and modifies it if necessary
- * @private
- * @method _constrainScale
- * @param {Number} value
- * @return {Number}
+ * Basic getter
+ * @method get
+ * @param {String} property
+ * @return {Any} value of a property
*/
- _constrainScale: function(value) {
- if (Math.abs(value) < this.minScaleLimit) {
- if (value < 0)
- return -this.minScaleLimit;
- else
- return this.minScaleLimit;
- }
-
- return value;
+ get: function(property) {
+ return this[property];
},
/**
@@ -587,16 +611,6 @@
return this;
},
- /**
- * Basic getter
- * @method get
- * @param {String} property
- * @return {Any} value of a property
- */
- get: function(property) {
- return this[property];
- },
-
/**
* Renders an object on a specified context
* @method render
@@ -621,15 +635,20 @@
if (this.stroke || this.strokeDashArray) {
ctx.lineWidth = this.strokeWidth;
- ctx.strokeStyle = this.stroke;
+ if (this.stroke && this.stroke.toLive) {
+ ctx.strokeStyle = this.stroke.toLive(ctx);
+ }
+ else {
+ ctx.strokeStyle = this.stroke;
+ }
}
if (this.overlayFill) {
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
- ctx.fillStyle = this.fill.toLiveGradient
- ? this.fill.toLiveGradient(ctx)
+ ctx.fillStyle = this.fill.toLive
+ ? this.fill.toLive(ctx)
: this.fill;
}
@@ -638,7 +657,9 @@
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
+ this._setShadow(ctx);
this._render(ctx, noTransform);
+ this._removeShadow(ctx);
if (this.active && !noTransform) {
this.drawBorders(ctx);
@@ -648,576 +669,25 @@
},
/**
- * Returns width of an object
- * @method getWidth
- * @return {Number} width value
+ * @private
+ * @method _setShadow
*/
- getWidth: function() {
- return this.width * this.scaleX;
- },
+ _setShadow: function(ctx) {
+ if (!this.shadow) return;
- /**
- * Returns height of an object
- * @method getHeight
- * @return {Number} height value
- */
- getHeight: function() {
- return this.height * this.scaleY;
- },
-
- /**
- * Translates the coordinates from origin to center coordinates (based on the object's dimensions)
- * @method translateToCenterPoint
- * @param {fabric.Point} point The point which corresponds to the originX and originY params
- * @param {string} enum('left', 'center', 'right') Horizontal origin
- * @param {string} enum('top', 'center', 'bottom') Vertical origin
- * @return {fabric.Point}
- */
- translateToCenterPoint: function(point, originX, originY) {
- var cx = point.x, cy = point.y;
-
- if ( originX === "left" ) {
- cx = point.x + this.getWidth() / 2;
- }
- else if ( originX === "right" ) {
- cx = point.x - this.getWidth() / 2;
- }
-
- if ( originY === "top" ) {
- cy = point.y + this.getHeight() / 2;
- }
- else if ( originY === "bottom" ) {
- cy = point.y - this.getHeight() / 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)
- * @method translateToOriginPoint
- * @param {fabric.Point} point The point which corresponds to center of the object
- * @param {string} enum('left', 'center', 'right') Horizontal origin
- * @param {string} enum('top', 'center', 'bottom') Vertical origin
- * @return {fabric.Point}
- */
- translateToOriginPoint: function(center, originX, originY) {
- var x = center.x, y = center.y;
-
- // Get the point coordinates
- if ( originX === "left" ) {
- x = center.x - this.getWidth() / 2;
- }
- else if ( originX === "right" ) {
- x = center.x + this.getWidth() / 2;
- }
- if ( originY === "top" ) {
- y = center.y - this.getHeight() / 2;
- }
- else if ( originY === "bottom" ) {
- y = center.y + this.getHeight() / 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
- * @method getCenterPoint
- * @return {fabric.Point}
- */
- getCenterPoint: function() {
- return this.translateToCenterPoint(
- new fabric.Point(this.left, this.top), this.originX, this.originY);
- },
-
- /**
- * Returns the coordinates of the object based on center coordinates
- * @method getOriginPoint
- * @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
- * @method getPointByOrigin
- * @param {string} enum('left', 'center', 'right') Horizontal origin
- * @param {string} enum('top', 'center', 'bottom') Vertical origin
- * @return {fabric.Point}
- */
- getPointByOrigin: function(originX, originY) {
- var center = this.getCenterPoint();
-
- return this.translateToOriginPoint(center, originX, originY);
- },
-
- /**
- * Returns the point in local coordinates
- * @method toLocalPoint
- * @param {fabric.Point} The point relative to the global coordinate system
- * @return {fabric.Point}
- */
- toLocalPoint: function(point, originX, originY) {
- var center = this.getCenterPoint();
-
- var x, y;
- if (originX !== undefined && originY !== undefined) {
- if ( originX === "left" ) {
- x = center.x - this.getWidth() / 2;
- }
- else if ( originX === "right" ) {
- x = center.x + this.getWidth() / 2;
- }
- else {
- x = center.x;
- }
-
- if ( originY === "top" ) {
- y = center.y - this.getHeight() / 2;
- }
- else if ( originY === "bottom" ) {
- y = center.y + this.getHeight() / 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
- * @method toGlobalPoint
- * @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
- * @method setPositionByOrigin
- * @param {fabric.Point} point The new position of the object
- * @param {string} enum('left', 'center', 'right') Horizontal origin
- * @param {string} enum('top', 'center', 'bottom') Vertical origin
- * @return {void}
- */
- setPositionByOrigin: function(pos, originX, originY) {
- var center = this.translateToCenterPoint(pos, originX, originY);
- var position = this.translateToOriginPoint(center, this.originX, this.originY);
-
- this.set('left', position.x);
- this.set('top', position.y);
- },
-
- /**
- * Scales an object (equally by x and y)
- * @method scale
- * @param value {Number} 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)
- * @method scaleToWidth
- * @param value {Number} new width value
- * @return {fabric.Object} thisArg
- * @chainable
- */
- scaleToWidth: function(value) {
- // adjust to bounding rect factor so that rotated shapes would fit as well
- var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth();
- return this.scale(value / this.width / boundingRectFactor);
- },
-
- /**
- * Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
- * @method scaleToHeight
- * @param value {Number} new height value
- * @return {fabric.Object} thisArg
- * @chainable
- */
- scaleToHeight: function(value) {
- // adjust to bounding rect factor so that rotated shapes would fit as well
- var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight();
- return this.scale(value / this.height / boundingRectFactor);
- },
-
- /**
- * Sets corner position coordinates based on current angle, width and height
- * @method setCoords
- * @return {fabric.Object} thisArg
- * @chainable
- */
- setCoords: function() {
-
- var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0,
- padding = this.padding,
- theta = degreesToRadians(this.angle);
-
- this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2;
- this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2;
-
- //If width is negative, make postive. Fixes path selection issue
- if(this.currentWidth < 0){
- this.currentWidth = Math.abs(this.currentWidth);
- }
-
- var _hypotenuse = Math.sqrt(
- Math.pow(this.currentWidth / 2, 2) +
- Math.pow(this.currentHeight / 2, 2));
-
- var _angle = Math.atan(this.currentHeight / this.currentWidth);
-
- // offset added for rotate and scale actions
- var offsetX = Math.cos(_angle + theta) * _hypotenuse,
- offsetY = Math.sin(_angle + theta) * _hypotenuse,
- sinTh = Math.sin(theta),
- cosTh = Math.cos(theta);
-
- var coords = this.getCenterPoint();
- var tl = {
- x: coords.x - offsetX,
- y: coords.y - offsetY
- };
- var tr = {
- x: tl.x + (this.currentWidth * cosTh),
- y: tl.y + (this.currentWidth * sinTh)
- };
- var br = {
- x: tr.x - (this.currentHeight * sinTh),
- y: tr.y + (this.currentHeight * cosTh)
- };
- var bl = {
- x: tl.x - (this.currentHeight * sinTh),
- y: tl.y + (this.currentHeight * cosTh)
- };
- var ml = {
- x: tl.x - (this.currentHeight/2 * sinTh),
- y: tl.y + (this.currentHeight/2 * cosTh)
- };
- var mt = {
- x: tl.x + (this.currentWidth/2 * cosTh),
- y: tl.y + (this.currentWidth/2 * sinTh)
- };
- var mr = {
- x: tr.x - (this.currentHeight/2 * sinTh),
- y: tr.y + (this.currentHeight/2 * cosTh)
- };
- var mb = {
- x: bl.x + (this.currentWidth/2 * cosTh),
- y: bl.y + (this.currentWidth/2 * sinTh)
- };
- var mtr = {
- x: tl.x + (this.currentWidth/2 * cosTh),
- y: tl.y + (this.currentWidth/2 * sinTh)
- };
-
- // debugging
-
- // setTimeout(function() {
- // canvas.contextTop.fillStyle = 'green';
- // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
- // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
- // canvas.contextTop.fillRect(br.x, br.y, 3, 3);
- // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
- // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
- // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
- // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
- // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
- // }, 50);
-
- // clockwise
- this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr };
-
- // set coordinates of the draggable boxes in the corners used to scale/rotate the image
- this._setCornerCoords();
-
- return this;
- },
-
- /**
- * Returns width of an object's bounding rectangle
- * @method getBoundingRectWidth
- * @return {Number} width value
- */
- getBoundingRectWidth: function() {
- this.oCoords || this.setCoords();
- var xCoords = [this.oCoords.tl.x, this.oCoords.tr.x, this.oCoords.br.x, this.oCoords.bl.x];
- var minX = fabric.util.array.min(xCoords);
- var maxX = fabric.util.array.max(xCoords);
- return Math.abs(minX - maxX);
- },
-
- /**
- * Returns height of an object's bounding rectangle
- * @method getBoundingRectHeight
- * @return {Number} height value
- */
- getBoundingRectHeight: function() {
- this.oCoords || this.setCoords();
- var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y];
- var minY = fabric.util.array.min(yCoords);
- var maxY = fabric.util.array.max(yCoords);
- return Math.abs(minY - maxY);
- },
-
- /**
- * Draws borders of an object's bounding box.
- * Requires public properties: width, height
- * Requires public options: padding, borderColor
- * @method drawBorders
- * @param {CanvasRenderingContext2D} ctx Context to draw on
- * @return {fabric.Object} thisArg
- * @chainable
- */
- drawBorders: function(ctx) {
- if (!this.hasBorders) return;
-
- var padding = this.padding,
- padding2 = padding * 2,
- strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0;
-
- ctx.save();
-
- ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
- ctx.strokeStyle = this.borderColor;
-
- var scaleX = 1 / this._constrainScale(this.scaleX),
- scaleY = 1 / this._constrainScale(this.scaleY);
-
- ctx.lineWidth = 1 / this.borderScaleFactor;
-
- ctx.scale(scaleX, scaleY);
-
- var w = this.getWidth(),
- h = this.getHeight();
-
- ctx.strokeRect(
- ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper
- ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5,
- ~~(w + padding2 + strokeWidth * this.scaleX),
- ~~(h + padding2 + strokeWidth * this.scaleY)
- );
-
- if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) {
-
- var rotateHeight = (
- this.flipY
- ? h + (strokeWidth * this.scaleY) + (padding * 2)
- : -h - (strokeWidth * this.scaleY) - (padding * 2)
- ) / 2;
-
- ctx.beginPath();
- ctx.moveTo(0, rotateHeight);
- ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset));
- ctx.closePath();
- ctx.stroke();
- }
-
- ctx.restore();
- return this;
+ ctx.shadowColor = this.shadow.color;
+ ctx.shadowBlur = this.shadow.blur;
+ ctx.shadowOffsetX = this.shadow.offsetX;
+ ctx.shadowOffsetY = this.shadow.offsetY;
},
/**
* @private
- * @method _renderDashedStroke
+ * @method _removeShadow
*/
- _renderDashedStroke: function(ctx) {
-
- if (1 & this.strokeDashArray.length /* if odd number of items */) {
- /* duplicate items */
- this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
- }
-
- var i = 0,
- x = -this.width/2, y = -this.height/2,
- _this = this,
- padding = this.padding,
- dashedArrayLength = this.strokeDashArray.length;
-
- ctx.save();
- ctx.beginPath();
-
- /** @ignore */
- function renderSide(xMultiplier, yMultiplier) {
-
- var lineLength = 0,
- lengthDiff = 0,
- sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2;
-
- while (lineLength < sideLength) {
-
- var lengthOfSubPath = _this.strokeDashArray[i++];
- lineLength += lengthOfSubPath;
-
- if (lineLength > sideLength) {
- lengthDiff = lineLength - sideLength;
- }
-
- // track coords
- if (xMultiplier) {
- x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0);
- }
- else {
- y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0);
- }
-
- ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y);
- if (i >= dashedArrayLength) {
- i = 0;
- }
- }
- }
-
- renderSide(1, 0);
- renderSide(0, 1);
- renderSide(-1, 0);
- renderSide(0, -1);
-
- ctx.stroke();
- ctx.closePath();
- ctx.restore();
- },
-
- /**
- * Draws corners of an object's bounding box.
- * Requires public properties: width, height, scaleX, scaleY
- * Requires public options: cornerSize, padding
- * @method drawCorners
- * @param {CanvasRenderingContext2D} ctx Context to draw on
- * @return {fabric.Object} thisArg
- * @chainable
- */
- drawCorners: function(ctx) {
- if (!this.hasControls) return;
-
- var size = this.cornerSize,
- size2 = size / 2,
- strokeWidth2 = this.strokeWidth / 2,
- left = -(this.width / 2),
- top = -(this.height / 2),
- _left,
- _top,
- sizeX = size / this.scaleX,
- sizeY = size / this.scaleY,
- paddingX = this.padding / this.scaleX,
- paddingY = this.padding / this.scaleY,
- scaleOffsetY = size2 / this.scaleY,
- scaleOffsetX = size2 / this.scaleX,
- scaleOffsetSizeX = (size2 - size) / this.scaleX,
- scaleOffsetSizeY = (size2 - size) / this.scaleY,
- height = this.height,
- width = this.width,
- methodName = this.transparentCorners ? 'strokeRect' : 'fillRect',
- isVML = typeof G_vmlCanvasManager !== 'undefined';
-
- ctx.save();
-
- ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY);
-
- ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
- ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
-
- // top-left
- _left = left - scaleOffsetX - strokeWidth2 - paddingX;
- _top = top - scaleOffsetY - strokeWidth2 - paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // top-right
- _left = left + width - scaleOffsetX + strokeWidth2 + paddingX;
- _top = top - scaleOffsetY - strokeWidth2 - paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // bottom-left
- _left = left - scaleOffsetX - strokeWidth2 - paddingX;
- _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // bottom-right
- _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
- _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- if (!this.get('lockUniScaling')) {
- // middle-top
- _left = left + width/2 - scaleOffsetX;
- _top = top - scaleOffsetY - strokeWidth2 - paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // middle-bottom
- _left = left + width/2 - scaleOffsetX;
- _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // middle-right
- _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
- _top = top + height/2 - scaleOffsetY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
-
- // middle-left
- _left = left - scaleOffsetX - strokeWidth2 - paddingX;
- _top = top + height/2 - scaleOffsetY;
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
- }
-
- // middle-top-rotate
- if (this.hasRotatingPoint) {
-
- _left = left + width/2 - scaleOffsetX;
- _top = this.flipY ?
- (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY)
- : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY);
-
- isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
- ctx[methodName](_left, _top, sizeX, sizeY);
- }
-
- ctx.restore();
-
- return this;
+ _removeShadow: function(ctx) {
+ ctx.shadowColor = '';
+ ctx.shadowBlur = ctx.shadowOffsetX = ctx.shadowOffsetY = 0;
},
/**
@@ -1274,10 +744,7 @@
* @param callback {Function} callback that recieves resulting data-url string
*/
toDataURL: function(callback) {
- var el = fabric.document.createElement('canvas');
- if (!el.getContext && typeof G_vmlCanvasManager !== 'undefined') {
- G_vmlCanvasManager.initElement(el);
- }
+ var el = fabric.util.createCanvasElement();
el.width = this.getBoundingRectWidth();
el.height = this.getBoundingRectHeight();
@@ -1344,84 +811,6 @@
this.saveState();
},
- /**
- * Returns true if object intersects with an area formed by 2 points
- * @method intersectsWithRect
- * @param {Object} selectionTL
- * @param {Object} selectionBR
- * @return {Boolean}
- */
- intersectsWithRect: function(selectionTL, selectionBR) {
- var oCoords = this.oCoords,
- tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
- tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
- bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
- br = new fabric.Point(oCoords.br.x, oCoords.br.y);
-
- var intersection = fabric.Intersection.intersectPolygonRectangle(
- [tl, tr, br, bl],
- selectionTL,
- selectionBR
- );
- return (intersection.status === 'Intersection');
- },
-
- /**
- * Returns true if object intersects with another object
- * @method intersectsWithObject
- * @param {Object} other Object to test
- * @return {Boolean}
- */
- intersectsWithObject: function(other) {
- // extracts coords
- function getCoords(oCoords) {
- return {
- tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
- tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
- bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
- br: new fabric.Point(oCoords.br.x, oCoords.br.y)
- };
- }
- var thisCoords = getCoords(this.oCoords),
- otherCoords = getCoords(other.oCoords);
-
- var intersection = fabric.Intersection.intersectPolygonPolygon(
- [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
- [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
- );
-
- return (intersection.status === 'Intersection');
- },
-
- /**
- * Returns true if object is fully contained within area of another object
- * @method isContainedWithinObject
- * @param {Object} other Object to test
- * @return {Boolean}
- */
- isContainedWithinObject: function(other) {
- return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br);
- },
-
- /**
- * Returns true if object is fully contained within area formed by 2 points
- * @method isContainedWithinRect
- * @param {Object} selectionTL
- * @param {Object} selectionBR
- * @return {Boolean}
- */
- isContainedWithinRect: function(selectionTL, selectionBR) {
- var oCoords = this.oCoords,
- tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
- tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
- bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y);
-
- return tl.x > selectionTL.x
- && tr.x < selectionBR.x
- && tl.y > selectionTL.y
- && bl.y < selectionBR.y;
- },
-
/**
* Returns true if specified type is identical to the type of an instance
* @method isType
@@ -1432,324 +821,6 @@
return this.type === type;
},
- /**
- * Determines which one of the four corners has been clicked
- * @method _findTargetCorner
- * @private
- * @param e {Event} event object
- * @param offset {Object} canvas offset
- * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
- */
- _findTargetCorner: function(e, offset) {
- if (!this.hasControls || !this.active) return false;
-
- var pointer = getPointer(e),
- ex = pointer.x - offset.left,
- ey = pointer.y - offset.top,
- xpoints,
- lines;
-
- for (var i in this.oCoords) {
-
- if (i === 'mtr' && !this.hasRotatingPoint) {
- continue;
- }
-
- if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) {
- continue;
- }
-
- lines = this._getImageLines(this.oCoords[i].corner, i);
-
- // debugging
-
- // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
- // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
-
- // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2);
- // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2);
-
- // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2);
- // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2);
-
- // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2);
- // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2);
-
- xpoints = this._findCrossPoints(ex, ey, lines);
- if (xpoints % 2 === 1 && xpoints !== 0) {
- this.__corner = i;
- return i;
- }
- }
- return false;
- },
-
- /**
- * Helper method to determine how many cross points are between the 4 image edges
- * and the horizontal line determined by the position of our mouse when clicked on canvas
- * @method _findCrossPoints
- * @private
- * @param ex {Number} x coordinate of the mouse
- * @param ey {Number} y coordinate of the mouse
- * @param oCoords {Object} Coordinates of the image being evaluated
- */
- _findCrossPoints: function(ex, ey, oCoords) {
- var b1, b2, a1, a2, xi, yi,
- xcount = 0,
- iLine;
-
- for (var lineKey in oCoords) {
- iLine = oCoords[lineKey];
- // optimisation 1: line below dot. no cross
- if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
- continue;
- }
- // optimisation 2: line above dot. no cross
- if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
- continue;
- }
- // optimisation 3: vertical line case
- if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) {
- xi = iLine.o.x;
- yi = ey;
- }
- // calculate the intersection point
- else {
- b1 = 0;
- b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
- a1 = ey-b1*ex;
- a2 = iLine.o.y-b2*iLine.o.x;
-
- xi = - (a1-a2)/(b1-b2);
- yi = a1+b1*xi;
- }
- // dont count xi < ex cases
- if (xi >= ex) {
- xcount += 1;
- }
- // optimisation 4: specific for square images
- if (xcount === 2) {
- break;
- }
- }
- return xcount;
- },
-
- /**
- * Method that returns an object with the image lines in it given the coordinates of the corners
- * @method _getImageLines
- * @private
- * @param oCoords {Object} coordinates of the image corners
- */
- _getImageLines: function(oCoords) {
- return {
- topline: {
- o: oCoords.tl,
- d: oCoords.tr
- },
- rightline: {
- o: oCoords.tr,
- d: oCoords.br
- },
- bottomline: {
- o: oCoords.br,
- d: oCoords.bl
- },
- leftline: {
- o: oCoords.bl,
- d: oCoords.tl
- }
- };
- },
-
- /**
- * Sets the coordinates of the draggable boxes in the corners of
- * the image used to scale/rotate it.
- * @method _setCornerCoords
- * @private
- */
- _setCornerCoords: function() {
- var coords = this.oCoords,
- theta = degreesToRadians(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)
- }
- };
- },
-
/**
* Makes object's color grayscale
* @method toGrayscale
@@ -1791,6 +862,22 @@
this.set('fill', fabric.Gradient.forObject(this, options));
},
+ /**
+ * Sets pattern fill of an object
+ * @method setPatternFill
+ */
+ setPatternFill: function(options) {
+ this.set('fill', new fabric.Pattern(options));
+ },
+
+ /**
+ * Sets shadow of an object
+ * @method setShadow
+ */
+ setShadow: function(options) {
+ this.set('shadow', new fabric.Shadow(options));
+ },
+
/**
* Animates object's properties
* @method animate
@@ -1823,7 +910,7 @@
* @method _animate
*/
_animate: function(property, to, options) {
- var obj = this;
+ var obj = this, propPair;
to = to.toString();
@@ -1834,12 +921,20 @@
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 = this.get(property);
+ options.from = currentValue;
}
if (~to.indexOf('=')) {
- to = this.get(property) + parseFloat(to.replace('=', ''));
+ to = currentValue + parseFloat(to.replace('=', ''));
}
else {
to = parseFloat(to);
@@ -1852,7 +947,12 @@
easing: options.easing,
duration: options.duration,
onChange: function(value) {
- obj.set(property, value);
+ if (propPair) {
+ obj[propPair[0]][propPair[1]] = value;
+ }
+ else {
+ obj.set(property, value);
+ }
options.onChange && options.onChange();
},
onComplete: function() {
@@ -1977,14 +1077,11 @@
extend(fabric.Object.prototype, fabric.Observable);
- extend(fabric.Object, {
-
- /**
- * @static
- * @constant
- * @type Number
- */
- NUM_FRACTION_DIGITS: 2
- });
+ /**
+ * @static
+ * @constant
+ * @type Number
+ */
+ fabric.Object.NUM_FRACTION_DIGITS = 2;
})(typeof exports !== 'undefined' ? exports : this);
diff --git a/src/object_geometry.mixin.js b/src/object_geometry.mixin.js
new file mode 100644
index 00000000..25222b2e
--- /dev/null
+++ b/src/object_geometry.mixin.js
@@ -0,0 +1,308 @@
+(function() {
+
+ var degreesToRadians = fabric.util.degreesToRadians;
+
+ fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
+
+ /**
+ * Returns true if object intersects with an area formed by 2 points
+ * @method intersectsWithRect
+ * @param {Object} selectionTL
+ * @param {Object} selectionBR
+ * @return {Boolean}
+ */
+ intersectsWithRect: function(selectionTL, selectionBR) {
+ var oCoords = this.oCoords,
+ tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
+ tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
+ bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y),
+ br = new fabric.Point(oCoords.br.x, oCoords.br.y);
+
+ var intersection = fabric.Intersection.intersectPolygonRectangle(
+ [tl, tr, br, bl],
+ selectionTL,
+ selectionBR
+ );
+ return intersection.status === 'Intersection';
+ },
+
+ /**
+ * Returns true if object intersects with another object
+ * @method intersectsWithObject
+ * @param {Object} other Object to test
+ * @return {Boolean}
+ */
+ intersectsWithObject: function(other) {
+ // extracts coords
+ function getCoords(oCoords) {
+ return {
+ tl: new fabric.Point(oCoords.tl.x, oCoords.tl.y),
+ tr: new fabric.Point(oCoords.tr.x, oCoords.tr.y),
+ bl: new fabric.Point(oCoords.bl.x, oCoords.bl.y),
+ br: new fabric.Point(oCoords.br.x, oCoords.br.y)
+ };
+ }
+ var thisCoords = getCoords(this.oCoords),
+ otherCoords = getCoords(other.oCoords);
+
+ var intersection = fabric.Intersection.intersectPolygonPolygon(
+ [thisCoords.tl, thisCoords.tr, thisCoords.br, thisCoords.bl],
+ [otherCoords.tl, otherCoords.tr, otherCoords.br, otherCoords.bl]
+ );
+
+ return intersection.status === 'Intersection';
+ },
+
+ /**
+ * Returns true if object is fully contained within area of another object
+ * @method isContainedWithinObject
+ * @param {Object} other Object to test
+ * @return {Boolean}
+ */
+ isContainedWithinObject: function(other) {
+ return this.isContainedWithinRect(other.oCoords.tl, other.oCoords.br);
+ },
+
+ /**
+ * Returns true if object is fully contained within area formed by 2 points
+ * @method isContainedWithinRect
+ * @param {Object} selectionTL
+ * @param {Object} selectionBR
+ * @return {Boolean}
+ */
+ isContainedWithinRect: function(selectionTL, selectionBR) {
+ var oCoords = this.oCoords,
+ tl = new fabric.Point(oCoords.tl.x, oCoords.tl.y),
+ tr = new fabric.Point(oCoords.tr.x, oCoords.tr.y),
+ bl = new fabric.Point(oCoords.bl.x, oCoords.bl.y);
+
+ return tl.x > selectionTL.x
+ && tr.x < selectionBR.x
+ && tl.y > selectionTL.y
+ && bl.y < selectionBR.y;
+ },
+
+ /**
+ * Returns width of an object's bounding rectangle
+ * @deprecated since 1.0.4
+ * @method getBoundingRectWidth
+ * @return {Number} width value
+ */
+ getBoundingRectWidth: function() {
+ return this.getBoundingRect().width;
+ },
+
+ /**
+ * Returns height of an object's bounding rectangle
+ * @deprecated since 1.0.4
+ * @method getBoundingRectHeight
+ * @return {Number} height value
+ */
+ getBoundingRectHeight: function() {
+ return this.getBoundingRect().height;
+ },
+
+ /**
+ * Returns coordinates of object's bounding rectangle (left, top, width, height)
+ * @method getBoundingRect
+ * @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];
+ var minX = fabric.util.array.min(xCoords);
+ var maxX = fabric.util.array.max(xCoords);
+ var width = Math.abs(minX - maxX);
+
+ var yCoords = [this.oCoords.tl.y, this.oCoords.tr.y, this.oCoords.br.y, this.oCoords.bl.y];
+ var minY = fabric.util.array.min(yCoords);
+ var maxY = fabric.util.array.max(yCoords);
+ var height = Math.abs(minY - maxY);
+
+ return {
+ left: minX,
+ top: minY,
+ width: width,
+ height: height
+ };
+ },
+
+ /**
+ * Returns width of an object
+ * @method getWidth
+ * @return {Number} width value
+ */
+ getWidth: function() {
+ return this.width * this.scaleX;
+ },
+
+ /**
+ * Returns height of an object
+ * @method getHeight
+ * @return {Number} height value
+ */
+ getHeight: function() {
+ return this.height * this.scaleY;
+ },
+
+ /**
+ * Makes sure the scale is valid and modifies it if necessary
+ * @private
+ * @method _constrainScale
+ * @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)
+ * @method scale
+ * @param value {Number} 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)
+ * @method scaleToWidth
+ * @param value {Number} new width value
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ scaleToWidth: function(value) {
+ // adjust to bounding rect factor so that rotated shapes would fit as well
+ var boundingRectFactor = this.getBoundingRectWidth() / this.getWidth();
+ return this.scale(value / this.width / boundingRectFactor);
+ },
+
+ /**
+ * Scales an object to a given height, with respect to bounding box (scaling by x/y equally)
+ * @method scaleToHeight
+ * @param value {Number} new height value
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ scaleToHeight: function(value) {
+ // adjust to bounding rect factor so that rotated shapes would fit as well
+ var boundingRectFactor = this.getBoundingRectHeight() / this.getHeight();
+ return this.scale(value / this.height / boundingRectFactor);
+ },
+
+ /**
+ * Sets corner position coordinates based on current angle, width and height
+ * @method setCoords
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ setCoords: function() {
+
+ var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0,
+ padding = this.padding,
+ theta = degreesToRadians(this.angle);
+
+ this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2;
+ this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2;
+
+ // If width is negative, make postive. Fixes path selection issue
+ if (this.currentWidth < 0) {
+ this.currentWidth = Math.abs(this.currentWidth);
+ }
+
+ var _hypotenuse = Math.sqrt(
+ Math.pow(this.currentWidth / 2, 2) +
+ Math.pow(this.currentHeight / 2, 2));
+
+ var _angle = Math.atan(this.currentHeight / this.currentWidth);
+
+ // offset added for rotate and scale actions
+ var offsetX = Math.cos(_angle + theta) * _hypotenuse,
+ offsetY = Math.sin(_angle + theta) * _hypotenuse,
+ sinTh = Math.sin(theta),
+ cosTh = Math.cos(theta);
+
+ var coords = this.getCenterPoint();
+ var tl = {
+ x: coords.x - offsetX,
+ y: coords.y - offsetY
+ };
+ var tr = {
+ x: tl.x + (this.currentWidth * cosTh),
+ y: tl.y + (this.currentWidth * sinTh)
+ };
+ var br = {
+ x: tr.x - (this.currentHeight * sinTh),
+ y: tr.y + (this.currentHeight * cosTh)
+ };
+ var bl = {
+ x: tl.x - (this.currentHeight * sinTh),
+ y: tl.y + (this.currentHeight * cosTh)
+ };
+ var ml = {
+ x: tl.x - (this.currentHeight/2 * sinTh),
+ y: tl.y + (this.currentHeight/2 * cosTh)
+ };
+ var mt = {
+ x: tl.x + (this.currentWidth/2 * cosTh),
+ y: tl.y + (this.currentWidth/2 * sinTh)
+ };
+ var mr = {
+ x: tr.x - (this.currentHeight/2 * sinTh),
+ y: tr.y + (this.currentHeight/2 * cosTh)
+ };
+ var mb = {
+ x: bl.x + (this.currentWidth/2 * cosTh),
+ y: bl.y + (this.currentWidth/2 * sinTh)
+ };
+ var mtr = {
+ x: tl.x + (this.currentWidth/2 * cosTh),
+ y: tl.y + (this.currentWidth/2 * sinTh)
+ };
+
+ // debugging
+
+ // setTimeout(function() {
+ // canvas.contextTop.fillStyle = 'green';
+ // canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
+ // canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
+ // canvas.contextTop.fillRect(br.x, br.y, 3, 3);
+ // canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
+ // canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
+ // canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
+ // canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
+ // canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
+ // }, 50);
+
+ // clockwise
+ this.oCoords = { tl: tl, tr: tr, br: br, bl: bl, ml: ml, mt: mt, mr: mr, mb: mb, mtr: mtr };
+
+ // set coordinates of the draggable boxes in the corners used to scale/rotate the image
+ this._setCornerCoords();
+
+ return this;
+ }
+ });
+})();
\ No newline at end of file
diff --git a/src/object_interactivity.mixin.js b/src/object_interactivity.mixin.js
new file mode 100644
index 00000000..f76467b7
--- /dev/null
+++ b/src/object_interactivity.mixin.js
@@ -0,0 +1,496 @@
+(function(){
+
+ var getPointer = fabric.util.getPointer,
+ degreesToRadians = fabric.util.degreesToRadians;
+
+ fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
+
+ /**
+ * Determines which one of the four corners has been clicked
+ * @method _findTargetCorner
+ * @private
+ * @param e {Event} event object
+ * @param offset {Object} canvas offset
+ * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found
+ */
+ _findTargetCorner: function(e, offset) {
+ if (!this.hasControls || !this.active) return false;
+
+ var pointer = getPointer(e, this.canvas.upperCanvasEl),
+ ex = pointer.x - offset.left,
+ ey = pointer.y - offset.top,
+ xpoints,
+ lines;
+
+ for (var i in this.oCoords) {
+
+ if (i === 'mtr' && !this.hasRotatingPoint) {
+ continue;
+ }
+
+ if (this.get('lockUniScaling') && (i === 'mt' || i === 'mr' || i === 'mb' || i === 'ml')) {
+ continue;
+ }
+
+ lines = this._getImageLines(this.oCoords[i].corner, i);
+
+ // debugging
+
+ // canvas.contextTop.fillRect(lines.bottomline.d.x, lines.bottomline.d.y, 2, 2);
+ // canvas.contextTop.fillRect(lines.bottomline.o.x, lines.bottomline.o.y, 2, 2);
+
+ // canvas.contextTop.fillRect(lines.leftline.d.x, lines.leftline.d.y, 2, 2);
+ // canvas.contextTop.fillRect(lines.leftline.o.x, lines.leftline.o.y, 2, 2);
+
+ // canvas.contextTop.fillRect(lines.topline.d.x, lines.topline.d.y, 2, 2);
+ // canvas.contextTop.fillRect(lines.topline.o.x, lines.topline.o.y, 2, 2);
+
+ // canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2);
+ // canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2);
+
+ xpoints = this._findCrossPoints(ex, ey, lines);
+ if (xpoints % 2 === 1 && xpoints !== 0) {
+ this.__corner = i;
+ return i;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Helper method to determine how many cross points are between the 4 image edges
+ * and the horizontal line determined by the position of our mouse when clicked on canvas
+ * @method _findCrossPoints
+ * @private
+ * @param ex {Number} x coordinate of the mouse
+ * @param ey {Number} y coordinate of the mouse
+ * @param oCoords {Object} Coordinates of the image being evaluated
+ */
+ _findCrossPoints: function(ex, ey, oCoords) {
+ var b1, b2, a1, a2, xi, yi,
+ xcount = 0,
+ iLine;
+
+ for (var lineKey in oCoords) {
+ iLine = oCoords[lineKey];
+ // optimisation 1: line below dot. no cross
+ if ((iLine.o.y < ey) && (iLine.d.y < ey)) {
+ continue;
+ }
+ // optimisation 2: line above dot. no cross
+ if ((iLine.o.y >= ey) && (iLine.d.y >= ey)) {
+ continue;
+ }
+ // optimisation 3: vertical line case
+ if ((iLine.o.x === iLine.d.x) && (iLine.o.x >= ex)) {
+ xi = iLine.o.x;
+ yi = ey;
+ }
+ // calculate the intersection point
+ else {
+ b1 = 0;
+ b2 = (iLine.d.y-iLine.o.y)/(iLine.d.x-iLine.o.x);
+ a1 = ey-b1*ex;
+ a2 = iLine.o.y-b2*iLine.o.x;
+
+ xi = - (a1-a2)/(b1-b2);
+ yi = a1+b1*xi;
+ }
+ // dont count xi < ex cases
+ if (xi >= ex) {
+ xcount += 1;
+ }
+ // optimisation 4: specific for square images
+ if (xcount === 2) {
+ break;
+ }
+ }
+ return xcount;
+ },
+
+ /**
+ * Method that returns an object with the image lines in it given the coordinates of the corners
+ * @method _getImageLines
+ * @private
+ * @param oCoords {Object} coordinates of the image corners
+ */
+ _getImageLines: function(oCoords) {
+ return {
+ topline: {
+ o: oCoords.tl,
+ d: oCoords.tr
+ },
+ rightline: {
+ o: oCoords.tr,
+ d: oCoords.br
+ },
+ bottomline: {
+ o: oCoords.br,
+ d: oCoords.bl
+ },
+ leftline: {
+ o: oCoords.bl,
+ d: oCoords.tl
+ }
+ };
+ },
+
+ /**
+ * Sets the coordinates of the draggable boxes in the corners of
+ * the image used to scale/rotate it.
+ * @method _setCornerCoords
+ * @private
+ */
+ _setCornerCoords: function() {
+ var coords = this.oCoords,
+ theta = degreesToRadians(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
+ * @method drawBorders
+ * @param {CanvasRenderingContext2D} ctx Context to draw on
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ drawBorders: function(ctx) {
+ if (!this.hasBorders) return;
+
+ var padding = this.padding,
+ padding2 = padding * 2,
+ strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0;
+
+ 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;
+
+ ctx.scale(scaleX, scaleY);
+
+ var w = this.getWidth(),
+ h = this.getHeight();
+
+ ctx.strokeRect(
+ ~~(-(w / 2) - padding - strokeWidth / 2 * this.scaleX) + 0.5, // offset needed to make lines look sharper
+ ~~(-(h / 2) - padding - strokeWidth / 2 * this.scaleY) + 0.5,
+ ~~(w + padding2 + strokeWidth * this.scaleX),
+ ~~(h + padding2 + strokeWidth * this.scaleY)
+ );
+
+ if (this.hasRotatingPoint && !this.get('lockRotation') && this.hasControls) {
+
+ var rotateHeight = (
+ this.flipY
+ ? h + (strokeWidth * this.scaleY) + (padding * 2)
+ : -h - (strokeWidth * this.scaleY) - (padding * 2)
+ ) / 2;
+
+ ctx.beginPath();
+ ctx.moveTo(0, rotateHeight);
+ ctx.lineTo(0, rotateHeight + (this.flipY ? this.rotatingPointOffset : -this.rotatingPointOffset));
+ ctx.closePath();
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ return this;
+ },
+
+ /**
+ * Draws corners of an object's bounding box.
+ * Requires public properties: width, height, scaleX, scaleY
+ * Requires public options: cornerSize, padding
+ * @method drawCorners
+ * @param {CanvasRenderingContext2D} ctx Context to draw on
+ * @return {fabric.Object} thisArg
+ * @chainable
+ */
+ drawCorners: function(ctx) {
+ if (!this.hasControls) return;
+
+ var size = this.cornerSize,
+ size2 = size / 2,
+ strokeWidth2 = this.strokeWidth / 2,
+ left = -(this.width / 2),
+ top = -(this.height / 2),
+ _left,
+ _top,
+ sizeX = size / this.scaleX,
+ sizeY = size / this.scaleY,
+ paddingX = this.padding / this.scaleX,
+ paddingY = this.padding / this.scaleY,
+ scaleOffsetY = size2 / this.scaleY,
+ scaleOffsetX = size2 / this.scaleX,
+ scaleOffsetSizeX = (size2 - size) / this.scaleX,
+ scaleOffsetSizeY = (size2 - size) / this.scaleY,
+ height = this.height,
+ width = this.width,
+ methodName = this.transparentCorners ? 'strokeRect' : 'fillRect',
+ isVML = typeof G_vmlCanvasManager !== 'undefined';
+
+ ctx.save();
+
+ ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY);
+
+ ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1;
+ ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
+
+ // top-left
+ _left = left - scaleOffsetX - strokeWidth2 - paddingX;
+ _top = top - scaleOffsetY - strokeWidth2 - paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // top-right
+ _left = left + width - scaleOffsetX + strokeWidth2 + paddingX;
+ _top = top - scaleOffsetY - strokeWidth2 - paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // bottom-left
+ _left = left - scaleOffsetX - strokeWidth2 - paddingX;
+ _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // bottom-right
+ _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
+ _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ if (!this.get('lockUniScaling')) {
+ // middle-top
+ _left = left + width/2 - scaleOffsetX;
+ _top = top - scaleOffsetY - strokeWidth2 - paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // middle-bottom
+ _left = left + width/2 - scaleOffsetX;
+ _top = top + height + scaleOffsetSizeY + strokeWidth2 + paddingY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // middle-right
+ _left = left + width + scaleOffsetSizeX + strokeWidth2 + paddingX;
+ _top = top + height/2 - scaleOffsetY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+
+ // middle-left
+ _left = left - scaleOffsetX - strokeWidth2 - paddingX;
+ _top = top + height/2 - scaleOffsetY;
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+ }
+
+ // middle-top-rotate
+ if (this.hasRotatingPoint) {
+
+ _left = left + width/2 - scaleOffsetX;
+ _top = this.flipY ?
+ (top + height + (this.rotatingPointOffset / this.scaleY) - sizeY/2 + strokeWidth2 + paddingY)
+ : (top - (this.rotatingPointOffset / this.scaleY) - sizeY/2 - strokeWidth2 - paddingY);
+
+ isVML || ctx.clearRect(_left, _top, sizeX, sizeY);
+ ctx[methodName](_left, _top, sizeX, sizeY);
+ }
+
+ ctx.restore();
+
+ return this;
+ }
+ });
+})();
\ No newline at end of file
diff --git a/src/object_origin.mixin.js b/src/object_origin.mixin.js
new file mode 100644
index 00000000..00981731
--- /dev/null
+++ b/src/object_origin.mixin.js
@@ -0,0 +1,207 @@
+(function() {
+
+ var degreesToRadians = fabric.util.degreesToRadians;
+
+ fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
+
+ /**
+ * Translates the coordinates from origin to center coordinates (based on the object's dimensions)
+ * @method translateToCenterPoint
+ * @param {fabric.Point} point The point which corresponds to the originX and originY params
+ * @param {string} enum('left', 'center', 'right') Horizontal origin
+ * @param {string} enum('top', 'center', 'bottom') Vertical origin
+ * @return {fabric.Point}
+ */
+ translateToCenterPoint: function(point, originX, originY) {
+ var cx = point.x, cy = point.y;
+
+ if ( originX === "left" ) {
+ cx = point.x + this.getWidth() / 2;
+ }
+ else if ( originX === "right" ) {
+ cx = point.x - this.getWidth() / 2;
+ }
+
+ if ( originY === "top" ) {
+ cy = point.y + this.getHeight() / 2;
+ }
+ else if ( originY === "bottom" ) {
+ cy = point.y - this.getHeight() / 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)
+ * @method translateToOriginPoint
+ * @param {fabric.Point} point The point which corresponds to center of the object
+ * @param {string} enum('left', 'center', 'right') Horizontal origin
+ * @param {string} enum('top', 'center', 'bottom') Vertical origin
+ * @return {fabric.Point}
+ */
+ translateToOriginPoint: function(center, originX, originY) {
+ var x = center.x, y = center.y;
+
+ // Get the point coordinates
+ if ( originX === "left" ) {
+ x = center.x - this.getWidth() / 2;
+ }
+ else if ( originX === "right" ) {
+ x = center.x + this.getWidth() / 2;
+ }
+ if ( originY === "top" ) {
+ y = center.y - this.getHeight() / 2;
+ }
+ else if ( originY === "bottom" ) {
+ y = center.y + this.getHeight() / 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
+ * @method getCenterPoint
+ * @return {fabric.Point}
+ */
+ getCenterPoint: function() {
+ return this.translateToCenterPoint(
+ new fabric.Point(this.left, this.top), this.originX, this.originY);
+ },
+
+ /**
+ * Returns the coordinates of the object based on center coordinates
+ * @method getOriginPoint
+ * @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
+ * @method getPointByOrigin
+ * @param {string} enum('left', 'center', 'right') Horizontal origin
+ * @param {string} enum('top', 'center', 'bottom') Vertical origin
+ * @return {fabric.Point}
+ */
+ // getPointByOrigin: function(originX, originY) {
+ // var center = this.getCenterPoint();
+
+ // return this.translateToOriginPoint(center, originX, originY);
+ // },
+
+ /**
+ * Returns the point in local coordinates
+ * @method toLocalPoint
+ * @param {fabric.Point} The point relative to the global coordinate system
+ * @return {fabric.Point}
+ */
+ toLocalPoint: function(point, originX, originY) {
+ var center = this.getCenterPoint();
+
+ var x, y;
+ if (originX !== undefined && originY !== undefined) {
+ if ( originX === "left" ) {
+ x = center.x - this.getWidth() / 2;
+ }
+ else if ( originX === "right" ) {
+ x = center.x + this.getWidth() / 2;
+ }
+ else {
+ x = center.x;
+ }
+
+ if ( originY === "top" ) {
+ y = center.y - this.getHeight() / 2;
+ }
+ else if ( originY === "bottom" ) {
+ y = center.y + this.getHeight() / 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
+ * @method toGlobalPoint
+ * @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
+ * @method setPositionByOrigin
+ * @param {fabric.Point} point The new position of the object
+ * @param {string} enum('left', 'center', 'right') Horizontal origin
+ * @param {string} enum('top', 'center', 'bottom') Vertical origin
+ * @return {void}
+ */
+ setPositionByOrigin: function(pos, originX, originY) {
+ var center = this.translateToCenterPoint(pos, originX, originY);
+ var position = this.translateToOriginPoint(center, this.originX, this.originY);
+
+ this.set('left', position.x);
+ this.set('top', position.y);
+ },
+
+ /**
+ * @method adjustPosition
+ * @param {String} to One of left, center, right
+ */
+ adjustPosition: function(to) {
+
+ var angle = degreesToRadians(this.angle);
+
+ var hypotHalf = this.getWidth() / 2;
+ var xHalf = Math.cos(angle) * hypotHalf;
+ var yHalf = Math.sin(angle) * hypotHalf;
+
+ var hypotFull = this.getWidth();
+ var xFull = Math.cos(angle) * hypotFull;
+ var 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;
+ }
+ });
+
+})();
\ No newline at end of file
diff --git a/src/object_straightening.js b/src/object_straightening.mixin.js
similarity index 96%
rename from src/object_straightening.js
rename to src/object_straightening.mixin.js
index bac388a5..b88aa4ea 100644
--- a/src/object_straightening.js
+++ b/src/object_straightening.mixin.js
@@ -1,4 +1,4 @@
-fabric.util.object.extend(fabric.Object.prototype, {
+fabric.util.object.extend(fabric.Object.prototype, /** @scope fabric.Object.prototype */ {
/**
* @private
diff --git a/src/observable.js b/src/observable.mixin.js
similarity index 100%
rename from src/observable.js
rename to src/observable.mixin.js
diff --git a/src/parser.js b/src/parser.js
index dd5e60dd..bc5c4ca1 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -365,8 +365,8 @@
checkIfDone();
}
}
- catch(e) {
- fabric.log(e.message || e);
+ catch(err) {
+ fabric.log(err);
}
}
else {
diff --git a/src/path.class.js b/src/path.class.js
index d327e880..3605cd09 100644
--- a/src/path.class.js
+++ b/src/path.class.js
@@ -543,21 +543,26 @@
ctx.fillStyle = this.overlayFill;
}
else if (this.fill) {
- ctx.fillStyle = this.fill.toLiveGradient
- ? this.fill.toLiveGradient(ctx)
+ ctx.fillStyle = this.fill.toLive
+ ? this.fill.toLive(ctx)
: this.fill;
}
if (this.stroke) {
- ctx.strokeStyle = this.stroke;
+ ctx.strokeStyle = this.stroke.toLive
+ ? this.stroke.toLive(ctx)
+ : this.stroke;
}
ctx.beginPath();
+ this._setShadow(ctx);
this._render(ctx);
if (this.fill) {
ctx.fill();
}
+ this._removeShadow(ctx);
+
if (this.stroke) {
ctx.strokeStyle = this.stroke;
ctx.lineWidth = this.strokeWidth;
diff --git a/src/path_group.class.js b/src/path_group.class.js
index 229d91aa..f912f92f 100644
--- a/src/path_group.class.js
+++ b/src/path_group.class.js
@@ -73,9 +73,13 @@
}
this.transform(ctx);
+
+ this._setShadow(ctx);
for (var i = 0, l = this.paths.length; i < l; ++i) {
this.paths[i].render(ctx, true);
}
+ this._removeShadow(ctx);
+
if (this.active) {
this.drawBorders(ctx);
this.hideCorners || this.drawCorners(ctx);
diff --git a/src/pattern.class.js b/src/pattern.class.js
new file mode 100644
index 00000000..da00bfc8
--- /dev/null
+++ b/src/pattern.class.js
@@ -0,0 +1,69 @@
+/**
+ * Pattern class
+ * @class Pattern
+ * @memberOf fabric
+ */
+fabric.Pattern = fabric.util.createClass(/** @scope fabric.Pattern.prototype */ {
+
+ /**
+ * Repeat property of a pattern (one of repeat, repeat-x, repeat-y)
+ * @property
+ * @type String
+ */
+ repeat: 'repeat',
+
+ /**
+ * Constructor
+ * @method initialize
+ * @param {Object} [options]
+ * @return {fabric.Pattern} thisArg
+ */
+ initialize: function(options) {
+ options || (options = { });
+
+ if (options.source) {
+ this.source = typeof options.source === 'string'
+ ? new Function(options.source)
+ : options.source;
+ }
+ if (options.repeat) {
+ this.repeat = options.repeat;
+ }
+ },
+
+ /**
+ * Returns object representation of a pattern
+ * @method toObject
+ * @return {Object}
+ */
+ toObject: function() {
+
+ var source;
+
+ // callback
+ if (typeof this.source === 'function') {
+ source = String(this.source)
+ .match(/function\s+\w*\s*\(.*\)\s+\{([\s\S]*)\}/)[1];
+ }
+ //
element
+ else if (typeof this.source.src === 'string') {
+ source = this.source.src;
+ }
+
+ return {
+ source: source,
+ repeat: this.repeat
+ };
+ },
+
+ /**
+ * Returns an instance of CanvasPattern
+ * @method toLive
+ * @param ctx
+ * @return {CanvasPattern}
+ */
+ toLive: function(ctx) {
+ var source = typeof this.source === 'function' ? this.source() : this.source;
+ return ctx.createPattern(source, this.repeat);
+ }
+});
\ No newline at end of file
diff --git a/src/pencil_brush.class.js b/src/pencil_brush.class.js
index 61d50e40..84f896ea 100644
--- a/src/pencil_brush.class.js
+++ b/src/pencil_brush.class.js
@@ -208,6 +208,20 @@
return path;
},
+ /**
+ * Creates fabric.Path object to add on canvas
+ * @method createPath
+ * @param {String} pathData Path data
+ * @return {fabric.Path} path to add on canvas
+ */
+ createPath: function(pathData) {
+ var path = new fabric.Path(pathData);
+ path.fill = null;
+ path.stroke = this.canvas.freeDrawingColor;
+ path.strokeWidth = this.canvas.freeDrawingLineWidth;
+ return path;
+ },
+
/**
* On mouseup after drawing the path on contextTop canvas
* we use the points captured to create an new fabric path object
@@ -219,10 +233,8 @@
var ctx = this.canvas.contextTop;
ctx.closePath();
- var path = this._getSVGPathData();
- path = path.join('');
-
- if (path === "M 0 0 Q 0 0 0 0 L 0 0") {
+ var pathData = this._getSVGPathData().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,
@@ -231,27 +243,23 @@
return;
}
- var p = new fabric.Path(path);
- p.fill = null;
- p.stroke = this.color;
- p.strokeWidth = this.width;
- this.canvas.add(p);
-
// set path origin coordinates based on our bounding box
var originLeft = this.box.minx + (this.box.maxx - this.box.minx) /2;
var originTop = this.box.miny + (this.box.maxy - this.box.miny) /2;
this.canvas.contextTop.arc(originLeft, originTop, 3, 0, Math.PI * 2, false);
- p.set({ left: originLeft, top: originTop });
+ var path = this.createPath(pathData);
+ path.set({ left: originLeft, top: originTop });
- // does not change position
- p.setCoords();
+ this.canvas.add(path);
+ path.setCoords();
+ this.canvas.contextTop && this.canvas.clearContext(this.canvas.contextTop);
this.canvas.renderAll();
// fire event 'path' created
- this.canvas.fire('path:created', { path: p });
+ this.canvas.fire('path:created', { path: path });
}
});
-})();
\ No newline at end of file
+})();
diff --git a/src/polygon.class.js b/src/polygon.class.js
index 1af1e67a..e3b30379 100644
--- a/src/polygon.class.js
+++ b/src/polygon.class.js
@@ -56,6 +56,15 @@
this.width = (maxX - minX) || 1;
this.height = (maxY - minY) || 1;
+ // var halfWidth = this.width / 2,
+ // halfHeight = this.height / 2;
+
+ // change points to offset polygon into a bounding box
+ // this.points.forEach(function(p) {
+ // p.x -= halfWidth;
+ // p.y -= halfHeight;
+ // }, this);
+
this.minX = minX;
this.minY = minY;
},
@@ -108,6 +117,7 @@
if (this.fill) {
ctx.fill();
}
+ this._removeShadow(ctx);
if (this.stroke) {
ctx.closePath();
ctx.stroke();
diff --git a/src/polyline.class.js b/src/polyline.class.js
index 21255d83..b725491f 100644
--- a/src/polyline.class.js
+++ b/src/polyline.class.js
@@ -92,6 +92,7 @@
if (this.fill) {
ctx.fill();
}
+ this._removeShadow(ctx);
if (this.stroke) {
ctx.stroke();
}
diff --git a/src/rect.class.js b/src/rect.class.js
index f5585ccc..0a7d6bde 100644
--- a/src/rect.class.js
+++ b/src/rect.class.js
@@ -121,6 +121,8 @@
ctx.fill();
}
+ this._removeShadow(ctx);
+
if (this.strokeDashArray) {
this._renderDashedStroke(ctx);
}
@@ -129,6 +131,67 @@
}
},
+ /**
+ * @private
+ * @method _renderDashedStroke
+ */
+ _renderDashedStroke: function(ctx) {
+
+ if (1 & this.strokeDashArray.length /* if odd number of items */) {
+ /* duplicate items */
+ this.strokeDashArray.push.apply(this.strokeDashArray, this.strokeDashArray);
+ }
+
+ var i = 0,
+ x = -this.width/2, y = -this.height/2,
+ _this = this,
+ padding = this.padding,
+ dashedArrayLength = this.strokeDashArray.length;
+
+ ctx.save();
+ ctx.beginPath();
+
+ /** @ignore */
+ function renderSide(xMultiplier, yMultiplier) {
+
+ var lineLength = 0,
+ lengthDiff = 0,
+ sideLength = (yMultiplier ? _this.height : _this.width) + padding * 2;
+
+ while (lineLength < sideLength) {
+
+ var lengthOfSubPath = _this.strokeDashArray[i++];
+ lineLength += lengthOfSubPath;
+
+ if (lineLength > sideLength) {
+ lengthDiff = lineLength - sideLength;
+ }
+
+ // track coords
+ if (xMultiplier) {
+ x += (lengthOfSubPath * xMultiplier) - (lengthDiff * xMultiplier || 0);
+ }
+ else {
+ y += (lengthOfSubPath * yMultiplier) - (lengthDiff * yMultiplier || 0);
+ }
+
+ ctx[1 & i /* odd */ ? 'moveTo' : 'lineTo'](x, y);
+ if (i >= dashedArrayLength) {
+ i = 0;
+ }
+ }
+ }
+
+ renderSide(1, 0);
+ renderSide(0, 1);
+ renderSide(-1, 0);
+ renderSide(0, -1);
+
+ ctx.stroke();
+ ctx.closePath();
+ ctx.restore();
+ },
+
/**
* @method _normalizeLeftTopProperties
* @private
diff --git a/src/shadow.class.js b/src/shadow.class.js
new file mode 100644
index 00000000..259c9b88
--- /dev/null
+++ b/src/shadow.class.js
@@ -0,0 +1,70 @@
+/**
+ * Shadow class
+ * @class Shadow
+ * @memberOf fabric
+ */
+fabric.Shadow = fabric.util.createClass(/** @scope fabric.Shadow.prototype */ {
+
+ /**
+ * Shadow color
+ * @property
+ * @type String
+ */
+ color: 'rgb(0,0,0)',
+
+ /**
+ * Shadow blur
+ * @property
+ * @type Number
+ */
+ blur: 0,
+
+ /**
+ * Shadow horizontal offset
+ * @property
+ * @type Number
+ */
+ offsetX: 0,
+
+ /**
+ * Shadow vertical offset
+ * @property
+ * @type Number
+ */
+ offsetY: 0,
+
+ /**
+ * Constructor
+ * @method initialize
+ * @param [options] Options object with any of color, blur, offsetX, offsetX properties
+ * @return {fabric.Shadow} thisArg
+ */
+ initialize: function(options) {
+ for (var prop in options) {
+ this[prop] = options[prop];
+ }
+ },
+
+ /**
+ * Returns object representation of a shadow
+ * @method toObject
+ * @return {Object}
+ */
+ toObject: function() {
+ return {
+ color: this.color,
+ blur: this.blur,
+ offsetX: this.offsetX,
+ offsetY: this.offsetY
+ };
+ },
+
+ /**
+ * Returns SVG representation of a shadow
+ * @method toSVG
+ * @return {String}
+ */
+ toSVG: function() {
+
+ }
+});
\ No newline at end of file
diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js
index 95e25b7f..e43acd3e 100644
--- a/src/static_canvas.class.js
+++ b/src/static_canvas.class.js
@@ -148,6 +148,9 @@
if (options.backgroundImage) {
this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this));
}
+ if (options.backgroundColor) {
+ this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this));
+ }
this.calcOffset();
},
@@ -211,6 +214,33 @@
return this;
},
+ /**
+ * Sets background color for this canvas
+ * @method setBackgroundColor
+ * @param {String|fabric.Pattern} Color of pattern to set background color to
+ * @param {Function} callback callback to invoke when background color is set
+ * @return {fabric.Canvas} thisArg
+ * @chainable
+ */
+ setBackgroundColor: function(backgroundColor, callback) {
+ if (backgroundColor.source) {
+ var _this = this;
+ fabric.util.loadImage(backgroundColor.source, function(img) {
+ _this.backgroundColor = new fabric.Pattern({
+ source: img,
+ pattern: backgroundColor.pattern
+ });
+ callback && callback();
+ });
+ }
+ else {
+ this.backgroundColor = backgroundColor;
+ callback && callback();
+ }
+
+ return this;
+ },
+
/**
* @private
* @method _createCanvasElement
@@ -232,12 +262,8 @@
* @param {HTMLElement} element
*/
_initCanvasElement: function(element) {
- if (typeof element.getContext === 'undefined' &&
- typeof G_vmlCanvasManager !== 'undefined' &&
- G_vmlCanvasManager.initElement) {
+ fabric.util.createCanvasElement(element);
- G_vmlCanvasManager.initElement(element);
- }
if (typeof element.getContext === 'undefined') {
throw CANVAS_INIT_ERROR;
}
@@ -510,6 +536,7 @@
if (this.contextTop) {
this.clearContext(this.contextTop);
}
+ this.fire('canvas:cleared');
this.renderAll();
return this;
},
@@ -533,12 +560,17 @@
this.clearContext(canvasToDrawOn);
}
+ this.fire('before:render');
+
if (this.clipTo) {
this._clipCanvas(canvasToDrawOn);
}
if (this.backgroundColor) {
- canvasToDrawOn.fillStyle = this.backgroundColor;
+ canvasToDrawOn.fillStyle = this.backgroundColor.toLive
+ ? this.backgroundColor.toLive(canvasToDrawOn)
+ : this.backgroundColor;
+
canvasToDrawOn.fillRect(0, 0, this.width, this.height);
}
@@ -546,8 +578,6 @@
this._drawBackroundImage(canvasToDrawOn);
}
- this.fire('before:render');
-
var activeGroup = this.getActiveGroup();
for (var i = 0, length = this._objects.length; i < length; ++i) {
if (!activeGroup ||
@@ -687,6 +717,8 @@
var data = (fabric.StaticCanvas.supports('toDataURLWithQuality'))
? canvasEl.toDataURL('image/' + format, quality)
: canvasEl.toDataURL('image/' + format);
+
+ this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return data;
},
@@ -740,6 +772,7 @@
this.setActiveObject(activeObject);
}
+ this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return dataURL;
@@ -874,7 +907,9 @@
}
return object;
}, this),
- background: this.backgroundColor
+ background: (this.backgroundColor && this.backgroundColor.toObject)
+ ? this.backgroundColor.toObject()
+ : this.backgroundColor
};
if (this.backgroundImage) {
data.backgroundImage = this.backgroundImage.src;
@@ -894,13 +929,22 @@
* Returns SVG representation of canvas
* @function
* @method toSVG
+ * @param {Object} [options] Options for SVG output ("suppressPreamble: true"
+ * will start the svg output directly at "',
- '',
+ toSVG: function(options) {
+ options || (options = { });
+ var markup = [];
+
+ if (!options.suppressPreamble) {
+ markup.push(
+ '',
+ ''
+ );
+ }
+ markup.push(
'