@kwiz/fluentui 1.0.40 → 1.0.41

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.
@@ -0,0 +1,668 @@
1
+ import { GetDefaultProp, isNotEmptyArray, isNullOrEmptyString, throttle } from "@kwiz/common";
2
+ import { CustomEventTargetBase } from './CustomEventTargetBase';
3
+ import { Bezier } from './bezier';
4
+ import { BasicPoint, Point } from './point';
5
+
6
+ declare global {
7
+ interface CSSStyleDeclaration {
8
+ msTouchAction: string | null;
9
+ }
10
+ }
11
+
12
+ export type DrawPadEvent = MouseEvent | Touch | PointerEvent;
13
+
14
+ export interface FromDataOptions {
15
+ clear?: boolean;
16
+ }
17
+
18
+ export interface PointGroupOptions {
19
+ dotSize: number;
20
+ minWidth: number;
21
+ maxWidth: number;
22
+ penColor: string;
23
+ }
24
+
25
+ export interface Options extends Partial<PointGroupOptions> {
26
+ minDistance?: number;
27
+ velocityFilterWeight?: number;
28
+ backgroundColor?: string;
29
+ throttle?: number;
30
+ }
31
+
32
+ export interface PointGroup extends PointGroupOptions {
33
+ points: BasicPoint[];
34
+ }
35
+
36
+ //inspired by https://www.npmjs.com/package/signature_pad
37
+
38
+ export default class DrawPadManager extends CustomEventTargetBase {
39
+ // Public stuff
40
+ public dotSize = GetDefaultProp<number>(0);
41
+ public minWidth = GetDefaultProp<number>(0.5);
42
+ public maxWidth = GetDefaultProp<number>(2.5);
43
+ public penColor = GetDefaultProp<string>("black");
44
+
45
+ public minDistance = GetDefaultProp<number>(5);
46
+ public velocityFilterWeight = GetDefaultProp<number>(0.7);
47
+ public backgroundColor = GetDefaultProp<string>(null);
48
+ public throttle = GetDefaultProp<number>(16);
49
+
50
+ // Private stuff
51
+ private _ctx: CanvasRenderingContext2D;
52
+ private _drawningStroke: boolean;
53
+ private _isEmpty: boolean;
54
+ private _lastPoints: Point[]; // Stores up to 4 most recent points; used to generate a new curve
55
+ private _data: PointGroup[]; // Stores all points in groups (one group per line or dot)
56
+ private _lastVelocity: number;
57
+ private _lastWidth: number;
58
+ private _strokeMoveUpdate: (event: DrawPadEvent) => void;
59
+
60
+ public constructor(private canvas: HTMLCanvasElement, options: Options = {}) {
61
+ super();
62
+ this.velocityFilterWeight.value = options.velocityFilterWeight;
63
+ this.minWidth.value = options.minWidth;
64
+ this.maxWidth.value = options.maxWidth;
65
+ this.throttle.value = options.throttle; // in milisecondss
66
+ this.minDistance.value = options.minDistance; // in pixels
67
+ this.dotSize.value = options.dotSize;
68
+ this.penColor.value = options.penColor;
69
+ this.backgroundColor.value = options.backgroundColor;
70
+
71
+ this._strokeMoveUpdate = this.throttle.value
72
+ ? throttle(this._strokeUpdate, this.throttle.value, this)
73
+ : this._strokeUpdate;
74
+ this._ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
75
+
76
+ this.clear();
77
+
78
+ // Enable mouse and touch event handlers
79
+ this.on();
80
+ }
81
+
82
+ public clear(): void {
83
+ const { _ctx: ctx, canvas } = this;
84
+
85
+ // Clear canvas using background color
86
+ ctx.fillStyle = this.backgroundColor.value;
87
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
88
+ if (!isNullOrEmptyString(this.backgroundColor.value))//otherwise, leave it transparent
89
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
90
+
91
+ this._data = [];
92
+ this._reset();
93
+ this._isEmpty = true;
94
+
95
+ this.resizeCanvas();
96
+ }
97
+
98
+ public fromDataURL(
99
+ dataUrl: string,
100
+ options: {
101
+ clear?: boolean;
102
+ } = {},
103
+ ): Promise<void> {
104
+ return new Promise((resolve, reject) => {
105
+ const img = new Image();
106
+ this._reset();
107
+ img.onload = (): void => {
108
+ if (options.clear) {
109
+ this.clear();
110
+ }
111
+ const width = Math.min(this.canvas.width, img.width);
112
+ const height = Math.min(this.canvas.height, img.height);
113
+ var centerShift_x = this.canvas.width > img.width ? width / 2 : 0;
114
+ var centerShift_y = this.canvas.height > img.height ? height / 2 : 0;
115
+ this._ctx.drawImage(img, centerShift_x, centerShift_y, width, height);
116
+ resolve();
117
+ };
118
+ img.onerror = (error): void => {
119
+ reject(error);
120
+ };
121
+ img.crossOrigin = 'anonymous';
122
+ img.src = dataUrl;
123
+
124
+ this._isEmpty = false;
125
+ });
126
+ }
127
+
128
+ public toDataURL(type: 'image/png' | 'image/jpeg' | 'image/svg+xml' = 'image/png', encoderOptions?: number): string {
129
+ switch (type) {
130
+ case 'image/svg+xml':
131
+ return this._toSVG();
132
+ default:
133
+ return this.canvas.toDataURL(type, encoderOptions);
134
+ }
135
+ }
136
+
137
+ public on(): void {
138
+ // Disable panning/zooming when touching canvas element
139
+ this.canvas.style.touchAction = 'none';
140
+ this.canvas.style.msTouchAction = 'none';
141
+ this.canvas.style.userSelect = 'none';
142
+
143
+ const isIOS =
144
+ /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
145
+
146
+ // The "Scribble" feature of iOS intercepts point events. So that we can lose some of them when tapping rapidly.
147
+ // Use touch events for iOS platforms to prevent it. See https://developer.apple.com/forums/thread/664108 for more information.
148
+ if (window.PointerEvent && !isIOS) {
149
+ this._handlePointerEvents();
150
+ } else {
151
+ this._handleMouseEvents();
152
+
153
+ if ('ontouchstart' in window) {
154
+ this._handleTouchEvents();
155
+ }
156
+ }
157
+ }
158
+
159
+ public off(): void {
160
+ // Enable panning/zooming when touching canvas element
161
+ this.canvas.style.touchAction = 'auto';
162
+ this.canvas.style.msTouchAction = 'auto';
163
+ this.canvas.style.userSelect = 'auto';
164
+
165
+ this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
166
+ this.canvas.removeEventListener('pointermove', this._handlePointerMove);
167
+ document.removeEventListener('pointerup', this._handlePointerEnd);
168
+
169
+ this.canvas.removeEventListener('mousedown', this._handleMouseDown);
170
+ this.canvas.removeEventListener('mousemove', this._handleMouseMove);
171
+ document.removeEventListener('mouseup', this._handleMouseUp);
172
+
173
+ this.canvas.removeEventListener('touchstart', this._handleTouchStart);
174
+ this.canvas.removeEventListener('touchmove', this._handleTouchMove);
175
+ this.canvas.removeEventListener('touchend', this._handleTouchEnd);
176
+ }
177
+
178
+ public isEmpty(): boolean {
179
+ return this._isEmpty;
180
+ }
181
+
182
+ public canUndo() {
183
+ var data = this.toData();
184
+ return isNotEmptyArray(data);
185
+ }
186
+ public undoLast() {
187
+ if (this.canUndo()) {
188
+ var data = this.toData();
189
+ data.pop(); // remove the last dot or line
190
+ this.fromData(data);
191
+ }
192
+ }
193
+ public resizeCanvas() {
194
+ var ratio = Math.max(window.devicePixelRatio || 1, 1);
195
+ this.canvas.width = this.canvas.offsetWidth * ratio;
196
+ this.canvas.height = this.canvas.offsetHeight * ratio;
197
+ this.canvas.getContext("2d").scale(ratio, ratio);
198
+ }
199
+
200
+ public fromData(
201
+ pointGroups: PointGroup[],
202
+ { clear = true }: FromDataOptions = {},
203
+ ): void {
204
+ if (clear) {
205
+ this.clear();
206
+ }
207
+
208
+ this._fromData(
209
+ pointGroups,
210
+ this._drawCurve.bind(this),
211
+ this._drawDot.bind(this),
212
+ );
213
+
214
+ this._data = clear ? pointGroups : this._data.concat(pointGroups);
215
+ }
216
+
217
+ public toData(): PointGroup[] {
218
+ return this._data;
219
+ }
220
+
221
+ // Event handlers
222
+ private _handleMouseDown = (event: MouseEvent): void => {
223
+ if (event.buttons === 1) {
224
+ this._drawningStroke = true;
225
+ this._strokeBegin(event);
226
+ }
227
+ };
228
+
229
+ private _handleMouseMove = (event: MouseEvent): void => {
230
+ if (this._drawningStroke) {
231
+ this._strokeMoveUpdate(event);
232
+ }
233
+ };
234
+
235
+ private _handleMouseUp = (event: MouseEvent): void => {
236
+ if (event.buttons === 1 && this._drawningStroke) {
237
+ this._drawningStroke = false;
238
+ this._strokeEnd(event);
239
+ }
240
+ };
241
+
242
+ private _handleTouchStart = (event: TouchEvent): void => {
243
+ // Prevent scrolling.
244
+ event.preventDefault();
245
+
246
+ if (event.targetTouches.length === 1) {
247
+ const touch = event.changedTouches[0];
248
+ this._strokeBegin(touch);
249
+ }
250
+ };
251
+
252
+ private _handleTouchMove = (event: TouchEvent): void => {
253
+ // Prevent scrolling.
254
+ event.preventDefault();
255
+
256
+ const touch = event.targetTouches[0];
257
+ this._strokeMoveUpdate(touch);
258
+ };
259
+
260
+ private _handleTouchEnd = (event: TouchEvent): void => {
261
+ const wasCanvasTouched = event.target === this.canvas;
262
+ if (wasCanvasTouched) {
263
+ event.preventDefault();
264
+
265
+ const touch = event.changedTouches[0];
266
+ this._strokeEnd(touch);
267
+ }
268
+ };
269
+
270
+ private _handlePointerStart = (event: PointerEvent): void => {
271
+ this._drawningStroke = true;
272
+ event.preventDefault();
273
+ this._strokeBegin(event);
274
+ };
275
+
276
+ private _handlePointerMove = (event: PointerEvent): void => {
277
+ if (this._drawningStroke) {
278
+ event.preventDefault();
279
+ this._strokeMoveUpdate(event);
280
+ }
281
+ };
282
+
283
+ private _handlePointerEnd = (event: PointerEvent): void => {
284
+ this._drawningStroke = false;
285
+ const wasCanvasTouched = event.target === this.canvas;
286
+ if (wasCanvasTouched) {
287
+ event.preventDefault();
288
+ this._strokeEnd(event);
289
+ }
290
+ };
291
+
292
+ // Private methods
293
+ private _strokeBegin(event: DrawPadEvent): void {
294
+ this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
295
+
296
+ const newPointGroup: PointGroup = {
297
+ dotSize: this.dotSize.value,
298
+ minWidth: this.minWidth.value,
299
+ maxWidth: this.maxWidth.value,
300
+ penColor: this.penColor.value,
301
+ points: [],
302
+ };
303
+
304
+ this._data.push(newPointGroup);
305
+ this._reset();
306
+ this._strokeUpdate(event);
307
+ }
308
+
309
+ private _strokeUpdate(event: DrawPadEvent): void {
310
+ if (this._data.length === 0) {
311
+ // This can happen if clear() was called while a drawing is still in progress,
312
+ // or if there is a race condition between start/update events.
313
+ this._strokeBegin(event);
314
+ return;
315
+ }
316
+
317
+ this.dispatchEvent(
318
+ new CustomEvent('beforeUpdateStroke', { detail: event }),
319
+ );
320
+
321
+ const x = event.clientX;
322
+ const y = event.clientY;
323
+ const pressure =
324
+ (event as PointerEvent).pressure !== undefined
325
+ ? (event as PointerEvent).pressure
326
+ : (event as Touch).force !== undefined
327
+ ? (event as Touch).force
328
+ : 0;
329
+
330
+ const point = this._createPoint(x, y, pressure);
331
+ const lastPointGroup = this._data[this._data.length - 1];
332
+ const lastPoints = lastPointGroup.points;
333
+ const lastPoint =
334
+ lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
335
+ const isLastPointTooClose = lastPoint
336
+ ? point.distanceTo(lastPoint) <= this.minDistance.value
337
+ : false;
338
+ const { penColor, dotSize, minWidth, maxWidth } = lastPointGroup;
339
+
340
+ // Skip this point if it's too close to the previous one
341
+ if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
342
+ const curve = this._addPoint(point);
343
+
344
+ if (!lastPoint) {
345
+ this._drawDot(point, {
346
+ penColor,
347
+ dotSize,
348
+ minWidth,
349
+ maxWidth,
350
+ });
351
+ } else if (curve) {
352
+ this._drawCurve(curve, {
353
+ penColor,
354
+ dotSize,
355
+ minWidth,
356
+ maxWidth,
357
+ });
358
+ }
359
+
360
+ lastPoints.push({
361
+ time: point.time,
362
+ x: point.x,
363
+ y: point.y,
364
+ pressure: point.pressure,
365
+ });
366
+ }
367
+
368
+ this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
369
+ }
370
+
371
+ private _strokeEnd(event: DrawPadEvent): void {
372
+ this._strokeUpdate(event);
373
+
374
+ this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
375
+ }
376
+
377
+ private _handlePointerEvents(): void {
378
+ this._drawningStroke = false;
379
+
380
+ this.canvas.addEventListener('pointerdown', this._handlePointerStart);
381
+ this.canvas.addEventListener('pointermove', this._handlePointerMove);
382
+ document.addEventListener('pointerup', this._handlePointerEnd);
383
+ }
384
+
385
+ private _handleMouseEvents(): void {
386
+ this._drawningStroke = false;
387
+
388
+ this.canvas.addEventListener('mousedown', this._handleMouseDown);
389
+ this.canvas.addEventListener('mousemove', this._handleMouseMove);
390
+ document.addEventListener('mouseup', this._handleMouseUp);
391
+ }
392
+
393
+ private _handleTouchEvents(): void {
394
+ this.canvas.addEventListener('touchstart', this._handleTouchStart);
395
+ this.canvas.addEventListener('touchmove', this._handleTouchMove);
396
+ this.canvas.addEventListener('touchend', this._handleTouchEnd);
397
+ }
398
+
399
+ // Called when a new line is started
400
+ private _reset(): void {
401
+ this._lastPoints = [];
402
+ this._lastVelocity = 0;
403
+ this._lastWidth = (this.minWidth.value + this.maxWidth.value) / 2;
404
+ this._ctx.fillStyle = this.penColor.value;
405
+ }
406
+
407
+ private _createPoint(x: number, y: number, pressure: number): Point {
408
+ const rect = this.canvas.getBoundingClientRect();
409
+
410
+ return new Point(
411
+ x - rect.left,
412
+ y - rect.top,
413
+ pressure,
414
+ new Date().getTime(),
415
+ );
416
+ }
417
+
418
+ // Add point to _lastPoints array and generate a new curve if there are enough points (i.e. 3)
419
+ private _addPoint(point: Point): Bezier | null {
420
+ const { _lastPoints } = this;
421
+
422
+ _lastPoints.push(point);
423
+
424
+ if (_lastPoints.length > 2) {
425
+ // To reduce the initial lag make it work with 3 points
426
+ // by copying the first point to the beginning.
427
+ if (_lastPoints.length === 3) {
428
+ _lastPoints.unshift(_lastPoints[0]);
429
+ }
430
+
431
+ // _points array will always have 4 points here.
432
+ const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2]);
433
+ const curve = Bezier.fromPoints(_lastPoints, widths);
434
+
435
+ // Remove the first element from the list, so that there are no more than 4 points at any time.
436
+ _lastPoints.shift();
437
+
438
+ return curve;
439
+ }
440
+
441
+ return null;
442
+ }
443
+
444
+ private _calculateCurveWidths(
445
+ startPoint: Point,
446
+ endPoint: Point,
447
+ ): { start: number; end: number; } {
448
+ const velocity =
449
+ this.velocityFilterWeight.value * endPoint.velocityFrom(startPoint) +
450
+ (1 - this.velocityFilterWeight.value) * this._lastVelocity;
451
+
452
+ const newWidth = this._strokeWidth(velocity);
453
+
454
+ const widths = {
455
+ end: newWidth,
456
+ start: this._lastWidth,
457
+ };
458
+
459
+ this._lastVelocity = velocity;
460
+ this._lastWidth = newWidth;
461
+
462
+ return widths;
463
+ }
464
+
465
+ private _strokeWidth(velocity: number): number {
466
+ return Math.max(this.maxWidth.value / (velocity + 1), this.minWidth.value);
467
+ }
468
+
469
+ private _drawCurveSegment(x: number, y: number, width: number): void {
470
+ const ctx = this._ctx;
471
+
472
+ ctx.moveTo(x, y);
473
+ ctx.arc(x, y, width, 0, 2 * Math.PI, false);
474
+ this._isEmpty = false;
475
+ }
476
+
477
+ private _drawCurve(curve: Bezier, options: PointGroupOptions): void {
478
+ const ctx = this._ctx;
479
+ const widthDelta = curve.endWidth - curve.startWidth;
480
+ // '2' is just an arbitrary number here. If only lenght is used, then
481
+ // there are gaps between curve segments :/
482
+ const drawSteps = Math.ceil(curve.length()) * 2;
483
+
484
+ ctx.beginPath();
485
+ ctx.fillStyle = options.penColor;
486
+
487
+ for (let i = 0; i < drawSteps; i += 1) {
488
+ // Calculate the Bezier (x, y) coordinate for this step.
489
+ const t = i / drawSteps;
490
+ const tt = t * t;
491
+ const ttt = tt * t;
492
+ const u = 1 - t;
493
+ const uu = u * u;
494
+ const uuu = uu * u;
495
+
496
+ let x = uuu * curve.startPoint.x;
497
+ x += 3 * uu * t * curve.control1.x;
498
+ x += 3 * u * tt * curve.control2.x;
499
+ x += ttt * curve.endPoint.x;
500
+
501
+ let y = uuu * curve.startPoint.y;
502
+ y += 3 * uu * t * curve.control1.y;
503
+ y += 3 * u * tt * curve.control2.y;
504
+ y += ttt * curve.endPoint.y;
505
+
506
+ const width = Math.min(
507
+ curve.startWidth + ttt * widthDelta,
508
+ options.maxWidth,
509
+ );
510
+ this._drawCurveSegment(x, y, width);
511
+ }
512
+
513
+ ctx.closePath();
514
+ ctx.fill();
515
+ }
516
+
517
+ private _drawDot(point: BasicPoint, options: PointGroupOptions): void {
518
+ const ctx = this._ctx;
519
+ const width =
520
+ options.dotSize > 0
521
+ ? options.dotSize
522
+ : (options.minWidth + options.maxWidth) / 2;
523
+
524
+ ctx.beginPath();
525
+ this._drawCurveSegment(point.x, point.y, width);
526
+ ctx.closePath();
527
+ ctx.fillStyle = options.penColor;
528
+ ctx.fill();
529
+ }
530
+
531
+ private _fromData(
532
+ pointGroups: PointGroup[],
533
+ drawCurve: DrawPadManager['_drawCurve'],
534
+ drawDot: DrawPadManager['_drawDot'],
535
+ ): void {
536
+ for (const group of pointGroups) {
537
+ const { penColor, dotSize, minWidth, maxWidth, points } = group;
538
+
539
+ if (points.length > 1) {
540
+ for (let j = 0; j < points.length; j += 1) {
541
+ const basicPoint = points[j];
542
+ const point = new Point(
543
+ basicPoint.x,
544
+ basicPoint.y,
545
+ basicPoint.pressure,
546
+ basicPoint.time,
547
+ );
548
+
549
+ // All points in the group have the same color, so it's enough to set
550
+ // penColor just at the beginning.
551
+ this.penColor.value = penColor;
552
+
553
+ if (j === 0) {
554
+ this._reset();
555
+ }
556
+
557
+ const curve = this._addPoint(point);
558
+
559
+ if (curve) {
560
+ drawCurve(curve, {
561
+ penColor,
562
+ dotSize,
563
+ minWidth,
564
+ maxWidth,
565
+ });
566
+ }
567
+ }
568
+ } else {
569
+ this._reset();
570
+
571
+ drawDot(points[0], {
572
+ penColor,
573
+ dotSize,
574
+ minWidth,
575
+ maxWidth,
576
+ });
577
+ }
578
+ }
579
+ }
580
+
581
+ private _toSVG(): string {
582
+ const pointGroups = this._data;
583
+ const ratio = Math.max(window.devicePixelRatio || 1, 1);
584
+ const minX = 0;
585
+ const minY = 0;
586
+ const maxX = this.canvas.width / ratio;
587
+ const maxY = this.canvas.height / ratio;
588
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
589
+
590
+ svg.setAttribute('width', this.canvas.width.toString());
591
+ svg.setAttribute('height', this.canvas.height.toString());
592
+
593
+ this._fromData(
594
+ pointGroups,
595
+
596
+ (curve, { penColor }) => {
597
+ const path = document.createElement('path');
598
+
599
+ // Need to check curve for NaN values, these pop up when drawing
600
+ // lines on the canvas that are not continuous. E.g. Sharp corners
601
+ // or stopping mid-stroke and than continuing without lifting mouse.
602
+ /* eslint-disable no-restricted-globals */
603
+ if (
604
+ !isNaN(curve.control1.x) &&
605
+ !isNaN(curve.control1.y) &&
606
+ !isNaN(curve.control2.x) &&
607
+ !isNaN(curve.control2.y)
608
+ ) {
609
+ const attr =
610
+ `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(
611
+ 3,
612
+ )} ` +
613
+ `C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
614
+ `${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
615
+ `${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
616
+ path.setAttribute('d', attr);
617
+ path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
618
+ path.setAttribute('stroke', penColor);
619
+ path.setAttribute('fill', 'none');
620
+ path.setAttribute('stroke-linecap', 'round');
621
+
622
+ svg.appendChild(path);
623
+ }
624
+ /* eslint-enable no-restricted-globals */
625
+ },
626
+
627
+ (point, { penColor, dotSize, minWidth, maxWidth }) => {
628
+ const circle = document.createElement('circle');
629
+ const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
630
+ circle.setAttribute('r', size.toString());
631
+ circle.setAttribute('cx', point.x.toString());
632
+ circle.setAttribute('cy', point.y.toString());
633
+ circle.setAttribute('fill', penColor);
634
+
635
+ svg.appendChild(circle);
636
+ },
637
+ );
638
+
639
+ const prefix = 'data:image/svg+xml;base64,';
640
+ const header =
641
+ '<svg' +
642
+ ' xmlns="http://www.w3.org/2000/svg"' +
643
+ ' xmlns:xlink="http://www.w3.org/1999/xlink"' +
644
+ ` viewBox="${minX} ${minY} ${this.canvas.width} ${this.canvas.height}"` +
645
+ ` width="${maxX}"` +
646
+ ` height="${maxY}"` +
647
+ '>';
648
+ let body = svg.innerHTML;
649
+
650
+ // IE hack for missing innerHTML property on SVGElement
651
+ if (body === undefined) {
652
+ const dummy = document.createElement('dummy');
653
+ const nodes = svg.childNodes;
654
+ dummy.innerHTML = '';
655
+
656
+ for (let i = 0; i < nodes.length; i += 1) {
657
+ dummy.appendChild(nodes[i].cloneNode(true));
658
+ }
659
+
660
+ body = dummy.innerHTML;
661
+ }
662
+
663
+ const footer = '</svg>';
664
+ const data = header + body + footer;
665
+
666
+ return prefix + btoa(data);
667
+ }
668
+ }