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