diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..82eecf05 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js + +node_js: + - "8" + - "9" + +cache: + yarn: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 5471a672..710effb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## Changelog +### master +#### Bug fixes +- Fix error in `touchend` event handler. +- Make both params in `#toDataURL` optional to match `Canvas#toDataURL`. + +#### Features +- Add optional callback param to `#fromDataURL`. +- Add basic unit tests for SignaturePad class. + ### 3.0.0-beta.1 #### Breaking changes - Rewrite library using TypeScript. TypeScript declaration files are now provided by the library. Hopefully, it should be a bit easier to refactor now... diff --git a/docs/js/signature_pad.umd.js b/docs/js/signature_pad.umd.js index 71d18f7d..a04ec991 100644 --- a/docs/js/signature_pad.umd.js +++ b/docs/js/signature_pad.umd.js @@ -4,493 +4,504 @@ */ (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.SignaturePad = factory()); + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.SignaturePad = factory()); }(this, (function () { 'use strict'; -var Point = (function () { - function Point(x, y, time) { - this.x = x; - this.y = y; - this.time = time || Date.now(); - } - Point.prototype.distanceTo = function (start) { - return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2)); - }; - Point.prototype.equals = function (other) { - return this.x === other.x && this.y === other.y && this.time === other.time; - }; - Point.prototype.velocityFrom = function (start) { - return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 0; - }; - return Point; -}()); + var Point = (function () { + function Point(x, y, time) { + this.x = x; + this.y = y; + this.time = time || Date.now(); + } + Point.prototype.distanceTo = function (start) { + return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2)); + }; + Point.prototype.equals = function (other) { + return this.x === other.x && this.y === other.y && this.time === other.time; + }; + Point.prototype.velocityFrom = function (start) { + return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 0; + }; + return Point; + }()); -var Bezier = (function () { - function Bezier(startPoint, control2, control1, endPoint, startWidth, endWidth) { - this.startPoint = startPoint; - this.control2 = control2; - this.control1 = control1; - this.endPoint = endPoint; - this.startWidth = startWidth; - this.endWidth = endWidth; - } - Bezier.fromPoints = function (points, widths) { - var c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2; - var c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1; - return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end); - }; - Bezier.calculateControlPoints = function (s1, s2, s3) { - var dx1 = s1.x - s2.x; - var dy1 = s1.y - s2.y; - var dx2 = s2.x - s3.x; - var dy2 = s2.y - s3.y; - var m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 }; - var m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 }; - var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1)); - var l2 = Math.sqrt((dx2 * dx2) + (dy2 * dy2)); - var dxm = (m1.x - m2.x); - var dym = (m1.y - m2.y); - var k = l2 / (l1 + l2); - var cm = { x: m2.x + (dxm * k), y: m2.y + (dym * k) }; - var tx = s2.x - cm.x; - var ty = s2.y - cm.y; - return { - c1: new Point(m1.x + tx, m1.y + ty), - c2: new Point(m2.x + tx, m2.y + ty) - }; - }; - Bezier.prototype.length = function () { - var steps = 10; - var length = 0; - var px; - var py; - for (var i = 0; i <= steps; i += 1) { - var t = i / steps; - var cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x); - var cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y); - if (i > 0) { - var xdiff = cx - px; - var ydiff = cy - py; - length += Math.sqrt((xdiff * xdiff) + (ydiff * ydiff)); - } - px = cx; - py = cy; - } - return length; - }; - Bezier.prototype.point = function (t, start, c1, c2, end) { - return (start * (1.0 - t) * (1.0 - t) * (1.0 - t)) - + (3.0 * c1 * (1.0 - t) * (1.0 - t) * t) - + (3.0 * c2 * (1.0 - t) * t * t) - + (end * t * t * t); - }; - return Bezier; -}()); + var Bezier = (function () { + function Bezier(startPoint, control2, control1, endPoint, startWidth, endWidth) { + this.startPoint = startPoint; + this.control2 = control2; + this.control1 = control1; + this.endPoint = endPoint; + this.startWidth = startWidth; + this.endWidth = endWidth; + } + Bezier.fromPoints = function (points, widths) { + var c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2; + var c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1; + return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end); + }; + Bezier.calculateControlPoints = function (s1, s2, s3) { + var dx1 = s1.x - s2.x; + var dy1 = s1.y - s2.y; + var dx2 = s2.x - s3.x; + var dy2 = s2.y - s3.y; + var m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 }; + var m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 }; + var l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1)); + var l2 = Math.sqrt((dx2 * dx2) + (dy2 * dy2)); + var dxm = (m1.x - m2.x); + var dym = (m1.y - m2.y); + var k = l2 / (l1 + l2); + var cm = { x: m2.x + (dxm * k), y: m2.y + (dym * k) }; + var tx = s2.x - cm.x; + var ty = s2.y - cm.y; + return { + c1: new Point(m1.x + tx, m1.y + ty), + c2: new Point(m2.x + tx, m2.y + ty) + }; + }; + Bezier.prototype.length = function () { + var steps = 10; + var length = 0; + var px; + var py; + for (var i = 0; i <= steps; i += 1) { + var t = i / steps; + var cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x); + var cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y); + if (i > 0) { + var xdiff = cx - px; + var ydiff = cy - py; + length += Math.sqrt((xdiff * xdiff) + (ydiff * ydiff)); + } + px = cx; + py = cy; + } + return length; + }; + Bezier.prototype.point = function (t, start, c1, c2, end) { + return (start * (1.0 - t) * (1.0 - t) * (1.0 - t)) + + (3.0 * c1 * (1.0 - t) * (1.0 - t) * t) + + (3.0 * c2 * (1.0 - t) * t * t) + + (end * t * t * t); + }; + return Bezier; + }()); -function throttle(fn, wait) { - if (wait === void 0) { wait = 250; } - var previous = 0; - var timeout = null; - var result; - var storedContext; - var storedArgs; - var later = function () { - previous = Date.now(); - timeout = null; - result = fn.apply(storedContext, storedArgs); - if (!timeout) { - storedContext = null; - storedArgs = []; - } - }; - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - var now = Date.now(); - var remaining = wait - (now - previous); - storedContext = this; - storedArgs = args; - if (remaining <= 0 || remaining > wait) { - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - previous = now; - result = fn.apply(storedContext, storedArgs); - if (!timeout) { - storedContext = null; - storedArgs = []; - } - } - else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; -} + function throttle(fn, wait) { + if (wait === void 0) { wait = 250; } + var previous = 0; + var timeout = null; + var result; + var storedContext; + var storedArgs; + var later = function () { + previous = Date.now(); + timeout = null; + result = fn.apply(storedContext, storedArgs); + if (!timeout) { + storedContext = null; + storedArgs = []; + } + }; + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var now = Date.now(); + var remaining = wait - (now - previous); + storedContext = this; + storedArgs = args; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = fn.apply(storedContext, storedArgs); + if (!timeout) { + storedContext = null; + storedArgs = []; + } + } + else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + } -var SignaturePad = (function () { - function SignaturePad(canvas, options) { - if (options === void 0) { options = {}; } - var _this = this; - this.canvas = canvas; - this.options = options; - this.velocityFilterWeight = options.velocityFilterWeight || 0.7; - this.minWidth = options.minWidth || 0.5; - this.maxWidth = options.maxWidth || 2.5; - this.throttle = ("throttle" in options ? options.throttle : 16); - this.minDistance = ("minDistance" in options ? options.minDistance : 5); - if (this.throttle) { - this._strokeMoveUpdate = throttle(SignaturePad.prototype._strokeUpdate, this.throttle); - } - else { - this._strokeMoveUpdate = SignaturePad.prototype._strokeUpdate; - } - this.dotSize = options.dotSize || function () { - return (this.minWidth + this.maxWidth) / 2; - }; - this.penColor = options.penColor || "black"; - this.backgroundColor = options.backgroundColor || "rgba(0,0,0,0)"; - this.onBegin = options.onBegin; - this.onEnd = options.onEnd; - this._ctx = canvas.getContext("2d"); - this.clear(); - this._handleMouseDown = function (event) { - if (event.which === 1) { - _this._mouseButtonDown = true; - _this._strokeBegin(event); - } - }; - this._handleMouseMove = function (event) { - if (_this._mouseButtonDown) { - _this._strokeMoveUpdate(event); - } - }; - this._handleMouseUp = function (event) { - if (event.which === 1 && _this._mouseButtonDown) { - _this._mouseButtonDown = false; - _this._strokeEnd(event); - } - }; - this._handleTouchStart = function (event) { - event.preventDefault(); - if (event.targetTouches.length === 1) { - var touch = event.changedTouches[0]; - _this._strokeBegin(touch); - } - }; - this._handleTouchMove = function (event) { - event.preventDefault(); - var touch = event.targetTouches[0]; - _this._strokeMoveUpdate(touch); - }; - this._handleTouchEnd = function (event) { - var wasCanvasTouched = event.target === _this.canvas; - if (wasCanvasTouched) { - event.preventDefault(); - var touch = event.changedTouches[0]; - _this._strokeEnd(touch); - } - }; - this.on(); - } - SignaturePad.prototype.clear = function () { - var ctx = this._ctx; - var canvas = this.canvas; - ctx.fillStyle = this.backgroundColor; - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillRect(0, 0, canvas.width, canvas.height); - this._data = []; - this._reset(); - this._isEmpty = true; - }; - SignaturePad.prototype.fromDataURL = function (dataUrl, options) { - var _this = this; - if (options === void 0) { options = {}; } - var image = new Image(); - var ratio = options.ratio || window.devicePixelRatio || 1; - var width = options.width || (this.canvas.width / ratio); - var height = options.height || (this.canvas.height / ratio); - this._reset(); - image.src = dataUrl; - image.onload = function () { - _this._ctx.drawImage(image, 0, 0, width, height); - }; - this._isEmpty = false; - }; - SignaturePad.prototype.toDataURL = function (type, encoderOptions) { - switch (type) { - case "image/svg+xml": - return this._toSVG(); - default: - return this.canvas.toDataURL(type, encoderOptions); - } - }; - SignaturePad.prototype.on = function () { - this._handleMouseEvents(); - this._handleTouchEvents(); - }; - SignaturePad.prototype.off = function () { - this.canvas.style.msTouchAction = "auto"; - this.canvas.style.touchAction = "auto"; - this.canvas.removeEventListener("mousedown", this._handleMouseDown); - this.canvas.removeEventListener("mousemove", this._handleMouseMove); - document.removeEventListener("mouseup", this._handleMouseUp); - this.canvas.removeEventListener("touchstart", this._handleTouchStart); - this.canvas.removeEventListener("touchmove", this._handleTouchMove); - this.canvas.removeEventListener("touchend", this._handleTouchEnd); - }; - SignaturePad.prototype.isEmpty = function () { - return this._isEmpty; - }; - SignaturePad.prototype.fromData = function (pointGroups) { - var _this = this; - this.clear(); - this._fromData(pointGroups, function (_a) { - var color = _a.color, curve = _a.curve; - return _this._drawCurve({ color: color, curve: curve }); - }, function (_a) { - var color = _a.color, point = _a.point; - return _this._drawDot({ color: color, point: point }); - }); - this._data = pointGroups; - }; - SignaturePad.prototype.toData = function () { - return this._data; - }; - SignaturePad.prototype._strokeBegin = function (event) { - var newPointGroup = { - color: this.penColor, - points: [] - }; - this._data.push(newPointGroup); - this._reset(); - this._strokeUpdate(event); - if (typeof this.onBegin === "function") { - this.onBegin(event); - } - }; - SignaturePad.prototype._strokeUpdate = function (event) { - var x = event.clientX; - var y = event.clientY; - var point = this._createPoint(x, y); - var lastPointGroup = this._data[this._data.length - 1]; - var lastPoints = lastPointGroup.points; - var lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1]; - var isLastPointTooClose = lastPoint ? point.distanceTo(lastPoint) <= this.minDistance : false; - var color = lastPointGroup.color; - if (!lastPoint || !(lastPoint && isLastPointTooClose)) { - var curve = this._addPoint(point); - if (!lastPoint) { - this._drawDot({ color: color, point: point }); - } - else if (curve) { - this._drawCurve({ color: color, curve: curve }); - } - lastPoints.push({ - time: point.time, - x: point.x, - y: point.y - }); - } - }; - SignaturePad.prototype._strokeEnd = function (event) { - this._strokeUpdate(event); - if (typeof this.onEnd === "function") { - this.onEnd(event); - } - }; - SignaturePad.prototype._handleMouseEvents = function () { - this._mouseButtonDown = false; - this.canvas.addEventListener("mousedown", this._handleMouseDown); - this.canvas.addEventListener("mousemove", this._handleMouseMove); - document.addEventListener("mouseup", this._handleMouseUp); - }; - SignaturePad.prototype._handleTouchEvents = function () { - this.canvas.style.msTouchAction = "none"; - this.canvas.style.touchAction = "none"; - this.canvas.addEventListener("touchstart", this._handleTouchStart); - this.canvas.addEventListener("touchmove", this._handleTouchMove); - this.canvas.addEventListener("touchend", this._handleTouchEnd); - }; - SignaturePad.prototype._reset = function () { - this._points = []; - this._lastVelocity = 0; - this._lastWidth = (this.minWidth + this.maxWidth) / 2; - this._ctx.fillStyle = this.penColor; - }; - SignaturePad.prototype._createPoint = function (x, y) { - var rect = this.canvas.getBoundingClientRect(); - return new Point(x - rect.left, y - rect.top, new Date().getTime()); - }; - SignaturePad.prototype._addPoint = function (point) { - var _points = this._points; - _points.push(point); - if (_points.length > 2) { - if (_points.length === 3) { - _points.unshift(_points[0]); - } - var widths = this._calculateCurveWidths(_points[1], _points[2]); - var curve = Bezier.fromPoints(_points, widths); - _points.shift(); - return curve; - } - return null; - }; - SignaturePad.prototype._calculateCurveWidths = function (startPoint, endPoint) { - var velocity = (this.velocityFilterWeight * endPoint.velocityFrom(startPoint)) - + ((1 - this.velocityFilterWeight) * this._lastVelocity); - var newWidth = this._strokeWidth(velocity); - var widths = { - end: newWidth, - start: this._lastWidth - }; - this._lastVelocity = velocity; - this._lastWidth = newWidth; - return widths; - }; - SignaturePad.prototype._strokeWidth = function (velocity) { - return Math.max(this.maxWidth / (velocity + 1), this.minWidth); - }; - SignaturePad.prototype._drawCurveSegment = function (x, y, width) { - var ctx = this._ctx; - ctx.moveTo(x, y); - ctx.arc(x, y, width, 0, 2 * Math.PI, false); - this._isEmpty = false; - }; - SignaturePad.prototype._drawCurve = function (_a) { - var color = _a.color, curve = _a.curve; - var ctx = this._ctx; - var widthDelta = curve.endWidth - curve.startWidth; - var drawSteps = Math.floor(curve.length()) * 2; - ctx.beginPath(); - ctx.fillStyle = color; - for (var i = 0; i < drawSteps; i += 1) { - var t = i / drawSteps; - var tt = t * t; - var ttt = tt * t; - var u = 1 - t; - var uu = u * u; - var uuu = uu * u; - var x = uuu * curve.startPoint.x; - x += 3 * uu * t * curve.control1.x; - x += 3 * u * tt * curve.control2.x; - x += ttt * curve.endPoint.x; - var y = uuu * curve.startPoint.y; - y += 3 * uu * t * curve.control1.y; - y += 3 * u * tt * curve.control2.y; - y += ttt * curve.endPoint.y; - var width = curve.startWidth + (ttt * widthDelta); - this._drawCurveSegment(x, y, width); - } - ctx.closePath(); - ctx.fill(); - }; - SignaturePad.prototype._drawDot = function (_a) { - var color = _a.color, point = _a.point; - var ctx = this._ctx; - var width = typeof this.dotSize === "function" ? this.dotSize() : this.dotSize; - ctx.beginPath(); - this._drawCurveSegment(point.x, point.y, width); - ctx.closePath(); - ctx.fillStyle = color; - ctx.fill(); - }; - SignaturePad.prototype._fromData = function (pointGroups, drawCurve, drawDot) { - for (var _i = 0, pointGroups_1 = pointGroups; _i < pointGroups_1.length; _i++) { - var group = pointGroups_1[_i]; - var color = group.color, points = group.points; - if (points.length > 1) { - for (var j = 0; j < points.length; j += 1) { - var basicPoint = points[j]; - var point = new Point(basicPoint.x, basicPoint.y, basicPoint.time); - this.penColor = color; - if (j === 0) { - this._reset(); - } - var curve = this._addPoint(point); - if (curve) { - drawCurve({ color: color, curve: curve }); - } - } - } - else { - this._reset(); - drawDot({ - color: color, - point: points[0] - }); - } - } - }; - SignaturePad.prototype._toSVG = function () { - var _this = this; - var pointGroups = this._data; - var ratio = Math.max(window.devicePixelRatio || 1, 1); - var minX = 0; - var minY = 0; - var maxX = this.canvas.width / ratio; - var maxY = this.canvas.height / ratio; - var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", this.canvas.width.toString()); - svg.setAttribute("height", this.canvas.height.toString()); - this._fromData(pointGroups, function (_a) { - var color = _a.color, curve = _a.curve; - var path = document.createElement("path"); - if (!isNaN(curve.control1.x) && - !isNaN(curve.control1.y) && - !isNaN(curve.control2.x) && - !isNaN(curve.control2.y)) { - var attr = "M " + curve.startPoint.x.toFixed(3) + "," + curve.startPoint.y.toFixed(3) + " " - + ("C " + curve.control1.x.toFixed(3) + "," + curve.control1.y.toFixed(3) + " ") - + (curve.control2.x.toFixed(3) + "," + curve.control2.y.toFixed(3) + " ") - + (curve.endPoint.x.toFixed(3) + "," + curve.endPoint.y.toFixed(3)); - path.setAttribute("d", attr); - path.setAttribute("stroke-width", (curve.endWidth * 2.25).toFixed(3)); - path.setAttribute("stroke", color); - path.setAttribute("fill", "none"); - path.setAttribute("stroke-linecap", "round"); - svg.appendChild(path); - } - }, function (_a) { - var color = _a.color, point = _a.point; - var circle = document.createElement("circle"); - var dotSize = typeof _this.dotSize === "function" ? _this.dotSize() : _this.dotSize; - circle.setAttribute("r", dotSize.toString()); - circle.setAttribute("cx", point.x.toString()); - circle.setAttribute("cy", point.y.toString()); - circle.setAttribute("fill", color); - svg.appendChild(circle); - }); - var prefix = "data:image/svg+xml;base64,"; - var header = ""; - var body = svg.innerHTML; - if (body === undefined) { - var dummy = document.createElement("dummy"); - var nodes = svg.childNodes; - dummy.innerHTML = ""; - for (var i = 0; i < nodes.length; i += 1) { - dummy.appendChild(nodes[i].cloneNode(true)); - } - body = dummy.innerHTML; - } - var footer = ""; - var data = header + body + footer; - return prefix + btoa(data); - }; - return SignaturePad; -}()); + var SignaturePad = (function () { + function SignaturePad(canvas, options) { + if (options === void 0) { options = {}; } + var _this = this; + this.canvas = canvas; + this.options = options; + this._handleMouseDown = function (event) { + if (event.which === 1) { + _this._mouseButtonDown = true; + _this._strokeBegin(event); + } + }; + this._handleMouseMove = function (event) { + if (_this._mouseButtonDown) { + _this._strokeMoveUpdate(event); + } + }; + this._handleMouseUp = function (event) { + if (event.which === 1 && _this._mouseButtonDown) { + _this._mouseButtonDown = false; + _this._strokeEnd(event); + } + }; + this._handleTouchStart = function (event) { + event.preventDefault(); + if (event.targetTouches.length === 1) { + var touch = event.changedTouches[0]; + _this._strokeBegin(touch); + } + }; + this._handleTouchMove = function (event) { + event.preventDefault(); + var touch = event.targetTouches[0]; + _this._strokeMoveUpdate(touch); + }; + this._handleTouchEnd = function (event) { + var wasCanvasTouched = event.target === _this.canvas; + if (wasCanvasTouched) { + event.preventDefault(); + var touch = event.changedTouches[0]; + _this._strokeEnd(touch); + } + }; + this.velocityFilterWeight = options.velocityFilterWeight || 0.7; + this.minWidth = options.minWidth || 0.5; + this.maxWidth = options.maxWidth || 2.5; + this.throttle = ("throttle" in options ? options.throttle : 16); + this.minDistance = ("minDistance" in options ? options.minDistance : 5); + if (this.throttle) { + this._strokeMoveUpdate = throttle(SignaturePad.prototype._strokeUpdate, this.throttle); + } + else { + this._strokeMoveUpdate = SignaturePad.prototype._strokeUpdate; + } + this.dotSize = options.dotSize || function () { + return (this.minWidth + this.maxWidth) / 2; + }; + this.penColor = options.penColor || "black"; + this.backgroundColor = options.backgroundColor || "rgba(0,0,0,0)"; + this.onBegin = options.onBegin; + this.onEnd = options.onEnd; + this._ctx = canvas.getContext("2d"); + this.clear(); + this.on(); + } + SignaturePad.prototype.clear = function () { + var ctx = this._ctx; + var canvas = this.canvas; + ctx.fillStyle = this.backgroundColor; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillRect(0, 0, canvas.width, canvas.height); + this._data = []; + this._reset(); + this._isEmpty = true; + }; + SignaturePad.prototype.fromDataURL = function (dataUrl, options, callback) { + var _this = this; + if (options === void 0) { options = {}; } + var image = new Image(); + var ratio = options.ratio || window.devicePixelRatio || 1; + var width = options.width || (this.canvas.width / ratio); + var height = options.height || (this.canvas.height / ratio); + this._reset(); + image.onload = function () { + _this._ctx.drawImage(image, 0, 0, width, height); + if (callback) { + callback(); + } + }; + image.onerror = function (error) { + if (callback) { + callback(error); + } + }; + image.src = dataUrl; + this._isEmpty = false; + }; + SignaturePad.prototype.toDataURL = function (type, encoderOptions) { + if (type === void 0) { type = "image/png"; } + switch (type) { + case "image/svg+xml": + return this._toSVG(); + default: + return this.canvas.toDataURL(type, encoderOptions); + } + }; + SignaturePad.prototype.on = function () { + this._handleMouseEvents(); + if ("ontouchstart" in window) { + this._handleTouchEvents(); + } + }; + SignaturePad.prototype.off = function () { + this.canvas.style.msTouchAction = "auto"; + this.canvas.style.touchAction = "auto"; + this.canvas.removeEventListener("mousedown", this._handleMouseDown); + this.canvas.removeEventListener("mousemove", this._handleMouseMove); + document.removeEventListener("mouseup", this._handleMouseUp); + this.canvas.removeEventListener("touchstart", this._handleTouchStart); + this.canvas.removeEventListener("touchmove", this._handleTouchMove); + this.canvas.removeEventListener("touchend", this._handleTouchEnd); + }; + SignaturePad.prototype.isEmpty = function () { + return this._isEmpty; + }; + SignaturePad.prototype.fromData = function (pointGroups) { + var _this = this; + this.clear(); + this._fromData(pointGroups, function (_a) { + var color = _a.color, curve = _a.curve; + return _this._drawCurve({ color: color, curve: curve }); + }, function (_a) { + var color = _a.color, point = _a.point; + return _this._drawDot({ color: color, point: point }); + }); + this._data = pointGroups; + }; + SignaturePad.prototype.toData = function () { + return this._data; + }; + SignaturePad.prototype._strokeBegin = function (event) { + var newPointGroup = { + color: this.penColor, + points: [] + }; + this._data.push(newPointGroup); + this._reset(); + this._strokeUpdate(event); + if (typeof this.onBegin === "function") { + this.onBegin(event); + } + }; + SignaturePad.prototype._strokeUpdate = function (event) { + var x = event.clientX; + var y = event.clientY; + var point = this._createPoint(x, y); + var lastPointGroup = this._data[this._data.length - 1]; + var lastPoints = lastPointGroup.points; + var lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1]; + var isLastPointTooClose = lastPoint ? point.distanceTo(lastPoint) <= this.minDistance : false; + var color = lastPointGroup.color; + if (!lastPoint || !(lastPoint && isLastPointTooClose)) { + var curve = this._addPoint(point); + if (!lastPoint) { + this._drawDot({ color: color, point: point }); + } + else if (curve) { + this._drawCurve({ color: color, curve: curve }); + } + lastPoints.push({ + time: point.time, + x: point.x, + y: point.y + }); + } + }; + SignaturePad.prototype._strokeEnd = function (event) { + this._strokeUpdate(event); + if (typeof this.onEnd === "function") { + this.onEnd(event); + } + }; + SignaturePad.prototype._handleMouseEvents = function () { + this._mouseButtonDown = false; + this.canvas.addEventListener("mousedown", this._handleMouseDown); + this.canvas.addEventListener("mousemove", this._handleMouseMove); + document.addEventListener("mouseup", this._handleMouseUp); + }; + SignaturePad.prototype._handleTouchEvents = function () { + this.canvas.style.msTouchAction = "none"; + this.canvas.style.touchAction = "none"; + this.canvas.addEventListener("touchstart", this._handleTouchStart); + this.canvas.addEventListener("touchmove", this._handleTouchMove); + this.canvas.addEventListener("touchend", this._handleTouchEnd); + }; + SignaturePad.prototype._reset = function () { + this._points = []; + this._lastVelocity = 0; + this._lastWidth = (this.minWidth + this.maxWidth) / 2; + this._ctx.fillStyle = this.penColor; + }; + SignaturePad.prototype._createPoint = function (x, y) { + var rect = this.canvas.getBoundingClientRect(); + return new Point(x - rect.left, y - rect.top, new Date().getTime()); + }; + SignaturePad.prototype._addPoint = function (point) { + var _points = this._points; + _points.push(point); + if (_points.length > 2) { + if (_points.length === 3) { + _points.unshift(_points[0]); + } + var widths = this._calculateCurveWidths(_points[1], _points[2]); + var curve = Bezier.fromPoints(_points, widths); + _points.shift(); + return curve; + } + return null; + }; + SignaturePad.prototype._calculateCurveWidths = function (startPoint, endPoint) { + var velocity = (this.velocityFilterWeight * endPoint.velocityFrom(startPoint)) + + ((1 - this.velocityFilterWeight) * this._lastVelocity); + var newWidth = this._strokeWidth(velocity); + var widths = { + end: newWidth, + start: this._lastWidth + }; + this._lastVelocity = velocity; + this._lastWidth = newWidth; + return widths; + }; + SignaturePad.prototype._strokeWidth = function (velocity) { + return Math.max(this.maxWidth / (velocity + 1), this.minWidth); + }; + SignaturePad.prototype._drawCurveSegment = function (x, y, width) { + var ctx = this._ctx; + ctx.moveTo(x, y); + ctx.arc(x, y, width, 0, 2 * Math.PI, false); + this._isEmpty = false; + }; + SignaturePad.prototype._drawCurve = function (_a) { + var color = _a.color, curve = _a.curve; + var ctx = this._ctx; + var widthDelta = curve.endWidth - curve.startWidth; + var drawSteps = Math.floor(curve.length()) * 2; + ctx.beginPath(); + ctx.fillStyle = color; + for (var i = 0; i < drawSteps; i += 1) { + var t = i / drawSteps; + var tt = t * t; + var ttt = tt * t; + var u = 1 - t; + var uu = u * u; + var uuu = uu * u; + var x = uuu * curve.startPoint.x; + x += 3 * uu * t * curve.control1.x; + x += 3 * u * tt * curve.control2.x; + x += ttt * curve.endPoint.x; + var y = uuu * curve.startPoint.y; + y += 3 * uu * t * curve.control1.y; + y += 3 * u * tt * curve.control2.y; + y += ttt * curve.endPoint.y; + var width = curve.startWidth + (ttt * widthDelta); + this._drawCurveSegment(x, y, width); + } + ctx.closePath(); + ctx.fill(); + }; + SignaturePad.prototype._drawDot = function (_a) { + var color = _a.color, point = _a.point; + var ctx = this._ctx; + var width = typeof this.dotSize === "function" ? this.dotSize() : this.dotSize; + ctx.beginPath(); + this._drawCurveSegment(point.x, point.y, width); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + }; + SignaturePad.prototype._fromData = function (pointGroups, drawCurve, drawDot) { + for (var _i = 0, pointGroups_1 = pointGroups; _i < pointGroups_1.length; _i++) { + var group = pointGroups_1[_i]; + var color = group.color, points = group.points; + if (points.length > 1) { + for (var j = 0; j < points.length; j += 1) { + var basicPoint = points[j]; + var point = new Point(basicPoint.x, basicPoint.y, basicPoint.time); + this.penColor = color; + if (j === 0) { + this._reset(); + } + var curve = this._addPoint(point); + if (curve) { + drawCurve({ color: color, curve: curve }); + } + } + } + else { + this._reset(); + drawDot({ + color: color, + point: points[0] + }); + } + } + }; + SignaturePad.prototype._toSVG = function () { + var _this = this; + var pointGroups = this._data; + var ratio = Math.max(window.devicePixelRatio || 1, 1); + var minX = 0; + var minY = 0; + var maxX = this.canvas.width / ratio; + var maxY = this.canvas.height / ratio; + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("width", this.canvas.width.toString()); + svg.setAttribute("height", this.canvas.height.toString()); + this._fromData(pointGroups, function (_a) { + var color = _a.color, curve = _a.curve; + var path = document.createElement("path"); + if (!isNaN(curve.control1.x) && + !isNaN(curve.control1.y) && + !isNaN(curve.control2.x) && + !isNaN(curve.control2.y)) { + var attr = "M " + curve.startPoint.x.toFixed(3) + "," + curve.startPoint.y.toFixed(3) + " " + + ("C " + curve.control1.x.toFixed(3) + "," + curve.control1.y.toFixed(3) + " ") + + (curve.control2.x.toFixed(3) + "," + curve.control2.y.toFixed(3) + " ") + + (curve.endPoint.x.toFixed(3) + "," + curve.endPoint.y.toFixed(3)); + path.setAttribute("d", attr); + path.setAttribute("stroke-width", (curve.endWidth * 2.25).toFixed(3)); + path.setAttribute("stroke", color); + path.setAttribute("fill", "none"); + path.setAttribute("stroke-linecap", "round"); + svg.appendChild(path); + } + }, function (_a) { + var color = _a.color, point = _a.point; + var circle = document.createElement("circle"); + var dotSize = typeof _this.dotSize === "function" ? _this.dotSize() : _this.dotSize; + circle.setAttribute("r", dotSize.toString()); + circle.setAttribute("cx", point.x.toString()); + circle.setAttribute("cy", point.y.toString()); + circle.setAttribute("fill", color); + svg.appendChild(circle); + }); + var prefix = "data:image/svg+xml;base64,"; + var header = ""; + var body = svg.innerHTML; + if (body === undefined) { + var dummy = document.createElement("dummy"); + var nodes = svg.childNodes; + dummy.innerHTML = ""; + for (var i = 0; i < nodes.length; i += 1) { + dummy.appendChild(nodes[i].cloneNode(true)); + } + body = dummy.innerHTML; + } + var footer = ""; + var data = header + body + footer; + return prefix + btoa(data); + }; + return SignaturePad; + }()); -return SignaturePad; + return SignaturePad; }))); diff --git a/package.json b/package.json index 85b2d764..612e29ba 100644 --- a/package.json +++ b/package.json @@ -30,27 +30,31 @@ "docs" ], "devDependencies": { + "@types/jest": "^22.2.3", + "canvas-prebuilt": "^1.6.5-prerelease.1", "del": "^3.0.0", "del-cli": "^1.1.0", + "jest": "^22.4.3", "rollup": "^0.57.1", "rollup-plugin-tslint": "^0.1.34", "rollup-plugin-typescript2": "^0.13.0", "rollup-plugin-uglify": "^3.0.0", - "typescript": "^2.8.1", - "@types/jest": "^22.2.3", - "jest": "^22.4.3", - "ts-jest": "^22.4.2" + "ts-jest": "^22.4.2", + "typescript": "^2.8.1" }, "jest": { - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "testMatch": [ - "/tests/**/*.ts" - ], "moduleFileExtensions": [ "ts", "js" - ] + ], + "testEnvironmentOptions": { + "resources": "usable" + }, + "testMatch": [ + "/tests/**/*.test.ts" + ], + "transform": { + "^.+\\.tsx?$": "ts-jest" + } } } diff --git a/src/signature_pad.ts b/src/signature_pad.ts index 6fdf6167..ae17c0e2 100644 --- a/src/signature_pad.ts +++ b/src/signature_pad.ts @@ -54,12 +54,6 @@ export default class SignaturePad { private _lastVelocity: number; private _lastWidth: number; private _strokeMoveUpdate: (event: MouseEvent | Touch) => void; - private _handleMouseDown: (event: MouseEvent) => void; - private _handleMouseMove: (event: MouseEvent) => void; - private _handleMouseUp: (event: MouseEvent) => void; - private _handleTouchStart: (event: TouchEvent) => void; - private _handleTouchMove: (event: TouchEvent) => void; - private _handleTouchEnd: (event: TouchEvent) => void; /* tslint:enable: variable-name */ constructor(private canvas: HTMLCanvasElement, private options: IOptions = {}) { @@ -86,56 +80,6 @@ export default class SignaturePad { this._ctx = canvas.getContext("2d") as CanvasRenderingContext2D; this.clear(); - // We need add these inline so they are available to unbind while still having - // access to 'this'. - this._handleMouseDown = (event: MouseEvent) => { - if (event.which === 1) { - this._mouseButtonDown = true; - this._strokeBegin(event); - } - }; - - this._handleMouseMove = (event) => { - if (this._mouseButtonDown) { - this._strokeMoveUpdate(event); - } - }; - - this._handleMouseUp = (event) => { - if (event.which === 1 && this._mouseButtonDown) { - this._mouseButtonDown = false; - this._strokeEnd(event); - } - }; - - this._handleTouchStart = (event) => { - // Prevent scrolling. - event.preventDefault(); - - if (event.targetTouches.length === 1) { - const touch = event.changedTouches[0]; - this._strokeBegin(touch); - } - }; - - this._handleTouchMove = (event) => { - // Prevent scrolling. - event.preventDefault(); - - const touch = event.targetTouches[0]; - this._strokeMoveUpdate(touch); - }; - - this._handleTouchEnd = (event) => { - const wasCanvasTouched = event.target === this.canvas; - if (wasCanvasTouched) { - event.preventDefault(); - - const touch = event.changedTouches[0]; - this._strokeEnd(touch); - } - }; - // Enable mouse and touch event handlers this.on(); } @@ -154,21 +98,31 @@ export default class SignaturePad { this._isEmpty = true; } - public fromDataURL(dataUrl: string, options: { ratio?: number, width?: number, height?: number } = {}): void { + public fromDataURL( + dataUrl: string, + options: { ratio?: number, width?: number, height?: number } = {}, + callback?: (error?: ErrorEvent) => void, + ): void { const image = new Image(); const ratio = options.ratio || window.devicePixelRatio || 1; const width = options.width || (this.canvas.width / ratio); const height = options.height || (this.canvas.height / ratio); this._reset(); - image.src = dataUrl; + image.onload = () => { this._ctx.drawImage(image, 0, 0, width, height); + if (callback) { callback(); } + }; + image.onerror = (error) => { + if (callback) { callback(error); } }; + image.src = dataUrl; + this._isEmpty = false; } - public toDataURL(type?: string, encoderOptions?: number) { + public toDataURL(type = "image/png", encoderOptions?: number) { switch (type) { case "image/svg+xml": return this._toSVG(); @@ -179,7 +133,10 @@ export default class SignaturePad { public on(): void { this._handleMouseEvents(); - this._handleTouchEvents(); + + if ("ontouchstart" in window) { + this._handleTouchEvents(); + } } public off(): void { @@ -191,8 +148,12 @@ export default class SignaturePad { this.canvas.removeEventListener("mousemove", this._handleMouseMove); document.removeEventListener("mouseup", this._handleMouseUp); + // TS 2.8.1 has incorrect type definition for touch event handlers + // @ts-ignore this.canvas.removeEventListener("touchstart", this._handleTouchStart); + // @ts-ignore this.canvas.removeEventListener("touchmove", this._handleTouchMove); + // @ts-ignore this.canvas.removeEventListener("touchend", this._handleTouchEnd); } @@ -216,6 +177,55 @@ export default class SignaturePad { return this._data; } + // Event handlers + private _handleMouseDown = (event: MouseEvent): void => { + if (event.which === 1) { + this._mouseButtonDown = true; + this._strokeBegin(event); + } + } + + private _handleMouseMove = (event: MouseEvent): void => { + if (this._mouseButtonDown) { + this._strokeMoveUpdate(event); + } + } + + private _handleMouseUp = (event: MouseEvent): void => { + if (event.which === 1 && this._mouseButtonDown) { + this._mouseButtonDown = false; + this._strokeEnd(event); + } + } + + private _handleTouchStart = (event: TouchEvent): void => { + // Prevent scrolling. + event.preventDefault(); + + if (event.targetTouches.length === 1) { + const touch = event.changedTouches[0]; + this._strokeBegin(touch); + } + } + + private _handleTouchMove = (event: TouchEvent): void => { + // Prevent scrolling. + event.preventDefault(); + + const touch = event.targetTouches[0]; + this._strokeMoveUpdate(touch); + } + + private _handleTouchEnd = (event: TouchEvent): void => { + const wasCanvasTouched = event.target === this.canvas; + if (wasCanvasTouched) { + event.preventDefault(); + + const touch = event.changedTouches[0]; + this._strokeEnd(touch); + } + } + // Private methods private _strokeBegin(event: MouseEvent | Touch): void { const newPointGroup = { @@ -282,8 +292,12 @@ export default class SignaturePad { this.canvas.style.msTouchAction = "none"; this.canvas.style.touchAction = "none"; + // TS 2.8.1 has incorrect type definition for touch event handlers + // @ts-ignore this.canvas.addEventListener("touchstart", this._handleTouchStart); + // @ts-ignore this.canvas.addEventListener("touchmove", this._handleTouchMove); + // @ts-ignore this.canvas.addEventListener("touchend", this._handleTouchEnd); } diff --git a/tests/fixtures/face.ts b/tests/fixtures/face.ts new file mode 100644 index 00000000..c596d49e --- /dev/null +++ b/tests/fixtures/face.ts @@ -0,0 +1,106 @@ +export const dataURL = { + png: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAABmJLR0QA/wD/AP+gvaeTAAAF7klEQVR4nO3de4hmdRnA8e+ul1W7bIhbambZhVJLMSjoQoWGFkFF0R00iKgIukF0x/8Co0D6IzKiC1hEF7pDqZUZlhUJhtKSYGHZ1dStbE3dpj9+s+zMzjvrrjvOmZn9fODAu7vMznPmvO8zv8tznlMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACw2KapA2BVHVa9bv71F6pdE8ZyMDbKeQD7cEE1N39cMHEsB2OjnAcHaPPUAQDsr8OnDmCDWC9TlEsbo5Iaca5XG+U8YBKmKLAKTAlXxsMWvH7sZFHABidhrYw/L3h9+mRRAOyH09ozJby7OnvacACWt6m6pT1Ja2f1zEkjAtiHz7YnYe1OWsdOGhFsMNawVs7eP8ujGlvuh00QC2xIPkwr53HVuXv93eOrI6ofrH44AMvbWt3c4mnhXPW/6mUTxgUw01Oqf7c0af2rOmPCuABmemVLE9Zc9Yfq5AnjYu26sPpTddHUgXBouqjZSevGJC0We0V73h+7qiOnDYdD0WHVN5udtH5XPWG60FhDnt7iJYTvTxsOh7KjqyuanbT+Wj1tutBYA55d3dae98S1jY0bmMyW6jPNTlq7qrdNFxoTenn1n/a8F66rtk0aESxwfnV7sxPXt6oTpguNVfamxi+r3df/ihZ3+4A14aTq281OWn+vXjJdaKyCLdVXWnzdL2kUFsOadWGza7XmqouzS7QRnVxd3eJr/b5JI4IDsK36crOT1s+qR08XGivsRY0R9O7r+9/qA5NGBPfTa6s7Wpq0/lG9asK4OHjHVB9v3Jq1sHjY7jDr2mOqq5o92vpq9cjJIuP+Oqf6bYuv5VXV8VMGBStlc/X26p8tTVo7G4uzOmusfY9p6cL6ruojWVxnAzqh+mRjnWPvxPX16qHThcY+PKT6cOOXy8JrdlNjtAUb2qOqj1Z3tvReRK2X144jq3c2HkKy8Drd1Uhgx0wXGqy+rdWnWzrF+HyjYSDT2FK9ufpbS0fCX8u14RD36paub93TuO1H94fVc3T11sZu396J6lfVc6cLDdaWU5pdKX939aVGO2YeGGc0WgbNGlHdVL2n8eQkYC/Pq37S7DKIa6v3pvD0YB3R6FN1cUvLE3Yfl2dBHfbbi1tcQb3wuLdxU+1rGk/v4b5tqV5Qfara0eyf613VFxt9rIADdEz1ocaI655mf8hurz7XeBCGfkuLbW20tL605btp7Ky+W72xOm6aMGHjObF6R2NaOOuDt3uH8RfVN6rXd+iNvjY1bo15f/Xjlk/ydzZGqOenNAEecE9u1HHN2s1aeNxafaKxu7URK+k3V2dVH2tUoN/avpP5ldUbUqALk9jcWG/5YHVZy6/NzDWKHy9p9OVarx/Yw6unVu+qvtO+z3d3aciPGrdFnTRBvBwgW7GHlk2NB2C8tNFT/NzGYvPe7q1+3tgJ+15jGjm3SjHury3VCxt1aKc2ngl5ZvXg+/i6GxtP4r68+mGjWwawDmyr3tKYCi23njPXeGjGFY3q7odPEWijaPMZ1bsb9WgLe6Ivd+xodEm4bP7rTln1qFlRRljstq0x4jqvOrvl29rMVdsbHTR/2qjw3t4oYF0JRzXW4E6rntQYEZ5aPbEx5VvOzuqGxqbDNY0R4vZGPyo2CAmLWTY1ksR5jSR2Tvtuj7KrscB/S/XHxojs2Eb31O3VXxo1Yzsa082tjQR5/Pzx/EaiOmv+++7PBsDN1S8b9+5dM//nXft/iqxHEhb740GN7qinN3YUz2z13zt3NBLglY0Hjl63yt+fNUDC4v7YWj2rUdN0ZmNUdEqzF/AP1F3V9dWvq9/MHzdUv1+B/5t1TsJiJW1rlAc8onpOY13rxPnjuMbz945oPDVoR+Nm4tvm/+3qxu7d9Y1pIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKvu/3i0tkPISVdDAAAAAElFTkSuQmCC", + svg: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzAwIDE1MCIgd2lkdGg9IjMwMCIgaGVpZ2h0PSIxNTAiPjxjaXJjbGUgcj0iMS41IiBjeD0iMTI1IiBjeT0iNTQiIGZpbGw9ImJsYWNrIj48L2NpcmNsZT48Y2lyY2xlIHI9IjEuNSIgY3g9IjE3NSIgY3k9IjU0IiBmaWxsPSJibGFjayI+PC9jaXJjbGU+PHBhdGggZD0iTSA4My4wMDAsNTcuMDAwIEMgODMuMTc0LDYwLjUxMCA4NC4wMDAsNjAuMDAwIDg1LjAwMCw2My4wMDAiIHN0cm9rZS13aWR0aD0iNS41MDgiIHN0cm9rZT0iYmxhY2siIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0gODUuMDAwLDYzLjAwMCBDIDg5LjA2MCw2OC4wNDggODguNjc0LDY4LjAxMCA5NC4wMDAsNzIuMDAwIiBzdHJva2Utd2lkdGg9IjQuNDA3IiBzdHJva2U9ImJsYWNrIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD48cGF0aCBkPSJNIDk0LjAwMCw3Mi4wMDAgQyA5OS40ODgsNzUuNTE5IDk5LjA2MCw3Ni4wNDggMTA1LjAwMCw3OS4wMDAiIHN0cm9rZS13aWR0aD0iMy40MDIiIHN0cm9rZT0iYmxhY2siIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0gMTA1LjAwMCw3OS4wMDAgQyAxMDguNzAwLDgyLjE1NyAxMDguOTg4LDgxLjUxOSAxMTMuMDAwLDg0LjAwMCIgc3Ryb2tlLXdpZHRoPSIzLjQ5NyIgc3Ryb2tlPSJibGFjayIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L3BhdGg+PHBhdGggZD0iTSAxMTMuMDAwLDg0LjAwMCBDIDExOS41MDAsODYuMDAwIDExOS4yMDAsODYuNjU3IDEyNi4wMDAsODguMDAwIiBzdHJva2Utd2lkdGg9IjMuMjI4IiBzdHJva2U9ImJsYWNrIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD48cGF0aCBkPSJNIDEyNi4wMDAsODguMDAwIEMgMTMyLjQwMiw5MC40MTYgMTMyLjUwMCw5MC4wMDAgMTM5LjAwMCw5Mi4wMDAiIHN0cm9rZS13aWR0aD0iMy4yMTEiIHN0cm9rZT0iYmxhY2siIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0gMTM5LjAwMCw5Mi4wMDAgQyAxNDQuOTYxLDkzLjQ3NSAxNDQuOTAyLDkzLjQxNiAxNTEuMDAwLDk0LjAwMCIgc3Ryb2tlLXdpZHRoPSIzLjI1OCIgc3Ryb2tlPSJibGFjayIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L3BhdGg+PHBhdGggZD0iTSAxNTEuMDAwLDk0LjAwMCBDIDE1Ni41NjksOTQuNjUyIDE1Ni40NjEsOTQuNDc1IDE2Mi4wMDAsOTQuMDAwIiBzdHJva2Utd2lkdGg9IjMuMzY2IiBzdHJva2U9ImJsYWNrIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD48cGF0aCBkPSJNIDE2Mi4wMDAsOTQuMDAwIEMgMTY4Ljk5Miw5Mi40NjAgMTY5LjA2OSw5My4xNTIgMTc2LjAwMCw5MS4wMDAiIHN0cm9rZS13aWR0aD0iMy4wNzgiIHN0cm9rZT0iYmxhY2siIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0gMTc2LjAwMCw5MS4wMDAgQyAxODEuMDcyLDkwLjI4NSAxODAuOTkyLDg5Ljk2MCAxODYuMDAwLDg5LjAwMCIgc3Ryb2tlLXdpZHRoPSIzLjM3MiIgc3Ryb2tlPSJibGFjayIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L3BhdGg+PHBhdGggZD0iTSAxODYuMDAwLDg5LjAwMCBDIDE5Mi41ODIsODcuMjMyIDE5Mi41NzIsODcuMjg1IDE5OS4wMDAsODUuMDAwIiBzdHJva2Utd2lkdGg9IjMuMTMzIiBzdHJva2U9ImJsYWNrIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD48cGF0aCBkPSJNIDE5OS4wMDAsODUuMDAwIEMgMjA0LjI5Nyw4My41NjUgMjA0LjA4Miw4My4yMzIgMjA5LjAwMCw4MS4wMDAiIHN0cm9rZS13aWR0aD0iMy4yOTAiIHN0cm9rZT0iYmxhY2siIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PC9wYXRoPjxwYXRoIGQ9Ik0gMjA5LjAwMCw4MS4wMDAgQyAyMTUuMzM0LDc3LjQwNyAyMTUuMjk3LDc3LjU2NSAyMjEuMDAwLDczLjAwMCIgc3Ryb2tlLXdpZHRoPSIzLjExMyIgc3Ryb2tlPSJibGFjayIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48L3BhdGg+PHBhdGggZD0iTSAyMjEuMDAwLDczLjAwMCBDIDIyNC4xMTUsNzAuMTA3IDIyNC4zMzQsNzAuNDA3IDIyNy4wMDAsNjcuMDAwIiBzdHJva2Utd2lkdGg9IjMuNTM1IiBzdHJva2U9ImJsYWNrIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiPjwvcGF0aD48L3N2Zz4=", +}; + +export const json = [{ + "color": "black", + "points": [{ + "time": 1523730547109, + "x": 125, + "y": 54 + }] + }, + { + "color": "black", + "points": [{ + "time": 1523730547775, + "x": 175, + "y": 54 + }] + }, + { + "color": "black", + "points": [{ + "time": 1523730548448, + "x": 83, + "y": 57 + }, + { + "time": 1523730548657, + "x": 85, + "y": 63 + }, + { + "time": 1523730548690, + "x": 94, + "y": 72 + }, + { + "time": 1523730548706, + "x": 105, + "y": 79 + }, + { + "time": 1523730548722, + "x": 113, + "y": 84 + }, + { + "time": 1523730548739, + "x": 126, + "y": 88 + }, + { + "time": 1523730548757, + "x": 139, + "y": 92 + }, + { + "time": 1523730548774, + "x": 151, + "y": 94 + }, + { + "time": 1523730548791, + "x": 162, + "y": 94 + }, + { + "time": 1523730548807, + "x": 176, + "y": 91 + }, + { + "time": 1523730548824, + "x": 186, + "y": 89 + }, + { + "time": 1523730548840, + "x": 199, + "y": 85 + }, + { + "time": 1523730548856, + "x": 209, + "y": 81 + }, + { + "time": 1523730548873, + "x": 221, + "y": 73 + }, + { + "time": 1523730548890, + "x": 227, + "y": 67 + }, + { + "time": 1523730548924, + "x": 234, + "y": 59 + } + ] + } +]; diff --git a/tests/signature_pad.test.ts b/tests/signature_pad.test.ts new file mode 100644 index 00000000..248cc41d --- /dev/null +++ b/tests/signature_pad.test.ts @@ -0,0 +1,89 @@ +import SignaturePad from "../src/signature_pad"; +import { json, dataURL } from "./fixtures/face"; + +let canvas: HTMLCanvasElement; + +beforeAll(() => { + canvas = document.createElement('canvas'); + canvas.setAttribute("width", "300"); + canvas.setAttribute("height", "150"); +}) + +describe("#constructor", () => { + it("returns an instance of SignaturePad", () => { + const pad = new SignaturePad(canvas); + + expect(pad).toBeInstanceOf(SignaturePad); + }); + + it("allows to set 'throttle' to 0", () => { + const pad = new SignaturePad(canvas, { throttle: 0 }); + + expect(pad.throttle).toBe(0); + }); + + it("allows to set 'minDistance' to 0", () => { + const pad = new SignaturePad(canvas, { minDistance: 0 }); + + expect(pad.minDistance).toBe(0); + }); +}); + +describe("#clear", () => { + it.skip("clears canvas", () => {}); + + it("clears data structures", () => { + const pad = new SignaturePad(canvas); + + pad.fromData(json); + expect(pad.isEmpty()).toBe(false); + + pad.clear(); + + expect(pad.isEmpty()).toBe(true); + expect(pad.toData()).toEqual([]); + }); +}); + +describe("#isEmpty", () => { + it("returns true if pad is empty", () => { + const pad = new SignaturePad(canvas); + + expect(pad.isEmpty()).toBe(true); + }); + + it("returns false if pad is not empty", () => { + const pad = new SignaturePad(canvas); + pad.fromData(json); + + expect(pad.isEmpty()).toBe(false); + }); +}); + +describe("#fromData", () => {}); + +describe("#toData", () => { + it("returns JSON with point groups", () => { + const pad = new SignaturePad(canvas); + pad.fromData(json); + + expect(pad.toData()).toEqual(json); + }); +}); + +describe("#fromDataURL", () => {}); + +describe("#toDataURL", () => { + // Unfortunately, results of Canvas#toDataURL depend on a platform :/ + it.skip("returns PNG image in data URL format"); + + // Synchronous Canvas#toDataURL for JPEG images is not supported by 'canvas' library :/ + it.skip("returns JPG image in data URL format"); + + it("returns SVG image in data URL format", () => { + const pad = new SignaturePad(canvas); + pad.fromData(json); + + expect(pad.toDataURL('image/svg+xml')).toEqual(dataURL.svg); + }); +}); diff --git a/tslint.json b/tslint.json index 2c2e962c..41c44eac 100644 --- a/tslint.json +++ b/tslint.json @@ -10,6 +10,15 @@ "method": "never", "named": "never" } + }, + + "variable-name": { + "options": [ + "ban-keywords", + "check-format", + "allow-pascal-case", + "allow-leading-underscore" + ] } } } diff --git a/yarn.lock b/yarn.lock index 10aa96fe..c7f4e9be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -527,6 +527,14 @@ camelcase@^4.0.0, camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" +canvas-prebuilt@^1.6.5-prerelease.1: + version "1.6.5-prerelease.1" + resolved "https://registry.yarnpkg.com/canvas-prebuilt/-/canvas-prebuilt-1.6.5-prerelease.1.tgz#6814b20b9c80835dcc24bfd6199147288630521c" + dependencies: + node-pre-gyp "^0.6.29" + parse-css-font "^2.0.2" + units-css "^0.4.0" + capture-stack-trace@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" @@ -720,6 +728,36 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" +css-font-size-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz#854875ace9aca6a8d2ee0d345a44aae9bb6db6cb" + +css-font-stretch-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz#50cee9b9ba031fb5c952d4723139f1e107b54b10" + +css-font-style-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz#5c3532813f63b4a1de954d13cea86ab4333409e4" + +css-font-weight-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz#9bc04671ac85bc724b574ef5d3ac96b0d604fd97" + +css-global-keywords@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-global-keywords/-/css-global-keywords-1.0.1.tgz#72a9aea72796d019b1d2a3252de4e5aaa37e4a69" + +css-list-helpers@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-list-helpers/-/css-list-helpers-1.0.1.tgz#fff57192202db83240c41686f919e449a7024f7d" + dependencies: + tcomb "^2.5.0" + +css-system-font-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz#85c6f086aba4eb32c571a3086affc434b84823ed" + cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.2" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b" @@ -1574,6 +1612,10 @@ isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" +isnumeric@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -2351,7 +2393,7 @@ node-notifier@^5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.6.39: +node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.39: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" dependencies: @@ -2518,6 +2560,20 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" +parse-css-font@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/parse-css-font/-/parse-css-font-2.0.2.tgz#7b60b060705a25a9b90b7f0ed493e5823248a652" + dependencies: + css-font-size-keywords "^1.0.0" + css-font-stretch-keywords "^1.0.1" + css-font-style-keywords "^1.0.1" + css-font-weight-keywords "^1.0.0" + css-global-keywords "^1.0.1" + css-list-helpers "^1.0.1" + css-system-font-keywords "^1.0.0" + tcomb "^2.5.0" + unquote "^1.1.0" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -3222,6 +3278,10 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" +tcomb@^2.5.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0" + term-size@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" @@ -3373,10 +3433,21 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +units-css@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" + dependencies: + isnumeric "^0.2.0" + viewport-dimensions "^0.2.0" + universalify@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" +unquote@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -3431,6 +3502,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +viewport-dimensions@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"