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