jSignature.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395
  1. /** @preserve
  2. jSignature v2 "${buildDate}" "${commitID}"
  3. Copyright (c) 2012 Willow Systems Corp http://willow-systems.com
  4. Copyright (c) 2010 Brinley Ang http://www.unbolt.net
  5. MIT License <http://www.opensource.org/licenses/mit-license.php>
  6. */
  7. ;(function() {
  8. var apinamespace = 'jSignature'
  9. /**
  10. Allows one to delay certain eventual action by setting up a timer for it and allowing one to delay it
  11. by "kick"ing it. Sorta like "kick the can down the road"
  12. @public
  13. @class
  14. @param
  15. @returns {Type}
  16. */
  17. var KickTimerClass = function(time, callback) {
  18. var timer
  19. this.kick = function() {
  20. clearTimeout(timer)
  21. timer = setTimeout(
  22. callback
  23. , time
  24. )
  25. }
  26. this.clear = function() {
  27. clearTimeout(timer)
  28. }
  29. return this
  30. }
  31. var PubSubClass = function(context){
  32. 'use strict'
  33. /* @preserve
  34. -----------------------------------------------------------------------------------------------
  35. JavaScript PubSub library
  36. 2012 (c) Willow Systems Corp (www.willow-systems.com)
  37. based on Peter Higgins (dante@dojotoolkit.org)
  38. Loosely based on Dojo publish/subscribe API, limited in scope. Rewritten blindly.
  39. Original is (c) Dojo Foundation 2004-2010. Released under either AFL or new BSD, see:
  40. http://dojofoundation.org/license for more information.
  41. -----------------------------------------------------------------------------------------------
  42. */
  43. this.topics = {}
  44. // here we choose what will be "this" for the called events.
  45. // if context is defined, it's context. Else, 'this' is this instance of PubSub
  46. this.context = context ? context : this
  47. /**
  48. * Allows caller to emit an event and pass arguments to event listeners.
  49. * @public
  50. * @function
  51. * @param topic {String} Name of the channel on which to voice this event
  52. * @param **arguments Any number of arguments you want to pass to the listeners of this event.
  53. */
  54. this.publish = function(topic, arg1, arg2, etc) {
  55. 'use strict'
  56. if (this.topics[topic]) {
  57. var currentTopic = this.topics[topic]
  58. , args = Array.prototype.slice.call(arguments, 1)
  59. , toremove = []
  60. , fn
  61. , i, l
  62. , pair
  63. for (i = 0, l = currentTopic.length; i < l; i++) {
  64. pair = currentTopic[i] // this is a [function, once_flag] array
  65. fn = pair[0]
  66. if (pair[1] /* 'run once' flag set */){
  67. pair[0] = function(){}
  68. toremove.push(i)
  69. }
  70. fn.apply(this.context, args)
  71. }
  72. for (i = 0, l = toremove.length; i < l; i++) {
  73. currentTopic.splice(toremove[i], 1)
  74. }
  75. }
  76. }
  77. /**
  78. * Allows listener code to subscribe to channel and be called when data is available
  79. * @public
  80. * @function
  81. * @param topic {String} Name of the channel on which to voice this event
  82. * @param callback {Function} Executable (function pointer) that will be ran when event is voiced on this channel.
  83. * @param once {Boolean} (optional. False by default) Flag indicating if the function is to be triggered only once.
  84. * @returns {Object} A token object that cen be used for unsubscribing.
  85. */
  86. this.subscribe = function(topic, callback, once) {
  87. 'use strict'
  88. if (!this.topics[topic]) {
  89. this.topics[topic] = [[callback, once]];
  90. } else {
  91. this.topics[topic].push([callback,once]);
  92. }
  93. return {
  94. "topic": topic,
  95. "callback": callback
  96. };
  97. };
  98. /**
  99. * Allows listener code to unsubscribe from a channel
  100. * @public
  101. * @function
  102. * @param token {Object} A token object that was returned by `subscribe` method
  103. */
  104. this.unsubscribe = function(token) {
  105. if (this.topics[token.topic]) {
  106. var currentTopic = this.topics[token.topic]
  107. for (var i = 0, l = currentTopic.length; i < l; i++) {
  108. if (currentTopic[i][0] === token.callback) {
  109. currentTopic.splice(i, 1)
  110. }
  111. }
  112. }
  113. }
  114. }
  115. /// Returns front, back and "decor" colors derived from element (as jQuery obj)
  116. function getColors($e){
  117. var tmp
  118. , undef
  119. , frontcolor = $e.css('color')
  120. , backcolor
  121. , e = $e[0]
  122. var toOfDOM = false
  123. while(e && !backcolor && !toOfDOM){
  124. try{
  125. tmp = $(e).css('background-color')
  126. } catch (ex) {
  127. tmp = 'transparent'
  128. }
  129. if (tmp !== 'transparent' && tmp !== 'rgba(0, 0, 0, 0)'){
  130. backcolor = tmp
  131. }
  132. toOfDOM = e.body
  133. e = e.parentNode
  134. }
  135. var rgbaregex = /rgb[a]*\((\d+),\s*(\d+),\s*(\d+)/ // modern browsers
  136. , hexregex = /#([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})([AaBbCcDdEeFf\d]{2})/ // IE 8 and less.
  137. , frontcolorcomponents
  138. // Decomposing Front color into R, G, B ints
  139. tmp = undef
  140. tmp = frontcolor.match(rgbaregex)
  141. if (tmp){
  142. frontcolorcomponents = {'r':parseInt(tmp[1],10),'g':parseInt(tmp[2],10),'b':parseInt(tmp[3],10)}
  143. } else {
  144. tmp = frontcolor.match(hexregex)
  145. if (tmp) {
  146. frontcolorcomponents = {'r':parseInt(tmp[1],16),'g':parseInt(tmp[2],16),'b':parseInt(tmp[3],16)}
  147. }
  148. }
  149. // if(!frontcolorcomponents){
  150. // frontcolorcomponents = {'r':255,'g':255,'b':255}
  151. // }
  152. var backcolorcomponents
  153. // Decomposing back color into R, G, B ints
  154. if(!backcolor){
  155. // HIghly unlikely since this means that no background styling was applied to any element from here to top of dom.
  156. // we'll pick up back color from front color
  157. if(frontcolorcomponents){
  158. if (Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b]) > 127){
  159. backcolorcomponents = {'r':0,'g':0,'b':0}
  160. } else {
  161. backcolorcomponents = {'r':255,'g':255,'b':255}
  162. }
  163. } else {
  164. // arg!!! front color is in format we don't understand (hsl, named colors)
  165. // Let's just go with white background.
  166. backcolorcomponents = {'r':255,'g':255,'b':255}
  167. }
  168. } else {
  169. tmp = undef
  170. tmp = backcolor.match(rgbaregex)
  171. if (tmp){
  172. backcolorcomponents = {'r':parseInt(tmp[1],10),'g':parseInt(tmp[2],10),'b':parseInt(tmp[3],10)}
  173. } else {
  174. tmp = backcolor.match(hexregex)
  175. if (tmp) {
  176. backcolorcomponents = {'r':parseInt(tmp[1],16),'g':parseInt(tmp[2],16),'b':parseInt(tmp[3],16)}
  177. }
  178. }
  179. // if(!backcolorcomponents){
  180. // backcolorcomponents = {'r':0,'g':0,'b':0}
  181. // }
  182. }
  183. // Deriving Decor color
  184. // THis is LAZY!!!! Better way would be to use HSL and adjust luminocity. However, that could be an overkill.
  185. var toRGBfn = function(o){return 'rgb(' + [o.r, o.g, o.b].join(', ') + ')'}
  186. , decorcolorcomponents
  187. , frontcolorbrightness
  188. , adjusted
  189. if (frontcolorcomponents && backcolorcomponents){
  190. var backcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b])
  191. frontcolorbrightness = Math.max.apply(null, [backcolorcomponents.r, backcolorcomponents.g, backcolorcomponents.b])
  192. adjusted = Math.round(frontcolorbrightness + (-1 * (frontcolorbrightness - backcolorbrightness) * 0.75)) // "dimming" the difference between pen and back.
  193. decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray
  194. } else if (frontcolorcomponents) {
  195. frontcolorbrightness = Math.max.apply(null, [frontcolorcomponents.r, frontcolorcomponents.g, frontcolorcomponents.b])
  196. var polarity = +1
  197. if (frontcolorbrightness > 127){
  198. polarity = -1
  199. }
  200. // shifting by 25% (64 points on RGB scale)
  201. adjusted = Math.round(frontcolorbrightness + (polarity * 96)) // "dimming" the pen's color by 75% to get decor color.
  202. decorcolorcomponents = {'r':adjusted,'g':adjusted,'b':adjusted} // always shade of gray
  203. } else {
  204. decorcolorcomponents = {'r':191,'g':191,'b':191} // always shade of gray
  205. }
  206. return {
  207. 'color': frontcolor
  208. , 'background-color': backcolorcomponents? toRGBfn(backcolorcomponents) : backcolor
  209. , 'decor-color': toRGBfn(decorcolorcomponents)
  210. }
  211. }
  212. function Vector(x,y){
  213. this.x = x
  214. this.y = y
  215. this.reverse = function(){
  216. return new this.constructor(
  217. this.x * -1
  218. , this.y * -1
  219. )
  220. }
  221. this._length = null
  222. this.getLength = function(){
  223. if (!this._length){
  224. this._length = Math.sqrt( Math.pow(this.x, 2) + Math.pow(this.y, 2) )
  225. }
  226. return this._length
  227. }
  228. var polarity = function (e){
  229. return Math.round(e / Math.abs(e))
  230. }
  231. this.resizeTo = function(length){
  232. // proportionally changes x,y such that the hypotenuse (vector length) is = new length
  233. if (this.x === 0 && this.y === 0){
  234. this._length = 0
  235. } else if (this.x === 0){
  236. this._length = length
  237. this.y = length * polarity(this.y)
  238. } else if(this.y === 0){
  239. this._length = length
  240. this.x = length * polarity(this.x)
  241. } else {
  242. var proportion = Math.abs(this.y / this.x)
  243. , x = Math.sqrt(Math.pow(length, 2) / (1 + Math.pow(proportion, 2)))
  244. , y = proportion * x
  245. this._length = length
  246. this.x = x * polarity(this.x)
  247. this.y = y * polarity(this.y)
  248. }
  249. return this
  250. }
  251. /**
  252. * Calculates the angle between 'this' vector and another.
  253. * @public
  254. * @function
  255. * @returns {Number} The angle between the two vectors as measured in PI.
  256. */
  257. this.angleTo = function(vectorB) {
  258. var divisor = this.getLength() * vectorB.getLength()
  259. if (divisor === 0) {
  260. return 0
  261. } else {
  262. // JavaScript floating point math is screwed up.
  263. // because of it, the core of the formula can, on occasion, have values
  264. // over 1.0 and below -1.0.
  265. return Math.acos(
  266. Math.min(
  267. Math.max(
  268. ( this.x * vectorB.x + this.y * vectorB.y ) / divisor
  269. , -1.0
  270. )
  271. , 1.0
  272. )
  273. ) / Math.PI
  274. }
  275. }
  276. }
  277. function Point(x,y){
  278. this.x = x
  279. this.y = y
  280. this.getVectorToCoordinates = function (x, y) {
  281. return new Vector(x - this.x, y - this.y)
  282. }
  283. this.getVectorFromCoordinates = function (x, y) {
  284. return this.getVectorToCoordinates(x, y).reverse()
  285. }
  286. this.getVectorToPoint = function (point) {
  287. return new Vector(point.x - this.x, point.y - this.y)
  288. }
  289. this.getVectorFromPoint = function (point) {
  290. return this.getVectorToPoint(point).reverse()
  291. }
  292. }
  293. /*
  294. * About data structure:
  295. * We don't store / deal with "pictures" this signature capture code captures "vectors"
  296. *
  297. * We don't store bitmaps. We store "strokes" as arrays of arrays. (Actually, arrays of objects containing arrays of coordinates.
  298. *
  299. * Stroke = mousedown + mousemoved * n (+ mouseup but we don't record that as that was the "end / lack of movement" indicator)
  300. *
  301. * Vectors = not classical vectors where numbers indicated shift relative last position. Our vectors are actually coordinates against top left of canvas.
  302. * we could calc the classical vectors, but keeping the the actual coordinates allows us (through Math.max / min)
  303. * to calc the size of resulting drawing very quickly. If we want classical vectors later, we can always get them in backend code.
  304. *
  305. * So, the data structure:
  306. *
  307. * var data = [
  308. * { // stroke starts
  309. * x : [101, 98, 57, 43] // x points
  310. * , y : [1, 23, 65, 87] // y points
  311. * } // stroke ends
  312. * , { // stroke starts
  313. * x : [55, 56, 57, 58] // x points
  314. * , y : [101, 97, 54, 4] // y points
  315. * } // stroke ends
  316. * , { // stroke consisting of just a dot
  317. * x : [53] // x points
  318. * , y : [151] // y points
  319. * } // stroke ends
  320. * ]
  321. *
  322. * we don't care or store stroke width (it's canvas-size-relative), color, shadow values. These can be added / changed on whim post-capture.
  323. *
  324. */
  325. function DataEngine(storageObject, context, startStrokeFn, addToStrokeFn, endStrokeFn){
  326. this.data = storageObject // we expect this to be an instance of Array
  327. this.context = context
  328. if (storageObject.length){
  329. // we have data to render
  330. var numofstrokes = storageObject.length
  331. , stroke
  332. , numofpoints
  333. for (var i = 0; i < numofstrokes; i++){
  334. stroke = storageObject[i]
  335. numofpoints = stroke.x.length
  336. startStrokeFn.call(context, stroke)
  337. for(var j = 1; j < numofpoints; j++){
  338. addToStrokeFn.call(context, stroke, j)
  339. }
  340. endStrokeFn.call(context, stroke)
  341. }
  342. }
  343. this.changed = function(){}
  344. this.startStrokeFn = startStrokeFn
  345. this.addToStrokeFn = addToStrokeFn
  346. this.endStrokeFn = endStrokeFn
  347. this.inStroke = false
  348. this._lastPoint = null
  349. this._stroke = null
  350. this.startStroke = function(point){
  351. if(point && typeof(point.x) == "number" && typeof(point.y) == "number"){
  352. this._stroke = {'x':[point.x], 'y':[point.y]}
  353. this.data.push(this._stroke)
  354. this._lastPoint = point
  355. this.inStroke = true
  356. // 'this' does not work same inside setTimeout(
  357. var stroke = this._stroke
  358. , fn = this.startStrokeFn
  359. , context = this.context
  360. setTimeout(
  361. // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
  362. function() {fn.call(context, stroke)}
  363. , 3
  364. )
  365. return point
  366. } else {
  367. return null
  368. }
  369. }
  370. // that "5" at the very end of this if is important to explain.
  371. // we do NOT render links between two captured points (in the middle of the stroke) if the distance is shorter than that number.
  372. // not only do we NOT render it, we also do NOT capture (add) these intermediate points to storage.
  373. // when clustering of these is too tight, it produces noise on the line, which, because of smoothing, makes lines too curvy.
  374. // maybe, later, we can expose this as a configurable setting of some sort.
  375. this.addToStroke = function(point){
  376. if (this.inStroke &&
  377. typeof(point.x) === "number" &&
  378. typeof(point.y) === "number" &&
  379. // calculates absolute shift in diagonal pixels away from original point
  380. (Math.abs(point.x - this._lastPoint.x) + Math.abs(point.y - this._lastPoint.y)) > 4
  381. ){
  382. var positionInStroke = this._stroke.x.length
  383. this._stroke.x.push(point.x)
  384. this._stroke.y.push(point.y)
  385. this._lastPoint = point
  386. var stroke = this._stroke
  387. , fn = this.addToStrokeFn
  388. , context = this.context
  389. setTimeout(
  390. // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
  391. function() {fn.call(context, stroke, positionInStroke)}
  392. , 3
  393. )
  394. return point
  395. } else {
  396. return null
  397. }
  398. }
  399. this.endStroke = function(){
  400. var c = this.inStroke
  401. this.inStroke = false
  402. this._lastPoint = null
  403. if (c){
  404. var stroke = this._stroke
  405. , fn = this.endStrokeFn // 'this' does not work same inside setTimeout(
  406. , context = this.context
  407. , changedfn = this.changed
  408. setTimeout(
  409. // some IE's don't support passing args per setTimeout API. Have to create closure every time instead.
  410. function(){
  411. fn.call(context, stroke)
  412. changedfn.call(context)
  413. }
  414. , 3
  415. )
  416. return true
  417. } else {
  418. return null
  419. }
  420. }
  421. }
  422. var basicDot = function(ctx, x, y, size){
  423. var fillStyle = ctx.fillStyle
  424. ctx.fillStyle = ctx.strokeStyle
  425. ctx.fillRect(x + size / -2 , y + size / -2, size, size)
  426. ctx.fillStyle = fillStyle
  427. }
  428. , basicLine = function(ctx, startx, starty, endx, endy){
  429. ctx.beginPath()
  430. ctx.moveTo(startx, starty)
  431. ctx.lineTo(endx, endy)
  432. ctx.closePath();
  433. ctx.stroke()
  434. }
  435. , basicCurve = function(ctx, startx, starty, endx, endy, cp1x, cp1y, cp2x, cp2y){
  436. ctx.beginPath()
  437. ctx.moveTo(startx, starty)
  438. ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endx, endy)
  439. ctx.closePath();
  440. ctx.stroke()
  441. }
  442. , strokeStartCallback = function(stroke) {
  443. // this = jSignatureClass instance
  444. basicDot(this.canvasContext, stroke.x[0], stroke.y[0], this.settings.lineWidth)
  445. }
  446. , strokeAddCallback = function(stroke, positionInStroke){
  447. // this = jSignatureClass instance
  448. // Because we are funky this way, here we draw TWO curves.
  449. // 1. POSSIBLY "this line" - spanning from point right before us, to this latest point.
  450. // 2. POSSIBLY "prior curve" - spanning from "latest point" to the one before it.
  451. // Why you ask?
  452. // long lines (ones with many pixels between them) do not look good when they are part of a large curvy stroke.
  453. // You know, the jaggedy crocodile spine instead of a pretty, smooth curve. Yuck!
  454. // We want to approximate pretty curves in-place of those ugly lines.
  455. // To approximate a very nice curve we need to know the direction of line before and after.
  456. // Hence, on long lines we actually wait for another point beyond it to come back from
  457. // mousemoved before we draw this curve.
  458. // So for "prior curve" to be calc'ed we need 4 points
  459. // A, B, C, D (we are on D now, A is 3 points in the past.)
  460. // and 3 lines:
  461. // pre-line (from points A to B),
  462. // this line (from points B to C), (we call it "this" because if it was not yet, it's the only one we can draw for sure.)
  463. // post-line (from points C to D) (even through D point is 'current' we don't know how we can draw it yet)
  464. //
  465. // Well, actually, we don't need to *know* the point A, just the vector A->B
  466. var Cpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1])
  467. , Dpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke])
  468. , CDvector = Cpoint.getVectorToPoint(Dpoint)
  469. // Again, we have a chance here to draw TWO things:
  470. // BC Curve (only if it's long, because if it was short, it was drawn by previous callback) and
  471. // CD Line (only if it's short)
  472. // So, let's start with BC curve.
  473. // if there is only 2 points in stroke array, we don't have "history" long enough to have point B, let alone point A.
  474. // Falling through to drawing line CD is proper, as that's the only line we have points for.
  475. if(positionInStroke > 1) {
  476. // we are here when there are at least 3 points in stroke array.
  477. var Bpoint = new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2])
  478. , BCvector = Bpoint.getVectorToPoint(Cpoint)
  479. , ABvector
  480. if(BCvector.getLength() > this.lineCurveThreshold){
  481. // Yey! Pretty curves, here we come!
  482. if(positionInStroke > 2) {
  483. // we are here when at least 4 points in stroke array.
  484. ABvector = (new Point(stroke.x[positionInStroke-3], stroke.y[positionInStroke-3])).getVectorToPoint(Bpoint)
  485. } else {
  486. ABvector = new Vector(0,0)
  487. }
  488. var minlenfraction = 0.05
  489. , maxlen = BCvector.getLength() * 0.35
  490. , ABCangle = BCvector.angleTo(ABvector.reverse())
  491. , BCDangle = CDvector.angleTo(BCvector.reverse())
  492. , BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo(
  493. Math.max(minlenfraction, ABCangle) * maxlen
  494. )
  495. , CCP2vector = (new Vector(BCvector.x + CDvector.x, BCvector.y + CDvector.y)).reverse().resizeTo(
  496. Math.max(minlenfraction, BCDangle) * maxlen
  497. )
  498. basicCurve(
  499. this.canvasContext
  500. , Bpoint.x
  501. , Bpoint.y
  502. , Cpoint.x
  503. , Cpoint.y
  504. , Bpoint.x + BCP1vector.x
  505. , Bpoint.y + BCP1vector.y
  506. , Cpoint.x + CCP2vector.x
  507. , Cpoint.y + CCP2vector.y
  508. )
  509. }
  510. }
  511. if(CDvector.getLength() <= this.lineCurveThreshold){
  512. basicLine(
  513. this.canvasContext
  514. , Cpoint.x
  515. , Cpoint.y
  516. , Dpoint.x
  517. , Dpoint.y
  518. )
  519. }
  520. }
  521. , strokeEndCallback = function(stroke){
  522. // this = jSignatureClass instance
  523. // Here we tidy up things left unfinished in last strokeAddCallback run.
  524. // What's POTENTIALLY left unfinished there is the curve between the last points
  525. // in the stroke, if the len of that line is more than lineCurveThreshold
  526. // If the last line was shorter than lineCurveThreshold, it was drawn there, and there
  527. // is nothing for us here to do.
  528. // We can also be called when there is only one point in the stroke (meaning, the
  529. // stroke was just a dot), in which case, again, there is nothing for us to do.
  530. // So for "this curve" to be calc'ed we need 3 points
  531. // A, B, C
  532. // and 2 lines:
  533. // pre-line (from points A to B),
  534. // this line (from points B to C)
  535. // Well, actually, we don't need to *know* the point A, just the vector A->B
  536. // so, we really need points B, C and AB vector.
  537. var positionInStroke = stroke.x.length - 1
  538. if (positionInStroke > 0){
  539. // there are at least 2 points in the stroke.we are in business.
  540. var Cpoint = new Point(stroke.x[positionInStroke], stroke.y[positionInStroke])
  541. , Bpoint = new Point(stroke.x[positionInStroke-1], stroke.y[positionInStroke-1])
  542. , BCvector = Bpoint.getVectorToPoint(Cpoint)
  543. , ABvector
  544. if (BCvector.getLength() > this.lineCurveThreshold){
  545. // yep. This one was left undrawn in prior callback. Have to draw it now.
  546. if (positionInStroke > 1){
  547. // we have at least 3 elems in stroke
  548. ABvector = (new Point(stroke.x[positionInStroke-2], stroke.y[positionInStroke-2])).getVectorToPoint(Bpoint)
  549. var BCP1vector = new Vector(ABvector.x + BCvector.x, ABvector.y + BCvector.y).resizeTo(BCvector.getLength() / 2)
  550. basicCurve(
  551. this.canvasContext
  552. , Bpoint.x
  553. , Bpoint.y
  554. , Cpoint.x
  555. , Cpoint.y
  556. , Bpoint.x + BCP1vector.x
  557. , Bpoint.y + BCP1vector.y
  558. , Cpoint.x
  559. , Cpoint.y
  560. )
  561. } else {
  562. // Since there is no AB leg, there is no curve to draw. This line is still "long" but no curve.
  563. basicLine(
  564. this.canvasContext
  565. , Bpoint.x
  566. , Bpoint.y
  567. , Cpoint.x
  568. , Cpoint.y
  569. )
  570. }
  571. }
  572. }
  573. }
  574. /*
  575. var getDataStats = function(){
  576. var strokecnt = strokes.length
  577. , stroke
  578. , pointid
  579. , pointcnt
  580. , x, y
  581. , maxX = Number.NEGATIVE_INFINITY
  582. , maxY = Number.NEGATIVE_INFINITY
  583. , minX = Number.POSITIVE_INFINITY
  584. , minY = Number.POSITIVE_INFINITY
  585. for(strokeid = 0; strokeid < strokecnt; strokeid++){
  586. stroke = strokes[strokeid]
  587. pointcnt = stroke.length
  588. for(pointid = 0; pointid < pointcnt; pointid++){
  589. x = stroke.x[pointid]
  590. y = stroke.y[pointid]
  591. if (x > maxX){
  592. maxX = x
  593. } else if (x < minX) {
  594. minX = x
  595. }
  596. if (y > maxY){
  597. maxY = y
  598. } else if (y < minY) {
  599. minY = y
  600. }
  601. }
  602. }
  603. return {'maxX': maxX, 'minX': minX, 'maxY': maxY, 'minY': minY}
  604. }
  605. */
  606. function conditionallyLinkCanvasResizeToWindowResize(jSignatureInstance, settingsWidth, apinamespace, globalEvents){
  607. 'use strict'
  608. if ( settingsWidth === 'ratio' || settingsWidth.split('')[settingsWidth.length - 1] === '%' ) {
  609. this.eventTokens[apinamespace + '.parentresized'] = globalEvents.subscribe(
  610. apinamespace + '.parentresized'
  611. , (function(eventTokens, $parent, originalParentWidth, sizeRatio){
  612. 'use strict'
  613. return function(){
  614. 'use strict'
  615. var w = $parent.width()
  616. if (w !== originalParentWidth) {
  617. // UNsubscribing this particular instance of signature pad only.
  618. // there is a separate `eventTokens` per each instance of signature pad
  619. for (var key in eventTokens){
  620. if (eventTokens.hasOwnProperty(key)) {
  621. globalEvents.unsubscribe(eventTokens[key])
  622. delete eventTokens[key]
  623. }
  624. }
  625. var settings = jSignatureInstance.settings
  626. jSignatureInstance.$parent.children().remove()
  627. for (var key in jSignatureInstance){
  628. if (jSignatureInstance.hasOwnProperty(key)) {
  629. delete jSignatureInstance[key]
  630. }
  631. }
  632. // scale data to new signature pad size
  633. settings.data = (function(data, scale){
  634. var newData = []
  635. var o, i, l, j, m, stroke
  636. for ( i = 0, l = data.length; i < l; i++) {
  637. stroke = data[i]
  638. o = {'x':[],'y':[]}
  639. for ( j = 0, m = stroke.x.length; j < m; j++) {
  640. o.x.push(stroke.x[j] * scale)
  641. o.y.push(stroke.y[j] * scale)
  642. }
  643. newData.push(o)
  644. }
  645. return newData
  646. })(
  647. settings.data
  648. , w * 1.0 / originalParentWidth
  649. )
  650. $parent[apinamespace](settings)
  651. }
  652. }
  653. })(
  654. this.eventTokens
  655. , this.$parent
  656. , this.$parent.width()
  657. , this.canvas.width * 1.0 / this.canvas.height
  658. )
  659. )
  660. }
  661. }
  662. function jSignatureClass(parent, options, instanceExtensions) {
  663. var $parent = this.$parent = $(parent)
  664. , eventTokens = this.eventTokens = {}
  665. , events = this.events = new PubSubClass(this)
  666. , globalEvents = $.fn[apinamespace]('globalEvents')
  667. , settings = {
  668. 'width' : '1160'
  669. ,'height' : '450'
  670. ,'sizeRatio': 3 // only used when height = 'ratio'
  671. ,'color' : '#000'
  672. ,'background-color': '#fff'
  673. ,'decor-color': '#eee'
  674. ,'lineWidth' : 0
  675. ,'minFatFingerCompensation' : -10
  676. ,'showUndoButton': false
  677. ,'data': []
  678. }
  679. $.extend(settings, getColors($parent))
  680. if (options) {
  681. $.extend(settings, options)
  682. }
  683. this.settings = settings
  684. for (var extensionName in instanceExtensions){
  685. if (instanceExtensions.hasOwnProperty(extensionName)) {
  686. instanceExtensions[extensionName].call(this, extensionName)
  687. }
  688. }
  689. this.events.publish(apinamespace+'.initializing')
  690. // these, when enabled, will hover above the sig area. Hence we append them to DOM before canvas.
  691. this.$controlbarUpper = (function(){
  692. var controlbarstyle = 'padding:0 !important;margin:0 !important;'+
  693. 'width: 100% !important; height: 0 !important;'+
  694. 'margin-top:-1em !important;margin-bottom:1em !important;'
  695. return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent)
  696. })();
  697. this.isCanvasEmulator = false // will be flipped by initializer when needed.
  698. var canvas = this.canvas = this.initializeCanvas(settings)
  699. , $canvas = $(canvas)
  700. this.$controlbarLower = (function(){
  701. var controlbarstyle = 'padding:0 !important;margin:0 !important;'+
  702. 'width: 100% !important; height: 0 !important;'+
  703. 'margin-top:-1.5em !important;margin-bottom:1.5em !important;'
  704. return $('<div style="'+controlbarstyle+'"></div>').appendTo($parent)
  705. })();
  706. this.canvasContext = canvas.getContext("2d")
  707. // Most of our exposed API will be looking for this:
  708. $canvas.data(apinamespace + '.this', this)
  709. settings.lineWidth = (function(defaultLineWidth, canvasWidth){
  710. if (!defaultLineWidth){
  711. return Math.max(
  712. Math.round(canvasWidth / 400) /*+1 pixel for every extra 300px of width.*/
  713. , 2 /* minimum line width */
  714. )
  715. } else {
  716. return defaultLineWidth
  717. }
  718. })(settings.lineWidth, canvas.width);
  719. this.lineCurveThreshold = settings.lineWidth * 3
  720. // Add custom class if defined
  721. if(settings.cssclass && $.trim(settings.cssclass) != "") {
  722. $canvas.addClass(settings.cssclass)
  723. }
  724. // used for shifting the drawing point up on touch devices, so one can see the drawing above the finger.
  725. this.fatFingerCompensation = 0
  726. var movementHandlers = (function(jSignatureInstance) {
  727. //================================
  728. // mouse down, move, up handlers:
  729. // shifts - adjustment values in viewport pixels drived from position of canvas on the page
  730. var shiftX
  731. , shiftY
  732. , setStartValues = function(){
  733. var tos = $(jSignatureInstance.canvas).offset()
  734. shiftX = tos.left * -1
  735. shiftY = tos.top * -1
  736. }
  737. , getPointFromEvent = function(e) {
  738. var firstEvent = (e.changedTouches && e.changedTouches.length > 0 ? e.changedTouches[0] : e)
  739. // All devices i tried report correct coordinates in pageX,Y
  740. // Android Chrome 2.3.x, 3.1, 3.2., Opera Mobile, safari iOS 4.x,
  741. // Windows: Chrome, FF, IE9, Safari
  742. // None of that scroll shift calc vs screenXY other sigs do is needed.
  743. // ... oh, yeah, the "fatFinger.." is for tablets so that people see what they draw.
  744. return new Point(
  745. Math.round(firstEvent.pageX + shiftX)
  746. , Math.round(firstEvent.pageY + shiftY) + jSignatureInstance.fatFingerCompensation
  747. )
  748. }
  749. , timer = new KickTimerClass(
  750. 750
  751. , function() { jSignatureInstance.dataEngine.endStroke() }
  752. )
  753. this.drawEndHandler = function(e) {
  754. try { e.preventDefault() } catch (ex) {}
  755. timer.clear()
  756. jSignatureInstance.dataEngine.endStroke()
  757. }
  758. this.drawStartHandler = function(e) {
  759. e.preventDefault()
  760. // for performance we cache the offsets
  761. // we recalc these only at the beginning the stroke
  762. setStartValues()
  763. jSignatureInstance.dataEngine.startStroke( getPointFromEvent(e) )
  764. timer.kick()
  765. }
  766. this.drawMoveHandler = function(e) {
  767. e.preventDefault()
  768. if (!jSignatureInstance.dataEngine.inStroke){
  769. return
  770. }
  771. jSignatureInstance.dataEngine.addToStroke( getPointFromEvent(e) )
  772. timer.kick()
  773. }
  774. return this
  775. }).call( {}, this )
  776. //
  777. //================================
  778. ;(function(drawEndHandler, drawStartHandler, drawMoveHandler) {
  779. var canvas = this.canvas
  780. , $canvas = $(canvas)
  781. , undef
  782. if (this.isCanvasEmulator){
  783. $canvas.bind('mousemove.'+apinamespace, drawMoveHandler)
  784. $canvas.bind('mouseup.'+apinamespace, drawEndHandler)
  785. $canvas.bind('mousedown.'+apinamespace, drawStartHandler)
  786. } else {
  787. canvas.ontouchstart = function(e) {
  788. canvas.onmousedown = undef
  789. canvas.onmouseup = undef
  790. canvas.onmousemove = undef
  791. this.fatFingerCompensation = (
  792. settings.minFatFingerCompensation &&
  793. settings.lineWidth * -3 > settings.minFatFingerCompensation
  794. ) ? settings.lineWidth * -3 : settings.minFatFingerCompensation
  795. drawStartHandler(e)
  796. canvas.ontouchend = drawEndHandler
  797. canvas.ontouchstart = drawStartHandler
  798. canvas.ontouchmove = drawMoveHandler
  799. }
  800. canvas.onmousedown = function(e) {
  801. canvas.ontouchstart = undef
  802. canvas.ontouchend = undef
  803. canvas.ontouchmove = undef
  804. drawStartHandler(e)
  805. canvas.onmousedown = drawStartHandler
  806. canvas.onmouseup = drawEndHandler
  807. canvas.onmousemove = drawMoveHandler
  808. }
  809. }
  810. }).call(
  811. this
  812. , movementHandlers.drawEndHandler
  813. , movementHandlers.drawStartHandler
  814. , movementHandlers.drawMoveHandler
  815. )
  816. //=========================================
  817. // various event handlers
  818. // on mouseout + mouseup canvas did not know that mouseUP fired. Continued to draw despite mouse UP.
  819. // it is bettr than
  820. // $canvas.bind('mouseout', drawEndHandler)
  821. // because we don't want to break the stroke where user accidentally gets ouside and wants to get back in quickly.
  822. eventTokens[apinamespace + '.windowmouseup'] = globalEvents.subscribe(
  823. apinamespace + '.windowmouseup'
  824. , movementHandlers.drawEndHandler
  825. )
  826. this.events.publish(apinamespace+'.attachingEventHandlers')
  827. // If we have proportional width, we sign up to events broadcasting "window resized" and checking if
  828. // parent's width changed. If so, we (1) extract settings + data from current signature pad,
  829. // (2) remove signature pad from parent, and (3) reinit new signature pad at new size with same settings, (rescaled) data.
  830. conditionallyLinkCanvasResizeToWindowResize.call(
  831. this
  832. , this
  833. , settings.width.toString(10)
  834. , apinamespace, globalEvents
  835. )
  836. // end of event handlers.
  837. // ===============================
  838. this.resetCanvas(settings.data)
  839. // resetCanvas renders the data on the screen and fires ONE "change" event
  840. // if there is data. If you have controls that rely on "change" firing
  841. // attach them to something that runs before this.resetCanvas, like
  842. // apinamespace+'.attachingEventHandlers' that fires a bit higher.
  843. this.events.publish(apinamespace+'.initialized')
  844. return this
  845. } // end of initBase
  846. //=========================================================================
  847. // jSignatureClass's methods and supporting fn's
  848. jSignatureClass.prototype.resetCanvas = function(data){
  849. var canvas = this.canvas
  850. , settings = this.settings
  851. , ctx = this.canvasContext
  852. , isCanvasEmulator = this.isCanvasEmulator
  853. , cw = canvas.width
  854. , ch = canvas.height
  855. // preparing colors, drawing area
  856. ctx.clearRect(0, 0, cw + 30, ch + 30)
  857. ctx.shadowColor = ctx.fillStyle = settings['background-color']
  858. if (isCanvasEmulator){
  859. // FLashCanvas fills with Black by default, covering up the parent div's background
  860. // hence we refill
  861. ctx.fillRect(0,0,cw + 30, ch + 30)
  862. }
  863. ctx.lineWidth = Math.ceil(parseInt(settings.lineWidth, 10))
  864. ctx.lineCap = ctx.lineJoin = "round"
  865. // signature line
  866. //ctx.strokeStyle = settings['decor-color']
  867. //ctx.shadowOffsetX = 0
  868. //ctx.shadowOffsetY = 0
  869. //var lineoffset = Math.round( ch / 5 )
  870. //basicLine(ctx, lineoffset * 1.5, ch - lineoffset, cw - (lineoffset * 1.5), ch - lineoffset)
  871. //ctx.strokeStyle = settings.color
  872. //if (!isCanvasEmulator){
  873. // ctx.shadowColor = ctx.strokeStyle
  874. // ctx.shadowOffsetX = ctx.lineWidth * 0.5
  875. // ctx.shadowOffsetY = ctx.lineWidth * -0.6
  876. // ctx.shadowBlur = 0
  877. //}
  878. ctx.strokeStyle = settings.color;
  879. // setting up new dataEngine
  880. if (!data) { data = [] }
  881. var dataEngine = this.dataEngine = new DataEngine(
  882. data
  883. , this
  884. , strokeStartCallback
  885. , strokeAddCallback
  886. , strokeEndCallback
  887. )
  888. settings.data = data // onwindowresize handler uses it, i think.
  889. $(canvas).data(apinamespace+'.data', data)
  890. .data(apinamespace+'.settings', settings)
  891. // we fire "change" event on every change in data.
  892. // setting this up:
  893. dataEngine.changed = (function(target, events, apinamespace) {
  894. 'use strict'
  895. return function() {
  896. events.publish(apinamespace+'.change')
  897. target.trigger('change')
  898. }
  899. })(this.$parent, this.events, apinamespace)
  900. // let's trigger change on all data reloads
  901. dataEngine.changed()
  902. // import filters will be passing this back as indication of "we rendered"
  903. return true
  904. }
  905. function initializeCanvasEmulator(canvas){
  906. if (canvas.getContext){
  907. return false
  908. } else {
  909. // for cases when jSignature, FlashCanvas is inserted
  910. // from one window into another (child iframe)
  911. // 'window' and 'FlashCanvas' may be stuck behind
  912. // in that other parent window.
  913. // we need to find it
  914. var window = canvas.ownerDocument.parentWindow
  915. var FC = window.FlashCanvas ?
  916. canvas.ownerDocument.parentWindow.FlashCanvas :
  917. (
  918. typeof FlashCanvas === "undefined" ?
  919. undefined :
  920. FlashCanvas
  921. )
  922. if (FC) {
  923. canvas = FC.initElement(canvas)
  924. var zoom = 1
  925. // FlashCanvas uses flash which has this annoying habit of NOT scaling with page zoom.
  926. // It matches pixel-to-pixel to screen instead.
  927. // Since we are targeting ONLY IE 7, 8 with FlashCanvas, we will test the zoom only the IE8, IE7 way
  928. if (window && window.screen && window.screen.deviceXDPI && window.screen.logicalXDPI){
  929. zoom = window.screen.deviceXDPI * 1.0 / window.screen.logicalXDPI
  930. }
  931. if (zoom !== 1){
  932. try {
  933. // We effectively abuse the brokenness of FlashCanvas and force the flash rendering surface to
  934. // occupy larger pixel dimensions than the wrapping, scaled up DIV and Canvas elems.
  935. $(canvas).children('object').get(0).resize(Math.ceil(canvas.width * zoom), Math.ceil(canvas.height * zoom))
  936. // And by applying "scale" transformation we can talk "browser pixels" to FlashCanvas
  937. // and have it translate the "browser pixels" to "screen pixels"
  938. canvas.getContext('2d').scale(zoom, zoom)
  939. // Note to self: don't reuse Canvas element. Repeated "scale" are cumulative.
  940. } catch (ex) {}
  941. }
  942. return true
  943. } else {
  944. throw new Error("Canvas element does not support 2d context. jSignature cannot proceed.")
  945. }
  946. }
  947. }
  948. jSignatureClass.prototype.initializeCanvas = function(settings) {
  949. // ===========
  950. // Init + Sizing code
  951. var canvas = document.createElement('canvas')
  952. , $canvas = $(canvas)
  953. // We cannot work with circular dependency
  954. if (settings.width === settings.height && settings.height === 'ratio') {
  955. settings.width = '100%'
  956. }
  957. $canvas.css(
  958. 'margin'
  959. , 0
  960. ).css(
  961. 'padding'
  962. , 0
  963. ).css(
  964. 'border'
  965. , 'none'
  966. ).css(
  967. 'height'
  968. , settings.height === 'ratio' || !settings.height ? 1 : settings.height.toString(10)
  969. ).css(
  970. 'width'
  971. , settings.width === 'ratio' || !settings.width ? 1 : settings.width.toString(10)
  972. )
  973. $canvas.appendTo(this.$parent)
  974. // we could not do this until canvas is rendered (appended to DOM)
  975. if (settings.height === 'ratio') {
  976. $canvas.css(
  977. 'height'
  978. , Math.round( $canvas.width() / settings.sizeRatio )
  979. )
  980. } else if (settings.width === 'ratio') {
  981. $canvas.css(
  982. 'width'
  983. , Math.round( $canvas.height() * settings.sizeRatio )
  984. )
  985. }
  986. $canvas.addClass(apinamespace)
  987. // canvas's drawing area resolution is independent from canvas's size.
  988. // pixels are just scaled up or down when internal resolution does not
  989. // match external size. So...
  990. canvas.width = $canvas.width()
  991. canvas.height = $canvas.height()
  992. // Special case Sizing code
  993. this.isCanvasEmulator = initializeCanvasEmulator(canvas)
  994. // End of Sizing Code
  995. // ===========
  996. // normally select preventer would be short, but
  997. // Canvas emulator on IE does NOT provide value for Event. Hence this convoluted line.
  998. canvas.onselectstart = function(e){if(e && e.preventDefault){e.preventDefault()}; if(e && e.stopPropagation){e.stopPropagation()}; return false;}
  999. return canvas
  1000. }
  1001. var GlobalJSignatureObjectInitializer = function(window){
  1002. var globalEvents = new PubSubClass()
  1003. // common "window resized" event listener.
  1004. // jSignature instances will subscribe to this chanel.
  1005. // to resize themselves when needed.
  1006. ;(function(globalEvents, apinamespace, $, window){
  1007. 'use strict'
  1008. var resizetimer
  1009. , runner = function(){
  1010. globalEvents.publish(
  1011. apinamespace + '.parentresized'
  1012. )
  1013. }
  1014. // jSignature knows how to resize its content when its parent is resized
  1015. // window resize is the only way we can catch resize events though...
  1016. $(window).bind('resize.'+apinamespace, function(){
  1017. if (resizetimer) {
  1018. clearTimeout(resizetimer)
  1019. }
  1020. resizetimer = setTimeout(
  1021. runner
  1022. , 500
  1023. )
  1024. })
  1025. // when mouse exists canvas element and "up"s outside, we cannot catch it with
  1026. // callbacks attached to canvas. This catches it outside.
  1027. .bind('mouseup.'+apinamespace, function(e){
  1028. globalEvents.publish(
  1029. apinamespace + '.windowmouseup'
  1030. )
  1031. })
  1032. })(globalEvents, apinamespace, $, window)
  1033. var jSignatureInstanceExtensions = {
  1034. /*
  1035. 'exampleExtension':function(extensionName){
  1036. // we are called very early in instance's life.
  1037. // right after the settings are resolved and
  1038. // jSignatureInstance.events is created
  1039. // and right before first ("jSignature.initializing") event is called.
  1040. // You don't really need to manupilate
  1041. // jSignatureInstance directly, just attach
  1042. // a bunch of events to jSignatureInstance.events
  1043. // (look at the source of jSignatureClass to see when these fire)
  1044. // and your special pieces of code will attach by themselves.
  1045. // this function runs every time a new instance is set up.
  1046. // this means every var you create will live only for one instance
  1047. // unless you attach it to something outside, like "window."
  1048. // and pick it up later from there.
  1049. // when globalEvents' events fire, 'this' is globalEvents object
  1050. // when jSignatureInstance's events fire, 'this' is jSignatureInstance
  1051. // Here,
  1052. // this = is new jSignatureClass's instance.
  1053. // The way you COULD approch setting this up is:
  1054. // if you have multistep set up, attach event to "jSignature.initializing"
  1055. // that attaches other events to be fired further lower the init stream.
  1056. // Or, if you know for sure you rely on only one jSignatureInstance's event,
  1057. // just attach to it directly
  1058. this.events.subscribe(
  1059. // name of the event
  1060. apinamespace + '.initializing'
  1061. // event handlers, can pass args too, but in majority of cases,
  1062. // 'this' which is jSignatureClass object instance pointer is enough to get by.
  1063. , function(){
  1064. if (this.settings.hasOwnProperty('non-existent setting category?')) {
  1065. console.log(extensionName + ' is here')
  1066. }
  1067. }
  1068. )
  1069. }
  1070. */
  1071. }
  1072. var exportplugins = {
  1073. 'default':function(data){return this.toDataURL()}
  1074. , 'native':function(data){return data}
  1075. , 'image':function(data){
  1076. /*this = canvas elem */
  1077. var imagestring = this.toDataURL()
  1078. if (typeof imagestring === 'string' &&
  1079. imagestring.length > 4 &&
  1080. imagestring.slice(0,5) === 'data:' &&
  1081. imagestring.indexOf(',') !== -1){
  1082. var splitterpos = imagestring.indexOf(',')
  1083. return [
  1084. imagestring.slice(5, splitterpos)
  1085. , imagestring.substr(splitterpos + 1)
  1086. ]
  1087. }
  1088. return []
  1089. }
  1090. }
  1091. // will be part of "importplugins"
  1092. function _renderImageOnCanvas( data, formattype, rerendercallable ) {
  1093. 'use strict'
  1094. // #1. Do NOT rely on this. No worky on IE
  1095. // (url max len + lack of base64 decoder + possibly other issues)
  1096. // #2. This does NOT affect what is captured as "signature" as far as vector data is
  1097. // concerned. This is treated same as "signature line" - i.e. completely ignored
  1098. // the only time you see imported image data exported is if you export as image.
  1099. // we do NOT call rerendercallable here (unlike in other import plugins)
  1100. // because importing image does absolutely nothing to the underlying vector data storage
  1101. // This could be a way to "import" old signatures stored as images
  1102. // This could also be a way to import extra decor into signature area.
  1103. var img = new Image()
  1104. // this = Canvas DOM elem. Not jQuery object. Not Canvas's parent div.
  1105. , c = this
  1106. img.onload = function() {
  1107. var ctx = c.getContext("2d").drawImage(
  1108. img, 0, 0
  1109. , ( img.width < c.width) ? img.width : c.width
  1110. , ( img.height < c.height) ? img.height : c.height
  1111. )
  1112. }
  1113. img.src = 'data:' + formattype + ',' + data
  1114. }
  1115. var importplugins = {
  1116. 'native':function(data, formattype, rerendercallable){
  1117. // we expect data as Array of objects of arrays here - whatever 'default' EXPORT plugin spits out.
  1118. // returning Truthy to indicate we are good, all updated.
  1119. rerendercallable( data )
  1120. }
  1121. , 'image': _renderImageOnCanvas
  1122. , 'image/png;base64': _renderImageOnCanvas
  1123. , 'image/jpeg;base64': _renderImageOnCanvas
  1124. , 'image/jpg;base64': _renderImageOnCanvas
  1125. }
  1126. function _clearDrawingArea( data ) {
  1127. this.find('canvas.'+apinamespace)
  1128. .add(this.filter('canvas.'+apinamespace))
  1129. .data(apinamespace+'.this').resetCanvas( data )
  1130. return this
  1131. }
  1132. function _setDrawingData( data, formattype ) {
  1133. var undef
  1134. if (formattype === undef && typeof data === 'string' && data.substr(0,5) === 'data:') {
  1135. formattype = data.slice(5).split(',')[0]
  1136. // 5 chars of "data:" + mimetype len + 1 "," char = all skipped.
  1137. data = data.slice(6 + formattype.length)
  1138. if (formattype === data) return
  1139. }
  1140. var $canvas = this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace))
  1141. if (!importplugins.hasOwnProperty(formattype)){
  1142. throw new Error(apinamespace + " is unable to find import plugin with for format '"+ String(formattype) +"'")
  1143. } else if ($canvas.length !== 0){
  1144. importplugins[formattype].call(
  1145. $canvas[0]
  1146. , data
  1147. , formattype
  1148. , (function(jSignatureInstance){
  1149. return function(){ return jSignatureInstance.resetCanvas.apply(jSignatureInstance, arguments) }
  1150. })($canvas.data(apinamespace+'.this'))
  1151. )
  1152. }
  1153. return this
  1154. }
  1155. var elementIsOrphan = function(e){
  1156. var topOfDOM = false
  1157. e = e.parentNode
  1158. while (e && !topOfDOM){
  1159. topOfDOM = e.body
  1160. e = e.parentNode
  1161. }
  1162. return !topOfDOM
  1163. }
  1164. //These are exposed as methods under $obj.jSignature('methodname', *args)
  1165. var plugins = {'export':exportplugins, 'import':importplugins, 'instance': jSignatureInstanceExtensions}
  1166. , methods = {
  1167. 'init' : function( options ) {
  1168. return this.each( function() {
  1169. if (!elementIsOrphan(this)) {
  1170. new jSignatureClass(this, options, jSignatureInstanceExtensions)
  1171. }
  1172. })
  1173. }
  1174. , 'getSettings' : function() {
  1175. return this.find('canvas.'+apinamespace)
  1176. .add(this.filter('canvas.'+apinamespace))
  1177. .data(apinamespace+'.this').settings
  1178. }
  1179. // around since v1
  1180. , 'clear' : _clearDrawingArea
  1181. // was mistakenly introduced instead of 'clear' in v2
  1182. , 'reset' : _clearDrawingArea
  1183. , 'addPlugin' : function(pluginType, pluginName, callable){
  1184. if (plugins.hasOwnProperty(pluginType)){
  1185. plugins[pluginType][pluginName] = callable
  1186. }
  1187. return this
  1188. }
  1189. , 'listPlugins' : function(pluginType){
  1190. var answer = []
  1191. if (plugins.hasOwnProperty(pluginType)){
  1192. var o = plugins[pluginType]
  1193. for (var k in o){
  1194. if (o.hasOwnProperty(k)){
  1195. answer.push(k)
  1196. }
  1197. }
  1198. }
  1199. return answer
  1200. }
  1201. , 'getData' : function( formattype ) {
  1202. var undef, $canvas=this.find('canvas.'+apinamespace).add(this.filter('canvas.'+apinamespace))
  1203. if (formattype === undef) formattype = 'default'
  1204. if ($canvas.length !== 0 && exportplugins.hasOwnProperty(formattype)){
  1205. return exportplugins[formattype].call(
  1206. $canvas.get(0) // canvas dom elem
  1207. , $canvas.data(apinamespace+'.data') // raw signature data as array of objects of arrays
  1208. )
  1209. }
  1210. }
  1211. // around since v1. Took only one arg - data-url-formatted string with (likely png of) signature image
  1212. , 'importData' : _setDrawingData
  1213. // was mistakenly introduced instead of 'importData' in v2
  1214. , 'setData' : _setDrawingData
  1215. // this is one and same instance for all jSignature.
  1216. , 'globalEvents' : function(){return globalEvents}
  1217. // there will be a separate one for each jSignature instance.
  1218. , 'events' : function() {
  1219. return this.find('canvas.'+apinamespace)
  1220. .add(this.filter('canvas.'+apinamespace))
  1221. .data(apinamespace+'.this').events
  1222. }
  1223. } // end of methods declaration.
  1224. $.fn[apinamespace] = function(method) {
  1225. 'use strict'
  1226. if ( !method || typeof method === 'object' ) {
  1227. return methods.init.apply( this, arguments )
  1228. } else if ( typeof method === 'string' && methods[method] ) {
  1229. return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ))
  1230. } else {
  1231. $.error( 'Method ' + String(method) + ' does not exist on jQuery.' + apinamespace )
  1232. }
  1233. }
  1234. } // end of GlobalJSignatureObjectInitializer
  1235. GlobalJSignatureObjectInitializer(window)
  1236. })();