@svgedit/svgcanvas 7.2.3 → 7.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core/math.js CHANGED
@@ -3,117 +3,111 @@
3
3
  * @module math
4
4
  * @license MIT
5
5
  *
6
- * @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
6
+ * ©2010 Alexis Deveria, ©2010 Jeff Schiller
7
7
  */
8
8
 
9
9
  /**
10
- * @typedef {PlainObject} module:math.AngleCoord45
11
- * @property {Float} x - The angle-snapped x value
12
- * @property {Float} y - The angle-snapped y value
13
- * @property {Integer} a - The angle at which to snap
10
+ * @typedef {Object} AngleCoord45
11
+ * @property {number} x - The angle-snapped x value
12
+ * @property {number} y - The angle-snapped y value
13
+ * @property {number} a - The angle (in radians) at which to snap
14
14
  */
15
15
 
16
16
  /**
17
- * @typedef {PlainObject} module:math.XYObject
18
- * @property {Float} x
19
- * @property {Float} y
17
+ * @typedef {Object} XYObject
18
+ * @property {number} x
19
+ * @property {number} y
20
20
  */
21
21
 
22
22
  import { NS } from './namespaces.js'
23
23
 
24
24
  // Constants
25
- const NEAR_ZERO = 1e-14
25
+ const NEAR_ZERO = 1e-10
26
26
 
27
- // Throw away SVGSVGElement used for creating matrices/transforms.
27
+ // Create a throwaway SVG element for matrix operations
28
28
  const svg = document.createElementNS(NS.SVG, 'svg')
29
29
 
30
30
  /**
31
- * A (hopefully) quicker function to transform a point by a matrix
32
- * (this function avoids any DOM calls and just does the math).
33
- * @function module:math.transformPoint
34
- * @param {Float} x - Float representing the x coordinate
35
- * @param {Float} y - Float representing the y coordinate
36
- * @param {SVGMatrix} m - Matrix object to transform the point with
37
- * @returns {module:math.XYObject} An x, y object representing the transformed point
31
+ * Transforms a point by a given matrix without DOM calls.
32
+ * @function transformPoint
33
+ * @param {number} x - The x coordinate
34
+ * @param {number} y - The y coordinate
35
+ * @param {SVGMatrix} m - The transformation matrix
36
+ * @returns {XYObject} The transformed point
38
37
  */
39
- export const transformPoint = function (x, y, m) {
40
- return { x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f }
41
- }
38
+ export const transformPoint = (x, y, m) => ({
39
+ x: m.a * x + m.c * y + m.e,
40
+ y: m.b * x + m.d * y + m.f
41
+ })
42
42
 
43
- export const getTransformList = (elem) => {
44
- if (elem.transform) {
45
- return elem.transform?.baseVal
43
+ /**
44
+ * Gets the transform list (baseVal) from an element if it exists.
45
+ * @function getTransformList
46
+ * @param {Element} elem - An SVG element or element with a transform list
47
+ * @returns {SVGTransformList|undefined} The transform list, if any
48
+ */
49
+ export const getTransformList = elem => {
50
+ if (elem.transform?.baseVal) {
51
+ return elem.transform.baseVal
46
52
  }
47
- if (elem.gradientTransform) {
48
- return elem.gradientTransform?.baseVal
53
+ if (elem.gradientTransform?.baseVal) {
54
+ return elem.gradientTransform.baseVal
49
55
  }
50
- if (elem.patternTransform) {
51
- return elem.patternTransform?.baseVal
56
+ if (elem.patternTransform?.baseVal) {
57
+ return elem.patternTransform.baseVal
52
58
  }
53
- console.warn('no transform list found - check browser version', elem)
59
+ console.warn('No transform list found. Check browser compatibility.', elem)
54
60
  }
55
61
 
56
62
  /**
57
- * Helper function to check if the matrix performs no actual transform
58
- * (i.e. exists for identity purposes).
59
- * @function module:math.isIdentity
60
- * @param {SVGMatrix} m - The matrix object to check
61
- * @returns {boolean} Indicates whether or not the matrix is 1,0,0,1,0,0
63
+ * Checks if a matrix is the identity matrix.
64
+ * @function isIdentity
65
+ * @param {SVGMatrix} m - The matrix to check
66
+ * @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0)
62
67
  */
63
- export const isIdentity = function (m) {
64
- return (
65
- m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
66
- )
67
- }
68
+ export const isIdentity = m =>
69
+ m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
68
70
 
69
71
  /**
70
- * This function tries to return a `SVGMatrix` that is the multiplication `m1 * m2`.
71
- * We also round to zero when it's near zero.
72
- * @function module:math.matrixMultiply
73
- * @param {...SVGMatrix} args - Matrix objects to multiply
74
- * @returns {SVGMatrix} The matrix object resulting from the calculation
72
+ * Multiplies multiple matrices together (m1 * m2 * ...).
73
+ * Near-zero values are rounded to zero.
74
+ * @function matrixMultiply
75
+ * @param {...SVGMatrix} args - The matrices to multiply
76
+ * @returns {SVGMatrix} The resulting matrix
75
77
  */
76
- export const matrixMultiply = function (...args) {
77
- const m = args.reduceRight((prev, m1) => {
78
- return m1.multiply(prev)
79
- })
80
-
81
- if (Math.abs(m.a) < NEAR_ZERO) {
82
- m.a = 0
83
- }
84
- if (Math.abs(m.b) < NEAR_ZERO) {
85
- m.b = 0
86
- }
87
- if (Math.abs(m.c) < NEAR_ZERO) {
88
- m.c = 0
89
- }
90
- if (Math.abs(m.d) < NEAR_ZERO) {
91
- m.d = 0
92
- }
93
- if (Math.abs(m.e) < NEAR_ZERO) {
94
- m.e = 0
95
- }
96
- if (Math.abs(m.f) < NEAR_ZERO) {
97
- m.f = 0
78
+ export const matrixMultiply = (...args) => {
79
+ // If no matrices are given, return an identity matrix
80
+ if (args.length === 0) {
81
+ return svg.createSVGMatrix()
98
82
  }
99
83
 
84
+ const m = args.reduceRight((prev, curr) => curr.multiply(prev))
85
+
86
+ // Round near-zero values to zero
87
+ if (Math.abs(m.a) < NEAR_ZERO) m.a = 0
88
+ if (Math.abs(m.b) < NEAR_ZERO) m.b = 0
89
+ if (Math.abs(m.c) < NEAR_ZERO) m.c = 0
90
+ if (Math.abs(m.d) < NEAR_ZERO) m.d = 0
91
+ if (Math.abs(m.e) < NEAR_ZERO) m.e = 0
92
+ if (Math.abs(m.f) < NEAR_ZERO) m.f = 0
93
+
100
94
  return m
101
95
  }
102
96
 
103
97
  /**
104
- * See if the given transformlist includes a non-indentity matrix transform.
105
- * @function module:math.hasMatrixTransform
106
- * @param {SVGTransformList} [tlist] - The transformlist to check
107
- * @returns {boolean} Whether or not a matrix transform was found
98
+ * Checks if a transform list includes a non-identity matrix transform.
99
+ * @function hasMatrixTransform
100
+ * @param {SVGTransformList} [tlist] - The transform list to check
101
+ * @returns {boolean} True if a matrix transform is found
108
102
  */
109
- export const hasMatrixTransform = function (tlist) {
110
- if (!tlist) {
111
- return false
112
- }
113
- let num = tlist.numberOfItems
114
- while (num--) {
115
- const xform = tlist.getItem(num)
116
- if (xform.type === 1 && !isIdentity(xform.matrix)) {
103
+ export const hasMatrixTransform = tlist => {
104
+ if (!tlist) return false
105
+ for (let i = 0; i < tlist.numberOfItems; i++) {
106
+ const xform = tlist.getItem(i)
107
+ if (
108
+ xform.type === SVGTransform.SVG_TRANSFORM_MATRIX &&
109
+ !isIdentity(xform.matrix)
110
+ ) {
117
111
  return true
118
112
  }
119
113
  }
@@ -121,29 +115,29 @@ export const hasMatrixTransform = function (tlist) {
121
115
  }
122
116
 
123
117
  /**
124
- * @typedef {PlainObject} module:math.TransformedBox An object with the following values
125
- * @property {module:math.XYObject} tl - The top left coordinate
126
- * @property {module:math.XYObject} tr - The top right coordinate
127
- * @property {module:math.XYObject} bl - The bottom left coordinate
128
- * @property {module:math.XYObject} br - The bottom right coordinate
129
- * @property {PlainObject} aabox - Object with the following values:
130
- * @property {Float} aabox.x - Float with the axis-aligned x coordinate
131
- * @property {Float} aabox.y - Float with the axis-aligned y coordinate
132
- * @property {Float} aabox.width - Float with the axis-aligned width coordinate
133
- * @property {Float} aabox.height - Float with the axis-aligned height coordinate
118
+ * @typedef {Object} TransformedBox
119
+ * @property {XYObject} tl - Top-left coordinate
120
+ * @property {XYObject} tr - Top-right coordinate
121
+ * @property {XYObject} bl - Bottom-left coordinate
122
+ * @property {XYObject} br - Bottom-right coordinate
123
+ * @property {Object} aabox
124
+ * @property {number} aabox.x - Axis-aligned x
125
+ * @property {number} aabox.y - Axis-aligned y
126
+ * @property {number} aabox.width - Axis-aligned width
127
+ * @property {number} aabox.height - Axis-aligned height
134
128
  */
135
129
 
136
130
  /**
137
- * Transforms a rectangle based on the given matrix.
138
- * @function module:math.transformBox
139
- * @param {Float} l - Float with the box's left coordinate
140
- * @param {Float} t - Float with the box's top coordinate
141
- * @param {Float} w - Float with the box width
142
- * @param {Float} h - Float with the box height
143
- * @param {SVGMatrix} m - Matrix object to transform the box by
144
- * @returns {module:math.TransformedBox}
131
+ * Transforms a rectangular box using a given matrix.
132
+ * @function transformBox
133
+ * @param {number} l - Left coordinate
134
+ * @param {number} t - Top coordinate
135
+ * @param {number} w - Width
136
+ * @param {number} h - Height
137
+ * @param {SVGMatrix} m - Transformation matrix
138
+ * @returns {TransformedBox} The transformed box information
145
139
  */
146
- export const transformBox = function (l, t, w, h, m) {
140
+ export const transformBox = (l, t, w, h, m) => {
147
141
  const tl = transformPoint(l, t, m)
148
142
  const tr = transformPoint(l + w, t, m)
149
143
  const bl = transformPoint(l, t + h, m)
@@ -169,91 +163,81 @@ export const transformBox = function (l, t, w, h, m) {
169
163
  }
170
164
 
171
165
  /**
172
- * This returns a single matrix Transform for a given Transform List
173
- * (this is the equivalent of `SVGTransformList.consolidate()` but unlike
174
- * that method, this one does not modify the actual `SVGTransformList`).
175
- * This function is very liberal with its `min`, `max` arguments.
176
- * @function module:math.transformListToTransform
177
- * @param {SVGTransformList} tlist - The transformlist object
178
- * @param {Integer} [min=0] - Optional integer indicating start transform position
179
- * @param {Integer} [max] - Optional integer indicating end transform position;
180
- * defaults to one less than the tlist's `numberOfItems`
181
- * @returns {SVGTransform} A single matrix transform object
166
+ * Consolidates a transform list into a single matrix transform without modifying the original list.
167
+ * @function transformListToTransform
168
+ * @param {SVGTransformList} tlist - The transform list
169
+ * @param {number} [min=0] - Optional start index
170
+ * @param {number} [max] - Optional end index, defaults to tlist length-1
171
+ * @returns {SVGTransform} A single transform from the combined matrices
182
172
  */
183
- export const transformListToTransform = function (tlist, min, max) {
173
+ export const transformListToTransform = (tlist, min = 0, max = null) => {
184
174
  if (!tlist) {
185
- // Or should tlist = null have been prevented before this?
186
175
  return svg.createSVGTransformFromMatrix(svg.createSVGMatrix())
187
176
  }
188
- min = min || 0
189
- max = max || tlist.numberOfItems - 1
190
- min = Number.parseInt(min)
191
- max = Number.parseInt(max)
192
- if (min > max) {
193
- const temp = max
194
- max = min
195
- min = temp
196
- }
197
- let m = svg.createSVGMatrix()
198
- for (let i = min; i <= max; ++i) {
199
- // if our indices are out of range, just use a harmless identity matrix
200
- const mtom =
177
+
178
+ const start = Number.parseInt(min, 10)
179
+ const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10)
180
+ const low = Math.min(start, end)
181
+ const high = Math.max(start, end)
182
+
183
+ let combinedMatrix = svg.createSVGMatrix()
184
+ for (let i = low; i <= high; i++) {
185
+ // If out of range, use identity
186
+ const currentMatrix =
201
187
  i >= 0 && i < tlist.numberOfItems
202
188
  ? tlist.getItem(i).matrix
203
189
  : svg.createSVGMatrix()
204
- m = matrixMultiply(m, mtom)
190
+ combinedMatrix = matrixMultiply(combinedMatrix, currentMatrix)
205
191
  }
206
- return svg.createSVGTransformFromMatrix(m)
192
+
193
+ return svg.createSVGTransformFromMatrix(combinedMatrix)
207
194
  }
208
195
 
209
196
  /**
210
- * Get the matrix object for a given element.
211
- * @function module:math.getMatrix
212
- * @param {Element} elem - The DOM element to check
213
- * @returns {SVGMatrix} The matrix object associated with the element's transformlist
197
+ * Gets the matrix of a given element's transform list.
198
+ * @function getMatrix
199
+ * @param {Element} elem - The element to check
200
+ * @returns {SVGMatrix} The transformation matrix
214
201
  */
215
- export const getMatrix = (elem) => {
202
+ export const getMatrix = elem => {
216
203
  const tlist = getTransformList(elem)
217
204
  return transformListToTransform(tlist).matrix
218
205
  }
219
206
 
220
207
  /**
221
- * Returns a 45 degree angle coordinate associated with the two given
222
- * coordinates.
223
- * @function module:math.snapToAngle
224
- * @param {Integer} x1 - First coordinate's x value
225
- * @param {Integer} y1 - First coordinate's y value
226
- * @param {Integer} x2 - Second coordinate's x value
227
- * @param {Integer} y2 - Second coordinate's y value
228
- * @returns {module:math.AngleCoord45}
208
+ * Returns a coordinate snapped to the nearest 45-degree angle.
209
+ * @function snapToAngle
210
+ * @param {number} x1 - First point's x
211
+ * @param {number} y1 - First point's y
212
+ * @param {number} x2 - Second point's x
213
+ * @param {number} y2 - Second point's y
214
+ * @returns {AngleCoord45} The angle-snapped coordinates and angle
229
215
  */
230
216
  export const snapToAngle = (x1, y1, x2, y2) => {
231
217
  const snap = Math.PI / 4 // 45 degrees
232
218
  const dx = x2 - x1
233
219
  const dy = y2 - y1
234
220
  const angle = Math.atan2(dy, dx)
235
- const dist = Math.sqrt(dx * dx + dy * dy)
236
- const snapangle = Math.round(angle / snap) * snap
221
+ const dist = Math.hypot(dx, dy)
222
+ const snapAngle = Math.round(angle / snap) * snap
237
223
 
238
224
  return {
239
- x: x1 + dist * Math.cos(snapangle),
240
- y: y1 + dist * Math.sin(snapangle),
241
- a: snapangle
225
+ x: x1 + dist * Math.cos(snapAngle),
226
+ y: y1 + dist * Math.sin(snapAngle),
227
+ a: snapAngle
242
228
  }
243
229
  }
244
230
 
245
231
  /**
246
- * Check if two rectangles (BBoxes objects) intersect each other.
247
- * @function module:math.rectsIntersect
248
- * @param {SVGRect} r1 - The first BBox-like object
249
- * @param {SVGRect} r2 - The second BBox-like object
250
- * @returns {boolean} True if rectangles intersect
232
+ * Checks if two rectangles intersect.
233
+ * Both r1 and r2 are expected to have {x, y, width, height}.
234
+ * @function rectsIntersect
235
+ * @param {{x:number,y:number,width:number,height:number}} r1 - First rectangle
236
+ * @param {{x:number,y:number,width:number,height:number}} r2 - Second rectangle
237
+ * @returns {boolean} True if the rectangles intersect
251
238
  */
252
- export const rectsIntersect = (r1, r2) => {
253
- return (
254
- r2.x < r1.x + r1.width &&
255
- r2.x + r2.width > r1.x &&
256
- r2.y < r1.y + r1.height &&
257
- r2.y + r2.height > r1.y
258
- )
259
- }
239
+ export const rectsIntersect = (r1, r2) =>
240
+ r2.x < r1.x + r1.width &&
241
+ r2.x + r2.width > r1.x &&
242
+ r2.y < r1.y + r1.height &&
243
+ r2.y + r2.height > r1.y