@kwiz/fluentui 1.0.40 → 1.0.42

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