Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

802 lines
23 KiB

/**
* Utility class to calculate [affine transformation](http://en.wikipedia.org/wiki/Affine_transformation) matrix.
*
* This class is compatible with [SVGMatrix](http://www.w3.org/TR/SVG11/coords.html#InterfaceSVGMatrix) except:
*
* 1. Ext.draw.Matrix is not read only.
* 2. Using Number as its components rather than floats.
*
* Using this class helps to reduce the severe numeric
* [problem with HTML Canvas and SVG transformation](http://stackoverflow.com/questions/8784405/large-numbers-in-html-canvas-translate-result-in-strange-behavior)
*
* There's also no way to get current transformation matrix [in Canvas](http://stackoverflow.com/questions/7395813/html5-canvas-get-transform-matrix).
*/
Ext.define('Ext.draw.Matrix', {
isMatrix: true,
statics: {
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
* @param {Number} y1
* @param {Number} x0p
* @param {Number} y0p
* @param {Number} x1p
* @param {Number} y1p
*/
createAffineMatrixFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
var dx = x1 - x0,
dy = y1 - y0,
dxp = x1p - x0p,
dyp = y1p - y0p,
r = 1 / (dx * dx + dy * dy),
a = dx * dxp + dy * dyp,
b = dxp * dy - dx * dyp,
c = -a * x0 - b * y0,
f = b * x0 - a * y0;
return new this(a * r, -b * r, b * r, a * r, c * r + x0p, f * r + y0p);
},
/**
* @static
* Return the affine matrix that transform two points (x0, y0) and (x1, y1) to (x0p, y0p) and (x1p, y1p)
* @param {Number} x0
* @param {Number} y0
* @param {Number} x1
* @param {Number} y1
* @param {Number} x0p
* @param {Number} y0p
* @param {Number} x1p
* @param {Number} y1p
*/
createPanZoomFromTwoPair: function (x0, y0, x1, y1, x0p, y0p, x1p, y1p) {
if (arguments.length === 2) {
return this.createPanZoomFromTwoPair.apply(this, x0.concat(y0));
}
var dx = x1 - x0,
dy = y1 - y0,
cx = (x0 + x1) * 0.5,
cy = (y0 + y1) * 0.5,
dxp = x1p - x0p,
dyp = y1p - y0p,
cxp = (x0p + x1p) * 0.5,
cyp = (y0p + y1p) * 0.5,
r = dx * dx + dy * dy,
rp = dxp * dxp + dyp * dyp,
scale = Math.sqrt(rp / r);
return new this(scale, 0, 0, scale, cxp - scale * cx, cyp - scale * cy);
},
/**
* @static
* Create a flyweight to wrap the given array.
* The flyweight will directly refer the object and the elements can be changed by other methods.
*
* Do not hold the instance of flyweight matrix.
*
* @param {Array} elements
* @return {Ext.draw.Matrix}
*/
fly: (function () {
var flyMatrix = null,
simplefly = function (elements) {
flyMatrix.elements = elements;
return flyMatrix;
};
return function (elements) {
if (!flyMatrix) {
flyMatrix = new Ext.draw.Matrix();
}
flyMatrix.elements = elements;
Ext.draw.Matrix.fly = simplefly;
return flyMatrix;
};
})(),
/**
* @static
* Create a matrix from `mat`. If `mat` is already a matrix, returns it.
* @param {Mixed} mat
* @return {Ext.draw.Matrix}
*/
create: function (mat) {
if (mat instanceof this) {
return mat;
}
return new this(mat);
}
},
/**
* Create an affine transform matrix.
*
* @param {Number} xx Coefficient from x to x
* @param {Number} xy Coefficient from x to y
* @param {Number} yx Coefficient from y to x
* @param {Number} yy Coefficient from y to y
* @param {Number} dx Offset of x
* @param {Number} dy Offset of y
*/
constructor: function (xx, xy, yx, yy, dx, dy) {
if (xx && xx.length === 6) {
this.elements = xx.slice();
} else if (xx !== undefined) {
this.elements = [xx, xy, yx, yy, dx, dy];
} else {
this.elements = [1, 0, 0, 1, 0, 0];
}
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
*
* @param {Number} xx Coefficient from x to x.
* @param {Number} xy Coefficient from x to y.
* @param {Number} yx Coefficient from y to x.
* @param {Number} yy Coefficient from y to y.
* @param {Number} dx Offset of x.
* @param {Number} dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
prepend: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + yx * xy0;
elements[1] = xy * xx0 + yy * xy0;
elements[2] = xx * yx0 + yx * yy0;
elements[3] = xy * yx0 + yy * yy0;
elements[4] = xx * dx0 + yx * dy0 + dx;
elements[5] = xy * dx0 + yy * dy0 + dy;
return this;
},
/**
* Prepend a matrix onto the current.
*
* __Note:__ The given transform will come after the current one.
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
prependMatrix: function (matrix) {
return this.prepend.apply(this, matrix.elements);
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param {Number} xx Coefficient from x to x.
* @param {Number} xy Coefficient from x to y.
* @param {Number} yx Coefficient from y to x.
* @param {Number} yy Coefficient from y to y.
* @param {Number} dx Offset of x.
* @param {Number} dy Offset of y.
* @return {Ext.draw.Matrix} this
*/
append: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements,
xx0 = elements[0],
xy0 = elements[1],
yx0 = elements[2],
yy0 = elements[3],
dx0 = elements[4],
dy0 = elements[5];
elements[0] = xx * xx0 + xy * yx0;
elements[1] = xx * xy0 + xy * yy0;
elements[2] = yx * xx0 + yy * yx0;
elements[3] = yx * xy0 + yy * yy0;
elements[4] = dx * xx0 + dy * yx0 + dx0;
elements[5] = dx * xy0 + dy * yy0 + dy0;
return this;
},
/**
* Postpend a matrix onto the current.
*
* __Note:__ The given transform will come before the current one.
*
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
appendMatrix: function (matrix) {
return this.append.apply(this, matrix.elements);
},
/**
* Set the elements of a Matrix
* @param {Number} xx
* @param {Number} xy
* @param {Number} yx
* @param {Number} yy
* @param {Number} dx
* @param {Number} dy
* @return {Ext.draw.Matrix} this
*/
set: function (xx, xy, yx, yy, dx, dy) {
var elements = this.elements;
elements[0] = xx;
elements[1] = xy;
elements[2] = yx;
elements[3] = yy;
elements[4] = dx;
elements[5] = dy;
return this;
},
/**
* Return a new matrix represents the opposite transformation of the current one.
*
* @param {Ext.draw.Matrix} [target] A target matrix. If present, it will receive
* the result of inversion to avoid creating a new object.
*
* @return {Ext.draw.Matrix}
*/
inverse: function (target) {
var elements = this.elements,
a = elements[0],
b = elements[1],
c = elements[2],
d = elements[3],
e = elements[4],
f = elements[5],
rDim = 1 / (a * d - b * c);
a *= rDim;
b *= rDim;
c *= rDim;
d *= rDim;
if (target) {
target.set(d, -b, -c, a, c * f - d * e, b * e - a * f);
return target;
} else {
return new Ext.draw.Matrix(d, -b, -c, a, c * f - d * e, b * e - a * f);
}
},
/**
* Translate the matrix.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
translate: function (x, y, prepend) {
if (prepend) {
return this.prepend(1, 0, 0, 1, x, y);
} else {
return this.append(1, 0, 0, 1, x, y);
}
},
/**
* Scale the matrix.
*
* @param {Number} sx
* @param {Number} sy
* @param {Number} scx
* @param {Number} scy
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
scale: function (sx, sy, scx, scy, prepend) {
var me = this;
// null or undefined
if (sy == null) {
sy = sx;
}
if (scx === undefined) {
scx = 0;
}
if (scy === undefined) {
scy = 0;
}
if (prepend) {
return me.prepend(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
} else {
return me.append(sx, 0, 0, sy, scx - scx * sx, scy - scy * sy);
}
},
/**
* Rotate the matrix.
*
* @param {Number} angle Radians to rotate
* @param {Number|null} rcx Center of rotation.
* @param {Number|null} rcy Center of rotation.
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotate: function (angle, rcx, rcy, prepend) {
var me = this,
cos = Math.cos(angle),
sin = Math.sin(angle);
rcx = rcx || 0;
rcy = rcy || 0;
if (prepend) {
return me.prepend(
cos, sin,
-sin, cos,
rcx - cos * rcx + rcy * sin,
rcy - cos * rcy - rcx * sin
);
} else {
return me.append(
cos, sin,
-sin, cos,
rcx - cos * rcx + rcy * sin,
rcy - cos * rcy - rcx * sin
);
}
},
/**
* Rotate the matrix by the angle of a vector.
*
* @param {Number} x
* @param {Number} y
* @param {Boolean} [prepend] If `true`, this will transformation be prepended to the matrix.
* @return {Ext.draw.Matrix} this
*/
rotateFromVector: function (x, y, prepend) {
var me = this,
d = Math.sqrt(x * x + y * y),
cos = x / d,
sin = y / d;
if (prepend) {
return me.prepend(cos, sin, -sin, cos, 0, 0);
} else {
return me.append(cos, sin, -sin, cos, 0, 0);
}
},
/**
* Clone this matrix.
* @return {Ext.draw.Matrix}
*/
clone: function () {
return new Ext.draw.Matrix(this.elements);
},
/**
* Horizontally flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipX: function () {
return this.append(-1, 0, 0, 1, 0, 0);
},
/**
* Vertically flip the matrix
* @return {Ext.draw.Matrix} this
*/
flipY: function () {
return this.append(1, 0, 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewX: function (angle) {
return this.append(1, Math.tan(angle), 0, -1, 0, 0);
},
/**
* Skew the matrix
* @param {Number} angle
* @return {Ext.draw.Matrix} this
*/
skewY: function (angle) {
return this.append(1, 0, Math.tan(angle), -1, 0, 0);
},
/**
* Reset the matrix to identical.
* @return {Ext.draw.Matrix} this
*/
reset: function () {
return this.set(1, 0, 0, 1, 0, 0);
},
/**
* @private
* Split Matrix to `{{devicePixelRatio,c,0},{b,devicePixelRatio,0},{0,0,1}}.{{xx,0,dx},{0,yy,dy},{0,0,1}}`
* @return {Object} Object with b,c,d=devicePixelRatio,xx,yy,dx,dy
*/
precisionCompensate: function (devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
r = x2y * y2x - x2x * y2y;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * y2x / y2y;
comp.d = devicePixelRatio;
comp.xx = x2x / devicePixelRatio;
comp.yy = y2y / devicePixelRatio;
comp.dx = (newDy * x2x * y2x - newDx * x2x * y2y) / r / devicePixelRatio;
comp.dy = (newDx * x2y * y2y - newDy * x2x * y2y) / r / devicePixelRatio;
},
/**
* @private
* Split Matrix to `{{1,c,0},{b,d,0},{0,0,1}}.{{xx,0,dx},{0,xx,dy},{0,0,1}}`
* @return {Object} Object with b,c,d,xx,yy=xx,dx,dy
*/
precisionCompensateRect: function (devicePixelRatio, comp) {
var elements = this.elements,
x2x = elements[0],
x2y = elements[1],
y2x = elements[2],
y2y = elements[3],
newDx = elements[4],
newDy = elements[5],
yxOnXx = y2x / x2x;
comp.b = devicePixelRatio * x2y / x2x;
comp.c = devicePixelRatio * yxOnXx;
comp.d = devicePixelRatio * y2y / x2x;
comp.xx = x2x / devicePixelRatio;
comp.yy = x2x / devicePixelRatio;
comp.dx = (newDy * y2x - newDx * y2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
comp.dy = -(newDy * x2x - newDx * x2y) / (x2y * yxOnXx - y2y) / devicePixelRatio;
},
/**
* Transform point returning the x component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} x component of the result.
*/
x: function (x, y) {
var elements = this.elements;
return x * elements[0] + y * elements[2] + elements[4];
},
/**
* Transform point returning the y component of the result.
* @param {Number} x
* @param {Number} y
* @return {Number} y component of the result.
*/
y: function (x, y) {
var elements = this.elements;
return x * elements[1] + y * elements[3] + elements[5];
},
/**
* @private
* @param {Number} i
* @param {Number} j
* @return {String}
*/
get: function (i, j) {
return +this.elements[i + j * 2].toFixed(4);
},
/**
* Transform a point to a new array.
* @param {Array} point
* @return {Array}
*/
transformPoint: function (point) {
var elements = this.elements;
return [
point[0] * elements[0] + point[1] * elements[2] + elements[4],
point[0] * elements[1] + point[1] * elements[3] + elements[5]
];
},
/**
* @param {Object} bbox Given as `{x: Number, y: Number, width: Number, height: Number}`.
* @param {Number} [radius]
* @param {Object} [target] Optional target object to recieve the result.
* Recommended to use it for better gc.
*
* @return {Object} Object with x, y, width and height.
*/
transformBBox: function (bbox, radius, target) {
var elements = this.elements,
l = bbox.x,
t = bbox.y,
w0 = bbox.width * 0.5,
h0 = bbox.height * 0.5,
xx = elements[0],
xy = elements[1],
yx = elements[2],
yy = elements[3],
cx = l + w0,
cy = t + h0,
w, h, scales;
if (radius) {
w0 -= radius;
h0 -= radius;
scales = [
Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]),
Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3])
];
w = Math.abs(w0 * xx) + Math.abs(h0 * yx) + Math.abs(scales[0] * radius);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy) + Math.abs(scales[1] * radius);
} else {
w = Math.abs(w0 * xx) + Math.abs(h0 * yx);
h = Math.abs(w0 * xy) + Math.abs(h0 * yy);
}
if (!target) {
target = {};
}
target.x = cx * xx + cy * yx + elements[4] - w;
target.y = cx * xy + cy * yy + elements[5] - h;
target.width = w + w;
target.height = h + h;
return target;
},
/**
* Transform a list for points.
*
* __Note:__ will change the original list but not points inside it.
* @param {Array} list
* @return {Array} list
*/
transformList: function (list) {
var elements = this.elements,
xx = elements[0], yx = elements[2], dx = elements[4],
xy = elements[1], yy = elements[3], dy = elements[5],
ln = list.length,
p, i;
for (i = 0; i < ln; i++) {
p = list[i];
list[i] = [
p[0] * xx + p[1] * yx + dx,
p[0] * xy + p[1] * yy + dy
];
}
return list;
},
/**
* Determines whether this matrix is an identity matrix (no transform).
* @return {Boolean}
*/
isIdentity: function () {
var elements = this.elements;
return elements[0] === 1 &&
elements[1] === 0 &&
elements[2] === 0 &&
elements[3] === 1 &&
elements[4] === 0 &&
elements[5] === 0;
},
/**
* Determines if this matrix has the same values as another matrix.
* @param {Ext.draw.Matrix} matrix
* @return {Boolean}
*/
equals: function (matrix) {
var elements = this.elements,
elements2 = matrix.elements;
return elements[0] === elements2[0] &&
elements[1] === elements2[1] &&
elements[2] === elements2[2] &&
elements[3] === elements2[3] &&
elements[4] === elements2[4] &&
elements[5] === elements2[5];
},
/**
* Create an array of elements by horizontal order (xx,yx,dx,yx,yy,dy).
* @return {Array}
*/
toArray: function () {
var elements = this.elements;
return [elements[0], elements[2], elements[4], elements[1], elements[3], elements[5]];
},
/**
* Create an array of elements by vertical order (xx,xy,yx,yy,dx,dy).
* @return {Array|String}
*/
toVerticalArray: function () {
return this.elements.slice();
},
/**
* Get an array of elements.
* The numbers are rounded to keep only 4 decimals.
* @return {Array}
*/
toString: function () {
var me = this;
return [me.get(0, 0), me.get(0, 1), me.get(1, 0), me.get(1, 1), me.get(2, 0), me.get(2, 1)].join(',');
},
/**
* Apply the matrix to a drawing context.
* @param {Object} ctx
* @return {Ext.draw.Matrix} this
*/
toContext: function (ctx) {
ctx.transform.apply(ctx, this.elements);
return this;
},
/**
* Return a string that can be used as transform attribute in SVG.
* @return {String}
*/
toSvg: function () {
var elements = this.elements;
// The reason why we cannot use `.join` is the `1e5` form is not accepted in svg.
return "matrix(" +
elements[0].toFixed(9) + ',' +
elements[1].toFixed(9) + ',' +
elements[2].toFixed(9) + ',' +
elements[3].toFixed(9) + ',' +
elements[4].toFixed(9) + ',' +
elements[5].toFixed(9) +
")";
},
/**
* Get the x scale of the matrix.
* @return {Number}
*/
getScaleX: function () {
var elements = this.elements;
return Math.sqrt(elements[0] * elements[0] + elements[2] * elements[2]);
},
/**
* Get the y scale of the matrix.
* @return {Number}
*/
getScaleY: function () {
var elements = this.elements;
return Math.sqrt(elements[1] * elements[1] + elements[3] * elements[3]);
},
/**
* Get x-to-x component of the matrix
* @return {Number}
*/
getXX: function () {
return this.elements[0];
},
/**
* Get x-to-y component of the matrix.
* @return {Number}
*/
getXY: function () {
return this.elements[1];
},
/**
* Get y-to-x component of the matrix.
* @return {Number}
*/
getYX: function () {
return this.elements[2];
},
/**
* Get y-to-y component of the matrix.
* @return {Number}
*/
getYY: function () {
return this.elements[3];
},
/**
* Get offset x component of the matrix.
* @return {Number}
*/
getDX: function () {
return this.elements[4];
},
/**
* Get offset y component of the matrix.
* @return {Number}
*/
getDY: function () {
return this.elements[5];
},
/**
* Split a transformation matrix into Scale, Rotate, Translate components.
* @return {Object}
*/
split: function () {
var el = this.elements,
xx = el[0],
xy = el[1],
yx = el[2],
yy = el[3],
out = {
translateX: el[4],
translateY: el[5]
};
out.scaleX = Ext.Number.sign(xx) * Math.sqrt(xx * xx + yx * yx);
out.scaleY = Ext.Number.sign(yy) * Math.sqrt(xy * xy + yy * yy);
out.rotation = Math.atan2(xy, yy);
return out;
}
}, function () {
function registerName(properties, name, i) {
properties[name] = {
get: function () {
return this.elements[i];
},
set: function (val) {
this.elements[i] = val;
}
};
}
// Compatibility with SVGMatrix.
if (Object.defineProperties) {
var properties = {};
/**
* @property {Number} a Get x-to-x component of the matrix. Avoid using it for performance consideration.
* Use {@link #getXX} instead.
*/
registerName(properties, 'a', 0);
registerName(properties, 'b', 1);
registerName(properties, 'c', 2);
registerName(properties, 'd', 3);
registerName(properties, 'e', 4);
registerName(properties, 'f', 5);
Object.defineProperties(this.prototype, properties);
}
/**
* Performs matrix multiplication. This matrix is post-multiplied by another matrix.
*
* __Note:__ The given transform will come before the current one.
*
* @method
* @param {Ext.draw.Matrix} matrix
* @return {Ext.draw.Matrix} this
*/
this.prototype.multiply = this.prototype.appendMatrix;
});