ag-charts-community
Version:
Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue
255 lines • 8.48 kB
JavaScript
import { BBox } from "./bbox";
/**
* As of Jan 8, 2019, Firefox still doesn't implement
* `getTransform(): DOMMatrix;`
* `setTransform(transform?: DOMMatrix2DInit)`
* in the `CanvasRenderingContext2D`.
* Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=928150
* IE11 and Edge 44 also don't have the support.
* Thus this class, to keep track of the current transform and
* combine transformations.
* Standards:
* https://html.spec.whatwg.org/dev/canvas.html
* https://www.w3.org/TR/geometry-1/
*/
export class Matrix {
constructor(elements = [1, 0, 0, 1, 0, 0]) {
this.elements = elements;
}
setElements(elements) {
const e = this.elements;
// `this.elements = elements.slice()` is 4-5 times slower
// (in Chrome 71 and FF 64) than manually copying elements,
// since slicing allocates new memory.
// The performance of passing parameters individually
// vs as an array is about the same in both browsers, so we
// go with a single (array of elements) parameter, because
// `setElements(elements)` and `setElements([a, b, c, d, e, f])`
// calls give us roughly the same performance, versus
// `setElements(...elements)` and `setElements(a, b, c, d, e, f)`,
// where the spread operator causes a 20-30x performance drop
// (30x when compiled to ES5's `.apply(this, elements)`
// 20x when used natively).
e[0] = elements[0];
e[1] = elements[1];
e[2] = elements[2];
e[3] = elements[3];
e[4] = elements[4];
e[5] = elements[5];
return this;
}
setIdentityElements() {
const e = this.elements;
e[0] = 1;
e[1] = 0;
e[2] = 0;
e[3] = 1;
e[4] = 0;
e[5] = 0;
return this;
}
get identity() {
const e = this.elements;
return e[0] === 1 && e[1] === 0 && e[2] === 0 &&
e[3] === 1 && e[4] === 0 && e[5] === 0;
}
set a(value) {
this.elements[0] = value;
}
get a() {
return this.elements[0];
}
set b(value) {
this.elements[1] = value;
}
get b() {
return this.elements[1];
}
set c(value) {
this.elements[2] = value;
}
get c() {
return this.elements[2];
}
set d(value) {
this.elements[3] = value;
}
get d() {
return this.elements[3];
}
set e(value) {
this.elements[4] = value;
}
get e() {
return this.elements[4];
}
set f(value) {
this.elements[5] = value;
}
get f() {
return this.elements[5];
}
/**
* Performs the AxB matrix multiplication and saves the result
* to `C`, if given, or to `A` otherwise.
*/
AxB(A, B, C) {
const [m11, m12, m21, m22, m31, m32] = A;
const [o11, o12, o21, o22, o31, o32] = B;
C = C || A;
C[0] = m11 * o11 + m21 * o12;
C[1] = m12 * o11 + m22 * o12;
C[2] = m11 * o21 + m21 * o22;
C[3] = m12 * o21 + m22 * o22;
C[4] = m11 * o31 + m21 * o32 + m31;
C[5] = m12 * o31 + m22 * o32 + m32;
}
/**
* The `other` matrix gets post-multiplied to the current matrix.
* Returns the current matrix.
* @param other
*/
multiplySelf(other) {
this.AxB(this.elements, other.elements);
return this;
}
/**
* The `other` matrix gets post-multiplied to the current matrix.
* Returns a new matrix.
* @param other
*/
multiply(other) {
const elements = new Array(6);
this.AxB(this.elements, other.elements, elements);
return new Matrix(elements);
}
preMultiplySelf(other) {
this.AxB(other.elements, this.elements, this.elements);
return this;
}
/**
* Returns the inverse of this matrix as a new matrix.
*/
inverse() {
let [a, b, c, d, e, f] = this.elements;
const rD = 1 / (a * d - b * c); // reciprocal of determinant
a *= rD;
b *= rD;
c *= rD;
d *= rD;
return new Matrix([d, -b, -c, a, c * f - d * e, b * e - a * f]);
}
/**
* Save the inverse of this matrix to the given matrix.
*/
inverseTo(other) {
let [a, b, c, d, e, f] = this.elements;
const rD = 1 / (a * d - b * c); // reciprocal of determinant
a *= rD;
b *= rD;
c *= rD;
d *= rD;
other.setElements([d, -b, -c, a, c * f - d * e, b * e - a * f]);
return this;
}
invertSelf() {
const elements = this.elements;
let [a, b, c, d, e, f] = elements;
const rD = 1 / (a * d - b * c); // reciprocal of determinant
a *= rD;
b *= rD;
c *= rD;
d *= rD;
elements[0] = d;
elements[1] = -b;
elements[2] = -c;
elements[3] = a;
elements[4] = c * f - d * e;
elements[5] = b * e - a * f;
return this;
}
clone() {
return new Matrix(this.elements.slice());
}
transformPoint(x, y) {
const e = this.elements;
return {
x: x * e[0] + y * e[2] + e[4],
y: x * e[1] + y * e[3] + e[5]
};
}
transformBBox(bbox, radius = 0, target) {
const elements = this.elements;
const xx = elements[0];
const xy = elements[1];
const yx = elements[2];
const yy = elements[3];
let h_w = bbox.width * 0.5;
let h_h = bbox.height * 0.5;
const cx = bbox.x + h_w;
const cy = bbox.y + h_h;
let w, h;
if (radius) {
h_w -= radius;
h_h -= radius;
const sx = Math.sqrt(xx * xx + yx * yx);
const sy = Math.sqrt(xy * xy + yy * yy);
w = Math.abs(h_w * xx) + Math.abs(h_h * yx) + Math.abs(sx * radius);
h = Math.abs(h_w * xy) + Math.abs(h_h * yy) + Math.abs(sy * radius);
}
else {
w = Math.abs(h_w * xx) + Math.abs(h_h * yx);
h = Math.abs(h_w * xy) + Math.abs(h_h * yy);
}
if (!target) {
target = new BBox(0, 0, 0, 0);
}
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;
}
toContext(ctx) {
// It's fair to say that matrix multiplications are not cheap.
// However, updating path definitions on every frame isn't either, so
// it may be cheaper to just translate paths. It's also fair to
// say, that most paths will have to be re-rendered anyway, say
// rectangle paths in a bar chart, where an animation would happen when
// the data set changes and existing bars are morphed into new ones.
// Or a pie chart, where old sectors are also morphed into new ones.
// Same for the line chart. The only plausible case where translating
// existing paths would be enough, is the scatter chart, where marker
// icons, typically circles, stay the same size. But if circle radii
// are bound to some data points, even circle paths would have to be
// updated. And thus it makes sense to optimize for fewer matrix
// transforms, where transform matrices of paths are mostly identity
// matrices and `x`/`y`, `centerX`/`centerY` and similar properties
// are used to define a path at specific coordinates. And only groups
// are used to collectively apply a transform to a set of nodes.
// If the matrix is mostly identity (95% of the time),
// the `if (this.isIdentity)` check can make this call 3-4 times
// faster on average: https://jsperf.com/matrix-check-first-vs-always-set
if (this.identity) {
return;
}
const e = this.elements;
ctx.transform(e[0], e[1], e[2], e[3], e[4], e[5]);
}
static flyweight(elements) {
if (elements) {
if (elements instanceof Matrix) {
Matrix.matrix.setElements(elements.elements);
}
else {
Matrix.matrix.setElements(elements);
}
}
else {
Matrix.matrix.setIdentityElements();
}
return Matrix.matrix;
}
}
Matrix.matrix = new Matrix();
//# sourceMappingURL=matrix.js.map