Version 2.0 🎉

This commit is contained in:
Luis Leiva 2017-11-12 17:10:45 +01:00
parent 02c59f02ef
commit bc4f2a1c74
22 changed files with 728 additions and 508 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules node_modules
.* .*
*~ *~
local
*.log
*.bak

21
LICENSE Normal file
View File

@ -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.

View File

@ -1,9 +1,36 @@
jsketch # jSketch
=======
A lightweight JavaScript library for drawing facilities on HTML5 canvas, A lightweight JavaScript library for drawing facilities on an HTML5 canvas.
conveniently wrapped as a jQuery plugin.
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 `<script src="dist/sketchable.full.min.js"></script>` to your page and just do:
```js
var sketcher = new Sketchable('canvas');
```
Add `<script src="dist/jquery.sketchable.full.min.js"></script>` 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.

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!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;l<k.length;l++)h(k[l]);e.isMementoReady=!0}return a.extend(f,{undo:function(){var b=a(this),c=b.data(d);c.memento.undo()},redo:function(){var b=a(this),c=b.data(d);c.memento.redo()},save:function(){var b=a(this),c=b.data(d);c.memento.save()}}),i}var c=function(b){function c(){if(h>0){h--;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function d(){if(h<g.length-1){h++;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function e(a){b.sketchable("handler",function(b,c){c.sketch.clear(),c.sketch.graphics.drawImage(a,0,0)})}function f(a){if(a.ctrlKey)switch(a.which){case 26:a.shiftKey?i.redo():i.undo();break;case 25:i.redo()}}var g=[],h=-1,i=this;this.undo=function(){c(),b.sketchable("handler",function(a,b){g[h]&&(b.strokes=g[h].strokes.slice())})},this.redo=function(){d(),b.sketchable("handler",function(a,b){g[h]&&(b.strokes=g[h].strokes.slice())})},this.reset=function(){g=[],h=-1},this.save=function(){h++,h<g.length&&(g.length=h),b.sketchable("handler",function(a,b){g.push({image:a[0].toDataURL(),strokes:b.strokes.slice()})})},this.init=function(){a(document).off("keypress",f),a(document).on("keypress",f)},this.destroy=function(){a(document).off("keypress",f),this.reset()}},d="sketchable",e=a.fn.sketchable,f=e("methods"),g=f.init;f.init=function(c){return this.each(function(){var d=a(this),e=b(d,c);g.call(d,e)})}}(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(h<g.length-1){h++;var a=new Image;a.src=g[h].image,a.onload=function(){e(this)}}}function e(a){b.sketchable("handler",function(b,c){c.sketch.clear(),c.sketch.graphics.drawImage(a,0,0)})}function f(a){if(a.ctrlKey)switch(a.which){case 26:a.shiftKey?i.redo():i.undo();break;case 25:i.redo()}}var g=[],h=-1,i=this;this.undo=function(){return c(),b.sketchable("handler",function(a,b){g[h]&&(b.strokes=g[h].strokes.slice())}),this},this.redo=function(){return d(),b.sketchable("handler",function(a,b){g[h]&&(b.strokes=g[h].strokes.slice())}),this},this.reset=function(){return g=[],h=-1,this},this.save=function(){return h++,h<g.length&&(g.length=h),b.sketchable("handler",function(a,b){g.push({image:a[0].toDataURL(),strokes:b.strokes.slice()})}),this},this.init=function(){return a(document).off("keypress",f),a(document).on("keypress",f),this},this.destroy=function(){return a(document).off("keypress",f),this.reset()}}var c="sketchable";a.fn.sketchable.plugins.memento=function(d){function e(a){if(!f.options.$$bound)if(f.options.$$bound=!0,f.options.events&&"function"==typeof f.options.events[a]){var b=f.options.events[a];f.options.events[a]=function(){b.apply(d,arguments),g[a].apply(d,arguments)}}else f.options.events[a]=g[a]}for(var f=d.sketchable("config"),g={clear:function(a,b){b.memento.reset()},mouseup:function(a,b,c){b.memento.save()},destroy:function(a,b){b.memento.destroy()}},h="mouseup clear destroy".split(" "),i=0;i<h.length;i++)e(h[i]);a.extend(a.fn.sketchable.api,{undo:function(){var b=a(this),d=b.data(c);d.memento.undo()},redo:function(){var b=a(this),d=b.data(c);d.memento.redo()},save:function(){var b=a(this),d=b.data(c);d.memento.save()}}),f.memento=new b(d),f.memento.init().save()}}(jQuery);

View File

@ -1 +1 @@
!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;h<g.length;h++){var i=g[h];i.type=b.type,c(i)}else{var i=g[0];i.type=b.type,c(i)}}function i(a){h(a,l),a.preventDefault()}function j(a){h(a,m),a.preventDefault()}function k(a){h(a,n),a.preventDefault()}function l(b){if(3===b.which)return!1;var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive){g.sketch.isDrawing=!0;var i=c(b);h.graphics.firstPointSize>0&&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(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;h<g.length;h++){var i=g[h];i.type=b.type,c(i)}else{var i=g[0];i.type=b.type,c(i)}}function i(a){h(a,l),a.preventDefault()}function j(a){h(a,m),a.preventDefault()}function k(a){h(a,n),a.preventDefault()}function l(b){if(3===b.which)return!1;var e=b.identifier||0,f=a(b.target),g=f.data(o),h=g.options;if(h.interactive){g.sketch.isDrawing=!0;var i=c(b);h.graphics.firstPointSize>0&&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);

2
dist/jsketch.min.js vendored
View File

@ -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); !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;g<e.length;g++){var h=e[i];f.addColorStop(i,h)}return this.beginFill(f).fillCircle(a,b,c).endFill(),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&&(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);

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!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;l<k.length;l++)g(k[l]);e.isMementoReady=!0}return deepExtend(e,{undo:function(){var a=this.elem,b=dataBind(a)[d];b.memento.undo()},redo:function(){var a=this.elem,b=dataBind(a)[d];b.memento.redo()},save:function(){var a=this.elem,b=dataBind(a)[d];b.memento.save()}}),h}var c=function(a){function b(){if(g>0){g--;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function c(){if(g<f.length-1){g++;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function d(b){a.handler(function(a,c){c.sketch.clear(),c.sketch.graphics.drawImage(b,0,0)})}function e(b){if(b.ctrlKey)switch(b.which){case 26:b.shiftKey?a.redo():a.undo();break;case 25:a.redo()}}var f=[],g=-1;this.undo=function(){b(),a.handler(function(a,b){f[g]&&(b.strokes=f[g].strokes.slice())})},this.redo=function(){c(),a.handler(function(a,b){f[g]&&(b.strokes=f[g].strokes.slice())})},this.reset=function(){f=[],g=-1},this.save=function(){g++,g<f.length&&(f.length=g),a.handler(function(a,b){f.push({image:a.toDataURL(),strokes:b.strokes.slice()})})},this.init=function(){Event.remove(document,"keypress",e),Event.add(document,"keypress",e)},this.destroy=function(){Event.remove(document,"keypress",e),this.reset()}},d="sketchable",e=Sketchable.fn,f=Sketchable.fn.defaults,g=e.init;e.init=function(a){var c=b(this,a);return g.call(this,c),this}}(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(g<f.length-1){g++;var a=new Image;a.src=f[g].image,a.onload=function(){d(this)}}}function d(b){a.handler(function(a,c){c.sketch.clear(),c.sketch.graphics.drawImage(b,0,0)})}function e(a){if(a.ctrlKey)switch(a.which){case 26:a.shiftKey?h.redo():h.undo();break;case 25:h.redo()}}var f=[],g=-1,h=this;this.undo=function(){return b(),a.handler(function(a,b){f[g]&&(b.strokes=f[g].strokes.slice())}),this},this.redo=function(){return c(),a.handler(function(a,b){f[g]&&(b.strokes=f[g].strokes.slice())}),this},this.reset=function(){return f=[],g=-1,this},this.save=function(){return g++,g<f.length&&(f.length=g),a.handler(function(a,b){f.push({image:a.toDataURL(),strokes:b.strokes.slice()})}),this},this.init=function(){return Event.remove(document,"keypress",e),Event.add(document,"keypress",e),this},this.destroy=function(){return Event.remove(document,"keypress",e),this.reset()}}var c="sketchable";Sketchable.prototype.plugins.memento=function(a){function d(b){if(!e.options.$$bound)if(e.options.$$bound=!0,e.options.events&&"function"==typeof e.options.events[b]){var c=e.options.events[b];e.options.events[b]=function(){c.apply(a,arguments),f[b].apply(a,arguments)}}else e.options.events[b]=f[b]}for(var e=a.config(),f={clear:function(a,b){b.memento.reset()},mouseup:function(a,b,c){b.memento.save()},destroy:function(a,b){b.memento.destroy()}},g="mouseup clear destroy".split(" "),h=0;h<g.length;h++)d(g[h]);deepExtend(a,{undo:function(){var a=this.elem,b=dataBind(a)[c];b.memento.undo()},redo:function(){var a=this.elem,b=dataBind(a)[c];b.memento.redo()},save:function(){var a=this.elem,b=dataBind(a)[c];b.memento.save()}}),e.memento=new b(a),e.memento.init().save()}}(this);

View File

@ -1 +1 @@
!function(a){function b(b){var c=b.getBoundingClientRect(),d=document.body,e=document.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 c(a){var c=a.target,d=b(c);return{x:Math.round(a.pageX-d.left),y:Math.round(a.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.touches?!1:void l(a)}function f(a){return a.touches?!1:void m(a)}function g(a){return a.touches?!1:void n(a)}function h(a,b){var c=a.target,d=dataBind(c)[o],e=d.options,f=a.changedTouches;if(e.multitouch)for(var g=0;g<f.length;g++){var h=f[g];h.type=a.type,b(h)}else{var h=f[0];h.type=a.type,b(h)}}function i(a){h(a,l),a.preventDefault()}function j(a){h(a,m),a.preventDefault()}function k(a){h(a,n),a.preventDefault()}function l(a){if(Event.isRightClick(a))return!1;var b=a.identifier||0,e=a.target,f=dataBind(e)[o],g=f.options;if(g.interactive){f.sketch.isDrawing=!0;var h=c(a);g.graphics.firstPointSize>0&&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){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;g<f.length;g++){var h=f[g];h.type=a.type,b(h)}else{var h=f[0];h.type=a.type,b(h)}a.preventDefault()}function j(a){i(a,m),a.preventDefault()}function k(a){i(a,n),a.preventDefault()}function l(a){i(a,o),a.preventDefault()}function m(a){if(Event.isRightClick(a))return!1;var b=a.identifier||0,c=a.target,f=dataBind(c)[p],g=f.options;if(g.interactive){f.sketch.isDrawing=!0;var h=d(a);g.graphics.firstPointSize>0&&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);

BIN
figs/res-demo-g3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
figs/res-demo-guessit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
figs/res-demo-mucaptcha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
figs/res-demo-slm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
figs/res-demo-smiley.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,59 +1,57 @@
/*! /*!
* jQuery sketchable | v1.8.1 | Luis A. Leiva | MIT license * jQuery sketchable | v2.0 | Luis A. Leiva | MIT license
* A jQuery plugin for the jSketch drawing library. * A jQuery plugin for the jSketch drawing library.
*/ */
/** /**
* @name $ * @method $
* @class * @description jQuery constructor. See {@link https://jquery.com}
* @ignore * @param {String} selector - jQuery selector.
* @description This just documents the method that is added to jQuery by this plugin. * @return {Object} jQuery
* See <a href="http://jquery.com/">the jQuery library</a> for full details.
*/ */
/** /**
* @name $.fn * @namespace $.fn
* @memberof $ * @description jQuery prototype. See {@link https://learn.jquery.com/plugins/}
* @description This just documents the method that is added to jQuery by this plugin.
* See <a href="http://jquery.com/">the jQuery library</a> for full details.
*/ */
;(function($){
// Custom namespace ID. /* eslint-env browser */
var _ns = 'sketchable'; ;(function($) {
/**
* jQuery sketchable plugin API. // Custom namespace ID, for private data bindind.
* @namespace methods var namespace = 'sketchable';
*/
// Begin jQuery Sketchable plugin API.
var methods = { var methods = {
/** /**
* Initializes the selected jQuery objects. * Initialize the selected jQuery objects.
* @param {Object} opts - Plugin configuration (see defaults). * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}).
* @return jQuery * @return jQuery
* @memberof $.fn.sketchable
* @ignore * @ignore
* @namespace methods.init * @protected
* @example $(selector).sketchable();
*/ */
init: function(opts) { init: function(opts) {
// Options will be available for all plugin methods.
var options = $.extend(true, {}, $.fn.sketchable.defaults, opts || {}); var options = $.extend(true, {}, $.fn.sketchable.defaults, opts || {});
return this.each(function() { 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. // Check if element is not initialized yet.
if (!data) { if (!data) {
// Attach event listeners. // Attach event listeners.
if (options.interactive) { elem.bind('mousedown', mousedownHandler);
elem.bind('mousedown', mousedownHandler); elem.bind('mousemove', mousemoveHandler);
elem.bind('mousemove', mousemoveHandler); elem.bind('mouseup', mouseupHandler);
elem.bind('mouseup', mouseupHandler); elem.bind('touchstart', touchdownHandler);
elem.bind('touchstart', touchdownHandler); elem.bind('touchmove', touchmoveHandler);
elem.bind('touchmove', touchmoveHandler); elem.bind('touchend', touchupHandler);
elem.bind('touchend', touchupHandler); // Fix unwanted highlight "bug". Note: `this` is the actual DOM element.
// Fix Chrome "bug". this.onselectstart = function() { return false };
this.onselectstart = function(){ return false };
}
postProcess(elem, options); postProcess(elem, options);
} }
var sketch = new jSketch(this, options.graphics); var sketch = new jSketch(this, options.graphics);
// Reconfigure element data. // Reconfigure element data.
elem.data(_ns, { elem.data(namespace, {
// All strokes will be stored here. // All strokes will be stored here.
strokes: [], strokes: [],
// This will store one stroke per touching finger. // This will store one stroke per touching finger.
@ -65,76 +63,93 @@
// Save also a pointer to the given options. // Save also a pointer to the given options.
options: options options: options
}); });
// Trigger init event. // Trigger init event.
if (typeof options.events.init === 'function') { if (options.events && typeof options.events.init === 'function') {
options.events.init(elem, elem.data(_ns)); 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. * Change configuration of an existing jQuery Sketchable element.
* Previous options are retained. To completely reconfigure them just use the reset method. * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}).
* @param {Object} opts - Plugin configuration (see defaults).
* @return jQuery * @return jQuery
* @namespace methods.config * @memberof $.fn.sketchable
* @example * @example
* $(selector).sketchable('config', { interactive: false }); // Later on: * var $canvas = $('canvas').sketchable('config', { interactive: false });
* $(selector).sketchable('config', { interactive: true }); * // Update later on:
* $canvas.sketchable('config', { interactive: true });
*/ */
config: function(opts) { config: function(opts) {
return this.each(function(){ if (opts) { // setter
var elem = $(this), data = elem.data(_ns); return this.each(function() {
data.options = $.extend(true, {}, $.fn.sketchable.defaults, data.options, opts || {}); var elem = $(this), data = elem.data(namespace);
postProcess(elem); data.options = $.extend(true, {}, $.fn.sketchable.defaults, data.options, opts);
}); postProcess(elem);
});
} else { // getter
return $(this).data(namespace);
}
}, },
/** /**
* Gets/Sets drawing data strokes sequence. * 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). * @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) * @return Strokes object on get, jQuery instance on set (with the new data attached).
* @namespace methods.strokes * @memberof $.fn.sketchable
* @example * @example
* $(selector).sketchable('strokes'); // Getter * // Getter: read associated strokes.
* $(selector).sketchable('strokes', [ [arr1], ..., [arrN] ]); // Setter * $('canvas').sketchable('strokes');
* // Setter: replace associated strokes.
* $('canvas').sketchable('strokes', [ [arr1], ..., [arrN] ]);
*/ */
strokes: function(arr) { strokes: function(arr) {
if (arr) { // setter if (arr) { // setter
return this.each(function() { return this.each(function() {
var elem = $(this), data = elem.data(_ns); var elem = $(this), data = elem.data(namespace);
data.strokes = arr; data.strokes = arr;
}); });
} else { // getter } else { // getter
var data = $(this).data(_ns); var data = $(this).data(namespace);
return data.strokes; return data.strokes;
} }
}, },
/** /**
* Allows low-level manipulation of the sketchable canvas. * Allow low-level manipulation of the sketchable canvas.
* @param {Function} callback - Callback function, invoked with 2 arguments: elem (jQuery element) and data (jQuery element data). * @param {Function} callback - Callback function, invoked with 2 arguments: elem (CANVAS element) and data (private element data).
* @return jQuery * @return jQuery
* @namespace methods.handler * @memberof $.fn.sketchable
* @example * @example
* $(selector).sketchable('handler', function(elem, data){ * $('canvas').sketchable('handler', function(elem, data) {
* // do something with elem or data * // do something with elem or data
* }); * });
*/ */
handler: function(callback) { handler: function(callback) {
return this.each(function() { return this.each(function() {
var elem = $(this), data = elem.data(_ns); var elem = $(this), data = elem.data(namespace);
callback(elem, data); callback(elem, data);
}); });
}, },
/** /**
* Clears canvas (together with strokes data). * Clears canvas <b>together with</b> associated strokes data.
* If you need to clear canvas only, just invoke <tt>data.sketch.clear()</tt> via <tt>$(selector).sketchable('handler')</tt>.
* @see methods.handler
* @return jQuery * @return jQuery
* @namespace methods.clear * @memberof $.fn.sketchable
* @example $(selector).sketchable('clear'); * @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() { clear: function() {
return this.each(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) { if (data.sketch) {
data.sketch.clear(); data.sketch.clear();
data.strokes = []; data.strokes = [];
@ -146,17 +161,21 @@
}); });
}, },
/** /**
* Reinitializes a sketchable canvas with given opts. * Reinitialize a sketchable canvas with given configuration options.
* @param {Object} opts - Plugin configuration (see defaults). * @param {Object} [options] - Configuration (default: {@link $.fn.sketchable.defaults}).
* @return jQuery * @return jQuery
* @namespace methods.reset * @memberof $.fn.sketchable
* @example * @example
* $(selector).sketchable('reset'); * var $canvas = $('canvas').sketchable();
* $(selector).sketchable('reset', {interactive:false}); * // Reset default state.
* $canvas.sketchable('reset');
* // Reset with custom configuration.
* $canvas.sketchable('reset', { interactive:false });
*/ */
reset: function(opts) { reset: function(opts) {
return this.each(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;
elem.sketchable('destroy').sketchable(opts); elem.sketchable('destroy').sketchable(opts);
if (options && typeof options.events.reset === 'function') { 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 * @return jQuery
* @namespace methods.destroy * @memberof $.fn.sketchable
* @example $(selector).sketchable('destroy'); * @example
* var $canvas = $('canvas').sketchable();
* // This will leave the canvas element intact.
* $canvas.sketchable('destroy');
*/ */
destroy: function() { destroy: function() {
return this.each(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 (options.interactive) {
elem.unbind('mouseup', mouseupHandler); elem.unbind('mouseup', mouseupHandler);
elem.unbind('mousemove', mousemoveHandler); elem.unbind('mousemove', mousemoveHandler);
elem.unbind('mousedown', mousedownHandler); elem.unbind('mousedown', mousedownHandler);
elem.unbind('touchstart', touchdownHandler); elem.unbind('touchstart', touchdownHandler);
elem.unbind('touchmove', touchmoveHandler); elem.unbind('touchmove', touchmoveHandler);
elem.unbind('touchend', touchupHandler); elem.unbind('touchend', touchupHandler);
}
elem.removeData(_ns); elem.removeData(namespace);
if (options && typeof options.events.destroy === 'function') { if (options && typeof options.events.destroy === 'function') {
options.events.destroy(elem, data); options.events.destroy(elem, data);
} }
}); });
} }
}; };
/** /**
* Creates a <tt>jQuery.sketchable</tt> instance. * Create a <tt>jQuery Sketchable</tt> instance.
* This is a jQuery plugin for the <tt>jSketch</tt> drawing class. * This is a jQuery wrapper for the <tt>jSketch</tt> drawing class.
* @namespace $.fn.sketchable
* @param {String|Object} method - Method to invoke, or a configuration object. * @param {String|Object} method - Method to invoke, or a configuration object.
* @return jQuery * @return jQuery
* @class * @version 1.9
* @version 1.8.1
* @date 28 Nov 2016
* @author Luis A. Leiva * @author Luis A. Leiva
* @license MIT license * @license MIT license
* @example * @example
* $(selector).sketchable(); * $('canvas').sketchable();
* $(selector).sketchable({interactive:false}); * $('canvas').sketchable({ interactive:false });
* @see methods
*/ */
$.fn.sketchable = function(method) { $.fn.sketchable = function(method) {
// These "magic" keywords return internal plugin methods, if (typeof method === 'object' || !method) {
// so that they can be easily extended/overriden. return methods.init.apply(this, arguments);
if ('methods functions hooks'.split(' ').indexOf(method) > -1) {
return methods;
} else if (methods[method]) { } else if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else { } else {
$.error('Method '+ method +' does not exist. See jQuery.sketchable("methods").'); $.error('Unknown method: ' + method);
} }
return this; return this;
}; };
/** /**
* Default configuration. * Public API. Provides access to all methods of jQuery Sketchable instances.<br>
* Note that mouse* callbacks are triggered only if <tt>interactive</tt> is set to <tt>true</tt>. * Note: This is equivalent to accessing `Sketchable.prototype` in the non-jQuery version.
* @name defaults * @namespace $.fn.sketchable.api
* @default * @type {Object}
* @memberof $.fn.sketchable * @see Sketchable.prototype
*/
$.fn.sketchable.api = methods;
/**
* Plugins store.
* @namespace $.fn.sketchable.plugins
* @type {Object}
* @example * @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 <tt>interactive</tt> is set to <tt>true</tt>.
* @namespace $.fn.sketchable.defaults
* @type {Object}
* @example
* // The following is the default configuration:
* new Sketchable('canvas', {
* interactive: true, * interactive: true,
* mouseupMovements: false, * mouseupMovements: false,
* relTimestamps: false, * relTimestamps: false,
* multitouch: true, * multitouch: false,
* cssCursors: true, * cssCursors: true,
* // Event hooks.
* events: { * events: {
* init: function(elem, data){ }, * init: function(elem, data) {
* clear: function(elem, data){ }, * // Called when the Sketchable instance is created.
* destroy: function(elem, data){ }, * },
* mousedown: function(elem, data, evt){ }, * destroy: function(elem, data) {
* mousemove: function(elem, data, evt){ }, * // Called when the Sketchable instance is destroyed.
* mouseup: function(elem, data, evt){ }, * },
* 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: { * graphics: {
* firstPointSize: 3, * firstPointSize: 3,
* lineWidth: 3, * lineWidth: 3,
@ -265,15 +316,16 @@
multitouch: true, multitouch: true,
// Display CSS cursors, mainly to indicate whether the element is interactive or not. // Display CSS cursors, mainly to indicate whether the element is interactive or not.
cssCursors: true, cssCursors: true,
// Event callbacks. // Event hooks.
events: { events: {
// init: function(elem, data){ }, // init: function(elem, data) { },
// clear: function(elem, data){ }, // clear: function(elem, data) { },
// destroy: function(elem, data){ }, // destroy: function(elem, data) { },
// mousedown: function(elem, data, evt){ }, // mousedown: function(elem, data, evt) { },
// mousemove: function(elem, data, evt){ }, // mousemove: function(elem, data, evt) { },
// mouseup: function(elem, data, evt){ }, // mouseup: function(elem, data, evt) { },
}, },
// Drawing options, to be used in jSketch lib.
graphics: { graphics: {
firstPointSize: 3, firstPointSize: 3,
lineWidth: 3, lineWidth: 3,
@ -289,7 +341,7 @@
* @private * @private
*/ */
function postProcess(elem, options) { function postProcess(elem, options) {
if (!options) options = elem.data(_ns).options; if (!options) options = elem.data(namespace).options;
if (options.cssCursors) { if (options.cssCursors) {
// Visually indicate whether this element is interactive or not. // Visually indicate whether this element is interactive or not.
elem[0].style.cursor = options.interactive ? 'pointer' : 'not-allowed'; elem[0].style.cursor = options.interactive ? 'pointer' : 'not-allowed';
@ -351,9 +403,12 @@
upHandler(e); upHandler(e);
}; };
/**
* @private
*/
function execTouchEvent(e, callback) { function execTouchEvent(e, callback) {
var elem = $(e.target), data = elem.data(_ns), options = data.options; var elem = $(e.target), data = elem.data(namespace), options = data.options;
var touches = e.originalEvent.changedTouches; var touches = e.originalEvent.touches;
if (options.multitouch) { if (options.multitouch) {
for (var i = 0; i < touches.length; i++) { for (var i = 0; i < touches.length; i++) {
var touch = touches[i]; var touch = touches[i];
@ -402,7 +457,7 @@
var idx = e.identifier || 0, var idx = e.identifier || 0,
elem = $(e.target), elem = $(e.target),
data = elem.data(_ns), data = elem.data(namespace),
options = data.options; options = data.options;
// Exit early if interactivity is disabled. // Exit early if interactivity is disabled.
if (!options.interactive) return; if (!options.interactive) return;
@ -433,11 +488,9 @@
* @private * @private
*/ */
function moveHandler(e) { function moveHandler(e) {
var idx = e.identifier || 0, var idx = e.identifier || 0;
elem = $(e.target), var elem = $(e.target), data = elem.data(namespace), options = data.options;
data = elem.data(_ns),
options = data.options;
// Exit early if interactivity is disabled.
if (!options.interactive) return; if (!options.interactive) return;
//if (!options.mouseupMovements && !data.sketch.isDrawing) return; //if (!options.mouseupMovements && !data.sketch.isDrawing) return;
@ -460,11 +513,9 @@
* @private * @private
*/ */
function upHandler(e) { function upHandler(e) {
var idx = e.identifier || 0, var idx = e.identifier || 0;
elem = $(e.target), var elem = $(e.target), data = elem.data(namespace), options = data.options;
data = elem.data(_ns),
options = data.options;
// Exit early if interactivity is disabled.
if (!options.interactive) return; if (!options.interactive) return;
data.sketch.isDrawing = false; data.sketch.isDrawing = false;

View File

@ -1,38 +1,31 @@
/*! /*!
* Memento plugin for jQuery sketchable | v1.2 | Luis A. Leiva | MIT license * Memento plugin for jQuery Sketchable | v2.0 | Luis A. Leiva | MIT license
*/
/**
* @name $
* @class
* See <a href="http://jquery.com/">the jQuery library</a> for full details.
* This just documents the method that is added to jQuery by this plugin.
*/
/**
* @name $.fn
* @class
* See <a href="http://jquery.com/">the jQuery library</a> for full details.
* This just documents the method that is added to jQuery by this plugin.
*/ */
/* eslint-env browser */
;(function($) { ;(function($) {
/** // Custom namespace ID, for private data bindind.
* This plugin implements the <a href="https://en.wikipedia.org/wiki/Memento_pattern">Memento pattern</a>. var namespace = 'sketchable';
* 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) {
// Private stuff ////////////////////////////////////////////////////////// /**
* This class implements the <a href="https://en.wikipedia.org/wiki/Memento_pattern">Memento pattern</a>
* 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 stack = [];
var stpos = -1; var stpos = -1;
var self = this; var self = this;
/**
* @private
*/
function prev() { function prev() {
if (stpos > 0) { if (stpos > 0) {
stpos--; stpos--;
@ -43,7 +36,9 @@
}; };
} }
}; };
/**
* @private
*/
function next() { function next() {
if (stpos < stack.length - 1) { if (stpos < stack.length - 1) {
stpos++; stpos++;
@ -54,11 +49,15 @@
}; };
} }
}; };
/**
* Snashot restorer.
* @param {String} snapshot Base64 image.
* @private
*/
function restore(snapshot) { function restore(snapshot) {
// Manipulate canvas via jQuery sketchable API. // Manipulate canvas via jQuery sketchable API.
// This way, we don't lose default drawing settings et al. // 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); //data.sketch.clear().drawImage(snapshot.src);
// Note: jSketch.drawImage after clear creates some flickering, // Note: jSketch.drawImage after clear creates some flickering,
// so use the native HTMLCanvasElement.drawImage method instead. // so use the native HTMLCanvasElement.drawImage method instead.
@ -66,11 +65,14 @@
data.sketch.graphics.drawImage(snapshot, 0,0); data.sketch.graphics.drawImage(snapshot, 0,0);
}); });
}; };
/**
// Key event manager. * Key event manager.
// Undo: "Ctrl + Z" * - Undo: "Ctrl + Z"
// Redo: "Ctrl + Y" or "Ctrl + Shift + Z" * - Redo: "Ctrl + Y" or "Ctrl + Shift + Z"
// TODO: decouple shortcut definition, perhaps via jquery.hotkeys plugin. * @param {Object} e DOM event.
* @private
* @todo Decouple shortcut definition, perhaps via jquery.hotkeys plugin.
*/
function keyManager(e) { function keyManager(e) {
if (e.ctrlKey) { if (e.ctrlKey) {
switch (e.which) { switch (e.which) {
@ -87,93 +89,82 @@
} }
}; };
// Public stuff ///////////////////////////////////////////////////////////
/** /**
* Goes back to the last saved state, if available. * Goes back to the last saved state, if available.
* @name undo * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.undo = function() { this.undo = function() {
prev(); prev();
$canvas.sketchable('handler', function(elem, data) { $instance.sketchable('handler', function(elem, data) {
if (stack[stpos]) if (stack[stpos])
data.strokes = stack[stpos].strokes.slice(); data.strokes = stack[stpos].strokes.slice();
}); });
return this;
}; };
/** /**
* Goes forward to the last saved state, if available. * Goes forward to the last saved state, if available.
* @name redo * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.redo = function() { this.redo = function() {
next(); next();
$canvas.sketchable('handler', function(elem, data) { $instance.sketchable('handler', function(elem, data) {
if (stack[stpos]) if (stack[stpos])
data.strokes = stack[stpos].strokes.slice(); data.strokes = stack[stpos].strokes.slice();
}); });
return this;
}; };
/** /**
* Resets stack. * Resets stack.
* @name reset * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.reset = function() { this.reset = function() {
stack = []; stack = [];
stpos = -1; stpos = -1;
return this;
}; };
/** /**
* Save state. * Save current state.
* @name save * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.save = function() { this.save = function() {
stpos++; stpos++;
if (stpos < stack.length) stack.length = 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() }); stack.push({ image: elem[0].toDataURL(), strokes: data.strokes.slice() });
}); });
return this;
}; };
/** /**
* Init instance. * Init instance. Currently just (re)attach key event listeners.
* @name init * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.init = function() { this.init = function() {
$(document).off('keypress', keyManager); $(document).off('keypress', keyManager);
$(document).on('keypress', keyManager); $(document).on('keypress', keyManager);
return this;
}; };
/** /**
* Destroy instance. * Destroy instance: reset state and remove key event listeners.
* @name destroy * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.destroy = function() { this.destroy = function() {
$(document).off('keypress', keyManager); $(document).off('keypress', keyManager);
this.reset(); return this.reset();
}; };
}; };
// Bind plugin extension //////////////////////////////////////////////////// /**
var namespace = 'sketchable'; * Memento plugin constructor for jQuery Sketchable instances.
var plugin = $.fn.sketchable; * @param {Object} $instance - A jQuery Sketchable instance.
var availMethods = plugin('methods'); * @memberof $.fn.sketchable.plugins
*/
function configure(elem, opts) { $.fn.sketchable.plugins.memento = function($instance) {
var options = $.extend(true, {}, plugin.defaults, opts); var config = $instance.sketchable('config');
// Actually this plugin is singleton, so exit early.
if (!options.interactive) return opts;
var callbacks = { var callbacks = {
init: function(elem, data) {
data.memento = new MementoCanvas(elem);
data.memento.save();
data.memento.init();
},
clear: function(elem, data) { clear: function(elem, data) {
data.memento.reset(); data.memento.reset();
data.memento.save();
}, },
mouseup: function(elem, data, evt) { mouseup: function(elem, data, evt) {
data.memento.save(); data.memento.save();
@ -185,62 +176,65 @@
// A helper function to override user-defined event listeners. // A helper function to override user-defined event listeners.
function override(ev) { function override(ev) {
if (options && options.events && typeof options.events[ev] === 'function') { // Flag event override so that it doesn't get fired more than once.
var fn = options.events[ev]; if (config.options.$$bound) return;
options.events[ev] = function() { 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. // Exec original function first, then exec our callback.
var args = Array.prototype.slice.call(arguments, 0); fn.apply($instance, arguments);
fn.apply(elem, args); callbacks[ev].apply($instance, arguments);
callbacks[ev].apply(elem, args);
} }
} else { } 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. // Note: the init event is used to create sketchable instances,
// Init must go first, since it's called when instantiating the plugin. // therefore it should NOT be overriden.
var events = 'init mouseup clear destroy'.split(' '); var events = 'mouseup clear destroy'.split(' ');
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
override(events[i]); override(events[i]);
} }
// Expose public API for jquery.sketchable plugin. // Expose public API: all sketchable instances will have these methods.
$.extend(availMethods, { $.extend($.fn.sketchable.api, {
/**
* Goes back to the previous CANVAS state, if available.
* @memberof $.fn.sketchable
* @example $('canvas').sketchable('undo');
*/
undo: function() { undo: function() {
var elem = $(this), data = elem.data(namespace); var elem = $(this), data = elem.data(namespace);
data.memento.undo(); data.memento.undo();
}, },
/**
* Goes forward to the previous CANVAS state, if available.
* @memberof $.fn.sketchable
* @example $('canvas').sketchable('redo');
*/
redo: function() { redo: function() {
var elem = $(this), data = elem.data(namespace); var elem = $(this), data = elem.data(namespace);
data.memento.redo(); data.memento.redo();
}, },
/**
* Save a snapshot of the current CANVAS status.
* @memberof $.fn.sketchable
* @example $('canvas').sketchable('save');
*/
save: function() { save: function() {
var elem = $(this), data = elem.data(namespace); var elem = $(this), data = elem.data(namespace);
data.memento.save(); data.memento.save();
}, },
}); });
return options; // Initialize plugin here.
}; config.memento = new MementoCanvas($instance);
config.memento.init().save();
/**
* 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);
});
}; };
})(jQuery); })(jQuery);

View File

@ -2,6 +2,7 @@
* jSketch 0.9 | Luis A. Leiva | MIT license * 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.
*/ */
/** /**
* 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, * This class is mostly a wrapper for the HTML5 canvas API with some syntactic sugar,
@ -16,7 +17,7 @@
* var canvas2 = document.getElementById('bar'); * var canvas2 = document.getElementById('bar');
* // Instantiate once, reuse everywhere. * // Instantiate once, reuse everywhere.
* var brush = new jSketch(canvas1).lineStyle('red').moveTo(50,50).lineTo(10,10).stroke(); * 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. * // Switching between contexts removes the need of having to reinstantiate the jSketch class.
* brush.context(canvas2).beginFill('#5F7').fillCircle(30,30,8).endFill(); * brush.context(canvas2).beginFill('#5F7').fillCircle(30,30,8).endFill();
*/ */
@ -110,7 +111,7 @@
}, },
/** /**
* Sets the background color of canvas. * Sets the background color of canvas.
* @param {Number|String} color - An HTML color. * @param {String} color - An HTML color.
* @return jSketch * @return jSketch
* @memberof jSketch * @memberof jSketch
*/ */
@ -124,7 +125,7 @@
* Shortcut for setting the size + background color. * Shortcut for setting the size + background color.
* @param {Number} width - New canvas width. * @param {Number} width - New canvas width.
* @param {Number} height - New canvas width. * @param {Number} height - New canvas width.
* @param {Number|String} bgcolor - An HTML color. * @param {String} bgcolor - An HTML color.
* @return jSketch * @return jSketch
* @memberof jSketch * @memberof jSketch
*/ */
@ -134,7 +135,7 @@
}, },
/** /**
* Sets the fill color. * Sets the fill color.
* @param {Number|String} color - An HTML color. * @param {String} color - An HTML color.
* @return jSketch * @return jSketch
* @memberof jSketch * @memberof jSketch
*/ */
@ -154,7 +155,7 @@
}, },
/** /**
* Sets the line style. * Sets the line style.
* @param {Number|String} color - An HTML color. * @param {String} color - An HTML color.
* @param {Number} thickness - Line thickness. * @param {Number} thickness - Line thickness.
* @param {String} capStyle - Style of line cap. * @param {String} capStyle - Style of line cap.
* @param {String} joinStyle - Style of line join. * @param {String} joinStyle - Style of line join.
@ -347,7 +348,7 @@
}, },
/** /**
* Sets brush to eraser mode. * Sets brush to eraser mode.
* @param {Number} [brushSize] - Brush size. * @param {Number} [brushSize] - Brush size. Default: 15.
* @return jSketch * @return jSketch
* @memberof jSketch * @memberof jSketch
*/ */
@ -359,7 +360,7 @@
}, },
/** /**
* Sets brush to pencil mode. * Sets brush to pencil mode.
* @param {Number} [brushSize] - Brush size. * @param {Number} [brushSize] - Brush size. Default: 2.
* @return jSketch * @return jSketch
* @memberof jSketch * @memberof jSketch
*/ */
@ -422,7 +423,7 @@
}, },
/** /**
* Draws an image. * Draws an image.
* @param {Number} src - Image source path. * @param {String} src - Image source path.
* @param {Number} [x] - Horizontal coordinate. * @param {Number} [x] - Horizontal coordinate.
* @param {Number} [y] - Vertical coordinate. * @param {Number} [y] - Vertical coordinate.
* @return jSketch * @return jSketch

View File

@ -1,9 +1,11 @@
{ {
"name": "jsketch", "name": "jsketch",
"version": "1.8.0", "version": "2.0.0",
"description": "jSketch drawing lib", "description": "jSketch drawing lib",
"scripts": { "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": { "repository": {
"type": "git", "type": "git",

View File

@ -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. * 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 <tt>sketchable</tt> instance.
* This is a plugin for the <tt>jSketch</tt> 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) { // XXX: Requires `sketchable.utils.js` to be loaded first.
// Although discouraged, we can instantiate the class without arguments.
if (!elem) return; /* 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.<br>
* 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; this.elem = elem;
// We can pass default setup values. // Instance methods are chainable.
if (typeof options === 'undefined') options = {};
// Instantiate the class.
return this.init(options); return this.init(options);
}; };
/** /**
* jSketchable methods (publicly extensible). * Sketchable prototype.
* @ignore * @namespace Sketchable.prototype
* @memberof jSketchable * @static
* @see jSketchable
*/ */
jSketchable.fn = Sketchable.prototype = { Sketchable.prototype = {
/** /**
* Initializes the selected objects. * Initialize the selected CANVAS elements.
* @param {Object} opts plugin configuration (see defaults). * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}).
* @return jSketchable * @return Sketchable
* @memberof Sketchable
* @protected
* @ignore * @ignore
* @namespace methods.init
* @example $(selector).sketchable();
*/ */
init: function(opts) { init: function(options) {
// Options will be available for all plugin methods. // Options will be available for all plugin methods.
var options = deepExtend(jSketchable.fn.defaults, opts || {}); var options = deepExtend({}, Sketchable.prototype.defaults, options || {});
var elem = this.elem, data = dataBind(elem)[_ns]; var elem = this.elem, data = dataBind(elem)[namespace];
// Check if element is not initialized yet. // Check if element is not initialized yet.
if (!data) { if (!data) {
// Attach event listeners. // Attach event listeners.
if (options.interactive) { Event.add(elem, 'mousedown', mousedownHandler);
Event.add(elem, 'mousedown', mousedownHandler); Event.add(elem, 'mousemove', mousemoveHandler);
Event.add(elem, 'mousemove', mousemoveHandler); Event.add(elem, 'mouseup', mouseupHandler);
Event.add(elem, 'mouseup', mouseupHandler); Event.add(elem, 'touchstart', touchdownHandler);
Event.add(elem, 'touchstart', touchdownHandler); Event.add(elem, 'touchmove', touchmoveHandler);
Event.add(elem, 'touchmove', touchmoveHandler); Event.add(elem, 'touchend', touchupHandler);
Event.add(elem, 'touchend', touchupHandler); // Fix unwanted highlight "bug".
// Fix Chrome "bug". elem.onselectstart = function() { return false };
this.onselectstart = function(){ return false };
}
if (options.cssCursors) { if (options.cssCursors) {
// Visually indicate whether this element is interactive or not. // Visually indicate whether this element is interactive or not.
elem.style.cursor = options.interactive ? 'pointer' : 'not-allowed'; elem.style.cursor = options.interactive ? 'pointer' : 'not-allowed';
} }
} }
var sketch = new jSketch(elem, options.graphics); var sketch = new jSketch(elem, options.graphics);
// Reconfigure element data. // Reconfigure element data.
dataBind(elem)[_ns] = { dataBind(elem)[namespace] = data = {
// All strokes will be stored here. // All strokes will be stored here.
strokes: [], strokes: [],
// This will store one stroke per touching finger. // This will store one stroke per touching finger.
@ -91,75 +89,97 @@
timestamp: (new Date).getTime(), timestamp: (new Date).getTime(),
// Save a pointer to the drawing canvas (jSketch instance). // Save a pointer to the drawing canvas (jSketch instance).
sketch: sketch, sketch: sketch,
// Save a pointer to the drawing canvas (jSketch instance).
sketchable: this,
// Save also a pointer to the given options. // Save also a pointer to the given options.
options: options options: options
}; };
// Trigger init event. // Trigger init event.
if (typeof options.events.init === 'function') { 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. // Make methods chainable.
return this; return this;
}, },
/** /**
* Changes config on the fly of an existing sketchable element. * Change configuration of an existing Sketchable instance.
* @param {Object} opts - Plugin configuration (see defaults). * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}).
* @return jQuery * @return Sketchable
* @namespace methods.config * @memberof Sketchable
* @example * @example
* $(selector).sketchable('config', { interactive: false }); // Later on: * var sketcher = new Sketchable('canvas').config({ interactive: false });
* $(selector).sketchable('config', { interactive: true }); * // Update later on:
* sketcher.config({ interactive: true });
*/ */
config: function(opts) { config: function(options) {
var elem = this.elem, data = dataBind(elem)[_ns]; var elem = this.elem, data = dataBind(elem)[namespace];
data.options = deepExtend(jSketchable.fn.defaults, opts || {}); if (options) { // setter
return this; data.options = deepExtend({}, Sketchable.prototype.defaults, options || {});
return this;
} else { // getter
return data;
}
}, },
/** /**
* Gets/Sets drawing data strokes sequence. * 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). * @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) * @return Strokes object on get, Sketchable instance on set (with the new data attached).
* @namespace methods.strokes * @memberof Sketchable
* @example * @example
* $(selector).sketchable('strokes'); // Getter * // Getter: read associated strokes.
* $(selector).sketchable('strokes', [ [arr1], ..., [arrN] ]); // Setter * new Sketchable('canvas').strokes();
* // Setter: replace associated strokes.
* new Sketchable('canvas').strokes([ [arr1], ..., [arrN] ]);
*/ */
strokes: function(arr) { strokes: function(arr) {
var elem = this.elem; var elem = this.elem;
if (arr) { // setter if (arr) { // setter
var data = dataBind(elem)[_ns]; var data = dataBind(elem)[namespace];
data.strokes = arr; data.strokes = arr;
return this; return this;
} else { // getter } else { // getter
var data = dataBind(elem)[_ns]; var data = dataBind(elem)[namespace];
return data.strokes; return data.strokes;
} }
}, },
/** /**
* Allows low-level manipulation of the sketchable canvas. * 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). * @param {Function} callback - Callback function, invoked with 2 arguments: elem (CANVAS element) and data (private element data).
* @return jSketchable * @return Sketchable
* @namespace methods.handler * @memberof Sketchable
* @example * @example
* $(selector).sketchable('handler', function(elem, data){ * new Sketchable('canvas').handler(function(elem, data) {
* // do something with elem or data * // do something with elem or data
* }); * });
*/ */
handler: function(callback) { handler: function(callback) {
var elem = this.elem, data = dataBind(elem)[_ns]; var elem = this.elem, data = dataBind(elem)[namespace];
callback(elem, data); callback(elem, data);
return this; return this;
}, },
/** /**
* Clears canvas (together with strokes data). * Clears canvas <b>together with</b> associated strokes data.
* If you need to clear canvas only, just invoke <tt>data.sketch.clear()</tt> via <tt>$(selector).sketchable('handler')</tt>. * @see Sketchable.handler
* @see methods.handler * @return Sketchable
* @return jSketchable * @memberof Sketchable
* @namespace methods.clear * @example
* @example $(selector).sketchable('clear'); * 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() { 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.sketch.clear();
data.strokes = []; data.strokes = [];
data.coords = {}; data.coords = {};
@ -170,17 +190,20 @@
return this; return this;
}, },
/** /**
* Reinitializes a sketchable canvas with given opts. * Reinitializes a sketchable canvas with given configuration options.
* @param {Object} opts - Configuration options. * @param {Object} [options] - Configuration (default: {@link Sketchable#defaults}).
* @return jSketchable * @return Sketchable
* @namespace methods.reset * @memberof Sketchable
* @example * @example
* $(selector).sketchable('reset'); * // Reset default state.
* $(selector).sketchable('reset', {interactive:false}); * new Sketchable('canvas').reset();
* // Reset with custom configuration.
* new Sketchable('canvas').reset({ interactive:false });
*/ */
reset: function(opts) { reset: function(options) {
var elem = this.elem, data = dataBind(elem)[_ns], options = data.options; var elem = this.elem, data = dataBind(elem)[namespace], options = data.options;
this.destroy().init(opts);
this.destroy().init(options);
if (typeof options.events.reset === 'function') { if (typeof options.events.reset === 'function') {
options.events.reset(elem, data); options.events.reset(elem, data);
@ -188,22 +211,24 @@
return this; return this;
}, },
/** /**
* Destroys sketchable canvas (together with strokes data and events). * Destroys sketchable canvas, together with strokes data and associated events.
* @return jSketchable * @return Sketchable
* @namespace methods.destroy * @memberof Sketchable
* @example $(selector).sketchable('destroy'); * @example
* // This will leave the canvas element intact.
* new Sketchable('canvas').destroy();
*/ */
destroy: function() { destroy: function() {
var elem = this.elem, data = dataBind(elem)[_ns], options = data.options; var elem = this.elem, data = dataBind(elem)[namespace], options = data.options;
if (options.interactive) {
Event.remove(elem, 'mouseup', mouseupHandler); Event.remove(elem, 'mouseup', mouseupHandler);
Event.remove(elem, 'mousemove', mousemoveHandler); Event.remove(elem, 'mousemove', mousemoveHandler);
Event.remove(elem, 'mousedown', mousedownHandler); Event.remove(elem, 'mousedown', mousedownHandler);
Event.remove(elem, 'touchstart', touchdownHandler); Event.remove(elem, 'touchstart', touchdownHandler);
Event.remove(elem, 'touchmove', touchmoveHandler); Event.remove(elem, 'touchmove', touchmoveHandler);
Event.remove(elem, 'touchend', touchupHandler); Event.remove(elem, 'touchend', touchupHandler);
}
dataBind(elem)[_ns] = null; dataBind(elem)[namespace] = null;
if (typeof options.events.destroy === 'function') { if (typeof options.events.destroy === 'function') {
options.events.destroy(elem, data); options.events.destroy(elem, data);
@ -214,26 +239,55 @@
}; };
/** /**
* Default configuration. * Plugins store.
* Note that mouse* callbacks are triggered only if <tt>interactive</tt> is set to <tt>true</tt>. * @namespace Sketchable.prototype.plugins
* @name defaults * @type {Object}
* @default * @static
* @memberof $.fn.sketchable
* @example * @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 <tt>interactive</tt> is set to <tt>true</tt>.
* @namespace Sketchable.prototype.defaults
* @type {Object}
* @static
* @example
* // The following is the default configuration:
* new Sketchable('canvas', {
* interactive: true, * interactive: true,
* mouseupMovements: false, * mouseupMovements: false,
* relTimestamps: false, * relTimestamps: false,
* multitouch: false, * multitouch: false,
* cssCursors: true, * cssCursors: true,
* // Event hooks.
* events: { * events: {
* init: function(elem, data){ }, * init: function(elem, data) {
* clear: function(elem, data){ }, * // Called when the Sketchable instance is created.
* destroy: function(elem, data){ }, * },
* mousedown: function(elem, data, evt){ }, * destroy: function(elem, data) {
* mousemove: function(elem, data, evt){ }, * // Called when the Sketchable instance is destroyed.
* mouseup: function(elem, data, evt){ }, * },
* 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: { * graphics: {
* firstPointSize: 3, * firstPointSize: 3,
* lineWidth: 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. // In interactive mode, it's possible to draw via mouse/pen/touch input.
interactive: true, interactive: true,
// Indicate whether non-drawing strokes should be registered as well. // Indicate whether non-drawing strokes should be registered as well.
@ -257,15 +311,16 @@
multitouch: true, multitouch: true,
// Display CSS cursors, mainly to indicate whether the element is interactive or not. // Display CSS cursors, mainly to indicate whether the element is interactive or not.
cssCursors: true, cssCursors: true,
// Event callbacks. // Event hooks.
events: { events: {
// init: function(elem, data){ }, // init: function(elem, data) { },
// clear: function(elem, data){ }, // clear: function(elem, data) { },
// destroy: function(elem, data){ }, // destroy: function(elem, data) { },
// mousedown: function(elem, data, evt){ }, // mousedown: function(elem, data, evt) { },
// mousemove: function(elem, data, evt){ }, // mousemove: function(elem, data, evt) { },
// mouseup: function(elem, data, evt){ }, // mouseup: function(elem, data, evt) { },
}, },
// Drawing options, to be used in jSketch lib.
graphics: { graphics: {
firstPointSize: 3, firstPointSize: 3,
lineWidth: 3, lineWidth: 3,
@ -277,6 +332,9 @@
} }
}; };
/**
* @private
*/
function offset(el) { function offset(el) {
var box = el.getBoundingClientRect(); var box = el.getBoundingClientRect();
var body = document.body; var body = document.body;
@ -348,9 +406,12 @@
upHandler(e); upHandler(e);
}; };
/**
* @private
*/
function execTouchEvent(e, callback) { function execTouchEvent(e, callback) {
var elem = e.target, data = dataBind(elem)[_ns], options = data.options; var elem = e.target, data = dataBind(elem)[namespace], options = data.options;
var touches = e.changedTouches; var touches = e.touches;
if (options.multitouch) { if (options.multitouch) {
for (var i = 0; i < touches.length; i++) { for (var i = 0; i < touches.length; i++) {
var touch = touches[i]; var touch = touches[i];
@ -364,6 +425,7 @@
touch.type = e.type; touch.type = e.type;
callback(touch); callback(touch);
} }
e.preventDefault();
}; };
/** /**
@ -398,7 +460,7 @@
if (Event.isRightClick(e)) return false; if (Event.isRightClick(e)) return false;
var idx = e.identifier || 0; 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. // Exit early if interactivity is disabled.
if (!options.interactive) return; if (!options.interactive) return;
@ -429,7 +491,8 @@
*/ */
function moveHandler(e) { function moveHandler(e) {
var idx = e.identifier || 0; 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.interactive) return;
//if (!options.mouseupMovements && !data.sketch.isDrawing) return; //if (!options.mouseupMovements && !data.sketch.isDrawing) return;
// This would grab all penup strokes AFTER drawing something on the canvas for the first time. // This would grab all penup strokes AFTER drawing something on the canvas for the first time.
@ -452,7 +515,8 @@
*/ */
function upHandler(e) { function upHandler(e) {
var idx = e.identifier || 0; 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.interactive) return;
data.sketch.isDrawing = false; data.sketch.isDrawing = false;
@ -465,6 +529,6 @@
}; };
// Expose. // Expose.
window.Sketchable = jSketchable; window.Sketchable = Sketchable;
})(this); })(this);

View File

@ -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. // XXX: Requires `sketchable.utils.js` to be loaded first.
globals: Event, dataBind, deepExtend.
*/ /* eslint-env browser */
/* global Event, dataBind, deepExtend */
;(function(window) { ;(function(window) {
/** // Custom namespace ID, for private data bindind.
* This plugin implements the <a href="https://en.wikipedia.org/wiki/Memento_pattern">Memento pattern</a>. var namespace = 'sketchable';
* 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) {
// Private stuff ////////////////////////////////////////////////////////// /**
* This class implements the <a href="https://en.wikipedia.org/wiki/Memento_pattern">Memento pattern</a>
* 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 stack = [];
var stpos = -1; var stpos = -1;
var self = this; var self = this;
/**
* @private
*/
function prev() { function prev() {
if (stpos > 0) { if (stpos > 0) {
stpos--; stpos--;
@ -34,7 +39,9 @@
}; };
} }
}; };
/**
* @private
*/
function next() { function next() {
if (stpos < stack.length - 1) { if (stpos < stack.length - 1) {
stpos++; stpos++;
@ -45,11 +52,15 @@
}; };
} }
}; };
/**
* Snashot restorer.
* @param {String} snapshot Base64 image.
* @private
*/
function restore(snapshot) { function restore(snapshot) {
// Manipulate canvas via jQuery sketchable API. // Manipulate canvas via jQuery sketchable API.
// This way, we don't lose default drawing settings et al. // 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); //data.sketch.clear().drawImage(snapshot.src);
// Note: jSketch.drawImage after clear creates some flickering, // Note: jSketch.drawImage after clear creates some flickering,
// so use the native HTMLCanvasElement.drawImage method instead. // so use the native HTMLCanvasElement.drawImage method instead.
@ -57,11 +68,14 @@
data.sketch.graphics.drawImage(snapshot, 0,0); data.sketch.graphics.drawImage(snapshot, 0,0);
}); });
}; };
/**
// Key event manager. * Key event manager.
// Undo: "Ctrl + Z" * - Undo: "Ctrl + Z"
// Redo: "Ctrl + Y" or "Ctrl + Shift + Z" * - Redo: "Ctrl + Y" or "Ctrl + Shift + Z"
// TODO: decouple shortcut definition. * @param {Object} e DOM event.
* @private
* @todo Decouple shortcut definition.
*/
function keyManager(e) { function keyManager(e) {
if (e.ctrlKey) { if (e.ctrlKey) {
switch (e.which) { switch (e.which) {
@ -78,93 +92,82 @@
} }
}; };
// Public stuff ///////////////////////////////////////////////////////////
/** /**
* Goes back to the last saved state, if available. * Goes back to the last saved state, if available.
* @name undo * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.undo = function() { this.undo = function() {
prev(); prev();
sketchable.handler(function(elem, data) { instance.handler(function(elem, data) {
if (stack[stpos]) if (stack[stpos])
data.strokes = stack[stpos].strokes.slice(); data.strokes = stack[stpos].strokes.slice();
}); });
return this;
}; };
/** /**
* Goes forward to the last saved state, if available. * Goes forward to the last saved state, if available.
* @name redo * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.redo = function() { this.redo = function() {
next(); next();
sketchable.handler(function(elem, data) { instance.handler(function(elem, data) {
if (stack[stpos]) if (stack[stpos])
data.strokes = stack[stpos].strokes.slice(); data.strokes = stack[stpos].strokes.slice();
}); });
return this;
}; };
/** /**
* Resets stack. * Resets stack.
* @name reset * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.reset = function() { this.reset = function() {
stack = []; stack = [];
stpos = -1; stpos = -1;
return this;
}; };
/** /**
* Save state. * Save current state.
* @name save
* @memberOf MementoCanvas
*/ */
this.save = function() { this.save = function() {
stpos++; stpos++;
if (stpos < stack.length) stack.length = 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() }); stack.push({ image: elem.toDataURL(), strokes: data.strokes.slice() });
}); });
return this;
}; };
/** /**
* Init instance. * Init instance. Currently just (re)attach key event listeners.
* @name init * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.init = function() { this.init = function() {
Event.remove(document, 'keypress', keyManager); Event.remove(document, 'keypress', keyManager);
Event.add(document, 'keypress', keyManager); Event.add(document, 'keypress', keyManager);
return this;
}; };
/** /**
* Destroy instance. * Destroy instance: reset state and remove key event listeners.
* @name destroy * @return {MementoCanvas} Class instance.
* @memberOf MementoCanvas
*/ */
this.destroy = function() { this.destroy = function() {
Event.remove(document, 'keypress', keyManager); Event.remove(document, 'keypress', keyManager);
this.reset(); return this.reset();
}; };
}; };
// Bind plugin extension //////////////////////////////////////////////////// /**
var namespace = 'sketchable'; * Memento plugin constructor for jQuery Sketchable instances.
var availMethods = Sketchable.fn; * @param {Object} sketchable - An Sketchable instance.
var defaults = Sketchable.fn.defaults; * @memberof Sketchable#plugins
*/
function configure(sketchable, opts) { Sketchable.prototype.plugins.memento = function(instance) {
var options = deepExtend({}, defaults, opts); // Access the instance configuration.
// Actually this plugin is singleton, so exit early. var config = instance.config();
if (!options.interactive) return opts;
var callbacks = { var callbacks = {
init: function(elem, data) {
data.memento = new MementoCanvas(sketchable);
data.memento.save();
data.memento.init();
},
clear: function(elem, data) { clear: function(elem, data) {
data.memento.reset(); data.memento.reset();
data.memento.save();
}, },
mouseup: function(elem, data, evt) { mouseup: function(elem, data, evt) {
data.memento.save(); data.memento.save();
@ -176,60 +179,62 @@
// A helper function to override user-defined event listeners. // A helper function to override user-defined event listeners.
function override(ev) { function override(ev) {
if (options && options.events && typeof options.events[ev] === 'function') { // Flag event override so that it doesn't get fired more than once.
var fn = options.events[ev]; if (config.options.$$bound) return;
options.events[ev] = function() { 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. // Exec original function first, then exec our callback.
var args = Array.prototype.slice.call(arguments, 0); fn.apply(instance, arguments);
fn.apply(sketchable, args); callbacks[ev].apply(instance, arguments);
callbacks[ev].apply(sketchable, args);
} }
} else { } 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. // Note: the init event is used to create Sketchable instances,
// Init must go first, since it's called when instantiating the plugin. // therefore it should NOT be overriden.
var events = 'init mouseup clear destroy'.split(' '); var events = 'mouseup clear destroy'.split(' ');
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
override(events[i]); override(events[i]);
} }
// Expose public API for sketchable plugin. // Expose public API: all Sketchable instances will have these methods.
deepExtend(availMethods, { deepExtend(instance, {
/**
* Goes back to the previous CANVAS state, if available.
* @memberof Sketchable
*/
undo: function() { undo: function() {
var elem = this.elem, data = dataBind(elem)[namespace]; var elem = this.elem, data = dataBind(elem)[namespace];
data.memento.undo(); data.memento.undo();
}, },
/**
* Goes forward to the previous CANVAS state, if available.
* @memberof Sketchable
*/
redo: function() { redo: function() {
var elem = this.elem, data = dataBind(elem)[namespace]; var elem = this.elem, data = dataBind(elem)[namespace];
data.memento.redo(); data.memento.redo();
}, },
/**
* Save a snapshot of the current CANVAS status.
* @memberof Sketchable
*/
save: function() { save: function() {
var elem = this.elem, data = dataBind(elem)[namespace]; var elem = this.elem, data = dataBind(elem)[namespace];
data.memento.save(); data.memento.save();
} }
}); });
return options; // Initialize plugin here.
}; config.memento = new MementoCanvas(instance);
config.memento.init().save();
/**
* 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);
}; };
})(this); })(this);

View File

@ -1,9 +1,5 @@
/**
* Data binding lib.
*/
(function(){ (function(){
var cache = [0], var cache = [0], expando = 'data' + +(new Date);
expando = 'data' + +(new Date);
function data(elem) { function data(elem) {
var cacheIndex = elem[expando], var cacheIndex = elem[expando],
nextCacheIndex = cache.length; nextCacheIndex = cache.length;
@ -13,36 +9,79 @@
} }
return cache[cacheIndex]; 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; window.dataBind = data;
})(); })();
/** /**
* Event manager. * Event manager.
* @global
* @module Event
*/ */
var 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) { add: function(elem, type, fn) {
if (!elem) return false; if (!elem) return false;
if (elem.addEventListener) { // W3C standard if (elem.addEventListener) { // W3C standard
elem.addEventListener(type, fn, false); elem.addEventListener(type, fn, false);
} else if (elem.attachEvent) { // IE versions } else if (elem.attachEvent) { // Old IE versions
elem.attachEvent("on"+type, fn); elem.attachEvent("on"+type, fn);
} else { // Really old browser } else { // Really old browser
elem[type+fn] = function(){ fn(window.event); }; 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) { remove: function(elem, type, fn) {
if (!elem) return false; if (!elem) return false;
if (elem.removeEventListener) { // W3C standard if (elem.removeEventListener) { // W3C standard
elem.removeEventListener(type, fn, false); elem.removeEventListener(type, fn, false);
} else if (elem.detachEvent) { // IE versions } else if (elem.detachEvent) { // Old IE versions
elem.detachEvent("on"+type, fn); elem.detachEvent("on"+type, fn);
} else { // Really old browser } else { // Really old browser
elem[type+fn] = null; 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) { isRightClick: function(ev) {
if (!ev) ev = window.event; if (!ev) ev = window.event;
if (ev.which) return ev.which === 3; if (ev.which) return ev.which === 3;
@ -54,6 +93,19 @@ var Event = {
/** /**
* A handy method to (deep) extend an object. * 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) { var deepExtend = function(myObj) {
myObj = myObj || {}; myObj = myObj || {};