You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1458 lines
54 KiB

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