@kwiz/fluentui 1.0.73 → 1.0.75

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