UNPKG

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