@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
402 lines (352 loc) • 10.4 kB
text/typescript
import { Angle, Point, Line, Rectangle, Polyline } from '../../geometry'
import { createSvgElement } from './elem'
const svgDocument = createSvgElement('svg') as SVGSVGElement
const transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi
const transformSeparatorRegex = /[ ,]+/
const transformationListRegex = /^(\w+)\((.*)\)/
export interface MatrixLike {
a: number
b: number
c: number
d: number
e: number
f: number
}
export interface Translation {
tx: number
ty: number
}
export interface Rotation {
angle: number
cx?: number
cy?: number
}
export interface Scale {
sx: number
sy: number
}
/**
* Returns a SVG point object initialized with the `x` and `y` coordinates.
* @see https://developer.mozilla.org/en/docs/Web/API/SVGPoint
*/
export function createSVGPoint(x: number, y: number) {
const p = svgDocument.createSVGPoint()
p.x = x
p.y = y
return p
}
/**
* Returns the SVG transformation matrix initialized with the given matrix.
*
* The given matrix is an object of the form:
* {
* a: number
* b: number
* c: number
* d: number
* e: number
* f: number
* }
*
* @see https://developer.mozilla.org/en/docs/Web/API/SVGMatrix
*/
export function createSVGMatrix(matrix?: DOMMatrix | MatrixLike | null) {
const mat = svgDocument.createSVGMatrix()
if (matrix != null) {
const source = matrix as any
const target = mat as any
// eslint-disable-next-line
for (const key in source) {
target[key] = source[key]
}
}
return mat
}
/**
* Returns a SVG transform object.
* @see https://developer.mozilla.org/en/docs/Web/API/SVGTransform
*/
export function createSVGTransform(matrix?: DOMMatrix | MatrixLike) {
if (matrix != null) {
if (!(matrix instanceof DOMMatrix)) {
matrix = createSVGMatrix(matrix) // eslint-disable-line
}
return svgDocument.createSVGTransformFromMatrix(matrix as DOMMatrix)
}
return svgDocument.createSVGTransform()
}
/**
* Returns the SVG transformation matrix built from the `transformString`.
*
* E.g. 'translate(10,10) scale(2,2)' will result in matrix:
* `{ a: 2, b: 0, c: 0, d: 2, e: 10, f: 10}`
*/
export function transformStringToMatrix(transform?: string | null) {
let mat = createSVGMatrix()
const matches = transform != null && transform.match(transformRegex)
if (!matches) {
return mat
}
for (let i = 0, n = matches.length; i < n; i += 1) {
const transformationString = matches[i]
const transformationMatch = transformationString.match(
transformationListRegex,
)
if (transformationMatch) {
let sx
let sy
let tx
let ty
let angle
let ctm = createSVGMatrix()
const args = transformationMatch[2].split(transformSeparatorRegex)
switch (transformationMatch[1].toLowerCase()) {
case 'scale':
sx = parseFloat(args[0])
sy = args[1] === undefined ? sx : parseFloat(args[1])
ctm = ctm.scaleNonUniform(sx, sy)
break
case 'translate':
tx = parseFloat(args[0])
ty = parseFloat(args[1])
ctm = ctm.translate(tx, ty)
break
case 'rotate':
angle = parseFloat(args[0])
tx = parseFloat(args[1]) || 0
ty = parseFloat(args[2]) || 0
if (tx !== 0 || ty !== 0) {
ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty)
} else {
ctm = ctm.rotate(angle)
}
break
case 'skewx':
angle = parseFloat(args[0])
ctm = ctm.skewX(angle)
break
case 'skewy':
angle = parseFloat(args[0])
ctm = ctm.skewY(angle)
break
case 'matrix':
ctm.a = parseFloat(args[0])
ctm.b = parseFloat(args[1])
ctm.c = parseFloat(args[2])
ctm.d = parseFloat(args[3])
ctm.e = parseFloat(args[4])
ctm.f = parseFloat(args[5])
break
default:
continue
}
mat = mat.multiply(ctm)
}
}
return mat
}
export function matrixToTransformString(
matrix?: DOMMatrix | Partial<MatrixLike>,
) {
const m = matrix || ({} as DOMMatrix)
const a = m.a != null ? m.a : 1
const b = m.b != null ? m.b : 0
const c = m.c != null ? m.c : 0
const d = m.d != null ? m.d : 1
const e = m.e != null ? m.e : 0
const f = m.f != null ? m.f : 0
return `matrix(${a},${b},${c},${d},${e},${f})`
}
export function parseTransformString(transform: string) {
let translation
let rotation
let scale
if (transform) {
const separator = transformSeparatorRegex
// Allow reading transform string with a single matrix
if (transform.trim().indexOf('matrix') >= 0) {
const matrix = transformStringToMatrix(transform)
const decomposedMatrix = decomposeMatrix(matrix)
translation = [decomposedMatrix.translateX, decomposedMatrix.translateY]
rotation = [decomposedMatrix.rotation]
scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]
const transformations = []
if (translation[0] !== 0 || translation[1] !== 0) {
transformations.push(`translate(${translation.join(',')})`)
}
if (scale[0] !== 1 || scale[1] !== 1) {
transformations.push(`scale(${scale.join(',')})`)
}
if (rotation[0] !== 0) {
transformations.push(`rotate(${rotation[0]})`)
}
transform = transformations.join(' ') // eslint-disable-line
} else {
const translateMatch = transform.match(/translate\((.*?)\)/)
if (translateMatch) {
translation = translateMatch[1].split(separator)
}
const rotateMatch = transform.match(/rotate\((.*?)\)/)
if (rotateMatch) {
rotation = rotateMatch[1].split(separator)
}
const scaleMatch = transform.match(/scale\((.*?)\)/)
if (scaleMatch) {
scale = scaleMatch[1].split(separator)
}
}
}
const sx = scale && scale[0] ? parseFloat(scale[0] as string) : 1
return {
raw: transform || '',
translation: {
tx:
translation && translation[0]
? parseInt(translation[0] as string, 10)
: 0,
ty:
translation && translation[1]
? parseInt(translation[1] as string, 10)
: 0,
} as Translation,
rotation: {
angle: rotation && rotation[0] ? parseInt(rotation[0] as string, 10) : 0,
cx:
rotation && rotation[1]
? parseInt(rotation[1] as string, 10)
: undefined,
cy:
rotation && rotation[2]
? parseInt(rotation[2] as string, 10)
: undefined,
} as Rotation,
scale: {
sx,
sy: scale && scale[1] ? parseFloat(scale[1] as string) : sx,
} as Scale,
}
}
function deltaTransformPoint(
matrix: DOMMatrix | MatrixLike,
point: Point | Point.PointLike,
) {
const dx = point.x * matrix.a + point.y * matrix.c + 0
const dy = point.x * matrix.b + point.y * matrix.d + 0
return { x: dx, y: dy }
}
/**
* Decomposes the SVG transformation matrix into separate transformations.
*
* Returns an object of the form:
* {
* translateX: number
* translateY: number
* scaleX: number
* scaleY: number
* skewX: number
* skewY: number
* rotation: number
* }
*
* @see https://developer.mozilla.org/en/docs/Web/API/SVGMatrix
*/
export function decomposeMatrix(matrix: DOMMatrix | MatrixLike) {
// @see https://gist.github.com/2052247
const px = deltaTransformPoint(matrix, { x: 0, y: 1 })
const py = deltaTransformPoint(matrix, { x: 1, y: 0 })
const skewX = (180 / Math.PI) * Math.atan2(px.y, px.x) - 90
const skewY = (180 / Math.PI) * Math.atan2(py.y, py.x)
return {
skewX,
skewY,
translateX: matrix.e,
translateY: matrix.f,
scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b),
scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d),
rotation: skewX,
}
}
export function matrixToScale(matrix: DOMMatrix | MatrixLike): Scale {
let a
let b
let c
let d
if (matrix) {
a = matrix.a == null ? 1 : matrix.a
d = matrix.d == null ? 1 : matrix.d
b = matrix.b
c = matrix.c
} else {
a = d = 1
}
return {
sx: b ? Math.sqrt(a * a + b * b) : a,
sy: c ? Math.sqrt(c * c + d * d) : d,
}
}
export function matrixToRotation(matrix: DOMMatrix | MatrixLike): Rotation {
let p = { x: 0, y: 1 }
if (matrix) {
p = deltaTransformPoint(matrix, p)
}
return {
angle: Angle.normalize(Angle.toDeg(Math.atan2(p.y, p.x)) - 90),
}
}
export function matrixToTranslation(
matrix: DOMMatrix | MatrixLike,
): Translation {
return {
tx: (matrix && matrix.e) || 0,
ty: (matrix && matrix.f) || 0,
}
}
/**
* Transforms point by an SVG transformation represented by `matrix`.
*/
export function transformPoint(point: Point.PointLike, matrix: DOMMatrix) {
const ret = createSVGPoint(point.x, point.y).matrixTransform(matrix)
return new Point(ret.x, ret.y)
}
/**
* Transforms line by an SVG transformation represented by `matrix`.
*/
export function transformLine(line: Line, matrix: DOMMatrix) {
return new Line(
transformPoint(line.start, matrix),
transformPoint(line.end, matrix),
)
}
/**
* Transforms polyline by an SVG transformation represented by `matrix`.
*/
export function transformPolyline(polyline: Polyline, matrix: DOMMatrix) {
let points = polyline instanceof Polyline ? polyline.points : polyline
if (!Array.isArray(points)) {
points = []
}
return new Polyline(points.map((p) => transformPoint(p, matrix)))
}
export function transformRectangle(
rect: Rectangle.RectangleLike,
matrix: DOMMatrix,
) {
const p = svgDocument.createSVGPoint()
p.x = rect.x
p.y = rect.y
const corner1 = p.matrixTransform(matrix)
p.x = rect.x + rect.width
p.y = rect.y
const corner2 = p.matrixTransform(matrix)
p.x = rect.x + rect.width
p.y = rect.y + rect.height
const corner3 = p.matrixTransform(matrix)
p.x = rect.x
p.y = rect.y + rect.height
const corner4 = p.matrixTransform(matrix)
const minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x)
const maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x)
const minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y)
const maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y)
return new Rectangle(minX, minY, maxX - minX, maxY - minY)
}