@jbrowse/svgcanvas 6.5.0 → 8.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 +12 -56
- package/dist/index.js +149 -531
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/context.test.ts +71 -0
- package/src/index.ts +162 -658
- 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,12 @@
|
|
|
9
9
|
* Copyright (c) 2014 Gliffy Inc.
|
|
10
10
|
* Copyright (c) 2021 Zeno Zeng
|
|
11
11
|
*
|
|
12
|
-
* Vendored
|
|
12
|
+
* Vendored, converted to ESM/TypeScript, and cut down to the calls the
|
|
13
|
+
* renderers make: rectangles, paths, arcs and glyphs.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
17
|
|
|
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
18
|
function getTextAnchor(textAlign: string): string {
|
|
68
19
|
const mapping: Record<string, string> = {
|
|
69
20
|
left: 'start',
|
|
@@ -87,14 +38,12 @@ function getDominantBaseline(textBaseline: string): string {
|
|
|
87
38
|
}
|
|
88
39
|
|
|
89
40
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* dropping the four below takes ~22% off a sequence-logo figure.
|
|
41
|
+
* SVG's initial values for text presentation attributes. Omitting them from
|
|
42
|
+
* every glyph takes ~22% off a sequence-logo figure.
|
|
93
43
|
*/
|
|
94
44
|
const TEXT_ATTR_DEFAULTS: Record<string, string> = {
|
|
95
45
|
'font-style': 'normal',
|
|
96
46
|
'font-weight': 'normal',
|
|
97
|
-
'font-variant': 'normal',
|
|
98
47
|
// the initial value is `auto`, which resolves to the alphabetic baseline
|
|
99
48
|
'dominant-baseline': 'alphabetic',
|
|
100
49
|
}
|
|
@@ -103,35 +52,6 @@ function omitDefault(attr: string, value: string | undefined) {
|
|
|
103
52
|
return value === TEXT_ATTR_DEFAULTS[attr] ? undefined : value
|
|
104
53
|
}
|
|
105
54
|
|
|
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
55
|
const STYLES: Record<string, any> = {
|
|
136
56
|
strokeStyle: {
|
|
137
57
|
svgAttr: 'stroke',
|
|
@@ -145,130 +65,48 @@ const STYLES: Record<string, any> = {
|
|
|
145
65
|
svg: null,
|
|
146
66
|
apply: 'fill',
|
|
147
67
|
},
|
|
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
68
|
lineWidth: {
|
|
167
69
|
svgAttr: 'stroke-width',
|
|
168
70
|
canvas: 1,
|
|
169
71
|
svg: 1,
|
|
170
72
|
apply: 'stroke',
|
|
171
73
|
},
|
|
172
|
-
globalAlpha: {
|
|
173
|
-
svgAttr: 'opacity',
|
|
174
|
-
canvas: 1,
|
|
175
|
-
svg: 1,
|
|
176
|
-
apply: 'fill stroke',
|
|
177
|
-
},
|
|
178
74
|
font: {
|
|
179
75
|
canvas: '10px sans-serif',
|
|
180
76
|
},
|
|
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
77
|
textAlign: {
|
|
194
78
|
canvas: 'start',
|
|
195
79
|
},
|
|
196
80
|
textBaseline: {
|
|
197
81
|
canvas: 'alphabetic',
|
|
198
82
|
},
|
|
83
|
+
// null on both sides so the canvas and svg defaults compare equal and strokes
|
|
84
|
+
// carry no empty stroke-dasharray
|
|
199
85
|
lineDash: {
|
|
200
86
|
svgAttr: 'stroke-dasharray',
|
|
201
|
-
canvas:
|
|
87
|
+
canvas: null,
|
|
202
88
|
svg: null,
|
|
203
89
|
apply: 'stroke',
|
|
204
90
|
},
|
|
205
91
|
}
|
|
206
92
|
|
|
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
|
-
}
|
|
93
|
+
const rgbaRegex =
|
|
94
|
+
/rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/i
|
|
250
95
|
|
|
251
96
|
interface ContextOptions {
|
|
252
|
-
width?: number
|
|
253
|
-
height?: number
|
|
254
|
-
enableMirroring?: boolean
|
|
255
97
|
document?: Document
|
|
256
98
|
ctx?: CanvasRenderingContext2D
|
|
257
|
-
debug?: boolean
|
|
258
99
|
}
|
|
259
100
|
|
|
260
101
|
export class Context {
|
|
261
102
|
width: number
|
|
262
103
|
height: number
|
|
263
|
-
enableMirroring: boolean
|
|
264
104
|
canvas: Context
|
|
265
105
|
|
|
266
106
|
__document: Document
|
|
267
107
|
__ctx: CanvasRenderingContext2D
|
|
268
108
|
__canvas?: HTMLCanvasElement
|
|
269
109
|
__root: SVGSVGElement
|
|
270
|
-
__ids: Record<string, string>
|
|
271
|
-
__defs: SVGDefsElement
|
|
272
110
|
__currentElement: SVGElement
|
|
273
111
|
__styleStack: any[]
|
|
274
112
|
__groupStack: SVGElement[]
|
|
@@ -276,50 +114,21 @@ export class Context {
|
|
|
276
114
|
__currentPosition: { x?: number; y?: number }
|
|
277
115
|
__transformMatrix: DOMMatrix
|
|
278
116
|
__transformMatrixStack?: DOMMatrix[]
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
__fontUnderline?: string
|
|
283
|
-
__fontHref?: string
|
|
117
|
+
// parsed fonts keyed by `font` string; each parse costs a DOM element and a
|
|
118
|
+
// CSS parse, and exports ask per glyph
|
|
119
|
+
__fontCache = new Map<string, CSSStyleDeclaration>()
|
|
284
120
|
|
|
285
|
-
// Style properties
|
|
286
121
|
strokeStyle: any
|
|
287
122
|
fillStyle: any
|
|
288
|
-
lineCap: any
|
|
289
|
-
lineJoin: any
|
|
290
|
-
miterLimit: any
|
|
291
123
|
lineWidth: any
|
|
292
|
-
globalAlpha: any
|
|
293
124
|
font: any
|
|
294
|
-
shadowColor: any
|
|
295
|
-
shadowOffsetX: any
|
|
296
|
-
shadowOffsetY: any
|
|
297
|
-
shadowBlur: any
|
|
298
125
|
textAlign: any
|
|
299
126
|
textBaseline: any
|
|
300
127
|
lineDash: any
|
|
301
128
|
|
|
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
|
-
|
|
129
|
+
constructor(width: number, height: number, options: ContextOptions = {}) {
|
|
130
|
+
this.width = width
|
|
131
|
+
this.height = height
|
|
323
132
|
this.canvas = this
|
|
324
133
|
this.__document = options.document || document
|
|
325
134
|
|
|
@@ -340,21 +149,8 @@ export class Context {
|
|
|
340
149
|
)
|
|
341
150
|
this.__root.setAttribute('version', '1.1')
|
|
342
151
|
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)
|
|
152
|
+
this.__root.setAttribute('width', String(width))
|
|
153
|
+
this.__root.setAttribute('height', String(height))
|
|
358
154
|
|
|
359
155
|
this.__currentElement = this.__document.createElementNS(
|
|
360
156
|
'http://www.w3.org/2000/svg',
|
|
@@ -367,9 +163,6 @@ export class Context {
|
|
|
367
163
|
this.__transformMatrix = new DOMMatrix()
|
|
368
164
|
|
|
369
165
|
this.resetTransform()
|
|
370
|
-
|
|
371
|
-
this.__options = options
|
|
372
|
-
this.__id = Math.random().toString(16).substring(2, 8)
|
|
373
166
|
}
|
|
374
167
|
|
|
375
168
|
__createElement(
|
|
@@ -382,14 +175,15 @@ export class Context {
|
|
|
382
175
|
elementName,
|
|
383
176
|
)
|
|
384
177
|
if (resetFill) {
|
|
178
|
+
// placeholders: fill()/stroke() overwrite their own attribute, and
|
|
179
|
+
// __applyStyleToCurrentElement drops the other when it is the svg default
|
|
385
180
|
element.setAttribute('fill', 'none')
|
|
386
181
|
element.setAttribute('stroke', 'none')
|
|
387
182
|
}
|
|
388
183
|
for (const key of Object.keys(properties)) {
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// text-decoration="undefined" -- invalid, and ~28 wasted bytes per letter
|
|
184
|
+
// __applyText passes optional attributes through unconditionally; skipping
|
|
185
|
+
// unset ones keeps an invalid text-decoration="undefined" (~28 bytes) off
|
|
186
|
+
// every glyph
|
|
393
187
|
const value = properties[key]
|
|
394
188
|
if (value !== undefined && value !== null) {
|
|
395
189
|
element.setAttribute(key, value)
|
|
@@ -418,91 +212,70 @@ export class Context {
|
|
|
418
212
|
return styleState
|
|
419
213
|
}
|
|
420
214
|
|
|
215
|
+
/**
|
|
216
|
+
* A pure translation folds into the element's x/y, saving ~35 bytes per
|
|
217
|
+
* glyph. Any other transform, such as a sequence-logo letter's scale, is
|
|
218
|
+
* written as a matrix.
|
|
219
|
+
*/
|
|
421
220
|
__applyTransformation(element: SVGElement, matrix?: DOMMatrix) {
|
|
422
221
|
const { a, b, c, d, e, f } = matrix || this.getTransform()
|
|
222
|
+
if (
|
|
223
|
+
a === 1 &&
|
|
224
|
+
b === 0 &&
|
|
225
|
+
c === 0 &&
|
|
226
|
+
d === 1 &&
|
|
227
|
+
element.hasAttribute('x') &&
|
|
228
|
+
element.hasAttribute('y')
|
|
229
|
+
) {
|
|
230
|
+
if (e !== 0) {
|
|
231
|
+
element.setAttribute('x', String(Number(element.getAttribute('x')) + e))
|
|
232
|
+
}
|
|
233
|
+
if (f !== 0) {
|
|
234
|
+
element.setAttribute('y', String(Number(element.getAttribute('y')) + f))
|
|
235
|
+
}
|
|
236
|
+
return
|
|
237
|
+
}
|
|
423
238
|
element.setAttribute('transform', `matrix(${a} ${b} ${c} ${d} ${e} ${f})`)
|
|
424
239
|
}
|
|
425
240
|
|
|
426
241
|
__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
|
-
}
|
|
242
|
+
const currentElement = this.__currentElement
|
|
436
243
|
|
|
437
|
-
const
|
|
438
|
-
for (const key of keys) {
|
|
244
|
+
for (const key of Object.keys(STYLES)) {
|
|
439
245
|
const style = STYLES[key]
|
|
440
246
|
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
|
-
}
|
|
247
|
+
if (!style.apply?.includes(type) || style.svg === value) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const matches =
|
|
251
|
+
(style.svgAttr === 'stroke' || style.svgAttr === 'fill') &&
|
|
252
|
+
typeof value === 'string' &&
|
|
253
|
+
value.includes('rgba')
|
|
254
|
+
? rgbaRegex.exec(value)
|
|
255
|
+
: null
|
|
256
|
+
if (matches) {
|
|
257
|
+
// SVG has no rgba(); the alpha goes in the -opacity attribute
|
|
258
|
+
currentElement.setAttribute(
|
|
259
|
+
style.svgAttr,
|
|
260
|
+
`rgb(${matches[1]},${matches[2]},${matches[3]})`,
|
|
261
|
+
)
|
|
262
|
+
currentElement.setAttribute(`${style.svgAttr}-opacity`, matches[4]!)
|
|
263
|
+
} else if (key === 'lineWidth') {
|
|
264
|
+
const scale = this.__getTransformScale()
|
|
265
|
+
currentElement.setAttribute(
|
|
266
|
+
style.svgAttr,
|
|
267
|
+
String(value * Math.max(scale.x, scale.y)),
|
|
268
|
+
)
|
|
269
|
+
} else {
|
|
270
|
+
currentElement.setAttribute(style.svgAttr, value)
|
|
504
271
|
}
|
|
505
272
|
}
|
|
273
|
+
|
|
274
|
+
// svg defaults stroke to none, so a fill drops stroke="none". Fill defaults
|
|
275
|
+
// to black, so a stroked element keeps fill="none".
|
|
276
|
+
if (type === 'fill' && currentElement.getAttribute('stroke') === 'none') {
|
|
277
|
+
currentElement.removeAttribute('stroke')
|
|
278
|
+
}
|
|
506
279
|
}
|
|
507
280
|
|
|
508
281
|
__closestGroupOrSvg(node?: SVGElement): SVGElement {
|
|
@@ -513,29 +286,6 @@ export class Context {
|
|
|
513
286
|
return this.__closestGroupOrSvg(node.parentNode as SVGElement)
|
|
514
287
|
}
|
|
515
288
|
|
|
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
289
|
getSvg(): SVGSVGElement {
|
|
540
290
|
return this.__root
|
|
541
291
|
}
|
|
@@ -546,8 +296,7 @@ export class Context {
|
|
|
546
296
|
this.__groupStack.push(parent)
|
|
547
297
|
parent.appendChild(group)
|
|
548
298
|
this.__currentElement = group
|
|
549
|
-
|
|
550
|
-
this.__styleStack.push(style)
|
|
299
|
+
this.__styleStack.push(this.__getStyleState())
|
|
551
300
|
if (!this.__transformMatrixStack) {
|
|
552
301
|
this.__transformMatrixStack = []
|
|
553
302
|
}
|
|
@@ -556,12 +305,10 @@ export class Context {
|
|
|
556
305
|
|
|
557
306
|
restore() {
|
|
558
307
|
this.__currentElement = this.__groupStack.pop()!
|
|
559
|
-
this.__currentElementsToStyle = undefined
|
|
560
308
|
if (!this.__currentElement) {
|
|
561
|
-
this.__currentElement = this.__root.childNodes[
|
|
309
|
+
this.__currentElement = this.__root.childNodes[0] as SVGElement
|
|
562
310
|
}
|
|
563
|
-
|
|
564
|
-
this.__applyStyleState(state)
|
|
311
|
+
this.__applyStyleState(this.__styleStack.pop())
|
|
565
312
|
if (this.__transformMatrixStack && this.__transformMatrixStack.length > 0) {
|
|
566
313
|
this.setTransform(this.__transformMatrixStack.pop()!)
|
|
567
314
|
}
|
|
@@ -571,15 +318,13 @@ export class Context {
|
|
|
571
318
|
this.__currentDefaultPath = ''
|
|
572
319
|
this.__currentPosition = {}
|
|
573
320
|
const path = this.__createElement('path', {}, true)
|
|
574
|
-
|
|
575
|
-
parent.appendChild(path)
|
|
321
|
+
this.__closestGroupOrSvg().appendChild(path)
|
|
576
322
|
this.__currentElement = path
|
|
577
323
|
}
|
|
578
324
|
|
|
579
325
|
__applyCurrentDefaultPath() {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
currentElement.setAttribute('d', this.__currentDefaultPath)
|
|
326
|
+
if (this.__currentElement.nodeName === 'path') {
|
|
327
|
+
this.__currentElement.setAttribute('d', this.__currentDefaultPath)
|
|
583
328
|
}
|
|
584
329
|
}
|
|
585
330
|
|
|
@@ -593,12 +338,8 @@ export class Context {
|
|
|
593
338
|
this.beginPath()
|
|
594
339
|
}
|
|
595
340
|
this.__currentPosition = { x, y }
|
|
596
|
-
this.
|
|
597
|
-
|
|
598
|
-
x: this.__matrixTransform(x, y).x,
|
|
599
|
-
y: this.__matrixTransform(x, y).y,
|
|
600
|
-
}),
|
|
601
|
-
)
|
|
341
|
+
const p = this.__matrixTransform(x, y)
|
|
342
|
+
this.__addPathCommand(`M ${p.x} ${p.y}`)
|
|
602
343
|
}
|
|
603
344
|
|
|
604
345
|
closePath() {
|
|
@@ -609,112 +350,51 @@ export class Context {
|
|
|
609
350
|
|
|
610
351
|
lineTo(x: number, y: number) {
|
|
611
352
|
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 }
|
|
353
|
+
const p = this.__matrixTransform(x, y)
|
|
638
354
|
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
|
-
}),
|
|
355
|
+
this.__currentDefaultPath.includes('M')
|
|
356
|
+
? `L ${p.x} ${p.y}`
|
|
357
|
+
: `M ${p.x} ${p.y}`,
|
|
647
358
|
)
|
|
648
359
|
}
|
|
649
360
|
|
|
650
361
|
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) {
|
|
651
362
|
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
|
-
)
|
|
363
|
+
const cp = this.__matrixTransform(cpx, cpy)
|
|
364
|
+
const p = this.__matrixTransform(x, y)
|
|
365
|
+
this.__addPathCommand(`Q ${cp.x} ${cp.y} ${p.x} ${p.y}`)
|
|
660
366
|
}
|
|
661
367
|
|
|
662
|
-
stroke
|
|
663
|
-
|
|
664
|
-
|
|
368
|
+
// paint-order matters only on an element with both fill and stroke: the
|
|
369
|
+
// collapsed-clade triangle
|
|
370
|
+
__applyPaintOrder(order: string, other: string) {
|
|
371
|
+
const current = this.__currentElement
|
|
372
|
+
if (
|
|
373
|
+
current.nodeName === 'path' &&
|
|
374
|
+
current.hasAttribute(other) &&
|
|
375
|
+
current.getAttribute(other) !== 'none'
|
|
376
|
+
) {
|
|
377
|
+
current.setAttribute('paint-order', order)
|
|
665
378
|
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
stroke() {
|
|
382
|
+
this.__applyPaintOrder('fill stroke markers', 'fill')
|
|
666
383
|
this.__applyCurrentDefaultPath()
|
|
667
384
|
this.__applyStyleToCurrentElement('stroke')
|
|
668
385
|
}
|
|
669
386
|
|
|
670
387
|
fill() {
|
|
671
|
-
|
|
672
|
-
this.__currentElement.setAttribute('paint-order', 'stroke fill markers')
|
|
673
|
-
}
|
|
388
|
+
this.__applyPaintOrder('stroke fill markers', 'stroke')
|
|
674
389
|
this.__applyCurrentDefaultPath()
|
|
675
390
|
this.__applyStyleToCurrentElement('fill')
|
|
676
391
|
}
|
|
677
392
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
this.beginPath()
|
|
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
|
-
|
|
393
|
+
// A fill covering the whole context paints over what is under it, as on a
|
|
394
|
+
// canvas; it does not clear the drawing.
|
|
701
395
|
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
396
|
const rect = this.__createElement('rect', { x, y, width, height }, true)
|
|
716
|
-
|
|
717
|
-
parent.appendChild(rect)
|
|
397
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
718
398
|
this.__currentElement = rect
|
|
719
399
|
this.__applyTransformation(rect)
|
|
720
400
|
this.__applyStyleToCurrentElement('fill')
|
|
@@ -722,8 +402,7 @@ export class Context {
|
|
|
722
402
|
|
|
723
403
|
strokeRect(x: number, y: number, width: number, height: number) {
|
|
724
404
|
const rect = this.__createElement('rect', { x, y, width, height }, true)
|
|
725
|
-
|
|
726
|
-
parent.appendChild(rect)
|
|
405
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
727
406
|
this.__currentElement = rect
|
|
728
407
|
this.__applyTransformation(rect)
|
|
729
408
|
this.__applyStyleToCurrentElement('stroke')
|
|
@@ -732,17 +411,19 @@ export class Context {
|
|
|
732
411
|
clearRect(x: number, y: number, width: number, height: number) {
|
|
733
412
|
const { a, b, c, d, e, f } = this.getTransform()
|
|
734
413
|
if (
|
|
735
|
-
|
|
414
|
+
a === 1 &&
|
|
415
|
+
b === 0 &&
|
|
416
|
+
c === 0 &&
|
|
417
|
+
d === 1 &&
|
|
418
|
+
e === 0 &&
|
|
419
|
+
f === 0 &&
|
|
420
|
+
x === 0 &&
|
|
421
|
+
y === 0 &&
|
|
422
|
+
width === this.width &&
|
|
423
|
+
height === this.height
|
|
736
424
|
) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
y === 0 &&
|
|
740
|
-
width === this.width &&
|
|
741
|
-
height === this.height
|
|
742
|
-
) {
|
|
743
|
-
this.__clearCanvas()
|
|
744
|
-
return
|
|
745
|
-
}
|
|
425
|
+
this.__clearCanvas()
|
|
426
|
+
return
|
|
746
427
|
}
|
|
747
428
|
const rect = this.__createElement(
|
|
748
429
|
'rect',
|
|
@@ -750,54 +431,32 @@ export class Context {
|
|
|
750
431
|
true,
|
|
751
432
|
)
|
|
752
433
|
this.__applyTransformation(rect)
|
|
753
|
-
|
|
754
|
-
parent.appendChild(rect)
|
|
434
|
+
this.__closestGroupOrSvg().appendChild(rect)
|
|
755
435
|
}
|
|
756
436
|
|
|
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)
|
|
437
|
+
__clearCanvas() {
|
|
438
|
+
this.__root.removeChild(this.__root.childNodes[0]!)
|
|
439
|
+
this.__currentElement = this.__document.createElementNS(
|
|
440
|
+
'http://www.w3.org/2000/svg',
|
|
441
|
+
'g',
|
|
442
|
+
)
|
|
443
|
+
this.__root.appendChild(this.__currentElement)
|
|
444
|
+
this.__groupStack = []
|
|
794
445
|
}
|
|
795
446
|
|
|
796
|
-
|
|
447
|
+
__parsedFont() {
|
|
448
|
+
const hit = this.__fontCache.get(this.font)
|
|
449
|
+
if (hit) {
|
|
450
|
+
return hit
|
|
451
|
+
}
|
|
797
452
|
const el = document.createElement('span')
|
|
798
|
-
el.setAttribute('style',
|
|
453
|
+
el.setAttribute('style', `font:${this.font}`)
|
|
454
|
+
this.__fontCache.set(this.font, el.style)
|
|
455
|
+
return el.style
|
|
456
|
+
}
|
|
799
457
|
|
|
800
|
-
|
|
458
|
+
fillText(text: string, x: number, y: number) {
|
|
459
|
+
const style = this.__parsedFont()
|
|
801
460
|
const parent = this.__closestGroupOrSvg()
|
|
802
461
|
const textElement = this.__createElement(
|
|
803
462
|
'text',
|
|
@@ -806,9 +465,8 @@ export class Context {
|
|
|
806
465
|
'font-size': style.fontSize,
|
|
807
466
|
'font-style': omitDefault('font-style', style.fontStyle),
|
|
808
467
|
'font-weight': omitDefault('font-weight', style.fontWeight),
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
y: y,
|
|
468
|
+
x,
|
|
469
|
+
y,
|
|
812
470
|
'text-anchor': getTextAnchor(this.textAlign),
|
|
813
471
|
'dominant-baseline': omitDefault(
|
|
814
472
|
'dominant-baseline',
|
|
@@ -821,29 +479,8 @@ export class Context {
|
|
|
821
479
|
textElement.appendChild(this.__document.createTextNode(text))
|
|
822
480
|
this.__currentElement = textElement
|
|
823
481
|
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')
|
|
482
|
+
this.__applyStyleToCurrentElement('fill')
|
|
483
|
+
parent.appendChild(textElement)
|
|
847
484
|
}
|
|
848
485
|
|
|
849
486
|
measureText(text: string): TextMetrics {
|
|
@@ -874,71 +511,30 @@ export class Context {
|
|
|
874
511
|
const startX = x + radius * Math.cos(startAngle)
|
|
875
512
|
const startY = y + radius * Math.sin(startAngle)
|
|
876
513
|
const sweepFlag = counterClockwise ? 0 : 1
|
|
877
|
-
let largeArcFlag = 0
|
|
878
514
|
let diff = endAngle - startAngle
|
|
879
|
-
|
|
880
515
|
if (diff < 0) {
|
|
881
516
|
diff += 2 * Math.PI
|
|
882
517
|
}
|
|
518
|
+
const largeArcFlag = counterClockwise
|
|
519
|
+
? diff > Math.PI
|
|
520
|
+
? 0
|
|
521
|
+
: 1
|
|
522
|
+
: diff > Math.PI
|
|
523
|
+
? 1
|
|
524
|
+
: 0
|
|
883
525
|
|
|
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
|
-
)
|
|
526
|
+
const { x: scaleX, y: scaleY } = this.__getTransformScale()
|
|
527
|
+
const end = this.__matrixTransform(endX, endY)
|
|
898
528
|
|
|
899
529
|
this.lineTo(startX, startY)
|
|
900
530
|
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
|
-
),
|
|
531
|
+
`A ${radius * scaleX} ${radius * scaleY} 0 ${largeArcFlag} ${sweepFlag} ${end.x} ${end.y}`,
|
|
913
532
|
)
|
|
914
|
-
|
|
915
533
|
this.__currentPosition = { x: endX, y: endY }
|
|
916
534
|
}
|
|
917
535
|
|
|
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
536
|
setLineDash(dashArray: number[]) {
|
|
937
|
-
|
|
938
|
-
this.lineDash = dashArray.join(',')
|
|
939
|
-
} else {
|
|
940
|
-
this.lineDash = null
|
|
941
|
-
}
|
|
537
|
+
this.lineDash = dashArray.length > 0 ? dashArray.join(',') : null
|
|
942
538
|
}
|
|
943
539
|
|
|
944
540
|
setTransform(
|
|
@@ -949,11 +545,10 @@ export class Context {
|
|
|
949
545
|
e?: number,
|
|
950
546
|
f?: number,
|
|
951
547
|
) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
}
|
|
548
|
+
this.__transformMatrix =
|
|
549
|
+
a instanceof DOMMatrix
|
|
550
|
+
? new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f])
|
|
551
|
+
: new DOMMatrix([a, b!, c!, d!, e!, f!])
|
|
957
552
|
}
|
|
958
553
|
|
|
959
554
|
getTransform(): DOMMatrix {
|
|
@@ -972,34 +567,11 @@ export class Context {
|
|
|
972
567
|
if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) {
|
|
973
568
|
return
|
|
974
569
|
}
|
|
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)
|
|
570
|
+
this.setTransform(this.getTransform().scale(x, y))
|
|
991
571
|
}
|
|
992
572
|
|
|
993
573
|
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)
|
|
574
|
+
this.setTransform(this.getTransform().translate(x, y))
|
|
1003
575
|
}
|
|
1004
576
|
|
|
1005
577
|
__matrixTransform(x: number, y: number): DOMPoint {
|
|
@@ -1012,72 +584,4 @@ export class Context {
|
|
|
1012
584
|
y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d),
|
|
1013
585
|
}
|
|
1014
586
|
}
|
|
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
587
|
}
|