Limit sin and cosin very small floats on 90/270 degrees (#4734)

* so far ok

* fix transformations decimal

* remove some changes

* less changes

* test passing

* less calculation

* added shortcut

* modified tests
This commit is contained in:
Andrea Bogazzi 2018-02-18 15:56:27 +01:00 committed by GitHub
parent 687cd7b495
commit 2e532cc362
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 260 additions and 73 deletions

View file

@ -45,7 +45,7 @@
mainParameter: 'rotation',
calculateMatrix: function() {
var rad = this.rotation * Math.PI, cos = Math.cos(rad), sin = Math.sin(rad),
var rad = this.rotation * Math.PI, cos = fabric.util.cos(rad), sin = fabric.util.sin(rad),
aThird = 1 / 3, aThirdSqtSin = Math.sqrt(aThird) * sin, OneMinusCos = 1 - cos;
this.matrix = [
1, 0, 0, 0, 0,

View file

@ -10,7 +10,8 @@
}
var degreesToRadians = fabric.util.degreesToRadians,
multiplyMatrices = fabric.util.multiplyTransformMatrices;
multiplyMatrices = fabric.util.multiplyTransformMatrices,
transformPoint = fabric.util.transformPoint;
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
@ -355,45 +356,55 @@
* @chainable
*/
calcCoords: function(absolute) {
var theta = degreesToRadians(this.angle),
var rotateMatrix = this._calcRotateMatrix(),
translateMatrix = this._calcTranslateMatrix(),
startMatrix = multiplyMatrices(translateMatrix, rotateMatrix),
vpt = this.getViewportTransform(),
dim = absolute ? this._getTransformedDimensions() : this._calculateCurrentDimensions(),
currentWidth = dim.x, currentHeight = dim.y,
sinTh = theta ? Math.sin(theta) : 0,
cosTh = theta ? Math.cos(theta) : 1,
_angle = currentWidth > 0 ? Math.atan(currentHeight / currentWidth) : 0,
_hypotenuse = (currentWidth / Math.cos(_angle)) / 2,
offsetX = Math.cos(_angle + theta) * _hypotenuse,
offsetY = Math.sin(_angle + theta) * _hypotenuse,
center = this.getCenterPoint(),
// offset added for rotate and scale actions
coords = absolute ? center : fabric.util.transformPoint(center, vpt),
tl = new fabric.Point(coords.x - offsetX, coords.y - offsetY),
tr = new fabric.Point(tl.x + (currentWidth * cosTh), tl.y + (currentWidth * sinTh)),
bl = new fabric.Point(tl.x - (currentHeight * sinTh), tl.y + (currentHeight * cosTh)),
br = new fabric.Point(coords.x + offsetX, coords.y + offsetY);
finalMatrix = absolute ? startMatrix : multiplyMatrices(vpt, startMatrix),
dim = this._getTransformedDimensions(),
w = dim.x / 2, h = dim.y / 2,
tl = transformPoint({ x: -w, y: -h }, finalMatrix),
tr = transformPoint({ x: w, y: -h }, finalMatrix),
bl = transformPoint({ x: -w, y: h }, finalMatrix),
br = transformPoint({ x: w, y: h }, finalMatrix);
if (!absolute) {
var padding = this.padding, angle = degreesToRadians(this.angle),
cos = fabric.util.cos(angle), sin = fabric.util.sin(angle),
cosP = cos * padding, sinP = sin * padding, cosPSinP = cosP + sinP,
cosPMinusSinP = cosP - sinP;
if (padding) {
tl.x -= cosPMinusSinP;
tl.y -= cosPSinP;
tr.x += cosPSinP;
tr.y -= cosPMinusSinP;
bl.x -= cosPSinP;
bl.y += cosPMinusSinP;
br.x += cosPMinusSinP;
br.y += cosPSinP;
}
var ml = new fabric.Point((tl.x + bl.x) / 2, (tl.y + bl.y) / 2),
mt = new fabric.Point((tr.x + tl.x) / 2, (tr.y + tl.y) / 2),
mr = new fabric.Point((br.x + tr.x) / 2, (br.y + tr.y) / 2),
mb = new fabric.Point((br.x + bl.x) / 2, (br.y + bl.y) / 2),
mtr = new fabric.Point(mt.x + sinTh * this.rotatingPointOffset, mt.y - cosTh * this.rotatingPointOffset);
mtr = new fabric.Point(mt.x + sin * this.rotatingPointOffset, mt.y - cos * this.rotatingPointOffset);
}
// debugging
/* setTimeout(function() {
canvas.contextTop.fillStyle = 'green';
canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
canvas.contextTop.fillRect(br.x, br.y, 3, 3);
canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3);
}, 50); */
// if (!absolute) {
// var canvas = this.canvas;
// setTimeout(function() {
// canvas.contextTop.clearRect(0, 0, 700, 700);
// canvas.contextTop.fillStyle = 'green';
// canvas.contextTop.fillRect(mb.x, mb.y, 3, 3);
// canvas.contextTop.fillRect(bl.x, bl.y, 3, 3);
// canvas.contextTop.fillRect(br.x, br.y, 3, 3);
// canvas.contextTop.fillRect(tl.x, tl.y, 3, 3);
// canvas.contextTop.fillRect(tr.x, tr.y, 3, 3);
// canvas.contextTop.fillRect(ml.x, ml.y, 3, 3);
// canvas.contextTop.fillRect(mr.x, mr.y, 3, 3);
// canvas.contextTop.fillRect(mt.x, mt.y, 3, 3);
// canvas.contextTop.fillRect(mtr.x, mtr.y, 3, 3);
// }, 50);
// }
var coords = {
// corners
@ -437,16 +448,21 @@
*/
_calcRotateMatrix: function() {
if (this.angle) {
var theta = degreesToRadians(this.angle), cos = Math.cos(theta), sin = Math.sin(theta);
// trying to keep rounding error small, ugly but it works.
if (cos === 6.123233995736766e-17 || cos === -1.8369701987210297e-16) {
cos = 0;
}
var theta = degreesToRadians(this.angle), cos = fabric.util.cos(theta), sin = fabric.util.sin(theta);
return [cos, sin, -sin, cos, 0, 0];
}
return fabric.iMatrix.concat();
},
/**
* calculate the translation matrix for an object transform
* @return {Array} rotation matrix for the object
*/
_calcTranslateMatrix: function() {
var center = this.getCenterPoint();
return [1, 0, 0, 1, center.x, center.y];
},
transformMatrixKey: function(skipGroup) {
var sep = '_', prefix = '';
if (!skipGroup && this.group) {
@ -485,8 +501,7 @@
if (cache.key === key) {
return cache.value;
}
var center = this.getCenterPoint(),
matrix = [1, 0, 0, 1, center.x, center.y],
var matrix = this._calcTranslateMatrix(),
rotateMatrix,
dimensionMatrix = this._calcDimensionsTransformMatrix(this.skewX, this.skewY, true);
if (this.angle) {
@ -542,8 +557,11 @@
if (typeof skewY === 'undefined') {
skewY = this.skewY;
}
var dimensions = this._getNonTransformedDimensions(),
dimX = dimensions.x / 2, dimY = dimensions.y / 2,
var dimensions = this._getNonTransformedDimensions();
if (skewX === 0 && skewY === 0) {
return { x: dimensions.x * this.scaleX, y: dimensions.y * this.scaleY };
}
var dimX = dimensions.x / 2, dimY = dimensions.y / 2,
points = [
{
x: -dimX,

View file

@ -79,8 +79,8 @@
/* Math.sqrt(2 * Math.pow(this.cornerSize, 2)) / 2, */
/* 0.707106 stands for sqrt(2)/2 */
cornerHypotenuse = this.cornerSize * 0.707106,
cosHalfOffset = cornerHypotenuse * Math.cos(newTheta),
sinHalfOffset = cornerHypotenuse * Math.sin(newTheta),
cosHalfOffset = cornerHypotenuse * fabric.util.cos(newTheta),
sinHalfOffset = cornerHypotenuse * fabric.util.sin(newTheta),
x, y;
for (var point in coords) {

View file

@ -182,8 +182,8 @@
adjustPosition: function(to) {
var angle = degreesToRadians(this.angle),
hypotFull = this.getScaledWidth(),
xFull = Math.cos(angle) * hypotFull,
yFull = Math.sin(angle) * hypotFull,
xFull = fabric.util.cos(angle) * hypotFull,
yFull = fabric.util.sin(angle) * hypotFull,
offsetFrom, offsetTo;
//TODO: this function does not consider mixed situation like top, center.

View file

@ -186,7 +186,7 @@
*/
fabric.parseTransformAttribute = (function() {
function rotateMatrix(matrix, args) {
var cos = Math.cos(args[0]), sin = Math.sin(args[0]),
var cos = fabric.util.cos(args[0]), sin = fabric.util.sin(args[0]),
x = 0, y = 0;
if (args.length === 3) {
x = args[1];

View file

@ -96,10 +96,10 @@
);
}
else {
var startX = Math.cos(this.startAngle) * this.radius,
startY = Math.sin(this.startAngle) * this.radius,
endX = Math.cos(this.endAngle) * this.radius,
endY = Math.sin(this.endAngle) * this.radius,
var startX = fabric.util.cos(this.startAngle) * this.radius,
startY = fabric.util.sin(this.startAngle) * this.radius,
endX = fabric.util.cos(this.endAngle) * this.radius,
endY = fabric.util.sin(this.endAngle) * this.radius,
largeFlag = angle > pi ? '1' : '0';
markup.push(

View file

@ -16,8 +16,8 @@
}
var PI = Math.PI, th = rotateX * PI / 180,
sinTh = Math.sin(th),
cosTh = Math.cos(th),
sinTh = fabric.util.sin(th),
cosTh = fabric.util.cos(th),
fromX = 0, fromY = 0;
rx = Math.abs(rx);
@ -76,10 +76,10 @@
return segmentToBezierCache[argsString2];
}
var costh2 = Math.cos(th2),
sinth2 = Math.sin(th2),
costh3 = Math.cos(th3),
sinth3 = Math.sin(th3),
var costh2 = fabric.util.cos(th2),
sinth2 = fabric.util.sin(th2),
costh3 = fabric.util.cos(th3),
sinth3 = fabric.util.sin(th3),
toX = cosTh * rx * costh3 - sinTh * ry * sinth3 + cx1,
toY = sinTh * rx * costh3 + cosTh * ry * sinth3 + cy1,
cp1X = fromX + mT * ( -cosTh * rx * sinth2 - sinTh * ry * costh2),

View file

@ -4,13 +4,57 @@
atan2 = Math.atan2,
pow = Math.pow,
abs = Math.abs,
PiBy180 = Math.PI / 180;
PiBy180 = Math.PI / 180,
PiBy2 = Math.PI / 2;
/**
* @namespace fabric.util
*/
fabric.util = {
/**
* Calculate the cos of an angle, avoiding returning floats for known results
* @static
* @memberOf fabric.util
* @param {Number} angle the angle in radians or in degree
* @return {Number}
*/
cos: function(angle) {
if (angle === 0) { return 1; }
if (angle < 0) {
// cos(a) = cos(-a)
angle = -angle;
}
var angleSlice = angle / PiBy2;
switch (angleSlice) {
case 1: case 3: return 0;
case 2: return -1;
}
return Math.cos(angle);
},
/**
* Calculate the sin of an angle, avoiding returning floats for known results
* @static
* @memberOf fabric.util
* @param {Number} angle the angle in radians or in degree
* @return {Number}
*/
sin: function(angle) {
if (angle === 0) { return 0; }
var angleSlice = angle / PiBy2, sign = 1;
if (angle < 0) {
// sin(-a) = -sin(a)
sign = -1;
}
switch (angleSlice) {
case 1: return sign;
case 2: return 0;
case 3: return -sign;
}
return Math.sin(angle);
},
/**
* Removes value from an array.
* Presence of value (and its position in an array) is determined via `Array.prototype.indexOf`
@ -86,8 +130,8 @@
* @return {Object} The new rotated point
*/
rotateVector: function(vector, radians) {
var sin = Math.sin(radians),
cos = Math.cos(radians),
var sin = fabric.util.sin(radians),
cos = fabric.util.cos(radians),
rx = vector.x * cos - vector.y * sin,
ry = vector.x * sin + vector.y * cos;
return {

View file

@ -672,13 +672,132 @@
assert.deepEqual(coords[3], new fabric.Point(40, 47), 'return bottom left corner cached oCoords');
});
// QUnit.test('getCoords return coordinate of object', function(assert) {
// var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40 });
// var coords = cObj.getCoords();
// assert.equal(coords[0], { x: 40, y: 30 }, 'return top left corner');
// assert.equal(coords[1], { x: 1, y: 1 }, 'return top right corner');
// assert.equal(coords[2], { x: 1, y: 1 }, 'return bottom right corner');
// assert.equal(coords[3], { x: 1, y: 1 }, 'return bottom left corner');
// });
QUnit.test('getCoords absolute with angle', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, angle: 20 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords(true);
assert.deepEqual(coords[0].x, 40, 'return top left absolute with angle X');
assert.deepEqual(coords[1].x, 51.2763114494309, 'return top right absolute with angle X');
assert.deepEqual(coords[2].x, 45.46196901289453, 'return bottom right absolute with angle X');
assert.deepEqual(coords[3].x, 34.18565756346363, 'return bottom left absolute with angle X');
assert.deepEqual(coords[0].y, 30, 'return top left absolute with angle Y');
assert.deepEqual(coords[1].y, 34.104241719908025, 'return top right absolute with angle Y');
assert.deepEqual(coords[2].y, 50.079016273268465, 'return bottom right absolute with angle Y');
assert.deepEqual(coords[3].y, 45.97477455336044, 'return bottom left absolute with angle Y');
});
QUnit.test('getCoords with angle', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, angle: 20 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords();
assert.deepEqual(coords[0].x, 115, 'return top left with angle X');
assert.deepEqual(coords[1].x, 137.55262289886178, 'return top right with angle X');
assert.deepEqual(coords[2].x, 125.92393802578906, 'return bottom right with angle X');
assert.deepEqual(coords[3].x, 103.37131512692726, 'return bottom left with angle X');
assert.deepEqual(coords[0].y, 85, 'return top left with angle Y');
assert.deepEqual(coords[1].y, 93.20848343981605, 'return top right with angle Y');
assert.deepEqual(coords[2].y, 125.15803254653693, 'return bottom right with angle Y');
assert.deepEqual(coords[3].y, 116.94954910672088, 'return bottom left with angle Y');
});
QUnit.test('getCoords absolute with skewX', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewX: 45 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords(true);
assert.deepEqual(coords[0].x, 40, 'return top left absolute with skewX X');
assert.deepEqual(coords[1].x, 69, 'return top right absolute with skewX X');
assert.deepEqual(coords[2].x, 69, 'return bottom absolute right with skewX X');
assert.deepEqual(coords[3].x, 40, 'return bottom absolute left with skewX X');
assert.deepEqual(coords[0].y, 30, 'return top left absolute with skewX Y');
assert.deepEqual(coords[1].y, 30, 'return top right absolute with skewX Y');
assert.deepEqual(coords[2].y, 47, 'return bottom absolute right with skewX Y');
assert.deepEqual(coords[3].y, 47, 'return bottom absolute left with skewX Y');
});
QUnit.test('getCoords with skewX', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewX: 45 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords();
assert.deepEqual(coords[0].x, 115, 'return top left with skewX X');
assert.deepEqual(coords[1].x, 173, 'return top right with skewX X');
assert.deepEqual(coords[2].x, 173, 'return bottom right with skewX X');
assert.deepEqual(coords[3].x, 115, 'return bottom left with skewX X');
assert.deepEqual(coords[0].y, 85, 'return top left with skewX Y');
assert.deepEqual(coords[1].y, 85, 'return top right with skewX Y');
assert.deepEqual(coords[2].y, 119, 'return bottom right with skewX Y');
assert.deepEqual(coords[3].y, 119, 'return bottom left with skewX Y');
});
QUnit.test('getCoords absolute with skewY', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewY: 45 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords(true);
assert.deepEqual(coords[0].x, 40, 'return top left absolute with skewY X');
assert.deepEqual(coords[1].x, 52, 'return top right absolute with skewY X');
assert.deepEqual(coords[2].x, 52, 'return bottom absolute right with skewY X');
assert.deepEqual(coords[3].x, 40, 'return bottom absolute left with skewY X');
assert.deepEqual(coords[0].y, 30, 'return top left absolute with skewY Y');
assert.deepEqual(coords[1].y, 30, 'return top right absolute with skewY Y');
assert.deepEqual(coords[2].y, 59, 'return bottom absolute right with skewY Y');
assert.deepEqual(coords[3].y, 59, 'return bottom absolute left with skewY Y');
});
QUnit.test('getCoords with skewY', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewY: 45 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords();
assert.deepEqual(coords[0].x, 115, 'return top left with skewY X');
assert.deepEqual(coords[1].x, 139, 'return top right with skewY X');
assert.deepEqual(coords[2].x, 139, 'return bottom right with skewY X');
assert.deepEqual(coords[3].x, 115, 'return bottom left with skewY X');
assert.deepEqual(coords[0].y, 85, 'return top left with skewY Y');
assert.deepEqual(coords[1].y, 85, 'return top right with skewY Y');
assert.deepEqual(coords[2].y, 143, 'return bottom right with skewY Y');
assert.deepEqual(coords[3].y, 143, 'return bottom left with skewY Y');
});
QUnit.test('getCoords absolute with skewY skewX angle', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewY: 45, skewX: 30, angle: 90 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords(true);
assert.deepEqual(coords[0].x, 40, 'return top left absolute with skewY skewX angle X');
assert.deepEqual(coords[1].x, 40, 'return top right absolute with skewY skewX angle X');
assert.deepEqual(coords[2].x, 11, 'return bottom absolute right with skewY skewX angle X');
assert.deepEqual(coords[3].x, 11, 'return bottom absolute left with skewY skewX angle X');
assert.deepEqual(coords[0].y, 30, 'return top left absolute with skewY skewX angle Y');
assert.deepEqual(coords[1].y, 58.74315780649914, 'return top right absolute with skewY skewX angle Y');
assert.deepEqual(coords[2].y, 58.74315780649914, 'return bottom absolute right with skewY skewX angle Y');
assert.deepEqual(coords[3].y, 30, 'return bottom absolute left with skewY skewX angle Y');
});
QUnit.test('getCoords with skewY skewX angle', function(assert) {
var cObj = new fabric.Object({ width: 10, height: 15, strokeWidth: 2, top: 30, left: 40, skewY: 45, skewX: 30, angle: 90 });
cObj.canvas = {
viewportTransform: [2, 0, 0, 2, 35, 25]
};
var coords = cObj.getCoords();
assert.deepEqual(coords[0].x, 115, 'return top left with skewY skewX angle X');
assert.deepEqual(coords[1].x, 115, 'return top right with skewY skewX angle X');
assert.deepEqual(coords[2].x, 57, 'return bottom right with skewY skewX angle X');
assert.deepEqual(coords[3].x, 57, 'return bottom left with skewY skewX angle X');
assert.deepEqual(coords[0].y, 85, 'return top left with skewY skewX angle Y');
assert.deepEqual(coords[1].y, 142.48631561299828, 'return top right with skewY skewX angle Y');
assert.deepEqual(coords[2].y, 142.48631561299828, 'return bottom right with skewY skewX angle Y');
assert.deepEqual(coords[3].y, 85, 'return bottom left with skewY skewX angle Y');
});
})();

View file

@ -286,7 +286,7 @@
var ANGLE = ANGLE_DEG * Math.PI / 180;
element.setAttribute('transform', 'rotate(' + ANGLE_DEG + ')');
parsedValue = fabric.parseTransformAttribute(element.getAttribute('transform'));
assert.deepEqual(parsedValue, [Math.cos(ANGLE), Math.sin(ANGLE), -Math.sin(ANGLE), Math.cos(ANGLE), 0, 0]);
assert.deepEqual(parsedValue, [fabric.util.cos(ANGLE), fabric.util.sin(ANGLE), -fabric.util.sin(ANGLE), fabric.util.cos(ANGLE), 0, 0]);
element.setAttribute('transform', 'scale(3.5)');
parsedValue = fabric.parseTransformAttribute(element.getAttribute('transform'));

View file

@ -221,14 +221,12 @@
}));
var ANGLE_DEG = 90;
var ANGLE = ANGLE_DEG * Math.PI / 180;
elPath.setAttribute('transform', 'rotate(' + ANGLE_DEG + ')');
fabric.Path.fromElement(elPath, function(path) {
assert.deepEqual(
path.get('transformMatrix'),
[Math.cos(ANGLE), Math.sin(ANGLE), -Math.sin(ANGLE), Math.cos(ANGLE), 0, 0]
[0, 1, -1, 0, 0, 0]
);
done();
});

View file

@ -910,7 +910,7 @@
{ x: 0, y: -8.318331151877368 },
{ x: 133.33333333333331, y: 19.99999999999999 },
{ x: 100.00000000000003, y: 19.99999999999999 },
{ x: 147.19721858646224, y: 100 }
{ x: 147.19721858646224, y: 100 },
];
assert.deepEqual(bounds, expectedBounds, 'bounds are as expected');
});
@ -951,4 +951,12 @@
var val = fabric.util.capValue(3, 80, 70);
assert.equal(val, 70, 'max cap');
});
QUnit.test('fabric.util.cos', function(assert) {
assert.ok(typeof fabric.util.cos === 'function');
assert.equal(fabric.util.cos(0), 1, 'cos 0 correct');
assert.equal(fabric.util.cos(Math.PI / 2), 0, 'cos 90 correct');
assert.equal(fabric.util.cos(Math.PI), -1, 'cos 180 correct');
assert.equal(fabric.util.cos(3 * Math.PI / 2), 0,' cos 270 correct');
});
})();