├── .editorconfig ├── .gitignore ├── Gruntfile.js ├── Readme.md ├── dist ├── teledraw-canvas.js └── teledraw-canvas.min.js ├── example ├── index.html └── jquery.mousewheel.js ├── lib ├── StackBlur.js ├── events.js ├── underscore.js └── vector.js ├── package.json └── src ├── canvas ├── teledraw-canvas.history.js ├── teledraw-canvas.snapshot.js ├── teledraw-canvas.stroke.js ├── teledraw-canvas.tool.js └── tools │ ├── arrow.js │ ├── ellipse.js │ ├── eraser.js │ ├── eyedropper.js │ ├── fill.js │ ├── grab.js │ ├── line.js │ ├── pencil.js │ ├── rectangle.js │ └── text.js ├── start.js ├── teledraw-canvas.js └── teledraw-canvas.util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style=space 3 | indent_size=4 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (grunt) { 3 | grunt.loadNpmTasks('grunt-contrib-uglify'); 4 | grunt.loadNpmTasks('grunt-contrib-concat'); 5 | grunt.loadNpmTasks('grunt-contrib-copy'); 6 | grunt.loadNpmTasks('grunt-contrib-jshint'); 7 | grunt.loadNpmTasks('grunt-contrib-connect'); 8 | 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | connect: { 12 | dev: { 13 | options: { 14 | hostname: '*', 15 | port: 9678, 16 | keepalive: true, 17 | open: true 18 | } 19 | } 20 | }, 21 | uglify: { 22 | options: { 23 | mangle: true, 24 | compress: {}, 25 | preserveComments: 'some' 26 | }, 27 | dist: { 28 | files: [{ 29 | 'build/teledraw-canvas.min.js': ['<%= concat.js.dest %>'] 30 | }] 31 | } 32 | }, 33 | concat: { 34 | options: { 35 | stripBanners: true, 36 | banner: '/*! Teledraw Canvas - v<%= pkg.version %> | (c) <%= grunt.template.today("yyyy") %> Cameron Lakenen */\n\n' + 37 | '(function () {\n\n', 38 | footer: '\n\n})();', 39 | }, 40 | js: { 41 | src: [ 42 | 'src/start.js', 43 | 'lib/*.js', 44 | 'src/teledraw-canvas.util.js', 45 | 'src/teledraw-canvas.js', 46 | 'src/canvas/*.js', 47 | 'src/canvas/tools/*.js' 48 | ], 49 | dest: 'build/teledraw-canvas.js' 50 | } 51 | }, 52 | copy: { 53 | dist: { 54 | files: [{ 55 | expand: true, 56 | cwd: 'build/', 57 | src: ['*'], 58 | dest: 'dist/', 59 | filter: 'isFile' 60 | }] 61 | } 62 | }, 63 | jshint: { 64 | options: { 65 | jshintrc: true, 66 | reporterOutput: '' 67 | }, 68 | files: ['Gruntfile.js', 'src/*.js', 'src/**/*.js'] 69 | } 70 | }); 71 | 72 | grunt.registerTask('default', ['jshint', 'concat']); 73 | grunt.registerTask('release', ['default', 'uglify', 'copy']); 74 | grunt.registerTask('serve', ['default', 'connect']); 75 | }; 76 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Teledraw Canvas 2 | 3 | Teledraw Canvas is an HTML5 Canvas drawing engine that is used in [Teledraw.com](http://teledraw.com/). It is currently dependent on [Underscore.js](http://documentcloud.github.com/underscore/) (1.x.x). 4 | 5 | You can see a very basic live demo [here](http://lakenen.com/old/teledraw-canvas). 6 | 7 | ## How to use 8 | 9 | Include teledraw-canvas.js in your page, and from there it's as simple as creating a canvas element with whatever width and height you want: 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | and initializing the TeledrawCanvas: 16 | 17 | ```js 18 | var canvas = new TeledrawCanvas('test-canvas'); 19 | 20 | // with options 21 | var canvas = new TeledrawCanvas('test-canvas', { 22 | width: 512, 23 | height: 512, 24 | fullWidth: 1024, 25 | fullHeight: 1024, 26 | 27 | maxHistory: 20, 28 | minStrokeSize: 500, 29 | maxStrokeSize: 10000, 30 | minStrokeSoftness: 0, 31 | maxStrokeSoftness: 100, 32 | 33 | enableZoom: true, // true by default 34 | maxZoom: 8 // 800% 35 | }); 36 | ``` 37 | 38 | ## API 39 | 40 | ### Setting tool color 41 | 42 | Teledraw Canvas will accept any color string that works with CSS, as well as an array of RGB(A) values 0 to 255 (A: 0-1), (e.g. [255, 0, 0, 1.0] would be a fully opaque red). 43 | 44 | ```js 45 | // by hex value 46 | canvas.setColor('#ec823f'); 47 | // or 48 | canvas.setColor('#fc0'); 49 | 50 | // by rgb 51 | canvas.setColor('rgb(255, 0, 0)'); 52 | 53 | // by hsla 54 | canvas.setColor('hsla(240, 100%, 50%, 0.5)'); 55 | 56 | // by common color name 57 | canvas.setColor('lightGoldenrodYellow'); 58 | 59 | // by RGBA array 60 | canvas.setColor([255, 0, 0, 0.5]); 61 | 62 | // and so on... 63 | ``` 64 | 65 | ### Picking a tool 66 | 67 | Currently, the tools available are 'pencil', 'arrow', 'line-arrow', 'eraser', 'fill', 'rectangle', 'line', 'ellipse', 'eyedropper' and (if zoom is enabled) 'grab'. You can select them with `setTool`. 68 | 69 | ```js 70 | // pencil 71 | canvas.setTool('pencil'); 72 | 73 | // eraser 74 | canvas.setTool('eraser'); 75 | 76 | // etc 77 | ``` 78 | 79 | ### Adjusting the stroke style 80 | 81 | You can modify how the stroke looks, including opacity, size and softness. 82 | 83 | ```js 84 | // relatively small stroke 85 | canvas.setStrokeSize(1000); 86 | // relatively large stroke 87 | canvas.setStrokeSize(10000); 88 | 89 | // no softness 90 | canvas.setStrokeSoftness(0); 91 | // super blurry 92 | canvas.setStrokeSoftness(100); 93 | 94 | // fully opaque 95 | canvas.setAlpha(1); 96 | // fully transparent 97 | canvas.setAlpha(0); 98 | ``` 99 | 100 | ### Clear the canvas, undo, redo, zoom, pan, etc 101 | 102 | Here are some basic useful functions... 103 | 104 | ```js 105 | // clear the canvas 106 | canvas.clear(); 107 | 108 | // undo 109 | canvas.undo(); 110 | // redo 111 | canvas.redo(); 112 | 113 | // reset tool defaults 114 | canvas.defaults(); 115 | 116 | // zoom 200% to the center of the display 117 | canvas.zoom(2); 118 | 119 | // zoom 200% centered at (100,100) 120 | canvas.zoom(2, 100, 100); 121 | 122 | // pan up and to the left 100px 123 | canvas.pan(-100, -100); 124 | ``` 125 | 126 | 127 | ### Lower-level stuff 128 | 129 | Changing the cursor, getting image data, reference to the canvas element, etc... 130 | 131 | ```js 132 | // change the css cursor attribute on the canvas element 133 | canvas.cursor('default'); 134 | 135 | // get a reference to the HTML Canvas element 136 | var elt = canvas.canvas(); 137 | 138 | // get a 2d canvas rendering context for the canvas element 139 | var ctx = canvas.ctx(); 140 | 141 | // returns a new (blank) canvas element the same size as this tdcanvas element 142 | var tmpCanvas = canvas.getTempCanvas(); 143 | 144 | // returns a data url (image/png) of the canvas, optionally a portion of the canvas specified by x, y, w, h 145 | var dataURL = canvas.toDataURL(0, 0, 100, 100); 146 | 147 | // draw an image to the canvas and when it's finished, calls the given 148 | // callback function (with `this` as the TeledrawCanvas Object) 149 | // this is also an alias for canvas.fromImageURL 150 | canvas.fromDataURL(url, function () { alert('done!'); }); 151 | 152 | // get the ImageData of the canvas element 153 | var data = canvas.getImageData(); 154 | 155 | // sets the ImageData of the canvas element 156 | canvas.putImageData(data); 157 | ``` 158 | 159 | ## Adding Tools 160 | 161 | Teledraw Canvas provides a ton of flexibility when it comes to adding new tools. Check back here soon for a detailed walkthrough and example on how to build your own tools! 162 | 163 | 164 | ## Tests 165 | 166 | Coming soon! (lol, or never) 167 | 168 | 169 | ## Changelog 170 | 171 | __0.14.1__ 172 | * Fix bug with line-arrow tool when using shift key 173 | 174 | __0.14.0__ 175 | * Added arrow and line-arrow tools 176 | 177 | __0.13.1__ 178 | * Fixed preview for filled versions of ellipse and rectangle tools 179 | 180 | __0.13.0__ 181 | * Added filled versions of ellipse and rectangle tools 182 | 183 | __0.12.0__ 184 | * Added preview functionality to all the tools 185 | * Added event triggers where appropriate (tool change, history change, etc) 186 | 187 | __0.11.2__ 188 | * Fixed a bug where user could undo while drawing 189 | 190 | __0.11.1__ 191 | * Fixed rendering issue when using shift key with some tools 192 | 193 | __0.11.0__ 194 | * Added keyboard shortcut support 195 | * Added destroy() API call to prevent event leaks when removing a canvas 196 | * Several bugfixes 197 | 198 | 199 | ## License 200 | 201 | (The MIT License) 202 | 203 | Copyright 2016 Cameron Lakenen 204 | 205 | Permission is hereby granted, free of charge, to any person obtaining 206 | a copy of this software and associated documentation files (the 207 | "Software"), to deal in the Software without restriction, including 208 | without limitation the rights to use, copy, modify, merge, publish, 209 | distribute, sublicense, and/or sell copies of the Software, and to 210 | permit persons to whom the Software is furnished to do so, subject to 211 | the following conditions: 212 | 213 | The above copyright notice and this permission notice shall be 214 | included in all copies or substantial portions of the Software. 215 | 216 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 217 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 218 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 219 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 220 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 221 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 222 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 223 | -------------------------------------------------------------------------------- /dist/teledraw-canvas.min.js: -------------------------------------------------------------------------------- 1 | /*! Teledraw Canvas - v0.14.1 | (c) 2016 Cameron Lakenen */ 2 | !function(){function a(b,d,f){if("mouseenter"===d||"mouseleave"===d){var g="mouseenter"===d,h=g?"fromElement":"toElement";return d=g?"mouseover":"mouseout",void a(b,d,function(a){a=a||window.event;var c=a.target||a.srcElement,d=a.relatedTarget||a[h];if((b===c||e(b,c))&&!e(b,d))return f.apply(this,arguments)})}if(b.addEventListener)b.addEventListener(d,f,!1);else{f.$$guid||(f.$$guid=a.guid++),b.events||(b.events={});var i=b.events[d];i||(i=b.events[d]={},b["on"+d]&&(i[0]=b["on"+d])),i[f.$$guid]=f,b["on"+d]=c}}function b(a,b,c){a.removeEventListener?a.removeEventListener(b,c,!1):a.events&&a.events[b]&&delete a.events[b][c.$$guid]}function c(a){var b=!0;a=a||d(((this.ownerDocument||this.document||this).parentWindow||window).event);var c=this.events[a.type];for(var e in c)this.$$handleEvent=c[e],this.$$handleEvent(a)===!1&&(b=!1);return b}function d(a){return a.preventDefault=d.preventDefault,a.stopPropagation=d.stopPropagation,a}function e(a,b){return a.contains?a.contains(b):!!(16&a.compareDocumentPosition(b))}function f(a,b,c){return a instanceof f?f.create(a):(this.x=a||0,this.y=b||0,void(this.z=c||0))}var g,h=Math,j=h.abs,k=(h.sqrt,h.floor),l=(h.round,h.min),m=h.max,n=h.pow;window.TeledrawCanvas=function(a,b){return new TeledrawCanvas.api(a,b)},stackBlurCanvasRGBA=function(){function a(){this.r=0,this.g=0,this.b=0,this.a=0,this.next=null}var b=[512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512,505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405,399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328,324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271,268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456,451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388,385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335,332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292,289,287,285,282,280,278,275,273,271,269,267,265,263,261,259],c=[9,11,12,13,13,14,14,15,15,15,15,16,16,16,16,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24,24],d=function(d,e){if(!(isNaN(e)||e<1)){e|=0;var f,g=0,h=0,i=d.width,j=d.height,k=d.getContext("2d");try{try{f=k.getImageData(g,h,i,j)}catch(a){try{netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"),f=k.getImageData(g,h,i,j)}catch(a){throw alert("Cannot access local image"),new Error("unable to access local image data: "+a)}}}catch(a){throw alert("Cannot access image"),new Error("unable to access image data: "+a)}var l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J=f.data,K=e+e+1,L=i-1,M=j-1,N=e+1,O=N*(N+1)/2,P=new a,Q=P;for(n=1;n>V,0!=H?(H=255/H,J[q]=(s*U>>V)*H,J[q+1]=(t*U>>V)*H,J[q+2]=(u*U>>V)*H):J[q]=J[q+1]=J[q+2]=0,s-=w,t-=x,u-=y,v-=z,w-=S.r,x-=S.g,y-=S.b,z-=S.a,o=r+((o=l+e+1)>V,H>0?(H=255/H,J[o]=(s*U>>V)*H,J[o+1]=(t*U>>V)*H,J[o+2]=(u*U>>V)*H):J[o]=J[o+1]=J[o+2]=0,s-=w,t-=x,u-=y,v-=z,w-=S.r,x-=S.g,y-=S.b,z-=S.a,o=l+((o=m+N)2;if(null==a&&(a=[]),n&&a.reduce===n)return d&&(b=x.bind(b,d)),e?a.reduce(b,c):a.reduce(b);if(y(a,function(a,f,g){e?c=b.call(d,c,a,f,g):(c=a,e=!0)}),!e)throw new TypeError("Reduce of empty array with no initial value");return c},x.reduceRight=x.foldr=function(a,b,c,d){var e=arguments.length>2;if(null==a&&(a=[]),o&&a.reduceRight===o)return d&&(b=x.bind(b,d)),e?a.reduceRight(b,c):a.reduceRight(b);var f=x.toArray(a).reverse();return d&&!e&&(b=x.bind(b,d)),e?x.reduce(f,b,c,d):x.reduce(f,b)},x.find=x.detect=function(a,b,c){var d;return z(a,function(a,e,f){if(b.call(c,a,e,f))return d=a,!0}),d},x.filter=x.select=function(a,b,c){var d=[];return null==a?d:p&&a.filter===p?a.filter(b,c):(y(a,function(a,e,f){b.call(c,a,e,f)&&(d[d.length]=a)}),d)},x.reject=function(a,b,c){var d=[];return null==a?d:(y(a,function(a,e,f){b.call(c,a,e,f)||(d[d.length]=a)}),d)},x.every=x.all=function(a,b,c){var e=!0;return null==a?e:q&&a.every===q?a.every(b,c):(y(a,function(a,f,g){if(!(e=e&&b.call(c,a,f,g)))return d}),!!e)};var z=x.some=x.any=function(a,b,c){b||(b=x.identity);var e=!1;return null==a?e:r&&a.some===r?a.some(b,c):(y(a,function(a,f,g){if(e||(e=b.call(c,a,f,g)))return d}),!!e)};x.include=x.contains=function(a,b){var c=!1;return null==a?c:s&&a.indexOf===s?a.indexOf(b)!=-1:c=z(a,function(a){return a===b})},x.invoke=function(a,b){var c=h.call(arguments,2);return x.map(a,function(a){return(x.isFunction(b)?b||a:a[b]).apply(a,c)})},x.pluck=function(a,b){return x.map(a,function(a){return a[b]})},x.max=function(a,b,c){if(!b&&x.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!b&&x.isEmpty(a))return-(1/0);var d={computed:-(1/0)};return y(a,function(a,e,f){var g=b?b.call(c,a,e,f):a;g>=d.computed&&(d={value:a,computed:g})}),d.value},x.min=function(a,b,c){if(!b&&x.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!b&&x.isEmpty(a))return 1/0;var d={computed:1/0};return y(a,function(a,e,f){var g=b?b.call(c,a,e,f):a;gd?1:0}),"value")},x.groupBy=function(a,b){var c={},d=x.isFunction(b)?b:function(a){return a[b]};return y(a,function(a,b){var e=d(a,b);(c[e]||(c[e]=[])).push(a)}),c},x.sortedIndex=function(a,b,c){c||(c=x.identity);for(var d=0,e=a.length;d>1;c(a[f])=0})})},x.difference=function(a){var b=x.flatten(h.call(arguments,1),!0);return x.filter(a,function(a){return!x.include(b,a)})},x.zip=function(){for(var a=h.call(arguments),b=x.max(x.pluck(a,"length")),c=new Array(b),d=0;d=0;c--)b=[a[c].apply(this,b)];return b[0]}},x.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}},x.keys=v||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[];for(var c in a)x.has(a,c)&&(b[b.length]=c);return b},x.values=function(a){return x.map(a,x.identity)},x.functions=x.methods=function(a){var b=[];for(var c in a)x.isFunction(a[c])&&b.push(c);return b.sort()},x.extend=function(a){return y(h.call(arguments,1),function(b){for(var c in b)a[c]=b[c]}),a},x.pick=function(a){var b={};return y(x.flatten(h.call(arguments,1)),function(c){c in a&&(b[c]=a[c])}),b},x.defaults=function(a){return y(h.call(arguments,1),function(b){for(var c in b)null==a[c]&&(a[c]=b[c])}),a},x.clone=function(a){return x.isObject(a)?x.isArray(a)?a.slice():x.extend({},a):a},x.tap=function(a,b){return b(a),a},x.isEqual=function(b,c){return a(b,c,[])},x.isEmpty=function(a){if(null==a)return!0;if(x.isArray(a)||x.isString(a))return 0===a.length;for(var b in a)if(x.has(a,b))return!1;return!0},x.isElement=function(a){return!(!a||1!=a.nodeType)},x.isArray=u||function(a){return"[object Array]"==j.call(a)},x.isObject=function(a){return a===Object(a)},x.isArguments=function(a){return"[object Arguments]"==j.call(a)},x.isArguments(arguments)||(x.isArguments=function(a){return!(!a||!x.has(a,"callee"))}),x.isFunction=function(a){return"[object Function]"==j.call(a)},x.isString=function(a){return"[object String]"==j.call(a)},x.isNumber=function(a){return"[object Number]"==j.call(a)},x.isFinite=function(a){return x.isNumber(a)&&isFinite(a)},x.isNaN=function(a){return a!==a},x.isBoolean=function(a){return a===!0||a===!1||"[object Boolean]"==j.call(a)},x.isDate=function(a){return"[object Date]"==j.call(a)},x.isRegExp=function(a){return"[object RegExp]"==j.call(a)},x.isNull=function(a){return null===a},x.isUndefined=function(a){return void 0===a},x.has=function(a,b){return k.call(a,b)},x.noConflict=function(){return b._=c,this},x.identity=function(a){return a},x.times=function(a,b,c){for(var d=0;d/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")},x.result=function(a,b){if(null==a)return null;var c=a[b];return x.isFunction(c)?c.call(a):c},x.mixin=function(a){y(x.functions(a),function(b){K(b,x[b]=a[b])})};var B=0;x.uniqueId=function(a){var b=B++;return a?a+b:b},x.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var C=/.^/,D={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"};for(var E in D)D[D[E]]=E;var F=/\\|'|\r|\n|\t|\u2028|\u2029/g,G=/\\(\\|'|r|n|t|u2028|u2029)/g,H=function(a){return a.replace(G,function(a,b){return D[b]})};x.template=function(a,b,c){c=x.defaults(c||{},x.templateSettings);var d="__p+='"+a.replace(F,function(a){return"\\"+D[a]}).replace(c.escape||C,function(a,b){return"'+\n_.escape("+H(b)+")+\n'"}).replace(c.interpolate||C,function(a,b){return"'+\n("+H(b)+")+\n'"}).replace(c.evaluate||C,function(a,b){return"';\n"+H(b)+"\n;__p+='"})+"';\n";c.variable||(d="with(obj||{}){\n"+d+"}\n"),d="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+d+"return __p;\n";var e=new Function(c.variable||"obj","_",d);if(b)return e(b,x);var f=function(a){return e.call(this,a,x)};return f.source="function("+(c.variable||"obj")+"){\n"+d+"}",f},x.chain=function(a){return x(a).chain()};var I=function(a){this._wrapped=a};x.prototype=I.prototype;var J=function(a,b){return b?x(a).chain():a},K=function(a,b){I.prototype[a]=function(){var a=h.call(arguments);return i.call(a,this._wrapped),J(b.apply(x,a),this._chain)}};x.mixin(x),y(["pop","push","reverse","shift","sort","splice","unshift"],function(a){var b=e[a];I.prototype[a]=function(){var c=this._wrapped;b.apply(c,arguments);var d=c.length;return"shift"!=a&&"splice"!=a||0!==d||delete c[0],J(c,this._chain)}}),y(["concat","join","slice"],function(a){var b=e[a];I.prototype[a]=function(){return J(b.apply(this._wrapped,arguments),this._chain)}}),I.prototype.chain=function(){return this._chain=!0,this},I.prototype.value=function(){return this._wrapped}}.call(this),f.prototype.add=function(a){return this.x+=a.x,this.y+=a.y,this.z+=a.z,this},f.prototype.scale=function(a){return this.x*=a,this.y*=a,this.z*=a,this},f.prototype.direction=function(){return Math.atan2(this.y,this.x)},f.prototype.magnitude=function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},f.prototype.addToMagnitude=function(a){a=a||0;var b=this.magnitude(),c=Math.sqrt((a+b)/b);return this.x*=c,this.y*=c,this.z*=c,this},f.prototype.unit=function(){return this.scale(1/this.magnitude())},f.prototype.rotateZ=function(a){var b=this.x,c=this.y;return this.x=b*Math.cos(a)-c*Math.sin(a),this.y=b*Math.sin(a)+c*Math.cos(a),this},f.add=function(a,b){return new f(a.x+b.x,a.y+b.y,a.z+b.z)},f.subtract=function(a,b){return new f(a.x-b.x,a.y-b.y,a.z-b.z)},f.dot=function(a,b){return a.x*b.x+a.y*b.y+a.z*b.z},f.scale=function(a,b){return new f(a.x*b,a.y*b,a.z*b)},f.cross=function(a,b){return new f(a.y*b.z-b.y*a.z,a.z*b.x-b.z*a.x,a.x*b.y-b.x*a.y)},f.average=function(){var a,b=new f,c=arguments;for(arguments[0].constructor.toString().indexOf("Array")!=-1&&(c=arguments[0]),a=c.length,i=0;i1&&(c-=1),d=6*c<1?a+(b-a)*c*6:2*c<1?b:3*c<2?a+(b-a)*(2/3-c)*6:a,255*d}var d=function(){return d};d.clear=function(a){a=a.canvas?a:a.getContext("2d"),a.clearRect(0,0,a.canvas.width,a.canvas.height)},d.cssColor=function(a){return 3===a.length?"rgb("+k(a[0])+","+k(a[1])+","+k(a[2])+")":"rgba("+k(a[0])+","+k(a[1])+","+k(a[2])+","+a[3]+")"},d.clamp=g=function(a,b,c){return ac?c:a},d.opposite=function(a){_.isArray(a)||(a=d.parseColorString(a));var b=d.rgb2hsl(a);b[0]=(b[0]+180)%360,b[1]=100-b[1],b[2]=100-b[2];var c=d.hsl2rgb(b);return 4===a.length&&c.push(a[3]),c},d.rgba2rgb=function(a){if(3===a.length||255===a[3])return a;var b=a[0],c=a[1],d=a[2],e=a[3],f=[];return f[0]=e*b+(255-255*e),f[1]=e*c+(255-255*e),f[2]=e*d+(255-255*e),f},d.rgb2hex=function(a){return a=d.rgba2rgb(a),"#"+b(a[0])+b(a[1])+b(a[2])},d.hex2rgb=function(a){return d.parseColorString(a)},d.rgb2hsl=function(a){var b,c,d,e=a[0]/255,f=a[1]/255,g=a[2]/255,h=m(e,f,g),i=l(e,f,g),j=(h+i)/2;if(h===i)c=d=0;else{switch(b=h-i,d=j>.5?b/(2-h-i):b/(h+i),m){case e:c=(f-g)/b+(f255?255:d,e=e<0||isNaN(e)?0:e>255?255:e,f=f<0||isNaN(f)?0:f>255?255:f,g=g<0||isNaN(g)?0:g>1?1:g,h?[d,e,f,g]:[0,0,0,1]},a.util=d}(TeledrawCanvas),function(){for(var a=0,b=["ms","moz","webkit","o"],c=0;c1)return!0;if(("touchmove"!=w||"mousemove"!=a.type)&&(a.target==u||v.mouseDown)){var b=r(a);return v.tool.move(v.mouseDown,v.last,b),v.last=b,t.trigger("mousemove",b,a),w=a.type,a.preventDefault(),!1}}function m(b){var c=v.last=r(b);return"touchstart"==b.type&&b.touches.length>1||(a(window,"mousedown"===b.type?"mouseup":"touchend",n),v.mouseDown=!0,v.enableWacomSupport&&f()&&"eraser"!==v.currentTool&&(t.setTool("eraser"),v.wacomWasEraser=!0),v.tool.down(c),t.trigger("mousedown",c,b),document.onselectstart=function(){return!1},b.preventDefault(),!1)}function n(a){return b(window,"mouseup"===a.type?"mouseup":"touchend",n),"touchend"==a.type&&a.touches.length>1||(v.mouseDown=!1,v.tool.up(v.last),t.trigger("mouseup",v.last,a),v.wacomWasEraser===!0&&(t.previousTool(),v.wacomWasEraser=!1),document.onselectstart=function(){return!0},a.preventDefault(),!1)}function o(a){"grab"==v.tool.name&&(s=v.currentZoom)}function p(a){if("grab"==v.tool.name){var b=v.last;t.zoom(s*a.scale,b.xd,b.yd)}return a.preventDefault(),!1}function q(a){}function r(a){var b=h(u),c=a.pageX||a.touches&&a.touches[0].pageX,d=a.pageY||a.touches&&a.touches[0].pageY,f=null;return v.enableWacomSupport&&Date.now()-y>25&&(y=Date.now(),f=e()),{x:k((c-b.left)/v.currentZoom)+v.currentOffset.x||0,y:k((d-b.top)/v.currentZoom)+v.currentOffset.y||0,xd:k(c-b.left)||0,yd:k(d-b.top)||0,p:f}}var s,t=this,u=t.element,v=t.state,w=null,x=0,y=0;a(u,"gesturestart",o),a(u,"gesturechange",p),a(u,"gestureend",q),a(u,"dblclick",g),a(u,"mouseenter",c),a(u,"mousedown",m),a(u,"touchstart",m),a(u,"mouseleave",d),a(window,"mousemove",l),a(window,"touchmove",l),a(window,"keydown",j),a(window,"keyup",i),t.unbindEvents=function(){b(u,"gesturestart",o),b(u,"gesturechange",p),b(u,"gestureend",q),b(u,"dblclick",g),b(u,"mouseenter",c),b(u,"mousedown",m),b(u,"touchstart",m),b(u,"mouseleave",d),b(window,"mousemove",l),b(window,"touchmove",l),b(window,"keydown",j),b(window,"keyup",i)}},q.setRGBAArrayColor=function(a){var b=this.state;4===a.length&&this.setAlpha(a.pop());for(var c=a.length;c<3;++c)a.push(0);var d=b.color;return b.color=a,this.trigger("tool.color",b.color,d),this},q.updateTool=function(){var a=1+k(n(this.state.strokeSize/1e3,2)),b=k(n(this.state.strokeSoftness,1.3)/300*a);this.state.lineWidth=a,this.state.shadowBlur=b},q.updateDisplayCanvas=function(a){if(this.state.enableZoom===!1)return this;var b=this.displayCtx(),d=this.state.currentOffset,e=this.state.currentZoom,f=b.canvas.width,g=b.canvas.height,h=k(f/e),i=k(g/e);c.util.clear(b),a!==!0&&this.trigger("display.update:before"),b.drawImage(this._canvas,d.x,d.y,h,i,0,0,f,g),a!==!0&&this.trigger("display.update:after")},q.canvas=function(){return this._canvas},q.ctx=function(){return this._ctx||(this._ctx=this._canvas.getContext("2d"))},q.displayCanvas=function(){return this._displayCanvas},q.displayCtx=function(){return this._displayCtx||(this._displayCtx=this._displayCanvas.getContext("2d"))},q.destroy=function(){this.unbindEvents()},q.cursor=function(a){a||(a="default");var b=a.split(/,\s*/);do a=b.shift(),this.element.style.cursor=a;while(a.length&&this.element.style.cursor!=a);return this},q.clear=function(a){var b=this;return c.util.clear(b.ctx()),a!==!0&&b.history.checkpoint(),b.updateDisplayCanvas(),b.trigger("clear"),b},q.defaults=function(){var a=this;return a.setTool(l.tool),a.setAlpha(l.alpha),a.setColor(l.color),a.setStrokeSize(l.strokeSize),a.setStrokeSoftness(l.strokeSoftness),a},q.toDataURL=function(a,b,c,d,e,f){if(arguments.length>=4){a=a||0,b=b||0,e=e||c,f=f||d;var g=this.getTempCanvas(e,f);return g.getContext("2d").drawImage(this.canvas(),a,b,c,d,0,0,e,f),g.toDataURL()}return this.canvas().toDataURL()},q.getTempCanvas=function(a,b){return new o(a||this._canvas.width,b||this._canvas.height)},q.fromDataURL=q.fromImageURL=function(a,b){var c=this,d=new Image;return d.onload=function(){c.clear(!0),c.ctx().drawImage(d,0,0),c.updateDisplayCanvas(),"function"==typeof b&&b.call(c)},d.src=a,c},q.isBlank=function(){for(var a=this.getImageData().data,b=a.length,c=0,d=b;ch?(e=-(i/h-i/a)/2-(b-i/2)/h,f=-(j/h-j/a)/2-(c-j/2)/h):athis.canvas.state.maxHistory&&this.past.shift().destroy(),this.current&&this.past.push(this.current),this.current=new a.Snapshot(this.canvas),this.future=[],this.rev++},b.prototype.undo=function(){this._move(this.past,this.future)&&this.rev--},b.prototype.redo=function(){this._move(this.future,this.past)&&this.rev++},b.prototype._move=function(a,b){return!!a.length&&(!!this.current&&(b.push(this.current),this.current=a.pop(),this.current.restore(),!0))},a.History=b}(TeledrawCanvas),function(a){var b=function(a){this.canvas=a,this.canvas._snapshotBuffers||(this.canvas._snapshotBuffers=[]),this._snapshotBufferCanvas()};b.prototype.restore=function(a){var b,c;a?(b=a.tl,c=a.br):(b={x:0,y:0},c={x:this.canvas.canvas().width,y:this.canvas.canvas().height}),this._restoreBufferCanvas(b,c),this.canvas.updateDisplayCanvas(!1,b,c)},b.prototype.destroy=function(){this._putBufferCtx()},b.prototype.toDataURL=function(){return this.buffer&&this.buffer.toDataURL()},b.prototype._snapshotBufferCanvas=function(){this._getBufferCtx(),this.buffer.drawImage(this.canvas.canvas(),0,0)},b.prototype._restoreBufferCanvas=function(a,b){var c=this.canvas.ctx(),d=b.x-a.x,e=b.y-a.y;0!==d&&0!==e&&(c.clearRect(a.x,a.y,d,e),c.drawImage(this.buffer.canvas,a.x,a.y,d,e,a.x,a.y,d,e))},b.prototype._snapshotImageData=function(){this.data=this.canvas.getImageData()},b.prototype._restoreImageData=function(){this.canvas.putImageData(this.data)},b.prototype._putBufferCtx=function(){this.buffer&&this.canvas._snapshotBuffers.push(this.buffer),this.buffer=null},b.prototype._getBufferCtx=function(){var a;this.buffer||(this.canvas._snapshotBuffers.length?(a=this.canvas._snapshotBuffers.pop(),a.clearRect(0,0,a.canvas.width,a.canvas.height)):a=this.canvas.getTempCanvas().getContext("2d")),this.buffer=a},a.Snapshot=b}(TeledrawCanvas),function(a){var b=function(a){this.canvas=a};b.prototype.start=function(a){},b.prototype.move=function(a,b){},b.prototype.end=function(){},b.prototype.draw=function(){},b.prototype.save=function(){this.snapshot=new a.Snapshot(this.canvas)},b.prototype.restore=function(){this.snapshot.restore(this)},b.prototype.destroy=function(){this.snapshot.destroy()},a.Stroke=b}(TeledrawCanvas),function(a){var b=function(){};b.prototype.down=function(a){},b.prototype.up=function(a){},b.prototype.move=function(a,b,c){},b.prototype.dblclick=function(a){},b.prototype.enter=function(a,b){},b.prototype.leave=function(a,b){},b.prototype.keydown=function(a,b){16===b&&(this.shiftKey=!0,a&&(this.updateBoundaries(),this.draw()))},b.prototype.keyup=function(a,b){16===b&&(this.shiftKey=!1,a&&(this.updateBoundaries(),this.draw()))},b.prototype.preview=function(b,c){return new a.Canvas(b||100,c||100)},b.createTool=function(c,d,e){var f=function(a,b){this.canvas=a,this.ctx=b||a.ctx(),this.color=a.getColor(),this.color.push(a.getAlpha()),this.points=[],this.tl={x:this.ctx.canvas.width,y:this.ctx.canvas.height},this.br={x:0,y:0},this.tool={}};f.prototype=new a.Stroke;var h=function(a){this.canvas=a,a.cursor(d),this.name=c,this.cursor=d||"default",this.currentStroke=null,"function"==typeof e&&e.call(this)};return h.prototype=new b,h.prototype.down=function(a){this.currentStroke=new f(this.canvas),this.currentStroke.tool=this,this.currentStroke.save(),this.currentStroke.points.push(a),this.currentStroke.start(a),this.updateBoundaries(a),this.draw()},h.prototype.move=function(a,b,c){a&&this.currentStroke&&(this.currentStroke.points.push(c),this.currentStroke.move(b,c),this.updateBoundaries(c),this.draw())},h.prototype.up=function(a){this.currentStroke&&(this.currentStroke.end(a),this.draw(),this.currentStroke.destroy(),this.currentStroke=null,this.canvas.history.checkpoint()),this.canvas.trigger("tool.up")},h.prototype.draw=function(){this.currentStroke.ctx.save(),this.currentStroke.restore(),this.currentStroke.draw(),this.canvas.updateDisplayCanvas(!1,this.currentStroke.tl,this.currentStroke.br),this.currentStroke.ctx.restore()},h.prototype.updateBoundaries=function(a){var b=this.currentStroke,c=b.ctx.canvas,d=this.canvas.state.shadowBlur+this.canvas.state.lineWidth;return this.shiftKey?(b.tl.x=b.tl.y=0,b.br.x=c.width,void(b.br.y=c.height)):void(a&&(a.x-db.br.x&&(b.br.x=g(k(a.x+d),0,c.width)),a.y-db.br.y&&(b.br.y=g(k(a.y+d),0,c.height))))},h.stroke=f,f.tool=h,a.tools[c]=h,h},a.Tool=b,a.tools={}}(TeledrawCanvas),function(a){function b(a,b,c){var d,e,g,h,i;return e=new f(a.x,a.y),d=new f(b.x,b.y),e=f.subtract(d,e).unit(),g=new f(e).rotateZ(Math.PI/2).scale(c).add(d),h=new f(e).rotateZ(-Math.PI/2).scale(c).add(d),i=new f(e).scale(c).add(d),{left:g,right:h,front:i}}var c=a.Tool.createTool("arrow","crosshair"),d=c.stroke.prototype,e=a.Tool.createTool("line-arrow","crosshair"),g=e.stroke.prototype,h=c.prototype.updateBoundaries;c.prototype.updateBoundaries=e.prototype.updateBoundaries=function(){var a=this;if(this.currentStroke.last&&this.currentStroke.current){var c=this.currentStroke.last,d=this.currentStroke.current,e=this.currentStroke.calculateEdgeLength(),f=b(c,d,e);_.values(f).forEach(function(b){h.call(a,b)})}},c.prototype.preview=e.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),d=b.getContext("2d");d.fillStyle="black",d.fillRect(0,0,b.width,b.height);var e=new c.stroke(this.canvas,d);return e.points=[{x:.25*b.width,y:.25*b.height},{x:.75*b.width,y:.75*b.height}],e.draw(),b},d.lineWidth=g.lineWidth=1,d.lineCap=g.lineCap="round",d.calculateEdgeLength=g.calculateEdgeLength=function(){return Math.max(10,2*this.canvas.state.lineWidth)},d.drawArrow=function(){var a,c=(this.canvas.state,this.ctx),d=this.calculateEdgeLength();this.last&&this.current&&(a=b(this.last,this.current,d),c.beginPath(),c.moveTo(this.current.x,this.current.y),c.lineTo(a.left.x,a.left.y),c.lineTo(a.front.x,a.front.y),c.lineTo(a.right.x,a.right.y),c.lineTo(this.current.x,this.current.y),c.closePath(),c.fill())},g.start=function(){a.tools.line.stroke.prototype.start.apply(this,arguments)},d.move=function(a,b){this.last=a,this.current=b},g.move=function(b,c){a.tools.line.stroke.prototype.move.apply(this,arguments),this.last=this.first,this.current=this.second},d.draw=function(){a.tools.pencil.stroke.prototype.draw.call(this),d.drawArrow.call(this)},g.draw=function(){a.tools.line.stroke.prototype.draw.call(this),this.last=this.points[0],this.current=this.points[1],d.drawArrow.call(this)},d.end=function(a){this.current=a},g.end=function(){a.tools.line.stroke.prototype.end.apply(this,arguments),d.end.apply(this,arguments)}}(TeledrawCanvas),function(a){function b(a,b,c,d,e){var f=.5522848,g=d/2*f,h=e/2*f,i=b+d,j=c+e,k=b+d/2,l=c+e/2;a.beginPath(),a.moveTo(b,l),a.bezierCurveTo(b,l-h,k-g,c,k,c),a.bezierCurveTo(k+g,c,i,l-h,i,l),a.bezierCurveTo(i,l+h,k+g,j,k,j),a.bezierCurveTo(k-g,j,b,l+h,b,l),a.closePath()}var c=a.Tool.createTool("ellipse","crosshair"),d=c.stroke.prototype;c.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),d=b.getContext("2d"),e=new c.stroke(this.canvas,d);return e.first={x:0,y:0},e.second={x:b.width,y:b.height},e.draw(),b},d.bgColor=[255,255,255],d.bgAlpha=0,d.lineWidth=1,d.start=function(a){this.first=a},d.move=function(a,b){this.second=b},d.end=function(a){this.second=a},d.draw=function(){if(this.first&&this.second){var c=this,d=c.first.x,e=c.first.y,f=c.second.x-d,g=c.second.y-e,h=c.ctx,i=c.canvas.state,k=i.shadowOffset,l=i.shadowBlur,m=i.lineWidth,n=a.util.cssColor(i.color);h.lineJoin=h.lineCap="round",h.globalAlpha=i.globalAlpha,h.fillStyle=h.strokeStyle=n,h.miterLimit=1e5,c.tool.shiftKey&&(g=c.second.y>e?j(f):-j(f)),c.tool.fill?(b(h,d,e,f,g),h.fill()):(l>0&&(h.shadowColor=n,h.shadowOffsetX=h.shadowOffsetY=k,h.shadowBlur=l,h.translate(-k,-k)),h.lineWidth=m,b(h,d,e,f,g),h.stroke())}};var e=a.Tool.createTool("filled-ellipse","crosshair");e.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),c=b.getContext("2d"),d=new e.stroke(this.canvas,c);d.tool=this;var f=b.width,g=b.height;return d.first={x:.1*f,y:.1*g},d.second={x:.9*f,y:.9*g},console.log(d),d.draw(),b},_.extend(e.stroke.prototype,c.stroke.prototype),e.prototype.fill=!0}(TeledrawCanvas),function(a){var b=a.Tool.createTool("eraser","crosshair");b.prototype.preview=function(){var c=a.Tool.prototype.preview.apply(this,arguments),d=c.getContext("2d");d.fillStyle="black",d.fillRect(0,0,c.width,c.height);var e=new b.stroke(this.canvas,d);return e.points=[{x:c.width/2,y:c.height/2}],e.draw(),c},b.stroke.prototype.lineWidth=1,b.stroke.prototype.lineCap="round",b.stroke.prototype.draw=function(){this.color=[255,255,255,255],this.ctx.globalCompositeOperation="destination-out",a.tools.pencil.stroke.prototype.draw.call(this)}}(TeledrawCanvas),function(a){var b=function(){this.previewContainer=document.createElement("div"),_.extend(this.previewContainer.style,{position:"absolute",width:"10px",height:"10px",border:"1px solid black",display:"none"}),document.body.appendChild(this.previewContainer),this.canvas.state.mouseOver&&(this.previewContainer.style.display="block")},c=a.Tool.createTool("eyedropper","crosshair",b);c.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),c=b.getContext("2d");return c.fillStyle=a.util.cssColor(this.color||[0,0,0,0]),c.fillRect(0,0,b.width,b.height),b},c.prototype.pick=function(b){var c,d,e=this.previewContainer,f=this.canvas.element.offsetLeft,g=this.canvas.element.offsetTop,h=this.canvas._displayCtx.getImageData(b.xd,b.yd,1,1).data;this.color=a.util.rgba2rgb(Array.prototype.slice.call(h)),d=a.util.rgb2hsl(this.color)[2],_.extend(e.style,{left:f+b.xd+15+"px",top:g+b.yd+5+"px",background:a.util.cssColor(this.color),"border-color":d>=50?"#000":"#888"}),this.canvas.state.mouseOver?(e.style.display="none",c=e.offsetHeight,e.style.display="block"):e.style.display="none"},c.prototype.enter=function(){this.previewContainer.style.display="block"},c.prototype.leave=function(){this.previewContainer.style.display="none"},c.prototype.move=function(a,b,c){this.pick(c)},c.prototype.down=function(a){this.pick(a)},c.prototype.up=function(a){this.pick(a),this.canvas.setColor(this.color),this.previewContainer.parentNode.removeChild(this.previewContainer),this.canvas.previousTool()}}(TeledrawCanvas),function(a){function b(b,e,h,i,j,k){var l,m,n,o,p,q,r=[[h.x,h.y]],s=c(b,h.x,h.y,i),t=(g.tolerance,a.util.opposite(s));for(t[3]/=2;r.length;){for(n=r.pop(),o=n[0],q=p=n[1];q>=0&&f(c(b,o,q,i),s);)q--;for(q++,l=m=!1;q0&&f(c(b,o-1,q,i),s)?(r.push([o-1,q]),l=!0):l&&o>0&&f(c(b,o-1,q,i),s)&&(l=!1),!m&&o.2&&(j.data[k]=h[0],j.data[k+1]=h[1],j.data[k+2]=h[2],j.data[k+3]=Math.min(h[3],3*j.data[k+3]));i.putImageData(j,0,0)}},g.stroke.prototype.draw=function(){this.tmp_canvas&&this.ctx.drawImage(this.tmp_canvas,0,0)}}(TeledrawCanvas),function(a){var b="hand, grab, -moz-grab, -webkit-grab, move",c="grabbing, -moz-grabbing, -webkit-grabbing, move",d=a.Tool.createTool("grab",b);d.prototype.move=function(a,b,c){var d=this;a&&(clearTimeout(this._clearDeltasId),cancelAnimationFrame(this._momentumId),this.dx=c.xd-b.xd,this.dy=c.yd-b.yd,this.canvas.pan(this.dx,this.dy),this._clearDeltasId=setTimeout(function(){d.dx=d.dy=0},100))},d.prototype.down=function(a){cancelAnimationFrame(this._momentumId),this.canvas.cursor(c)},d.prototype.up=function(a){cancelAnimationFrame(this._momentumId),this.momentum(this.dx,this.dy),this.dx=this.dy=0,this.canvas.cursor(b)},d.prototype.dblclick=function(a){cancelAnimationFrame(this._momentumId),this.dx=this.dy=0;var b=2;this.shiftKey&&(b/=4),this.canvas.zoom(this.canvas.state.currentZoom*b,a.xd,a.yd)},d.prototype.momentum=function(a,b){var c=this;(Math.abs(a)>=1||Math.abs(b)>=1)&&(a/=1.1,b/=1.1,this.canvas.pan(a,b),this._momentumId=requestAnimationFrame(function(){c.momentum(a,b)}))}}(TeledrawCanvas),function(a){var b=a.Tool.createTool("line","crosshair");b.prototype.preview=function(){var c=a.Tool.prototype.preview.apply(this,arguments),d=c.getContext("2d"),e=new b.stroke(this.canvas,d);return e.first={x:0,y:0},e.second={x:c.width,y:c.height},e.draw(),c},b.stroke.prototype.lineCap="round",b.stroke.prototype.start=function(a){this.first=a},b.stroke.prototype.move=function(a,b){this.second=b},b.stroke.prototype.end=function(a){this.second=a},b.stroke.prototype.drawLine=function(){if(this.first&&this.second){var a,b,c,d=_.extend({},this.first),e=_.extend({},this.second),f=Math.PI;delete d.p,delete e.p,this.tool.shiftKey&&(b=e.x-d.x,c=e.y-d.y,a=Math.atan2(c,b),a>=7*-f/8&&a<5*-f/8||a>=3*-f/8&&a<-f/8?e.y=d.y-Math.abs(b):a>=5*-f/8&&a<3*-f/8||a>=3*f/8&&a<5*f/8?e.x=d.x:a>=f/8&&a<3*f/8||a>=5*f/8&&a<7*f/8?e.y=d.y+Math.abs(b):e.y=d.y),this.points=[d,e]}},b.stroke.prototype.draw=function(){b.stroke.prototype.drawLine.call(this),a.tools.pencil.stroke.prototype.draw.call(this)}}(TeledrawCanvas),function(a){function b(a,b){for(var c,d,e,g,h,i={left:[],right:[]},j=a.length,k=a[0],l=new f(k.x,k.y),m=1,n=j;m0&&(e.shadowColor=j,e.shadowOffsetX=e.shadowOffsetY=g,e.shadowBlur=h,e.translate(-g,-g)),1===f.length)switch(this.lineCap){case"round":e.beginPath(),f[0].p&&(i*=2*f[0].p),e.arc(f[0].x,f[0].y,i/2,0,2*Math.PI,!0),e.closePath(),e.fill();break;case"square":e.fillRect(f[0].x-i/2,f[0].y-i/2,i,i)}else f.length>1&&(e.lineJoin="round",e.lineCap=this.lineCap,e.lineWidth=i,f[0].p||f[1].p?(e.beginPath(),c(e,b(f,i),this.smoothing),e.closePath(),e.fill()):(e.beginPath(),c(e,f,this.smoothing),e.stroke()))}}(TeledrawCanvas),function(a){function b(a,b,c){a.beginPath(),a.moveTo(b.x,b.y),a.lineTo(c.x,b.y),a.lineTo(c.x,c.y),a.lineTo(b.x,c.y),a.lineTo(b.x,b.y)}var c=a.Tool.createTool("rectangle","crosshair");c.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),d=b.getContext("2d"),e=new c.stroke(this.canvas,d);return e.first={x:0,y:0},e.second={x:b.width,y:b.height},e.draw(),b},c.stroke.prototype.bgColor=[255,255,255],c.stroke.prototype.bgAlpha=0,c.stroke.prototype.lineWidth=1,c.stroke.prototype.start=function(a){this.first=a},c.stroke.prototype.move=function(a,b){this.second=b},c.stroke.prototype.draw=function(){if(this.first&&this.second){var c=this.first,d=_.extend({},this.second),e=this.ctx,f=this.canvas.state,g=f.shadowOffset,h=f.shadowBlur,i=f.lineWidth,j=a.util.cssColor(f.color);if(e.lineJoin=e.lineCap="round",e.globalAlpha=f.globalAlpha,e.fillStyle=e.strokeStyle=j,e.miterLimit=1e5,this.tool.shiftKey){var k=Math.abs(d.x-c.x);d.y=c.y+(d.y>c.y?k:-k)}this.tool.fill?(b(e,c,d),e.fill()):(h>0&&(e.shadowColor=j,e.shadowOffsetX=e.shadowOffsetY=g,e.shadowBlur=h,e.translate(-g,-g)),e.lineWidth=i,b(e,c,d),e.stroke())}};var d=a.Tool.createTool("filled-rectangle","crosshair");d.prototype.preview=function(){var b=a.Tool.prototype.preview.apply(this,arguments),c=b.getContext("2d"),e=new d.stroke(this.canvas,c);e.tool=this;var f=b.width,g=b.height;return e.first={x:.1*f,y:.1*g},e.second={x:.9*f,y:.9*g},e.draw(),b},_.extend(d.stroke.prototype,c.stroke.prototype),d.prototype.fill=!0}(TeledrawCanvas),function(c){function d(b){var c=document.createElement("input");c.style.opacity=0,document.body.appendChild(c),c.focus(),b.input=c,b.inputHandler=function(){b.text=c.value,b.tool.draw()},b.keydownHandler=function(a){13===a.keyCode&&f(b)},a(c,"input",b.inputHandler),a(c,"keydown",b.keydownHandler)}function e(a){a.input&&(b(a.input,"input",a.inputHandler),b(a.input,"input",a.keydownHandler),a.input.parentNode.removeChild(a.input))}function f(a){var b=a.tool;e(a),a.finished=!0,b.draw(),a.destroy(),b.currentStroke=null,b.canvas.history.checkpoint()}var g=c.Tool.createTool("text","text"),h=g.stroke.prototype,i=.05,j=g.prototype.down;g.prototype.down=function(a){this.currentStroke&&f(this.currentStroke),j.call(this,a)},g.prototype.up=function(a){this.currentStroke&&(this.currentStroke.end(a),this.currentStroke.draw(),d(this.currentStroke)),this.canvas.trigger("tool.up")},g.prototype.preview=function(){var a=c.Tool.prototype.preview.apply(this,arguments),b=a.getContext("2d"),d=new g.stroke(this.canvas,b);return d.first={x:0,y:0},d.second={x:a.width,y:a.height},d.text="Aa",d.draw(),a},h.start=function(a){this.text="",this.first=a},h.move=function(a,b){this.second=b},h.end=function(a){this.second=a},h.draw=function(){if(this.first&&this.second){var a,b=this.first.x,d=this.first.y,e=this.second.x-b,f=this.second.y-d,g=this.ctx,h=this.canvas.state,j=c.util.cssColor(h.color);e<0&&(e=-e,b-=e),f<0&&(f=-f,d-=f),a=f*i,this.finished||(g.save(),g.lineWidth=1,g.strokeStyle="black",g.strokeRect(b,d,e,f),g.restore()),b+=a,e-=2*a,d+=a,f-=2*a,g.globalAlpha=h.globalAlpha,g.fillStyle=g.strokeStyle=j,g.textAlign="center",g.textBaseline="middle",g.font=f+"px "+(h.font?h.font:"Arial"),g.fillText(this.text,b+e/2,d+f/2,e)}}}(TeledrawCanvas)}(); -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Teledraw Canvas Example 6 | 16 | 17 | 18 | 19 | 133 | 134 | 135 | 136 |

Teledraw Canvas Example

137 | Oh crap... your browser doesn't support canvas! 138 | 139 |
140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /example/jquery.mousewheel.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 5 | * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 6 | * Thanks to: Seamus Leahy for adding deltaX and deltaY 7 | * 8 | * Version: 3.0.6 9 | * 10 | * Requires: 1.2.2+ 11 | */ 12 | 13 | (function($) { 14 | 15 | var types = ['DOMMouseScroll', 'mousewheel']; 16 | 17 | if ($.event.fixHooks) { 18 | for ( var i=types.length; i; ) { 19 | $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; 20 | } 21 | } 22 | 23 | $.event.special.mousewheel = { 24 | setup: function() { 25 | if ( this.addEventListener ) { 26 | for ( var i=types.length; i; ) { 27 | this.addEventListener( types[--i], handler, false ); 28 | } 29 | } else { 30 | this.onmousewheel = handler; 31 | } 32 | }, 33 | 34 | teardown: function() { 35 | if ( this.removeEventListener ) { 36 | for ( var i=types.length; i; ) { 37 | this.removeEventListener( types[--i], handler, false ); 38 | } 39 | } else { 40 | this.onmousewheel = null; 41 | } 42 | } 43 | }; 44 | 45 | $.fn.extend({ 46 | mousewheel: function(fn) { 47 | return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); 48 | }, 49 | 50 | unmousewheel: function(fn) { 51 | return this.unbind("mousewheel", fn); 52 | } 53 | }); 54 | 55 | 56 | function handler(event) { 57 | var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; 58 | event = $.event.fix(orgEvent); 59 | event.type = "mousewheel"; 60 | 61 | // Old school scrollwheel delta 62 | if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } 63 | if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } 64 | 65 | // New school multidimensional scroll (touchpads) deltas 66 | deltaY = delta; 67 | 68 | // Gecko 69 | if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 70 | deltaY = 0; 71 | deltaX = -1*delta; 72 | } 73 | 74 | // Webkit 75 | if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } 76 | if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } 77 | 78 | // Add event and delta to the front of the arguments 79 | args.unshift(event, delta, deltaX, deltaY); 80 | 81 | return ($.event.dispatch || $.event.handle).apply(this, args); 82 | } 83 | 84 | })(jQuery); 85 | -------------------------------------------------------------------------------- /lib/StackBlur.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | StackBlur - a fast almost Gaussian Blur For Canvas 4 | 5 | Version: 0.5 6 | Author: Mario Klingemann 7 | Contact: mario@quasimondo.com 8 | Website: http://www.quasimondo.com/StackBlurForCanvas 9 | Twitter: @quasimondo 10 | 11 | In case you find this class useful - especially in commercial projects - 12 | I am not totally unhappy for a small donation to my PayPal account 13 | mario@quasimondo.de 14 | 15 | Or support me on flattr: 16 | https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript 17 | 18 | Copyright (c) 2010 Mario Klingemann 19 | 20 | Permission is hereby granted, free of charge, to any person 21 | obtaining a copy of this software and associated documentation 22 | files (the "Software"), to deal in the Software without 23 | restriction, including without limitation the rights to use, 24 | copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the 26 | Software is furnished to do so, subject to the following 27 | conditions: 28 | 29 | The above copyright notice and this permission notice shall be 30 | included in all copies or substantial portions of the Software. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 33 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 34 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 35 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 36 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 37 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 38 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 39 | OTHER DEALINGS IN THE SOFTWARE. 40 | */ 41 | 42 | stackBlurCanvasRGBA = (function () { 43 | 44 | var mul_table = [ 45 | 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512, 46 | 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512, 47 | 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456, 48 | 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512, 49 | 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328, 50 | 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456, 51 | 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335, 52 | 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512, 53 | 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405, 54 | 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328, 55 | 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271, 56 | 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456, 57 | 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388, 58 | 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335, 59 | 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292, 60 | 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259]; 61 | 62 | 63 | var shg_table = [ 64 | 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, 65 | 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 66 | 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 67 | 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 68 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 69 | 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 70 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 71 | 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 72 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 73 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 74 | 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 75 | 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 76 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 77 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 78 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 79 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ]; 80 | 81 | 82 | var stackBlurCanvasRGBA = function( canvas, radius ) 83 | { 84 | if ( isNaN(radius) || radius < 1 ) return; 85 | radius |= 0; 86 | var top_x = 0, 87 | top_y = 0, 88 | width = canvas.width, 89 | height = canvas.height; 90 | var context = canvas.getContext("2d"); 91 | var imageData; 92 | 93 | try { 94 | try { 95 | imageData = context.getImageData( top_x, top_y, width, height ); 96 | } catch(e) { 97 | 98 | // NOTE: this part is supposedly only needed if you want to work with local files 99 | // so it might be okay to remove the whole try/catch block and just use 100 | // imageData = context.getImageData( top_x, top_y, width, height ); 101 | try { 102 | netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); 103 | imageData = context.getImageData( top_x, top_y, width, height ); 104 | } catch(e) { 105 | alert("Cannot access local image"); 106 | throw new Error("unable to access local image data: " + e); 107 | return; 108 | } 109 | } 110 | } catch(e) { 111 | alert("Cannot access image"); 112 | throw new Error("unable to access image data: " + e); 113 | } 114 | 115 | var pixels = imageData.data; 116 | 117 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, 118 | r_out_sum, g_out_sum, b_out_sum, a_out_sum, 119 | r_in_sum, g_in_sum, b_in_sum, a_in_sum, 120 | pr, pg, pb, pa, rbs; 121 | 122 | var div = radius + radius + 1; 123 | var w4 = width << 2; 124 | var widthMinus1 = width - 1; 125 | var heightMinus1 = height - 1; 126 | var radiusPlus1 = radius + 1; 127 | var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; 128 | 129 | var stackStart = new BlurStack(); 130 | var stack = stackStart; 131 | for ( i = 1; i < div; i++ ) 132 | { 133 | stack = stack.next = new BlurStack(); 134 | if ( i == radiusPlus1 ) var stackEnd = stack; 135 | } 136 | stack.next = stackStart; 137 | var stackIn = null; 138 | var stackOut = null; 139 | 140 | yw = yi = 0; 141 | 142 | var mul_sum = mul_table[radius]; 143 | var shg_sum = shg_table[radius]; 144 | 145 | for ( y = 0; y < height; y++ ) 146 | { 147 | r_in_sum = g_in_sum = b_in_sum = a_in_sum = r_sum = g_sum = b_sum = a_sum = 0; 148 | 149 | r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); 150 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); 151 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); 152 | a_out_sum = radiusPlus1 * ( pa = pixels[yi+3] ); 153 | 154 | r_sum += sumFactor * pr; 155 | g_sum += sumFactor * pg; 156 | b_sum += sumFactor * pb; 157 | a_sum += sumFactor * pa; 158 | 159 | stack = stackStart; 160 | 161 | for( i = 0; i < radiusPlus1; i++ ) 162 | { 163 | stack.r = pr; 164 | stack.g = pg; 165 | stack.b = pb; 166 | stack.a = pa; 167 | stack = stack.next; 168 | } 169 | 170 | for( i = 1; i < radiusPlus1; i++ ) 171 | { 172 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 173 | r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); 174 | g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; 175 | b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; 176 | a_sum += ( stack.a = ( pa = pixels[p+3])) * rbs; 177 | 178 | r_in_sum += pr; 179 | g_in_sum += pg; 180 | b_in_sum += pb; 181 | a_in_sum += pa; 182 | 183 | stack = stack.next; 184 | } 185 | 186 | 187 | stackIn = stackStart; 188 | stackOut = stackEnd; 189 | for ( x = 0; x < width; x++ ) 190 | { 191 | pixels[yi+3] = pa = (a_sum * mul_sum) >> shg_sum; 192 | if ( pa != 0 ) 193 | { 194 | pa = 255 / pa; 195 | pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa; 196 | pixels[yi+1] = ((g_sum * mul_sum) >> shg_sum) * pa; 197 | pixels[yi+2] = ((b_sum * mul_sum) >> shg_sum) * pa; 198 | } else { 199 | pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0; 200 | } 201 | 202 | r_sum -= r_out_sum; 203 | g_sum -= g_out_sum; 204 | b_sum -= b_out_sum; 205 | a_sum -= a_out_sum; 206 | 207 | r_out_sum -= stackIn.r; 208 | g_out_sum -= stackIn.g; 209 | b_out_sum -= stackIn.b; 210 | a_out_sum -= stackIn.a; 211 | 212 | p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 213 | 214 | r_in_sum += ( stackIn.r = pixels[p]); 215 | g_in_sum += ( stackIn.g = pixels[p+1]); 216 | b_in_sum += ( stackIn.b = pixels[p+2]); 217 | a_in_sum += ( stackIn.a = pixels[p+3]); 218 | 219 | r_sum += r_in_sum; 220 | g_sum += g_in_sum; 221 | b_sum += b_in_sum; 222 | a_sum += a_in_sum; 223 | 224 | stackIn = stackIn.next; 225 | 226 | r_out_sum += ( pr = stackOut.r ); 227 | g_out_sum += ( pg = stackOut.g ); 228 | b_out_sum += ( pb = stackOut.b ); 229 | a_out_sum += ( pa = stackOut.a ); 230 | 231 | r_in_sum -= pr; 232 | g_in_sum -= pg; 233 | b_in_sum -= pb; 234 | a_in_sum -= pa; 235 | 236 | stackOut = stackOut.next; 237 | 238 | yi += 4; 239 | } 240 | yw += width; 241 | } 242 | 243 | 244 | for ( x = 0; x < width; x++ ) 245 | { 246 | g_in_sum = b_in_sum = a_in_sum = r_in_sum = g_sum = b_sum = a_sum = r_sum = 0; 247 | 248 | yi = x << 2; 249 | r_out_sum = radiusPlus1 * ( pr = pixels[yi]); 250 | g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); 251 | b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); 252 | a_out_sum = radiusPlus1 * ( pa = pixels[yi+3]); 253 | 254 | r_sum += sumFactor * pr; 255 | g_sum += sumFactor * pg; 256 | b_sum += sumFactor * pb; 257 | a_sum += sumFactor * pa; 258 | 259 | stack = stackStart; 260 | 261 | for( i = 0; i < radiusPlus1; i++ ) 262 | { 263 | stack.r = pr; 264 | stack.g = pg; 265 | stack.b = pb; 266 | stack.a = pa; 267 | stack = stack.next; 268 | } 269 | 270 | yp = width; 271 | 272 | for( i = 1; i <= radius; i++ ) 273 | { 274 | yi = ( yp + x ) << 2; 275 | 276 | r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); 277 | g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; 278 | b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; 279 | a_sum += ( stack.a = ( pa = pixels[yi+3])) * rbs; 280 | 281 | r_in_sum += pr; 282 | g_in_sum += pg; 283 | b_in_sum += pb; 284 | a_in_sum += pa; 285 | 286 | stack = stack.next; 287 | 288 | if( i < heightMinus1 ) 289 | { 290 | yp += width; 291 | } 292 | } 293 | 294 | yi = x; 295 | stackIn = stackStart; 296 | stackOut = stackEnd; 297 | for ( y = 0; y < height; y++ ) 298 | { 299 | p = yi << 2; 300 | pixels[p+3] = pa = (a_sum * mul_sum) >> shg_sum; 301 | if ( pa > 0 ) 302 | { 303 | pa = 255 / pa; 304 | pixels[p] = ((r_sum * mul_sum) >> shg_sum ) * pa; 305 | pixels[p+1] = ((g_sum * mul_sum) >> shg_sum ) * pa; 306 | pixels[p+2] = ((b_sum * mul_sum) >> shg_sum ) * pa; 307 | } else { 308 | pixels[p] = pixels[p+1] = pixels[p+2] = 0; 309 | } 310 | 311 | r_sum -= r_out_sum; 312 | g_sum -= g_out_sum; 313 | b_sum -= b_out_sum; 314 | a_sum -= a_out_sum; 315 | 316 | r_out_sum -= stackIn.r; 317 | g_out_sum -= stackIn.g; 318 | b_out_sum -= stackIn.b; 319 | a_out_sum -= stackIn.a; 320 | 321 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 322 | 323 | r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); 324 | g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); 325 | b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); 326 | a_sum += ( a_in_sum += ( stackIn.a = pixels[p+3])); 327 | 328 | stackIn = stackIn.next; 329 | 330 | r_out_sum += ( pr = stackOut.r ); 331 | g_out_sum += ( pg = stackOut.g ); 332 | b_out_sum += ( pb = stackOut.b ); 333 | a_out_sum += ( pa = stackOut.a ); 334 | 335 | r_in_sum -= pr; 336 | g_in_sum -= pg; 337 | b_in_sum -= pb; 338 | a_in_sum -= pa; 339 | 340 | stackOut = stackOut.next; 341 | 342 | yi += width; 343 | } 344 | } 345 | 346 | context.putImageData( imageData, top_x, top_y ); 347 | 348 | } 349 | 350 | function BlurStack() 351 | { 352 | this.r = 0; 353 | this.g = 0; 354 | this.b = 0; 355 | this.a = 0; 356 | this.next = null; 357 | } 358 | return stackBlurCanvasRGBA; 359 | })(); 360 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 0.9.2 2 | 3 | // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | Events = (function () { 9 | // Backbone.Events 10 | // ----------------- 11 | 12 | var slice = Array.prototype.slice; 13 | // Regular expression used to split event strings 14 | var eventSplitter = /\s+/; 15 | 16 | // A module that can be mixed in to *any object* in order to provide it with 17 | // custom events. You may bind with `on` or remove with `off` callback functions 18 | // to an event; trigger`-ing an event fires all callbacks in succession. 19 | // 20 | // var object = {}; 21 | // _.extend(object, Backbone.Events); 22 | // object.on('expand', function(){ alert('expanded'); }); 23 | // object.trigger('expand'); 24 | // 25 | var Events = { 26 | 27 | // Bind one or more space separated events, `events`, to a `callback` 28 | // function. Passing `"all"` will bind the callback to all events fired. 29 | on: function(events, callback, context) { 30 | 31 | var calls, event, node, tail, list; 32 | if (!callback) return this; 33 | events = events.split(eventSplitter); 34 | calls = this._callbacks || (this._callbacks = {}); 35 | 36 | // Create an immutable callback list, allowing traversal during 37 | // modification. The tail is an empty object that will always be used 38 | // as the next node. 39 | while (event = events.shift()) { 40 | list = calls[event]; 41 | node = list ? list.tail : {}; 42 | node.next = tail = {}; 43 | node.context = context; 44 | node.callback = callback; 45 | calls[event] = {tail: tail, next: list ? list.next : node}; 46 | } 47 | 48 | return this; 49 | }, 50 | 51 | // Remove one or many callbacks. If `context` is null, removes all callbacks 52 | // with that function. If `callback` is null, removes all callbacks for the 53 | // event. If `events` is null, removes all bound callbacks for all events. 54 | off: function(events, callback, context) { 55 | var event, calls, node, tail, cb, ctx; 56 | 57 | // No events, or removing *all* events. 58 | if (!(calls = this._callbacks)) return; 59 | if (!(events || callback || context)) { 60 | delete this._callbacks; 61 | return this; 62 | } 63 | 64 | // Loop through the listed events and contexts, splicing them out of the 65 | // linked list of callbacks if appropriate. 66 | events = events ? events.split(eventSplitter) : _.keys(calls); 67 | while (event = events.shift()) { 68 | node = calls[event]; 69 | delete calls[event]; 70 | if (!node || !(callback || context)) continue; 71 | // Create a new list, omitting the indicated callbacks. 72 | tail = node.tail; 73 | while ((node = node.next) !== tail) { 74 | cb = node.callback; 75 | ctx = node.context; 76 | if ((callback && cb !== callback) || (context && ctx !== context)) { 77 | this.on(event, cb, ctx); 78 | } 79 | } 80 | } 81 | 82 | return this; 83 | }, 84 | 85 | // Trigger one or many events, firing all bound callbacks. Callbacks are 86 | // passed the same arguments as `trigger` is, apart from the event name 87 | // (unless you're listening on `"all"`, which will cause your callback to 88 | // receive the true name of the event as the first argument). 89 | trigger: function(events) { 90 | var event, node, calls, tail, args, all, rest; 91 | if (!(calls = this._callbacks)) return this; 92 | all = calls.all; 93 | events = events.split(eventSplitter); 94 | rest = slice.call(arguments, 1); 95 | 96 | // For each event, walk through the linked list of callbacks twice, 97 | // first to trigger the event, then to trigger any `"all"` callbacks. 98 | while (event = events.shift()) { 99 | if (node = calls[event]) { 100 | tail = node.tail; 101 | while ((node = node.next) !== tail) { 102 | node.callback.apply(node.context || this, rest); 103 | } 104 | } 105 | if (node = all) { 106 | tail = node.tail; 107 | args = [event].concat(rest); 108 | while ((node = node.next) !== tail) { 109 | node.callback.apply(node.context || this, args); 110 | } 111 | } 112 | } 113 | 114 | return this; 115 | } 116 | 117 | }; 118 | 119 | // Aliases for backwards compatibility. 120 | Events.bind = Events.on; 121 | Events.unbind = Events.off; 122 | 123 | return Events; 124 | })(); 125 | 126 | 127 | // written by Dean Edwards, 2005 128 | // with input from Tino Zijdel, Matthias Miller, Diego Perini 129 | 130 | // http://dean.edwards.name/weblog/2005/10/add-event/ 131 | 132 | // modified to support mouseenter and mouseleave events 133 | 134 | function addEvent(element, type, handler) { 135 | // add mouseenter and mouseleave events 136 | if (type === 'mouseenter' || type === 'mouseleave') { 137 | var mouseEnter = type === 'mouseenter', 138 | ie = mouseEnter ? 'fromElement' : 'toElement'; 139 | type = mouseEnter ? 'mouseover' : 'mouseout'; 140 | addEvent(element, type, function (e) { 141 | e = e || window.event; 142 | var target = e.target || e.srcElement, 143 | related = e.relatedTarget || e[ie]; 144 | if ((element === target || contains(element, target)) && 145 | !contains(element, related)) 146 | { 147 | return handler.apply(this, arguments); 148 | } 149 | }); 150 | return; 151 | } 152 | 153 | if (element.addEventListener) { 154 | element.addEventListener(type, handler, false); 155 | } else { 156 | // assign each event handler a unique ID 157 | if (!handler.$$guid) handler.$$guid = addEvent.guid++; 158 | // create a hash table of event types for the element 159 | if (!element.events) element.events = {}; 160 | // create a hash table of event handlers for each element/event pair 161 | var handlers = element.events[type]; 162 | if (!handlers) { 163 | handlers = element.events[type] = {}; 164 | // store the existing event handler (if there is one) 165 | if (element["on" + type]) { 166 | handlers[0] = element["on" + type]; 167 | } 168 | } 169 | // store the event handler in the hash table 170 | handlers[handler.$$guid] = handler; 171 | // assign a global event handler to do all the work 172 | element["on" + type] = handleEvent; 173 | } 174 | }; 175 | // a counter used to create unique IDs 176 | addEvent.guid = 1; 177 | 178 | function removeEvent(element, type, handler) { 179 | if (element.removeEventListener) { 180 | element.removeEventListener(type, handler, false); 181 | } else { 182 | // delete the event handler from the hash table 183 | if (element.events && element.events[type]) { 184 | delete element.events[type][handler.$$guid]; 185 | } 186 | } 187 | }; 188 | 189 | function handleEvent(event) { 190 | var returnValue = true; 191 | // grab the event object (IE uses a global event object) 192 | event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); 193 | // get a reference to the hash table of event handlers 194 | var handlers = this.events[event.type]; 195 | // execute each event handler 196 | for (var i in handlers) { 197 | this.$$handleEvent = handlers[i]; 198 | if (this.$$handleEvent(event) === false) { 199 | returnValue = false; 200 | } 201 | } 202 | return returnValue; 203 | }; 204 | 205 | function fixEvent(event) { 206 | // add W3C standard event methods 207 | event.preventDefault = fixEvent.preventDefault; 208 | event.stopPropagation = fixEvent.stopPropagation; 209 | return event; 210 | }; 211 | fixEvent.preventDefault = function() { 212 | this.returnValue = false; 213 | }; 214 | fixEvent.stopPropagation = function() { 215 | this.cancelBubble = true; 216 | }; 217 | 218 | function contains(container, maybe) { 219 | return container.contains ? container.contains(maybe) : 220 | !!(container.compareDocumentPosition(maybe) & 16); 221 | } -------------------------------------------------------------------------------- /lib/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | 9 | (function() { 10 | 11 | // Baseline setup 12 | // -------------- 13 | 14 | // Establish the root object, `window` in the browser, or `global` on the server. 15 | var root = this; 16 | 17 | // Save the previous value of the `_` variable. 18 | var previousUnderscore = root._; 19 | 20 | // Establish the object that gets returned to break out of a loop iteration. 21 | var breaker = {}; 22 | 23 | // Save bytes in the minified (but not gzipped) version: 24 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 25 | 26 | // Create quick reference variables for speed access to core prototypes. 27 | var slice = ArrayProto.slice, 28 | unshift = ArrayProto.unshift, 29 | toString = ObjProto.toString, 30 | hasOwnProperty = ObjProto.hasOwnProperty; 31 | 32 | // All **ECMAScript 5** native function implementations that we hope to use 33 | // are declared here. 34 | var 35 | nativeForEach = ArrayProto.forEach, 36 | nativeMap = ArrayProto.map, 37 | nativeReduce = ArrayProto.reduce, 38 | nativeReduceRight = ArrayProto.reduceRight, 39 | nativeFilter = ArrayProto.filter, 40 | nativeEvery = ArrayProto.every, 41 | nativeSome = ArrayProto.some, 42 | nativeIndexOf = ArrayProto.indexOf, 43 | nativeLastIndexOf = ArrayProto.lastIndexOf, 44 | nativeIsArray = Array.isArray, 45 | nativeKeys = Object.keys, 46 | nativeBind = FuncProto.bind; 47 | 48 | // Create a safe reference to the Underscore object for use below. 49 | var _ = function(obj) { return new wrapper(obj); }; 50 | 51 | // Export the Underscore object for **Node.js**, with 52 | // backwards-compatibility for the old `require()` API. If we're in 53 | // the browser, add `_` as a global object via a string identifier, 54 | // for Closure Compiler "advanced" mode. 55 | if (typeof exports !== 'undefined') { 56 | if (typeof module !== 'undefined' && module.exports) { 57 | exports = module.exports = _; 58 | } 59 | exports._ = _; 60 | } else { 61 | root['_'] = _; 62 | } 63 | 64 | // Current version. 65 | _.VERSION = '1.3.3'; 66 | 67 | // Collection Functions 68 | // -------------------- 69 | 70 | // The cornerstone, an `each` implementation, aka `forEach`. 71 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 72 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 73 | var each = _.each = _.forEach = function(obj, iterator, context) { 74 | if (obj == null) return; 75 | if (nativeForEach && obj.forEach === nativeForEach) { 76 | obj.forEach(iterator, context); 77 | } else if (obj.length === +obj.length) { 78 | for (var i = 0, l = obj.length; i < l; i++) { 79 | if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; 80 | } 81 | } else { 82 | for (var key in obj) { 83 | if (_.has(obj, key)) { 84 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 85 | } 86 | } 87 | } 88 | }; 89 | 90 | // Return the results of applying the iterator to each element. 91 | // Delegates to **ECMAScript 5**'s native `map` if available. 92 | _.map = _.collect = function(obj, iterator, context) { 93 | var results = []; 94 | if (obj == null) return results; 95 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 96 | each(obj, function(value, index, list) { 97 | results[results.length] = iterator.call(context, value, index, list); 98 | }); 99 | if (obj.length === +obj.length) results.length = obj.length; 100 | return results; 101 | }; 102 | 103 | // **Reduce** builds up a single result from a list of values, aka `inject`, 104 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 105 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 106 | var initial = arguments.length > 2; 107 | if (obj == null) obj = []; 108 | if (nativeReduce && obj.reduce === nativeReduce) { 109 | if (context) iterator = _.bind(iterator, context); 110 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 111 | } 112 | each(obj, function(value, index, list) { 113 | if (!initial) { 114 | memo = value; 115 | initial = true; 116 | } else { 117 | memo = iterator.call(context, memo, value, index, list); 118 | } 119 | }); 120 | if (!initial) throw new TypeError('Reduce of empty array with no initial value'); 121 | return memo; 122 | }; 123 | 124 | // The right-associative version of reduce, also known as `foldr`. 125 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 126 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 127 | var initial = arguments.length > 2; 128 | if (obj == null) obj = []; 129 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 130 | if (context) iterator = _.bind(iterator, context); 131 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 132 | } 133 | var reversed = _.toArray(obj).reverse(); 134 | if (context && !initial) iterator = _.bind(iterator, context); 135 | return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); 136 | }; 137 | 138 | // Return the first value which passes a truth test. Aliased as `detect`. 139 | _.find = _.detect = function(obj, iterator, context) { 140 | var result; 141 | any(obj, function(value, index, list) { 142 | if (iterator.call(context, value, index, list)) { 143 | result = value; 144 | return true; 145 | } 146 | }); 147 | return result; 148 | }; 149 | 150 | // Return all the elements that pass a truth test. 151 | // Delegates to **ECMAScript 5**'s native `filter` if available. 152 | // Aliased as `select`. 153 | _.filter = _.select = function(obj, iterator, context) { 154 | var results = []; 155 | if (obj == null) return results; 156 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 157 | each(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) results[results.length] = value; 159 | }); 160 | return results; 161 | }; 162 | 163 | // Return all the elements for which a truth test fails. 164 | _.reject = function(obj, iterator, context) { 165 | var results = []; 166 | if (obj == null) return results; 167 | each(obj, function(value, index, list) { 168 | if (!iterator.call(context, value, index, list)) results[results.length] = value; 169 | }); 170 | return results; 171 | }; 172 | 173 | // Determine whether all of the elements match a truth test. 174 | // Delegates to **ECMAScript 5**'s native `every` if available. 175 | // Aliased as `all`. 176 | _.every = _.all = function(obj, iterator, context) { 177 | var result = true; 178 | if (obj == null) return result; 179 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 180 | each(obj, function(value, index, list) { 181 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 182 | }); 183 | return !!result; 184 | }; 185 | 186 | // Determine if at least one element in the object matches a truth test. 187 | // Delegates to **ECMAScript 5**'s native `some` if available. 188 | // Aliased as `any`. 189 | var any = _.some = _.any = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = false; 192 | if (obj == null) return result; 193 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if a given value is included in the array or object using `===`. 201 | // Aliased as `contains`. 202 | _.include = _.contains = function(obj, target) { 203 | var found = false; 204 | if (obj == null) return found; 205 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 206 | found = any(obj, function(value) { 207 | return value === target; 208 | }); 209 | return found; 210 | }; 211 | 212 | // Invoke a method (with arguments) on every item in a collection. 213 | _.invoke = function(obj, method) { 214 | var args = slice.call(arguments, 2); 215 | return _.map(obj, function(value) { 216 | return (_.isFunction(method) ? method || value : value[method]).apply(value, args); 217 | }); 218 | }; 219 | 220 | // Convenience version of a common use case of `map`: fetching a property. 221 | _.pluck = function(obj, key) { 222 | return _.map(obj, function(value){ return value[key]; }); 223 | }; 224 | 225 | // Return the maximum element or (element-based computation). 226 | _.max = function(obj, iterator, context) { 227 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj); 228 | if (!iterator && _.isEmpty(obj)) return -Infinity; 229 | var result = {computed : -Infinity}; 230 | each(obj, function(value, index, list) { 231 | var computed = iterator ? iterator.call(context, value, index, list) : value; 232 | computed >= result.computed && (result = {value : value, computed : computed}); 233 | }); 234 | return result.value; 235 | }; 236 | 237 | // Return the minimum element (or element-based computation). 238 | _.min = function(obj, iterator, context) { 239 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj); 240 | if (!iterator && _.isEmpty(obj)) return Infinity; 241 | var result = {computed : Infinity}; 242 | each(obj, function(value, index, list) { 243 | var computed = iterator ? iterator.call(context, value, index, list) : value; 244 | computed < result.computed && (result = {value : value, computed : computed}); 245 | }); 246 | return result.value; 247 | }; 248 | 249 | // Shuffle an array. 250 | _.shuffle = function(obj) { 251 | var shuffled = [], rand; 252 | each(obj, function(value, index, list) { 253 | rand = Math.floor(Math.random() * (index + 1)); 254 | shuffled[index] = shuffled[rand]; 255 | shuffled[rand] = value; 256 | }); 257 | return shuffled; 258 | }; 259 | 260 | // Sort the object's values by a criterion produced by an iterator. 261 | _.sortBy = function(obj, val, context) { 262 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 263 | return _.pluck(_.map(obj, function(value, index, list) { 264 | return { 265 | value : value, 266 | criteria : iterator.call(context, value, index, list) 267 | }; 268 | }).sort(function(left, right) { 269 | var a = left.criteria, b = right.criteria; 270 | if (a === void 0) return 1; 271 | if (b === void 0) return -1; 272 | return a < b ? -1 : a > b ? 1 : 0; 273 | }), 'value'); 274 | }; 275 | 276 | // Groups the object's values by a criterion. Pass either a string attribute 277 | // to group by, or a function that returns the criterion. 278 | _.groupBy = function(obj, val) { 279 | var result = {}; 280 | var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; 281 | each(obj, function(value, index) { 282 | var key = iterator(value, index); 283 | (result[key] || (result[key] = [])).push(value); 284 | }); 285 | return result; 286 | }; 287 | 288 | // Use a comparator function to figure out at what index an object should 289 | // be inserted so as to maintain order. Uses binary search. 290 | _.sortedIndex = function(array, obj, iterator) { 291 | iterator || (iterator = _.identity); 292 | var low = 0, high = array.length; 293 | while (low < high) { 294 | var mid = (low + high) >> 1; 295 | iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; 296 | } 297 | return low; 298 | }; 299 | 300 | // Safely convert anything iterable into a real, live array. 301 | _.toArray = function(obj) { 302 | if (!obj) return []; 303 | if (_.isArray(obj)) return slice.call(obj); 304 | if (_.isArguments(obj)) return slice.call(obj); 305 | if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray(); 306 | return _.values(obj); 307 | }; 308 | 309 | // Return the number of elements in an object. 310 | _.size = function(obj) { 311 | return _.isArray(obj) ? obj.length : _.keys(obj).length; 312 | }; 313 | 314 | // Array Functions 315 | // --------------- 316 | 317 | // Get the first element of an array. Passing **n** will return the first N 318 | // values in the array. Aliased as `head` and `take`. The **guard** check 319 | // allows it to work with `_.map`. 320 | _.first = _.head = _.take = function(array, n, guard) { 321 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 322 | }; 323 | 324 | // Returns everything but the last entry of the array. Especcialy useful on 325 | // the arguments object. Passing **n** will return all the values in 326 | // the array, excluding the last N. The **guard** check allows it to work with 327 | // `_.map`. 328 | _.initial = function(array, n, guard) { 329 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 330 | }; 331 | 332 | // Get the last element of an array. Passing **n** will return the last N 333 | // values in the array. The **guard** check allows it to work with `_.map`. 334 | _.last = function(array, n, guard) { 335 | if ((n != null) && !guard) { 336 | return slice.call(array, Math.max(array.length - n, 0)); 337 | } else { 338 | return array[array.length - 1]; 339 | } 340 | }; 341 | 342 | // Returns everything but the first entry of the array. Aliased as `tail`. 343 | // Especially useful on the arguments object. Passing an **index** will return 344 | // the rest of the values in the array from that index onward. The **guard** 345 | // check allows it to work with `_.map`. 346 | _.rest = _.tail = function(array, index, guard) { 347 | return slice.call(array, (index == null) || guard ? 1 : index); 348 | }; 349 | 350 | // Trim out all falsy values from an array. 351 | _.compact = function(array) { 352 | return _.filter(array, function(value){ return !!value; }); 353 | }; 354 | 355 | // Return a completely flattened version of an array. 356 | _.flatten = function(array, shallow) { 357 | return _.reduce(array, function(memo, value) { 358 | if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); 359 | memo[memo.length] = value; 360 | return memo; 361 | }, []); 362 | }; 363 | 364 | // Return a version of the array that does not contain the specified value(s). 365 | _.without = function(array) { 366 | return _.difference(array, slice.call(arguments, 1)); 367 | }; 368 | 369 | // Produce a duplicate-free version of the array. If the array has already 370 | // been sorted, you have the option of using a faster algorithm. 371 | // Aliased as `unique`. 372 | _.uniq = _.unique = function(array, isSorted, iterator) { 373 | var initial = iterator ? _.map(array, iterator) : array; 374 | var results = []; 375 | // The `isSorted` flag is irrelevant if the array only contains two elements. 376 | if (array.length < 3) isSorted = true; 377 | _.reduce(initial, function (memo, value, index) { 378 | if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) { 379 | memo.push(value); 380 | results.push(array[index]); 381 | } 382 | return memo; 383 | }, []); 384 | return results; 385 | }; 386 | 387 | // Produce an array that contains the union: each distinct element from all of 388 | // the passed-in arrays. 389 | _.union = function() { 390 | return _.uniq(_.flatten(arguments, true)); 391 | }; 392 | 393 | // Produce an array that contains every item shared between all the 394 | // passed-in arrays. (Aliased as "intersect" for back-compat.) 395 | _.intersection = _.intersect = function(array) { 396 | var rest = slice.call(arguments, 1); 397 | return _.filter(_.uniq(array), function(item) { 398 | return _.every(rest, function(other) { 399 | return _.indexOf(other, item) >= 0; 400 | }); 401 | }); 402 | }; 403 | 404 | // Take the difference between one array and a number of other arrays. 405 | // Only the elements present in just the first array will remain. 406 | _.difference = function(array) { 407 | var rest = _.flatten(slice.call(arguments, 1), true); 408 | return _.filter(array, function(value){ return !_.include(rest, value); }); 409 | }; 410 | 411 | // Zip together multiple lists into a single array -- elements that share 412 | // an index go together. 413 | _.zip = function() { 414 | var args = slice.call(arguments); 415 | var length = _.max(_.pluck(args, 'length')); 416 | var results = new Array(length); 417 | for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); 418 | return results; 419 | }; 420 | 421 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 422 | // we need this function. Return the position of the first occurrence of an 423 | // item in an array, or -1 if the item is not included in the array. 424 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 425 | // If the array is large and already in sort order, pass `true` 426 | // for **isSorted** to use binary search. 427 | _.indexOf = function(array, item, isSorted) { 428 | if (array == null) return -1; 429 | var i, l; 430 | if (isSorted) { 431 | i = _.sortedIndex(array, item); 432 | return array[i] === item ? i : -1; 433 | } 434 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); 435 | for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; 436 | return -1; 437 | }; 438 | 439 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 440 | _.lastIndexOf = function(array, item) { 441 | if (array == null) return -1; 442 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); 443 | var i = array.length; 444 | while (i--) if (i in array && array[i] === item) return i; 445 | return -1; 446 | }; 447 | 448 | // Generate an integer Array containing an arithmetic progression. A port of 449 | // the native Python `range()` function. See 450 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 451 | _.range = function(start, stop, step) { 452 | if (arguments.length <= 1) { 453 | stop = start || 0; 454 | start = 0; 455 | } 456 | step = arguments[2] || 1; 457 | 458 | var len = Math.max(Math.ceil((stop - start) / step), 0); 459 | var idx = 0; 460 | var range = new Array(len); 461 | 462 | while(idx < len) { 463 | range[idx++] = start; 464 | start += step; 465 | } 466 | 467 | return range; 468 | }; 469 | 470 | // Function (ahem) Functions 471 | // ------------------ 472 | 473 | // Reusable constructor function for prototype setting. 474 | var ctor = function(){}; 475 | 476 | // Create a function bound to a given object (assigning `this`, and arguments, 477 | // optionally). Binding with arguments is also known as `curry`. 478 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 479 | // We check for `func.bind` first, to fail fast when `func` is undefined. 480 | _.bind = function bind(func, context) { 481 | var bound, args; 482 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 483 | if (!_.isFunction(func)) throw new TypeError; 484 | args = slice.call(arguments, 2); 485 | return bound = function() { 486 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 487 | ctor.prototype = func.prototype; 488 | var self = new ctor; 489 | var result = func.apply(self, args.concat(slice.call(arguments))); 490 | if (Object(result) === result) return result; 491 | return self; 492 | }; 493 | }; 494 | 495 | // Bind all of an object's methods to that object. Useful for ensuring that 496 | // all callbacks defined on an object belong to it. 497 | _.bindAll = function(obj) { 498 | var funcs = slice.call(arguments, 1); 499 | if (funcs.length == 0) funcs = _.functions(obj); 500 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 501 | return obj; 502 | }; 503 | 504 | // Memoize an expensive function by storing its results. 505 | _.memoize = function(func, hasher) { 506 | var memo = {}; 507 | hasher || (hasher = _.identity); 508 | return function() { 509 | var key = hasher.apply(this, arguments); 510 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 511 | }; 512 | }; 513 | 514 | // Delays a function for the given number of milliseconds, and then calls 515 | // it with the arguments supplied. 516 | _.delay = function(func, wait) { 517 | var args = slice.call(arguments, 2); 518 | return setTimeout(function(){ return func.apply(null, args); }, wait); 519 | }; 520 | 521 | // Defers a function, scheduling it to run after the current call stack has 522 | // cleared. 523 | _.defer = function(func) { 524 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 525 | }; 526 | 527 | // Returns a function, that, when invoked, will only be triggered at most once 528 | // during a given window of time. 529 | _.throttle = function(func, wait) { 530 | var context, args, timeout, throttling, more, result; 531 | var whenDone = _.debounce(function(){ more = throttling = false; }, wait); 532 | return function() { 533 | context = this; args = arguments; 534 | var later = function() { 535 | timeout = null; 536 | if (more) func.apply(context, args); 537 | whenDone(); 538 | }; 539 | if (!timeout) timeout = setTimeout(later, wait); 540 | if (throttling) { 541 | more = true; 542 | } else { 543 | result = func.apply(context, args); 544 | } 545 | whenDone(); 546 | throttling = true; 547 | return result; 548 | }; 549 | }; 550 | 551 | // Returns a function, that, as long as it continues to be invoked, will not 552 | // be triggered. The function will be called after it stops being called for 553 | // N milliseconds. If `immediate` is passed, trigger the function on the 554 | // leading edge, instead of the trailing. 555 | _.debounce = function(func, wait, immediate) { 556 | var timeout; 557 | return function() { 558 | var context = this, args = arguments; 559 | var later = function() { 560 | timeout = null; 561 | if (!immediate) func.apply(context, args); 562 | }; 563 | if (immediate && !timeout) func.apply(context, args); 564 | clearTimeout(timeout); 565 | timeout = setTimeout(later, wait); 566 | }; 567 | }; 568 | 569 | // Returns a function that will be executed at most one time, no matter how 570 | // often you call it. Useful for lazy initialization. 571 | _.once = function(func) { 572 | var ran = false, memo; 573 | return function() { 574 | if (ran) return memo; 575 | ran = true; 576 | return memo = func.apply(this, arguments); 577 | }; 578 | }; 579 | 580 | // Returns the first function passed as an argument to the second, 581 | // allowing you to adjust arguments, run code before and after, and 582 | // conditionally execute the original function. 583 | _.wrap = function(func, wrapper) { 584 | return function() { 585 | var args = [func].concat(slice.call(arguments, 0)); 586 | return wrapper.apply(this, args); 587 | }; 588 | }; 589 | 590 | // Returns a function that is the composition of a list of functions, each 591 | // consuming the return value of the function that follows. 592 | _.compose = function() { 593 | var funcs = arguments; 594 | return function() { 595 | var args = arguments; 596 | for (var i = funcs.length - 1; i >= 0; i--) { 597 | args = [funcs[i].apply(this, args)]; 598 | } 599 | return args[0]; 600 | }; 601 | }; 602 | 603 | // Returns a function that will only be executed after being called N times. 604 | _.after = function(times, func) { 605 | if (times <= 0) return func(); 606 | return function() { 607 | if (--times < 1) { return func.apply(this, arguments); } 608 | }; 609 | }; 610 | 611 | // Object Functions 612 | // ---------------- 613 | 614 | // Retrieve the names of an object's properties. 615 | // Delegates to **ECMAScript 5**'s native `Object.keys` 616 | _.keys = nativeKeys || function(obj) { 617 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 618 | var keys = []; 619 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 620 | return keys; 621 | }; 622 | 623 | // Retrieve the values of an object's properties. 624 | _.values = function(obj) { 625 | return _.map(obj, _.identity); 626 | }; 627 | 628 | // Return a sorted list of the function names available on the object. 629 | // Aliased as `methods` 630 | _.functions = _.methods = function(obj) { 631 | var names = []; 632 | for (var key in obj) { 633 | if (_.isFunction(obj[key])) names.push(key); 634 | } 635 | return names.sort(); 636 | }; 637 | 638 | // Extend a given object with all the properties in passed-in object(s). 639 | _.extend = function(obj) { 640 | each(slice.call(arguments, 1), function(source) { 641 | for (var prop in source) { 642 | obj[prop] = source[prop]; 643 | } 644 | }); 645 | return obj; 646 | }; 647 | 648 | // Return a copy of the object only containing the whitelisted properties. 649 | _.pick = function(obj) { 650 | var result = {}; 651 | each(_.flatten(slice.call(arguments, 1)), function(key) { 652 | if (key in obj) result[key] = obj[key]; 653 | }); 654 | return result; 655 | }; 656 | 657 | // Fill in a given object with default properties. 658 | _.defaults = function(obj) { 659 | each(slice.call(arguments, 1), function(source) { 660 | for (var prop in source) { 661 | if (obj[prop] == null) obj[prop] = source[prop]; 662 | } 663 | }); 664 | return obj; 665 | }; 666 | 667 | // Create a (shallow-cloned) duplicate of an object. 668 | _.clone = function(obj) { 669 | if (!_.isObject(obj)) return obj; 670 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 671 | }; 672 | 673 | // Invokes interceptor with the obj, and then returns obj. 674 | // The primary purpose of this method is to "tap into" a method chain, in 675 | // order to perform operations on intermediate results within the chain. 676 | _.tap = function(obj, interceptor) { 677 | interceptor(obj); 678 | return obj; 679 | }; 680 | 681 | // Internal recursive comparison function. 682 | function eq(a, b, stack) { 683 | // Identical objects are equal. `0 === -0`, but they aren't identical. 684 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 685 | if (a === b) return a !== 0 || 1 / a == 1 / b; 686 | // A strict comparison is necessary because `null == undefined`. 687 | if (a == null || b == null) return a === b; 688 | // Unwrap any wrapped objects. 689 | if (a._chain) a = a._wrapped; 690 | if (b._chain) b = b._wrapped; 691 | // Invoke a custom `isEqual` method if one is provided. 692 | if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); 693 | if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); 694 | // Compare `[[Class]]` names. 695 | var className = toString.call(a); 696 | if (className != toString.call(b)) return false; 697 | switch (className) { 698 | // Strings, numbers, dates, and booleans are compared by value. 699 | case '[object String]': 700 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 701 | // equivalent to `new String("5")`. 702 | return a == String(b); 703 | case '[object Number]': 704 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 705 | // other numeric values. 706 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 707 | case '[object Date]': 708 | case '[object Boolean]': 709 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 710 | // millisecond representations. Note that invalid dates with millisecond representations 711 | // of `NaN` are not equivalent. 712 | return +a == +b; 713 | // RegExps are compared by their source patterns and flags. 714 | case '[object RegExp]': 715 | return a.source == b.source && 716 | a.global == b.global && 717 | a.multiline == b.multiline && 718 | a.ignoreCase == b.ignoreCase; 719 | } 720 | if (typeof a != 'object' || typeof b != 'object') return false; 721 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 722 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 723 | var length = stack.length; 724 | while (length--) { 725 | // Linear search. Performance is inversely proportional to the number of 726 | // unique nested structures. 727 | if (stack[length] == a) return true; 728 | } 729 | // Add the first object to the stack of traversed objects. 730 | stack.push(a); 731 | var size = 0, result = true; 732 | // Recursively compare objects and arrays. 733 | if (className == '[object Array]') { 734 | // Compare array lengths to determine if a deep comparison is necessary. 735 | size = a.length; 736 | result = size == b.length; 737 | if (result) { 738 | // Deep compare the contents, ignoring non-numeric properties. 739 | while (size--) { 740 | // Ensure commutative equality for sparse arrays. 741 | if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; 742 | } 743 | } 744 | } else { 745 | // Objects with different constructors are not equivalent. 746 | if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; 747 | // Deep compare objects. 748 | for (var key in a) { 749 | if (_.has(a, key)) { 750 | // Count the expected number of properties. 751 | size++; 752 | // Deep compare each member. 753 | if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; 754 | } 755 | } 756 | // Ensure that both objects contain the same number of properties. 757 | if (result) { 758 | for (key in b) { 759 | if (_.has(b, key) && !(size--)) break; 760 | } 761 | result = !size; 762 | } 763 | } 764 | // Remove the first object from the stack of traversed objects. 765 | stack.pop(); 766 | return result; 767 | } 768 | 769 | // Perform a deep comparison to check if two objects are equal. 770 | _.isEqual = function(a, b) { 771 | return eq(a, b, []); 772 | }; 773 | 774 | // Is a given array, string, or object empty? 775 | // An "empty" object has no enumerable own-properties. 776 | _.isEmpty = function(obj) { 777 | if (obj == null) return true; 778 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 779 | for (var key in obj) if (_.has(obj, key)) return false; 780 | return true; 781 | }; 782 | 783 | // Is a given value a DOM element? 784 | _.isElement = function(obj) { 785 | return !!(obj && obj.nodeType == 1); 786 | }; 787 | 788 | // Is a given value an array? 789 | // Delegates to ECMA5's native Array.isArray 790 | _.isArray = nativeIsArray || function(obj) { 791 | return toString.call(obj) == '[object Array]'; 792 | }; 793 | 794 | // Is a given variable an object? 795 | _.isObject = function(obj) { 796 | return obj === Object(obj); 797 | }; 798 | 799 | // Is a given variable an arguments object? 800 | _.isArguments = function(obj) { 801 | return toString.call(obj) == '[object Arguments]'; 802 | }; 803 | if (!_.isArguments(arguments)) { 804 | _.isArguments = function(obj) { 805 | return !!(obj && _.has(obj, 'callee')); 806 | }; 807 | } 808 | 809 | // Is a given value a function? 810 | _.isFunction = function(obj) { 811 | return toString.call(obj) == '[object Function]'; 812 | }; 813 | 814 | // Is a given value a string? 815 | _.isString = function(obj) { 816 | return toString.call(obj) == '[object String]'; 817 | }; 818 | 819 | // Is a given value a number? 820 | _.isNumber = function(obj) { 821 | return toString.call(obj) == '[object Number]'; 822 | }; 823 | 824 | // Is a given object a finite number? 825 | _.isFinite = function(obj) { 826 | return _.isNumber(obj) && isFinite(obj); 827 | }; 828 | 829 | // Is the given value `NaN`? 830 | _.isNaN = function(obj) { 831 | // `NaN` is the only value for which `===` is not reflexive. 832 | return obj !== obj; 833 | }; 834 | 835 | // Is a given value a boolean? 836 | _.isBoolean = function(obj) { 837 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 838 | }; 839 | 840 | // Is a given value a date? 841 | _.isDate = function(obj) { 842 | return toString.call(obj) == '[object Date]'; 843 | }; 844 | 845 | // Is the given value a regular expression? 846 | _.isRegExp = function(obj) { 847 | return toString.call(obj) == '[object RegExp]'; 848 | }; 849 | 850 | // Is a given value equal to null? 851 | _.isNull = function(obj) { 852 | return obj === null; 853 | }; 854 | 855 | // Is a given variable undefined? 856 | _.isUndefined = function(obj) { 857 | return obj === void 0; 858 | }; 859 | 860 | // Has own property? 861 | _.has = function(obj, key) { 862 | return hasOwnProperty.call(obj, key); 863 | }; 864 | 865 | // Utility Functions 866 | // ----------------- 867 | 868 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 869 | // previous owner. Returns a reference to the Underscore object. 870 | _.noConflict = function() { 871 | root._ = previousUnderscore; 872 | return this; 873 | }; 874 | 875 | // Keep the identity function around for default iterators. 876 | _.identity = function(value) { 877 | return value; 878 | }; 879 | 880 | // Run a function **n** times. 881 | _.times = function (n, iterator, context) { 882 | for (var i = 0; i < n; i++) iterator.call(context, i); 883 | }; 884 | 885 | // Escape a string for HTML interpolation. 886 | _.escape = function(string) { 887 | return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); 888 | }; 889 | 890 | // If the value of the named property is a function then invoke it; 891 | // otherwise, return it. 892 | _.result = function(object, property) { 893 | if (object == null) return null; 894 | var value = object[property]; 895 | return _.isFunction(value) ? value.call(object) : value; 896 | }; 897 | 898 | // Add your own custom functions to the Underscore object, ensuring that 899 | // they're correctly added to the OOP wrapper as well. 900 | _.mixin = function(obj) { 901 | each(_.functions(obj), function(name){ 902 | addToWrapper(name, _[name] = obj[name]); 903 | }); 904 | }; 905 | 906 | // Generate a unique integer id (unique within the entire client session). 907 | // Useful for temporary DOM ids. 908 | var idCounter = 0; 909 | _.uniqueId = function(prefix) { 910 | var id = idCounter++; 911 | return prefix ? prefix + id : id; 912 | }; 913 | 914 | // By default, Underscore uses ERB-style template delimiters, change the 915 | // following template settings to use alternative delimiters. 916 | _.templateSettings = { 917 | evaluate : /<%([\s\S]+?)%>/g, 918 | interpolate : /<%=([\s\S]+?)%>/g, 919 | escape : /<%-([\s\S]+?)%>/g 920 | }; 921 | 922 | // When customizing `templateSettings`, if you don't want to define an 923 | // interpolation, evaluation or escaping regex, we need one that is 924 | // guaranteed not to match. 925 | var noMatch = /.^/; 926 | 927 | // Certain characters need to be escaped so that they can be put into a 928 | // string literal. 929 | var escapes = { 930 | '\\': '\\', 931 | "'": "'", 932 | 'r': '\r', 933 | 'n': '\n', 934 | 't': '\t', 935 | 'u2028': '\u2028', 936 | 'u2029': '\u2029' 937 | }; 938 | 939 | for (var p in escapes) escapes[escapes[p]] = p; 940 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 941 | var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; 942 | 943 | // Within an interpolation, evaluation, or escaping, remove HTML escaping 944 | // that had been previously added. 945 | var unescape = function(code) { 946 | return code.replace(unescaper, function(match, escape) { 947 | return escapes[escape]; 948 | }); 949 | }; 950 | 951 | // JavaScript micro-templating, similar to John Resig's implementation. 952 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 953 | // and correctly escapes quotes within interpolated code. 954 | _.template = function(text, data, settings) { 955 | settings = _.defaults(settings || {}, _.templateSettings); 956 | 957 | // Compile the template source, taking care to escape characters that 958 | // cannot be included in a string literal and then unescape them in code 959 | // blocks. 960 | var source = "__p+='" + text 961 | .replace(escaper, function(match) { 962 | return '\\' + escapes[match]; 963 | }) 964 | .replace(settings.escape || noMatch, function(match, code) { 965 | return "'+\n_.escape(" + unescape(code) + ")+\n'"; 966 | }) 967 | .replace(settings.interpolate || noMatch, function(match, code) { 968 | return "'+\n(" + unescape(code) + ")+\n'"; 969 | }) 970 | .replace(settings.evaluate || noMatch, function(match, code) { 971 | return "';\n" + unescape(code) + "\n;__p+='"; 972 | }) + "';\n"; 973 | 974 | // If a variable is not specified, place data values in local scope. 975 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 976 | 977 | source = "var __p='';" + 978 | "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + 979 | source + "return __p;\n"; 980 | 981 | var render = new Function(settings.variable || 'obj', '_', source); 982 | if (data) return render(data, _); 983 | var template = function(data) { 984 | return render.call(this, data, _); 985 | }; 986 | 987 | // Provide the compiled function source as a convenience for build time 988 | // precompilation. 989 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + 990 | source + '}'; 991 | 992 | return template; 993 | }; 994 | 995 | // Add a "chain" function, which will delegate to the wrapper. 996 | _.chain = function(obj) { 997 | return _(obj).chain(); 998 | }; 999 | 1000 | // The OOP Wrapper 1001 | // --------------- 1002 | 1003 | // If Underscore is called as a function, it returns a wrapped object that 1004 | // can be used OO-style. This wrapper holds altered versions of all the 1005 | // underscore functions. Wrapped objects may be chained. 1006 | var wrapper = function(obj) { this._wrapped = obj; }; 1007 | 1008 | // Expose `wrapper.prototype` as `_.prototype` 1009 | _.prototype = wrapper.prototype; 1010 | 1011 | // Helper function to continue chaining intermediate results. 1012 | var result = function(obj, chain) { 1013 | return chain ? _(obj).chain() : obj; 1014 | }; 1015 | 1016 | // A method to easily add functions to the OOP wrapper. 1017 | var addToWrapper = function(name, func) { 1018 | wrapper.prototype[name] = function() { 1019 | var args = slice.call(arguments); 1020 | unshift.call(args, this._wrapped); 1021 | return result(func.apply(_, args), this._chain); 1022 | }; 1023 | }; 1024 | 1025 | // Add all of the Underscore functions to the wrapper object. 1026 | _.mixin(_); 1027 | 1028 | // Add all mutator Array functions to the wrapper. 1029 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1030 | var method = ArrayProto[name]; 1031 | wrapper.prototype[name] = function() { 1032 | var wrapped = this._wrapped; 1033 | method.apply(wrapped, arguments); 1034 | var length = wrapped.length; 1035 | if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; 1036 | return result(wrapped, this._chain); 1037 | }; 1038 | }); 1039 | 1040 | // Add all accessor Array functions to the wrapper. 1041 | each(['concat', 'join', 'slice'], function(name) { 1042 | var method = ArrayProto[name]; 1043 | wrapper.prototype[name] = function() { 1044 | return result(method.apply(this._wrapped, arguments), this._chain); 1045 | }; 1046 | }); 1047 | 1048 | // Start chaining a wrapped Underscore object. 1049 | wrapper.prototype.chain = function() { 1050 | this._chain = true; 1051 | return this; 1052 | }; 1053 | 1054 | // Extracts the result from a wrapped and chained object. 1055 | wrapper.prototype.value = function() { 1056 | return this._wrapped; 1057 | }; 1058 | 1059 | }).call(this); 1060 | -------------------------------------------------------------------------------- /lib/vector.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Vector.js 4 | Copyright 2012 Cameron Lakenen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | **/ 26 | 27 | function Vector(x, y, z) { 28 | if (x instanceof Vector) { 29 | return Vector.create(x); 30 | } 31 | this.x = x || 0; 32 | this.y = y || 0; 33 | this.z = z || 0; 34 | } 35 | Vector.prototype.add = function (v) { 36 | this.x += v.x; 37 | this.y += v.y; 38 | this.z += v.z; 39 | return this; 40 | }; 41 | Vector.prototype.scale = function (s) { 42 | this.x *= s; 43 | this.y *= s; 44 | this.z *= s; 45 | return this; 46 | }; 47 | Vector.prototype.direction = function () { 48 | return Math.atan2(this.y, this.x); 49 | }; 50 | Vector.prototype.magnitude = function () { 51 | return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); 52 | }; 53 | Vector.prototype.addToMagnitude = function (n) { 54 | n = n || 0; 55 | var mag = this.magnitude(); 56 | var magTransformation = Math.sqrt((n + mag) / mag); 57 | this.x *= magTransformation; 58 | this.y *= magTransformation; 59 | this.z *= magTransformation; 60 | return this; 61 | }; 62 | Vector.prototype.unit = function () { 63 | return this.scale(1/this.magnitude()); 64 | }; 65 | Vector.prototype.rotateZ = function (t) { 66 | var oldX = this.x; 67 | var oldY = this.y; 68 | this.x = oldX*Math.cos(t) - oldY*Math.sin(t); 69 | this.y = oldX*Math.sin(t) + oldY*Math.cos(t); 70 | return this; 71 | }; 72 | Vector.add = function (v1, v2) { 73 | return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); 74 | }; 75 | Vector.subtract = function (v1, v2) { 76 | return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); 77 | }; 78 | Vector.dot = function (v1, v2) { 79 | return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; 80 | }; 81 | Vector.scale = function (v, s) { 82 | return new Vector(v.x * s, v.y * s, v.z * s); 83 | }; 84 | Vector.cross = function (v1, v2) { 85 | return new Vector( 86 | v1.y * v2.z - v2.y * v1.z, 87 | v1.z * v2.x - v2.z * v1.x, 88 | v1.x * v2.y - v2.x * v1.y 89 | ); 90 | }; 91 | Vector.average = function () { 92 | var num, result = new Vector(), items = arguments; 93 | if (arguments[0].constructor.toString().indexOf('Array') != -1) 94 | items = arguments[0]; 95 | num = items.length; 96 | for (i = 0; i < num;i++) { 97 | result.add(Vector.create(items[i])); 98 | } 99 | return result.scale(1/num); 100 | }; 101 | Vector.create = function (o) { 102 | return new Vector(o.x, o.y, o.z); 103 | }; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teledraw-canvas", 3 | "description": "Teledraw Canvas is an HTML5 Canvas drawing engine that is used in [Teledraw.com](http://teledraw.com/). It is currently dependent on [Underscore.js](http://documentcloud.github.com/underscore/) (1.x.x).", 4 | "version": "0.14.1", 5 | "main": "dist/teledraw-canvas.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "devDependencies": { 10 | "grunt": "^0.4.5", 11 | "grunt-cli": "^0.1.13", 12 | "grunt-contrib-concat": "^0.5.0", 13 | "grunt-contrib-connect": "^0.8.0", 14 | "grunt-contrib-copy": "^0.7.0", 15 | "grunt-contrib-jshint": "^0.10.0", 16 | "grunt-contrib-uglify": "^0.6.0", 17 | "jquery": "^2.1.1" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/lakenen/teledraw-canvas" 25 | }, 26 | "keywords": [ 27 | "teledraw", 28 | "canvas", 29 | "drawing" 30 | ], 31 | "author": "Cameron Lakenen ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/lakenen/teledraw-canvas/issues" 35 | }, 36 | "homepage": "https://github.com/lakenen/teledraw-canvas" 37 | } 38 | -------------------------------------------------------------------------------- /src/canvas/teledraw-canvas.history.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TeledrawCanvas.History 3 | */ 4 | 5 | (function (TeledrawCanvas) { 6 | var History = function (canvas) { 7 | this.canvas = canvas; 8 | this.rev = 0; 9 | this.clear(); 10 | }; 11 | 12 | History.prototype.clear = function () { 13 | this.past = []; 14 | this.current = null; 15 | this.future = []; 16 | }; 17 | 18 | History.prototype.checkpoint = function () { 19 | if (this.past.length > this.canvas.state.maxHistory) { 20 | this.past.shift().destroy(); 21 | } 22 | 23 | if (this.current) { 24 | this.past.push(this.current); 25 | } 26 | this.current = new TeledrawCanvas.Snapshot(this.canvas); 27 | this.future = []; 28 | this.rev++; 29 | }; 30 | 31 | History.prototype.undo = function () { 32 | if (this._move(this.past, this.future)) { 33 | this.rev--; 34 | } 35 | }; 36 | 37 | History.prototype.redo = function () { 38 | if (this._move(this.future, this.past)) { 39 | this.rev++; 40 | } 41 | }; 42 | 43 | History.prototype._move = function(from, to) { 44 | if (!from.length) return false; 45 | if (!this.current) return false; 46 | to.push(this.current); 47 | this.current = from.pop(); 48 | this.current.restore(); 49 | return true; 50 | }; 51 | TeledrawCanvas.History = History; 52 | })(TeledrawCanvas); 53 | 54 | -------------------------------------------------------------------------------- /src/canvas/teledraw-canvas.snapshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TeledrawCanvas.Snapshot 3 | */ 4 | 5 | (function (TeledrawCanvas) { 6 | var Snapshot = function (canvas) { 7 | this.canvas = canvas; 8 | if (!this.canvas._snapshotBuffers) { 9 | this.canvas._snapshotBuffers = []; 10 | } 11 | this._snapshotBufferCanvas(); 12 | }; 13 | 14 | Snapshot.prototype.restore = function (stroke) { 15 | var tl, br; 16 | if (stroke) { 17 | tl = stroke.tl; 18 | br = stroke.br; 19 | } else { 20 | tl = { x:0, y:0 }; 21 | br = { x:this.canvas.canvas().width, y:this.canvas.canvas().height }; 22 | } 23 | this._restoreBufferCanvas(tl, br); 24 | this.canvas.updateDisplayCanvas(false, tl, br); 25 | }; 26 | 27 | Snapshot.prototype.destroy = function () { 28 | this._putBufferCtx(); 29 | }; 30 | 31 | Snapshot.prototype.toDataURL = function () { 32 | return this.buffer && this.buffer.toDataURL(); 33 | }; 34 | 35 | // doing this with a buffer canvas instead of get/put image data seems to be significantly faster 36 | Snapshot.prototype._snapshotBufferCanvas = function () { 37 | this._getBufferCtx(); 38 | this.buffer.drawImage(this.canvas.canvas(), 0, 0); 39 | }; 40 | 41 | Snapshot.prototype._restoreBufferCanvas = function (tl, br) { 42 | var ctx = this.canvas.ctx(); 43 | 44 | var w = br.x - tl.x, h = br.y - tl.y; 45 | if (w === 0 || h === 0) { 46 | return; 47 | } 48 | ctx.clearRect(tl.x, tl.y, w, h); 49 | ctx.drawImage(this.buffer.canvas, tl.x, tl.y, w, h, tl.x, tl.y, w, h); 50 | }; 51 | 52 | Snapshot.prototype._snapshotImageData = function () { 53 | this.data = this.canvas.getImageData(); 54 | }; 55 | 56 | Snapshot.prototype._restoreImageData = function () { 57 | this.canvas.putImageData(this.data); 58 | }; 59 | 60 | Snapshot.prototype._putBufferCtx = function () { 61 | if (this.buffer) { 62 | this.canvas._snapshotBuffers.push(this.buffer); 63 | } 64 | this.buffer = null; 65 | }; 66 | 67 | Snapshot.prototype._getBufferCtx = function () { 68 | var ctx; 69 | if (!this.buffer) { 70 | if (this.canvas._snapshotBuffers.length) { 71 | ctx = this.canvas._snapshotBuffers.pop(); 72 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 73 | } else { 74 | ctx = this.canvas.getTempCanvas().getContext('2d'); 75 | } 76 | } 77 | this.buffer = ctx; 78 | }; 79 | 80 | TeledrawCanvas.Snapshot = Snapshot; 81 | })(TeledrawCanvas); 82 | 83 | -------------------------------------------------------------------------------- /src/canvas/teledraw-canvas.stroke.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TeledrawCanvas.Stroke 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Stroke = function (canvas) { 6 | this.canvas = canvas; 7 | }; 8 | 9 | Stroke.prototype.start = function (pt) {}; 10 | Stroke.prototype.move = function (pt1, pt2) {}; 11 | Stroke.prototype.end = function () {}; 12 | Stroke.prototype.draw = function () {}; 13 | 14 | Stroke.prototype.save = function () { 15 | this.snapshot = new TeledrawCanvas.Snapshot(this.canvas); 16 | }; 17 | 18 | Stroke.prototype.restore = function () { 19 | this.snapshot.restore(this); 20 | }; 21 | 22 | Stroke.prototype.destroy = function () { 23 | this.snapshot.destroy(); 24 | }; 25 | 26 | TeledrawCanvas.Stroke = Stroke; 27 | })(TeledrawCanvas); 28 | 29 | -------------------------------------------------------------------------------- /src/canvas/teledraw-canvas.tool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TeledrawCanvas.Tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Tool = function () {}; 6 | 7 | Tool.prototype.down = function (pt) {}; 8 | Tool.prototype.up = function (pt) {}; 9 | Tool.prototype.move = function (mouseDown, from, to) {}; 10 | Tool.prototype.dblclick = function (pt) {}; 11 | Tool.prototype.enter = function (mouseDown, pt) {}; 12 | Tool.prototype.leave = function (mouseDown, pt) {}; 13 | Tool.prototype.keydown = function (mouseDown, key) { 14 | if (key === 16) { 15 | this.shiftKey = true; 16 | if (mouseDown) { 17 | this.updateBoundaries(); 18 | this.draw(); 19 | } 20 | } 21 | }; 22 | Tool.prototype.keyup = function (mouseDown, key) { 23 | if (key === 16) { 24 | this.shiftKey = false; 25 | if (mouseDown) { 26 | this.updateBoundaries(); 27 | this.draw(); 28 | } 29 | } 30 | }; 31 | 32 | // should return a preview image (canvas) for this tool 33 | Tool.prototype.preview = function (w, h) { 34 | return new TeledrawCanvas.Canvas(w || 100, h || 100); 35 | }; 36 | 37 | // A factory for creating tools 38 | Tool.createTool = function (name, cursor, ctor) { 39 | var Stroke = function (canvas, ctx) { 40 | this.canvas = canvas; 41 | this.ctx = ctx || canvas.ctx(); 42 | this.color = canvas.getColor(); 43 | this.color.push(canvas.getAlpha()); 44 | this.points = []; 45 | this.tl = {x: this.ctx.canvas.width, y: this.ctx.canvas.height}; 46 | this.br = {x: 0, y: 0}; 47 | this.tool = {}; 48 | }; 49 | Stroke.prototype = new TeledrawCanvas.Stroke(); 50 | 51 | var tool = function (canvas) { 52 | this.canvas = canvas; 53 | canvas.cursor(cursor); 54 | this.name = name; 55 | this.cursor = cursor || 'default'; 56 | this.currentStroke = null; 57 | 58 | if (typeof ctor=='function') { 59 | ctor.call(this); 60 | } 61 | }; 62 | 63 | tool.prototype = new Tool(); 64 | 65 | tool.prototype.down = function (pt) { 66 | this.currentStroke = new Stroke(this.canvas); 67 | this.currentStroke.tool = this; 68 | this.currentStroke.save(); 69 | this.currentStroke.points.push(pt); 70 | this.currentStroke.start(pt); 71 | this.updateBoundaries(pt); 72 | this.draw(); 73 | }; 74 | 75 | tool.prototype.move = function (mdown, from, to) { 76 | if (mdown && this.currentStroke) { 77 | this.currentStroke.points.push(to); 78 | this.currentStroke.move(from, to); 79 | this.updateBoundaries(to); 80 | this.draw(); 81 | } 82 | }; 83 | 84 | tool.prototype.up = function (pt) { 85 | if (this.currentStroke) { 86 | this.currentStroke.end(pt); 87 | this.draw(); 88 | this.currentStroke.destroy(); 89 | this.currentStroke = null; 90 | this.canvas.history.checkpoint(); 91 | } 92 | this.canvas.trigger('tool.up'); 93 | }; 94 | 95 | tool.prototype.draw = function () { 96 | this.currentStroke.ctx.save(); 97 | this.currentStroke.restore(); 98 | this.currentStroke.draw(); 99 | this.canvas.updateDisplayCanvas(false, this.currentStroke.tl, this.currentStroke.br); 100 | this.currentStroke.ctx.restore(); 101 | }; 102 | 103 | tool.prototype.updateBoundaries = function (pt) { 104 | var stroke = this.currentStroke, 105 | canvas = stroke.ctx.canvas, 106 | strokeSize = this.canvas.state.shadowBlur + this.canvas.state.lineWidth; 107 | if (this.shiftKey) { 108 | // hack to avoid bugginess when shift keying for ellipse, line and rect 109 | stroke.tl.x = stroke.tl.y = 0; 110 | stroke.br.x = canvas.width; 111 | stroke.br.y = canvas.height; 112 | return; 113 | } 114 | if (pt) { 115 | if (pt.x - strokeSize < stroke.tl.x) { 116 | stroke.tl.x = clamp(floor(pt.x - strokeSize), 0, canvas.width); 117 | } 118 | if (pt.x + strokeSize > stroke.br.x) { 119 | stroke.br.x = clamp(floor(pt.x + strokeSize), 0, canvas.width); 120 | } 121 | if (pt.y - strokeSize < stroke.tl.y) { 122 | stroke.tl.y = clamp(floor(pt.y - strokeSize), 0, canvas.height); 123 | } 124 | if (pt.y + strokeSize > stroke.br.y) { 125 | stroke.br.y = clamp(floor(pt.y + strokeSize), 0, canvas.height); 126 | } 127 | } 128 | }; 129 | 130 | tool.stroke = Stroke; 131 | Stroke.tool = tool; 132 | TeledrawCanvas.tools[name] = tool; 133 | return tool; 134 | }; 135 | 136 | TeledrawCanvas.Tool = Tool; 137 | TeledrawCanvas.tools = {}; 138 | })(TeledrawCanvas); 139 | 140 | -------------------------------------------------------------------------------- /src/canvas/tools/arrow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Arrow tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Arrow = TeledrawCanvas.Tool.createTool("arrow", "crosshair"); 6 | var ArrowStroke = Arrow.stroke.prototype; 7 | var LineArrow = TeledrawCanvas.Tool.createTool("line-arrow", "crosshair"); 8 | var LineArrowStroke = LineArrow.stroke.prototype; 9 | 10 | function calculateArrowVectors(a, b, edgeLength) { 11 | var currv, tmpv, leftv, rightv, frontv; 12 | tmpv = new Vector(a.x, a.y); 13 | currv = new Vector(b.x, b.y); 14 | tmpv = Vector.subtract(currv, tmpv).unit(); 15 | leftv = new Vector(tmpv).rotateZ(Math.PI/2).scale(edgeLength).add(currv); 16 | rightv = new Vector(tmpv).rotateZ(-Math.PI/2).scale(edgeLength).add(currv); 17 | frontv = new Vector(tmpv).scale(edgeLength).add(currv); 18 | return { 19 | left: leftv, 20 | right: rightv, 21 | front: frontv 22 | }; 23 | } 24 | 25 | var updateBoundaries = Arrow.prototype.updateBoundaries; 26 | Arrow.prototype.updateBoundaries = LineArrow.prototype.updateBoundaries = function () { 27 | var tool = this; 28 | // update the box if the arrow falls outside 29 | if (this.currentStroke.last && this.currentStroke.current) { 30 | var a = this.currentStroke.last, 31 | b = this.currentStroke.current, 32 | edgeLength = this.currentStroke.calculateEdgeLength(), 33 | vectors = calculateArrowVectors(a, b, edgeLength); 34 | _.values(vectors).forEach(function (pt) { 35 | updateBoundaries.call(tool, pt); 36 | }); 37 | } 38 | }; 39 | 40 | Arrow.prototype.preview = LineArrow.prototype.preview = function () { 41 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 42 | var ctx = canv.getContext('2d'); 43 | ctx.fillStyle = 'black'; 44 | ctx.fillRect(0, 0, canv.width, canv.height); 45 | var stroke = new Arrow.stroke(this.canvas, ctx); 46 | stroke.points = [{ x: canv.width * 0.25, y: canv.height * 0.25 }, { x: canv.width * 0.75, y: canv.height * 0.75 }]; 47 | stroke.draw(); 48 | return canv; 49 | }; 50 | 51 | 52 | ArrowStroke.lineWidth = LineArrowStroke.lineWidth = 1; 53 | ArrowStroke.lineCap = LineArrowStroke.lineCap = 'round'; 54 | 55 | ArrowStroke.calculateEdgeLength = LineArrowStroke.calculateEdgeLength = function () { 56 | return Math.max(10, this.canvas.state.lineWidth * 2); 57 | }; 58 | ArrowStroke.drawArrow = function () { 59 | var state = this.canvas.state, 60 | ctx = this.ctx, 61 | edgeLength = this.calculateEdgeLength(), 62 | vectors; 63 | 64 | if (this.last && this.current) { 65 | vectors = calculateArrowVectors(this.last, this.current, edgeLength); 66 | ctx.beginPath(); 67 | ctx.moveTo(this.current.x, this.current.y); 68 | ctx.lineTo(vectors.left.x, vectors.left.y); 69 | ctx.lineTo(vectors.front.x, vectors.front.y); 70 | ctx.lineTo(vectors.right.x, vectors.right.y); 71 | ctx.lineTo(this.current.x, this.current.y); 72 | ctx.closePath(); 73 | ctx.fill(); 74 | } 75 | }; 76 | 77 | LineArrowStroke.start = function () { 78 | TeledrawCanvas.tools.line.stroke.prototype.start.apply(this, arguments); 79 | }; 80 | 81 | ArrowStroke.move = function (a, b) { 82 | this.last = a; 83 | this.current = b; 84 | }; 85 | LineArrowStroke.move = function (a, b) { 86 | TeledrawCanvas.tools.line.stroke.prototype.move.apply(this, arguments); 87 | this.last = this.first; 88 | this.current = this.second; 89 | }; 90 | 91 | ArrowStroke.draw = function () { 92 | TeledrawCanvas.tools.pencil.stroke.prototype.draw.call(this); 93 | ArrowStroke.drawArrow.call(this); 94 | }; 95 | LineArrowStroke.draw = function () { 96 | TeledrawCanvas.tools.line.stroke.prototype.draw.call(this); 97 | // update points because they might be using shift key 98 | this.last = this.points[0]; 99 | this.current = this.points[1]; 100 | ArrowStroke.drawArrow.call(this); 101 | }; 102 | 103 | ArrowStroke.end = function (pt) { 104 | this.current = pt; 105 | }; 106 | LineArrowStroke.end = function () { 107 | TeledrawCanvas.tools.line.stroke.prototype.end.apply(this, arguments); 108 | ArrowStroke.end.apply(this, arguments); 109 | }; 110 | })(TeledrawCanvas); 111 | 112 | -------------------------------------------------------------------------------- /src/canvas/tools/ellipse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ellipse tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Ellipse = TeledrawCanvas.Tool.createTool("ellipse", "crosshair"), 6 | EllipseStrokePrototype = Ellipse.stroke.prototype; 7 | 8 | Ellipse.prototype.preview = function () { 9 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 10 | var ctx = canv.getContext('2d'); 11 | var stroke = new Ellipse.stroke(this.canvas, ctx); 12 | stroke.first = { x: 0, y: 0 }; 13 | stroke.second = { x: canv.width, y: canv.height }; 14 | stroke.draw(); 15 | return canv; 16 | }; 17 | 18 | EllipseStrokePrototype.bgColor = [255, 255, 255]; 19 | EllipseStrokePrototype.bgAlpha = 0; 20 | EllipseStrokePrototype.lineWidth = 1; 21 | 22 | EllipseStrokePrototype.start = function (pt) { 23 | this.first = pt; 24 | }; 25 | 26 | EllipseStrokePrototype.move = function (a, b) { 27 | this.second = b; 28 | }; 29 | 30 | EllipseStrokePrototype.end = function (pt) { 31 | this.second = pt; 32 | }; 33 | 34 | EllipseStrokePrototype.draw = function () { 35 | if (!this.first || !this.second) return; 36 | var self = this, 37 | x = self.first.x, 38 | y = self.first.y, 39 | w = self.second.x - x, 40 | h = self.second.y - y, 41 | ctx = self.ctx, 42 | state = self.canvas.state, 43 | shadowOffset = state.shadowOffset, 44 | shadowBlur = state.shadowBlur, 45 | lineWidth = state.lineWidth, 46 | color = TeledrawCanvas.util.cssColor(state.color); 47 | 48 | ctx.lineJoin = ctx.lineCap = "round"; 49 | ctx.globalAlpha = state.globalAlpha; 50 | ctx.fillStyle = ctx.strokeStyle = color; 51 | ctx.miterLimit = 100000; 52 | 53 | if (self.tool.shiftKey) { 54 | h = self.second.y > y ? abs(w) : -abs(w); 55 | } 56 | 57 | if (self.tool.fill) { 58 | drawEllipse(ctx, x, y, w, h); 59 | ctx.fill(); 60 | } else { 61 | if (shadowBlur > 0) { 62 | ctx.shadowColor = color; 63 | ctx.shadowOffsetX = ctx.shadowOffsetY = shadowOffset; 64 | ctx.shadowBlur = shadowBlur; 65 | ctx.translate(-shadowOffset,-shadowOffset); 66 | } 67 | 68 | ctx.lineWidth = lineWidth; 69 | drawEllipse(ctx, x, y, w, h); 70 | ctx.stroke(); 71 | } 72 | }; 73 | 74 | function drawEllipse(ctx, x, y, w, h) { 75 | var kappa = 0.5522848, 76 | ox = (w / 2) * kappa, // control point offset horizontal 77 | oy = (h / 2) * kappa, // control point offset vertical 78 | xe = x + w, // x-end 79 | ye = y + h, // y-end 80 | xm = x + w / 2, // x-middle 81 | ym = y + h / 2; // y-middle 82 | 83 | ctx.beginPath(); 84 | ctx.moveTo(x, ym); 85 | ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); 86 | ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); 87 | ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); 88 | ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); 89 | ctx.closePath(); 90 | } 91 | 92 | 93 | var FilledEllipse = TeledrawCanvas.Tool.createTool("filled-ellipse", "crosshair"); 94 | 95 | FilledEllipse.prototype.preview = function () { 96 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 97 | var ctx = canv.getContext('2d'); 98 | var stroke = new FilledEllipse.stroke(this.canvas, ctx); 99 | stroke.tool = this; 100 | var w = canv.width, h = canv.height; 101 | stroke.first = { x: w*0.1, y: h*0.1 }; 102 | stroke.second = { x: w*0.9, y: h*0.9 }; 103 | console.log(stroke); 104 | stroke.draw(); 105 | return canv; 106 | }; 107 | _.extend(FilledEllipse.stroke.prototype, Ellipse.stroke.prototype); 108 | FilledEllipse.prototype.fill = true; 109 | 110 | })(TeledrawCanvas); 111 | 112 | -------------------------------------------------------------------------------- /src/canvas/tools/eraser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Eraser tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Eraser = TeledrawCanvas.Tool.createTool("eraser", "crosshair"); 6 | 7 | Eraser.prototype.preview = function () { 8 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 9 | var ctx = canv.getContext('2d'); 10 | ctx.fillStyle = 'black'; 11 | ctx.fillRect(0, 0, canv.width, canv.height); 12 | var stroke = new Eraser.stroke(this.canvas, ctx); 13 | stroke.points = [{ x: canv.width/2, y: canv.height/2 }]; 14 | stroke.draw(); 15 | return canv; 16 | } 17 | 18 | 19 | Eraser.stroke.prototype.lineWidth = 1; 20 | Eraser.stroke.prototype.lineCap = 'round'; 21 | 22 | Eraser.stroke.prototype.draw = function () { 23 | this.color = [255, 255, 255, 255]; 24 | this.ctx.globalCompositeOperation = 'destination-out'; 25 | TeledrawCanvas.tools.pencil.stroke.prototype.draw.call(this); 26 | }; 27 | })(TeledrawCanvas); 28 | 29 | -------------------------------------------------------------------------------- /src/canvas/tools/eyedropper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Eyedropper tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var ctor = function () { 6 | this.previewContainer = document.createElement('div'); 7 | _.extend(this.previewContainer.style, { 8 | position: 'absolute', 9 | width: '10px', 10 | height: '10px', 11 | border: '1px solid black', 12 | display: 'none' 13 | }); 14 | document.body.appendChild(this.previewContainer); 15 | if (this.canvas.state.mouseOver) { 16 | this.previewContainer.style.display = 'block'; 17 | } 18 | }; 19 | var EyeDropper = TeledrawCanvas.Tool.createTool("eyedropper", "crosshair", ctor); 20 | 21 | EyeDropper.prototype.preview = function () { 22 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 23 | var ctx = canv.getContext('2d'); 24 | ctx.fillStyle = TeledrawCanvas.util.cssColor(this.color || [0,0,0,0]); 25 | ctx.fillRect(0, 0, canv.width, canv.height); 26 | return canv; 27 | }; 28 | 29 | EyeDropper.prototype.pick = function (pt) { 30 | var none, lightness, 31 | previewContainer = this.previewContainer, 32 | left = this.canvas.element.offsetLeft, 33 | top = this.canvas.element.offsetTop, 34 | pixel = this.canvas._displayCtx.getImageData(pt.xd,pt.yd,1,1).data; 35 | this.color = TeledrawCanvas.util.rgba2rgb(Array.prototype.slice.call(pixel)); 36 | lightness = TeledrawCanvas.util.rgb2hsl(this.color)[2]; 37 | _.extend(previewContainer.style, { 38 | left: (left + pt.xd + 15) + 'px', 39 | top: (top + pt.yd + 5) + 'px', 40 | background: TeledrawCanvas.util.cssColor(this.color), 41 | 'border-color': lightness >= 50 ? '#000' : '#888' 42 | }); 43 | if (this.canvas.state.mouseOver) { 44 | // hack for chrome, since it seems to ignore this and not redraw for some reason... 45 | previewContainer.style.display='none'; 46 | none = previewContainer.offsetHeight; // no need to store this anywhere, the reference is enough 47 | previewContainer.style.display='block'; 48 | } else { 49 | previewContainer.style.display = 'none'; 50 | } 51 | }; 52 | 53 | EyeDropper.prototype.enter = function () { 54 | this.previewContainer.style.display = 'block'; 55 | }; 56 | 57 | EyeDropper.prototype.leave = function () { 58 | this.previewContainer.style.display = 'none'; 59 | }; 60 | 61 | EyeDropper.prototype.move = function (down, from, pt) { 62 | this.pick(pt); 63 | }; 64 | 65 | EyeDropper.prototype.down = function (pt) { 66 | this.pick(pt); 67 | }; 68 | 69 | EyeDropper.prototype.up = function (pt) { 70 | this.pick(pt); 71 | this.canvas.setColor(this.color); 72 | this.previewContainer.parentNode.removeChild(this.previewContainer); 73 | this.canvas.previousTool(); 74 | }; 75 | })(TeledrawCanvas); 76 | 77 | -------------------------------------------------------------------------------- /src/canvas/tools/fill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fill tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Fill = TeledrawCanvas.Tool.createTool("fill", "crosshair"); 6 | 7 | Fill.prototype.preview = function () { 8 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 9 | var ctx = canv.getContext('2d'); 10 | ctx.fillStyle = TeledrawCanvas.util.cssColor(this.canvas.getColor()); 11 | ctx.fillRect(0, 0, canv.width, canv.height); 12 | return canv; 13 | } 14 | 15 | Fill.blur = true; 16 | 17 | Fill.stroke.prototype.end = function (target) { 18 | var w = this.ctx.canvas.width, h = this.ctx.canvas.height; 19 | var pixels = this.ctx.getImageData(0,0, w,h); 20 | var fill_mask = this.ctx.createImageData(w,h); 21 | var color = this.color; 22 | color[3]*=0xFF; 23 | floodFillScanlineStack(pixels.data, fill_mask.data, target, w, h, this.color); 24 | this.tmp_canvas = this.canvas.getTempCanvas(); 25 | var tmp_ctx = this.tmp_canvas.getContext('2d'); 26 | tmp_ctx.putImageData(fill_mask, 0, 0); 27 | 28 | if (Fill.blur) { 29 | stackBlurCanvasRGBA(this.tmp_canvas, 1); 30 | var tmp_data = tmp_ctx.getImageData(0, 0, w, h); 31 | for (var i = 0, l = tmp_data.data.length; i < l; i += 4) { 32 | if (tmp_data.data[i+3]/0xFF > 0.2) { 33 | tmp_data.data[i] = color[0]; 34 | tmp_data.data[i+1] = color[1]; 35 | tmp_data.data[i+2] = color[2]; 36 | tmp_data.data[i+3] = Math.min(color[3], tmp_data.data[i+3] * 3); 37 | } 38 | } 39 | tmp_ctx.putImageData(tmp_data, 0, 0); 40 | } 41 | }; 42 | 43 | Fill.stroke.prototype.draw = function () { 44 | if (this.tmp_canvas) { 45 | this.ctx.drawImage(this.tmp_canvas, 0, 0); 46 | } 47 | }; 48 | 49 | function floodFillScanlineStack(dataFrom, dataTo, target, w, h, newColor) { 50 | var stack = [[target.x, target.y]]; 51 | var oldColor = getColor(dataFrom, target.x, target.y, w); 52 | var tolerance = Fill.tolerance; 53 | var spanLeft, spanRight; 54 | var color, dist, pt, x, y, y1; 55 | var oppColor = TeledrawCanvas.util.opposite(oldColor); 56 | oppColor[3]/=2; 57 | while (stack.length) { 58 | pt = stack.pop(); 59 | x = pt[0]; 60 | y1 = y = pt[1]; 61 | while (y1 >= 0 && colorsEqual(getColor(dataFrom, x, y1, w), oldColor)) y1--; 62 | y1++; 63 | spanLeft = spanRight = false; 64 | 65 | while(y1 < h && colorsEqual(getColor(dataFrom, x, y1, w), oldColor)) 66 | { 67 | setColor(dataFrom, x, y1, w, oppColor); 68 | setColor(dataTo, x, y1, w, newColor); 69 | if (!spanLeft && x > 0 && colorsEqual(getColor(dataFrom, x - 1, y1, w), oldColor)) 70 | { 71 | stack.push([x - 1, y1]); 72 | spanLeft = true; 73 | } 74 | else if (spanLeft && x > 0 && colorsEqual(getColor(dataFrom, x - 1, y1, w), oldColor)) 75 | { 76 | spanLeft = false; 77 | } 78 | if (!spanRight && x < w - 1 && colorsEqual(getColor(dataFrom, x + 1, y1, w), oldColor)) 79 | { 80 | stack.push([x + 1, y1]); 81 | spanRight = true; 82 | } 83 | else if (spanRight && x < w - 1 && colorsEqual(getColor(dataFrom, x + 1, y1, w), oldColor)) 84 | { 85 | spanRight = false; 86 | } 87 | y1++; 88 | } 89 | } 90 | } 91 | 92 | function getColor(data, x, y, w) { 93 | var start = (y * w + x) * 4; 94 | return [ 95 | data[start], 96 | data[start+1], 97 | data[start+2], 98 | data[start+3] 99 | ]; 100 | } 101 | 102 | function setColor(data, x, y, w, color) { 103 | var start = (y * w + x) * 4; 104 | data[start] = color[0]; 105 | data[start+1] = color[1]; 106 | data[start+2] = color[2]; 107 | data[start+3] = color[3]; 108 | } 109 | 110 | function colorDistance(col1, col2) { 111 | return abs(col1[0] - col2[0]) + 112 | abs(col1[1] - col2[1]) + 113 | abs(col1[2] - col2[2]) + 114 | abs(col1[3] - col2[3]); 115 | } 116 | 117 | function colorsEqual(col1, col2) { 118 | return colorDistance(col1, col2) < 5; 119 | } 120 | })(TeledrawCanvas); 121 | 122 | -------------------------------------------------------------------------------- /src/canvas/tools/grab.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grab tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var cursorUp = "hand, grab, -moz-grab, -webkit-grab, move", 6 | cursorDown = "grabbing, -moz-grabbing, -webkit-grabbing, move"; 7 | 8 | var Grab = TeledrawCanvas.Tool.createTool("grab", cursorUp); 9 | 10 | Grab.prototype.move = function (down, from, to) { 11 | var self = this; 12 | if (down) { 13 | clearTimeout(this._clearDeltasId); 14 | cancelAnimationFrame(this._momentumId); 15 | this.dx = to.xd - from.xd; 16 | this.dy = to.yd - from.yd; 17 | this.canvas.pan(this.dx, this.dy); 18 | this._clearDeltasId = setTimeout(function () { 19 | self.dx = self.dy = 0; 20 | }, 100); 21 | } 22 | }; 23 | 24 | Grab.prototype.down = function (pt) { 25 | cancelAnimationFrame(this._momentumId); 26 | this.canvas.cursor(cursorDown); 27 | }; 28 | 29 | Grab.prototype.up = function (pt) { 30 | cancelAnimationFrame(this._momentumId); 31 | this.momentum(this.dx, this.dy); 32 | this.dx = this.dy = 0; 33 | this.canvas.cursor(cursorUp); 34 | }; 35 | 36 | Grab.prototype.dblclick = function (pt) { 37 | cancelAnimationFrame(this._momentumId); 38 | this.dx = this.dy = 0; 39 | var mult = 2; 40 | if (this.shiftKey) { 41 | mult /= 4; 42 | } 43 | this.canvas.zoom(this.canvas.state.currentZoom*mult, pt.xd, pt.yd); 44 | }; 45 | 46 | Grab.prototype.momentum = function (dx, dy) { 47 | var self = this; 48 | if (Math.abs(dx) >= 1 || Math.abs(dy) >= 1) { 49 | dx /= 1.1; 50 | dy /= 1.1; 51 | this.canvas.pan(dx, dy); 52 | this._momentumId = requestAnimationFrame(function () { 53 | self.momentum(dx, dy); 54 | }); 55 | } 56 | } 57 | })(TeledrawCanvas); 58 | -------------------------------------------------------------------------------- /src/canvas/tools/line.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Line tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Line = TeledrawCanvas.Tool.createTool("line", "crosshair"); 6 | 7 | Line.prototype.preview = function () { 8 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 9 | var ctx = canv.getContext('2d'); 10 | var stroke = new Line.stroke(this.canvas, ctx); 11 | stroke.first = { x: 0, y: 0 }; 12 | stroke.second = { x: canv.width, y: canv.height }; 13 | stroke.draw(); 14 | return canv; 15 | } 16 | 17 | Line.stroke.prototype.lineCap = 'round'; 18 | 19 | Line.stroke.prototype.start = function (pt) { 20 | this.first = pt; 21 | }; 22 | 23 | Line.stroke.prototype.move = function (a, b) { 24 | this.second = b; 25 | }; 26 | 27 | Line.stroke.prototype.end = function (pt) { 28 | this.second = pt; 29 | }; 30 | 31 | Line.stroke.prototype.drawLine = function () { 32 | if (!this.first || !this.second) return; 33 | var first = _.extend({}, this.first), 34 | second = _.extend({}, this.second), 35 | a, x, y, pi = Math.PI; 36 | delete first.p; 37 | delete second.p; 38 | if (this.tool.shiftKey) { 39 | x = second.x - first.x; 40 | y = second.y - first.y; 41 | a = Math.atan2(y, x); 42 | 43 | if ((a >= -pi*7/8 && a < -pi*5/8) || 44 | (a >= -pi*3/8 && a < -pi/8)) 45 | { 46 | second.y = first.y - Math.abs(x); // NW, NE 47 | } else 48 | if ((a >= -pi*5/8 && a < -pi*3/8) || 49 | (a >= pi*3/8 && a < pi*5/8)) 50 | { 51 | second.x = first.x; // N, S 52 | } else 53 | if ((a >= pi/8 && a < pi*3/8) || 54 | (a >= pi*5/8 && a < pi*7/8)) 55 | { 56 | second.y = first.y + Math.abs(x); // SE, SW 57 | } else { 58 | second.y = first.y; // E, W 59 | } 60 | } 61 | this.points = [first, second]; 62 | }; 63 | 64 | Line.stroke.prototype.draw = function () { 65 | Line.stroke.prototype.drawLine.call(this); 66 | TeledrawCanvas.tools.pencil.stroke.prototype.draw.call(this); 67 | }; 68 | })(TeledrawCanvas); 69 | 70 | -------------------------------------------------------------------------------- /src/canvas/tools/pencil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pencil tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Pencil = TeledrawCanvas.Tool.createTool("pencil", "crosshair"); 6 | 7 | Pencil.prototype.preview = function () { 8 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 9 | var ctx = canv.getContext('2d'); 10 | var stroke = new Pencil.stroke(this.canvas, ctx); 11 | stroke.points = [{ x: canv.width/2, y: canv.height/2 }]; 12 | stroke.draw(); 13 | return canv; 14 | } 15 | 16 | Pencil.stroke.prototype.lineCap = 'round'; 17 | Pencil.stroke.prototype.smoothing = true; 18 | 19 | Pencil.stroke.prototype.draw = function () { 20 | var state = this.canvas.state, 21 | ctx = this.ctx, 22 | points = this.points, 23 | shadowOffset = state.shadowOffset, 24 | shadowBlur = state.shadowBlur, 25 | lineWidth = state.lineWidth, 26 | color = TeledrawCanvas.util.cssColor(state.color); 27 | 28 | ctx.globalAlpha = state.globalAlpha; 29 | ctx.fillStyle = ctx.strokeStyle = color; 30 | ctx.miterLimit = 100000; 31 | if (shadowBlur > 0) { 32 | ctx.shadowColor = color; 33 | ctx.shadowOffsetX = ctx.shadowOffsetY = shadowOffset; 34 | ctx.shadowBlur = shadowBlur; 35 | ctx.translate(-shadowOffset,-shadowOffset); 36 | } 37 | 38 | if (points.length === 1) { 39 | // draw a single point 40 | switch (this.lineCap) { 41 | case 'round': 42 | ctx.beginPath(); 43 | if (points[0].p) { 44 | lineWidth *= points[0].p * 2; 45 | } 46 | ctx.arc(points[0].x, points[0].y, lineWidth / 2, 0, 2 * Math.PI, true); 47 | ctx.closePath(); 48 | ctx.fill(); 49 | break; 50 | case 'square': 51 | ctx.fillRect(points[0].x - lineWidth/2, points[0].y - lineWidth/2, lineWidth, lineWidth); 52 | } 53 | } else if (points.length > 1) { 54 | ctx.lineJoin = 'round'; 55 | ctx.lineCap = this.lineCap; 56 | ctx.lineWidth = lineWidth; 57 | 58 | if (points[0].p || points[1].p) { 59 | ctx.beginPath(); 60 | drawLine(ctx, generatePressurePoints(points, lineWidth), this.smoothing); 61 | ctx.closePath(); 62 | ctx.fill(); 63 | } else { 64 | ctx.beginPath(); 65 | drawLine(ctx, points, this.smoothing); 66 | ctx.stroke(); 67 | } 68 | } 69 | }; 70 | 71 | function generatePressurePoints(points, thickness) { 72 | var path = {left:[], right:[]}, 73 | len = points.length, 74 | lastp = points[0], 75 | lastv = new Vector(lastp.x, lastp.y), 76 | currp, currv, left, right, tmp; 77 | for (var i = 1, l = len; i < l; ++i) { 78 | currp = points[i]; 79 | 80 | // ignore this point if they didn't actually move 81 | if (currp.x === lastp.x && currp.y === lastp.y) continue; 82 | 83 | currv = new Vector(currp.x, currp.y); 84 | left = Vector.subtract(currv, lastv).unit().rotateZ(Math.PI/2); 85 | right = Vector.subtract(currv, lastv).unit().rotateZ(-Math.PI/2); 86 | 87 | tmp = Vector(left).scale(lastp.p*thickness).add(lastv); 88 | path.left.push({ x: tmp.x, y: tmp.y }); 89 | 90 | tmp = Vector(right).scale(lastp.p*thickness).add(lastv); 91 | path.right.unshift({ x: tmp.x, y: tmp.y }); 92 | 93 | lastp = currp; 94 | lastv = currv; 95 | } 96 | 97 | 98 | //add the last points 99 | tmp = Vector(left).scale(lastp.p*thickness).add(lastv); 100 | path.left.push({ x: tmp.x, y: tmp.y }); 101 | 102 | tmp = Vector(right).scale(lastp.p*thickness).add(lastv); 103 | path.right.unshift({ x: tmp.x, y: tmp.y }); 104 | 105 | // combine them into one full path 106 | result = path.left.concat(path.right); 107 | result.push(path.left[0]); 108 | return result; 109 | } 110 | 111 | function drawLine(ctx, points, smoothing) { 112 | if (points.length === 0) return; 113 | ctx.moveTo(points[0].x, points[0].y); 114 | var prev = points[0], 115 | prevprev = null, curr = prev, len = points.length; 116 | for (var i = 1, l = len; i < l; ++i) { 117 | curr = points[i]; 118 | 119 | if (prevprev && (prevprev.x == curr.x || prevprev.y == curr.y)) { 120 | // hack to avoid weird linejoins cutting the line 121 | curr.x += 0.1; 122 | curr.y += 0.1; 123 | } 124 | if (smoothing) { 125 | var mid = {x:(prev.x+curr.x)/2, y: (prev.y+curr.y)/2}; 126 | ctx.quadraticCurveTo(prev.x, prev.y, mid.x, mid.y); 127 | } else { 128 | ctx.lineTo(curr.x, curr.y); 129 | } 130 | prevprev = prev; 131 | prev = points[i]; 132 | } 133 | if (smoothing) { 134 | ctx.quadraticCurveTo(prev.x, prev.y, curr.x, curr.y); 135 | } 136 | } 137 | 138 | function distance(p1, p2) { 139 | return sqrt(pow2(p1.x - p2.x) + pow2(p1.y - p2.y)); 140 | } 141 | 142 | function avg(arr) { 143 | var sum = 0, 144 | len = arr.length; 145 | for (var i = 0, l = len; i < l; i++) { 146 | sum += +arr[i]; 147 | } 148 | return sum/len; 149 | } 150 | })(TeledrawCanvas); 151 | 152 | -------------------------------------------------------------------------------- /src/canvas/tools/rectangle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rectangle tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Rectangle = TeledrawCanvas.Tool.createTool("rectangle", "crosshair"); 6 | 7 | Rectangle.prototype.preview = function () { 8 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 9 | var ctx = canv.getContext('2d'); 10 | var stroke = new Rectangle.stroke(this.canvas, ctx); 11 | stroke.first = { x: 0, y: 0 }; 12 | stroke.second = { x: canv.width, y: canv.height }; 13 | stroke.draw(); 14 | return canv; 15 | }; 16 | 17 | Rectangle.stroke.prototype.bgColor = [255, 255, 255]; 18 | Rectangle.stroke.prototype.bgAlpha = 0; 19 | Rectangle.stroke.prototype.lineWidth = 1; 20 | 21 | Rectangle.stroke.prototype.start = function (pt) { 22 | this.first = pt; 23 | }; 24 | 25 | Rectangle.stroke.prototype.move = function (a, b) { 26 | this.second = b; 27 | }; 28 | 29 | Rectangle.stroke.prototype.draw = function () { 30 | if (!this.first || !this.second) return; 31 | var first = this.first, 32 | second = _.extend({}, this.second), 33 | ctx = this.ctx, 34 | state = this.canvas.state, 35 | shadowOffset = state.shadowOffset, 36 | shadowBlur = state.shadowBlur, 37 | lineWidth = state.lineWidth, 38 | color = TeledrawCanvas.util.cssColor(state.color); 39 | 40 | ctx.lineJoin = ctx.lineCap = "round"; 41 | ctx.globalAlpha = state.globalAlpha; 42 | ctx.fillStyle = ctx.strokeStyle = color; 43 | ctx.miterLimit = 100000; 44 | 45 | if (this.tool.shiftKey) { 46 | var w = Math.abs(second.x - first.x); 47 | second.y = first.y + (second.y > first.y ? w : -w); 48 | } 49 | 50 | if (this.tool.fill) { 51 | drawRect(ctx, first, second); 52 | ctx.fill(); 53 | } else { 54 | if (shadowBlur > 0) { 55 | ctx.shadowColor = color; 56 | ctx.shadowOffsetX = ctx.shadowOffsetY = shadowOffset; 57 | ctx.shadowBlur = shadowBlur; 58 | ctx.translate(-shadowOffset,-shadowOffset); 59 | } 60 | 61 | ctx.lineWidth = lineWidth; 62 | drawRect(ctx, first, second); 63 | ctx.stroke(); 64 | } 65 | }; 66 | 67 | function drawRect(ctx, first, second) { 68 | ctx.beginPath(); 69 | ctx.moveTo(first.x, first.y); 70 | ctx.lineTo(second.x, first.y); 71 | ctx.lineTo(second.x, second.y); 72 | ctx.lineTo(first.x, second.y); 73 | ctx.lineTo(first.x, first.y); 74 | } 75 | 76 | 77 | var FilledRectangle = TeledrawCanvas.Tool.createTool("filled-rectangle", "crosshair"); 78 | 79 | FilledRectangle.prototype.preview = function () { 80 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 81 | var ctx = canv.getContext('2d'); 82 | var stroke = new FilledRectangle.stroke(this.canvas, ctx); 83 | stroke.tool = this; 84 | var w = canv.width, h = canv.height; 85 | stroke.first = { x: w*0.1, y: h*0.1 }; 86 | stroke.second = { x: w*0.9, y: h*0.9 }; 87 | stroke.draw(); 88 | return canv; 89 | }; 90 | _.extend(FilledRectangle.stroke.prototype, Rectangle.stroke.prototype); 91 | FilledRectangle.prototype.fill = true; 92 | 93 | })(TeledrawCanvas); 94 | -------------------------------------------------------------------------------- /src/canvas/tools/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Text tool 3 | */ 4 | (function (TeledrawCanvas) { 5 | var TextTool = TeledrawCanvas.Tool.createTool('text', 'text'), 6 | TextStrokePrototype = TextTool.stroke.prototype; 7 | 8 | var BUFFER = 0.05; 9 | 10 | var down = TextTool.prototype.down; 11 | TextTool.prototype.down = function (pt) { 12 | if (this.currentStroke) { 13 | finish(this.currentStroke); 14 | } 15 | down.call(this, pt); 16 | }; 17 | 18 | TextTool.prototype.up = function (pt) { 19 | if (this.currentStroke) { 20 | this.currentStroke.end(pt); 21 | this.currentStroke.draw(); 22 | initHandlers(this.currentStroke); 23 | } 24 | this.canvas.trigger('tool.up'); 25 | }; 26 | 27 | TextTool.prototype.preview = function () { 28 | var canv = TeledrawCanvas.Tool.prototype.preview.apply(this, arguments); 29 | var ctx = canv.getContext('2d'); 30 | var stroke = new TextTool.stroke(this.canvas, ctx); 31 | stroke.first = { x: 0, y: 0 }; 32 | stroke.second = { x: canv.width, y: canv.height }; 33 | stroke.text = 'Aa'; 34 | stroke.draw(); 35 | return canv; 36 | }; 37 | 38 | TextStrokePrototype.start = function (pt) { 39 | this.text = ''; 40 | this.first = pt; 41 | }; 42 | 43 | TextStrokePrototype.move = function (a, b) { 44 | this.second = b; 45 | }; 46 | 47 | TextStrokePrototype.end = function (pt) { 48 | this.second = pt; 49 | }; 50 | 51 | TextStrokePrototype.draw = function () { 52 | if (!this.first || !this.second) return; 53 | var buffer, 54 | x = this.first.x, 55 | y = this.first.y, 56 | w = this.second.x - x, 57 | h = this.second.y - y, 58 | ctx = this.ctx, 59 | state = this.canvas.state, 60 | color = TeledrawCanvas.util.cssColor(state.color); 61 | 62 | // reposition x/y coords if h or w is negative 63 | if (w < 0) { 64 | w = -w; 65 | x -= w; 66 | } 67 | if (h < 0) { 68 | h = -h; 69 | y -= h; 70 | } 71 | 72 | buffer = h * BUFFER; 73 | 74 | // only draw the outline if not finished 75 | if (!this.finished) { 76 | ctx.save(); 77 | ctx.lineWidth = 1; 78 | ctx.strokeStyle = 'black'; 79 | ctx.strokeRect(x, y, w, h); 80 | ctx.restore(); 81 | } 82 | 83 | // add a buffer to keep the text inside the drawing area 84 | x += buffer; 85 | w -= buffer * 2; 86 | 87 | y += buffer; 88 | h -= buffer * 2; 89 | 90 | ctx.globalAlpha = state.globalAlpha; 91 | ctx.fillStyle = ctx.strokeStyle = color; 92 | ctx.textAlign = 'center'; 93 | ctx.textBaseline = 'middle'; 94 | ctx.font = h + 'px ' + (!!state.font ? state.font : 'Arial'); 95 | ctx.fillText(this.text, x + w / 2, y + h / 2, w); 96 | }; 97 | 98 | function initHandlers(stroke) { 99 | var input = document.createElement('input'); 100 | input.style.opacity = 0; 101 | document.body.appendChild(input); 102 | input.focus(); 103 | stroke.input = input; 104 | stroke.inputHandler = function () { 105 | stroke.text = input.value; 106 | stroke.tool.draw(); 107 | }; 108 | stroke.keydownHandler = function (ev) { 109 | if (ev.keyCode === 13) { //enter 110 | finish(stroke); 111 | } 112 | } 113 | addEvent(input, 'input', stroke.inputHandler); 114 | addEvent(input, 'keydown', stroke.keydownHandler); 115 | } 116 | 117 | function removeHandlers(stroke) { 118 | if (stroke.input) { 119 | removeEvent(stroke.input, 'input', stroke.inputHandler); 120 | removeEvent(stroke.input, 'input', stroke.keydownHandler); 121 | stroke.input.parentNode.removeChild(stroke.input); 122 | } 123 | } 124 | 125 | function finish(stroke) { 126 | var tool = stroke.tool; 127 | removeHandlers(stroke); 128 | stroke.finished = true; 129 | tool.draw(); 130 | stroke.destroy(); 131 | tool.currentStroke = null; 132 | tool.canvas.history.checkpoint(); 133 | } 134 | 135 | })(TeledrawCanvas); 136 | 137 | -------------------------------------------------------------------------------- /src/start.js: -------------------------------------------------------------------------------- 1 | var math = Math, 2 | abs = math.abs, 3 | sqrt = math.sqrt, 4 | floor = math.floor, 5 | round = math.round, 6 | min = math.min, 7 | max = math.max, 8 | pow = math.pow, 9 | pow2 = function (x) { return pow(x, 2); }, 10 | clamp; 11 | 12 | window.TeledrawCanvas = function (elt, opt) { 13 | return new TeledrawCanvas.api(elt, opt); 14 | }; 15 | -------------------------------------------------------------------------------- /src/teledraw-canvas.js: -------------------------------------------------------------------------------- 1 | 2 | (function (TeledrawCanvas) { 3 | TeledrawCanvas.canvases = []; 4 | var _id = 0; 5 | 6 | // global default tool settings 7 | var toolDefaults = { 8 | tool: 'pencil', 9 | alpha: 255, 10 | color: [0, 0, 0], 11 | strokeSize: 1000, 12 | strokeSoftness: 0 13 | }; 14 | 15 | // global default state 16 | var defaultState = { 17 | last: null, 18 | currentTool: null, 19 | previousTool: null, 20 | tool: null, 21 | mouseDown: false, 22 | mouseOver: false, 23 | width: null, 24 | height: null, 25 | 26 | currentZoom: 1, 27 | currentOffset: { x: 0, y: 0 }, 28 | 29 | // if you are using strokeSoftness, make sure shadowOffset >= max(canvas.width, canvas.height) 30 | // related note: safari has trouble with high values for shadowOffset 31 | shadowOffset: 5000, 32 | 33 | enableZoom: true, 34 | enableKeyboardShortcuts: true, 35 | enableWacomSupport: true, 36 | 37 | // default limits 38 | maxHistory: 10, 39 | minStrokeSize: 500, 40 | maxStrokeSize: 10000, 41 | minStrokeSoftness: 0, 42 | maxStrokeSoftness: 100, 43 | maxZoom: 8 // (8 == 800%) 44 | }; 45 | 46 | var wacomPlugin; 47 | 48 | function wacomEmbedObject() { 49 | if (!wacomPlugin) { 50 | var plugin; 51 | if (navigator.mimeTypes["application/x-wacomtabletplugin"]) { 52 | plugin = document.createElement('embed'); 53 | plugin.name = plugin.id = 'wacom-plugin'; 54 | plugin.type = 'application/x-wacomtabletplugin'; 55 | } else { 56 | plugin = document.createElement('object'); 57 | plugin.classid = 'CLSID:092dfa86-5807-5a94-bf3b-5a53ba9e5308'; 58 | plugin.codebase = "fbWacomTabletPlugin.cab"; 59 | } 60 | 61 | plugin.style.width = plugin.style.height = '1px'; 62 | plugin.style.top = plugin.style.left = '-10000px'; 63 | plugin.style.position = 'absolute'; 64 | document.body.appendChild(plugin); 65 | wacomPlugin = plugin; 66 | } 67 | } 68 | 69 | /*var lastPressure = null, 70 | lastPressureTime = now(); 71 | function wacomGetPressure() { 72 | if (wacomPlugin && wacomPlugin.penAPI) { 73 | var pressure; 74 | // only get pressure once every other poll; 75 | if (now() - lastPressureTime > 20) { 76 | pressure = wacomPlugin.penAPI.pressure; 77 | lastPressure = pressure; 78 | lastPressureTime = now(); 79 | } else { 80 | pressure = lastPressure; 81 | } 82 | return pressure; 83 | } 84 | }*/ 85 | 86 | function wacomGetPressure() { 87 | if (wacomPlugin && wacomPlugin.penAPI) { 88 | var p = wacomPlugin.penAPI.pressure; 89 | return p; 90 | } 91 | } 92 | 93 | function wacomIsEraser() { 94 | if (wacomPlugin && wacomPlugin.penAPI) { 95 | return parseInt(wacomPlugin.penAPI.pointerType) === 3; 96 | } 97 | } 98 | 99 | function getOffset(el) { 100 | var _x = 0; 101 | var _y = 0; 102 | while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { 103 | _x += el.offsetLeft; 104 | _y += el.offsetTop; 105 | el = el.offsetParent; 106 | } 107 | return { top: _y, left: _x }; 108 | } 109 | 110 | var Canvas = TeledrawCanvas.Canvas = typeof _Canvas !== 'undefined' ? _Canvas : function (w, h) { 111 | var c = document.createElement('canvas'); 112 | if (w) c.width = w; 113 | if (h) c.height = h; 114 | return c; 115 | }; 116 | 117 | var API = function (elt, options) { 118 | var self = this, 119 | element = self.element = elt.getContext ? elt : document.getElementById(elt); 120 | state = self.state = _.extend({}, defaultState, options); 121 | 122 | if (typeof (new Canvas()).getContext != 'function') { 123 | throw new Error('Error: Your browser does not support HTML canvas!'); 124 | } 125 | 126 | if (state.enableWacomSupport) { 127 | wacomEmbedObject(); 128 | } 129 | 130 | element.width = state.width = state.displayWidth || state.width || element.width; 131 | element.height = state.height = state.displayHeight || state.height || element.height; 132 | state.fullWidth = state.fullWidth || state.width; 133 | state.fullHeight = state.fullHeight || state.height; 134 | 135 | if (state.width / state.height !== state.fullWidth / state.fullHeight) { 136 | //Display and full canvas aspect ratios differ! 137 | //Adjusting full size to match display aspect ratio... 138 | state.fullHeight = state.fullWidth * state.height / state.width; 139 | } 140 | 141 | self._displayCanvas = element; 142 | if (state.enableZoom) { 143 | self._canvas = new Canvas(state.fullWidth, state.fullHeight); 144 | } else { 145 | self._canvas = element; 146 | } 147 | self.history = new TeledrawCanvas.History(self); 148 | 149 | self.defaults(); 150 | self.zoom(0); 151 | self.history.checkpoint(); 152 | TeledrawCanvas.canvases[_id++] = self; 153 | self.bindEvents(); 154 | }; 155 | 156 | var APIprototype = API.prototype; 157 | 158 | APIprototype.bindEvents = function () { 159 | var self = this, 160 | element = self.element, 161 | state = self.state, 162 | gInitZoom, 163 | lastMoveEvent = null, 164 | lastmove = 0, 165 | lastpressure = 0; 166 | 167 | addEvent(element, 'gesturestart', gestureStart); 168 | addEvent(element, 'gesturechange', gestureChange); 169 | addEvent(element, 'gestureend', gestureEnd); 170 | addEvent(element, 'dblclick', dblClick); 171 | addEvent(element, 'mouseenter', mouseEnter); 172 | addEvent(element, 'mousedown', mouseDown); 173 | addEvent(element, 'touchstart', mouseDown); 174 | addEvent(element, 'mouseleave', mouseLeave); 175 | addEvent(window, 'mousemove', mouseMove); 176 | addEvent(window, 'touchmove', mouseMove); 177 | addEvent(window, 'keydown', keyDown); 178 | addEvent(window, 'keyup', keyUp); 179 | 180 | self.unbindEvents = function () { 181 | removeEvent(element, 'gesturestart', gestureStart); 182 | removeEvent(element, 'gesturechange', gestureChange); 183 | removeEvent(element, 'gestureend', gestureEnd); 184 | removeEvent(element, 'dblclick', dblClick); 185 | removeEvent(element, 'mouseenter', mouseEnter); 186 | removeEvent(element, 'mousedown', mouseDown); 187 | removeEvent(element, 'touchstart', mouseDown); 188 | removeEvent(element, 'mouseleave', mouseLeave); 189 | removeEvent(window, 'mousemove', mouseMove); 190 | removeEvent(window, 'touchmove', mouseMove); 191 | removeEvent(window, 'keydown', keyDown); 192 | removeEvent(window, 'keyup', keyUp); 193 | }; 194 | 195 | function mouseEnter(evt) { 196 | var pt = getCoord(evt); 197 | state.tool.enter(state.mouseDown, pt); 198 | state.last = pt; 199 | state.mouseOver = true; 200 | } 201 | 202 | function mouseLeave(evt) { 203 | var pt = getCoord(evt); 204 | state.tool.leave(state.mouseDown, pt); 205 | state.mouseOver = false; 206 | } 207 | 208 | function dblClick(evt) { 209 | var pt = getCoord(evt); 210 | state.tool.dblclick(pt); 211 | } 212 | 213 | function keyUp(e) { 214 | state.tool.keyup(state.mouseDown, e.keyCode); 215 | } 216 | 217 | function keyDown(e) { 218 | state.tool.keydown(state.mouseDown, e.keyCode); 219 | if (!state.enableKeyboardShortcuts) { 220 | return; 221 | } 222 | var eltName = document.activeElement.nodeName.toLowerCase(); 223 | if (eltName === 'input' || eltName === 'textarea') { 224 | return; 225 | } else { 226 | switch (e.keyCode) { 227 | case 69: // e 228 | self.setTool('eraser'); 229 | break; 230 | case 70: // f 231 | self.setTool('fill'); 232 | break; 233 | case 71: // g 234 | self.setTool('grab'); 235 | break; 236 | case 73: // i 237 | self.setTool('eyedropper'); 238 | break; 239 | case 76: // l 240 | self.setTool('line'); 241 | break; 242 | case 79: // o 243 | self.setTool((e.shiftKey ? 'filled-' : '')+'ellipse'); 244 | break; 245 | case 80: // p 246 | self.setTool('pencil'); 247 | break; 248 | case 82: // r 249 | self.setTool((e.shiftKey ? 'filled-' : '')+'rectangle'); 250 | break; 251 | case 90: // z 252 | if (state.mouseDown) { 253 | return false; 254 | } 255 | if (e.metaKey || e.ctrlKey) { 256 | if (e.shiftKey) { 257 | self.redo(); 258 | } else { 259 | self.undo(); 260 | } 261 | return false; 262 | } 263 | break; 264 | case 189: // - 265 | if (e.shiftKey) { // _ 266 | // decrease brush size 267 | self.setStrokeSize(state.strokeSize - 500); 268 | } 269 | break; 270 | case 187: // = 271 | if (e.shiftKey) { // + 272 | // increase brush size 273 | self.setStrokeSize(state.strokeSize + 500); 274 | } 275 | break; 276 | case 188: // , 277 | if (e.shiftKey) { // < 278 | // decrease alpha 279 | self.setAlpha(state.globalAlpha - 0.1); 280 | } 281 | break; 282 | case 190: // . 283 | if (e.shiftKey) { // > 284 | // increase alpha 285 | self.setAlpha(state.globalAlpha + 0.1); 286 | } 287 | break; 288 | case 219: // [ 289 | if (e.shiftKey) { // { 290 | // decrease brush softness 291 | self.setStrokeSoftness(state.strokeSoftness - 10); 292 | } 293 | break; 294 | case 221: // ] 295 | if (e.shiftKey) { // } 296 | // increase brush softness 297 | self.setStrokeSoftness(state.strokeSoftness + 10); 298 | } 299 | break; 300 | } 301 | } 302 | } 303 | 304 | function mouseMove(e) { 305 | if (Date.now() - lastmove < 25) { 306 | return false; 307 | } 308 | lastmove = Date.now(); 309 | 310 | if (e.type == 'touchmove' && e.touches.length > 1) { 311 | return true; 312 | } 313 | if (lastMoveEvent == 'touchmove' && e.type == 'mousemove') return; 314 | if (e.target == element || state.mouseDown) { 315 | var pt = getCoord(e); 316 | state.tool.move(state.mouseDown, state.last, pt); 317 | state.last = pt; 318 | self.trigger('mousemove', pt, e); 319 | lastMoveEvent = e.type; 320 | e.preventDefault(); 321 | return false; 322 | } 323 | } 324 | 325 | function mouseDown(e) { 326 | var pt = state.last = getCoord(e); 327 | if (e.type == 'touchstart' && e.touches.length > 1) { 328 | return true; 329 | } 330 | addEvent(window, e.type === 'mousedown' ? 'mouseup' : 'touchend', mouseUp); 331 | 332 | state.mouseDown = true; 333 | if (state.enableWacomSupport && wacomIsEraser() && state.currentTool !== 'eraser') { 334 | self.setTool('eraser'); 335 | state.wacomWasEraser = true; 336 | } 337 | state.tool.down(pt); 338 | self.trigger('mousedown', pt, e); 339 | 340 | document.onselectstart = function() { return false; }; 341 | e.preventDefault(); 342 | return false; 343 | } 344 | 345 | function mouseUp(e) { 346 | removeEvent(window, e.type === 'mouseup' ? 'mouseup' : 'touchend', mouseUp); 347 | 348 | if (e.type == 'touchend' && e.touches.length > 1) { 349 | return true; 350 | } 351 | 352 | state.mouseDown = false; 353 | state.tool.up(state.last); 354 | self.trigger('mouseup', state.last, e); 355 | 356 | if (state.wacomWasEraser === true) { 357 | self.previousTool(); 358 | state.wacomWasEraser = false; 359 | } 360 | 361 | document.onselectstart = function() { return true; }; 362 | e.preventDefault(); 363 | return false; 364 | } 365 | 366 | function gestureStart(evt) { 367 | if (state.tool.name == 'grab') { 368 | gInitZoom = state.currentZoom; 369 | } 370 | } 371 | 372 | function gestureChange(evt) { 373 | if (state.tool.name == 'grab') { 374 | var pt = state.last; 375 | self.zoom(gInitZoom*evt.scale, pt.xd, pt.yd); 376 | } 377 | evt.preventDefault(); 378 | return false; 379 | } 380 | 381 | function gestureEnd(evt) { 382 | 383 | } 384 | 385 | function getCoord(e) { 386 | var off = getOffset(element), 387 | pageX = e.pageX || e.touches && e.touches[0].pageX, 388 | pageY = e.pageY || e.touches && e.touches[0].pageY, 389 | pressure = null; 390 | if (state.enableWacomSupport) { 391 | if (Date.now() - lastpressure > 25) { 392 | lastpressure = Date.now(); 393 | pressure = wacomGetPressure(); 394 | } 395 | } 396 | 397 | return { 398 | x: floor((pageX - off.left)/state.currentZoom) + state.currentOffset.x || 0, 399 | y: floor((pageY - off.top)/state.currentZoom) + state.currentOffset.y || 0, 400 | xd: floor(pageX - off.left) || 0, 401 | yd: floor(pageY - off.top) || 0, 402 | p: pressure 403 | }; 404 | } 405 | }; 406 | 407 | APIprototype.setRGBAArrayColor = function (rgba) { 408 | var state = this.state; 409 | if (rgba.length === 4) { 410 | this.setAlpha(rgba.pop()); 411 | } 412 | for (var i = rgba.length; i < 3; ++i) { 413 | rgba.push(0); 414 | } 415 | var old = state.color; 416 | state.color = rgba; 417 | this.trigger('tool.color', state.color, old); 418 | return this; 419 | }; 420 | 421 | APIprototype.updateTool = function () { 422 | var lw = 1 + floor(pow(this.state.strokeSize / 1000.0, 2)); 423 | var sb = floor(pow(this.state.strokeSoftness, 1.3) / 300.0 * lw); 424 | this.state.lineWidth = lw; 425 | this.state.shadowBlur = sb; 426 | }; 427 | 428 | APIprototype.updateDisplayCanvas = function (noTrigger) { 429 | if (this.state.enableZoom === false) { 430 | return this; 431 | } 432 | var dctx = this.displayCtx(), 433 | off = this.state.currentOffset, 434 | zoom = this.state.currentZoom, 435 | dw = dctx.canvas.width, 436 | dh = dctx.canvas.height, 437 | sw = floor(dw / zoom), 438 | sh = floor(dh / zoom); 439 | TeledrawCanvas.util.clear(dctx); 440 | if (noTrigger !== true) this.trigger('display.update:before'); 441 | dctx.drawImage(this._canvas, off.x, off.y, sw, sh, 0, 0, dw, dh); 442 | if (noTrigger !== true) this.trigger('display.update:after'); 443 | }; 444 | 445 | /* this version attempts at better performance by drawing only the bounding rect of the changes 446 | APIprototype.updateDisplayCanvas = function (noTrigger, tl, br) { 447 | if (this.state.enableZoom === false) { 448 | return this; 449 | } 450 | var dctx = this.displayCtx(), 451 | off = this.state.currentOffset, 452 | zoom = this.state.currentZoom, 453 | // bounding rect of the change 454 | stl = tl || { x: 0, y: 0 }, 455 | sbr = br || { x: this._canvas.width, y: this._canvas.height }, 456 | dtl = { x: floor((stl.x - off.x)*zoom), y: floor((stl.y - off.y)*zoom) }, 457 | dbr = { x: floor((sbr.x - off.x)*zoom), y: floor((sbr.y - off.y)*zoom) }, 458 | sw = sbr.x - stl.x, 459 | sh = sbr.y - stl.y, 460 | dw = dbr.x - dtl.x, 461 | dh = dbr.y - dtl.y; 462 | if (sw === 0 || sh === 0) { 463 | return; 464 | } 465 | // only clear and draw what we need to 466 | dctx.clearRect(dtl.x, dtl.y, dw, dh); 467 | if (noTrigger !== true) this.trigger('display.update:before'); 468 | dctx.drawImage(this._canvas, stl.x, stl.y, sw, sh, dtl.x, dtl.y, dw, dh); 469 | if (noTrigger !== true) this.trigger('display.update:after'); 470 | };*/ 471 | 472 | 473 | // API 474 | 475 | // returns the HTML Canvas element associated with this tdcanvas 476 | APIprototype.canvas = function () { 477 | return this._canvas; 478 | }; 479 | 480 | // returns a 2d rendering context for the canvas element 481 | APIprototype.ctx = function () { 482 | return this._ctx || (this._ctx = this._canvas.getContext('2d')); 483 | }; 484 | 485 | APIprototype.displayCanvas = function () { 486 | return this._displayCanvas; 487 | }; 488 | 489 | APIprototype.displayCtx = function () { 490 | return this._displayCtx || (this._displayCtx = this._displayCanvas.getContext('2d')); 491 | }; 492 | 493 | // this should be called when removing a canvas to avoid event leaks 494 | APIprototype.destroy = function () { 495 | this.unbindEvents(); 496 | }; 497 | 498 | // sets the cursor css to be used when the mouse is over the canvas element 499 | APIprototype.cursor = function (c) { 500 | if (!c) { 501 | c = "default"; 502 | } 503 | var cursors = c.split(/,\s*/); 504 | do { 505 | c = cursors.shift(); 506 | this.element.style.cursor = c; 507 | } while (c.length && this.element.style.cursor != c); 508 | return this; 509 | }; 510 | 511 | // clears the canvas and (unless noCheckpoint===true) pushes to the undoable history 512 | APIprototype.clear = function (noCheckpoint) { 513 | var self = this; 514 | TeledrawCanvas.util.clear(self.ctx()); 515 | if (noCheckpoint !== true) { 516 | self.history.checkpoint(); 517 | } 518 | self.updateDisplayCanvas(); 519 | self.trigger('clear'); 520 | return self; 521 | }; 522 | 523 | // resets the default tool and properties 524 | APIprototype.defaults = function () { 525 | var self = this; 526 | self.setTool(toolDefaults.tool); 527 | self.setAlpha(toolDefaults.alpha); 528 | self.setColor(toolDefaults.color); 529 | self.setStrokeSize(toolDefaults.strokeSize); 530 | self.setStrokeSoftness(toolDefaults.strokeSoftness); 531 | return self; 532 | }; 533 | 534 | // returns a data url (image/png) of the canvas, 535 | // optionally a portion of the canvas specified by sx, sy, sw, sh, and output size by dw, dh 536 | APIprototype.toDataURL = function (sx, sy, sw, sh, dw, dh) { 537 | if (arguments.length >= 4) { 538 | sx = sx || 0; 539 | sy = sy || 0; 540 | dw = dw || sw; 541 | dh = dh || sh; 542 | var tmpcanvas = this.getTempCanvas(dw, dh); 543 | tmpcanvas.getContext('2d').drawImage(this.canvas(), sx, sy, sw, sh, 0, 0, dw, dh); 544 | return tmpcanvas.toDataURL(); 545 | } 546 | return this.canvas().toDataURL(); 547 | }; 548 | 549 | // returns a new (blank) canvas element the same size as this tdcanvas element 550 | APIprototype.getTempCanvas = function (w, h) { 551 | return new Canvas(w || this._canvas.width, h || this._canvas.height); 552 | }; 553 | 554 | // draws an image data url to the canvas and when it's finished, calls the given callback function 555 | APIprototype.fromDataURL = APIprototype.fromImageURL = function (url, cb) { 556 | var self = this, 557 | img = new Image(); 558 | img.onload = function () { 559 | self.clear(true); 560 | self.ctx().drawImage(img, 0, 0); 561 | self.updateDisplayCanvas(); 562 | if (typeof cb == 'function') { 563 | cb.call(self); 564 | } 565 | }; 566 | img.src = url; 567 | return self; 568 | }; 569 | 570 | // returns true if the canvas has no data 571 | APIprototype.isBlank = function () { 572 | var data = this.getImageData().data; 573 | var len = data.length; 574 | for (var i = 0, l = len; i < l; ++i) { 575 | if (data[i] !== 0) return false; 576 | } 577 | return true; 578 | }; 579 | 580 | // clears the canvas and draws the supplied image, video or canvas element 581 | APIprototype.fromImage = APIprototype.fromVideo = APIprototype.fromCanvas = function (element) { 582 | this.clear(true); 583 | this.ctx().drawImage(element, 0, 0); 584 | this.updateDisplayCanvas(); 585 | return this; 586 | }; 587 | 588 | // returns the ImageData of the whole canvas element 589 | APIprototype.getImageData = function () { 590 | var ctx = this.ctx(); 591 | return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); 592 | }; 593 | 594 | // sets the ImageData of the canvas 595 | APIprototype.putImageData = function (data) { 596 | this.ctx().putImageData(data, 0, 0); 597 | this.updateDisplayCanvas(); 598 | return this; 599 | }; 600 | 601 | // returns the current color in the form [r, g, b, a], e.g. [255, 0, 0, 0.5] 602 | APIprototype.getColor = function () { 603 | return this.state.color.slice(); 604 | }; 605 | 606 | // sets the current color, either as an array (see getColor) or any acceptable css color string 607 | APIprototype.setColor = function (color) { 608 | if (!_.isArray(color)) { 609 | color = TeledrawCanvas.util.parseColorString(color); 610 | } 611 | this.setRGBAArrayColor(color); 612 | return this; 613 | }; 614 | 615 | // sets the current alpha to a, where a is a number in [0,1] 616 | APIprototype.setAlpha = function (a) { 617 | var old = this.state.globalAlpha; 618 | this.state.globalAlpha = clamp(a, 0, 1); 619 | this.trigger('tool.alpha', this.state.globalAlpha, old); 620 | return this; 621 | }; 622 | 623 | // returns the current alpha 624 | APIprototype.getAlpha = function () { 625 | return this.state.globalAlpha; 626 | }; 627 | 628 | // sets the current stroke size to s, where a is a number in [minStrokeSize, maxStrokeSize] 629 | // lineWidth = 1 + floor(pow(strokeSize / 1000.0, 2)); 630 | APIprototype.setStrokeSize = function (s) { 631 | var old = this.state.strokeSize; 632 | this.state.strokeSize = clamp(s, this.state.minStrokeSize, this.state.maxStrokeSize); 633 | this.updateTool(); 634 | this.trigger('tool.size', this.state.strokeSize, old); 635 | return this; 636 | }; 637 | 638 | // sets the current stroke size to s, where a is a number in [minStrokeSoftness, maxStrokeSoftness] 639 | APIprototype.setStrokeSoftness = function (s) { 640 | var old = this.state.strokeSoftness; 641 | this.state.strokeSoftness = clamp(s, this.state.minStrokeSoftness, this.state.maxStrokeSoftness); 642 | this.updateTool(); 643 | this.trigger('tool.softness', this.state.strokeSoftness, old); 644 | return this; 645 | }; 646 | 647 | // set the current tool, given the string name of the tool (e.g. 'pencil') 648 | APIprototype.setTool = function (name) { 649 | if (this.state.currentTool === name) { 650 | return this; 651 | } 652 | this.state.previousTool = this.state.currentTool; 653 | this.state.currentTool = name; 654 | if (!TeledrawCanvas.tools[name]) { 655 | throw new Error('Tool "'+name+'" not defined.'); 656 | } 657 | this.state.tool = new TeledrawCanvas.tools[name](this); 658 | this.trigger('tool.change', this.state.currentTool, this.state.previousTool); 659 | return this; 660 | }; 661 | 662 | 663 | APIprototype.previousTool = function () { 664 | return this.setTool(this.state.previousTool); 665 | }; 666 | 667 | // undo to the last history checkpoint (if available) 668 | APIprototype.undo = function () { 669 | this.history.undo(); 670 | this.trigger('history undo', this.history.past.length, this.history.future.length); 671 | return this; 672 | }; 673 | 674 | // redo to the next history checkpoint (if available) 675 | APIprototype.redo = function () { 676 | this.history.redo(); 677 | this.trigger('history redo', this.history.past.length, this.history.future.length); 678 | return this; 679 | }; 680 | 681 | // resize the display canvas to the given width and height 682 | // (throws an error if it's not the same aspect ratio as the source canvas) 683 | // @todo/consider: release this constraint and just change the size of the source canvas? 684 | APIprototype.resize = function (w, h) { 685 | if (this.state.enableZoom === false) { 686 | return this; 687 | } 688 | var self = this, 689 | ar0 = Math.round(self._canvas.width/self._canvas.height*100)/100, 690 | ar1 = Math.round(w/h*100)/100; 691 | if (ar0 !== ar1) { 692 | throw new Error('Not the same aspect ratio!'); 693 | } 694 | self._displayCanvas.width = self.state.width = w; 695 | self._displayCanvas.height = self.state.height = h; 696 | this.trigger('resize', w, h); 697 | return self.zoom(self.state.currentZoom); 698 | }; 699 | 700 | // zoom the canvas to the given multiplier, z (e.g. if z is 2, zoom to 2:1) 701 | // optionally at a given point (in display canvas coordinates) 702 | // otherwise in the center of the current display 703 | // if no arguments are specified, returns the current zoom level 704 | APIprototype.zoom = function (z, x, y) { 705 | if (arguments.length === 0) { 706 | return this.state.currentZoom; 707 | } 708 | if (this.state.enableZoom === false) { 709 | return this; 710 | } 711 | var self = this, 712 | panx = 0, 713 | pany = 0, 714 | currentZoom = self.state.currentZoom, 715 | displayWidth = self._displayCanvas.width, 716 | displayHeight = self._displayCanvas.height; 717 | 718 | // if no point is specified, use the center of the canvas 719 | x = clamp(x || displayWidth/2, 0, displayWidth); 720 | y = clamp(y || displayHeight/2, 0, displayHeight); 721 | 722 | // restrict the zoom 723 | z = clamp(z || 0, displayWidth / self._canvas.width, self.state.maxZoom); 724 | 725 | // figure out where to zoom at 726 | if (z !== currentZoom) { 727 | if (z > currentZoom) { 728 | // zooming in 729 | panx = -(displayWidth/currentZoom - displayWidth/z)/2 - (x - displayWidth/2)/currentZoom; 730 | pany = -(displayHeight/currentZoom - displayHeight/z)/2 - (y - displayHeight/2)/currentZoom; 731 | } else if (z < currentZoom) { 732 | // zooming out 733 | panx = (displayWidth/z - displayWidth/currentZoom)/2; 734 | pany = (displayHeight/z - displayHeight/currentZoom)/2; 735 | } 736 | panx *= z; 737 | pany *= z; 738 | } 739 | self.state.currentZoom = z; 740 | self.trigger('zoom', z, currentZoom); 741 | self.pan(panx, pany); 742 | self.updateDisplayCanvas(); 743 | return self; 744 | }; 745 | 746 | // pan the canvas to the given (relative) x,y position 747 | // unless absolute === true 748 | // if no arguments are specified, returns the current absolute position 749 | APIprototype.pan = function (x, y, absolute) { 750 | if (arguments.length === 0) { 751 | return this.state.currentOffset; 752 | } 753 | if (this.state.enableZoom === false) { 754 | return this; 755 | } 756 | var self = this, 757 | zoom = self.state.currentZoom, 758 | currentX = self.state.currentOffset.x, 759 | currentY = self.state.currentOffset.y, 760 | maxWidth = self._canvas.width - floor(self._displayCanvas.width/zoom), 761 | maxHeight = self._canvas.height - floor(self._displayCanvas.height/zoom); 762 | x = absolute === true ? x/zoom : currentX - (x || 0)/zoom; 763 | y = absolute === true ? y/zoom : currentY - (y || 0)/zoom; 764 | x = floor(clamp(x, 0, maxWidth)); 765 | y = floor(clamp(y, 0, maxHeight)) 766 | self.state.currentOffset = { x: x, y: y }; 767 | self.trigger('pan', self.state.currentOffset, { x: currentX, y: currentY }); 768 | self.updateDisplayCanvas(); 769 | return self; 770 | }; 771 | 772 | // events mixin 773 | _.extend(APIprototype, Events); 774 | TeledrawCanvas.api = API; 775 | })(TeledrawCanvas); 776 | 777 | 778 | -------------------------------------------------------------------------------- /src/teledraw-canvas.util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TeledrawCanvas.util 3 | */ 4 | (function (TeledrawCanvas) { 5 | var Util = function () { return Util; }; 6 | 7 | Util.clear = function (ctx) { 8 | ctx = ctx.canvas ? ctx : /* (canvas) */ctx.getContext('2d'); 9 | ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); 10 | }; 11 | 12 | // returns a CSS-style rgb(a) string for the given RGBA array 13 | Util.cssColor = function (rgba) { 14 | if (rgba.length === 3) { 15 | return "rgb(" + floor(rgba[0]) + "," + floor(rgba[1]) + "," + floor(rgba[2]) + ")"; 16 | } 17 | return "rgba(" + floor(rgba[0]) + "," + floor(rgba[1]) + "," + floor(rgba[2]) + "," + rgba[3] + ")"; 18 | }; 19 | 20 | // constrains c within a and b 21 | Util.clamp = clamp = function (c, a, b) { 22 | return (c < a ? a : c > b ? b : c); 23 | }; 24 | 25 | // returns the opposite color. I think? 26 | Util.opposite = function (color) { 27 | if (!_.isArray(color)) { 28 | color = Util.parseColorString(color); 29 | } 30 | var hsl = Util.rgb2hsl(color); 31 | hsl[0] = (hsl[0] + 180) % 360; 32 | hsl[1] = 100 - hsl[1]; 33 | hsl[2] = 100 - hsl[2]; 34 | var rgb = Util.hsl2rgb(hsl); 35 | if (color.length === 4) { 36 | rgb.push(color[3]); 37 | } 38 | return rgb; 39 | }; 40 | 41 | // kill the alpha channel! 42 | Util.rgba2rgb = function(rgba) { 43 | if (rgba.length === 3 || rgba[3] === 255) { 44 | return rgba; 45 | } 46 | var r = rgba[0], 47 | g = rgba[1], 48 | b = rgba[2], 49 | a = rgba[3], 50 | out = []; 51 | out[0] = (a * r) + (255 - a*255); 52 | out[1] = (a * g) + (255 - a*255); 53 | out[2] = (a * b) + (255 - a*255); 54 | return out; 55 | }; 56 | 57 | Util.rgb2hex = function (rgb) { 58 | rgb = Util.rgba2rgb(rgb); 59 | return '#' + toHex(rgb[0]) + toHex(rgb[1]) + toHex(rgb[2]); 60 | }; 61 | 62 | Util.hex2rgb = function (hex) { 63 | return Util.parseColorString(hex); 64 | }; 65 | 66 | Util.rgb2hsl = function (rgb) { 67 | var r = rgb[0]/255, 68 | g = rgb[1]/255, 69 | b = rgb[2]/255, 70 | _max = max(r, g, b), 71 | _min = min(r, g, b), 72 | d, h, s, l = (_max + _min) / 2; 73 | if (_max === _min) { 74 | h = s = 0; 75 | } else { 76 | d = _max - _min; 77 | s = l > 0.5 ? d / (2 - _max - _min) : d / (_max + _min); 78 | switch (max) { 79 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 80 | case g: h = (b - r) / d + 2; break; 81 | case b: h = (r - g) / d + 4; break; 82 | } 83 | h /= 6; 84 | } 85 | return [h, s*100, l*100]; 86 | }; 87 | 88 | Util.hex2hsl = function (hex) { 89 | return Util.rgb2hsl(Util.hex2rgb(hex)); 90 | }; 91 | 92 | Util.hsl2rgb = function (hsl) { 93 | var m1, m2, hue; 94 | var r, g, b; 95 | var h = hsl[0], 96 | s = hsl[1]/100, 97 | l = hsl[2]/100; 98 | if (s === 0) 99 | r = g = b = (l * 255); 100 | else { 101 | if (l <= 0.5) 102 | m2 = l * (s + 1); 103 | else 104 | m2 = l + s - l * s; 105 | m1 = l * 2 - m2; 106 | hue = h / 360; 107 | r = hue2rgb(m1, m2, hue + 1/3); 108 | g = hue2rgb(m1, m2, hue); 109 | b = hue2rgb(m1, m2, hue - 1/3); 110 | } 111 | return [r, g, b]; 112 | }; 113 | 114 | function toHex(n) { 115 | n = parseInt(n, 10) || 0; 116 | n = clamp(n, 0, 255).toString(16); 117 | if (n.length === 1) { 118 | n = '0'+n; 119 | } 120 | return n; 121 | } 122 | 123 | 124 | function hue2rgb(m1, m2, hue) { 125 | var v; 126 | if (hue < 0) 127 | hue += 1; 128 | else if (hue > 1) 129 | hue -= 1; 130 | 131 | if (6 * hue < 1) 132 | v = m1 + (m2 - m1) * hue * 6; 133 | else if (2 * hue < 1) 134 | v = m2; 135 | else if (3 * hue < 2) 136 | v = m1 + (m2 - m1) * (2/3 - hue) * 6; 137 | else 138 | v = m1; 139 | 140 | return 255 * v; 141 | } 142 | 143 | // parses any valid css color into an RGBA array 144 | Util.parseColorString = function(color_string) 145 | { 146 | function getRGB(str) { 147 | var a = document.createElement('a'); 148 | a.style.color = str; 149 | document.body.appendChild(a); 150 | var color = getComputedStyle(a).color; 151 | document.body.removeChild(a); 152 | return color; 153 | } 154 | 155 | var channels; 156 | var ok = false, r, g, b, a; 157 | color_string = getRGB(color_string); 158 | 159 | // array of color definition objects 160 | var color_defs = [ 161 | { 162 | re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, 163 | //example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], 164 | process: function (bits){ 165 | return [ 166 | parseInt(bits[1]), 167 | parseInt(bits[2]), 168 | parseInt(bits[3]), 169 | 1 170 | ]; 171 | } 172 | }, 173 | { 174 | re: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d*(\.\d+)?)\)$/, 175 | //example: ['rgba(123, 234, 45, 0.5)', 'rgba(255,234,245, .1)'], 176 | process: function (bits){ 177 | return [ 178 | parseInt(bits[1]), 179 | parseInt(bits[2]), 180 | parseInt(bits[3]), 181 | parseFloat(bits[4]) 182 | ]; 183 | } 184 | }]; 185 | 186 | // search through the definitions to find a match 187 | for (var i = 0; i < color_defs.length; i++) { 188 | var re = color_defs[i].re; 189 | var processor = color_defs[i].process; 190 | var bits = re.exec(color_string); 191 | if (bits) { 192 | channels = processor(bits); 193 | r = channels[0]; 194 | g = channels[1]; 195 | b = channels[2]; 196 | a = channels[3]; 197 | ok = true; 198 | } 199 | 200 | } 201 | 202 | // validate/cleanup values 203 | r = (r < 0 || isNaN(r)) ? 0 : ((r > 255) ? 255 : r); 204 | g = (g < 0 || isNaN(g)) ? 0 : ((g > 255) ? 255 : g); 205 | b = (b < 0 || isNaN(b)) ? 0 : ((b > 255) ? 255 : b); 206 | a = (a < 0 || isNaN(a)) ? 0 : ((a > 1) ? 1 : a); 207 | return ok ? [r, g, b, a] : [0, 0, 0, 1]; 208 | } 209 | 210 | TeledrawCanvas.util = Util; 211 | })(TeledrawCanvas); 212 | 213 | 214 | // requestAnimationFrame polyfill by Erik Möller 215 | // fixes from Paul Irish and Tino Zijdel 216 | (function() { 217 | var lastTime = 0; 218 | var vendors = ['ms', 'moz', 'webkit', 'o']; 219 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 220 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 221 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 222 | || window[vendors[x]+'CancelRequestAnimationFrame']; 223 | } 224 | 225 | if (!window.requestAnimationFrame) { 226 | window.requestAnimationFrame = function(callback, element) { 227 | var currTime = new Date().getTime(); 228 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 229 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 230 | timeToCall); 231 | lastTime = currTime + timeToCall; 232 | return id; 233 | }; 234 | } 235 | if (!window.cancelAnimationFrame) { 236 | window.cancelAnimationFrame = function(id) { 237 | clearTimeout(id); 238 | }; 239 | } 240 | }()); 241 | --------------------------------------------------------------------------------