├── .bowerrc
├── .gitignore
├── README.md
├── bower.json
├── dist
├── jr-crop.css
├── jr-crop.js
└── jr-crop.min.js
├── example.jpg
├── examples
├── app.js
├── images
│ ├── circle-mask.svg
│ ├── kitten_1.jpeg
│ ├── kitten_2.jpeg
│ ├── kitten_3.jpeg
│ ├── kitten_4.jpeg
│ └── kitten_5.jpeg
├── index.html
└── uploads
│ └── .gitempty
├── gulpfile.js
├── package.json
└── src
├── jr-crop.js
└── jr-crop.scss
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .sass-cache
3 | node_modules
4 | bower
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | jr-crop
2 | ======
3 |
4 | A simple ionic plugin to crop your images, inspired by whatsapp and telegram.
5 | * Specifiy width and height of target
6 | * Doesn't actually scale the image, only returns a cropped version. Since the quality of images while scaling is inconsistent it's up to the developper to implement this, preferably on the server.
7 | * Returns a canvas element with the new cropped image.
8 |
9 | 
10 |
11 | ## Simple enough, let's get started.
12 |
13 | Install the files: `bower install jr-crop --save`.
14 |
15 | Import the static files jr-crop.js and jr-crop.css. Declare jrCrop as a dependency.
16 | ```
17 | .module('myModule', ['ionic', 'jrCrop'])
18 | ```
19 | Inject jr-crop.
20 | ```
21 | .controller('MyController', function($jrCrop) {
22 | ```
23 |
24 | Call the crop function to open a new modal where the user can crop this image. Pass in the image url and targetsize. The function will return a promise that resolves when the user is done or fails when the user cancels.
25 | ```
26 | $jrCrop.crop({
27 | url: url,
28 | width: 200,
29 | height: 200
30 | }).then(function(canvas) {
31 | // success!
32 | var image = canvas.toDataURL();
33 | }, function() {
34 | // User canceled or couldn't load image.
35 | });
36 | ```
37 |
38 | ##### Changing the title
39 | Additionally you can add a title in the footer.
40 | ```
41 | $jrCrop.crop({
42 | url: url,
43 | width: 200,
44 | height: 200,
45 | title: 'Move and Scale'
46 | });
47 | ```
48 |
49 | ##### Circle mask
50 | Add circle:true to the options to overlay the image with a circle. Note: it won't actually crop the image with a circle, just the visual representation.
51 | ```
52 | $jrCrop.crop({
53 | url: url,
54 | circle: true
55 | });
56 | ```
57 |
58 | ##### Changing default options.
59 | Overwriting default options can be done as well.
60 | `$jrCrop.defaultOptions.template = '
...
';`
61 | `$jrCrop.defaultOptions.width = 300;`
62 | `$jrCrop.defaultOptions.circle = true;`
63 |
64 | #### Templates
65 |
66 | ##### Custom templates
67 | The template can be overwritten by passing your custom HTML in the options.
68 | ```
69 | $jrCrop.crop({
70 | url: url,
71 | width: 200,
72 | height: 200,
73 | template: '...
'
74 | });
75 | ```
76 |
77 | ##### Interpolation Markup
78 | If you configured the expressions of interpolated strings, you can apply this to the template by replacing the markup with your custom markup.
79 | ```
80 | $jrCrop.defaultOptions.template = $jrCrop.defaultOptions.template
81 | .replace(/{{/g, '<%')
82 | .replace(/}}/g, '%>');
83 | ```
84 |
85 | ## Examples please!!
86 | I got ya. Run `bower install && npm install && npm test` and visit `localhost:8181`. Great, now you can visit this from your phone too. It works best when packaged in cordova, as how you should use ionic anyway.
87 |
88 | ## Support
89 | Though I'm only supporting iOS, I did get reports that it's working well on Android. If it doesn't, feel free to fork and update my codebase. If you just want to leave your thoughts you can reply in the [ionic forum topic](http://forum.ionicframework.com/t/sharing-my-photo-crop-plugin/4961).
90 |
91 | ## Contributing
92 | Open an issue or create a pull request. Please exclude the /dist files from your pull request.
93 |
94 | ## Release History
95 | * 2015-11-13 v1.1.2 Overwrite the template through options. Documentation on defaultOptions.
96 | * 2015-11-12 v1.1.1 Circle mask should not be shown by default.
97 | * 2015-11-12 v1.1.0 Add `circle` option to overlay the image with a circle mask.
98 | * 2015-04-05 v1.0.0 Breaking: jr-crop is now its own module, import it first. Support ionic v1.0.0 release candidate. Setting options will no longer overwrite the default options.
99 | * 2015-01-04 v0.1.1 Customize Cancel and Choose text.
100 | * 2014-12-14 v0.1.0 Release on bower, move from grunt to gulp, version numbering in header. Clean up examples and test server. Place the image in the center on initializing. Add maximum scale option. Hide picture overflow in modal at bigger viewport. Add example pictures as static files rather than from url.
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jr-crop",
3 | "version": "1.1.2",
4 | "homepage": "https://github.com/JrSchild/jr-crop",
5 | "authors": [
6 | "Joram Ruitenschild"
7 | ],
8 | "description": "A simple ionic plugin to crop your images.",
9 | "main": [
10 | "dist/jr-crop.css",
11 | "dist/jr-crop.min.js"
12 | ],
13 | "keywords": [
14 | "jr-crop",
15 | "crop",
16 | "ionic",
17 | "ionic-crop"
18 | ],
19 | "license": "MIT",
20 | "devDependencies": {
21 | "ionic": "1.1.1",
22 | "blueimp-canvas-to-blob": "~2.2.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dist/jr-crop.css:
--------------------------------------------------------------------------------
1 | /**
2 | * jr-crop - A simple ionic plugin to crop your images.
3 | * @version 1.1.2
4 | * @link https://github.com/JrSchild/jr-crop
5 | * @author Joram Ruitenschild
6 | * @license MIT
7 | */
8 |
9 | .jr-crop {
10 | background-color: #000;
11 | overflow: hidden; }
12 |
13 | .jr-crop-center-container {
14 | display: -webkit-box;
15 | display: -webkit-flex;
16 | display: -moz-box;
17 | display: -moz-flex;
18 | display: -ms-flexbox;
19 | display: flex;
20 | -webkit-box-pack: center;
21 | -ms-flex-pack: center;
22 | -webkit-justify-content: center;
23 | -moz-justify-content: center;
24 | justify-content: center;
25 | -webkit-box-align: center;
26 | -ms-flex-align: center;
27 | -webkit-align-items: center;
28 | -moz-align-items: center;
29 | align-items: center;
30 | position: absolute;
31 | width: 100%;
32 | height: 100%; }
33 |
34 | .jr-crop-img {
35 | opacity: 0.6; }
36 |
37 | .bar.jr-crop-footer {
38 | border: none;
39 | background-color: transparent;
40 | background-image: none; }
41 |
42 | .jr-crop-select.jr-crop-select-circle {
43 | -webkit-mask-box-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTAwcHgiIGhlaWdodD0iMTAwcHgiIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMDAgMTAwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+DQo8L3N2Zz4=);
44 | mask-box-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTAwcHgiIGhlaWdodD0iMTAwcHgiIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMDAgMTAwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+DQo8L3N2Zz4=); }
45 |
--------------------------------------------------------------------------------
/dist/jr-crop.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jr-crop - A simple ionic plugin to crop your images.
3 | * @version 1.1.2
4 | * @link https://github.com/JrSchild/jr-crop
5 | * @author Joram Ruitenschild
6 | * @license MIT
7 | */
8 |
9 | angular.module('jrCrop', ['ionic'])
10 |
11 | .factory('$jrCrop', [
12 | '$ionicModal',
13 | '$rootScope',
14 | '$q',
15 | function($ionicModal, $rootScope, $q) {
16 |
17 | var template = '' +
18 | '
' +
21 | '
' +
24 | '' +
29 | '
';
30 |
31 | var jrCropController = ionic.views.View.inherit({
32 |
33 | promise: null,
34 | imgWidth: null,
35 | imgHeight: null,
36 |
37 | // Elements that hold the cropped version and the full
38 | // overlaying image.
39 | imgSelect: null,
40 | imgFull: null,
41 |
42 | // Values exposed by scaling and moving. Needed
43 | // to calculate the result of cropped image
44 | posX: 0,
45 | posY: 0,
46 | scale: 1,
47 |
48 | last_scale: 1,
49 | last_posX: 0,
50 | last_posY: 0,
51 |
52 | initialize: function(options) {
53 | var self = this;
54 |
55 | self.options = options;
56 | self.promise = $q.defer();
57 | self.loadImage().then(function(elem) {
58 | self.imgWidth = elem.naturalWidth;
59 | self.imgHeight = elem.naturalHeight;
60 |
61 | self.options.modal.el.querySelector('.jr-crop-img').appendChild(self.imgSelect = elem.cloneNode());
62 | self.options.modal.el.querySelector('.jr-crop-select').appendChild(self.imgFull = elem.cloneNode());
63 |
64 | self.bindHandlers();
65 | self.initImage();
66 | });
67 |
68 | // options === scope. Expose actions for modal.
69 | self.options.cancel = this.cancel.bind(this);
70 | self.options.crop = this.crop.bind(this);
71 | },
72 |
73 | /**
74 | * Init the image in a center position
75 | */
76 | initImage: function() {
77 | if (this.options.height < this.imgHeight || this.options.width < this.imgWidth) {
78 | var imgAspectRatio = this.imgWidth / this.imgHeight;
79 | var selectAspectRatio = this.options.width / this.options.height;
80 |
81 | if (selectAspectRatio > imgAspectRatio) {
82 | this.scale = this.last_scale = this.options.width / this.imgWidth;
83 | } else {
84 | this.scale = this.last_scale = this.options.height / this.imgHeight;
85 | }
86 | }
87 |
88 | var centerX = (this.imgWidth - this.options.width) / 2;
89 | var centerY = (this.imgHeight - this.options.height) / 2;
90 |
91 | this.posX = this.last_posX = -centerX;
92 | this.posY = this.last_posY = -centerY;
93 |
94 | this.setImageTransform();
95 | },
96 |
97 | cancel: function() {
98 | var self = this;
99 |
100 | self.options.modal.remove().then(function() {
101 | self.promise.reject('canceled');
102 | });
103 | },
104 |
105 | /**
106 | * This is where the magic happens
107 | */
108 | bindHandlers: function() {
109 | var self = this,
110 |
111 | min_pos_x = 0, min_pos_y = 0,
112 | max_pos_x = 0, max_pos_y = 0,
113 | transforming_correctX = 0, transforming_correctY = 0,
114 |
115 | scaleMax = 1, scaleMin,
116 | image_ratio = self.imgWidth / self.imgHeight,
117 | cropper_ratio = self.options.width / self.options.height;
118 |
119 | if (cropper_ratio < image_ratio) {
120 | scaleMin = self.options.height / self.imgHeight;
121 | } else {
122 | scaleMin = self.options.width / self.imgWidth;
123 | }
124 |
125 | function setPosWithinBoundaries() {
126 | calcMaxPos();
127 | if (self.posX > min_pos_x) {
128 | self.posX = min_pos_x;
129 | }
130 | if (self.posX < max_pos_x) {
131 | self.posX = max_pos_x;
132 | }
133 | if (self.posY > min_pos_y) {
134 | self.posY = min_pos_y;
135 | }
136 | if (self.posY < max_pos_y) {
137 | self.posY = max_pos_y;
138 | }
139 | }
140 |
141 | /**
142 | * Calculate the minimum and maximum positions.
143 | * This took some headaches to write, good luck
144 | * figuring this out.
145 | */
146 | function calcMaxPos() {
147 | // Calculate current proportions of the image.
148 | var currWidth = self.scale * self.imgWidth;
149 | var currHeight = self.scale * self.imgHeight;
150 |
151 | // Images are scaled from the center
152 | min_pos_x = (currWidth - self.imgWidth) / 2;
153 | min_pos_y = (currHeight - self.imgHeight) / 2;
154 | max_pos_x = -(currWidth - min_pos_x - self.options.width);
155 | max_pos_y = -(currHeight - min_pos_y - self.options.height);
156 | }
157 |
158 | // Based on: http://stackoverflow.com/questions/18011099/pinch-to-zoom-using-hammer-js
159 | var options = {
160 | prevent_default_directions: ['left','right', 'up', 'down']
161 | };
162 | ionic.onGesture('touch transform drag dragstart dragend', function(e) {
163 | switch (e.type) {
164 | case 'touch':
165 | self.last_scale = self.scale;
166 | break;
167 | case 'drag':
168 | self.posX = self.last_posX + e.gesture.deltaX - transforming_correctX;
169 | self.posY = self.last_posY + e.gesture.deltaY - transforming_correctY;
170 | setPosWithinBoundaries();
171 | break;
172 | case 'transform':
173 | self.scale = Math.max(scaleMin, Math.min(self.last_scale * e.gesture.scale, scaleMax));
174 | setPosWithinBoundaries();
175 | break;
176 | case 'dragend':
177 | self.last_posX = self.posX;
178 | self.last_posY = self.posY;
179 | break;
180 | case 'dragstart':
181 | self.last_scale = self.scale;
182 |
183 | // After scaling, hammerjs needs time to reset the deltaX and deltaY values,
184 | // when the user drags the image before this is done the image will jump.
185 | // This is an attempt to fix that.
186 | if (e.gesture.deltaX > 1 || e.gesture.deltaX < -1) {
187 | transforming_correctX = e.gesture.deltaX;
188 | transforming_correctY = e.gesture.deltaY;
189 | } else {
190 | transforming_correctX = 0;
191 | transforming_correctY = 0;
192 | }
193 | break;
194 | }
195 |
196 | self.setImageTransform();
197 |
198 | }, self.options.modal.el, options);
199 | },
200 |
201 | setImageTransform: function() {
202 | var self = this;
203 |
204 | var transform =
205 | 'translate3d(' + self.posX + 'px,' + self.posY + 'px, 0) ' +
206 | 'scale3d(' + self.scale + ',' + self.scale + ', 1)';
207 |
208 | self.imgSelect.style[ionic.CSS.TRANSFORM] = transform;
209 | self.imgFull.style[ionic.CSS.TRANSFORM] = transform;
210 | },
211 |
212 | /**
213 | * Calculate the new image from the values calculated by
214 | * user input. Return a canvas-object with the image on it.
215 | *
216 | * Note: It doesn't actually downsize the image, it only returns
217 | * a cropped version. Since there's inconsistenties in image-quality
218 | * when downsizing it's up to the developer to implement this. Preferably
219 | * on the server.
220 | */
221 | crop: function() {
222 | var canvas = document.createElement('canvas');
223 | var context = canvas.getContext('2d');
224 |
225 | // Canvas size is original proportions but scaled down.
226 | canvas.width = this.options.width / this.scale;
227 | canvas.height = this.options.height / this.scale;
228 |
229 | // The full proportions
230 | var currWidth = this.imgWidth * this.scale;
231 | var currHeight = this.imgHeight * this.scale;
232 |
233 | // Because the top/left position doesn't take the scale of the image in
234 | // we need to correct this value.
235 | var correctX = (currWidth - this.imgWidth) / 2;
236 | var correctY = (currHeight - this.imgHeight) / 2;
237 |
238 | var sourceX = (this.posX - correctX) / this.scale;
239 | var sourceY = (this.posY - correctY) / this.scale;
240 |
241 | context.drawImage(this.imgFull, sourceX, sourceY);
242 |
243 | this.options.modal.remove();
244 | this.promise.resolve(canvas);
245 | },
246 |
247 | /**
248 | * Load the image and return the element.
249 | * Return Promise that will fail when unable to load image.
250 | */
251 | loadImage: function() {
252 | var promise = $q.defer();
253 |
254 | // Load the image and resolve with the DOM node when done.
255 | angular.element('
')
256 | .bind('load', function(e) {
257 | promise.resolve(this);
258 | })
259 | .bind('error', promise.reject)
260 | .prop('src', this.options.url);
261 |
262 | // Return the promise
263 | return promise.promise;
264 | }
265 | });
266 |
267 | return {
268 | defaultOptions: {
269 | width: 0,
270 | height: 0,
271 | aspectRatio: 0,
272 | cancelText: 'Cancel',
273 | chooseText: 'Choose',
274 | template: template,
275 | circle: false
276 | },
277 |
278 | crop: function(options) {
279 | options = this.initOptions(options);
280 |
281 | var scope = $rootScope.$new(true);
282 |
283 | ionic.extend(scope, options);
284 |
285 | scope.modal = $ionicModal.fromTemplate(options.template, {
286 | scope: scope
287 | });
288 |
289 | // Show modal and initialize cropper.
290 | return scope.modal.show().then(function() {
291 | return (new jrCropController(scope)).promise.promise;
292 | });
293 | },
294 |
295 | initOptions: function(_options) {
296 | var options;
297 |
298 | // Apply default values to options.
299 | options = ionic.extend({}, this.defaultOptions);
300 | options = ionic.extend(options, _options);
301 |
302 | if (options.aspectRatio) {
303 |
304 | if (!options.width && options.height) {
305 | options.width = 200;
306 | }
307 |
308 | if (options.width) {
309 | options.height = options.width / options.aspectRatio;
310 | } else if (options.height) {
311 | options.width = options.height * options.aspectRatio;
312 | }
313 | }
314 |
315 | return options;
316 | }
317 | };
318 | }]);
--------------------------------------------------------------------------------
/dist/jr-crop.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jr-crop - A simple ionic plugin to crop your images.
3 | * @version 1.1.2
4 | * @link https://github.com/JrSchild/jr-crop
5 | * @author Joram Ruitenschild
6 | * @license MIT
7 | */
8 |
9 | angular.module("jrCrop",["ionic"]).factory("$jrCrop",["$ionicModal","$rootScope","$q",function(t,i,e){var s='',o=ionic.views.View.inherit({promise:null,imgWidth:null,imgHeight:null,imgSelect:null,imgFull:null,posX:0,posY:0,scale:1,last_scale:1,last_posX:0,last_posY:0,initialize:function(t){var i=this;i.options=t,i.promise=e.defer(),i.loadImage().then(function(t){i.imgWidth=t.naturalWidth,i.imgHeight=t.naturalHeight,i.options.modal.el.querySelector(".jr-crop-img").appendChild(i.imgSelect=t.cloneNode()),i.options.modal.el.querySelector(".jr-crop-select").appendChild(i.imgFull=t.cloneNode()),i.bindHandlers(),i.initImage()}),i.options.cancel=this.cancel.bind(this),i.options.crop=this.crop.bind(this)},initImage:function(){if(this.options.heightt?this.scale=this.last_scale=this.options.width/this.imgWidth:this.scale=this.last_scale=this.options.height/this.imgHeight}var e=(this.imgWidth-this.options.width)/2,s=(this.imgHeight-this.options.height)/2;this.posX=this.last_posX=-e,this.posY=this.last_posY=-s,this.setImageTransform()},cancel:function(){var t=this;t.options.modal.remove().then(function(){t.promise.reject("canceled")})},bindHandlers:function(){function t(){i(),s.posX>o&&(s.posX=o),s.posXa&&(s.posY=a),s.posYp?s.options.height/s.imgHeight:s.options.width/s.imgWidth;var g={prevent_default_directions:["left","right","up","down"]};ionic.onGesture("touch transform drag dragstart dragend",function(i){switch(i.type){case"touch":s.last_scale=s.scale;break;case"drag":s.posX=s.last_posX+i.gesture.deltaX-l,s.posY=s.last_posY+i.gesture.deltaY-c,t();break;case"transform":s.scale=Math.max(e,Math.min(s.last_scale*i.gesture.scale,r)),t();break;case"dragend":s.last_posX=s.posX,s.last_posY=s.posY;break;case"dragstart":s.last_scale=s.scale,i.gesture.deltaX>1||i.gesture.deltaX<-1?(l=i.gesture.deltaX,c=i.gesture.deltaY):(l=0,c=0)}s.setImageTransform()},s.options.modal.el,g)},setImageTransform:function(){var t=this,i="translate3d("+t.posX+"px,"+t.posY+"px, 0) scale3d("+t.scale+","+t.scale+", 1)";t.imgSelect.style[ionic.CSS.TRANSFORM]=i,t.imgFull.style[ionic.CSS.TRANSFORM]=i},crop:function(){var t=document.createElement("canvas"),i=t.getContext("2d");t.width=this.options.width/this.scale,t.height=this.options.height/this.scale;var e=this.imgWidth*this.scale,s=this.imgHeight*this.scale,o=(e-this.imgWidth)/2,a=(s-this.imgHeight)/2,n=(this.posX-o)/this.scale,h=(this.posY-a)/this.scale;i.drawImage(this.imgFull,n,h),this.options.modal.remove(),this.promise.resolve(t)},loadImage:function(){var t=e.defer();return angular.element("
").bind("load",function(i){t.resolve(this)}).bind("error",t.reject).prop("src",this.options.url),t.promise}});return{defaultOptions:{width:0,height:0,aspectRatio:0,cancelText:"Cancel",chooseText:"Choose",template:s,circle:!1},crop:function(e){e=this.initOptions(e);var s=i.$new(!0);return ionic.extend(s,e),s.modal=t.fromTemplate(e.template,{scope:s}),s.modal.show().then(function(){return new o(s).promise.promise})},initOptions:function(t){var i;return i=ionic.extend({},this.defaultOptions),i=ionic.extend(i,t),i.aspectRatio&&(!i.width&&i.height&&(i.width=200),i.width?i.height=i.width/i.aspectRatio:i.height&&(i.width=i.height*i.aspectRatio)),i}}}]);
--------------------------------------------------------------------------------
/example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/example.jpg
--------------------------------------------------------------------------------
/examples/app.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var express = require('express');
4 | var formidable = require('formidable');
5 | var app = express();
6 |
7 | app.use(express.static('./'));
8 | app.use(express.static('./examples'));
9 |
10 | var types = {
11 | 'image/jpeg': 'jpg',
12 | 'image/png': 'png'
13 | };
14 |
15 | // Upload the file to memory.
16 | app.post('/upload', function (req, res) {
17 |
18 | // Parse the request. Any body-parser can be used for this.
19 | var form = new formidable.IncomingForm();
20 | form.parse(req, function(err, fields, files) {
21 |
22 | // The key was set as 'image' on the formData object in the client.
23 | var file = files.image;
24 |
25 | if (!file) {
26 | return res.status(400).send('No image received');
27 | }
28 |
29 | var extension = types[file.type.toLowerCase()];
30 |
31 | // Make sure the extension is valid.
32 | if (!extension) {
33 | return res.status(400).send('Invalid file type');
34 | }
35 |
36 | // Target path is uploads directory relative to this file.
37 | var target = path.resolve(__dirname, 'uploads', Date.now() + '.' + extension);
38 |
39 | // Copy the file from the tmp folder to the uploads location
40 | fs.createReadStream(file.path)
41 | .pipe(fs.createWriteStream(target));
42 |
43 | res.status(200).send();
44 | });
45 | });
46 |
47 | var server = app.listen(8181, function () {
48 | console.log('Running jr-crop example server. Visit http://localhost:8181');
49 | });
--------------------------------------------------------------------------------
/examples/images/circle-mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/examples/images/kitten_1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/images/kitten_1.jpeg
--------------------------------------------------------------------------------
/examples/images/kitten_2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/images/kitten_2.jpeg
--------------------------------------------------------------------------------
/examples/images/kitten_3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/images/kitten_3.jpeg
--------------------------------------------------------------------------------
/examples/images/kitten_4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/images/kitten_4.jpeg
--------------------------------------------------------------------------------
/examples/images/kitten_5.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/images/kitten_5.jpeg
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | jr-crop demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Ionic Delete/Option Buttons
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
60 |
61 |
107 |
108 |
--------------------------------------------------------------------------------
/examples/uploads/.gitempty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JrSchild/jr-crop/74536d46a2b66ed444b06b1ab3a11affd32edb00/examples/uploads/.gitempty
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var jshint = require('gulp-jshint');
3 | var sass = require('gulp-sass');
4 | var uglify = require('gulp-uglify');
5 | var rename = require('gulp-rename');
6 | var header = require('gulp-header');
7 | var bower = require('./bower.json');
8 |
9 | var banner = [
10 | '/**',
11 | ' * <%= bower.name %> - <%= bower.description %>',
12 | ' * @version <%= bower.version %>',
13 | ' * @link <%= bower.homepage %>',
14 | ' * @author <%= bower.authors.join(", ") %>',
15 | ' * @license <%= bower.license %>',
16 | ' */', '', ''].join('\n');
17 |
18 | gulp.task('lint', function () {
19 | return gulp.src('src/jr-crop.js')
20 | .pipe(jshint())
21 | .pipe(jshint.reporter('default'));
22 | });
23 |
24 | gulp.task('scripts', function () {
25 | return gulp.src('src/jr-crop.js')
26 | .pipe(header(banner, { bower: bower } ))
27 | .pipe(gulp.dest('dist'))
28 | .pipe(rename('jr-crop.min.js'))
29 | .pipe(uglify())
30 | .pipe(header(banner, { bower: bower } ))
31 | .pipe(gulp.dest('dist'));
32 | });
33 |
34 | gulp.task('style', function () {
35 | return gulp.src('src/jr-crop.scss')
36 | .pipe(sass())
37 | .pipe(header(banner, { bower: bower } ))
38 | .pipe(gulp.dest('dist'));
39 | });
40 |
41 | gulp.task('watch', function() {
42 | gulp.watch('src/**/*', ['default']);
43 | });
44 |
45 | gulp.task('default', ['lint', 'scripts', 'style']);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jr-crop",
3 | "version": "1.1.2",
4 | "scripts": {
5 | "test": "node examples/app.js"
6 | },
7 | "devDependencies": {
8 | "express": "^4.13.3",
9 | "formidable": "^1.0.17",
10 | "gulp": "^3.9.0",
11 | "gulp-concat": "^2.6.0",
12 | "gulp-header": "^1.7.1",
13 | "gulp-jshint": "^2.0.0",
14 | "gulp-rename": "^1.2.2",
15 | "gulp-sass": "^2.1.0",
16 | "gulp-uglify": "^1.5.1",
17 | "jshint": "^2.8.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/jr-crop.js:
--------------------------------------------------------------------------------
1 | angular.module('jrCrop', ['ionic'])
2 |
3 | .factory('$jrCrop', [
4 | '$ionicModal',
5 | '$rootScope',
6 | '$q',
7 | function($ionicModal, $rootScope, $q) {
8 |
9 | var template = '' +
10 | '
' +
13 | '
' +
16 | '' +
21 | '
';
22 |
23 | var jrCropController = ionic.views.View.inherit({
24 |
25 | promise: null,
26 | imgWidth: null,
27 | imgHeight: null,
28 |
29 | // Elements that hold the cropped version and the full
30 | // overlaying image.
31 | imgSelect: null,
32 | imgFull: null,
33 |
34 | // Values exposed by scaling and moving. Needed
35 | // to calculate the result of cropped image
36 | posX: 0,
37 | posY: 0,
38 | scale: 1,
39 |
40 | last_scale: 1,
41 | last_posX: 0,
42 | last_posY: 0,
43 |
44 | initialize: function(options) {
45 | var self = this;
46 |
47 | self.options = options;
48 | self.promise = $q.defer();
49 | self.loadImage().then(function(elem) {
50 | self.imgWidth = elem.naturalWidth;
51 | self.imgHeight = elem.naturalHeight;
52 |
53 | self.options.modal.el.querySelector('.jr-crop-img').appendChild(self.imgSelect = elem.cloneNode());
54 | self.options.modal.el.querySelector('.jr-crop-select').appendChild(self.imgFull = elem.cloneNode());
55 |
56 | self.bindHandlers();
57 | self.initImage();
58 | });
59 |
60 | // options === scope. Expose actions for modal.
61 | self.options.cancel = this.cancel.bind(this);
62 | self.options.crop = this.crop.bind(this);
63 | },
64 |
65 | /**
66 | * Init the image in a center position
67 | */
68 | initImage: function() {
69 | if (this.options.height < this.imgHeight || this.options.width < this.imgWidth) {
70 | var imgAspectRatio = this.imgWidth / this.imgHeight;
71 | var selectAspectRatio = this.options.width / this.options.height;
72 |
73 | if (selectAspectRatio > imgAspectRatio) {
74 | this.scale = this.last_scale = this.options.width / this.imgWidth;
75 | } else {
76 | this.scale = this.last_scale = this.options.height / this.imgHeight;
77 | }
78 | }
79 |
80 | var centerX = (this.imgWidth - this.options.width) / 2;
81 | var centerY = (this.imgHeight - this.options.height) / 2;
82 |
83 | this.posX = this.last_posX = -centerX;
84 | this.posY = this.last_posY = -centerY;
85 |
86 | this.setImageTransform();
87 | },
88 |
89 | cancel: function() {
90 | var self = this;
91 |
92 | self.options.modal.remove().then(function() {
93 | self.promise.reject('canceled');
94 | });
95 | },
96 |
97 | /**
98 | * This is where the magic happens
99 | */
100 | bindHandlers: function() {
101 | var self = this,
102 |
103 | min_pos_x = 0, min_pos_y = 0,
104 | max_pos_x = 0, max_pos_y = 0,
105 | transforming_correctX = 0, transforming_correctY = 0,
106 |
107 | scaleMax = 1, scaleMin,
108 | image_ratio = self.imgWidth / self.imgHeight,
109 | cropper_ratio = self.options.width / self.options.height;
110 |
111 | if (cropper_ratio < image_ratio) {
112 | scaleMin = self.options.height / self.imgHeight;
113 | } else {
114 | scaleMin = self.options.width / self.imgWidth;
115 | }
116 |
117 | function setPosWithinBoundaries() {
118 | calcMaxPos();
119 | if (self.posX > min_pos_x) {
120 | self.posX = min_pos_x;
121 | }
122 | if (self.posX < max_pos_x) {
123 | self.posX = max_pos_x;
124 | }
125 | if (self.posY > min_pos_y) {
126 | self.posY = min_pos_y;
127 | }
128 | if (self.posY < max_pos_y) {
129 | self.posY = max_pos_y;
130 | }
131 | }
132 |
133 | /**
134 | * Calculate the minimum and maximum positions.
135 | * This took some headaches to write, good luck
136 | * figuring this out.
137 | */
138 | function calcMaxPos() {
139 | // Calculate current proportions of the image.
140 | var currWidth = self.scale * self.imgWidth;
141 | var currHeight = self.scale * self.imgHeight;
142 |
143 | // Images are scaled from the center
144 | min_pos_x = (currWidth - self.imgWidth) / 2;
145 | min_pos_y = (currHeight - self.imgHeight) / 2;
146 | max_pos_x = -(currWidth - min_pos_x - self.options.width);
147 | max_pos_y = -(currHeight - min_pos_y - self.options.height);
148 | }
149 |
150 | // Based on: http://stackoverflow.com/questions/18011099/pinch-to-zoom-using-hammer-js
151 | var options = {
152 | prevent_default_directions: ['left','right', 'up', 'down']
153 | };
154 | ionic.onGesture('touch transform drag dragstart dragend', function(e) {
155 | switch (e.type) {
156 | case 'touch':
157 | self.last_scale = self.scale;
158 | break;
159 | case 'drag':
160 | self.posX = self.last_posX + e.gesture.deltaX - transforming_correctX;
161 | self.posY = self.last_posY + e.gesture.deltaY - transforming_correctY;
162 | setPosWithinBoundaries();
163 | break;
164 | case 'transform':
165 | self.scale = Math.max(scaleMin, Math.min(self.last_scale * e.gesture.scale, scaleMax));
166 | setPosWithinBoundaries();
167 | break;
168 | case 'dragend':
169 | self.last_posX = self.posX;
170 | self.last_posY = self.posY;
171 | break;
172 | case 'dragstart':
173 | self.last_scale = self.scale;
174 |
175 | // After scaling, hammerjs needs time to reset the deltaX and deltaY values,
176 | // when the user drags the image before this is done the image will jump.
177 | // This is an attempt to fix that.
178 | if (e.gesture.deltaX > 1 || e.gesture.deltaX < -1) {
179 | transforming_correctX = e.gesture.deltaX;
180 | transforming_correctY = e.gesture.deltaY;
181 | } else {
182 | transforming_correctX = 0;
183 | transforming_correctY = 0;
184 | }
185 | break;
186 | }
187 |
188 | self.setImageTransform();
189 |
190 | }, self.options.modal.el, options);
191 | },
192 |
193 | setImageTransform: function() {
194 | var self = this;
195 |
196 | var transform =
197 | 'translate3d(' + self.posX + 'px,' + self.posY + 'px, 0) ' +
198 | 'scale3d(' + self.scale + ',' + self.scale + ', 1)';
199 |
200 | self.imgSelect.style[ionic.CSS.TRANSFORM] = transform;
201 | self.imgFull.style[ionic.CSS.TRANSFORM] = transform;
202 | },
203 |
204 | /**
205 | * Calculate the new image from the values calculated by
206 | * user input. Return a canvas-object with the image on it.
207 | *
208 | * Note: It doesn't actually downsize the image, it only returns
209 | * a cropped version. Since there's inconsistenties in image-quality
210 | * when downsizing it's up to the developer to implement this. Preferably
211 | * on the server.
212 | */
213 | crop: function() {
214 | var canvas = document.createElement('canvas');
215 | var context = canvas.getContext('2d');
216 |
217 | // Canvas size is original proportions but scaled down.
218 | canvas.width = this.options.width / this.scale;
219 | canvas.height = this.options.height / this.scale;
220 |
221 | // The full proportions
222 | var currWidth = this.imgWidth * this.scale;
223 | var currHeight = this.imgHeight * this.scale;
224 |
225 | // Because the top/left position doesn't take the scale of the image in
226 | // we need to correct this value.
227 | var correctX = (currWidth - this.imgWidth) / 2;
228 | var correctY = (currHeight - this.imgHeight) / 2;
229 |
230 | var sourceX = (this.posX - correctX) / this.scale;
231 | var sourceY = (this.posY - correctY) / this.scale;
232 |
233 | context.drawImage(this.imgFull, sourceX, sourceY);
234 |
235 | this.options.modal.remove();
236 | this.promise.resolve(canvas);
237 | },
238 |
239 | /**
240 | * Load the image and return the element.
241 | * Return Promise that will fail when unable to load image.
242 | */
243 | loadImage: function() {
244 | var promise = $q.defer();
245 |
246 | // Load the image and resolve with the DOM node when done.
247 | angular.element('
')
248 | .bind('load', function(e) {
249 | promise.resolve(this);
250 | })
251 | .bind('error', promise.reject)
252 | .prop('src', this.options.url);
253 |
254 | // Return the promise
255 | return promise.promise;
256 | }
257 | });
258 |
259 | return {
260 | defaultOptions: {
261 | width: 0,
262 | height: 0,
263 | aspectRatio: 0,
264 | cancelText: 'Cancel',
265 | chooseText: 'Choose',
266 | template: template,
267 | circle: false
268 | },
269 |
270 | crop: function(options) {
271 | options = this.initOptions(options);
272 |
273 | var scope = $rootScope.$new(true);
274 |
275 | ionic.extend(scope, options);
276 |
277 | scope.modal = $ionicModal.fromTemplate(options.template, {
278 | scope: scope
279 | });
280 |
281 | // Show modal and initialize cropper.
282 | return scope.modal.show().then(function() {
283 | return (new jrCropController(scope)).promise.promise;
284 | });
285 | },
286 |
287 | initOptions: function(_options) {
288 | var options;
289 |
290 | // Apply default values to options.
291 | options = ionic.extend({}, this.defaultOptions);
292 | options = ionic.extend(options, _options);
293 |
294 | if (options.aspectRatio) {
295 |
296 | if (!options.width && options.height) {
297 | options.width = 200;
298 | }
299 |
300 | if (options.width) {
301 | options.height = options.width / options.aspectRatio;
302 | } else if (options.height) {
303 | options.width = options.height * options.aspectRatio;
304 | }
305 | }
306 |
307 | return options;
308 | }
309 | };
310 | }]);
--------------------------------------------------------------------------------
/src/jr-crop.scss:
--------------------------------------------------------------------------------
1 | @import "../bower/ionic/scss/mixins";
2 |
3 | @mixin maskBoxImage($value) {
4 | -webkit-mask-box-image: url(#{$value});
5 | mask-box-image: url(#{$value});
6 | }
7 |
8 | .jr-crop {
9 | background-color: #000;
10 | overflow: hidden;
11 | }
12 |
13 | .jr-crop-center-container {
14 | @include display-flex();
15 | @include justify-content(center);
16 | @include align-items(center);
17 | position: absolute;
18 | width: 100%;
19 | height: 100%;
20 | }
21 |
22 | .jr-crop-img {
23 | opacity: 0.6;
24 | }
25 |
26 | .bar.jr-crop-footer {
27 | border: none;
28 | background-color: transparent;
29 | background-image: none;
30 | }
31 |
32 | .jr-crop-select.jr-crop-select-circle {
33 | @include maskBoxImage("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMTAwcHgiIGhlaWdodD0iMTAwcHgiIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMDAgMTAwIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+DQo8L3N2Zz4=");
34 | }
--------------------------------------------------------------------------------