@kwiz/fluentui 1.0.39 → 1.0.41

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. package/dist/controls/ColorPickerDialog.d.ts +13 -0
  2. package/dist/controls/ColorPickerDialog.js +34 -0
  3. package/dist/controls/ColorPickerDialog.js.map +1 -0
  4. package/dist/controls/canvas/CustomEventTargetBase.d.ts +7 -0
  5. package/dist/controls/canvas/CustomEventTargetBase.js +22 -0
  6. package/dist/controls/canvas/CustomEventTargetBase.js.map +1 -0
  7. package/dist/controls/canvas/DrawPad.d.ts +15 -0
  8. package/dist/controls/canvas/DrawPad.js +151 -0
  9. package/dist/controls/canvas/DrawPad.js.map +1 -0
  10. package/dist/controls/canvas/DrawPadManager.d.ts +84 -0
  11. package/dist/controls/canvas/DrawPadManager.js +478 -0
  12. package/dist/controls/canvas/DrawPadManager.js.map +1 -0
  13. package/dist/controls/canvas/bezier.d.ts +17 -0
  14. package/dist/controls/canvas/bezier.js +65 -0
  15. package/dist/controls/canvas/bezier.js.map +1 -0
  16. package/dist/controls/canvas/point.d.ts +16 -0
  17. package/dist/controls/canvas/point.js +26 -0
  18. package/dist/controls/canvas/point.js.map +1 -0
  19. package/dist/controls/file-upload.d.ts +8 -3
  20. package/dist/controls/file-upload.js +110 -28
  21. package/dist/controls/file-upload.js.map +1 -1
  22. package/dist/controls/kwizoverflow.js +3 -1
  23. package/dist/controls/kwizoverflow.js.map +1 -1
  24. package/dist/helpers/drag-drop/exports.d.ts +8 -0
  25. package/dist/helpers/drag-drop/exports.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/package.json +3 -2
  30. package/src/controls/ColorPickerDialog.tsx +76 -0
  31. package/src/controls/canvas/CustomEventTargetBase.ts +33 -0
  32. package/src/controls/canvas/DrawPad.tsx +195 -0
  33. package/src/controls/canvas/DrawPadManager.ts +668 -0
  34. package/src/controls/canvas/bezier.ts +110 -0
  35. package/src/controls/canvas/point.ts +45 -0
  36. package/src/controls/file-upload.tsx +117 -36
  37. package/src/controls/kwizoverflow.tsx +5 -2
  38. package/src/helpers/drag-drop/exports.ts +11 -1
  39. package/src/index.ts +2 -0
@@ -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
+ }