UNPKG

22.9 kBJavaScriptView Raw
1/*!
2 * Signature Pad v4.0.0 | https://github.com/szimek/signature_pad
3 * (c) 2021 Szymon Nowak | Released under the MIT license
4 */
5
6(function (global, factory) {
7 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8 typeof define === 'function' && define.amd ? define(factory) :
9 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SignaturePad = factory());
10})(this, (function () { 'use strict';
11
12 class Point {
13 constructor(x, y, pressure, time) {
14 if (isNaN(x) || isNaN(y)) {
15 throw new Error(`Point is invalid: (${x}, ${y})`);
16 }
17 this.x = +x;
18 this.y = +y;
19 this.pressure = pressure || 0;
20 this.time = time || Date.now();
21 }
22 distanceTo(start) {
23 return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
24 }
25 equals(other) {
26 return (this.x === other.x &&
27 this.y === other.y &&
28 this.pressure === other.pressure &&
29 this.time === other.time);
30 }
31 velocityFrom(start) {
32 return this.time !== start.time
33 ? this.distanceTo(start) / (this.time - start.time)
34 : 0;
35 }
36 }
37
38 class Bezier {
39 constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
40 this.startPoint = startPoint;
41 this.control2 = control2;
42 this.control1 = control1;
43 this.endPoint = endPoint;
44 this.startWidth = startWidth;
45 this.endWidth = endWidth;
46 }
47 static fromPoints(points, widths) {
48 const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
49 const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
50 return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
51 }
52 static calculateControlPoints(s1, s2, s3) {
53 const dx1 = s1.x - s2.x;
54 const dy1 = s1.y - s2.y;
55 const dx2 = s2.x - s3.x;
56 const dy2 = s2.y - s3.y;
57 const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
58 const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
59 const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
60 const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
61 const dxm = m1.x - m2.x;
62 const dym = m1.y - m2.y;
63 const k = l2 / (l1 + l2);
64 const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
65 const tx = s2.x - cm.x;
66 const ty = s2.y - cm.y;
67 return {
68 c1: new Point(m1.x + tx, m1.y + ty),
69 c2: new Point(m2.x + tx, m2.y + ty),
70 };
71 }
72 length() {
73 const steps = 10;
74 let length = 0;
75 let px;
76 let py;
77 for (let i = 0; i <= steps; i += 1) {
78 const t = i / steps;
79 const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
80 const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
81 if (i > 0) {
82 const xdiff = cx - px;
83 const ydiff = cy - py;
84 length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
85 }
86 px = cx;
87 py = cy;
88 }
89 return length;
90 }
91 point(t, start, c1, c2, end) {
92 return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
93 + (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
94 + (3.0 * c2 * (1.0 - t) * t * t)
95 + (end * t * t * t);
96 }
97 }
98
99 function throttle(fn, wait = 250) {
100 let previous = 0;
101 let timeout = null;
102 let result;
103 let storedContext;
104 let storedArgs;
105 const later = () => {
106 previous = Date.now();
107 timeout = null;
108 result = fn.apply(storedContext, storedArgs);
109 if (!timeout) {
110 storedContext = null;
111 storedArgs = [];
112 }
113 };
114 return function wrapper(...args) {
115 const now = Date.now();
116 const remaining = wait - (now - previous);
117 storedContext = this;
118 storedArgs = args;
119 if (remaining <= 0 || remaining > wait) {
120 if (timeout) {
121 clearTimeout(timeout);
122 timeout = null;
123 }
124 previous = now;
125 result = fn.apply(storedContext, storedArgs);
126 if (!timeout) {
127 storedContext = null;
128 storedArgs = [];
129 }
130 }
131 else if (!timeout) {
132 timeout = window.setTimeout(later, remaining);
133 }
134 return result;
135 };
136 }
137
138 class SignaturePad extends EventTarget {
139 constructor(canvas, options = {}) {
140 super();
141 this.canvas = canvas;
142 this.options = options;
143 this._handleMouseDown = (event) => {
144 if (event.buttons === 1) {
145 this._drawningStroke = true;
146 this._strokeBegin(event);
147 }
148 };
149 this._handleMouseMove = (event) => {
150 if (this._drawningStroke) {
151 this._strokeMoveUpdate(event);
152 }
153 };
154 this._handleMouseUp = (event) => {
155 if (event.buttons === 1 && this._drawningStroke) {
156 this._drawningStroke = false;
157 this._strokeEnd(event);
158 }
159 };
160 this._handleTouchStart = (event) => {
161 event.preventDefault();
162 if (event.targetTouches.length === 1) {
163 const touch = event.changedTouches[0];
164 this._strokeBegin(touch);
165 }
166 };
167 this._handleTouchMove = (event) => {
168 event.preventDefault();
169 const touch = event.targetTouches[0];
170 this._strokeMoveUpdate(touch);
171 };
172 this._handleTouchEnd = (event) => {
173 const wasCanvasTouched = event.target === this.canvas;
174 if (wasCanvasTouched) {
175 event.preventDefault();
176 const touch = event.changedTouches[0];
177 this._strokeEnd(touch);
178 }
179 };
180 this._handlePointerStart = (event) => {
181 this._drawningStroke = true;
182 event.preventDefault();
183 this._strokeBegin(event);
184 };
185 this._handlePointerMove = (event) => {
186 if (this._drawningStroke) {
187 event.preventDefault();
188 this._strokeMoveUpdate(event);
189 }
190 };
191 this._handlePointerEnd = (event) => {
192 this._drawningStroke = false;
193 const wasCanvasTouched = event.target === this.canvas;
194 if (wasCanvasTouched) {
195 event.preventDefault();
196 this._strokeEnd(event);
197 }
198 };
199 this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
200 this.minWidth = options.minWidth || 0.5;
201 this.maxWidth = options.maxWidth || 2.5;
202 this.throttle = ('throttle' in options ? options.throttle : 16);
203 this.minDistance = ('minDistance' in options ? options.minDistance : 5);
204 this.dotSize = options.dotSize || 0;
205 this.penColor = options.penColor || 'black';
206 this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
207 this._strokeMoveUpdate = this.throttle
208 ? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
209 : SignaturePad.prototype._strokeUpdate;
210 this._ctx = canvas.getContext('2d');
211 this.clear();
212 this.on();
213 }
214 clear() {
215 const { _ctx: ctx, canvas } = this;
216 ctx.fillStyle = this.backgroundColor;
217 ctx.clearRect(0, 0, canvas.width, canvas.height);
218 ctx.fillRect(0, 0, canvas.width, canvas.height);
219 this._data = [];
220 this._reset();
221 this._isEmpty = true;
222 }
223 fromDataURL(dataUrl, options = {}) {
224 return new Promise((resolve, reject) => {
225 const image = new Image();
226 const ratio = options.ratio || window.devicePixelRatio || 1;
227 const width = options.width || this.canvas.width / ratio;
228 const height = options.height || this.canvas.height / ratio;
229 const xOffset = options.xOffset || 0;
230 const yOffset = options.yOffset || 0;
231 this._reset();
232 image.onload = () => {
233 this._ctx.drawImage(image, xOffset, yOffset, width, height);
234 resolve();
235 };
236 image.onerror = (error) => {
237 reject(error);
238 };
239 image.crossOrigin = 'anonymous';
240 image.src = dataUrl;
241 this._isEmpty = false;
242 });
243 }
244 toDataURL(type = 'image/png', encoderOptions) {
245 switch (type) {
246 case 'image/svg+xml':
247 return this._toSVG();
248 default:
249 return this.canvas.toDataURL(type, encoderOptions);
250 }
251 }
252 on() {
253 this.canvas.style.touchAction = 'none';
254 this.canvas.style.msTouchAction = 'none';
255 if (window.PointerEvent) {
256 this._handlePointerEvents();
257 }
258 else {
259 this._handleMouseEvents();
260 if ('ontouchstart' in window) {
261 this._handleTouchEvents();
262 }
263 }
264 }
265 off() {
266 this.canvas.style.touchAction = 'auto';
267 this.canvas.style.msTouchAction = 'auto';
268 this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
269 this.canvas.removeEventListener('pointermove', this._handlePointerMove);
270 document.removeEventListener('pointerup', this._handlePointerEnd);
271 this.canvas.removeEventListener('mousedown', this._handleMouseDown);
272 this.canvas.removeEventListener('mousemove', this._handleMouseMove);
273 document.removeEventListener('mouseup', this._handleMouseUp);
274 this.canvas.removeEventListener('touchstart', this._handleTouchStart);
275 this.canvas.removeEventListener('touchmove', this._handleTouchMove);
276 this.canvas.removeEventListener('touchend', this._handleTouchEnd);
277 }
278 isEmpty() {
279 return this._isEmpty;
280 }
281 fromData(pointGroups, { clear = true } = {}) {
282 if (clear) {
283 this.clear();
284 }
285 this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
286 this._data = clear ? pointGroups : this._data.concat(pointGroups);
287 }
288 toData() {
289 return this._data;
290 }
291 _strokeBegin(event) {
292 this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
293 const newPointGroup = {
294 dotSize: this.dotSize,
295 minWidth: this.minWidth,
296 maxWidth: this.maxWidth,
297 penColor: this.penColor,
298 points: [],
299 };
300 this._data.push(newPointGroup);
301 this._reset();
302 this._strokeUpdate(event);
303 }
304 _strokeUpdate(event) {
305 if (this._data.length === 0) {
306 this._strokeBegin(event);
307 return;
308 }
309 this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
310 const x = event.clientX;
311 const y = event.clientY;
312 const pressure = event.pressure !== undefined
313 ? event.pressure
314 : event.force !== undefined
315 ? event.force
316 : 0;
317 const point = this._createPoint(x, y, pressure);
318 const lastPointGroup = this._data[this._data.length - 1];
319 const lastPoints = lastPointGroup.points;
320 const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
321 const isLastPointTooClose = lastPoint
322 ? point.distanceTo(lastPoint) <= this.minDistance
323 : false;
324 const { penColor, dotSize, minWidth, maxWidth } = lastPointGroup;
325 if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
326 const curve = this._addPoint(point);
327 if (!lastPoint) {
328 this._drawDot(point, {
329 penColor,
330 dotSize,
331 minWidth,
332 maxWidth,
333 });
334 }
335 else if (curve) {
336 this._drawCurve(curve, {
337 penColor,
338 dotSize,
339 minWidth,
340 maxWidth,
341 });
342 }
343 lastPoints.push({
344 time: point.time,
345 x: point.x,
346 y: point.y,
347 pressure: point.pressure,
348 });
349 }
350 this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
351 }
352 _strokeEnd(event) {
353 this._strokeUpdate(event);
354 this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
355 }
356 _handlePointerEvents() {
357 this._drawningStroke = false;
358 this.canvas.addEventListener('pointerdown', this._handlePointerStart);
359 this.canvas.addEventListener('pointermove', this._handlePointerMove);
360 document.addEventListener('pointerup', this._handlePointerEnd);
361 }
362 _handleMouseEvents() {
363 this._drawningStroke = false;
364 this.canvas.addEventListener('mousedown', this._handleMouseDown);
365 this.canvas.addEventListener('mousemove', this._handleMouseMove);
366 document.addEventListener('mouseup', this._handleMouseUp);
367 }
368 _handleTouchEvents() {
369 this.canvas.addEventListener('touchstart', this._handleTouchStart);
370 this.canvas.addEventListener('touchmove', this._handleTouchMove);
371 this.canvas.addEventListener('touchend', this._handleTouchEnd);
372 }
373 _reset() {
374 this._lastPoints = [];
375 this._lastVelocity = 0;
376 this._lastWidth = (this.minWidth + this.maxWidth) / 2;
377 this._ctx.fillStyle = this.penColor;
378 }
379 _createPoint(x, y, pressure) {
380 const rect = this.canvas.getBoundingClientRect();
381 return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
382 }
383 _addPoint(point) {
384 const { _lastPoints } = this;
385 _lastPoints.push(point);
386 if (_lastPoints.length > 2) {
387 if (_lastPoints.length === 3) {
388 _lastPoints.unshift(_lastPoints[0]);
389 }
390 const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2]);
391 const curve = Bezier.fromPoints(_lastPoints, widths);
392 _lastPoints.shift();
393 return curve;
394 }
395 return null;
396 }
397 _calculateCurveWidths(startPoint, endPoint) {
398 const velocity = this.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
399 (1 - this.velocityFilterWeight) * this._lastVelocity;
400 const newWidth = this._strokeWidth(velocity);
401 const widths = {
402 end: newWidth,
403 start: this._lastWidth,
404 };
405 this._lastVelocity = velocity;
406 this._lastWidth = newWidth;
407 return widths;
408 }
409 _strokeWidth(velocity) {
410 return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
411 }
412 _drawCurveSegment(x, y, width) {
413 const ctx = this._ctx;
414 ctx.moveTo(x, y);
415 ctx.arc(x, y, width, 0, 2 * Math.PI, false);
416 this._isEmpty = false;
417 }
418 _drawCurve(curve, options) {
419 const ctx = this._ctx;
420 const widthDelta = curve.endWidth - curve.startWidth;
421 const drawSteps = Math.ceil(curve.length()) * 2;
422 ctx.beginPath();
423 ctx.fillStyle = options.penColor;
424 for (let i = 0; i < drawSteps; i += 1) {
425 const t = i / drawSteps;
426 const tt = t * t;
427 const ttt = tt * t;
428 const u = 1 - t;
429 const uu = u * u;
430 const uuu = uu * u;
431 let x = uuu * curve.startPoint.x;
432 x += 3 * uu * t * curve.control1.x;
433 x += 3 * u * tt * curve.control2.x;
434 x += ttt * curve.endPoint.x;
435 let y = uuu * curve.startPoint.y;
436 y += 3 * uu * t * curve.control1.y;
437 y += 3 * u * tt * curve.control2.y;
438 y += ttt * curve.endPoint.y;
439 const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
440 this._drawCurveSegment(x, y, width);
441 }
442 ctx.closePath();
443 ctx.fill();
444 }
445 _drawDot(point, options) {
446 const ctx = this._ctx;
447 const width = options.dotSize > 0
448 ? options.dotSize
449 : (options.minWidth + options.maxWidth) / 2;
450 ctx.beginPath();
451 this._drawCurveSegment(point.x, point.y, width);
452 ctx.closePath();
453 ctx.fillStyle = options.penColor;
454 ctx.fill();
455 }
456 _fromData(pointGroups, drawCurve, drawDot) {
457 for (const group of pointGroups) {
458 const { penColor, dotSize, minWidth, maxWidth, points } = group;
459 if (points.length > 1) {
460 for (let j = 0; j < points.length; j += 1) {
461 const basicPoint = points[j];
462 const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
463 this.penColor = penColor;
464 if (j === 0) {
465 this._reset();
466 }
467 const curve = this._addPoint(point);
468 if (curve) {
469 drawCurve(curve, {
470 penColor,
471 dotSize,
472 minWidth,
473 maxWidth,
474 });
475 }
476 }
477 }
478 else {
479 this._reset();
480 drawDot(points[0], {
481 penColor,
482 dotSize,
483 minWidth,
484 maxWidth,
485 });
486 }
487 }
488 }
489 _toSVG() {
490 const pointGroups = this._data;
491 const ratio = Math.max(window.devicePixelRatio || 1, 1);
492 const minX = 0;
493 const minY = 0;
494 const maxX = this.canvas.width / ratio;
495 const maxY = this.canvas.height / ratio;
496 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
497 svg.setAttribute('width', this.canvas.width.toString());
498 svg.setAttribute('height', this.canvas.height.toString());
499 this._fromData(pointGroups, (curve, { penColor }) => {
500 const path = document.createElement('path');
501 if (!isNaN(curve.control1.x) &&
502 !isNaN(curve.control1.y) &&
503 !isNaN(curve.control2.x) &&
504 !isNaN(curve.control2.y)) {
505 const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
506 `C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
507 `${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
508 `${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
509 path.setAttribute('d', attr);
510 path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
511 path.setAttribute('stroke', penColor);
512 path.setAttribute('fill', 'none');
513 path.setAttribute('stroke-linecap', 'round');
514 svg.appendChild(path);
515 }
516 }, (point, { penColor, dotSize, minWidth, maxWidth }) => {
517 const circle = document.createElement('circle');
518 const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
519 circle.setAttribute('r', size.toString());
520 circle.setAttribute('cx', point.x.toString());
521 circle.setAttribute('cy', point.y.toString());
522 circle.setAttribute('fill', penColor);
523 svg.appendChild(circle);
524 });
525 const prefix = 'data:image/svg+xml;base64,';
526 const header = '<svg' +
527 ' xmlns="http://www.w3.org/2000/svg"' +
528 ' xmlns:xlink="http://www.w3.org/1999/xlink"' +
529 ` viewBox="${minX} ${minY} ${this.canvas.width} ${this.canvas.height}"` +
530 ` width="${maxX}"` +
531 ` height="${maxY}"` +
532 '>';
533 let body = svg.innerHTML;
534 if (body === undefined) {
535 const dummy = document.createElement('dummy');
536 const nodes = svg.childNodes;
537 dummy.innerHTML = '';
538 for (let i = 0; i < nodes.length; i += 1) {
539 dummy.appendChild(nodes[i].cloneNode(true));
540 }
541 body = dummy.innerHTML;
542 }
543 const footer = '</svg>';
544 const data = header + body + footer;
545 return prefix + btoa(data);
546 }
547 }
548
549 return SignaturePad;
550
551}));
552//# sourceMappingURL=signature_pad.umd.js.map