UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

378 lines (324 loc) 10.1 kB
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;