@bplok20010/viewbox
Version:
a tool class for matrix transformation of views (rotate, scale, translate, skew, etc.)
385 lines (384 loc) • 10.5 kB
JavaScript
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;
}
}