diff --git a/HEADER.js b/HEADER.js index 68a4b2810ab..f2e5bf5983f 100644 --- a/HEADER.js +++ b/HEADER.js @@ -61,6 +61,7 @@ fabric.SHARED_ATTRIBUTES = [ fabric.DPI = 96; fabric.reNum = '(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)'; fabric.fontPaths = { }; +fabric.iMatrix = [1, 0, 0, 1, 0, 0]; /** * Cache Object for widths of chars in text rendering. diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 07e50e1214c..d06a0ebd86e 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -1,11 +1,11 @@ (function() { - function getCoords(oCoords) { + function getCoords(coords) { return [ - new fabric.Point(oCoords.tl.x, oCoords.tl.y), - new fabric.Point(oCoords.tr.x, oCoords.tr.y), - new fabric.Point(oCoords.br.x, oCoords.br.y), - new fabric.Point(oCoords.bl.x, oCoords.bl.y) + new fabric.Point(coords.tl.x, coords.tl.y), + new fabric.Point(coords.tr.x, coords.tr.y), + new fabric.Point(coords.br.x, coords.br.y), + new fabric.Point(coords.bl.x, coords.bl.y) ]; } @@ -15,22 +15,56 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ { /** - * Object containing coordinates of object's controls - * @type Object - * @default + * Describe object's corner position in canvas element coordinates. + * properties are tl,mt,tr,ml,mr,bl,mb,br,mtr for the main controls. + * each property is an object with x, y and corner. + * The `corner` property contains in a similar manner the 4 points of the + * interactive area of the corner. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, viewportTransform, top, left, padding. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords; + * @memberOf fabric.Object.prototype */ oCoords: null, + /** + * Describe object's corner position in canvas object absolute coordinates + * properties are tl,tr,bl,br and describe the four main corner. + * each property is an object with x, y, instance of Fabric.Point. + * The coordinates depends from this properties: width, height, scaleX, scaleY + * skewX, skewY, angle, strokeWidth, top, left. + * Those coordinates are usefull to understand where an object is. They get updated + * with oCoords but they do not need to be updated when zoom or panning change. + * The coordinates get updated with @method setCoords. + * You can calculate them without updating with @method calcCoords(true); + * @memberOf fabric.Object.prototype + */ + aCoords: null, + + /** + * return correct set of coordinates for intersection + */ + getCoords: function(absolute, calculate) { + if (!this.oCoords) { + this.setCoords(); + } + var coords = absolute ? this.aCoords : this.oCoords; + return getCoords(calculate ? this.calcCoords(absolute) : coords); + }, + /** * Checks if object intersects with an area formed by 2 points * @param {Object} pointTL top-left point of area * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Boolean} true if object intersects with an area formed by 2 points */ - intersectsWithRect: function(pointTL, pointBR) { - var oCoords = getCoords(this.oCoords), + intersectsWithRect: function(pointTL, pointBR, absolute, calculate) { + var coords = this.getCoords(absolute, calculate), intersection = fabric.Intersection.intersectPolygonRectangle( - oCoords, + coords, pointTL, pointBR ); @@ -40,29 +74,35 @@ /** * Checks if object intersects with another object * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Boolean} true if object intersects with another object */ - intersectsWithObject: function(other) { + intersectsWithObject: function(other, absolute, calculate) { var intersection = fabric.Intersection.intersectPolygonPolygon( - getCoords(this.oCoords), - getCoords(other.oCoords) + this.getCoords(absolute, calculate), + other.getCoords(absolute, calculate) ); return intersection.status === 'Intersection' - || other.isContainedWithinObject(this) - || this.isContainedWithinObject(other); + || other.isContainedWithinObject(this, absolute, calculate) + || this.isContainedWithinObject(other, absolute, calculate); }, /** * Checks if object is fully contained within area of another object * @param {Object} other Object to test + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Boolean} true if object is fully contained within area of another object */ - isContainedWithinObject: function(other) { - var points = getCoords(this.oCoords), - i = 0; + isContainedWithinObject: function(other, absolute, calculate) { + var points = this.getCoords(absolute, calculate), + i = 0, lines = other._getImageLines( + calculate ? other.calcCoords(absolute) : absolute ? other.aCoords : other.oCoords + ); for (; i < 4; i++) { - if (!other.containsPoint(points[i])) { + if (!other.containsPoint(points[i], lines)) { return false; } } @@ -73,10 +113,12 @@ * Checks if object is fully contained within area formed by 2 points * @param {Object} pointTL top-left point of area * @param {Object} pointBR bottom-right point of area + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Boolean} true if object is fully contained within area formed by 2 points */ - isContainedWithinRect: function(pointTL, pointBR) { - var boundingRect = this.getBoundingRect(); + isContainedWithinRect: function(pointTL, pointBR, absolute, calculate) { + var boundingRect = this.getBoundingRect(absolute, calculate); return ( boundingRect.left >= pointTL.x && @@ -89,19 +131,42 @@ /** * Checks if point is inside the object * @param {fabric.Point} point Point to check against + * @param {Object} [lines] object returned from @method _getImageLines + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Boolean} true if point is inside the object */ - containsPoint: function(point) { - if (!this.oCoords) { - this.setCoords(); - } - var lines = this._getImageLines(this.oCoords), + containsPoint: function(point, lines, absolute, calculate) { + var lines = lines || this._getImageLines( + calculate ? this.calcCoords(absolute) : absolute ? this.aCoords : this.oCoords + ), xPoints = this._findCrossPoints(point, lines); // if xPoints is odd then point is inside the object return (xPoints !== 0 && xPoints % 2 === 1); }, + /** + * Checks if object is contained within the canvas with current viewportTransform + * the check is done stopping at first point that appear on screen + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords + * @return {Boolean} true if object is fully contained within canvas + */ + isOnScreen: function(calculate) { + if (!this.canvas) { + return false; + } + var pointTL = this.canvas.vptCoords.tl, pointBR = this.canvas.vptCoords.br; + var points = this.getCoords(true, calculate), point; + for (var i = 0; i < 4; i++) { + point = points[i]; + if (point.x <= pointBR.x && point.x >= pointTL.x && point.y <= pointBR.y && point.y >= pointTL.y) { + return true; + } + } + return false; + }, + /** * Method that returns an object with the object edges in it, given the coordinates of the corners * @private @@ -133,16 +198,16 @@ * and the horizontal line determined by a point on canvas * @private * @param {fabric.Point} point Point to check - * @param {Object} oCoords Coordinates of the object being evaluated + * @param {Object} lines Coordinates of the object being evaluated */ // remove yi, not used but left code here just in case. - _findCrossPoints: function(point, oCoords) { + _findCrossPoints: function(point, lines) { var b1, b2, a1, a2, xi, // yi, xcount = 0, iLine; - for (var lineKey in oCoords) { - iLine = oCoords[lineKey]; + for (var lineKey in lines) { + iLine = lines[lineKey]; // optimisation 1: line below point. no cross if ((iLine.o.y < point.y) && (iLine.d.y < point.y)) { continue; @@ -199,17 +264,13 @@ /** * Returns coordinates of object's bounding rectangle (left, top, width, height) * the box is intented as aligned to axis of canvas. - * @param {Boolean} ignoreVpt bounding box will not be affected by viewportTransform + * @param {Boolean} [absolute] use coordinates without viewportTransform + * @param {Boolean} [calculate] use coordinates of current position instead of .oCoords * @return {Object} Object with left, top, width, height properties */ - getBoundingRect: function(ignoreVpt) { - var coords = this.calcCoords(ignoreVpt); - return fabric.util.makeBoundingBoxFromPoints([ - coords.tl, - coords.tr, - coords.br, - coords.bl - ]); + getBoundingRect: function(absolute, calculate) { + var coords = this.getCoords(absolute, calculate); + return fabric.util.makeBoundingBoxFromPoints(coords); }, /** @@ -296,10 +357,10 @@ * @return {Object} Object with tl, tr, br, bl .... * @chainable */ - calcCoords: function(ignoreVpt) { + calcCoords: function(absolute) { var theta = degreesToRadians(this.angle), vpt = this.getViewportTransform(), - dim = ignoreVpt ? this._getTransformedDimensions() : this._calculateCurrentDimensions(), + dim = absolute ? this._getTransformedDimensions() : this._calculateCurrentDimensions(), currentWidth = dim.x, currentHeight = dim.y, sinTh = Math.sin(theta), cosTh = Math.cos(theta), @@ -309,12 +370,12 @@ offsetY = Math.sin(_angle + theta) * _hypotenuse, center = this.getCenterPoint(), // offset added for rotate and scale actions - coords = ignoreVpt ? center : fabric.util.transformPoint(center, vpt), + coords = absolute ? center : fabric.util.transformPoint(center, vpt), tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY), tr = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)), bl = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)), br = new fabric.Point(coords.x + offsetX, coords.y + offsetY); - if (!ignoreVpt) { + if (!absolute) { var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2), mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2), mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2), @@ -341,7 +402,7 @@ // corners tl: tl, tr: tr, br: br, bl: bl, }; - if (!ignoreVpt) { + if (!absolute) { // middle coords.ml = ml; coords.mt = mt; @@ -356,22 +417,24 @@ /** * Sets corner position coordinates based on current angle, width and height * See https://github.com/kangax/fabric.js/wiki/When-to-call-setCoords + * @param {Boolean} [ignoreZoom] set oCoords with or without the viewport transform. + * @param {Boolean} [skipAbsolute] skip calculation of aCoords, usefull in setViewportTransform * @return {fabric.Object} thisArg * @chainable */ setCoords: function(ignoreZoom, skipAbsolute) { this.oCoords = this.calcCoords(ignoreZoom); - if (!skipAbsolute && !ignoreZoom) { - this.absoluteCoords = this.calcCoords(true); + if (!skipAbsolute) { + this.aCoords = this.calcCoords(true); } // set coordinates of the draggable boxes in the corners used to scale/rotate the image - ignoreZoom || this._setCornerCoords && this._setCornerCoords(); + ignoreZoom || (this._setCornerCoords && this._setCornerCoords()); return this; }, - /* + /** * calculate rotation matrix of an object * @return {Array} rotation matrix for the object */ @@ -380,20 +443,21 @@ var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta); return [cos, sin, -sin, cos, 0, 0]; } - return [1, 0, 0, 1, 0, 0]; + return fabric.iMatrix.concat(); }, - /* + /** * calculate trasform Matrix that represent current transformation from * object properties. + * @param {Boolean} [skipGroup] return transformMatrix for object and not go upward with parents * @return {Array} matrix Transform Matrix for the object */ - calcTransformMatrix: function() { + calcTransformMatrix: function(skipGroup) { var center = this.getCenterPoint(), translateMatrix = [1, 0, 0, 1, center.x, center.y], rotateMatrix = this._calcRotateMatrix(), dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true), - matrix = this.group ? this.group.calcTransformMatrix() : [1, 0, 0, 1, 0, 0]; + matrix = this.group && !skipGroup ? this.group.calcTransformMatrix() : fabric.iMatrix.concat(); matrix = multiplyMatrices(matrix, translateMatrix); matrix = multiplyMatrices(matrix, rotateMatrix); matrix = multiplyMatrices(matrix, dimensionMatrix); diff --git a/src/shapes/circle.class.js b/src/shapes/circle.class.js index 401178a83d1..9bcca05a5e8 100644 --- a/src/shapes/circle.class.js +++ b/src/shapes/circle.class.js @@ -176,14 +176,6 @@ this.radius = value; return this.set('width', value * 2).set('height', value * 2); }, - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function() { - return 1; - } }); /* _FROM_SVG_START_ */ diff --git a/src/shapes/ellipse.class.js b/src/shapes/ellipse.class.js index 4829a965c48..6614707f33a 100644 --- a/src/shapes/ellipse.class.js +++ b/src/shapes/ellipse.class.js @@ -156,14 +156,6 @@ this._renderFill(ctx); this._renderStroke(ctx); }, - - /** - * Returns complexity of an instance - * @return {Number} complexity - */ - complexity: function() { - return 1; - } }); /* _FROM_SVG_START_ */ diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 19e476ed4f8..469942501ea 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -128,15 +128,13 @@ var objectLeft = object.getLeft(), objectTop = object.getTop(), - ignoreZoom = true; + ignoreZoom = true, skipAbsolute = true; object.set({ - originalLeft: objectLeft, - originalTop: objectTop, left: objectLeft - center.x, top: objectTop - center.y }); - object.setCoords(ignoreZoom); + object.setCoords(ignoreZoom, skipAbsolute); }, /** @@ -423,14 +421,14 @@ }, /** - * Sets coordinates of all group objects + * Sets coordinates of all objects inside group * @return {fabric.Group} thisArg * @chainable */ setObjectsCoords: function() { - var ignoreZoom = true; + var ignoreZoom = true, skipAbsolute = true; this.forEachObject(function(object) { - object.setCoords(ignoreZoom); + object.setCoords(ignoreZoom, skipAbsolute); }); return this; }, diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index 25093787f20..fecbedc6dc3 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -597,14 +597,6 @@ ? this.getElement().height || 0 : 0); }, - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function() { - return 1; - } }); /** diff --git a/src/shapes/line.class.js b/src/shapes/line.class.js index c7293cea434..c1289567ea4 100644 --- a/src/shapes/line.class.js +++ b/src/shapes/line.class.js @@ -280,14 +280,6 @@ return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ - - /** - * Returns complexity of an instance - * @return {Number} complexity - */ - complexity: function() { - return 1; - } }); /* _FROM_SVG_START_ */ diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 19016de1a83..6fa9461857d 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -1111,7 +1111,7 @@ if (this.canvas && this.canvas.viewportTransform) { return this.canvas.viewportTransform; } - return [1, 0, 0, 1, 0, 0]; + return fabric.iMatrix.concat(); }, /** @@ -1497,10 +1497,10 @@ /** * Returns complexity of an instance - * @return {Number} complexity of this instance + * @return {Number} complexity of this instance (is 1 unless subclassed) */ complexity: function() { - return 0; + return 1; }, /** diff --git a/src/shapes/polygon.class.js b/src/shapes/polygon.class.js index a56138b0e49..c8676c45461 100644 --- a/src/shapes/polygon.class.js +++ b/src/shapes/polygon.class.js @@ -3,26 +3,20 @@ 'use strict'; var fabric = global.fabric || (global.fabric = { }), - extend = fabric.util.object.extend, - min = fabric.util.array.min, - max = fabric.util.array.max, - toFixed = fabric.util.toFixed; + extend = fabric.util.object.extend; if (fabric.Polygon) { fabric.warn('fabric.Polygon is already defined'); return; } - var cacheProperties = fabric.Object.prototype.cacheProperties.concat(); - cacheProperties.push('points'); - /** * Polygon class * @class fabric.Polygon - * @extends fabric.Object + * @extends fabric.Polyline * @see {@link fabric.Polygon#initialize} for constructor definition */ - fabric.Polygon = fabric.util.createClass(fabric.Object, /** @lends fabric.Polygon.prototype */ { + fabric.Polygon = fabric.util.createClass(fabric.Polyline, /** @lends fabric.Polygon.prototype */ { /** * Type of an object @@ -31,109 +25,6 @@ */ type: 'polygon', - /** - * Points array - * @type Array - * @default - */ - points: null, - - /** - * Minimum X from points values, necessary to offset points - * @type Number - * @default - */ - minX: 0, - - /** - * Minimum Y from points values, necessary to offset points - * @type Number - * @default - */ - minY: 0, - - cacheProperties: cacheProperties, - - /** - * Constructor - * @param {Array} points Array of points - * @param {Object} [options] Options object - * @return {fabric.Polygon} thisArg - */ - initialize: function(points, options) { - options = options || {}; - this.points = points || []; - this.callSuper('initialize', options); - this._calcDimensions(); - if (!('top' in options)) { - this.top = this.minY; - } - if (!('left' in options)) { - this.left = this.minX; - } - this.pathOffset = { - x: this.minX + this.width / 2, - y: this.minY + this.height / 2 - }; - }, - - /** - * @private - */ - _calcDimensions: function() { - - var points = this.points, - minX = min(points, 'x'), - minY = min(points, 'y'), - maxX = max(points, 'x'), - maxY = max(points, 'y'); - - this.width = (maxX - minX) || 0; - this.height = (maxY - minY) || 0; - this.minX = minX || 0; - this.minY = minY || 0; - }, - - /** - * Returns object representation of an instance - * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output - * @return {Object} Object representation of an instance - */ - toObject: function(propertiesToInclude) { - return extend(this.callSuper('toObject', propertiesToInclude), { - points: this.points.concat() - }); - }, - - /* _TO_SVG_START_ */ - /** - * Returns svg representation of an instance - * @param {Function} [reviver] Method for further parsing of svg representation. - * @return {String} svg representation of an instance - */ - toSVG: function(reviver) { - var points = [], addTransform, - markup = this._createBaseSVGMarkup(); - - for (var i = 0, len = this.points.length; i < len; i++) { - points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); - } - if (!(this.group && this.group.type === 'path-group')) { - addTransform = ' translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ') '; - } - markup.push( - '<', this.type, ' ', this.getSvgId(), - 'points="', points.join(''), - '" style="', this.getSvgStyles(), - '" transform="', this.getSvgTransform(), addTransform, - ' ', this.getSvgTransformMatrix(), - '"/>\n' - ); - - return reviver ? reviver(markup.join('')) : markup.join(''); - }, - /* _TO_SVG_END_ */ - /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on @@ -143,35 +34,9 @@ if (!this.commonRender(ctx, noTransform)) { return; } + ctx.closePath(); this._renderFill(ctx); - if (this.stroke || this.strokeDashArray) { - ctx.closePath(); - this._renderStroke(ctx); - } - }, - - /** - * @private - * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} noTransform - */ - commonRender: function(ctx, noTransform) { - var point, len = this.points.length, - x = noTransform ? 0 : this.pathOffset.x, - y = noTransform ? 0 : this.pathOffset.y; - - if (!len || isNaN(this.points[len - 1].y)) { - // do not draw if no points or odd points - // NaN comes from parseFloat of a empty string in parser - return false; - } - ctx.beginPath(); - ctx.moveTo(this.points[0].x - x, this.points[0].y - y); - for (var i = 0; i < len; i++) { - point = this.points[i]; - ctx.lineTo(point.x - x, point.y - y); - } - return true; + this._renderStroke(ctx); }, /** @@ -179,17 +44,9 @@ * @param {CanvasRenderingContext2D} ctx Context to render on */ _renderDashedStroke: function(ctx) { - fabric.Polyline.prototype._renderDashedStroke.call(this, ctx); + this.callSuper('_renderDashedStroke', ctx); ctx.closePath(); }, - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function() { - return this.points.length; - } }); /* _FROM_SVG_START_ */ diff --git a/src/shapes/polyline.class.js b/src/shapes/polyline.class.js index e0e6fd5e376..86d482230a3 100644 --- a/src/shapes/polyline.class.js +++ b/src/shapes/polyline.class.js @@ -2,7 +2,11 @@ 'use strict'; - var fabric = global.fabric || (global.fabric = { }); + var fabric = global.fabric || (global.fabric = { }), + extend = fabric.util.object.extend, + min = fabric.util.array.min, + max = fabric.util.array.max, + toFixed = fabric.util.toFixed; if (fabric.Polyline) { fabric.warn('fabric.Polyline is already defined'); @@ -70,14 +74,37 @@ * }); */ initialize: function(points, options) { - return fabric.Polygon.prototype.initialize.call(this, points, options); + options = options || {}; + this.points = points || []; + this.callSuper('initialize', options); + this._calcDimensions(); + if (!('top' in options)) { + this.top = this.minY; + } + if (!('left' in options)) { + this.left = this.minX; + } + this.pathOffset = { + x: this.minX + this.width / 2, + y: this.minY + this.height / 2 + }; }, /** * @private */ _calcDimensions: function() { - return fabric.Polygon.prototype._calcDimensions.call(this); + + var points = this.points, + minX = min(points, 'x'), + minY = min(points, 'y'), + maxX = max(points, 'x'), + maxY = max(points, 'y'); + + this.width = (maxX - minX) || 0; + this.height = (maxY - minY) || 0; + this.minX = minX || 0; + this.minY = minY || 0; }, /** @@ -86,27 +113,72 @@ * @return {Object} Object representation of an instance */ toObject: function(propertiesToInclude) { - return fabric.Polygon.prototype.toObject.call(this, propertiesToInclude); + return extend(this.callSuper('toObject', propertiesToInclude), { + points: this.points.concat() + }); }, /* _TO_SVG_START_ */ /** - * Returns SVG representation of an instance + * Returns svg representation of an instance * @param {Function} [reviver] Method for further parsing of svg representation. * @return {String} svg representation of an instance */ toSVG: function(reviver) { - return fabric.Polygon.prototype.toSVG.call(this, reviver); + var points = [], addTransform, + markup = this._createBaseSVGMarkup(); + + for (var i = 0, len = this.points.length; i < len; i++) { + points.push(toFixed(this.points[i].x, 2), ',', toFixed(this.points[i].y, 2), ' '); + } + if (!(this.group && this.group.type === 'path-group')) { + addTransform = ' translate(' + (-this.pathOffset.x) + ', ' + (-this.pathOffset.y) + ') '; + } + markup.push( + '<', this.type, ' ', this.getSvgId(), + 'points="', points.join(''), + '" style="', this.getSvgStyles(), + '" transform="', this.getSvgTransform(), addTransform, + ' ', this.getSvgTransformMatrix(), + '"/>\n' + ); + + return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ + + /** + * @private + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} noTransform + */ + commonRender: function(ctx, noTransform) { + var point, len = this.points.length, + x = noTransform ? 0 : this.pathOffset.x, + y = noTransform ? 0 : this.pathOffset.y; + + if (!len || isNaN(this.points[len - 1].y)) { + // do not draw if no points or odd points + // NaN comes from parseFloat of a empty string in parser + return false; + } + ctx.beginPath(); + ctx.moveTo(this.points[0].x - x, this.points[0].y - y); + for (var i = 0; i < len; i++) { + point = this.points[i]; + ctx.lineTo(point.x - x, point.y - y); + } + return true; + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on * @param {Boolean} noTransform */ _render: function(ctx, noTransform) { - if (!fabric.Polygon.prototype.commonRender.call(this, ctx, noTransform)) { + if (!this.commonRender(ctx, noTransform)) { return; } this._renderFill(ctx); diff --git a/src/shapes/rect.class.js b/src/shapes/rect.class.js index 815084c4a90..4ef74370895 100644 --- a/src/shapes/rect.class.js +++ b/src/shapes/rect.class.js @@ -175,14 +175,6 @@ return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ - - /** - * Returns complexity of an instance - * @return {Number} complexity - */ - complexity: function() { - return 1; - } }); /* _FROM_SVG_START_ */ diff --git a/src/shapes/triangle.class.js b/src/shapes/triangle.class.js index 9f831b2f372..0b5fb45bc59 100644 --- a/src/shapes/triangle.class.js +++ b/src/shapes/triangle.class.js @@ -97,14 +97,6 @@ return reviver ? reviver(markup.join('')) : markup.join(''); }, /* _TO_SVG_END_ */ - - /** - * Returns complexity of an instance - * @return {Number} complexity of this instance - */ - complexity: function() { - return 1; - } }); /** diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index 7969ee41afb..1f0c46898f5 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -12,6 +12,8 @@ getElementOffset = fabric.util.getElementOffset, removeFromArray = fabric.util.removeFromArray, toFixed = fabric.util.toFixed, + transformPoint = fabric.util.transformPoint, + invertTransform = fabric.util.invertTransform, CANVAS_INIT_ERROR = new Error('Could not initialize `canvas` element'); @@ -138,7 +140,7 @@ * @type Array * @default */ - viewportTransform: [1, 0, 0, 1, 0, 0], + viewportTransform: fabric.iMatrix.concat(), /** * if set to false background image is not affected by viewport transform @@ -168,6 +170,17 @@ */ enableRetinaScaling: true, + /** + * Describe canvas element extension over design + * properties are tl,tr,bl,br. + * if canvas is not zoomed/panned those points are the four corner of canvas + * if canvas is viewportTransformed you those points indicate the extension + * of canvas element in plain untrasformed coordinates + * The coordinates get updated with @method calcViewportBoundaries. + * @memberOf fabric.StaticCanvas.prototype + */ + vptCoords: { }, + /** * @private * @param {HTMLElement | String} el <canvas> element to initialize instance on @@ -648,6 +661,7 @@ if (activeGroup) { activeGroup.setCoords(ingoreVpt, skipAbsolute); } + this.calcViewportBoundaries(); this.renderAll(); return this; }, @@ -662,10 +676,10 @@ zoomToPoint: function (point, value) { // TODO: just change the scale, preserve other transformations var before = point, vpt = this.viewportTransform.slice(0); - point = fabric.util.transformPoint(point, fabric.util.invertTransform(this.viewportTransform)); + point = transformPoint(point, invertTransform(this.viewportTransform)); vpt[0] = value; vpt[3] = value; - var after = fabric.util.transformPoint(point, vpt); + var after = transformPoint(point, vpt); vpt[4] += before.x - after.x; vpt[5] += before.y - after.y; return this.setViewportTransform(vpt); @@ -782,7 +796,7 @@ }, /** - * Renders both the canvas. + * Renders the canvas * @return {fabric.Canvas} instance * @chainable */ @@ -792,6 +806,24 @@ return this; }, + /** + * Calculate the position of the 4 corner of canvas with current viewportTransform. + * helps to determinate when an object is in the current rendering viewport using + * object absolute coordinates ( aCoords ) + * @return {Object} points.tl + * @chainable + */ + calcViewportBoundaries: function() { + var points = { }, width = this.getWidth(), height = this.getHeight(), + iVpt = invertTransform(this.viewportTransform); + points.tl = transformPoint({ x: 0, y: 0 }, iVpt); + points.br = transformPoint({ x: width, y: height }, iVpt); + points.tr = new fabric.Point(points.br.x, points.tl.y); + points.bl = new fabric.Point(points.tl.x, points.br.y); + this.vptCoords = points; + return points; + }, + /** * Renders background, objects, overlay and controls. * @param {CanvasRenderingContext2D} ctx @@ -800,6 +832,7 @@ * @chainable */ renderCanvas: function(ctx, objects) { + this.calcViewportBoundaries(); this.clearContext(ctx); this.fire('before:render'); if (this.clipTo) { @@ -973,8 +1006,8 @@ */ getVpCenter: function() { var center = this.getCenter(), - iVpt = fabric.util.invertTransform(this.viewportTransform); - return fabric.util.transformPoint({ x: center.left, y: center.top }, iVpt); + iVpt = invertTransform(this.viewportTransform); + return transformPoint({ x: center.left, y: center.top }, iVpt); }, /** diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 70400637300..97fa4835c9e 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -1399,7 +1399,7 @@ }); test('relativePan', function() { - ok(typeof canvas.relativePan == 'function'); + ok(typeof canvas.relativePan === 'function'); deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix'); var point = new fabric.Point(-50, -50); canvas.relativePan(point); @@ -1410,11 +1410,38 @@ }); test('getContext', function() { - ok(typeof canvas.getContext == 'function'); + ok(typeof canvas.getContext === 'function'); var context = canvas.getContext(); equal(context, canvas.contextContainer, 'should return the context container'); }); + test('calcViewportBoundaries', function() { + ok(typeof canvas.calcViewportBoundaries === 'function'); + canvas.calcViewportBoundaries(); + deepEqual(canvas.vptCoords.tl, new fabric.Point(0, 0), 'tl is 0,0'); + deepEqual(canvas.vptCoords.tr, new fabric.Point(canvas.getWidth(), 0), 'tr is width, 0'); + deepEqual(canvas.vptCoords.bl, new fabric.Point(0, canvas.getHeight()), 'bl is 0, height'); + deepEqual(canvas.vptCoords.br, new fabric.Point(canvas.getWidth(), canvas.getHeight()), 'tl is width, height'); + }); + + test('calcViewportBoundaries with zoom', function() { + ok(typeof canvas.calcViewportBoundaries === 'function'); + canvas.setViewportTransform([2, 0, 0, 2, 0, 0]); + deepEqual(canvas.vptCoords.tl, new fabric.Point(0, 0), 'tl is 0,0'); + deepEqual(canvas.vptCoords.tr, new fabric.Point(canvas.getWidth() / 2, 0), 'tl is 0,0'); + deepEqual(canvas.vptCoords.bl, new fabric.Point(0, canvas.getHeight() / 2), 'tl is 0,0'); + deepEqual(canvas.vptCoords.br, new fabric.Point(canvas.getWidth() / 2, canvas.getHeight() / 2), 'tl is 0,0'); + }); + + test('calcViewportBoundaries with zoom and translation', function() { + ok(typeof canvas.calcViewportBoundaries === 'function'); + canvas.setViewportTransform([2, 0, 0, 2, -60, 60]); + deepEqual(canvas.vptCoords.tl, new fabric.Point(30, -30), 'tl is 0,0'); + deepEqual(canvas.vptCoords.tr, new fabric.Point(30 + canvas.getWidth() / 2, -30), 'tl is 0,0'); + deepEqual(canvas.vptCoords.bl, new fabric.Point(30, canvas.getHeight() / 2 - 30), 'tl is 0,0'); + deepEqual(canvas.vptCoords.br, new fabric.Point(30 + canvas.getWidth() / 2, canvas.getHeight() / 2 - 30), 'tl is 0,0'); + }); + //how to test with an exception? /*asyncTest('options in setBackgroundImage from invalid URL', function() { canvas.backgroundImage = null; diff --git a/test/unit/object.js b/test/unit/object.js index 520c0982678..b27fc837a03 100644 --- a/test/unit/object.js +++ b/test/unit/object.js @@ -578,17 +578,11 @@ start(); } else { - var image; - - setTimeout(function() { + cObj.cloneAsImage(function(image) { ok(image); ok(image instanceof fabric.Image); equal(image.width, 100, 'the image has same dimension of object'); start(); - }, 500); - - cObj.cloneAsImage(function(i) { - image = i; }); } }); @@ -601,18 +595,12 @@ start(); } else { - var image; - - setTimeout(function() { + cObj.cloneAsImage(function(image) { ok(image); ok(image instanceof fabric.Image); equal(image.width, 200, 'the image has been scaled by retina'); fabric.devicePixelRatio = 1; start(); - }, 500); - - cObj.cloneAsImage(function(i) { - image = i; }, { enableRetinaScaling: true }); } }); @@ -679,54 +667,6 @@ } }); - test('intersectsWithRectangle', function() { - var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); - cObj.setCoords(); - ok(typeof cObj.intersectsWithRect == 'function'); - - var point1 = new fabric.Point(110, 100), - point2 = new fabric.Point(210, 200), - point3 = new fabric.Point(0, 0), - point4 = new fabric.Point(10, 10); - - ok(cObj.intersectsWithRect(point1, point2)); - ok(!cObj.intersectsWithRect(point3, point4)); - }); - - test('intersectsWithObject', function() { - var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); - cObj.setCoords(); - ok(typeof cObj.intersectsWithObject == 'function', 'has intersectsWithObject method'); - - var cObj2 = new fabric.Object({ left: -150, top: -150, width: 200, height: 200 }); - cObj2.setCoords(); - ok(cObj.intersectsWithObject(cObj2), 'cobj2 does intersect with cobj'); - ok(cObj2.intersectsWithObject(cObj), 'cobj2 does intersect with cobj'); - - var cObj3 = new fabric.Object({ left: 392.5, top: 339.5, width: 13, height: 33 }); - cObj3.setCoords(); - ok(!cObj.intersectsWithObject(cObj3), 'cobj3 does not intersect with cobj (external)'); - ok(!cObj3.intersectsWithObject(cObj), 'cobj3 does not intersect with cobj (external)'); - - var cObj4 = new fabric.Object({ left: 0, top: 0, width: 200, height: 200 }); - cObj4.setCoords(); - ok(cObj4.intersectsWithObject(cObj), 'overlapping objects are considered intersecting'); - ok(cObj.intersectsWithObject(cObj4), 'overlapping objects are considered intersecting'); - }); - - test('isContainedWithinRect', function() { - var cObj = new fabric.Object({ left: 20, top: 20, width: 10, height: 10 }); - cObj.setCoords(); - ok(typeof cObj.isContainedWithinRect == 'function'); - - // fully contained - ok(cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(100,100))); - // only intersects - ok(!cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(25, 25))); - // doesn't intersect - ok(!cObj.isContainedWithinRect(new fabric.Point(100,100), new fabric.Point(110, 110))); - }); - test('isType', function() { var cObj = new fabric.Object(); ok(typeof cObj.isType == 'function'); @@ -1274,163 +1214,6 @@ equal(object.get('fill'), '123456'); }); - test('intersectsWithRect', function() { - var object = new fabric.Object({ left: 0, top: 0, width: 40, height: 50, angle: 160 }), - point1 = new fabric.Point(-10, -10), - point2 = new fabric.Point(20, 30), - point3 = new fabric.Point(10, 15), - point4 = new fabric.Point(30, 35), - point5 = new fabric.Point(50, 60), - point6 = new fabric.Point(70, 80); - - object.setCoords(); - - // object and area intersects - equal(object.intersectsWithRect(point1, point2), true); - // area is contained in object (no intersection) - equal(object.intersectsWithRect(point3, point4), false); - // area is outside of object (no intersection) - equal(object.intersectsWithRect(point5, point6), false); - }); - - test('intersectsWithObject', function() { - var object = new fabric.Object({ left: 20, top: 30, width: 40, height: 50, angle: 230, strokeWidth: 0 }), - object1 = new fabric.Object({ left: 20, top: 30, width: 60, height: 30, angle: 10, strokeWidth: 0 }), - object2 = new fabric.Object({ left: 25, top: 35, width: 20, height: 20, angle: 50, strokeWidth: 0 }), - object3 = new fabric.Object({ left: 50, top: 50, width: 20, height: 20, angle: 0, strokeWidth: 0 }); - - object.set({ originX: 'center', originY: 'center' }).setCoords(); - object1.set({ originX: 'center', originY: 'center' }).setCoords(); - object2.set({ originX: 'center', originY: 'center' }).setCoords(); - object3.set({ originX: 'center', originY: 'center' }).setCoords(); - - equal(object.intersectsWithObject(object1), true, 'object and object1 intersects'); - equal(object.intersectsWithObject(object2), true, 'object2 is contained in object'); - equal(object.intersectsWithObject(object3), false, 'object3 is outside of object (no intersection)'); - }); - - test('isContainedWithinObject', function() { - var object = new fabric.Object({ left: 0, top: 0, width: 40, height: 40, angle: 0 }), - object1 = new fabric.Object({ left: 1, top: 1, width: 38, height: 38, angle: 0 }), - object2 = new fabric.Object({ left: 20, top: 20, width: 40, height: 40, angle: 0 }), - object3 = new fabric.Object({ left: 50, top: 50, width: 40, height: 40, angle: 0 }); - - object.setCoords(); - object1.setCoords(); - object2.setCoords(); - object3.setCoords(); - - equal(object1.isContainedWithinObject(object), true, 'object1 is fully contained within object'); - equal(object2.isContainedWithinObject(object), false, 'object2 intersects object (not fully contained)'); - equal(object3.isContainedWithinObject(object), false, 'object3 is outside of object (not fully contained)'); - object1.angle = 45; - object1.setCoords(); - equal(object1.isContainedWithinObject(object), false, 'object1 rotated is not contained within object'); - - var rect1 = new fabric.Rect({ - width: 50, - height: 50, - left: 50, - top: 50 - }); - - var rect2 = new fabric.Rect({ - width: 100, - height: 100, - left: 100, - top: 0, - angle: 45, - }); - rect1.setCoords(); - rect2.setCoords(); - equal(rect1.isContainedWithinObject(rect2), false, 'rect1 rotated is not contained within rect2'); - }); - - test('isContainedWithinRect', function() { - var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160 }), - point1 = new fabric.Point(0, 0), - point2 = new fabric.Point(80, 80), - point3 = new fabric.Point(0, 0), - point4 = new fabric.Point(80, 60), - point5 = new fabric.Point(80, 80), - point6 = new fabric.Point(90, 90); - - object.set({ originX: 'center', originY: 'center' }).setCoords(); - - // area is contained in object (no intersection) - equal(object.isContainedWithinRect(point1, point2), true); - // object and area intersects - equal(object.isContainedWithinRect(point3, point4), false); - // area is outside of object (no intersection) - equal(object.isContainedWithinRect(point5, point6), false); - }); - - test('isContainedWithinRect', function() { - var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160 }), - point1 = new fabric.Point(0, 0), - point2 = new fabric.Point(80, 80), - point3 = new fabric.Point(0, 0), - point4 = new fabric.Point(80, 60), - point5 = new fabric.Point(80, 80), - point6 = new fabric.Point(90, 90); - - object.set({ originX: 'center', originY: 'center' }).setCoords(); - - // area is contained in object (no intersection) - equal(object.isContainedWithinRect(point1, point2), true); - // object and area intersects - equal(object.isContainedWithinRect(point3, point4), false); - // area is outside of object (no intersection) - equal(object.isContainedWithinRect(point5, point6), false); - }); - - test('containsPoint', function() { - var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160, strokeWidth: 0 }), - point1 = new fabric.Point(30, 30), - point2 = new fabric.Point(60, 30), - point3 = new fabric.Point(45, 65), - point4 = new fabric.Point(15, 40), - point5 = new fabric.Point(30, 15); - - object.set({ originX: 'center', originY: 'center' }).setCoords(); - - // point1 is contained in object - equal(object.containsPoint(point1), true); - // point2 is outside of object (right) - equal(object.containsPoint(point2), false); - // point3 is outside of object (bottom) - equal(object.containsPoint(point3), false); - // point4 is outside of object (left) - equal(object.containsPoint(point4), false); - // point5 is outside of object (top) - equal(object.containsPoint(point5), false); - }); - - test('containsPoint with padding', function() { - var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160, padding: 5 }), - point1 = new fabric.Point(30, 30), - point2 = new fabric.Point(10, 20), - point3 = new fabric.Point(65, 30), - point4 = new fabric.Point(45, 75), - point5 = new fabric.Point(10, 40), - point6 = new fabric.Point(30, 5); - - object.set({ originX: 'center', originY: 'center' }).setCoords(); - - // point1 is contained in object - equal(object.containsPoint(point1), true); - // point2 is contained in object (padding area) - equal(object.containsPoint(point2), true); - // point2 is outside of object (right) - equal(object.containsPoint(point3), false); - // point3 is outside of object (bottom) - equal(object.containsPoint(point4), false); - // point4 is outside of object (left) - equal(object.containsPoint(point5), false); - // point5 is outside of object (top) - equal(object.containsPoint(point6), false); - }); - test('clipTo', function() { var object = new fabric.Object({ left: 40, diff --git a/test/unit/object_geometry.js b/test/unit/object_geometry.js index 13da582c353..adf6acee398 100644 --- a/test/unit/object_geometry.js +++ b/test/unit/object_geometry.js @@ -1,12 +1,264 @@ (function() { - + var canvas = this.canvas = fabric.isLikelyNode ? fabric.createCanvasForNode() : new fabric.StaticCanvas(); QUnit.module('fabric.ObjectGeometry'); + test('intersectsWithRectangle', function() { + var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); + cObj.setCoords(); + ok(typeof cObj.intersectsWithRect == 'function'); + + var point1 = new fabric.Point(110, 100), + point2 = new fabric.Point(210, 200), + point3 = new fabric.Point(0, 0), + point4 = new fabric.Point(10, 10); + + ok(cObj.intersectsWithRect(point1, point2)); + ok(!cObj.intersectsWithRect(point3, point4)); + }); + + test('intersectsWithRectangle absolute', function() { + var cObj = new fabric.Rect({ left: 10, top: 10, width: 20, height: 20 }); + var absolute = true; + canvas.add(cObj); + canvas.viewportTransform = [2, 0, 0, 2, 0, 0]; + cObj.setCoords(); + canvas.calcViewportBoundaries(); + + var point1 = new fabric.Point(5, 5), + point2 = new fabric.Point(15, 15), + point3 = new fabric.Point(25, 25), + point4 = new fabric.Point(35, 35); + + ok(!cObj.intersectsWithRect(point1, point2), 'Does not intersect because there is a 2x zoom'); + ok(!cObj.intersectsWithRect(point3, point4), 'Does not intersect because there is a 2x zoom'); + ok(cObj.intersectsWithRect(point1, point2, absolute), 'absolute coordinates intersect'); + ok(cObj.intersectsWithRect(point3, point4, absolute), 'absolute coordinates intersect'); + }); + + test('intersectsWithObject', function() { + var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100 }); + cObj.setCoords(); + ok(typeof cObj.intersectsWithObject == 'function', 'has intersectsWithObject method'); + + var cObj2 = new fabric.Object({ left: -150, top: -150, width: 200, height: 200 }); + cObj2.setCoords(); + ok(cObj.intersectsWithObject(cObj2), 'cobj2 does intersect with cobj'); + ok(cObj2.intersectsWithObject(cObj), 'cobj2 does intersect with cobj'); + + var cObj3 = new fabric.Object({ left: 392.5, top: 339.5, width: 13, height: 33 }); + cObj3.setCoords(); + ok(!cObj.intersectsWithObject(cObj3), 'cobj3 does not intersect with cobj (external)'); + ok(!cObj3.intersectsWithObject(cObj), 'cobj3 does not intersect with cobj (external)'); + + var cObj4 = new fabric.Object({ left: 0, top: 0, width: 200, height: 200 }); + cObj4.setCoords(); + ok(cObj4.intersectsWithObject(cObj), 'overlapping objects are considered intersecting'); + ok(cObj.intersectsWithObject(cObj4), 'overlapping objects are considered intersecting'); + }); + + test('isContainedWithinRect', function() { + var cObj = new fabric.Object({ left: 20, top: 20, width: 10, height: 10 }); + cObj.setCoords(); + ok(typeof cObj.isContainedWithinRect == 'function'); + + // fully contained + ok(cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(100,100))); + // only intersects + ok(!cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(25, 25))); + // doesn't intersect + ok(!cObj.isContainedWithinRect(new fabric.Point(100,100), new fabric.Point(110, 110))); + }); + + test('isContainedWithinRect absolute', function() { + var cObj = new fabric.Rect({ left: 20, top: 20, width: 10, height: 10 }); + var absolute = true; + canvas.add(cObj); + canvas.viewportTransform = [2, 0, 0, 2, 0, 0]; + cObj.setCoords(); + canvas.calcViewportBoundaries(); + ok(typeof cObj.isContainedWithinRect == 'function'); + + // fully contained + ok(cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(100,100), absolute)); + // only intersects + ok(!cObj.isContainedWithinRect(new fabric.Point(10,10), new fabric.Point(25, 25), absolute)); + // doesn't intersect + ok(!cObj.isContainedWithinRect(new fabric.Point(100,100), new fabric.Point(110, 110), absolute)); + }); + + test('intersectsWithRect', function() { + var object = new fabric.Object({ left: 0, top: 0, width: 40, height: 50, angle: 160 }), + point1 = new fabric.Point(-10, -10), + point2 = new fabric.Point(20, 30), + point3 = new fabric.Point(10, 15), + point4 = new fabric.Point(30, 35), + point5 = new fabric.Point(50, 60), + point6 = new fabric.Point(70, 80); + + object.setCoords(); + + // object and area intersects + equal(object.intersectsWithRect(point1, point2), true); + // area is contained in object (no intersection) + equal(object.intersectsWithRect(point3, point4), false); + // area is outside of object (no intersection) + equal(object.intersectsWithRect(point5, point6), false); + }); + + test('intersectsWithObject', function() { + var object = new fabric.Object({ left: 20, top: 30, width: 40, height: 50, angle: 230, strokeWidth: 0 }), + object1 = new fabric.Object({ left: 20, top: 30, width: 60, height: 30, angle: 10, strokeWidth: 0 }), + object2 = new fabric.Object({ left: 25, top: 35, width: 20, height: 20, angle: 50, strokeWidth: 0 }), + object3 = new fabric.Object({ left: 50, top: 50, width: 20, height: 20, angle: 0, strokeWidth: 0 }); + + object.set({ originX: 'center', originY: 'center' }).setCoords(); + object1.set({ originX: 'center', originY: 'center' }).setCoords(); + object2.set({ originX: 'center', originY: 'center' }).setCoords(); + object3.set({ originX: 'center', originY: 'center' }).setCoords(); + + equal(object.intersectsWithObject(object1), true, 'object and object1 intersects'); + equal(object.intersectsWithObject(object2), true, 'object2 is contained in object'); + equal(object.intersectsWithObject(object3), false, 'object3 is outside of object (no intersection)'); + }); + + test('isContainedWithinObject', function() { + var object = new fabric.Object({ left: 0, top: 0, width: 40, height: 40, angle: 0 }), + object1 = new fabric.Object({ left: 1, top: 1, width: 38, height: 38, angle: 0 }), + object2 = new fabric.Object({ left: 20, top: 20, width: 40, height: 40, angle: 0 }), + object3 = new fabric.Object({ left: 50, top: 50, width: 40, height: 40, angle: 0 }); + + object.setCoords(); + object1.setCoords(); + object2.setCoords(); + object3.setCoords(); + + equal(object1.isContainedWithinObject(object), true, 'object1 is fully contained within object'); + equal(object2.isContainedWithinObject(object), false, 'object2 intersects object (not fully contained)'); + equal(object3.isContainedWithinObject(object), false, 'object3 is outside of object (not fully contained)'); + object1.angle = 45; + object1.setCoords(); + equal(object1.isContainedWithinObject(object), false, 'object1 rotated is not contained within object'); + + var rect1 = new fabric.Rect({ + width: 50, + height: 50, + left: 50, + top: 50 + }); + + var rect2 = new fabric.Rect({ + width: 100, + height: 100, + left: 100, + top: 0, + angle: 45, + }); + rect1.setCoords(); + rect2.setCoords(); + equal(rect1.isContainedWithinObject(rect2), false, 'rect1 rotated is not contained within rect2'); + }); + + test('isContainedWithinRect', function() { + var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160 }), + point1 = new fabric.Point(0, 0), + point2 = new fabric.Point(80, 80), + point3 = new fabric.Point(0, 0), + point4 = new fabric.Point(80, 60), + point5 = new fabric.Point(80, 80), + point6 = new fabric.Point(90, 90); + + object.set({ originX: 'center', originY: 'center' }).setCoords(); + + // area is contained in object (no intersection) + equal(object.isContainedWithinRect(point1, point2), true); + // object and area intersects + equal(object.isContainedWithinRect(point3, point4), false); + // area is outside of object (no intersection) + equal(object.isContainedWithinRect(point5, point6), false); + }); + + test('isContainedWithinRect', function() { + var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160 }), + point1 = new fabric.Point(0, 0), + point2 = new fabric.Point(80, 80), + point3 = new fabric.Point(0, 0), + point4 = new fabric.Point(80, 60), + point5 = new fabric.Point(80, 80), + point6 = new fabric.Point(90, 90); + + object.set({ originX: 'center', originY: 'center' }).setCoords(); + + // area is contained in object (no intersection) + equal(object.isContainedWithinRect(point1, point2), true); + // object and area intersects + equal(object.isContainedWithinRect(point3, point4), false); + // area is outside of object (no intersection) + equal(object.isContainedWithinRect(point5, point6), false); + }); + + test('containsPoint', function() { + var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160, strokeWidth: 0 }), + point1 = new fabric.Point(30, 30), + point2 = new fabric.Point(60, 30), + point3 = new fabric.Point(45, 65), + point4 = new fabric.Point(15, 40), + point5 = new fabric.Point(30, 15); + + object.set({ originX: 'center', originY: 'center' }).setCoords(); + + // point1 is contained in object + equal(object.containsPoint(point1), true); + // point2 is outside of object (right) + equal(object.containsPoint(point2), false); + // point3 is outside of object (bottom) + equal(object.containsPoint(point3), false); + // point4 is outside of object (left) + equal(object.containsPoint(point4), false); + // point5 is outside of object (top) + equal(object.containsPoint(point5), false); + }); + + test('containsPoint with padding', function() { + var object = new fabric.Object({ left: 40, top: 40, width: 40, height: 50, angle: 160, padding: 5 }), + point1 = new fabric.Point(30, 30), + point2 = new fabric.Point(10, 20), + point3 = new fabric.Point(65, 30), + point4 = new fabric.Point(45, 75), + point5 = new fabric.Point(10, 40), + point6 = new fabric.Point(30, 5); + + object.set({ originX: 'center', originY: 'center' }).setCoords(); + + // point1 is contained in object + equal(object.containsPoint(point1), true); + // point2 is contained in object (padding area) + equal(object.containsPoint(point2), true); + // point2 is outside of object (right) + equal(object.containsPoint(point3), false); + // point3 is outside of object (bottom) + equal(object.containsPoint(point4), false); + // point4 is outside of object (left) + equal(object.containsPoint(point5), false); + // point5 is outside of object (top) + equal(object.containsPoint(point6), false); + }); + test('setCoords', function() { var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100, strokeWidth: 0}); ok(typeof cObj.setCoords == 'function'); equal(cObj.setCoords(), cObj, 'chainable'); + equal(cObj.oCoords.tl.x, 150); + equal(cObj.oCoords.tl.y, 150); + equal(cObj.oCoords.tr.x, 250); + equal(cObj.oCoords.tr.y, 150); + equal(cObj.oCoords.bl.x, 150); + equal(cObj.oCoords.bl.y, 250); + equal(cObj.oCoords.br.x, 250); + equal(cObj.oCoords.br.y, 250); + equal(cObj.oCoords.mtr.x, 200); + equal(cObj.oCoords.mtr.y, 110); + cObj.set('left', 250).set('top', 250); // coords should still correspond to initial one, even after invoking `set` @@ -37,6 +289,46 @@ equal(cObj.oCoords.mtr.y, 210); }); + test('setCoords and aCoords', function() { + var cObj = new fabric.Object({ left: 150, top: 150, width: 100, height: 100, strokeWidth: 0}); + cObj.canvas = { + viewportTransform: [2, 0, 0, 2, 0, 0] + }; + cObj.setCoords(); + + equal(cObj.oCoords.tl.x, 300, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.tl.y, 300, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.tr.x, 500, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.tr.y, 300, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.bl.x, 300, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.bl.y, 500, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.br.x, 500, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.br.y, 500, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.mtr.x, 400, 'oCoords are modified by viewportTransform'); + equal(cObj.oCoords.mtr.y, 260, 'oCoords are modified by viewportTransform'); + + equal(cObj.aCoords.tl.x, 150, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.tl.y, 150, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.tr.x, 250, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.tr.y, 150, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.bl.x, 150, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.bl.y, 250, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.br.x, 250, 'aCoords do not interfere with viewportTransform'); + equal(cObj.aCoords.br.y, 250, 'aCoords do not interfere with viewportTransform'); + }); + + test('isOnScreen', function(){ + var cObj = new fabric.Object({ left: 50, top: 50, width: 100, height: 100, strokeWidth: 0}); + cObj.canvas = canvas; + cObj.setCoords(); + ok(cObj.isOnScreen(), 'object is onScreen'); + cObj.top = 1000; + cObj.setCoords(); + ok(!cObj.isOnScreen(), 'object is not onScreen with top 1000'); + canvas.setZoom(0.2); + ok(cObj.isOnScreen(), 'zooming out the object is again on screen'); + }); + test('calcTransformMatrix', function(){ var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 0 }); ok(typeof cObj.calcTransformMatrix == 'function', 'calcTransformMatrix should exist'); @@ -72,4 +364,59 @@ ok(typeof cObj._constrainScale == 'function', '_constrainScale should exist'); }); + test('getCoords return coordinate of object in canvas coordinate.', function() { + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); + var coords = cObj.getCoords(); + deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner'); + deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner'); + deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner'); + deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner'); + + cObj.left += 5; + coords = cObj.getCoords(); + deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached oCoords'); + deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached oCoords'); + deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached oCoords'); + deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached oCoords'); + + coords = cObj.getCoords(false, true); + deepEqual(coords[0], new fabric.Point(45, 30), 'return top left corner recalculated'); + deepEqual(coords[1], new fabric.Point(57, 30), 'return top right corner recalculated'); + deepEqual(coords[2], new fabric.Point(57, 47), 'return bottom right corner recalculated'); + deepEqual(coords[3], new fabric.Point(45, 47), 'return bottom left corner recalculated'); + }); + + test('getCoords return coordinate of object in zoomed canvas coordinate.', function() { + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); + cObj.canvas = { + viewportTransform: [2, 0, 0, 2, 35, 35] + }; + var coords = cObj.getCoords(); + deepEqual(coords[0], new fabric.Point(115, 95), 'return top left corner is influenced by canvas zoom'); + deepEqual(coords[1], new fabric.Point(139, 95), 'return top right corner is influenced by canvas zoom'); + deepEqual(coords[2], new fabric.Point(139, 129), 'return bottom right corner is influenced by canvas zoom'); + deepEqual(coords[3], new fabric.Point(115, 129), 'return bottom left corner is influenced by canvas zoom'); + }); + + test('getCoords return coordinate of object in absolute coordinates and ignore canvas zoom', function() { + var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); + cObj.canvas = { + viewportTransform: [2, 0, 0, 2, 35, 35] + }; + var coords = cObj.getCoords(true); + deepEqual(coords[0], new fabric.Point(40, 30), 'return top left corner cached oCoords'); + deepEqual(coords[1], new fabric.Point(52, 30), 'return top right corner cached oCoords'); + deepEqual(coords[2], new fabric.Point(52, 47), 'return bottom right corner cached oCoords'); + deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached oCoords'); + }); + + // test('getCoords return coordinate of object', function() { + // var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 }); + // var coords = cObj.getCoords(); + // equal(coords[0], { x: 40, y: 30 }, 'return top left corner'); + // equal(coords[1], { x: 1, y: 1 }, 'return top right corner'); + // equal(coords[2], { x: 1, y: 1 }, 'return bottom right corner'); + // equal(coords[3], { x: 1, y: 1 }, 'return bottom left corner'); + // }); + })();