@jbrowse/svgcanvas 6.4.2 → 7.0.0
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/dist/context.test.js +67 -0
- package/dist/context.test.js.map +1 -0
- package/dist/index.d.ts +15 -56
- package/dist/index.js +154 -525
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/context.test.ts +71 -0
- package/src/index.ts +167 -652
- package/dist/drawImage.test.js +0 -76
- package/dist/drawImage.test.js.map +0 -1
- package/src/drawImage.test.ts +0 -81
- /package/dist/{drawImage.test.d.ts → context.test.d.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -9,61 +9,15 @@
|
|
|
9
9
|
* Copyright (c) 2014 Gliffy Inc.
|
|
10
10
|
* Copyright (c) 2021 Zeno Zeng
|
|
11
11
|
*
|
|
12
|
-
* Vendored and converted to ESM/TypeScript for pure ESM compatibility
|
|
12
|
+
* Vendored and converted to ESM/TypeScript for pure ESM compatibility, and cut
|
|
13
|
+
* down to the drawing calls the renderers make: what a multiple sequence
|
|
14
|
+
* alignment needs is rectangles, paths, arcs and a great many glyphs. Gradients,
|
|
15
|
+
* patterns, clipping, rotation, bezier curves, stroked text, shadows and image
|
|
16
|
+
* drawing are gone with the state they carried.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
20
|
|
|
17
|
-
function toString(obj: any): string {
|
|
18
|
-
if (!obj) {
|
|
19
|
-
return obj
|
|
20
|
-
}
|
|
21
|
-
if (typeof obj === 'string') {
|
|
22
|
-
return obj
|
|
23
|
-
}
|
|
24
|
-
return obj + ''
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function format(str: string, args: Record<string, any>): string {
|
|
28
|
-
const keys = Object.keys(args)
|
|
29
|
-
for (const key of keys) {
|
|
30
|
-
str = str.replace(new RegExp('\\{' + key + '\\}', 'gi'), args[key])
|
|
31
|
-
}
|
|
32
|
-
return str
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function randomString(holder: Record<string, string>): string {
|
|
36
|
-
if (!holder) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
'cannot create a random attribute name for an undefined object',
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'
|
|
42
|
-
let randomstring = ''
|
|
43
|
-
do {
|
|
44
|
-
randomstring = ''
|
|
45
|
-
for (let i = 0; i < 12; i++) {
|
|
46
|
-
randomstring += chars[Math.floor(Math.random() * chars.length)]
|
|
47
|
-
}
|
|
48
|
-
} while (holder[randomstring])
|
|
49
|
-
return randomstring
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function createNamedToNumberedLookup(
|
|
53
|
-
items: string,
|
|
54
|
-
radix: number = 10,
|
|
55
|
-
): Record<string, string> {
|
|
56
|
-
const lookup: Record<string, string> = {}
|
|
57
|
-
const parts = items.split(',')
|
|
58
|
-
for (let i = 0; i < parts.length; i += 2) {
|
|
59
|
-
const entity = '&' + parts[i + 1] + ';'
|
|
60
|
-
const base10 = parseInt(parts[i]!, radix)
|
|
61
|
-
lookup[entity] = '&#' + base10 + ';'
|
|
62
|
-
}
|
|
63
|
-
lookup['\\xa0'] = ' '
|
|
64
|
-
return lookup
|
|
65
|
-
}
|
|
66
|
-
|
|
67
21
|
function getTextAnchor(textAlign: string): string {
|
|
68
22
|
const mapping: Record<string, string> = {
|
|
69
23
|
left: 'start',
|
|
@@ -94,7 +48,6 @@ function getDominantBaseline(textBaseline: string): string {
|
|
|
94
48
|
const TEXT_ATTR_DEFAULTS: Record<string, string> = {
|
|
95
49
|
'font-style': 'normal',
|
|
96
50
|
'font-weight': 'normal',
|
|
97
|
-
'font-variant': 'normal',
|
|
98
51
|
// the initial value is `auto`, which resolves to the alphabetic baseline
|
|
99
52
|
'dominant-baseline': 'alphabetic',
|
|
100
53
|
}
|
|
@@ -103,35 +56,6 @@ function omitDefault(attr: string, value: string | undefined) {
|
|
|
103
56
|
return value === TEXT_ATTR_DEFAULTS[attr] ? undefined : value
|
|
104
57
|
}
|
|
105
58
|
|
|
106
|
-
const namedEntities = createNamedToNumberedLookup(
|
|
107
|
-
'50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
|
|
108
|
-
'5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
|
|
109
|
-
'5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
|
|
110
|
-
'5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
|
|
111
|
-
'68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
|
|
112
|
-
'6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
|
|
113
|
-
'6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
|
|
114
|
-
'75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
|
|
115
|
-
'7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
|
|
116
|
-
'7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
|
|
117
|
-
'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
|
|
118
|
-
'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
|
|
119
|
-
't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
|
|
120
|
-
'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
|
|
121
|
-
'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
|
|
122
|
-
'81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
|
|
123
|
-
'8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
|
|
124
|
-
'8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
|
|
125
|
-
'8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
|
|
126
|
-
'8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
|
|
127
|
-
'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
|
|
128
|
-
'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
|
|
129
|
-
'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
|
|
130
|
-
'80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
|
|
131
|
-
'811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro',
|
|
132
|
-
32,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
59
|
const STYLES: Record<string, any> = {
|
|
136
60
|
strokeStyle: {
|
|
137
61
|
svgAttr: 'stroke',
|
|
@@ -145,130 +69,49 @@ const STYLES: Record<string, any> = {
|
|
|
145
69
|
svg: null,
|
|
146
70
|
apply: 'fill',
|
|
147
71
|
},
|
|
148
|
-
lineCap: {
|
|
149
|
-
svgAttr: 'stroke-linecap',
|
|
150
|
-
canvas: 'butt',
|
|
151
|
-
svg: 'butt',
|
|
152
|
-
apply: 'stroke',
|
|
153
|
-
},
|
|
154
|
-
lineJoin: {
|
|
155
|
-
svgAttr: 'stroke-linejoin',
|
|
156
|
-
canvas: 'miter',
|
|
157
|
-
svg: 'miter',
|
|
158
|
-
apply: 'stroke',
|
|
159
|
-
},
|
|
160
|
-
miterLimit: {
|
|
161
|
-
svgAttr: 'stroke-miterlimit',
|
|
162
|
-
canvas: 10,
|
|
163
|
-
svg: 4,
|
|
164
|
-
apply: 'stroke',
|
|
165
|
-
},
|
|
166
72
|
lineWidth: {
|
|
167
73
|
svgAttr: 'stroke-width',
|
|
168
74
|
canvas: 1,
|
|
169
75
|
svg: 1,
|
|
170
76
|
apply: 'stroke',
|
|
171
77
|
},
|
|
172
|
-
globalAlpha: {
|
|
173
|
-
svgAttr: 'opacity',
|
|
174
|
-
canvas: 1,
|
|
175
|
-
svg: 1,
|
|
176
|
-
apply: 'fill stroke',
|
|
177
|
-
},
|
|
178
78
|
font: {
|
|
179
79
|
canvas: '10px sans-serif',
|
|
180
80
|
},
|
|
181
|
-
shadowColor: {
|
|
182
|
-
canvas: '#000000',
|
|
183
|
-
},
|
|
184
|
-
shadowOffsetX: {
|
|
185
|
-
canvas: 0,
|
|
186
|
-
},
|
|
187
|
-
shadowOffsetY: {
|
|
188
|
-
canvas: 0,
|
|
189
|
-
},
|
|
190
|
-
shadowBlur: {
|
|
191
|
-
canvas: 0,
|
|
192
|
-
},
|
|
193
81
|
textAlign: {
|
|
194
82
|
canvas: 'start',
|
|
195
83
|
},
|
|
196
84
|
textBaseline: {
|
|
197
85
|
canvas: 'alphabetic',
|
|
198
86
|
},
|
|
87
|
+
// null rather than an empty array: the canvas default is "no dashes", which is
|
|
88
|
+
// also the svg default, and the two have to compare equal or every stroke
|
|
89
|
+
// carries an empty stroke-dasharray
|
|
199
90
|
lineDash: {
|
|
200
91
|
svgAttr: 'stroke-dasharray',
|
|
201
|
-
canvas:
|
|
92
|
+
canvas: null,
|
|
202
93
|
svg: null,
|
|
203
94
|
apply: 'stroke',
|
|
204
95
|
},
|
|
205
96
|
}
|
|
206
97
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
__ctx: Context
|
|
210
|
-
|
|
211
|
-
constructor(gradientNode: SVGElement, ctx: Context) {
|
|
212
|
-
this.__root = gradientNode
|
|
213
|
-
this.__ctx = ctx
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
addColorStop(offset: number, color: string) {
|
|
217
|
-
const stop = this.__ctx.__createElement('stop')
|
|
218
|
-
stop.setAttribute('offset', String(offset))
|
|
219
|
-
if (toString(color).includes('rgba')) {
|
|
220
|
-
const regex =
|
|
221
|
-
/rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi
|
|
222
|
-
const matches = regex.exec(color)
|
|
223
|
-
if (matches) {
|
|
224
|
-
stop.setAttribute(
|
|
225
|
-
'stop-color',
|
|
226
|
-
format('rgb({r},{g},{b})', {
|
|
227
|
-
r: matches[1],
|
|
228
|
-
g: matches[2],
|
|
229
|
-
b: matches[3],
|
|
230
|
-
}),
|
|
231
|
-
)
|
|
232
|
-
stop.setAttribute('stop-opacity', matches[4]!)
|
|
233
|
-
}
|
|
234
|
-
} else {
|
|
235
|
-
stop.setAttribute('stop-color', toString(color))
|
|
236
|
-
}
|
|
237
|
-
this.__root.appendChild(stop)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
class CanvasPattern {
|
|
242
|
-
__root: SVGElement
|
|
243
|
-
__ctx: Context
|
|
244
|
-
|
|
245
|
-
constructor(pattern: SVGElement, ctx: Context) {
|
|
246
|
-
this.__root = pattern
|
|
247
|
-
this.__ctx = ctx
|
|
248
|
-
}
|
|
249
|
-
}
|
|
98
|
+
const rgbaRegex =
|
|
99
|
+
/rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/i
|
|
250
100
|
|
|
251
101
|
interface ContextOptions {
|
|
252
|
-
width?: number
|
|
253
|
-
height?: number
|
|
254
|
-
enableMirroring?: boolean
|
|
255
102
|
document?: Document
|
|
256
103
|
ctx?: CanvasRenderingContext2D
|
|
257
|
-
debug?: boolean
|
|
258
104
|
}
|
|
259
105
|
|
|
260
106
|
export class Context {
|
|
261
107
|
width: number
|
|
262
108
|
height: number
|
|
263
|
-
enableMirroring: boolean
|
|
264
109
|
canvas: Context
|
|
265
110
|
|
|
266
111
|
__document: Document
|
|
267
112
|
__ctx: CanvasRenderingContext2D
|
|
268
113
|
__canvas?: HTMLCanvasElement
|
|
269
114
|
__root: SVGSVGElement
|
|
270
|
-
__ids: Record<string, string>
|
|
271
|
-
__defs: SVGDefsElement
|
|
272
115
|
__currentElement: SVGElement
|
|
273
116
|
__styleStack: any[]
|
|
274
117
|
__groupStack: SVGElement[]
|
|
@@ -276,50 +119,22 @@ export class Context {
|
|
|
276
119
|
__currentPosition: { x?: number; y?: number }
|
|
277
120
|
__transformMatrix: DOMMatrix
|
|
278
121
|
__transformMatrixStack?: DOMMatrix[]
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
__fontUnderline?: string
|
|
283
|
-
__fontHref?: string
|
|
122
|
+
// the parsed font, keyed by the `font` string that produced it: parsing costs
|
|
123
|
+
// a DOM element and a CSS parse, and a text-heavy export asks per glyph
|
|
124
|
+
__fontCache = new Map<string, CSSStyleDeclaration>()
|
|
284
125
|
|
|
285
126
|
// Style properties
|
|
286
127
|
strokeStyle: any
|
|
287
128
|
fillStyle: any
|
|
288
|
-
lineCap: any
|
|
289
|
-
lineJoin: any
|
|
290
|
-
miterLimit: any
|
|
291
129
|
lineWidth: any
|
|
292
|
-
globalAlpha: any
|
|
293
130
|
font: any
|
|
294
|
-
shadowColor: any
|
|
295
|
-
shadowOffsetX: any
|
|
296
|
-
shadowOffsetY: any
|
|
297
|
-
shadowBlur: any
|
|
298
131
|
textAlign: any
|
|
299
132
|
textBaseline: any
|
|
300
133
|
lineDash: any
|
|
301
134
|
|
|
302
|
-
constructor(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (typeof o === 'number' && height !== undefined) {
|
|
307
|
-
options = { ...defaultOptions, width: o, height }
|
|
308
|
-
} else if (!o) {
|
|
309
|
-
options = defaultOptions
|
|
310
|
-
} else if (typeof o === 'object') {
|
|
311
|
-
options = o
|
|
312
|
-
} else {
|
|
313
|
-
options = defaultOptions
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
this.width = options.width || defaultOptions.width
|
|
317
|
-
this.height = options.height || defaultOptions.height
|
|
318
|
-
this.enableMirroring =
|
|
319
|
-
options.enableMirroring !== undefined
|
|
320
|
-
? options.enableMirroring
|
|
321
|
-
: defaultOptions.enableMirroring
|
|
322
|
-
|
|
135
|
+
constructor(width: number, height: number, options: ContextOptions = {}) {
|
|
136
|
+
this.width = width
|
|
137
|
+
this.height = height
|
|
323
138
|
this.canvas = this
|
|
324
139
|
this.__document = options.document || document
|
|
325
140
|
|
|
@@ -340,21 +155,8 @@ export class Context {
|
|
|
340
155
|
)
|
|
341
156
|
this.__root.setAttribute('version', '1.1')
|
|
342
157
|
this.__root.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
|
343
|
-
this.__root.
|
|
344
|
-
|
|
345
|
-
'xmlns:xlink',
|
|
346
|
-
'http://www.w3.org/1999/xlink',
|
|
347
|
-
)
|
|
348
|
-
this.__root.setAttribute('width', String(this.width))
|
|
349
|
-
this.__root.setAttribute('height', String(this.height))
|
|
350
|
-
|
|
351
|
-
this.__ids = {}
|
|
352
|
-
|
|
353
|
-
this.__defs = this.__document.createElementNS(
|
|
354
|
-
'http://www.w3.org/2000/svg',
|
|
355
|
-
'defs',
|
|
356
|
-
)
|
|
357
|
-
this.__root.appendChild(this.__defs)
|
|
158
|
+
this.__root.setAttribute('width', String(width))
|
|
159
|
+
this.__root.setAttribute('height', String(height))
|
|
358
160
|
|
|
359
161
|
this.__currentElement = this.__document.createElementNS(
|
|
360
162
|
'http://www.w3.org/2000/svg',
|
|
@@ -367,9 +169,6 @@ export class Context {
|
|
|
367
169
|
this.__transformMatrix = new DOMMatrix()
|
|
368
170
|
|
|
369
171
|
this.resetTransform()
|
|
370
|
-
|
|
371
|
-
this.__options = options
|
|
372
|
-
this.__id = Math.random().toString(16).substring(2, 8)
|
|
373
172
|
}
|
|
374
173
|
|
|
375
174
|
__createElement(
|
|
@@ -382,13 +181,16 @@ export class Context {
|
|
|
382
181
|
elementName,
|
|
383
182
|
)
|
|
384
183
|
if (resetFill) {
|
|
184
|
+
// a placeholder: whichever of fill()/stroke() paints this element
|
|
185
|
+
// overwrites its own, and __applyStyleToCurrentElement drops the other
|
|
186
|
+
// when it is the svg default anyway
|
|
385
187
|
element.setAttribute('fill', 'none')
|
|
386
188
|
element.setAttribute('stroke', 'none')
|
|
387
189
|
}
|
|
388
190
|
for (const key of Object.keys(properties)) {
|
|
389
191
|
// an unset property means "no such attribute", not the string
|
|
390
|
-
// "undefined". __applyText passes
|
|
391
|
-
// so without this every glyph in an export carries
|
|
192
|
+
// "undefined". __applyText passes optional attributes through
|
|
193
|
+
// unconditionally, so without this every glyph in an export carries
|
|
392
194
|
// text-decoration="undefined" -- invalid, and ~28 wasted bytes per letter
|
|
393
195
|
const value = properties[key]
|
|
394
196
|
if (value !== undefined && value !== null) {
|
|
@@ -418,91 +220,71 @@ export class Context {
|
|
|
418
220
|
return styleState
|
|
419
221
|
}
|
|
420
222
|
|
|
223
|
+
/**
|
|
224
|
+
* A translation is what an element's own x/y already say, and repeating it as
|
|
225
|
+
* a matrix costs ~35 bytes on every glyph of an alignment. Anything else --
|
|
226
|
+
* the scale a sequence-logo letter is stretched by -- still needs the matrix.
|
|
227
|
+
*/
|
|
421
228
|
__applyTransformation(element: SVGElement, matrix?: DOMMatrix) {
|
|
422
229
|
const { a, b, c, d, e, f } = matrix || this.getTransform()
|
|
230
|
+
if (
|
|
231
|
+
a === 1 &&
|
|
232
|
+
b === 0 &&
|
|
233
|
+
c === 0 &&
|
|
234
|
+
d === 1 &&
|
|
235
|
+
element.hasAttribute('x') &&
|
|
236
|
+
element.hasAttribute('y')
|
|
237
|
+
) {
|
|
238
|
+
if (e !== 0) {
|
|
239
|
+
element.setAttribute('x', String(Number(element.getAttribute('x')) + e))
|
|
240
|
+
}
|
|
241
|
+
if (f !== 0) {
|
|
242
|
+
element.setAttribute('y', String(Number(element.getAttribute('y')) + f))
|
|
243
|
+
}
|
|
244
|
+
return
|
|
245
|
+
}
|
|
423
246
|
element.setAttribute('transform', `matrix(${a} ${b} ${c} ${d} ${e} ${f})`)
|
|
424
247
|
}
|
|
425
248
|
|
|
426
249
|
__applyStyleToCurrentElement(type: string) {
|
|
427
|
-
|
|
428
|
-
const currentStyleGroup = this.__currentElementsToStyle
|
|
429
|
-
if (currentStyleGroup) {
|
|
430
|
-
currentElement.setAttribute(type, '')
|
|
431
|
-
currentElement = currentStyleGroup.element
|
|
432
|
-
for (const node of currentStyleGroup.children) {
|
|
433
|
-
node.setAttribute(type, '')
|
|
434
|
-
}
|
|
435
|
-
}
|
|
250
|
+
const currentElement = this.__currentElement
|
|
436
251
|
|
|
437
|
-
const
|
|
438
|
-
for (const key of keys) {
|
|
252
|
+
for (const key of Object.keys(STYLES)) {
|
|
439
253
|
const style = STYLES[key]
|
|
440
254
|
const value = (this as any)[key]
|
|
441
|
-
if (style.apply) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
value.includes('rgba')
|
|
466
|
-
) {
|
|
467
|
-
const regex =
|
|
468
|
-
/rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi
|
|
469
|
-
const matches = regex.exec(value)
|
|
470
|
-
if (matches) {
|
|
471
|
-
currentElement.setAttribute(
|
|
472
|
-
style.svgAttr,
|
|
473
|
-
format('rgb({r},{g},{b})', {
|
|
474
|
-
r: matches[1],
|
|
475
|
-
g: matches[2],
|
|
476
|
-
b: matches[3],
|
|
477
|
-
}),
|
|
478
|
-
)
|
|
479
|
-
let opacity = Number(matches[4])
|
|
480
|
-
const globalAlpha = this.globalAlpha
|
|
481
|
-
if (globalAlpha != null) {
|
|
482
|
-
opacity *= globalAlpha
|
|
483
|
-
}
|
|
484
|
-
currentElement.setAttribute(
|
|
485
|
-
style.svgAttr + '-opacity',
|
|
486
|
-
String(opacity),
|
|
487
|
-
)
|
|
488
|
-
}
|
|
489
|
-
} else {
|
|
490
|
-
let attr = style.svgAttr
|
|
491
|
-
let val = value
|
|
492
|
-
if (key === 'globalAlpha') {
|
|
493
|
-
attr = type + '-' + style.svgAttr
|
|
494
|
-
if (currentElement.getAttribute(attr)) {
|
|
495
|
-
continue
|
|
496
|
-
}
|
|
497
|
-
} else if (key === 'lineWidth') {
|
|
498
|
-
const scale = this.__getTransformScale()
|
|
499
|
-
val = value * Math.max(scale.x, scale.y)
|
|
500
|
-
}
|
|
501
|
-
currentElement.setAttribute(attr, val)
|
|
502
|
-
}
|
|
503
|
-
}
|
|
255
|
+
if (!style.apply?.includes(type) || style.svg === value) {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
const matches =
|
|
259
|
+
(style.svgAttr === 'stroke' || style.svgAttr === 'fill') &&
|
|
260
|
+
typeof value === 'string' &&
|
|
261
|
+
value.includes('rgba')
|
|
262
|
+
? rgbaRegex.exec(value)
|
|
263
|
+
: null
|
|
264
|
+
if (matches) {
|
|
265
|
+
// SVG has no rgba(): the alpha belongs in its own -opacity attribute
|
|
266
|
+
currentElement.setAttribute(
|
|
267
|
+
style.svgAttr,
|
|
268
|
+
`rgb(${matches[1]},${matches[2]},${matches[3]})`,
|
|
269
|
+
)
|
|
270
|
+
currentElement.setAttribute(`${style.svgAttr}-opacity`, matches[4]!)
|
|
271
|
+
} else if (key === 'lineWidth') {
|
|
272
|
+
const scale = this.__getTransformScale()
|
|
273
|
+
currentElement.setAttribute(
|
|
274
|
+
style.svgAttr,
|
|
275
|
+
String(value * Math.max(scale.x, scale.y)),
|
|
276
|
+
)
|
|
277
|
+
} else {
|
|
278
|
+
currentElement.setAttribute(style.svgAttr, value)
|
|
504
279
|
}
|
|
505
280
|
}
|
|
281
|
+
|
|
282
|
+
// a filled element with no stroke does not have to say so: `none` is what
|
|
283
|
+
// svg assumes. The reverse is not true -- an unstroked fill defaults to
|
|
284
|
+
// black -- so a stroked element keeps its fill="none"
|
|
285
|
+
if (type === 'fill' && currentElement.getAttribute('stroke') === 'none') {
|
|
286
|
+
currentElement.removeAttribute('stroke')
|
|
287
|
+
}
|
|
506
288
|
}
|
|
507
289
|
|
|
508
290
|
__closestGroupOrSvg(node?: SVGElement): SVGElement {
|
|
@@ -513,29 +295,6 @@ export class Context {
|
|
|
513
295
|
return this.__closestGroupOrSvg(node.parentNode as SVGElement)
|
|
514
296
|
}
|
|
515
297
|
|
|
516
|
-
getSerializedSvg(fixNamedEntities?: boolean): string {
|
|
517
|
-
let serialized = new XMLSerializer().serializeToString(this.__root)
|
|
518
|
-
const xmlns =
|
|
519
|
-
/xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi
|
|
520
|
-
if (xmlns.test(serialized)) {
|
|
521
|
-
serialized = serialized.replace(
|
|
522
|
-
'xmlns="http://www.w3.org/2000/svg',
|
|
523
|
-
'xmlns:xlink="http://www.w3.org/1999/xlink',
|
|
524
|
-
)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (fixNamedEntities) {
|
|
528
|
-
for (const key of Object.keys(namedEntities)) {
|
|
529
|
-
const regexp = new RegExp(key, 'gi')
|
|
530
|
-
if (regexp.test(serialized)) {
|
|
531
|
-
serialized = serialized.replace(regexp, namedEntities[key]!)
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
return serialized
|
|
537
|
-
}
|
|
538
|
-
|
|
539
298
|
getSvg(): SVGSVGElement {
|
|
540
299
|
return this.__root
|
|
541
300
|
}
|
|
@@ -546,8 +305,7 @@ export class Context {
|
|
|
546
305
|
this.__groupStack.push(parent)
|
|
547
306
|
parent.appendChild(group)
|
|
548
307
|
this.__currentElement = group
|
|
549
|
-
|
|
550
|
-
this.__styleStack.push(style)
|
|
308
|
+
this.__styleStack.push(this.__getStyleState())
|
|
551
309
|
if (!this.__transformMatrixStack) {
|
|
552
310
|
this.__transformMatrixStack = []
|
|
553
311
|
}
|
|
@@ -556,12 +314,10 @@ export class Context {
|
|
|
556
314
|
|
|
557
315
|
restore() {
|
|
558
316
|
this.__currentElement = this.__groupStack.pop()!
|
|
559
|
-
this.__currentElementsToStyle = undefined
|
|
560
317
|
if (!this.__currentElement) {
|
|
561
|
-
this.__currentElement = this.__root.childNodes[
|
|
318
|
+
this.__currentElement = this.__root.childNodes[0] as SVGElement
|
|
562
319
|
}
|
|
563
|
-
|
|
564
|
-
this.__applyStyleState(state)
|
|
320
|
+
this.__applyStyleState(this.__styleStack.pop())
|
|
565
321
|
if (this.__transformMatrixStack && this.__transformMatrixStack.length > 0) {
|
|
566
322
|
this.setTransform(this.__transformMatrixStack.pop()!)
|
|
567
323
|
}
|
|
@@ -571,15 +327,13 @@ export class Context {
|
|
|
571
327
|
this.__currentDefaultPath = ''
|
|
572
328
|
this.__currentPosition = {}
|
|
573
329
|
const path = this.__createElement('path', {}, true)
|
|
574
|
-
|
|
575
|
-
parent.appendChild(path)
|
|
330
|
+
this.__closestGroupOrSvg().appendChild(path)
|
|
576
331
|
this.__currentElement = path
|
|
577
332
|
}
|
|
578
333
|
|
|
579
334
|
__applyCurrentDefaultPath() {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
currentElement.setAttribute('d', this.__currentDefaultPath)
|
|
335
|
+
if (this.__currentElement.nodeName === 'path') {
|
|
336
|
+
this.__currentElement.setAttribute('d', this.__currentDefaultPath)
|
|
583
337
|
}
|
|
584
338
|
}
|
|
585
339
|
|
|
@@ -593,12 +347,8 @@ export class Context {
|
|
|
593
347
|
this.beginPath()
|
|
594
348
|
}
|
|
595
349
|
this.__currentPosition = { x, y }
|
|
596
|
-
this.
|
|
597
|
-
|
|
598
|
-
x: this.__matrixTransform(x, y).x,
|
|
599
|
-
y: this.__matrixTransform(x, y).y,
|
|
600
|
-
}),
|
|
601
|
-
)
|
|
350
|
+
const p = this.__matrixTransform(x, y)
|
|
351
|
+
this.__addPathCommand(`M ${p.x} ${p.y}`)
|
|
602
352
|
}
|
|
603
353
|
|
|
604
354
|
closePath() {
|
|
@@ -609,112 +359,53 @@ export class Context {
|
|
|
609
359
|
|
|
610
360
|
lineTo(x: number, y: number) {
|
|
611
361
|
this.__currentPosition = { x, y }
|
|
612
|
-
|
|
613
|
-
this.__addPathCommand(
|
|
614
|
-
format('L {x} {y}', {
|
|
615
|
-
x: this.__matrixTransform(x, y).x,
|
|
616
|
-
y: this.__matrixTransform(x, y).y,
|
|
617
|
-
}),
|
|
618
|
-
)
|
|
619
|
-
} else {
|
|
620
|
-
this.__addPathCommand(
|
|
621
|
-
format('M {x} {y}', {
|
|
622
|
-
x: this.__matrixTransform(x, y).x,
|
|
623
|
-
y: this.__matrixTransform(x, y).y,
|
|
624
|
-
}),
|
|
625
|
-
)
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
bezierCurveTo(
|
|
630
|
-
cp1x: number,
|
|
631
|
-
cp1y: number,
|
|
632
|
-
cp2x: number,
|
|
633
|
-
cp2y: number,
|
|
634
|
-
x: number,
|
|
635
|
-
y: number,
|
|
636
|
-
) {
|
|
637
|
-
this.__currentPosition = { x, y }
|
|
362
|
+
const p = this.__matrixTransform(x, y)
|
|
638
363
|
this.__addPathCommand(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
cp2x: this.__matrixTransform(cp2x, cp2y).x,
|
|
643
|
-
cp2y: this.__matrixTransform(cp2x, cp2y).y,
|
|
644
|
-
x: this.__matrixTransform(x, y).x,
|
|
645
|
-
y: this.__matrixTransform(x, y).y,
|
|
646
|
-
}),
|
|
364
|
+
this.__currentDefaultPath.includes('M')
|
|
365
|
+
? `L ${p.x} ${p.y}`
|
|
366
|
+
: `M ${p.x} ${p.y}`,
|
|
647
367
|
)
|
|
648
368
|
}
|
|
649
369
|
|
|
650
370
|
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) {
|
|
651
371
|
this.__currentPosition = { x, y }
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
cpy: this.__matrixTransform(cpx, cpy).y,
|
|
656
|
-
x: this.__matrixTransform(x, y).x,
|
|
657
|
-
y: this.__matrixTransform(x, y).y,
|
|
658
|
-
}),
|
|
659
|
-
)
|
|
372
|
+
const cp = this.__matrixTransform(cpx, cpy)
|
|
373
|
+
const p = this.__matrixTransform(x, y)
|
|
374
|
+
this.__addPathCommand(`Q ${cp.x} ${cp.y} ${p.x} ${p.y}`)
|
|
660
375
|
}
|
|
661
376
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
377
|
+
// paint-order only says something when the element carries both a fill and a
|
|
378
|
+
// stroke, which is the collapsed-clade triangle and nothing else
|
|
379
|
+
__applyPaintOrder(order: string, other: string) {
|
|
380
|
+
const current = this.__currentElement
|
|
381
|
+
if (
|
|
382
|
+
current.nodeName === 'path' &&
|
|
383
|
+
current.hasAttribute(other) &&
|
|
384
|
+
current.getAttribute(other) !== 'none'
|
|
385
|
+
) {
|
|
386
|
+
current.setAttribute('paint-order', order)
|
|
665
387
|
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
stroke() {
|
|
391
|
+
this.__applyPaintOrder('fill stroke markers', 'fill')
|
|
666
392
|
this.__applyCurrentDefaultPath()
|
|
667
393
|
this.__applyStyleToCurrentElement('stroke')
|
|
668
394
|
}
|
|
669
395
|
|
|
670
396
|
fill() {
|
|
671
|
-
|
|
672
|
-
this.__currentElement.setAttribute('paint-order', 'stroke fill markers')
|
|
673
|
-
}
|
|
397
|
+
this.__applyPaintOrder('stroke fill markers', 'stroke')
|
|
674
398
|
this.__applyCurrentDefaultPath()
|
|
675
399
|
this.__applyStyleToCurrentElement('fill')
|
|
676
400
|
}
|
|
677
401
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
this.moveTo(x, y)
|
|
683
|
-
this.lineTo(x + width, y)
|
|
684
|
-
this.lineTo(x + width, y + height)
|
|
685
|
-
this.lineTo(x, y + height)
|
|
686
|
-
this.lineTo(x, y)
|
|
687
|
-
this.closePath()
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
__clearCanvas() {
|
|
691
|
-
const rootGroup = this.__root.childNodes[1]!
|
|
692
|
-
this.__root.removeChild(rootGroup)
|
|
693
|
-
this.__currentElement = this.__document.createElementNS(
|
|
694
|
-
'http://www.w3.org/2000/svg',
|
|
695
|
-
'g',
|
|
696
|
-
)
|
|
697
|
-
this.__root.appendChild(this.__currentElement)
|
|
698
|
-
this.__groupStack = []
|
|
699
|
-
}
|
|
700
|
-
|
|
402
|
+
// A fill that happens to cover the context is still a fill: it paints over
|
|
403
|
+
// what is under it, exactly as the canvas would. Treating it as a clear threw
|
|
404
|
+
// the drawing away instead -- a full-width highlight band over an alignment
|
|
405
|
+
// took the export from 124 letters to 4.
|
|
701
406
|
fillRect(x: number, y: number, width: number, height: number) {
|
|
702
|
-
const { a, b, c, d, e, f } = this.getTransform()
|
|
703
|
-
if (
|
|
704
|
-
JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])
|
|
705
|
-
) {
|
|
706
|
-
if (
|
|
707
|
-
x === 0 &&
|
|
708
|
-
y === 0 &&
|
|
709
|
-
width === this.width &&
|
|
710
|
-
height === this.height
|
|
711
|
-
) {
|
|
712
|
-
this.__clearCanvas()
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
407
|
const rect = this.__createElement('rect', { x, y, width, height }, true)
|
|
716
|
-
|
|
717
|
-
parent.appendChild(rect)
|
|
408
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
718
409
|
this.__currentElement = rect
|
|
719
410
|
this.__applyTransformation(rect)
|
|
720
411
|
this.__applyStyleToCurrentElement('fill')
|
|
@@ -722,8 +413,7 @@ export class Context {
|
|
|
722
413
|
|
|
723
414
|
strokeRect(x: number, y: number, width: number, height: number) {
|
|
724
415
|
const rect = this.__createElement('rect', { x, y, width, height }, true)
|
|
725
|
-
|
|
726
|
-
parent.appendChild(rect)
|
|
416
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
727
417
|
this.__currentElement = rect
|
|
728
418
|
this.__applyTransformation(rect)
|
|
729
419
|
this.__applyStyleToCurrentElement('stroke')
|
|
@@ -732,17 +422,19 @@ export class Context {
|
|
|
732
422
|
clearRect(x: number, y: number, width: number, height: number) {
|
|
733
423
|
const { a, b, c, d, e, f } = this.getTransform()
|
|
734
424
|
if (
|
|
735
|
-
|
|
425
|
+
a === 1 &&
|
|
426
|
+
b === 0 &&
|
|
427
|
+
c === 0 &&
|
|
428
|
+
d === 1 &&
|
|
429
|
+
e === 0 &&
|
|
430
|
+
f === 0 &&
|
|
431
|
+
x === 0 &&
|
|
432
|
+
y === 0 &&
|
|
433
|
+
width === this.width &&
|
|
434
|
+
height === this.height
|
|
736
435
|
) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
y === 0 &&
|
|
740
|
-
width === this.width &&
|
|
741
|
-
height === this.height
|
|
742
|
-
) {
|
|
743
|
-
this.__clearCanvas()
|
|
744
|
-
return
|
|
745
|
-
}
|
|
436
|
+
this.__clearCanvas()
|
|
437
|
+
return
|
|
746
438
|
}
|
|
747
439
|
const rect = this.__createElement(
|
|
748
440
|
'rect',
|
|
@@ -750,54 +442,32 @@ export class Context {
|
|
|
750
442
|
true,
|
|
751
443
|
)
|
|
752
444
|
this.__applyTransformation(rect)
|
|
753
|
-
|
|
754
|
-
parent.appendChild(rect)
|
|
445
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
755
446
|
}
|
|
756
447
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
x1: x1 + 'px',
|
|
766
|
-
x2: x2 + 'px',
|
|
767
|
-
y1: y1 + 'px',
|
|
768
|
-
y2: y2 + 'px',
|
|
769
|
-
gradientUnits: 'userSpaceOnUse',
|
|
770
|
-
})
|
|
771
|
-
this.__defs.appendChild(grad)
|
|
772
|
-
return new CanvasGradient(grad, this)
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
createRadialGradient(
|
|
776
|
-
x0: number,
|
|
777
|
-
y0: number,
|
|
778
|
-
r0: number,
|
|
779
|
-
x1: number,
|
|
780
|
-
y1: number,
|
|
781
|
-
r1: number,
|
|
782
|
-
): CanvasGradient {
|
|
783
|
-
const grad = this.__createElement('radialGradient', {
|
|
784
|
-
id: randomString(this.__ids),
|
|
785
|
-
cx: x1 + 'px',
|
|
786
|
-
cy: y1 + 'px',
|
|
787
|
-
r: r1 + 'px',
|
|
788
|
-
fx: x0 + 'px',
|
|
789
|
-
fy: y0 + 'px',
|
|
790
|
-
gradientUnits: 'userSpaceOnUse',
|
|
791
|
-
})
|
|
792
|
-
this.__defs.appendChild(grad)
|
|
793
|
-
return new CanvasGradient(grad, this)
|
|
448
|
+
__clearCanvas() {
|
|
449
|
+
this.__root.removeChild(this.__root.childNodes[0]!)
|
|
450
|
+
this.__currentElement = this.__document.createElementNS(
|
|
451
|
+
'http://www.w3.org/2000/svg',
|
|
452
|
+
'g',
|
|
453
|
+
)
|
|
454
|
+
this.__root.appendChild(this.__currentElement)
|
|
455
|
+
this.__groupStack = []
|
|
794
456
|
}
|
|
795
457
|
|
|
796
|
-
|
|
458
|
+
__parsedFont() {
|
|
459
|
+
const hit = this.__fontCache.get(this.font)
|
|
460
|
+
if (hit) {
|
|
461
|
+
return hit
|
|
462
|
+
}
|
|
797
463
|
const el = document.createElement('span')
|
|
798
|
-
el.setAttribute('style',
|
|
464
|
+
el.setAttribute('style', `font:${this.font}`)
|
|
465
|
+
this.__fontCache.set(this.font, el.style)
|
|
466
|
+
return el.style
|
|
467
|
+
}
|
|
799
468
|
|
|
800
|
-
|
|
469
|
+
fillText(text: string, x: number, y: number) {
|
|
470
|
+
const style = this.__parsedFont()
|
|
801
471
|
const parent = this.__closestGroupOrSvg()
|
|
802
472
|
const textElement = this.__createElement(
|
|
803
473
|
'text',
|
|
@@ -806,9 +476,8 @@ export class Context {
|
|
|
806
476
|
'font-size': style.fontSize,
|
|
807
477
|
'font-style': omitDefault('font-style', style.fontStyle),
|
|
808
478
|
'font-weight': omitDefault('font-weight', style.fontWeight),
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
y: y,
|
|
479
|
+
x,
|
|
480
|
+
y,
|
|
812
481
|
'text-anchor': getTextAnchor(this.textAlign),
|
|
813
482
|
'dominant-baseline': omitDefault(
|
|
814
483
|
'dominant-baseline',
|
|
@@ -821,29 +490,8 @@ export class Context {
|
|
|
821
490
|
textElement.appendChild(this.__document.createTextNode(text))
|
|
822
491
|
this.__currentElement = textElement
|
|
823
492
|
this.__applyTransformation(textElement)
|
|
824
|
-
this.__applyStyleToCurrentElement(
|
|
825
|
-
|
|
826
|
-
let finalElement: SVGElement = textElement
|
|
827
|
-
if (this.__fontHref) {
|
|
828
|
-
const a = this.__createElement('a')
|
|
829
|
-
a.setAttributeNS(
|
|
830
|
-
'http://www.w3.org/1999/xlink',
|
|
831
|
-
'xlink:href',
|
|
832
|
-
this.__fontHref,
|
|
833
|
-
)
|
|
834
|
-
a.appendChild(textElement)
|
|
835
|
-
finalElement = a
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
parent.appendChild(finalElement)
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
fillText(text: string, x: number, y: number) {
|
|
842
|
-
this.__applyText(text, x, y, 'fill')
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
strokeText(text: string, x: number, y: number) {
|
|
846
|
-
this.__applyText(text, x, y, 'stroke')
|
|
493
|
+
this.__applyStyleToCurrentElement('fill')
|
|
494
|
+
parent.appendChild(textElement)
|
|
847
495
|
}
|
|
848
496
|
|
|
849
497
|
measureText(text: string): TextMetrics {
|
|
@@ -874,71 +522,30 @@ export class Context {
|
|
|
874
522
|
const startX = x + radius * Math.cos(startAngle)
|
|
875
523
|
const startY = y + radius * Math.sin(startAngle)
|
|
876
524
|
const sweepFlag = counterClockwise ? 0 : 1
|
|
877
|
-
let largeArcFlag = 0
|
|
878
525
|
let diff = endAngle - startAngle
|
|
879
|
-
|
|
880
526
|
if (diff < 0) {
|
|
881
527
|
diff += 2 * Math.PI
|
|
882
528
|
}
|
|
529
|
+
const largeArcFlag = counterClockwise
|
|
530
|
+
? diff > Math.PI
|
|
531
|
+
? 0
|
|
532
|
+
: 1
|
|
533
|
+
: diff > Math.PI
|
|
534
|
+
? 1
|
|
535
|
+
: 0
|
|
883
536
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
} else {
|
|
887
|
-
largeArcFlag = diff > Math.PI ? 1 : 0
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const scaleX = Math.hypot(
|
|
891
|
-
this.__transformMatrix.a,
|
|
892
|
-
this.__transformMatrix.b,
|
|
893
|
-
)
|
|
894
|
-
const scaleY = Math.hypot(
|
|
895
|
-
this.__transformMatrix.c,
|
|
896
|
-
this.__transformMatrix.d,
|
|
897
|
-
)
|
|
537
|
+
const { x: scaleX, y: scaleY } = this.__getTransformScale()
|
|
538
|
+
const end = this.__matrixTransform(endX, endY)
|
|
898
539
|
|
|
899
540
|
this.lineTo(startX, startY)
|
|
900
541
|
this.__addPathCommand(
|
|
901
|
-
|
|
902
|
-
'A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}',
|
|
903
|
-
{
|
|
904
|
-
rx: radius * scaleX,
|
|
905
|
-
ry: radius * scaleY,
|
|
906
|
-
xAxisRotation: 0,
|
|
907
|
-
largeArcFlag,
|
|
908
|
-
sweepFlag,
|
|
909
|
-
endX: this.__matrixTransform(endX, endY).x,
|
|
910
|
-
endY: this.__matrixTransform(endX, endY).y,
|
|
911
|
-
},
|
|
912
|
-
),
|
|
542
|
+
`A ${radius * scaleX} ${radius * scaleY} 0 ${largeArcFlag} ${sweepFlag} ${end.x} ${end.y}`,
|
|
913
543
|
)
|
|
914
|
-
|
|
915
544
|
this.__currentPosition = { x: endX, y: endY }
|
|
916
545
|
}
|
|
917
546
|
|
|
918
|
-
clip() {
|
|
919
|
-
const group = this.__closestGroupOrSvg()
|
|
920
|
-
const clipPath = this.__createElement('clipPath')
|
|
921
|
-
const id = randomString(this.__ids)
|
|
922
|
-
const newGroup = this.__createElement('g')
|
|
923
|
-
|
|
924
|
-
this.__applyCurrentDefaultPath()
|
|
925
|
-
group.removeChild(this.__currentElement)
|
|
926
|
-
clipPath.setAttribute('id', id)
|
|
927
|
-
clipPath.appendChild(this.__currentElement)
|
|
928
|
-
|
|
929
|
-
this.__defs.appendChild(clipPath)
|
|
930
|
-
group.setAttribute('clip-path', format('url(#{id})', { id }))
|
|
931
|
-
group.appendChild(newGroup)
|
|
932
|
-
|
|
933
|
-
this.__currentElement = newGroup
|
|
934
|
-
}
|
|
935
|
-
|
|
936
547
|
setLineDash(dashArray: number[]) {
|
|
937
|
-
|
|
938
|
-
this.lineDash = dashArray.join(',')
|
|
939
|
-
} else {
|
|
940
|
-
this.lineDash = null
|
|
941
|
-
}
|
|
548
|
+
this.lineDash = dashArray.length > 0 ? dashArray.join(',') : null
|
|
942
549
|
}
|
|
943
550
|
|
|
944
551
|
setTransform(
|
|
@@ -949,11 +556,10 @@ export class Context {
|
|
|
949
556
|
e?: number,
|
|
950
557
|
f?: number,
|
|
951
558
|
) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
559
|
+
this.__transformMatrix =
|
|
560
|
+
a instanceof DOMMatrix
|
|
561
|
+
? new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f])
|
|
562
|
+
: new DOMMatrix([a, b!, c!, d!, e!, f!])
|
|
957
563
|
}
|
|
958
564
|
|
|
959
565
|
getTransform(): DOMMatrix {
|
|
@@ -972,34 +578,11 @@ export class Context {
|
|
|
972
578
|
if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) {
|
|
973
579
|
return
|
|
974
580
|
}
|
|
975
|
-
|
|
976
|
-
this.setTransform(matrix)
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
rotate(angle: number) {
|
|
980
|
-
const matrix = this.getTransform().multiply(
|
|
981
|
-
new DOMMatrix([
|
|
982
|
-
Math.cos(angle),
|
|
983
|
-
Math.sin(angle),
|
|
984
|
-
-Math.sin(angle),
|
|
985
|
-
Math.cos(angle),
|
|
986
|
-
0,
|
|
987
|
-
0,
|
|
988
|
-
]),
|
|
989
|
-
)
|
|
990
|
-
this.setTransform(matrix)
|
|
581
|
+
this.setTransform(this.getTransform().scale(x, y))
|
|
991
582
|
}
|
|
992
583
|
|
|
993
584
|
translate(x: number, y: number) {
|
|
994
|
-
|
|
995
|
-
this.setTransform(matrix)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
transform(a: number, b: number, c: number, d: number, e: number, f: number) {
|
|
999
|
-
const matrix = this.getTransform().multiply(
|
|
1000
|
-
new DOMMatrix([a, b, c, d, e, f]),
|
|
1001
|
-
)
|
|
1002
|
-
this.setTransform(matrix)
|
|
585
|
+
this.setTransform(this.getTransform().translate(x, y))
|
|
1003
586
|
}
|
|
1004
587
|
|
|
1005
588
|
__matrixTransform(x: number, y: number): DOMPoint {
|
|
@@ -1012,72 +595,4 @@ export class Context {
|
|
|
1012
595
|
y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d),
|
|
1013
596
|
}
|
|
1014
597
|
}
|
|
1015
|
-
|
|
1016
|
-
__getTransformRotation(): number {
|
|
1017
|
-
return Math.atan2(this.__transformMatrix.b, this.__transformMatrix.a)
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* An <image> of the source, cropped and scaled the way the canvas call asks:
|
|
1022
|
-
* a nested <svg> whose viewBox is the source rectangle maps it onto the
|
|
1023
|
-
* destination rectangle exactly. Throws rather than dropping the image, so a
|
|
1024
|
-
* renderer that draws one into an export finds out.
|
|
1025
|
-
*/
|
|
1026
|
-
drawImage(image: unknown, ...args: number[]) {
|
|
1027
|
-
const source = image as {
|
|
1028
|
-
width: number
|
|
1029
|
-
height: number
|
|
1030
|
-
toDataURL?: (type?: string) => string
|
|
1031
|
-
}
|
|
1032
|
-
if (typeof source.toDataURL !== 'function') {
|
|
1033
|
-
throw new TypeError(
|
|
1034
|
-
'svgcanvas drawImage needs a source with toDataURL (a canvas); an OffscreenCanvas has to be copied onto one first',
|
|
1035
|
-
)
|
|
1036
|
-
}
|
|
1037
|
-
const [sx, sy, sw, sh, dx, dy, dw, dh] =
|
|
1038
|
-
args.length >= 8
|
|
1039
|
-
? args
|
|
1040
|
-
: [
|
|
1041
|
-
0,
|
|
1042
|
-
0,
|
|
1043
|
-
source.width,
|
|
1044
|
-
source.height,
|
|
1045
|
-
args[0],
|
|
1046
|
-
args[1],
|
|
1047
|
-
args[2] ?? source.width,
|
|
1048
|
-
args[3] ?? source.height,
|
|
1049
|
-
]
|
|
1050
|
-
const group = this.__createElement('g')
|
|
1051
|
-
const viewport = this.__createElement('svg', {
|
|
1052
|
-
x: dx,
|
|
1053
|
-
y: dy,
|
|
1054
|
-
width: dw,
|
|
1055
|
-
height: dh,
|
|
1056
|
-
viewBox: `${sx} ${sy} ${sw} ${sh}`,
|
|
1057
|
-
preserveAspectRatio: 'none',
|
|
1058
|
-
})
|
|
1059
|
-
const img = this.__createElement('image', {
|
|
1060
|
-
href: source.toDataURL('image/png'),
|
|
1061
|
-
width: source.width,
|
|
1062
|
-
height: source.height,
|
|
1063
|
-
preserveAspectRatio: 'none',
|
|
1064
|
-
'image-rendering': this.imageSmoothingEnabled ? undefined : 'pixelated',
|
|
1065
|
-
})
|
|
1066
|
-
viewport.appendChild(img)
|
|
1067
|
-
group.appendChild(viewport)
|
|
1068
|
-
this.__closestGroupOrSvg().appendChild(group)
|
|
1069
|
-
this.__currentElement = group
|
|
1070
|
-
this.__applyTransformation(group)
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
imageSmoothingEnabled = true
|
|
1074
|
-
|
|
1075
|
-
// Stubs for unimplemented methods
|
|
1076
|
-
drawFocusRing() {}
|
|
1077
|
-
createImageData() {}
|
|
1078
|
-
putImageData() {}
|
|
1079
|
-
globalCompositeOperation() {}
|
|
1080
|
-
createPattern() {
|
|
1081
|
-
return null
|
|
1082
|
-
}
|
|
1083
598
|
}
|