├── .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 |
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 |
--------------------------------------------------------------------------------