From bc4f2a1c743d49dbc779a42cf366482320f72ca2 Mon Sep 17 00:00:00 2001 From: Luis Leiva Date: Sun, 12 Nov 2017 17:10:45 +0100 Subject: [PATCH] Version 2.0 :tada: --- .gitignore | 3 + LICENSE | 21 ++ README.md | 39 ++- dist/jquery.sketchable.full.min.js | 4 +- dist/jquery.sketchable.memento.min.js | 2 +- dist/jquery.sketchable.min.js | 2 +- dist/jsketch.min.js | 2 +- dist/sketchable.full.min.js | 4 +- dist/sketchable.memento.min.js | 2 +- dist/sketchable.min.js | 2 +- figs/res-demo-g3.png | Bin 0 -> 6782 bytes figs/res-demo-guessit.png | Bin 0 -> 8248 bytes figs/res-demo-mucaptcha.png | Bin 0 -> 13233 bytes figs/res-demo-slm.png | Bin 0 -> 12606 bytes figs/res-demo-smiley.png | Bin 0 -> 6451 bytes jquery.sketchable.js | 325 ++++++++++++++---------- jquery.sketchable.memento.js | 200 ++++++++------- jsketch.js | 17 +- package.json | 6 +- sketchable.js | 346 +++++++++++++++----------- sketchable.memento.js | 189 +++++++------- sketchable.utils.js | 72 +++++- 22 files changed, 728 insertions(+), 508 deletions(-) create mode 100644 LICENSE create mode 100644 figs/res-demo-g3.png create mode 100644 figs/res-demo-guessit.png create mode 100644 figs/res-demo-mucaptcha.png create mode 100644 figs/res-demo-slm.png create mode 100644 figs/res-demo-smiley.png diff --git a/.gitignore b/.gitignore index 8c36127..a2193ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules .* *~ +local +*.log +*.bak diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..70cefbd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012--2017 Luis A. Leiva + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 496f85a..5e29205 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,36 @@ -jsketch -======= +# jSketch -A lightweight JavaScript library for drawing facilities on HTML5 canvas, -conveniently wrapped as a jQuery plugin. +A lightweight JavaScript library for drawing facilities on an HTML5 canvas. -Demos and docs available at http://personales.upv.es/luileito/jsketch/ +Conveniently wrapped in a `Sketchable` class. +Available also as a jQuery plugin. -Documented with JSDoc: https://github.com/jsdoc3/jsdoc +[Demos and documentation](https://luis.leiva.name/jsketch/) + +![g3 demo](figs/res-demo-g3.png) +![slm demo](figs/res-demo-slm.png) +![guessit demo](figs/res-demo-guessit.png) +![mucaptcha demo](figs/res-demo-mucaptcha.png) +![smiley demo](figs/res-demo-smiley.png) + +## TL;DR: + +Add `` to your page and just do: +```js +var sketcher = new Sketchable('canvas'); +``` + +Add `` to your page and just do: +```js +var $sketcher = $('canvas').sketchable(); +``` + +That's it! + +**Want to know more?** +Go to [demos and documentation](https://luis.leiva.name/jsketch/). + +## License + +This libray is released with the [MIT license](LICENSE). +The only requirement is that you keep my copyright notice intact when you repurpose, redistribute, or reuse this code. diff --git a/dist/jquery.sketchable.full.min.js b/dist/jquery.sketchable.full.min.js index 1fb6414..6c1b8a8 100644 --- a/dist/jquery.sketchable.full.min.js +++ b/dist/jquery.sketchable.full.min.js @@ -1,2 +1,2 @@ -/*! jSketch drawing lib (all in one) | v1.8.0 | 2017-11-05 */ -!function(a){var b=function(a,b){return new c(a,b)},c=function(a,b){return a?(this.context(a),this.stageWidth=a.width,this.stageHeight=a.height,this.data={},this.drawingDefaults(b),this):void 0};b.fn=c.prototype={context:function(a){if(null===a)throw"No canvas element specified.";return this.canvas=a,this.graphics=a.getContext("2d"),this},drawingDefaults:function(a){return"undefined"==typeof a&&(a={}),"undefined"==typeof a.fillStyle&&(a.fillStyle="#F00"),"undefined"==typeof a.strokeStyle&&(a.strokeStyle="#F0F"),"undefined"==typeof a.lineWidth&&(a.lineWidth=2),"undefined"==typeof a.lineCap&&(a.lineCap="round"),"undefined"==typeof a.lineJoin&&(a.lineJoin="round"),"undefined"==typeof a.miterLimit&&(a.miterLimit=10),this.saveGraphics(a),this.restoreGraphics(a),this},size:function(a,b){return this.stageWidth=a,this.stageHeight=b,this.canvas.width=a,this.canvas.height=b,this.restoreGraphics(),this},background:function(a){return this.beginFill(a),this.graphics.fillRect(0,0,this.stageWidth,this.stageHeight),this.endFill(),this},stage:function(a,b,c){return this.size(a,b).background(c),this},beginFill:function(a){return this.saveGraphics(),this.graphics.fillStyle=a,this},endFill:function(){return this.restoreGraphics(),this},lineStyle:function(a,b,c,d,e){var f={strokeStyle:a||this.graphics.strokeStyle,lineWidth:b||this.graphics.lineWidth,lineCap:c||this.graphics.lineCap,lineJoin:d||this.graphics.lineJoin,miterLimit:e||this.graphics.miterLimit};return this.saveGraphics(f),this.restoreGraphics(f),this},moveTo:function(a,b){return this.graphics.moveTo(a,b),this},lineTo:function(a,b){return this.graphics.lineTo(a,b),this},line:function(a,b,c,d){return this.graphics.moveTo(a,b),this.lineTo(c,d),this},curveTo:function(a,b,c,d){return this.graphics.quadraticCurveTo(c,d,a,b),this},curve:function(a,b,c,d,e,f){return this.graphics.moveTo(a,b),this.curveTo(c,d,e,f),this},stroke:function(){return this.graphics.stroke(),this},strokeRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.strokeRect(a,b,c,d),this.graphics.closePath(),this},fillRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.fillRect(a,b,c,d),this.graphics.closePath(),this},strokeCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.stroke(),this.graphics.closePath(),this},fillCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.fill(),this.graphics.closePath(),this},radialCircle:function(a,b,c,d,e){var f=this.graphics.createRadialGradient(a,b,c,a,b,e);return f.addColorStop(0,d),f.addColorStop(1,"rgba(0,0,0,0)"),this.graphics.fillStyle=f,this.fillCircle(a,b,c),this},beginPath:function(){return this.saveGraphics(),this.graphics.beginPath(),this},closePath:function(){return this.graphics.closePath(),this.restoreGraphics(),this},eraser:function(a){return"undefined"==typeof a&&(a=15),this.graphics.globalCompositeOperation="destination-out",this.lineStyle(null,a),this},pencil:function(a){return"undefined"==typeof a&&(a=2),this.graphics.globalCompositeOperation="source-over",this.lineStyle(null,a),this},clear:function(){return this.graphics.clearRect(0,0,this.stageWidth,this.stageHeight),this},save:function(){return this.graphics.save(),this},restore:function(){return this.graphics.restore(),this},saveGraphics:function(a){return"undefined"==typeof a&&(a=this.data.options),this.data.options=a,this},restoreGraphics:function(a){"undefined"==typeof a&&(a=this.data.options);for(var b in a)this.graphics[b]=a[b];return this},drawImage:function(a,b,c){"undefined"==typeof b&&(b=0),"undefined"==typeof c&&(c=0);var d=this,e=new Image;return e.src=a,e.onload=function(){d.graphics.drawImage(e,b,c)},this}},a.jSketch=b}(this),function(a){function b(a,b){b||(b=a.data(o).options),b.cssCursors&&(a[0].style.cursor=b.interactive?"pointer":"not-allowed")}function c(b){var c=a(b.target),d=c.offset();return{x:Math.round(b.pageX-d.left),y:Math.round(b.pageY-d.top)}}function d(a,b,c){b.coords[a]||(b.coords[a]=[]);var d=(new Date).getTime();b.options.relTimestamps&&(0===b.strokes.length&&0===b.coords[a].length&&(b.timestamp=d),d-=b.timestamp),b.coords[a].push([c.x,c.y,d,+b.sketch.isDrawing])}function e(a){return a.originalEvent.touches?!1:void l(a)}function f(a){return a.originalEvent.touches?!1:void m(a)}function g(a){return a.originalEvent.touches?!1:void n(a)}function h(b,c){var d=a(b.target),e=d.data(o),f=e.options,g=b.originalEvent.changedTouches;if(f.multitouch)for(var h=0;h0&&g.sketch.fillCircle(i.x,i.y,h.graphics.firstPointSize),g.coords[e]||(g.coords[e]=[]),g.coords[e].length>0&&(g.strokes.push(g.coords[e]),g.coords[e]=[]),d(e,g,i),"function"==typeof h.events.mousedown&&h.events.mousedown(f,g,b)}}function m(b){var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive&&(h.mouseupMovements&&0!==g.strokes.length||g.sketch.isDrawing)){var i=c(b);if(g.sketch.isDrawing){var j=g.coords[e][g.coords[e].length-1];g.sketch.beginPath().line(j[0],j[1],i.x,i.y).stroke().closePath()}d(e,g,i),"function"==typeof h.events.mousemove&&h.events.mousemove(f,g,b)}}function n(b){var c=b.identifier||0,d=a(b.target),e=d.data(o),f=e.options;f.interactive&&(e.sketch.isDrawing=!1,e.strokes.push(e.coords[c]),e.coords[c]=[],"function"==typeof f.events.mouseup&&f.events.mouseup(d,e,b))}var o="sketchable",p={init:function(c){var d=a.extend(!0,{},a.fn.sketchable.defaults,c||{});return this.each(function(){var c=a(this),h=c.data(o);h||(d.interactive&&(c.bind("mousedown",e),c.bind("mousemove",f),c.bind("mouseup",g),c.bind("touchstart",i),c.bind("touchmove",j),c.bind("touchend",k),this.onselectstart=function(){return!1}),b(c,d));var l=new jSketch(this,d.graphics);c.data(o,{strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:l,options:d}),"function"==typeof d.events.init&&d.events.init(c,c.data(o))})},config:function(c){return this.each(function(){var d=a(this),e=d.data(o);e.options=a.extend(!0,{},a.fn.sketchable.defaults,e.options,c||{}),b(d)})},strokes:function(b){if(b)return this.each(function(){var c=a(this),d=c.data(o);d.strokes=b});var c=a(this).data(o);return c.strokes},handler:function(b){return this.each(function(){var c=a(this),d=c.data(o);b(c,d)})},clear:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;c.sketch&&(c.sketch.clear(),c.strokes=[],c.coords={}),d&&"function"==typeof d.events.clear&&d.events.clear(b,c)})},reset:function(b){return this.each(function(){var c=a(this),d=c.data(o)||{},e=d.options;c.sketchable("destroy").sketchable(b),e&&"function"==typeof e.events.reset&&e.events.reset(c,d)})},destroy:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;d.interactive&&(b.unbind("mouseup",g),b.unbind("mousemove",f),b.unbind("mousedown",e),b.unbind("touchstart",i),b.unbind("touchmove",j),b.unbind("touchend",k)),b.removeData(o),d&&"function"==typeof d.events.destroy&&d.events.destroy(b,c)})}};a.fn.sketchable=function(b){return"methods functions hooks".split(" ").indexOf(b)>-1?p:p[b]?p[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?(a.error("Method "+b+' does not exist. See jQuery.sketchable("methods").'),this):p.init.apply(this,arguments)},a.fn.sketchable.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}}}(jQuery),function(a){function b(b,g){function h(a){if(i&&i.events&&"function"==typeof i.events[a]){var c=i.events[a];i.events[a]=function(){var d=Array.prototype.slice.call(arguments,0);c.apply(b,d),j[a].apply(b,d)}}else i.events[a]=j[a]}var i=a.extend(!0,{},e.defaults,g);if(!i.interactive)return g;var j={init:function(a,b){b.memento=new c(a),b.memento.save(),b.memento.init()},clear:function(a,b){b.memento.reset(),b.memento.save()},mouseup:function(a,b,c){b.memento.save()},destroy:function(a,b){b.memento.destroy()}};if(!e.isMementoReady){for(var k="init mouseup clear destroy".split(" "),l=0;l0){h--;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function d(){if(h0&&g.sketch.beginFill(h.graphics.fillStyle).fillCircle(i.x,i.y,h.graphics.firstPointSize).endFill(),g.coords[e]||(g.coords[e]=[]),g.coords[e].length>0&&(g.strokes.push(g.coords[e]),g.coords[e]=[]),d(e,g,i),"function"==typeof h.events.mousedown&&h.events.mousedown(f,g,b)}}function m(b){var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive&&(h.mouseupMovements&&0!==g.strokes.length||g.sketch.isDrawing)){var i=c(b);if(g.sketch.isDrawing){var j=g.coords[e][g.coords[e].length-1];g.sketch.beginPath().line(j[0],j[1],i.x,i.y).stroke().closePath()}d(e,g,i),"function"==typeof h.events.mousemove&&h.events.mousemove(f,g,b)}}function n(b){var c=b.identifier||0,d=a(b.target),e=d.data(o),f=e.options;f.interactive&&(e.sketch.isDrawing=!1,e.strokes.push(e.coords[c]),e.coords[c]=[],"function"==typeof f.events.mouseup&&f.events.mouseup(d,e,b))}var o="sketchable",p={init:function(c){var d=a.extend(!0,{},a.fn.sketchable.defaults,c||{});return this.each(function(){var c=a(this),h=c.data(o);h||(c.bind("mousedown",e),c.bind("mousemove",f),c.bind("mouseup",g),c.bind("touchstart",i),c.bind("touchmove",j),c.bind("touchend",k),this.onselectstart=function(){return!1},b(c,d));var l=new jSketch(this,d.graphics);c.data(o,{strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:l,options:d}),d.events&&"function"==typeof d.events.init&&d.events.init(c,c.data(o));for(var m in a.fn.sketchable.plugins)a.fn.sketchable.plugins[m](c)})},config:function(c){return c?this.each(function(){var d=a(this),e=d.data(o);e.options=a.extend(!0,{},a.fn.sketchable.defaults,e.options,c),b(d)}):a(this).data(o)},strokes:function(b){if(b)return this.each(function(){var c=a(this),d=c.data(o);d.strokes=b});var c=a(this).data(o);return c.strokes},handler:function(b){return this.each(function(){var c=a(this),d=c.data(o);b(c,d)})},clear:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;c.sketch&&(c.sketch.clear(),c.strokes=[],c.coords={}),d&&"function"==typeof d.events.clear&&d.events.clear(b,c)})},reset:function(b){return this.each(function(){var c=a(this),d=c.data(o)||{},e=d.options;c.sketchable("destroy").sketchable(b),e&&"function"==typeof e.events.reset&&e.events.reset(c,d)})},destroy:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;b.unbind("mouseup",g),b.unbind("mousemove",f),b.unbind("mousedown",e),b.unbind("touchstart",i),b.unbind("touchmove",j),b.unbind("touchend",k),b.removeData(o),d&&"function"==typeof d.events.destroy&&d.events.destroy(b,c)})}};a.fn.sketchable=function(b){return"object"!=typeof b&&b?p[b]?p[b].apply(this,Array.prototype.slice.call(arguments,1)):(a.error("Unknown method: "+b),this):p.init.apply(this,arguments)},a.fn.sketchable.api=p,a.fn.sketchable.plugins={},a.fn.sketchable.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}}}(jQuery),function(a){function b(b){function c(){if(h>0){h--;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function d(){if(h0){h--;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function d(){if(h0){h--;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function d(){if(h0&&g.sketch.fillCircle(i.x,i.y,h.graphics.firstPointSize),g.coords[e]||(g.coords[e]=[]),g.coords[e].length>0&&(g.strokes.push(g.coords[e]),g.coords[e]=[]),d(e,g,i),"function"==typeof h.events.mousedown&&h.events.mousedown(f,g,b)}}function m(b){var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive&&(h.mouseupMovements&&0!==g.strokes.length||g.sketch.isDrawing)){var i=c(b);if(g.sketch.isDrawing){var j=g.coords[e][g.coords[e].length-1];g.sketch.beginPath().line(j[0],j[1],i.x,i.y).stroke().closePath()}d(e,g,i),"function"==typeof h.events.mousemove&&h.events.mousemove(f,g,b)}}function n(b){var c=b.identifier||0,d=a(b.target),e=d.data(o),f=e.options;f.interactive&&(e.sketch.isDrawing=!1,e.strokes.push(e.coords[c]),e.coords[c]=[],"function"==typeof f.events.mouseup&&f.events.mouseup(d,e,b))}var o="sketchable",p={init:function(c){var d=a.extend(!0,{},a.fn.sketchable.defaults,c||{});return this.each(function(){var c=a(this),h=c.data(o);h||(d.interactive&&(c.bind("mousedown",e),c.bind("mousemove",f),c.bind("mouseup",g),c.bind("touchstart",i),c.bind("touchmove",j),c.bind("touchend",k),this.onselectstart=function(){return!1}),b(c,d));var l=new jSketch(this,d.graphics);c.data(o,{strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:l,options:d}),"function"==typeof d.events.init&&d.events.init(c,c.data(o))})},config:function(c){return this.each(function(){var d=a(this),e=d.data(o);e.options=a.extend(!0,{},a.fn.sketchable.defaults,e.options,c||{}),b(d)})},strokes:function(b){if(b)return this.each(function(){var c=a(this),d=c.data(o);d.strokes=b});var c=a(this).data(o);return c.strokes},handler:function(b){return this.each(function(){var c=a(this),d=c.data(o);b(c,d)})},clear:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;c.sketch&&(c.sketch.clear(),c.strokes=[],c.coords={}),d&&"function"==typeof d.events.clear&&d.events.clear(b,c)})},reset:function(b){return this.each(function(){var c=a(this),d=c.data(o)||{},e=d.options;c.sketchable("destroy").sketchable(b),e&&"function"==typeof e.events.reset&&e.events.reset(c,d)})},destroy:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;d.interactive&&(b.unbind("mouseup",g),b.unbind("mousemove",f),b.unbind("mousedown",e),b.unbind("touchstart",i),b.unbind("touchmove",j),b.unbind("touchend",k)),b.removeData(o),d&&"function"==typeof d.events.destroy&&d.events.destroy(b,c)})}};a.fn.sketchable=function(b){return"methods functions hooks".split(" ").indexOf(b)>-1?p:p[b]?p[b].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof b&&b?(a.error("Method "+b+' does not exist. See jQuery.sketchable("methods").'),this):p.init.apply(this,arguments)},a.fn.sketchable.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}}}(jQuery); \ No newline at end of file +!function(a){function b(a,b){b||(b=a.data(o).options),b.cssCursors&&(a[0].style.cursor=b.interactive?"pointer":"not-allowed")}function c(b){var c=a(b.target),d=c.offset();return{x:Math.round(b.pageX-d.left),y:Math.round(b.pageY-d.top)}}function d(a,b,c){b.coords[a]||(b.coords[a]=[]);var d=(new Date).getTime();b.options.relTimestamps&&(0===b.strokes.length&&0===b.coords[a].length&&(b.timestamp=d),d-=b.timestamp),b.coords[a].push([c.x,c.y,d,+b.sketch.isDrawing])}function e(a){return a.originalEvent.touches?!1:void l(a)}function f(a){return a.originalEvent.touches?!1:void m(a)}function g(a){return a.originalEvent.touches?!1:void n(a)}function h(b,c){var d=a(b.target),e=d.data(o),f=e.options,g=b.originalEvent.touches;if(f.multitouch)for(var h=0;h0&&g.sketch.beginFill(h.graphics.fillStyle).fillCircle(i.x,i.y,h.graphics.firstPointSize).endFill(),g.coords[e]||(g.coords[e]=[]),g.coords[e].length>0&&(g.strokes.push(g.coords[e]),g.coords[e]=[]),d(e,g,i),"function"==typeof h.events.mousedown&&h.events.mousedown(f,g,b)}}function m(b){var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive&&(h.mouseupMovements&&0!==g.strokes.length||g.sketch.isDrawing)){var i=c(b);if(g.sketch.isDrawing){var j=g.coords[e][g.coords[e].length-1];g.sketch.beginPath().line(j[0],j[1],i.x,i.y).stroke().closePath()}d(e,g,i),"function"==typeof h.events.mousemove&&h.events.mousemove(f,g,b)}}function n(b){var c=b.identifier||0,d=a(b.target),e=d.data(o),f=e.options;f.interactive&&(e.sketch.isDrawing=!1,e.strokes.push(e.coords[c]),e.coords[c]=[],"function"==typeof f.events.mouseup&&f.events.mouseup(d,e,b))}var o="sketchable",p={init:function(c){var d=a.extend(!0,{},a.fn.sketchable.defaults,c||{});return this.each(function(){var c=a(this),h=c.data(o);h||(c.bind("mousedown",e),c.bind("mousemove",f),c.bind("mouseup",g),c.bind("touchstart",i),c.bind("touchmove",j),c.bind("touchend",k),this.onselectstart=function(){return!1},b(c,d));var l=new jSketch(this,d.graphics);c.data(o,{strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:l,options:d}),d.events&&"function"==typeof d.events.init&&d.events.init(c,c.data(o));for(var m in a.fn.sketchable.plugins)a.fn.sketchable.plugins[m](c)})},config:function(c){return c?this.each(function(){var d=a(this),e=d.data(o);e.options=a.extend(!0,{},a.fn.sketchable.defaults,e.options,c),b(d)}):a(this).data(o)},strokes:function(b){if(b)return this.each(function(){var c=a(this),d=c.data(o);d.strokes=b});var c=a(this).data(o);return c.strokes},handler:function(b){return this.each(function(){var c=a(this),d=c.data(o);b(c,d)})},clear:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;c.sketch&&(c.sketch.clear(),c.strokes=[],c.coords={}),d&&"function"==typeof d.events.clear&&d.events.clear(b,c)})},reset:function(b){return this.each(function(){var c=a(this),d=c.data(o)||{},e=d.options;c.sketchable("destroy").sketchable(b),e&&"function"==typeof e.events.reset&&e.events.reset(c,d)})},destroy:function(){return this.each(function(){var b=a(this),c=b.data(o)||{},d=c.options;b.unbind("mouseup",g),b.unbind("mousemove",f),b.unbind("mousedown",e),b.unbind("touchstart",i),b.unbind("touchmove",j),b.unbind("touchend",k),b.removeData(o),d&&"function"==typeof d.events.destroy&&d.events.destroy(b,c)})}};a.fn.sketchable=function(b){return"object"!=typeof b&&b?p[b]?p[b].apply(this,Array.prototype.slice.call(arguments,1)):(a.error("Unknown method: "+b),this):p.init.apply(this,arguments)},a.fn.sketchable.api=p,a.fn.sketchable.plugins={},a.fn.sketchable.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}}}(jQuery); \ No newline at end of file diff --git a/dist/jsketch.min.js b/dist/jsketch.min.js index 8bf2fe3..9f498d1 100644 --- a/dist/jsketch.min.js +++ b/dist/jsketch.min.js @@ -1 +1 @@ -!function(a){var b=function(a,b){return new c(a,b)},c=function(a,b){return a?(this.context(a),this.stageWidth=a.width,this.stageHeight=a.height,this.data={},this.drawingDefaults(b),this):void 0};b.fn=c.prototype={context:function(a){if(null===a)throw"No canvas element specified.";return this.canvas=a,this.graphics=a.getContext("2d"),this},drawingDefaults:function(a){return"undefined"==typeof a&&(a={}),"undefined"==typeof a.fillStyle&&(a.fillStyle="#F00"),"undefined"==typeof a.strokeStyle&&(a.strokeStyle="#F0F"),"undefined"==typeof a.lineWidth&&(a.lineWidth=2),"undefined"==typeof a.lineCap&&(a.lineCap="round"),"undefined"==typeof a.lineJoin&&(a.lineJoin="round"),"undefined"==typeof a.miterLimit&&(a.miterLimit=10),this.saveGraphics(a),this.restoreGraphics(a),this},size:function(a,b){return this.stageWidth=a,this.stageHeight=b,this.canvas.width=a,this.canvas.height=b,this.restoreGraphics(),this},background:function(a){return this.beginFill(a),this.graphics.fillRect(0,0,this.stageWidth,this.stageHeight),this.endFill(),this},stage:function(a,b,c){return this.size(a,b).background(c),this},beginFill:function(a){return this.saveGraphics(),this.graphics.fillStyle=a,this},endFill:function(){return this.restoreGraphics(),this},lineStyle:function(a,b,c,d,e){var f={strokeStyle:a||this.graphics.strokeStyle,lineWidth:b||this.graphics.lineWidth,lineCap:c||this.graphics.lineCap,lineJoin:d||this.graphics.lineJoin,miterLimit:e||this.graphics.miterLimit};return this.saveGraphics(f),this.restoreGraphics(f),this},moveTo:function(a,b){return this.graphics.moveTo(a,b),this},lineTo:function(a,b){return this.graphics.lineTo(a,b),this},line:function(a,b,c,d){return this.graphics.moveTo(a,b),this.lineTo(c,d),this},curveTo:function(a,b,c,d){return this.graphics.quadraticCurveTo(c,d,a,b),this},curve:function(a,b,c,d,e,f){return this.graphics.moveTo(a,b),this.curveTo(c,d,e,f),this},stroke:function(){return this.graphics.stroke(),this},strokeRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.strokeRect(a,b,c,d),this.graphics.closePath(),this},fillRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.fillRect(a,b,c,d),this.graphics.closePath(),this},strokeCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.stroke(),this.graphics.closePath(),this},fillCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.fill(),this.graphics.closePath(),this},radialCircle:function(a,b,c,d,e){var f=this.graphics.createRadialGradient(a,b,c,a,b,e);return f.addColorStop(0,d),f.addColorStop(1,"rgba(0,0,0,0)"),this.graphics.fillStyle=f,this.fillCircle(a,b,c),this},beginPath:function(){return this.saveGraphics(),this.graphics.beginPath(),this},closePath:function(){return this.graphics.closePath(),this.restoreGraphics(),this},eraser:function(a){return"undefined"==typeof a&&(a=15),this.graphics.globalCompositeOperation="destination-out",this.lineStyle(null,a),this},pencil:function(a){return"undefined"==typeof a&&(a=2),this.graphics.globalCompositeOperation="source-over",this.lineStyle(null,a),this},clear:function(){return this.graphics.clearRect(0,0,this.stageWidth,this.stageHeight),this},save:function(){return this.graphics.save(),this},restore:function(){return this.graphics.restore(),this},saveGraphics:function(a){return"undefined"==typeof a&&(a=this.data.options),this.data.options=a,this},restoreGraphics:function(a){"undefined"==typeof a&&(a=this.data.options);for(var b in a)this.graphics[b]=a[b];return this},drawImage:function(a,b,c){"undefined"==typeof b&&(b=0),"undefined"==typeof c&&(c=0);var d=this,e=new Image;return e.src=a,e.onload=function(){d.graphics.drawImage(e,b,c)},this}},a.jSketch=b}(this); \ No newline at end of file +!function(a){var b=function(a,b){return new c(a,b)},c=function(a,b){return a?(this.context(a),this.stageWidth=a.width,this.stageHeight=a.height,this.data={},this.drawingDefaults(b),this):void 0};b.fn=c.prototype={context:function(a){if(null===a)throw"No canvas element specified.";return this.canvas=a,this.graphics=a.getContext("2d"),this},drawingDefaults:function(a){return"undefined"==typeof a&&(a={}),"undefined"==typeof a.fillStyle&&(a.fillStyle="#F00"),"undefined"==typeof a.strokeStyle&&(a.strokeStyle="#F0F"),"undefined"==typeof a.lineWidth&&(a.lineWidth=2),"undefined"==typeof a.lineCap&&(a.lineCap="round"),"undefined"==typeof a.lineJoin&&(a.lineJoin="round"),"undefined"==typeof a.miterLimit&&(a.miterLimit=10),this.saveGraphics(a),this.restoreGraphics(),this},size:function(a,b){return this.stageWidth=a,this.stageHeight=b,this.canvas.width=a,this.canvas.height=b,this.restoreGraphics(),this},background:function(a){return this.beginFill(a),this.graphics.fillRect(0,0,this.stageWidth,this.stageHeight),this.endFill(),this},stage:function(a,b,c){return this.size(a,b).background(c),this},beginFill:function(a){return this.saveGraphics(),this.graphics.fillStyle=a,this},endFill:function(){return this.restoreGraphics(),this},lineStyle:function(a,b,c,d,e){var f={strokeStyle:a||this.graphics.strokeStyle,lineWidth:b||this.graphics.lineWidth,lineCap:c||this.graphics.lineCap,lineJoin:d||this.graphics.lineJoin,miterLimit:e||this.graphics.miterLimit};return this.saveGraphics(f),this.restoreGraphics(),this},moveTo:function(a,b){return this.graphics.moveTo(a,b),this},lineTo:function(a,b){return this.graphics.lineTo(a,b),this},line:function(a,b,c,d){return this.graphics.moveTo(a,b),this.lineTo(c,d),this},curveTo:function(a,b,c,d){return this.graphics.quadraticCurveTo(c,d,a,b),this},curve:function(a,b,c,d,e,f){return this.graphics.moveTo(a,b),this.curveTo(c,d,e,f),this},stroke:function(){return this.graphics.stroke(),this},strokeRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.strokeRect(a,b,c,d),this.graphics.closePath(),this},fillRect:function(a,b,c,d){return this.graphics.beginPath(),this.graphics.fillRect(a,b,c,d),this.graphics.closePath(),this},strokeCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.stroke(),this.graphics.closePath(),this},fillCircle:function(a,b,c){return this.graphics.beginPath(),this.graphics.arc(a,b,c,0,2*Math.PI,!1),this.graphics.fill(),this.graphics.closePath(),this},radialCircle:function(a,b,c,d,e){var f=this.graphics.createRadialGradient(a,b,c,a,b,d||5);if("array"!==e.constructor.name)e=[this.graphics.fillStyle,"white"];else for(var g=0;g0&&f.sketch.fillCircle(h.x,h.y,g.graphics.firstPointSize),f.coords[b]||(f.coords[b]=[]),f.coords[b].length>0&&(f.strokes.push(f.coords[b]),f.coords[b]=[]),d(b,f,h),"function"==typeof g.events.mousedown&&g.events.mousedown(e,f,a)}}function m(a){var b=a.identifier||0,e=a.target,f=dataBind(e)[o],g=f.options;if(g.interactive&&(g.mouseupMovements&&0!==f.strokes.length||f.sketch.isDrawing)){var h=c(a);if(f.sketch.isDrawing){var i=f.coords[b][f.coords[b].length-1];f.sketch.beginPath().line(i[0],i[1],h.x,h.y).stroke().closePath()}d(b,f,h),"function"==typeof g.events.mousemove&&g.events.mousemove(e,f,a)}}function n(a){var b=a.identifier||0,c=a.target,d=dataBind(c)[o],e=d.options;e.interactive&&(d.sketch.isDrawing=!1,d.strokes.push(d.coords[b]),d.coords[b]=[],"function"==typeof e.events.mouseup&&e.events.mouseup(c,d,a))}var o="sketchable",p=function(a,b){return new q(a,b)},q=function(a,b){return a?(this.elem=a,"undefined"==typeof b&&(b={}),this.init(b)):void 0};p.fn=q.prototype={init:function(a){var b=deepExtend(p.fn.defaults,a||{}),c=this.elem,d=dataBind(c)[o];d||(b.interactive&&(Event.add(c,"mousedown",e),Event.add(c,"mousemove",f),Event.add(c,"mouseup",g),Event.add(c,"touchstart",i),Event.add(c,"touchmove",j),Event.add(c,"touchend",k),this.onselectstart=function(){return!1}),b.cssCursors&&(c.style.cursor=b.interactive?"pointer":"not-allowed"));var h=new jSketch(c,b.graphics);return dataBind(c)[o]={strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:h,options:b},"function"==typeof b.events.init&&b.events.init(c,dataBind(c)[o]),this},config:function(a){var b=this.elem,c=dataBind(b)[o];return c.options=deepExtend(p.fn.defaults,a||{}),this},strokes:function(a){var b=this.elem;if(a){var c=dataBind(b)[o];return c.strokes=a,this}var c=dataBind(b)[o];return c.strokes},handler:function(a){var b=this.elem,c=dataBind(b)[o];return a(b,c),this},clear:function(){var a=this.elem,b=dataBind(a)[o],c=b.options;return b.sketch.clear(),b.strokes=[],b.coords={},"function"==typeof c.events.clear&&c.events.clear(a,b),this},reset:function(a){var b=this.elem,c=dataBind(b)[o],d=c.options;return this.destroy().init(a),"function"==typeof d.events.reset&&d.events.reset(b,c),this},destroy:function(){var a=this.elem,b=dataBind(a)[o],c=b.options;return c.interactive&&(Event.remove(a,"mouseup",g),Event.remove(a,"mousemove",f),Event.remove(a,"mousedown",e),Event.remove(a,"touchstart",i),Event.remove(a,"touchmove",j),Event.remove(a,"touchend",k)),dataBind(a)[o]=null,"function"==typeof c.events.destroy&&c.events.destroy(a,b),this}},p.fn.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}},a.Sketchable=p}(this),function(a){function b(a,b){function g(a){if(h&&h.events&&"function"==typeof h.events[a]){var b=h.events[a];h.events[a]=function(){var c=Array.prototype.slice.call(arguments,0);b.apply(i,c),j[a].apply(i,c)}}else f.events[a]=j[a]}var h=deepExtend(f,b);if(!h.interactive)return b;var i=a.elem,j={init:function(b,d){d.memento=new c(a),d.memento.save(),d.memento.init()},clear:function(a,b){b.memento.reset(),b.memento.save()},mouseup:function(a,b,c){b.memento.save()},destroy:function(a,b){b.memento.destroy()}};if(!e.isMementoReady){for(var k="init mouseup clear destroy".split(" "),l=0;l0){g--;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function c(){if(g0&&f.sketch.beginFill(g.graphics.fillStyle).fillCircle(h.x,h.y,g.graphics.firstPointSize).endFill(),f.coords[b]||(f.coords[b]=[]),f.coords[b].length>0&&(f.strokes.push(f.coords[b]),f.coords[b]=[]),e(b,f,h),"function"==typeof g.events.mousedown&&g.events.mousedown(c,f,a)}}function n(a){var b=a.identifier||0,c=a.target,f=dataBind(c)[p],g=f.options;if(g.interactive&&(g.mouseupMovements&&0!==f.strokes.length||f.sketch.isDrawing)){var h=d(a);if(f.sketch.isDrawing){var i=f.coords[b][f.coords[b].length-1];f.sketch.beginPath().line(i[0],i[1],h.x,h.y).stroke().closePath()}e(b,f,h),"function"==typeof g.events.mousemove&&g.events.mousemove(c,f,a)}}function o(a){var b=a.identifier||0,c=a.target,d=dataBind(c)[p],e=d.options;e.interactive&&(d.sketch.isDrawing=!1,d.strokes.push(d.coords[b]),d.coords[b]=[],"function"==typeof e.events.mouseup&&e.events.mouseup(c,d,a))}var p="sketchable",q=a.document;b.prototype={init:function(a){var a=deepExtend({},b.prototype.defaults,a||{}),c=this.elem,d=dataBind(c)[p];d||(Event.add(c,"mousedown",f),Event.add(c,"mousemove",g),Event.add(c,"mouseup",h),Event.add(c,"touchstart",j),Event.add(c,"touchmove",k),Event.add(c,"touchend",l),c.onselectstart=function(){return!1},a.cssCursors&&(c.style.cursor=a.interactive?"pointer":"not-allowed"));var e=new jSketch(c,a.graphics);dataBind(c)[p]=d={strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:e,sketchable:this,options:a},"function"==typeof a.events.init&&a.events.init(c,d);for(var i in this.plugins)this.plugins[i](this);return this},config:function(a){var c=this.elem,d=dataBind(c)[p];return a?(d.options=deepExtend({},b.prototype.defaults,a||{}),this):d},strokes:function(a){var b=this.elem;if(a){var c=dataBind(b)[p];return c.strokes=a,this}var c=dataBind(b)[p];return c.strokes},handler:function(a){var b=this.elem,c=dataBind(b)[p];return a(b,c),this},clear:function(){var a=this.elem,b=dataBind(a)[p],c=b.options;return b.sketch.clear(),b.strokes=[],b.coords={},"function"==typeof c.events.clear&&c.events.clear(a,b),this},reset:function(a){var b=this.elem,c=dataBind(b)[p],a=c.options;return this.destroy().init(a),"function"==typeof a.events.reset&&a.events.reset(b,c),this},destroy:function(){var a=this.elem,b=dataBind(a)[p],c=b.options;return Event.remove(a,"mouseup",h),Event.remove(a,"mousemove",g),Event.remove(a,"mousedown",f),Event.remove(a,"touchstart",j),Event.remove(a,"touchmove",k),Event.remove(a,"touchend",l),dataBind(a)[p]=null,"function"==typeof c.events.destroy&&c.events.destroy(a,b),this}},b.prototype.plugins={},b.prototype.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}},a.Sketchable=b}(this),function(a){function b(a){function b(){if(g>0){g--;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function c(){if(g0){g--;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function c(){if(g0){g--;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function c(){if(g0&&f.sketch.fillCircle(h.x,h.y,g.graphics.firstPointSize),f.coords[b]||(f.coords[b]=[]),f.coords[b].length>0&&(f.strokes.push(f.coords[b]),f.coords[b]=[]),d(b,f,h),"function"==typeof g.events.mousedown&&g.events.mousedown(e,f,a)}}function m(a){var b=a.identifier||0,e=a.target,f=dataBind(e)[o],g=f.options;if(g.interactive&&(g.mouseupMovements&&0!==f.strokes.length||f.sketch.isDrawing)){var h=c(a);if(f.sketch.isDrawing){var i=f.coords[b][f.coords[b].length-1];f.sketch.beginPath().line(i[0],i[1],h.x,h.y).stroke().closePath()}d(b,f,h),"function"==typeof g.events.mousemove&&g.events.mousemove(e,f,a)}}function n(a){var b=a.identifier||0,c=a.target,d=dataBind(c)[o],e=d.options;e.interactive&&(d.sketch.isDrawing=!1,d.strokes.push(d.coords[b]),d.coords[b]=[],"function"==typeof e.events.mouseup&&e.events.mouseup(c,d,a))}var o="sketchable",p=function(a,b){return new q(a,b)},q=function(a,b){return a?(this.elem=a,"undefined"==typeof b&&(b={}),this.init(b)):void 0};p.fn=q.prototype={init:function(a){var b=deepExtend(p.fn.defaults,a||{}),c=this.elem,d=dataBind(c)[o];d||(b.interactive&&(Event.add(c,"mousedown",e),Event.add(c,"mousemove",f),Event.add(c,"mouseup",g),Event.add(c,"touchstart",i),Event.add(c,"touchmove",j),Event.add(c,"touchend",k),this.onselectstart=function(){return!1}),b.cssCursors&&(c.style.cursor=b.interactive?"pointer":"not-allowed"));var h=new jSketch(c,b.graphics);return dataBind(c)[o]={strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:h,options:b},"function"==typeof b.events.init&&b.events.init(c,dataBind(c)[o]),this},config:function(a){var b=this.elem,c=dataBind(b)[o];return c.options=deepExtend(p.fn.defaults,a||{}),this},strokes:function(a){var b=this.elem;if(a){var c=dataBind(b)[o];return c.strokes=a,this}var c=dataBind(b)[o];return c.strokes},handler:function(a){var b=this.elem,c=dataBind(b)[o];return a(b,c),this},clear:function(){var a=this.elem,b=dataBind(a)[o],c=b.options;return b.sketch.clear(),b.strokes=[],b.coords={},"function"==typeof c.events.clear&&c.events.clear(a,b),this},reset:function(a){var b=this.elem,c=dataBind(b)[o],d=c.options;return this.destroy().init(a),"function"==typeof d.events.reset&&d.events.reset(b,c),this},destroy:function(){var a=this.elem,b=dataBind(a)[o],c=b.options;return c.interactive&&(Event.remove(a,"mouseup",g),Event.remove(a,"mousemove",f),Event.remove(a,"mousedown",e),Event.remove(a,"touchstart",i),Event.remove(a,"touchmove",j),Event.remove(a,"touchend",k)),dataBind(a)[o]=null,"function"==typeof c.events.destroy&&c.events.destroy(a,b),this}},p.fn.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}},a.Sketchable=p}(this); \ No newline at end of file +!function(a){function b(a,b){if(!a)throw new Error("Sketchable requires a DOM element.");return"string"==typeof a&&(a=q.querySelector(a)),this.elem=a,this.init(b)}function c(b){var c=b.getBoundingClientRect(),d=q.body,e=q.documentElement,f=a.pageYOffset||e.scrollTop||d.scrollTop,g=a.pageXOffset||e.scrollLeft||d.scrollLeft,h=e.clientTop||d.clientTop||0,i=e.clientLeft||d.clientLeft||0,j=c.top+f-h,k=c.left+g-i;return{top:Math.round(j),left:Math.round(k)}}function d(a){var b=a.target,d=c(b);return{x:Math.round(a.pageX-d.left),y:Math.round(a.pageY-d.top)}}function e(a,b,c){b.coords[a]||(b.coords[a]=[]);var d=(new Date).getTime();b.options.relTimestamps&&(0===b.strokes.length&&0===b.coords[a].length&&(b.timestamp=d),d-=b.timestamp),b.coords[a].push([c.x,c.y,d,+b.sketch.isDrawing])}function f(a){return a.touches?!1:void m(a)}function g(a){return a.touches?!1:void n(a)}function h(a){return a.touches?!1:void o(a)}function i(a,b){var c=a.target,d=dataBind(c)[p],e=d.options,f=a.touches;if(e.multitouch)for(var g=0;g0&&f.sketch.beginFill(g.graphics.fillStyle).fillCircle(h.x,h.y,g.graphics.firstPointSize).endFill(),f.coords[b]||(f.coords[b]=[]),f.coords[b].length>0&&(f.strokes.push(f.coords[b]),f.coords[b]=[]),e(b,f,h),"function"==typeof g.events.mousedown&&g.events.mousedown(c,f,a)}}function n(a){var b=a.identifier||0,c=a.target,f=dataBind(c)[p],g=f.options;if(g.interactive&&(g.mouseupMovements&&0!==f.strokes.length||f.sketch.isDrawing)){var h=d(a);if(f.sketch.isDrawing){var i=f.coords[b][f.coords[b].length-1];f.sketch.beginPath().line(i[0],i[1],h.x,h.y).stroke().closePath()}e(b,f,h),"function"==typeof g.events.mousemove&&g.events.mousemove(c,f,a)}}function o(a){var b=a.identifier||0,c=a.target,d=dataBind(c)[p],e=d.options;e.interactive&&(d.sketch.isDrawing=!1,d.strokes.push(d.coords[b]),d.coords[b]=[],"function"==typeof e.events.mouseup&&e.events.mouseup(c,d,a))}var p="sketchable",q=a.document;b.prototype={init:function(a){var a=deepExtend({},b.prototype.defaults,a||{}),c=this.elem,d=dataBind(c)[p];d||(Event.add(c,"mousedown",f),Event.add(c,"mousemove",g),Event.add(c,"mouseup",h),Event.add(c,"touchstart",j),Event.add(c,"touchmove",k),Event.add(c,"touchend",l),c.onselectstart=function(){return!1},a.cssCursors&&(c.style.cursor=a.interactive?"pointer":"not-allowed"));var e=new jSketch(c,a.graphics);dataBind(c)[p]=d={strokes:[],coords:{},timestamp:(new Date).getTime(),sketch:e,sketchable:this,options:a},"function"==typeof a.events.init&&a.events.init(c,d);for(var i in this.plugins)this.plugins[i](this);return this},config:function(a){var c=this.elem,d=dataBind(c)[p];return a?(d.options=deepExtend({},b.prototype.defaults,a||{}),this):d},strokes:function(a){var b=this.elem;if(a){var c=dataBind(b)[p];return c.strokes=a,this}var c=dataBind(b)[p];return c.strokes},handler:function(a){var b=this.elem,c=dataBind(b)[p];return a(b,c),this},clear:function(){var a=this.elem,b=dataBind(a)[p],c=b.options;return b.sketch.clear(),b.strokes=[],b.coords={},"function"==typeof c.events.clear&&c.events.clear(a,b),this},reset:function(a){var b=this.elem,c=dataBind(b)[p],a=c.options;return this.destroy().init(a),"function"==typeof a.events.reset&&a.events.reset(b,c),this},destroy:function(){var a=this.elem,b=dataBind(a)[p],c=b.options;return Event.remove(a,"mouseup",h),Event.remove(a,"mousemove",g),Event.remove(a,"mousedown",f),Event.remove(a,"touchstart",j),Event.remove(a,"touchmove",k),Event.remove(a,"touchend",l),dataBind(a)[p]=null,"function"==typeof c.events.destroy&&c.events.destroy(a,b),this}},b.prototype.plugins={},b.prototype.defaults={interactive:!0,mouseupMovements:!1,relTimestamps:!1,multitouch:!0,cssCursors:!0,events:{},graphics:{firstPointSize:3,lineWidth:3,strokeStyle:"#F0F",fillStyle:"#F0F",lineCap:"round",lineJoin:"round",miterLimit:10}},a.Sketchable=b}(this); \ No newline at end of file diff --git a/figs/res-demo-g3.png b/figs/res-demo-g3.png new file mode 100644 index 0000000000000000000000000000000000000000..39d451791d067dc96e364105229960b8b7ebdc6c GIT binary patch literal 6782 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 literal 0 HcmV?d00001 diff --git a/figs/res-demo-mucaptcha.png b/figs/res-demo-mucaptcha.png new file mode 100644 index 0000000000000000000000000000000000000000..c981a0323bd4fe5179fb4e11e7d924466c5b998b GIT binary patch literal 13233 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 Wzz#X4Q4YWkASF5Vx7D)dpZ_0azz7Hc literal 0 HcmV?d00001 diff --git a/figs/res-demo-slm.png b/figs/res-demo-slm.png new file mode 100644 index 0000000000000000000000000000000000000000..09f27aacc7a3218d61d2c302a12b03aab81e04ae GIT binary patch literal 12606 zcmV-EF~QD>P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 jQuery library for full details. + * @method $ + * @description jQuery constructor. See {@link https://jquery.com} + * @param {String} selector - jQuery selector. + * @return {Object} jQuery */ /** - * @name $.fn - * @memberof $ - * @description This just documents the method that is added to jQuery by this plugin. - * See the jQuery library for full details. + * @namespace $.fn + * @description jQuery prototype. See {@link https://learn.jquery.com/plugins/} */ -;(function($){ - // Custom namespace ID. - var _ns = 'sketchable'; - /** - * jQuery sketchable plugin API. - * @namespace methods - */ + +/* eslint-env browser */ +;(function($) { + + // Custom namespace ID, for private data bindind. + var namespace = 'sketchable'; + + // Begin jQuery Sketchable plugin API. var methods = { /** - * Initializes the selected jQuery objects. - * @param {Object} opts - Plugin configuration (see defaults). + * Initialize the selected jQuery objects. + * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return jQuery + * @memberof $.fn.sketchable * @ignore - * @namespace methods.init - * @example $(selector).sketchable(); + * @protected */ init: function(opts) { - // Options will be available for all plugin methods. var options = $.extend(true, {}, $.fn.sketchable.defaults, opts || {}); + return this.each(function() { - var elem = $(this), data = elem.data(_ns); + var elem = $(this), data = elem.data(namespace); // Check if element is not initialized yet. if (!data) { // Attach event listeners. - if (options.interactive) { - elem.bind('mousedown', mousedownHandler); - elem.bind('mousemove', mousemoveHandler); - elem.bind('mouseup', mouseupHandler); - elem.bind('touchstart', touchdownHandler); - elem.bind('touchmove', touchmoveHandler); - elem.bind('touchend', touchupHandler); - // Fix Chrome "bug". - this.onselectstart = function(){ return false }; - } + elem.bind('mousedown', mousedownHandler); + elem.bind('mousemove', mousemoveHandler); + elem.bind('mouseup', mouseupHandler); + elem.bind('touchstart', touchdownHandler); + elem.bind('touchmove', touchmoveHandler); + elem.bind('touchend', touchupHandler); + // Fix unwanted highlight "bug". Note: `this` is the actual DOM element. + this.onselectstart = function() { return false }; postProcess(elem, options); } + var sketch = new jSketch(this, options.graphics); // Reconfigure element data. - elem.data(_ns, { + elem.data(namespace, { // All strokes will be stored here. strokes: [], // This will store one stroke per touching finger. @@ -65,76 +63,93 @@ // Save also a pointer to the given options. options: options }); + // Trigger init event. - if (typeof options.events.init === 'function') { - options.events.init(elem, elem.data(_ns)); + if (options.events && typeof options.events.init === 'function') { + options.events.init(elem, elem.data(namespace)); + } + // Initialize plugins. + for (var name in $.fn.sketchable.plugins) { + $.fn.sketchable.plugins[name](elem); } }); }, /** - * Changes config on the fly of an existing sketchable element. - * Previous options are retained. To completely reconfigure them just use the reset method. - * @param {Object} opts - Plugin configuration (see defaults). + * Change configuration of an existing jQuery Sketchable element. + * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return jQuery - * @namespace methods.config + * @memberof $.fn.sketchable * @example - * $(selector).sketchable('config', { interactive: false }); // Later on: - * $(selector).sketchable('config', { interactive: true }); + * var $canvas = $('canvas').sketchable('config', { interactive: false }); + * // Update later on: + * $canvas.sketchable('config', { interactive: true }); */ config: function(opts) { - return this.each(function(){ - var elem = $(this), data = elem.data(_ns); - data.options = $.extend(true, {}, $.fn.sketchable.defaults, data.options, opts || {}); - postProcess(elem); - }); + if (opts) { // setter + return this.each(function() { + var elem = $(this), data = elem.data(namespace); + data.options = $.extend(true, {}, $.fn.sketchable.defaults, data.options, opts); + postProcess(elem); + }); + } else { // getter + return $(this).data(namespace); + } }, /** - * Gets/Sets drawing data strokes sequence. - * @param {Array} arr - Multidimensional array of [x,y,time,status] tuples; status = 0 (pen down) or 1 (pen up). - * @return Strokes object on get, jQuery on set (with the new data attached) - * @namespace methods.strokes + * Get/Set drawing data strokes sequence. + * @param {Array} [arr] - Multidimensional array of [x,y,time,status] tuples; status = 0 (pen down) or 1 (pen up). + * @return Strokes object on get, jQuery instance on set (with the new data attached). + * @memberof $.fn.sketchable * @example - * $(selector).sketchable('strokes'); // Getter - * $(selector).sketchable('strokes', [ [arr1], ..., [arrN] ]); // Setter + * // Getter: read associated strokes. + * $('canvas').sketchable('strokes'); + * // Setter: replace associated strokes. + * $('canvas').sketchable('strokes', [ [arr1], ..., [arrN] ]); */ strokes: function(arr) { if (arr) { // setter return this.each(function() { - var elem = $(this), data = elem.data(_ns); + var elem = $(this), data = elem.data(namespace); data.strokes = arr; }); } else { // getter - var data = $(this).data(_ns); + var data = $(this).data(namespace); return data.strokes; } }, /** - * Allows low-level manipulation of the sketchable canvas. - * @param {Function} callback - Callback function, invoked with 2 arguments: elem (jQuery element) and data (jQuery element data). + * Allow low-level manipulation of the sketchable canvas. + * @param {Function} callback - Callback function, invoked with 2 arguments: elem (CANVAS element) and data (private element data). * @return jQuery - * @namespace methods.handler + * @memberof $.fn.sketchable * @example - * $(selector).sketchable('handler', function(elem, data){ + * $('canvas').sketchable('handler', function(elem, data) { * // do something with elem or data * }); */ handler: function(callback) { return this.each(function() { - var elem = $(this), data = elem.data(_ns); + var elem = $(this), data = elem.data(namespace); callback(elem, data); }); }, /** - * Clears canvas (together with strokes data). - * If you need to clear canvas only, just invoke data.sketch.clear() via $(selector).sketchable('handler'). - * @see methods.handler + * Clears canvas together with associated strokes data. * @return jQuery - * @namespace methods.clear - * @example $(selector).sketchable('clear'); + * @memberof $.fn.sketchable + * @see $.fn.sketchable.handler + * @example + * var $canvas = $('canvas').sketchable(); + * // This will remove strokes data as well. + * $canvas.clear(); + * // If you only need to clear the canvas, just do: + * $canvas.sketchable('handler', function(elem, data) { + * data.sketch.clear(); + * }); */ clear: function() { return this.each(function() { - var elem = $(this), data = elem.data(_ns) || {}, options = data.options; + var elem = $(this), data = elem.data(namespace) || {}, options = data.options; if (data.sketch) { data.sketch.clear(); data.strokes = []; @@ -146,17 +161,21 @@ }); }, /** - * Reinitializes a sketchable canvas with given opts. - * @param {Object} opts - Plugin configuration (see defaults). + * Reinitialize a sketchable canvas with given configuration options. + * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}). * @return jQuery - * @namespace methods.reset + * @memberof $.fn.sketchable * @example - * $(selector).sketchable('reset'); - * $(selector).sketchable('reset', {interactive:false}); + * var $canvas = $('canvas').sketchable(); + * // Reset default state. + * $canvas.sketchable('reset'); + * // Reset with custom configuration. + * $canvas.sketchable('reset', { interactive:false }); */ reset: function(opts) { - return this.each(function(){ - var elem = $(this), data = elem.data(_ns) || {}, options = data.options; + return this.each(function() { + var elem = $(this), data = elem.data(namespace) || {}, options = data.options; + elem.sketchable('destroy').sketchable(opts); if (options && typeof options.events.reset === 'function') { @@ -165,83 +184,115 @@ }); }, /** - * Destroys sketchable canvas (together with strokes data and events). + * Destroy sketchable canvas, together with strokes data and associated events. * @return jQuery - * @namespace methods.destroy - * @example $(selector).sketchable('destroy'); + * @memberof $.fn.sketchable + * @example + * var $canvas = $('canvas').sketchable(); + * // This will leave the canvas element intact. + * $canvas.sketchable('destroy'); */ destroy: function() { - return this.each(function(){ - var elem = $(this), data = elem.data(_ns) || {}, options = data.options; - if (options.interactive) { - elem.unbind('mouseup', mouseupHandler); - elem.unbind('mousemove', mousemoveHandler); - elem.unbind('mousedown', mousedownHandler); - elem.unbind('touchstart', touchdownHandler); - elem.unbind('touchmove', touchmoveHandler); - elem.unbind('touchend', touchupHandler); - } - elem.removeData(_ns); + return this.each(function() { + var elem = $(this), data = elem.data(namespace) || {}, options = data.options; + + elem.unbind('mouseup', mouseupHandler); + elem.unbind('mousemove', mousemoveHandler); + elem.unbind('mousedown', mousedownHandler); + elem.unbind('touchstart', touchdownHandler); + elem.unbind('touchmove', touchmoveHandler); + elem.unbind('touchend', touchupHandler); + + elem.removeData(namespace); if (options && typeof options.events.destroy === 'function') { options.events.destroy(elem, data); } }); } - }; /** - * Creates a jQuery.sketchable instance. - * This is a jQuery plugin for the jSketch drawing class. + * Create a jQuery Sketchable instance. + * This is a jQuery wrapper for the jSketch drawing class. + * @namespace $.fn.sketchable * @param {String|Object} method - Method to invoke, or a configuration object. * @return jQuery - * @class - * @version 1.8.1 - * @date 28 Nov 2016 + * @version 1.9 * @author Luis A. Leiva * @license MIT license * @example - * $(selector).sketchable(); - * $(selector).sketchable({interactive:false}); - * @see methods + * $('canvas').sketchable(); + * $('canvas').sketchable({ interactive:false }); */ $.fn.sketchable = function(method) { - // These "magic" keywords return internal plugin methods, - // so that they can be easily extended/overriden. - if ('methods functions hooks'.split(' ').indexOf(method) > -1) { - return methods; + if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); } else if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); } else { - $.error('Method '+ method +' does not exist. See jQuery.sketchable("methods").'); + $.error('Unknown method: ' + method); } return this; }; /** - * Default configuration. - * Note that mouse* callbacks are triggered only if interactive is set to true. - * @name defaults - * @default - * @memberof $.fn.sketchable + * Public API. Provides access to all methods of jQuery Sketchable instances.
+ * Note: This is equivalent to accessing `Sketchable.prototype` in the non-jQuery version. + * @namespace $.fn.sketchable.api + * @type {Object} + * @see Sketchable.prototype + */ + $.fn.sketchable.api = methods; + + /** + * Plugins store. + * @namespace $.fn.sketchable.plugins + * @type {Object} * @example - * $(selector).sketchable({ + * // All plugins are created after instance initialization: + * $.fn.sketchable.plugins['your-awesome-plugin'] = function($instance) { + * // Do something with the jQuery Sketchable instance. + * } + */ + $.fn.sketchable.plugins = {}; + + /** + * Default configuration. + * Note that `events.mouse*` callbacks are triggered only if interactive is set to true. + * @namespace $.fn.sketchable.defaults + * @type {Object} + * @example + * // The following is the default configuration: + * new Sketchable('canvas', { * interactive: true, * mouseupMovements: false, * relTimestamps: false, - * multitouch: true, + * multitouch: false, * cssCursors: true, + * // Event hooks. * events: { - * init: function(elem, data){ }, - * clear: function(elem, data){ }, - * destroy: function(elem, data){ }, - * mousedown: function(elem, data, evt){ }, - * mousemove: function(elem, data, evt){ }, - * mouseup: function(elem, data, evt){ }, + * init: function(elem, data) { + * // Called when the Sketchable instance is created. + * }, + * destroy: function(elem, data) { + * // Called when the Sketchable instance is destroyed. + * }, + * clear: function(elem, data) { + * // Called when the canvas is cleared. + * // This event includes clearing strokes data, too. + * }, + * mousedown: function(elem, data, evt) { + * // Called when the user clicks or taps on the canvas. + * }, + * mousemove: function(elem, data, evt) { + * // Called when the user moves the mouse or finger over the canvas. + * }, + * mouseup: function(elem, data, evt) { + * // Called when the user lifts the mouse or finger off the canvas. + * }, * }, + * // Drawing options, to be used in jSketch lib. * graphics: { * firstPointSize: 3, * lineWidth: 3, @@ -265,15 +316,16 @@ multitouch: true, // Display CSS cursors, mainly to indicate whether the element is interactive or not. cssCursors: true, - // Event callbacks. + // Event hooks. events: { - // init: function(elem, data){ }, - // clear: function(elem, data){ }, - // destroy: function(elem, data){ }, - // mousedown: function(elem, data, evt){ }, - // mousemove: function(elem, data, evt){ }, - // mouseup: function(elem, data, evt){ }, + // init: function(elem, data) { }, + // clear: function(elem, data) { }, + // destroy: function(elem, data) { }, + // mousedown: function(elem, data, evt) { }, + // mousemove: function(elem, data, evt) { }, + // mouseup: function(elem, data, evt) { }, }, + // Drawing options, to be used in jSketch lib. graphics: { firstPointSize: 3, lineWidth: 3, @@ -289,7 +341,7 @@ * @private */ function postProcess(elem, options) { - if (!options) options = elem.data(_ns).options; + if (!options) options = elem.data(namespace).options; if (options.cssCursors) { // Visually indicate whether this element is interactive or not. elem[0].style.cursor = options.interactive ? 'pointer' : 'not-allowed'; @@ -351,9 +403,12 @@ upHandler(e); }; + /** + * @private + */ function execTouchEvent(e, callback) { - var elem = $(e.target), data = elem.data(_ns), options = data.options; - var touches = e.originalEvent.changedTouches; + var elem = $(e.target), data = elem.data(namespace), options = data.options; + var touches = e.originalEvent.touches; if (options.multitouch) { for (var i = 0; i < touches.length; i++) { var touch = touches[i]; @@ -402,7 +457,7 @@ var idx = e.identifier || 0, elem = $(e.target), - data = elem.data(_ns), + data = elem.data(namespace), options = data.options; // Exit early if interactivity is disabled. if (!options.interactive) return; @@ -433,11 +488,9 @@ * @private */ function moveHandler(e) { - var idx = e.identifier || 0, - elem = $(e.target), - data = elem.data(_ns), - options = data.options; - // Exit early if interactivity is disabled. + var idx = e.identifier || 0; + var elem = $(e.target), data = elem.data(namespace), options = data.options; + if (!options.interactive) return; //if (!options.mouseupMovements && !data.sketch.isDrawing) return; @@ -460,11 +513,9 @@ * @private */ function upHandler(e) { - var idx = e.identifier || 0, - elem = $(e.target), - data = elem.data(_ns), - options = data.options; - // Exit early if interactivity is disabled. + var idx = e.identifier || 0; + var elem = $(e.target), data = elem.data(namespace), options = data.options; + if (!options.interactive) return; data.sketch.isDrawing = false; diff --git a/jquery.sketchable.memento.js b/jquery.sketchable.memento.js index 9e4392e..61e0ea1 100644 --- a/jquery.sketchable.memento.js +++ b/jquery.sketchable.memento.js @@ -1,38 +1,31 @@ /*! - * Memento plugin for jQuery sketchable | v1.2 | Luis A. Leiva | MIT license - */ -/** - * @name $ - * @class - * See the jQuery library for full details. - * This just documents the method that is added to jQuery by this plugin. - */ -/** - * @name $.fn - * @class - * See the jQuery library for full details. - * This just documents the method that is added to jQuery by this plugin. + * Memento plugin for jQuery Sketchable | v2.0 | Luis A. Leiva | MIT license */ + +/* eslint-env browser */ ;(function($) { - /** - * This plugin implements the Memento pattern. - * This plugin automatically modifies the jSketch instances, so no need to configure it. - * @name MementoCanvas - * @class - * @version 1.2 - * @date 28 Nov 2016 - * @return Object - * @example - * var mc = new MementoCanvas( $('canvas-selector') ); - */ - var MementoCanvas = function($canvas) { + // Custom namespace ID, for private data bindind. + var namespace = 'sketchable'; - // Private stuff ////////////////////////////////////////////////////////// + /** + * This class implements the Memento pattern + * and is part of the {@link $.fn.sketchable.plugins.memento} plugin. + * @class + * @version 2.0 + * @example + * var sketcher = $('canvas').sketchable(); + * // This is internally done by the plugin, plus some checks: + * new MementoCanvas(sketcher); + */ + function MementoCanvas($instance) { + // Begin private stuff. var stack = []; var stpos = -1; var self = this; - + /** + * @private + */ function prev() { if (stpos > 0) { stpos--; @@ -43,7 +36,9 @@ }; } }; - + /** + * @private + */ function next() { if (stpos < stack.length - 1) { stpos++; @@ -54,11 +49,15 @@ }; } }; - + /** + * Snashot restorer. + * @param {String} snapshot Base64 image. + * @private + */ function restore(snapshot) { // Manipulate canvas via jQuery sketchable API. // This way, we don't lose default drawing settings et al. - $canvas.sketchable('handler', function(elem, data){ + $instance.sketchable('handler', function(elem, data){ //data.sketch.clear().drawImage(snapshot.src); // Note: jSketch.drawImage after clear creates some flickering, // so use the native HTMLCanvasElement.drawImage method instead. @@ -66,11 +65,14 @@ data.sketch.graphics.drawImage(snapshot, 0,0); }); }; - - // Key event manager. - // Undo: "Ctrl + Z" - // Redo: "Ctrl + Y" or "Ctrl + Shift + Z" - // TODO: decouple shortcut definition, perhaps via jquery.hotkeys plugin. + /** + * Key event manager. + * - Undo: "Ctrl + Z" + * - Redo: "Ctrl + Y" or "Ctrl + Shift + Z" + * @param {Object} e DOM event. + * @private + * @todo Decouple shortcut definition, perhaps via jquery.hotkeys plugin. + */ function keyManager(e) { if (e.ctrlKey) { switch (e.which) { @@ -87,93 +89,82 @@ } }; - // Public stuff /////////////////////////////////////////////////////////// - /** * Goes back to the last saved state, if available. - * @name undo - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.undo = function() { prev(); - $canvas.sketchable('handler', function(elem, data) { + $instance.sketchable('handler', function(elem, data) { if (stack[stpos]) data.strokes = stack[stpos].strokes.slice(); }); + return this; }; /** * Goes forward to the last saved state, if available. - * @name redo - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.redo = function() { next(); - $canvas.sketchable('handler', function(elem, data) { + $instance.sketchable('handler', function(elem, data) { if (stack[stpos]) data.strokes = stack[stpos].strokes.slice(); }); + return this; }; /** * Resets stack. - * @name reset - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.reset = function() { stack = []; stpos = -1; + return this; }; /** - * Save state. - * @name save - * @memberOf MementoCanvas + * Save current state. + * @return {MementoCanvas} Class instance. */ this.save = function() { stpos++; if (stpos < stack.length) stack.length = stpos; - $canvas.sketchable('handler', function(elem, data) { + $instance.sketchable('handler', function(elem, data) { stack.push({ image: elem[0].toDataURL(), strokes: data.strokes.slice() }); }); + return this; }; /** - * Init instance. - * @name init - * @memberOf MementoCanvas + * Init instance. Currently just (re)attach key event listeners. + * @return {MementoCanvas} Class instance. */ this.init = function() { $(document).off('keypress', keyManager); $(document).on('keypress', keyManager); + return this; }; /** - * Destroy instance. - * @name destroy - * @memberOf MementoCanvas + * Destroy instance: reset state and remove key event listeners. + * @return {MementoCanvas} Class instance. */ this.destroy = function() { $(document).off('keypress', keyManager); - this.reset(); + return this.reset(); }; }; - // Bind plugin extension //////////////////////////////////////////////////// - var namespace = 'sketchable'; - var plugin = $.fn.sketchable; - var availMethods = plugin('methods'); - - function configure(elem, opts) { - var options = $.extend(true, {}, plugin.defaults, opts); - // Actually this plugin is singleton, so exit early. - if (!options.interactive) return opts; + /** + * Memento plugin constructor for jQuery Sketchable instances. + * @param {Object} $instance - A jQuery Sketchable instance. + * @memberof $.fn.sketchable.plugins + */ + $.fn.sketchable.plugins.memento = function($instance) { + var config = $instance.sketchable('config'); var callbacks = { - init: function(elem, data) { - data.memento = new MementoCanvas(elem); - data.memento.save(); - data.memento.init(); - }, clear: function(elem, data) { data.memento.reset(); - data.memento.save(); }, mouseup: function(elem, data, evt) { data.memento.save(); @@ -185,62 +176,65 @@ // A helper function to override user-defined event listeners. function override(ev) { - if (options && options.events && typeof options.events[ev] === 'function') { - var fn = options.events[ev]; - options.events[ev] = function() { + // Flag event override so that it doesn't get fired more than once. + if (config.options.$$bound) return; + config.options.$$bound = true; + + if (config.options.events && typeof config.options.events[ev] === 'function') { + // User has defined this event, so wrap it. + var fn = config.options.events[ev]; + config.options.events[ev] = function() { // Exec original function first, then exec our callback. - var args = Array.prototype.slice.call(arguments, 0); - fn.apply(elem, args); - callbacks[ev].apply(elem, args); + fn.apply($instance, arguments); + callbacks[ev].apply($instance, arguments); } } else { - options.events[ev] = callbacks[ev]; + // User has not defined this event, so attach our callback. + config.options.events[ev] = callbacks[ev]; } }; - // Event order matters. - // Init must go first, since it's called when instantiating the plugin. - var events = 'init mouseup clear destroy'.split(' '); + // Note: the init event is used to create sketchable instances, + // therefore it should NOT be overriden. + var events = 'mouseup clear destroy'.split(' '); for (var i = 0; i < events.length; i++) { override(events[i]); } - // Expose public API for jquery.sketchable plugin. - $.extend(availMethods, { + // Expose public API: all sketchable instances will have these methods. + $.extend($.fn.sketchable.api, { + /** + * Goes back to the previous CANVAS state, if available. + * @memberof $.fn.sketchable + * @example $('canvas').sketchable('undo'); + */ undo: function() { var elem = $(this), data = elem.data(namespace); data.memento.undo(); }, + /** + * Goes forward to the previous CANVAS state, if available. + * @memberof $.fn.sketchable + * @example $('canvas').sketchable('redo'); + */ redo: function() { var elem = $(this), data = elem.data(namespace); data.memento.redo(); }, + /** + * Save a snapshot of the current CANVAS status. + * @memberof $.fn.sketchable + * @example $('canvas').sketchable('save'); + */ save: function() { var elem = $(this), data = elem.data(namespace); data.memento.save(); }, }); - return options; - }; - - /** - * Creates a new memento-capable jQuery.sketchable object. - * @param {String|Object} method name of the method to invoke, - * or a configuration object. - * @return jQuery - * @class - * @example - * $(selector).sketchable(); - * $(selector).sketchable({interactive:false}); - */ - var initfn = availMethods.init; - availMethods.init = function(opts) { - return this.each(function() { - var elem = $(this); - var conf = configure(elem, opts); - initfn.call(elem, conf); - }); + // Initialize plugin here. + config.memento = new MementoCanvas($instance); + config.memento.init().save(); }; })(jQuery); diff --git a/jsketch.js b/jsketch.js index e9334d9..4a012ac 100644 --- a/jsketch.js +++ b/jsketch.js @@ -2,6 +2,7 @@ * jSketch 0.9 | Luis A. Leiva | MIT license * A simple JavaScript library for drawing facilities on HTML5 canvas. */ + /** * A simple JavaScript library for drawing facilities on HTML5 canvas. * This class is mostly a wrapper for the HTML5 canvas API with some syntactic sugar, @@ -16,7 +17,7 @@ * var canvas2 = document.getElementById('bar'); * // Instantiate once, reuse everywhere. * var brush = new jSketch(canvas1).lineStyle('red').moveTo(50,50).lineTo(10,10).stroke(); - * // Actually, .moveTo(50,50).lineTo(10,10) can be just .line(50,50, 10,10) + * // Actually, `.moveTo(50,50).lineTo(10,10)` can be just `.line(50,50, 10,10)`. * // Switching between contexts removes the need of having to reinstantiate the jSketch class. * brush.context(canvas2).beginFill('#5F7').fillCircle(30,30,8).endFill(); */ @@ -110,7 +111,7 @@ }, /** * Sets the background color of canvas. - * @param {Number|String} color - An HTML color. + * @param {String} color - An HTML color. * @return jSketch * @memberof jSketch */ @@ -124,7 +125,7 @@ * Shortcut for setting the size + background color. * @param {Number} width - New canvas width. * @param {Number} height - New canvas width. - * @param {Number|String} bgcolor - An HTML color. + * @param {String} bgcolor - An HTML color. * @return jSketch * @memberof jSketch */ @@ -134,7 +135,7 @@ }, /** * Sets the fill color. - * @param {Number|String} color - An HTML color. + * @param {String} color - An HTML color. * @return jSketch * @memberof jSketch */ @@ -154,7 +155,7 @@ }, /** * Sets the line style. - * @param {Number|String} color - An HTML color. + * @param {String} color - An HTML color. * @param {Number} thickness - Line thickness. * @param {String} capStyle - Style of line cap. * @param {String} joinStyle - Style of line join. @@ -347,7 +348,7 @@ }, /** * Sets brush to eraser mode. - * @param {Number} [brushSize] - Brush size. + * @param {Number} [brushSize] - Brush size. Default: 15. * @return jSketch * @memberof jSketch */ @@ -359,7 +360,7 @@ }, /** * Sets brush to pencil mode. - * @param {Number} [brushSize] - Brush size. + * @param {Number} [brushSize] - Brush size. Default: 2. * @return jSketch * @memberof jSketch */ @@ -422,7 +423,7 @@ }, /** * Draws an image. - * @param {Number} src - Image source path. + * @param {String} src - Image source path. * @param {Number} [x] - Horizontal coordinate. * @param {Number} [y] - Vertical coordinate. * @return jSketch diff --git a/package.json b/package.json index 76f5e43..4bf5859 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "jsketch", - "version": "1.8.0", + "version": "2.0.0", "description": "jSketch drawing lib", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "dist": "grunt", + "docs": "jsdoc --debug -c jsdoc.json && cp -rn figs/ docs/ || echo [sudo] npm i -g jsdoc" }, "repository": { "type": "git", diff --git a/sketchable.js b/sketchable.js index cf8cccb..317fc39 100644 --- a/sketchable.js +++ b/sketchable.js @@ -1,88 +1,86 @@ /*! - * sketchable | v1.8 | Luis A. Leiva | MIT license + * Sketchable | v2.0 | Luis A. Leiva | MIT license * A plugin for the jSketch drawing library. */ -/* - Requires sketchable.utils.js to be loaded first. - globals: Event, dataBind, deepExtend. -*/ -;(function(window){ - // Custom namespace ID. - var _ns = 'sketchable'; - /** - * Creates a sketchable instance. - * This is a plugin for the jSketch drawing class. - * @param {String|Object} method - Method to invoke, or a configuration object. - * @return jSketchable - * @class - * @version 1.8 - * @date 9 Jul 2014 - * @author Luis A. Leiva - * @license MIT license - * @example - * var canvas = document.getElementById('foo'); - * var sketcher = new Sketchable(canvas, {interactive:false}); - * @see methods - */ - /** - * @constructor - * @param {Object} elem - MUST be a DOM element - * @param {Object} options - Configuration - */ - var jSketchable = function(elem, options) { - return new Sketchable(elem, options); - }; - var Sketchable = function(elem, options) { - // Although discouraged, we can instantiate the class without arguments. - if (!elem) return; +// XXX: Requires `sketchable.utils.js` to be loaded first. + +/* eslint-env browser */ +/* global Event, dataBind, deepExtend */ +;(function(window) { + + // Custom namespace ID, for private data bindind. + var namespace = 'sketchable'; + // Convenient shortcut. + var document = window.document; + + /** + * Initialize the plugin: make CANVAS elements drawable.
+ * Contrary to the jQuery version, only one element can be passed in at a time. + * @param {Object|Strig} elem - DOM element or selector. + * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}). + * @class + * @global + * @version 1.9 + * @author Luis A. Leiva + * @license MIT + * @example + * // Passing a DOM element: + * var sketcher1 = new Sketchable(document.getElementById('foo')); + * // Passing a selector: + * var sketcher2 = new Sketchable('#foo'); + * // With custom configuration: + * var sketcher2 = new Sketchable('#foo', { multitouch:false }); + * @see Sketchable#defaults + */ + function Sketchable(elem, options) { + if (!elem) throw new Error('Sketchable requires a DOM element.'); + if (typeof elem === 'string') elem = document.querySelector(elem); + // Save a pointer to the DOM element to emulate jQuery capability. this.elem = elem; - // We can pass default setup values. - if (typeof options === 'undefined') options = {}; - // Instantiate the class. + // Instance methods are chainable. return this.init(options); }; /** - * jSketchable methods (publicly extensible). - * @ignore - * @memberof jSketchable - * @see jSketchable + * Sketchable prototype. + * @namespace Sketchable.prototype + * @static */ - jSketchable.fn = Sketchable.prototype = { + Sketchable.prototype = { /** - * Initializes the selected objects. - * @param {Object} opts plugin configuration (see defaults). - * @return jSketchable + * Initialize the selected CANVAS elements. + * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}). + * @return Sketchable + * @memberof Sketchable + * @protected * @ignore - * @namespace methods.init - * @example $(selector).sketchable(); */ - init: function(opts) { + init: function(options) { // Options will be available for all plugin methods. - var options = deepExtend(jSketchable.fn.defaults, opts || {}); - var elem = this.elem, data = dataBind(elem)[_ns]; + var options = deepExtend({}, Sketchable.prototype.defaults, options || {}); + var elem = this.elem, data = dataBind(elem)[namespace]; // Check if element is not initialized yet. if (!data) { // Attach event listeners. - if (options.interactive) { - Event.add(elem, 'mousedown', mousedownHandler); - Event.add(elem, 'mousemove', mousemoveHandler); - Event.add(elem, 'mouseup', mouseupHandler); - Event.add(elem, 'touchstart', touchdownHandler); - Event.add(elem, 'touchmove', touchmoveHandler); - Event.add(elem, 'touchend', touchupHandler); - // Fix Chrome "bug". - this.onselectstart = function(){ return false }; - } + Event.add(elem, 'mousedown', mousedownHandler); + Event.add(elem, 'mousemove', mousemoveHandler); + Event.add(elem, 'mouseup', mouseupHandler); + Event.add(elem, 'touchstart', touchdownHandler); + Event.add(elem, 'touchmove', touchmoveHandler); + Event.add(elem, 'touchend', touchupHandler); + // Fix unwanted highlight "bug". + elem.onselectstart = function() { return false }; + if (options.cssCursors) { // Visually indicate whether this element is interactive or not. elem.style.cursor = options.interactive ? 'pointer' : 'not-allowed'; } } + var sketch = new jSketch(elem, options.graphics); // Reconfigure element data. - dataBind(elem)[_ns] = { + dataBind(elem)[namespace] = data = { // All strokes will be stored here. strokes: [], // This will store one stroke per touching finger. @@ -91,75 +89,97 @@ timestamp: (new Date).getTime(), // Save a pointer to the drawing canvas (jSketch instance). sketch: sketch, + // Save a pointer to the drawing canvas (jSketch instance). + sketchable: this, // Save also a pointer to the given options. options: options }; + // Trigger init event. if (typeof options.events.init === 'function') { - options.events.init(elem, dataBind(elem)[_ns]); + options.events.init(elem, data); + } + // Initialize plugins. + for (var name in this.plugins) { + this.plugins[name](this); } // Make methods chainable. return this; }, /** - * Changes config on the fly of an existing sketchable element. - * @param {Object} opts - Plugin configuration (see defaults). - * @return jQuery - * @namespace methods.config + * Change configuration of an existing Sketchable instance. + * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}). + * @return Sketchable + * @memberof Sketchable * @example - * $(selector).sketchable('config', { interactive: false }); // Later on: - * $(selector).sketchable('config', { interactive: true }); + * var sketcher = new Sketchable('canvas').config({ interactive: false }); + * // Update later on: + * sketcher.config({ interactive: true }); */ - config: function(opts) { - var elem = this.elem, data = dataBind(elem)[_ns]; - data.options = deepExtend(jSketchable.fn.defaults, opts || {}); - return this; + config: function(options) { + var elem = this.elem, data = dataBind(elem)[namespace]; + if (options) { // setter + data.options = deepExtend({}, Sketchable.prototype.defaults, options || {}); + return this; + } else { // getter + return data; + } }, /** - * Gets/Sets drawing data strokes sequence. - * @param {Array} arr - Multidimensional array of [x,y,time,status] tuples; status = 0 (pen down) or 1 (pen up). - * @return Strokes object on get, jSketchable on set (with the new data attached) - * @namespace methods.strokes + * Get/Set drawing data strokes sequence. + * @param {Array} [arr] - Multidimensional array of [x,y,time,status] tuples; status = 0 (pen down) or 1 (pen up). + * @return Strokes object on get, Sketchable instance on set (with the new data attached). + * @memberof Sketchable * @example - * $(selector).sketchable('strokes'); // Getter - * $(selector).sketchable('strokes', [ [arr1], ..., [arrN] ]); // Setter + * // Getter: read associated strokes. + * new Sketchable('canvas').strokes(); + * // Setter: replace associated strokes. + * new Sketchable('canvas').strokes([ [arr1], ..., [arrN] ]); */ strokes: function(arr) { var elem = this.elem; if (arr) { // setter - var data = dataBind(elem)[_ns]; + var data = dataBind(elem)[namespace]; data.strokes = arr; return this; } else { // getter - var data = dataBind(elem)[_ns]; + var data = dataBind(elem)[namespace]; return data.strokes; } }, /** * Allows low-level manipulation of the sketchable canvas. - * @param {Function} callback - Callback function, invoked with 2 arguments: elem (jSketchable element) and data (jSketchable element data). - * @return jSketchable - * @namespace methods.handler + * @param {Function} callback - Callback function, invoked with 2 arguments: elem (CANVAS element) and data (private element data). + * @return Sketchable + * @memberof Sketchable * @example - * $(selector).sketchable('handler', function(elem, data){ + * new Sketchable('canvas').handler(function(elem, data) { * // do something with elem or data * }); */ handler: function(callback) { - var elem = this.elem, data = dataBind(elem)[_ns]; + var elem = this.elem, data = dataBind(elem)[namespace]; + callback(elem, data); return this; }, /** - * Clears canvas (together with strokes data). - * If you need to clear canvas only, just invoke data.sketch.clear() via $(selector).sketchable('handler'). - * @see methods.handler - * @return jSketchable - * @namespace methods.clear - * @example $(selector).sketchable('clear'); + * Clears canvas together with associated strokes data. + * @see Sketchable.handler + * @return Sketchable + * @memberof Sketchable + * @example + * var sketcher = new Sketchable('canvas'); + * // This will remove strokes data as well. + * sketcher.clear(); + * // If you only need to clear the canvas, just do: + * sketcher.handler(function(elem, data) { + * data.sketch.clear(); + * }); */ clear: function() { - var elem = this.elem, data = dataBind(elem)[_ns], options = data.options; + var elem = this.elem, data = dataBind(elem)[namespace], options = data.options; + data.sketch.clear(); data.strokes = []; data.coords = {}; @@ -170,17 +190,20 @@ return this; }, /** - * Reinitializes a sketchable canvas with given opts. - * @param {Object} opts - Configuration options. - * @return jSketchable - * @namespace methods.reset + * Reinitializes a sketchable canvas with given configuration options. + * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}). + * @return Sketchable + * @memberof Sketchable * @example - * $(selector).sketchable('reset'); - * $(selector).sketchable('reset', {interactive:false}); + * // Reset default state. + * new Sketchable('canvas').reset(); + * // Reset with custom configuration. + * new Sketchable('canvas').reset({ interactive:false }); */ - reset: function(opts) { - var elem = this.elem, data = dataBind(elem)[_ns], options = data.options; - this.destroy().init(opts); + reset: function(options) { + var elem = this.elem, data = dataBind(elem)[namespace], options = data.options; + + this.destroy().init(options); if (typeof options.events.reset === 'function') { options.events.reset(elem, data); @@ -188,22 +211,24 @@ return this; }, /** - * Destroys sketchable canvas (together with strokes data and events). - * @return jSketchable - * @namespace methods.destroy - * @example $(selector).sketchable('destroy'); + * Destroys sketchable canvas, together with strokes data and associated events. + * @return Sketchable + * @memberof Sketchable + * @example + * // This will leave the canvas element intact. + * new Sketchable('canvas').destroy(); */ destroy: function() { - var elem = this.elem, data = dataBind(elem)[_ns], options = data.options; - if (options.interactive) { - Event.remove(elem, 'mouseup', mouseupHandler); - Event.remove(elem, 'mousemove', mousemoveHandler); - Event.remove(elem, 'mousedown', mousedownHandler); - Event.remove(elem, 'touchstart', touchdownHandler); - Event.remove(elem, 'touchmove', touchmoveHandler); - Event.remove(elem, 'touchend', touchupHandler); - } - dataBind(elem)[_ns] = null; + var elem = this.elem, data = dataBind(elem)[namespace], options = data.options; + + Event.remove(elem, 'mouseup', mouseupHandler); + Event.remove(elem, 'mousemove', mousemoveHandler); + Event.remove(elem, 'mousedown', mousedownHandler); + Event.remove(elem, 'touchstart', touchdownHandler); + Event.remove(elem, 'touchmove', touchmoveHandler); + Event.remove(elem, 'touchend', touchupHandler); + + dataBind(elem)[namespace] = null; if (typeof options.events.destroy === 'function') { options.events.destroy(elem, data); @@ -214,26 +239,55 @@ }; /** - * Default configuration. - * Note that mouse* callbacks are triggered only if interactive is set to true. - * @name defaults - * @default - * @memberof $.fn.sketchable + * Plugins store. + * @namespace Sketchable.prototype.plugins + * @type {Object} + * @static * @example - * $(selector).sketchable({ + * // All plugins are created after instance initialization: + * Sketchable.prototype.plugins['your-awesome-plugin'] = function(instance) { + * // Do something with the Sketchable instance. + * } + */ + Sketchable.prototype.plugins = {}; + + /** + * Default configuration. + * Note that `events.mouse*` callbacks are triggered only if interactive is set to true. + * @namespace Sketchable.prototype.defaults + * @type {Object} + * @static + * @example + * // The following is the default configuration: + * new Sketchable('canvas', { * interactive: true, * mouseupMovements: false, * relTimestamps: false, * multitouch: false, * cssCursors: true, + * // Event hooks. * events: { - * init: function(elem, data){ }, - * clear: function(elem, data){ }, - * destroy: function(elem, data){ }, - * mousedown: function(elem, data, evt){ }, - * mousemove: function(elem, data, evt){ }, - * mouseup: function(elem, data, evt){ }, + * init: function(elem, data) { + * // Called when the Sketchable instance is created. + * }, + * destroy: function(elem, data) { + * // Called when the Sketchable instance is destroyed. + * }, + * clear: function(elem, data) { + * // Called when the canvas is cleared. + * // This event includes clearing strokes data, too. + * }, + * mousedown: function(elem, data, evt) { + * // Called when the user clicks or taps on the canvas. + * }, + * mousemove: function(elem, data, evt) { + * // Called when the user moves the mouse or finger over the canvas. + * }, + * mouseup: function(elem, data, evt) { + * // Called when the user lifts the mouse or finger off the canvas. + * }, * }, + * // Drawing options, to be used in jSketch lib. * graphics: { * firstPointSize: 3, * lineWidth: 3, @@ -245,7 +299,7 @@ * } * }); */ - jSketchable.fn.defaults = { + Sketchable.prototype.defaults = { // In interactive mode, it's possible to draw via mouse/pen/touch input. interactive: true, // Indicate whether non-drawing strokes should be registered as well. @@ -257,15 +311,16 @@ multitouch: true, // Display CSS cursors, mainly to indicate whether the element is interactive or not. cssCursors: true, - // Event callbacks. + // Event hooks. events: { - // init: function(elem, data){ }, - // clear: function(elem, data){ }, - // destroy: function(elem, data){ }, - // mousedown: function(elem, data, evt){ }, - // mousemove: function(elem, data, evt){ }, - // mouseup: function(elem, data, evt){ }, + // init: function(elem, data) { }, + // clear: function(elem, data) { }, + // destroy: function(elem, data) { }, + // mousedown: function(elem, data, evt) { }, + // mousemove: function(elem, data, evt) { }, + // mouseup: function(elem, data, evt) { }, }, + // Drawing options, to be used in jSketch lib. graphics: { firstPointSize: 3, lineWidth: 3, @@ -277,6 +332,9 @@ } }; + /** + * @private + */ function offset(el) { var box = el.getBoundingClientRect(); var body = document.body; @@ -348,9 +406,12 @@ upHandler(e); }; + /** + * @private + */ function execTouchEvent(e, callback) { - var elem = e.target, data = dataBind(elem)[_ns], options = data.options; - var touches = e.changedTouches; + var elem = e.target, data = dataBind(elem)[namespace], options = data.options; + var touches = e.touches; if (options.multitouch) { for (var i = 0; i < touches.length; i++) { var touch = touches[i]; @@ -364,6 +425,7 @@ touch.type = e.type; callback(touch); } + e.preventDefault(); }; /** @@ -398,7 +460,7 @@ if (Event.isRightClick(e)) return false; var idx = e.identifier || 0; - var elem = e.target, data = dataBind(elem)[_ns], options = data.options; + var elem = e.target, data = dataBind(elem)[namespace], options = data.options; // Exit early if interactivity is disabled. if (!options.interactive) return; @@ -429,7 +491,8 @@ */ function moveHandler(e) { var idx = e.identifier || 0; - var elem = e.target, data = dataBind(elem)[_ns], options = data.options; + var elem = e.target, data = dataBind(elem)[namespace], options = data.options; + if (!options.interactive) return; //if (!options.mouseupMovements && !data.sketch.isDrawing) return; // This would grab all penup strokes AFTER drawing something on the canvas for the first time. @@ -452,7 +515,8 @@ */ function upHandler(e) { var idx = e.identifier || 0; - var elem = e.target, data = dataBind(elem)[_ns], options = data.options; + var elem = e.target, data = dataBind(elem)[namespace], options = data.options; + if (!options.interactive) return; data.sketch.isDrawing = false; @@ -465,6 +529,6 @@ }; // Expose. - window.Sketchable = jSketchable; + window.Sketchable = Sketchable; })(this); diff --git a/sketchable.memento.js b/sketchable.memento.js index 68d6641..774cb03 100644 --- a/sketchable.memento.js +++ b/sketchable.memento.js @@ -1,29 +1,34 @@ /*! - * Memento plugin for sketchable | v1.2 | Luis A. Leiva | MIT license + * Memento plugin for Sketchable | v2.0 | Luis A. Leiva | MIT license */ -/* - Requires sketchable.utils.js to be loaded first. - globals: Event, dataBind, deepExtend. -*/ + +// XXX: Requires `sketchable.utils.js` to be loaded first. + +/* eslint-env browser */ +/* global Event, dataBind, deepExtend */ ;(function(window) { - /** - * This plugin implements the Memento pattern. - * This plugin automatically modifies the jSketch instances, so no need to configure it. - * @name MementoCanvas - * @class - * @version 1.2 - * @return Object - * @example - * var mc = new MementoCanvas( $('canvas-selector') ); - */ - var MementoCanvas = function(sketchable) { + // Custom namespace ID, for private data bindind. + var namespace = 'sketchable'; - // Private stuff ////////////////////////////////////////////////////////// + /** + * This class implements the Memento pattern + * and is part of the {@link Sketchable.plugins.memento} plugin. + * @class + * @version 2.0 + * @example + * var sketcher = new Sketchable('canvas'); + * // This is internally done by the plugin, plus some checks: + * new MementoCanvas(sketcher); + */ + function MementoCanvas(instance) { + // Begin private stuff. var stack = []; var stpos = -1; var self = this; - + /** + * @private + */ function prev() { if (stpos > 0) { stpos--; @@ -34,7 +39,9 @@ }; } }; - + /** + * @private + */ function next() { if (stpos < stack.length - 1) { stpos++; @@ -45,11 +52,15 @@ }; } }; - + /** + * Snashot restorer. + * @param {String} snapshot Base64 image. + * @private + */ function restore(snapshot) { // Manipulate canvas via jQuery sketchable API. // This way, we don't lose default drawing settings et al. - sketchable.handler(function(elem, data){ + instance.handler(function(elem, data){ //data.sketch.clear().drawImage(snapshot.src); // Note: jSketch.drawImage after clear creates some flickering, // so use the native HTMLCanvasElement.drawImage method instead. @@ -57,11 +68,14 @@ data.sketch.graphics.drawImage(snapshot, 0,0); }); }; - - // Key event manager. - // Undo: "Ctrl + Z" - // Redo: "Ctrl + Y" or "Ctrl + Shift + Z" - // TODO: decouple shortcut definition. + /** + * Key event manager. + * - Undo: "Ctrl + Z" + * - Redo: "Ctrl + Y" or "Ctrl + Shift + Z" + * @param {Object} e DOM event. + * @private + * @todo Decouple shortcut definition. + */ function keyManager(e) { if (e.ctrlKey) { switch (e.which) { @@ -78,93 +92,82 @@ } }; - // Public stuff /////////////////////////////////////////////////////////// - /** * Goes back to the last saved state, if available. - * @name undo - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.undo = function() { prev(); - sketchable.handler(function(elem, data) { + instance.handler(function(elem, data) { if (stack[stpos]) data.strokes = stack[stpos].strokes.slice(); }); + return this; }; /** * Goes forward to the last saved state, if available. - * @name redo - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.redo = function() { next(); - sketchable.handler(function(elem, data) { + instance.handler(function(elem, data) { if (stack[stpos]) data.strokes = stack[stpos].strokes.slice(); }); + return this; }; /** * Resets stack. - * @name reset - * @memberOf MementoCanvas + * @return {MementoCanvas} Class instance. */ this.reset = function() { stack = []; stpos = -1; + return this; }; /** - * Save state. - * @name save - * @memberOf MementoCanvas + * Save current state. */ this.save = function() { stpos++; if (stpos < stack.length) stack.length = stpos; - sketchable.handler(function(elem, data) { + instance.handler(function(elem, data) { stack.push({ image: elem.toDataURL(), strokes: data.strokes.slice() }); }); + return this; }; /** - * Init instance. - * @name init - * @memberOf MementoCanvas + * Init instance. Currently just (re)attach key event listeners. + * @return {MementoCanvas} Class instance. */ this.init = function() { Event.remove(document, 'keypress', keyManager); Event.add(document, 'keypress', keyManager); + return this; }; /** - * Destroy instance. - * @name destroy - * @memberOf MementoCanvas + * Destroy instance: reset state and remove key event listeners. + * @return {MementoCanvas} Class instance. */ this.destroy = function() { Event.remove(document, 'keypress', keyManager); - this.reset(); + return this.reset(); }; }; - // Bind plugin extension //////////////////////////////////////////////////// - var namespace = 'sketchable'; - var availMethods = Sketchable.fn; - var defaults = Sketchable.fn.defaults; - - function configure(sketchable, opts) { - var options = deepExtend({}, defaults, opts); - // Actually this plugin is singleton, so exit early. - if (!options.interactive) return opts; + /** + * Memento plugin constructor for jQuery Sketchable instances. + * @param {Object} sketchable - An Sketchable instance. + * @memberof Sketchable#plugins + */ + Sketchable.prototype.plugins.memento = function(instance) { + // Access the instance configuration. + var config = instance.config(); var callbacks = { - init: function(elem, data) { - data.memento = new MementoCanvas(sketchable); - data.memento.save(); - data.memento.init(); - }, clear: function(elem, data) { data.memento.reset(); - data.memento.save(); }, mouseup: function(elem, data, evt) { data.memento.save(); @@ -176,60 +179,62 @@ // A helper function to override user-defined event listeners. function override(ev) { - if (options && options.events && typeof options.events[ev] === 'function') { - var fn = options.events[ev]; - options.events[ev] = function() { + // Flag event override so that it doesn't get fired more than once. + if (config.options.$$bound) return; + config.options.$$bound = true; + + if (config.options.events && typeof config.options.events[ev] === 'function') { + // User has defined this event, so wrap it. + var fn = config.options.events[ev]; + config.options.events[ev] = function() { // Exec original function first, then exec our callback. - var args = Array.prototype.slice.call(arguments, 0); - fn.apply(sketchable, args); - callbacks[ev].apply(sketchable, args); + fn.apply(instance, arguments); + callbacks[ev].apply(instance, arguments); } } else { - defaults.events[ev] = callbacks[ev]; + // User has not defined this event, so attach our callback. + config.options.events[ev] = callbacks[ev]; } }; - // Event order matters. - // Init must go first, since it's called when instantiating the plugin. - var events = 'init mouseup clear destroy'.split(' '); + // Note: the init event is used to create Sketchable instances, + // therefore it should NOT be overriden. + var events = 'mouseup clear destroy'.split(' '); for (var i = 0; i < events.length; i++) { override(events[i]); } - // Expose public API for sketchable plugin. - deepExtend(availMethods, { + // Expose public API: all Sketchable instances will have these methods. + deepExtend(instance, { + /** + * Goes back to the previous CANVAS state, if available. + * @memberof Sketchable + */ undo: function() { var elem = this.elem, data = dataBind(elem)[namespace]; data.memento.undo(); }, + /** + * Goes forward to the previous CANVAS state, if available. + * @memberof Sketchable + */ redo: function() { var elem = this.elem, data = dataBind(elem)[namespace]; data.memento.redo(); }, + /** + * Save a snapshot of the current CANVAS status. + * @memberof Sketchable + */ save: function() { var elem = this.elem, data = dataBind(elem)[namespace]; data.memento.save(); } }); - return options; - }; - - /** - * Creates a new memento-capable sketchable object. - * @param {String|Object} method name of the method to invoke, - * or a configuration object. - * @return Sketchable - * @class - * @example - * $(selector).sketchable(); - * $(selector).sketchable({interactive:false}); - */ - var initfn = availMethods.init; - availMethods.init = function(opts) { - // Here `this` is a Sketchable instance. - var conf = configure(this, opts); - return initfn.call(this, conf); + // Initialize plugin here. + config.memento = new MementoCanvas(instance); + config.memento.init().save(); }; })(this); diff --git a/sketchable.utils.js b/sketchable.utils.js index 688d640..56a7f2b 100644 --- a/sketchable.utils.js +++ b/sketchable.utils.js @@ -1,9 +1,5 @@ -/** - * Data binding lib. - */ (function(){ - var cache = [0], - expando = 'data' + +(new Date); + var cache = [0], expando = 'data' + +(new Date); function data(elem) { var cacheIndex = elem[expando], nextCacheIndex = cache.length; @@ -13,36 +9,79 @@ } return cache[cacheIndex]; }; + /** + * Add/Read private data to a DOM element. + * @global + * @method + * @param {Object} elem - DOM element to attach data to. + * @example + * var elem = document.getElementById('foo'); + * // Attach private data to element: + * dataBind(elem)['some-name'] = { value: 42 }; + * // Read private data from element: + * var dat = dataBind(elem)['some-name']; + */ window.dataBind = data; })(); /** * Event manager. + * @global + * @module Event */ var Event = { - + /** + * Add event to DOM element. + * @memberof module:Event + * @param {Object} elem - DOM element. + * @param {String} type - Event type. + * @param {Function} fn - Callback. + * @example + * Event.add(document.getElementById('foo'), 'click', function fooClick(evt) { + * // Element was clicked. + * }); + */ add: function(elem, type, fn) { if (!elem) return false; if (elem.addEventListener) { // W3C standard elem.addEventListener(type, fn, false); - } else if (elem.attachEvent) { // IE versions + } else if (elem.attachEvent) { // Old IE versions elem.attachEvent("on"+type, fn); } else { // Really old browser elem[type+fn] = function(){ fn(window.event); }; } }, - + /** + * Remove event from DOM element. + * @memberof module:Event + * @param {Object} elem - DOM element. + * @param {String} type - Event type. + * @param {Function} fn - Callback. + * @example + * // Assuming elemen had the `fooClick` function (see previous example): + * Event.remove(document.getElementById('foo'), 'click', fooClick); + */ remove: function(elem, type, fn) { if (!elem) return false; if (elem.removeEventListener) { // W3C standard elem.removeEventListener(type, fn, false); - } else if (elem.detachEvent) { // IE versions + } else if (elem.detachEvent) { // Old IE versions elem.detachEvent("on"+type, fn); } else { // Really old browser elem[type+fn] = null; } }, - + /** + * Determine if an event is a "right click" event. + * @memberof module:Event + * @param {Object} ev - DOM event. + * @return {Boolean} + * @example + * // Assume this function is a click event listener. + * function clickHandler(evt) { + * alert(Event.isRightClick(evt)); + * }); + */ isRightClick: function(ev) { if (!ev) ev = window.event; if (ev.which) return ev.which === 3; @@ -54,6 +93,19 @@ var Event = { /** * A handy method to (deep) extend an object. + * The input object is modified. + * @global + * @param {Object} myObj - Input object. + * @return {Object} + * @example + * var one = { foo:1, bar: { a:true, b:false } }; + * var two = { bar: { a:false } }; + * // In this case both `ext` and `one` will be the same object. + * var ext = deepExtend(one, two); + * // To create a fresh copy, pass in an empty object as first arg. + * var ext = deepExtend({}, one, two); + * // Now `ext` is `{ foo:1, bar: { a:false, b:false } }` + * // and `one` is left intact. */ var deepExtend = function(myObj) { myObj = myObj || {};