2 * Signature Pad v4.0.4 | https://github.com/szimek/signature_pad
3 * (c) 2022 Szymon Nowak | Released under the MIT license
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';
13 constructor(x, y, pressure, time) {
14 if (isNaN(x) || isNaN(y)) {
15 throw new Error(`Point is invalid: (${x}, ${y})`);
19 this.pressure = pressure || 0;
20 this.time = time || Date.now();
23 return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
26 return (this.x === other.x &&
28 this.pressure === other.pressure &&
29 this.time === other.time);
32 return this.time !== start.time
33 ? this.distanceTo(start) / (this.time - start.time)
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;
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);
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;
68 c1: new Point(m1.x + tx, m1.y + ty),
69 c2: new Point(m2.x + tx, m2.y + ty),
77 for (let i = 0; i <= steps; i += 1) {
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);
82 const xdiff = cx - px;
83 const ydiff = cy - py;
84 length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
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)
99 class SignatureEventTarget {
102 this._et = new EventTarget();
108 addEventListener(type, listener, options) {
109 this._et.addEventListener(type, listener, options);
111 dispatchEvent(event) {
112 return this._et.dispatchEvent(event);
114 removeEventListener(type, callback, options) {
115 this._et.removeEventListener(type, callback, options);
119 function throttle(fn, wait = 250) {
125 const later = () => {
126 previous = Date.now();
128 result = fn.apply(storedContext, storedArgs);
130 storedContext = null;
134 return function wrapper(...args) {
135 const now = Date.now();
136 const remaining = wait - (now - previous);
137 storedContext = this;
139 if (remaining <= 0 || remaining > wait) {
141 clearTimeout(timeout);
145 result = fn.apply(storedContext, storedArgs);
147 storedContext = null;
152 timeout = window.setTimeout(later, remaining);
158 class SignaturePad extends SignatureEventTarget {
159 constructor(canvas, options = {}) {
161 this.canvas = canvas;
162 this._handleMouseDown = (event) => {
163 if (event.buttons === 1) {
164 this._drawningStroke = true;
165 this._strokeBegin(event);
168 this._handleMouseMove = (event) => {
169 if (this._drawningStroke) {
170 this._strokeMoveUpdate(event);
173 this._handleMouseUp = (event) => {
174 if (event.buttons === 1 && this._drawningStroke) {
175 this._drawningStroke = false;
176 this._strokeEnd(event);
179 this._handleTouchStart = (event) => {
180 event.preventDefault();
181 if (event.targetTouches.length === 1) {
182 const touch = event.changedTouches[0];
183 this._strokeBegin(touch);
186 this._handleTouchMove = (event) => {
187 event.preventDefault();
188 const touch = event.targetTouches[0];
189 this._strokeMoveUpdate(touch);
191 this._handleTouchEnd = (event) => {
192 const wasCanvasTouched = event.target === this.canvas;
193 if (wasCanvasTouched) {
194 event.preventDefault();
195 const touch = event.changedTouches[0];
196 this._strokeEnd(touch);
199 this._handlePointerStart = (event) => {
200 this._drawningStroke = true;
201 event.preventDefault();
202 this._strokeBegin(event);
204 this._handlePointerMove = (event) => {
205 if (this._drawningStroke) {
206 event.preventDefault();
207 this._strokeMoveUpdate(event);
210 this._handlePointerEnd = (event) => {
211 if (this._drawningStroke) {
212 event.preventDefault();
213 this._drawningStroke = false;
214 this._strokeEnd(event);
217 this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
218 this.minWidth = options.minWidth || 0.5;
219 this.maxWidth = options.maxWidth || 2.5;
220 this.throttle = ('throttle' in options ? options.throttle : 16);
221 this.minDistance = ('minDistance' in options ? options.minDistance : 5);
222 this.dotSize = options.dotSize || 0;
223 this.penColor = options.penColor || 'black';
224 this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
225 this._strokeMoveUpdate = this.throttle
226 ? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
227 : SignaturePad.prototype._strokeUpdate;
228 this._ctx = canvas.getContext('2d');
233 const { _ctx: ctx, canvas } = this;
234 ctx.fillStyle = this.backgroundColor;
235 ctx.clearRect(0, 0, canvas.width, canvas.height);
236 ctx.fillRect(0, 0, canvas.width, canvas.height);
239 this._isEmpty = true;
241 fromDataURL(dataUrl, options = {}) {
242 return new Promise((resolve, reject) => {
243 const image = new Image();
244 const ratio = options.ratio || window.devicePixelRatio || 1;
245 const width = options.width || this.canvas.width / ratio;
246 const height = options.height || this.canvas.height / ratio;
247 const xOffset = options.xOffset || 0;
248 const yOffset = options.yOffset || 0;
250 image.onload = () => {
251 this._ctx.drawImage(image, xOffset, yOffset, width, height);
254 image.onerror = (error) => {
257 image.crossOrigin = 'anonymous';
259 this._isEmpty = false;
262 toDataURL(type = 'image/png', encoderOptions) {
264 case 'image/svg+xml':
265 return this._toSVG();
267 return this.canvas.toDataURL(type, encoderOptions);
271 this.canvas.style.touchAction = 'none';
272 this.canvas.style.msTouchAction = 'none';
273 this.canvas.style.userSelect = 'none';
274 const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
275 if (window.PointerEvent && !isIOS) {
276 this._handlePointerEvents();
279 this._handleMouseEvents();
280 if ('ontouchstart' in window) {
281 this._handleTouchEvents();
286 this.canvas.style.touchAction = 'auto';
287 this.canvas.style.msTouchAction = 'auto';
288 this.canvas.style.userSelect = 'auto';
289 this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
290 this.canvas.removeEventListener('pointermove', this._handlePointerMove);
291 document.removeEventListener('pointerup', this._handlePointerEnd);
292 this.canvas.removeEventListener('mousedown', this._handleMouseDown);
293 this.canvas.removeEventListener('mousemove', this._handleMouseMove);
294 document.removeEventListener('mouseup', this._handleMouseUp);
295 this.canvas.removeEventListener('touchstart', this._handleTouchStart);
296 this.canvas.removeEventListener('touchmove', this._handleTouchMove);
297 this.canvas.removeEventListener('touchend', this._handleTouchEnd);
300 return this._isEmpty;
302 fromData(pointGroups, { clear = true } = {}) {
306 this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
307 this._data = this._data.concat(pointGroups);
312 _strokeBegin(event) {
313 this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
314 const newPointGroup = {
315 dotSize: this.dotSize,
316 minWidth: this.minWidth,
317 maxWidth: this.maxWidth,
318 penColor: this.penColor,
321 this._data.push(newPointGroup);
323 this._strokeUpdate(event);
325 _strokeUpdate(event) {
326 if (this._data.length === 0) {
327 this._strokeBegin(event);
330 this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
331 const x = event.clientX;
332 const y = event.clientY;
333 const pressure = event.pressure !== undefined
335 : event.force !== undefined
338 const point = this._createPoint(x, y, pressure);
339 const lastPointGroup = this._data[this._data.length - 1];
340 const lastPoints = lastPointGroup.points;
341 const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
342 const isLastPointTooClose = lastPoint
343 ? point.distanceTo(lastPoint) <= this.minDistance
345 const { penColor, dotSize, minWidth, maxWidth } = lastPointGroup;
346 if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
347 const curve = this._addPoint(point);
349 this._drawDot(point, {
357 this._drawCurve(curve, {
368 pressure: point.pressure,
371 this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
374 this._strokeUpdate(event);
375 this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
377 _handlePointerEvents() {
378 this._drawningStroke = false;
379 this.canvas.addEventListener('pointerdown', this._handlePointerStart);
380 this.canvas.addEventListener('pointermove', this._handlePointerMove);
381 document.addEventListener('pointerup', this._handlePointerEnd);
383 _handleMouseEvents() {
384 this._drawningStroke = false;
385 this.canvas.addEventListener('mousedown', this._handleMouseDown);
386 this.canvas.addEventListener('mousemove', this._handleMouseMove);
387 document.addEventListener('mouseup', this._handleMouseUp);
389 _handleTouchEvents() {
390 this.canvas.addEventListener('touchstart', this._handleTouchStart);
391 this.canvas.addEventListener('touchmove', this._handleTouchMove);
392 this.canvas.addEventListener('touchend', this._handleTouchEnd);
395 this._lastPoints = [];
396 this._lastVelocity = 0;
397 this._lastWidth = (this.minWidth + this.maxWidth) / 2;
398 this._ctx.fillStyle = this.penColor;
400 _createPoint(x, y, pressure) {
401 const rect = this.canvas.getBoundingClientRect();
402 return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
405 const { _lastPoints } = this;
406 _lastPoints.push(point);
407 if (_lastPoints.length > 2) {
408 if (_lastPoints.length === 3) {
409 _lastPoints.unshift(_lastPoints[0]);
411 const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2]);
412 const curve = Bezier.fromPoints(_lastPoints, widths);
418 _calculateCurveWidths(startPoint, endPoint) {
419 const velocity = this.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
420 (1 - this.velocityFilterWeight) * this._lastVelocity;
421 const newWidth = this._strokeWidth(velocity);
424 start: this._lastWidth,
426 this._lastVelocity = velocity;
427 this._lastWidth = newWidth;
430 _strokeWidth(velocity) {
431 return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
433 _drawCurveSegment(x, y, width) {
434 const ctx = this._ctx;
436 ctx.arc(x, y, width, 0, 2 * Math.PI, false);
437 this._isEmpty = false;
439 _drawCurve(curve, options) {
440 const ctx = this._ctx;
441 const widthDelta = curve.endWidth - curve.startWidth;
442 const drawSteps = Math.ceil(curve.length()) * 2;
444 ctx.fillStyle = options.penColor;
445 for (let i = 0; i < drawSteps; i += 1) {
446 const t = i / drawSteps;
452 let x = uuu * curve.startPoint.x;
453 x += 3 * uu * t * curve.control1.x;
454 x += 3 * u * tt * curve.control2.x;
455 x += ttt * curve.endPoint.x;
456 let y = uuu * curve.startPoint.y;
457 y += 3 * uu * t * curve.control1.y;
458 y += 3 * u * tt * curve.control2.y;
459 y += ttt * curve.endPoint.y;
460 const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
461 this._drawCurveSegment(x, y, width);
466 _drawDot(point, options) {
467 const ctx = this._ctx;
468 const width = options.dotSize > 0
470 : (options.minWidth + options.maxWidth) / 2;
472 this._drawCurveSegment(point.x, point.y, width);
474 ctx.fillStyle = options.penColor;
477 _fromData(pointGroups, drawCurve, drawDot) {
478 for (const group of pointGroups) {
479 const { penColor, dotSize, minWidth, maxWidth, points } = group;
480 if (points.length > 1) {
481 for (let j = 0; j < points.length; j += 1) {
482 const basicPoint = points[j];
483 const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
484 this.penColor = penColor;
488 const curve = this._addPoint(point);
511 const pointGroups = this._data;
512 const ratio = Math.max(window.devicePixelRatio || 1, 1);
515 const maxX = this.canvas.width / ratio;
516 const maxY = this.canvas.height / ratio;
517 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
518 svg.setAttribute('width', this.canvas.width.toString());
519 svg.setAttribute('height', this.canvas.height.toString());
520 this._fromData(pointGroups, (curve, { penColor }) => {
521 const path = document.createElement('path');
522 if (!isNaN(curve.control1.x) &&
523 !isNaN(curve.control1.y) &&
524 !isNaN(curve.control2.x) &&
525 !isNaN(curve.control2.y)) {
526 const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
527 `C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
528 `${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
529 `${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
530 path.setAttribute('d', attr);
531 path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
532 path.setAttribute('stroke', penColor);
533 path.setAttribute('fill', 'none');
534 path.setAttribute('stroke-linecap', 'round');
535 svg.appendChild(path);
537 }, (point, { penColor, dotSize, minWidth, maxWidth }) => {
538 const circle = document.createElement('circle');
539 const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
540 circle.setAttribute('r', size.toString());
541 circle.setAttribute('cx', point.x.toString());
542 circle.setAttribute('cy', point.y.toString());
543 circle.setAttribute('fill', penColor);
544 svg.appendChild(circle);
546 const prefix = 'data:image/svg+xml;base64,';
547 const header = '<svg' +
548 ' xmlns="http://www.w3.org/2000/svg"' +
549 ' xmlns:xlink="http://www.w3.org/1999/xlink"' +
550 ` viewBox="${minX} ${minY} ${this.canvas.width} ${this.canvas.height}"` +
552 ` height="${maxY}"` +
554 let body = svg.innerHTML;
555 if (body === undefined) {
556 const dummy = document.createElement('dummy');
557 const nodes = svg.childNodes;
558 dummy.innerHTML = '';
559 for (let i = 0; i < nodes.length; i += 1) {
560 dummy.appendChild(nodes[i].cloneNode(true));
562 body = dummy.innerHTML;
564 const footer = '</svg>';
565 const data = header + body + footer;
566 return prefix + btoa(data);
573 //# sourceMappingURL=signature_pad.umd.js.map