@kwiz/fluentui 1.0.40 → 1.0.41
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/controls/ColorPickerDialog.d.ts +13 -0
- package/dist/controls/ColorPickerDialog.js +34 -0
- package/dist/controls/ColorPickerDialog.js.map +1 -0
- package/dist/controls/canvas/CustomEventTargetBase.d.ts +7 -0
- package/dist/controls/canvas/CustomEventTargetBase.js +22 -0
- package/dist/controls/canvas/CustomEventTargetBase.js.map +1 -0
- package/dist/controls/canvas/DrawPad.d.ts +15 -0
- package/dist/controls/canvas/DrawPad.js +151 -0
- package/dist/controls/canvas/DrawPad.js.map +1 -0
- package/dist/controls/canvas/DrawPadManager.d.ts +84 -0
- package/dist/controls/canvas/DrawPadManager.js +478 -0
- package/dist/controls/canvas/DrawPadManager.js.map +1 -0
- package/dist/controls/canvas/bezier.d.ts +17 -0
- package/dist/controls/canvas/bezier.js +65 -0
- package/dist/controls/canvas/bezier.js.map +1 -0
- package/dist/controls/canvas/point.d.ts +16 -0
- package/dist/controls/canvas/point.js +26 -0
- package/dist/controls/canvas/point.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/controls/ColorPickerDialog.tsx +76 -0
- package/src/controls/canvas/CustomEventTargetBase.ts +33 -0
- package/src/controls/canvas/DrawPad.tsx +195 -0
- package/src/controls/canvas/DrawPadManager.ts +668 -0
- package/src/controls/canvas/bezier.ts +110 -0
- package/src/controls/canvas/point.ts +45 -0
- 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
|
+
}
|