* fix release script * fix svgcanvas edge cases * Update path-actions.js * add modern js * update deps * Update CHANGES.md
323 lines
8.6 KiB
JavaScript
323 lines
8.6 KiB
JavaScript
/**
|
|
* Mathematical utilities.
|
|
* @module math
|
|
* @license MIT
|
|
*
|
|
* ©2010 Alexis Deveria, ©2010 Jeff Schiller
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} AngleCoord45
|
|
* @property {number} x - The angle-snapped x value
|
|
* @property {number} y - The angle-snapped y value
|
|
* @property {number} a - The angle (in radians) at which to snap
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} XYObject
|
|
* @property {number} x
|
|
* @property {number} y
|
|
*/
|
|
|
|
import { NS } from './namespaces.js'
|
|
import { warn } from '../common/logger.js'
|
|
|
|
// Constants
|
|
const NEAR_ZERO = 1e-10
|
|
|
|
// Create a throwaway SVG element for matrix operations
|
|
const svg = document.createElementNS(NS.SVG, 'svg')
|
|
|
|
const createTransformFromMatrix = (m) => {
|
|
const createFallback = (matrix) => {
|
|
const fallback = svg.createSVGMatrix()
|
|
Object.assign(fallback, {
|
|
a: matrix.a,
|
|
b: matrix.b,
|
|
c: matrix.c,
|
|
d: matrix.d,
|
|
e: matrix.e,
|
|
f: matrix.f
|
|
})
|
|
return fallback
|
|
}
|
|
|
|
try {
|
|
return svg.createSVGTransformFromMatrix(m)
|
|
} catch (e) {
|
|
const t = svg.createSVGTransform()
|
|
try {
|
|
t.setMatrix(m)
|
|
return t
|
|
} catch (err) {
|
|
try {
|
|
return svg.createSVGTransformFromMatrix(createFallback(m))
|
|
} catch (e2) {
|
|
t.setMatrix(createFallback(m))
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transforms a point by a given matrix without DOM calls.
|
|
* @function transformPoint
|
|
* @param {number} x - The x coordinate
|
|
* @param {number} y - The y coordinate
|
|
* @param {SVGMatrix} m - The transformation matrix
|
|
* @returns {XYObject} The transformed point
|
|
*/
|
|
export const transformPoint = (x, y, m) => ({
|
|
x: m.a * x + m.c * y + m.e,
|
|
y: m.b * x + m.d * y + m.f
|
|
})
|
|
|
|
/**
|
|
* Gets the transform list (baseVal) from an element if it exists.
|
|
* @function getTransformList
|
|
* @param {Element} elem - An SVG element or element with a transform list
|
|
* @returns {SVGTransformList|undefined} The transform list, if any
|
|
*/
|
|
export const getTransformList = elem => {
|
|
if (elem.transform?.baseVal) {
|
|
return elem.transform.baseVal
|
|
}
|
|
if (elem.gradientTransform?.baseVal) {
|
|
return elem.gradientTransform.baseVal
|
|
}
|
|
if (elem.patternTransform?.baseVal) {
|
|
return elem.patternTransform.baseVal
|
|
}
|
|
warn('No transform list found. Check browser compatibility.', elem, 'math')
|
|
}
|
|
|
|
/**
|
|
* Checks if a matrix is the identity matrix.
|
|
* @function isIdentity
|
|
* @param {SVGMatrix} m - The matrix to check
|
|
* @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0)
|
|
*/
|
|
export const isIdentity = m =>
|
|
Math.abs(m.a - 1) < NEAR_ZERO &&
|
|
Math.abs(m.b) < NEAR_ZERO &&
|
|
Math.abs(m.c) < NEAR_ZERO &&
|
|
Math.abs(m.d - 1) < NEAR_ZERO &&
|
|
Math.abs(m.e) < NEAR_ZERO &&
|
|
Math.abs(m.f) < NEAR_ZERO
|
|
|
|
/**
|
|
* Multiplies multiple matrices together (m1 * m2 * ...).
|
|
* Near-zero values are rounded to zero.
|
|
* @function matrixMultiply
|
|
* @param {...SVGMatrix} args - The matrices to multiply
|
|
* @returns {SVGMatrix} The resulting matrix
|
|
*/
|
|
export const matrixMultiply = (...args) => {
|
|
if (args.length === 0) {
|
|
return svg.createSVGMatrix()
|
|
}
|
|
|
|
const normalizeNearZero = (matrix) => {
|
|
const props = ['a', 'b', 'c', 'd', 'e', 'f']
|
|
for (const prop of props) {
|
|
if (Math.abs(matrix[prop]) < NEAR_ZERO) {
|
|
matrix[prop] = 0
|
|
}
|
|
}
|
|
return matrix
|
|
}
|
|
|
|
if (typeof DOMMatrix === 'function' && typeof DOMMatrix.fromMatrix === 'function') {
|
|
const result = args.reduce(
|
|
(acc, curr) => acc.multiply(DOMMatrix.fromMatrix(curr)),
|
|
new DOMMatrix()
|
|
)
|
|
|
|
const out = svg.createSVGMatrix()
|
|
Object.assign(out, {
|
|
a: result.a,
|
|
b: result.b,
|
|
c: result.c,
|
|
d: result.d,
|
|
e: result.e,
|
|
f: result.f
|
|
})
|
|
|
|
return normalizeNearZero(out)
|
|
}
|
|
|
|
let m = svg.createSVGMatrix()
|
|
for (const curr of args) {
|
|
const next = svg.createSVGMatrix()
|
|
Object.assign(next, {
|
|
a: m.a * curr.a + m.c * curr.b,
|
|
b: m.b * curr.a + m.d * curr.b,
|
|
c: m.a * curr.c + m.c * curr.d,
|
|
d: m.b * curr.c + m.d * curr.d,
|
|
e: m.a * curr.e + m.c * curr.f + m.e,
|
|
f: m.b * curr.e + m.d * curr.f + m.f
|
|
})
|
|
m = next
|
|
}
|
|
|
|
return normalizeNearZero(m)
|
|
}
|
|
|
|
/**
|
|
* Checks if a transform list includes a non-identity matrix transform.
|
|
* @function hasMatrixTransform
|
|
* @param {SVGTransformList} [tlist] - The transform list to check
|
|
* @returns {boolean} True if a matrix transform is found
|
|
*/
|
|
export const hasMatrixTransform = tlist => {
|
|
if (!tlist) return false
|
|
for (let i = 0; i < tlist.numberOfItems; i++) {
|
|
const xform = tlist.getItem(i)
|
|
if (
|
|
xform.type === SVGTransform.SVG_TRANSFORM_MATRIX &&
|
|
!isIdentity(xform.matrix)
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} TransformedBox
|
|
* @property {XYObject} tl - Top-left coordinate
|
|
* @property {XYObject} tr - Top-right coordinate
|
|
* @property {XYObject} bl - Bottom-left coordinate
|
|
* @property {XYObject} br - Bottom-right coordinate
|
|
* @property {Object} aabox
|
|
* @property {number} aabox.x - Axis-aligned x
|
|
* @property {number} aabox.y - Axis-aligned y
|
|
* @property {number} aabox.width - Axis-aligned width
|
|
* @property {number} aabox.height - Axis-aligned height
|
|
*/
|
|
|
|
/**
|
|
* Transforms a rectangular box using a given matrix.
|
|
* @function transformBox
|
|
* @param {number} l - Left coordinate
|
|
* @param {number} t - Top coordinate
|
|
* @param {number} w - Width
|
|
* @param {number} h - Height
|
|
* @param {SVGMatrix} m - Transformation matrix
|
|
* @returns {TransformedBox} The transformed box information
|
|
*/
|
|
export const transformBox = (l, t, w, h, m) => {
|
|
const tl = transformPoint(l, t, m)
|
|
const tr = transformPoint(l + w, t, m)
|
|
const bl = transformPoint(l, t + h, m)
|
|
const br = transformPoint(l + w, t + h, m)
|
|
|
|
const minx = Math.min(tl.x, tr.x, bl.x, br.x)
|
|
const maxx = Math.max(tl.x, tr.x, bl.x, br.x)
|
|
const miny = Math.min(tl.y, tr.y, bl.y, br.y)
|
|
const maxy = Math.max(tl.y, tr.y, bl.y, br.y)
|
|
|
|
return {
|
|
tl,
|
|
tr,
|
|
bl,
|
|
br,
|
|
aabox: {
|
|
x: minx,
|
|
y: miny,
|
|
width: maxx - minx,
|
|
height: maxy - miny
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consolidates a transform list into a single matrix transform without modifying the original list.
|
|
* @function transformListToTransform
|
|
* @param {SVGTransformList} tlist - The transform list
|
|
* @param {number} [min=0] - Optional start index
|
|
* @param {number} [max] - Optional end index, defaults to tlist length-1
|
|
* @returns {SVGTransform} A single transform from the combined matrices
|
|
*/
|
|
export const transformListToTransform = (tlist, min = 0, max = null) => {
|
|
if (!tlist) {
|
|
return createTransformFromMatrix(svg.createSVGMatrix())
|
|
}
|
|
|
|
const start = Number.parseInt(min, 10)
|
|
const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10)
|
|
const [low, high] = [Math.min(start, end), Math.max(start, end)]
|
|
|
|
const matrices = []
|
|
for (let i = low; i <= high; i++) {
|
|
const matrix = (i >= 0 && i < tlist.numberOfItems)
|
|
? tlist.getItem(i).matrix
|
|
: svg.createSVGMatrix()
|
|
matrices.push(matrix)
|
|
}
|
|
|
|
const combinedMatrix = matrixMultiply(...matrices)
|
|
|
|
const out = svg.createSVGMatrix()
|
|
Object.assign(out, {
|
|
a: combinedMatrix.a,
|
|
b: combinedMatrix.b,
|
|
c: combinedMatrix.c,
|
|
d: combinedMatrix.d,
|
|
e: combinedMatrix.e,
|
|
f: combinedMatrix.f
|
|
})
|
|
|
|
return createTransformFromMatrix(out)
|
|
}
|
|
|
|
/**
|
|
* Gets the matrix of a given element's transform list.
|
|
* @function getMatrix
|
|
* @param {Element} elem - The element to check
|
|
* @returns {SVGMatrix} The transformation matrix
|
|
*/
|
|
export const getMatrix = elem => {
|
|
const tlist = getTransformList(elem)
|
|
return transformListToTransform(tlist).matrix
|
|
}
|
|
|
|
/**
|
|
* Returns a coordinate snapped to the nearest 45-degree angle.
|
|
* @function snapToAngle
|
|
* @param {number} x1 - First point's x
|
|
* @param {number} y1 - First point's y
|
|
* @param {number} x2 - Second point's x
|
|
* @param {number} y2 - Second point's y
|
|
* @returns {AngleCoord45} The angle-snapped coordinates and angle
|
|
*/
|
|
export const snapToAngle = (x1, y1, x2, y2) => {
|
|
const snap = Math.PI / 4 // 45 degrees
|
|
const dx = x2 - x1
|
|
const dy = y2 - y1
|
|
const angle = Math.atan2(dy, dx)
|
|
const dist = Math.hypot(dx, dy)
|
|
const snapAngle = Math.round(angle / snap) * snap
|
|
|
|
return {
|
|
x: x1 + dist * Math.cos(snapAngle),
|
|
y: y1 + dist * Math.sin(snapAngle),
|
|
a: snapAngle
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if two rectangles intersect.
|
|
* Both r1 and r2 are expected to have {x, y, width, height}.
|
|
* @function rectsIntersect
|
|
* @param {{x:number,y:number,width:number,height:number}} r1 - First rectangle
|
|
* @param {{x:number,y:number,width:number,height:number}} r2 - Second rectangle
|
|
* @returns {boolean} True if the rectangles intersect
|
|
*/
|
|
export const rectsIntersect = (r1, r2) =>
|
|
r2.x < r1.x + r1.width &&
|
|
r2.x + r2.width > r1.x &&
|
|
r2.y < r1.y + r1.height &&
|
|
r2.y + r2.height > r1.y
|