UNPKG

@bplok20010/viewbox

Version:

a tool class for matrix transformation of views (rotate, scale, translate, skew, etc.)

385 lines (384 loc) 10.5 kB
import { Matrix2D } from "matrix2d.js"; const DEFAULT_WIDTH = 400; const DEFAULT_HEIGHT = 400; function isset(value) { return !(value == null); } function getDefaultTransform() { return { a: 1, b: 0, c: 0, d: 1, x: 0, y: 0, scaleX: 1, scaleY: 1, rotation: 0, flipX: false, flipY: false, skewX: 0, skewY: 0, }; } /** * ViewBox */ export class ViewBox { options; _matrix = new Matrix2D(); _transform = getDefaultTransform(); transformOrigin = { x: 0, y: 0 }; get matrix() { return this._matrix; } set matrix(mtx) { this._matrix = mtx; } get transform() { this._transform.x = this.x; this._transform.y = this.y; return this._transform; // return this.decompose(); } set transform(value) { this._transform = { ...this._transform, ...value, }; } constructor(options = {}) { this.options = options; this.transformOrigin = options.transformOrigin || this.transformOrigin; if (options.transform) { this.setTransform(options.transform); } } decompose() { return this.matrix.decompose(); } get cx() { return this.transformOrigin.x; } get cy() { return this.transformOrigin.y; } setTransformOrigin(x, y) { this.transformOrigin = { x, y, }; } getTransformOrigin() { return { x: this.cx, y: this.cy, }; } get x() { return this.matrix.tx; } set x(value) { const delta = value - this.x; this.translate(delta, 0); } get y() { return this.matrix.ty; } set y(value) { const delta = value - this.y; this.translate(0, delta); } getPosition() { return { x: this.x, y: this.y, }; } setPosition(x, y) { this.x = x; this.y = y; return this; } getTransform() { const { a, b, c, d, tx, ty } = this.matrix; return { ...this.transform, a, b, c, d, x: tx, y: ty, }; } setTransform(transform) { const { a = 1, b = 0, c = 0, d = 1, x = 0, y = 0 } = transform; const matrix = new Matrix2D(a, b, c, d, x, y); const decompose = matrix.decompose(); this.transform = { ...getDefaultTransform(), scaleX: decompose.scaleX, scaleY: decompose.scaleY, rotation: decompose.rotation, x: decompose.x, y: decompose.y, ...transform, }; this.matrix = matrix; return this; } getMatrixObject() { return this.matrix.clone(); } getMatrix() { const { a, b, c, d, tx, ty } = this.matrix; return [a, b, c, d, tx, ty]; } /** * 视图坐标(相对viewBox)转本地坐标(viewBox内容实际坐标) * @examples * viewBox.translate(100, 100) * viewBox.globalToLocal(0, 0) // {x: -100, y: -100} */ globalToLocal(x, y) { return this.getMatrixObject().invert().transformPoint(x, y); } /** * 本地坐标(viewBox内容实际坐标)转视图坐标(相对viewBox) * @examples * viewBox.translate(100, 100) * viewBox.localToGlobal(-100, -100) // {x: 0, y: 0} */ localToGlobal(x, y) { return this.matrix.transformPoint(x, y); } /** * 对viewBox内容进行平移 * @param x 相对viewBox的(视图坐标)x偏移量 * @param y 同上 y偏移量 * @returns */ translate(x, y) { this.matrix.prepend(1, 0, 0, 1, x, y); return this; } translateX(x) { return this.translate(x, 0); } translateY(y) { return this.translate(0, y); } scale(scaleX, scaleY, cx, cy) { const m0 = new Matrix2D(); m0.scale(scaleX, scaleY, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.scaleX *= scaleX; this.transform.scaleY *= scaleY; return this; } rotate(rotation, cx, cy) { const m0 = new Matrix2D(); m0.rotate(rotation, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.rotation += rotation; return this; } flipX(cx, cy) { cx = cx ?? this.cx; cy = cy ?? this.cy; const m0 = new Matrix2D(); m0.flipX(cx, cy); this.matrix.prependMatrix(m0); this.transform.flipX = !this.transform.flipX; return this; } flipY(cx, cy) { cx = cx ?? this.cx; cy = cy ?? this.cy; const m0 = new Matrix2D(); m0.flipY(cx, cy); this.matrix.prependMatrix(m0); this.transform.flipX = !this.transform.flipX; return this; } skewX(value, cx, cy) { const m0 = new Matrix2D(); m0.skewX(value, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.skewX += value; return this; } setSkewX(value, cx, cy) { const skewX = this.transform.skewX; const m0 = new Matrix2D(); m0.skewX(value - skewX, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.skewX = value; return this; } skewY(value, cx, cy) { const m0 = new Matrix2D(); m0.skewY(value, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.skewY += value; return this; } setSkewY(value, cx, cy) { const skewY = this.transform.skewY; const m0 = new Matrix2D(); m0.skewY(value - skewY, cx ?? this.cx, cy ?? this.cy); this.matrix.prependMatrix(m0); this.transform.skewY = value; return this; } /** * 获取当前缩放值 * @returns */ getZoom() { return this.transform.scaleX; } setZoom(value, cx, cy) { const scaleX = this.transform.scaleX; const scaleY = this.transform.scaleY; this.transform.scaleX = value; this.transform.scaleY = value; cx = cx ?? this.cx; cy = cy ?? this.cy; const m0 = new Matrix2D(); m0.scale(value / scaleX, value / scaleY, cx, cy); this.matrix.prependMatrix(m0); return this; } setRotation(rotation, cx, cy) { const delta = rotation - this.transform.rotation; this.rotate(delta, cx, cy); return this; } getObjectFitScale(objectFit, nodeSize, viewBoxSize) { let scaleX = 1, scaleY = 1; const { width, height } = nodeSize; const vWidth = viewBoxSize.width; const vHeight = viewBoxSize.height; switch (objectFit) { case "cover": scaleX = scaleY = Math.max(vWidth / width, vHeight / height); break; case "none": scaleX = scaleY = 1; break; case "scale-down": scaleX = scaleY = Math.min(1, Math.min(vWidth / width, vHeight / height)); break; case "fill": scaleX = vWidth / width; scaleY = vHeight / height; break; default: //case "contain": scaleX = scaleY = Math.min(vWidth / width, vHeight / height); break; } return { scaleX, scaleY, }; } /** * 缩放以显示指定矩形区域(基于视图的区域)内容,并将区域中心移动到指定的坐标(transformOrigin) * @param rect */ zoomToRect(rect, options = {}) { if (!rect) { return this; } if (options.matrix) { const m = options.matrix; this.setTransform({ a: m[0], b: m[1], c: m[2], d: m[3], x: m[4], y: m[5], }); } const objectFit = options.objectFit || "contain"; const size = options?.viewBoxSize || options?.size || { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }; const padding = options.padding ?? 0; const vWidth = size.width - padding * 2; const vHeight = size.height - padding * 2; let scaleX = 1, scaleY = 1; if (isset(options.scale)) { scaleX = scaleY = options.scale; } else { const r = this.getObjectFitScale(objectFit, rect, { width: vWidth, height: vHeight, }); scaleX = r.scaleX; scaleY = r.scaleY; } const cx = options.transformOrigin ? options.transformOrigin.x : this.cx; const cy = options.transformOrigin ? options.transformOrigin.y : this.cy; const rectCx = rect.x + rect.width / 2; const rectCy = rect.y + rect.height / 2; this.translate(cx - rectCx, cy - rectCy); this.scale(scaleX, scaleY, cx, cy); return this; } /** * 缩放以居中显示指定矩形区域内容 * @returns */ zoomToFit(rect, options = {}) { const size = options?.viewBoxSize || options?.size || { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }; return this.zoomToRect(rect, { ...options, transformOrigin: { x: size.width / 2, y: size.height / 2, }, }); } reset() { const m = new Matrix2D(); this.setTransform({ a: m.a, b: m.b, c: m.c, d: m.d, x: m.tx, y: m.ty, }); return this; } toCSS() { const mtx = this.getMatrix(); return `matrix(${mtx.join(",")})`; } /** * alias toCSS * @returns */ toString() { return this.toCSS(); } clone() { const viewBox = new ViewBox({ transformOrigin: this.getTransformOrigin(), transform: this.getTransform(), }); return viewBox; } }