From ded585b7200ca87f1b09b0a15062b011a6b478a6 Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Sun, 19 Feb 2017 23:58:07 +0100 Subject: [PATCH 01/12] Change test setup --- Gruntfile.coffee | 151 ------------------------------- README.md | 14 ++- bower.json | 17 ---- karma.conf.coffee | 48 ---------- karma.conf.js | 20 ++++ package.json | 66 ++++++++------ src/crop.coffee | 1 + src/events.coffee | 2 + src/srcissors.coffee | 1 + srcissors.js | 22 ++++- srcissors.min.js | 2 +- test/specs/srcissors_spec.coffee | 1 + 12 files changed, 92 insertions(+), 253 deletions(-) delete mode 100644 Gruntfile.coffee delete mode 100644 bower.json delete mode 100644 karma.conf.coffee create mode 100644 karma.conf.js diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index 0389ba8..0000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,151 +0,0 @@ -module.exports = (grunt) -> - - # load all grunt tasks - require('load-grunt-tasks')(grunt) - - grunt.initConfig - - watch: - src: - files: [ - 'src/*.coffee' - 'test/specs/*.coffee' - ] - tasks: ['browserify:tmp', 'browserify:test'] - gruntfile: - files: ['Gruntfile.coffee'] - - connect: - options: - port: 9040 - # Change this to '0.0.0.0' to access the server from outside. - hostname: 'localhost' - livereload: 35749 # Default livereload listening port: 35729 - livereload: - options: - open: true - base: [ - '.tmp' - 'examples' - ] - - clean: - tmp: '.tmp' - - browserify: - options: - browserifyOptions: - extensions: ['.coffee'] - transform: ['coffeeify'] - debug: true - tmp: - files: - '.tmp/srcissors.js' : [ - 'src/srcissors.coffee' - ] - test: - files: - '.tmp/srcissors-test.js' : [ - 'test/specs/*.coffee' - ] - build: - options: - debug: false - files: - 'srcissors.js' : [ - 'src/srcissors.coffee' - ] - - mochaTest: - test: - options: - reporter: 'dot' - compilers: 'coffee-script/register' - require: './test/node/mocha_test.js' - src: [ - 'test/specs/*.coffee' - ] - - karma: - unit: - configFile: 'karma.conf.coffee' - unit_once: - configFile: 'karma.conf.coffee' - browsers: ['PhantomJS'] - singleRun: true - browsers: - configFile: 'karma.conf.coffee' - browsers: ['Chrome', 'Firefox', 'Safari'] - build: - configFile: 'karma.conf.coffee' - browsers: ['Chrome', 'Firefox', 'Safari'] - singleRun: true - - # note: run grunt uglify --verbose to see the file size report - uglify: - options: - report: 'gzip' - dist: - files: - 'srcissors.min.js': [ - 'srcissors.js' - ] - - bump: - options: - files: ['package.json', 'bower.json'] - commitFiles: ['-a'], # '-a' for all files - pushTo: 'origin' - push: true - - shell: - npm: - command: 'npm publish' - - - # Tasks - # ----- - - grunt.registerTask('dev', [ - 'browserify:tmp' - 'connect:livereload' - 'watch' - ]) - - grunt.registerTask('test', [ - 'browserify:test' - 'karma:unit' - ]) - - grunt.registerTask('full-test', [ - 'clean:tmp' - 'browserify:test' - 'karma:build' - ]) - - grunt.registerTask('build', [ - 'full-test' - 'browserify:build' - 'uglify' - ]) - - grunt.registerTask('quickbuild', [ - 'browserify:build' - 'uglify' - ]) - - # Release a new version - # Only do this on the `master` branch. - # - # options: - # release:patch - # release:minor - # release:major - grunt.registerTask 'release', (type) -> - type ?= 'patch' - grunt.task.run('build') - grunt.task.run('bump:' + type) - grunt.task.run('shell:npm') - - - grunt.registerTask('default', ['dev']) diff --git a/README.md b/README.md index 140495c..0d9d8f3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # srcissors Image cropping for responsive images. -(~4kB minified and gzipped) +(~5kB minified and gzipped) ### Js @@ -109,13 +109,19 @@ All the UI elements are styled with CSS. Just start with the CSS from `examples/ ```bash # Watch for changes and fire up a webserver with livereload -grunt dev +npm run start # Run the tests with karma -grunt test +npm run test + +# Run the tests in the major browsers (Chrome,Firefox,Safari,Electron) +npm run test:browsers # Run tests and build scrissors.js and scrissors.min.js -grunt build +npm run build + +# Publish the module to npm +npm run build && npm version minor && git push && npm publish ``` diff --git a/bower.json b/bower.json deleted file mode 100644 index c032bd9..0000000 --- a/bower.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "srcissors", - "version": "1.1.0", - "homepage": "https://github.com/upfrontIO/srcissors", - "description": "Image cropping for responsive images", - "author": "upfront.io", - "license": "LGPL-3.0+", - "ignore": [ - "**/**", - "!README.*", - "!Changelog.*", - "!LICENSE", - "!LGPL", - "!srcissors.*" - ], - "main": "srcissors.js" -} diff --git a/karma.conf.coffee b/karma.conf.coffee deleted file mode 100644 index 8e4d0e6..0000000 --- a/karma.conf.coffee +++ /dev/null @@ -1,48 +0,0 @@ -module.exports = (config) -> - config.set - - # base path, that will be used to resolve files and exclude - basePath: '' - - # testing framework to use (jasmine/mocha/qunit/...) - frameworks: ['mocha', 'sinon-chai'] - - # list of files / patterns to load in the browser - files: [ - 'node_modules/jquery/dist/jquery.js' - '.tmp/srcissors-test.js' - { pattern: './test/**/*', included: false } - ], - - # list of files / patterns to exclude - exclude: [], - - # web server port - port: 8080 - - reporters: ['dots'] - - # level of logging - # possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: config.LOG_INFO - - # enable / disable watching file and executing tests whenever any file changes - autoWatch: true - - # Start these browsers, currently available: - # - Chrome - # - ChromeCanary - # - Firefox - # - Opera - # - Safari (only Mac) - # - PhantomJS - # - IE (only Windows) - browsers: ['PhantomJS'] - - # If browser does not capture in given timeout [ms], kill it - captureTimeout: 20000 - - # Continuous Integration mode - # if true, it capture browsers, run tests and exit - singleRun: false - diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..6ff590f --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,20 @@ +module.exports = function (config) { + config.set({ + frameworks: ['mocha', 'sinon-chai', 'browserify'], + files: ['./test/specs/*.coffee', {pattern: './test/**/*', included: false}], + preprocessors: { + './test/specs/*': ['browserify'] + }, + browserify: { + transform: ['coffeeify'], + extensions: ['.js', '.coffee'] + }, + port: 8080, + reporters: ['dots'], + logLevel: config.LOG_INFO, + browsers: ['Electron'], + captureTimeout: 20000, + autoWatch: true, + singleRun: true + }) +} diff --git a/package.json b/package.json index 0074550..cfd112c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,15 @@ "description": "Image cropping for responsive images", "author": "upfront.io", "license": "LGPL-3.0+", + "scripts": { + "test": "karma start", + "test:watch": "karma start --no-single-run", + "test:browsers": "karma start --browsers Chrome,Firefox,Safari,Electron", + "start": "cd ./examples && budo ../src/srcissors.coffee:srcissors.js --live --dir . -p 8080 -o -- -t coffeeify --extension='.coffee'", + "build": "npm test -s && npm run normal -s && npm run minified -s", + "normal": "browserify ./src/srcissors.coffee -t coffeeify --extension='.coffee' -t browserify-shim > srcissors.js", + "minified": "uglifyjs srcissors.js > srcissors.min.js" + }, "keywords": [ "crop", "image" @@ -15,38 +24,37 @@ }, "browser": "src/srcissors.coffee", "main": "src/srcissors.coffee", + "peerDependencies": { + "jquery": ">=2.1.3" + }, "devDependencies": { - "browserify": "^6.2.0", - "chai": "^1.9.2", - "coffeeify": "^0.7.0", - "grunt": "^0.4.5", - "grunt-browserify": "^3.1.0", - "grunt-bump": "0.0.16", - "grunt-contrib-clean": "^0.6.0", - "grunt-contrib-coffee": "^0.12.0", - "grunt-contrib-concat": "^0.5.0", - "grunt-contrib-connect": "^0.9.0", - "grunt-contrib-copy": "^0.7.0", - "grunt-contrib-uglify": "^0.7.0", - "grunt-contrib-watch": "^0.6.1", - "grunt-karma": "^0.9.0", - "grunt-mocha-test": "^0.12.2", - "grunt-shell": "^1.1.1", - "jquery": "^2.1.3", - "karma": "^0.12.24", - "karma-chrome-launcher": "^0.1.5", - "karma-coffee-preprocessor": "^0.2.1", - "karma-firefox-launcher": "^0.1.3", - "karma-mocha": "^0.1.9", - "karma-phantomjs-launcher": "^0.1.4", - "karma-safari-launcher": "^0.1.1", - "karma-script-launcher": "^0.1.0", - "karma-sinon-chai": "^0.2.0", - "load-grunt-tasks": "^1.0.0", - "mocha": "^2.0.1", - "sinon": "^1.11.1" + "browserify": "^14.1.0", + "browserify-shim": "^3.8.13", + "chai": "^3.5.0", + "coffeeify": "^2.1.0", + "electron": "^1.4.15", + "jquery": "^3.1.1", + "karma": "^1.4.1", + "karma-browserify": "^5.1.1", + "karma-chrome-launcher": "^2.0.0", + "karma-electron": "^5.1.1", + "karma-firefox-launcher": "^1.0.0", + "karma-mocha": "^1.3.0", + "karma-safari-launcher": "^1.0.0", + "karma-sinon-chai": "^1.2.4", + "mocha": "^3.2.0", + "uglify-js": "^2.7.5", + "watchify": "^3.9.0" }, "engines": { "node": ">=0.10.0" + }, + "browserify-shim": { + "jquery": "global:jQuery" + }, + "dependencies": { + "karma-chai": "^0.1.0", + "sinon": "^1.17.7", + "sinon-chai": "^2.8.0" } } diff --git a/src/crop.coffee b/src/crop.coffee index 53fc760..0943e0d 100644 --- a/src/crop.coffee +++ b/src/crop.coffee @@ -1,3 +1,4 @@ +$ = require('jquery') Preview = require('./preview') Events = require('./events') diff --git a/src/events.coffee b/src/events.coffee index 0d6e8f6..69ee77c 100644 --- a/src/events.coffee +++ b/src/events.coffee @@ -1,3 +1,5 @@ +$ = require('jquery') + module.exports = class Events constructor: ({ @parent, @view, horizontal, vertical, actions }) -> diff --git a/src/srcissors.coffee b/src/srcissors.coffee index c2245d1..ae886c9 100644 --- a/src/srcissors.coffee +++ b/src/srcissors.coffee @@ -1,3 +1,4 @@ +$ = require('jquery') Crop = require('./crop') module.exports = window.srcissors = diff --git a/srcissors.js b/srcissors.js index 354d167..383724b 100644 --- a/srcissors.js +++ b/srcissors.js @@ -1,7 +1,10 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oc&&delete this.minResolution,this.minResolution&&(g=this.minResolution/(this.imageHeight*this.imageHeight),(!this.minViewRatio||this.minViewRatiof)&&(this.maxViewRatio=f)),this.calcMaxMinDimensions(),this.fixedWidth&&(d="width"),this.fixedHeight&&(d="height"),this.setViewDimensions({width:this.imageWidth,height:this.imageHeight,keepDimension:d}),this.isReady=!0,this.view.removeClass(this.loadingCssClass),this.isInitialized||null==this.initialCrop?(this.zoomAllOut(),this.center()):this.setCrop(this.initialCrop),this.isInitialized=!0,this.readyEvent.fire(),this.loadEvent.fire()},a.prototype.setCrop=function(a){var b,c,d,e,f,g;return f=a.x,g=a.y,e=a.width,c=a.height,this.isReady?(this.resize({width:e,height:c}),b=this.viewWidth/e,d=this.imageWidth*b,this.zoom({width:d}),this.pan({x:f*b,y:g*b})):void this.on("ready",function(a){return function(){return a.setCrop({x:f,y:g,width:e,height:c})}}(this))},a.prototype.getCrop=function(){var a,b;return b=this.preview.width/this.imageWidth,a={x:this.preview.x/b,y:this.preview.y/b,width:this.viewWidth/b,height:this.viewHeight/b},this.roundCrop(a),this.validateCrop(a),a},a.prototype.roundCrop=function(a){var b,c,d;c=[];for(b in a)d=a[b],c.push(a[b]=Math.round(d));return c},a.prototype.validateCrop=function(a){var b,c,d,e;return d=a.x,e=a.y,c=a.width,b=a.height,d+c>this.imageWidth?a.width=this.imageWidth-d:e+b>this.imageHeight&&(a.height=this.imageHeight-e),a},a.prototype.setRatio=function(a,b){var c,d;return this.isReady?(a=this.enforceValidRatio(a),"height"===b?(c=this.viewHeight,d=c*a):(d=this.viewWidth,c=d/a),this.resizeFocusPoint=this.getFocusPoint(),this.resize({width:d,height:c})):void this.on("ready",function(c){return function(){return c.setRatio(a,b)}}(this))},a.prototype.onPan=function(a){var b,c;return this.isPanning||(this.isPanning=!0,this.arena.addClass(this.panningCssClass),this.outline.addClass(this.outlineCssClass)),b=a.startX-a.dx,c=a.startY-a.dy,this.pan({x:b,y:c})},a.prototype.onPanEnd=function(){return this.isPanning=!1,this.arena.removeClass(this.panningCssClass),this.outline.removeClass(this.outlineCssClass)},a.prototype.onDoubleClick=function(a){var b,c,d,e,f,g,h;return c=a.pageX,d=a.pageY,e=this.view[0].getBoundingClientRect(),b=e.left,f=e.top,g=c-b,h=d-f,this.zoomIn({viewX:g,viewY:h})},a.prototype.onResize=function(a){var b,c,d;return d=a.position,b=a.dx,c=a.dy,this.isResizing||(this.isResizing=!0,this.resizeFocusPoint=this.getFocusPoint()),"top"===d||"bottom"===d?(c=2*c,this.resize({width:this.viewWidth,height:this.viewHeight+c,keepDimension:"height"})):"left"===d||"right"===d?(b=2*b,this.resize({width:this.viewWidth+b,height:this.viewHeight,keepDimension:"width"})):void 0},a.prototype.onResizeEnd=function(){return this.isResizing=!1,this.resizeFocusPoint=void 0},a.prototype.resize=function(a){var b,c,d;return d=a.width,b=a.height,c=a.keepDimension,this.setViewDimensions({width:d,height:b,keepDimension:c}),this.resizeFocusPoint&&(this.resizeFocusPoint.viewX=this.viewWidth/2,this.resizeFocusPoint.viewY=this.viewHeight/2),this.zoom({width:this.preview.width,height:this.preview.height,focusPoint:this.resizeFocusPoint})},a.prototype.setViewDimensions=function(a){var b,c,d,e,f,g,h;return h=a.width,b=a.height,c=a.keepDimension,this.maxArea&&(f=this.enforceMaxArea({width:h,height:b,keepDimension:c}),h=f.width,b=f.height),g=this.enforceViewDimensions({width:h,height:b,keepDimension:c}),h=g.width,b=g.height,this.view.css({width:h,height:b}),this.viewWidth=h,this.viewHeight=b,this.viewRatio=h/b,this.minResolution&&(e=Math.sqrt(this.minResolution*this.viewRatio),d=Math.sqrt(this.minResolution/this.viewRatio),this.maxImageWidth=this.viewWidth/e*this.imageWidth,this.maxImageHeight=this.viewHeight/d*this.imageHeight),this.fireChange()},a.prototype.zoomAllOut=function(){return this.isWidthRestricting()?this.zoom({width:this.viewWidth}):this.zoom({height:this.viewHeight})},a.prototype.zoomIn=function(a){return null==a&&(a={}),this.isWidthRestricting()?a.width=this.preview.width*this.zoomInStep:a.height=this.preview.height*this.zoomInStep,this.zoom(a)},a.prototype.zoomOut=function(a){return null==a&&(a={}),this.isWidthRestricting()?a.width=this.preview.width*this.zoomOutStep:a.height=this.preview.height*this.zoomOutStep,this.zoom(a)},a.prototype.zoom=function(a){var b,c,d,e,f,g;return g=a.width,c=a.height,e=a.viewX,f=a.viewY,b=a.focusPoint,null==b&&(b=this.getFocusPoint({viewX:e,viewY:f})),d=this.enforceZoom({width:g,height:c}),g=d.width,c=d.height,null!=g?(this.preview.setWidth(g),this.fireChange()):null!=c&&(this.preview.setHeight(c),this.fireChange()),this.focus(b)},a.prototype.getFocusPoint=function(a){var b,c,d,e,f,g,h;return d=null!=a?a:{},e=d.viewX,f=d.viewY,null==e&&(e=this.viewWidth/2),null==f&&(f=this.viewHeight/2),g=this.preview.x+e,h=this.preview.y+f,b=g/this.preview.width,c=h/this.preview.height,{percentX:b,percentY:c,viewX:e,viewY:f}},a.prototype.focus=function(a){var b,c,d,e,f,g;return b=a.percentX,c=a.percentY,d=a.viewX,e=a.viewY,f=this.preview.width*b,g=this.preview.height*c,f-=d,g-=e,this.pan({x:f,y:g})},a.prototype.center=function(){var a,b;return a=(this.preview.width-this.viewWidth)/2,b=(this.preview.height-this.viewHeight)/2,this.pan({x:a,y:b})},a.prototype.pan=function(a){return a=this.enforceXy(a),this.preview.pan(a.x,a.y),this.fireChange()},a.prototype.enforceXy=function(a){var b,c;return b=a.x,c=a.y,0>b?b=0:b>this.preview.width-this.viewWidth&&(b=this.preview.width-this.viewWidth),0>c?c=0:c>this.preview.height-this.viewHeight&&(c=this.preview.height-this.viewHeight),{x:b,y:c}},a.prototype.enforceZoom=function(a){var b,c;return c=a.width,b=a.height,null!=c&&this.maxImageWidth&&c>this.maxImageWidth?{width:this.maxImageWidth}:null!=c&&cthis.maxImageHeight?{height:this.maxImageHeight}:null!=b&&bthis.maxWidth||bthis.maxHeight||ethis.maxViewRatio,!c},a.prototype.isValidRatio=function(a){return!(athis.maxViewRatio)},a.prototype.enforceValidRatio=function(a){return athis.maxViewRatio?this.maxViewRatio:a},a.prototype.enforceViewDimensions=function(a){var b,c,d,e,f,g,h,i,j;return j=a.width,b=a.height,c=a.keepDimension,jthis.maxWidth&&(e=this.maxWidth),bthis.maxHeight&&(d=this.maxHeight),c?(e&&(j=e),d&&(b=d),f=j/b,this.isValidRatio(f)||(f=this.enforceValidRatio(f),g=this.getRatioBox({ratio:f,width:j,height:b,keepDimension:c}),j=g.width,b=g.height,(j>this.arenaWidth||b>this.arenaHeight)&&(h=this.centerAlign(this.maxWidth,this.maxHeight,f),j=h.width,b=h.height))):(e||d)&&(f=this.enforceValidRatio(j/b),i=this.centerAlign(this.maxWidth,this.maxHeight,f),j=i.width,b=i.height),{width:j,height:b}},a.prototype.enforceMaxArea=function(a){var b,c,d,e;return e=a.width,b=a.height,c=a.keepDimension,d=e/b,"width"===c?(b=this.maxArea/e,d=e/b):"height"===c?(e=this.maxArea/b,d=e/b):(e=Math.sqrt(this.maxArea*d),b=e/d),this.isValidRatio(d)||(d=this.enforceValidRatio(d),e=Math.sqrt(this.maxArea*d),b=e/d),{width:e,height:b}},a.prototype.isWidthRestricting=function(){return this.viewRatio>=this.imageRatio},a.prototype.getRatioBox=function(a){var b,c,d,e;return d=a.ratio,e=a.width,b=a.height,c=a.keepDimension,"width"===c||null==b?b=e/d:"height"===c||null==e?e=b*d:b=e/d,{width:e,height:b}},a.prototype.centerAlign=function(a,b,c){var d,e,f,g;return a/b>c?(e=b*c,f=(a-e)/2):(d=a/c,g=(b-d)/2),{x:f||0,y:g||0,width:e||a,height:d||b}},a.prototype.min=function(a){var b,c,d,e;for(d=a[0],b=0,c=a.length;c>b;b++)e=a[b],d>e&&(d=e);return d},a.prototype.on=function(a,b){return this[a+"Event"].add(b)},a.prototype.off=function(a,b){return this[a+"Event"].remove(b)},a.prototype.fireChange=function(){return null==this.changeDispatch?this.changeDispatch=setTimeout(function(a){return function(){return a.changeDispatch=void 0,a.changeEvent.fire(a.getCrop())}}(this),0):void 0},a.prototype.debug=function(){var a,b;return b=function(a){return Math.round(10*a)/10},a={arena:b(this.arenaWidth)+"x"+b(this.arenaHeight),view:b(this.viewWidth)+"x"+b(this.viewHeight),image:b(this.imageWidth)+"x"+b(this.imageHeight),preview:b(this.preview.width)+"x"+b(this.preview.height),previewXy:b(this.preview.x)+"x"+b(this.preview.y)},console.log(a),a},a}()},{"./events":2,"./preview":3}],2:[function(a,b,c){var d;b.exports=d=function(){function a(a){var b,c,d;this.parent=a.parent,this.view=a.view,c=a.horizontal,d=a.vertical,b=a.actions,this.doubleClickThreshold=300,b.pan&&this.pan(),b.zoomOnDoubleClick&&this.doubleClick(),b.resize&&this.resizeView({horizontal:b.resizeHorizontal,vertical:b.resizeVertical}),this.preventBrowserDragDrop(),this.responsiveArena()}return a.prototype.pan=function(){var a;return a=$(document),this.view.on("mousedown.srcissors",function(b){return function(c){var d;return d={startX:b.parent.preview.x,startY:b.parent.preview.y},c.preventDefault(),a.on("mousemove.srcissors-pan",function(a){return d.dx=a.pageX-c.pageX,d.dy=a.pageY-c.pageY,b.parent.onPan(d)}).on("mouseup.srcissors-pan",function(){return a.off("mouseup.srcissors-pan"),a.off("mousemove.srcissors-pan"),null!=d.dx?b.parent.onPanEnd():void 0})}}(this))},a.prototype.doubleClick=function(){var a;return a=void 0,this.view.on("mousedown.srcissors",function(b){return function(c){var d;return d=(new Date).getTime(),a&&a>d-b.doubleClickThreshold&&b.parent.onDoubleClick({pageX:c.pageX,pageY:c.pageY}),a=d}}(this))},a.prototype.preventBrowserDragDrop=function(){return this.view.on("dragstart.srcissors",function(){return!1})},a.prototype.resizeView=function(a){var b,c,d,e;return c=a.horizontal,e=a.vertical,b=$("
"),b.addClass("resize-handler"),d=[],c&&(d=d.concat(["right","left"])),e&&(d=d.concat(["top","bottom"])),d.forEach(function(a){return function(c){var d;return d=b.clone(),d.addClass("resize-handler-"+c),d.on("mousedown.srcissors",a.getResizeMouseDown(c)),a.view.append(d)}}(this))},a.prototype.getResizeMouseDown=function(a){var b;return b=$(document),function(c){return function(d){var e,f;return e=d.pageX,f=d.pageY,d.stopPropagation(),b.on("mousemove.srcissors-resize",function(b){var d,g;switch(a){case"top":case"bottom":g=b.pageY-f,"top"===a&&(g=-g),f=b.pageY;break;case"left":case"right":d=b.pageX-e,"left"===a&&(d=-d),e=b.pageX}return c.parent.onResize({position:a,dx:d,dy:g})}).on("mouseup.srcissors-resize",function(){return b.off("mouseup.srcissors-resize"),b.off("mousemove.srcissors-resize"),c.parent.onResizeEnd({position:a})})}}(this)},a.prototype.responsiveArena=function(){},a}()},{}],3:[function(a,b,c){var d;b.exports=d=function(){function a(a){this.onReady=a.onReady,this.img=a.img,this.outline=a.outline,this.x=this.y=0,this.width=this.height=0,this.img.on("load",function(a){return function(){var b,c;return c=a.img.width(),b=a.img.height(),a.ratio=c/b,a.updateImageDimensions({width:c,height:b}),a.onReady({width:a.width,height:a.height}),a.img.show()}}(this))}return a.prototype.setImage=function(a){return this.url=a.url,this.img.attr("src",this.url)},a.prototype.reset=function(){return this.url=void 0,this.x=this.y=0,this.width=this.height=0,this.img.attr("src",""),this.img.css({width:"",height:"",transform:""}),this.outline?this.outline.css({transform:""}):void 0},a.prototype.setWidth=function(a){var b;return this.img.css({width:a+"px",height:"auto"}),b=a/this.ratio,this.updateImageDimensions({width:a,height:b})},a.prototype.setHeight=function(a){var b;return this.img.css({width:"auto",height:a+"px"}),b=a*this.ratio,this.updateImageDimensions({width:b,height:a})},a.prototype.updateImageDimensions=function(a){var b,c;return c=a.width,b=a.height,this.width=c,this.height=b,this.outline?this.outline.css({width:this.width+"px",height:this.height+"px"}):void 0},a.prototype.pan=function(a,b){var c,d;return this.x=a,this.y=b,c=Math.round(this.x),d=Math.round(this.y),this.img.css({transform:"translate(-"+c+"px, -"+d+"px)"}),this.outline?this.outline.css({transform:"translate(-"+c+"px, -"+d+"px)"}):void 0},a}()},{}],4:[function(a,b,c){var d;d=a("./crop"),b.exports=window.srcissors={"new":function(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t;return e=a.arena,r=a.url,h=a.fixedWidth,g=a.fixedHeight,o=a.minWidth,l=a.minHeight,m=a.minRatio,k=a.maxRatio,j=a.maxArea,t=a.zoomStep,f=a.crop,b=a.actions,n=a.minResolution,e=$(e),s=e.find(".crop-view"),q=s.find(".crop-preview"),i=$(""),q.append(i),p=s.find(".crop-outline"),p.length||(p=void 0),c={pan:!0,zoomOnDoubleClick:!0,resize:!0,resizeHorizontal:!h,resizeVertical:!g},$.extend(c,b),null==t&&(t=1.25),null==o&&(o=50),null==l&&(l=50),new d({url:r,crop:f,arena:e,view:s,img:i,outline:p,fixedWidth:h,fixedHeight:g,minViewWidth:o,minViewHeight:l,minViewRatio:m,maxViewRatio:k,maxArea:j,zoomStep:t,actions:c,minResolution:n})}}},{"./crop":1}]},{},[4]); \ No newline at end of file +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oimageResolution){delete this.minResolution}if(this.minResolution){minRatioForResolution=this.minResolution/(this.imageHeight*this.imageHeight);if(!this.minViewRatio||this.minViewRatiomaxRatioForResolution){this.maxViewRatio=maxRatioForResolution}}this.calcMaxMinDimensions();if(this.fixedWidth){keepDimension="width"}if(this.fixedHeight){keepDimension="height"}this.setViewDimensions({width:this.imageWidth,height:this.imageHeight,keepDimension:keepDimension});this.isReady=true;this.view.removeClass(this.loadingCssClass);if(!this.isInitialized&&this.initialCrop!=null){this.setCrop(this.initialCrop)}else{this.zoomAllOut();this.center()}this.isInitialized=true;this.readyEvent.fire();return this.loadEvent.fire()};Crop.prototype.setCrop=function(arg){var factor,height,previewWidth,width,x,y;x=arg.x,y=arg.y,width=arg.width,height=arg.height;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setCrop({x:x,y:y,width:width,height:height})}}(this));return}this.resize({width:width,height:height});factor=this.viewWidth/width;previewWidth=this.imageWidth*factor;this.zoom({width:previewWidth});return this.pan({x:x*factor,y:y*factor})};Crop.prototype.getCrop=function(){var crop,factor;factor=this.preview.width/this.imageWidth;crop={x:this.preview.x/factor,y:this.preview.y/factor,width:this.viewWidth/factor,height:this.viewHeight/factor};this.roundCrop(crop);this.validateCrop(crop);return crop};Crop.prototype.roundCrop=function(crop){var name,results,value;results=[];for(name in crop){value=crop[name];results.push(crop[name]=Math.round(value))}return results};Crop.prototype.validateCrop=function(crop){var height,width,x,y;x=crop.x,y=crop.y,width=crop.width,height=crop.height;if(x+width>this.imageWidth){crop.width=this.imageWidth-x}else if(y+height>this.imageHeight){crop.height=this.imageHeight-y}return crop};Crop.prototype.setRatio=function(ratio,keepDimension){var height,width;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setRatio(ratio,keepDimension)}}(this));return}ratio=this.enforceValidRatio(ratio);if(keepDimension==="height"){height=this.viewHeight;width=height*ratio}else{width=this.viewWidth;height=width/ratio}this.resizeFocusPoint=this.getFocusPoint();return this.resize({width:width,height:height})};Crop.prototype.onPan=function(data){var newX,newY;if(!this.isPanning){this.isPanning=true;this.arena.addClass(this.panningCssClass);this.outline.addClass(this.outlineCssClass)}newX=data.startX-data.dx;newY=data.startY-data.dy;return this.pan({x:newX,y:newY})};Crop.prototype.onPanEnd=function(){this.isPanning=false;this.arena.removeClass(this.panningCssClass);return this.outline.removeClass(this.outlineCssClass)};Crop.prototype.onDoubleClick=function(arg){var left,pageX,pageY,ref,top,viewX,viewY;pageX=arg.pageX,pageY=arg.pageY;ref=this.view[0].getBoundingClientRect(),left=ref.left,top=ref.top;viewX=pageX-left;viewY=pageY-top;return this.zoomIn({viewX:viewX,viewY:viewY})};Crop.prototype.onResize=function(arg){var dx,dy,position;position=arg.position,dx=arg.dx,dy=arg.dy;if(!this.isResizing){this.isResizing=true;this.resizeFocusPoint=this.getFocusPoint()}if(position==="top"||position==="bottom"){dy=2*dy;return this.resize({width:this.viewWidth,height:this.viewHeight+dy,keepDimension:"height"})}else if(position==="left"||position==="right"){dx=2*dx;return this.resize({width:this.viewWidth+dx,height:this.viewHeight,keepDimension:"width"})}};Crop.prototype.onResizeEnd=function(){this.isResizing=false;return this.resizeFocusPoint=void 0};Crop.prototype.resize=function(arg){var height,keepDimension,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;this.setViewDimensions({width:width,height:height,keepDimension:keepDimension});if(this.resizeFocusPoint){this.resizeFocusPoint.viewX=this.viewWidth/2;this.resizeFocusPoint.viewY=this.viewHeight/2}return this.zoom({width:this.preview.width,height:this.preview.height,focusPoint:this.resizeFocusPoint})};Crop.prototype.setViewDimensions=function(arg){var height,keepDimension,minZoomPixelHeight,minZoomPixelWidth,ref,ref1,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(this.maxArea){ref=this.enforceMaxArea({width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height}ref1=this.enforceViewDimensions({width:width,height:height,keepDimension:keepDimension}),width=ref1.width,height=ref1.height;this.view.css({width:width,height:height});this.viewWidth=width;this.viewHeight=height;this.viewRatio=width/height;if(this.minResolution){minZoomPixelWidth=Math.sqrt(this.minResolution*this.viewRatio);minZoomPixelHeight=Math.sqrt(this.minResolution/this.viewRatio);this.maxImageWidth=this.viewWidth/minZoomPixelWidth*this.imageWidth;this.maxImageHeight=this.viewHeight/minZoomPixelHeight*this.imageHeight}return this.fireChange()};Crop.prototype.zoomAllOut=function(){if(this.isWidthRestricting()){return this.zoom({width:this.viewWidth})}else{return this.zoom({height:this.viewHeight})}};Crop.prototype.zoomIn=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomInStep}else{params.height=this.preview.height*this.zoomInStep}return this.zoom(params)};Crop.prototype.zoomOut=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomOutStep}else{params.height=this.preview.height*this.zoomOutStep}return this.zoom(params)};Crop.prototype.zoom=function(arg){var focusPoint,height,ref,viewX,viewY,width;width=arg.width,height=arg.height,viewX=arg.viewX,viewY=arg.viewY,focusPoint=arg.focusPoint;if(focusPoint==null){focusPoint=this.getFocusPoint({viewX:viewX,viewY:viewY})}ref=this.enforceZoom({width:width,height:height}),width=ref.width,height=ref.height;if(width!=null){this.preview.setWidth(width);this.fireChange()}else if(height!=null){this.preview.setHeight(height);this.fireChange()}return this.focus(focusPoint)};Crop.prototype.getFocusPoint=function(arg){var percentX,percentY,ref,viewX,viewY,x,y;ref=arg!=null?arg:{},viewX=ref.viewX,viewY=ref.viewY;if(viewX==null){viewX=this.viewWidth/2}if(viewY==null){viewY=this.viewHeight/2}x=this.preview.x+viewX;y=this.preview.y+viewY;percentX=x/this.preview.width;percentY=y/this.preview.height;return{percentX:percentX,percentY:percentY,viewX:viewX,viewY:viewY}};Crop.prototype.focus=function(arg){var percentX,percentY,viewX,viewY,x,y;percentX=arg.percentX,percentY=arg.percentY,viewX=arg.viewX,viewY=arg.viewY;x=this.preview.width*percentX;y=this.preview.height*percentY;x=x-viewX;y=y-viewY;return this.pan({x:x,y:y})};Crop.prototype.center=function(){var newX,newY;newX=(this.preview.width-this.viewWidth)/2;newY=(this.preview.height-this.viewHeight)/2;return this.pan({x:newX,y:newY})};Crop.prototype.pan=function(data){data=this.enforceXy(data);this.preview.pan(data.x,data.y);return this.fireChange()};Crop.prototype.enforceXy=function(arg){var x,y;x=arg.x,y=arg.y;if(x<0){x=0}else if(x>this.preview.width-this.viewWidth){x=this.preview.width-this.viewWidth}if(y<0){y=0}else if(y>this.preview.height-this.viewHeight){y=this.preview.height-this.viewHeight}return{x:x,y:y}};Crop.prototype.enforceZoom=function(arg){var height,width;width=arg.width,height=arg.height;if(width!=null&&this.maxImageWidth&&width>this.maxImageWidth){return{width:this.maxImageWidth}}if(width!=null&&widththis.maxImageHeight){return{height:this.maxImageHeight}}if(height!=null&&heightthis.maxWidth||heightthis.maxHeight||ratiothis.maxViewRatio;return!invalid};Crop.prototype.isValidRatio=function(ratio){return!(ratiothis.maxViewRatio)};Crop.prototype.enforceValidRatio=function(ratio){if(ratiothis.maxViewRatio){return this.maxViewRatio}return ratio};Crop.prototype.enforceViewDimensions=function(arg){var height,keepDimension,newHeight,newWidth,ratio,ref,ref1,ref2,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(widththis.maxWidth){newWidth=this.maxWidth}if(heightthis.maxHeight){newHeight=this.maxHeight}if(keepDimension){if(newWidth){width=newWidth}if(newHeight){height=newHeight}ratio=width/height;if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);ref=this.getRatioBox({ratio:ratio,width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height;if(width>this.arenaWidth||height>this.arenaHeight){ref1=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref1.width,height=ref1.height}}}else if(newWidth||newHeight){ratio=this.enforceValidRatio(width/height);ref2=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref2.width,height=ref2.height}return{width:width,height:height}};Crop.prototype.enforceMaxArea=function(arg){var height,keepDimension,ratio,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;ratio=width/height;if(keepDimension==="width"){height=this.maxArea/width;ratio=width/height}else if(keepDimension==="height"){width=this.maxArea/height;ratio=width/height}else{width=Math.sqrt(this.maxArea*ratio);height=width/ratio}if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);width=Math.sqrt(this.maxArea*ratio);height=width/ratio}return{width:width,height:height}};Crop.prototype.isWidthRestricting=function(){return this.viewRatio>=this.imageRatio};Crop.prototype.getRatioBox=function(arg){var height,keepDimension,ratio,width;ratio=arg.ratio,width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(keepDimension==="width"||height==null){height=width/ratio}else if(keepDimension==="height"||width==null){width=height*ratio}else{height=width/ratio}return{width:width,height:height}};Crop.prototype.centerAlign=function(areaWidth,areaHeight,ratio){var height,width,x,y;if(areaWidth/areaHeight>ratio){width=areaHeight*ratio;x=(areaWidth-width)/2}else{height=areaWidth/ratio;y=(areaHeight-height)/2}return{x:x||0,y:y||0,width:width||areaWidth,height:height||areaHeight}};Crop.prototype.min=function(array){var i,len,min,number;min=array[0];for(i=0,len=array.length;inow-_this.doubleClickThreshold){_this.parent.onDoubleClick({pageX:event.pageX,pageY:event.pageY})}else{}return lastClick=now}}(this))};Events.prototype.preventBrowserDragDrop=function(){return this.view.on("dragstart.srcissors",function(){return false})};Events.prototype.resizeView=function(arg){var $template,horizontal,positions,vertical;horizontal=arg.horizontal,vertical=arg.vertical;$template=$("
");$template.addClass("resize-handler");positions=[];if(horizontal){positions=positions.concat(["right","left"])}if(vertical){positions=positions.concat(["top","bottom"])}return positions.forEach(function(_this){return function(position){var $handler;$handler=$template.clone();$handler.addClass("resize-handler-"+position);$handler.on("mousedown.srcissors",_this.getResizeMouseDown(position));return _this.view.append($handler)}}(this))};Events.prototype.getResizeMouseDown=function(position){var $doc;$doc=$(document);return function(_this){return function(event){var lastX,lastY;lastX=event.pageX;lastY=event.pageY;event.stopPropagation();return $doc.on("mousemove.srcissors-resize",function(e2){var dx,dy;switch(position){case"top":case"bottom":dy=e2.pageY-lastY;if(position==="top"){dy=-dy}lastY=e2.pageY;break;case"left":case"right":dx=e2.pageX-lastX;if(position==="left"){dx=-dx}lastX=e2.pageX}return _this.parent.onResize({position:position,dx:dx,dy:dy})}).on("mouseup.srcissors-resize",function(){$doc.off("mouseup.srcissors-resize");$doc.off("mousemove.srcissors-resize");return _this.parent.onResizeEnd({position:position})})}}(this)};Events.prototype.responsiveArena=function(){};return Events}()}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],3:[function(require,module,exports){var Preview;module.exports=Preview=function(){function Preview(arg){this.onReady=arg.onReady,this.img=arg.img,this.outline=arg.outline;this.x=this.y=0;this.width=this.height=0;this.img.on("load",function(_this){return function(){var height,width;width=_this.img.width();height=_this.img.height();_this.ratio=width/height;_this.updateImageDimensions({width:width,height:height});_this.onReady({width:_this.width,height:_this.height});return _this.img.show()}}(this))}Preview.prototype.setImage=function(arg){this.url=arg.url;return this.img.attr("src",this.url)};Preview.prototype.reset=function(){this.url=void 0;this.x=this.y=0;this.width=this.height=0;this.img.attr("src","");this.img.css({width:"",height:"",transform:""});if(this.outline){return this.outline.css({transform:""})}};Preview.prototype.setWidth=function(width){var height;this.img.css({width:width+"px",height:"auto"});height=width/this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.setHeight=function(height){var width;this.img.css({width:"auto",height:height+"px"});width=height*this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.updateImageDimensions=function(arg){var height,width;width=arg.width,height=arg.height;this.width=width;this.height=height;if(this.outline){return this.outline.css({width:this.width+"px",height:this.height+"px"})}};Preview.prototype.pan=function(x1,y1){var x,y;this.x=x1;this.y=y1;x=Math.round(this.x);y=Math.round(this.y);this.img.css({transform:"translate(-"+x+"px, -"+y+"px)"});if(this.outline){return this.outline.css({transform:"translate(-"+x+"px, -"+y+"px)"})}};return Preview}()},{}],4:[function(require,module,exports){(function(global){var $,Crop;$=typeof window!=="undefined"?window["jQuery"]:typeof global!=="undefined"?global["jQuery"]:null;Crop=require("./crop");module.exports=window.srcissors={new:function(arg){var actions,allowedActions,arena,crop,fixedHeight,fixedWidth,img,maxArea,maxRatio,minHeight,minRatio,minResolution,minWidth,outline,preview,url,view,zoomStep;arena=arg.arena,url=arg.url,fixedWidth=arg.fixedWidth,fixedHeight=arg.fixedHeight,minWidth=arg.minWidth,minHeight=arg.minHeight,minRatio=arg.minRatio,maxRatio=arg.maxRatio,maxArea=arg.maxArea,zoomStep=arg.zoomStep,crop=arg.crop,actions=arg.actions,minResolution=arg.minResolution;arena=$(arena);view=arena.find(".crop-view");preview=view.find(".crop-preview");img=$("");preview.append(img);outline=view.find(".crop-outline");if(!outline.length){outline=void 0}allowedActions={pan:true,zoomOnDoubleClick:true,resize:true,resizeHorizontal:!fixedWidth,resizeVertical:!fixedHeight};$.extend(allowedActions,actions);if(zoomStep==null){zoomStep=1.25}if(minWidth==null){minWidth=50}if(minHeight==null){minHeight=50}return new Crop({url:url,crop:crop,arena:arena,view:view,img:img,outline:outline,fixedWidth:fixedWidth,fixedHeight:fixedHeight,minViewWidth:minWidth,minViewHeight:minHeight,minViewRatio:minRatio,maxViewRatio:maxRatio,maxArea:maxArea,zoomStep:zoomStep,actions:allowedActions,minResolution:minResolution})}}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./crop":1}]},{},[4]); diff --git a/test/specs/srcissors_spec.coffee b/test/specs/srcissors_spec.coffee index 1d9e0df..3937dac 100644 --- a/test/specs/srcissors_spec.coffee +++ b/test/specs/srcissors_spec.coffee @@ -1,3 +1,4 @@ +$ = require('jquery') srcissors = require('../../src/srcissors') template = """ From 169fb2aea1506c7790794943e1f4acd9bf179e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Immanuel=20H=C3=A4ussermann?= Date: Thu, 4 May 2017 10:21:15 +0200 Subject: [PATCH 02/12] fix: add budo to dev dependencies, required in npm start --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cfd112c..d39056d 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "devDependencies": { "browserify": "^14.1.0", "browserify-shim": "^3.8.13", + "budo": "^10.0.3", "chai": "^3.5.0", "coffeeify": "^2.1.0", "electron": "^1.4.15", From 5f43540ec279e4c15327d1c771b55e8fec734d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Immanuel=20H=C3=A4ussermann?= Date: Thu, 4 May 2017 13:49:53 +0200 Subject: [PATCH 03/12] feat: configurable visibility for surrounding image new options are: showSurroundingImage: 'never|always|panning' # default: never surroundingImageOpacity: 0.2 # default 0.2 --- README.md | 2 + examples/css/srcissors.css | 5 +- examples/index.html | 3 +- src/crop.coffee | 20 ++++++- src/preview.coffee | 13 ++++- src/srcissors.coffee | 5 +- test/specs/srcissors_spec.coffee | 89 ++++++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0d9d8f3..5035a1e 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ cropper.on('change', function(crop) { | `maxArea` | Number | e.g. `0.8` means max 80% of the area will be covered by the image. This makes for smoth transitions between wide and tall image formats. | | `zoomStep` | Number | e.g. `1.25` means every zoom will enlarge the preview by 25%. | | `actions` | Object | {pan, zoomOnDoubleClick, resize } Allowed user interactions. By default they are all set to `true`. | +| `showSurroundingImage` | String | {always, never, panning } Shows the uncropped part of the image. By default set to `never`. | +| `surroundingImageOpacity` | Number | {0.0 - 1.0} Sets the opacity when showing the uncropped part of the image. By default set to `0.2`. | ### HTML diff --git a/examples/css/srcissors.css b/examples/css/srcissors.css index b2ba0d9..b9f138c 100644 --- a/examples/css/srcissors.css +++ b/examples/css/srcissors.css @@ -95,11 +95,14 @@ transition: opacity 0.5s; } +.crop-outline img { + width: 100%; +} + .crop-outline--active { opacity: 1; } - /* Zoom Controls */ .top-left { diff --git a/examples/index.html b/examples/index.html index aafa69b..72d559d 100644 --- a/examples/index.html +++ b/examples/index.html @@ -55,7 +55,8 @@ url: "/images/diagonal.jpg", fixedWidth: 300, minRatio: 1 / 1.5, - maxRatio: 1.5 / 1 + maxRatio: 1.5 / 1, + showSurroundingImage: 'always' }); crop.setCrop({ diff --git a/src/crop.coffee b/src/crop.coffee index 0943e0d..fb46bbd 100644 --- a/src/crop.coffee +++ b/src/crop.coffee @@ -7,7 +7,8 @@ module.exports = class Crop constructor: ({ @arena, @view, @img, @outline, url, @fixedWidth, @fixedHeight, @minViewWidth, @minViewHeight, @minViewRatio, @maxViewRatio, crop - zoomStep, maxArea, @actions, @minResolution + zoomStep, maxArea, @actions, @minResolution, @surroundingImageOpacity, + showSurroundingImage }) -> # CSS classes @@ -38,10 +39,13 @@ module.exports = class Crop # be more reliable. @maxArea = (@arenaWidth * @arenaHeight) * maxArea if maxArea + @setSurroundingImageVisibility(showSurroundingImage) if @outline + @preview = new Preview onReady: @onPreviewReady img: @img outline: @outline + opacity: @surroundingImageOpacity @setImage(url) @@ -60,6 +64,20 @@ module.exports = class Crop @view.addClass(@loadingCssClass) @preview.setImage({ url }) + + setSurroundingImageVisibility: (visibility) -> + # visibility: always|panning|never + # override opacity in crop-outline--active css class + @surroundingImageOpacity = parseFloat(@surroundingImageOpacity || 0.2) + + if visibility == 'always' + @outline.css('opacity', 1.0) + else if visibility == 'panning' + @outline.css('opacity', null) + else # 'never' default + @outline.css('opacity', 0) + @surroundingImageOpacity = 0 + reset: -> return unless @isReady diff --git a/src/preview.coffee b/src/preview.coffee index 228dac9..cb3a00b 100644 --- a/src/preview.coffee +++ b/src/preview.coffee @@ -1,6 +1,8 @@ +$ = require('jquery') + module.exports = class Preview - constructor: ({ @onReady, @img, @outline }) -> + constructor: ({ @onReady, @img, @opacity, @outline }) -> @x = @y = 0 @width = @height = 0 @@ -16,6 +18,13 @@ module.exports = class Preview setImage: ({ @url }) -> @img.attr('src', @url) + @setBackgroundImage({url: @url}) if @outline + + + setBackgroundImage: ({ url }) -> + if @opacity > 0 + bg_img = $('').css(opacity: @opacity).attr('src', url) + @outline.append(bg_img) reset: -> @@ -24,7 +33,7 @@ module.exports = class Preview @width = @height = 0 @img.attr('src', '') @img.css(width: '', height: '', transform: '') - @outline.css(transform: '') if @outline + @outline.css(transform: '').html('') if @outline setWidth: (width) -> diff --git a/src/srcissors.coffee b/src/srcissors.coffee index ae886c9..031893f 100644 --- a/src/srcissors.coffee +++ b/src/srcissors.coffee @@ -5,7 +5,8 @@ module.exports = window.srcissors = new: ({ arena, url, fixedWidth, fixedHeight, minWidth, minHeight, - minRatio, maxRatio, maxArea, zoomStep, crop, actions, minResolution + minRatio, maxRatio, maxArea, zoomStep, crop, actions, minResolution, + surroundingImageOpacity, showSurroundingImage }) -> arena = $(arena) view = arena.find('.crop-view') @@ -36,6 +37,8 @@ module.exports = window.srcissors = view: view # {jQuery Element} img: img # {jQuery Element} outline: outline # {jQuery Element or undefined} + showSurroundingImage: showSurroundingImage # {String} always|panning|never + surroundingImageOpacity: surroundingImageOpacity # {Number} e.g. in the 0.0 - 1.0 range fixedWidth: fixedWidth # {Number} e.g. 300 fixedHeight: fixedHeight # {Number} e.g. 500 minViewWidth: minWidth # {Number} e.g. 100 diff --git a/test/specs/srcissors_spec.coffee b/test/specs/srcissors_spec.coffee index 3937dac..4a158ce 100644 --- a/test/specs/srcissors_spec.coffee +++ b/test/specs/srcissors_spec.coffee @@ -4,6 +4,7 @@ srcissors = require('../../src/srcissors') template = """
+
@@ -200,3 +201,91 @@ describe 'srcissors', -> height: 300 done() + + describe 'with surrounding image always enabled', -> + + beforeEach (done) -> + @arena = $(template) + @arena.css(width: 100, height: 100) + $(document.body).append(@arena) + + # Crop a 400x300 image + @crop = srcissors.new + arena: @arena + url: 'base/test/images/diagonal.jpg' + showSurroundingImage: 'always' + surroundingImageOpacity: 0.4 + @crop.on 'ready', done + + + afterEach -> + @arena.remove() + + + it 'has initialized the crop outline background correctly', -> + outline = @arena.find('.crop-outline') + bgImg = outline.find('img') + + expect(bgImg.length).to.equal(1) + expect(bgImg.get(0).style.opacity).to.equal('0.4') + expect(outline.get(0).style.opacity).to.equal('1') + + + it 'cleans up the crop outline when setting a different image', -> + @crop.setImage('base/test/images/berge.jpg') + + bgImg = @arena.find('.crop-outline img') + expect(bgImg.length).to.equal(1) + + describe 'with surrounding image enabled when panning', -> + + beforeEach (done) -> + @arena = $(template) + @arena.css(width: 100, height: 100) + $(document.body).append(@arena) + + # Crop a 400x300 image + @crop = srcissors.new + arena: @arena + url: 'base/test/images/diagonal.jpg' + showSurroundingImage: 'panning' + @crop.on 'ready', done + + + afterEach -> + @arena.remove() + + + it 'has initialized the crop outline background correctly', -> + outline = @arena.find('.crop-outline') + bgImg = outline.find('img') + + expect(bgImg.length).to.equal(1) + expect(bgImg.get(0).style.opacity).to.equal('0.2') + expect(outline.get(0).style.opacity).to.equal('') + + + describe 'with surrounding image disabled by default', -> + + beforeEach -> + @arena = $(template) + @arena.css(width: 100, height: 100) + $(document.body).append(@arena) + + + afterEach -> + @arena.remove() + + + it 'omits the background image without surrounding image config', (done) -> + @crop = srcissors.new + arena: @arena + url: 'base/test/images/diagonal.jpg' + @crop.on 'ready', done + + outline = @arena.find('.crop-outline') + bgImg = outline.find('img') + + expect(bgImg.length).to.equal(0) + expect(outline.get(0).style.opacity).to.equal('0') + \ No newline at end of file From 1dd3c185327dd6a3251a3730bab6501776108906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Immanuel=20H=C3=A4ussermann?= Date: Thu, 4 May 2017 16:34:35 +0200 Subject: [PATCH 04/12] Release 1.2.0 --- Changelog.md | 4 ++++ package.json | 2 +- srcissors.js | 60 +++++++++++++++++++++++++++++++++++++----------- srcissors.min.js | 2 +- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/Changelog.md b/Changelog.md index d77ed23..774f335 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +# v.1.2.0 + +- Add `showSurroundingImage` and `surroundingImageOpacity` options [#9](https://github.com/upfrontIO/srcissors/pull/9) + # v.1.1.0 - Add a `minResolution` parameter [#6](https://github.com/upfrontIO/srcissors/pull/6) diff --git a/package.json b/package.json index d39056d..53393ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "srcissors", - "version": "1.1.0", + "version": "1.2.0", "homepage": "https://github.com/upfrontIO/srcissors", "description": "Image cropping for responsive images", "author": "upfront.io", diff --git a/srcissors.js b/srcissors.js index 383724b..d0eb7bd 100644 --- a/srcissors.js +++ b/srcissors.js @@ -11,8 +11,8 @@ Events = require('./events'); module.exports = Crop = (function() { function Crop(arg) { - var crop, maxArea, url, zoomStep; - this.arena = arg.arena, this.view = arg.view, this.img = arg.img, this.outline = arg.outline, url = arg.url, this.fixedWidth = arg.fixedWidth, this.fixedHeight = arg.fixedHeight, this.minViewWidth = arg.minViewWidth, this.minViewHeight = arg.minViewHeight, this.minViewRatio = arg.minViewRatio, this.maxViewRatio = arg.maxViewRatio, crop = arg.crop, zoomStep = arg.zoomStep, maxArea = arg.maxArea, this.actions = arg.actions, this.minResolution = arg.minResolution; + var crop, maxArea, showSurroundingImage, url, zoomStep; + this.arena = arg.arena, this.view = arg.view, this.img = arg.img, this.outline = arg.outline, url = arg.url, this.fixedWidth = arg.fixedWidth, this.fixedHeight = arg.fixedHeight, this.minViewWidth = arg.minViewWidth, this.minViewHeight = arg.minViewHeight, this.minViewRatio = arg.minViewRatio, this.maxViewRatio = arg.maxViewRatio, crop = arg.crop, zoomStep = arg.zoomStep, maxArea = arg.maxArea, this.actions = arg.actions, this.minResolution = arg.minResolution, this.surroundingImageOpacity = arg.surroundingImageOpacity, showSurroundingImage = arg.showSurroundingImage; this.onPreviewReady = bind(this.onPreviewReady, this); this.loadingCssClass = 'crop-view--is-loading'; this.panningCssClass = 'crop-view--is-panning'; @@ -29,10 +29,14 @@ module.exports = Crop = (function() { if (maxArea) { this.maxArea = (this.arenaWidth * this.arenaHeight) * maxArea; } + if (this.outline) { + this.setSurroundingImageVisibility(showSurroundingImage); + } this.preview = new Preview({ onReady: this.onPreviewReady, img: this.img, - outline: this.outline + outline: this.outline, + opacity: this.surroundingImageOpacity }); this.setImage(url); } @@ -60,6 +64,18 @@ module.exports = Crop = (function() { }); }; + Crop.prototype.setSurroundingImageVisibility = function(visibility) { + this.surroundingImageOpacity = parseFloat(this.surroundingImageOpacity || 0.2); + if (visibility === 'always') { + return this.outline.css('opacity', 1.0); + } else if (visibility === 'panning') { + return this.outline.css('opacity', null); + } else { + this.outline.css('opacity', 0); + return this.surroundingImageOpacity = 0; + } + }; + Crop.prototype.reset = function() { if (!this.isReady) { return; @@ -678,7 +694,6 @@ module.exports = Crop = (function() { })(); - }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./events":2,"./preview":3}],2:[function(require,module,exports){ (function (global){ @@ -833,14 +848,16 @@ module.exports = Events = (function() { })(); - }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],3:[function(require,module,exports){ -var Preview; +(function (global){ +var $, Preview; + +$ = (typeof window !== "undefined" ? window['jQuery'] : typeof global !== "undefined" ? global['jQuery'] : null); module.exports = Preview = (function() { function Preview(arg) { - this.onReady = arg.onReady, this.img = arg.img, this.outline = arg.outline; + this.onReady = arg.onReady, this.img = arg.img, this.opacity = arg.opacity, this.outline = arg.outline; this.x = this.y = 0; this.width = this.height = 0; this.img.on('load', (function(_this) { @@ -864,7 +881,23 @@ module.exports = Preview = (function() { Preview.prototype.setImage = function(arg) { this.url = arg.url; - return this.img.attr('src', this.url); + this.img.attr('src', this.url); + if (this.outline) { + return this.setBackgroundImage({ + url: this.url + }); + } + }; + + Preview.prototype.setBackgroundImage = function(arg) { + var bg_img, url; + url = arg.url; + if (this.opacity > 0) { + bg_img = $('').css({ + opacity: this.opacity + }).attr('src', url); + return this.outline.append(bg_img); + } }; Preview.prototype.reset = function() { @@ -880,7 +913,7 @@ module.exports = Preview = (function() { if (this.outline) { return this.outline.css({ transform: '' - }); + }).html(''); } }; @@ -944,7 +977,7 @@ module.exports = Preview = (function() { })(); - +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],4:[function(require,module,exports){ (function (global){ var $, Crop; @@ -955,8 +988,8 @@ Crop = require('./crop'); module.exports = window.srcissors = { "new": function(arg) { - var actions, allowedActions, arena, crop, fixedHeight, fixedWidth, img, maxArea, maxRatio, minHeight, minRatio, minResolution, minWidth, outline, preview, url, view, zoomStep; - arena = arg.arena, url = arg.url, fixedWidth = arg.fixedWidth, fixedHeight = arg.fixedHeight, minWidth = arg.minWidth, minHeight = arg.minHeight, minRatio = arg.minRatio, maxRatio = arg.maxRatio, maxArea = arg.maxArea, zoomStep = arg.zoomStep, crop = arg.crop, actions = arg.actions, minResolution = arg.minResolution; + var actions, allowedActions, arena, crop, fixedHeight, fixedWidth, img, maxArea, maxRatio, minHeight, minRatio, minResolution, minWidth, outline, preview, showSurroundingImage, surroundingImageOpacity, url, view, zoomStep; + arena = arg.arena, url = arg.url, fixedWidth = arg.fixedWidth, fixedHeight = arg.fixedHeight, minWidth = arg.minWidth, minHeight = arg.minHeight, minRatio = arg.minRatio, maxRatio = arg.maxRatio, maxArea = arg.maxArea, zoomStep = arg.zoomStep, crop = arg.crop, actions = arg.actions, minResolution = arg.minResolution, surroundingImageOpacity = arg.surroundingImageOpacity, showSurroundingImage = arg.showSurroundingImage; arena = $(arena); view = arena.find('.crop-view'); preview = view.find('.crop-preview'); @@ -990,6 +1023,8 @@ module.exports = window.srcissors = { view: view, img: img, outline: outline, + showSurroundingImage: showSurroundingImage, + surroundingImageOpacity: surroundingImageOpacity, fixedWidth: fixedWidth, fixedHeight: fixedHeight, minViewWidth: minWidth, @@ -1005,6 +1040,5 @@ module.exports = window.srcissors = { }; - }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./crop":1}]},{},[4]); diff --git a/srcissors.min.js b/srcissors.min.js index 75d298c..6e35fc4 100644 --- a/srcissors.min.js +++ b/srcissors.min.js @@ -1 +1 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oimageResolution){delete this.minResolution}if(this.minResolution){minRatioForResolution=this.minResolution/(this.imageHeight*this.imageHeight);if(!this.minViewRatio||this.minViewRatiomaxRatioForResolution){this.maxViewRatio=maxRatioForResolution}}this.calcMaxMinDimensions();if(this.fixedWidth){keepDimension="width"}if(this.fixedHeight){keepDimension="height"}this.setViewDimensions({width:this.imageWidth,height:this.imageHeight,keepDimension:keepDimension});this.isReady=true;this.view.removeClass(this.loadingCssClass);if(!this.isInitialized&&this.initialCrop!=null){this.setCrop(this.initialCrop)}else{this.zoomAllOut();this.center()}this.isInitialized=true;this.readyEvent.fire();return this.loadEvent.fire()};Crop.prototype.setCrop=function(arg){var factor,height,previewWidth,width,x,y;x=arg.x,y=arg.y,width=arg.width,height=arg.height;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setCrop({x:x,y:y,width:width,height:height})}}(this));return}this.resize({width:width,height:height});factor=this.viewWidth/width;previewWidth=this.imageWidth*factor;this.zoom({width:previewWidth});return this.pan({x:x*factor,y:y*factor})};Crop.prototype.getCrop=function(){var crop,factor;factor=this.preview.width/this.imageWidth;crop={x:this.preview.x/factor,y:this.preview.y/factor,width:this.viewWidth/factor,height:this.viewHeight/factor};this.roundCrop(crop);this.validateCrop(crop);return crop};Crop.prototype.roundCrop=function(crop){var name,results,value;results=[];for(name in crop){value=crop[name];results.push(crop[name]=Math.round(value))}return results};Crop.prototype.validateCrop=function(crop){var height,width,x,y;x=crop.x,y=crop.y,width=crop.width,height=crop.height;if(x+width>this.imageWidth){crop.width=this.imageWidth-x}else if(y+height>this.imageHeight){crop.height=this.imageHeight-y}return crop};Crop.prototype.setRatio=function(ratio,keepDimension){var height,width;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setRatio(ratio,keepDimension)}}(this));return}ratio=this.enforceValidRatio(ratio);if(keepDimension==="height"){height=this.viewHeight;width=height*ratio}else{width=this.viewWidth;height=width/ratio}this.resizeFocusPoint=this.getFocusPoint();return this.resize({width:width,height:height})};Crop.prototype.onPan=function(data){var newX,newY;if(!this.isPanning){this.isPanning=true;this.arena.addClass(this.panningCssClass);this.outline.addClass(this.outlineCssClass)}newX=data.startX-data.dx;newY=data.startY-data.dy;return this.pan({x:newX,y:newY})};Crop.prototype.onPanEnd=function(){this.isPanning=false;this.arena.removeClass(this.panningCssClass);return this.outline.removeClass(this.outlineCssClass)};Crop.prototype.onDoubleClick=function(arg){var left,pageX,pageY,ref,top,viewX,viewY;pageX=arg.pageX,pageY=arg.pageY;ref=this.view[0].getBoundingClientRect(),left=ref.left,top=ref.top;viewX=pageX-left;viewY=pageY-top;return this.zoomIn({viewX:viewX,viewY:viewY})};Crop.prototype.onResize=function(arg){var dx,dy,position;position=arg.position,dx=arg.dx,dy=arg.dy;if(!this.isResizing){this.isResizing=true;this.resizeFocusPoint=this.getFocusPoint()}if(position==="top"||position==="bottom"){dy=2*dy;return this.resize({width:this.viewWidth,height:this.viewHeight+dy,keepDimension:"height"})}else if(position==="left"||position==="right"){dx=2*dx;return this.resize({width:this.viewWidth+dx,height:this.viewHeight,keepDimension:"width"})}};Crop.prototype.onResizeEnd=function(){this.isResizing=false;return this.resizeFocusPoint=void 0};Crop.prototype.resize=function(arg){var height,keepDimension,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;this.setViewDimensions({width:width,height:height,keepDimension:keepDimension});if(this.resizeFocusPoint){this.resizeFocusPoint.viewX=this.viewWidth/2;this.resizeFocusPoint.viewY=this.viewHeight/2}return this.zoom({width:this.preview.width,height:this.preview.height,focusPoint:this.resizeFocusPoint})};Crop.prototype.setViewDimensions=function(arg){var height,keepDimension,minZoomPixelHeight,minZoomPixelWidth,ref,ref1,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(this.maxArea){ref=this.enforceMaxArea({width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height}ref1=this.enforceViewDimensions({width:width,height:height,keepDimension:keepDimension}),width=ref1.width,height=ref1.height;this.view.css({width:width,height:height});this.viewWidth=width;this.viewHeight=height;this.viewRatio=width/height;if(this.minResolution){minZoomPixelWidth=Math.sqrt(this.minResolution*this.viewRatio);minZoomPixelHeight=Math.sqrt(this.minResolution/this.viewRatio);this.maxImageWidth=this.viewWidth/minZoomPixelWidth*this.imageWidth;this.maxImageHeight=this.viewHeight/minZoomPixelHeight*this.imageHeight}return this.fireChange()};Crop.prototype.zoomAllOut=function(){if(this.isWidthRestricting()){return this.zoom({width:this.viewWidth})}else{return this.zoom({height:this.viewHeight})}};Crop.prototype.zoomIn=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomInStep}else{params.height=this.preview.height*this.zoomInStep}return this.zoom(params)};Crop.prototype.zoomOut=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomOutStep}else{params.height=this.preview.height*this.zoomOutStep}return this.zoom(params)};Crop.prototype.zoom=function(arg){var focusPoint,height,ref,viewX,viewY,width;width=arg.width,height=arg.height,viewX=arg.viewX,viewY=arg.viewY,focusPoint=arg.focusPoint;if(focusPoint==null){focusPoint=this.getFocusPoint({viewX:viewX,viewY:viewY})}ref=this.enforceZoom({width:width,height:height}),width=ref.width,height=ref.height;if(width!=null){this.preview.setWidth(width);this.fireChange()}else if(height!=null){this.preview.setHeight(height);this.fireChange()}return this.focus(focusPoint)};Crop.prototype.getFocusPoint=function(arg){var percentX,percentY,ref,viewX,viewY,x,y;ref=arg!=null?arg:{},viewX=ref.viewX,viewY=ref.viewY;if(viewX==null){viewX=this.viewWidth/2}if(viewY==null){viewY=this.viewHeight/2}x=this.preview.x+viewX;y=this.preview.y+viewY;percentX=x/this.preview.width;percentY=y/this.preview.height;return{percentX:percentX,percentY:percentY,viewX:viewX,viewY:viewY}};Crop.prototype.focus=function(arg){var percentX,percentY,viewX,viewY,x,y;percentX=arg.percentX,percentY=arg.percentY,viewX=arg.viewX,viewY=arg.viewY;x=this.preview.width*percentX;y=this.preview.height*percentY;x=x-viewX;y=y-viewY;return this.pan({x:x,y:y})};Crop.prototype.center=function(){var newX,newY;newX=(this.preview.width-this.viewWidth)/2;newY=(this.preview.height-this.viewHeight)/2;return this.pan({x:newX,y:newY})};Crop.prototype.pan=function(data){data=this.enforceXy(data);this.preview.pan(data.x,data.y);return this.fireChange()};Crop.prototype.enforceXy=function(arg){var x,y;x=arg.x,y=arg.y;if(x<0){x=0}else if(x>this.preview.width-this.viewWidth){x=this.preview.width-this.viewWidth}if(y<0){y=0}else if(y>this.preview.height-this.viewHeight){y=this.preview.height-this.viewHeight}return{x:x,y:y}};Crop.prototype.enforceZoom=function(arg){var height,width;width=arg.width,height=arg.height;if(width!=null&&this.maxImageWidth&&width>this.maxImageWidth){return{width:this.maxImageWidth}}if(width!=null&&widththis.maxImageHeight){return{height:this.maxImageHeight}}if(height!=null&&heightthis.maxWidth||heightthis.maxHeight||ratiothis.maxViewRatio;return!invalid};Crop.prototype.isValidRatio=function(ratio){return!(ratiothis.maxViewRatio)};Crop.prototype.enforceValidRatio=function(ratio){if(ratiothis.maxViewRatio){return this.maxViewRatio}return ratio};Crop.prototype.enforceViewDimensions=function(arg){var height,keepDimension,newHeight,newWidth,ratio,ref,ref1,ref2,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(widththis.maxWidth){newWidth=this.maxWidth}if(heightthis.maxHeight){newHeight=this.maxHeight}if(keepDimension){if(newWidth){width=newWidth}if(newHeight){height=newHeight}ratio=width/height;if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);ref=this.getRatioBox({ratio:ratio,width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height;if(width>this.arenaWidth||height>this.arenaHeight){ref1=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref1.width,height=ref1.height}}}else if(newWidth||newHeight){ratio=this.enforceValidRatio(width/height);ref2=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref2.width,height=ref2.height}return{width:width,height:height}};Crop.prototype.enforceMaxArea=function(arg){var height,keepDimension,ratio,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;ratio=width/height;if(keepDimension==="width"){height=this.maxArea/width;ratio=width/height}else if(keepDimension==="height"){width=this.maxArea/height;ratio=width/height}else{width=Math.sqrt(this.maxArea*ratio);height=width/ratio}if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);width=Math.sqrt(this.maxArea*ratio);height=width/ratio}return{width:width,height:height}};Crop.prototype.isWidthRestricting=function(){return this.viewRatio>=this.imageRatio};Crop.prototype.getRatioBox=function(arg){var height,keepDimension,ratio,width;ratio=arg.ratio,width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(keepDimension==="width"||height==null){height=width/ratio}else if(keepDimension==="height"||width==null){width=height*ratio}else{height=width/ratio}return{width:width,height:height}};Crop.prototype.centerAlign=function(areaWidth,areaHeight,ratio){var height,width,x,y;if(areaWidth/areaHeight>ratio){width=areaHeight*ratio;x=(areaWidth-width)/2}else{height=areaWidth/ratio;y=(areaHeight-height)/2}return{x:x||0,y:y||0,width:width||areaWidth,height:height||areaHeight}};Crop.prototype.min=function(array){var i,len,min,number;min=array[0];for(i=0,len=array.length;inow-_this.doubleClickThreshold){_this.parent.onDoubleClick({pageX:event.pageX,pageY:event.pageY})}else{}return lastClick=now}}(this))};Events.prototype.preventBrowserDragDrop=function(){return this.view.on("dragstart.srcissors",function(){return false})};Events.prototype.resizeView=function(arg){var $template,horizontal,positions,vertical;horizontal=arg.horizontal,vertical=arg.vertical;$template=$("
");$template.addClass("resize-handler");positions=[];if(horizontal){positions=positions.concat(["right","left"])}if(vertical){positions=positions.concat(["top","bottom"])}return positions.forEach(function(_this){return function(position){var $handler;$handler=$template.clone();$handler.addClass("resize-handler-"+position);$handler.on("mousedown.srcissors",_this.getResizeMouseDown(position));return _this.view.append($handler)}}(this))};Events.prototype.getResizeMouseDown=function(position){var $doc;$doc=$(document);return function(_this){return function(event){var lastX,lastY;lastX=event.pageX;lastY=event.pageY;event.stopPropagation();return $doc.on("mousemove.srcissors-resize",function(e2){var dx,dy;switch(position){case"top":case"bottom":dy=e2.pageY-lastY;if(position==="top"){dy=-dy}lastY=e2.pageY;break;case"left":case"right":dx=e2.pageX-lastX;if(position==="left"){dx=-dx}lastX=e2.pageX}return _this.parent.onResize({position:position,dx:dx,dy:dy})}).on("mouseup.srcissors-resize",function(){$doc.off("mouseup.srcissors-resize");$doc.off("mousemove.srcissors-resize");return _this.parent.onResizeEnd({position:position})})}}(this)};Events.prototype.responsiveArena=function(){};return Events}()}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],3:[function(require,module,exports){var Preview;module.exports=Preview=function(){function Preview(arg){this.onReady=arg.onReady,this.img=arg.img,this.outline=arg.outline;this.x=this.y=0;this.width=this.height=0;this.img.on("load",function(_this){return function(){var height,width;width=_this.img.width();height=_this.img.height();_this.ratio=width/height;_this.updateImageDimensions({width:width,height:height});_this.onReady({width:_this.width,height:_this.height});return _this.img.show()}}(this))}Preview.prototype.setImage=function(arg){this.url=arg.url;return this.img.attr("src",this.url)};Preview.prototype.reset=function(){this.url=void 0;this.x=this.y=0;this.width=this.height=0;this.img.attr("src","");this.img.css({width:"",height:"",transform:""});if(this.outline){return this.outline.css({transform:""})}};Preview.prototype.setWidth=function(width){var height;this.img.css({width:width+"px",height:"auto"});height=width/this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.setHeight=function(height){var width;this.img.css({width:"auto",height:height+"px"});width=height*this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.updateImageDimensions=function(arg){var height,width;width=arg.width,height=arg.height;this.width=width;this.height=height;if(this.outline){return this.outline.css({width:this.width+"px",height:this.height+"px"})}};Preview.prototype.pan=function(x1,y1){var x,y;this.x=x1;this.y=y1;x=Math.round(this.x);y=Math.round(this.y);this.img.css({transform:"translate(-"+x+"px, -"+y+"px)"});if(this.outline){return this.outline.css({transform:"translate(-"+x+"px, -"+y+"px)"})}};return Preview}()},{}],4:[function(require,module,exports){(function(global){var $,Crop;$=typeof window!=="undefined"?window["jQuery"]:typeof global!=="undefined"?global["jQuery"]:null;Crop=require("./crop");module.exports=window.srcissors={new:function(arg){var actions,allowedActions,arena,crop,fixedHeight,fixedWidth,img,maxArea,maxRatio,minHeight,minRatio,minResolution,minWidth,outline,preview,url,view,zoomStep;arena=arg.arena,url=arg.url,fixedWidth=arg.fixedWidth,fixedHeight=arg.fixedHeight,minWidth=arg.minWidth,minHeight=arg.minHeight,minRatio=arg.minRatio,maxRatio=arg.maxRatio,maxArea=arg.maxArea,zoomStep=arg.zoomStep,crop=arg.crop,actions=arg.actions,minResolution=arg.minResolution;arena=$(arena);view=arena.find(".crop-view");preview=view.find(".crop-preview");img=$("");preview.append(img);outline=view.find(".crop-outline");if(!outline.length){outline=void 0}allowedActions={pan:true,zoomOnDoubleClick:true,resize:true,resizeHorizontal:!fixedWidth,resizeVertical:!fixedHeight};$.extend(allowedActions,actions);if(zoomStep==null){zoomStep=1.25}if(minWidth==null){minWidth=50}if(minHeight==null){minHeight=50}return new Crop({url:url,crop:crop,arena:arena,view:view,img:img,outline:outline,fixedWidth:fixedWidth,fixedHeight:fixedHeight,minViewWidth:minWidth,minViewHeight:minHeight,minViewRatio:minRatio,maxViewRatio:maxRatio,maxArea:maxArea,zoomStep:zoomStep,actions:allowedActions,minResolution:minResolution})}}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./crop":1}]},{},[4]); +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;oimageResolution){delete this.minResolution}if(this.minResolution){minRatioForResolution=this.minResolution/(this.imageHeight*this.imageHeight);if(!this.minViewRatio||this.minViewRatiomaxRatioForResolution){this.maxViewRatio=maxRatioForResolution}}this.calcMaxMinDimensions();if(this.fixedWidth){keepDimension="width"}if(this.fixedHeight){keepDimension="height"}this.setViewDimensions({width:this.imageWidth,height:this.imageHeight,keepDimension:keepDimension});this.isReady=true;this.view.removeClass(this.loadingCssClass);if(!this.isInitialized&&this.initialCrop!=null){this.setCrop(this.initialCrop)}else{this.zoomAllOut();this.center()}this.isInitialized=true;this.readyEvent.fire();return this.loadEvent.fire()};Crop.prototype.setCrop=function(arg){var factor,height,previewWidth,width,x,y;x=arg.x,y=arg.y,width=arg.width,height=arg.height;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setCrop({x:x,y:y,width:width,height:height})}}(this));return}this.resize({width:width,height:height});factor=this.viewWidth/width;previewWidth=this.imageWidth*factor;this.zoom({width:previewWidth});return this.pan({x:x*factor,y:y*factor})};Crop.prototype.getCrop=function(){var crop,factor;factor=this.preview.width/this.imageWidth;crop={x:this.preview.x/factor,y:this.preview.y/factor,width:this.viewWidth/factor,height:this.viewHeight/factor};this.roundCrop(crop);this.validateCrop(crop);return crop};Crop.prototype.roundCrop=function(crop){var name,results,value;results=[];for(name in crop){value=crop[name];results.push(crop[name]=Math.round(value))}return results};Crop.prototype.validateCrop=function(crop){var height,width,x,y;x=crop.x,y=crop.y,width=crop.width,height=crop.height;if(x+width>this.imageWidth){crop.width=this.imageWidth-x}else if(y+height>this.imageHeight){crop.height=this.imageHeight-y}return crop};Crop.prototype.setRatio=function(ratio,keepDimension){var height,width;if(!this.isReady){this.on("ready",function(_this){return function(){return _this.setRatio(ratio,keepDimension)}}(this));return}ratio=this.enforceValidRatio(ratio);if(keepDimension==="height"){height=this.viewHeight;width=height*ratio}else{width=this.viewWidth;height=width/ratio}this.resizeFocusPoint=this.getFocusPoint();return this.resize({width:width,height:height})};Crop.prototype.onPan=function(data){var newX,newY;if(!this.isPanning){this.isPanning=true;this.arena.addClass(this.panningCssClass);this.outline.addClass(this.outlineCssClass)}newX=data.startX-data.dx;newY=data.startY-data.dy;return this.pan({x:newX,y:newY})};Crop.prototype.onPanEnd=function(){this.isPanning=false;this.arena.removeClass(this.panningCssClass);return this.outline.removeClass(this.outlineCssClass)};Crop.prototype.onDoubleClick=function(arg){var left,pageX,pageY,ref,top,viewX,viewY;pageX=arg.pageX,pageY=arg.pageY;ref=this.view[0].getBoundingClientRect(),left=ref.left,top=ref.top;viewX=pageX-left;viewY=pageY-top;return this.zoomIn({viewX:viewX,viewY:viewY})};Crop.prototype.onResize=function(arg){var dx,dy,position;position=arg.position,dx=arg.dx,dy=arg.dy;if(!this.isResizing){this.isResizing=true;this.resizeFocusPoint=this.getFocusPoint()}if(position==="top"||position==="bottom"){dy=2*dy;return this.resize({width:this.viewWidth,height:this.viewHeight+dy,keepDimension:"height"})}else if(position==="left"||position==="right"){dx=2*dx;return this.resize({width:this.viewWidth+dx,height:this.viewHeight,keepDimension:"width"})}};Crop.prototype.onResizeEnd=function(){this.isResizing=false;return this.resizeFocusPoint=void 0};Crop.prototype.resize=function(arg){var height,keepDimension,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;this.setViewDimensions({width:width,height:height,keepDimension:keepDimension});if(this.resizeFocusPoint){this.resizeFocusPoint.viewX=this.viewWidth/2;this.resizeFocusPoint.viewY=this.viewHeight/2}return this.zoom({width:this.preview.width,height:this.preview.height,focusPoint:this.resizeFocusPoint})};Crop.prototype.setViewDimensions=function(arg){var height,keepDimension,minZoomPixelHeight,minZoomPixelWidth,ref,ref1,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(this.maxArea){ref=this.enforceMaxArea({width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height}ref1=this.enforceViewDimensions({width:width,height:height,keepDimension:keepDimension}),width=ref1.width,height=ref1.height;this.view.css({width:width,height:height});this.viewWidth=width;this.viewHeight=height;this.viewRatio=width/height;if(this.minResolution){minZoomPixelWidth=Math.sqrt(this.minResolution*this.viewRatio);minZoomPixelHeight=Math.sqrt(this.minResolution/this.viewRatio);this.maxImageWidth=this.viewWidth/minZoomPixelWidth*this.imageWidth;this.maxImageHeight=this.viewHeight/minZoomPixelHeight*this.imageHeight}return this.fireChange()};Crop.prototype.zoomAllOut=function(){if(this.isWidthRestricting()){return this.zoom({width:this.viewWidth})}else{return this.zoom({height:this.viewHeight})}};Crop.prototype.zoomIn=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomInStep}else{params.height=this.preview.height*this.zoomInStep}return this.zoom(params)};Crop.prototype.zoomOut=function(params){if(params==null){params={}}if(this.isWidthRestricting()){params.width=this.preview.width*this.zoomOutStep}else{params.height=this.preview.height*this.zoomOutStep}return this.zoom(params)};Crop.prototype.zoom=function(arg){var focusPoint,height,ref,viewX,viewY,width;width=arg.width,height=arg.height,viewX=arg.viewX,viewY=arg.viewY,focusPoint=arg.focusPoint;if(focusPoint==null){focusPoint=this.getFocusPoint({viewX:viewX,viewY:viewY})}ref=this.enforceZoom({width:width,height:height}),width=ref.width,height=ref.height;if(width!=null){this.preview.setWidth(width);this.fireChange()}else if(height!=null){this.preview.setHeight(height);this.fireChange()}return this.focus(focusPoint)};Crop.prototype.getFocusPoint=function(arg){var percentX,percentY,ref,viewX,viewY,x,y;ref=arg!=null?arg:{},viewX=ref.viewX,viewY=ref.viewY;if(viewX==null){viewX=this.viewWidth/2}if(viewY==null){viewY=this.viewHeight/2}x=this.preview.x+viewX;y=this.preview.y+viewY;percentX=x/this.preview.width;percentY=y/this.preview.height;return{percentX:percentX,percentY:percentY,viewX:viewX,viewY:viewY}};Crop.prototype.focus=function(arg){var percentX,percentY,viewX,viewY,x,y;percentX=arg.percentX,percentY=arg.percentY,viewX=arg.viewX,viewY=arg.viewY;x=this.preview.width*percentX;y=this.preview.height*percentY;x=x-viewX;y=y-viewY;return this.pan({x:x,y:y})};Crop.prototype.center=function(){var newX,newY;newX=(this.preview.width-this.viewWidth)/2;newY=(this.preview.height-this.viewHeight)/2;return this.pan({x:newX,y:newY})};Crop.prototype.pan=function(data){data=this.enforceXy(data);this.preview.pan(data.x,data.y);return this.fireChange()};Crop.prototype.enforceXy=function(arg){var x,y;x=arg.x,y=arg.y;if(x<0){x=0}else if(x>this.preview.width-this.viewWidth){x=this.preview.width-this.viewWidth}if(y<0){y=0}else if(y>this.preview.height-this.viewHeight){y=this.preview.height-this.viewHeight}return{x:x,y:y}};Crop.prototype.enforceZoom=function(arg){var height,width;width=arg.width,height=arg.height;if(width!=null&&this.maxImageWidth&&width>this.maxImageWidth){return{width:this.maxImageWidth}}if(width!=null&&widththis.maxImageHeight){return{height:this.maxImageHeight}}if(height!=null&&heightthis.maxWidth||heightthis.maxHeight||ratiothis.maxViewRatio;return!invalid};Crop.prototype.isValidRatio=function(ratio){return!(ratiothis.maxViewRatio)};Crop.prototype.enforceValidRatio=function(ratio){if(ratiothis.maxViewRatio){return this.maxViewRatio}return ratio};Crop.prototype.enforceViewDimensions=function(arg){var height,keepDimension,newHeight,newWidth,ratio,ref,ref1,ref2,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(widththis.maxWidth){newWidth=this.maxWidth}if(heightthis.maxHeight){newHeight=this.maxHeight}if(keepDimension){if(newWidth){width=newWidth}if(newHeight){height=newHeight}ratio=width/height;if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);ref=this.getRatioBox({ratio:ratio,width:width,height:height,keepDimension:keepDimension}),width=ref.width,height=ref.height;if(width>this.arenaWidth||height>this.arenaHeight){ref1=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref1.width,height=ref1.height}}}else if(newWidth||newHeight){ratio=this.enforceValidRatio(width/height);ref2=this.centerAlign(this.maxWidth,this.maxHeight,ratio),width=ref2.width,height=ref2.height}return{width:width,height:height}};Crop.prototype.enforceMaxArea=function(arg){var height,keepDimension,ratio,width;width=arg.width,height=arg.height,keepDimension=arg.keepDimension;ratio=width/height;if(keepDimension==="width"){height=this.maxArea/width;ratio=width/height}else if(keepDimension==="height"){width=this.maxArea/height;ratio=width/height}else{width=Math.sqrt(this.maxArea*ratio);height=width/ratio}if(!this.isValidRatio(ratio)){ratio=this.enforceValidRatio(ratio);width=Math.sqrt(this.maxArea*ratio);height=width/ratio}return{width:width,height:height}};Crop.prototype.isWidthRestricting=function(){return this.viewRatio>=this.imageRatio};Crop.prototype.getRatioBox=function(arg){var height,keepDimension,ratio,width;ratio=arg.ratio,width=arg.width,height=arg.height,keepDimension=arg.keepDimension;if(keepDimension==="width"||height==null){height=width/ratio}else if(keepDimension==="height"||width==null){width=height*ratio}else{height=width/ratio}return{width:width,height:height}};Crop.prototype.centerAlign=function(areaWidth,areaHeight,ratio){var height,width,x,y;if(areaWidth/areaHeight>ratio){width=areaHeight*ratio;x=(areaWidth-width)/2}else{height=areaWidth/ratio;y=(areaHeight-height)/2}return{x:x||0,y:y||0,width:width||areaWidth,height:height||areaHeight}};Crop.prototype.min=function(array){var i,len,min,number;min=array[0];for(i=0,len=array.length;inow-_this.doubleClickThreshold){_this.parent.onDoubleClick({pageX:event.pageX,pageY:event.pageY})}else{}return lastClick=now}}(this))};Events.prototype.preventBrowserDragDrop=function(){return this.view.on("dragstart.srcissors",function(){return false})};Events.prototype.resizeView=function(arg){var $template,horizontal,positions,vertical;horizontal=arg.horizontal,vertical=arg.vertical;$template=$("
");$template.addClass("resize-handler");positions=[];if(horizontal){positions=positions.concat(["right","left"])}if(vertical){positions=positions.concat(["top","bottom"])}return positions.forEach(function(_this){return function(position){var $handler;$handler=$template.clone();$handler.addClass("resize-handler-"+position);$handler.on("mousedown.srcissors",_this.getResizeMouseDown(position));return _this.view.append($handler)}}(this))};Events.prototype.getResizeMouseDown=function(position){var $doc;$doc=$(document);return function(_this){return function(event){var lastX,lastY;lastX=event.pageX;lastY=event.pageY;event.stopPropagation();return $doc.on("mousemove.srcissors-resize",function(e2){var dx,dy;switch(position){case"top":case"bottom":dy=e2.pageY-lastY;if(position==="top"){dy=-dy}lastY=e2.pageY;break;case"left":case"right":dx=e2.pageX-lastX;if(position==="left"){dx=-dx}lastX=e2.pageX}return _this.parent.onResize({position:position,dx:dx,dy:dy})}).on("mouseup.srcissors-resize",function(){$doc.off("mouseup.srcissors-resize");$doc.off("mousemove.srcissors-resize");return _this.parent.onResizeEnd({position:position})})}}(this)};Events.prototype.responsiveArena=function(){};return Events}()}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],3:[function(require,module,exports){(function(global){var $,Preview;$=typeof window!=="undefined"?window["jQuery"]:typeof global!=="undefined"?global["jQuery"]:null;module.exports=Preview=function(){function Preview(arg){this.onReady=arg.onReady,this.img=arg.img,this.opacity=arg.opacity,this.outline=arg.outline;this.x=this.y=0;this.width=this.height=0;this.img.on("load",function(_this){return function(){var height,width;width=_this.img.width();height=_this.img.height();_this.ratio=width/height;_this.updateImageDimensions({width:width,height:height});_this.onReady({width:_this.width,height:_this.height});return _this.img.show()}}(this))}Preview.prototype.setImage=function(arg){this.url=arg.url;this.img.attr("src",this.url);if(this.outline){return this.setBackgroundImage({url:this.url})}};Preview.prototype.setBackgroundImage=function(arg){var bg_img,url;url=arg.url;if(this.opacity>0){bg_img=$("").css({opacity:this.opacity}).attr("src",url);return this.outline.append(bg_img)}};Preview.prototype.reset=function(){this.url=void 0;this.x=this.y=0;this.width=this.height=0;this.img.attr("src","");this.img.css({width:"",height:"",transform:""});if(this.outline){return this.outline.css({transform:""}).html("")}};Preview.prototype.setWidth=function(width){var height;this.img.css({width:width+"px",height:"auto"});height=width/this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.setHeight=function(height){var width;this.img.css({width:"auto",height:height+"px"});width=height*this.ratio;return this.updateImageDimensions({width:width,height:height})};Preview.prototype.updateImageDimensions=function(arg){var height,width;width=arg.width,height=arg.height;this.width=width;this.height=height;if(this.outline){return this.outline.css({width:this.width+"px",height:this.height+"px"})}};Preview.prototype.pan=function(x1,y1){var x,y;this.x=x1;this.y=y1;x=Math.round(this.x);y=Math.round(this.y);this.img.css({transform:"translate(-"+x+"px, -"+y+"px)"});if(this.outline){return this.outline.css({transform:"translate(-"+x+"px, -"+y+"px)"})}};return Preview}()}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],4:[function(require,module,exports){(function(global){var $,Crop;$=typeof window!=="undefined"?window["jQuery"]:typeof global!=="undefined"?global["jQuery"]:null;Crop=require("./crop");module.exports=window.srcissors={new:function(arg){var actions,allowedActions,arena,crop,fixedHeight,fixedWidth,img,maxArea,maxRatio,minHeight,minRatio,minResolution,minWidth,outline,preview,showSurroundingImage,surroundingImageOpacity,url,view,zoomStep;arena=arg.arena,url=arg.url,fixedWidth=arg.fixedWidth,fixedHeight=arg.fixedHeight,minWidth=arg.minWidth,minHeight=arg.minHeight,minRatio=arg.minRatio,maxRatio=arg.maxRatio,maxArea=arg.maxArea,zoomStep=arg.zoomStep,crop=arg.crop,actions=arg.actions,minResolution=arg.minResolution,surroundingImageOpacity=arg.surroundingImageOpacity,showSurroundingImage=arg.showSurroundingImage;arena=$(arena);view=arena.find(".crop-view");preview=view.find(".crop-preview");img=$("");preview.append(img);outline=view.find(".crop-outline");if(!outline.length){outline=void 0}allowedActions={pan:true,zoomOnDoubleClick:true,resize:true,resizeHorizontal:!fixedWidth,resizeVertical:!fixedHeight};$.extend(allowedActions,actions);if(zoomStep==null){zoomStep=1.25}if(minWidth==null){minWidth=50}if(minHeight==null){minHeight=50}return new Crop({url:url,crop:crop,arena:arena,view:view,img:img,outline:outline,showSurroundingImage:showSurroundingImage,surroundingImageOpacity:surroundingImageOpacity,fixedWidth:fixedWidth,fixedHeight:fixedHeight,minViewWidth:minWidth,minViewHeight:minHeight,minViewRatio:minRatio,maxViewRatio:maxRatio,maxArea:maxArea,zoomStep:zoomStep,actions:allowedActions,minResolution:minResolution})}}}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{"./crop":1}]},{},[4]); From daff98fee2b2a0333d7f8509e3a9e1b44e74d04b Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Fri, 20 Jul 2018 01:46:08 +0200 Subject: [PATCH 05/12] chore: Rename github organization to livingdocsIO --- Changelog.md | 12 ++++++------ package.json | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Changelog.md b/Changelog.md index 774f335..bcd9a9d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,26 +1,26 @@ # v.1.2.0 -- Add `showSurroundingImage` and `surroundingImageOpacity` options [#9](https://github.com/upfrontIO/srcissors/pull/9) +- Add `showSurroundingImage` and `surroundingImageOpacity` options [#9](https://github.com/livingdocsIO/srcissors/pull/9) # v.1.1.0 -- Add a `minResolution` parameter [#6](https://github.com/upfrontIO/srcissors/pull/6) +- Add a `minResolution` parameter [#6](https://github.com/livingdocsIO/srcissors/pull/6) # v1.0.1 -- Avoid infinite loop when setting ratio [#5](https://github.com/upfrontIO/srcissors/pull/5) +- Avoid infinite loop when setting ratio [#5](https://github.com/livingdocsIO/srcissors/pull/5) # v1.0.0 -- Add reset() method [#4](https://github.com/upfrontIO/srcissors/pull/4) +- Add reset() method [#4](https://github.com/livingdocsIO/srcissors/pull/4) # v0.3.0 -- Add setImage() method [#3](https://github.com/upfrontIO/srcissors/pull/3) +- Add setImage() method [#3](https://github.com/livingdocsIO/srcissors/pull/3) # v0.2.0 -- Add actions options [#1](https://github.com/upfrontIO/srcissors/pull/1) +- Add actions options [#1](https://github.com/livingdocsIO/srcissors/pull/1) # v0.1.0 diff --git a/package.json b/package.json index 53393ab..2a0e476 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "srcissors", "version": "1.2.0", - "homepage": "https://github.com/upfrontIO/srcissors", + "homepage": "https://github.com/livingdocsIO/srcissors", "description": "Image cropping for responsive images", - "author": "upfront.io", + "author": "Livingdocs ", "license": "LGPL-3.0+", "scripts": { "test": "karma start", @@ -20,7 +20,7 @@ ], "repository": { "type": "git", - "url": "git://github.com/upfrontIO/srcissors.git" + "url": "git://github.com/livingdocsIO/srcissors.git" }, "browser": "src/srcissors.coffee", "main": "src/srcissors.coffee", From 526de13f037125f75be614ef75c7fd8218ff2e91 Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Thu, 13 Sep 2018 19:30:07 +0200 Subject: [PATCH 06/12] chore: Migrate to javascript --- src/crop.coffee | 560 --------------------------- src/crop.js | 627 +++++++++++++++++++++++++++++++ src/events.coffee | 111 ------ src/events.js | 120 ++++++ src/preview.coffee | 64 ---- src/preview.js | 75 ++++ src/preview_css_zoom.coffee | 63 ---- src/preview_css_zoom.js | 72 ++++ src/srcissors.coffee | 51 --- src/srcissors.js | 54 +++ test/specs/srcissors_spec.coffee | 291 -------------- test/specs/srcissors_spec.js | 325 ++++++++++++++++ 12 files changed, 1273 insertions(+), 1140 deletions(-) delete mode 100644 src/crop.coffee create mode 100644 src/crop.js delete mode 100644 src/events.coffee create mode 100644 src/events.js delete mode 100644 src/preview.coffee create mode 100644 src/preview.js delete mode 100644 src/preview_css_zoom.coffee create mode 100644 src/preview_css_zoom.js delete mode 100644 src/srcissors.coffee create mode 100644 src/srcissors.js delete mode 100644 test/specs/srcissors_spec.coffee create mode 100644 test/specs/srcissors_spec.js diff --git a/src/crop.coffee b/src/crop.coffee deleted file mode 100644 index fb46bbd..0000000 --- a/src/crop.coffee +++ /dev/null @@ -1,560 +0,0 @@ -$ = require('jquery') -Preview = require('./preview') -Events = require('./events') - -module.exports = class Crop - - constructor: ({ - @arena, @view, @img, @outline, url, @fixedWidth, @fixedHeight, - @minViewWidth, @minViewHeight, @minViewRatio, @maxViewRatio, crop - zoomStep, maxArea, @actions, @minResolution, @surroundingImageOpacity, - showSurroundingImage - }) -> - - # CSS classes - @loadingCssClass = 'crop-view--is-loading' - @panningCssClass = 'crop-view--is-panning' - @outlineCssClass = 'crop-outline--active' - - # State - @isPanning = false - @initialCrop = crop - - # Events - @loadEvent = $.Callbacks() - @changeEvent = $.Callbacks() - - # Sets up the ready event and state - @initializeReadyState() - - # Confguration - @zoomInStep = zoomStep - @zoomOutStep = 1 / @zoomInStep - - @arenaWidth = @arena.width() - @arenaHeight = @arena.height() - - # todo: consider to calculate maxArea with regards to the - # maximum space an image can within the area. That should - # be more reliable. - @maxArea = (@arenaWidth * @arenaHeight) * maxArea if maxArea - - @setSurroundingImageVisibility(showSurroundingImage) if @outline - - @preview = new Preview - onReady: @onPreviewReady - img: @img - outline: @outline - opacity: @surroundingImageOpacity - - @setImage(url) - - - initializeReadyState: -> - @isReady = false - @readyEvent?.empty() - @readyEvent = $.Callbacks('memory once') - - - setImage: (url) -> - return if url == @preview.url - - @preview.reset() if @isInitialized - @initializeReadyState() - @view.addClass(@loadingCssClass) - @preview.setImage({ url }) - - - setSurroundingImageVisibility: (visibility) -> - # visibility: always|panning|never - # override opacity in crop-outline--active css class - @surroundingImageOpacity = parseFloat(@surroundingImageOpacity || 0.2) - - if visibility == 'always' - @outline.css('opacity', 1.0) - else if visibility == 'panning' - @outline.css('opacity', null) - else # 'never' default - @outline.css('opacity', 0) - @surroundingImageOpacity = 0 - - - reset: -> - return unless @isReady - - @resize(width: @imageWidth, height: @imageHeight) - @zoomAllOut() - - - onPreviewReady: ({ width, height }) => - if not @isInitialized - @events = new Events - parent: this - view: @view - actions: @actions - - @imageWidth = width - @imageHeight = height - @imageRatio = @imageWidth / @imageHeight - imageResolution = @imageWidth * @imageHeight - - if @minResolution && @minResolution > imageResolution - # If the minimal required resolution is bigger than the actual image - # resolution, we ignore the configuration - delete @minResolution - - if @minResolution - # For any given image resolution with a minimal required resolution - # we can calculate both, a minimal resolution and a maximal resolution - minRatioForResolution = @minResolution / (@imageHeight * @imageHeight) - if !@minViewRatio || @minViewRatio < minRatioForResolution - @minViewRatio = minRatioForResolution - maxRatioForResolution = (@imageWidth * @imageWidth) / @minResolution - if !@maxViewRatio || @maxViewRatio > maxRatioForResolution - @maxViewRatio = maxRatioForResolution - - @calcMaxMinDimensions() - - keepDimension = 'width' if @fixedWidth - keepDimension = 'height' if @fixedHeight - @setViewDimensions - width: @imageWidth - height: @imageHeight - keepDimension: keepDimension - - # ready state - @isReady = true - @view.removeClass(@loadingCssClass) - - if not @isInitialized && @initialCrop? - @setCrop(@initialCrop) - else - @zoomAllOut() - @center() - - @isInitialized = true - @readyEvent.fire() - @loadEvent.fire() - - - setCrop: ({ x, y, width, height }) -> - if not @isReady - this.on 'ready', => - @setCrop({ x, y, width, height }) - return - - @resize({ width, height }) - - factor = @viewWidth / width - previewWidth = @imageWidth * factor - - @zoom(width: previewWidth) - @pan({ x: x * factor, y: y * factor }) - - - getCrop: -> - factor = @preview.width / @imageWidth - crop = - x: @preview.x / factor - y: @preview.y / factor - width: @viewWidth / factor - height: @viewHeight / factor - - @roundCrop(crop) - @validateCrop(crop) - crop - - - roundCrop: (crop) -> - for name, value of crop - crop[name] = Math.round(value) - - - validateCrop: (crop) -> - { x, y, width, height } = crop - if x + width > @imageWidth - crop.width = @imageWidth - x - else if y + height > @imageHeight - crop.height = @imageHeight - y - - crop - - - setRatio: (ratio, keepDimension) -> - if not @isReady - this.on 'ready', => - @setRatio(ratio, keepDimension) - return - - ratio = @enforceValidRatio(ratio) - - if keepDimension == 'height' - height = @viewHeight - width = height * ratio - else - width = @viewWidth - height = width / ratio - - @resizeFocusPoint = @getFocusPoint() - @resize({ width, height }) - - - # Event handling - # -------------- - - onPan: (data) -> - if not @isPanning - @isPanning = true - @arena.addClass(@panningCssClass) - @outline.addClass(@outlineCssClass) - - newX = data.startX - data.dx - newY = data.startY - data.dy - @pan(x: newX, y: newY) - - - onPanEnd: -> - @isPanning = false - @arena.removeClass(@panningCssClass) - @outline.removeClass(@outlineCssClass) - - - onDoubleClick: ({ pageX, pageY }) -> - { left, top } = @view[0].getBoundingClientRect() - viewX = pageX - left - viewY = pageY - top - @zoomIn({ viewX, viewY }) - - - onResize: ({ position, dx, dy }) -> - if not @isResizing - @isResizing = true - @resizeFocusPoint = @getFocusPoint() - - if position in ['top', 'bottom'] - dy = 2 * dy # Because it's centered we need to change width by factor two - @resize(width: @viewWidth, height: @viewHeight + dy, keepDimension: 'height') - else if position in ['left', 'right'] - dx = 2 * dx - @resize(width: @viewWidth + dx, height: @viewHeight, keepDimension: 'width') - - - onResizeEnd: -> - @isResizing = false - @resizeFocusPoint = undefined - - - resize: ({ width, height, keepDimension }) -> - @setViewDimensions({ width, height, keepDimension }) - - # Update view center of focus point - if @resizeFocusPoint - @resizeFocusPoint.viewX = @viewWidth / 2 - @resizeFocusPoint.viewY = @viewHeight / 2 - - # Ensure dimensions and focus - @zoom - width: @preview.width - height: @preview.height - focusPoint: @resizeFocusPoint - - - setViewDimensions: ({ width, height, keepDimension }) -> - if @maxArea - { width, height } = @enforceMaxArea({ width, height, keepDimension }) - - { width, height } = @enforceViewDimensions({ width, height, keepDimension }) - - @view.css(width: width, height: height) - @viewWidth = width - @viewHeight = height - @viewRatio = width / height - - if @minResolution - minZoomPixelWidth = Math.sqrt(@minResolution * @viewRatio) - minZoomPixelHeight = Math.sqrt(@minResolution / @viewRatio) - @maxImageWidth = (@viewWidth / minZoomPixelWidth) * @imageWidth - @maxImageHeight = (@viewHeight / minZoomPixelHeight) * @imageHeight - - @fireChange() - - - # Update view - # ----------- - - zoomAllOut: -> - if @isWidthRestricting() - @zoom(width: @viewWidth) - else - @zoom(height: @viewHeight) - - - zoomIn: (params={}) -> - if @isWidthRestricting() - params.width = @preview.width * @zoomInStep - else - params.height = @preview.height * @zoomInStep - - @zoom(params) - - - zoomOut: (params={}) -> - if @isWidthRestricting() - params.width = @preview.width * @zoomOutStep - else - params.height = @preview.height * @zoomOutStep - - @zoom(params) - - - zoom: ({ width, height, viewX, viewY, focusPoint }) -> - focusPoint ?= @getFocusPoint({ viewX, viewY }) - - { width, height } = @enforceZoom({ width, height }) - if width? - @preview.setWidth(width) - @fireChange() - else if height? - @preview.setHeight(height) - @fireChange() - - @focus(focusPoint) - - - # returns {Object} e.g. percentX: 0.2, percentY: 0.5 - getFocusPoint: ({ viewX, viewY }={}) -> - viewX ?= @viewWidth / 2 - viewY ?= @viewHeight / 2 - x = @preview.x + viewX - y = @preview.y + viewY - percentX = x / @preview.width - percentY = y / @preview.height - { percentX, percentY, viewX, viewY } - - - focus: ({ percentX, percentY, viewX, viewY }) -> - x = @preview.width * percentX - y = @preview.height * percentY - x = x - viewX - y = y - viewY - - @pan({ x, y }) - - - center: -> - newX = (@preview.width - @viewWidth) / 2 - newY = (@preview.height - @viewHeight) / 2 - @pan(x: newX, y: newY) - - - # @param { Object } - # - x {Number} pixel to pan to the left - # - y {Number} pixels to pan to the top - pan: (data) -> - data = @enforceXy(data) - @preview.pan(data.x, data.y) - @fireChange() - - - # Validations - # ----------- - - enforceXy: ({ x, y }) -> - if x < 0 - x = 0 - else if x > @preview.width - @viewWidth - x = @preview.width - @viewWidth - - if y < 0 - y = 0 - else if y > @preview.height - @viewHeight - y = @preview.height - @viewHeight - - { x, y } - - - enforceZoom: ({ width, height }) -> - - if width? && @maxImageWidth && width > @maxImageWidth - # prevent zooming in past the required resolution defined by minResolution - return { width: @maxImageWidth} - - if width? && width < @viewWidth - # prevent zooming out past covering the view completely - return { width: @viewWidth } - - if height? && @maxImageHeight && height > @maxImageHeight - # prevent zooming in past the required resolution defined by minResolution - return { height: @maxImageHeight} - - if height? && height < @viewHeight - # prevent zooming out past covering the view completely - return { height: @viewHeight } - - { width, height } - - - calcMaxMinDimensions: -> - @maxWidth = @min([@arenaWidth, @imageWidth]) - @maxHeight = @min([@arenaHeight, @imageHeight]) - @minWidth = @minViewWidth || 0 - @minHeight = @minViewHeight || 0 - - @maxWidth = @minWidth = @fixedWidth if @fixedWidth - @maxHeight = @minHeight = @fixedHeight if @fixedHeight - - - areDimensionsValid: ({ width, height, keepDimension }) -> - ratio = width / height - - invalid = - width < @minWidth || - width > @maxWidth || - height < @minHeight || - height > @maxHeight || - ratio < @minViewRatio || - ratio > @maxViewRatio - - not invalid - - - isValidRatio: (ratio) -> - not (ratio < @minViewRatio || ratio > @maxViewRatio) - - - enforceValidRatio: (ratio) -> - return @minViewRatio if ratio < @minViewRatio - return @maxViewRatio if ratio > @maxViewRatio - ratio - - - enforceViewDimensions: ({ width, height, keepDimension }) -> - newWidth = @minWidth if width < @minWidth - newWidth = @maxWidth if width > @maxWidth - newHeight = @minHeight if height < @minHeight - newHeight = @maxHeight if height > @maxHeight - - if keepDimension - width = newWidth if newWidth - height = newHeight if newHeight - - # check max/min ratios - ratio = width / height - if not @isValidRatio(ratio) - ratio = @enforceValidRatio(ratio) - { width, height } = @getRatioBox({ ratio: ratio, width, height, keepDimension }) - if width > @arenaWidth || height > @arenaHeight - { width, height } = @centerAlign(@maxWidth, @maxHeight, ratio) - - else if newWidth || newHeight - ratio = @enforceValidRatio(width / height) - { width, height } = @centerAlign(@maxWidth, @maxHeight, ratio) - - { width, height } - - - enforceMaxArea: ({ width, height, keepDimension }) -> - ratio = width / height - - if keepDimension == 'width' - height = @maxArea / width - ratio = width / height - else if keepDimension == 'height' - width = @maxArea / height - ratio = width / height - else # keep ratio - width = Math.sqrt(@maxArea * ratio) - height = width / ratio - - if not @isValidRatio(ratio) - ratio = @enforceValidRatio(ratio) - width = Math.sqrt(@maxArea * ratio) - height = width / ratio - - { width, height } - - - # Calculations - # ------------ - # - # Ratio: width / height - # Tall < 1 (Square) < Wide - # (A ratio less than one is a tall image format and - # a ratio greater than one is a wide image format) - - # Check if the width or height is restricting - isWidthRestricting: -> - @viewRatio >= @imageRatio - - - getRatioBox: ({ ratio, width, height, keepDimension }) -> - if keepDimension == 'width' || not height? - height = width / ratio - else if keepDimension == 'height' || not width? - width = height * ratio - else - height = width / ratio - - { width, height } - - - centerAlign: (areaWidth, areaHeight, ratio) -> - if ( areaWidth / areaHeight ) > ratio - width = areaHeight * ratio - x = (areaWidth - width) / 2 - else - height = areaWidth / ratio - y = (areaHeight - height) / 2 - - # return - x: x || 0 - y: y || 0 - width: width || areaWidth - height: height || areaHeight - - - min: (array) -> - min = array[0] - for number in array - min = number if number < min - - return min - - - # Events - # ------ - - on: (name, callback) -> - this["#{ name }Event"].add(callback) - - - off: (name, callback) -> - this["#{ name }Event"].remove(callback) - - - # Debounce change events so they are not fired more - # than once per tick. - fireChange: -> - return if @changeDispatch? - - @changeDispatch = setTimeout => - @changeDispatch = undefined - @changeEvent.fire(@getCrop()) - , 0 - - - # Development helpers - # ------------------- - - debug: -> - r = (num) -> Math.round(num * 10) / 10 - - obj = - arena: "#{ r @arenaWidth }x#{ r @arenaHeight }" - view: "#{ r @viewWidth }x#{ r @viewHeight }" - image: "#{ r @imageWidth }x#{ r @imageHeight }" - preview: "#{ r @preview.width }x#{ r @preview.height }" - previewXy: "#{ r @preview.x }x#{ r @preview.y }" - - console.log(obj) - return obj diff --git a/src/crop.js b/src/crop.js new file mode 100644 index 0000000..7843bda --- /dev/null +++ b/src/crop.js @@ -0,0 +1,627 @@ +const $ = require('jquery') +const Preview = require('./preview') +const Events = require('./events') + +module.exports = class Crop { + constructor ({ + arena, view, img, outline, url, fixedWidth, fixedHeight, + minViewWidth, minViewHeight, minViewRatio, maxViewRatio, crop, + zoomStep, maxArea, actions, minResolution, surroundingImageOpacity, + showSurroundingImage + }) { + // CSS classes + this.onPreviewReady = this.onPreviewReady.bind(this) + this.arena = arena + this.view = view + this.img = img + this.outline = outline + this.fixedWidth = fixedWidth + this.fixedHeight = fixedHeight + this.minViewWidth = minViewWidth + this.minViewHeight = minViewHeight + this.minViewRatio = minViewRatio + this.maxViewRatio = maxViewRatio + this.actions = actions + this.minResolution = minResolution + this.surroundingImageOpacity = surroundingImageOpacity + this.loadingCssClass = 'crop-view--is-loading' + this.panningCssClass = 'crop-view--is-panning' + this.outlineCssClass = 'crop-outline--active' + + // State + this.isPanning = false + this.initialCrop = crop + + // Events + this.loadEvent = $.Callbacks() + this.changeEvent = $.Callbacks() + + // Sets up the ready event and state + this.initializeReadyState() + + // Confguration + this.zoomInStep = zoomStep + this.zoomOutStep = 1 / this.zoomInStep + + this.arenaWidth = this.arena.width() + this.arenaHeight = this.arena.height() + + // todo: consider to calculate maxArea with regards to the + // maximum space an image can within the area. That should + // be more reliable. + if (maxArea) { this.maxArea = (this.arenaWidth * this.arenaHeight) * maxArea } + + if (this.outline) { this.setSurroundingImageVisibility(showSurroundingImage) } + + this.preview = new Preview({ + onReady: this.onPreviewReady, + img: this.img, + outline: this.outline, + opacity: this.surroundingImageOpacity + }) + + this.setImage(url) + } + + initializeReadyState () { + this.isReady = false + if (this.readyEvent != null) { + this.readyEvent.empty() + } + this.readyEvent = $.Callbacks('memory once') + } + + setImage (url) { + if (url === this.preview.url) return + + if (this.isInitialized) this.preview.reset() + this.initializeReadyState() + this.view.addClass(this.loadingCssClass) + this.preview.setImage({url}) + } + + setSurroundingImageVisibility (visibility) { + // visibility: always|panning|never + // override opacity in crop-outline--active css class + this.surroundingImageOpacity = parseFloat(this.surroundingImageOpacity || 0.2) + + if (visibility === 'always') { + this.outline.css('opacity', 1.0) + } else if (visibility === 'panning') { + this.outline.css('opacity', null) + } else { // 'never' default + this.outline.css('opacity', 0) + this.surroundingImageOpacity = 0 + } + } + + reset () { + if (!this.isReady) return + + this.resize({width: this.imageWidth, height: this.imageHeight}) + this.zoomAllOut() + } + + onPreviewReady ({width, height}) { + let keepDimension + if (!this.isInitialized) { + this.events = new Events({ + parent: this, + view: this.view, + actions: this.actions + }) + } + + this.imageWidth = width + this.imageHeight = height + this.imageRatio = this.imageWidth / this.imageHeight + const imageResolution = this.imageWidth * this.imageHeight + + if (this.minResolution && (this.minResolution > imageResolution)) { + // If the minimal required resolution is bigger than the actual image + // resolution, we ignore the configuration + delete this.minResolution + } + + if (this.minResolution) { + // For any given image resolution with a minimal required resolution + // we can calculate both, a minimal resolution and a maximal resolution + const minRatioForResolution = this.minResolution / (this.imageHeight * this.imageHeight) + if (!this.minViewRatio || (this.minViewRatio < minRatioForResolution)) { + this.minViewRatio = minRatioForResolution + } + const maxRatioForResolution = (this.imageWidth * this.imageWidth) / this.minResolution + if (!this.maxViewRatio || (this.maxViewRatio > maxRatioForResolution)) { + this.maxViewRatio = maxRatioForResolution + } + } + + this.calcMaxMinDimensions() + + if (this.fixedWidth) { keepDimension = 'width' } + if (this.fixedHeight) { keepDimension = 'height' } + this.setViewDimensions({ + width: this.imageWidth, + height: this.imageHeight, + keepDimension + }) + + // ready state + this.isReady = true + this.view.removeClass(this.loadingCssClass) + + if (!this.isInitialized && (this.initialCrop != null)) { + this.setCrop(this.initialCrop) + } else { + this.zoomAllOut() + this.center() + } + + this.isInitialized = true + this.readyEvent.fire() + this.loadEvent.fire() + } + + setCrop ({x, y, width, height}) { + if (!this.isReady) { + this.on('ready', () => this.setCrop({x, y, width, height})) + return + } + + this.resize({width, height}) + + const factor = this.viewWidth / width + const previewWidth = this.imageWidth * factor + + this.zoom({width: previewWidth}) + this.pan({x: x * factor, y: y * factor}) + } + + getCrop () { + const factor = this.preview.width / this.imageWidth + const crop = { + x: this.preview.x / factor, + y: this.preview.y / factor, + width: this.viewWidth / factor, + height: this.viewHeight / factor + } + + this.roundCrop(crop) + this.validateCrop(crop) + return crop + } + + roundCrop (crop) { + for (const name in crop) { + const value = crop[name] + crop[name] = Math.round(value) + } + } + + validateCrop (crop) { + const {x, y, width, height} = crop + if ((x + width) > this.imageWidth) { + crop.width = this.imageWidth - x + } else if ((y + height) > this.imageHeight) { + crop.height = this.imageHeight - y + } + + return crop + } + + setRatio (ratio, keepDimension) { + let height, width + if (!this.isReady) { + this.on('ready', () => this.setRatio(ratio, keepDimension)) + return + } + + ratio = this.enforceValidRatio(ratio) + + if (keepDimension === 'height') { + height = this.viewHeight + width = height * ratio + } else { + width = this.viewWidth + height = width / ratio + } + + this.resizeFocusPoint = this.getFocusPoint() + return this.resize({width, height}) + } + + // Event handling + // -------------- + + onPan (data) { + if (!this.isPanning) { + this.isPanning = true + this.arena.addClass(this.panningCssClass) + this.outline.addClass(this.outlineCssClass) + } + + const newX = data.startX - data.dx + const newY = data.startY - data.dy + this.pan({x: newX, y: newY}) + } + + onPanEnd () { + this.isPanning = false + this.arena.removeClass(this.panningCssClass) + return this.outline.removeClass(this.outlineCssClass) + } + + onDoubleClick ({pageX, pageY}) { + const {left, top} = this.view[0].getBoundingClientRect() + const viewX = pageX - left + const viewY = pageY - top + this.zoomIn({viewX, viewY}) + } + + onResize ({position, dx, dy}) { + if (!this.isResizing) { + this.isResizing = true + this.resizeFocusPoint = this.getFocusPoint() + } + + if (['top', 'bottom'].includes(position)) { + dy = 2 * dy // Because it's centered we need to change width by factor two + return this.resize({width: this.viewWidth, height: this.viewHeight + dy, keepDimension: 'height'}) + } else if (['left', 'right'].includes(position)) { + dx = 2 * dx + return this.resize({width: this.viewWidth + dx, height: this.viewHeight, keepDimension: 'width'}) + } + } + + onResizeEnd () { + this.isResizing = false + return this.resizeFocusPoint = undefined + } + + resize ({width, height, keepDimension}) { + this.setViewDimensions({width, height, keepDimension}) + + // Update view center of focus point + if (this.resizeFocusPoint) { + this.resizeFocusPoint.viewX = this.viewWidth / 2 + this.resizeFocusPoint.viewY = this.viewHeight / 2 + } + + // Ensure dimensions and focus + this.zoom({ + width: this.preview.width, + height: this.preview.height, + focusPoint: this.resizeFocusPoint + }) + } + + setViewDimensions ({width, height, keepDimension}) { + if (this.maxArea) { + ({width, height} = this.enforceMaxArea({width, height, keepDimension})) + } + + ({width, height} = this.enforceViewDimensions({width, height, keepDimension})) + + this.view.css({width, height}) + this.viewWidth = width + this.viewHeight = height + this.viewRatio = width / height + + if (this.minResolution) { + const minZoomPixelWidth = Math.sqrt(this.minResolution * this.viewRatio) + const minZoomPixelHeight = Math.sqrt(this.minResolution / this.viewRatio) + this.maxImageWidth = (this.viewWidth / minZoomPixelWidth) * this.imageWidth + this.maxImageHeight = (this.viewHeight / minZoomPixelHeight) * this.imageHeight + } + + return this.fireChange() + } + + // Update view + // ----------- + + zoomAllOut () { + if (this.isWidthRestricting()) { + this.zoom({width: this.viewWidth}) + } else { + this.zoom({height: this.viewHeight}) + } + } + + zoomIn (params) { + if (params == null) { params = {} } + if (this.isWidthRestricting()) { + params.width = this.preview.width * this.zoomInStep + } else { + params.height = this.preview.height * this.zoomInStep + } + + this.zoom(params) + } + + zoomOut (params) { + if (params == null) { params = {} } + if (this.isWidthRestricting()) { + params.width = this.preview.width * this.zoomOutStep + } else { + params.height = this.preview.height * this.zoomOutStep + } + + this.zoom(params) + } + + zoom ({width, height, viewX, viewY, focusPoint}) { + if (focusPoint == null) { focusPoint = this.getFocusPoint({viewX, viewY}) } + + ({width, height} = this.enforceZoom({width, height})) + if (width != null) { + this.preview.setWidth(width) + this.fireChange() + } else if (height != null) { + this.preview.setHeight(height) + this.fireChange() + } + + return this.focus(focusPoint) + } + + // returns {Object} e.g. percentX: 0.2, percentY: 0.5 + getFocusPoint (param) { + if (param == null) { param = {} } + let {viewX, viewY} = param + if (viewX == null) { viewX = this.viewWidth / 2 } + if (viewY == null) { viewY = this.viewHeight / 2 } + const x = this.preview.x + viewX + const y = this.preview.y + viewY + const percentX = x / this.preview.width + const percentY = y / this.preview.height + return {percentX, percentY, viewX, viewY} + } + + focus ({percentX, percentY, viewX, viewY}) { + let x = this.preview.width * percentX + let y = this.preview.height * percentY + x = x - viewX + y = y - viewY + + this.pan({x, y}) + } + + center () { + const newX = (this.preview.width - this.viewWidth) / 2 + const newY = (this.preview.height - this.viewHeight) / 2 + this.pan({x: newX, y: newY}) + } + + // @param { Object } + // - x {Number} pixel to pan to the left + // - y {Number} pixels to pan to the top + pan (data) { + data = this.enforceXy(data) + this.preview.pan(data.x, data.y) + this.fireChange() + } + + // Validations + // ----------- + + enforceXy ({x, y}) { + if (x < 0) { + x = 0 + } else if (x > (this.preview.width - this.viewWidth)) { + x = this.preview.width - this.viewWidth + } + + if (y < 0) { + y = 0 + } else if (y > (this.preview.height - this.viewHeight)) { + y = this.preview.height - this.viewHeight + } + + return {x, y} + } + + enforceZoom ({width, height}) { + + if ((width != null) && this.maxImageWidth && (width > this.maxImageWidth)) { + // prevent zooming in past the required resolution defined by minResolution + return {width: this.maxImageWidth} + } + + if ((width != null) && (width < this.viewWidth)) { + // prevent zooming out past covering the view completely + return {width: this.viewWidth} + } + + if ((height != null) && this.maxImageHeight && (height > this.maxImageHeight)) { + // prevent zooming in past the required resolution defined by minResolution + return {height: this.maxImageHeight} + } + + if ((height != null) && (height < this.viewHeight)) { + // prevent zooming out past covering the view completely + return {height: this.viewHeight} + } + + return {width, height} + } + + calcMaxMinDimensions () { + this.maxWidth = this.min([this.arenaWidth, this.imageWidth]) + this.maxHeight = this.min([this.arenaHeight, this.imageHeight]) + this.minWidth = this.minViewWidth || 0 + this.minHeight = this.minViewHeight || 0 + + if (this.fixedWidth) this.maxWidth = (this.minWidth = this.fixedWidth) + if (this.fixedHeight) this.maxHeight = (this.minHeight = this.fixedHeight) + } + + areDimensionsValid ({width, height, keepDimension}) { + const ratio = width / height + + const invalid = + (width < this.minWidth) || + (width > this.maxWidth) || + (height < this.minHeight) || + (height > this.maxHeight) || + (ratio < this.minViewRatio) || + (ratio > this.maxViewRatio) + + return !invalid + } + + isValidRatio (ratio) { + return !((ratio < this.minViewRatio) || (ratio > this.maxViewRatio)) + } + + enforceValidRatio (ratio) { + if (ratio < this.minViewRatio) return this.minViewRatio + if (ratio > this.maxViewRatio) return this.maxViewRatio + return ratio + } + + enforceViewDimensions ({width, height, keepDimension}) { + let newHeight, newWidth, ratio + if (width < this.minWidth) newWidth = this.minWidth + if (width > this.maxWidth) newWidth = this.maxWidth + if (height < this.minHeight) newHeight = this.minHeight + if (height > this.maxHeight) newHeight = this.maxHeight + + if (keepDimension) { + if (newWidth) width = newWidth + if (newHeight) height = newHeight + + // check max/min ratios + ratio = width / height + if (!this.isValidRatio(ratio)) { + ratio = this.enforceValidRatio(ratio); + ({width, height} = this.getRatioBox({ratio, width, height, keepDimension})) + if ((width > this.arenaWidth) || (height > this.arenaHeight)) { + ({width, height} = this.centerAlign(this.maxWidth, this.maxHeight, ratio)) + } + } + + } else if (newWidth || newHeight) { + ratio = this.enforceValidRatio(width / height); + ({width, height} = this.centerAlign(this.maxWidth, this.maxHeight, ratio)) + } + + return {width, height} + } + + enforceMaxArea ({width, height, keepDimension}) { + let ratio = width / height + + if (keepDimension === 'width') { + height = this.maxArea / width + ratio = width / height + } else if (keepDimension === 'height') { + width = this.maxArea / height + ratio = width / height + } else { // keep ratio + width = Math.sqrt(this.maxArea * ratio) + height = width / ratio + } + + if (!this.isValidRatio(ratio)) { + ratio = this.enforceValidRatio(ratio) + width = Math.sqrt(this.maxArea * ratio) + height = width / ratio + } + + return {width, height} + } + + // Calculations + // ------------ + // + // Ratio: width / height + // Tall < 1 (Square) < Wide + // (A ratio less than one is a tall image format and + // a ratio greater than one is a wide image format) + + // Check if the width or height is restricting + isWidthRestricting () { + return this.viewRatio >= this.imageRatio + } + + getRatioBox ({ratio, width, height, keepDimension}) { + if ((keepDimension === 'width') || (height == null)) { + height = width / ratio + } else if ((keepDimension === 'height') || (width == null)) { + width = height * ratio + } else { + height = width / ratio + } + + return {width, height} + } + + centerAlign (areaWidth, areaHeight, ratio) { + let height, width, x, y + if ((areaWidth / areaHeight) > ratio) { + width = areaHeight * ratio + x = (areaWidth - width) / 2 + } else { + height = areaWidth / ratio + y = (areaHeight - height) / 2 + } + + // return + return { + x: x || 0, + y: y || 0, + width: width || areaWidth, + height: height || areaHeight + } + } + + min (array) { + let min = array[0] + for (const number of array) { + if (number < min) min = number + } + + return min + } + + // Events + // ------ + + on (name, callback) { + return this[`${name}Event`].add(callback) + } + + off (name, callback) { + return this[`${name}Event`].remove(callback) + } + + // Debounce change events so they are not fired more + // than once per tick. + fireChange () { + if (this.changeDispatch != null) return + + this.changeDispatch = setTimeout(() => { + this.changeDispatch = undefined + this.changeEvent.fire(this.getCrop()) + }, 0) + } + + // Development helpers + // ------------------- + + debug () { + const r = num => Math.round(num * 10) / 10 + + const obj = { + arena: `${r(this.arenaWidth)}x${r(this.arenaHeight)}`, + view: `${r(this.viewWidth)}x${r(this.viewHeight)}`, + image: `${r(this.imageWidth)}x${r(this.imageHeight)}`, + preview: `${r(this.preview.width)}x${r(this.preview.height)}`, + previewXy: `${r(this.preview.x)}x${r(this.preview.y)}` + } + + console.log(obj) + return obj + } +} diff --git a/src/events.coffee b/src/events.coffee deleted file mode 100644 index 69ee77c..0000000 --- a/src/events.coffee +++ /dev/null @@ -1,111 +0,0 @@ -$ = require('jquery') - -module.exports = class Events - - constructor: ({ @parent, @view, horizontal, vertical, actions }) -> - @doubleClickThreshold = 300 - - # setup events - @pan() if actions.pan - @doubleClick() if actions.zoomOnDoubleClick - if actions.resize - @resizeView - horizontal: actions.resizeHorizontal - vertical: actions.resizeVertical - - @preventBrowserDragDrop() - @responsiveArena() - - - pan: -> - $doc = $(document) - @view.on 'mousedown.srcissors', (e1) => - panData = - startX: @parent.preview.x - startY: @parent.preview.y - - e1.preventDefault() - - $doc.on 'mousemove.srcissors-pan', (e2) => - panData.dx = e2.pageX - e1.pageX - panData.dy = e2.pageY - e1.pageY - @parent.onPan(panData) - - .on 'mouseup.srcissors-pan', => - $doc.off('mouseup.srcissors-pan') - $doc.off('mousemove.srcissors-pan') - - # only trigger panEnd if pan has been called - @parent.onPanEnd() if panData.dx? - - - doubleClick: -> - lastClick = undefined - - @view.on 'mousedown.srcissors', (event) => - now = new Date().getTime() - if lastClick && lastClick > now - @doubleClickThreshold - @parent.onDoubleClick(pageX: event.pageX, pageY: event.pageY) - else - lastClick = now - - - preventBrowserDragDrop: -> - @view.on('dragstart.srcissors', -> return false) - - - # Resize View - # ----------- - - resizeView: ({ horizontal, vertical }) -> - $template = $('
') - $template.addClass('resize-handler') - - positions = [] - if horizontal - positions = positions.concat(['right', 'left']) - if vertical - positions = positions.concat(['top', 'bottom']) - - positions.forEach (position) => - $handler = $template.clone() - $handler.addClass("resize-handler-#{ position }") - $handler.on 'mousedown.srcissors', @getResizeMouseDown(position) - - @view.append($handler) - - - getResizeMouseDown: (position) -> - $doc = $(document) - - (event) => - lastX = event.pageX - lastY = event.pageY - - event.stopPropagation() - - $doc.on 'mousemove.srcissors-resize', (e2) => - switch position - when 'top', 'bottom' - dy = e2.pageY - lastY - dy = -dy if position == 'top' - lastY = e2.pageY - when 'left', 'right' - dx = e2.pageX - lastX - dx = -dx if position == 'left' - lastX = e2.pageX - - @parent.onResize({ position, dx, dy }) - - .on 'mouseup.srcissors-resize', => - $doc.off('mouseup.srcissors-resize') - $doc.off('mousemove.srcissors-resize') - - # only trigger panEnd if pan has been called - @parent.onResizeEnd({ position }) - - - responsiveArena: -> - # $(window).on 'resize', (event) -> - # console.log 'on window resize' - diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..6029eee --- /dev/null +++ b/src/events.js @@ -0,0 +1,120 @@ +const $ = require('jquery') + +module.exports = class Events { + constructor ({parent, view, horizontal, vertical, actions}) { + this.parent = parent + this.view = view + this.doubleClickThreshold = 300 + + // setup events + if (actions.pan) { this.pan() } + if (actions.zoomOnDoubleClick) { this.doubleClick() } + if (actions.resize) { + this.resizeView({ + horizontal: actions.resizeHorizontal, + vertical: actions.resizeVertical + }) + } + + this.preventBrowserDragDrop() + this.responsiveArena() + } + + pan () { + const $doc = $(document) + this.view.on('mousedown.srcissors', (e1) => { + const panData = { + startX: this.parent.preview.x, + startY: this.parent.preview.y + } + + e1.preventDefault() + $doc.on('mousemove.srcissors-pan', (e2) => { + panData.dx = e2.pageX - e1.pageX + panData.dy = e2.pageY - e1.pageY + this.parent.onPan(panData) + }).on('mouseup.srcissors-pan', () => { + $doc.off('mouseup.srcissors-pan') + $doc.off('mousemove.srcissors-pan') + + // only trigger panEnd if pan has been called + if (panData.dx != null) this.parent.onPanEnd() + }) + }) + } + + doubleClick () { + let lastClick + + this.view.on('mousedown.srcissors', event => { + const now = new Date().getTime() + if (lastClick && (lastClick > (now - this.doubleClickThreshold))) { + this.parent.onDoubleClick({pageX: event.pageX, pageY: event.pageY}) + } + lastClick = now + }) + } + + preventBrowserDragDrop () { + this.view.on('dragstart.srcissors', () => false) + } + + // Resize View + // ----------- + + resizeView ({horizontal, vertical}) { + const $template = $('
') + $template.addClass('resize-handler') + + let positions = [] + if (horizontal) positions = positions.concat(['right', 'left']) + if (vertical) positions = positions.concat(['top', 'bottom']) + + positions.forEach(position => { + const $handler = $template.clone() + $handler.addClass(`resize-handler-${position}`) + $handler.on('mousedown.srcissors', this.getResizeMouseDown(position)) + + this.view.append($handler) + }) + } + + getResizeMouseDown (position) { + const $doc = $(document) + + return (event) => { + let lastX = event.pageX + let lastY = event.pageY + + event.stopPropagation() + + $doc.on('mousemove.srcissors-resize', e2 => { + let dx, dy + switch (position) { + case 'top': case 'bottom': + dy = e2.pageY - lastY + if (position === 'top') { dy = -dy } + lastY = e2.pageY + break + case 'left': case 'right': + dx = e2.pageX - lastX + if (position === 'left') { dx = -dx } + lastX = e2.pageX + break + } + + this.parent.onResize({position, dx, dy}) + }).on('mouseup.srcissors-resize', () => { + $doc.off('mouseup.srcissors-resize') + $doc.off('mousemove.srcissors-resize') + + // only trigger panEnd if pan has been called + this.parent.onResizeEnd({position}) + }) + } + } + + responsiveArena () {} +} + +// $(window).on('resize', (event) => console.log 'on window resize') diff --git a/src/preview.coffee b/src/preview.coffee deleted file mode 100644 index cb3a00b..0000000 --- a/src/preview.coffee +++ /dev/null @@ -1,64 +0,0 @@ -$ = require('jquery') - -module.exports = class Preview - - constructor: ({ @onReady, @img, @opacity, @outline }) -> - @x = @y = 0 - @width = @height = 0 - - @img.on 'load', => - width = @img.width() - height = @img.height() - @ratio = width / height - - @updateImageDimensions({ width, height }) - @onReady(width: @width, height: @height) - @img.show() - - - setImage: ({ @url }) -> - @img.attr('src', @url) - @setBackgroundImage({url: @url}) if @outline - - - setBackgroundImage: ({ url }) -> - if @opacity > 0 - bg_img = $('').css(opacity: @opacity).attr('src', url) - @outline.append(bg_img) - - - reset: -> - @url = undefined - @x = @y = 0 - @width = @height = 0 - @img.attr('src', '') - @img.css(width: '', height: '', transform: '') - @outline.css(transform: '').html('') if @outline - - - setWidth: (width) -> - @img.css(width: "#{ width }px", height: 'auto') - height = width / @ratio - @updateImageDimensions({ width, height }) - - - setHeight: (height) -> - @img.css(width: 'auto', height: "#{ height }px") - width = height * @ratio - @updateImageDimensions({ width, height }) - - - updateImageDimensions: ({ width, height }) -> - @width = width - @height = height - @outline.css(width: "#{ @width }px", height: "#{ @height }px") if @outline - - - pan: (@x, @y) -> - # Without rounding some numbers would not be set to css. - # e.g: '-5.14957320384e-14' - x = Math.round(@x) - y = Math.round(@y) - @img.css(transform: "translate(-#{ x }px, -#{ y }px)") - @outline.css(transform: "translate(-#{ x }px, -#{ y }px)") if @outline - diff --git a/src/preview.js b/src/preview.js new file mode 100644 index 0000000..517dfb2 --- /dev/null +++ b/src/preview.js @@ -0,0 +1,75 @@ +const $ = require('jquery') + +module.exports = class Preview { + + constructor ({onReady, img, opacity, outline}) { + this.onReady = onReady + this.img = img + this.opacity = opacity + this.outline = outline + this.x = (this.y = 0) + this.width = (this.height = 0) + + this.img.on('load', () => { + const width = this.img.width() + const height = this.img.height() + this.ratio = width / height + + this.updateImageDimensions({width, height}) + this.onReady({width: this.width, height: this.height}) + this.img.show() + }) + } + + setImage ({url}) { + this.url = url + this.img.attr('src', this.url) + if (this.outline) this.setBackgroundImage({url: this.url}) + } + + setBackgroundImage ({url}) { + if (this.opacity > 0) { + const bgImg = $('').css({opacity: this.opacity}).attr('src', url) + this.outline.append(bgImg) + } + } + + reset () { + this.url = undefined + this.x = (this.y = 0) + this.width = (this.height = 0) + this.img.attr('src', '') + this.img.css({width: '', height: '', transform: ''}) + if (this.outline) this.outline.css({transform: ''}).html('') + } + + setWidth (width) { + this.img.css({width: `${width}px`, height: 'auto'}) + const height = width / this.ratio + this.updateImageDimensions({width, height}) + } + + setHeight (height) { + this.img.css({width: 'auto', height: `${height}px`}) + const width = height * this.ratio + this.updateImageDimensions({width, height}) + } + + updateImageDimensions ({width, height}) { + this.width = width + this.height = height + if (this.outline) this.outline.css({width: `${this.width}px`, height: `${this.height}px`}) + } + + pan (x1, y1) { + // Without rounding some numbers would not be set to css. + // e.g: '-5.14957320384e-14' + this.x = x1 + this.y = y1 + const x = Math.round(this.x) + const y = Math.round(this.y) + this.img.css({transform: `translate(-${x}px, -${y}px)`}) + if (this.outline) this.outline.css({transform: `translate(-${x}px, -${y}px)`}) + } +} + diff --git a/src/preview_css_zoom.coffee b/src/preview_css_zoom.coffee deleted file mode 100644 index f351954..0000000 --- a/src/preview_css_zoom.coffee +++ /dev/null @@ -1,63 +0,0 @@ -module.exports = class Preview - - constructor: ({ @onReady, @img, @outline }) -> - @x = @y = 0 - @width = @height = 0 - - @img.on 'load', => - @updateImageDimensions() - - @ratio = @width / @height - @originalWidth = @width - @originalHeight = @height - @scaleFactor = 1 - - @img.css(transformOrigin: '0 0 0') - @outline.css(transformOrigin: '0 0 0') if @outline - - @onReady(width: @width, height: @height) - @img.show() - - - setImage: ({ url }) -> - @img.attr('src', url) - - - setWidth: (width) -> - @scale({ width }) - - - setHeight: (height) -> - @scale({ height }) - - - scale: ({ width, height }) -> - if width - height = width / @ratio - else - width = height * @ratio - - @scaleFactor = width / @originalWidth - - @transform(@img) - @transform(@outline) - - @width = width - @height = height - - - transform: ($elem) -> - return unless $elem - $elem.css(transform: "scale(#{ @scaleFactor }) translate(-#{ @x / @scaleFactor }px, -#{ @y / @scaleFactor }px)") - - - updateImageDimensions: -> - @width = @img.width() - @height = @img.height() - @outline.css(width: "#{ @width }px", height: "#{ @height }px") if @outline - - - pan: (@x, @y) -> - @transform(@img) - @transform(@outline) - diff --git a/src/preview_css_zoom.js b/src/preview_css_zoom.js new file mode 100644 index 0000000..cf24443 --- /dev/null +++ b/src/preview_css_zoom.js @@ -0,0 +1,72 @@ +module.exports = class Preview { + + constructor ({onReady, img, outline}) { + this.onReady = onReady + this.img = img + this.outline = outline + this.x = (this.y = 0) + this.width = (this.height = 0) + + this.img.on('load', () => { + this.updateImageDimensions() + + this.ratio = this.width / this.height + this.originalWidth = this.width + this.originalHeight = this.height + this.scaleFactor = 1 + + this.img.css({transformOrigin: '0 0 0'}) + if (this.outline) { this.outline.css({transformOrigin: '0 0 0'}) } + + this.onReady({width: this.width, height: this.height}) + this.img.show() + }) + } + + setImage ({url}) { + this.img.attr('src', url) + } + + setWidth (width) { + this.scale({width}) + } + + setHeight (height) { + this.scale({height}) + } + + scale ({width, height}) { + if (width) { + height = width / this.ratio + } else { + width = height * this.ratio + } + + this.scaleFactor = width / this.originalWidth + + this.transform(this.img) + this.transform(this.outline) + + this.width = width + this.height = height + } + + transform ($elem) { + if (!$elem) return + $elem.css({transform: `scale(${this.scaleFactor}) translate(-${this.x / this.scaleFactor}px, -${this.y / this.scaleFactor}px)`}) + } + + updateImageDimensions () { + this.width = this.img.width() + this.height = this.img.height() + if (this.outline) this.outline.css({width: `${this.width}px`, height: `${this.height}px`}) + } + + pan (x, y) { + this.x = x + this.y = y + this.transform(this.img) + this.transform(this.outline) + } +} + diff --git a/src/srcissors.coffee b/src/srcissors.coffee deleted file mode 100644 index 031893f..0000000 --- a/src/srcissors.coffee +++ /dev/null @@ -1,51 +0,0 @@ -$ = require('jquery') -Crop = require('./crop') - -module.exports = window.srcissors = - - new: ({ - arena, url, fixedWidth, fixedHeight, minWidth, minHeight, - minRatio, maxRatio, maxArea, zoomStep, crop, actions, minResolution, - surroundingImageOpacity, showSurroundingImage - }) -> - arena = $(arena) - view = arena.find('.crop-view') - preview = view.find('.crop-preview') - img = $('') - preview.append(img) - outline = view.find('.crop-outline') - outline = undefined if not outline.length - - allowedActions = - pan: true - zoomOnDoubleClick: true - resize: true - resizeHorizontal: !fixedWidth - resizeVertical: !fixedHeight - - $.extend(allowedActions, actions) - - zoomStep ?= 1.25 - - minWidth ?= 50 - minHeight ?= 50 - - new Crop - url: url # {String} - crop: crop # {Object} Set an inital crop. This is the same as calling setCrop() - arena: arena # {jQuery Element} - view: view # {jQuery Element} - img: img # {jQuery Element} - outline: outline # {jQuery Element or undefined} - showSurroundingImage: showSurroundingImage # {String} always|panning|never - surroundingImageOpacity: surroundingImageOpacity # {Number} e.g. in the 0.0 - 1.0 range - fixedWidth: fixedWidth # {Number} e.g. 300 - fixedHeight: fixedHeight # {Number} e.g. 500 - minViewWidth: minWidth # {Number} e.g. 100 - minViewHeight: minHeight # {Number} e.g. 100 - minViewRatio: minRatio # {Number} e.g. 1.5/2 - maxViewRatio: maxRatio # {Number} e.g. 2/1 - maxArea: maxArea # {Number} 0.8 -> max 80% of arena area are covered by the preview - zoomStep: zoomStep # {Number} e.g. 1.25 -> 125% - actions: allowedActions - minResolution: minResolution diff --git a/src/srcissors.js b/src/srcissors.js new file mode 100644 index 0000000..9b35f5e --- /dev/null +++ b/src/srcissors.js @@ -0,0 +1,54 @@ +const $ = require('jquery') +const Crop = require('./crop') + +module.exports = { + new ({ + arena, url, fixedWidth, fixedHeight, minWidth, minHeight, + minRatio, maxRatio, maxArea, zoomStep, crop, actions, minResolution, + surroundingImageOpacity, showSurroundingImage + }) { + arena = $(arena) + const view = arena.find('.crop-view') + const preview = view.find('.crop-preview') + const img = $('') + preview.append(img) + let outline = view.find('.crop-outline') + if (!outline.length) { outline = undefined } + + const allowedActions = { + pan: true, + zoomOnDoubleClick: true, + resize: true, + resizeHorizontal: !fixedWidth, + resizeVertical: !fixedHeight + } + + $.extend(allowedActions, actions) + + if (zoomStep == null) { zoomStep = 1.25 } + + if (minWidth == null) { minWidth = 50 } + if (minHeight == null) { minHeight = 50 } + + return new Crop({ + url, // {String} + crop, // {Object} Set an inital crop. This is the same as calling setCrop() + arena, // {jQuery Element} + view, // {jQuery Element} + img, // {jQuery Element} + outline, // {jQuery Element or undefined} + showSurroundingImage, // {String} always|panning|never + surroundingImageOpacity, // {Number} e.g. in the 0.0 - 1.0 range + fixedWidth, // {Number} e.g. 300 + fixedHeight, // {Number} e.g. 500 + minViewWidth: minWidth, // {Number} e.g. 100 + minViewHeight: minHeight, // {Number} e.g. 100 + minViewRatio: minRatio, // {Number} e.g. 1.5/2 + maxViewRatio: maxRatio, // {Number} e.g. 2/1 + maxArea, // {Number} 0.8 -> max 80% of arena area are covered by the preview + zoomStep, // {Number} e.g. 1.25 -> 125% + actions: allowedActions, + minResolution + }) + } +} diff --git a/test/specs/srcissors_spec.coffee b/test/specs/srcissors_spec.coffee deleted file mode 100644 index 4a158ce..0000000 --- a/test/specs/srcissors_spec.coffee +++ /dev/null @@ -1,291 +0,0 @@ -$ = require('jquery') -srcissors = require('../../src/srcissors') - -template = """ -
-
-
- - -
- -
-
- """ - -describe 'srcissors', -> - - it 'creates a new instance', -> - html = $(template) - crop = srcissors.new - arena: html - - expect(crop).to.exist - - - describe 'with a 100x100 arena', -> - - beforeEach (done) -> - @arena = $(template) - @arena.css(width: 100, height: 100) - $(document.body).append(@arena) - - # Crop a 400x300 image - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - @crop.on 'ready', done - - - afterEach -> - @arena.remove() - - - it 'has initialized the image correctly', -> - expect(@crop.imageWidth).to.equal(400) - expect(@crop.imageHeight).to.equal(300) - - - it 'fires a change event after ready', (done) -> - @crop.on 'change', (crop) -> - expect(crop).to.deep.equal - x: 0 - y: 0 - width: 400 - height: 300 - - done() - - - describe 'zoom()', -> - - # Zoom a 400x300 image by factor 2 in an arena of 100x100 - # The view should keep the images aspect ratio and have a size of 100x75 - it 'zooms 2x into the center', (done) -> - @crop.zoom(width: 200) - @crop.on 'change', (crop) -> - expect(crop).to.deep.equal - x: 100 - y: 75 - width: 200 - height: 150 - - done() - - - describe 'setRatio()', -> - - it 'sets a square ratio', (done) -> - @crop.setRatio(1) - @crop.on 'change', (crop) -> - expect(crop).to.deep.equal - x: 50 - y: 0 - width: 300 - height: 300 - - done() - - - describe 'setImage()', -> - - beforeEach (done) -> - @crop.on 'load', done - - # Set a different 300x400 image - @crop.setImage('base/test/images/berge.jpg') - - - it 'sets the new image dimensions', -> - expect(@crop.imageWidth).to.equal(300) - expect(@crop.imageHeight).to.equal(400) - - - describe 'reset()', -> - - it 'resets the zoom and ratio to the original', -> - @crop.setRatio(1) - @crop.zoom(width: 200) - @crop.reset() - expect(@crop.getCrop()).to.deep.equal - x: 0 - y: 0 - width: 400 - height: 300 - - - describe 'with a 100x200 arena', -> - - beforeEach (done) -> - @arena = $(template) - @arena.css(width: 100, height: 200) - $(document.body).append(@arena) - - # Crop a 400x300 image - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - @crop.on 'ready', done - - - afterEach -> - @arena.remove() - - - it 'has initialized the view correctly', -> - expect(@crop.viewWidth).to.equal(100) - expect(@crop.viewHeight).to.equal(75) - - - describe 'setRatio()', -> - - it 'sets a square ratio', (done) -> - @crop.setRatio(1) - @crop.on 'change', (crop) -> - expect(crop).to.deep.equal - x: 50 - y: 0 - width: 300 - height: 300 - - done() - - - describe 'when it is loading the image', -> - - beforeEach -> - @arena = $(template) - @arena.css(width: 100, height: 100) - $(document.body).append(@arena) - - # Crop a 400x300 image - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - - - afterEach -> - @arena.remove() - - - it 'calls ready, load and change events', (done) -> - readyIsCalled = 0 - loadIsCalled = 0 - changeIsCalled = 0 - - @crop.on 'ready', -> - readyIsCalled += 1 - expect(changeIsCalled).to.equal(0) - expect(loadIsCalled).to.equal(0) - - @crop.on 'load', -> - loadIsCalled += 1 - expect(readyIsCalled).to.equal(1) - expect(changeIsCalled).to.equal(0) - - @crop.on 'change', (crop) -> - changeIsCalled += 1 - expect(loadIsCalled).to.equal(1) - expect(readyIsCalled).to.equal(1) - done() - - - it 'calling setCrop() before cropper is ready still works', (done) -> - @crop.setRatio(1) - @crop.on 'ready', => - info = @crop.getCrop() - expect(info).to.deep.equal - x: 50 - y: 0 - width: 300 - height: 300 - - done() - - describe 'with surrounding image always enabled', -> - - beforeEach (done) -> - @arena = $(template) - @arena.css(width: 100, height: 100) - $(document.body).append(@arena) - - # Crop a 400x300 image - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - showSurroundingImage: 'always' - surroundingImageOpacity: 0.4 - @crop.on 'ready', done - - - afterEach -> - @arena.remove() - - - it 'has initialized the crop outline background correctly', -> - outline = @arena.find('.crop-outline') - bgImg = outline.find('img') - - expect(bgImg.length).to.equal(1) - expect(bgImg.get(0).style.opacity).to.equal('0.4') - expect(outline.get(0).style.opacity).to.equal('1') - - - it 'cleans up the crop outline when setting a different image', -> - @crop.setImage('base/test/images/berge.jpg') - - bgImg = @arena.find('.crop-outline img') - expect(bgImg.length).to.equal(1) - - describe 'with surrounding image enabled when panning', -> - - beforeEach (done) -> - @arena = $(template) - @arena.css(width: 100, height: 100) - $(document.body).append(@arena) - - # Crop a 400x300 image - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - showSurroundingImage: 'panning' - @crop.on 'ready', done - - - afterEach -> - @arena.remove() - - - it 'has initialized the crop outline background correctly', -> - outline = @arena.find('.crop-outline') - bgImg = outline.find('img') - - expect(bgImg.length).to.equal(1) - expect(bgImg.get(0).style.opacity).to.equal('0.2') - expect(outline.get(0).style.opacity).to.equal('') - - - describe 'with surrounding image disabled by default', -> - - beforeEach -> - @arena = $(template) - @arena.css(width: 100, height: 100) - $(document.body).append(@arena) - - - afterEach -> - @arena.remove() - - - it 'omits the background image without surrounding image config', (done) -> - @crop = srcissors.new - arena: @arena - url: 'base/test/images/diagonal.jpg' - @crop.on 'ready', done - - outline = @arena.find('.crop-outline') - bgImg = outline.find('img') - - expect(bgImg.length).to.equal(0) - expect(outline.get(0).style.opacity).to.equal('0') - \ No newline at end of file diff --git a/test/specs/srcissors_spec.js b/test/specs/srcissors_spec.js new file mode 100644 index 0000000..83e24cf --- /dev/null +++ b/test/specs/srcissors_spec.js @@ -0,0 +1,325 @@ +const $ = require('jquery') +const srcissors = require('../../src/srcissors') + +const template = `\ +
+
+
+ + +
+ +
+
\ +` + +describe('srcissors', function () { + + it('creates a new instance', function () { + const html = $(template) + const crop = srcissors.new({ + arena: html}) + + expect(crop).to.exist + }) + + describe('with a 100x100 arena', function () { + + beforeEach(function (done) { + this.arena = $(template) + this.arena.css({width: 100, height: 100}) + $(document.body).append(this.arena) + + // Crop a 400x300 image + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg' + }) + this.crop.on('ready', done) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('has initialized the image correctly', function () { + expect(this.crop.imageWidth).to.equal(400) + expect(this.crop.imageHeight).to.equal(300) + }) + + it('fires a change event after ready', function (done) { + this.crop.on('change', function (crop) { + expect(crop).to.deep.equal({ + x: 0, + y: 0, + width: 400, + height: 300 + }) + + done() + }) + }) + + describe('zoom()', () => + + // Zoom a 400x300 image by factor 2 in an arena of 100x100 + // The view should keep the images aspect ratio and have a size of 100x75 + it('zooms 2x into the center', function (done) { + this.crop.zoom({width: 200}) + this.crop.on('change', function (crop) { + expect(crop).to.deep.equal({ + x: 100, + y: 75, + width: 200, + height: 150 + }) + + done() + }) + }) + ) + + describe('setRatio()', () => + + it('sets a square ratio', function (done) { + this.crop.setRatio(1) + this.crop.on('change', function (crop) { + expect(crop).to.deep.equal({ + x: 50, + y: 0, + width: 300, + height: 300 + }) + + done() + }) + }) + ) + + describe('setImage()', function () { + + beforeEach(function (done) { + this.crop.on('load', done) + + // Set a different 300x400 image + this.crop.setImage('base/test/images/berge.jpg') + }) + + it('sets the new image dimensions', function () { + expect(this.crop.imageWidth).to.equal(300) + expect(this.crop.imageHeight).to.equal(400) + }) + }) + + describe('reset()', () => + + it('resets the zoom and ratio to the original', function () { + this.crop.setRatio(1) + this.crop.zoom({width: 200}) + this.crop.reset() + expect(this.crop.getCrop()).to.deep.equal({ + x: 0, + y: 0, + width: 400, + height: 300 + }) + }) + ) + }) + + describe('with a 100x200 arena', function () { + + beforeEach(function (done) { + this.arena = $(template) + this.arena.css({width: 100, height: 200}) + $(document.body).append(this.arena) + + // Crop a 400x300 image + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg' + }) + this.crop.on('ready', done) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('has initialized the view correctly', function () { + expect(this.crop.viewWidth).to.equal(100) + expect(this.crop.viewHeight).to.equal(75) + }) + + describe('setRatio()', () => + + it('sets a square ratio', function (done) { + this.crop.setRatio(1) + this.crop.on('change', function (crop) { + expect(crop).to.deep.equal({ + x: 50, + y: 0, + width: 300, + height: 300 + }) + + done() + }) + }) + ) + }) + + describe('when it is loading the image', function () { + + beforeEach(function () { + this.arena = $(template) + this.arena.css({width: 100, height: 100}) + $(document.body).append(this.arena) + + // Crop a 400x300 image + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg' + }) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('calls ready, load and change events', function (done) { + let readyIsCalled = 0 + let loadIsCalled = 0 + let changeIsCalled = 0 + + this.crop.on('ready', function () { + readyIsCalled += 1 + expect(changeIsCalled).to.equal(0) + expect(loadIsCalled).to.equal(0) + }) + + this.crop.on('load', function () { + loadIsCalled += 1 + expect(readyIsCalled).to.equal(1) + expect(changeIsCalled).to.equal(0) + }) + + this.crop.on('change', function (crop) { + changeIsCalled += 1 + expect(loadIsCalled).to.equal(1) + expect(readyIsCalled).to.equal(1) + done() + }) + }) + + it('calling setCrop() before cropper is ready still works', function (done) { + this.crop.setRatio(1) + this.crop.on('ready', () => { + const info = this.crop.getCrop() + expect(info).to.deep.equal({ + x: 50, + y: 0, + width: 300, + height: 300 + }) + + done() + }) + }) + }) + + describe('with surrounding image always enabled', function () { + + beforeEach(function (done) { + this.arena = $(template) + this.arena.css({width: 100, height: 100}) + $(document.body).append(this.arena) + + // Crop a 400x300 image + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg', + showSurroundingImage: 'always', + surroundingImageOpacity: 0.4 + }) + this.crop.on('ready', done) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('has initialized the crop outline background correctly', function () { + const outline = this.arena.find('.crop-outline') + const bgImg = outline.find('img') + + expect(bgImg.length).to.equal(1) + expect(bgImg.get(0).style.opacity).to.equal('0.4') + expect(outline.get(0).style.opacity).to.equal('1') + }) + + it('cleans up the crop outline when setting a different image', function () { + this.crop.setImage('base/test/images/berge.jpg') + + const bgImg = this.arena.find('.crop-outline img') + expect(bgImg.length).to.equal(1) + }) + }) + + describe('with surrounding image enabled when panning', function () { + + beforeEach(function (done) { + this.arena = $(template) + this.arena.css({width: 100, height: 100}) + $(document.body).append(this.arena) + + // Crop a 400x300 image + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg', + showSurroundingImage: 'panning' + }) + this.crop.on('ready', done) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('has initialized the crop outline background correctly', function () { + const outline = this.arena.find('.crop-outline') + const bgImg = outline.find('img') + + expect(bgImg.length).to.equal(1) + expect(bgImg.get(0).style.opacity).to.equal('0.2') + expect(outline.get(0).style.opacity).to.equal('') + }) + }) + + describe('with surrounding image disabled by default', function () { + + beforeEach(function () { + this.arena = $(template) + this.arena.css({width: 100, height: 100}) + $(document.body).append(this.arena) + }) + + afterEach(function () { + this.arena.remove() + }) + + it('omits the background image without surrounding image config', function (done) { + this.crop = srcissors.new({ + arena: this.arena, + url: 'base/test/images/diagonal.jpg' + }) + this.crop.on('ready', done) + + const outline = this.arena.find('.crop-outline') + const bgImg = outline.find('img') + + expect(bgImg.length).to.equal(0) + expect(outline.get(0).style.opacity).to.equal('0') + }) + }) +}) From 341e9c2cd34b55d5ecd62dacd8c4eb4c23f889da Mon Sep 17 00:00:00 2001 From: Marc Bachmann Date: Fri, 14 Sep 2018 00:40:53 +0200 Subject: [PATCH 07/12] chore: Migrate to webpack setup --- .eslintrc.json | 203 +++++++ .gitignore | 1 + examples/index.html | 1 - examples/min-resolution.html | 2 +- examples/srcissors.js | 2 + examples/srcissors.js.map | 1 + karma.conf.js | 15 +- package.json | 39 +- src/crop.js | 16 +- src/preview_css_zoom.js | 2 +- srcissors.js | 1046 +--------------------------------- srcissors.js.map | 1 + srcissors.min.js | 1 - test/specs/srcissors_spec.js | 50 +- webpack.config.js | 40 ++ 15 files changed, 308 insertions(+), 1112 deletions(-) create mode 100644 .eslintrc.json create mode 100644 examples/srcissors.js create mode 100644 examples/srcissors.js.map create mode 100644 srcissors.js.map delete mode 100644 srcissors.min.js create mode 100644 webpack.config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..188f289 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,203 @@ +{ + "parserOptions": { + "ecmaVersion": 8, + "ecmaFeatures": { + "experimentalObjectRestSpread": true, + "jsx": true + }, + "sourceType": "module" + }, + + "env": { + "es6": true, + "node": true, + "mocha": true + }, + + "plugins": [], + + "globals": { + "cy": true, + "Cypress": true, + "document": false, + "navigator": false, + "window": false, + "expect": true, + "test": true, + "sinon": true, + "angular": true, + "inject": true + }, + + "rules": { + "accessor-pairs": 2, + "arrow-spacing": [2, {"before": true, "after": true}], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "camelcase": [2, {"properties": "never"}], + "comma-dangle": [2, "never"], + "comma-spacing": [2, {"before": false, "after": true}], + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "constructor-super": 2, + "curly": [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": [2, "always"], + "eqeqeq": [2, "allow-null"], + "func-call-spacing": [2, "never"], + "callback-return": [1, ["callback", "cb", "done"]], + "handle-callback-err": [2, "^(err|error)$"], + "indent": [2, 2, { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + } + }], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "keyword-spacing": [2, {"before": true, "after": true}], + "linebreak-style": [2, "unix"], + "max-len": ["error", { + "code": 100, + "ignoreRegExpLiterals": true, + "ignorePattern": "\\s+require\\(|https?://" + }], + "new-cap": [2, {"newIsCap": true, "capIsNew": false}], + "new-parens": 2, + "newline-per-chained-call": [2, {"ignoreChainWithDepth": 4}], + "no-array-constructor": 2, + "no-caller": 2, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-console": [1, {"allow": ["error"]}], + "no-const-assign": 2, + "no-constant-condition": [2, {"checkLoops": false}], + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-duplicate-imports": 2, + "no-empty-character-class": 2, + "no-empty-pattern": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-global-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": [2, {"allowLoop": false, "allowSwitch": false}], + "no-lone-blocks": 2, + "no-lonely-if": 2, + "no-mixed-operators": [2, { + "groups": [ + ["+", "-", "*", "/", "%", "**"], + ["&", "|", "^", "~", "<<", ">>", ">>>"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||"], + ["in", "instanceof"] + ], + "allowSamePrecedence": false + }], + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, {"max": 2}], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + "no-new-symbol": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": [2, "except-parens"], + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-shadow": ["error", { "allow": [ + "argv", + "callback", + "cb", + "done", + "err", + "params" + ] }], + "no-sparse-arrays": 2, + "no-tabs": 2, + "no-template-curly-in-string": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + "no-unmodified-loop-condition": 2, + "no-unneeded-ternary": [2, {"defaultAssignment": false}], + "no-unreachable": 2, + "no-unsafe-finally": 2, + "no-unsafe-negation": 2, + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-useless-call": 2, + "no-useless-computed-key": 2, + "no-useless-constructor": 2, + "no-useless-escape": 2, + "no-useless-rename": 2, + "no-var": 2, + "no-whitespace-before-property": 2, + "no-with": 2, + "object-curly-spacing": [2, "never"], + "object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}], + "one-var": [2, {"initialized": "never"}], + "operator-linebreak": [2, "after", {"overrides": {"?": "before", ":": "before"}}], + "padded-blocks": [0, "never"], + "prefer-template": 2, + "prefer-const": [2, {"destructuring": "any", "ignoreReadBeforeAssign": true}], + "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], + "rest-spread-spacing": [2, "never"], + "semi": [2, "never"], + "semi-spacing": [2, {"before": false, "after": true}], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, {"words": true, "nonwords": false}], + "spaced-comment": [2, "always", + {"line": {"markers": ["*package", "!", ","]}, "block": {"balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"]}} + ], + "template-curly-spacing": [2, "never"], + "unicode-bom": [2, "never"], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any", {"functionPrototypeMethods": true}], + "yield-star-spacing": [2, "both"], + "yoda": [2, "never"] + } +} diff --git a/.gitignore b/.gitignore index 6daee3e..faf2a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store /.tmp node_modules +/.fusebox diff --git a/examples/index.html b/examples/index.html index 72d559d..01046cf 100644 --- a/examples/index.html +++ b/examples/index.html @@ -48,7 +48,6 @@ - + - + - + - +