diff --git a/src/canvas.class.js b/src/canvas.class.js index faea517093d..9ae94ff4b85 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -225,6 +225,13 @@ */ isDrawingMode: false, + /** + * Indicates whether objects should remain in current stack position when selected. When false objects are brought to top and rendered as part of the selection group + * @type Boolean + * @default + */ + preserveObjectStacking: false, + /** * @private */ @@ -242,6 +249,73 @@ this.calcOffset(); }, + /** + * Divides objects in two groups, one to render immediately + * and one to render as activeGroup. + * @return {Array} objects to render immediately and pushes the other in the activeGroup. + */ + _chooseObjectsToRender: function() { + var activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(), + object, objsToRender = [ ], activeGroupObjects = [ ]; + + if ((activeGroup || activeObject) && !this.preserveObjectStacking) { + for (var i = 0, length = this._objects.length; i < length; i++) { + object = this._objects[i]; + if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { + objsToRender.push(object); + } + else { + activeGroupObjects.push(object); + } + } + if (activeGroup) { + activeGroup._set('_objects', activeGroupObjects); + objsToRender.push(activeGroup); + } + activeObject && objsToRender.push(activeObject); + } + else { + objsToRender = this._objects; + } + return objsToRender; + }, + + /** + * Renders both the top canvas and the secondary container canvas. + * @param {Boolean} [allOnTop] Whether we want to force all images to be rendered on the top canvas + * @return {fabric.Canvas} instance + * @chainable + */ + renderAll: function () { + if (this.selection && !this._groupSelector && !this.isDrawingMode) { + this.clearContext(this.contextTop); + } + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._chooseObjectsToRender()); + return this; + }, + + /** + * Method to render only the top canvas. + * Also used to render the group selection box. + * @return {fabric.Canvas} thisArg + * @chainable + */ + renderTop: function () { + var ctx = this.contextTop; + this.clearContext(ctx); + + // we render the top context - last object + if (this.selection && this._groupSelector) { + this._drawSelection(ctx); + } + + this.fire('after:render'); + + return this; + }, + /** * Resets the current transform to its original values and chooses the type of resizing based on the event * @private @@ -299,8 +373,9 @@ * @return {Boolean} true if point is contained within an area of given object */ containsPoint: function (e, target, point) { - var pointer = point || this.getPointer(e, true), - xy = this._normalizePointer(target, pointer); + var ignoreZoom = true, + pointer = point || this.getPointer(e, ignoreZoom), + xy; if (target.group && target.group === this.getActiveGroup()) { xy = this._normalizePointer(target.group, pointer); @@ -317,18 +392,13 @@ * @private */ _normalizePointer: function (object, pointer) { - var lt, m; - - m = fabric.util.multiplyTransformMatrices( - this.viewportTransform, - object.calcTransformMatrix()); - - m = fabric.util.invertTransform(m); - pointer = fabric.util.transformPoint(pointer, m , false); - lt = fabric.util.transformPoint(object.getCenterPoint(), m , false); - pointer.x -= lt.x; - pointer.y -= lt.y; - return { x: pointer.x, y: pointer.y }; + var m = object.calcTransformMatrix(), + invertedM = fabric.util.invertTransform(m), + vpt = this.viewportTransform, + vptPointer = this.restorePointerVpt(pointer), + p = fabric.util.transformPoint(vptPointer, invertedM); + return fabric.util.transformPoint(p, vpt); + //return { x: p.x * vpt[0], y: p.y * vpt[3] }; }, /** @@ -864,6 +934,7 @@ }, /** + * @param {fabric.Object} target to reset transform * @private */ _resetObjectTransform: function (target) { @@ -876,10 +947,10 @@ /** * @private + * @param {CanvasRenderingContext2D} ctx to draw the selection on */ - _drawSelection: function () { - var ctx = this.contextTop, - groupSelector = this._groupSelector, + _drawSelection: function (ctx) { + var groupSelector = this._groupSelector, left = groupSelector.left, top = groupSelector.top, aleft = abs(left), @@ -933,9 +1004,10 @@ return; } - var pointer = this.getPointer(e, true), - activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(); + var ignoreZoom = true, + pointer = this.getPointer(e, ignoreZoom), + activeGroup = this.getActiveGroup(), + activeObject = this.getActiveObject(); // first check current group (if one exists) // active group does not check sub targets like normal groups. @@ -1020,6 +1092,18 @@ return target; }, + /** + * Returns pointer coordinates without the effect of the viewport + * @param {Object} pointer with "x" and "y" number values + * @return {Object} object with "x" and "y" number values + */ + restorePointerVpt: function(pointer) { + return fabric.util.transformPoint( + pointer, + fabric.util.invertTransform(this.viewportTransform) + ); + }, + /** * Returns pointer coordinates relative to canvas. * @param {Event} e @@ -1049,10 +1133,7 @@ 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) - ); + pointer = this.restorePointerVpt(pointer); } if (boundsWidth === 0 || boundsHeight === 0) { @@ -1195,6 +1276,20 @@ return this._activeObject; }, + /** + * @private + * @param {fabric.Object} obj Object that was removed + */ + _onObjectRemoved: function(obj) { + // removing active object should fire "selection:cleared" events + if (this.getActiveObject() === obj) { + this.fire('before:selection:cleared', { target: obj }); + this._discardActiveObject(); + this.fire('selection:cleared'); + } + this.callSuper('_onObjectRemoved', obj); + }, + /** * @private */ @@ -1323,6 +1418,18 @@ return this; }, + /** + * Clears all contexts (background, main, top) of an instance + * @return {fabric.Canvas} thisArg + * @chainable + */ + clear: function () { + this.discardActiveGroup(); + this.discardActiveObject(); + this.clearContext(this.contextTop); + return this.callSuper('clear'); + }, + /** * Draws objects' controls (borders/controls) * @param {CanvasRenderingContext2D} ctx Context to render controls on @@ -1350,22 +1457,6 @@ } }, - /** - * @private - * @param {fabric.Object} obj Object that was removed - */ - _onObjectRemoved: function(obj) { - this.callSuper('_onObjectRemoved', obj); - }, - - /** - * Clears all contexts (background, main, top) of an instance - * @return {fabric.Canvas} thisArg - * @chainable - */ - clear: function () { - return this.callSuper('clear'); - } }); // copying static properties manually to work around Opera's bug, diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index b5ad6c491e8..ca8e8b71421 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -383,8 +383,7 @@ if (this.clipTo) { fabric.util.clipContext(this, this.contextTop); } - var ivt = fabric.util.invertTransform(this.viewportTransform), - pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); + var pointer = this.getPointer(e); this.freeDrawingBrush.onMouseDown(pointer); this._handleEvent(e, 'down'); }, @@ -395,8 +394,7 @@ */ _onMouseMoveInDrawingMode: function(e) { if (this._isCurrentlyDrawing) { - var ivt = fabric.util.invertTransform(this.viewportTransform), - pointer = fabric.util.transformPoint(this.getPointer(e, true), ivt); + var pointer = this.getPointer(e); this.freeDrawingBrush.onMouseMove(pointer); } this.setCursor(this.freeDrawingCursor); @@ -672,11 +670,13 @@ /** * @private + * @param {Event} e Event object + * @param {Object} transform current tranform + * @param {Number} x mouse position x from origin + * @param {Number} y mouse poistion y from origin * @return {Boolean} true if the scaling occurred */ _onScale: function(e, transform, x, y) { - // rotate object only if shift key is not pressed - // and if it is not a group we are transforming if ((e[this.uniScaleKey] || this.uniScaleTransform) && !transform.target.get('lockUniScaling')) { transform.currentAction = 'scale'; return this._scaleObject(x, y); diff --git a/src/static_canvas.class.js b/src/static_canvas.class.js index ad1a9e8e44a..8f66df8662d 100644 --- a/src/static_canvas.class.js +++ b/src/static_canvas.class.js @@ -133,13 +133,6 @@ */ imageSmoothingEnabled: true, - /** - * Indicates whether objects should remain in current stack position when selected. When false objects are brought to top and rendered as part of the selection group - * @type Boolean - * @default - */ - preserveObjectStacking: false, - /** * The transformation (in the format of Canvas transform) which focuses the viewport * @type Array @@ -181,6 +174,7 @@ * @param {Object} [options] Options object */ _initStatic: function(el, options) { + var cb = fabric.StaticCanvas.prototype.renderAll.bind(this); this._objects = []; this._createLowerCanvas(el); @@ -193,16 +187,16 @@ } if (options.overlayImage) { - this.setOverlayImage(options.overlayImage, this.renderAll.bind(this)); + this.setOverlayImage(options.overlayImage, cb); } if (options.backgroundImage) { - this.setBackgroundImage(options.backgroundImage, this.renderAll.bind(this)); + this.setBackgroundImage(options.backgroundImage, cb); } if (options.backgroundColor) { - this.setBackgroundColor(options.backgroundColor, this.renderAll.bind(this)); + this.setBackgroundColor(options.backgroundColor, cb); } if (options.overlayColor) { - this.setOverlayColor(options.overlayColor, this.renderAll.bind(this)); + this.setOverlayColor(options.overlayColor, cb); } this.calcOffset(); }, @@ -669,13 +663,13 @@ setViewportTransform: function (vpt) { var activeGroup = this.getActiveGroup(); this.viewportTransform = vpt; - this.renderAll(); for (var i = 0, len = this._objects.length; i < len; i++) { this._objects[i].setCoords(); } if (activeGroup) { activeGroup.setCoords(); } + this.renderAll(); return this; }, @@ -688,18 +682,14 @@ */ zoomToPoint: function (point, value) { // TODO: just change the scale, preserve other transformations - var before = point; + var before = point, vpt = this.viewportTransform.slice(0); 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; + vpt[0] = value; + vpt[3] = value; + var after = fabric.util.transformPoint(point, vpt); + vpt[4] += before.x - after.x; + vpt[5] += before.y - after.y; + return this.setViewportTransform(vpt); }, /** @@ -720,13 +710,10 @@ * @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; + var vpt = this.viewportTransform.slice(0); + vpt[4] = -point.x; + vpt[5] = -point.y; + return this.setViewportTransform(vpt); }, /** @@ -783,13 +770,6 @@ * @param {fabric.Object} obj Object that was removed */ _onObjectRemoved: function(obj) { - // removing active object should fire "selection:cleared" events - if (this.getActiveObject() === obj) { - this.fire('before:selection:cleared', { target: obj }); - this._discardActiveObject(); - this.fire('selection:cleared'); - } - this.fire('object:removed', { target: obj }); obj.fire('removed'); }, @@ -820,95 +800,62 @@ */ clear: function () { this._objects.length = 0; - if (this.discardActiveGroup) { - this.discardActiveGroup(); - } - if (this.discardActiveObject) { - this.discardActiveObject(); - } this.clearContext(this.contextContainer); - if (this.contextTop) { - this.clearContext(this.contextTop); - } this.fire('canvas:cleared'); this.renderAll(); return this; }, /** - * Divides objects in two groups, one to render immediately - * and one to render as activeGroup. - * return objects to render immediately and pushes the other in the activeGroup. + * Renders both the canvas. + * @return {fabric.Canvas} instance + * @chainable */ - _chooseObjectsToRender: function() { - var activeGroup = this.getActiveGroup(), - activeObject = this.getActiveObject(), - object, objsToRender = [ ], activeGroupObjects = [ ]; - - if ((activeGroup || activeObject) && !this.preserveObjectStacking) { - for (var i = 0, length = this._objects.length; i < length; i++) { - object = this._objects[i]; - if ((!activeGroup || !activeGroup.contains(object)) && object !== activeObject) { - objsToRender.push(object); - } - else { - activeGroupObjects.push(object); - } - } - activeGroup && activeGroup._set('_objects', activeGroupObjects); - } - else { - objsToRender = this._objects; - } - return objsToRender; + renderAll: function () { + var canvasToDrawOn = this.contextContainer; + this.renderCanvas(canvasToDrawOn, this._objects); + return this; }, /** - * Renders both the top canvas and the secondary container canvas. - * @param {Boolean} [allOnTop] Whether we want to force all images to be rendered on the top canvas + * Renders background, objects, overlay and controls. + * @param {CanvasRenderingContext2D} ctx + * @param {Array} objects to render * @return {fabric.Canvas} instance * @chainable */ - renderAll: function () { - var canvasToDrawOn = this.contextContainer, objsToRender; - - if (this.contextTop && this.selection && !this._groupSelector && !this.isDrawingMode) { - this.clearContext(this.contextTop); - } - - this.clearContext(canvasToDrawOn); - + renderCanvas: function(ctx, objects) { + this.clearContext(ctx); this.fire('before:render'); - if (this.clipTo) { - fabric.util.clipContext(this, canvasToDrawOn); + fabric.util.clipContext(this, ctx); } - this._renderBackground(canvasToDrawOn); + this._renderBackground(ctx); - canvasToDrawOn.save(); - objsToRender = this._chooseObjectsToRender(); + ctx.save(); //apply viewport transform once for all rendering process - canvasToDrawOn.transform.apply(canvasToDrawOn, this.viewportTransform); - this._renderObjects(canvasToDrawOn, objsToRender); - if (!this.preserveObjectStacking) { - objsToRender = [this.getActiveGroup(), this.getActiveObject()]; - this._renderObjects(canvasToDrawOn, objsToRender); - } - canvasToDrawOn.restore(); - + ctx.transform.apply(ctx, this.viewportTransform); + this._renderObjects(ctx, objects); + ctx.restore(); if (!this.controlsAboveOverlay && this.interactive) { - this.drawControls(canvasToDrawOn); + this.drawControls(ctx); } if (this.clipTo) { - canvasToDrawOn.restore(); + ctx.restore(); } - this._renderOverlay(canvasToDrawOn); + this._renderOverlay(ctx); if (this.controlsAboveOverlay && this.interactive) { - this.drawControls(canvasToDrawOn); + this.drawControls(ctx); } - this.fire('after:render'); - return this; + }, + + /** + * dummy function for organization purpouse. + * @private + */ + drawControls: function() { + // NOOP }, /** @@ -967,26 +914,6 @@ this._renderBackgroundOrOverlay(ctx, 'overlay'); }, - /** - * Method to render only the top canvas. - * Also used to render the group selection box. - * @return {fabric.Canvas} thisArg - * @chainable - */ - renderTop: function () { - var ctx = this.contextTop || this.contextContainer; - this.clearContext(ctx); - - // we render the top context - last object - if (this.selection && this._groupSelector) { - this._drawSelection(); - } - - this.fire('after:render'); - - return this; - }, - /** * Returns coordinates of a center of canvas. * Returned value is an object with top and left properties diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 3066f880574..9dcecca3154 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -48,6 +48,22 @@ '"shadow":null,'+ '"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"rx":0,"ry":0}],"background":"#ff5555","overlay":"rgba(0,0,0,0.2)"}'; + function _createImageElement() { + return fabric.isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'); + } + + function getAbsolutePath(path) { + var isAbsolute = /^https?:/.test(path); + if (isAbsolute) return path; + var imgEl = _createImageElement(); + imgEl.src = path; + var src = imgEl.src; + imgEl = null; + return src; + } + + var IMG_SRC = fabric.isLikelyNode ? (__dirname + '/../fixtures/test_image.gif') : getAbsolutePath('../fixtures/test_image.gif'); + var el = fabric.document.createElement('canvas'); el.width = 600; el.height = 600; @@ -68,6 +84,8 @@ QUnit.module('fabric.Canvas', { setup: function() { upperCanvasEl.style.display = ''; + canvas.controlsAboveOverlay = fabric.Canvas.prototype.controlsAboveOverlay; + canvas.preserveObjectStacking = fabric.Canvas.prototype.preserveObjectStacking; }, teardown: function() { canvas.clear(); @@ -103,6 +121,83 @@ equal(canvas.item(0), rect, 'should return proper item'); }); + test('preserveObjectStacking', function() { + ok(typeof canvas.preserveObjectStacking == 'boolean'); + ok(!canvas.preserveObjectStacking, 'default is false'); + }); + + test('uniScaleTransform', function() { + ok(typeof canvas.uniScaleTransform == 'boolean'); + ok(!canvas.uniScaleTransform, 'default is false'); + }); + + test('uniScaleKey', function() { + ok(typeof canvas.uniScaleKey == 'string'); + equal(canvas.uniScaleKey, 'shiftKey', 'default is shift'); + }); + + test('centeredScaling', function() { + ok(typeof canvas.centeredScaling == 'boolean'); + ok(!canvas.centeredScaling, 'default is false'); + }); + + test('centeredRotation', function() { + ok(typeof canvas.centeredRotation == 'boolean'); + ok(!canvas.centeredRotation, 'default is false'); + }); + + test('centeredKey', function() { + ok(typeof canvas.centeredKey == 'string'); + equal(canvas.centeredKey, 'altKey', 'default is alt'); + }); + + test('altActionKey', function() { + ok(typeof canvas.altActionKey == 'string'); + equal(canvas.altActionKey, 'shiftKey', 'default is shift'); + }); + + test('interactive', function() { + ok(typeof canvas.interactive == 'boolean'); + ok(canvas.interactive, 'default is true'); + }); + + test('selection', function() { + ok(typeof canvas.selection == 'boolean'); + ok(canvas.selection, 'default is true'); + }); + + test('_initInteractive', function() { + ok(typeof canvas._initInteractive == 'function'); + }); + + test('renderTop', function() { + ok(typeof canvas.renderTop == 'function'); + equal(canvas, canvas.renderTop()); + }); + + test('_chooseObjectsToRender', function() { + ok(typeof canvas._chooseObjectsToRender == 'function'); + var rect = makeRect(), rect2 = makeRect(), rect3 = makeRect(); + canvas.add(rect); + canvas.add(rect2); + canvas.add(rect3); + var objs = canvas._chooseObjectsToRender(); + equal(objs[0], rect); + equal(objs[1], rect2); + equal(objs[2], rect3); + canvas.setActiveObject(rect); + objs = canvas._chooseObjectsToRender(); + equal(objs[0], rect2); + equal(objs[1], rect3); + equal(objs[2], rect); + canvas.setActiveObject(rect2); + canvas.preserveObjectStacking = true; + objs = canvas._chooseObjectsToRender(); + equal(objs[0], rect); + equal(objs[1], rect2); + equal(objs[2], rect3); + }); + test('calcOffset', function() { ok(typeof canvas.calcOffset == 'function', 'should respond to `calcOffset`'); equal(canvas.calcOffset(), canvas, 'should be chainable'); @@ -214,9 +309,8 @@ equal(canvas, canvas.renderAll()); }); - test('renderTop', function() { - ok(typeof canvas.renderTop == 'function'); - equal(canvas, canvas.renderTop()); + test('_drawSelection', function() { + ok(typeof canvas._drawSelection == 'function'); }); test('findTarget', function() { @@ -793,6 +887,78 @@ }); }); + test('loadFromJSON with custom properties on Canvas with no async object', function() { + var serialized = JSON.parse(PATH_JSON); + serialized.controlsAboveOverlay = true; + serialized.preserveObjectStacking = true; + equal(canvas.controlsAboveOverlay, fabric.Canvas.prototype.controlsAboveOverlay); + equal(canvas.preserveObjectStacking, fabric.Canvas.prototype.preserveObjectStacking); + canvas.loadFromJSON(serialized, function() { + ok(!canvas.isEmpty(), 'canvas is not empty'); + equal(canvas.controlsAboveOverlay, true); + equal(canvas.preserveObjectStacking, true); + }); + // if no async object the callback is called syncronously + equal(canvas.controlsAboveOverlay, true); + equal(canvas.preserveObjectStacking, true); + }); + + asyncTest('loadFromJSON with custom properties on Canvas with image', function() { + var JSON_STRING = '{"objects":[{"type":"image","originX":"left","originY":"top","left":13.6,"top":-1.4,"width":3000,"height":3351,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":0,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":0.05,"scaleY":0.05,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"src":"' + IMG_SRC + '","filters":[],"crossOrigin":"","alignX":"none","alignY":"none","meetOrSlice":"meet"}],' ++ '"background":"green"}'; + var serialized = JSON.parse(JSON_STRING); + serialized.controlsAboveOverlay = true; + serialized.preserveObjectStacking = true; + equal(canvas.controlsAboveOverlay, fabric.Canvas.prototype.controlsAboveOverlay); + equal(canvas.preserveObjectStacking, fabric.Canvas.prototype.preserveObjectStacking); + canvas.loadFromJSON(serialized, function() { + ok(!canvas.isEmpty(), 'canvas is not empty'); + equal(canvas.controlsAboveOverlay, true); + equal(canvas.preserveObjectStacking, true); + start(); + }); + // before callback the properties are still false. + equal(canvas.controlsAboveOverlay, false); + equal(canvas.preserveObjectStacking, false); + }); + + + test('normalize pointer', function(){ + ok(typeof canvas._normalizePointer == 'function'); + var pointer = { x: 10, y: 20 }, + object = makeRect({ top: 10, left: 10, width: 50, height: 50, strokeWidth: 0}), + normalizedPointer = canvas._normalizePointer(object, pointer); + equal(normalizedPointer.x, -25, 'should be in top left corner of rect'); + equal(normalizedPointer.y, -15, 'should be in top left corner of rect'); + object.angle = 90; + normalizedPointer = canvas._normalizePointer(object, pointer); + equal(normalizedPointer.x, -15, 'should consider angle'); + equal(normalizedPointer.y, -25, 'should consider angle'); + object.angle = 0; + object.scaleX = 2; + object.scaleY = 2; + normalizedPointer = canvas._normalizePointer(object, pointer); + equal(normalizedPointer.x, -25, 'should consider scale'); + equal(normalizedPointer.y, -20, 'should consider scale'); + object.skewX = 60; + normalizedPointer = canvas._normalizePointer(object, pointer); + equal(normalizedPointer.x.toFixed(2), -33.66, 'should consider skewX'); + equal(normalizedPointer.y, -20, 'should not change'); + }); + + test('restorePointerVpt', function(){ + ok(typeof canvas.restorePointerVpt == 'function'); + var pointer = { x: 10, y: 20 }, + restoredPointer = canvas.restorePointerVpt(pointer); + equal(restoredPointer.x, pointer.x, 'no changes if not vpt is set'); + equal(restoredPointer.y, pointer.y, 'no changes if not vpt is set'); + canvas.viewportTransform = [2, 0, 0, 2, 50, -60]; + restoredPointer = canvas.restorePointerVpt(pointer); + equal(restoredPointer.x, -20, 'vpt changes restored'); + equal(restoredPointer.y, 40, 'vpt changes restored'); + canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; + }); + // asyncTest('loadFromJSON with backgroundImage', function() { // canvas.setBackgroundImage('../../assets/pug.jpg'); // var anotherCanvas = new fabric.Canvas(); @@ -1315,6 +1481,104 @@ ok(canvas.containsPoint(eventStub, rect), 'on rect at (200, 200) should be within area (175, 175, 225, 225)'); }); + test('setupCurrentTransform', function() { + ok(typeof canvas._setupCurrentTransform == 'function'); + + var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 }); + canvas.add(rect); + var canvasEl = canvas.getElement(), + canvasOffset = fabric.util.getElementOffset(canvasEl); + var eventStub = { + clientX: canvasOffset.left + 100, + clientY: canvasOffset.top + 100, + target: rect + }; + rect.active = true; + canvas._setupCurrentTransform(eventStub, rect); + var t = canvas._currentTransform; + equal(t.target, rect, 'should have rect as a target'); + equal(t.action, 'drag', 'should target inside rect and setup drag'); + equal(t.corner, 0, 'no corner selected'); + equal(t.originX, rect.originX, 'no origin change for drag'); + equal(t.originY, rect.originY, 'no origin change for drag'); + + eventStub = { + clientX: canvasOffset.left + rect.oCoords.tl.corner.tl.x + 1, + clientY: canvasOffset.top + rect.oCoords.tl.corner.tl.y + 1, + target: rect + }; + canvas._setupCurrentTransform(eventStub, rect); + t = canvas._currentTransform; + equal(t.target, rect, 'should have rect as a target'); + equal(t.action, 'scale', 'should target a corner and setup scale'); + equal(t.corner, 'tl', 'tl selected'); + equal(t.originX, 'right', 'origin in opposite direction'); + equal(t.originY, 'bottom', 'origin in opposite direction'); + equal(t.shiftKey, undefined, 'shift was not pressed'); + + eventStub = { + clientX: canvasOffset.left + rect.left - 2, + clientY: canvasOffset.top + rect.top + rect.height/2, + target: rect, + shiftKey: true + }; + canvas._setupCurrentTransform(eventStub, rect); + t = canvas._currentTransform; + equal(t.target, rect, 'should have rect as a target'); + equal(t.action, 'skewY', 'should target a corner and setup skew'); + equal(t.shiftKey, true, 'shift was pressed'); + equal(t.corner, 'ml', 'ml selected'); + equal(t.originX, 'right', 'origin in opposite direction'); + + eventStub = { + clientX: canvasOffset.left + rect.oCoords.mtr.x, + clientY: canvasOffset.top + rect.oCoords.mtr.y, + target: rect, + }; + canvas._setupCurrentTransform(eventStub, rect); + t = canvas._currentTransform; + equal(t.target, rect, 'should have rect as a target'); + equal(t.action, 'rotate', 'should target a corner and setup rotate'); + equal(t.corner, 'mtr', 'mtr selected'); + canvas._currentTransform = false; + }); + + test('_scaleObject', function() { + ok(typeof canvas._scaleObject == 'function'); + var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 }); + canvas.add(rect); + var canvasEl = canvas.getElement(), + canvasOffset = fabric.util.getElementOffset(canvasEl); + var eventStub = { + clientX: canvasOffset.left + rect.oCoords.tl.corner.tl.x + 1, + clientY: canvasOffset.top + rect.oCoords.tl.corner.tl.y + 1, + target: rect + }; + canvas._setupCurrentTransform(eventStub, rect); + var scaled = canvas._scaleObject(30, 30, 'equally'); + equal(scaled, true, 'return true if scaling happened'); + scaled = canvas._scaleObject(30, 30, 'equally'); + equal(scaled, false, 'return false if no movement happen'); + }); + + test('containsPoint in viewport transform', function() { + canvas.viewportTransform = [2, 0, 0, 2, 50, 50]; + var rect = new fabric.Rect({ left: 75, top: 75, width: 50, height: 50 }); + canvas.add(rect); + + var canvasEl = canvas.getElement(), + canvasOffset = fabric.util.getElementOffset(canvasEl); + + var eventStub = { + clientX: canvasOffset.left + 250, + clientY: canvasOffset.top + 250, + target: rect + }; + + ok(canvas.containsPoint(eventStub, rect), 'point at (250, 250) should be within area (75, 75, 125, 125)'); + canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; + }); + asyncTest('fxRemove', function() { ok(typeof canvas.fxRemove == 'function'); diff --git a/test/unit/canvas_static.js b/test/unit/canvas_static.js index 55e663a2ce9..306f1ccb308 100644 --- a/test/unit/canvas_static.js +++ b/test/unit/canvas_static.js @@ -81,7 +81,7 @@ 'backgroundColor': '', 'clipTo': null, 'filters': [], - 'resizeFilters': [], + 'resizeFilters': [], 'fillRule': 'nonzero', 'globalCompositeOperation': 'source-over', 'transformMatrix': null, @@ -158,9 +158,7 @@ canvas.backgroundColor = fabric.StaticCanvas.prototype.backgroundColor; canvas.backgroundImage = fabric.StaticCanvas.prototype.backgroundImage; canvas.overlayColor = fabric.StaticCanvas.prototype.overlayColor; - canvas.controlsAboveOverlay = fabric.StaticCanvas.prototype.controlsAboveOverlay; - canvas.preserveObjectStacking = fabric.StaticCanvas.prototype.preserveObjectStacking; - canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + canvas.viewportTransform = [1, 0, 0, 1, 0, 0]; canvas.calcOffset(); } }); @@ -434,16 +432,6 @@ equal(canvas, canvas.renderAll()); }); - test('preserveObjectStacking', function() { - ok(typeof canvas.preserveObjectStacking == 'boolean'); - ok(!canvas.preserveObjectStacking); - }); - - test('renderTop', function() { - ok(typeof canvas.renderTop == 'function'); - equal(canvas, canvas.renderTop()); - }); - test('toDataURL', function() { ok(typeof canvas.toDataURL == 'function'); if (!fabric.Canvas.supports('toDataURL')) { @@ -486,7 +474,7 @@ equal(rect.getCenterPoint().x, canvas.width / 2, 'object\'s "center.y" property should correspond to canvas element\'s center'); canvas.setZoom(4); equal(rect.getCenterPoint().x, canvas.height / 2, 'object\'s "center.x" property should correspond to canvas element\'s center when canvas is transformed'); - + canvas.setZoom(1); }); test('centerObjectV', function() { @@ -511,6 +499,7 @@ canvas.setZoom(4); equal(rect.getCenterPoint().y, canvas.height / 2, 'object\'s "center.y" property should correspond to canvas element\'s center when canvas is transformed'); equal(rect.getCenterPoint().x, canvas.height / 2, 'object\'s "center.x" property should correspond to canvas element\'s center when canvas is transformed'); + canvas.setZoom(1); }); test('viewportCenterObjectH', function() { @@ -924,42 +913,6 @@ }); }); - test('loadFromJSON with custom properties on Canvas with no async object', function() { - var serialized = JSON.parse(PATH_JSON); - serialized.controlsAboveOverlay = true; - serialized.preserveObjectStacking = true; - equal(canvas.controlsAboveOverlay, fabric.Canvas.prototype.controlsAboveOverlay); - equal(canvas.preserveObjectStacking, fabric.Canvas.prototype.preserveObjectStacking); - canvas.loadFromJSON(serialized, function() { - ok(!canvas.isEmpty(), 'canvas is not empty'); - equal(canvas.controlsAboveOverlay, true); - equal(canvas.preserveObjectStacking, true); - }); - // if no async object the callback is called syncronously - equal(canvas.controlsAboveOverlay, true); - equal(canvas.preserveObjectStacking, true); - }); - - asyncTest('loadFromJSON with custom properties on Canvas with image', function() { - var JSON_STRING = '{"objects":[{"type":"image","originX":"left","originY":"top","left":13.6,"top":-1.4,"width":3000,"height":3351,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":0,"strokeDashArray":null,"strokeLineCap":"butt","strokeLineJoin":"miter","strokeMiterLimit":10,"scaleX":0.05,"scaleY":0.05,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"clipTo":null,"backgroundColor":"","fillRule":"nonzero","globalCompositeOperation":"source-over","transformMatrix":null,"skewX":0,"skewY":0,"src":"' + IMG_SRC + '","filters":[],"crossOrigin":"","alignX":"none","alignY":"none","meetOrSlice":"meet"}],' -+ '"background":"green"}'; - var serialized = JSON.parse(JSON_STRING); - serialized.controlsAboveOverlay = true; - serialized.preserveObjectStacking = true; - equal(canvas.controlsAboveOverlay, fabric.Canvas.prototype.controlsAboveOverlay); - equal(canvas.preserveObjectStacking, fabric.Canvas.prototype.preserveObjectStacking); - canvas.loadFromJSON(serialized, function() { - ok(!canvas.isEmpty(), 'canvas is not empty'); - equal(canvas.controlsAboveOverlay, true); - equal(canvas.preserveObjectStacking, true); - start(); - }); - // before callback the properties are still false. - equal(canvas.controlsAboveOverlay, false); - equal(canvas.preserveObjectStacking, false); - }); - - asyncTest('loadFromJSON with image background and color', function() { var serialized = JSON.parse(PATH_JSON); serialized.background = 'green'; @@ -1324,6 +1277,87 @@ }); }); + test('setViewportTransform', function() { + ok(typeof canvas.setViewportTransform == 'function'); + var vpt = [2, 0, 0, 2, 50, 50]; + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix'); + canvas.setViewportTransform(vpt); + deepEqual(canvas.viewportTransform, vpt, 'viewport now is the set one'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('getZoom', function() { + ok(typeof canvas.getZoom == 'function'); + var vpt = [2, 0, 0, 2, 50, 50]; + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + deepEqual(canvas.getZoom(), 1, 'initial zoom is 1'); + canvas.setViewportTransform(vpt); + deepEqual(canvas.getZoom(), 2, 'zoom is set to 2'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('setZoom', function() { + ok(typeof canvas.setZoom == 'function'); + deepEqual(canvas.getZoom(), 1, 'initial zoom is 1'); + canvas.setZoom(2); + deepEqual(canvas.getZoom(), 2, 'zoom is set to 2'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('zoomToPoint', function() { + ok(typeof canvas.zoomToPoint == 'function'); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix'); + var point = new fabric.Point(50, 50); + canvas.zoomToPoint(point, 1); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'viewport has no changes if not moving with zoom level'); + canvas.zoomToPoint(point, 2); + deepEqual(canvas.viewportTransform, [2, 0, 0, 2, -50, -50], 'viewport has a translation effect and zoom'); + canvas.zoomToPoint(point, 3); + deepEqual(canvas.viewportTransform, [3, 0, 0, 3, -100, -100], 'viewport has a translation effect and zoom'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('absolutePan', function() { + ok(typeof canvas.absolutePan == 'function'); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, 0, 0], 'initial viewport is identity matrix'); + var point = new fabric.Point(50, 50); + canvas.absolutePan(point); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, -point.x, -point.y], 'viewport has translation effect applied'); + canvas.absolutePan(point); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, -point.x, -point.y], 'viewport has same translation effect applied'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('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); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, -50, -50], 'viewport has translation effect applied'); + canvas.relativePan(point); + deepEqual(canvas.viewportTransform, [1, 0, 0, 1, -100, -100], 'viewport has translation effect applied on top of old one'); + canvas.viewportTransform = fabric.StaticCanvas.prototype.viewportTransform; + }); + + test('getActiveObject', function() { + ok(typeof canvas.getActiveObject == 'function'); + var activeObject = canvas.getActiveObject(); + equal(activeObject, null, 'should return null'); + }); + + test('getActiveGroup', function() { + ok(typeof canvas.getActiveGroup == 'function'); + var activeGroup = canvas.getActiveGroup(); + equal(activeGroup, null, 'should return null'); + }); + + test('getContext', function() { + ok(typeof canvas.getContext == 'function'); + var context = canvas.getContext(); + equal(context, canvas.contextContainer, 'should return the context container'); + }); + //how to test with an exception? /*asyncTest('options in setBackgroundImage from invalid URL', function() { canvas.backgroundImage = null;