@jbrowse/svgcanvas 1.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/src/index.ts ADDED
@@ -0,0 +1,998 @@
1
+ /**
2
+ * SVGCanvas v2.0.3
3
+ * Draw on SVG using Canvas's 2D Context API.
4
+ *
5
+ * Licensed under the MIT license:
6
+ * http://www.opensource.org/licenses/mit-license.php
7
+ *
8
+ * Original Authors: Kerry Liu, Zeno Zeng
9
+ * Copyright (c) 2014 Gliffy Inc.
10
+ * Copyright (c) 2021 Zeno Zeng
11
+ *
12
+ * Vendored and converted to ESM/TypeScript for pure ESM compatibility.
13
+ */
14
+
15
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
+
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'] = '&#160;'
64
+ return lookup
65
+ }
66
+
67
+ function getTextAnchor(textAlign: string): string {
68
+ const mapping: Record<string, string> = {
69
+ left: 'start',
70
+ right: 'end',
71
+ center: 'middle',
72
+ start: 'start',
73
+ end: 'end',
74
+ }
75
+ return mapping[textAlign] || mapping.start!
76
+ }
77
+
78
+ function getDominantBaseline(textBaseline: string): string {
79
+ const mapping: Record<string, string> = {
80
+ alphabetic: 'alphabetic',
81
+ hanging: 'hanging',
82
+ top: 'text-before-edge',
83
+ bottom: 'text-after-edge',
84
+ middle: 'central',
85
+ }
86
+ return mapping[textBaseline] || mapping.alphabetic!
87
+ }
88
+
89
+ const namedEntities = createNamedToNumberedLookup(
90
+ '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' +
91
+ '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' +
92
+ '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' +
93
+ '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' +
94
+ '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' +
95
+ '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' +
96
+ '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' +
97
+ '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' +
98
+ '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' +
99
+ '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' +
100
+ 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' +
101
+ 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' +
102
+ 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' +
103
+ 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' +
104
+ 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' +
105
+ '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' +
106
+ '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' +
107
+ '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' +
108
+ '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' +
109
+ '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' +
110
+ 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' +
111
+ 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' +
112
+ 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' +
113
+ '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' +
114
+ '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro',
115
+ 32,
116
+ )
117
+
118
+ const STYLES: Record<string, any> = {
119
+ strokeStyle: {
120
+ svgAttr: 'stroke',
121
+ canvas: '#000000',
122
+ svg: 'none',
123
+ apply: 'stroke',
124
+ },
125
+ fillStyle: {
126
+ svgAttr: 'fill',
127
+ canvas: '#000000',
128
+ svg: null,
129
+ apply: 'fill',
130
+ },
131
+ lineCap: {
132
+ svgAttr: 'stroke-linecap',
133
+ canvas: 'butt',
134
+ svg: 'butt',
135
+ apply: 'stroke',
136
+ },
137
+ lineJoin: {
138
+ svgAttr: 'stroke-linejoin',
139
+ canvas: 'miter',
140
+ svg: 'miter',
141
+ apply: 'stroke',
142
+ },
143
+ miterLimit: {
144
+ svgAttr: 'stroke-miterlimit',
145
+ canvas: 10,
146
+ svg: 4,
147
+ apply: 'stroke',
148
+ },
149
+ lineWidth: {
150
+ svgAttr: 'stroke-width',
151
+ canvas: 1,
152
+ svg: 1,
153
+ apply: 'stroke',
154
+ },
155
+ globalAlpha: {
156
+ svgAttr: 'opacity',
157
+ canvas: 1,
158
+ svg: 1,
159
+ apply: 'fill stroke',
160
+ },
161
+ font: {
162
+ canvas: '10px sans-serif',
163
+ },
164
+ shadowColor: {
165
+ canvas: '#000000',
166
+ },
167
+ shadowOffsetX: {
168
+ canvas: 0,
169
+ },
170
+ shadowOffsetY: {
171
+ canvas: 0,
172
+ },
173
+ shadowBlur: {
174
+ canvas: 0,
175
+ },
176
+ textAlign: {
177
+ canvas: 'start',
178
+ },
179
+ textBaseline: {
180
+ canvas: 'alphabetic',
181
+ },
182
+ lineDash: {
183
+ svgAttr: 'stroke-dasharray',
184
+ canvas: [],
185
+ svg: null,
186
+ apply: 'stroke',
187
+ },
188
+ }
189
+
190
+ class CanvasGradient {
191
+ __root: SVGElement
192
+ __ctx: Context
193
+
194
+ constructor(gradientNode: SVGElement, ctx: Context) {
195
+ this.__root = gradientNode
196
+ this.__ctx = ctx
197
+ }
198
+
199
+ addColorStop(offset: number, color: string) {
200
+ const stop = this.__ctx.__createElement('stop')
201
+ stop.setAttribute('offset', String(offset))
202
+ if (toString(color).includes('rgba')) {
203
+ const regex =
204
+ /rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi
205
+ const matches = regex.exec(color)
206
+ if (matches) {
207
+ stop.setAttribute(
208
+ 'stop-color',
209
+ format('rgb({r},{g},{b})', {
210
+ r: matches[1],
211
+ g: matches[2],
212
+ b: matches[3],
213
+ }),
214
+ )
215
+ stop.setAttribute('stop-opacity', matches[4]!)
216
+ }
217
+ } else {
218
+ stop.setAttribute('stop-color', toString(color))
219
+ }
220
+ this.__root.appendChild(stop)
221
+ }
222
+ }
223
+
224
+ class CanvasPattern {
225
+ __root: SVGElement
226
+ __ctx: Context
227
+
228
+ constructor(pattern: SVGElement, ctx: Context) {
229
+ this.__root = pattern
230
+ this.__ctx = ctx
231
+ }
232
+ }
233
+
234
+ interface ContextOptions {
235
+ width?: number
236
+ height?: number
237
+ enableMirroring?: boolean
238
+ document?: Document
239
+ ctx?: CanvasRenderingContext2D
240
+ debug?: boolean
241
+ }
242
+
243
+ export class Context {
244
+ width: number
245
+ height: number
246
+ enableMirroring: boolean
247
+ canvas: Context
248
+
249
+ __document: Document
250
+ __ctx: CanvasRenderingContext2D
251
+ __canvas?: HTMLCanvasElement
252
+ __root: SVGSVGElement
253
+ __ids: Record<string, string>
254
+ __defs: SVGDefsElement
255
+ __currentElement: SVGElement
256
+ __styleStack: any[]
257
+ __groupStack: SVGElement[]
258
+ __currentDefaultPath: string
259
+ __currentPosition: { x?: number; y?: number }
260
+ __transformMatrix: DOMMatrix
261
+ __transformMatrixStack?: DOMMatrix[]
262
+ __currentElementsToStyle?: { element: SVGElement; children: SVGElement[] }
263
+ __options: ContextOptions
264
+ __id: string
265
+ __fontUnderline?: string
266
+ __fontHref?: string
267
+
268
+ // Style properties
269
+ strokeStyle: any
270
+ fillStyle: any
271
+ lineCap: any
272
+ lineJoin: any
273
+ miterLimit: any
274
+ lineWidth: any
275
+ globalAlpha: any
276
+ font: any
277
+ shadowColor: any
278
+ shadowOffsetX: any
279
+ shadowOffsetY: any
280
+ shadowBlur: any
281
+ textAlign: any
282
+ textBaseline: any
283
+ lineDash: any
284
+
285
+ constructor(o?: ContextOptions | number, height?: number) {
286
+ const defaultOptions = { width: 500, height: 500, enableMirroring: false }
287
+ let options: ContextOptions
288
+
289
+ if (typeof o === 'number' && height !== undefined) {
290
+ options = { ...defaultOptions, width: o, height }
291
+ } else if (!o) {
292
+ options = defaultOptions
293
+ } else if (typeof o === 'object') {
294
+ options = o
295
+ } else {
296
+ options = defaultOptions
297
+ }
298
+
299
+ this.width = options.width || defaultOptions.width
300
+ this.height = options.height || defaultOptions.height
301
+ this.enableMirroring =
302
+ options.enableMirroring !== undefined
303
+ ? options.enableMirroring
304
+ : defaultOptions.enableMirroring
305
+
306
+ this.canvas = this
307
+ this.__document = options.document || document
308
+
309
+ if (options.ctx) {
310
+ this.__ctx = options.ctx
311
+ } else {
312
+ this.__canvas = this.__document.createElement('canvas')
313
+ this.__ctx = this.__canvas.getContext('2d')!
314
+ }
315
+
316
+ this.__setDefaultStyles()
317
+ this.__styleStack = [this.__getStyleState()]
318
+ this.__groupStack = []
319
+
320
+ this.__root = this.__document.createElementNS(
321
+ 'http://www.w3.org/2000/svg',
322
+ 'svg',
323
+ )
324
+ this.__root.setAttribute('version', '1.1')
325
+ this.__root.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
326
+ this.__root.setAttributeNS(
327
+ 'http://www.w3.org/2000/xmlns/',
328
+ 'xmlns:xlink',
329
+ 'http://www.w3.org/1999/xlink',
330
+ )
331
+ this.__root.setAttribute('width', String(this.width))
332
+ this.__root.setAttribute('height', String(this.height))
333
+
334
+ this.__ids = {}
335
+
336
+ this.__defs = this.__document.createElementNS(
337
+ 'http://www.w3.org/2000/svg',
338
+ 'defs',
339
+ )
340
+ this.__root.appendChild(this.__defs)
341
+
342
+ this.__currentElement = this.__document.createElementNS(
343
+ 'http://www.w3.org/2000/svg',
344
+ 'g',
345
+ )
346
+ this.__root.appendChild(this.__currentElement)
347
+
348
+ this.__currentDefaultPath = ''
349
+ this.__currentPosition = {}
350
+ this.__transformMatrix = new DOMMatrix()
351
+
352
+ this.resetTransform()
353
+
354
+ this.__options = options
355
+ this.__id = Math.random().toString(16).substring(2, 8)
356
+ }
357
+
358
+ __createElement(
359
+ elementName: string,
360
+ properties: Record<string, any> = {},
361
+ resetFill?: boolean,
362
+ ): SVGElement {
363
+ const element = this.__document.createElementNS(
364
+ 'http://www.w3.org/2000/svg',
365
+ elementName,
366
+ )
367
+ if (resetFill) {
368
+ element.setAttribute('fill', 'none')
369
+ element.setAttribute('stroke', 'none')
370
+ }
371
+ for (const key of Object.keys(properties)) {
372
+ element.setAttribute(key, properties[key])
373
+ }
374
+ return element
375
+ }
376
+
377
+ __setDefaultStyles() {
378
+ for (const key of Object.keys(STYLES)) {
379
+ ;(this as any)[key] = STYLES[key].canvas
380
+ }
381
+ }
382
+
383
+ __applyStyleState(styleState: Record<string, any>) {
384
+ for (const key of Object.keys(styleState)) {
385
+ ;(this as any)[key] = styleState[key]
386
+ }
387
+ }
388
+
389
+ __getStyleState(): Record<string, any> {
390
+ const styleState: Record<string, any> = {}
391
+ for (const key of Object.keys(STYLES)) {
392
+ styleState[key] = (this as any)[key]
393
+ }
394
+ return styleState
395
+ }
396
+
397
+ __applyTransformation(element: SVGElement, matrix?: DOMMatrix) {
398
+ const { a, b, c, d, e, f } = matrix || this.getTransform()
399
+ element.setAttribute('transform', `matrix(${a} ${b} ${c} ${d} ${e} ${f})`)
400
+ }
401
+
402
+ __applyStyleToCurrentElement(type: string) {
403
+ let currentElement = this.__currentElement
404
+ const currentStyleGroup = this.__currentElementsToStyle
405
+ if (currentStyleGroup) {
406
+ currentElement.setAttribute(type, '')
407
+ currentElement = currentStyleGroup.element
408
+ for (const node of currentStyleGroup.children) {
409
+ node.setAttribute(type, '')
410
+ }
411
+ }
412
+
413
+ const keys = Object.keys(STYLES)
414
+ for (const key of keys) {
415
+ const style = STYLES[key]
416
+ const value = (this as any)[key]
417
+ if (style.apply) {
418
+ if (value instanceof CanvasPattern) {
419
+ if (value.__ctx) {
420
+ for (const node of Array.from(value.__ctx.__defs.childNodes)) {
421
+ const id = (node as Element).getAttribute('id')
422
+ if (id) {
423
+ this.__ids[id] = id
424
+ this.__defs.appendChild(node)
425
+ }
426
+ }
427
+ }
428
+ currentElement.setAttribute(
429
+ style.apply,
430
+ format('url(#{id})', { id: value.__root.getAttribute('id') }),
431
+ )
432
+ } else if (value instanceof CanvasGradient) {
433
+ currentElement.setAttribute(
434
+ style.apply,
435
+ format('url(#{id})', { id: value.__root.getAttribute('id') }),
436
+ )
437
+ } else if (style.apply.includes(type) && style.svg !== value) {
438
+ if (
439
+ (style.svgAttr === 'stroke' || style.svgAttr === 'fill') &&
440
+ value &&
441
+ value.includes('rgba')
442
+ ) {
443
+ const regex =
444
+ /rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/gi
445
+ const matches = regex.exec(value)
446
+ if (matches) {
447
+ currentElement.setAttribute(
448
+ style.svgAttr,
449
+ format('rgb({r},{g},{b})', {
450
+ r: matches[1],
451
+ g: matches[2],
452
+ b: matches[3],
453
+ }),
454
+ )
455
+ let opacity = Number(matches[4])
456
+ const globalAlpha = this.globalAlpha
457
+ if (globalAlpha != null) {
458
+ opacity *= globalAlpha
459
+ }
460
+ currentElement.setAttribute(
461
+ style.svgAttr + '-opacity',
462
+ String(opacity),
463
+ )
464
+ }
465
+ } else {
466
+ let attr = style.svgAttr
467
+ let val = value
468
+ if (key === 'globalAlpha') {
469
+ attr = type + '-' + style.svgAttr
470
+ if (currentElement.getAttribute(attr)) {
471
+ continue
472
+ }
473
+ } else if (key === 'lineWidth') {
474
+ const scale = this.__getTransformScale()
475
+ val = value * Math.max(scale.x, scale.y)
476
+ }
477
+ currentElement.setAttribute(attr, val)
478
+ }
479
+ }
480
+ }
481
+ }
482
+ }
483
+
484
+ __closestGroupOrSvg(node?: SVGElement): SVGElement {
485
+ node = node || this.__currentElement
486
+ if (node.nodeName === 'g' || node.nodeName === 'svg') {
487
+ return node
488
+ }
489
+ return this.__closestGroupOrSvg(node.parentNode as SVGElement)
490
+ }
491
+
492
+ getSerializedSvg(fixNamedEntities?: boolean): string {
493
+ let serialized = new XMLSerializer().serializeToString(this.__root)
494
+ const xmlns =
495
+ /xmlns="http:\/\/www\.w3\.org\/2000\/svg".+xmlns="http:\/\/www\.w3\.org\/2000\/svg/gi
496
+ if (xmlns.test(serialized)) {
497
+ serialized = serialized.replace(
498
+ 'xmlns="http://www.w3.org/2000/svg',
499
+ 'xmlns:xlink="http://www.w3.org/1999/xlink',
500
+ )
501
+ }
502
+
503
+ if (fixNamedEntities) {
504
+ for (const key of Object.keys(namedEntities)) {
505
+ const regexp = new RegExp(key, 'gi')
506
+ if (regexp.test(serialized)) {
507
+ serialized = serialized.replace(regexp, namedEntities[key]!)
508
+ }
509
+ }
510
+ }
511
+
512
+ return serialized
513
+ }
514
+
515
+ getSvg(): SVGSVGElement {
516
+ return this.__root
517
+ }
518
+
519
+ save() {
520
+ const group = this.__createElement('g')
521
+ const parent = this.__closestGroupOrSvg()
522
+ this.__groupStack.push(parent)
523
+ parent.appendChild(group)
524
+ this.__currentElement = group
525
+ const style = this.__getStyleState()
526
+ this.__styleStack.push(style)
527
+ if (!this.__transformMatrixStack) {
528
+ this.__transformMatrixStack = []
529
+ }
530
+ this.__transformMatrixStack.push(this.getTransform())
531
+ }
532
+
533
+ restore() {
534
+ this.__currentElement = this.__groupStack.pop()!
535
+ this.__currentElementsToStyle = undefined
536
+ if (!this.__currentElement) {
537
+ this.__currentElement = this.__root.childNodes[1] as SVGElement
538
+ }
539
+ const state = this.__styleStack.pop()
540
+ this.__applyStyleState(state)
541
+ if (this.__transformMatrixStack && this.__transformMatrixStack.length > 0) {
542
+ this.setTransform(this.__transformMatrixStack.pop()!)
543
+ }
544
+ }
545
+
546
+ beginPath() {
547
+ this.__currentDefaultPath = ''
548
+ this.__currentPosition = {}
549
+ const path = this.__createElement('path', {}, true)
550
+ const parent = this.__closestGroupOrSvg()
551
+ parent.appendChild(path)
552
+ this.__currentElement = path
553
+ }
554
+
555
+ __applyCurrentDefaultPath() {
556
+ const currentElement = this.__currentElement
557
+ if (currentElement.nodeName === 'path') {
558
+ currentElement.setAttribute('d', this.__currentDefaultPath)
559
+ }
560
+ }
561
+
562
+ __addPathCommand(command: string) {
563
+ this.__currentDefaultPath += ' '
564
+ this.__currentDefaultPath += command
565
+ }
566
+
567
+ moveTo(x: number, y: number) {
568
+ if (this.__currentElement.nodeName !== 'path') {
569
+ this.beginPath()
570
+ }
571
+ this.__currentPosition = { x, y }
572
+ this.__addPathCommand(
573
+ format('M {x} {y}', {
574
+ x: this.__matrixTransform(x, y).x,
575
+ y: this.__matrixTransform(x, y).y,
576
+ }),
577
+ )
578
+ }
579
+
580
+ closePath() {
581
+ if (this.__currentDefaultPath) {
582
+ this.__addPathCommand('Z')
583
+ }
584
+ }
585
+
586
+ lineTo(x: number, y: number) {
587
+ this.__currentPosition = { x, y }
588
+ if (this.__currentDefaultPath.includes('M')) {
589
+ this.__addPathCommand(
590
+ format('L {x} {y}', {
591
+ x: this.__matrixTransform(x, y).x,
592
+ y: this.__matrixTransform(x, y).y,
593
+ }),
594
+ )
595
+ } else {
596
+ this.__addPathCommand(
597
+ format('M {x} {y}', {
598
+ x: this.__matrixTransform(x, y).x,
599
+ y: this.__matrixTransform(x, y).y,
600
+ }),
601
+ )
602
+ }
603
+ }
604
+
605
+ bezierCurveTo(
606
+ cp1x: number,
607
+ cp1y: number,
608
+ cp2x: number,
609
+ cp2y: number,
610
+ x: number,
611
+ y: number,
612
+ ) {
613
+ this.__currentPosition = { x, y }
614
+ this.__addPathCommand(
615
+ format('C {cp1x} {cp1y} {cp2x} {cp2y} {x} {y}', {
616
+ cp1x: this.__matrixTransform(cp1x, cp1y).x,
617
+ cp1y: this.__matrixTransform(cp1x, cp1y).y,
618
+ cp2x: this.__matrixTransform(cp2x, cp2y).x,
619
+ cp2y: this.__matrixTransform(cp2x, cp2y).y,
620
+ x: this.__matrixTransform(x, y).x,
621
+ y: this.__matrixTransform(x, y).y,
622
+ }),
623
+ )
624
+ }
625
+
626
+ quadraticCurveTo(cpx: number, cpy: number, x: number, y: number) {
627
+ this.__currentPosition = { x, y }
628
+ this.__addPathCommand(
629
+ format('Q {cpx} {cpy} {x} {y}', {
630
+ cpx: this.__matrixTransform(cpx, cpy).x,
631
+ cpy: this.__matrixTransform(cpx, cpy).y,
632
+ x: this.__matrixTransform(x, y).x,
633
+ y: this.__matrixTransform(x, y).y,
634
+ }),
635
+ )
636
+ }
637
+
638
+ stroke() {
639
+ if (this.__currentElement.nodeName === 'path') {
640
+ this.__currentElement.setAttribute('paint-order', 'fill stroke markers')
641
+ }
642
+ this.__applyCurrentDefaultPath()
643
+ this.__applyStyleToCurrentElement('stroke')
644
+ }
645
+
646
+ fill() {
647
+ if (this.__currentElement.nodeName === 'path') {
648
+ this.__currentElement.setAttribute('paint-order', 'stroke fill markers')
649
+ }
650
+ this.__applyCurrentDefaultPath()
651
+ this.__applyStyleToCurrentElement('fill')
652
+ }
653
+
654
+ rect(x: number, y: number, width: number, height: number) {
655
+ if (this.__currentElement.nodeName !== 'path') {
656
+ this.beginPath()
657
+ }
658
+ this.moveTo(x, y)
659
+ this.lineTo(x + width, y)
660
+ this.lineTo(x + width, y + height)
661
+ this.lineTo(x, y + height)
662
+ this.lineTo(x, y)
663
+ this.closePath()
664
+ }
665
+
666
+ __clearCanvas() {
667
+ const rootGroup = this.__root.childNodes[1]!
668
+ this.__root.removeChild(rootGroup)
669
+ this.__currentElement = this.__document.createElementNS(
670
+ 'http://www.w3.org/2000/svg',
671
+ 'g',
672
+ )
673
+ this.__root.appendChild(this.__currentElement)
674
+ this.__groupStack = []
675
+ }
676
+
677
+ fillRect(x: number, y: number, width: number, height: number) {
678
+ const { a, b, c, d, e, f } = this.getTransform()
679
+ if (
680
+ JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])
681
+ ) {
682
+ if (x === 0 && y === 0 && width === this.width && height === this.height) {
683
+ this.__clearCanvas()
684
+ }
685
+ }
686
+ const rect = this.__createElement(
687
+ 'rect',
688
+ { x, y, width, height },
689
+ true,
690
+ )
691
+ const parent = this.__closestGroupOrSvg()
692
+ parent.appendChild(rect)
693
+ this.__currentElement = rect
694
+ this.__applyTransformation(rect)
695
+ this.__applyStyleToCurrentElement('fill')
696
+ }
697
+
698
+ strokeRect(x: number, y: number, width: number, height: number) {
699
+ const rect = this.__createElement(
700
+ 'rect',
701
+ { x, y, width, height },
702
+ true,
703
+ )
704
+ const parent = this.__closestGroupOrSvg()
705
+ parent.appendChild(rect)
706
+ this.__currentElement = rect
707
+ this.__applyTransformation(rect)
708
+ this.__applyStyleToCurrentElement('stroke')
709
+ }
710
+
711
+ clearRect(x: number, y: number, width: number, height: number) {
712
+ const { a, b, c, d, e, f } = this.getTransform()
713
+ if (
714
+ JSON.stringify([a, b, c, d, e, f]) === JSON.stringify([1, 0, 0, 1, 0, 0])
715
+ ) {
716
+ if (x === 0 && y === 0 && width === this.width && height === this.height) {
717
+ this.__clearCanvas()
718
+ return
719
+ }
720
+ }
721
+ const rect = this.__createElement(
722
+ 'rect',
723
+ { x, y, width, height, fill: '#FFFFFF' },
724
+ true,
725
+ )
726
+ this.__applyTransformation(rect)
727
+ const parent = this.__closestGroupOrSvg()
728
+ parent.appendChild(rect)
729
+ }
730
+
731
+ createLinearGradient(
732
+ x1: number,
733
+ y1: number,
734
+ x2: number,
735
+ y2: number,
736
+ ): CanvasGradient {
737
+ const grad = this.__createElement('linearGradient', {
738
+ id: randomString(this.__ids),
739
+ x1: x1 + 'px',
740
+ x2: x2 + 'px',
741
+ y1: y1 + 'px',
742
+ y2: y2 + 'px',
743
+ gradientUnits: 'userSpaceOnUse',
744
+ })
745
+ this.__defs.appendChild(grad)
746
+ return new CanvasGradient(grad, this)
747
+ }
748
+
749
+ createRadialGradient(
750
+ x0: number,
751
+ y0: number,
752
+ r0: number,
753
+ x1: number,
754
+ y1: number,
755
+ r1: number,
756
+ ): CanvasGradient {
757
+ const grad = this.__createElement('radialGradient', {
758
+ id: randomString(this.__ids),
759
+ cx: x1 + 'px',
760
+ cy: y1 + 'px',
761
+ r: r1 + 'px',
762
+ fx: x0 + 'px',
763
+ fy: y0 + 'px',
764
+ gradientUnits: 'userSpaceOnUse',
765
+ })
766
+ this.__defs.appendChild(grad)
767
+ return new CanvasGradient(grad, this)
768
+ }
769
+
770
+ __applyText(text: string, x: number, y: number, action: string) {
771
+ const el = document.createElement('span')
772
+ el.setAttribute('style', 'font:' + this.font)
773
+
774
+ const style = el.style
775
+ const parent = this.__closestGroupOrSvg()
776
+ const textElement = this.__createElement(
777
+ 'text',
778
+ {
779
+ 'font-family': style.fontFamily,
780
+ 'font-size': style.fontSize,
781
+ 'font-style': style.fontStyle,
782
+ 'font-weight': style.fontWeight,
783
+ 'text-decoration': this.__fontUnderline,
784
+ x: x,
785
+ y: y,
786
+ 'text-anchor': getTextAnchor(this.textAlign),
787
+ 'dominant-baseline': getDominantBaseline(this.textBaseline),
788
+ },
789
+ true,
790
+ )
791
+
792
+ textElement.appendChild(this.__document.createTextNode(text))
793
+ this.__currentElement = textElement
794
+ this.__applyTransformation(textElement)
795
+ this.__applyStyleToCurrentElement(action)
796
+
797
+ let finalElement: SVGElement = textElement
798
+ if (this.__fontHref) {
799
+ const a = this.__createElement('a')
800
+ a.setAttributeNS(
801
+ 'http://www.w3.org/1999/xlink',
802
+ 'xlink:href',
803
+ this.__fontHref,
804
+ )
805
+ a.appendChild(textElement)
806
+ finalElement = a
807
+ }
808
+
809
+ parent.appendChild(finalElement)
810
+ }
811
+
812
+ fillText(text: string, x: number, y: number) {
813
+ this.__applyText(text, x, y, 'fill')
814
+ }
815
+
816
+ strokeText(text: string, x: number, y: number) {
817
+ this.__applyText(text, x, y, 'stroke')
818
+ }
819
+
820
+ measureText(text: string): TextMetrics {
821
+ this.__ctx.font = this.font
822
+ return this.__ctx.measureText(text)
823
+ }
824
+
825
+ arc(
826
+ x: number,
827
+ y: number,
828
+ radius: number,
829
+ startAngle: number,
830
+ endAngle: number,
831
+ counterClockwise?: boolean,
832
+ ) {
833
+ if (startAngle === endAngle) {
834
+ return
835
+ }
836
+ startAngle = startAngle % (2 * Math.PI)
837
+ endAngle = endAngle % (2 * Math.PI)
838
+ if (startAngle === endAngle) {
839
+ endAngle =
840
+ ((endAngle + 2 * Math.PI - 0.001 * (counterClockwise ? -1 : 1)) %
841
+ (2 * Math.PI))
842
+ }
843
+ const endX = x + radius * Math.cos(endAngle)
844
+ const endY = y + radius * Math.sin(endAngle)
845
+ const startX = x + radius * Math.cos(startAngle)
846
+ const startY = y + radius * Math.sin(startAngle)
847
+ const sweepFlag = counterClockwise ? 0 : 1
848
+ let largeArcFlag = 0
849
+ let diff = endAngle - startAngle
850
+
851
+ if (diff < 0) {
852
+ diff += 2 * Math.PI
853
+ }
854
+
855
+ if (counterClockwise) {
856
+ largeArcFlag = diff > Math.PI ? 0 : 1
857
+ } else {
858
+ largeArcFlag = diff > Math.PI ? 1 : 0
859
+ }
860
+
861
+ const scaleX = Math.hypot(
862
+ this.__transformMatrix.a,
863
+ this.__transformMatrix.b,
864
+ )
865
+ const scaleY = Math.hypot(
866
+ this.__transformMatrix.c,
867
+ this.__transformMatrix.d,
868
+ )
869
+
870
+ this.lineTo(startX, startY)
871
+ this.__addPathCommand(
872
+ format(
873
+ 'A {rx} {ry} {xAxisRotation} {largeArcFlag} {sweepFlag} {endX} {endY}',
874
+ {
875
+ rx: radius * scaleX,
876
+ ry: radius * scaleY,
877
+ xAxisRotation: 0,
878
+ largeArcFlag,
879
+ sweepFlag,
880
+ endX: this.__matrixTransform(endX, endY).x,
881
+ endY: this.__matrixTransform(endX, endY).y,
882
+ },
883
+ ),
884
+ )
885
+
886
+ this.__currentPosition = { x: endX, y: endY }
887
+ }
888
+
889
+ clip() {
890
+ const group = this.__closestGroupOrSvg()
891
+ const clipPath = this.__createElement('clipPath')
892
+ const id = randomString(this.__ids)
893
+ const newGroup = this.__createElement('g')
894
+
895
+ this.__applyCurrentDefaultPath()
896
+ group.removeChild(this.__currentElement)
897
+ clipPath.setAttribute('id', id)
898
+ clipPath.appendChild(this.__currentElement)
899
+
900
+ this.__defs.appendChild(clipPath)
901
+ group.setAttribute('clip-path', format('url(#{id})', { id }))
902
+ group.appendChild(newGroup)
903
+
904
+ this.__currentElement = newGroup
905
+ }
906
+
907
+ setLineDash(dashArray: number[]) {
908
+ if (dashArray && dashArray.length > 0) {
909
+ this.lineDash = dashArray.join(',')
910
+ } else {
911
+ this.lineDash = null
912
+ }
913
+ }
914
+
915
+ setTransform(
916
+ a: number | DOMMatrix,
917
+ b?: number,
918
+ c?: number,
919
+ d?: number,
920
+ e?: number,
921
+ f?: number,
922
+ ) {
923
+ if (a instanceof DOMMatrix) {
924
+ this.__transformMatrix = new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f])
925
+ } else {
926
+ this.__transformMatrix = new DOMMatrix([a, b!, c!, d!, e!, f!])
927
+ }
928
+ }
929
+
930
+ getTransform(): DOMMatrix {
931
+ const { a, b, c, d, e, f } = this.__transformMatrix
932
+ return new DOMMatrix([a, b, c, d, e, f])
933
+ }
934
+
935
+ resetTransform() {
936
+ this.setTransform(1, 0, 0, 1, 0, 0)
937
+ }
938
+
939
+ scale(x: number, y?: number) {
940
+ if (y === undefined) {
941
+ y = x
942
+ }
943
+ if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) {
944
+ return
945
+ }
946
+ const matrix = this.getTransform().scale(x, y)
947
+ this.setTransform(matrix)
948
+ }
949
+
950
+ rotate(angle: number) {
951
+ const matrix = this.getTransform().multiply(
952
+ new DOMMatrix([
953
+ Math.cos(angle),
954
+ Math.sin(angle),
955
+ -Math.sin(angle),
956
+ Math.cos(angle),
957
+ 0,
958
+ 0,
959
+ ]),
960
+ )
961
+ this.setTransform(matrix)
962
+ }
963
+
964
+ translate(x: number, y: number) {
965
+ const matrix = this.getTransform().translate(x, y)
966
+ this.setTransform(matrix)
967
+ }
968
+
969
+ transform(a: number, b: number, c: number, d: number, e: number, f: number) {
970
+ const matrix = this.getTransform().multiply(new DOMMatrix([a, b, c, d, e, f]))
971
+ this.setTransform(matrix)
972
+ }
973
+
974
+ __matrixTransform(x: number, y: number): DOMPoint {
975
+ return new DOMPoint(x, y).matrixTransform(this.__transformMatrix)
976
+ }
977
+
978
+ __getTransformScale(): { x: number; y: number } {
979
+ return {
980
+ x: Math.hypot(this.__transformMatrix.a, this.__transformMatrix.b),
981
+ y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d),
982
+ }
983
+ }
984
+
985
+ __getTransformRotation(): number {
986
+ return Math.atan2(this.__transformMatrix.b, this.__transformMatrix.a)
987
+ }
988
+
989
+ // Stubs for unimplemented methods
990
+ drawFocusRing() {}
991
+ createImageData() {}
992
+ putImageData() {}
993
+ globalCompositeOperation() {}
994
+ drawImage() {}
995
+ createPattern() {
996
+ return null
997
+ }
998
+ }