1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | import { Bezier } from './bezier';
|
13 | import { BasicPoint, Point } from './point';
|
14 | import { SignatureEventTarget } from './signature_event_target';
|
15 | import { throttle } from './throttle';
|
16 |
|
17 | export interface SignatureEvent {
|
18 | event: MouseEvent | TouchEvent | PointerEvent;
|
19 | type: string;
|
20 | x: number;
|
21 | y: number;
|
22 | pressure: number;
|
23 | }
|
24 |
|
25 | export interface FromDataOptions {
|
26 | clear?: boolean;
|
27 | }
|
28 |
|
29 | export interface ToSVGOptions {
|
30 | includeBackgroundColor?: boolean;
|
31 | }
|
32 |
|
33 | export interface PointGroupOptions {
|
34 | dotSize: number;
|
35 | minWidth: number;
|
36 | maxWidth: number;
|
37 | penColor: string;
|
38 | velocityFilterWeight: number;
|
39 | |
40 |
|
41 |
|
42 |
|
43 |
|
44 | compositeOperation: GlobalCompositeOperation;
|
45 | }
|
46 |
|
47 | export interface Options extends Partial<PointGroupOptions> {
|
48 | minDistance?: number;
|
49 | backgroundColor?: string;
|
50 | throttle?: number;
|
51 | canvasContextOptions?: CanvasRenderingContext2DSettings;
|
52 | }
|
53 |
|
54 | export interface PointGroup extends PointGroupOptions {
|
55 | points: BasicPoint[];
|
56 | }
|
57 |
|
58 | export default class SignaturePad extends SignatureEventTarget {
|
59 |
|
60 | public dotSize: number;
|
61 | public minWidth: number;
|
62 | public maxWidth: number;
|
63 | public penColor: string;
|
64 | public minDistance: number;
|
65 | public velocityFilterWeight: number;
|
66 | public compositeOperation: GlobalCompositeOperation;
|
67 | public backgroundColor: string;
|
68 | public throttle: number;
|
69 | public canvasContextOptions: CanvasRenderingContext2DSettings;
|
70 |
|
71 |
|
72 |
|
73 | private _ctx: CanvasRenderingContext2D;
|
74 | private _drawingStroke = false;
|
75 | private _isEmpty = true;
|
76 | private _lastPoints: Point[] = [];
|
77 | private _data: PointGroup[] = [];
|
78 | private _lastVelocity = 0;
|
79 | private _lastWidth = 0;
|
80 | private _strokeMoveUpdate: (event: SignatureEvent) => void;
|
81 |
|
82 |
|
83 | constructor(
|
84 | private canvas: HTMLCanvasElement,
|
85 | options: Options = {},
|
86 | ) {
|
87 | super();
|
88 | this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
|
89 | this.minWidth = options.minWidth || 0.5;
|
90 | this.maxWidth = options.maxWidth || 2.5;
|
91 |
|
92 |
|
93 | this.throttle = options.throttle ?? 16;
|
94 | this.minDistance = options.minDistance ?? 5;
|
95 | this.dotSize = options.dotSize || 0;
|
96 | this.penColor = options.penColor || 'black';
|
97 | this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
|
98 | this.compositeOperation = options.compositeOperation || 'source-over';
|
99 | this.canvasContextOptions = options.canvasContextOptions ?? {};
|
100 |
|
101 | this._strokeMoveUpdate = this.throttle
|
102 | ? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
|
103 | : SignaturePad.prototype._strokeUpdate;
|
104 | this._ctx = canvas.getContext(
|
105 | '2d',
|
106 | this.canvasContextOptions,
|
107 | ) as CanvasRenderingContext2D;
|
108 |
|
109 | this.clear();
|
110 |
|
111 |
|
112 | this.on();
|
113 | }
|
114 |
|
115 | public clear(): void {
|
116 | const { _ctx: ctx, canvas } = this;
|
117 |
|
118 |
|
119 | ctx.fillStyle = this.backgroundColor;
|
120 | ctx.clearRect(0, 0, canvas.width, canvas.height);
|
121 | ctx.fillRect(0, 0, canvas.width, canvas.height);
|
122 |
|
123 | this._data = [];
|
124 | this._reset(this._getPointGroupOptions());
|
125 | this._isEmpty = true;
|
126 | }
|
127 |
|
128 | public fromDataURL(
|
129 | dataUrl: string,
|
130 | options: {
|
131 | ratio?: number;
|
132 | width?: number;
|
133 | height?: number;
|
134 | xOffset?: number;
|
135 | yOffset?: number;
|
136 | } = {},
|
137 | ): Promise<void> {
|
138 | return new Promise((resolve, reject) => {
|
139 | const image = new Image();
|
140 | const ratio = options.ratio || window.devicePixelRatio || 1;
|
141 | const width = options.width || this.canvas.width / ratio;
|
142 | const height = options.height || this.canvas.height / ratio;
|
143 | const xOffset = options.xOffset || 0;
|
144 | const yOffset = options.yOffset || 0;
|
145 |
|
146 | this._reset(this._getPointGroupOptions());
|
147 |
|
148 | image.onload = (): void => {
|
149 | this._ctx.drawImage(image, xOffset, yOffset, width, height);
|
150 | resolve();
|
151 | };
|
152 | image.onerror = (error): void => {
|
153 | reject(error);
|
154 | };
|
155 | image.crossOrigin = 'anonymous';
|
156 | image.src = dataUrl;
|
157 |
|
158 | this._isEmpty = false;
|
159 | });
|
160 | }
|
161 |
|
162 | public toDataURL(
|
163 | type: 'image/svg+xml',
|
164 | encoderOptions?: ToSVGOptions,
|
165 | ): string;
|
166 | public toDataURL(type?: string, encoderOptions?: number): string;
|
167 | public toDataURL(
|
168 | type = 'image/png',
|
169 | encoderOptions?: number | ToSVGOptions | undefined,
|
170 | ): string {
|
171 | switch (type) {
|
172 | case 'image/svg+xml':
|
173 | if (typeof encoderOptions !== 'object') {
|
174 | encoderOptions = undefined;
|
175 | }
|
176 | return `data:image/svg+xml;base64,${btoa(
|
177 | this.toSVG(encoderOptions as ToSVGOptions),
|
178 | )}`;
|
179 | default:
|
180 | if (typeof encoderOptions !== 'number') {
|
181 | encoderOptions = undefined;
|
182 | }
|
183 | return this.canvas.toDataURL(type, encoderOptions);
|
184 | }
|
185 | }
|
186 |
|
187 | public on(): void {
|
188 |
|
189 | this.canvas.style.touchAction = 'none';
|
190 | (this.canvas.style as CSSStyleDeclaration & { msTouchAction: string | null }).msTouchAction = 'none';
|
191 | this.canvas.style.userSelect = 'none';
|
192 |
|
193 | const isIOS =
|
194 | /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | if (window.PointerEvent && !isIOS) {
|
201 | this._handlePointerEvents();
|
202 | } else {
|
203 | this._handleMouseEvents();
|
204 |
|
205 | if ('ontouchstart' in window) {
|
206 | this._handleTouchEvents();
|
207 | }
|
208 | }
|
209 | }
|
210 |
|
211 | public off(): void {
|
212 |
|
213 | this.canvas.style.touchAction = 'auto';
|
214 | (this.canvas.style as CSSStyleDeclaration & { msTouchAction: string | null }).msTouchAction = 'auto';
|
215 | this.canvas.style.userSelect = 'auto';
|
216 |
|
217 | this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
|
218 | this.canvas.removeEventListener('mousedown', this._handleMouseDown);
|
219 | this.canvas.removeEventListener('touchstart', this._handleTouchStart);
|
220 |
|
221 | this._removeMoveUpEventListeners();
|
222 | }
|
223 |
|
224 | private _getListenerFunctions() {
|
225 | const canvasWindow =
|
226 | window.document === this.canvas.ownerDocument
|
227 | ? window
|
228 | : this.canvas.ownerDocument.defaultView ?? this.canvas.ownerDocument;
|
229 |
|
230 | return {
|
231 | addEventListener: canvasWindow.addEventListener.bind(
|
232 | canvasWindow,
|
233 | ) as typeof window.addEventListener,
|
234 | removeEventListener: canvasWindow.removeEventListener.bind(
|
235 | canvasWindow,
|
236 | ) as typeof window.removeEventListener,
|
237 | };
|
238 | }
|
239 |
|
240 | private _removeMoveUpEventListeners(): void {
|
241 | const { removeEventListener } = this._getListenerFunctions();
|
242 | removeEventListener('pointermove', this._handlePointerMove);
|
243 | removeEventListener('pointerup', this._handlePointerUp);
|
244 |
|
245 | removeEventListener('mousemove', this._handleMouseMove);
|
246 | removeEventListener('mouseup', this._handleMouseUp);
|
247 |
|
248 | removeEventListener('touchmove', this._handleTouchMove);
|
249 | removeEventListener('touchend', this._handleTouchEnd);
|
250 | }
|
251 |
|
252 | public isEmpty(): boolean {
|
253 | return this._isEmpty;
|
254 | }
|
255 |
|
256 | public fromData(
|
257 | pointGroups: PointGroup[],
|
258 | { clear = true }: FromDataOptions = {},
|
259 | ): void {
|
260 | if (clear) {
|
261 | this.clear();
|
262 | }
|
263 |
|
264 | this._fromData(
|
265 | pointGroups,
|
266 | this._drawCurve.bind(this),
|
267 | this._drawDot.bind(this),
|
268 | );
|
269 |
|
270 | this._data = this._data.concat(pointGroups);
|
271 | }
|
272 |
|
273 | public toData(): PointGroup[] {
|
274 | return this._data;
|
275 | }
|
276 |
|
277 | public _isLeftButtonPressed(event: MouseEvent, only?: boolean): boolean {
|
278 | if (only) {
|
279 | return event.buttons === 1;
|
280 | }
|
281 |
|
282 | return (event.buttons & 1) === 1;
|
283 | }
|
284 | private _pointerEventToSignatureEvent(
|
285 | event: MouseEvent | PointerEvent,
|
286 | ): SignatureEvent {
|
287 | return {
|
288 | event: event,
|
289 | type: event.type,
|
290 | x: event.clientX,
|
291 | y: event.clientY,
|
292 | pressure: 'pressure' in event ? event.pressure : 0,
|
293 | };
|
294 | }
|
295 |
|
296 | private _touchEventToSignatureEvent(event: TouchEvent): SignatureEvent {
|
297 | const touch = event.changedTouches[0];
|
298 | return {
|
299 | event: event,
|
300 | type: event.type,
|
301 | x: touch.clientX,
|
302 | y: touch.clientY,
|
303 | pressure: touch.force,
|
304 | };
|
305 | }
|
306 |
|
307 |
|
308 | private _handleMouseDown = (event: MouseEvent): void => {
|
309 | if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
|
310 | return;
|
311 | }
|
312 | this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
313 | };
|
314 |
|
315 | private _handleMouseMove = (event: MouseEvent): void => {
|
316 | if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
317 |
|
318 | this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
319 | return;
|
320 | }
|
321 |
|
322 | this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
323 | };
|
324 |
|
325 | private _handleMouseUp = (event: MouseEvent): void => {
|
326 | if (this._isLeftButtonPressed(event)) {
|
327 | return;
|
328 | }
|
329 |
|
330 | this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
331 | };
|
332 |
|
333 | private _handleTouchStart = (event: TouchEvent): void => {
|
334 | if (event.targetTouches.length !== 1 || this._drawingStroke) {
|
335 | return;
|
336 | }
|
337 |
|
338 |
|
339 | if (event.cancelable) {
|
340 | event.preventDefault();
|
341 | }
|
342 |
|
343 | this._strokeBegin(this._touchEventToSignatureEvent(event));
|
344 | };
|
345 |
|
346 | private _handleTouchMove = (event: TouchEvent): void => {
|
347 | if (event.targetTouches.length !== 1) {
|
348 | return;
|
349 | }
|
350 |
|
351 |
|
352 | if (event.cancelable) {
|
353 | event.preventDefault();
|
354 | }
|
355 |
|
356 | if (!this._drawingStroke) {
|
357 | this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
358 | return;
|
359 | }
|
360 |
|
361 | this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
|
362 | };
|
363 |
|
364 | private _handleTouchEnd = (event: TouchEvent): void => {
|
365 | if (event.targetTouches.length !== 0) {
|
366 | return;
|
367 | }
|
368 |
|
369 | if (event.cancelable) {
|
370 | event.preventDefault();
|
371 | }
|
372 |
|
373 | this.canvas.removeEventListener('touchmove', this._handleTouchMove);
|
374 |
|
375 | this._strokeEnd(this._touchEventToSignatureEvent(event));
|
376 | };
|
377 |
|
378 | private _handlePointerDown = (event: PointerEvent): void => {
|
379 | if (!this._isLeftButtonPressed(event) || this._drawingStroke) {
|
380 | return;
|
381 | }
|
382 |
|
383 | event.preventDefault();
|
384 |
|
385 | this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
386 | };
|
387 |
|
388 | private _handlePointerMove = (event: PointerEvent): void => {
|
389 | if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
390 |
|
391 | this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
392 | return;
|
393 | }
|
394 |
|
395 | event.preventDefault();
|
396 | this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
397 | };
|
398 |
|
399 | private _handlePointerUp = (event: PointerEvent): void => {
|
400 | if (this._isLeftButtonPressed(event)) {
|
401 | return;
|
402 | }
|
403 |
|
404 | event.preventDefault();
|
405 | this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
406 | };
|
407 |
|
408 | private _getPointGroupOptions(group?: PointGroup): PointGroupOptions {
|
409 | return {
|
410 | penColor: group && 'penColor' in group ? group.penColor : this.penColor,
|
411 | dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
|
412 | minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
|
413 | maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
|
414 | velocityFilterWeight:
|
415 | group && 'velocityFilterWeight' in group
|
416 | ? group.velocityFilterWeight
|
417 | : this.velocityFilterWeight,
|
418 | compositeOperation:
|
419 | group && 'compositeOperation' in group
|
420 | ? group.compositeOperation
|
421 | : this.compositeOperation,
|
422 | };
|
423 | }
|
424 |
|
425 |
|
426 | private _strokeBegin(event: SignatureEvent): void {
|
427 | const cancelled = !this.dispatchEvent(
|
428 | new CustomEvent('beginStroke', { detail: event, cancelable: true }),
|
429 | );
|
430 | if (cancelled) {
|
431 | return;
|
432 | }
|
433 |
|
434 | const { addEventListener } = this._getListenerFunctions();
|
435 | switch (event.event.type) {
|
436 | case 'mousedown':
|
437 | addEventListener('mousemove', this._handleMouseMove);
|
438 | addEventListener('mouseup', this._handleMouseUp);
|
439 | break;
|
440 | case 'touchstart':
|
441 | addEventListener('touchmove', this._handleTouchMove);
|
442 | addEventListener('touchend', this._handleTouchEnd);
|
443 | break;
|
444 | case 'pointerdown':
|
445 | addEventListener('pointermove', this._handlePointerMove);
|
446 | addEventListener('pointerup', this._handlePointerUp);
|
447 | break;
|
448 | default:
|
449 |
|
450 | }
|
451 |
|
452 | this._drawingStroke = true;
|
453 |
|
454 | const pointGroupOptions = this._getPointGroupOptions();
|
455 |
|
456 | const newPointGroup: PointGroup = {
|
457 | ...pointGroupOptions,
|
458 | points: [],
|
459 | };
|
460 |
|
461 | this._data.push(newPointGroup);
|
462 | this._reset(pointGroupOptions);
|
463 | this._strokeUpdate(event);
|
464 | }
|
465 |
|
466 | private _strokeUpdate(event: SignatureEvent): void {
|
467 | if (!this._drawingStroke) {
|
468 | return;
|
469 | }
|
470 |
|
471 | if (this._data.length === 0) {
|
472 |
|
473 |
|
474 | this._strokeBegin(event);
|
475 | return;
|
476 | }
|
477 |
|
478 | this.dispatchEvent(
|
479 | new CustomEvent('beforeUpdateStroke', { detail: event }),
|
480 | );
|
481 |
|
482 | const point = this._createPoint(event.x, event.y, event.pressure);
|
483 | const lastPointGroup = this._data[this._data.length - 1];
|
484 | const lastPoints = lastPointGroup.points;
|
485 | const lastPoint =
|
486 | lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
|
487 | const isLastPointTooClose = lastPoint
|
488 | ? point.distanceTo(lastPoint) <= this.minDistance
|
489 | : false;
|
490 | const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
|
491 |
|
492 |
|
493 | if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
|
494 | const curve = this._addPoint(point, pointGroupOptions);
|
495 |
|
496 | if (!lastPoint) {
|
497 | this._drawDot(point, pointGroupOptions);
|
498 | } else if (curve) {
|
499 | this._drawCurve(curve, pointGroupOptions);
|
500 | }
|
501 |
|
502 | lastPoints.push({
|
503 | time: point.time,
|
504 | x: point.x,
|
505 | y: point.y,
|
506 | pressure: point.pressure,
|
507 | });
|
508 | }
|
509 |
|
510 | this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
|
511 | }
|
512 |
|
513 | private _strokeEnd(event: SignatureEvent, shouldUpdate = true): void {
|
514 | this._removeMoveUpEventListeners();
|
515 |
|
516 | if (!this._drawingStroke) {
|
517 | return;
|
518 | }
|
519 |
|
520 | if (shouldUpdate) {
|
521 | this._strokeUpdate(event);
|
522 | }
|
523 |
|
524 | this._drawingStroke = false;
|
525 | this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
|
526 | }
|
527 |
|
528 | private _handlePointerEvents(): void {
|
529 | this._drawingStroke = false;
|
530 |
|
531 | this.canvas.addEventListener('pointerdown', this._handlePointerDown);
|
532 | }
|
533 |
|
534 | private _handleMouseEvents(): void {
|
535 | this._drawingStroke = false;
|
536 |
|
537 | this.canvas.addEventListener('mousedown', this._handleMouseDown);
|
538 | }
|
539 |
|
540 | private _handleTouchEvents(): void {
|
541 | this.canvas.addEventListener('touchstart', this._handleTouchStart);
|
542 | }
|
543 |
|
544 |
|
545 | private _reset(options: PointGroupOptions): void {
|
546 | this._lastPoints = [];
|
547 | this._lastVelocity = 0;
|
548 | this._lastWidth = (options.minWidth + options.maxWidth) / 2;
|
549 | this._ctx.fillStyle = options.penColor;
|
550 | this._ctx.globalCompositeOperation = options.compositeOperation;
|
551 | }
|
552 |
|
553 | private _createPoint(x: number, y: number, pressure: number): Point {
|
554 | const rect = this.canvas.getBoundingClientRect();
|
555 |
|
556 | return new Point(
|
557 | x - rect.left,
|
558 | y - rect.top,
|
559 | pressure,
|
560 | new Date().getTime(),
|
561 | );
|
562 | }
|
563 |
|
564 |
|
565 | private _addPoint(point: Point, options: PointGroupOptions): Bezier | null {
|
566 | const { _lastPoints } = this;
|
567 |
|
568 | _lastPoints.push(point);
|
569 |
|
570 | if (_lastPoints.length > 2) {
|
571 |
|
572 |
|
573 | if (_lastPoints.length === 3) {
|
574 | _lastPoints.unshift(_lastPoints[0]);
|
575 | }
|
576 |
|
577 |
|
578 | const widths = this._calculateCurveWidths(
|
579 | _lastPoints[1],
|
580 | _lastPoints[2],
|
581 | options,
|
582 | );
|
583 | const curve = Bezier.fromPoints(_lastPoints, widths);
|
584 |
|
585 |
|
586 | _lastPoints.shift();
|
587 |
|
588 | return curve;
|
589 | }
|
590 |
|
591 | return null;
|
592 | }
|
593 |
|
594 | private _calculateCurveWidths(
|
595 | startPoint: Point,
|
596 | endPoint: Point,
|
597 | options: PointGroupOptions,
|
598 | ): { start: number; end: number } {
|
599 | const velocity =
|
600 | options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
|
601 | (1 - options.velocityFilterWeight) * this._lastVelocity;
|
602 |
|
603 | const newWidth = this._strokeWidth(velocity, options);
|
604 |
|
605 | const widths = {
|
606 | end: newWidth,
|
607 | start: this._lastWidth,
|
608 | };
|
609 |
|
610 | this._lastVelocity = velocity;
|
611 | this._lastWidth = newWidth;
|
612 |
|
613 | return widths;
|
614 | }
|
615 |
|
616 | private _strokeWidth(velocity: number, options: PointGroupOptions): number {
|
617 | return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
|
618 | }
|
619 |
|
620 | private _drawCurveSegment(x: number, y: number, width: number): void {
|
621 | const ctx = this._ctx;
|
622 |
|
623 | ctx.moveTo(x, y);
|
624 | ctx.arc(x, y, width, 0, 2 * Math.PI, false);
|
625 | this._isEmpty = false;
|
626 | }
|
627 |
|
628 | private _drawCurve(curve: Bezier, options: PointGroupOptions): void {
|
629 | const ctx = this._ctx;
|
630 | const widthDelta = curve.endWidth - curve.startWidth;
|
631 |
|
632 |
|
633 | const drawSteps = Math.ceil(curve.length()) * 2;
|
634 |
|
635 | ctx.beginPath();
|
636 | ctx.fillStyle = options.penColor;
|
637 |
|
638 | for (let i = 0; i < drawSteps; i += 1) {
|
639 |
|
640 | const t = i / drawSteps;
|
641 | const tt = t * t;
|
642 | const ttt = tt * t;
|
643 | const u = 1 - t;
|
644 | const uu = u * u;
|
645 | const uuu = uu * u;
|
646 |
|
647 | let x = uuu * curve.startPoint.x;
|
648 | x += 3 * uu * t * curve.control1.x;
|
649 | x += 3 * u * tt * curve.control2.x;
|
650 | x += ttt * curve.endPoint.x;
|
651 |
|
652 | let y = uuu * curve.startPoint.y;
|
653 | y += 3 * uu * t * curve.control1.y;
|
654 | y += 3 * u * tt * curve.control2.y;
|
655 | y += ttt * curve.endPoint.y;
|
656 |
|
657 | const width = Math.min(
|
658 | curve.startWidth + ttt * widthDelta,
|
659 | options.maxWidth,
|
660 | );
|
661 | this._drawCurveSegment(x, y, width);
|
662 | }
|
663 |
|
664 | ctx.closePath();
|
665 | ctx.fill();
|
666 | }
|
667 |
|
668 | private _drawDot(point: BasicPoint, options: PointGroupOptions): void {
|
669 | const ctx = this._ctx;
|
670 | const width =
|
671 | options.dotSize > 0
|
672 | ? options.dotSize
|
673 | : (options.minWidth + options.maxWidth) / 2;
|
674 |
|
675 | ctx.beginPath();
|
676 | this._drawCurveSegment(point.x, point.y, width);
|
677 | ctx.closePath();
|
678 | ctx.fillStyle = options.penColor;
|
679 | ctx.fill();
|
680 | }
|
681 |
|
682 | private _fromData(
|
683 | pointGroups: PointGroup[],
|
684 | drawCurve: SignaturePad['_drawCurve'],
|
685 | drawDot: SignaturePad['_drawDot'],
|
686 | ): void {
|
687 | for (const group of pointGroups) {
|
688 | const { points } = group;
|
689 | const pointGroupOptions = this._getPointGroupOptions(group);
|
690 |
|
691 | if (points.length > 1) {
|
692 | for (let j = 0; j < points.length; j += 1) {
|
693 | const basicPoint = points[j];
|
694 | const point = new Point(
|
695 | basicPoint.x,
|
696 | basicPoint.y,
|
697 | basicPoint.pressure,
|
698 | basicPoint.time,
|
699 | );
|
700 |
|
701 | if (j === 0) {
|
702 | this._reset(pointGroupOptions);
|
703 | }
|
704 |
|
705 | const curve = this._addPoint(point, pointGroupOptions);
|
706 |
|
707 | if (curve) {
|
708 | drawCurve(curve, pointGroupOptions);
|
709 | }
|
710 | }
|
711 | } else {
|
712 | this._reset(pointGroupOptions);
|
713 |
|
714 | drawDot(points[0], pointGroupOptions);
|
715 | }
|
716 | }
|
717 | }
|
718 |
|
719 | public toSVG({ includeBackgroundColor = false }: ToSVGOptions = {}): string {
|
720 | const pointGroups = this._data;
|
721 | const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
722 | const minX = 0;
|
723 | const minY = 0;
|
724 | const maxX = this.canvas.width / ratio;
|
725 | const maxY = this.canvas.height / ratio;
|
726 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
727 |
|
728 | svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
729 | svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
730 | svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
|
731 | svg.setAttribute('width', maxX.toString());
|
732 | svg.setAttribute('height', maxY.toString());
|
733 |
|
734 | if (includeBackgroundColor && this.backgroundColor) {
|
735 | const rect = document.createElement('rect');
|
736 | rect.setAttribute('width', '100%');
|
737 | rect.setAttribute('height', '100%');
|
738 | rect.setAttribute('fill', this.backgroundColor);
|
739 |
|
740 | svg.appendChild(rect);
|
741 | }
|
742 |
|
743 | this._fromData(
|
744 | pointGroups,
|
745 |
|
746 | (curve, { penColor }) => {
|
747 | const path = document.createElement('path');
|
748 |
|
749 |
|
750 |
|
751 |
|
752 |
|
753 | if (
|
754 | !isNaN(curve.control1.x) &&
|
755 | !isNaN(curve.control1.y) &&
|
756 | !isNaN(curve.control2.x) &&
|
757 | !isNaN(curve.control2.y)
|
758 | ) {
|
759 | const attr =
|
760 | `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(
|
761 | 3,
|
762 | )} ` +
|
763 | `C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
|
764 | `${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
|
765 | `${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
|
766 | path.setAttribute('d', attr);
|
767 | path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
|
768 | path.setAttribute('stroke', penColor);
|
769 | path.setAttribute('fill', 'none');
|
770 | path.setAttribute('stroke-linecap', 'round');
|
771 |
|
772 | svg.appendChild(path);
|
773 | }
|
774 |
|
775 | },
|
776 |
|
777 | (point, { penColor, dotSize, minWidth, maxWidth }) => {
|
778 | const circle = document.createElement('circle');
|
779 | const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
|
780 | circle.setAttribute('r', size.toString());
|
781 | circle.setAttribute('cx', point.x.toString());
|
782 | circle.setAttribute('cy', point.y.toString());
|
783 | circle.setAttribute('fill', penColor);
|
784 |
|
785 | svg.appendChild(circle);
|
786 | },
|
787 | );
|
788 |
|
789 | return svg.outerHTML;
|
790 | }
|
791 | }
|