diff --git a/src/brushes/circle_brush.class.js b/src/brushes/circle_brush.class.js index 72c3672c..16f19f82 100644 --- a/src/brushes/circle_brush.class.js +++ b/src/brushes/circle_brush.class.js @@ -26,13 +26,18 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric */ drawDot: function(pointer) { var point = this.addPoint(pointer), - ctx = this.canvas.contextTop; + ctx = this.canvas.contextTop, + v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); ctx.fillStyle = point.fill; ctx.beginPath(); ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2, false); ctx.closePath(); ctx.fill(); + + ctx.restore(); }, /** @@ -78,6 +83,7 @@ fabric.CircleBrush = fabric.util.createClass(fabric.BaseBrush, /** @lends fabric circles.push(circle); } var group = new fabric.Group(circles, { originX: 'center', originY: 'center' }); + group.canvas = this.canvas; this.canvas.add(group); this.canvas.fire('path:created', { path: group }); diff --git a/src/brushes/pencil_brush.class.js b/src/brushes/pencil_brush.class.js index 7f31179a..09fc93f3 100644 --- a/src/brushes/pencil_brush.class.js +++ b/src/brushes/pencil_brush.class.js @@ -104,6 +104,9 @@ */ _render: function() { var ctx = this.canvas.contextTop; + var v = this.canvas.viewportTransform; + ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); ctx.beginPath(); var p1 = this._points[0], @@ -133,6 +136,7 @@ // the bezier control point ctx.lineTo(p1.x, p1.y); ctx.stroke(); + ctx.restore(); }, /** diff --git a/src/brushes/spray_brush.class.js b/src/brushes/spray_brush.class.js index a0946c3a..afd9ce26 100644 --- a/src/brushes/spray_brush.class.js +++ b/src/brushes/spray_brush.class.js @@ -112,6 +112,8 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric } var group = new fabric.Group(rects, { originX: 'center', originY: 'center' }); + group.canvas = this.canvas; + this.canvas.add(group); this.canvas.fire('path:created', { path: group }); @@ -146,7 +148,10 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric render: function() { var ctx = this.canvas.contextTop; ctx.fillStyle = this.color; + + var v = this.canvas.viewportTransform; ctx.save(); + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); for (var i = 0, len = this.sprayChunkPoints.length; i < len; i++) { var point = this.sprayChunkPoints[i]; @@ -180,8 +185,9 @@ fabric.SprayBrush = fabric.util.createClass( fabric.BaseBrush, /** @lends fabric else { width = this.dotWidth; } - - var point = { x: x, y: y, width: width }; + + var point = new fabric.Point(x, y); + point.width = width; if (this.randomOpacity) { point.opacity = fabric.util.getRandomInt(0, 100) / 100; diff --git a/src/canvas.class.js b/src/canvas.class.js index 66ac3a61..a6865c03 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -250,7 +250,7 @@ * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target) { - var pointer = this.getPointer(e), + var pointer = this.getPointer(e, true), xy = this._normalizePointer(target, pointer); // http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html @@ -268,11 +268,14 @@ isObjectInGroup = ( activeGroup && object.type !== 'group' && - activeGroup.contains(object)); + activeGroup.contains(object)), + lt; if (isObjectInGroup) { - x -= activeGroup.left; - y -= activeGroup.top; + lt = new fabric.Point(activeGroup.left, activeGroup.top); + lt = fabric.util.transformPoint(lt, this.viewportTransform, true); + x -= lt.x; + y -= lt.y; } return { x: x, y: y }; }, @@ -403,7 +406,7 @@ if (!target) return; var pointer = this.getPointer(e), - corner = target._findTargetCorner(pointer), + corner = target._findTargetCorner(this.getPointer(e, true)), action = this._getActionFromCorner(target, corner), origin = this._getOriginFromCorner(target, corner); @@ -708,7 +711,7 @@ this.lastRenderedObjectWithControlsAboveOverlay && this.lastRenderedObjectWithControlsAboveOverlay.visible && this.containsPoint(e, this.lastRenderedObjectWithControlsAboveOverlay) && - this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(this.getPointer(e))); + this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(this.getPointer(e, true))); }, /** @@ -731,6 +734,7 @@ var target = this._searchPossibleTargets(e); this._fireOverOutEvents(target); + return target; }, @@ -783,7 +787,7 @@ // Cache all targets where their bounding box contains point. var target, - pointer = this.getPointer(e), + pointer = this.getPointer(e, true), i = this._objects.length; while (i--) { @@ -802,24 +806,36 @@ * @param {Event} e * @return {Object} object with "x" and "y" number values */ - getPointer: function (e) { - var pointer = getPointer(e, this.upperCanvasEl), - bounds = this.upperCanvasEl.getBoundingClientRect(), + getPointer: function (e, ignoreZoom, upperCanvasEl) { + if (!upperCanvasEl) { + upperCanvasEl = this.upperCanvasEl; + } + var pointer = getPointer(e, upperCanvasEl), + bounds = upperCanvasEl.getBoundingClientRect(), cssScale; + pointer.x = pointer.x - this._offset.left; + pointer.y = pointer.y - this._offset.top; + if (!ignoreZoom) { + pointer = fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + } + if (bounds.width === 0 || bounds.height === 0) { // If bounds are not available (i.e. not visible), do not apply scale. cssScale = { width: 1, height: 1 }; } else { cssScale = { - width: this.upperCanvasEl.width / bounds.width, - height: this.upperCanvasEl.height / bounds.height + width: upperCanvasEl.width / bounds.width, + height: upperCanvasEl.height / bounds.height }; } return { - x: (pointer.x - this._offset.left) * cssScale.width, - y: (pointer.y - this._offset.top) * cssScale.height + x: pointer.x * cssScale.width, + y: pointer.y * cssScale.height }; }, @@ -978,7 +994,6 @@ _setActiveGroup: function(group) { this._activeGroup = group; if (group) { - group.canvas = this; group.set('active', true); } }, @@ -1077,7 +1092,7 @@ * @private */ _drawGroupControls: function(ctx, activeGroup) { - this._drawControls(ctx, activeGroup, 'Group'); + activeGroup._renderControls(ctx); }, /** @@ -1086,19 +1101,9 @@ _drawObjectsControls: function(ctx) { for (var i = 0, len = this._objects.length; i < len; ++i) { if (!this._objects[i] || !this._objects[i].active) continue; - this._drawControls(ctx, this._objects[i], 'Object'); + this._objects[i]._renderControls(ctx); this.lastRenderedObjectWithControlsAboveOverlay = this._objects[i]; } - }, - - /** - * @private - */ - _drawControls: function(ctx, object, klass) { - ctx.save(); - fabric[klass].prototype.transform.call(object, ctx); - object.drawBorders(ctx).drawControls(ctx); - ctx.restore(); } }); diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index 55987271..353b5abf 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -334,7 +334,9 @@ if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } - this.freeDrawingBrush.onMouseDown(this.getPointer(e)); + var ivt = fabric.util.invertTransform(this.viewportTransform); + var pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); + this.freeDrawingBrush.onMouseDown(pointer); this.fire('mouse:down', { e: e }); }, @@ -344,7 +346,8 @@ */ _onMouseMoveInDrawingMode: function(e) { if (this._isCurrentlyDrawing) { - var pointer = this.getPointer(e); + var ivt = fabric.util.invertTransform(this.viewportTransform), + pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); this.freeDrawingBrush.onMouseMove(pointer); } this.upperCanvasEl.style.cursor = this.freeDrawingCursor; @@ -387,7 +390,7 @@ if (this._currentTransform) return; var target = this.findTarget(e), - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); // save pointer for check in __onMouseUp event this._previousPointer = pointer; @@ -514,7 +517,7 @@ // We initially clicked in an empty area, so we draw a box for multiple selection if (groupSelector) { - pointer = this.getPointer(e); + pointer = this.getPointer(e, true); groupSelector.left = pointer.x - groupSelector.ex; groupSelector.top = pointer.y - groupSelector.ey; @@ -545,7 +548,6 @@ * @param {Event} e Event fired on mousemove */ _transformObject: function(e) { - var pointer = this.getPointer(e), transform = this._currentTransform; @@ -655,7 +657,7 @@ // only show proper corner when group selection is not active corner = target._findTargetCorner && (!activeGroup || !activeGroup.contains(target)) - && target._findTargetCorner(this.getPointer(e)); + && target._findTargetCorner(this.getPointer(e, true)); if (!corner) { style.cursor = target.hoverCursor || this.hoverCursor; diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index b277ba94..54f5877e 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -83,6 +83,7 @@ if (this._activeObject && target !== this._activeObject) { var group = this._createGroup(target); + group.addWithUpdate(); this.setActiveGroup(group); this._activeObject = null; @@ -107,7 +108,8 @@ return new fabric.Group(groupObjects, { originX: 'center', - originY: 'center' + originY: 'center', + canvas: this }); }, @@ -126,8 +128,10 @@ else if (group.length > 1) { group = new fabric.Group(group.reverse(), { originX: 'center', - originY: 'center' + originY: 'center', + canvas: this }); + group.addWithUpdate(); this.setActiveGroup(group, e); group.saveCoords(); this.fire('selection:created', { target: group }); diff --git a/src/mixins/object.svg_export.js b/src/mixins/object.svg_export.js index 43717121..6b0346a8 100644 --- a/src/mixins/object.svg_export.js +++ b/src/mixins/object.svg_export.js @@ -46,7 +46,8 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot getSvgTransform: function() { var toFixed = fabric.util.toFixed, angle = this.getAngle(), - center = this.getCenterPoint(), + vpt = this.getViewportTransform(), + center = fabric.util.transformPoint(this.getCenterPoint(), vpt), NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS, @@ -60,12 +61,12 @@ fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prot ? (' rotate(' + toFixed(angle, NUM_FRACTION_DIGITS) + ')') : '', - scalePart = (this.scaleX === 1 && this.scaleY === 1) + scalePart = (this.scaleX === 1 && this.scaleY === 1 && vpt[0] === 1 && vpt[3] === 1) ? '' : (' scale(' + - toFixed(this.scaleX, NUM_FRACTION_DIGITS) + + toFixed(this.scaleX * vpt[0], NUM_FRACTION_DIGITS) + ' ' + - toFixed(this.scaleY, NUM_FRACTION_DIGITS) + + toFixed(this.scaleY * vpt[3], NUM_FRACTION_DIGITS) + ')'), flipXPart = this.flipX ? 'matrix(-1 0 0 1 0 0) ' : '', diff --git a/src/mixins/object_geometry.mixin.js b/src/mixins/object_geometry.mixin.js index 42de50db..30e18c1f 100644 --- a/src/mixins/object_geometry.mixin.js +++ b/src/mixins/object_geometry.mixin.js @@ -304,11 +304,15 @@ setCoords: function() { var strokeWidth = this.strokeWidth > 1 ? this.strokeWidth : 0, - padding = this.padding, - theta = degreesToRadians(this.angle); + theta = degreesToRadians(this.angle), + vpt = this.getViewportTransform(); - this.currentWidth = (this.width + strokeWidth) * this.scaleX + padding * 2; - this.currentHeight = (this.height + strokeWidth) * this.scaleY + padding * 2; + var f = function (p) { + return fabric.util.transformPoint(p, vpt); + }; + + this.currentWidth = (this.width + strokeWidth) * this.scaleX; + this.currentHeight = (this.height + strokeWidth) * this.scaleY; // If width is negative, make postive. Fixes path selection issue if (this.currentWidth < 0) { @@ -326,45 +330,34 @@ offsetY = Math.sin(_angle + theta) * _hypotenuse, sinTh = Math.sin(theta), cosTh = Math.cos(theta), - coords = this.getCenterPoint(), + wh = new fabric.Point(this.currentWidth, this.currentHeight), + _tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY), + _tr = new fabric.Point(_tl.x + (wh.x * cosTh), _tl.y + (wh.x * sinTh)), + _bl = new fabric.Point(_tl.x - (wh.y * sinTh), _tl.y + (wh.y * cosTh)), + _mt = new fabric.Point(_tl.x + (wh.x/2 * cosTh), _tl.y + (wh.x/2 * sinTh)), + tl = f(_tl), + tr = f(_tr), + br = f(new fabric.Point(_tr.x - (wh.y * sinTh), _tr.y + (wh.y * cosTh))), + bl = f(_bl), + ml = f(new fabric.Point(_tl.x - (wh.y/2 * sinTh), _tl.y + (wh.y/2 * cosTh))), + mt = f(_mt), + mr = f(new fabric.Point(_tr.x - (wh.y/2 * sinTh), _tr.y + (wh.y/2 * cosTh))), + mb = f(new fabric.Point(_bl.x + (wh.x/2 * cosTh), _bl.y + (wh.x/2 * sinTh))), + mtr = f(new fabric.Point(_mt.x, _mt.y)); - tl = { - x: coords.x - offsetX, - y: coords.y - offsetY - }, - tr = { - x: tl.x + (this.currentWidth * cosTh), - y: tl.y + (this.currentWidth * sinTh) - }, - br = { - x: tr.x - (this.currentHeight * sinTh), - y: tr.y + (this.currentHeight * cosTh) - }, - bl = { - x: tl.x - (this.currentHeight * sinTh), - y: tl.y + (this.currentHeight * cosTh) - }, - ml = { - x: tl.x - (this.currentHeight/2 * sinTh), - y: tl.y + (this.currentHeight/2 * cosTh) - }, - mt = { - x: tl.x + (this.currentWidth/2 * cosTh), - y: tl.y + (this.currentWidth/2 * sinTh) - }, - mr = { - x: tr.x - (this.currentHeight/2 * sinTh), - y: tr.y + (this.currentHeight/2 * cosTh) - }, - mb = { - x: bl.x + (this.currentWidth/2 * cosTh), - y: bl.y + (this.currentWidth/2 * sinTh) - }, - mtr = { - x: mt.x, - y: mt.y - }; + // padding + var padX = Math.cos(_angle + theta) * this.padding * Math.sqrt(2), + padY = Math.sin(_angle + theta) * this.padding * Math.sqrt(2); + tl = tl.add(new fabric.Point(-padX, -padY)); + tr = tr.add(new fabric.Point(padY, -padX)); + br = br.add(new fabric.Point(padX, padY)); + bl = bl.add(new fabric.Point(-padY, padX)); + ml = ml.add(new fabric.Point((-padX - padY) / 2, (-padY + padX) / 2)); + mt = mt.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); + mr = mr.add(new fabric.Point((padY + padX) / 2, (padY - padX) / 2)); + mb = mb.add(new fabric.Point((padX - padY) / 2, (padX + padY) / 2)); + mtr = mtr.add(new fabric.Point((padY - padX) / 2, -(padY + padX) / 2)); // debugging diff --git a/src/mixins/object_interactivity.mixin.js b/src/mixins/object_interactivity.mixin.js index ecd51aa0..ac1f6d60 100644 --- a/src/mixins/object_interactivity.mixin.js +++ b/src/mixins/object_interactivity.mixin.js @@ -277,25 +277,32 @@ scaleY = 1 / this._constrainScale(this.scaleY); ctx.lineWidth = 1 / this.borderScaleFactor; - - ctx.scale(scaleX, scaleY); - - var w = this.getWidth(), - h = this.getHeight(); + + var vpt = this.getViewportTransform(), + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), vpt, true), + sxy = fabric.util.transformPoint(new fabric.Point(scaleX, scaleY), vpt, true), + w = wh.x, + h = wh.y, + sx= sxy.x, + sy= sxy.y; + if (this.group) { + w = w * this.group.scaleX; + h = h * this.group.scaleY; + } 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) + 1, // double offset needed to make lines look sharper - ~~(h + padding2 + strokeWidth * this.scaleY) + 1 + ~~(-(w / 2) - padding - strokeWidth / 2 * sx) - 0.5, // offset needed to make lines look sharper + ~~(-(h / 2) - padding - strokeWidth / 2 * sy) - 0.5, + ~~(w + padding2 + strokeWidth * sx) + 1, // double offset needed to make lines look sharper + ~~(h + padding2 + strokeWidth * sy) + 1 ); if (this.hasRotatingPoint && this.isControlVisible('mtr') && !this.get('lockRotation') && this.hasControls) { var rotateHeight = ( this.flipY - ? h + (strokeWidth * this.scaleY) + (padding * 2) - : -h - (strokeWidth * this.scaleY) - (padding * 2) + ? h + (strokeWidth * sx) + (padding * 2) + : -h - (strokeWidth * sy) - (padding * 2) ) / 2; ctx.beginPath(); @@ -311,7 +318,7 @@ /** * Draws corners of an object's bounding box. - * Requires public properties: width, height, scaleX, scaleY + * Requires public properties: width, height * Requires public options: cornerSize, padding * @param {CanvasRenderingContext2D} ctx Context to draw on * @return {fabric.Object} thisArg @@ -323,75 +330,73 @@ var size = this.cornerSize, size2 = size / 2, strokeWidth2 = ~~(this.strokeWidth / 2), // half strokeWidth rounded down - left = -(this.width / 2), - top = -(this.height / 2), - 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, + wh = fabric.util.transformPoint(new fabric.Point(this.getWidth(), this.getHeight()), this.getViewportTransform(), true), + width = wh.x, + height = wh.y, + left = -(width / 2), + top = -(height / 2), + padding = this.padding, + scaleOffset = size2, + scaleOffsetSize = size2 - size, methodName = this.transparentCorners ? 'strokeRect' : 'fillRect'; ctx.save(); - ctx.lineWidth = 1 / Math.max(this.scaleX, this.scaleY); + ctx.lineWidth = 1; ctx.globalAlpha = this.isMoving ? this.borderOpacityWhenMoving : 1; ctx.strokeStyle = ctx.fillStyle = this.cornerColor; // top-left this._drawControl('tl', ctx, methodName, - left - scaleOffsetX - strokeWidth2 - paddingX, - top - scaleOffsetY - strokeWidth2 - paddingY); + left - scaleOffset - strokeWidth2 - padding, + top - scaleOffset - strokeWidth2 - padding); // top-right this._drawControl('tr', ctx, methodName, - left + width - scaleOffsetX + strokeWidth2 + paddingX, - top - scaleOffsetY - strokeWidth2 - paddingY); + left + width - scaleOffset + strokeWidth2 + padding, + top - scaleOffset - strokeWidth2 - padding); // bottom-left this._drawControl('bl', ctx, methodName, - left - scaleOffsetX - strokeWidth2 - paddingX, - top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + left - scaleOffset - strokeWidth2 - padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); // bottom-right this._drawControl('br', ctx, methodName, - left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, - top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height + scaleOffsetSize + strokeWidth2 + padding); if (!this.get('lockUniScaling')) { // middle-top this._drawControl('mt', ctx, methodName, - left + width/2 - scaleOffsetX, - top - scaleOffsetY - strokeWidth2 - paddingY); + left + width/2 - scaleOffset, + top - scaleOffset - strokeWidth2 - padding); // middle-bottom this._drawControl('mb', ctx, methodName, - left + width/2 - scaleOffsetX, - top + height + scaleOffsetSizeY + strokeWidth2 + paddingY); + left + width/2 - scaleOffset, + top + height + scaleOffsetSize + strokeWidth2 + padding); // middle-right this._drawControl('mr', ctx, methodName, - left + width + scaleOffsetSizeX + strokeWidth2 + paddingX, - top + height/2 - scaleOffsetY); + left + width + scaleOffsetSize + strokeWidth2 + padding, + top + height/2 - scaleOffset); // middle-left this._drawControl('ml', ctx, methodName, - left - scaleOffsetX - strokeWidth2 - paddingX, - top + height/2 - scaleOffsetY); + left - scaleOffset - strokeWidth2 - padding, + top + height/2 - scaleOffset); } // middle-top-rotate if (this.hasRotatingPoint) { this._drawControl('mtr', ctx, methodName, - left + width/2 - scaleOffsetX, + left + width/2 - scaleOffset, this.flipY - ? (top + height + (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleX/2 + strokeWidth2 + paddingY) - : (top - (this.rotatingPointOffset / this.scaleY) - this.cornerSize/this.scaleY/2 - strokeWidth2 - paddingY)); + ? (top + height + this.rotatingPointOffset - this.cornerSize/2 + strokeWidth2 + padding) + : (top - this.rotatingPointOffset - this.cornerSize/2 - strokeWidth2 - padding)); } ctx.restore(); @@ -403,12 +408,11 @@ * @private */ _drawControl: function(control, ctx, methodName, left, top) { - var sizeX = this.cornerSize / this.scaleX, - sizeY = this.cornerSize / this.scaleY; + var size = this.cornerSize; if (this.isControlVisible(control)) { - isVML || this.transparentCorners || ctx.clearRect(left, top, sizeX, sizeY); - ctx[methodName](left, top, sizeX, sizeY); + isVML || this.transparentCorners || ctx.clearRect(left, top, size, size); + ctx[methodName](left, top, size, size); } }, diff --git a/src/shapes/circle.class.js b/src/shapes/circle.class.js index bdf1a6c6..fccffd58 100644 --- a/src/shapes/circle.class.js +++ b/src/shapes/circle.class.js @@ -172,7 +172,7 @@ parsedAttributes.left = 0; } if (!('top' in parsedAttributes)) { - parsedAttributes.top = 0 + parsedAttributes.top = 0; } if (!('transformMatrix' in parsedAttributes)) { parsedAttributes.left -= (options.width / 2); diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 6cd86af4..04b7c299 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -66,7 +66,7 @@ } this._setOpacityIfSame(); - this.setCoords(true); + this.setCoords(); this.saveCoords(); }, @@ -114,8 +114,10 @@ */ addWithUpdate: function(object) { this._restoreObjectsState(); - this._objects.push(object); - object.group = this; + if (object) { + this._objects.push(object); + object.group = this; + } // since _restoreObjectsState set objects inactive this.forEachObject(this._setObjectActive, this); this._calcBounds(); @@ -213,15 +215,12 @@ /** * Renders instance on a given context * @param {CanvasRenderingContext2D} ctx context to render instance on - * @param {Boolean} [noTransform] When true, context is not transformed */ - render: function(ctx, noTransform) { + render: function(ctx) { // do not render if object is not visible if (!this.visible) return; ctx.save(); - this.transform(ctx); - this.clipTo && fabric.util.clipContext(this, ctx); // the array is now sorted in order of highest first, so start from end @@ -231,31 +230,34 @@ this.clipTo && ctx.restore(); - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.drawControls(ctx); - } ctx.restore(); }, + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + _renderControls: function(ctx, noTransform) { + this.callSuper('_renderControls', ctx, noTransform); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i]._renderControls(ctx); + } + }, + /** * @private */ _renderObject: function(object, ctx) { - - var originalScaleFactor = object.borderScaleFactor, - originalHasRotatingPoint = object.hasRotatingPoint, - groupScaleFactor = Math.max(this.scaleX, this.scaleY); + var originalHasRotatingPoint = object.hasRotatingPoint; // do not render if object is not visible if (!object.visible) return; - object.borderScaleFactor = groupScaleFactor; object.hasRotatingPoint = false; object.render(ctx); - object.borderScaleFactor = originalScaleFactor; object.hasRotatingPoint = originalHasRotatingPoint; }, @@ -450,20 +452,17 @@ * @private */ _getBounds: function(aX, aY, onlyWidthHeight) { - var minX = min(aX), - maxX = max(aX), - minY = min(aY), - maxY = max(aY), - width = (maxX - minX) || 0, - height = (maxY - minY) || 0, - obj = { - width: width, - height: height + var ivt = fabric.util.invertTransform(this.getViewportTransform()), + minXY = fabric.util.transformPoint(new fabric.Point(min(aX), min(aY)), ivt), + maxXY = fabric.util.transformPoint(new fabric.Point(max(aX), max(aY)), ivt), + obj = { + width: (maxXY.x - minXY.x) || 0, + height: (maxXY.y - minXY.y) || 0 }; if (!onlyWidthHeight) { - obj.left = (minX + width / 2) || 0; - obj.top = (minY + height / 2) || 0; + obj.left = (minXY.x + maxXY.x) / 2 || 0; + obj.top = (minXY.y + maxXY.y) / 2 || 0; } return obj; }, diff --git a/src/shapes/image.class.js b/src/shapes/image.class.js index bc411655..7a9b84db 100644 --- a/src/shapes/image.class.js +++ b/src/shapes/image.class.js @@ -122,7 +122,6 @@ if (!this.visible) return; ctx.save(); - var m = this.transformMatrix, isInPathGroup = this.group && this.group.type === 'path-group'; @@ -140,7 +139,6 @@ ctx.translate(this.width/2, this.height/2); } - ctx.save(); this._setShadow(ctx); this.clipTo && fabric.util.clipContext(this, ctx); this._render(ctx); @@ -150,12 +148,6 @@ this._renderStroke(ctx); this.clipTo && ctx.restore(); ctx.restore(); - - if (this.active && !noTransform) { - this.drawBorders(ctx); - this.drawControls(ctx); - } - ctx.restore(); }, /** diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index cb732056..5c839cc2 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -732,6 +732,9 @@ * @param {Boolean} fromLeft When true, context is transformed to object's top/left corner. This is used when rendering text on Node */ transform: function(ctx, fromLeft) { + if (this.group) { + this.group.transform(ctx, fromLeft); + } ctx.globalAlpha = this.opacity; var center = fromLeft ? this._getLeftTopCoords() : this.getCenterPoint(); @@ -920,6 +923,18 @@ return this; }, + /** + * Retrieves viewportTransform from Object's canvas if possible + * @method getViewportTransform + * @memberOf fabric.Object.prototype + * @return {Boolean} flipY value // TODO + */ + getViewportTransform: function() { + if (this.canvas && this.canvas.viewportTransform) + return this.canvas.viewportTransform; + return [1, 0, 0, 1, 0, 0]; + }, + /** * Renders an object on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on @@ -949,19 +964,14 @@ this._render(ctx, noTransform); this.clipTo && ctx.restore(); this._removeShadow(ctx); - this._restoreFillRule(ctx); - if (this.active && !noTransform) { - this.drawBorders(ctx); - this.drawControls(ctx); - } - ctx.restore(); }, _transform: function(ctx, noTransform) { var m = this.transformMatrix; + if (m && !this.group) { ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -990,6 +1000,35 @@ } }, + /** + * Renders controls and borders for the object + * @param {CanvasRenderingContext2D} ctx Context to render on + * @param {Boolean} [noTransform] When true, context is not transformed + */ + _renderControls: function(ctx, noTransform) { + var v = this.getViewportTransform(); + + ctx.save(); + if (this.active && !noTransform) { + var center; + if (this.group) { + center = fabric.util.transformPoint(this.group.getCenterPoint(), v); + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.group.angle)); + } + center = fabric.util.transformPoint(this.getCenterPoint(), v, null != this.group); + if (this.group) { + center.x *= this.group.scaleX; + center.y *= this.group.scaleY; + } + ctx.translate(center.x, center.y); + ctx.rotate(degreesToRadians(this.angle)); + this.drawBorders(ctx); + this.drawControls(ctx); + } + ctx.restore(); + }, + /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on diff --git a/src/shapes/path.class.js b/src/shapes/path.class.js index 9b98c0ca..47895ce0 100644 --- a/src/shapes/path.class.js +++ b/src/shapes/path.class.js @@ -453,6 +453,7 @@ ctx.save(); var m = this.transformMatrix; + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } @@ -470,11 +471,6 @@ this._renderStroke(ctx); this.clipTo && ctx.restore(); this._removeShadow(ctx); - - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.drawControls(ctx); - } ctx.restore(); }, diff --git a/src/shapes/path_group.class.js b/src/shapes/path_group.class.js index 513cd873..da89a2c4 100644 --- a/src/shapes/path_group.class.js +++ b/src/shapes/path_group.class.js @@ -77,10 +77,10 @@ ctx.save(); var m = this.transformMatrix; + if (m) { ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } - this.transform(ctx); this._setShadow(ctx); @@ -90,11 +90,6 @@ } this.clipTo && ctx.restore(); this._removeShadow(ctx); - - if (this.active) { - this.drawBorders(ctx); - this.drawControls(ctx); - } ctx.restore(); }, diff --git a/src/shapes/text.class.js b/src/shapes/text.class.js index a0c50679..e164b1a7 100644 --- a/src/shapes/text.class.js +++ b/src/shapes/text.class.js @@ -319,7 +319,6 @@ this.setOptions(options); this.__skipDimension = false; this._initDimensions(); - this.setCoords(); }, /** @@ -760,9 +759,8 @@ /** * Renders text instance on a specified context * @param {CanvasRenderingContext2D} ctx Context to render on - * @param {Boolean} [noTransform] When true, context is not transformed */ - render: function(ctx, noTransform) { + render: function(ctx) { // do not render if object is not visible if (!this.visible) return; @@ -772,10 +770,6 @@ ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]); } this._render(ctx); - if (!noTransform && this.active) { - this.drawBorders(ctx); - this.drawControls(ctx); - } ctx.restore(); }, diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index bc33f1dd..f3ca801a 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -133,6 +133,13 @@ */ imageSmoothingEnabled: true, + /** + * The transformation (in the format of Canvas transform) which focuses the viewport + * @type Array + * @default + */ + viewportTransform: [1, 0, 0, 1, 0, 0], + /** * Callback; invoked right before object is about to be scaled/rotated * @param {fabric.Object} target Object that's about to be scaled/rotated @@ -526,6 +533,92 @@ return this; }, + /** + * Returns canvas zoom level + * @return {Number} + */ + getZoom: function () { + return Math.sqrt(this.viewportTransform[0] * this.viewportTransform[3]); + }, + + /** + * Sets viewport transform of this canvas instance + * @param {Array} vpt the transform in the form of context.transform + * @return {fabric.Canvas} instance + * @chainable true + */ + setViewportTransform: function (vpt) { + this.viewportTransform = vpt; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } + return this; + }, + + /** + * Sets zoom level of this canvas instance, zoom centered around point + * @param {fabric.Point} point to zoom with respect to + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + zoomToPoint: function (point, value) { + // TODO: just change the scale, preserve other transformations + var before = point; + point = fabric.util.transformPoint(point, fabric.util.invertTransform(this.viewportTransform)); + this.viewportTransform[0] = value; + this.viewportTransform[3] = value; + var after = fabric.util.transformPoint(point, this.viewportTransform); + this.viewportTransform[4] += before.x - after.x; + this.viewportTransform[5] += before.y - after.y; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } + return this; + }, + + /** + * Sets zoom level of this canvas instance + * @param {Number} value to set zoom to, less than 1 zooms out + * @return {fabric.Canvas} instance + * @chainable true + */ + setZoom: function (value) { + this.zoomToPoint(new fabric.Point(0, 0), value); + return this; + }, + + /** + * Pan viewport so as to place point at top left corner of canvas + * @param {fabric.Point} point to move to + * @return {fabric.Canvas} instance + * @chainable true + */ + absolutePan: function (point) { + this.viewportTransform[4] = -point.x; + this.viewportTransform[5] = -point.y; + this.renderAll(); + for (var i = 0, len = this._objects.length; i < len; i++) { + this._objects[i].setCoords(); + } + return this; + }, + + /** + * Pans viewpoint relatively + * @param {fabric.Point} point (position vector) to move by + * @return {fabric.Canvas} instance + * @chainable true + */ + relativePan: function (point) { + return this.absolutePan(new fabric.Point( + -point.x - this.viewportTransform[4], + -point.y - this.viewportTransform[5] + )); + }, + /** * Returns <canvas> element corresponding to this instance * @return {HTMLCanvasElement} @@ -558,17 +651,13 @@ */ _draw: function (ctx, object) { if (!object) return; - - if (this.controlsAboveOverlay) { - var hasBorders = object.hasBorders, hasControls = object.hasControls; - object.hasBorders = object.hasControls = false; - object.render(ctx); - object.hasBorders = hasBorders; - object.hasControls = hasControls; - } - else { - object.render(ctx); - } + + ctx.save(); + var v = this.viewportTransform; + ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]); + object.render(ctx); + ctx.restore(); + if (!this.controlsAboveOverlay) object._renderControls(ctx); }, /** @@ -577,8 +666,8 @@ */ _onObjectAdded: function(obj) { this.stateful && obj.setupState(); - obj.setCoords(); obj.canvas = this; + obj.setCoords(); this.fire('object:added', { target: obj }); obj.fire('added'); }, @@ -647,7 +736,6 @@ * @chainable */ renderAll: function (allOnTop) { - var canvasToDrawOn = this[(allOnTop === true && this.interactive) ? 'contextTop' : 'contextContainer'], activeGroup = this.getActiveGroup(); @@ -746,7 +834,7 @@ this.height); } if (this.backgroundImage) { - this.backgroundImage.render(ctx); + this._draw(ctx, this.backgroundImage); } }, @@ -767,7 +855,7 @@ this.height); } if (this.overlayImage) { - this.overlayImage.render(ctx); + this._draw(ctx, this.overlayImage); } }, diff --git a/src/util/misc.js b/src/util/misc.js index 48200449..60a07ee5 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -81,6 +81,45 @@ return new fabric.Point(rx, ry).addEquals(origin); }, + /** + * Apply transform t to point p + * @static + * @memberOf fabric.util + * @param {fabric.Point} p The point to transform + * @param {Array} t The transform + * @param {Boolean} [ignoreOffset] Indicates that the offset should not be applied + * @return {fabric.Point} The transformed point + */ + transformPoint: function(p, t, ignoreOffset) { + if (ignoreOffset) { + return new fabric.Point( + t[0] * p.x + t[1] * p.y, + t[2] * p.x + t[3] * p.y + ); + } + return new fabric.Point( + t[0] * p.x + t[1] * p.y + t[4], + t[2] * p.x + t[3] * p.y + t[5] + ); + }, + + /** + * Invert transformation t + * @static + * @memberOf fabric.util + * @param {Array} t The transform + * @return {Array} The inverted transform + */ + invertTransform: function(t) { + var r = t.slice(), + a = 1 / (t[0] * t[3] - t[1] * t[2]); + r = [a * t[3], -a * t[1], -a * t[2], a * t[0], 0, 0]; + var o = fabric.util.transformPoint({x: t[4], y: t[5]}, r); + r[4] = -o.x; + r[5] = -o.y; + return r; + }, + /** * A wrapper around Number#toFixed, which contrary to native method returns number, not string. * @static