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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
{
"name": "jsketch",
"version": "1.8.0",
"version": "2.0.0",
"description": "jSketch drawing lib",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"dist": "grunt",
"docs": "jsdoc --debug -c jsdoc.json && cp -rn figs/ docs/ || echo [sudo] npm i -g jsdoc"
},
"repository": {
"type": "git",

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

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

View File

@ -1,9 +1,5 @@
/**
* Data binding lib.
*/
(function(){
var cache = [0],
expando = 'data' + +(new Date);
var cache = [0], expando = 'data' + +(new Date);
function data(elem) {
var cacheIndex = elem[expando],
nextCacheIndex = cache.length;
@ -13,36 +9,79 @@
}
return cache[cacheIndex];
};
/**
* Add/Read private data to a DOM element.
* @global
* @method
* @param {Object} elem - DOM element to attach data to.
* @example
* var elem = document.getElementById('foo');
* // Attach private data to element:
* dataBind(elem)['some-name'] = { value: 42 };
* // Read private data from element:
* var dat = dataBind(elem)['some-name'];
*/
window.dataBind = data;
})();
/**
* Event manager.
* @global
* @module Event
*/
var Event = {
/**
* Add event to DOM element.
* @memberof module:Event
* @param {Object} elem - DOM element.
* @param {String} type - Event type.
* @param {Function} fn - Callback.
* @example
* Event.add(document.getElementById('foo'), 'click', function fooClick(evt) {
* // Element was clicked.
* });
*/
add: function(elem, type, fn) {
if (!elem) return false;
if (elem.addEventListener) { // W3C standard
elem.addEventListener(type, fn, false);
} else if (elem.attachEvent) { // IE versions
} else if (elem.attachEvent) { // Old IE versions
elem.attachEvent("on"+type, fn);
} else { // Really old browser
elem[type+fn] = function(){ fn(window.event); };
}
},
/**
* Remove event from DOM element.
* @memberof module:Event
* @param {Object} elem - DOM element.
* @param {String} type - Event type.
* @param {Function} fn - Callback.
* @example
* // Assuming elemen had the `fooClick` function (see previous example):
* Event.remove(document.getElementById('foo'), 'click', fooClick);
*/
remove: function(elem, type, fn) {
if (!elem) return false;
if (elem.removeEventListener) { // W3C standard
elem.removeEventListener(type, fn, false);
} else if (elem.detachEvent) { // IE versions
} else if (elem.detachEvent) { // Old IE versions
elem.detachEvent("on"+type, fn);
} else { // Really old browser
elem[type+fn] = null;
}
},
/**
* Determine if an event is a "right click" event.
* @memberof module:Event
* @param {Object} ev - DOM event.
* @return {Boolean}
* @example
* // Assume this function is a click event listener.
* function clickHandler(evt) {
* alert(Event.isRightClick(evt));
* });
*/
isRightClick: function(ev) {
if (!ev) ev = window.event;
if (ev.which) return ev.which === 3;
@ -54,6 +93,19 @@ var Event = {
/**
* A handy method to (deep) extend an object.
* The input object is modified.
* @global
* @param {Object} myObj - Input object.
* @return {Object}
* @example
* var one = { foo:1, bar: { a:true, b:false } };
* var two = { bar: { a:false } };
* // In this case both `ext` and `one` will be the same object.
* var ext = deepExtend(one, two);
* // To create a fresh copy, pass in an empty object as first arg.
* var ext = deepExtend({}, one, two);
* // Now `ext` is `{ foo:1, bar: { a:false, b:false } }`
* // and `one` is left intact.
*/
var deepExtend = function(myObj) {
myObj = myObj || {};