Merge pull request #1452 from twffy/master

Programmatic Canvas Zooming
This commit is contained in:
Juriy Zaytsev 2014-07-06 19:57:23 +02:00
commit 4e64cf499a
18 changed files with 372 additions and 205 deletions

View file

@ -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 });

View file

@ -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();
},
/**

View file

@ -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;

View file

@ -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();
}
});

View file

@ -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;

View file

@ -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 });

View file

@ -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) ' : '',

View file

@ -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

View file

@ -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);
}
},

View file

@ -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);

View file

@ -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;
},

View file

@ -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();
},
/**

View file

@ -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

View file

@ -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();
},

View file

@ -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();
},

View file

@ -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();
},

View file

@ -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 &lt;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);
}
},

View file

@ -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