├── .gitignore ├── LICENSE ├── README.md ├── angular-cropper.js ├── bower.json ├── package.json └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | bower_components 28 | 29 | *.swp 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tim Whitbeck 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 | angular-cropper 2 | =============== 3 | 4 | > Touch-enabled image cropper for AngularJS 5 | 6 | [Demo](http://plnkr.co/edit/s96ZCYmP5ELtqdIMieKC?p=preview) 7 | 8 | ### Get it 9 | ```sh 10 | $ bower install angular-cropper 11 | ``` 12 | 13 | or 14 | 15 | ```sh 16 | $ npm install angular-cropper 17 | ``` 18 | 19 | ### Use it 20 | * Include files in your html 21 | * `bower_components/angular-file-reader/angular-file-reader.js` 22 | * `bower_components/angular-cropper/angular-cropper.js` 23 | * Include `tw.directives.cropper` in your module dependencies 24 | * `angular.module('myApp', ['tw.directives.cropper']);` 25 | * Place the `tw-cropper` attribute on a canvas 26 | * `` 27 | * Give your cropper a `source` 28 | * `` 29 | * `source` must be a File 30 | * See the demo for one way to get a File to your scope 31 | * Or use [twhitbeck/angular-file-input](https://github.com/twhitbeck/angular-file-input) 32 | * Get the dataURL of your crop with controller `toDataURL` method 33 | * `` 34 | * That's it! 35 | -------------------------------------------------------------------------------- /angular-cropper.js: -------------------------------------------------------------------------------- 1 | angular.module('tw.directives.cropper', ['tw.services.fileReader']); 2 | 3 | angular.module('tw.directives.cropper').directive('twCropper', ['$parse', '$window', '$document', 'twFileReader', function($parse, $window, $document, twFileReader) { 4 | var document = $document[0], 5 | Math = $window.Math; 6 | 7 | return { 8 | restrict: 'A', 9 | controller: ['$scope', '$attrs', '$element', function($scope, $attrs, $element) { 10 | var canvas = $element[0]; 11 | 12 | // If twCropper attribute is provided 13 | if ($attrs.twCropper) { 14 | // Publish this controller to the scope via the expression 15 | $parse($attrs.twCropper).assign($scope, this); 16 | } 17 | 18 | this.toDataURL = function toDataURL() { 19 | return canvas.toDataURL(); 20 | }; 21 | }], 22 | link: function(scope, el, attrs) { 23 | if (angular.lowercase(el[0].nodeName) !== 'canvas') { 24 | return; 25 | } 26 | 27 | var canvas = el[0]; 28 | var ctx = canvas.getContext('2d'); 29 | var img = new Image(); 30 | var x, y, scale, maxScale, minScale; 31 | 32 | var draw = function draw() { 33 | var sx = x, 34 | sy = y, 35 | sWidth = canvas.width * scale, 36 | sHeight = canvas.height * scale, 37 | dx = 0, 38 | dy = 0, 39 | dWidth = canvas.width, 40 | dHeight = canvas.height; 41 | 42 | ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); 43 | }; 44 | 45 | var zoom = function zoom(dScale) { 46 | var s = scale; 47 | 48 | scale += dScale; 49 | 50 | if (scale < minScale) { 51 | scale = minScale; 52 | } else if (scale > maxScale) { 53 | scale = maxScale; 54 | } 55 | 56 | var newWidth = scale * canvas.width; 57 | var newHeight = scale * canvas.height; 58 | var oldWidth = s * canvas.width; 59 | var oldHeight = s * canvas.height; 60 | 61 | var dWidth = newWidth - oldWidth; 62 | var dHeight = newHeight - oldHeight; 63 | 64 | x -= dWidth / 2; 65 | y -= dHeight / 2; 66 | 67 | if (x < 0) { 68 | x = 0; 69 | } else { 70 | if (x + newWidth > img.width) { 71 | x = img.width - newWidth; 72 | } 73 | } 74 | 75 | if (y < 0) { 76 | y = 0; 77 | } else { 78 | if (y + newHeight > img.height) { 79 | y = img.height - newHeight; 80 | } 81 | } 82 | }; 83 | 84 | scope.$watch(attrs.source, function(newVal) { 85 | if (!newVal) { 86 | ctx.clearRect(0, 0, canvas.width, canvas.height); 87 | 88 | return; 89 | } 90 | 91 | twFileReader.readAsDataURL(newVal).then(function(dataURL) { 92 | img.onload = function() { 93 | x = 0; 94 | y = 0; 95 | scale = minScale = 1; 96 | 97 | if (img.width > img.height) { 98 | maxScale = img.height / canvas.height; 99 | } else { 100 | maxScale = img.width / canvas.width; 101 | 102 | } 103 | 104 | if (img.height < canvas.height) { 105 | scale = minScale = maxScale; 106 | } 107 | 108 | if (img.width < canvas.width) { 109 | scale = minScale = maxScale; 110 | } 111 | 112 | draw(); 113 | }; 114 | 115 | img.src = dataURL; 116 | }); 117 | }); 118 | 119 | var sx, sy; 120 | var move = function move(newX, newY) { 121 | x += (sx - newX) * scale; 122 | y += (sy - newY) * scale; 123 | 124 | if (x < 0) { 125 | x = 0; 126 | } else { 127 | var scaledWidth = canvas.width * scale; 128 | 129 | if (x + scaledWidth > img.width) { 130 | x = img.width - scaledWidth; 131 | } 132 | } 133 | 134 | if (y < 0) { 135 | y = 0; 136 | } else { 137 | var scaledHeight = canvas.height * scale; 138 | 139 | if (y + scaledHeight > img.height) { 140 | y = img.height - scaledHeight; 141 | } 142 | } 143 | 144 | draw(); 145 | 146 | sx = newX; 147 | sy = newY; 148 | }; 149 | 150 | var mousemove = function mousemove(e) { 151 | move(e.clientX, e.clientY); 152 | }; 153 | 154 | var d = null; 155 | var pinch = function pinch(touch1, touch2) { 156 | var x1 = touch1.clientX; 157 | var y1 = touch1.clientY; 158 | var x2 = touch2.clientX; 159 | var y2 = touch2.clientY; 160 | 161 | var newD = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2)); 162 | 163 | if (d !== null) { 164 | var dx = newD - d; 165 | 166 | zoom(dx * -.015); 167 | 168 | draw(); 169 | } 170 | 171 | d = newD; 172 | }; 173 | 174 | var touchmove = function touchmove(e) { 175 | // jQuery doesn't copy touches over to its event, so we need to go through originalEvent in that case 176 | e = e.originalEvent || e; 177 | 178 | if (e.touches.length === 1) { 179 | move(e.touches[0].clientX, e.touches[0].clientY); 180 | } else if (e.touches.length === 2) { 181 | pinch(e.touches[0], e.touches[1]); 182 | } 183 | }; 184 | 185 | var start = function(x, y) { 186 | if (!img.src) { 187 | return; 188 | } 189 | 190 | sx = x; 191 | sy = y; 192 | 193 | $document.on('mousemove', mousemove); 194 | $document.on('touchmove', touchmove); 195 | }; 196 | 197 | var mousedown = function mousedown(e) { 198 | start(e.clientX, e.clientY); 199 | }; 200 | 201 | var touchstart = function touchstart(e) { 202 | // jQuery doesn't copy touches over to its event, so we need to go through originalEvent in that case 203 | e = e.originalEvent || e; 204 | 205 | e.preventDefault(); 206 | 207 | if (e.touches.length === 1) { 208 | start(e.touches[0].clientX, e.touches[0].clientY); 209 | } 210 | }; 211 | 212 | el.on('mousedown', mousedown); 213 | el.on('touchstart', touchstart); 214 | 215 | var end = function end() { 216 | $document.off('mousemove', mousemove); 217 | $document.off('touchmove', touchmove); 218 | 219 | d = null; 220 | }; 221 | 222 | $document.on('mouseup touchend', end); 223 | 224 | el.on('wheel', function(e) { 225 | e.preventDefault(); 226 | 227 | e = e.originalEvent || e; 228 | 229 | if (!img.src) { 230 | return; 231 | } 232 | 233 | if (e.deltaY < 0) { 234 | zoom(-.15); 235 | } else { 236 | zoom(.15); 237 | } 238 | 239 | draw(); 240 | }); 241 | } 242 | }; 243 | }]); 244 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cropper", 3 | "main": "angular-cropper.js", 4 | "version": "0.8.4", 5 | "homepage": "https://github.com/twhitbeck/angular-cropper", 6 | "authors": [ 7 | "Tim Whitbeck " 8 | ], 9 | "description": "Touch-enabled, client side image cropping with AngularJS", 10 | "keywords": [ 11 | "angular", 12 | "angularjs", 13 | "image", 14 | "crop", 15 | "cropping", 16 | "cropper", 17 | "directive" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests" 26 | ], 27 | "dependencies": { 28 | "angular": "^1.3", 29 | "angular-file-reader": "~0.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cropper", 3 | "version": "0.9.0", 4 | "description": "A simple image cropper angular.js (1.x) directive", 5 | "main": "angular-cropper.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/twhitbeck/angular-cropper.git" 9 | }, 10 | "keywords": [ 11 | "angular", 12 | "angular.js", 13 | "angularjs", 14 | "crop", 15 | "image", 16 | "mobile", 17 | "cropper" 18 | ], 19 | "author": "twhitbeck", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/twhitbeck/angular-cropper/issues" 23 | }, 24 | "homepage": "https://github.com/twhitbeck/angular-cropper#readme", 25 | "dependencies": { 26 | "angular-file-reader": "^1.0.0" 27 | }, 28 | "peerDependencies": { 29 | "angular": "^1.4.8" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 46 | 47 | 48 | --------------------------------------------------------------------------------