@jbrowse/svgcanvas 6.5.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,50 +9,13 @@
9
9
  * Copyright (c) 2014 Gliffy Inc.
10
10
  * Copyright (c) 2021 Zeno Zeng
11
11
  *
12
- * Vendored and converted to ESM/TypeScript for pure ESM compatibility.
12
+ * Vendored and converted to ESM/TypeScript for pure ESM compatibility, and cut
13
+ * down to the drawing calls the renderers make: what a multiple sequence
14
+ * alignment needs is rectangles, paths, arcs and a great many glyphs. Gradients,
15
+ * patterns, clipping, rotation, bezier curves, stroked text, shadows and image
16
+ * drawing are gone with the state they carried.
13
17
  */
14
18
  /* 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
19
  function getTextAnchor(textAlign) {
57
20
  const mapping = {
58
21
  left: 'start',
@@ -81,38 +44,12 @@ function getDominantBaseline(textBaseline) {
81
44
  const TEXT_ATTR_DEFAULTS = {
82
45
  'font-style': 'normal',
83
46
  'font-weight': 'normal',
84
- 'font-variant': 'normal',
85
47
  // the initial value is `auto`, which resolves to the alphabetic baseline
86
48
  'dominant-baseline': 'alphabetic',
87
49
  };
88
50
  function omitDefault(attr, value) {
89
51
  return value === TEXT_ATTR_DEFAULTS[attr] ? undefined : value;
90
52
  }
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
53
  const STYLES = {
117
54
  strokeStyle: {
118
55
  svgAttr: 'stroke',
@@ -126,111 +63,40 @@ const STYLES = {
126
63
  svg: null,
127
64
  apply: 'fill',
128
65
  },
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
66
  lineWidth: {
148
67
  svgAttr: 'stroke-width',
149
68
  canvas: 1,
150
69
  svg: 1,
151
70
  apply: 'stroke',
152
71
  },
153
- globalAlpha: {
154
- svgAttr: 'opacity',
155
- canvas: 1,
156
- svg: 1,
157
- apply: 'fill stroke',
158
- },
159
72
  font: {
160
73
  canvas: '10px sans-serif',
161
74
  },
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
75
  textAlign: {
175
76
  canvas: 'start',
176
77
  },
177
78
  textBaseline: {
178
79
  canvas: 'alphabetic',
179
80
  },
81
+ // null rather than an empty array: the canvas default is "no dashes", which is
82
+ // also the svg default, and the two have to compare equal or every stroke
83
+ // carries an empty stroke-dasharray
180
84
  lineDash: {
181
85
  svgAttr: 'stroke-dasharray',
182
- canvas: [],
86
+ canvas: null,
183
87
  svg: null,
184
88
  apply: 'stroke',
185
89
  },
186
90
  };
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
- }
91
+ const rgbaRegex = /rgba\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d?\.?\d*)\s*\)/i;
223
92
  export class Context {
224
93
  width;
225
94
  height;
226
- enableMirroring;
227
95
  canvas;
228
96
  __document;
229
97
  __ctx;
230
98
  __canvas;
231
99
  __root;
232
- __ids;
233
- __defs;
234
100
  __currentElement;
235
101
  __styleStack;
236
102
  __groupStack;
@@ -238,48 +104,20 @@ export class Context {
238
104
  __currentPosition;
239
105
  __transformMatrix;
240
106
  __transformMatrixStack;
241
- __currentElementsToStyle;
242
- __options;
243
- __id;
244
- __fontUnderline;
245
- __fontHref;
107
+ // the parsed font, keyed by the `font` string that produced it: parsing costs
108
+ // a DOM element and a CSS parse, and a text-heavy export asks per glyph
109
+ __fontCache = new Map();
246
110
  // Style properties
247
111
  strokeStyle;
248
112
  fillStyle;
249
- lineCap;
250
- lineJoin;
251
- miterLimit;
252
113
  lineWidth;
253
- globalAlpha;
254
114
  font;
255
- shadowColor;
256
- shadowOffsetX;
257
- shadowOffsetY;
258
- shadowBlur;
259
115
  textAlign;
260
116
  textBaseline;
261
117
  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;
118
+ constructor(width, height, options = {}) {
119
+ this.width = width;
120
+ this.height = height;
283
121
  this.canvas = this;
284
122
  this.__document = options.document || document;
285
123
  if (options.ctx) {
@@ -295,31 +133,28 @@ export class Context {
295
133
  this.__root = this.__document.createElementNS('http://www.w3.org/2000/svg', 'svg');
296
134
  this.__root.setAttribute('version', '1.1');
297
135
  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);
136
+ this.__root.setAttribute('width', String(width));
137
+ this.__root.setAttribute('height', String(height));
304
138
  this.__currentElement = this.__document.createElementNS('http://www.w3.org/2000/svg', 'g');
305
139
  this.__root.appendChild(this.__currentElement);
306
140
  this.__currentDefaultPath = '';
307
141
  this.__currentPosition = {};
308
142
  this.__transformMatrix = new DOMMatrix();
309
143
  this.resetTransform();
310
- this.__options = options;
311
- this.__id = Math.random().toString(16).substring(2, 8);
312
144
  }
313
145
  __createElement(elementName, properties = {}, resetFill) {
314
146
  const element = this.__document.createElementNS('http://www.w3.org/2000/svg', elementName);
315
147
  if (resetFill) {
148
+ // a placeholder: whichever of fill()/stroke() paints this element
149
+ // overwrites its own, and __applyStyleToCurrentElement drops the other
150
+ // when it is the svg default anyway
316
151
  element.setAttribute('fill', 'none');
317
152
  element.setAttribute('stroke', 'none');
318
153
  }
319
154
  for (const key of Object.keys(properties)) {
320
155
  // 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
156
+ // "undefined". __applyText passes optional attributes through
157
+ // unconditionally, so without this every glyph in an export carries
323
158
  // text-decoration="undefined" -- invalid, and ~28 wasted bytes per letter
324
159
  const value = properties[key];
325
160
  if (value !== undefined && value !== null) {
@@ -347,77 +182,60 @@ export class Context {
347
182
  }
348
183
  return styleState;
349
184
  }
185
+ /**
186
+ * A translation is what an element's own x/y already say, and repeating it as
187
+ * a matrix costs ~35 bytes on every glyph of an alignment. Anything else --
188
+ * the scale a sequence-logo letter is stretched by -- still needs the matrix.
189
+ */
350
190
  __applyTransformation(element, matrix) {
351
191
  const { a, b, c, d, e, f } = matrix || this.getTransform();
192
+ if (a === 1 &&
193
+ b === 0 &&
194
+ c === 0 &&
195
+ d === 1 &&
196
+ element.hasAttribute('x') &&
197
+ element.hasAttribute('y')) {
198
+ if (e !== 0) {
199
+ element.setAttribute('x', String(Number(element.getAttribute('x')) + e));
200
+ }
201
+ if (f !== 0) {
202
+ element.setAttribute('y', String(Number(element.getAttribute('y')) + f));
203
+ }
204
+ return;
205
+ }
352
206
  element.setAttribute('transform', `matrix(${a} ${b} ${c} ${d} ${e} ${f})`);
353
207
  }
354
208
  __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) {
209
+ const currentElement = this.__currentElement;
210
+ for (const key of Object.keys(STYLES)) {
366
211
  const style = STYLES[key];
367
212
  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
- }
213
+ if (!style.apply?.includes(type) || style.svg === value) {
214
+ continue;
215
+ }
216
+ const matches = (style.svgAttr === 'stroke' || style.svgAttr === 'fill') &&
217
+ typeof value === 'string' &&
218
+ value.includes('rgba')
219
+ ? rgbaRegex.exec(value)
220
+ : null;
221
+ if (matches) {
222
+ // SVG has no rgba(): the alpha belongs in its own -opacity attribute
223
+ currentElement.setAttribute(style.svgAttr, `rgb(${matches[1]},${matches[2]},${matches[3]})`);
224
+ currentElement.setAttribute(`${style.svgAttr}-opacity`, matches[4]);
225
+ }
226
+ else if (key === 'lineWidth') {
227
+ const scale = this.__getTransformScale();
228
+ currentElement.setAttribute(style.svgAttr, String(value * Math.max(scale.x, scale.y)));
420
229
  }
230
+ else {
231
+ currentElement.setAttribute(style.svgAttr, value);
232
+ }
233
+ }
234
+ // a filled element with no stroke does not have to say so: `none` is what
235
+ // svg assumes. The reverse is not true -- an unstroked fill defaults to
236
+ // black -- so a stroked element keeps its fill="none"
237
+ if (type === 'fill' && currentElement.getAttribute('stroke') === 'none') {
238
+ currentElement.removeAttribute('stroke');
421
239
  }
422
240
  }
423
241
  __closestGroupOrSvg(node) {
@@ -427,22 +245,6 @@ export class Context {
427
245
  }
428
246
  return this.__closestGroupOrSvg(node.parentNode);
429
247
  }
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
248
  getSvg() {
447
249
  return this.__root;
448
250
  }
@@ -452,8 +254,7 @@ export class Context {
452
254
  this.__groupStack.push(parent);
453
255
  parent.appendChild(group);
454
256
  this.__currentElement = group;
455
- const style = this.__getStyleState();
456
- this.__styleStack.push(style);
257
+ this.__styleStack.push(this.__getStyleState());
457
258
  if (!this.__transformMatrixStack) {
458
259
  this.__transformMatrixStack = [];
459
260
  }
@@ -461,12 +262,10 @@ export class Context {
461
262
  }
462
263
  restore() {
463
264
  this.__currentElement = this.__groupStack.pop();
464
- this.__currentElementsToStyle = undefined;
465
265
  if (!this.__currentElement) {
466
- this.__currentElement = this.__root.childNodes[1];
266
+ this.__currentElement = this.__root.childNodes[0];
467
267
  }
468
- const state = this.__styleStack.pop();
469
- this.__applyStyleState(state);
268
+ this.__applyStyleState(this.__styleStack.pop());
470
269
  if (this.__transformMatrixStack && this.__transformMatrixStack.length > 0) {
471
270
  this.setTransform(this.__transformMatrixStack.pop());
472
271
  }
@@ -475,14 +274,12 @@ export class Context {
475
274
  this.__currentDefaultPath = '';
476
275
  this.__currentPosition = {};
477
276
  const path = this.__createElement('path', {}, true);
478
- const parent = this.__closestGroupOrSvg();
479
- parent.appendChild(path);
277
+ this.__closestGroupOrSvg().appendChild(path);
480
278
  this.__currentElement = path;
481
279
  }
482
280
  __applyCurrentDefaultPath() {
483
- const currentElement = this.__currentElement;
484
- if (currentElement.nodeName === 'path') {
485
- currentElement.setAttribute('d', this.__currentDefaultPath);
281
+ if (this.__currentElement.nodeName === 'path') {
282
+ this.__currentElement.setAttribute('d', this.__currentDefaultPath);
486
283
  }
487
284
  }
488
285
  __addPathCommand(command) {
@@ -494,10 +291,8 @@ export class Context {
494
291
  this.beginPath();
495
292
  }
496
293
  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
- }));
294
+ const p = this.__matrixTransform(x, y);
295
+ this.__addPathCommand(`M ${p.x} ${p.y}`);
501
296
  }
502
297
  closePath() {
503
298
  if (this.__currentDefaultPath) {
@@ -506,171 +301,108 @@ export class Context {
506
301
  }
507
302
  lineTo(x, y) {
508
303
  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
- }));
304
+ const p = this.__matrixTransform(x, y);
305
+ this.__addPathCommand(this.__currentDefaultPath.includes('M')
306
+ ? `L ${p.x} ${p.y}`
307
+ : `M ${p.x} ${p.y}`);
532
308
  }
533
309
  quadraticCurveTo(cpx, cpy, x, y) {
534
310
  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
- }));
311
+ const cp = this.__matrixTransform(cpx, cpy);
312
+ const p = this.__matrixTransform(x, y);
313
+ this.__addPathCommand(`Q ${cp.x} ${cp.y} ${p.x} ${p.y}`);
541
314
  }
542
- stroke() {
543
- if (this.__currentElement.nodeName === 'path') {
544
- this.__currentElement.setAttribute('paint-order', 'fill stroke markers');
315
+ // paint-order only says something when the element carries both a fill and a
316
+ // stroke, which is the collapsed-clade triangle and nothing else
317
+ __applyPaintOrder(order, other) {
318
+ const current = this.__currentElement;
319
+ if (current.nodeName === 'path' &&
320
+ current.hasAttribute(other) &&
321
+ current.getAttribute(other) !== 'none') {
322
+ current.setAttribute('paint-order', order);
545
323
  }
324
+ }
325
+ stroke() {
326
+ this.__applyPaintOrder('fill stroke markers', 'fill');
546
327
  this.__applyCurrentDefaultPath();
547
328
  this.__applyStyleToCurrentElement('stroke');
548
329
  }
549
330
  fill() {
550
- if (this.__currentElement.nodeName === 'path') {
551
- this.__currentElement.setAttribute('paint-order', 'stroke fill markers');
552
- }
331
+ this.__applyPaintOrder('stroke fill markers', 'stroke');
553
332
  this.__applyCurrentDefaultPath();
554
333
  this.__applyStyleToCurrentElement('fill');
555
334
  }
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
- }
335
+ // A fill that happens to cover the context is still a fill: it paints over
336
+ // what is under it, exactly as the canvas would. Treating it as a clear threw
337
+ // the drawing away instead -- a full-width highlight band over an alignment
338
+ // took the export from 124 letters to 4.
574
339
  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
340
  const rect = this.__createElement('rect', { x, y, width, height }, true);
585
- const parent = this.__closestGroupOrSvg();
586
- parent.appendChild(rect);
341
+ this.__closestGroupOrSvg().appendChild(rect);
587
342
  this.__currentElement = rect;
588
343
  this.__applyTransformation(rect);
589
344
  this.__applyStyleToCurrentElement('fill');
590
345
  }
591
346
  strokeRect(x, y, width, height) {
592
347
  const rect = this.__createElement('rect', { x, y, width, height }, true);
593
- const parent = this.__closestGroupOrSvg();
594
- parent.appendChild(rect);
348
+ this.__closestGroupOrSvg().appendChild(rect);
595
349
  this.__currentElement = rect;
596
350
  this.__applyTransformation(rect);
597
351
  this.__applyStyleToCurrentElement('stroke');
598
352
  }
599
353
  clearRect(x, y, width, height) {
600
354
  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
- }
355
+ if (a === 1 &&
356
+ b === 0 &&
357
+ c === 0 &&
358
+ d === 1 &&
359
+ e === 0 &&
360
+ f === 0 &&
361
+ x === 0 &&
362
+ y === 0 &&
363
+ width === this.width &&
364
+ height === this.height) {
365
+ this.__clearCanvas();
366
+ return;
609
367
  }
610
368
  const rect = this.__createElement('rect', { x, y, width, height, fill: '#FFFFFF' }, true);
611
369
  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) {
370
+ this.__closestGroupOrSvg().appendChild(rect);
371
+ }
372
+ __clearCanvas() {
373
+ this.__root.removeChild(this.__root.childNodes[0]);
374
+ this.__currentElement = this.__document.createElementNS('http://www.w3.org/2000/svg', 'g');
375
+ this.__root.appendChild(this.__currentElement);
376
+ this.__groupStack = [];
377
+ }
378
+ __parsedFont() {
379
+ const hit = this.__fontCache.get(this.font);
380
+ if (hit) {
381
+ return hit;
382
+ }
641
383
  const el = document.createElement('span');
642
- el.setAttribute('style', 'font:' + this.font);
643
- const style = el.style;
384
+ el.setAttribute('style', `font:${this.font}`);
385
+ this.__fontCache.set(this.font, el.style);
386
+ return el.style;
387
+ }
388
+ fillText(text, x, y) {
389
+ const style = this.__parsedFont();
644
390
  const parent = this.__closestGroupOrSvg();
645
391
  const textElement = this.__createElement('text', {
646
392
  'font-family': style.fontFamily,
647
393
  'font-size': style.fontSize,
648
394
  'font-style': omitDefault('font-style', style.fontStyle),
649
395
  'font-weight': omitDefault('font-weight', style.fontWeight),
650
- 'text-decoration': this.__fontUnderline,
651
- x: x,
652
- y: y,
396
+ x,
397
+ y,
653
398
  'text-anchor': getTextAnchor(this.textAlign),
654
399
  'dominant-baseline': omitDefault('dominant-baseline', getDominantBaseline(this.textBaseline)),
655
400
  }, true);
656
401
  textElement.appendChild(this.__document.createTextNode(text));
657
402
  this.__currentElement = textElement;
658
403
  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');
404
+ this.__applyStyleToCurrentElement('fill');
405
+ parent.appendChild(textElement);
674
406
  }
675
407
  measureText(text) {
676
408
  this.__ctx.font = this.font;
@@ -692,60 +424,31 @@ export class Context {
692
424
  const startX = x + radius * Math.cos(startAngle);
693
425
  const startY = y + radius * Math.sin(startAngle);
694
426
  const sweepFlag = counterClockwise ? 0 : 1;
695
- let largeArcFlag = 0;
696
427
  let diff = endAngle - startAngle;
697
428
  if (diff < 0) {
698
429
  diff += 2 * Math.PI;
699
430
  }
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);
431
+ const largeArcFlag = counterClockwise
432
+ ? diff > Math.PI
433
+ ? 0
434
+ : 1
435
+ : diff > Math.PI
436
+ ? 1
437
+ : 0;
438
+ const { x: scaleX, y: scaleY } = this.__getTransformScale();
439
+ const end = this.__matrixTransform(endX, endY);
708
440
  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
- }));
441
+ this.__addPathCommand(`A ${radius * scaleX} ${radius * scaleY} 0 ${largeArcFlag} ${sweepFlag} ${end.x} ${end.y}`);
718
442
  this.__currentPosition = { x: endX, y: endY };
719
443
  }
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
444
  setLineDash(dashArray) {
735
- if (dashArray && dashArray.length > 0) {
736
- this.lineDash = dashArray.join(',');
737
- }
738
- else {
739
- this.lineDash = null;
740
- }
445
+ this.lineDash = dashArray.length > 0 ? dashArray.join(',') : null;
741
446
  }
742
447
  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
- }
448
+ this.__transformMatrix =
449
+ a instanceof DOMMatrix
450
+ ? new DOMMatrix([a.a, a.b, a.c, a.d, a.e, a.f])
451
+ : new DOMMatrix([a, b, c, d, e, f]);
749
452
  }
750
453
  getTransform() {
751
454
  const { a, b, c, d, e, f } = this.__transformMatrix;
@@ -761,27 +464,10 @@ export class Context {
761
464
  if (isNaN(x) || isNaN(y) || !isFinite(x) || !isFinite(y)) {
762
465
  return;
763
466
  }
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);
467
+ this.setTransform(this.getTransform().scale(x, y));
777
468
  }
778
469
  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);
470
+ this.setTransform(this.getTransform().translate(x, y));
785
471
  }
786
472
  __matrixTransform(x, y) {
787
473
  return new DOMPoint(x, y).matrixTransform(this.__transformMatrix);
@@ -792,62 +478,5 @@ export class Context {
792
478
  y: Math.hypot(this.__transformMatrix.c, this.__transformMatrix.d),
793
479
  };
794
480
  }
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
481
  }
853
482
  //# sourceMappingURL=index.js.map