summaryrefslogtreecommitdiffstats
path: root/main/survey/js/signature
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--main/survey/js/signature/LICENSE.txt27
-rw-r--r--main/survey/js/signature/flashcanvas.js28
-rw-r--r--main/survey/js/signature/flashcanvas.swfbin0 -> 21235 bytes
-rw-r--r--main/survey/js/signature/jquery.signaturepad.js895
-rw-r--r--main/survey/js/signature_pad.js323
5 files changed, 1273 insertions, 0 deletions
diff --git a/main/survey/js/signature/LICENSE.txt b/main/survey/js/signature/LICENSE.txt
new file mode 100644
index 0000000..f0c14ea
--- /dev/null
+++ b/main/survey/js/signature/LICENSE.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Thomas J Bradley
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ - Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+ - Neither the name of Thomas J Bradley nor the names of its contributors may
+ be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/main/survey/js/signature/flashcanvas.js b/main/survey/js/signature/flashcanvas.js
new file mode 100644
index 0000000..e7b6ddf
--- /dev/null
+++ b/main/survey/js/signature/flashcanvas.js
@@ -0,0 +1,28 @@
+/*
+ * FlashCanvas
+ *
+ * Copyright (c) 2009 Tim Cameron Ryan
+ * Copyright (c) 2009-2011 FlashCanvas Project
+ * Released under the MIT/X License
+ */
+window.ActiveXObject&&!window.CanvasRenderingContext2D&&function(h,j){function D(a){this.code=a;this.message=T[a]}function U(a){this.width=a}function E(a){this.id=a.C++}function t(a){this.G=a;this.id=a.C++}function u(a,b){this.canvas=a;this.B=b;this.d=a.uniqueID;this.D();this.C=0;this.t="";var c=this;setInterval(function(){n[c.d]===0&&c.e()},30)}function A(){if(j.readyState==="complete"){j.detachEvent(F,A);for(var a=j.getElementsByTagName(r),b=0,c=a.length;b<c;++b)B.initElement(a[b])}}function G(){var a=
+event.srcElement,b=a.parentNode;a.blur();b.focus()}function H(){var a=event.propertyName;if(a==="width"||a==="height"){var b=event.srcElement,c=b[a],d=parseInt(c,10);if(isNaN(d)||d<0)d=a==="width"?300:150;if(c===d){b.style[a]=d+"px";b.getContext("2d").I(b.width,b.height)}else b[a]=d}}function I(){h.detachEvent(J,I);for(var a in s){var b=s[a],c=b.firstChild,d;for(d in c)if(typeof c[d]==="function")c[d]=k;for(d in b)if(typeof b[d]==="function")b[d]=k;c.detachEvent(K,G);b.detachEvent(L,H)}h[M]=k;h[N]=
+k;h[O]=k;h[C]=k;h[P]=k}function V(){var a=j.getElementsByTagName("script");a=a[a.length-1];return j.documentMode>=8?a.src:a.getAttribute("src",4)}function v(a){return(""+a).replace(/&/g,"&amp;").replace(/</g,"&lt;")}function W(a){return a.toLowerCase()}function i(a){throw new D(a);}function Q(a){var b=parseInt(a.width,10),c=parseInt(a.height,10);if(isNaN(b)||b<0)b=300;if(isNaN(c)||c<0)c=150;a.width=b;a.height=c}var k=null,r="canvas",M="CanvasRenderingContext2D",N="CanvasGradient",O="CanvasPattern",
+C="FlashCanvas",P="G_vmlCanvasManager",K="onfocus",L="onpropertychange",F="onreadystatechange",J="onunload",w=((h[C+"Options"]||{}).swfPath||V().replace(/[^\/]+$/,""))+"flashcanvas.swf",e=new function(a){for(var b=0,c=a.length;b<c;b++)this[a[b]]=b}(["toDataURL","save","restore","scale","rotate","translate","transform","setTransform","globalAlpha","globalCompositeOperation","strokeStyle","fillStyle","createLinearGradient","createRadialGradient","createPattern","lineWidth","lineCap","lineJoin","miterLimit",
+"shadowOffsetX","shadowOffsetY","shadowBlur","shadowColor","clearRect","fillRect","strokeRect","beginPath","closePath","moveTo","lineTo","quadraticCurveTo","bezierCurveTo","arcTo","rect","arc","fill","stroke","clip","isPointInPath","font","textAlign","textBaseline","fillText","strokeText","measureText","drawImage","createImageData","getImageData","putImageData","addColorStop","direction","resize"]),x={},n={},s={},y={};u.prototype={save:function(){this.b();this.c();this.m();this.l();this.z();this.w();
+this.F.push([this.f,this.g,this.A,this.u,this.j,this.h,this.i,this.k,this.p,this.q,this.n,this.o,this.v,this.r,this.s]);this.a.push(e.save)},restore:function(){var a=this.F;if(a.length){a=a.pop();this.globalAlpha=a[0];this.globalCompositeOperation=a[1];this.strokeStyle=a[2];this.fillStyle=a[3];this.lineWidth=a[4];this.lineCap=a[5];this.lineJoin=a[6];this.miterLimit=a[7];this.shadowOffsetX=a[8];this.shadowOffsetY=a[9];this.shadowBlur=a[10];this.shadowColor=a[11];this.font=a[12];this.textAlign=a[13];
+this.textBaseline=a[14]}this.a.push(e.restore)},scale:function(a,b){this.a.push(e.scale,a,b)},rotate:function(a){this.a.push(e.rotate,a)},translate:function(a,b){this.a.push(e.translate,a,b)},transform:function(a,b,c,d,f,g){this.a.push(e.transform,a,b,c,d,f,g)},setTransform:function(a,b,c,d,f,g){this.a.push(e.setTransform,a,b,c,d,f,g)},b:function(){var a=this.a;if(this.f!==this.globalAlpha){this.f=this.globalAlpha;a.push(e.globalAlpha,this.f)}if(this.g!==this.globalCompositeOperation){this.g=this.globalCompositeOperation;
+a.push(e.globalCompositeOperation,this.g)}},m:function(){if(this.A!==this.strokeStyle){var a=this.A=this.strokeStyle;this.a.push(e.strokeStyle,typeof a==="object"?a.id:a)}},l:function(){if(this.u!==this.fillStyle){var a=this.u=this.fillStyle;this.a.push(e.fillStyle,typeof a==="object"?a.id:a)}},createLinearGradient:function(a,b,c,d){isFinite(a)&&isFinite(b)&&isFinite(c)&&isFinite(d)||i(9);this.a.push(e.createLinearGradient,a,b,c,d);return new t(this)},createRadialGradient:function(a,b,c,d,f,g){isFinite(a)&&
+isFinite(b)&&isFinite(c)&&isFinite(d)&&isFinite(f)&&isFinite(g)||i(9);if(c<0||g<0)i(1);this.a.push(e.createRadialGradient,a,b,c,d,f,g);return new t(this)},createPattern:function(a,b){a||i(17);var c=a.tagName,d,f=this.d;if(c){c=c.toLowerCase();if(c==="img")d=a.getAttribute("src",2);else if(c===r||c==="video")return;else i(17)}else if(a.src)d=a.src;else i(17);b==="repeat"||b==="no-repeat"||b==="repeat-x"||b==="repeat-y"||b===""||b===k||i(12);this.a.push(e.createPattern,v(d),b);if(x[f]){this.e();++n[f]}return new E(this)},
+z:function(){var a=this.a;if(this.j!==this.lineWidth){this.j=this.lineWidth;a.push(e.lineWidth,this.j)}if(this.h!==this.lineCap){this.h=this.lineCap;a.push(e.lineCap,this.h)}if(this.i!==this.lineJoin){this.i=this.lineJoin;a.push(e.lineJoin,this.i)}if(this.k!==this.miterLimit){this.k=this.miterLimit;a.push(e.miterLimit,this.k)}},c:function(){var a=this.a;if(this.p!==this.shadowOffsetX){this.p=this.shadowOffsetX;a.push(e.shadowOffsetX,this.p)}if(this.q!==this.shadowOffsetY){this.q=this.shadowOffsetY;
+a.push(e.shadowOffsetY,this.q)}if(this.n!==this.shadowBlur){this.n=this.shadowBlur;a.push(e.shadowBlur,this.n)}if(this.o!==this.shadowColor){this.o=this.shadowColor;a.push(e.shadowColor,this.o)}},clearRect:function(a,b,c,d){this.a.push(e.clearRect,a,b,c,d)},fillRect:function(a,b,c,d){this.b();this.c();this.l();this.a.push(e.fillRect,a,b,c,d)},strokeRect:function(a,b,c,d){this.b();this.c();this.m();this.z();this.a.push(e.strokeRect,a,b,c,d)},beginPath:function(){this.a.push(e.beginPath)},closePath:function(){this.a.push(e.closePath)},
+moveTo:function(a,b){this.a.push(e.moveTo,a,b)},lineTo:function(a,b){this.a.push(e.lineTo,a,b)},quadraticCurveTo:function(a,b,c,d){this.a.push(e.quadraticCurveTo,a,b,c,d)},bezierCurveTo:function(a,b,c,d,f,g){this.a.push(e.bezierCurveTo,a,b,c,d,f,g)},arcTo:function(a,b,c,d,f){f<0&&isFinite(f)&&i(1);this.a.push(e.arcTo,a,b,c,d,f)},rect:function(a,b,c,d){this.a.push(e.rect,a,b,c,d)},arc:function(a,b,c,d,f,g){c<0&&isFinite(c)&&i(1);this.a.push(e.arc,a,b,c,d,f,g?1:0)},fill:function(){this.b();this.c();
+this.l();this.a.push(e.fill)},stroke:function(){this.b();this.c();this.m();this.z();this.a.push(e.stroke)},clip:function(){this.a.push(e.clip)},w:function(){var a=this.a;if(this.v!==this.font)try{var b=y[this.d];b.style.font=this.v=this.font;var c=b.currentStyle;a.push(e.font,[c.fontStyle,c.fontWeight,b.offsetHeight,c.fontFamily].join(" "))}catch(d){}if(this.r!==this.textAlign){this.r=this.textAlign;a.push(e.textAlign,this.r)}if(this.s!==this.textBaseline){this.s=this.textBaseline;a.push(e.textBaseline,
+this.s)}if(this.t!==this.canvas.currentStyle.direction){this.t=this.canvas.currentStyle.direction;a.push(e.direction,this.t)}},fillText:function(a,b,c,d){this.b();this.l();this.c();this.w();this.a.push(e.fillText,v(a),b,c,d===void 0?Infinity:d)},strokeText:function(a,b,c,d){this.b();this.m();this.c();this.w();this.a.push(e.strokeText,v(a),b,c,d===void 0?Infinity:d)},measureText:function(a){var b=y[this.d];try{b.style.font=this.font}catch(c){}b.innerText=a.replace(/[ \n\f\r]/g,"\t");return new U(b.offsetWidth)},
+drawImage:function(a,b,c,d,f,g,o,l,z){a||i(17);var p=a.tagName,m,q=arguments.length,R=this.d;if(p){p=p.toLowerCase();if(p==="img")m=a.getAttribute("src",2);else if(p===r||p==="video")return;else i(17)}else if(a.src)m=a.src;else i(17);this.b();this.c();m=v(m);if(q===3)this.a.push(e.drawImage,q,m,b,c);else if(q===5)this.a.push(e.drawImage,q,m,b,c,d,f);else if(q===9){if(d===0||f===0)i(1);this.a.push(e.drawImage,q,m,b,c,d,f,g,o,l,z)}else return;if(x[R]){this.e();++n[R]}},D:function(){this.globalAlpha=
+this.f=1;this.globalCompositeOperation=this.g="source-over";this.fillStyle=this.u=this.strokeStyle=this.A="#000000";this.lineWidth=this.j=1;this.lineCap=this.h="butt";this.lineJoin=this.i="miter";this.miterLimit=this.k=10;this.shadowBlur=this.n=this.shadowOffsetY=this.q=this.shadowOffsetX=this.p=0;this.shadowColor=this.o="rgba(0, 0, 0, 0.0)";this.font=this.v="10px sans-serif";this.textAlign=this.r="start";this.textBaseline=this.s="alphabetic";this.a=[];this.F=[]},H:function(){var a=this.a;this.a=
+[];return a},e:function(){var a=this.H();if(a.length>0)return eval(this.B.CallFunction('<invoke name="executeCommand" returntype="javascript"><arguments><string>'+a.join("&#0;")+"</string></arguments></invoke>"))},I:function(a,b){this.e();this.D();if(a>0)this.B.width=a;if(b>0)this.B.height=b;this.a.push(e.resize,a,b)}};t.prototype={addColorStop:function(a,b){if(isNaN(a)||a<0||a>1)i(1);this.G.a.push(e.addColorStop,this.id,a,b)}};D.prototype=Error();var T={1:"INDEX_SIZE_ERR",9:"NOT_SUPPORTED_ERR",11:"INVALID_STATE_ERR",
+12:"SYNTAX_ERR",17:"TYPE_MISMATCH_ERR",18:"SECURITY_ERR"},B={initElement:function(a){if(a.getContext)return a;var b=a.uniqueID,c="external"+b;x[b]=false;n[b]=1;Q(a);a.innerHTML='<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="'+location.protocol+'//fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="100%" height="100%" id="'+c+'"><param name="allowScriptAccess" value="always"><param name="flashvars" value="id='+c+'"><param name="wmode" value="transparent"></object><span style="margin:0;padding:0;border:0;display:inline-block;position:static;height:1em;overflow:visible;white-space:nowrap"></span>';
+s[b]=a;var d=a.firstChild;y[b]=a.lastChild;var f=j.body.contains;if(f(a))d.movie=w;else var g=setInterval(function(){if(f(a)){clearInterval(g);d.movie=w}},0);if(j.compatMode==="BackCompat"||!h.XMLHttpRequest)y[b].style.overflow="hidden";var o=new u(a,d);a.getContext=function(l){return l==="2d"?o:k};a.toDataURL=function(l,z){(""+l).replace(/[A-Z]+/g,W)==="image/jpeg"?o.a.push(e.toDataURL,l,typeof z==="number"?z:""):o.a.push(e.toDataURL,l);return o.e()};d.attachEvent(K,G);return a},saveImage:function(a){a.firstChild.saveImage()},
+setOptions:function(){},trigger:function(a,b){s[a].fireEvent("on"+b)},unlock:function(a,b){n[a]&&--n[a];if(b){var c=s[a],d=c.firstChild,f,g;Q(c);f=c.width;g=c.height;c.style.width=f+"px";c.style.height=g+"px";if(f>0)d.width=f;if(g>0)d.height=g;d.resize(f,g);c.attachEvent(L,H);x[a]=true}}};j.createElement(r);j.createStyleSheet().cssText=r+"{display:inline-block;overflow:hidden;width:300px;height:150px}";j.readyState==="complete"?A():j.attachEvent(F,A);h.attachEvent(J,I);if(w.indexOf(location.protocol+
+"//"+location.host+"/")===0){var S=new ActiveXObject("Microsoft.XMLHTTP");S.open("GET",w,false);S.send(k)}h[M]=u;h[N]=t;h[O]=E;h[C]=B;h[P]={init:function(){},init_:function(){},initElement:B.initElement};keep=u.measureText}(window,document);
diff --git a/main/survey/js/signature/flashcanvas.swf b/main/survey/js/signature/flashcanvas.swf
new file mode 100644
index 0000000..66ff213
--- /dev/null
+++ b/main/survey/js/signature/flashcanvas.swf
Binary files differ
diff --git a/main/survey/js/signature/jquery.signaturepad.js b/main/survey/js/signature/jquery.signaturepad.js
new file mode 100644
index 0000000..bcef407
--- /dev/null
+++ b/main/survey/js/signature/jquery.signaturepad.js
@@ -0,0 +1,895 @@
+/**
+ * Usage for accepting signatures:
+ * $('.sigPad').signaturePad()
+ *
+ * Usage for displaying previous signatures:
+ * $('.sigPad').signaturePad({displayOnly:true}).regenerate(sig)
+ * or
+ * var api = $('.sigPad').signaturePad({displayOnly:true})
+ * api.regenerate(sig)
+ */
+(function ($) {
+
+function SignaturePad (selector, options) {
+ /**
+ * Reference to the object for use in public methods
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ var self = this
+
+ /**
+ * Holds the merged default settings and user passed settings
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , settings = $.extend({}, $.fn.signaturePad.defaults, options)
+
+ /**
+ * The current context, as passed by jQuery, of selected items
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , context = $(selector)
+
+ /**
+ * jQuery reference to the canvas element inside the signature pad
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , canvas = $(settings.canvas, context)
+
+ /**
+ * Dom reference to the canvas element inside the signature pad
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , element = canvas.get(0)
+
+ /**
+ * The drawing context for the signature canvas
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , canvasContext = null
+
+ /**
+ * Holds the previous point of drawing
+ * Disallows drawing over the same location to make lines more delicate
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , previous = {'x': null, 'y': null}
+
+ /**
+ * An array holding all the points and lines to generate the signature
+ * Each item is an object like:
+ * {
+ * mx: moveTo x coordinate
+ * my: moveTo y coordinate
+ * lx: lineTo x coordinate
+ * lx: lineTo y coordinate
+ * }
+ *
+ * @private
+ *
+ * @type {Array}
+ */
+ , output = []
+
+ /**
+ * Stores a timeout for when the mouse leaves the canvas
+ * If the mouse has left the canvas for a specific amount of time
+ * Stops drawing on the canvas
+ *
+ * @private
+ *
+ * @type {Object}
+ */
+ , mouseLeaveTimeout = false
+
+ /**
+ * Whether the mouse button is currently pressed down or not
+ *
+ * @private
+ *
+ * @type {Boolean}
+ */
+ , mouseButtonDown = false
+
+ /**
+ * Whether the browser is a touch event browser or not
+ *
+ * @private
+ *
+ * @type {Boolean}
+ */
+ , touchable = false
+
+ /**
+ * Whether events have already been bound to the canvas or not
+ *
+ * @private
+ *
+ * @type {Boolean}
+ */
+ , eventsBound = false
+
+ /**
+ * Remembers the default font-size when typing, and will allow it to be scaled for bigger/smaller names
+ *
+ * @private
+ *
+ * @type {Number}
+ */
+ , typeItDefaultFontSize = 30
+
+ /**
+ * Remembers the current font-size when typing
+ *
+ * @private
+ *
+ * @type {Number}
+ */
+ , typeItCurrentFontSize = typeItDefaultFontSize
+
+ /**
+ * Remembers how many characters are in the name field, to help with the scaling feature
+ *
+ * @private
+ *
+ * @type {Number}
+ */
+ , typeItNumChars = 0
+
+
+ /**
+ * Clears the mouseLeaveTimeout
+ * Resets some other variables that may be active
+ *
+ * @private
+ */
+ function clearMouseLeaveTimeout () {
+ clearTimeout(mouseLeaveTimeout)
+ mouseLeaveTimeout = false
+ mouseButtonDown = false
+ }
+
+ /**
+ * Draws a line on canvas using the mouse position
+ * Checks previous position to not draw over top of previous drawing
+ * (makes the line really thick and poorly anti-aliased)
+ *
+ * @private
+ *
+ * @param {Object} e The event object
+ * @param {Number} newYOffset A pixel value for drawing the newY, used for drawing a single dot on click
+ */
+ function drawLine (e, newYOffset) {
+ var offset, newX, newY
+
+ e.preventDefault()
+
+ offset = $(e.target).offset()
+
+ clearTimeout(mouseLeaveTimeout)
+ mouseLeaveTimeout = false
+
+ if (typeof e.targetTouches !== 'undefined') {
+ newX = Math.floor(e.targetTouches[0].pageX - offset.left)
+ newY = Math.floor(e.targetTouches[0].pageY - offset.top)
+ } else {
+ newX = Math.floor(e.pageX - offset.left)
+ newY = Math.floor(e.pageY - offset.top)
+ }
+
+ if (previous.x === newX && previous.y === newY)
+ return true
+
+ if (previous.x === null)
+ previous.x = newX
+
+ if (previous.y === null)
+ previous.y = newY
+
+ if (newYOffset)
+ newY += newYOffset
+
+ canvasContext.beginPath()
+ canvasContext.moveTo(previous.x, previous.y)
+ canvasContext.lineTo(newX, newY)
+ canvasContext.lineCap = settings.penCap
+ canvasContext.stroke()
+ canvasContext.closePath()
+
+ output.push({
+ 'lx' : newX
+ , 'ly' : newY
+ , 'mx' : previous.x
+ , 'my' : previous.y
+ })
+
+ previous.x = newX
+ previous.y = newY
+
+ if (settings.onDraw && typeof settings.onDraw === 'function')
+ settings.onDraw.apply(self)
+ }
+
+ /**
+ * Callback wrapper for executing stopDrawing without the event
+ * Put up here so that it can be removed at a later time
+ *
+ * @private
+ */
+ function stopDrawingWrapper () {
+ stopDrawing()
+ }
+
+ /**
+ * Callback registered to mouse/touch events of the canvas
+ * Stops the drawing abilities
+ *
+ * @private
+ *
+ * @param {Object} e The event object
+ */
+ function stopDrawing (e) {
+ if (!!e) {
+ drawLine(e, 1)
+ } else {
+ if (touchable) {
+ canvas.each(function () {
+ this.removeEventListener('touchmove', drawLine)
+ // this.removeEventListener('MSPointerMove', drawLine)
+ })
+ } else {
+ canvas.unbind('mousemove.signaturepad')
+ }
+
+ if (output.length > 0 && settings.onDrawEnd && typeof settings.onDrawEnd === 'function')
+ settings.onDrawEnd.apply(self)
+ }
+
+ previous.x = null
+ previous.y = null
+
+ if (settings.output && output.length > 0)
+ $(settings.output, context).val(JSON.stringify(output))
+ }
+
+ /**
+ * Draws the signature line
+ *
+ * @private
+ */
+ function drawSigLine () {
+ if (!settings.lineWidth)
+ return false
+
+ canvasContext.beginPath()
+ canvasContext.lineWidth = settings.lineWidth
+ canvasContext.strokeStyle = settings.lineColour
+ canvasContext.moveTo(settings.lineMargin, settings.lineTop)
+ canvasContext.lineTo(element.width - settings.lineMargin, settings.lineTop)
+ //canvasContext.moveTo(50, 250)
+ //canvasContext.moveTo(50, 140)
+ //canvasContext.lineTo(element.width - 50, 250)
+ //canvasContext.lineTo(element.width - 50, 140)
+ canvasContext.stroke()
+ canvasContext.closePath()
+ }
+
+ /**
+ * Clears all drawings off the canvas and redraws the signature line
+ *
+ * @private
+ */
+ function clearCanvas () {
+ canvasContext.clearRect(0, 0, element.width, element.height)
+ canvasContext.fillStyle = settings.bgColour
+ canvasContext.fillRect(0, 0, element.width, element.height)
+
+ if (!settings.displayOnly)
+ //drawSigLine()
+
+ canvasContext.lineWidth = settings.penWidth
+ canvasContext.strokeStyle = settings.penColour
+
+ $(settings.output, context).val('')
+ output = []
+
+ stopDrawing()
+ }
+
+ /**
+ * Callback registered to mouse/touch events of the canvas
+ * Draws a line at the mouse cursor location, starting a new line if necessary
+ *
+ * @private
+ *
+ * @param {Object} e The event object
+ * @param {Object} o The object context registered to the event; canvas
+ */
+ function onMouseMove(e, o) {
+ if (previous.x == null) {
+ drawLine(e, 1)
+ } else {
+ drawLine(e, o)
+ }
+ }
+
+ /**
+ * Callback registered to mouse/touch events of canvas
+ * Triggers the drawLine function
+ *
+ * @private
+ *
+ * @param {Object} e The event object
+ * @param {Object} touchObject The object context registered to the event; canvas
+ */
+ function startDrawing (e, touchObject) {
+ if (touchable) {
+ touchObject.addEventListener('touchmove', onMouseMove, false)
+ // touchObject.addEventListener('MSPointerMove', onMouseMove, false)
+ } else {
+ canvas.bind('mousemove.signaturepad', onMouseMove)
+ }
+
+ // Draws a single point on initial mouse down, for people with periods in their name
+ drawLine(e, 1)
+ }
+
+ /**
+ * Removes all the mouse events from the canvas
+ *
+ * @private
+ */
+ function disableCanvas () {
+ eventsBound = false
+
+ canvas.each(function () {
+ if (this.removeEventListener) {
+ this.removeEventListener('touchend', stopDrawingWrapper)
+ this.removeEventListener('touchcancel', stopDrawingWrapper)
+ this.removeEventListener('touchmove', drawLine)
+ // this.removeEventListener('MSPointerUp', stopDrawingWrapper)
+ // this.removeEventListener('MSPointerCancel', stopDrawingWrapper)
+ // this.removeEventListener('MSPointerMove', drawLine)
+ }
+
+ if (this.ontouchstart)
+ this.ontouchstart = null;
+ })
+
+ $(document).unbind('mouseup.signaturepad')
+ canvas.unbind('mousedown.signaturepad')
+ canvas.unbind('mousemove.signaturepad')
+ canvas.unbind('mouseleave.signaturepad')
+
+ $(settings.clear, context).unbind('click.signaturepad')
+ }
+
+ /**
+ * Lazy touch event detection
+ * Uses the first press on the canvas to detect either touch or mouse reliably
+ * Will then bind other events as needed
+ *
+ * @private
+ *
+ * @param {Object} e The event object
+ */
+ function initDrawEvents (e) {
+ if (eventsBound)
+ return false
+
+ eventsBound = true
+
+ // Closes open keyboards to free up space
+ $('input').blur();
+
+ if (typeof e.targetTouches !== 'undefined')
+ touchable = true
+
+ if (touchable) {
+ canvas.each(function () {
+ this.addEventListener('touchend', stopDrawingWrapper, false)
+ this.addEventListener('touchcancel', stopDrawingWrapper, false)
+ // this.addEventListener('MSPointerUp', stopDrawingWrapper, false)
+ // this.addEventListener('MSPointerCancel', stopDrawingWrapper, false)
+ })
+
+ canvas.unbind('mousedown.signaturepad')
+ } else {
+ $(document).bind('mouseup.signaturepad', function () {
+ if (mouseButtonDown) {
+ stopDrawing()
+ clearMouseLeaveTimeout()
+ }
+ })
+ canvas.bind('mouseleave.signaturepad', function (e) {
+ if (mouseButtonDown) stopDrawing(e)
+
+ if (mouseButtonDown && !mouseLeaveTimeout) {
+ mouseLeaveTimeout = setTimeout(function () {
+ stopDrawing()
+ clearMouseLeaveTimeout()
+ }, 500)
+ }
+ })
+
+ canvas.each(function () {
+ this.ontouchstart = null
+ })
+ }
+ }
+
+ /**
+ * Triggers the abilities to draw on the canvas
+ * Sets up mouse/touch events, hides and shows descriptions and sets current classes
+ *
+ * @private
+ */
+ function drawIt () {
+ $(settings.typed, context).hide()
+ clearCanvas()
+
+ canvas.each(function () {
+ this.ontouchstart = function (e) {
+ e.preventDefault()
+ mouseButtonDown = true
+ initDrawEvents(e)
+ startDrawing(e, this)
+ }
+ })
+
+ canvas.bind('mousedown.signaturepad', function (e) {
+ e.preventDefault()
+
+ // Only allow left mouse clicks to trigger signature drawing
+ if (e.which > 1) return false
+
+ mouseButtonDown = true
+ initDrawEvents(e)
+ startDrawing(e)
+ })
+
+ $(settings.clear, context).bind('click.signaturepad', function (e) { e.preventDefault(); clearCanvas() })
+
+ $(settings.typeIt, context).bind('click.signaturepad', function (e) { e.preventDefault(); typeIt() })
+ $(settings.drawIt, context).unbind('click.signaturepad')
+ $(settings.drawIt, context).bind('click.signaturepad', function (e) { e.preventDefault() })
+
+ $(settings.typeIt, context).removeClass(settings.currentClass)
+ $(settings.drawIt, context).addClass(settings.currentClass)
+ $(settings.sig, context).addClass(settings.currentClass)
+
+ $(settings.typeItDesc, context).hide()
+ $(settings.drawItDesc, context).show()
+ $(settings.clear, context).show()
+ }
+
+ /**
+ * Triggers the abilities to type in the input for generating a signature
+ * Sets up mouse events, hides and shows descriptions and sets current classes
+ *
+ * @private
+ */
+ function typeIt () {
+ clearCanvas()
+ disableCanvas()
+ $(settings.typed, context).show()
+
+ $(settings.drawIt, context).bind('click.signaturepad', function (e) { e.preventDefault(); drawIt() })
+ $(settings.typeIt, context).unbind('click.signaturepad')
+ $(settings.typeIt, context).bind('click.signaturepad', function (e) { e.preventDefault() })
+
+ $(settings.output, context).val('')
+
+ $(settings.drawIt, context).removeClass(settings.currentClass)
+ $(settings.typeIt, context).addClass(settings.currentClass)
+ $(settings.sig, context).removeClass(settings.currentClass)
+
+ $(settings.drawItDesc, context).hide()
+ $(settings.clear, context).hide()
+ $(settings.typeItDesc, context).show()
+
+ typeItCurrentFontSize = typeItDefaultFontSize = $(settings.typed, context).css('font-size').replace(/px/, '')
+ }
+
+ /**
+ * Callback registered on key up and blur events for input field
+ * Writes the text fields value as Html into an element
+ *
+ * @private
+ *
+ * @param {String} val The value of the input field
+ */
+ function type (val) {
+ var typed = $(settings.typed, context)
+ , cleanedVal = $.trim(val.replace(/>/g, '&gt;').replace(/</g, '&lt;'))
+ , oldLength = typeItNumChars
+ , edgeOffset = typeItCurrentFontSize * 0.5
+
+ typeItNumChars = cleanedVal.length
+ typed.html(cleanedVal)
+
+ if (!cleanedVal) {
+ typed.css('font-size', typeItDefaultFontSize + 'px')
+ return
+ }
+
+ if (typeItNumChars > oldLength && typed.outerWidth() > element.width) {
+ while (typed.outerWidth() > element.width) {
+ typeItCurrentFontSize--
+ typed.css('font-size', typeItCurrentFontSize + 'px')
+ }
+ }
+
+ if (typeItNumChars < oldLength && typed.outerWidth() + edgeOffset < element.width && typeItCurrentFontSize < typeItDefaultFontSize) {
+ while (typed.outerWidth() + edgeOffset < element.width && typeItCurrentFontSize < typeItDefaultFontSize) {
+ typeItCurrentFontSize++
+ typed.css('font-size', typeItCurrentFontSize + 'px')
+ }
+ }
+ }
+
+ /**
+ * Default onBeforeValidate function to clear errors
+ *
+ * @private
+ *
+ * @param {Object} context current context object
+ * @param {Object} settings provided settings
+ */
+ function onBeforeValidate (context, settings) {
+ $('p.' + settings.errorClass, context).remove()
+ context.removeClass(settings.errorClass)
+ $('input, label', context).removeClass(settings.errorClass)
+ }
+
+ /**
+ * Default onFormError function to show errors
+ *
+ * @private
+ *
+ * @param {Object} errors object contains validation errors (e.g. nameInvalid=true)
+ * @param {Object} context current context object
+ * @param {Object} settings provided settings
+ */
+ function onFormError (errors, context, settings) {
+ if (errors.nameInvalid) {
+ context.prepend(['<p class="', settings.errorClass, '">', settings.errorMessage, '</p>'].join(''))
+ $(settings.name, context).focus()
+ $(settings.name, context).addClass(settings.errorClass)
+ $('label[for=' + $(settings.name).attr('id') + ']', context).addClass(settings.errorClass)
+ }
+
+ if (errors.drawInvalid)
+ context.prepend(['<p class="', settings.errorClass, '">', settings.errorMessageDraw, '</p>'].join(''))
+ }
+
+ /**
+ * Validates the form to confirm a name was typed in the field
+ * If drawOnly also confirms that the user drew a signature
+ *
+ * @private
+ *
+ * @return {Boolean}
+ */
+ function validateForm () {
+ var valid = true
+ , errors = {drawInvalid: false, nameInvalid: false}
+ , onBeforeArguments = [context, settings]
+ , onErrorArguments = [errors, context, settings]
+
+ if (settings.onBeforeValidate && typeof settings.onBeforeValidate === 'function') {
+ settings.onBeforeValidate.apply(self,onBeforeArguments)
+ } else {
+ onBeforeValidate.apply(self, onBeforeArguments)
+ }
+
+ if (settings.drawOnly && output.length < 1) {
+ errors.drawInvalid = true
+ valid = false
+ }
+
+ if ($(settings.name, context).val() === '') {
+ errors.nameInvalid = true
+ valid = false
+ }
+
+ if (settings.onFormError && typeof settings.onFormError === 'function') {
+ settings.onFormError.apply(self,onErrorArguments)
+ } else {
+ onFormError.apply(self, onErrorArguments)
+ }
+
+ return valid
+ }
+
+ /**
+ * Redraws the signature on a specific canvas
+ *
+ * @private
+ *
+ * @param {Array} paths the signature JSON
+ * @param {Object} context the canvas context to draw on
+ * @param {Boolean} saveOutput whether to write the path to the output array or not
+ */
+ function drawSignature (paths, context, saveOutput) {
+ for(var i in paths) {
+ if (typeof paths[i] === 'object') {
+ context.beginPath()
+ context.moveTo(paths[i].mx, paths[i].my)
+ context.lineTo(paths[i].lx, paths[i].ly)
+ context.lineCap = settings.penCap
+ context.stroke()
+ context.closePath()
+
+ if (saveOutput) {
+ output.push({
+ 'lx' : paths[i].lx
+ , 'ly' : paths[i].ly
+ , 'mx' : paths[i].mx
+ , 'my' : paths[i].my
+ })
+ }
+ }
+ }
+ }
+
+ /**
+ * Initialisation function, called immediately after all declarations
+ * Technically public, but only should be used internally
+ *
+ * @private
+ */
+ function init () {
+ // Fixes the jQuery.fn.offset() function for Mobile Safari Browsers i.e. iPod Touch, iPad and iPhone
+ // https://gist.github.com/661844
+ // http://bugs.jquery.com/ticket/6446
+ if (parseFloat(((/CPU.+OS ([0-9_]{3}).*AppleWebkit.*Mobile/i.exec(navigator.userAgent)) || [0,'4_2'])[1].replace('_','.')) < 4.1) {
+ $.fn.Oldoffset = $.fn.offset;
+ $.fn.offset = function () {
+ var result = $(this).Oldoffset()
+ result.top -= window.scrollY
+ result.left -= window.scrollX
+
+ return result
+ }
+ }
+
+ // Disable selection on the typed div and canvas
+ $(settings.typed, context).bind('selectstart.signaturepad', function (e) { return $(e.target).is(':input') })
+ canvas.bind('selectstart.signaturepad', function (e) { return $(e.target).is(':input') })
+
+ if (!element.getContext && FlashCanvas)
+ FlashCanvas.initElement(element)
+
+ if (element.getContext) {
+ canvasContext = element.getContext('2d')
+
+ $(settings.sig, context).show()
+
+ if (!settings.displayOnly) {
+ if (!settings.drawOnly) {
+ $(settings.name, context).bind('keyup.signaturepad', function () {
+ type($(this).val())
+ })
+
+ $(settings.name, context).bind('blur.signaturepad', function () {
+ type($(this).val())
+ })
+
+ $(settings.drawIt, context).bind('click.signaturepad', function (e) {
+ e.preventDefault()
+ drawIt()
+ })
+ }
+
+ if (settings.drawOnly || settings.defaultAction === 'drawIt') {
+ drawIt()
+ } else {
+ typeIt()
+ }
+
+ if (settings.validateFields) {
+ if ($(selector).is('form')) {
+ $(selector).bind('submit.signaturepad', function () { return validateForm() })
+ } else {
+ $(selector).parents('form').bind('submit.signaturepad', function () { return validateForm() })
+ }
+ }
+
+ $(settings.sigNav, context).show()
+ }
+ }
+ }
+
+ $.extend(self, {
+ /**
+ * A property to store the current version of Signature Pad
+ */
+ signaturePad : '{{version}}'
+
+ /**
+ * Initializes SignaturePad
+ */
+ , init : function () { init() }
+
+ /**
+ * Allows options to be updated after initialization
+ *
+ * @param {Object} options An object containing the options to be changed
+ */
+ , updateOptions : function (options) {
+ $.extend(settings, options)
+ }
+
+ /**
+ * Regenerates a signature on the canvas using an array of objects
+ * Follows same format as object property
+ * @see var object
+ *
+ * @param {Array} paths An array of the lines and points
+ */
+ , regenerate : function (paths) {
+ self.clearCanvas()
+ $(settings.typed, context).hide()
+
+ if (typeof paths === 'string')
+ paths = JSON.parse(paths)
+
+ drawSignature(paths, canvasContext, true)
+
+ if (settings.output && $(settings.output, context).length > 0)
+ $(settings.output, context).val(JSON.stringify(output))
+ }
+
+ /**
+ * Clears the canvas
+ * Redraws the background colour and the signature line
+ */
+ , clearCanvas : function () { clearCanvas() }
+
+ /**
+ * Returns the signature as a Js array
+ *
+ * @return {Array}
+ */
+ , getSignature : function () { return output }
+
+ /**
+ * Returns the signature as a Json string
+ *
+ * @return {String}
+ */
+ , getSignatureString : function () { return JSON.stringify(output) }
+
+ /**
+ * Returns the signature as an image
+ * Re-draws the signature in a shadow canvas to create a clean version
+ *
+ * @return {String}
+ */
+ , getSignatureImage : function () {
+ var tmpCanvas = document.createElement('canvas')
+ , tmpContext = null
+ , data = null
+
+ tmpCanvas.style.position = 'absolute'
+ tmpCanvas.style.top = '-999em'
+ tmpCanvas.width = element.width
+ tmpCanvas.height = element.height
+ document.body.appendChild(tmpCanvas)
+
+ if (!tmpCanvas.getContext && FlashCanvas)
+ FlashCanvas.initElement(tmpCanvas)
+
+ tmpContext = tmpCanvas.getContext('2d')
+
+ tmpContext.fillStyle = settings.bgColour
+ tmpContext.fillRect(0, 0, element.width, element.height)
+ tmpContext.lineWidth = settings.penWidth
+ tmpContext.strokeStyle = settings.penColour
+
+ drawSignature(output, tmpContext)
+ data = tmpCanvas.toDataURL.apply(tmpCanvas, arguments)
+
+ document.body.removeChild(tmpCanvas)
+ tmpCanvas = null
+
+ return data
+ }
+
+ /**
+ * The form validation function
+ * Validates that the signature has been filled in properly
+ * Allows it to be hooked into another validation function and called at a different time
+ *
+ * @return {Boolean}
+ */
+ , validateForm : function () { return validateForm() }
+ })
+}
+
+/**
+ * Create the plugin
+ * Returns an Api which can be used to call specific methods
+ *
+ * @param {Object} options The options array
+ *
+ * @return {Object} The Api for controlling the instance
+ */
+$.fn.signaturePad = function (options) {
+ var api = null
+
+ this.each(function () {
+ if (!$.data(this, 'plugin-signaturePad')) {
+ api = new SignaturePad(this, options)
+ api.init()
+ $.data(this, 'plugin-signaturePad', api)
+ } else {
+ api = $.data(this, 'plugin-signaturePad')
+ api.updateOptions(options)
+ }
+ })
+
+ return api
+}
+
+/**
+ * Expose the defaults so they can be overwritten for multiple instances
+ *
+ * @type {Object}
+ */
+$.fn.signaturePad.defaults = {
+ defaultAction : 'typeIt' // What action should be highlighted first: typeIt or drawIt
+ , displayOnly : false // Initialize canvas for signature display only; ignore buttons and inputs
+ , drawOnly : false // Whether the to allow a typed signature or not
+ , canvas : 'canvas' // Selector for selecting the canvas element
+ , sig : '.sig' // Parts of the signature form that require Javascript (hidden by default)
+ , sigNav : '.sigNav' // The TypeIt/DrawIt navigation (hidden by default)
+ , bgColour : '#ffffff' // The colour fill for the background of the canvas; or transparent
+ , penColour : '#145394' // Colour of the drawing ink
+ , penWidth : 2 // Thickness of the pen
+ , penCap : 'round' // Determines how the end points of each line are drawn (values: 'butt', 'round', 'square')
+ , lineColour : '#ccc' // Colour of the signature line
+ , lineWidth : 2 // Thickness of the signature line
+ , lineMargin : 5 // Margin on right and left of signature line
+ , lineTop : 35 // Distance to draw the line from the top
+ , name : '.name' // The input field for typing a name
+ , typed : '.typed' // The Html element to accept the printed name
+ , clear : '.clearButton' // Button for clearing the canvas
+ , typeIt : '.typeIt a' // Button to trigger name typing actions (current by default)
+ , drawIt : '.drawIt a' // Button to trigger name drawing actions
+ , typeItDesc : '.typeItDesc' // The description for TypeIt actions
+ , drawItDesc : '.drawItDesc' // The description for DrawIt actions (hidden by default)
+ , output : '.output' // The hidden input field for remembering line coordinates
+ , currentClass : 'current' // The class used to mark items as being currently active
+ , validateFields : true // Whether the name, draw fields should be validated
+ , errorClass : 'error' // The class applied to the new error Html element
+ , errorMessage : 'Please enter whose the entered signature' // The error message displayed on invalid submission
+ , errorMessageDraw : 'Please sign the document' // The error message displayed when drawOnly and no signature is drawn
+ , onBeforeValidate : null // Pass a callback to be used instead of the built-in function
+ , onFormError : null // Pass a callback to be used instead of the built-in function
+ , onDraw : null // Pass a callback to be used to capture the drawing process
+ , onDrawEnd : null // Pass a callback to be exectued after the drawing process
+}
+
+}(jQuery));
diff --git a/main/survey/js/signature_pad.js b/main/survey/js/signature_pad.js
new file mode 100644
index 0000000..703c579
--- /dev/null
+++ b/main/survey/js/signature_pad.js
@@ -0,0 +1,323 @@
+var SignaturePad = (function (document) {
+ "use strict";
+
+ var SignaturePad = function (canvas, options) {
+ var self = this,
+ opts = options || {};
+
+ this.velocityFilterWeight = opts.velocityFilterWeight || 0.7;
+ this.minWidth = opts.minWidth || 0.5;
+ this.maxWidth = opts.maxWidth || 2.5;
+ this.dotSize = opts.dotSize || function () {
+ return (this.minWidth + this.maxWidth) / 2;
+ };
+ this.penColor = opts.penColor || "black";
+ this.backgroundColor = opts.backgroundColor || "rgba(0,0,0,0)";
+ this.onEnd = opts.onEnd;
+ this.onBegin = opts.onBegin;
+
+ this._canvas = canvas;
+ this._ctx = canvas.getContext("2d");
+ this.clear();
+
+ this._handleMouseEvents();
+ this._handleTouchEvents();
+ };
+
+ SignaturePad.prototype.clear = function () {
+ var ctx = this._ctx,
+ canvas = this._canvas;
+
+ ctx.fillStyle = this.backgroundColor;
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ this._reset();
+ };
+
+ SignaturePad.prototype.toDataURL = function (imageType, quality) {
+ var canvas = this._canvas;
+ return canvas.toDataURL.apply(canvas, arguments);
+ };
+
+ SignaturePad.prototype.fromDataURL = function (dataUrl) {
+ var self = this,
+ image = new Image();
+
+ this._reset();
+ image.src = dataUrl;
+ image.onload = function () {
+ self._ctx.drawImage(image, 0, 0, self._canvas.width, self._canvas.height);
+ };
+ this._isEmpty = false;
+ };
+
+ SignaturePad.prototype._strokeUpdate = function (event) {
+ var point = this._createPoint(event);
+ this._addPoint(point);
+ };
+
+ SignaturePad.prototype._strokeBegin = function (event) {
+ this._reset();
+ this._strokeUpdate(event);
+ if (typeof this.onBegin === 'function') {
+ this.onBegin(event);
+ }
+ };
+
+ SignaturePad.prototype._strokeDraw = function (point) {
+ var ctx = this._ctx,
+ dotSize = typeof(this.dotSize) === 'function' ? this.dotSize() : this.dotSize;
+
+ ctx.beginPath();
+ this._drawPoint(point.x, point.y, dotSize);
+ ctx.closePath();
+ ctx.fill();
+ };
+
+ SignaturePad.prototype._strokeEnd = function (event) {
+ var canDrawCurve = this.points.length > 2,
+ point = this.points[0];
+
+ if (!canDrawCurve && point) {
+ this._strokeDraw(point);
+ }
+ if (typeof this.onEnd === 'function') {
+ this.onEnd(event);
+ }
+ };
+
+ SignaturePad.prototype._handleMouseEvents = function () {
+ var self = this;
+ this._mouseButtonDown = false;
+
+ this._canvas.addEventListener("mousedown", function (event) {
+ if (event.which === 1) {
+ self._mouseButtonDown = true;
+ self._strokeBegin(event);
+ }
+ });
+
+ this._canvas.addEventListener("mousemove", function (event) {
+ if (self._mouseButtonDown) {
+ self._strokeUpdate(event);
+ }
+ });
+
+ document.addEventListener("mouseup", function (event) {
+ if (event.which === 1 && self._mouseButtonDown) {
+ self._mouseButtonDown = false;
+ self._strokeEnd(event);
+ }
+ });
+ };
+
+ SignaturePad.prototype._handleTouchEvents = function () {
+ var self = this;
+
+ // Pass touch events to canvas element on mobile IE.
+ this._canvas.style.msTouchAction = 'none';
+
+ this._canvas.addEventListener("touchstart", function (event) {
+ var touch = event.changedTouches[0];
+ self._strokeBegin(touch);
+ });
+
+ this._canvas.addEventListener("touchmove", function (event) {
+ // Prevent scrolling.
+ event.preventDefault();
+
+ var touch = event.changedTouches[0];
+ self._strokeUpdate(touch);
+ });
+
+ document.addEventListener("touchend", function (event) {
+ var wasCanvasTouched = event.target === self._canvas;
+ if (wasCanvasTouched) {
+ self._strokeEnd(event);
+ }
+ });
+ };
+
+ SignaturePad.prototype.isEmpty = function () {
+ return this._isEmpty;
+ };
+
+ SignaturePad.prototype._reset = function () {
+ this.points = [];
+ this._lastVelocity = 0;
+ this._lastWidth = (this.minWidth + this.maxWidth) / 2;
+ this._isEmpty = true;
+ this._ctx.fillStyle = this.penColor;
+ };
+
+ SignaturePad.prototype._createPoint = function (event) {
+ var rect = this._canvas.getBoundingClientRect();
+ return new Point(
+ event.clientX - rect.left,
+ event.clientY - rect.top
+ );
+ };
+
+ SignaturePad.prototype._addPoint = function (point) {
+ var points = this.points,
+ c2, c3,
+ curve, tmp;
+
+ points.push(point);
+
+ if (points.length > 2) {
+ // To reduce the initial lag make it work with 3 points
+ // by copying the first point to the beginning.
+ if (points.length === 3) points.unshift(points[0]);
+
+ tmp = this._calculateCurveControlPoints(points[0], points[1], points[2]);
+ c2 = tmp.c2;
+ tmp = this._calculateCurveControlPoints(points[1], points[2], points[3]);
+ c3 = tmp.c1;
+ curve = new Bezier(points[1], c2, c3, points[2]);
+ this._addCurve(curve);
+
+ // Remove the first element from the list,
+ // so that we always have no more than 4 points in points array.
+ points.shift();
+ }
+ };
+
+ SignaturePad.prototype._calculateCurveControlPoints = function (s1, s2, s3) {
+ var dx1 = s1.x - s2.x, dy1 = s1.y - s2.y,
+ dx2 = s2.x - s3.x, dy2 = s2.y - s3.y,
+
+ m1 = {x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0},
+ m2 = {x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0},
+
+ l1 = Math.sqrt(dx1*dx1 + dy1*dy1),
+ l2 = Math.sqrt(dx2*dx2 + dy2*dy2),
+
+ dxm = (m1.x - m2.x),
+ dym = (m1.y - m2.y),
+
+ k = l2 / (l1 + l2),
+ cm = {x: m2.x + dxm*k, y: m2.y + dym*k},
+
+ tx = s2.x - cm.x,
+ ty = s2.y - cm.y;
+
+ return {
+ c1: new Point(m1.x + tx, m1.y + ty),
+ c2: new Point(m2.x + tx, m2.y + ty)
+ };
+ };
+
+ SignaturePad.prototype._addCurve = function (curve) {
+ var startPoint = curve.startPoint,
+ endPoint = curve.endPoint,
+ velocity, newWidth;
+
+ velocity = endPoint.velocityFrom(startPoint);
+ velocity = this.velocityFilterWeight * velocity
+ + (1 - this.velocityFilterWeight) * this._lastVelocity;
+
+ newWidth = this._strokeWidth(velocity);
+ this._drawCurve(curve, this._lastWidth, newWidth);
+
+ this._lastVelocity = velocity;
+ this._lastWidth = newWidth;
+ };
+
+ SignaturePad.prototype._drawPoint = function (x, y, size) {
+ var ctx = this._ctx;
+
+ ctx.moveTo(x, y);
+ ctx.arc(x, y, size, 0, 2 * Math.PI, false);
+ this._isEmpty = false;
+ };
+
+ SignaturePad.prototype._drawCurve = function (curve, startWidth, endWidth) {
+ var ctx = this._ctx,
+ widthDelta = endWidth - startWidth,
+ drawSteps, width, i, t, tt, ttt, u, uu, uuu, x, y;
+
+ drawSteps = Math.floor(curve.length());
+ ctx.beginPath();
+ for (i = 0; i < drawSteps; i++) {
+ // Calculate the Bezier (x, y) coordinate for this step.
+ t = i / drawSteps;
+ tt = t * t;
+ ttt = tt * t;
+ u = 1 - t;
+ uu = u * u;
+ uuu = uu * u;
+
+ x = uuu * curve.startPoint.x;
+ x += 3 * uu * t * curve.control1.x;
+ x += 3 * u * tt * curve.control2.x;
+ x += ttt * curve.endPoint.x;
+
+ y = uuu * curve.startPoint.y;
+ y += 3 * uu * t * curve.control1.y;
+ y += 3 * u * tt * curve.control2.y;
+ y += ttt * curve.endPoint.y;
+
+ width = startWidth + ttt * widthDelta;
+ this._drawPoint(x, y, width);
+ }
+ ctx.closePath();
+ ctx.fill();
+ };
+
+ SignaturePad.prototype._strokeWidth = function (velocity) {
+ return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
+ };
+
+
+ var Point = function (x, y, time) {
+ this.x = x;
+ this.y = y;
+ this.time = time || new Date().getTime();
+ };
+
+ Point.prototype.velocityFrom = function (start) {
+ return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 1;
+ };
+
+ Point.prototype.distanceTo = function (start) {
+ return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
+ };
+
+ var Bezier = function (startPoint, control1, control2, endPoint) {
+ this.startPoint = startPoint;
+ this.control1 = control1;
+ this.control2 = control2;
+ this.endPoint = endPoint;
+ };
+
+ // Returns approximated length.
+ Bezier.prototype.length = function () {
+ var steps = 10,
+ length = 0,
+ i, t, cx, cy, px, py, xdiff, ydiff;
+
+ for (i = 0; i <= steps; i++) {
+ t = i / steps;
+ cx = this._point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
+ cy = this._point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
+ if (i > 0) {
+ xdiff = cx - px;
+ 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 SignaturePad;
+})(document);