zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
378 lines (324 loc) • 10.1 kB
text/typescript
import * as matrix from './matrix';
import * as vector from './vector';
const mIdentity = matrix.identity;
const EPSILON = 5e-5;
function isNotAroundZero(val: number) {
return val > EPSILON || val < -EPSILON;
}
const scaleTmp: vector.VectorArray = [];
const tmpTransform: matrix.MatrixArray = [];
const originTransform = matrix.create();
const abs = Math.abs;
class Transformable {
parent: Transformable
x: number
y: number
scaleX: number
scaleY: number
skewX: number
skewY: number
rotation: number
/**
* Will translated the element to the anchor position before applying other transforms.
*/
anchorX: number
anchorY: number
/**
* Origin of scale, rotation, skew
*/
originX: number
originY: number
/**
* Scale ratio
*/
globalScaleRatio: number
transform: matrix.MatrixArray
invTransform: matrix.MatrixArray
/**
* Get computed local transform
*/
getLocalTransform(m?: matrix.MatrixArray) {
return Transformable.getLocalTransform(this, m);
}
/**
* Set position from array
*/
setPosition(arr: number[]) {
this.x = arr[0];
this.y = arr[1];
}
/**
* Set scale from array
*/
setScale(arr: number[]) {
this.scaleX = arr[0];
this.scaleY = arr[1];
}
/**
* Set skew from array
*/
setSkew(arr: number[]) {
this.skewX = arr[0];
this.skewY = arr[1];
}
/**
* Set origin from array
*/
setOrigin(arr: number[]) {
this.originX = arr[0];
this.originY = arr[1];
}
/**
* If needs to compute transform
*/
needLocalTransform(): boolean {
return isNotAroundZero(this.rotation)
|| isNotAroundZero(this.x)
|| isNotAroundZero(this.y)
|| isNotAroundZero(this.scaleX - 1)
|| isNotAroundZero(this.scaleY - 1)
|| isNotAroundZero(this.skewX)
|| isNotAroundZero(this.skewY);
}
/**
* Update global transform
*/
updateTransform() {
const parentTransform = this.parent && this.parent.transform;
const needLocalTransform = this.needLocalTransform();
let m = this.transform;
if (!(needLocalTransform || parentTransform)) {
if (m) {
mIdentity(m);
// reset invTransform
this.invTransform = null;
}
return;
}
m = m || matrix.create();
if (needLocalTransform) {
this.getLocalTransform(m);
}
else {
mIdentity(m);
}
// 应用父节点变换
if (parentTransform) {
if (needLocalTransform) {
matrix.mul(m, parentTransform, m);
}
else {
matrix.copy(m, parentTransform);
}
}
// 保存这个变换矩阵
this.transform = m;
this._resolveGlobalScaleRatio(m);
}
private _resolveGlobalScaleRatio(m: matrix.MatrixArray) {
const globalScaleRatio = this.globalScaleRatio;
if (globalScaleRatio != null && globalScaleRatio !== 1) {
this.getGlobalScale(scaleTmp);
const relX = scaleTmp[0] < 0 ? -1 : 1;
const relY = scaleTmp[1] < 0 ? -1 : 1;
const sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0;
const sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0;
m[0] *= sx;
m[1] *= sx;
m[2] *= sy;
m[3] *= sy;
}
this.invTransform = this.invTransform || matrix.create();
matrix.invert(this.invTransform, m);
}
/**
* Get computed global transform
* NOTE: this method will force update transform on all ancestors.
* Please be aware of the potential performance cost.
*/
getComputedTransform() {
let transformNode: Transformable = this;
const ancestors: Transformable[] = [];
while (transformNode) {
ancestors.push(transformNode);
transformNode = transformNode.parent;
}
// Update from topdown.
while (transformNode = ancestors.pop()) {
transformNode.updateTransform();
}
return this.transform;
}
setLocalTransform(m: vector.VectorArray) {
if (!m) {
// TODO return or set identity?
return;
}
let sx = m[0] * m[0] + m[1] * m[1];
let sy = m[2] * m[2] + m[3] * m[3];
const rotation = Math.atan2(m[1], m[0]);
const shearX = Math.PI / 2 + rotation - Math.atan2(m[3], m[2]);
sy = Math.sqrt(sy) * Math.cos(shearX);
sx = Math.sqrt(sx);
this.skewX = shearX;
this.skewY = 0;
this.rotation = -rotation;
this.x = +m[4];
this.y = +m[5];
this.scaleX = sx;
this.scaleY = sy;
this.originX = 0;
this.originY = 0;
}
/**
* 分解`transform`矩阵到`position`, `rotation`, `scale`
*/
decomposeTransform() {
if (!this.transform) {
return;
}
const parent = this.parent;
let m = this.transform;
if (parent && parent.transform) {
// Get local transform and decompose them to position, scale, rotation
parent.invTransform = parent.invTransform || matrix.create();
matrix.mul(tmpTransform, parent.invTransform, m);
m = tmpTransform;
}
const ox = this.originX;
const oy = this.originY;
if (ox || oy) {
originTransform[4] = ox;
originTransform[5] = oy;
matrix.mul(tmpTransform, m, originTransform);
tmpTransform[4] -= ox;
tmpTransform[5] -= oy;
m = tmpTransform;
}
this.setLocalTransform(m);
}
/**
* Get global scale
*/
getGlobalScale(out?: vector.VectorArray): vector.VectorArray {
const m = this.transform;
out = out || [];
if (!m) {
out[0] = 1;
out[1] = 1;
return out;
}
out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
if (m[0] < 0) {
out[0] = -out[0];
}
if (m[3] < 0) {
out[1] = -out[1];
}
return out;
}
/**
* 变换坐标位置到 shape 的局部坐标空间
*/
transformCoordToLocal(x: number, y: number): number[] {
const v2 = [x, y];
const invTransform = this.invTransform;
if (invTransform) {
vector.applyTransform(v2, v2, invTransform);
}
return v2;
}
/**
* 变换局部坐标位置到全局坐标空间
*/
transformCoordToGlobal(x: number, y: number): number[] {
const v2 = [x, y];
const transform = this.transform;
if (transform) {
vector.applyTransform(v2, v2, transform);
}
return v2;
}
getLineScale() {
const m = this.transform;
// Get the line scale.
// Determinant of `m` means how much the area is enlarged by the
// transformation. So its square root can be used as a scale factor
// for width.
return m && abs(m[0] - 1) > 1e-10 && abs(m[3] - 1) > 1e-10
? Math.sqrt(abs(m[0] * m[3] - m[2] * m[1]))
: 1;
}
copyTransform(source: Transformable) {
copyTransform(this, source);
}
static getLocalTransform(target: Transformable, m?: matrix.MatrixArray): matrix.MatrixArray {
m = m || [];
const ox = target.originX || 0;
const oy = target.originY || 0;
const sx = target.scaleX;
const sy = target.scaleY;
const ax = target.anchorX;
const ay = target.anchorY;
const rotation = target.rotation || 0;
const x = target.x;
const y = target.y;
const skewX = target.skewX ? Math.tan(target.skewX) : 0;
// TODO: zrender use different hand in coordinate system and y axis is inversed.
const skewY = target.skewY ? Math.tan(-target.skewY) : 0;
// The order of transform (-anchor * -origin * scale * skew * rotate * origin * translate).
// We merge (-origin * scale * skew) into one. Also did identity in these operations.
// origin
if (ox || oy || ax || ay) {
const dx = ox + ax;
const dy = oy + ay;
m[4] = -dx * sx - skewX * dy * sy;
m[5] = -dy * sy - skewY * dx * sx;
}
else {
m[4] = m[5] = 0;
}
// scale
m[0] = sx;
m[3] = sy;
// skew
m[1] = skewY * sx;
m[2] = skewX * sy;
// Apply rotation
rotation && matrix.rotate(m, m, rotation);
// Translate back from origin and apply translation
m[4] += ox + x;
m[5] += oy + y;
return m;
}
private static initDefaultProps = (function () {
const proto = Transformable.prototype;
proto.scaleX =
proto.scaleY =
proto.globalScaleRatio = 1;
proto.x =
proto.y =
proto.originX =
proto.originY =
proto.skewX =
proto.skewY =
proto.rotation =
proto.anchorX =
proto.anchorY = 0;
})()
};
export const TRANSFORMABLE_PROPS = [
'x', 'y', 'originX', 'originY', 'anchorX', 'anchorY', 'rotation', 'scaleX', 'scaleY', 'skewX', 'skewY'
] as const;
export type TransformProp = (typeof TRANSFORMABLE_PROPS)[number]
export function copyTransform(
target: Partial<Pick<Transformable, TransformProp>>,
source: Pick<Transformable, TransformProp>
) {
for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) {
const propName = TRANSFORMABLE_PROPS[i];
target[propName] = source[propName];
}
}
export default Transformable;