├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── docs ├── demos.md ├── download │ ├── martin.js │ ├── martin.min.js │ └── martin.min.js.map ├── effects.md ├── elements.md ├── events.md ├── images │ ├── bunny-old.jpg │ ├── bunny.jpg │ ├── martin.jpg │ ├── marty-banner.png │ └── marty-full.jpg ├── index.md ├── js │ ├── demos.js │ └── index.js ├── layers.md ├── overview.md ├── plugins.md └── utilities.md ├── gulpfile.js ├── images └── humphrey.jpg ├── index.html ├── js ├── dist │ ├── martin.gradientmap.js │ ├── martin.gradientmap.min.js │ ├── martin.js │ ├── martin.min.js │ ├── martin.min.js.map │ ├── martin.tile.js │ ├── martin.tile.min.js │ ├── martin.watermark.js │ ├── martin.watermark.min.js │ └── watermark.js └── src │ ├── core │ ├── dimensions.js │ ├── helpers.js │ ├── init.js │ ├── utils.js │ └── version.js │ ├── effect │ ├── blur.js │ ├── desaturate.js │ ├── init.js │ ├── invert.js │ ├── lighten.js │ ├── opacity.js │ └── sharpen.js │ ├── element │ ├── circle.js │ ├── ellipse.js │ ├── image.js │ ├── init.js │ ├── line.js │ ├── polygon.js │ ├── rect.js │ └── text.js │ ├── end.js │ ├── event │ └── events.js │ ├── layer │ └── layers.js │ ├── object │ └── object.js │ ├── plugins │ ├── gradientmap.js │ ├── tile.js │ └── watermark.js │ └── start.js ├── mkdocs.yml ├── package.json ├── server.js └── test ├── index.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | site 3 | aws.json 4 | test/jasmine 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Scott Parker Donaldson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Martin.js 2 | 3 | Martin.js is a JavaScript library for working with HTML5 canvas, to make photo manipulation and drawing in browser easy for developers and users. 4 | 5 | ## Installation and Initializing 6 | 7 | `bower install martinjs` 8 | 9 | Bower will download and install minified and un-minified versions in the `js/dist` folder (`martin.min.js` and `martin.js`, respectively), as well as source files in the `js/src` folder. 10 | 11 | In the `` of your document, include Martin.js: 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | Then, after including Martin.js, you can initialize it from a ``, ``, or independently (to later include it in your document). 18 | 19 | ```js 20 | // Grab a or with id="id" and instantiate from it 21 | var canvas = Martin('id'); 22 | 23 | // Grab an element and pass it in 24 | var myImage = document.getElementsByTagName('img')[0]; 25 | var myCanvas = Martin(myImage); 26 | 27 | // You can then call the available methods! 28 | canvas.newLayer(); 29 | canvas.text({ 30 | text: 'Hello!', 31 | font: 'Futura' 32 | }); 33 | 34 | myCanvas.saturate(25); 35 | ``` 36 | 37 | **Important:** If you are calling Martin from an `` element, it must be hosted on the some domain that you are running your code from. This is due to `` not allowing manipulation of image data from cross-origin resources. ([There is a workaround](http://keithwyland.com/2013/09/29/Canvas-CORS-Codepen.html), but it requires proper CORS header being sent as well as specifying that the `` uses cross-origin data) 38 | 39 | ## Contributing 40 | 41 | Clone this repository and run `npm install` to download dependencies. Once those have downloaded, running `gulp` will set up the development environment and automatically open `localhost:3000` in your browser of choice. 42 | 43 | **Do not change any code in the `js/dist` directory.** All files there are automatically generated based on code in the `js/src` directory, and you should only make changes there. 44 | 45 | Unit tests are not comprehensive (yet!), but before submitting a pull request, visit `localhost:3000/test` to make sure that all tests are passing. 46 | 47 | ## Documentation 48 | 49 | To build documentation, run `mkdocs serve`. This will set up a server at `localhost:8000`. [MkDocs](http://www.mkdocs.org) builds the documentation, with configuration in the root `mkdocs.yml`, and all pages and included js/images in `docs/`. 50 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "martinjs", 3 | "version": "0.4.2", 4 | "homepage": "http://martinjs.org", 5 | "authors": [ 6 | "Scott Donaldson " 7 | ], 8 | "main": "js/dist/martin.min.js", 9 | "moduleType": [ 10 | "globals" 11 | ], 12 | "keywords": [ 13 | "martin", 14 | "martinjs", 15 | "canvas" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "test", 22 | "docs", 23 | "images", 24 | "index.html", 25 | "mkdocs.yml", 26 | "README.md" 27 | ] 28 | } -------------------------------------------------------------------------------- /docs/demos.md: -------------------------------------------------------------------------------- 1 | # Demos 2 | 3 | ### Sepia photo with caption 4 | 5 | 6 | 7 | ```js 8 | // darken and desaturate the base image 9 | canvas.darken(10); 10 | canvas.desaturate(100); 11 | 12 | // create a new layer, give it a brownish background, 13 | // and reduce the opacity to 40% 14 | canvas.newLayer(); 15 | canvas.background('#ea0'); 16 | canvas.opacity(40); 17 | 18 | // Draw a black rectangle that will be the background 19 | // for the text. This inherits the layer's 40% opacity! 20 | canvas.rect({ 21 | width: '100%', 22 | height: '15%' 23 | }); 24 | 25 | // Create a new layer, and write the text on it. 26 | canvas.newLayer(); 27 | canvas.text({ 28 | text: 'The loneliest bunny in the west.', 29 | x: '50%', 30 | y: 13, 31 | align: 'center', 32 | color: '#fff' 33 | }); 34 | 35 | canvas.convertToImage(); 36 | ``` 37 | 38 | ### Checkerboard pattern 39 | 40 | 41 | 42 | ```js 43 | // .canvas refers to the actual element 44 | var canvas = Martin('demo-checkerboard'); 45 | var w = canvas.width(), 46 | h = canvas.height(), 47 | iter = 0; 48 | 49 | var size = 40, 50 | x = w / size, // tiles in x direction 51 | y = h / size; // tiles in y direction 52 | 53 | for ( var i = 0; i < x; i++ ) { 54 | 55 | if ( y % 2 !== 1 ) iter++; 56 | 57 | for ( var j = 0; j < y; j++ ) { 58 | 59 | if ( iter % 2 === 0 ) { 60 | 61 | canvas.rect({ 62 | x: i * size, 63 | y: j * size, 64 | height: size, 65 | width: size 66 | }); 67 | } 68 | iter++; 69 | } 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/download/martin.min.js: -------------------------------------------------------------------------------- 1 | !function(){function t(e,i){return this instanceof t?(this.original=null,"string"==typeof e?this.original=document.getElementById(e):e instanceof HTMLElement&&(this.original=e),this.options=i||{},this.makeCanvas()):new t(e,i)}function e(t,e){t&&t.forEach(e)}function i(){}function n(e,i){function n(n){var a=new t.Element(e,this,n);return a.renderElement=function(){var e=this.layer,n=this.context;this.clear(),n.scale(e.scale.x,e.scale.y),n.beginPath(),i.call(a,this.data),this.setContext(this.data),n.closePath(),n.scale(1/e.scale.x,1/e.scale.y),t.utils.forEach(this.effects,function(t){t.renderEffect&&t.renderEffect()}),this.canvas.width>0&&this.canvas.height>0&&e.context.drawImage(this.canvas,0,0)},a}t.prototype[e]=function(t){var e=n.call(this,t);return this.autorender(),e},t.Layer.prototype[e]=function(t){var e=n.call(this.base,t);return this.base.autorender(),e}}function a(t){this.context.drawImage(t,0,0)}function r(t){var e=this.layer,i=this.context;i.rect(e.normalizeX(t.x||0),e.normalizeY(t.y||0),e.normalizeX(t.width||e.width()),e.normalizeY(t.height||e.height()))}function s(e,i){function n(n,a,r){var s=new t.Effect(e,this,n,a,r);return s.renderEffect=function(){i.call(s,this.data)},s}t.prototype[e]=function(t){var e=n.call(this,t,this.currentLayer.effects,this.currentLayer);return this.autorender(),e},t.Layer.prototype[e]=t.Element.prototype[e]=function(t){var e=n.call(this.base,t,this.effects,this);return this.base.autorender(),e}}function o(t){this.context.loop(function(e,i,n){var a=n.r,r=n.g,s=n.b,o=.3*a+.59*r+.11*s;return a=(1-t)*a+t*o,r=(1-t)*r+t*o,s=(1-t)*s+t*o,n.r=a,n.g=r,n.b=s,n})}function h(t){this.context.loop(function(e,i,n){return n.r+=Math.round(255*t),n.g+=Math.round(255*t),n.b+=Math.round(255*t),n})}function c(){this.r=this.g=this.b=this.a=0,this.next=null}function l(t,e,i){return{exec:function(i){var n,a={};for(n in i)a[n]=i[n];a.x=i.offsetX?i.offsetX:i.clientX-t.canvas.getBoundingClientRect().left,a.y=i.offsetY?i.offsetY:i.clientY-t.canvas.getBoundingClientRect().top,e(a),t.autorender()}}}t.utils={},t.prototype.makeCanvas=function(){function e(){var e=this.currentLayer.stackIndex();this.layer(0),i.width=n.naturalWidth,i.height=n.naturalHeight,this.width(i.width),this.height(i.height),n.parentNode.insertBefore(i,n),n.parentNode.removeChild(n),t.registerElement("image",function(t){a.call(this,t)}),this.image(n).bumpToBottom(),this.layer(e)}if(this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.newLayer(),this.original)if("IMG"===this.original.tagName){var i=this.canvas,n=(this.context,this.original);if(n.complete)return e.call(this);n.onload=e.bind(this)}else"CANVAS"===this.original.tagName&&(this.canvas=this.original,this.context=this.original.getContext("2d"));return this.autorender(),this},t._version="0.4.2",t.degToRad=function(t){return t*(Math.PI/180)},t.radToDeg=function(t){return t*(180/Math.PI)},t.hexToRGB=function(t){if(!t)return!1;"#"===t.slice(0,1)&&(t=t.slice(1));var e,i,n;return 6===t.length?(e=t.slice(0,2),i=t.slice(2,4),n=t.slice(4,6)):3===t.length&&(e=t.slice(0,1)+t.slice(0,1),i=t.slice(1,2)+t.slice(1,2),n=t.slice(2,3)+t.slice(2,3)),{r:parseInt(e,16),g:parseInt(i,16),b:parseInt(n,16)}},t.utils.forEach=e,t.utils.noop=i;var u,f={extend:function(e){for(var i in e){if(t.prototype.hasOwnProperty(i))throw new Error("Careful! This method already exists on the Martin prototype. Try a different name after checking the docs: http://martinjs.org");t.prototype[i]=e[i]}},remove:function(){var t=this.canvas,e=t.parentNode;return e&&e.removeChild(this.canvas),this},render:function(e){var i=this.context;return i.clearRect(0,0,this.width(),this.height()),t.utils.forEach(this.layers,function(e){e.clear(),t.utils.forEach(e.elements,function(t){t.renderElement&&t.renderElement()}),t.utils.forEach(e.effects,function(t){t.renderEffect&&t.renderEffect()}),e.canvas.width>0&&e.canvas.height>0&&i.drawImage(e.canvas,0,0)}),e?e():this},autorender:function(t){return this.options.autorender!==!1?this.render(t):t?t():null},toDataURL:function(){return this.canvas.toDataURL()},convertToImage:function(){var t=this.toDataURL(),e=document.createElement("img");e.src=t,this.layers.forEach(function(t,e){this.deleteLayer(e)},this),this.container&&this.container.appendChild(e)}};for(u in f)t.prototype[u]=f[u];t.Object=function(){};var d,p;d={loop:function(t,e){var i,n,a,r,s,o,h,c,l,u,f,d,p=this.base.width();this.base.height();if(i=this.getImageData()){n=i.data,a=n.length;for(var g=0;a>g;g+=4)r=g/4,s=r%p,o=Math.floor(r/p),h=n[g],c=n[g+1],l=n[g+2],u=n[g+3],f={r:h,g:c,b:l,a:u},d=t.call(this.context,s,o,f),n[g]=d.r,n[g+1]=d.g,n[g+2]=d.b,n[g+3]=d.a;e!==!1&&this.putImageData(i)}return this},getImageData:function(){var t=this.context&&this.canvas.width>0&&this.canvas.height>0?this.context.getImageData(0,0,this.canvas.width,this.canvas.height):null;return t},putImageData:function(t){return this.context.putImageData(t,0,0),this},clear:function(){return this.context.clearRect(0,0,this.base.width(),this.base.height()),this},stackIndex:function(){return this.stack.indexOf(this)},remove:function(){return this.stack.splice(this.stackIndex(),1),this.base.autorender(),this},bump:function(t){var e=this.stackIndex();return this.remove(),this.stack.splice(e+t,0,this),this.base.autorender(),this},bumpUp:function(){return this.bump(1)},bumpDown:function(){return this.bump(-1)},bumpToTop:function(){return this.remove(),this.stack.push(this),this.base.autorender(),this},bumpToBottom:function(){return this.remove(),this.stack.unshift(this),this.base.autorender(),this}};for(p in d)t.Object.prototype[p]=d[p];t.Layer=function(t,e){if(this.base=t,this.canvas=document.createElement("canvas"),this.canvas.width=t.original?t.original.naturalWidth||t.original.width:t.width(),this.canvas.height=t.original?t.original.naturalHeight||t.original.height:t.height(),this.context=this.canvas.getContext("2d"),this.scale={x:1,y:1},this.elements=[],this.effects=[],this.base.layers||(this.base.layers=[]),this.stack=this.base.layers,this.stack.push(this),"string"==typeof e)this.type=e;else for(var i in e)this[i]=e[i];return this},t.Layer.prototype=Object.create(t.Object.prototype),t.Layer.prototype.normalizeX=function(t){return"string"==typeof t&&"%"===t.slice(-1)&&(t=this.normalizePercentX(+t.slice(0,-1))),t/this.scale.x},t.Layer.prototype.normalizeY=function(t){return"string"==typeof t&&"%"===t.slice(-1)&&(t=this.normalizePercentY(+t.slice(0,-1))),t/this.scale.y},t.Layer.prototype.normalizePercentX=function(t){return t/100*this.canvas.width},t.Layer.prototype.normalizePercentY=function(t){return t/100*this.canvas.height},t.prototype.newLayer=function(e){var i=new t.Layer(this,e);return this.currentLayer=i,this.autorender(),i},t.prototype.layer=function(t){return this.currentLayer=this.layers[t||0],this.layers[t||0]},t.registerElement=n,t.Element=function(t,e,i){var n=e.base||e,a=e.currentLayer||e;if(this.base=n,this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.canvas.width=n.original?n.original.naturalWidth||n.original.width:n.width(),this.canvas.height=n.original?n.original.naturalHeight||n.original.height:n.height(),this.scale={x:1,y:1},this.data=i||{},"string"==typeof i.x||"string"==typeof i.y){var r=i.x||"",s=i.y||"";"string"==typeof r&&"%"===r.slice(-1)&&(this.data.percentX=r,this.relativePosition=!0),"string"==typeof s&&"%"===s.slice(-1)&&(this.data.percentY=s)}return i.x&&(this.data.x=a.normalizeX(i.x)),i.y&&(this.data.y=a.normalizeY(i.y)),this.type=t,this.layer=a,this.effects=[],this.stack=this.layer.elements,this.stack.push(this),"background"===this.type&&(this.data={color:i},this.bumpToBottom()),this},t.Element.prototype=Object.create(t.Object.prototype),t.Element.prototype.setContext=function(t){var e=this.context;e.save(),e.fillStyle=t.color||"#000",e.fill(),e.scale(this.scale.x,this.scale.y),e.globalAlpha=t.alpha||1,e.lineWidth=t.strokeWidth?t.strokeWidth:0,e.lineCap=t.cap?t.cap:"square",e.strokeStyle=t.stroke?t.stroke:"transparent",e.stroke(),e.restore()},t.Element.prototype.update=function(t,e){var i,n;if(e)i=t,n=e,this.data[i]=n;else for(i in t)n=t[i],this.data[i]=n;("x"===i||"y"===i)&&(this.relativePosition=!1),this.base.autorender()},t.Element.prototype.moveTo=function(t,e){var i=this.data;return t=t||0,e=e||0,"line"===this.type?(i.endX+=t-i.x,i.endY+=e-i.y):"polygon"===this.type&&(i.points.forEach(function(n,a){if(a>0){var r=n[0],s=n[1];i.points[a]=[r+(t-i.points[0][0]),s+(e-i.points[0][1])]}}),i.points[0]=[t,e]),i.x=t,i.y=e,this.relativePosition=!1,this.base.autorender(),this},n("image",function(t){a.call(this,t)}),n("rect",function(t){r.call(this,t)}),n("background",function(t){r.call(this,t)}),n("line",function(t){var e=this.layer,i=this.context;return i.moveTo(e.normalizeX(t.x||0),e.normalizeY(t.y||0)),i.lineTo(e.normalizeX(t.endX),e.normalizeY(t.endY)),t.strokeWidth||(t.strokeWidth=1),t.stroke=t.color?t.color:"#000",this}),n("circle",function(t){var e=this.layer,i=this.context,n=e.normalizeX(t.x||0),a=e.normalizeY(t.y||0);i.arc(n,a,t.radius,0,2*Math.PI,!1)}),n("ellipse",function(t){var e,i=this.layer,n=this.context,a=i.normalizeX(t.x||0),r=i.normalizeY(t.y||0);return t.radiusX>t.radiusY?(e=t.radiusX/t.radiusY,n.scale(e,1),n.arc(a/e,r,t.radiusX/e,0,2*Math.PI,!1),n.scale(1/e,1)):(e=t.radiusY/t.radiusX,n.scale(1,e),n.arc(a,r/e,t.radiusY/e,0,2*Math.PI,!1),n.scale(1,1/e)),this}),n("polygon",function(t){for(var e=this.layer,i=this.context,n=0;nt)return this;t=Math.round(t);var e=2,i=this.base.width(),n=this.base.height(),a=i-1,r=n-1,s=t+1,o=2*t+1,h=c.mul_shift_table(t)[0],l=c.mul_shift_table(t)[1],u=e,f=this.context.getImageData();if(f){var d,p,g,y,m,v,x,b,E,w,z,k,I,X,Y,L,T,P=f.data,C=new c,D=C;for(g=1;o>g;g++)D=D.next=new c,g===s&&(L=D);for(D.next=C,T=null;u-->0;){for(x=v=0,p=n;--p>-1;){for(b=s*(k=P[v]),E=s*(I=P[v+1]),w=s*(X=P[v+2]),z=s*(Y=P[v+3]),D=C,g=s;--g>-1;)D.r=k,D.g=I,D.b=X,D.a=Y,D=D.next;for(g=1;s>g;g++)y=v+((g>a?a:g)<<2),b+=D.r=P[y],E+=D.g=P[y+1],w+=D.b=P[y+2],z+=D.a=P[y+3],D=D.next;for(T=C,d=0;i>d;d++)P[v++]=b*h>>>l,P[v++]=E*h>>>l,P[v++]=w*h>>>l,P[v++]=z*h>>>l,y=x+((y=d+t+1)d;d++){for(v=d<<2,b=s*(k=P[v]),E=s*(I=P[v+1]),w=s*(X=P[v+2]),z=s*(Y=P[v+3]),D=C,g=0;s>g;g++)D.r=k,D.g=I,D.b=X,D.a=Y,D=D.next;for(m=i,g=1;t>=g;g++)v=m+d<<2,b+=D.r=P[v],E+=D.g=P[v+1],w+=D.b=P[v+2],z+=D.a=P[v+3],D=D.next,r>g&&(m+=i);for(v=d,T=C,p=0;n>p;p++)y=v<<2,P[y+3]=Y=z*h>>>l,Y>0?(Y=255/Y,P[y]=(b*h>>>l)*Y,P[y+1]=(E*h>>>l)*Y,P[y+2]=(w*h>>>l)*Y):P[y]=P[y+1]=P[y+2]=0,y=d+((y=p+s)v;v++)for(var b=0;o>b;b++){var E=u+v-h,w=f+b-h;if(E>=0&&i>E&&w>=0&&e>w){var z=4*(E*e+w),k=s[v*o+b];p+=c[z]*k,g+=c[z+1]*k,y+=c[z+2]*k,m+=c[z+3]*k}}r[d]=p*t+c[d]*(1-t),r[d+1]=g*t+c[d+1]*(1-t),r[d+2]=y*t+c[d+2]*(1-t),r[d+3]=c[d+3]}this.context.putImageData(a)});var g=["click","mouseover","mousemove","mouseenter","mouseleave","mouseout","mousedown","mouseup"];g.forEach(function(e){t.prototype[e]=function(t){var i=l(this,t,e);return this.canvas.addEventListener(e,i.exec),this}}),t.prototype.on=function(t,e){return t=t.split(" "),t.forEach(function(t){var i=l(this,e,t);g.indexOf(t)>-1&&this.canvas.addEventListener(t,i.exec)},this),this},["width","height"].forEach(function(e){t.prototype[e]=function(t,i){return t?(this.canvas[e]=t,this.layers.forEach(function(n){n[e](t,i)}),this):this.canvas[e]},t.Layer.prototype[e]=function(i,n){var a,r=this;return i?(i=this["normalize"+("width"===e?"X":"Y")](i),this.canvas[e]=i,t.utils.forEach(this.elements,function(t){t.canvas[e]=i,t.relativePosition&&(t.data.percentX&&(t.data.x=r.normalizeX(t.data.percentX)),t.data.percentY&&(t.data.y=r.normalizeY(t.data.percentY)))}),a=n?i/this.canvas[e]:1,n&&("width"===e&&(this.scale.x*=a),"height"===e&&(this.scale.y*=a),this.canvas[e]=i),this.base.autorender(),this):this.canvas[e]}}),this.Martin=t}(); 2 | //# sourceMappingURL=martin.min.js.map 3 | -------------------------------------------------------------------------------- /docs/effects.md: -------------------------------------------------------------------------------- 1 | # Effects 2 | 3 | Martin.js comes with a number of built-in effects, which can be called from the working canvas (in which case it is added to the current layer), to a layer, or to a single element. 4 | 5 | ### .saturate(`amt`) 6 | 7 | Makes colors appear more intense by `amount` (number, 0-100). 8 | 9 | ``` 10 | canvas.saturate(100); 11 | ``` 12 | 13 | 14 | 15 | ### .desaturate(`amt`) 16 | 17 | Makes colors appear less intense by `amount` (number, 0-100). 18 | 19 | ``` 20 | canvas.desaturate(80); 21 | ``` 22 | 23 | 24 | 25 | ### .lighten(`amount`) 26 | 27 | Lightens by `amount` (number, 0-100). 28 | 29 | ``` 30 | canvas.lighten(25); 31 | ``` 32 | 33 | 34 | 35 | ### .darken(`amount`) 36 | 37 | Darkens by `amount` (number, 0-100). 38 | 39 | ``` 40 | canvas.darken(25); 41 | ``` 42 | 43 | 44 | 45 | ### .opacity(`amount`) 46 | 47 | Sets the layer's or element's opacity to `amount` (0-100). 48 | 49 | ``` 50 | canvas.opacity(50); 51 | ``` 52 | 53 | 54 | 55 | ### .blur(`amount`) 56 | 57 | Places an `amount` px blur on the layer or element, following Mario Klingemann's [StackBlur algorithm](https://github.com/Quasimondo/QuasimondoJS/blob/master/blur/StackBlur.js). `amount` must be between 0 and 256. 58 | 59 | ``` 60 | canvas.blur(15); 61 | ``` 62 | 63 | 64 | 65 | ### .sharpen(`amount`) 66 | 67 | Sharpen the element or layer by `amount`, where `amount` is a number greater than 0. There is technically no limit, but rendering will slow down with higher values. 68 | 69 | ``` 70 | canvas.sharpen(150); 71 | ``` 72 | 73 | 74 | 75 | ### .invert() 76 | 77 | Inverts the layer or element's colors. Does not take any arguments. 78 | 79 | ``` 80 | canvas.invert(); 81 | ``` 82 | 83 | 84 | 85 |
86 | 87 | All of the above methods return the `Effect`, on which can be called other, `Effect`-specific methods. **Any of the above `Effect`'s intensity/amount can be retrieved as a key on the effect: `var intensity = effect.data`** . 88 | 89 | ### effect.increase(`amount = 1`) 90 | 91 | Intensifies the `Effect` by `amount` (a number, relative to the effect's current intensity). If `amount` is left empty, increases the intensity by 1. 92 | 93 | ### effect.decrease(`amount = 1`) 94 | 95 | Decreases the `Effect`'s intensity by `amount` (a number, relative to the effect's current intensity). If `amount` is left empty, decreases the intensity by 1. 96 | 97 | ```js 98 | var effect = canvas.lighten(0); 99 | var increasing = true; 100 | (function flash() { 101 | var amount = effect.data; 102 | if ( increasing && amount < 100 ) { 103 | effect.increase(); 104 | } else if ( increasing && amount === 100 ) { 105 | increasing = false; 106 | effect.decrease() 107 | 108 | // .lighten() and .darken() are the inverses of each other, 109 | // and so actually range between -100 and 100 110 | } else if ( !increasing && amount > -100 ) { 111 | effect.decrease(); 112 | } else { 113 | increasing = true; 114 | effect.increase(); 115 | } 116 | requestAnimationFrame(flash); 117 | })(); 118 | ``` 119 | 120 | 121 | `Effects` can also be removed from their layer by calling: 122 | 123 | ### .remove() 124 | 125 | ```js 126 | var effect = canvas.lighten(50); 127 | 128 | // When the user clicks the canvas, remove the effect 129 | canvas.click(function() { 130 | effect.remove(); 131 | }); 132 | ``` 133 | 134 | 135 | 136 | ### .bump(`i`) 137 | 138 | Bump to position `i` (0-based). Can also use `.bumpUp()`, `.bumpDown()`, `.bumpToTop()`, or `.bumpToBottom()`. Like layers and elements, effects can also be reordered on their layer's or element's stack of effects. 139 | 140 |   141 | -------------------------------------------------------------------------------- /docs/elements.md: -------------------------------------------------------------------------------- 1 | # Elements 2 | 3 | **From the working canvas or a layer**, an element will be returned from the following methods. Elements are added to the top of the current layer, but may be reordered. 4 | 5 | ### .background(`color`) 6 | 7 | Fills the background of the current layer with `color` . The background is automatically bumped to the bottom on its layer, so even if you call it after adding other elements to a layer, it will appear as the background. 8 | 9 | ```js 10 | // create a new layer, give it a red background, set opacity to 50% 11 | canvas.newLayer(); 12 | canvas.background('#f00'); 13 | canvas.opacity(50); 14 | 15 | // acts as a red overlay on the base layer's image 16 | ``` 17 | 18 | 19 | 20 | ### .line(`obj`) 21 | 22 | `obj` must have key-value pairs for: 23 | 24 | * `x` 25 | * `y` 26 | * `width` 27 | * `height` 28 | 29 | For all of the above, value must be a number (will be read as pixels from upper left 0) or a percent as a string (ex. '50%'). 30 | 31 | Optional: 32 | 33 | * `strokeWidth` (defaults to 1) 34 | * `color` (defaults to black) 35 | * `cap` (defaults to square) 36 | 37 | ```js 38 | canvas.line({ 39 | x: 40, 40 | y: 100, 41 | endX: '95%', 42 | endY: 30, 43 | strokeWidth: 10, 44 | color: '#fff', 45 | cap: 'round' 46 | }); 47 | ``` 48 | 49 | 50 | 51 | ### .rect(obj) 52 | 53 | `obj` must have key-value pairs for: 54 | 55 | * `x` 56 | * `y` 57 | * `width` 58 | * `height` 59 | 60 | For all of the above, value must be a number (will be read as pixels from upper left 0) or a percent as a string (ex. '50%'). 61 | 62 | Optional: 63 | 64 | * `color` (defaults to black) 65 | * `strokeWidth` (defaults to 0) 66 | * `stroke` (defaults to transparent) 67 | 68 | ```js 69 | canvas.rect({ 70 | x: '60%', 71 | y: 20, 72 | width: '40%', 73 | height: 260, 74 | color: '#ff0' 75 | }); 76 | ``` 77 | 78 | 79 | 80 | ### .polygon(`obj`) 81 | 82 | `obj` must have a key `points`, which must be an array of arrays, where each nested array represents a point of the polygon. Points may be numbers, percentages, or mixed (ex. `arr = [[20, 40], ['10%', '50%'], [80, '75%']]` ). 83 | 84 | `obj` accepts the same optional key-value pairs as `.rect()` , with the same defaults. 85 | 86 | ```js 87 | canvas.polygon({ 88 | points: [ 89 | ['20%', '10%'], 90 | ['40%', '40%'], 91 | ['20%', '40%'] 92 | ], 93 | color: '#fff' // white 94 | }); 95 | 96 | canvas.polygon({ 97 | points: [ 98 | [300, 200], 99 | [350, 200], 100 | [300, 250], 101 | [250, 250] 102 | ], 103 | color: '#00f', // blue fill 104 | strokeWidth: 2, 105 | stroke: '#000' // black stroke 106 | }); 107 | ``` 108 | 109 | 110 | 111 | ### .circle(`obj`) 112 | 113 | Requires `x`, `y`, `radius` (`radius` may only be a number, *not* a percent as string). 114 | 115 | Same options and defaults as `.rect()` . 116 | 117 | ``` 118 | canvas.circle({ 119 | x: 320, 120 | y: 250, 121 | radius: 35, 122 | color: '#ef3' 123 | }); 124 | ``` 125 | 126 | 127 | 128 | ### .ellipse(`obj`) 129 | 130 | Requires `x`, `y`, `radiusX`, `radiusY` (the two radii may only be numbers). 131 | 132 | Same options and defaults as `.rect()` . 133 | 134 | ``` 135 | canvas.ellipse({ 136 | offsetX: 300, 137 | offsetY: 250, 138 | radiusX: 100, 139 | radiusY: 35, 140 | color: '#ef3' 141 | }); 142 | ``` 143 | 144 | 145 | 146 | ### .text(`obj`) 147 | 148 | Requires a string key `text` . Writes that text on one line. `obj` defaults to: 149 | 150 | - `x` : 0 151 | - `y` : 0 152 | - `color` : black 153 | - `size` : 16 154 | - `align` : left 155 | - `font` : 'Helvetica' 156 | 157 | ```js 158 | canvas.text({ 159 | text: 'Hello, world!', 160 | x: 140, 161 | y: 220, 162 | size: 20, 163 | color: '#fe0', 164 | font: 'Georgia' 165 | }); 166 | ``` 167 | 168 | 169 | 170 |
171 | 172 | All of the above methods return the element, on which can be called other, element-specific methods: 173 | 174 | ### .remove() 175 | 176 | Removes the element from its layer. It can then be added to another layer by calling `otherLayer.addElement(element)` . Returns the element. 177 | 178 | ### .bump(`i`) 179 | 180 | Bump to position `i` (0-based). Can also use `.bumpUp()`, `.bumpDown()`, `.bumpToTop()`, or `.bumpToBottom()`. As with layers, elements can be reordered within their stack on the layer. 181 | 182 | In the below example, calling `circle1.bumpUp()` has the same effect as calling `circle1.bumpToTop()` , and `circle2.bumpDown()` . 183 | 184 | ```js 185 | var circle1 = canvas.circle({ 186 | radius: 100, 187 | color: '#f00' 188 | }); 189 | var circle2 = canvas.circle({ 190 | x: 100, 191 | radius: 100, 192 | color: '#00f' 193 | }); 194 | // circle 1 is now below circle 2 195 | circle1.bumpUp(); 196 | // circle 1 is now above circle 2 197 | ``` 198 | 199 | 200 | 201 | **In the above example, calling `circle2.bump2Bottom()` would effectively hide `circle2` from the viewer.** The reason for this is that, when a working canvas is created from an image, an element is created from that image. So the element stack for the above layer, after running the example code, looks like: 202 | 203 | - 0: `image` (bunny) 204 | - 1: `circle` (blue circle) 205 | - 2: `circle` (red circle) 206 | 207 | ### .moveTo(`x = 0`, `y = 0`) 208 | 209 | Move an element in space on the canvas. `x` and `y` must be numbers (will be read as pixels from upper left 0) or percents as a string (ex. '50%'). 210 | 211 | For circles and ellipses, movement is relative to the center. For rectangles, relative to upper left. For lines, relative to the line's starting point. For polygons, relative to the first declared point. For text, the position depends on the alignment (left, center, or right). 212 | 213 | ```js 214 | // Since this example involves animating elements, be sure to 215 | // instantiate with `autorender: false` 216 | 217 | var circle1 = canvas.circle({ 218 | radius: 100, 219 | x: '50%', 220 | y: '50%', 221 | color: '#f00' 222 | }); 223 | var circle2 = canvas.circle({ 224 | radius: 60, 225 | x: '50%', 226 | y: '50%', 227 | color: '#fff' 228 | }); 229 | 230 | var t = 0; 231 | 232 | (function bounce() { 233 | 234 | // increment the time counter 235 | t++; 236 | 237 | // this would go much too fast if we call sin(t) directly, 238 | // so massage the numbers a bit 239 | var amount = 30 * Math.sin(t * Math.PI / 180) 240 | 241 | // move both circles 242 | circle1.moveTo( 0.5 * canvas.width(), 0.5 * canvas.height() + 30 * Math.sin(t) ); 243 | circle2.moveTo( 0.5 * canvas.width(), 0.5 * canvas.height() + 30 * Math.sin(t) ); 244 | 245 | // since the canvas is not automatically rendering, 246 | // force it to render here 247 | canvas.render(); 248 | 249 | // call the function again on the next animation frame 250 | // see: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame 251 | requestAnimationFrame(bounce); 252 | })(); 253 | ``` 254 | 255 | 256 | 257 | ### element.update(`key`, `value`) or element.update(`data`) 258 | 259 | Updates data on an element and attempts to autorender the working canvas. If two parameters are passed, the first is taken as a key with the second its value. If an object is passed, checks each key-value pair and adds or updates on the element. 260 | 261 | ```js 262 | // create a text element 263 | var text = canvas.text({ 264 | text: 'Hello, world!' 265 | }); 266 | 267 | // update the text's text 268 | text.update('text', 'I am the new text!'); 269 | 270 | // update the text's color and size 271 | text.update({ 272 | color: '#f00', 273 | size: 40 274 | }); 275 | ``` 276 | 277 | 278 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | You can listen for a number of mouse events, and attach functions to run when those events are fired on the working canvas. In your callback function, include a single parameter for the `Event` object, which contains keys `x` and `y` (as well as the usual `MouseEvent` keys), corresponding to the mouse's coordinates over the working canvas. 4 | 5 | If you use the `.on()` method instead of the shorthand, you can attach listeners for multiple events by calling `.on('event1 event2 event3', callback)` , with events separated by spaces. 6 | 7 | ### .click(`callback`) or .on('click', `callback`) 8 | 9 | ```js 10 | // CLICK TO TOGGLE A BLUR 11 | 12 | var blurred = false, 13 | effect = canvas.blur(0); // don't actually blur, but add an effect 14 | canvas.click(function() { 15 | if ( !blurred ) { 16 | effect.increase(20); 17 | blurred = true; 18 | } else { 19 | effect.decrease(20); 20 | blurred = false; 21 | } 22 | }); 23 | ``` 24 | 25 | 26 | ### .mouseover(`callback`) or .on('mouseover', `callback`) 27 | ### .mouseout(`callback`) or .on('mouseout', `callback`) 28 | 29 | ```js 30 | // Mousing over and mousing out of the canvas will move 31 | // the circle to random coordinates on the canvas 32 | var circle = canvas.circle({ 33 | x: Math.random() * canvas.width(), 34 | y: Math.random() * canvas.height(), 35 | radius: 50, 36 | color: '#00f' 37 | }); 38 | 39 | canvas.on('mouseover mouseout', function() { 40 | // move the circle to random coordinates on the canvas 41 | circle.moveTo( Math.random() * canvas.width(), Math.random() * canvas.height() ); 42 | }); 43 | ``` 44 | 45 | 46 | ### .mouseenter(`callback`) or .on('mouseenter', `callback`) 47 | ### .mouseleave(`callback`) or .on('mouseleave', `callback`) 48 | 49 | Read about the differences between `mouseenter` and `mouseover` / `mouseleave` and `mouseout` [here](http://www.quirksmode.org/js/events_mouse.html). 50 | 51 | ### .mousedown(`callback`) or .on('mousedown', `callback`) 52 | ### .mouseup(`callback`) or .on('mouseup', `callback`) 53 | 54 | ```js 55 | function randomCircle() { 56 | 57 | // generate a random color and size 58 | var hex = '0123456789abcdef'; 59 | var radius = Math.random() * 50, 60 | r = hex[Math.floor(Math.random() * hex.length)], 61 | g = hex[Math.floor(Math.random() * hex.length)], 62 | b = hex[Math.floor(Math.random() * hex.length)]; 63 | 64 | canvas.circle({ 65 | radius: radius, 66 | color: '#' + r + g + b, 67 | x: Math.random() * canvas.width(), 68 | y: Math.random() * canvas.height() 69 | }); 70 | } 71 | // fire once for good measure :-) 72 | randomCircle(); 73 | canvas.on('mousedown mouseup', randomCircle); 74 | ``` 75 | 76 | 77 | 78 | ### .mousemove(`callback`) or .on('mousemove', `callback`) 79 | 80 | ```js 81 | // Move the mouse to move the text 82 | var text = canvas.text({ 83 | text: 'Follow that mouse!', 84 | x: '50%', 85 | y: '50%', 86 | color: '#fff', 87 | size: 24, 88 | align: 'center' 89 | }); 90 | 91 | canvas.mousemove(function(e) { 92 | text.moveTo(e.x, e.y); 93 | }); 94 | ``` 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/images/bunny-old.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/docs/images/bunny-old.jpg -------------------------------------------------------------------------------- /docs/images/bunny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/docs/images/bunny.jpg -------------------------------------------------------------------------------- /docs/images/martin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/docs/images/martin.jpg -------------------------------------------------------------------------------- /docs/images/marty-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/docs/images/marty-banner.png -------------------------------------------------------------------------------- /docs/images/marty-full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/docs/images/marty-full.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martin.js is a JavaScript library for working with HTML5 canvas. Martin supports jQuery-like chained methods, and makes photo manipulation, drawing, and animation in browser easy for developers and users. 4 | 5 | You can download Martin (latest: v0.4.2) here: 6 | 7 | - [Full version, annotated (36 kb)](download/martin.js) 8 | - [Minified (16 kb)](download/martin.min.js) 9 | 10 | Or through Bower: 11 | 12 | ```js 13 | bower install martinjs --save 14 | ``` 15 | 16 | \- Scottland / [@scottpdonaldson](https://twitter.com/scottpdonaldson) 17 | 18 |
19 | 20 | ## Initializing 21 | 22 | First, make sure you've included the source file, ideally in the `` of the page, but always before you call `Martin`. 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | Set up a canvas like this: 29 | ```js 30 | var canvas = Martin('el', options); 31 | ``` 32 | 33 | `el` can be the ID of an existing `` or `` element, an element itself, or nothing (in which case you will work on a virtual/buffer canvas). If you include `options` , it must be an object with `key: value` pairs that describe how you want to instantiate. 34 | 35 |
36 | 37 | ## Example 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | 44 | 45 | We're going to take the original image, draw a white rectangle on it, write "Hello!" over the rectangle, and then desaturate it all by 100% -- turning it black and white. 46 | 47 | ```js 48 | var canvas = Martin('image'); 49 | 50 | canvas.rect({ 51 | x: 100, 52 | y: 200, 53 | width: 90, 54 | height: 50, 55 | color: '#fff' 56 | }); 57 | 58 | canvas.text({ 59 | text: 'Hello!', 60 | align: 'center', 61 | size: 18, 62 | x: 145, 63 | y: 215 64 | }); 65 | 66 | canvas.desaturate(100); 67 | ``` 68 | 69 | 90 | -------------------------------------------------------------------------------- /docs/js/demos.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | (function() { 4 | var canvas = Martin('demo-sepia'); 5 | canvas.darken(10); 6 | canvas.desaturate(100); 7 | 8 | canvas.newLayer(); 9 | canvas.background('#ea0'); 10 | canvas.opacity(40); 11 | canvas.rect({ 12 | width: '100%', 13 | height: '15%' 14 | }); 15 | 16 | canvas.newLayer(); 17 | canvas.text({ 18 | text: 'The loneliest bunny in the west.', 19 | x: '50%', 20 | y: 13, 21 | align: 'center', 22 | color: '#fff' 23 | }); 24 | 25 | })(); 26 | 27 | (function() { 28 | var canvas = Martin('demo-checkerboard'); 29 | var w = canvas.width(), 30 | h = canvas.height(), 31 | iter = 0; 32 | 33 | var size = 40, 34 | x = w / size, // tiles in x direction 35 | y = h / size; // tiles in y direction 36 | 37 | for ( var i = 0; i < x; i++ ) { 38 | 39 | if ( y % 2 !== 1 ) iter++; 40 | 41 | for ( var j = 0; j < y; j++ ) { 42 | 43 | if ( iter % 2 === 0 ) { 44 | 45 | canvas.rect({ 46 | x: i * size, 47 | y: j * size, 48 | height: size, 49 | width: size 50 | }); 51 | } 52 | iter++; 53 | } 54 | } 55 | 56 | })(); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /docs/js/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | (function() { 4 | 5 | var canvas = Martin('martin-home-blur', { autorender: false }), 6 | rainbow, 7 | t = 0, // time starts at 0 8 | hasMousedOver = false, 9 | animatingHelper = false; 10 | 11 | Martin.registerEffect('rainbow', function(t) { 12 | this.context.loop(function(x, y, pixel) { 13 | 14 | // Increase by a maximum of 100, and take in x value 15 | // and time as parameters. 16 | pixel.r += 100 * Math.sin((x - 4 * t) / 100); 17 | pixel.g += 100 * Math.sin((x - 8 * t) / 100); 18 | pixel.b += 100 * Math.sin((x - 12 * t) / 100); 19 | 20 | return pixel; 21 | }); 22 | }); 23 | 24 | (function createRainbow() { 25 | 26 | if ( rainbow ) rainbow.remove(); 27 | rainbow = canvas.layer(0).rainbow(t); 28 | t++; 29 | 30 | if ( t === 100 && !hasMousedOver && !animatingHelper ) { 31 | animatingHelper = true; 32 | animateHelperTo(280); 33 | } 34 | 35 | if ( animatingHelper && hasMousedOver ) { 36 | animatingHelper = false; 37 | animateHelperTo(360); 38 | } 39 | 40 | canvas.render(); 41 | 42 | // requestAnimationFrame will wait until the browser is ready to 43 | // repaint the canvas. 44 | requestAnimationFrame(createRainbow); 45 | })(); 46 | 47 | var circle = canvas.circle({ 48 | x: '50%', 49 | y: '50%', 50 | radius: 80, 51 | color: '#fff' 52 | }); 53 | 54 | circle.blur(60); 55 | 56 | canvas.mousemove(function(e) { 57 | hasMousedOver = true; 58 | circle.moveTo(e.x, e.y); 59 | }); 60 | 61 | canvas.newLayer(); 62 | 63 | var text = canvas.text({ 64 | font: 'Futura', 65 | text: 'THIS IS MARTIN.JS', 66 | align: 'center', 67 | x: '50%', 68 | y: 20, 69 | color: '#fff', 70 | size: 66 71 | }); 72 | 73 | var helperText = canvas.text({ 74 | font: 'Futura', 75 | align: 'center', 76 | x: '50%', 77 | y: 360, 78 | text: '(Hover over or touch the rabbit)', 79 | size: 20, 80 | color: '#fff' 81 | }); 82 | 83 | function animateHelperTo(target) { 84 | 85 | var diff; 86 | 87 | if ( helperText.data.y !== target ) { 88 | 89 | diff = target - helperText.data.y; 90 | 91 | diff = diff > 0 ? 1 : -1; 92 | 93 | requestAnimationFrame(function() { 94 | helperText.data.y += diff; 95 | animateHelperTo(target); 96 | }); 97 | } 98 | } 99 | 100 | })(); 101 | 102 | (function() { 103 | 104 | var canvas = Martin('martin-autorender', { 105 | autorender: false 106 | }), 107 | t = 0, 108 | circles = [], 109 | num = 60, 110 | text, 111 | offText = 'Automatic rendering is off (smooth).', 112 | onText = 'Automatic rendering is on (janky).'; 113 | 114 | function x(i) { 115 | return ( (100 * i) / num) + (50 / num) + '%'; 116 | } 117 | 118 | function y(i, t) { 119 | 120 | var heightOffset = canvas.height() / 4; 121 | 122 | i *= 0.25; 123 | t *= Math.PI / 40; 124 | 125 | return 0.5 * canvas.height() + heightOffset * Math.sin(i + t); 126 | } 127 | 128 | function toggleText(t) { 129 | return (t === onText) ? offText : onText; 130 | } 131 | 132 | canvas.width(500).height(200); 133 | 134 | canvas.newLayer(); 135 | canvas.background('#ccc'); 136 | 137 | for ( var i = 0; i < num; i++ ) { 138 | circles.push(canvas.circle({ 139 | radius: Math.round(canvas.width() / ( 2.5 * num )) 140 | })); 141 | } 142 | 143 | text = canvas.text({ 144 | color: '#fff', 145 | text: offText 146 | }); 147 | 148 | (function moveCircles() { 149 | 150 | circles.forEach(function(circle, i) { 151 | circle.moveTo( x(i), y(i, t) ); 152 | }); 153 | 154 | if ( canvas.options.autorender === false ) canvas.render(); 155 | 156 | t++; 157 | 158 | requestAnimationFrame(moveCircles); 159 | })(); 160 | 161 | canvas.click(function() { 162 | canvas.options.autorender = !canvas.options.autorender; 163 | text.data.text = toggleText(text.data.text); 164 | canvas.render(); 165 | }); 166 | 167 | })(); 168 | 169 | Martin('martin-height-200-crop').height(200); 170 | Martin('martin-height-200-resize').height(200, true); 171 | Martin('martin-height-400-resize').height(400, true); 172 | Martin('martin-width-200-crop').width(200); 173 | Martin('martin-width-200-resize').width(200, true); 174 | Martin('martin-width-500-resize').width(500, true); 175 | 176 | (function(){ 177 | var back = Martin('martin-background'); 178 | back.newLayer(); 179 | back.background('#f00'); 180 | back.opacity(50); 181 | })(); 182 | 183 | line = Martin('martin-line').line({ 184 | x: 40, 185 | y: 100, 186 | endX: '95%', 187 | endY: 30, 188 | strokeWidth: 10, 189 | color: '#fff', 190 | cap: 'round' 191 | }); 192 | 193 | Martin('martin-rect').rect({ 194 | x: '60%', 195 | y: 20, 196 | width: '40%', 197 | height: 260, 198 | color: '#ff0' 199 | }); 200 | 201 | var poly = Martin('martin-polygon'); 202 | poly.polygon({ 203 | points: [ 204 | ['20%', '10%'], 205 | ['40%', '40%'], 206 | ['20%', '40%'] 207 | ], 208 | color: '#fff' // white 209 | }); 210 | 211 | poly.polygon({ 212 | points: [ 213 | [300, 200], 214 | [350, 200], 215 | [300, 250], 216 | [250, 250] 217 | ], 218 | color: '#00f', // blue fill 219 | strokeWidth: 2, 220 | stroke: '#000' // black stroke 221 | }); 222 | 223 | Martin('martin-circle').circle({ 224 | x: 320, 225 | y: 250, 226 | radius: 35, 227 | color: '#ef3' 228 | }); 229 | 230 | Martin('martin-ellipse').ellipse({ 231 | x: 300, 232 | y: 250, 233 | radiusX: 100, 234 | radiusY: 35, 235 | color: '#ef3' 236 | }); 237 | 238 | Martin('martin-text').text({ 239 | text: 'Hello, world!', 240 | x: 140, 241 | y: 220, 242 | size: 20, 243 | color: '#fe0', 244 | font: 'Georgia' 245 | }); 246 | 247 | (function() { 248 | var canvas = Martin('martin-bump-up'); 249 | var circle1 = canvas.circle({ 250 | radius: 100, 251 | color: '#f00' 252 | }); 253 | var circle2 = canvas.circle({ 254 | x: 100, 255 | radius: 100, 256 | color: '#00f' 257 | }); 258 | // circle 1 is now below circle 2 259 | circle1.bumpUp(); 260 | })(); 261 | 262 | (function() { 263 | var canvas = Martin('martin-move-to', { autorender: false }); 264 | var circle1 = canvas.circle({ 265 | radius: 100, 266 | x: '50%', 267 | y: '50%', 268 | color: '#f00' 269 | }); 270 | var circle2 = canvas.circle({ 271 | radius: 60, 272 | x: '50%', 273 | y: '50%', 274 | color: '#fff' 275 | }); 276 | var t = 0; 277 | (function bounce() { 278 | t += Math.PI / 180; 279 | circle1.moveTo( 0.5 * canvas.width(), 0.5 * canvas.height() + 30 * Math.sin(t) ); 280 | circle2.moveTo( 0.5 * canvas.width(), 0.5 * canvas.height() + 30 * Math.sin(t) ); 281 | canvas.render(); 282 | requestAnimationFrame(bounce); 283 | })(); 284 | })(); 285 | 286 | (function() { 287 | var canvas = Martin('martin-update'); 288 | canvas.background('#eee'); 289 | var text = canvas.text({ 290 | text: 'Hello, world!' 291 | }); 292 | text.update('text', 'I am the new text!'); 293 | text.update({ 294 | color: '#f00', 295 | size: 40 296 | }); 297 | })(); 298 | 299 | Martin('martin-saturate').saturate(100); 300 | Martin('martin-desaturate').desaturate(80); 301 | Martin('martin-lighten').lighten(25); 302 | Martin('martin-darken').darken(25); 303 | Martin('martin-opacity').opacity(50); 304 | Martin('martin-blur').blur(15); 305 | Martin('martin-sharpen').sharpen(150); 306 | Martin('martin-invert').invert(); 307 | 308 | (function() { 309 | var canvas = Martin('martin-flash', { autorender: false }); 310 | var effect = canvas.lighten(0); 311 | var increasing = true; 312 | (function flash() { 313 | var amount = effect.data; 314 | if ( increasing && amount < 100 ) { 315 | effect.increase(); 316 | } else if ( increasing && amount === 100 ) { 317 | increasing = false; 318 | effect.decrease() 319 | 320 | // .lighten() and .darken() are the inverses of each other, 321 | // and so actually range between -100 and 100 322 | } else if ( !increasing && amount > -100 ) { 323 | effect.decrease(); 324 | } else { 325 | increasing = true; 326 | effect.increase(); 327 | } 328 | canvas.render(); 329 | requestAnimationFrame(flash); 330 | })(); 331 | })(); 332 | 333 | (function() { 334 | var canvas = Martin('martin-effect-remove'); 335 | var effect = canvas.lighten(50); 336 | canvas.click(function() { 337 | effect.remove(); 338 | }); 339 | })(); 340 | 341 | (function() { 342 | 343 | Martin.registerEffect('myNewEffect', function(data) { 344 | 345 | this.context.loop(function(x, y, pixel) { 346 | 347 | pixel.r = 100; 348 | pixel.g += 5 + data.a; 349 | pixel.b -= Math.round(data.b / 2); 350 | 351 | return pixel; 352 | }); 353 | }); 354 | 355 | // After having registered the new effect, call it on the instance of Martin 356 | var canvas = Martin('martin-my-new-effect'); 357 | 358 | canvas.myNewEffect({ 359 | a: 100, 360 | b: 100 361 | }); 362 | })(); 363 | 364 | (function() { 365 | var canvas = Martin('martin-click'); 366 | var blurred = false, 367 | effect = canvas.blur(0); // don't actually blur, but add an effect 368 | 369 | canvas.click(function() { 370 | if ( !blurred ) { 371 | effect.increase(20); 372 | blurred = true; 373 | } else { 374 | effect.decrease(20); 375 | blurred = false; 376 | } 377 | }); 378 | })(); 379 | 380 | (function() { 381 | var canvas = Martin('martin-mouseover'); 382 | var circle = canvas.circle({ 383 | x: Math.random() * canvas.width(), 384 | y: Math.random() * canvas.height(), 385 | radius: 50, 386 | color: '#00f' 387 | }); 388 | 389 | canvas.on('mouseover mouseout', function() { 390 | // move the circle to random coordinates on the canvas 391 | circle.moveTo( Math.random() * canvas.width(), Math.random() * canvas.height() ); 392 | }); 393 | })(); 394 | 395 | (function() { 396 | var canvas = Martin('martin-mousedown'); 397 | function randomCircle() { 398 | 399 | // generate a random color and size 400 | var hex = '0123456789abcdef'; 401 | var radius = Math.random() * 50, 402 | r = hex[Math.floor(Math.random() * hex.length)], 403 | g = hex[Math.floor(Math.random() * hex.length)], 404 | b = hex[Math.floor(Math.random() * hex.length)]; 405 | 406 | canvas.circle({ 407 | radius: radius, 408 | color: '#' + r + g + b, 409 | x: Math.random() * canvas.width(), 410 | y: Math.random() * canvas.height() 411 | }); 412 | } 413 | // fire once for good measure :-) 414 | randomCircle(); 415 | canvas.on('mousedown mouseup', randomCircle); 416 | })(); 417 | 418 | (function() { 419 | var canvas = Martin('martin-mousemove'); 420 | var text = canvas.text({ 421 | text: 'Follow that mouse!', 422 | x: '50%', 423 | y: '50%', 424 | color: '#fff', 425 | size: 24, 426 | align: 'center' 427 | }); 428 | 429 | canvas.mousemove(function(e) { 430 | text.moveTo(e.x, e.y); 431 | }); 432 | })(); 433 | 434 | (function() { 435 | Martin.registerElement('star', function(data) { 436 | // let data.size be the radius of the star 437 | var size = data.size, 438 | centerX = data.x, 439 | centerY = data.y; 440 | 441 | var context = this.context; 442 | 443 | var angles = [54, 126, 198, 270, 342]; 444 | angles = angles.map(Martin.degToRad); 445 | 446 | angles.forEach(function(angle, i) { 447 | 448 | var next = angles[i + 1] || angle + Martin.degToRad(72), 449 | average = 0.5 * (angle + next); 450 | 451 | context.lineTo(centerX + Math.cos(angle) * size, centerY + Math.sin(angle) * size); 452 | context.lineTo(centerX + Math.cos(average) * size / 2.5, centerY + Math.sin(average) * size / 2.5); 453 | }); 454 | 455 | context.lineTo(centerX + Math.cos(angles[0]) * size, centerY + Math.sin(angles[0]) * size); 456 | context.closePath(); 457 | }); 458 | 459 | var canvas = Martin('martin-plugins-star'); 460 | var star = canvas.star({ 461 | color: '#f00', 462 | stroke: '#000', 463 | strokeWidth: 10, 464 | size: 50, 465 | x: '50%', 466 | y: '50%' 467 | }); 468 | 469 | canvas.mousemove(function(e) { 470 | star.moveTo(e.x, e.y); 471 | }); 472 | })(); 473 | 474 | }); 475 | -------------------------------------------------------------------------------- /docs/layers.md: -------------------------------------------------------------------------------- 1 | ## Layers 2 | 3 | **From the working canvas**, a layer will be returned from the following methods. At any given moment, the working canvas has a **current layer**, and any new elements or effects called from the working canvas are added to the current layer. Elements or effects called from a layer are always added to that layer. 4 | 5 | ### canvas.newLayer() 6 | 7 | Creates a new layer at the top of the layer stack and retrieves that layer. 8 | 9 | ```js 10 | var layer1 = canvas.newLayer(); 11 | 12 | layer1 === canvas.currentLayer; 13 | // true 14 | 15 | var layer2 = canvas.newLayer(); 16 | 17 | layer1 === canvas.currentLayer; 18 | // now this is false: 19 | // layer2 is the current layer 20 | ``` 21 | 22 | ### canvas.layer(`i`) 23 | 24 | Retrieve and switch the current layer to the layer at index `i`. If `i` is not given, retrieve the bottom-most layer. 25 | 26 | ```js 27 | var canvas = Martin('canvas'), // instantiates with a base layer (0) 28 | baseLayer = canvas.layer(0), 29 | newLayer = canvas.newLayer(); 30 | 31 | canvas.layer(0); // switches context to the base layer 32 | var sameLayer = canvas.layer(1); 33 | 34 | sameLayer === newLayer; // true 35 | ``` 36 | 37 |
38 | 39 | Once a layer has been retrieved, you can call the following methods **on the layer**: 40 | 41 | ### layer.clear() 42 | 43 | Clears the layer of all pixel data, but remembers elements and effects. The layer remains on the working canvas. Returns the layer. 44 | 45 | ### layer.remove() 46 | 47 | Removes the layer from the working canvas (it will no longer be rendered), but remembers elements and effects. Returns the layer. 48 | 49 | ### layer.bump(`i`) 50 | 51 | Bumps the layer to position `i` (a number), with `0` being the bottom layer. Returns the layer. 52 | 53 | ### layer.bumpUp() 54 | 55 | Bumps the layer up in the layer stack. Returns the layer. 56 | 57 | ### layer.bumpDown() 58 | 59 | Bumps the layer down in the layer stack. Returns the layer. 60 | 61 | ### layer.bumpToTop() 62 | 63 | Bumps the layer to the top of the layer stack. Returns the layer. 64 | 65 | ### layer.bumpToBottom() 66 | 67 | Bumps the layer to the bottom of the layer stack. Equivalent to calling `layer.bump(0)`. Returns the layer. 68 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ## How Martin.js Works 4 | 5 | In order to make the most of Martin.js, you'll need to understand these three object types: 6 | 7 | 1. **Layers** 8 | 2. **Elements** 9 | 3. **Effects** 10 | 11 | Similar to working with images in Photoshop, you can set up multiple layers in Martin.js and work on them individually. If one layer is "above" another, then you only see on the lower layer what's not covered by the higher layer. 12 | 13 | **Layers** may contain **elements** and **effects**. An element is an image, geometric shape, or piece of text. An effect is a visual effect such as lightening or blurring. Effects can be applied to layers, altering the appearance of all the layer's elements, or only to a single element on a layer. 14 | 15 | When you initialize, a base layer is automatically created. If you initialize from an existing `` or ``, any existing image data is automatically put into an element on the base layer. 16 | 17 | ## Rendering 18 | 19 | If you instantiate without any `options` , any changes you make will automatically appear on the canvas. For example, if you call `canvas.darken(25)` , you will see it darken immediately. However, if you instantiate like this: 20 | 21 | ```js 22 | var canvas = Martin('canvas', { 23 | autorender: false 24 | }); 25 | ``` 26 | 27 | Then any changes you make will not immediately appear. You will need to call `canvas.render()` after making your changes in order to see them take place. This is most useful when animating multiple elements on a canvas — since each individual change re-renders the canvas, if you are making several changes with each animation frame, it is more efficient to render all the changes in one fell swoop. 28 | 29 | See the below canvas for a performance comparison. Initially, `autorender` is set to `false` , and performance should be relatively smooth. Click the canvas to toggle `autorender` off and on again to see changes in performance. 30 | 31 | 32 | 33 |   34 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | New elements can be registered with: 4 | 5 | ### Martin.registerElement(`name`, `cb`) 6 | 7 | `name` should be a string that will allow the new element to be called with `canvas.name()` or `layer.name()` . 8 | 9 | `cb` should be a callback function that describes how the element should be drawn on its layer. It will be helpful to use `this.context` inside the callback function to refer to the drawing context. 10 | 11 | `cb` takes a single parameter, an object with key-value pairs that will be automatically set on the element and can be used within the rendering function. 12 | 13 | ```js 14 | Martin.registerElement('myNewElement', function(data) { 15 | // in the body of this function, describe how 16 | // the element will be drawn 17 | }); 18 | ``` 19 | 20 | **Example:** 21 | 22 | ```js 23 | Martin.registerElement('star', function(data) { 24 | // let data.size be the radius of the star 25 | var size = data.size, 26 | centerX = data.x, 27 | centerY = data.y; 28 | 29 | var context = this.context; 30 | 31 | // convert these angles to radians 32 | var angles = [54, 126, 198, 270, 342]; 33 | angles = angles.map(Martin.degToRad); 34 | 35 | angles.forEach(function(angle, i) { 36 | 37 | // find the angle that is halfway between the current and next angle 38 | var next = angles[i + 1] || angle + Martin.degToRad(72), 39 | average = 0.5 * (angle + next); 40 | 41 | // draw a long line to the first point 42 | context.lineTo(centerX + Math.cos(angle) * size, centerY + Math.sin(angle) * size); 43 | // and a short line to the second 44 | context.lineTo(centerX + Math.cos(average) * size / 2.5, centerY + Math.sin(average) * size / 2.5); 45 | }); 46 | 47 | // once we're done, bring our line back to the first point 48 | context.lineTo(centerX + Math.cos(angles[0]) * size, centerY + Math.sin(angles[0]) * size); 49 | // and close the path 50 | context.closePath(); 51 | }); 52 | 53 | // since this is an element, it can take all of the usual 54 | // attributes that are applied to elements 55 | var star = canvas.star({ 56 | color: '#f00', 57 | stroke: '#000', 58 | strokeWidth: 10, 59 | size: 50, 60 | x: '50%', 61 | y: '50%' 62 | }); 63 | 64 | // it can also be updated just like any other element 65 | canvas.mousemove(function(e) { 66 | star.moveTo(e.offsetX, e.offsetY); 67 | }) 68 | ``` 69 | 70 | 71 | 72 |
73 | 74 | New `Effects` can be registered with: 75 | 76 | ### Martin.registerEffect(`name`, `cb`) 77 | 78 | `name` should be a string that will allow the new `Effect` to be called with `canvas.name()`. 79 | 80 | `cb` should be a callback function that describes how the `Effect` manipulates the canvas. The best way to use this is to use the `Layer.loop()` function. 81 | 82 | ```js 83 | // Register an effect with a name and a callback function that takes a 84 | // single parameter, data, which will dictate how the effect interacts with the canvas 85 | Martin.registerEffect('myNewEffect', function(data) { 86 | 87 | // this.context, when used within the callback of Martin.registerEffect, 88 | // refers to either the element or layer on which the effect 89 | // was called 90 | this.context.loop(function(x, y, pixel) { 91 | 92 | // x and y are the pixel's coordinates, from the upper-left, starting with 0 93 | // pixel is an object with keys r, g, b, a representing its 94 | // red, green, blue, and alpha values, all clamped between 0 and 255 95 | pixel.r; 96 | pixel.g; 97 | pixel.b; 98 | pixel.a; 99 | 100 | // These values can be mutated 101 | pixel.r = 200; 102 | pixel.g += 5 + data.a; 103 | pixel.b -= Math.round(data.b / 2); 104 | 105 | // To make sure the changes are saved, return the 106 | // pixel object at the end. 107 | return pixel; 108 | }); 109 | }); 110 | 111 | // After having registered the new effect, call it 112 | // on the working canvas, a layer, or an element 113 | var params = { 114 | a: 100, 115 | b: 100 116 | }; 117 | 118 | canvas.myNewEffect(params); // or 119 | layer.myNewEffect(params); // or 120 | element.myNewEffect(params); 121 | ``` 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | 3 | ### canvas.render() 4 | 5 | Renders the canvas. If you set `autorender: false` when including `options` , you **must** call this to see any changes made to the canvas. 6 | 7 | ### canvas.toDataURL() 8 | 9 | Similar to the native canvas `.toDataURL()` method, this returns a data URL (beginning with `data:img/png;base64,`) that can be set as the `src` of some image, opened in a new tab, or downloaded. 10 | 11 | ```js 12 | canvas.toDataURL(); 13 | ``` 14 | 15 | Returns: 16 | 17 | ``` 18 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsCAYAAADtt+XCAAAgAElEQ…AhmOkBkzZBB5jvYB+rK1vlMqtQ1ArpK8ABM2mg9b9244b8P3MPH12RU3nkAAAAAElFTkSuQmCC 19 | ``` 20 | 21 | ### canvas.convertToImage() 22 | 23 | Replaces the working canvas and layers with an image whose `src` is the data URL returned by the `.toDataURL()` method. 24 | 25 | ```js 26 | canvas.convertToImage(); 27 | ``` 28 | 29 | Returns: 30 | 31 | 32 | 33 | ### canvas.height(`null` or `value`, `resize = false`) 34 | 35 | If no argument is passed, returns the height as a number. 36 | 37 | If a value is given, adjusts the height to be `value` pixels high, or by `value` percent (if given as a string, ex. '150%'). If `resize` is left empty or set to `false`, the image will be enlarged or cropped but not resized. This happens relative to the upper-left corner of the image. If `true`, the existing pixel data will be stretched or shrunk to the new height. 38 | 39 | ```js 40 | canvas.height(200); // crop 41 | ``` 42 | 43 | 44 | 45 | ```js 46 | canvas.height(200, true); // resize 47 | ``` 48 | 49 | 50 | 51 | ```js 52 | canvas.height(400, true); // resize 53 | ``` 54 | 55 | 56 | 57 | ### canvas.width(`null` or `value`, `resize = false`) 58 | 59 | If no argument is passed, returns the width as a number. 60 | 61 | If a value is given, adjusts the width to be `value` pixels wide, or by `value` percent (if given as a string, ex. '150%'). If `resize` is left empty or set to `false`, the image will be enlarged or cropped but not resized. This happens relative to the lower-left corner of the image. If `true`, the existing pixel data will be stretched or shrunk to the new width. 62 | 63 | ```js 64 | canvas.width(200); // crop 65 | ``` 66 | 67 | 68 | 69 | ```js 70 | canvas.width(200, true); // resize 71 | ``` 72 | 73 | 74 | 75 | ```js 76 | canvas.width(500, true); // resize 77 | ``` 78 | 79 | 80 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | gulp = require('gulp'), 3 | uglify = require('gulp-uglify'), 4 | jslint = require('gulp-jslint'), 5 | concat = require('gulp-concat'), 6 | rename = require('gulp-rename'), 7 | sourcemaps = require('gulp-sourcemaps'), 8 | awspublish = require('gulp-awspublish'), 9 | browserSync = require('browser-sync').create(), 10 | shell = require('gulp-shell'), 11 | server = require('./server'); 12 | 13 | var reload = browserSync.reload; 14 | server.start(); 15 | 16 | // ----- Config 17 | var bower = require('./bower.json'), 18 | aws = require('./aws.json'); 19 | 20 | var jsPrefix = 'js/src/', 21 | pluginsPrefix = 'js/src/plugins/'; 22 | 23 | var paths = { 24 | jsCoreIn: [ 25 | 'start', 26 | 'core/init', 27 | 'core/version', 28 | 'core/helpers', 29 | 'core/utils', 30 | 'object/object', 31 | 'layer/layers', 32 | 'element/init', 33 | 'element/image', 34 | 'element/rect', 35 | 'element/line', 36 | 'element/circle', 37 | 'element/ellipse', 38 | 'element/polygon', 39 | 'element/text', 40 | 'effect/init', 41 | 'effect/desaturate', 42 | 'effect/lighten', 43 | 'effect/opacity', 44 | 'effect/blur', 45 | 'effect/invert', 46 | 'effect/sharpen', 47 | 'event/events', 48 | 'core/dimensions', 49 | 'end' 50 | ], 51 | plugins: [ 52 | 'watermark', 53 | 'gradientmap', 54 | 'tile' 55 | ], 56 | jsCoreDist: 'js/dist', 57 | jsCoreDocs: 'docs/download', 58 | html: ['./**/*.html'] 59 | }; 60 | 61 | paths.jsCoreIn.forEach(function(path, i) { 62 | paths.jsCoreIn[i] = jsPrefix + path + '.js'; 63 | }); 64 | 65 | // looks for filename martin.PLUGIN.js 66 | paths.plugins.forEach(function(path, i) { 67 | paths.plugins[i] = pluginsPrefix + path + '.js' 68 | }); 69 | 70 | function writeBowerVersion(version) { 71 | bower.version = version; 72 | fs.writeFile('./bower.json', JSON.stringify(bower, null, ' '), function(err, data) { 73 | if (err) return console.log(err); 74 | console.log('Wrote version to bower.json'); 75 | }); 76 | } 77 | 78 | function writeVersion(callback) { 79 | 80 | var versionFile = './js/src/core/version.js'; 81 | 82 | fs.readFile(versionFile, 'utf8', function read(err, data) { 83 | 84 | var lines = data.split('\n'), 85 | versionString = 'Martin._version = ', 86 | version; 87 | 88 | lines.forEach(function(line, i) { 89 | if ( line.indexOf(versionString) > -1 ) { 90 | version = line.replace(versionString, ''); 91 | } 92 | }); 93 | 94 | writeBowerVersion(version.replace(/["';]/g, '')); 95 | 96 | callback(); 97 | }); 98 | } 99 | 100 | function fullAndMin(dest) { 101 | 102 | function processFiles() { 103 | gulp.src( paths.jsCoreIn ) 104 | .pipe(concat('martin.js')) 105 | .pipe(gulp.dest( dest )); 106 | 107 | gulp.src( paths.jsCoreIn ) 108 | .pipe(concat('martin.min.js')) 109 | .pipe(sourcemaps.init()) 110 | .pipe(uglify()) 111 | .pipe(sourcemaps.write('.')) 112 | .pipe(gulp.dest( dest )); 113 | 114 | if ( dest !== 'docs/download' ) { 115 | 116 | gulp.src( paths.plugins ) 117 | .pipe(rename(function(path) { 118 | path.basename = 'martin.' + path.basename 119 | })) 120 | .pipe(gulp.dest( dest )); 121 | 122 | gulp.src( paths.plugins ) 123 | .pipe(uglify()) 124 | .pipe(rename(function(path) { 125 | path.basename = 'martin.' + path.basename + '.min' 126 | })) 127 | .pipe(gulp.dest( dest )); 128 | } 129 | } 130 | 131 | writeVersion(processFiles); 132 | } 133 | 134 | gulp.task('js', function() { 135 | 136 | fullAndMin( paths.jsCoreDist ); 137 | fullAndMin( paths.jsCoreDocs ); 138 | }); 139 | 140 | gulp.task('docs', shell.task([ 141 | 'mkdocs serve' 142 | ])); 143 | 144 | gulp.task('publish', function() { 145 | 146 | var publisher = awspublish.create({ 147 | params: { 148 | Bucket: aws.bucket 149 | }, 150 | accessKeyId: aws.key, 151 | secretAccessKey: aws.secret 152 | }); 153 | 154 | // build docs 155 | shell.task([ 'mkdocs build --clean' ]); 156 | 157 | gulp.src('./site/**/*') 158 | .pipe(publisher.publish()) 159 | .pipe(publisher.sync()) 160 | .pipe(awspublish.reporter()); 161 | 162 | }); 163 | 164 | gulp.task('serve', ['js'], function() { 165 | 166 | browserSync.init({ 167 | server: { 168 | baseDir: './' 169 | } 170 | }); 171 | 172 | gulp.watch( paths.jsCoreIn, ['js'] ).on('change', reload); 173 | gulp.watch( paths.plugins, ['js'] ).on('change', reload); 174 | gulp.watch( './test/test.js' ).on('change', reload); 175 | gulp.watch( paths.html ).on('change', reload); 176 | 177 | }); 178 | 179 | gulp.task('default', ['serve']); 180 | -------------------------------------------------------------------------------- /images/humphrey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottpdo/martinjs/ab3b7080502eaf8917dc72867b0fc14028ddcb3a/images/humphrey.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Martin 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /js/dist/martin.gradientmap.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('gradientMap', function(data) { 2 | 3 | var min = parseHex(data.start), 4 | max = parseHex(data.end); 5 | 6 | function parseHex(hex) { 7 | 8 | var output; 9 | 10 | if ( hex.charAt(0) === '#' ) hex = hex.slice(1); 11 | 12 | // coerce to six-digit hex if only 3 given 13 | if ( hex.length === 3 ) { 14 | hex = hex.split(''); 15 | hex.splice(2, 0, hex[2]); 16 | hex.splice(1, 0, hex[1]); 17 | hex.splice(0, 0, hex[0]); 18 | hex = hex.join(''); 19 | } 20 | 21 | output = { 22 | r: parseInt(hex[0] + hex[1], 16), 23 | g: parseInt(hex[2] + hex[3], 16), 24 | b: parseInt(hex[4] + hex[5], 16) 25 | }; 26 | 27 | return output; 28 | } 29 | 30 | this.context.loop(function(x, y, pixel) { 31 | pixel.r = Math.round(min.r + (pixel.r / 256) * (max.r - min.r)); 32 | pixel.g = Math.round(min.g + (pixel.g / 256) * (max.g - min.g)); 33 | pixel.b = Math.round(min.b + (pixel.b / 256) * (max.b - min.b)); 34 | return pixel; 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /js/dist/martin.gradientmap.min.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect("gradientMap",function(r){function t(r){var t;return"#"===r.charAt(0)&&(r=r.slice(1)),3===r.length&&(r=r.split(""),r.splice(2,0,r[2]),r.splice(1,0,r[1]),r.splice(0,0,r[0]),r=r.join("")),t={r:parseInt(r[0]+r[1],16),g:parseInt(r[2]+r[3],16),b:parseInt(r[4]+r[5],16)}}var n=t(r.start),e=t(r.end);this.context.loop(function(r,t,a){return a.r=Math.round(n.r+a.r/256*(e.r-n.r)),a.g=Math.round(n.g+a.g/256*(e.g-n.g)),a.b=Math.round(n.b+a.b/256*(e.b-n.b)),a})}); -------------------------------------------------------------------------------- /js/dist/martin.js: -------------------------------------------------------------------------------- 1 | /* 2 | Martin.js: In-browser photo and image editing 3 | Author: Scott Donaldson 4 | Contact: scott.p.donaldson@gmail.com 5 | Twitter: @scottpdonaldson 6 | 7 | ---------------------------------------- 8 | 9 | MARTIN 10 | */ 11 | 12 | (function() { 13 | 14 | // The great initializer. Pass in a string to select element by ID, 15 | // or an HTMLElement 16 | function Martin( val, options ) { 17 | 18 | if ( !(this instanceof Martin) ) return new Martin( val, options ); 19 | 20 | // Set the original element, if there is one 21 | this.original = null; 22 | if ( typeof val === 'string' ) { 23 | this.original = document.getElementById(val); 24 | } else if ( val instanceof HTMLElement ) { 25 | this.original = val; 26 | } 27 | 28 | this.options = options || {}; 29 | 30 | // Now prepare yourself... 31 | return this.makeCanvas(); 32 | 33 | }; 34 | 35 | Martin.utils = {}; 36 | 37 | // Convert an image to a canvas or just return the canvas. 38 | Martin.prototype.makeCanvas = function() { 39 | 40 | this.canvas = document.createElement('canvas'); 41 | this.context = this.canvas.getContext('2d'); 42 | 43 | // Create an empty layer 44 | this.newLayer(); 45 | 46 | if ( this.original ) { 47 | 48 | if ( this.original.tagName === 'IMG' ) { 49 | 50 | var canvas = this.canvas, 51 | context = this.context, 52 | original = this.original; 53 | 54 | function d() { 55 | 56 | // switch to bottom layer 57 | var curLayer = this.currentLayer.stackIndex(); 58 | this.layer(0); 59 | 60 | canvas.width = original.naturalWidth; 61 | canvas.height = original.naturalHeight; 62 | 63 | this.width(canvas.width); 64 | this.height(canvas.height); 65 | 66 | original.parentNode.insertBefore( canvas, original ); 67 | original.parentNode.removeChild( original ); 68 | 69 | // Give that layer some image data (see src/element/image.js) 70 | Martin.registerElement('image', function(img) { 71 | drawImage.call(this, img); 72 | }); 73 | 74 | this.image(original).bumpToBottom(); 75 | 76 | // switch back to previous layer 77 | this.layer(curLayer); 78 | } 79 | 80 | // This should only fire once! Fire if the image is complete, 81 | // or add a handler for once it has finished loading. 82 | if ( original.complete ) return d.call(this); 83 | original.onload = d.bind(this); 84 | 85 | } else if ( this.original.tagName === 'CANVAS' ) { 86 | 87 | this.canvas = this.original; 88 | this.context = this.original.getContext('2d'); 89 | } 90 | } 91 | 92 | // only render and execute callback immediately 93 | // if the original is not an image 94 | this.autorender(); 95 | 96 | return this; 97 | }; 98 | 99 | Martin._version = '0.4.2'; 100 | 101 | /* 102 | For helper functions that don't extend Martin prototype. 103 | 104 | degToRad() 105 | radToDeg() 106 | hexToRGB() 107 | */ 108 | 109 | Martin.degToRad = function(deg) { 110 | return deg * ( Math.PI / 180 ); 111 | }; 112 | 113 | Martin.radToDeg = function(rad) { 114 | return rad * ( 180 / Math.PI ); 115 | }; 116 | 117 | Martin.hexToRGB = function( hex ) { 118 | 119 | if ( !hex ) { return false; } 120 | 121 | if ( hex.slice(0, 1) === '#' ) { hex = hex.slice(1); } 122 | 123 | var r, g, b; 124 | 125 | if ( hex.length === 6 ) { 126 | 127 | r = hex.slice(0, 2); 128 | g = hex.slice(2, 4); 129 | b = hex.slice(4, 6); 130 | 131 | } else if ( hex.length === 3 ) { 132 | 133 | r = hex.slice(0, 1) + hex.slice(0, 1); 134 | g = hex.slice(1, 2) + hex.slice(1, 2); 135 | b = hex.slice(2, 3) + hex.slice(2, 3); 136 | 137 | } 138 | 139 | return { 140 | r: parseInt(r, 16), 141 | g: parseInt(g, 16), 142 | b: parseInt(b, 16) 143 | }; 144 | 145 | }; 146 | 147 | /* 148 | For (mostly) utility functions that extend Martin prototype. 149 | 150 | extend() 151 | .remove() 152 | .render() 153 | .toDataURL() 154 | .convertToImage() 155 | */ 156 | 157 | function forEach(arr, cb) { 158 | if (arr) { 159 | arr.forEach(cb); 160 | } 161 | } 162 | 163 | function noop() {} 164 | 165 | Martin.utils.forEach = forEach; 166 | Martin.utils.noop = noop; 167 | 168 | var i, 169 | func, 170 | funcs = { 171 | 172 | // Extend Martin with plugins, if you want 173 | extend: function extend( obj ) { 174 | for ( var method in obj ) { 175 | if ( Martin.prototype.hasOwnProperty(method) ) { 176 | throw new Error('Careful! This method already exists on the Martin prototype. Try a different name after checking the docs: http://martinjs.org'); 177 | } else { 178 | Martin.prototype[method] = obj[method]; 179 | } 180 | } 181 | }, 182 | 183 | remove: function remove() { 184 | var canvas = this.canvas, 185 | parent = canvas.parentNode; 186 | if ( parent ) parent.removeChild(this.canvas); 187 | return this; 188 | }, 189 | 190 | // Render: looping through layers, loop through elements 191 | // and render each (with optional callback) 192 | render: function render(cb) { 193 | 194 | var ctx = this.context; 195 | 196 | ctx.clearRect(0, 0, this.width(), this.height()); 197 | 198 | Martin.utils.forEach(this.layers, function(layer) { 199 | 200 | layer.clear(); 201 | 202 | Martin.utils.forEach(layer.elements, function renderElement(element) { 203 | element.renderElement && element.renderElement(); 204 | }); 205 | 206 | Martin.utils.forEach(layer.effects, function renderEffect(effect) { 207 | effect.renderEffect && effect.renderEffect(); 208 | }); 209 | 210 | if ( layer.canvas.width > 0 && layer.canvas.height > 0 ) { 211 | ctx.drawImage(layer.canvas, 0, 0); 212 | } 213 | }); 214 | 215 | if (cb) return cb(); 216 | 217 | return this; 218 | }, 219 | 220 | // Autorender: Only render if the `autorender` option is not false 221 | autorender: function autorender(cb) { 222 | if ( this.options.autorender !== false ) return this.render(cb); 223 | return cb ? cb() : null; 224 | }, 225 | 226 | // Return's a data URL of all the working layers 227 | toDataURL: function toDataURL() { 228 | return this.canvas.toDataURL(); 229 | }, 230 | 231 | // Get the dataURL of the merged layers of the canvas, 232 | // then turn that into one image 233 | convertToImage: function convertToImage() { 234 | 235 | var dataURL = this.toDataURL(), 236 | img = document.createElement('img'); 237 | 238 | img.src = dataURL; 239 | 240 | this.layers.forEach(function(layer, i){ 241 | this.deleteLayer(i); 242 | }, this); 243 | 244 | if ( this.container ) this.container.appendChild( img ); 245 | 246 | } 247 | 248 | }; 249 | 250 | for ( func in funcs ) { 251 | Martin.prototype[func] = funcs[func]; 252 | } 253 | 254 | // shared methods for objects: layers, elements, effects 255 | 256 | Martin.Object = function() {}; 257 | var ObjMethods, 258 | ObjMethod; 259 | 260 | ObjMethods = { 261 | 262 | loop: function(cb, put) { 263 | 264 | var width = this.base.width(), 265 | height = this.base.height(); 266 | 267 | var imageData, pixels, len, 268 | n, x, y, 269 | r, g, b, a, 270 | pixel, 271 | output; 272 | 273 | imageData = this.getImageData(); 274 | 275 | if ( imageData ) { 276 | 277 | pixels = imageData.data; 278 | len = pixels.length; 279 | 280 | for ( var i = 0; i < len; i += 4 ) { 281 | 282 | // xy coordinates 283 | n = i / 4; 284 | x = n % width; 285 | y = Math.floor(n / width); 286 | 287 | // rgba values 288 | r = pixels[i]; 289 | g = pixels[i + 1]; 290 | b = pixels[i + 2]; 291 | a = pixels[i + 3]; 292 | 293 | // pass an object corresponding to the pixel to the callback 294 | pixel = { r: r, g: g, b: b, a: a }; 295 | 296 | // execute the callback within the context of this layer's, uh... context 297 | output = cb.call( this.context, x, y, pixel ); 298 | 299 | // reassign the actual rgba values of the pixel based on the output from the loop 300 | pixels[i] = output.r; 301 | pixels[i + 1] = output.g; 302 | pixels[i + 2] = output.b; 303 | pixels[i + 3] = output.a; 304 | 305 | } 306 | 307 | // explicitly declare if image data from callback is not to be used 308 | if ( put !== false ) this.putImageData( imageData ); 309 | 310 | } 311 | 312 | return this; 313 | }, 314 | 315 | getImageData: function() { 316 | var imageData = this.context && this.canvas.width > 0 && this.canvas.height > 0 ? 317 | this.context.getImageData(0, 0, this.canvas.width, this.canvas.height) : 318 | null; 319 | return imageData; 320 | }, 321 | 322 | // Simple shell for putting image data 323 | putImageData: function(imageData) { 324 | this.context.putImageData( imageData, 0, 0 ); 325 | return this; 326 | }, 327 | 328 | clear: function clear() { 329 | this.context.clearRect(0, 0, this.base.width(), this.base.height()); 330 | return this; 331 | }, 332 | 333 | stackIndex: function() { 334 | return this.stack.indexOf(this); 335 | }, 336 | 337 | remove: function() { 338 | this.stack.splice(this.stackIndex(), 1); 339 | this.base.autorender(); 340 | return this; 341 | }, 342 | 343 | bump: function(i) { 344 | var index = this.stackIndex(); 345 | this.remove(); 346 | this.stack.splice(index + i, 0, this); 347 | this.base.autorender(); 348 | return this; 349 | }, 350 | 351 | bumpUp: function() { 352 | return this.bump(1); 353 | }, 354 | 355 | bumpDown: function() { 356 | return this.bump(-1); 357 | }, 358 | 359 | bumpToTop: function() { 360 | this.remove(); 361 | this.stack.push(this); 362 | this.base.autorender(); 363 | return this; 364 | }, 365 | 366 | bumpToBottom: function() { 367 | this.remove(); 368 | this.stack.unshift(this); 369 | this.base.autorender(); 370 | return this; 371 | }, 372 | }; 373 | 374 | for ( ObjMethod in ObjMethods ) { 375 | Martin.Object.prototype[ObjMethod] = ObjMethods[ObjMethod]; 376 | } 377 | 378 | /* 379 | 380 | Martin.Layer constructor 381 | 382 | Methods: 383 | .normalizeX() 384 | .normalizeY() 385 | .normalizePercentX() 386 | .normalizePercentY() 387 | .loop() 388 | .setContext() 389 | .getImageData() 390 | .putImageData() 391 | .render() 392 | .clear() 393 | .remove() 394 | 395 | Methods for working with Layers 396 | 397 | .newLayer() 398 | .layer() 399 | */ 400 | 401 | // ----- Layer constructor 402 | Martin.Layer = function(base, arg) { 403 | 404 | this.base = base; 405 | this.canvas = document.createElement('canvas'); 406 | this.canvas.width = base.original ? (base.original.naturalWidth || base.original.width) : base.width(); 407 | this.canvas.height = base.original ? (base.original.naturalHeight || base.original.height) : base.height(); 408 | this.context = this.canvas.getContext('2d'); 409 | this.scale = { 410 | x: 1, 411 | y: 1 412 | }; 413 | 414 | this.elements = []; 415 | this.effects = []; 416 | 417 | // if no layers yet (initializing), 418 | // the layers are just this new layer, 419 | // and the new layer's context should be the base's 420 | if ( !this.base.layers ) { 421 | this.base.layers = []; 422 | } 423 | this.stack = this.base.layers; 424 | this.stack.push(this); 425 | 426 | if ( typeof arg === 'string' ) { 427 | this.type = arg; 428 | } else { 429 | for ( var i in arg ) this[i] = arg[i]; 430 | } 431 | 432 | return this; 433 | 434 | }; 435 | 436 | Martin.Layer.prototype = Object.create(Martin.Object.prototype); 437 | 438 | // Normalize X and Y values 439 | Martin.Layer.prototype.normalizeX = function( val ) { 440 | if ( typeof val === 'string' && val.slice(-1) === '%' ) { 441 | val = this.normalizePercentX( +val.slice(0, -1) ); 442 | } 443 | return val / this.scale.x; 444 | }; 445 | 446 | Martin.Layer.prototype.normalizeY = function( val ) { 447 | if ( typeof val === 'string' && val.slice(-1) === '%' ) { 448 | val = this.normalizePercentY( +val.slice(0, -1) ); 449 | } 450 | return val / this.scale.y; 451 | }; 452 | 453 | Martin.Layer.prototype.normalizePercentX = function( val ) { 454 | return ( val / 100 ) * this.canvas.width; 455 | }; 456 | 457 | Martin.Layer.prototype.normalizePercentY = function( val ) { 458 | return ( val / 100 ) * this.canvas.height; 459 | }; 460 | 461 | // Create a new (top-most) layer and switch to that layer. 462 | Martin.prototype.newLayer = function(arg) { 463 | 464 | var newLayer = new Martin.Layer(this, arg); 465 | 466 | this.currentLayer = newLayer; 467 | 468 | this.autorender(); 469 | 470 | return newLayer; 471 | 472 | }; 473 | 474 | // Switch the context and return the requested later 475 | Martin.prototype.layer = function( num ) { 476 | 477 | this.currentLayer = this.layers[num || 0]; 478 | 479 | return this.layers[num || 0]; 480 | 481 | }; 482 | 483 | /* 484 | 485 | Martin.Element constructor 486 | 487 | Element methods: 488 | .update() 489 | .moveTo() 490 | */ 491 | 492 | function registerElement(name, cb) { 493 | 494 | function attachRender(data) { 495 | 496 | // create new element 497 | var element = new Martin.Element(name, this, data); 498 | 499 | // attach render function (callback) -- 500 | // execute with element's data 501 | element.renderElement = function renderElement() { 502 | 503 | var layer = this.layer, 504 | context = this.context; 505 | 506 | // clear any image data 507 | this.clear(); 508 | 509 | // scale the context 510 | context.scale( 511 | layer.scale.x, 512 | layer.scale.y 513 | ); 514 | 515 | context.beginPath(); 516 | 517 | cb.call(element, this.data); 518 | 519 | this.setContext(this.data); 520 | 521 | context.closePath(); 522 | 523 | // undo scaling 524 | context.scale( 525 | 1 / layer.scale.x, 526 | 1 / layer.scale.y 527 | ); 528 | 529 | // render this element's effects 530 | Martin.utils.forEach(this.effects, function(effect) { 531 | effect.renderEffect && effect.renderEffect(); 532 | }); 533 | 534 | // draw to layer 535 | if ( this.canvas.width > 0 && this.canvas.height > 0 ) { 536 | layer.context.drawImage(this.canvas, 0, 0); 537 | } 538 | }; 539 | 540 | return element; 541 | } 542 | 543 | Martin.prototype[name] = function registerToBase(data) { 544 | var el = attachRender.call(this, data); 545 | this.autorender(); 546 | return el; 547 | }; 548 | 549 | Martin.Layer.prototype[name] = function registerToLayer(data) { 550 | var el = attachRender.call(this.base, data); 551 | this.base.autorender(); 552 | return el; 553 | }; 554 | }; 555 | 556 | Martin.registerElement = registerElement; 557 | 558 | Martin.Element = function(type, caller, data) { 559 | 560 | var base = caller.base || caller, 561 | layer = caller.currentLayer || caller; 562 | 563 | // base refers to the instance of Martin 564 | this.base = base; 565 | this.canvas = document.createElement('canvas'); 566 | this.context = this.canvas.getContext('2d'); 567 | 568 | // TODO: bounding box 569 | this.canvas.width = base.original ? (base.original.naturalWidth || base.original.width) : base.width(); 570 | this.canvas.height = base.original ? (base.original.naturalHeight || base.original.height) : base.height(); 571 | 572 | this.scale = { 573 | x: 1, 574 | y: 1 575 | }; 576 | 577 | this.data = data || {}; 578 | 579 | // if given a percentage x or y position, the element has a relative position -- 580 | // it should be updated on layer resizing 581 | if ( typeof data.x === 'string' || typeof data.y === 'string' ) { 582 | var x = data.x || '', 583 | y = data.y || ''; 584 | 585 | if ( typeof x === 'string' && x.slice(-1) === '%' ) { 586 | this.data.percentX = x; 587 | this.relativePosition = true; 588 | } 589 | 590 | if ( typeof y === 'string' && y.slice(-1) === '%' ) { 591 | this.data.percentY = y; 592 | } 593 | } 594 | 595 | if ( data.x ) this.data.x = layer.normalizeX(data.x); 596 | if ( data.y ) this.data.y = layer.normalizeY(data.y); 597 | 598 | this.type = type; 599 | this.layer = layer; 600 | 601 | this.effects = []; 602 | 603 | this.stack = this.layer.elements; 604 | this.stack.push(this); 605 | 606 | // automatically push backgrounds to the bottom of the layer 607 | if ( this.type === 'background' ) { 608 | this.data = { 609 | color: data 610 | }; 611 | this.bumpToBottom(); 612 | } 613 | 614 | return this; 615 | }; 616 | 617 | Martin.Element.prototype = Object.create(Martin.Object.prototype); 618 | 619 | // Set the fill, stroke, alpha for a new shape 620 | Martin.Element.prototype.setContext = function( obj ) { 621 | 622 | var context = this.context; 623 | 624 | context.save(); 625 | 626 | context.fillStyle = obj.color || '#000'; 627 | context.fill(); 628 | 629 | context.scale( 630 | this.scale.x, 631 | this.scale.y 632 | ); 633 | 634 | context.globalAlpha = obj.alpha || 1; 635 | 636 | context.lineWidth = obj.strokeWidth ? obj.strokeWidth : 0; 637 | context.lineCap = obj.cap ? obj.cap : 'square'; 638 | context.strokeStyle = obj.stroke ? obj.stroke : 'transparent'; 639 | context.stroke(); 640 | 641 | context.restore(); 642 | 643 | }; 644 | 645 | // ----- Update an element with new data 646 | Martin.Element.prototype.update = function(arg1, arg2) { 647 | 648 | var key, value, data; 649 | 650 | if ( arg2 ) { 651 | key = arg1; 652 | value = arg2; 653 | this.data[key] = value; 654 | } else { 655 | for ( key in arg1 ) { 656 | value = arg1[key]; 657 | this.data[key] = value; 658 | } 659 | } 660 | 661 | if ( key === 'x' || key === 'y' ) { 662 | this.relativePosition = false; 663 | } 664 | 665 | this.base.autorender(); 666 | }; 667 | 668 | // ----- Move an element to new coordinates 669 | Martin.Element.prototype.moveTo = function(x, y) { 670 | 671 | var data = this.data; 672 | 673 | // if no params given, move to 0, 0 674 | x = x || 0; 675 | y = y || 0; 676 | 677 | if ( this.type === 'line' ) { 678 | data.endX += x - data.x; 679 | data.endY += y - data.y; 680 | } else if ( this.type === 'polygon' ) { 681 | data.points.forEach(function(pt, i) { 682 | if ( i > 0 ) { 683 | var thisX = pt[0], 684 | thisY = pt[1]; 685 | data.points[i] = [ 686 | thisX + (x - data.points[0][0]), 687 | thisY + (y - data.points[0][1]) 688 | ]; 689 | } 690 | }); 691 | data.points[0] = [x, y]; 692 | } 693 | 694 | data.x = x; 695 | data.y = y; 696 | 697 | this.relativePosition = false; 698 | 699 | this.base.autorender(); 700 | 701 | return this; 702 | 703 | }; 704 | 705 | function drawImage(img) { 706 | this.context.drawImage( img, 0, 0 ); 707 | } 708 | 709 | registerElement('image', function(img) { 710 | drawImage.call(this, img); 711 | }); 712 | 713 | function rect(data) { 714 | 715 | var layer = this.layer, 716 | context = this.context; 717 | 718 | context.rect( 719 | layer.normalizeX( data.x || 0 ), 720 | layer.normalizeY( data.y || 0 ), 721 | layer.normalizeX( data.width || layer.width() ), 722 | layer.normalizeY( data.height || layer.height() ) 723 | ); 724 | } 725 | 726 | registerElement('rect', function(data) { 727 | rect.call(this, data); 728 | }); 729 | 730 | registerElement('background', function(data) { 731 | rect.call(this, data); 732 | }); 733 | 734 | registerElement('line', function(data) { 735 | 736 | var layer = this.layer, 737 | context = this.context; 738 | 739 | context.moveTo( 740 | layer.normalizeX( data.x || 0 ), 741 | layer.normalizeY( data.y || 0 ) 742 | ); 743 | 744 | context.lineTo( 745 | layer.normalizeX( data.endX ), 746 | layer.normalizeY( data.endY ) 747 | ); 748 | 749 | if ( !data.strokeWidth ) data.strokeWidth = 1; 750 | data.stroke = data.color ? data.color : '#000'; 751 | 752 | return this; 753 | }); 754 | 755 | registerElement('circle', function(data) { 756 | 757 | var layer = this.layer, 758 | context = this.context, 759 | centerX = layer.normalizeX( data.x || 0 ), 760 | centerY = layer.normalizeY( data.y || 0 ); 761 | 762 | context.arc( centerX, centerY, data.radius, 0, 2 * Math.PI, false); 763 | 764 | }); 765 | 766 | registerElement('ellipse', function(data) { 767 | 768 | var layer = this.layer, 769 | context = this.context, 770 | centerX = layer.normalizeX( data.x || 0 ), 771 | centerY = layer.normalizeY( data.y || 0 ), 772 | scale; 773 | 774 | if ( data.radiusX > data.radiusY ) { 775 | 776 | scale = data.radiusX / data.radiusY; 777 | 778 | context.scale( scale, 1 ); 779 | 780 | context.arc( centerX / scale, centerY, data.radiusX / scale, 0, 2 * Math.PI, false); 781 | 782 | context.scale( 1 / scale, 1 ); 783 | 784 | } else { 785 | 786 | scale = data.radiusY / data.radiusX; 787 | 788 | context.scale( 1, scale ); 789 | 790 | context.arc( centerX, centerY / scale, data.radiusY / scale, 0, 2 * Math.PI, false); 791 | 792 | context.scale( 1, 1 / scale ); 793 | 794 | } 795 | 796 | return this; 797 | }); 798 | 799 | registerElement('polygon', function(data) { 800 | 801 | var layer = this.layer, 802 | context = this.context; 803 | 804 | for ( var i = 0; i < data.points.length; i++ ) { 805 | 806 | var x = data.points[i][0], 807 | y = data.points[i][1], 808 | toX = layer.normalizeX( x ), 809 | toY = layer.normalizeY( y ); 810 | 811 | if ( i === 0 ) context.moveTo( toX, toY ); 812 | 813 | context.lineTo( toX, toY ); 814 | 815 | } 816 | 817 | // close the path 818 | context.lineTo( 819 | layer.normalizeX(data.points[0][0]), 820 | layer.normalizeY(data.points[0][1]) 821 | ); 822 | 823 | return this; 824 | }); 825 | 826 | registerElement('text', function(data) { 827 | 828 | var layer = this.layer, 829 | context = this.context, 830 | size, 831 | style, 832 | font, 833 | fontOutput; 834 | 835 | var clone = {}; 836 | 837 | // use custom getters and setters for these properties 838 | style = data.style || ''; 839 | size = data.size || ''; 840 | font = data.font || ''; 841 | 842 | function fontString(style, size, font) { 843 | return (style ? style + ' ' : '') + (size || 16) + 'px ' + (font || 'sans-serif'); 844 | }; 845 | 846 | fontOutput = fontString(data.style, data.size, data.font); 847 | 848 | this.fontSize = function(size) { 849 | if ( size ) { 850 | this.data.size = size; 851 | return size; 852 | } else { 853 | return this.data.size; 854 | } 855 | }; 856 | 857 | this.fontStyle = function(style) { 858 | if ( style ) { 859 | this.data.style = style; 860 | return style; 861 | } 862 | 863 | return this.data.style; 864 | }; 865 | 866 | this.font = function(font) { 867 | if ( font ) { 868 | this.data.font = font; 869 | return font; 870 | } 871 | 872 | return this.data.style; 873 | }; 874 | 875 | this.width = function() { 876 | return context.measureText(data.text || '').width; 877 | }; 878 | 879 | Object.defineProperty(clone, 'theStyle', { 880 | get: function() { 881 | return style; 882 | }, 883 | set: function(style) { 884 | fontOutput = fontString(style, data.size, data.font); 885 | } 886 | }); 887 | 888 | Object.defineProperty(clone, 'theSize', { 889 | get: function() { 890 | return size; 891 | }, 892 | set: function(size) { 893 | fontOutput = fontString(data.style, size, data.font); 894 | } 895 | }); 896 | 897 | Object.defineProperty(clone, 'theFont', { 898 | get: function() { 899 | return font; 900 | }, 901 | set: function(font) { 902 | fontOutput = fontString(data.style, data.size, font); 903 | } 904 | }); 905 | 906 | context.font = fontOutput; 907 | context.fillStyle = data.color || '#000'; 908 | context.textBaseline = 'top'; 909 | context.textAlign = data.align || 'left'; 910 | context.fillText( 911 | data.text || '', 912 | layer.normalizeX(data.x || 0), 913 | layer.normalizeY(data.y || 0) 914 | ); 915 | }); 916 | 917 | /* 918 | 919 | Martin.Effect constructor 920 | 921 | Effect methods: 922 | .increase() 923 | .decrease() 924 | */ 925 | 926 | function registerEffect(name, cb) { 927 | 928 | function attachRender(data, stack, stackContainer) { 929 | 930 | // create new effect 931 | var effect = new Martin.Effect(name, this, data, stack, stackContainer); 932 | 933 | // attach render function (callback) -- 934 | // execute with effect's data 935 | effect.renderEffect = function renderEffect() { 936 | cb.call(effect, this.data); 937 | }; 938 | 939 | return effect; 940 | } 941 | 942 | Martin.prototype[name] = function attachToBase(data) { 943 | var effect = attachRender.call(this, data, this.currentLayer.effects, this.currentLayer); 944 | this.autorender(); 945 | return effect; 946 | }; 947 | 948 | Martin.Layer.prototype[name] = 949 | Martin.Element.prototype[name] = function attachToLayerOrElement(data) { 950 | var effect = attachRender.call(this.base, data, this.effects, this); 951 | this.base.autorender(); 952 | return effect; 953 | }; 954 | }; 955 | 956 | Martin.registerEffect = registerEffect; 957 | 958 | Martin.Effect = function(type, base, data, stack, stackContainer) { 959 | 960 | this.base = base; 961 | this.type = type; 962 | 963 | this.data = data; 964 | 965 | this.context = stackContainer; 966 | 967 | this.stack = stack; 968 | this.stack.push(this); 969 | 970 | return this; 971 | }; 972 | 973 | Martin.Effect.prototype = Object.create(Martin.Object.prototype); 974 | 975 | // Adjust the intensity of an Effect (linear effects only) 976 | Martin.Effect.prototype.increase = function(amt) { 977 | 978 | if ( typeof this.data === 'number' ) { 979 | this.data += amt || 1; 980 | this.base.autorender(); 981 | } 982 | 983 | return this; 984 | }; 985 | 986 | Martin.Effect.prototype.decrease = function(amt) { 987 | return this.increase(-amt || -1); 988 | }; 989 | 990 | // Desaturate 991 | function desaturate(amt) { 992 | 993 | this.context.loop(function(x, y, pixel) { 994 | 995 | var r = pixel.r, 996 | g = pixel.g, 997 | b = pixel.b; 998 | 999 | var grayscale = r * 0.3 + g * 0.59 + b * 0.11; 1000 | r = (1 - amt) * r + amt * grayscale; // red 1001 | g = (1 - amt) * g + amt * grayscale; // green 1002 | b = (1 - amt) * b + amt * grayscale; // blue 1003 | 1004 | pixel.r = r; 1005 | pixel.g = g; 1006 | pixel.b = b; 1007 | 1008 | return pixel; 1009 | 1010 | }); 1011 | } 1012 | 1013 | registerEffect('desaturate', function(amt) { 1014 | amt = amt / 100; 1015 | desaturate.call(this, amt); 1016 | }); 1017 | 1018 | // inverse of saturate 1019 | registerEffect('saturate', function(amt) { 1020 | amt = -amt / 100; 1021 | desaturate.call(this, amt); 1022 | }); 1023 | 1024 | // Lighten and darken. (Darken just returns the opposite of lighten). 1025 | // Takes an input from 0 to 100. Higher values return pure white or black. 1026 | function lighten(amt) { 1027 | 1028 | this.context.loop(function(x, y, pixel) { 1029 | 1030 | pixel.r += Math.round(amt * 255); 1031 | pixel.g += Math.round(amt * 255); 1032 | pixel.b += Math.round(amt * 255); 1033 | 1034 | return pixel; 1035 | }); 1036 | } 1037 | 1038 | registerEffect('lighten', function(amt) { 1039 | amt = amt / 100; 1040 | lighten.call(this, amt); 1041 | }); 1042 | 1043 | registerEffect('darken', function(amt) { 1044 | amt = -amt / 100; 1045 | lighten.call(this, amt); 1046 | }); 1047 | 1048 | // Fade uniform 1049 | registerEffect('opacity', function(amt) { 1050 | 1051 | amt = amt / 100; 1052 | 1053 | var base = this.base; 1054 | 1055 | this.context.loop(function(x, y, pixel) { 1056 | pixel.a *= amt; 1057 | return pixel; 1058 | }); 1059 | }); 1060 | 1061 | /* 1062 | * 1063 | * StackBlur Algorithm Copyright (c) 2010 Mario Klingemann 1064 | * Version: 0.6 1065 | * Author: Mario Klingemann 1066 | * Contact: mario@quasimondo.com 1067 | * Website: http://www.quasimondo.com/StackBlurForCanvas 1068 | * Twitter: @quasimondo 1069 | * 1070 | */ 1071 | 1072 | // simple stack maker 1073 | function BlurStack() { 1074 | this.r = this.g = this.b = this.a = 0; 1075 | this.next = null; 1076 | } 1077 | 1078 | // helper functions for .blur() 1079 | BlurStack.mul_shift_table = function(i) { 1080 | var mul_table = [1,171,205,293,57,373,79,137,241,27,391,357,41,19,283,265,497,469,443,421,25,191,365,349,335,161,155,149,9,278,269,261,505,245,475,231,449,437,213,415,405,395,193,377,369,361,353,345,169,331,325,319,313,307,301,37,145,285,281,69,271,267,263,259,509,501,493,243,479,118,465,459,113,446,55,435,429,423,209,413,51,403,199,393,97,3,379,375,371,367,363,359,355,351,347,43,85,337,333,165,327,323,5,317,157,311,77,305,303,75,297,294,73,289,287,71,141,279,277,275,68,135,67,133,33,262,260,129,511,507,503,499,495,491,61,121,481,477,237,235,467,232,115,457,227,451,7,445,221,439,218,433,215,427,425,211,419,417,207,411,409,203,202,401,399,396,197,49,389,387,385,383,95,189,47,187,93,185,23,183,91,181,45,179,89,177,11,175,87,173,345,343,341,339,337,21,167,83,331,329,327,163,81,323,321,319,159,79,315,313,39,155,309,307,153,305,303,151,75,299,149,37,295,147,73,291,145,289,287,143,285,71,141,281,35,279,139,69,275,137,273,17,271,135,269,267,133,265,33,263,131,261,130,259,129,257,1]; 1081 | 1082 | 1083 | var shg_table = [0,9,10,11,9,12,10,11,12,9,13,13,10,9,13,13,14,14,14,14,10,13,14,14,14,13,13,13,9,14,14,14,15,14,15,14,15,15,14,15,15,15,14,15,15,15,15,15,14,15,15,15,15,15,15,12,14,15,15,13,15,15,15,15,16,16,16,15,16,14,16,16,14,16,13,16,16,16,15,16,13,16,15,16,14,9,16,16,16,16,16,16,16,16,16,13,14,16,16,15,16,16,10,16,15,16,14,16,16,14,16,16,14,16,16,14,15,16,16,16,14,15,14,15,13,16,16,15,17,17,17,17,17,17,14,15,17,17,16,16,17,16,15,17,16,17,11,17,16,17,16,17,16,17,17,16,17,17,16,17,17,16,16,17,17,17,16,14,17,17,17,17,15,16,14,16,15,16,13,16,15,16,14,16,15,16,12,16,15,16,17,17,17,17,17,13,16,15,17,17,17,16,15,17,17,17,16,15,17,17,14,16,17,17,16,17,17,16,15,17,16,14,17,16,15,17,16,17,17,16,17,15,16,17,14,17,16,15,17,16,17,13,17,16,17,17,16,17,14,17,16,17,16,17,16,17,9]; 1084 | 1085 | return [ mul_table[i] || mul_table[mul_table.length - 1], shg_table[i] || shg_table[shg_table.length - 1] ]; 1086 | }; 1087 | 1088 | // And, what we've all been waiting for: 1089 | registerEffect('blur', function(amt) { 1090 | 1091 | if ( isNaN(amt) || amt < 1 ) return this; 1092 | // Round to nearest pixel 1093 | amt = Math.round(amt); 1094 | 1095 | var iterations = 2, // increase for smoother blurring 1096 | width = this.base.width(), 1097 | height = this.base.height(), 1098 | widthMinus1 = width - 1, 1099 | heightMinus1 = height - 1, 1100 | radiusPlus1 = amt + 1, 1101 | div = 2 * amt + 1, 1102 | mul_sum = BlurStack.mul_shift_table(amt)[0], 1103 | shg_sum = BlurStack.mul_shift_table(amt)[1]; 1104 | 1105 | var it = iterations, // internal iterations in case doing multiple layers 1106 | imageData = this.context.getImageData(); 1107 | 1108 | if ( imageData ) { 1109 | var pixels = imageData.data; 1110 | 1111 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, 1112 | r_out_sum, g_out_sum, b_out_sum, a_out_sum, 1113 | r_in_sum, g_in_sum, b_in_sum, a_in_sum, 1114 | pr, pg, pb, pa; 1115 | 1116 | var stackStart = new BlurStack(), 1117 | stack = stackStart, 1118 | stackEnd, 1119 | stackIn; 1120 | 1121 | for ( i = 1; i < div; i++ ) { 1122 | stack = stack.next = new BlurStack(); 1123 | if ( i === radiusPlus1 ) stackEnd = stack; 1124 | } 1125 | 1126 | stack.next = stackStart; 1127 | stackIn = null; 1128 | 1129 | // repeat for as many iterations as given 1130 | while ( it-- > 0 ) { 1131 | 1132 | yw = yi = 0; 1133 | 1134 | // loop through rows from top down 1135 | for ( y = height; --y > -1; ) { 1136 | 1137 | // start summing pixel values 1138 | r_sum = radiusPlus1 * ( pr = pixels[yi] ); 1139 | g_sum = radiusPlus1 * ( pg = pixels[yi + 1] ); 1140 | b_sum = radiusPlus1 * ( pb = pixels[yi + 2] ); 1141 | a_sum = radiusPlus1 * ( pa = pixels[yi + 3] ); 1142 | 1143 | stack = stackStart; 1144 | 1145 | for ( i = radiusPlus1; --i > -1; ) { 1146 | stack.r = pr; 1147 | stack.g = pg; 1148 | stack.b = pb; 1149 | stack.a = pa; 1150 | 1151 | stack = stack.next; 1152 | } 1153 | 1154 | for ( i = 1; i < radiusPlus1; i++ ) { 1155 | 1156 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 1157 | 1158 | r_sum += ( stack.r = pixels[p]); 1159 | g_sum += ( stack.g = pixels[p + 1]); 1160 | b_sum += ( stack.b = pixels[p + 2]); 1161 | a_sum += ( stack.a = pixels[p + 3]); 1162 | 1163 | stack = stack.next; 1164 | } 1165 | 1166 | stackIn = stackStart; 1167 | 1168 | for ( x = 0; x < width; x++ ) { 1169 | pixels[yi++] = (r_sum * mul_sum) >>> shg_sum; 1170 | pixels[yi++] = (g_sum * mul_sum) >>> shg_sum; 1171 | pixels[yi++] = (b_sum * mul_sum) >>> shg_sum; 1172 | pixels[yi++] = (a_sum * mul_sum) >>> shg_sum; 1173 | 1174 | p = ( yw + ( ( p = x + amt + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 1175 | 1176 | r_sum -= stackIn.r - ( stackIn.r = pixels[p]); 1177 | g_sum -= stackIn.g - ( stackIn.g = pixels[p + 1]); 1178 | b_sum -= stackIn.b - ( stackIn.b = pixels[p + 2]); 1179 | a_sum -= stackIn.a - ( stackIn.a = pixels[p + 3]); 1180 | 1181 | stackIn = stackIn.next; 1182 | } 1183 | 1184 | // next row 1185 | yw += width; 1186 | } 1187 | 1188 | for ( x = 0; x < width; x++ ) { 1189 | 1190 | // with each column, divide yi by 4 (4 values per px) 1191 | yi = x << 2; 1192 | 1193 | r_sum = radiusPlus1 * ( pr = pixels[yi]); 1194 | g_sum = radiusPlus1 * ( pg = pixels[yi + 1]); 1195 | b_sum = radiusPlus1 * ( pb = pixels[yi + 2]); 1196 | a_sum = radiusPlus1 * ( pa = pixels[yi + 3]); 1197 | 1198 | stack = stackStart; 1199 | 1200 | for ( i = 0; i < radiusPlus1; i++ ) { 1201 | stack.r = pr; 1202 | stack.g = pg; 1203 | stack.b = pb; 1204 | stack.a = pa; 1205 | stack = stack.next; 1206 | } 1207 | 1208 | yp = width; 1209 | 1210 | for ( i = 1; i <= amt; i++ ) { 1211 | yi = ( yp + x ) << 2; 1212 | 1213 | r_sum += ( stack.r = pixels[yi]); 1214 | g_sum += ( stack.g = pixels[yi + 1]); 1215 | b_sum += ( stack.b = pixels[yi + 2]); 1216 | a_sum += ( stack.a = pixels[yi + 3]); 1217 | 1218 | stack = stack.next; 1219 | 1220 | if ( i < heightMinus1 ) yp += width; 1221 | } 1222 | 1223 | yi = x; 1224 | stackIn = stackStart; 1225 | 1226 | for ( y = 0; y < height; y++ ) { 1227 | 1228 | p = yi << 2; 1229 | 1230 | pixels[p + 3] = pa =(a_sum * mul_sum) >>> shg_sum; 1231 | 1232 | if ( pa > 0 ) { 1233 | pa = 255 / pa; 1234 | pixels[p] = ((r_sum * mul_sum) >>> shg_sum ) * pa; 1235 | pixels[p + 1] = ((g_sum * mul_sum) >>> shg_sum ) * pa; 1236 | pixels[p + 2] = ((b_sum * mul_sum) >>> shg_sum ) * pa; 1237 | } else { 1238 | pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; 1239 | } 1240 | 1241 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 1242 | 1243 | r_sum -= stackIn.r - ( stackIn.r = pixels[p]); 1244 | g_sum -= stackIn.g - ( stackIn.g = pixels[p + 1]); 1245 | b_sum -= stackIn.b - ( stackIn.b = pixels[p + 2]); 1246 | a_sum -= stackIn.a - ( stackIn.a = pixels[p + 3]); 1247 | 1248 | stackIn = stackIn.next; 1249 | 1250 | yi += width; 1251 | } 1252 | } 1253 | } 1254 | 1255 | this.context.putImageData( imageData ); 1256 | } 1257 | }); 1258 | Martin.registerEffect('invert', function() { 1259 | this.context.loop(function(x, y, pixel) { 1260 | pixel.r = 255 - pixel.r; 1261 | pixel.g = 255 - pixel.g; 1262 | pixel.b = 255 - pixel.b; 1263 | 1264 | return pixel; 1265 | }); 1266 | }); 1267 | 1268 | Martin.registerEffect('sharpen', function(amt) { 1269 | 1270 | if ( isNaN(amt) ) return this; 1271 | 1272 | var w = this.base.width(), 1273 | h = this.base.height(); 1274 | 1275 | var buffer = document.createElement('canvas'); 1276 | buffer.width = this.base.width(); 1277 | buffer.height = this.base.height(); 1278 | 1279 | var dstData = buffer.getContext('2d').createImageData(w, h), 1280 | dstBuff = dstData.data; 1281 | 1282 | var weights = [0, -1, 0, -1, 5, -1, 0, -1, 0], 1283 | katet = Math.round(Math.sqrt(weights.length)), 1284 | half = (katet * 0.5) | 0, 1285 | srcBuff = this.context.getImageData(0, 0, w, h).data, 1286 | y = h; 1287 | 1288 | amt /= 100; 1289 | 1290 | while (y--) { 1291 | 1292 | x = w; 1293 | 1294 | while (x--) { 1295 | 1296 | var sy = y, 1297 | sx = x, 1298 | dstOff = (y * w + x) * 4, 1299 | r = 0, 1300 | g = 0, 1301 | b = 0, 1302 | a = 0; 1303 | 1304 | for (var cy = 0; cy < katet; cy++) { 1305 | for (var cx = 0; cx < katet; cx++) { 1306 | 1307 | var scy = sy + cy - half; 1308 | var scx = sx + cx - half; 1309 | 1310 | if (scy >= 0 && scy < h && scx >= 0 && scx < w) { 1311 | 1312 | var srcOff = (scy * w + scx) * 4; 1313 | var wt = weights[cy * katet + cx]; 1314 | 1315 | r += srcBuff[srcOff] * wt; 1316 | g += srcBuff[srcOff + 1] * wt; 1317 | b += srcBuff[srcOff + 2] * wt; 1318 | a += srcBuff[srcOff + 3] * wt; 1319 | } 1320 | } 1321 | } 1322 | 1323 | dstBuff[dstOff] = r * amt + srcBuff[dstOff] * (1 - amt); 1324 | dstBuff[dstOff + 1] = g * amt + srcBuff[dstOff + 1] * (1 - amt); 1325 | dstBuff[dstOff + 2] = b * amt + srcBuff[dstOff + 2] * (1 - amt) 1326 | dstBuff[dstOff + 3] = srcBuff[dstOff + 3]; 1327 | } 1328 | } 1329 | 1330 | this.context.putImageData(dstData); 1331 | }); 1332 | var events = ['click', 'mouseover', 'mousemove', 'mouseenter', 'mouseleave', 'mouseout', 'mousedown', 'mouseup']; 1333 | 1334 | function EventCallback(base, cb, type) { 1335 | return { 1336 | exec: function exec(e) { 1337 | 1338 | var eventObj = {}, k; 1339 | 1340 | for ( k in e ) { 1341 | eventObj[k] = e[k]; 1342 | } 1343 | 1344 | eventObj.x = e.offsetX ? e.offsetX : e.clientX - base.canvas.getBoundingClientRect().left; 1345 | eventObj.y = e.offsetY ? e.offsetY : e.clientY - base.canvas.getBoundingClientRect().top; 1346 | 1347 | cb(eventObj); 1348 | base.autorender(); 1349 | } 1350 | }; 1351 | } 1352 | 1353 | events.forEach(function(evt){ 1354 | Martin.prototype[evt] = function(cb) { 1355 | 1356 | var callback = EventCallback(this, cb, evt); 1357 | 1358 | this.canvas.addEventListener(evt, callback.exec); 1359 | return this; 1360 | }; 1361 | }); 1362 | 1363 | Martin.prototype.on = function(evt, cb) { 1364 | 1365 | evt = evt.split(' '); 1366 | 1367 | evt.forEach(function(ev) { 1368 | var callback = EventCallback(this, cb, ev); 1369 | if ( events.indexOf(ev) > -1 ) { 1370 | this.canvas.addEventListener(ev, callback.exec); 1371 | } 1372 | }, this); 1373 | 1374 | return this; 1375 | }; 1376 | 1377 | /* 1378 | Need to find a place for the rest of these important methods. 1379 | 1380 | .width() 1381 | .height() 1382 | */ 1383 | 1384 | // Set or change dimensions. 1385 | [ 'width', 'height' ].forEach(function( which ) { 1386 | 1387 | Martin.prototype[which] = function( val, resize ) { 1388 | 1389 | // if no value given, return the corresponding value 1390 | if ( !val ) return this.canvas[which]; 1391 | 1392 | // Update height or width of all the layers' canvases 1393 | // and update their contexts 1394 | this.canvas[which] = val; 1395 | this.layers.forEach(function(layer) { 1396 | layer[which](val, resize); 1397 | }); 1398 | 1399 | return this; 1400 | 1401 | }; 1402 | 1403 | Martin.Layer.prototype[which] = function(val, resize) { 1404 | 1405 | var layer = this, 1406 | ratio; 1407 | 1408 | if ( !val ) return this.canvas[which]; 1409 | 1410 | // normalize the value 1411 | val = this['normalize' + (which === 'width' ? 'X' : 'Y')](val); 1412 | 1413 | // resize this layer's canvas 1414 | this.canvas[which] = val; 1415 | 1416 | // resize element canvases 1417 | Martin.utils.forEach(this.elements, function(element) { 1418 | element.canvas[which] = val; 1419 | 1420 | // if relatively positioned, reposition 1421 | if ( element.relativePosition ) { 1422 | if ( element.data.percentX ) { 1423 | element.data.x = layer.normalizeX(element.data.percentX); 1424 | } 1425 | if ( element.data.percentY ) { 1426 | element.data.y = layer.normalizeY(element.data.percentY); 1427 | } 1428 | } 1429 | }); 1430 | 1431 | // get the ratio, in case we're resizing 1432 | ratio = resize ? val / this.canvas[which] : 1; 1433 | 1434 | if ( resize ) { 1435 | 1436 | if ( which === 'width' ) this.scale.x *= ratio; 1437 | if ( which === 'height' ) this.scale.y *= ratio; 1438 | 1439 | this.canvas[which] = val; 1440 | } 1441 | 1442 | this.base.autorender(); 1443 | 1444 | return this; 1445 | }; 1446 | }); 1447 | 1448 | this.Martin = Martin; 1449 | 1450 | })(); 1451 | -------------------------------------------------------------------------------- /js/dist/martin.min.js: -------------------------------------------------------------------------------- 1 | !function(){function t(e,i){return this instanceof t?(this.original=null,"string"==typeof e?this.original=document.getElementById(e):e instanceof HTMLElement&&(this.original=e),this.options=i||{},this.makeCanvas()):new t(e,i)}function e(t,e){t&&t.forEach(e)}function i(){}function n(e,i){function n(n){var a=new t.Element(e,this,n);return a.renderElement=function(){var e=this.layer,n=this.context;this.clear(),n.scale(e.scale.x,e.scale.y),n.beginPath(),i.call(a,this.data),this.setContext(this.data),n.closePath(),n.scale(1/e.scale.x,1/e.scale.y),t.utils.forEach(this.effects,function(t){t.renderEffect&&t.renderEffect()}),this.canvas.width>0&&this.canvas.height>0&&e.context.drawImage(this.canvas,0,0)},a}t.prototype[e]=function(t){var e=n.call(this,t);return this.autorender(),e},t.Layer.prototype[e]=function(t){var e=n.call(this.base,t);return this.base.autorender(),e}}function a(t){this.context.drawImage(t,0,0)}function r(t){var e=this.layer,i=this.context;i.rect(e.normalizeX(t.x||0),e.normalizeY(t.y||0),e.normalizeX(t.width||e.width()),e.normalizeY(t.height||e.height()))}function s(e,i){function n(n,a,r){var s=new t.Effect(e,this,n,a,r);return s.renderEffect=function(){i.call(s,this.data)},s}t.prototype[e]=function(t){var e=n.call(this,t,this.currentLayer.effects,this.currentLayer);return this.autorender(),e},t.Layer.prototype[e]=t.Element.prototype[e]=function(t){var e=n.call(this.base,t,this.effects,this);return this.base.autorender(),e}}function o(t){this.context.loop(function(e,i,n){var a=n.r,r=n.g,s=n.b,o=.3*a+.59*r+.11*s;return a=(1-t)*a+t*o,r=(1-t)*r+t*o,s=(1-t)*s+t*o,n.r=a,n.g=r,n.b=s,n})}function h(t){this.context.loop(function(e,i,n){return n.r+=Math.round(255*t),n.g+=Math.round(255*t),n.b+=Math.round(255*t),n})}function c(){this.r=this.g=this.b=this.a=0,this.next=null}function l(t,e,i){return{exec:function(i){var n,a={};for(n in i)a[n]=i[n];a.x=i.offsetX?i.offsetX:i.clientX-t.canvas.getBoundingClientRect().left,a.y=i.offsetY?i.offsetY:i.clientY-t.canvas.getBoundingClientRect().top,e(a),t.autorender()}}}t.utils={},t.prototype.makeCanvas=function(){function e(){var e=this.currentLayer.stackIndex();this.layer(0),i.width=n.naturalWidth,i.height=n.naturalHeight,this.width(i.width),this.height(i.height),n.parentNode.insertBefore(i,n),n.parentNode.removeChild(n),t.registerElement("image",function(t){a.call(this,t)}),this.image(n).bumpToBottom(),this.layer(e)}if(this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.newLayer(),this.original)if("IMG"===this.original.tagName){var i=this.canvas,n=(this.context,this.original);if(n.complete)return e.call(this);n.onload=e.bind(this)}else"CANVAS"===this.original.tagName&&(this.canvas=this.original,this.context=this.original.getContext("2d"));return this.autorender(),this},t._version="0.4.2",t.degToRad=function(t){return t*(Math.PI/180)},t.radToDeg=function(t){return t*(180/Math.PI)},t.hexToRGB=function(t){if(!t)return!1;"#"===t.slice(0,1)&&(t=t.slice(1));var e,i,n;return 6===t.length?(e=t.slice(0,2),i=t.slice(2,4),n=t.slice(4,6)):3===t.length&&(e=t.slice(0,1)+t.slice(0,1),i=t.slice(1,2)+t.slice(1,2),n=t.slice(2,3)+t.slice(2,3)),{r:parseInt(e,16),g:parseInt(i,16),b:parseInt(n,16)}},t.utils.forEach=e,t.utils.noop=i;var u,f={extend:function(e){for(var i in e){if(t.prototype.hasOwnProperty(i))throw new Error("Careful! This method already exists on the Martin prototype. Try a different name after checking the docs: http://martinjs.org");t.prototype[i]=e[i]}},remove:function(){var t=this.canvas,e=t.parentNode;return e&&e.removeChild(this.canvas),this},render:function(e){var i=this.context;return i.clearRect(0,0,this.width(),this.height()),t.utils.forEach(this.layers,function(e){e.clear(),t.utils.forEach(e.elements,function(t){t.renderElement&&t.renderElement()}),t.utils.forEach(e.effects,function(t){t.renderEffect&&t.renderEffect()}),e.canvas.width>0&&e.canvas.height>0&&i.drawImage(e.canvas,0,0)}),e?e():this},autorender:function(t){return this.options.autorender!==!1?this.render(t):t?t():null},toDataURL:function(){return this.canvas.toDataURL()},convertToImage:function(){var t=this.toDataURL(),e=document.createElement("img");e.src=t,this.layers.forEach(function(t,e){this.deleteLayer(e)},this),this.container&&this.container.appendChild(e)}};for(u in f)t.prototype[u]=f[u];t.Object=function(){};var d,p;d={loop:function(t,e){var i,n,a,r,s,o,h,c,l,u,f,d,p=this.base.width();this.base.height();if(i=this.getImageData()){n=i.data,a=n.length;for(var g=0;a>g;g+=4)r=g/4,s=r%p,o=Math.floor(r/p),h=n[g],c=n[g+1],l=n[g+2],u=n[g+3],f={r:h,g:c,b:l,a:u},d=t.call(this.context,s,o,f),n[g]=d.r,n[g+1]=d.g,n[g+2]=d.b,n[g+3]=d.a;e!==!1&&this.putImageData(i)}return this},getImageData:function(){var t=this.context&&this.canvas.width>0&&this.canvas.height>0?this.context.getImageData(0,0,this.canvas.width,this.canvas.height):null;return t},putImageData:function(t){return this.context.putImageData(t,0,0),this},clear:function(){return this.context.clearRect(0,0,this.base.width(),this.base.height()),this},stackIndex:function(){return this.stack.indexOf(this)},remove:function(){return this.stack.splice(this.stackIndex(),1),this.base.autorender(),this},bump:function(t){var e=this.stackIndex();return this.remove(),this.stack.splice(e+t,0,this),this.base.autorender(),this},bumpUp:function(){return this.bump(1)},bumpDown:function(){return this.bump(-1)},bumpToTop:function(){return this.remove(),this.stack.push(this),this.base.autorender(),this},bumpToBottom:function(){return this.remove(),this.stack.unshift(this),this.base.autorender(),this}};for(p in d)t.Object.prototype[p]=d[p];t.Layer=function(t,e){if(this.base=t,this.canvas=document.createElement("canvas"),this.canvas.width=t.original?t.original.naturalWidth||t.original.width:t.width(),this.canvas.height=t.original?t.original.naturalHeight||t.original.height:t.height(),this.context=this.canvas.getContext("2d"),this.scale={x:1,y:1},this.elements=[],this.effects=[],this.base.layers||(this.base.layers=[]),this.stack=this.base.layers,this.stack.push(this),"string"==typeof e)this.type=e;else for(var i in e)this[i]=e[i];return this},t.Layer.prototype=Object.create(t.Object.prototype),t.Layer.prototype.normalizeX=function(t){return"string"==typeof t&&"%"===t.slice(-1)&&(t=this.normalizePercentX(+t.slice(0,-1))),t/this.scale.x},t.Layer.prototype.normalizeY=function(t){return"string"==typeof t&&"%"===t.slice(-1)&&(t=this.normalizePercentY(+t.slice(0,-1))),t/this.scale.y},t.Layer.prototype.normalizePercentX=function(t){return t/100*this.canvas.width},t.Layer.prototype.normalizePercentY=function(t){return t/100*this.canvas.height},t.prototype.newLayer=function(e){var i=new t.Layer(this,e);return this.currentLayer=i,this.autorender(),i},t.prototype.layer=function(t){return this.currentLayer=this.layers[t||0],this.layers[t||0]},t.registerElement=n,t.Element=function(t,e,i){var n=e.base||e,a=e.currentLayer||e;if(this.base=n,this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.canvas.width=n.original?n.original.naturalWidth||n.original.width:n.width(),this.canvas.height=n.original?n.original.naturalHeight||n.original.height:n.height(),this.scale={x:1,y:1},this.data=i||{},"string"==typeof i.x||"string"==typeof i.y){var r=i.x||"",s=i.y||"";"string"==typeof r&&"%"===r.slice(-1)&&(this.data.percentX=r,this.relativePosition=!0),"string"==typeof s&&"%"===s.slice(-1)&&(this.data.percentY=s)}return i.x&&(this.data.x=a.normalizeX(i.x)),i.y&&(this.data.y=a.normalizeY(i.y)),this.type=t,this.layer=a,this.effects=[],this.stack=this.layer.elements,this.stack.push(this),"background"===this.type&&(this.data={color:i},this.bumpToBottom()),this},t.Element.prototype=Object.create(t.Object.prototype),t.Element.prototype.setContext=function(t){var e=this.context;e.save(),e.fillStyle=t.color||"#000",e.fill(),e.scale(this.scale.x,this.scale.y),e.globalAlpha=t.alpha||1,e.lineWidth=t.strokeWidth?t.strokeWidth:0,e.lineCap=t.cap?t.cap:"square",e.strokeStyle=t.stroke?t.stroke:"transparent",e.stroke(),e.restore()},t.Element.prototype.update=function(t,e){var i,n;if(e)i=t,n=e,this.data[i]=n;else for(i in t)n=t[i],this.data[i]=n;("x"===i||"y"===i)&&(this.relativePosition=!1),this.base.autorender()},t.Element.prototype.moveTo=function(t,e){var i=this.data;return t=t||0,e=e||0,"line"===this.type?(i.endX+=t-i.x,i.endY+=e-i.y):"polygon"===this.type&&(i.points.forEach(function(n,a){if(a>0){var r=n[0],s=n[1];i.points[a]=[r+(t-i.points[0][0]),s+(e-i.points[0][1])]}}),i.points[0]=[t,e]),i.x=t,i.y=e,this.relativePosition=!1,this.base.autorender(),this},n("image",function(t){a.call(this,t)}),n("rect",function(t){r.call(this,t)}),n("background",function(t){r.call(this,t)}),n("line",function(t){var e=this.layer,i=this.context;return i.moveTo(e.normalizeX(t.x||0),e.normalizeY(t.y||0)),i.lineTo(e.normalizeX(t.endX),e.normalizeY(t.endY)),t.strokeWidth||(t.strokeWidth=1),t.stroke=t.color?t.color:"#000",this}),n("circle",function(t){var e=this.layer,i=this.context,n=e.normalizeX(t.x||0),a=e.normalizeY(t.y||0);i.arc(n,a,t.radius,0,2*Math.PI,!1)}),n("ellipse",function(t){var e,i=this.layer,n=this.context,a=i.normalizeX(t.x||0),r=i.normalizeY(t.y||0);return t.radiusX>t.radiusY?(e=t.radiusX/t.radiusY,n.scale(e,1),n.arc(a/e,r,t.radiusX/e,0,2*Math.PI,!1),n.scale(1/e,1)):(e=t.radiusY/t.radiusX,n.scale(1,e),n.arc(a,r/e,t.radiusY/e,0,2*Math.PI,!1),n.scale(1,1/e)),this}),n("polygon",function(t){for(var e=this.layer,i=this.context,n=0;nt)return this;t=Math.round(t);var e=2,i=this.base.width(),n=this.base.height(),a=i-1,r=n-1,s=t+1,o=2*t+1,h=c.mul_shift_table(t)[0],l=c.mul_shift_table(t)[1],u=e,f=this.context.getImageData();if(f){var d,p,g,y,m,v,x,b,E,w,z,k,I,X,Y,L,T,P=f.data,C=new c,D=C;for(g=1;o>g;g++)D=D.next=new c,g===s&&(L=D);for(D.next=C,T=null;u-->0;){for(x=v=0,p=n;--p>-1;){for(b=s*(k=P[v]),E=s*(I=P[v+1]),w=s*(X=P[v+2]),z=s*(Y=P[v+3]),D=C,g=s;--g>-1;)D.r=k,D.g=I,D.b=X,D.a=Y,D=D.next;for(g=1;s>g;g++)y=v+((g>a?a:g)<<2),b+=D.r=P[y],E+=D.g=P[y+1],w+=D.b=P[y+2],z+=D.a=P[y+3],D=D.next;for(T=C,d=0;i>d;d++)P[v++]=b*h>>>l,P[v++]=E*h>>>l,P[v++]=w*h>>>l,P[v++]=z*h>>>l,y=x+((y=d+t+1)d;d++){for(v=d<<2,b=s*(k=P[v]),E=s*(I=P[v+1]),w=s*(X=P[v+2]),z=s*(Y=P[v+3]),D=C,g=0;s>g;g++)D.r=k,D.g=I,D.b=X,D.a=Y,D=D.next;for(m=i,g=1;t>=g;g++)v=m+d<<2,b+=D.r=P[v],E+=D.g=P[v+1],w+=D.b=P[v+2],z+=D.a=P[v+3],D=D.next,r>g&&(m+=i);for(v=d,T=C,p=0;n>p;p++)y=v<<2,P[y+3]=Y=z*h>>>l,Y>0?(Y=255/Y,P[y]=(b*h>>>l)*Y,P[y+1]=(E*h>>>l)*Y,P[y+2]=(w*h>>>l)*Y):P[y]=P[y+1]=P[y+2]=0,y=d+((y=p+s)v;v++)for(var b=0;o>b;b++){var E=u+v-h,w=f+b-h;if(E>=0&&i>E&&w>=0&&e>w){var z=4*(E*e+w),k=s[v*o+b];p+=c[z]*k,g+=c[z+1]*k,y+=c[z+2]*k,m+=c[z+3]*k}}r[d]=p*t+c[d]*(1-t),r[d+1]=g*t+c[d+1]*(1-t),r[d+2]=y*t+c[d+2]*(1-t),r[d+3]=c[d+3]}this.context.putImageData(a)});var g=["click","mouseover","mousemove","mouseenter","mouseleave","mouseout","mousedown","mouseup"];g.forEach(function(e){t.prototype[e]=function(t){var i=l(this,t,e);return this.canvas.addEventListener(e,i.exec),this}}),t.prototype.on=function(t,e){return t=t.split(" "),t.forEach(function(t){var i=l(this,e,t);g.indexOf(t)>-1&&this.canvas.addEventListener(t,i.exec)},this),this},["width","height"].forEach(function(e){t.prototype[e]=function(t,i){return t?(this.canvas[e]=t,this.layers.forEach(function(n){n[e](t,i)}),this):this.canvas[e]},t.Layer.prototype[e]=function(i,n){var a,r=this;return i?(i=this["normalize"+("width"===e?"X":"Y")](i),this.canvas[e]=i,t.utils.forEach(this.elements,function(t){t.canvas[e]=i,t.relativePosition&&(t.data.percentX&&(t.data.x=r.normalizeX(t.data.percentX)),t.data.percentY&&(t.data.y=r.normalizeY(t.data.percentY)))}),a=n?i/this.canvas[e]:1,n&&("width"===e&&(this.scale.x*=a),"height"===e&&(this.scale.y*=a),this.canvas[e]=i),this.base.autorender(),this):this.canvas[e]}}),this.Martin=t}(); 2 | //# sourceMappingURL=martin.min.js.map 3 | -------------------------------------------------------------------------------- /js/dist/martin.tile.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('tile', function(px) { 2 | 3 | var r, g, b, a, 4 | imageData = this.context.getImageData(), 5 | pixels = imageData.data; 6 | 7 | px = parseInt(px, 10); 8 | 9 | if ( px > 1 ) { 10 | 11 | this.context.loop(function(x, y, pixel) { 12 | 13 | x -= x % px; 14 | y -= y % px; 15 | 16 | var target = 4 * (x + canvas.width() * y); 17 | 18 | pixel.r = pixels[target]; 19 | pixel.g = pixels[target + 1]; 20 | pixel.b = pixels[target + 2]; 21 | pixel.a = pixels[target + 3]; 22 | 23 | return pixel; 24 | }); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /js/dist/martin.tile.min.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect("tile",function(t){var a=this.context.getImageData(),e=a.data;t=parseInt(t,10),t>1&&this.context.loop(function(a,n,r){a-=a%t,n-=n%t;var i=4*(a+canvas.width()*n);return r.r=e[i],r.g=e[i+1],r.b=e[i+2],r.a=e[i+3],r})}); -------------------------------------------------------------------------------- /js/dist/martin.watermark.js: -------------------------------------------------------------------------------- 1 | Martin.registerElement('watermark', function(data) { 2 | 3 | 4 | var padding = 2, 5 | size = data.size || this.data.size || 12; 6 | 7 | data = { 8 | text: data.text || '\u00A9', // default to the copyright symbol 9 | align: data.align || 'right', 10 | color: data.color || '#fff', 11 | x: data.x || this.base.width() - padding, 12 | y: data.y || this.base.height() - size - padding, 13 | size: size 14 | }; 15 | 16 | this.data = data; 17 | 18 | if ( !this._textElement ) { 19 | this._textElement = this.layer.text(data); 20 | } else { 21 | this._textElement.update(data); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /js/dist/martin.watermark.min.js: -------------------------------------------------------------------------------- 1 | Martin.registerElement("watermark",function(t){var e=2,i=t.size||this.data.size||12;t={text:t.text||"©",align:t.align||"right",color:t.color||"#fff",x:t.x||this.base.width()-e,y:t.y||this.base.height()-i-e,size:i},this.data=t,this._textElement?this._textElement.update(t):this._textElement=this.layer.text(t)}); -------------------------------------------------------------------------------- /js/dist/watermark.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var watermark = function(text, color, size) { 4 | 5 | // default to the copyright symbol 6 | text = text || '\u00A9'; 7 | color = color || '#fff'; 8 | size = size || 12; 9 | var padding = 2; 10 | var data = { 11 | text: text, 12 | align: 'right', 13 | color: color, 14 | x: this.width() - padding, 15 | y: this.height() - size - padding, 16 | size: size 17 | }; 18 | 19 | return this.text(data); 20 | 21 | }; 22 | 23 | Martin.extend({ watermark: watermark }); 24 | 25 | })(); 26 | -------------------------------------------------------------------------------- /js/src/core/dimensions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Need to find a place for the rest of these important methods. 3 | 4 | .width() 5 | .height() 6 | */ 7 | 8 | // Set or change dimensions. 9 | [ 'width', 'height' ].forEach(function( which ) { 10 | 11 | Martin.prototype[which] = function( val, resize ) { 12 | 13 | // if no value given, return the corresponding value 14 | if ( !val ) return this.canvas[which]; 15 | 16 | // Update height or width of all the layers' canvases 17 | // and update their contexts 18 | this.canvas[which] = val; 19 | this.layers.forEach(function(layer) { 20 | layer[which](val, resize); 21 | }); 22 | 23 | return this; 24 | 25 | }; 26 | 27 | Martin.Layer.prototype[which] = function(val, resize) { 28 | 29 | var layer = this, 30 | ratio; 31 | 32 | if ( !val ) return this.canvas[which]; 33 | 34 | // normalize the value 35 | val = this['normalize' + (which === 'width' ? 'X' : 'Y')](val); 36 | 37 | // resize this layer's canvas 38 | this.canvas[which] = val; 39 | 40 | // resize element canvases 41 | Martin.utils.forEach(this.elements, function(element) { 42 | element.canvas[which] = val; 43 | 44 | // if relatively positioned, reposition 45 | if ( element.relativePosition ) { 46 | if ( element.data.percentX ) { 47 | element.data.x = layer.normalizeX(element.data.percentX); 48 | } 49 | if ( element.data.percentY ) { 50 | element.data.y = layer.normalizeY(element.data.percentY); 51 | } 52 | } 53 | }); 54 | 55 | // get the ratio, in case we're resizing 56 | ratio = resize ? val / this.canvas[which] : 1; 57 | 58 | if ( resize ) { 59 | 60 | if ( which === 'width' ) this.scale.x *= ratio; 61 | if ( which === 'height' ) this.scale.y *= ratio; 62 | 63 | this.canvas[which] = val; 64 | } 65 | 66 | this.base.autorender(); 67 | 68 | return this; 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /js/src/core/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | For helper functions that don't extend Martin prototype. 3 | 4 | degToRad() 5 | radToDeg() 6 | hexToRGB() 7 | */ 8 | 9 | Martin.degToRad = function(deg) { 10 | return deg * ( Math.PI / 180 ); 11 | }; 12 | 13 | Martin.radToDeg = function(rad) { 14 | return rad * ( 180 / Math.PI ); 15 | }; 16 | 17 | Martin.hexToRGB = function( hex ) { 18 | 19 | if ( !hex ) { return false; } 20 | 21 | if ( hex.slice(0, 1) === '#' ) { hex = hex.slice(1); } 22 | 23 | var r, g, b; 24 | 25 | if ( hex.length === 6 ) { 26 | 27 | r = hex.slice(0, 2); 28 | g = hex.slice(2, 4); 29 | b = hex.slice(4, 6); 30 | 31 | } else if ( hex.length === 3 ) { 32 | 33 | r = hex.slice(0, 1) + hex.slice(0, 1); 34 | g = hex.slice(1, 2) + hex.slice(1, 2); 35 | b = hex.slice(2, 3) + hex.slice(2, 3); 36 | 37 | } 38 | 39 | return { 40 | r: parseInt(r, 16), 41 | g: parseInt(g, 16), 42 | b: parseInt(b, 16) 43 | }; 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /js/src/core/init.js: -------------------------------------------------------------------------------- 1 | // Convert an image to a canvas or just return the canvas. 2 | Martin.prototype.makeCanvas = function() { 3 | 4 | this.canvas = document.createElement('canvas'); 5 | this.context = this.canvas.getContext('2d'); 6 | 7 | // Create an empty layer 8 | this.newLayer(); 9 | 10 | if ( this.original ) { 11 | 12 | if ( this.original.tagName === 'IMG' ) { 13 | 14 | var canvas = this.canvas, 15 | context = this.context, 16 | original = this.original; 17 | 18 | function d() { 19 | 20 | // switch to bottom layer 21 | var curLayer = this.currentLayer.stackIndex(); 22 | this.layer(0); 23 | 24 | canvas.width = original.naturalWidth; 25 | canvas.height = original.naturalHeight; 26 | 27 | this.width(canvas.width); 28 | this.height(canvas.height); 29 | 30 | original.parentNode.insertBefore( canvas, original ); 31 | original.parentNode.removeChild( original ); 32 | 33 | // Give that layer some image data (see src/element/image.js) 34 | Martin.registerElement('image', function(img) { 35 | drawImage.call(this, img); 36 | }); 37 | 38 | this.image(original).bumpToBottom(); 39 | 40 | // switch back to previous layer 41 | this.layer(curLayer); 42 | } 43 | 44 | // This should only fire once! Fire if the image is complete, 45 | // or add a handler for once it has finished loading. 46 | if ( original.complete ) return d.call(this); 47 | original.onload = d.bind(this); 48 | 49 | } else if ( this.original.tagName === 'CANVAS' ) { 50 | 51 | this.canvas = this.original; 52 | this.context = this.original.getContext('2d'); 53 | } 54 | } 55 | 56 | // only render and execute callback immediately 57 | // if the original is not an image 58 | this.autorender(); 59 | 60 | return this; 61 | }; 62 | -------------------------------------------------------------------------------- /js/src/core/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | For (mostly) utility functions that extend Martin prototype. 3 | 4 | extend() 5 | .remove() 6 | .render() 7 | .toDataURL() 8 | .convertToImage() 9 | */ 10 | 11 | function forEach(arr, cb) { 12 | if (arr) { 13 | arr.forEach(cb); 14 | } 15 | } 16 | 17 | function noop() {} 18 | 19 | Martin.utils.forEach = forEach; 20 | Martin.utils.noop = noop; 21 | 22 | var i, 23 | func, 24 | funcs = { 25 | 26 | // Extend Martin with plugins, if you want 27 | extend: function extend( obj ) { 28 | for ( var method in obj ) { 29 | if ( Martin.prototype.hasOwnProperty(method) ) { 30 | throw new Error('Careful! This method already exists on the Martin prototype. Try a different name after checking the docs: http://martinjs.org'); 31 | } else { 32 | Martin.prototype[method] = obj[method]; 33 | } 34 | } 35 | }, 36 | 37 | remove: function remove() { 38 | var canvas = this.canvas, 39 | parent = canvas.parentNode; 40 | if ( parent ) parent.removeChild(this.canvas); 41 | return this; 42 | }, 43 | 44 | // Render: looping through layers, loop through elements 45 | // and render each (with optional callback) 46 | render: function render(cb) { 47 | 48 | var ctx = this.context; 49 | 50 | ctx.clearRect(0, 0, this.width(), this.height()); 51 | 52 | Martin.utils.forEach(this.layers, function(layer) { 53 | 54 | layer.clear(); 55 | 56 | Martin.utils.forEach(layer.elements, function renderElement(element) { 57 | element.renderElement && element.renderElement(); 58 | }); 59 | 60 | Martin.utils.forEach(layer.effects, function renderEffect(effect) { 61 | effect.renderEffect && effect.renderEffect(); 62 | }); 63 | 64 | if ( layer.canvas.width > 0 && layer.canvas.height > 0 ) { 65 | ctx.drawImage(layer.canvas, 0, 0); 66 | } 67 | }); 68 | 69 | if (cb) return cb(); 70 | 71 | return this; 72 | }, 73 | 74 | // Autorender: Only render if the `autorender` option is not false 75 | autorender: function autorender(cb) { 76 | if ( this.options.autorender !== false ) return this.render(cb); 77 | return cb ? cb() : null; 78 | }, 79 | 80 | // Return's a data URL of all the working layers 81 | toDataURL: function toDataURL() { 82 | return this.canvas.toDataURL(); 83 | }, 84 | 85 | // Get the dataURL of the merged layers of the canvas, 86 | // then turn that into one image 87 | convertToImage: function convertToImage() { 88 | 89 | var dataURL = this.toDataURL(), 90 | img = document.createElement('img'); 91 | 92 | img.src = dataURL; 93 | 94 | this.layers.forEach(function(layer, i){ 95 | this.deleteLayer(i); 96 | }, this); 97 | 98 | if ( this.container ) this.container.appendChild( img ); 99 | 100 | } 101 | 102 | }; 103 | 104 | for ( func in funcs ) { 105 | Martin.prototype[func] = funcs[func]; 106 | } 107 | -------------------------------------------------------------------------------- /js/src/core/version.js: -------------------------------------------------------------------------------- 1 | Martin._version = '0.4.2'; 2 | -------------------------------------------------------------------------------- /js/src/effect/blur.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * StackBlur Algorithm Copyright (c) 2010 Mario Klingemann 4 | * Version: 0.6 5 | * Author: Mario Klingemann 6 | * Contact: mario@quasimondo.com 7 | * Website: http://www.quasimondo.com/StackBlurForCanvas 8 | * Twitter: @quasimondo 9 | * 10 | */ 11 | 12 | // simple stack maker 13 | function BlurStack() { 14 | this.r = this.g = this.b = this.a = 0; 15 | this.next = null; 16 | } 17 | 18 | // helper functions for .blur() 19 | BlurStack.mul_shift_table = function(i) { 20 | var mul_table = [1,171,205,293,57,373,79,137,241,27,391,357,41,19,283,265,497,469,443,421,25,191,365,349,335,161,155,149,9,278,269,261,505,245,475,231,449,437,213,415,405,395,193,377,369,361,353,345,169,331,325,319,313,307,301,37,145,285,281,69,271,267,263,259,509,501,493,243,479,118,465,459,113,446,55,435,429,423,209,413,51,403,199,393,97,3,379,375,371,367,363,359,355,351,347,43,85,337,333,165,327,323,5,317,157,311,77,305,303,75,297,294,73,289,287,71,141,279,277,275,68,135,67,133,33,262,260,129,511,507,503,499,495,491,61,121,481,477,237,235,467,232,115,457,227,451,7,445,221,439,218,433,215,427,425,211,419,417,207,411,409,203,202,401,399,396,197,49,389,387,385,383,95,189,47,187,93,185,23,183,91,181,45,179,89,177,11,175,87,173,345,343,341,339,337,21,167,83,331,329,327,163,81,323,321,319,159,79,315,313,39,155,309,307,153,305,303,151,75,299,149,37,295,147,73,291,145,289,287,143,285,71,141,281,35,279,139,69,275,137,273,17,271,135,269,267,133,265,33,263,131,261,130,259,129,257,1]; 21 | 22 | 23 | var shg_table = [0,9,10,11,9,12,10,11,12,9,13,13,10,9,13,13,14,14,14,14,10,13,14,14,14,13,13,13,9,14,14,14,15,14,15,14,15,15,14,15,15,15,14,15,15,15,15,15,14,15,15,15,15,15,15,12,14,15,15,13,15,15,15,15,16,16,16,15,16,14,16,16,14,16,13,16,16,16,15,16,13,16,15,16,14,9,16,16,16,16,16,16,16,16,16,13,14,16,16,15,16,16,10,16,15,16,14,16,16,14,16,16,14,16,16,14,15,16,16,16,14,15,14,15,13,16,16,15,17,17,17,17,17,17,14,15,17,17,16,16,17,16,15,17,16,17,11,17,16,17,16,17,16,17,17,16,17,17,16,17,17,16,16,17,17,17,16,14,17,17,17,17,15,16,14,16,15,16,13,16,15,16,14,16,15,16,12,16,15,16,17,17,17,17,17,13,16,15,17,17,17,16,15,17,17,17,16,15,17,17,14,16,17,17,16,17,17,16,15,17,16,14,17,16,15,17,16,17,17,16,17,15,16,17,14,17,16,15,17,16,17,13,17,16,17,17,16,17,14,17,16,17,16,17,16,17,9]; 24 | 25 | return [ mul_table[i] || mul_table[mul_table.length - 1], shg_table[i] || shg_table[shg_table.length - 1] ]; 26 | }; 27 | 28 | // And, what we've all been waiting for: 29 | registerEffect('blur', function(amt) { 30 | 31 | if ( isNaN(amt) || amt < 1 ) return this; 32 | // Round to nearest pixel 33 | amt = Math.round(amt); 34 | 35 | var iterations = 2, // increase for smoother blurring 36 | width = this.base.width(), 37 | height = this.base.height(), 38 | widthMinus1 = width - 1, 39 | heightMinus1 = height - 1, 40 | radiusPlus1 = amt + 1, 41 | div = 2 * amt + 1, 42 | mul_sum = BlurStack.mul_shift_table(amt)[0], 43 | shg_sum = BlurStack.mul_shift_table(amt)[1]; 44 | 45 | var it = iterations, // internal iterations in case doing multiple layers 46 | imageData = this.context.getImageData(); 47 | 48 | if ( imageData ) { 49 | var pixels = imageData.data; 50 | 51 | var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, 52 | r_out_sum, g_out_sum, b_out_sum, a_out_sum, 53 | r_in_sum, g_in_sum, b_in_sum, a_in_sum, 54 | pr, pg, pb, pa; 55 | 56 | var stackStart = new BlurStack(), 57 | stack = stackStart, 58 | stackEnd, 59 | stackIn; 60 | 61 | for ( i = 1; i < div; i++ ) { 62 | stack = stack.next = new BlurStack(); 63 | if ( i === radiusPlus1 ) stackEnd = stack; 64 | } 65 | 66 | stack.next = stackStart; 67 | stackIn = null; 68 | 69 | // repeat for as many iterations as given 70 | while ( it-- > 0 ) { 71 | 72 | yw = yi = 0; 73 | 74 | // loop through rows from top down 75 | for ( y = height; --y > -1; ) { 76 | 77 | // start summing pixel values 78 | r_sum = radiusPlus1 * ( pr = pixels[yi] ); 79 | g_sum = radiusPlus1 * ( pg = pixels[yi + 1] ); 80 | b_sum = radiusPlus1 * ( pb = pixels[yi + 2] ); 81 | a_sum = radiusPlus1 * ( pa = pixels[yi + 3] ); 82 | 83 | stack = stackStart; 84 | 85 | for ( i = radiusPlus1; --i > -1; ) { 86 | stack.r = pr; 87 | stack.g = pg; 88 | stack.b = pb; 89 | stack.a = pa; 90 | 91 | stack = stack.next; 92 | } 93 | 94 | for ( i = 1; i < radiusPlus1; i++ ) { 95 | 96 | p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); 97 | 98 | r_sum += ( stack.r = pixels[p]); 99 | g_sum += ( stack.g = pixels[p + 1]); 100 | b_sum += ( stack.b = pixels[p + 2]); 101 | a_sum += ( stack.a = pixels[p + 3]); 102 | 103 | stack = stack.next; 104 | } 105 | 106 | stackIn = stackStart; 107 | 108 | for ( x = 0; x < width; x++ ) { 109 | pixels[yi++] = (r_sum * mul_sum) >>> shg_sum; 110 | pixels[yi++] = (g_sum * mul_sum) >>> shg_sum; 111 | pixels[yi++] = (b_sum * mul_sum) >>> shg_sum; 112 | pixels[yi++] = (a_sum * mul_sum) >>> shg_sum; 113 | 114 | p = ( yw + ( ( p = x + amt + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; 115 | 116 | r_sum -= stackIn.r - ( stackIn.r = pixels[p]); 117 | g_sum -= stackIn.g - ( stackIn.g = pixels[p + 1]); 118 | b_sum -= stackIn.b - ( stackIn.b = pixels[p + 2]); 119 | a_sum -= stackIn.a - ( stackIn.a = pixels[p + 3]); 120 | 121 | stackIn = stackIn.next; 122 | } 123 | 124 | // next row 125 | yw += width; 126 | } 127 | 128 | for ( x = 0; x < width; x++ ) { 129 | 130 | // with each column, divide yi by 4 (4 values per px) 131 | yi = x << 2; 132 | 133 | r_sum = radiusPlus1 * ( pr = pixels[yi]); 134 | g_sum = radiusPlus1 * ( pg = pixels[yi + 1]); 135 | b_sum = radiusPlus1 * ( pb = pixels[yi + 2]); 136 | a_sum = radiusPlus1 * ( pa = pixels[yi + 3]); 137 | 138 | stack = stackStart; 139 | 140 | for ( i = 0; i < radiusPlus1; i++ ) { 141 | stack.r = pr; 142 | stack.g = pg; 143 | stack.b = pb; 144 | stack.a = pa; 145 | stack = stack.next; 146 | } 147 | 148 | yp = width; 149 | 150 | for ( i = 1; i <= amt; i++ ) { 151 | yi = ( yp + x ) << 2; 152 | 153 | r_sum += ( stack.r = pixels[yi]); 154 | g_sum += ( stack.g = pixels[yi + 1]); 155 | b_sum += ( stack.b = pixels[yi + 2]); 156 | a_sum += ( stack.a = pixels[yi + 3]); 157 | 158 | stack = stack.next; 159 | 160 | if ( i < heightMinus1 ) yp += width; 161 | } 162 | 163 | yi = x; 164 | stackIn = stackStart; 165 | 166 | for ( y = 0; y < height; y++ ) { 167 | 168 | p = yi << 2; 169 | 170 | pixels[p + 3] = pa =(a_sum * mul_sum) >>> shg_sum; 171 | 172 | if ( pa > 0 ) { 173 | pa = 255 / pa; 174 | pixels[p] = ((r_sum * mul_sum) >>> shg_sum ) * pa; 175 | pixels[p + 1] = ((g_sum * mul_sum) >>> shg_sum ) * pa; 176 | pixels[p + 2] = ((b_sum * mul_sum) >>> shg_sum ) * pa; 177 | } else { 178 | pixels[p] = pixels[p + 1] = pixels[p + 2] = 0; 179 | } 180 | 181 | p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; 182 | 183 | r_sum -= stackIn.r - ( stackIn.r = pixels[p]); 184 | g_sum -= stackIn.g - ( stackIn.g = pixels[p + 1]); 185 | b_sum -= stackIn.b - ( stackIn.b = pixels[p + 2]); 186 | a_sum -= stackIn.a - ( stackIn.a = pixels[p + 3]); 187 | 188 | stackIn = stackIn.next; 189 | 190 | yi += width; 191 | } 192 | } 193 | } 194 | 195 | this.context.putImageData( imageData ); 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /js/src/effect/desaturate.js: -------------------------------------------------------------------------------- 1 | // Desaturate 2 | function desaturate(amt) { 3 | 4 | this.context.loop(function(x, y, pixel) { 5 | 6 | var r = pixel.r, 7 | g = pixel.g, 8 | b = pixel.b; 9 | 10 | var grayscale = r * 0.3 + g * 0.59 + b * 0.11; 11 | r = (1 - amt) * r + amt * grayscale; // red 12 | g = (1 - amt) * g + amt * grayscale; // green 13 | b = (1 - amt) * b + amt * grayscale; // blue 14 | 15 | pixel.r = r; 16 | pixel.g = g; 17 | pixel.b = b; 18 | 19 | return pixel; 20 | 21 | }); 22 | } 23 | 24 | registerEffect('desaturate', function(amt) { 25 | amt = amt / 100; 26 | desaturate.call(this, amt); 27 | }); 28 | 29 | // inverse of saturate 30 | registerEffect('saturate', function(amt) { 31 | amt = -amt / 100; 32 | desaturate.call(this, amt); 33 | }); 34 | -------------------------------------------------------------------------------- /js/src/effect/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Martin.Effect constructor 4 | 5 | Effect methods: 6 | .increase() 7 | .decrease() 8 | */ 9 | 10 | function registerEffect(name, cb) { 11 | 12 | function attachRender(data, stack, stackContainer) { 13 | 14 | // create new effect 15 | var effect = new Martin.Effect(name, this, data, stack, stackContainer); 16 | 17 | // attach render function (callback) -- 18 | // execute with effect's data 19 | effect.renderEffect = function renderEffect() { 20 | cb.call(effect, this.data); 21 | }; 22 | 23 | return effect; 24 | } 25 | 26 | Martin.prototype[name] = function attachToBase(data) { 27 | var effect = attachRender.call(this, data, this.currentLayer.effects, this.currentLayer); 28 | this.autorender(); 29 | return effect; 30 | }; 31 | 32 | Martin.Layer.prototype[name] = 33 | Martin.Element.prototype[name] = function attachToLayerOrElement(data) { 34 | var effect = attachRender.call(this.base, data, this.effects, this); 35 | this.base.autorender(); 36 | return effect; 37 | }; 38 | }; 39 | 40 | Martin.registerEffect = registerEffect; 41 | 42 | Martin.Effect = function(type, base, data, stack, stackContainer) { 43 | 44 | this.base = base; 45 | this.type = type; 46 | 47 | this.data = data; 48 | 49 | this.context = stackContainer; 50 | 51 | this.stack = stack; 52 | this.stack.push(this); 53 | 54 | return this; 55 | }; 56 | 57 | Martin.Effect.prototype = Object.create(Martin.Object.prototype); 58 | 59 | // Adjust the intensity of an Effect (linear effects only) 60 | Martin.Effect.prototype.increase = function(amt) { 61 | 62 | if ( typeof this.data === 'number' ) { 63 | this.data += amt || 1; 64 | this.base.autorender(); 65 | } 66 | 67 | return this; 68 | }; 69 | 70 | Martin.Effect.prototype.decrease = function(amt) { 71 | return this.increase(-amt || -1); 72 | }; 73 | -------------------------------------------------------------------------------- /js/src/effect/invert.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('invert', function() { 2 | this.context.loop(function(x, y, pixel) { 3 | pixel.r = 255 - pixel.r; 4 | pixel.g = 255 - pixel.g; 5 | pixel.b = 255 - pixel.b; 6 | 7 | return pixel; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /js/src/effect/lighten.js: -------------------------------------------------------------------------------- 1 | // Lighten and darken. (Darken just returns the opposite of lighten). 2 | // Takes an input from 0 to 100. Higher values return pure white or black. 3 | function lighten(amt) { 4 | 5 | this.context.loop(function(x, y, pixel) { 6 | 7 | pixel.r += Math.round(amt * 255); 8 | pixel.g += Math.round(amt * 255); 9 | pixel.b += Math.round(amt * 255); 10 | 11 | return pixel; 12 | }); 13 | } 14 | 15 | registerEffect('lighten', function(amt) { 16 | amt = amt / 100; 17 | lighten.call(this, amt); 18 | }); 19 | 20 | registerEffect('darken', function(amt) { 21 | amt = -amt / 100; 22 | lighten.call(this, amt); 23 | }); 24 | -------------------------------------------------------------------------------- /js/src/effect/opacity.js: -------------------------------------------------------------------------------- 1 | // Fade uniform 2 | registerEffect('opacity', function(amt) { 3 | 4 | amt = amt / 100; 5 | 6 | var base = this.base; 7 | 8 | this.context.loop(function(x, y, pixel) { 9 | pixel.a *= amt; 10 | return pixel; 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /js/src/effect/sharpen.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('sharpen', function(amt) { 2 | 3 | if ( isNaN(amt) ) return this; 4 | 5 | var w = this.base.width(), 6 | h = this.base.height(); 7 | 8 | var buffer = document.createElement('canvas'); 9 | buffer.width = this.base.width(); 10 | buffer.height = this.base.height(); 11 | 12 | var dstData = buffer.getContext('2d').createImageData(w, h), 13 | dstBuff = dstData.data; 14 | 15 | var weights = [0, -1, 0, -1, 5, -1, 0, -1, 0], 16 | katet = Math.round(Math.sqrt(weights.length)), 17 | half = (katet * 0.5) | 0, 18 | srcBuff = this.context.getImageData(0, 0, w, h).data, 19 | y = h; 20 | 21 | amt /= 100; 22 | 23 | while (y--) { 24 | 25 | x = w; 26 | 27 | while (x--) { 28 | 29 | var sy = y, 30 | sx = x, 31 | dstOff = (y * w + x) * 4, 32 | r = 0, 33 | g = 0, 34 | b = 0, 35 | a = 0; 36 | 37 | for (var cy = 0; cy < katet; cy++) { 38 | for (var cx = 0; cx < katet; cx++) { 39 | 40 | var scy = sy + cy - half; 41 | var scx = sx + cx - half; 42 | 43 | if (scy >= 0 && scy < h && scx >= 0 && scx < w) { 44 | 45 | var srcOff = (scy * w + scx) * 4; 46 | var wt = weights[cy * katet + cx]; 47 | 48 | r += srcBuff[srcOff] * wt; 49 | g += srcBuff[srcOff + 1] * wt; 50 | b += srcBuff[srcOff + 2] * wt; 51 | a += srcBuff[srcOff + 3] * wt; 52 | } 53 | } 54 | } 55 | 56 | dstBuff[dstOff] = r * amt + srcBuff[dstOff] * (1 - amt); 57 | dstBuff[dstOff + 1] = g * amt + srcBuff[dstOff + 1] * (1 - amt); 58 | dstBuff[dstOff + 2] = b * amt + srcBuff[dstOff + 2] * (1 - amt) 59 | dstBuff[dstOff + 3] = srcBuff[dstOff + 3]; 60 | } 61 | } 62 | 63 | this.context.putImageData(dstData); 64 | }); -------------------------------------------------------------------------------- /js/src/element/circle.js: -------------------------------------------------------------------------------- 1 | registerElement('circle', function(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context, 5 | centerX = layer.normalizeX( data.x || 0 ), 6 | centerY = layer.normalizeY( data.y || 0 ); 7 | 8 | context.arc( centerX, centerY, data.radius, 0, 2 * Math.PI, false); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /js/src/element/ellipse.js: -------------------------------------------------------------------------------- 1 | registerElement('ellipse', function(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context, 5 | centerX = layer.normalizeX( data.x || 0 ), 6 | centerY = layer.normalizeY( data.y || 0 ), 7 | scale; 8 | 9 | if ( data.radiusX > data.radiusY ) { 10 | 11 | scale = data.radiusX / data.radiusY; 12 | 13 | context.scale( scale, 1 ); 14 | 15 | context.arc( centerX / scale, centerY, data.radiusX / scale, 0, 2 * Math.PI, false); 16 | 17 | context.scale( 1 / scale, 1 ); 18 | 19 | } else { 20 | 21 | scale = data.radiusY / data.radiusX; 22 | 23 | context.scale( 1, scale ); 24 | 25 | context.arc( centerX, centerY / scale, data.radiusY / scale, 0, 2 * Math.PI, false); 26 | 27 | context.scale( 1, 1 / scale ); 28 | 29 | } 30 | 31 | return this; 32 | }); 33 | -------------------------------------------------------------------------------- /js/src/element/image.js: -------------------------------------------------------------------------------- 1 | function drawImage(img) { 2 | this.context.drawImage( img, 0, 0 ); 3 | } 4 | 5 | registerElement('image', function(img) { 6 | drawImage.call(this, img); 7 | }); 8 | -------------------------------------------------------------------------------- /js/src/element/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Martin.Element constructor 4 | 5 | Element methods: 6 | .update() 7 | .moveTo() 8 | */ 9 | 10 | function registerElement(name, cb) { 11 | 12 | function attachRender(data) { 13 | 14 | // create new element 15 | var element = new Martin.Element(name, this, data); 16 | 17 | // attach render function (callback) -- 18 | // execute with element's data 19 | element.renderElement = function renderElement() { 20 | 21 | var layer = this.layer, 22 | context = this.context; 23 | 24 | // clear any image data 25 | this.clear(); 26 | 27 | // scale the context 28 | context.scale( 29 | layer.scale.x, 30 | layer.scale.y 31 | ); 32 | 33 | context.beginPath(); 34 | 35 | cb.call(element, this.data); 36 | 37 | this.setContext(this.data); 38 | 39 | context.closePath(); 40 | 41 | // undo scaling 42 | context.scale( 43 | 1 / layer.scale.x, 44 | 1 / layer.scale.y 45 | ); 46 | 47 | // render this element's effects 48 | Martin.utils.forEach(this.effects, function(effect) { 49 | effect.renderEffect && effect.renderEffect(); 50 | }); 51 | 52 | // draw to layer 53 | if ( this.canvas.width > 0 && this.canvas.height > 0 ) { 54 | layer.context.drawImage(this.canvas, 0, 0); 55 | } 56 | }; 57 | 58 | return element; 59 | } 60 | 61 | Martin.prototype[name] = function registerToBase(data) { 62 | var el = attachRender.call(this, data); 63 | this.autorender(); 64 | return el; 65 | }; 66 | 67 | Martin.Layer.prototype[name] = function registerToLayer(data) { 68 | var el = attachRender.call(this.base, data); 69 | this.base.autorender(); 70 | return el; 71 | }; 72 | }; 73 | 74 | Martin.registerElement = registerElement; 75 | 76 | Martin.Element = function(type, caller, data) { 77 | 78 | var base = caller.base || caller, 79 | layer = caller.currentLayer || caller; 80 | 81 | // base refers to the instance of Martin 82 | this.base = base; 83 | this.canvas = document.createElement('canvas'); 84 | this.context = this.canvas.getContext('2d'); 85 | 86 | // TODO: bounding box 87 | this.canvas.width = base.original ? (base.original.naturalWidth || base.original.width) : base.width(); 88 | this.canvas.height = base.original ? (base.original.naturalHeight || base.original.height) : base.height(); 89 | 90 | this.scale = { 91 | x: 1, 92 | y: 1 93 | }; 94 | 95 | this.data = data || {}; 96 | 97 | // if given a percentage x or y position, the element has a relative position -- 98 | // it should be updated on layer resizing 99 | if ( typeof data.x === 'string' || typeof data.y === 'string' ) { 100 | var x = data.x || '', 101 | y = data.y || ''; 102 | 103 | if ( typeof x === 'string' && x.slice(-1) === '%' ) { 104 | this.data.percentX = x; 105 | this.relativePosition = true; 106 | } 107 | 108 | if ( typeof y === 'string' && y.slice(-1) === '%' ) { 109 | this.data.percentY = y; 110 | } 111 | } 112 | 113 | if ( data.x ) this.data.x = layer.normalizeX(data.x); 114 | if ( data.y ) this.data.y = layer.normalizeY(data.y); 115 | 116 | this.type = type; 117 | this.layer = layer; 118 | 119 | this.effects = []; 120 | 121 | this.stack = this.layer.elements; 122 | this.stack.push(this); 123 | 124 | // automatically push backgrounds to the bottom of the layer 125 | if ( this.type === 'background' ) { 126 | this.data = { 127 | color: data 128 | }; 129 | this.bumpToBottom(); 130 | } 131 | 132 | return this; 133 | }; 134 | 135 | Martin.Element.prototype = Object.create(Martin.Object.prototype); 136 | 137 | // Set the fill, stroke, alpha for a new shape 138 | Martin.Element.prototype.setContext = function( obj ) { 139 | 140 | var context = this.context; 141 | 142 | context.save(); 143 | 144 | context.fillStyle = obj.color || '#000'; 145 | context.fill(); 146 | 147 | context.scale( 148 | this.scale.x, 149 | this.scale.y 150 | ); 151 | 152 | context.globalAlpha = obj.alpha || 1; 153 | 154 | context.lineWidth = obj.strokeWidth ? obj.strokeWidth : 0; 155 | context.lineCap = obj.cap ? obj.cap : 'square'; 156 | context.strokeStyle = obj.stroke ? obj.stroke : 'transparent'; 157 | context.stroke(); 158 | 159 | context.restore(); 160 | 161 | }; 162 | 163 | // ----- Update an element with new data 164 | Martin.Element.prototype.update = function(arg1, arg2) { 165 | 166 | var key, value, data; 167 | 168 | if ( arg2 ) { 169 | key = arg1; 170 | value = arg2; 171 | this.data[key] = value; 172 | } else { 173 | for ( key in arg1 ) { 174 | value = arg1[key]; 175 | this.data[key] = value; 176 | } 177 | } 178 | 179 | if ( key === 'x' || key === 'y' ) { 180 | this.relativePosition = false; 181 | } 182 | 183 | this.base.autorender(); 184 | }; 185 | 186 | // ----- Move an element to new coordinates 187 | Martin.Element.prototype.moveTo = function(x, y) { 188 | 189 | var data = this.data; 190 | 191 | // if no params given, move to 0, 0 192 | x = x || 0; 193 | y = y || 0; 194 | 195 | if ( this.type === 'line' ) { 196 | data.endX += x - data.x; 197 | data.endY += y - data.y; 198 | } else if ( this.type === 'polygon' ) { 199 | data.points.forEach(function(pt, i) { 200 | if ( i > 0 ) { 201 | var thisX = pt[0], 202 | thisY = pt[1]; 203 | data.points[i] = [ 204 | thisX + (x - data.points[0][0]), 205 | thisY + (y - data.points[0][1]) 206 | ]; 207 | } 208 | }); 209 | data.points[0] = [x, y]; 210 | } 211 | 212 | data.x = x; 213 | data.y = y; 214 | 215 | this.relativePosition = false; 216 | 217 | this.base.autorender(); 218 | 219 | return this; 220 | 221 | }; 222 | -------------------------------------------------------------------------------- /js/src/element/line.js: -------------------------------------------------------------------------------- 1 | registerElement('line', function(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context; 5 | 6 | context.moveTo( 7 | layer.normalizeX( data.x || 0 ), 8 | layer.normalizeY( data.y || 0 ) 9 | ); 10 | 11 | context.lineTo( 12 | layer.normalizeX( data.endX ), 13 | layer.normalizeY( data.endY ) 14 | ); 15 | 16 | if ( !data.strokeWidth ) data.strokeWidth = 1; 17 | data.stroke = data.color ? data.color : '#000'; 18 | 19 | return this; 20 | }); 21 | -------------------------------------------------------------------------------- /js/src/element/polygon.js: -------------------------------------------------------------------------------- 1 | registerElement('polygon', function(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context; 5 | 6 | for ( var i = 0; i < data.points.length; i++ ) { 7 | 8 | var x = data.points[i][0], 9 | y = data.points[i][1], 10 | toX = layer.normalizeX( x ), 11 | toY = layer.normalizeY( y ); 12 | 13 | if ( i === 0 ) context.moveTo( toX, toY ); 14 | 15 | context.lineTo( toX, toY ); 16 | 17 | } 18 | 19 | // close the path 20 | context.lineTo( 21 | layer.normalizeX(data.points[0][0]), 22 | layer.normalizeY(data.points[0][1]) 23 | ); 24 | 25 | return this; 26 | }); 27 | -------------------------------------------------------------------------------- /js/src/element/rect.js: -------------------------------------------------------------------------------- 1 | function rect(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context; 5 | 6 | context.rect( 7 | layer.normalizeX( data.x || 0 ), 8 | layer.normalizeY( data.y || 0 ), 9 | layer.normalizeX( data.width || layer.width() ), 10 | layer.normalizeY( data.height || layer.height() ) 11 | ); 12 | } 13 | 14 | registerElement('rect', function(data) { 15 | rect.call(this, data); 16 | }); 17 | 18 | registerElement('background', function(data) { 19 | rect.call(this, data); 20 | }); 21 | -------------------------------------------------------------------------------- /js/src/element/text.js: -------------------------------------------------------------------------------- 1 | registerElement('text', function(data) { 2 | 3 | var layer = this.layer, 4 | context = this.context, 5 | size, 6 | style, 7 | font, 8 | fontOutput; 9 | 10 | var clone = {}; 11 | 12 | // use custom getters and setters for these properties 13 | style = data.style || ''; 14 | size = data.size || ''; 15 | font = data.font || ''; 16 | 17 | function fontString(style, size, font) { 18 | return (style ? style + ' ' : '') + (size || 16) + 'px ' + (font || 'sans-serif'); 19 | }; 20 | 21 | fontOutput = fontString(data.style, data.size, data.font); 22 | 23 | this.fontSize = function(size) { 24 | if ( size ) { 25 | this.data.size = size; 26 | return size; 27 | } else { 28 | return this.data.size; 29 | } 30 | }; 31 | 32 | this.fontStyle = function(style) { 33 | if ( style ) { 34 | this.data.style = style; 35 | return style; 36 | } 37 | 38 | return this.data.style; 39 | }; 40 | 41 | this.font = function(font) { 42 | if ( font ) { 43 | this.data.font = font; 44 | return font; 45 | } 46 | 47 | return this.data.style; 48 | }; 49 | 50 | this.width = function() { 51 | return context.measureText(data.text || '').width; 52 | }; 53 | 54 | Object.defineProperty(clone, 'theStyle', { 55 | get: function() { 56 | return style; 57 | }, 58 | set: function(style) { 59 | fontOutput = fontString(style, data.size, data.font); 60 | } 61 | }); 62 | 63 | Object.defineProperty(clone, 'theSize', { 64 | get: function() { 65 | return size; 66 | }, 67 | set: function(size) { 68 | fontOutput = fontString(data.style, size, data.font); 69 | } 70 | }); 71 | 72 | Object.defineProperty(clone, 'theFont', { 73 | get: function() { 74 | return font; 75 | }, 76 | set: function(font) { 77 | fontOutput = fontString(data.style, data.size, font); 78 | } 79 | }); 80 | 81 | context.font = fontOutput; 82 | context.fillStyle = data.color || '#000'; 83 | context.textBaseline = 'top'; 84 | context.textAlign = data.align || 'left'; 85 | context.fillText( 86 | data.text || '', 87 | layer.normalizeX(data.x || 0), 88 | layer.normalizeY(data.y || 0) 89 | ); 90 | }); 91 | -------------------------------------------------------------------------------- /js/src/end.js: -------------------------------------------------------------------------------- 1 | this.Martin = Martin; 2 | 3 | })(); 4 | -------------------------------------------------------------------------------- /js/src/event/events.js: -------------------------------------------------------------------------------- 1 | var events = ['click', 'mouseover', 'mousemove', 'mouseenter', 'mouseleave', 'mouseout', 'mousedown', 'mouseup']; 2 | 3 | function EventCallback(base, cb, type) { 4 | return { 5 | exec: function exec(e) { 6 | 7 | var eventObj = {}, k; 8 | 9 | for ( k in e ) { 10 | eventObj[k] = e[k]; 11 | } 12 | 13 | eventObj.x = e.offsetX ? e.offsetX : e.clientX - base.canvas.getBoundingClientRect().left; 14 | eventObj.y = e.offsetY ? e.offsetY : e.clientY - base.canvas.getBoundingClientRect().top; 15 | 16 | cb(eventObj); 17 | base.autorender(); 18 | } 19 | }; 20 | } 21 | 22 | events.forEach(function(evt){ 23 | Martin.prototype[evt] = function(cb) { 24 | 25 | var callback = EventCallback(this, cb, evt); 26 | 27 | this.canvas.addEventListener(evt, callback.exec); 28 | return this; 29 | }; 30 | }); 31 | 32 | Martin.prototype.on = function(evt, cb) { 33 | 34 | evt = evt.split(' '); 35 | 36 | evt.forEach(function(ev) { 37 | var callback = EventCallback(this, cb, ev); 38 | if ( events.indexOf(ev) > -1 ) { 39 | this.canvas.addEventListener(ev, callback.exec); 40 | } 41 | }, this); 42 | 43 | return this; 44 | }; 45 | -------------------------------------------------------------------------------- /js/src/layer/layers.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Martin.Layer constructor 4 | 5 | Methods: 6 | .normalizeX() 7 | .normalizeY() 8 | .normalizePercentX() 9 | .normalizePercentY() 10 | .loop() 11 | .setContext() 12 | .getImageData() 13 | .putImageData() 14 | .render() 15 | .clear() 16 | .remove() 17 | 18 | Methods for working with Layers 19 | 20 | .newLayer() 21 | .layer() 22 | */ 23 | 24 | // ----- Layer constructor 25 | Martin.Layer = function(base, arg) { 26 | 27 | this.base = base; 28 | this.canvas = document.createElement('canvas'); 29 | this.canvas.width = base.original ? (base.original.naturalWidth || base.original.width) : base.width(); 30 | this.canvas.height = base.original ? (base.original.naturalHeight || base.original.height) : base.height(); 31 | this.context = this.canvas.getContext('2d'); 32 | this.scale = { 33 | x: 1, 34 | y: 1 35 | }; 36 | 37 | this.elements = []; 38 | this.effects = []; 39 | 40 | // if no layers yet (initializing), 41 | // the layers are just this new layer, 42 | // and the new layer's context should be the base's 43 | if ( !this.base.layers ) { 44 | this.base.layers = []; 45 | } 46 | this.stack = this.base.layers; 47 | this.stack.push(this); 48 | 49 | if ( typeof arg === 'string' ) { 50 | this.type = arg; 51 | } else { 52 | for ( var i in arg ) this[i] = arg[i]; 53 | } 54 | 55 | return this; 56 | 57 | }; 58 | 59 | Martin.Layer.prototype = Object.create(Martin.Object.prototype); 60 | 61 | // Normalize X and Y values 62 | Martin.Layer.prototype.normalizeX = function( val ) { 63 | if ( typeof val === 'string' && val.slice(-1) === '%' ) { 64 | val = this.normalizePercentX( +val.slice(0, -1) ); 65 | } 66 | return val / this.scale.x; 67 | }; 68 | 69 | Martin.Layer.prototype.normalizeY = function( val ) { 70 | if ( typeof val === 'string' && val.slice(-1) === '%' ) { 71 | val = this.normalizePercentY( +val.slice(0, -1) ); 72 | } 73 | return val / this.scale.y; 74 | }; 75 | 76 | Martin.Layer.prototype.normalizePercentX = function( val ) { 77 | return ( val / 100 ) * this.canvas.width; 78 | }; 79 | 80 | Martin.Layer.prototype.normalizePercentY = function( val ) { 81 | return ( val / 100 ) * this.canvas.height; 82 | }; 83 | 84 | // Create a new (top-most) layer and switch to that layer. 85 | Martin.prototype.newLayer = function(arg) { 86 | 87 | var newLayer = new Martin.Layer(this, arg); 88 | 89 | this.currentLayer = newLayer; 90 | 91 | this.autorender(); 92 | 93 | return newLayer; 94 | 95 | }; 96 | 97 | // Switch the context and return the requested later 98 | Martin.prototype.layer = function( num ) { 99 | 100 | this.currentLayer = this.layers[num || 0]; 101 | 102 | return this.layers[num || 0]; 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /js/src/object/object.js: -------------------------------------------------------------------------------- 1 | // shared methods for objects: layers, elements, effects 2 | 3 | Martin.Object = function() {}; 4 | var ObjMethods, 5 | ObjMethod; 6 | 7 | ObjMethods = { 8 | 9 | loop: function(cb, put) { 10 | 11 | var width = this.base.width(), 12 | height = this.base.height(); 13 | 14 | var imageData, pixels, len, 15 | n, x, y, 16 | r, g, b, a, 17 | pixel, 18 | output; 19 | 20 | imageData = this.getImageData(); 21 | 22 | if ( imageData ) { 23 | 24 | pixels = imageData.data; 25 | len = pixels.length; 26 | 27 | for ( var i = 0; i < len; i += 4 ) { 28 | 29 | // xy coordinates 30 | n = i / 4; 31 | x = n % width; 32 | y = Math.floor(n / width); 33 | 34 | // rgba values 35 | r = pixels[i]; 36 | g = pixels[i + 1]; 37 | b = pixels[i + 2]; 38 | a = pixels[i + 3]; 39 | 40 | // pass an object corresponding to the pixel to the callback 41 | pixel = { r: r, g: g, b: b, a: a }; 42 | 43 | // execute the callback within the context of this layer's, uh... context 44 | output = cb.call( this.context, x, y, pixel ); 45 | 46 | // reassign the actual rgba values of the pixel based on the output from the loop 47 | pixels[i] = output.r; 48 | pixels[i + 1] = output.g; 49 | pixels[i + 2] = output.b; 50 | pixels[i + 3] = output.a; 51 | 52 | } 53 | 54 | // explicitly declare if image data from callback is not to be used 55 | if ( put !== false ) this.putImageData( imageData ); 56 | 57 | } 58 | 59 | return this; 60 | }, 61 | 62 | getImageData: function() { 63 | var imageData = this.context && this.canvas.width > 0 && this.canvas.height > 0 ? 64 | this.context.getImageData(0, 0, this.canvas.width, this.canvas.height) : 65 | null; 66 | return imageData; 67 | }, 68 | 69 | // Simple shell for putting image data 70 | putImageData: function(imageData) { 71 | this.context.putImageData( imageData, 0, 0 ); 72 | return this; 73 | }, 74 | 75 | clear: function clear() { 76 | this.context.clearRect(0, 0, this.base.width(), this.base.height()); 77 | return this; 78 | }, 79 | 80 | stackIndex: function() { 81 | return this.stack.indexOf(this); 82 | }, 83 | 84 | remove: function() { 85 | this.stack.splice(this.stackIndex(), 1); 86 | this.base.autorender(); 87 | return this; 88 | }, 89 | 90 | bump: function(i) { 91 | var index = this.stackIndex(); 92 | this.remove(); 93 | this.stack.splice(index + i, 0, this); 94 | this.base.autorender(); 95 | return this; 96 | }, 97 | 98 | bumpUp: function() { 99 | return this.bump(1); 100 | }, 101 | 102 | bumpDown: function() { 103 | return this.bump(-1); 104 | }, 105 | 106 | bumpToTop: function() { 107 | this.remove(); 108 | this.stack.push(this); 109 | this.base.autorender(); 110 | return this; 111 | }, 112 | 113 | bumpToBottom: function() { 114 | this.remove(); 115 | this.stack.unshift(this); 116 | this.base.autorender(); 117 | return this; 118 | }, 119 | }; 120 | 121 | for ( ObjMethod in ObjMethods ) { 122 | Martin.Object.prototype[ObjMethod] = ObjMethods[ObjMethod]; 123 | } 124 | -------------------------------------------------------------------------------- /js/src/plugins/gradientmap.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('gradientMap', function(data) { 2 | 3 | var min = parseHex(data.start), 4 | max = parseHex(data.end); 5 | 6 | function parseHex(hex) { 7 | 8 | var output; 9 | 10 | if ( hex.charAt(0) === '#' ) hex = hex.slice(1); 11 | 12 | // coerce to six-digit hex if only 3 given 13 | if ( hex.length === 3 ) { 14 | hex = hex.split(''); 15 | hex.splice(2, 0, hex[2]); 16 | hex.splice(1, 0, hex[1]); 17 | hex.splice(0, 0, hex[0]); 18 | hex = hex.join(''); 19 | } 20 | 21 | output = { 22 | r: parseInt(hex[0] + hex[1], 16), 23 | g: parseInt(hex[2] + hex[3], 16), 24 | b: parseInt(hex[4] + hex[5], 16) 25 | }; 26 | 27 | return output; 28 | } 29 | 30 | this.context.loop(function(x, y, pixel) { 31 | pixel.r = Math.round(min.r + (pixel.r / 256) * (max.r - min.r)); 32 | pixel.g = Math.round(min.g + (pixel.g / 256) * (max.g - min.g)); 33 | pixel.b = Math.round(min.b + (pixel.b / 256) * (max.b - min.b)); 34 | return pixel; 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /js/src/plugins/tile.js: -------------------------------------------------------------------------------- 1 | Martin.registerEffect('tile', function(px) { 2 | 3 | var r, g, b, a, 4 | imageData = this.context.getImageData(), 5 | pixels = imageData.data; 6 | 7 | px = parseInt(px, 10); 8 | 9 | if ( px > 1 ) { 10 | 11 | this.context.loop(function(x, y, pixel) { 12 | 13 | x -= x % px; 14 | y -= y % px; 15 | 16 | var target = 4 * (x + canvas.width() * y); 17 | 18 | pixel.r = pixels[target]; 19 | pixel.g = pixels[target + 1]; 20 | pixel.b = pixels[target + 2]; 21 | pixel.a = pixels[target + 3]; 22 | 23 | return pixel; 24 | }); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /js/src/plugins/watermark.js: -------------------------------------------------------------------------------- 1 | Martin.registerElement('watermark', function(data) { 2 | 3 | 4 | var padding = 2, 5 | size = data.size || this.data.size || 12; 6 | 7 | data = { 8 | text: data.text || '\u00A9', // default to the copyright symbol 9 | align: data.align || 'right', 10 | color: data.color || '#fff', 11 | x: data.x || this.base.width() - padding, 12 | y: data.y || this.base.height() - size - padding, 13 | size: size 14 | }; 15 | 16 | this.data = data; 17 | 18 | if ( !this._textElement ) { 19 | this._textElement = this.layer.text(data); 20 | } else { 21 | this._textElement.update(data); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /js/src/start.js: -------------------------------------------------------------------------------- 1 | /* 2 | Martin.js: In-browser photo and image editing 3 | Author: Scott Donaldson 4 | Contact: scott.p.donaldson@gmail.com 5 | Twitter: @scottpdonaldson 6 | 7 | ---------------------------------------- 8 | 9 | MARTIN 10 | */ 11 | 12 | (function() { 13 | 14 | // The great initializer. Pass in a string to select element by ID, 15 | // or an HTMLElement 16 | function Martin( val, options ) { 17 | 18 | if ( !(this instanceof Martin) ) return new Martin( val, options ); 19 | 20 | // Set the original element, if there is one 21 | this.original = null; 22 | if ( typeof val === 'string' ) { 23 | this.original = document.getElementById(val); 24 | } else if ( val instanceof HTMLElement ) { 25 | this.original = val; 26 | } 27 | 28 | this.options = options || {}; 29 | 30 | // Now prepare yourself... 31 | return this.makeCanvas(); 32 | 33 | }; 34 | 35 | Martin.utils = {}; 36 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Martin.js 2 | pages: 3 | - [index.md, Home] 4 | - [overview.md, Overview] 5 | - [utilities.md, Utilities] 6 | - [layers.md, Layers] 7 | - [elements.md, Elements] 8 | - [effects.md, Effects] 9 | - [events.md, Events] 10 | - [plugins.md, Plugins] 11 | - [demos.md, Demos] 12 | theme: readthedocs 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "martinjs", 3 | "version": "1.0.0", 4 | "description": "Martin.js is a browser-based library for photo manipulation and drawing with HTML5 canvas.", 5 | "repository": "https://github.com/scottdonaldson/martin", 6 | "devDependencies": { 7 | "autoprefixer-core": "^5.2.0", 8 | "browser-sync": "^2.7.6", 9 | "express": "^4.13.3", 10 | "fs": "0.0.2", 11 | "gm": "^1.21.1", 12 | "gulp": "^3.9.0", 13 | "gulp-awspublish": "^2.0.2", 14 | "gulp-concat": "^2.5.2", 15 | "gulp-jslint": "^0.2.2", 16 | "gulp-rename": "^1.2.2", 17 | "gulp-shell": "^0.4.1", 18 | "gulp-sourcemaps": "^1.5.2", 19 | "gulp-uglify": "^1.2.0", 20 | "jasmine": "^2.3.1", 21 | "path": "^0.12.7" 22 | }, 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | path = require('path'), 3 | PORT = process.env.PORT || 8000, 4 | imgUrl = 'http://lorempixel.com/800/600/animals/', 5 | gm = require('gm'), 6 | app = express(); 7 | 8 | app.set('views', path.join(__dirname, 'views')); 9 | app.set('view engine', 'ejs'); 10 | 11 | app.get('/', function(req, res) { 12 | var img = gm(imgUrl).stream(function(err, stdout) { 13 | stdout.pipe(res); 14 | }); 15 | }); 16 | 17 | app.use(function(req, res) { 18 | res.status(404).send('Page not found'); 19 | }); 20 | 21 | module.exports = { 22 | start: function() { 23 | app.listen(PORT); 24 | console.log('Started server on port', PORT); 25 | } 26 | }; -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unit Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Initialize an empty instance of Martin. 4 | * 5 | */ 6 | 7 | describe('Initializing an empty instance of Martin', function() { 8 | 9 | var canvas = Martin(), 10 | baseLayer = canvas.layers[0]; 11 | 12 | it('sets key canvas: HTML node canvas', function() { 13 | expect(canvas.canvas.tagName).toBe('CANVAS'); 14 | }); 15 | 16 | it('sets key context: CanvasRenderingContext2D', function() { 17 | expect(canvas.context instanceof CanvasRenderingContext2D).toBe(true); 18 | }); 19 | 20 | it('sets a base layer', function() { 21 | expect(canvas.layers.length).toBe(1); 22 | }); 23 | 24 | it('...ith its own key canvas: HTML node canvas', function() { 25 | expect(baseLayer.canvas.tagName).toBe('CANVAS'); 26 | }); 27 | 28 | it('..with its own key context: CanvasRenderingContext2D', function() { 29 | expect(baseLayer.context instanceof CanvasRenderingContext2D).toBe(true); 30 | }); 31 | 32 | }); 33 | 34 | /* 35 | * 36 | * Initialize an instance of Martin from an image. 37 | * 38 | */ 39 | 40 | describe('Initializing an instance of Martin from an image', function() { 41 | 42 | // get image 43 | var img = document.createElement('img'); 44 | img.src = '../images/humphrey.jpg'; 45 | img.style.display = 'none'; 46 | document.body.appendChild(img); 47 | 48 | // create instance of Martin from image 49 | var canvas = Martin(img); 50 | canvas.canvas.style.display = 'none'; 51 | 52 | var baseLayer = canvas.layers[0]; 53 | 54 | it('sets key canvas: HTML node canvas', function() { 55 | expect(canvas.canvas.tagName).toBe('CANVAS'); 56 | }); 57 | 58 | it('sets a canvas width', function() { 59 | expect(canvas.canvas.width).toBeGreaterThan(0); 60 | }); 61 | 62 | it('sets a canvas height', function() { 63 | expect(canvas.canvas.height).toBeGreaterThan(0); 64 | }); 65 | 66 | it('sets key context: CanvasRenderingContext2D', function() { 67 | expect(canvas.context instanceof CanvasRenderingContext2D).toBe(true); 68 | }); 69 | 70 | it('sets a base layer', function() { 71 | expect(canvas.layers.length).toBe(1); 72 | }); 73 | 74 | it('..with its own key canvas: HTML node canvas', function() { 75 | expect(baseLayer.canvas.tagName).toBe('CANVAS'); 76 | }); 77 | 78 | it('..with its own key context: CanvasRenderingContext2D', function() { 79 | expect(baseLayer.context instanceof CanvasRenderingContext2D).toBe(true); 80 | }); 81 | 82 | it('..with a single element', function() { 83 | expect(baseLayer.elements.length).toBe(1); 84 | expect(baseLayer.elements[0] instanceof Martin.Element).toBe(true); 85 | }); 86 | 87 | it('....whose type is image', function() { 88 | expect(baseLayer.elements[0].type).toBe('image'); 89 | }); 90 | 91 | }); 92 | 93 | /* 94 | * 95 | * Initialize an instance of Martin from a canvas. 96 | * 97 | */ 98 | 99 | describe('Initializing an instance of Martin from a 400x200 canvas', function() { 100 | 101 | // get image 102 | var canvas = document.createElement('canvas'); 103 | canvas.width = 400; 104 | canvas.height = 200; 105 | document.body.appendChild(canvas); 106 | 107 | // create instance of Martin from canvas 108 | canvas = Martin(canvas); 109 | canvas.canvas.style.display = 'none'; 110 | 111 | var baseLayer = canvas.layers[0]; 112 | 113 | it('sets key canvas: HTML node canvas', function() { 114 | expect(canvas.canvas.tagName).toBe('CANVAS'); 115 | }); 116 | 117 | it('sets a canvas width', function() { 118 | expect(canvas.canvas.width).toBeGreaterThan(0); 119 | }); 120 | 121 | it('sets a canvas height', function() { 122 | expect(canvas.canvas.height).toBeGreaterThan(0); 123 | }); 124 | 125 | it('sets key context: CanvasRenderingContext2D', function() { 126 | expect(canvas.context instanceof CanvasRenderingContext2D).toBe(true); 127 | }); 128 | 129 | it('sets a base layer', function() { 130 | expect(canvas.layers.length).toBe(1); 131 | }); 132 | 133 | it('..with its own key canvas: HTML node canvas', function() { 134 | expect(baseLayer.canvas.tagName).toBe('CANVAS'); 135 | }); 136 | 137 | it('..with its own key context: CanvasRenderingContext2D', function() { 138 | expect(baseLayer.context instanceof CanvasRenderingContext2D).toBe(true); 139 | }); 140 | 141 | }); 142 | --------------------------------------------------------------------------------