@svgedit/svgcanvas 7.2.7 → 7.4.2
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/common/browser.js +104 -37
- package/common/logger.js +151 -0
- package/common/util.js +96 -155
- package/core/blur-event.js +106 -42
- package/core/clear.js +13 -3
- package/core/coords.js +214 -90
- package/core/copy-elem.js +27 -13
- package/core/dataStorage.js +84 -21
- package/core/draw.js +80 -40
- package/core/elem-get-set.js +247 -115
- package/core/event.js +171 -29
- package/core/history.js +105 -50
- package/core/historyrecording.js +4 -2
- package/core/json.js +54 -12
- package/core/layer.js +11 -17
- package/core/math.js +102 -23
- package/core/namespaces.js +5 -5
- package/core/paint.js +95 -24
- package/core/paste-elem.js +58 -19
- package/core/path-actions.js +812 -791
- package/core/path-method.js +236 -37
- package/core/path.js +45 -10
- package/core/recalculate.js +410 -15
- package/core/sanitize.js +51 -18
- package/core/select.js +44 -20
- package/core/selected-elem.js +159 -38
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +363 -307
- package/core/undo.js +47 -15
- package/core/units.js +8 -6
- package/core/utilities.js +282 -170
- package/dist/svgcanvas.js +40021 -55172
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +55 -54
- package/publish.md +1 -6
- package/svgcanvas.d.ts +225 -0
- package/svgcanvas.js +11 -9
- package/vite.config.mjs +20 -0
- package/rollup.config.mjs +0 -38
package/core/json.js
CHANGED
|
@@ -27,7 +27,7 @@ let svgdoc_ = null
|
|
|
27
27
|
*/
|
|
28
28
|
export const init = (canvas) => {
|
|
29
29
|
svgCanvas = canvas
|
|
30
|
-
svgdoc_ = canvas.getDOMDocument()
|
|
30
|
+
svgdoc_ = canvas.getDOMDocument?.() || (typeof document !== 'undefined' ? document : null)
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* @function module:json.getJsonFromSvgElements Iterate element and return json format
|
|
@@ -35,8 +35,12 @@ export const init = (canvas) => {
|
|
|
35
35
|
* @returns {svgRootElement}
|
|
36
36
|
*/
|
|
37
37
|
export const getJsonFromSvgElements = (data) => {
|
|
38
|
+
if (!data) return null
|
|
39
|
+
|
|
38
40
|
// Text node
|
|
39
|
-
if (data.nodeType === 3) return data.nodeValue
|
|
41
|
+
if (data.nodeType === 3 || data.nodeType === 4) return data.nodeValue
|
|
42
|
+
// Ignore non-element nodes (e.g., comments)
|
|
43
|
+
if (data.nodeType !== 1) return null
|
|
40
44
|
|
|
41
45
|
const retval = {
|
|
42
46
|
element: data.tagName,
|
|
@@ -46,13 +50,25 @@ export const getJsonFromSvgElements = (data) => {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
// Iterate attributes
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const attributes = data.attributes
|
|
54
|
+
if (attributes) {
|
|
55
|
+
for (let i = 0; i < attributes.length; i++) {
|
|
56
|
+
const attr = attributes[i]
|
|
57
|
+
if (!attr) continue
|
|
58
|
+
retval.attr[attr.name] = attr.value
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
// Iterate children
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
const childNodes = data.childNodes
|
|
64
|
+
if (childNodes) {
|
|
65
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
66
|
+
const node = childNodes[i]
|
|
67
|
+
const child = getJsonFromSvgElements(node)
|
|
68
|
+
if (child !== null && child !== undefined) {
|
|
69
|
+
retval.children.push(child)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
return retval
|
|
@@ -65,11 +81,29 @@ export const getJsonFromSvgElements = (data) => {
|
|
|
65
81
|
*/
|
|
66
82
|
|
|
67
83
|
export const addSVGElementsFromJson = (data) => {
|
|
84
|
+
if (!svgdoc_) { return null }
|
|
85
|
+
if (data === null || data === undefined) return svgdoc_.createTextNode('')
|
|
68
86
|
if (typeof data === 'string') return svgdoc_.createTextNode(data)
|
|
69
87
|
|
|
70
|
-
|
|
88
|
+
const attrs = data.attr || {}
|
|
89
|
+
const id = attrs.id
|
|
90
|
+
let shape = null
|
|
91
|
+
if (typeof id === 'string' && id) {
|
|
92
|
+
try {
|
|
93
|
+
shape = getElement(id)
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// Ignore (CSS selector may be invalid); fallback to getElementById below
|
|
96
|
+
}
|
|
97
|
+
if (!shape) {
|
|
98
|
+
const byId = svgdoc_.getElementById?.(id)
|
|
99
|
+
const svgRoot = svgCanvas?.getSvgRoot?.()
|
|
100
|
+
if (byId && (!svgRoot || svgRoot.contains(byId))) {
|
|
101
|
+
shape = byId
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
71
105
|
// if shape is a path but we need to create a rect/ellipse, then remove the path
|
|
72
|
-
const currentLayer = svgCanvas
|
|
106
|
+
const currentLayer = svgCanvas?.getDrawing?.()?.getCurrentLayer?.()
|
|
73
107
|
if (shape && data.element !== shape.tagName) {
|
|
74
108
|
shape.remove()
|
|
75
109
|
shape = null
|
|
@@ -81,8 +115,10 @@ export const addSVGElementsFromJson = (data) => {
|
|
|
81
115
|
(svgCanvas.getCurrentGroup() || currentLayer).append(shape)
|
|
82
116
|
}
|
|
83
117
|
}
|
|
84
|
-
const curShape = svgCanvas.getCurShape()
|
|
118
|
+
const curShape = svgCanvas.getCurShape?.() || {}
|
|
85
119
|
if (data.curStyles) {
|
|
120
|
+
const curOpacity = Number(curShape.opacity)
|
|
121
|
+
const opacity = Number.isFinite(curOpacity) ? (curOpacity / 2) : 0.5
|
|
86
122
|
assignAttributes(shape, {
|
|
87
123
|
fill: curShape.fill,
|
|
88
124
|
stroke: curShape.stroke,
|
|
@@ -92,17 +128,23 @@ export const addSVGElementsFromJson = (data) => {
|
|
|
92
128
|
'stroke-linecap': curShape.stroke_linecap,
|
|
93
129
|
'stroke-opacity': curShape.stroke_opacity,
|
|
94
130
|
'fill-opacity': curShape.fill_opacity,
|
|
95
|
-
opacity
|
|
131
|
+
opacity,
|
|
96
132
|
style: 'pointer-events:inherit'
|
|
97
133
|
}, 100)
|
|
98
134
|
}
|
|
99
|
-
assignAttributes(shape,
|
|
135
|
+
assignAttributes(shape, attrs, 100)
|
|
100
136
|
cleanupElement(shape)
|
|
101
137
|
|
|
102
138
|
// Children
|
|
103
139
|
if (data.children) {
|
|
140
|
+
while (shape.firstChild) {
|
|
141
|
+
shape.firstChild.remove()
|
|
142
|
+
}
|
|
104
143
|
data.children.forEach((child) => {
|
|
105
|
-
|
|
144
|
+
const childNode = addSVGElementsFromJson(child)
|
|
145
|
+
if (childNode) {
|
|
146
|
+
shape.append(childNode)
|
|
147
|
+
}
|
|
106
148
|
})
|
|
107
149
|
}
|
|
108
150
|
|
package/core/layer.js
CHANGED
|
@@ -40,19 +40,16 @@ class Layer {
|
|
|
40
40
|
const layerTitle = svgdoc.createElementNS(NS.SVG, 'title')
|
|
41
41
|
layerTitle.textContent = name
|
|
42
42
|
this.group_.append(layerTitle)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} else {
|
|
46
|
-
svgElem.append(this.group_)
|
|
47
|
-
}
|
|
43
|
+
|
|
44
|
+
group ? group.insertAdjacentElement('afterend', this.group_) : svgElem.append(this.group_)
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
addLayerClass(this.group_)
|
|
51
48
|
walkTree(this.group_, function (e) {
|
|
52
|
-
e.
|
|
49
|
+
e.style.pointerEvents = 'inherit'
|
|
53
50
|
})
|
|
54
51
|
|
|
55
|
-
this.group_.
|
|
52
|
+
this.group_.style.pointerEvents = svgElem ? 'all' : 'none'
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
/**
|
|
@@ -76,7 +73,7 @@ class Layer {
|
|
|
76
73
|
* @returns {void}
|
|
77
74
|
*/
|
|
78
75
|
activate () {
|
|
79
|
-
this.group_.
|
|
76
|
+
this.group_.style.pointerEvents = 'all'
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
/**
|
|
@@ -84,7 +81,7 @@ class Layer {
|
|
|
84
81
|
* @returns {void}
|
|
85
82
|
*/
|
|
86
83
|
deactivate () {
|
|
87
|
-
this.group_.
|
|
84
|
+
this.group_.style.pointerEvents = 'none'
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
/**
|
|
@@ -93,7 +90,7 @@ class Layer {
|
|
|
93
90
|
* @returns {void}
|
|
94
91
|
*/
|
|
95
92
|
setVisible (visible) {
|
|
96
|
-
const expected = visible === undefined || visible ? 'inline' : 'none'
|
|
93
|
+
const expected = (visible === undefined || visible) ? 'inline' : 'none'
|
|
97
94
|
const oldDisplay = this.group_.getAttribute('display')
|
|
98
95
|
if (oldDisplay !== expected) {
|
|
99
96
|
this.group_.setAttribute('display', expected)
|
|
@@ -114,10 +111,7 @@ class Layer {
|
|
|
114
111
|
*/
|
|
115
112
|
getOpacity () {
|
|
116
113
|
const opacity = this.group_.getAttribute('opacity')
|
|
117
|
-
|
|
118
|
-
return 1
|
|
119
|
-
}
|
|
120
|
-
return Number.parseFloat(opacity)
|
|
114
|
+
return opacity ? Number.parseFloat(opacity) : 1
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
/**
|
|
@@ -208,7 +202,7 @@ Layer.CLASS_NAME = 'layer'
|
|
|
208
202
|
/**
|
|
209
203
|
* @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
|
|
210
204
|
*/
|
|
211
|
-
Layer.CLASS_REGEX = new RegExp(
|
|
205
|
+
Layer.CLASS_REGEX = new RegExp(`(\\s|^)${Layer.CLASS_NAME}(\\s|$)`)
|
|
212
206
|
|
|
213
207
|
/**
|
|
214
208
|
* Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
|
|
@@ -216,12 +210,12 @@ Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)')
|
|
|
216
210
|
* @param {SVGGElement} elem - The SVG element to update
|
|
217
211
|
* @returns {void}
|
|
218
212
|
*/
|
|
219
|
-
|
|
213
|
+
const addLayerClass = (elem) => {
|
|
220
214
|
const classes = elem.getAttribute('class')
|
|
221
215
|
if (!classes || !classes.length) {
|
|
222
216
|
elem.setAttribute('class', Layer.CLASS_NAME)
|
|
223
217
|
} else if (!Layer.CLASS_REGEX.test(classes)) {
|
|
224
|
-
elem.setAttribute('class', classes
|
|
218
|
+
elem.setAttribute('class', `${classes} ${Layer.CLASS_NAME}`)
|
|
225
219
|
}
|
|
226
220
|
}
|
|
227
221
|
|
package/core/math.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { NS } from './namespaces.js'
|
|
23
|
+
import { warn } from '../common/logger.js'
|
|
23
24
|
|
|
24
25
|
// Constants
|
|
25
26
|
const NEAR_ZERO = 1e-10
|
|
@@ -27,6 +28,38 @@ const NEAR_ZERO = 1e-10
|
|
|
27
28
|
// Create a throwaway SVG element for matrix operations
|
|
28
29
|
const svg = document.createElementNS(NS.SVG, 'svg')
|
|
29
30
|
|
|
31
|
+
const createTransformFromMatrix = (m) => {
|
|
32
|
+
const createFallback = (matrix) => {
|
|
33
|
+
const fallback = svg.createSVGMatrix()
|
|
34
|
+
Object.assign(fallback, {
|
|
35
|
+
a: matrix.a,
|
|
36
|
+
b: matrix.b,
|
|
37
|
+
c: matrix.c,
|
|
38
|
+
d: matrix.d,
|
|
39
|
+
e: matrix.e,
|
|
40
|
+
f: matrix.f
|
|
41
|
+
})
|
|
42
|
+
return fallback
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return svg.createSVGTransformFromMatrix(m)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
const t = svg.createSVGTransform()
|
|
49
|
+
try {
|
|
50
|
+
t.setMatrix(m)
|
|
51
|
+
return t
|
|
52
|
+
} catch (err) {
|
|
53
|
+
try {
|
|
54
|
+
return svg.createSVGTransformFromMatrix(createFallback(m))
|
|
55
|
+
} catch (e2) {
|
|
56
|
+
t.setMatrix(createFallback(m))
|
|
57
|
+
return t
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
/**
|
|
31
64
|
* Transforms a point by a given matrix without DOM calls.
|
|
32
65
|
* @function transformPoint
|
|
@@ -56,7 +89,7 @@ export const getTransformList = elem => {
|
|
|
56
89
|
if (elem.patternTransform?.baseVal) {
|
|
57
90
|
return elem.patternTransform.baseVal
|
|
58
91
|
}
|
|
59
|
-
|
|
92
|
+
warn('No transform list found. Check browser compatibility.', elem, 'math')
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
/**
|
|
@@ -66,7 +99,12 @@ export const getTransformList = elem => {
|
|
|
66
99
|
* @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0)
|
|
67
100
|
*/
|
|
68
101
|
export const isIdentity = m =>
|
|
69
|
-
m.a
|
|
102
|
+
Math.abs(m.a - 1) < NEAR_ZERO &&
|
|
103
|
+
Math.abs(m.b) < NEAR_ZERO &&
|
|
104
|
+
Math.abs(m.c) < NEAR_ZERO &&
|
|
105
|
+
Math.abs(m.d - 1) < NEAR_ZERO &&
|
|
106
|
+
Math.abs(m.e) < NEAR_ZERO &&
|
|
107
|
+
Math.abs(m.f) < NEAR_ZERO
|
|
70
108
|
|
|
71
109
|
/**
|
|
72
110
|
* Multiplies multiple matrices together (m1 * m2 * ...).
|
|
@@ -76,22 +114,54 @@ export const isIdentity = m =>
|
|
|
76
114
|
* @returns {SVGMatrix} The resulting matrix
|
|
77
115
|
*/
|
|
78
116
|
export const matrixMultiply = (...args) => {
|
|
79
|
-
// If no matrices are given, return an identity matrix
|
|
80
117
|
if (args.length === 0) {
|
|
81
118
|
return svg.createSVGMatrix()
|
|
82
119
|
}
|
|
83
120
|
|
|
84
|
-
const
|
|
121
|
+
const normalizeNearZero = (matrix) => {
|
|
122
|
+
const props = ['a', 'b', 'c', 'd', 'e', 'f']
|
|
123
|
+
for (const prop of props) {
|
|
124
|
+
if (Math.abs(matrix[prop]) < NEAR_ZERO) {
|
|
125
|
+
matrix[prop] = 0
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return matrix
|
|
129
|
+
}
|
|
85
130
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (Math.abs(m.e) < NEAR_ZERO) m.e = 0
|
|
92
|
-
if (Math.abs(m.f) < NEAR_ZERO) m.f = 0
|
|
131
|
+
if (typeof DOMMatrix === 'function' && typeof DOMMatrix.fromMatrix === 'function') {
|
|
132
|
+
const result = args.reduce(
|
|
133
|
+
(acc, curr) => acc.multiply(DOMMatrix.fromMatrix(curr)),
|
|
134
|
+
new DOMMatrix()
|
|
135
|
+
)
|
|
93
136
|
|
|
94
|
-
|
|
137
|
+
const out = svg.createSVGMatrix()
|
|
138
|
+
Object.assign(out, {
|
|
139
|
+
a: result.a,
|
|
140
|
+
b: result.b,
|
|
141
|
+
c: result.c,
|
|
142
|
+
d: result.d,
|
|
143
|
+
e: result.e,
|
|
144
|
+
f: result.f
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
return normalizeNearZero(out)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let m = svg.createSVGMatrix()
|
|
151
|
+
for (const curr of args) {
|
|
152
|
+
const next = svg.createSVGMatrix()
|
|
153
|
+
Object.assign(next, {
|
|
154
|
+
a: m.a * curr.a + m.c * curr.b,
|
|
155
|
+
b: m.b * curr.a + m.d * curr.b,
|
|
156
|
+
c: m.a * curr.c + m.c * curr.d,
|
|
157
|
+
d: m.b * curr.c + m.d * curr.d,
|
|
158
|
+
e: m.a * curr.e + m.c * curr.f + m.e,
|
|
159
|
+
f: m.b * curr.e + m.d * curr.f + m.f
|
|
160
|
+
})
|
|
161
|
+
m = next
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return normalizeNearZero(m)
|
|
95
165
|
}
|
|
96
166
|
|
|
97
167
|
/**
|
|
@@ -172,25 +242,34 @@ export const transformBox = (l, t, w, h, m) => {
|
|
|
172
242
|
*/
|
|
173
243
|
export const transformListToTransform = (tlist, min = 0, max = null) => {
|
|
174
244
|
if (!tlist) {
|
|
175
|
-
return
|
|
245
|
+
return createTransformFromMatrix(svg.createSVGMatrix())
|
|
176
246
|
}
|
|
177
247
|
|
|
178
248
|
const start = Number.parseInt(min, 10)
|
|
179
249
|
const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10)
|
|
180
|
-
const low = Math.min(start, end)
|
|
181
|
-
const high = Math.max(start, end)
|
|
250
|
+
const [low, high] = [Math.min(start, end), Math.max(start, end)]
|
|
182
251
|
|
|
183
|
-
|
|
252
|
+
const matrices = []
|
|
184
253
|
for (let i = low; i <= high; i++) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: svg.createSVGMatrix()
|
|
190
|
-
combinedMatrix = matrixMultiply(combinedMatrix, currentMatrix)
|
|
254
|
+
const matrix = (i >= 0 && i < tlist.numberOfItems)
|
|
255
|
+
? tlist.getItem(i).matrix
|
|
256
|
+
: svg.createSVGMatrix()
|
|
257
|
+
matrices.push(matrix)
|
|
191
258
|
}
|
|
192
259
|
|
|
193
|
-
|
|
260
|
+
const combinedMatrix = matrixMultiply(...matrices)
|
|
261
|
+
|
|
262
|
+
const out = svg.createSVGMatrix()
|
|
263
|
+
Object.assign(out, {
|
|
264
|
+
a: combinedMatrix.a,
|
|
265
|
+
b: combinedMatrix.b,
|
|
266
|
+
c: combinedMatrix.c,
|
|
267
|
+
d: combinedMatrix.d,
|
|
268
|
+
e: combinedMatrix.e,
|
|
269
|
+
f: combinedMatrix.f
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return createTransformFromMatrix(out)
|
|
194
273
|
}
|
|
195
274
|
|
|
196
275
|
/**
|
package/core/namespaces.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Common
|
|
8
|
+
* Common namespaces constants in alpha order.
|
|
9
9
|
* @enum {string}
|
|
10
10
|
* @type {PlainObject}
|
|
11
11
|
* @memberof module:namespaces
|
|
@@ -29,12 +29,12 @@ export const NS = {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* @function module:namespaces.getReverseNS
|
|
32
|
-
* @returns {string} The
|
|
32
|
+
* @returns {PlainObject<string, string>} The namespace URI map with values swapped to their lowercase keys
|
|
33
33
|
*/
|
|
34
|
-
export const getReverseNS =
|
|
34
|
+
export const getReverseNS = () => {
|
|
35
35
|
const reverseNS = {}
|
|
36
|
-
|
|
36
|
+
for (const [name, URI] of Object.entries(NS)) {
|
|
37
37
|
reverseNS[URI] = name.toLowerCase()
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
return reverseNS
|
|
40
40
|
}
|
package/core/paint.js
CHANGED
|
@@ -2,12 +2,95 @@
|
|
|
2
2
|
*
|
|
3
3
|
*/
|
|
4
4
|
export default class Paint {
|
|
5
|
+
static #normalizeAlpha (alpha) {
|
|
6
|
+
const numeric = Number(alpha)
|
|
7
|
+
if (!Number.isFinite(numeric)) return 100
|
|
8
|
+
return Math.min(100, Math.max(0, numeric))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static #normalizeSolidColor (color) {
|
|
12
|
+
if (color === null || color === undefined) return null
|
|
13
|
+
const str = String(color).trim()
|
|
14
|
+
if (!str) return null
|
|
15
|
+
if (str === 'none') return 'none'
|
|
16
|
+
return str.startsWith('#') ? str.slice(1) : str
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static #extractHrefId (hrefAttr) {
|
|
20
|
+
if (!hrefAttr) return null
|
|
21
|
+
const href = String(hrefAttr).trim()
|
|
22
|
+
if (!href) return null
|
|
23
|
+
if (href.startsWith('#')) return href.slice(1)
|
|
24
|
+
const urlMatch = href.match(/url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/)
|
|
25
|
+
if (urlMatch?.[1]) return urlMatch[1]
|
|
26
|
+
const hashIndex = href.lastIndexOf('#')
|
|
27
|
+
if (hashIndex >= 0 && hashIndex < href.length - 1) {
|
|
28
|
+
return href.slice(hashIndex + 1)
|
|
29
|
+
}
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static #resolveGradient (gradient) {
|
|
34
|
+
if (!gradient?.cloneNode) return null
|
|
35
|
+
const doc = gradient.ownerDocument || document
|
|
36
|
+
const visited = new Set()
|
|
37
|
+
const clone = gradient.cloneNode(true)
|
|
38
|
+
|
|
39
|
+
let refId = Paint.#extractHrefId(
|
|
40
|
+
clone.getAttribute('href') || clone.getAttribute('xlink:href')
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
while (refId && !visited.has(refId)) {
|
|
44
|
+
visited.add(refId)
|
|
45
|
+
|
|
46
|
+
const referenced = doc.getElementById(refId)
|
|
47
|
+
if (!referenced?.getAttribute) break
|
|
48
|
+
|
|
49
|
+
const cloneTag = String(clone.tagName || '').toLowerCase()
|
|
50
|
+
const referencedTag = String(referenced.tagName || '').toLowerCase()
|
|
51
|
+
if (
|
|
52
|
+
!['lineargradient', 'radialgradient'].includes(referencedTag) ||
|
|
53
|
+
referencedTag !== cloneTag
|
|
54
|
+
) {
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Copy missing attributes from referenced gradient (matches SVG href inheritance).
|
|
59
|
+
for (const attr of referenced.attributes || []) {
|
|
60
|
+
const name = attr.name
|
|
61
|
+
if (name === 'id' || name === 'href' || name === 'xlink:href') continue
|
|
62
|
+
const current = clone.getAttribute(name)
|
|
63
|
+
if (current === null || current === '') {
|
|
64
|
+
clone.setAttribute(name, attr.value)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If the referencing gradient has no stops, inherit stops from the referenced gradient.
|
|
69
|
+
if (clone.querySelectorAll('stop').length === 0) {
|
|
70
|
+
for (const stop of referenced.querySelectorAll?.('stop') || []) {
|
|
71
|
+
clone.append(stop.cloneNode(true))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Prepare to continue resolving deeper links if present.
|
|
76
|
+
refId = Paint.#extractHrefId(
|
|
77
|
+
referenced.getAttribute('href') || referenced.getAttribute('xlink:href')
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The clone is now self-contained; remove any href.
|
|
82
|
+
clone.removeAttribute('href')
|
|
83
|
+
clone.removeAttribute('xlink:href')
|
|
84
|
+
|
|
85
|
+
return clone
|
|
86
|
+
}
|
|
87
|
+
|
|
5
88
|
/**
|
|
6
89
|
* @param {module:jGraduate.jGraduatePaintOptions} [opt]
|
|
7
90
|
*/
|
|
8
91
|
constructor (opt) {
|
|
9
92
|
const options = opt || {}
|
|
10
|
-
this.alpha =
|
|
93
|
+
this.alpha = Paint.#normalizeAlpha(options.alpha)
|
|
11
94
|
// copy paint object
|
|
12
95
|
if (options.copy) {
|
|
13
96
|
/**
|
|
@@ -20,7 +103,7 @@ export default class Paint {
|
|
|
20
103
|
* @name module:jGraduate~Paint#alpha
|
|
21
104
|
* @type {Float}
|
|
22
105
|
*/
|
|
23
|
-
this.alpha = options.copy.alpha
|
|
106
|
+
this.alpha = Paint.#normalizeAlpha(options.copy.alpha)
|
|
24
107
|
/**
|
|
25
108
|
* Represents #RRGGBB hex of color.
|
|
26
109
|
* @name module:jGraduate~Paint#solidColor
|
|
@@ -42,13 +125,17 @@ export default class Paint {
|
|
|
42
125
|
case 'none':
|
|
43
126
|
break
|
|
44
127
|
case 'solidColor':
|
|
45
|
-
this.solidColor = options.copy.solidColor
|
|
128
|
+
this.solidColor = Paint.#normalizeSolidColor(options.copy.solidColor)
|
|
46
129
|
break
|
|
47
130
|
case 'linearGradient':
|
|
48
|
-
this.linearGradient = options.copy.linearGradient
|
|
131
|
+
this.linearGradient = options.copy.linearGradient?.cloneNode
|
|
132
|
+
? options.copy.linearGradient.cloneNode(true)
|
|
133
|
+
: null
|
|
49
134
|
break
|
|
50
135
|
case 'radialGradient':
|
|
51
|
-
this.radialGradient = options.copy.radialGradient
|
|
136
|
+
this.radialGradient = options.copy.radialGradient?.cloneNode
|
|
137
|
+
? options.copy.radialGradient.cloneNode(true)
|
|
138
|
+
: null
|
|
52
139
|
break
|
|
53
140
|
}
|
|
54
141
|
// create linear gradient paint
|
|
@@ -56,33 +143,17 @@ export default class Paint {
|
|
|
56
143
|
this.type = 'linearGradient'
|
|
57
144
|
this.solidColor = null
|
|
58
145
|
this.radialGradient = null
|
|
59
|
-
|
|
60
|
-
options.linearGradient.getAttribute('href') ||
|
|
61
|
-
options.linearGradient.getAttribute('xlink:href')
|
|
62
|
-
if (hrefAttr) {
|
|
63
|
-
const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
|
|
64
|
-
this.linearGradient = xhref.cloneNode(true)
|
|
65
|
-
} else {
|
|
66
|
-
this.linearGradient = options.linearGradient.cloneNode(true)
|
|
67
|
-
}
|
|
146
|
+
this.linearGradient = Paint.#resolveGradient(options.linearGradient)
|
|
68
147
|
// create linear gradient paint
|
|
69
148
|
} else if (options.radialGradient) {
|
|
70
149
|
this.type = 'radialGradient'
|
|
71
150
|
this.solidColor = null
|
|
72
151
|
this.linearGradient = null
|
|
73
|
-
|
|
74
|
-
options.radialGradient.getAttribute('href') ||
|
|
75
|
-
options.radialGradient.getAttribute('xlink:href')
|
|
76
|
-
if (hrefAttr) {
|
|
77
|
-
const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
|
|
78
|
-
this.radialGradient = xhref.cloneNode(true)
|
|
79
|
-
} else {
|
|
80
|
-
this.radialGradient = options.radialGradient.cloneNode(true)
|
|
81
|
-
}
|
|
152
|
+
this.radialGradient = Paint.#resolveGradient(options.radialGradient)
|
|
82
153
|
// create solid color paint
|
|
83
154
|
} else if (options.solidColor) {
|
|
84
155
|
this.type = 'solidColor'
|
|
85
|
-
this.solidColor = options.solidColor
|
|
156
|
+
this.solidColor = Paint.#normalizeSolidColor(options.solidColor)
|
|
86
157
|
// create empty paint
|
|
87
158
|
} else {
|
|
88
159
|
this.type = 'none'
|