@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.
- package/.github/workflows/npm-publish.yml +24 -24
- package/LICENSE +21 -21
- package/README.md +53 -53
- package/dist/@types/forwardRef.d.ts +0 -0
- package/dist/@types/forwardRef.js +1 -0
- package/dist/@types/forwardRef.js.map +1 -0
- package/dist/controls/error-boundary copy.d.ts +23 -0
- package/dist/controls/error-boundary copy.js +33 -0
- package/dist/controls/error-boundary copy.js.map +1 -0
- package/dist/controls/menu.js +2 -2
- package/dist/controls/menu.js.map +1 -1
- package/dist/controls/search.js +19 -11
- package/dist/controls/search.js.map +1 -1
- package/dist/controls/svg.js +21 -21
- package/dist/controls/svg.js.map +1 -1
- package/dist/helpers/common.d.ts +4 -0
- package/dist/helpers/common.js +2 -0
- package/dist/helpers/common.js.map +1 -0
- package/dist/helpers/context.d.ts +26 -0
- package/dist/helpers/context.js +15 -0
- package/dist/helpers/context.js.map +1 -0
- package/dist/helpers/drag-drop/exports.d.ts +12 -0
- package/dist/helpers/drag-drop/exports.js +3 -0
- package/dist/helpers/drag-drop/exports.js.map +1 -0
- package/dist/helpers/exports.d.ts +7 -0
- package/dist/helpers/exports.js +8 -0
- package/dist/helpers/exports.js.map +1 -0
- package/dist/helpers/use-editable-control.d.ts +1 -1
- package/dist/helpers/use-editable-control.js.map +1 -1
- package/package.json +85 -84
- package/src/_modules/config.ts +9 -9
- package/src/_modules/constants.ts +3 -3
- package/src/controls/ColorPickerDialog.tsx +83 -83
- package/src/controls/accordion.tsx +62 -62
- package/src/controls/button.tsx +180 -180
- package/src/controls/canvas/CustomEventTargetBase.ts +32 -32
- package/src/controls/canvas/DrawPad.tsx +296 -296
- package/src/controls/canvas/DrawPadManager.ts +694 -694
- package/src/controls/canvas/bezier.ts +109 -109
- package/src/controls/canvas/point.ts +44 -44
- package/src/controls/card-list.tsx +31 -31
- package/src/controls/card.tsx +77 -77
- package/src/controls/centered.tsx +14 -14
- package/src/controls/date.tsx +87 -87
- package/src/controls/diagram-picker.tsx +96 -96
- package/src/controls/divider.tsx +15 -15
- package/src/controls/dropdown.tsx +66 -66
- package/src/controls/error-boundary.tsx +41 -41
- package/src/controls/field-editor.tsx +42 -42
- package/src/controls/file-upload.tsx +155 -155
- package/src/controls/horizontal.tsx +48 -48
- package/src/controls/html-editor/editor.tsx +182 -182
- package/src/controls/index.ts +33 -33
- package/src/controls/input.tsx +160 -160
- package/src/controls/kwizoverflow.tsx +106 -106
- package/src/controls/list.tsx +119 -119
- package/src/controls/loading.tsx +10 -10
- package/src/controls/menu.tsx +173 -173
- package/src/controls/merge-text.tsx +126 -126
- package/src/controls/please-wait.tsx +32 -32
- package/src/controls/progress-bar.tsx +109 -109
- package/src/controls/prompt.tsx +121 -121
- package/src/controls/qrcode.tsx +36 -36
- package/src/controls/search.tsx +71 -61
- package/src/controls/section.tsx +133 -133
- package/src/controls/svg.tsx +138 -138
- package/src/controls/toolbar.tsx +46 -46
- package/src/controls/vertical-content.tsx +49 -49
- package/src/controls/vertical.tsx +42 -42
- package/src/helpers/block-nav.tsx +88 -88
- package/src/helpers/context-const.ts +29 -29
- package/src/helpers/context-export.tsx +77 -77
- package/src/helpers/context-internal.ts +13 -13
- package/src/helpers/drag-drop/drag-drop-container.tsx +53 -53
- package/src/helpers/drag-drop/drag-drop-context-internal.tsx +9 -9
- package/src/helpers/drag-drop/drag-drop-context.tsx +61 -61
- package/src/helpers/drag-drop/drag-drop.types.ts +21 -21
- package/src/helpers/drag-drop/index.ts +12 -12
- package/src/helpers/drag-drop/readme.md +75 -75
- package/src/helpers/drag-drop/use-draggable.ts +47 -47
- package/src/helpers/drag-drop/use-droppable.ts +38 -38
- package/src/helpers/forwardRef.ts +7 -7
- package/src/helpers/hooks-events.ts +149 -149
- package/src/helpers/hooks.tsx +141 -141
- package/src/helpers/index.ts +8 -8
- package/src/helpers/use-alerts.tsx +74 -74
- package/src/helpers/use-editable-control.tsx +37 -37
- package/src/helpers/use-toast.tsx +29 -29
- package/src/index.ts +2 -2
- package/src/styles/index.ts +1 -1
- package/src/styles/styles.ts +104 -104
- 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
|
}
|