├── dist ├── imgViewer.min.css └── imgViewer.min.js ├── .gitignore ├── .github └── FUNDING.yml ├── docs ├── images │ └── test_image.jpg ├── dynamic_decorated_image.html ├── fixed_image_size.html ├── dynamic_image_size.html ├── custom_onclick_callback.html ├── multiple_viewers.html ├── control_options.html └── lib │ ├── imgViewer.js │ └── hammer.min.js ├── src ├── .jshintrc └── imgViewer.js ├── plugindocs ├── extending.md ├── usage.md ├── releases.md ├── methods.adoc └── options.adoc ├── test ├── .jshintrc ├── imgViewer.html ├── imgViewer_options.js └── imgViewer_methods.js ├── LICENSE-MIT ├── package.json └── README.md /dist/imgViewer.min.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .jshintrc 3 | .directory 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: waynegm 4 | -------------------------------------------------------------------------------- /docs/images/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waynegm/imgViewer/HEAD/docs/images/test_image.jpg -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": ["jQuery"] 15 | } 16 | -------------------------------------------------------------------------------- /plugindocs/extending.md: -------------------------------------------------------------------------------- 1 | # Extending 2 | The imgViewer widget is built using the [jQuery UI Widget Factory](https://learn.jquery.com/jquery-ui/widget-factory/). Extending the functionaity of the widget is a relatively simple process as described in [Extending Widgets with the Widget Factory](https://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/). 3 | 4 | As an example 5 | ```javascript 6 | $.widget("wgm.imgExtension", $.wgm.imgViewer, { 7 | options: { 8 | } 9 | }); 10 | ``` 11 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": [ 15 | "jQuery", 16 | "QUnit", 17 | "module", 18 | "test", 19 | "asyncTest", 20 | "expect", 21 | "start", 22 | "stop", 23 | "ok", 24 | "equal", 25 | "notEqual", 26 | "deepEqual", 27 | "notDeepEqual", 28 | "strictEqual", 29 | "notStrictEqual", 30 | "throws" 31 | ] 32 | } -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Wayne Mogg 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/imgViewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | imgNotes Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /plugindocs/usage.md: -------------------------------------------------------------------------------- 1 | #Using the Plugin 2 | ## Dependencies 3 | The plugin has the following dependencies: 4 | - [jQuery](http://jquery.com/) (>=1.8) 5 | - [jQuery UI](http://jqueryui.com/) (>=1.8) 6 | * [Widget Factory](http://api.jqueryui.com/jQuery.widget/) 7 | - [Hammer.js](http://hammerjs.github.io/) (>=2.0.8) 8 | - [jquery.hammer.js](https://github.com/hammerjs/jquery.hammer.js) (>=2.0.0) 9 | - [jquery-mousewheel](https://github.com/brandonaaron/jquery-mousewheel) (>=3.0) 10 | 11 | ## Usage 12 | Include either the development version or minified production version of the JS file located 13 | in the `dist` folder and associated dependencies into your web page: 14 | 15 | ```html 16 | 17 | ... 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ... 26 | 27 | ``` 28 | Put an image element and a javascript block to attach the plugin to the image in the web page body: 29 | 30 | ```html 31 | 32 | ... 33 | 34 | ... 35 | 42 | ... 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imgviewer", 3 | "version": "1.0.1", 4 | "description": "extensible, responsive jquery plugin to zoom and pan images", 5 | "keywords": ["jquery-plugin", "ecosystem:jquery", "image"], 6 | "repository": {"type": "git", "url": "https://github.com/waynegm/imgViewer.git"}, 7 | "homepage": "https://github.com/waynegm/imgViewer", 8 | "bugs": "https://github.com/waynegm/imgViewer/issues", 9 | "main": "src/imgViewer.js", 10 | "dependencies": { 11 | "jquery": "^1.8 || ^2.0 || ^3.0", 12 | "hammerjs": "^2.0.8", 13 | "jquery-hammerjs": "^2.0.0", 14 | "jquery-mousewheel": "^3.1.13" 15 | }, 16 | "scripts": { 17 | "clean": "rimraf dist/*", 18 | "lint": "jshint src/*.js", 19 | "postlint": "cp src/*.js docs/lib", 20 | "minify:js": "echo '=> minify:js' && uglifyjs src/*.js -o dist/imgViewer.min.js", 21 | "minify:css": "echo '=> minify:css' && cleancss -o dist/imgViewer.min.css src/*.css", 22 | "prebuild:js": "npm run lint", 23 | "build:js": "npm run minify:js", 24 | "build:css": "npm run minify:css", 25 | "build": "echo '=> building' && npm run clean && npm run build:css && npm run build:js", 26 | "push": "git push", 27 | "patch-release": "npm version patch && npm publish", 28 | "minor-release": "npm version minor && npm publish", 29 | "major-release": "npm version major && npm publish", 30 | "watch": "watch 'npm run build' src/", 31 | "server": "http-server docs/" 32 | }, 33 | "author": "waynegm", 34 | "license": "MIT", 35 | "devDependencies": { 36 | "clean-css-cli": "^4.0.0", 37 | "http-server": "^0.9.0", 38 | "jshint": "^2.9.4", 39 | "rimraf": "^2.5.4", 40 | "uglify-js": "^2.7.5", 41 | "watch": "^1.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/dynamic_decorated_image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - dynamic decorated image example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

imgViewer Plugin - dynamic decorated image example

16 |
17 | 18 |
19 |

This example demonstrates the widget with a decorated (borders and padding) dynamic width image. 20 | Zoom in and out using the mousewheel. Left mouseclick and drag to pan. On touch enable devices pinch gestures can be used to zoom in and out and tap and drag to pan around. 21 |

22 |

Resize the window and try again 23 |

24 | 25 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | imgViewer 2 | ========= 3 | 4 | imgViewer is a jQuery plugin that allows an image to be zoomed and panned. Zooming and panning works on desktop browsers using the mousewheel to zoom in and out left mouse button click and drag to pan around. On touch enabled devices pinch gestures can be used to zoom in and out and tap and drag to pan around. 5 | 6 | It is an easily extended, responsive UI component based on the jQuery widget factory and uses [Hammer.js](http://hammerjs.github.io/) for touch event support. It should work on all browsers and platforms supported by [Hammer.js](http://hammerjs.github.io/). 7 | 8 | If you don't find this plugin has what you need check out my [imgViewer2](https://github.com/waynegm/imgViewer2) plugin which provides the same functionality but uses the [Leaflet](http://leafletjs.com/index.html) mapping library for the underlying image display. 9 | 10 | ## Examples 11 | 1. [Fixed size image](http://waynegm.github.io/imgViewer/fixed_image_size.html) 12 | 2. [Dynamic image size](http://waynegm.github.io/imgViewer/dynamic_image_size.html) 13 | 3. [Decorated, dynamic image size](http://waynegm.github.io/imgViewer/dynamic_decorated_image.html) 14 | 4. [Custom onClick callback](http://waynegm.github.io/imgViewer/custom_onclick_callback.html) 15 | 5. [Control options](http://waynegm.github.io/imgViewer/control_options.html) 16 | 6. [Multiple viewer](http://waynegm.github.io/imgViewer/multiple_viewers.html) 17 | 18 | ## Documentation 19 | 1. [Using the plugin](plugindocs/usage.md) 20 | 2. [Plugin options](plugindocs/options.adoc) 21 | 3. [Plugin methods](plugindocs/methods.adoc) 22 | 4. [Extending the plugin](plugindocs/extending.md) 23 | 5. [Release History](releases.md) 24 | 25 | ## License 26 | 27 | This plugin is provided under the [MIT License](http://opensource.org/licenses/MIT). 28 | Copyright (c) 2017 Wayne Mogg. 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/fixed_image_size.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - Fixed image size example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 25 | 26 |
18 |

imgViewer Plugin - fixed image size example

19 |
20 | 21 |
22 |

This example demonstrates the basic functionality for a plain fixed size image. Zoom in and out using the mousewheel. Left mouseclick and drag to pan. On touch enable devices pinch gestures can be used to zoom in and out and tap and drag to pan around. 23 |

24 |
27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/dynamic_image_size.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - dynamic image size example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 |
19 |

imgViewer Plugin - dynamic image size example

20 |
21 | 22 |
23 |

This example demonstrates the widget with a dynamic image width. Zoom in and out using the mousewheel. Left mouseclick and drag to pan. On touch enable devices pinch gestures can be used to zoom in and out and tap and drag to pan around. 24 |

25 |

Resize the window and try again.

26 |
29 | 30 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /plugindocs/releases.md: -------------------------------------------------------------------------------- 1 | #Release History 2 | ## 1.0.1 3 | - replace call to img.load() event trigger depreciated in jQuery 3 with img.trigger("load") 4 | ## 1.0.0 5 | - Restructure repository layout 6 | - Change build system from grunt to vanilla npm 7 | - Edit package.json for npmjs package publishing 8 | - Move examples to docs folder for serving by ghpages 9 | - Revamp examples 10 | - Split documetation out of README.md and place in plugindocs folder 11 | - Update Hammerjs to latest version (2.0.8) 12 | - Fix pageX and pageY for IE11 tap events 13 | - Fix drag bug in Firefox 14 | - Improve behaviour of touch gestures 15 | 16 | ## 0.9.1 17 | - Add zoomMax option to limit maximum possible zoom level 18 | 19 | ## 0.9.0 20 | - Replace jquery.event.ue with hammer.js and jquery.hammer.js for more flexibility with touch gesture support 21 | - Add dragable option allowing user to disable dragging 22 | 23 | ## 0.8.0 24 | - Replace toe.js with jquery.event.ue for better touch gesture support 25 | 26 | ## 0.7.4 27 | - Fix for triggering of drag events during pinch gestures 28 | 29 | ## 0.7.3 30 | - Fix multiple click/tap events when using jQuery Mobile 31 | 32 | ## 0.7.2 33 | - Add dependency on jquery-mousewheel 34 | - Stop IE 10 & 11 continuously dragging image 35 | 36 | ## 0.7.1 37 | - Add zoomable option allowing user to disable zooming 38 | - Fix bug in drag implementation on mobile devices 39 | 40 | ## 0.7 41 | - Added support for pinch and drag touch gestures for mobile device support (adds requirement for toe.js). 42 | - Added dependency on the requestAnimationFrame polyfill provided by Zoetrope for more responsive image scaling and dragging. 43 | - Changed to using css transform to scale and translate image for better performance on mobile platforms. 44 | - Minimum IE supported is now IE 9 - stick with version 0.6 if you need IE 8 support. 45 | - Updated Grunfile.js to include tests against latest version (2.1.0) of jQuery. 46 | 47 | ## 0.6 48 | - Major refactoring of the code to make it work in IE8. 49 | - Instead of manipulating a background image a new image element with the same src as the original image is positioned over it. 50 | - Added the panTo, getView, isVisible, imgtoView and viewToImg public methods. 51 | - Added unit tests to cover most of the code. 52 | 53 | ## 0.5 54 | - Proof of concept - everything seems to work as I want but unit tests are needed and the exposed interface may need refinement to increase it's flexibility and usefulness. 55 | -------------------------------------------------------------------------------- /test/imgViewer_options.js: -------------------------------------------------------------------------------- 1 | /* 2 | * imgViewer_options.js 3 | */ 4 | (function($) { 5 | module("imgViewer: options"); 6 | 7 | test( "zoom option", 3, function() { 8 | var $img = $("#qunit-fixture img"); 9 | var zoom = 5; 10 | var tst = $img.imgViewer({ 11 | zoom: zoom 12 | }); 13 | equal(tst.imgViewer("option", "zoom"), zoom, "set zoom option in constructor"); 14 | zoom = 2; 15 | tst.imgViewer("option", "zoom", zoom); 16 | equal(tst.imgViewer("option", "zoom"), zoom, "set zoom option on built object"); 17 | tst.imgViewer("option", "zoom", 0.5); 18 | equal(tst.imgViewer("option", "zoom"), zoom, "no change if zoom is less than 1"); 19 | tst.remove(); 20 | }); 21 | 22 | test( "zoomStep option", 3, function() { 23 | var $img = $("#qunit-fixture img"); 24 | var step = 0.5; 25 | var tst = $img.imgViewer({ 26 | zoomStep: step 27 | }); 28 | equal(tst.imgViewer("option", "zoomStep"), step, "set zoomStep option in constructor"); 29 | step = 0.25; 30 | tst.imgViewer("option", "zoomStep", step); 31 | equal(tst.imgViewer("option", "zoomStep"), step, "set zoomStep option on built object"); 32 | tst.imgViewer("option", "zoomStep", -0.2); 33 | equal(tst.imgViewer("option", "zoomStep"), step, "no change if zoomStep is less than 0"); 34 | tst.remove(); 35 | }); 36 | 37 | test( "zoomMax option", 3, function() { 38 | var $img = $("#qunit-fixture img"); 39 | var zmax = 5; 40 | var tst = $img.imgViewer({ 41 | zoomMax: zmax 42 | }); 43 | equal(tst.imgViewer("option", "zoomMax"), zmax, "set zoomMax option in constructor"); 44 | zmax = 4; 45 | tst.imgViewer("option", "zoomMax", zmax); 46 | equal(tst.imgViewer("option", "zoomMax"), zmax, "set zoomMax option on built object"); 47 | tst.imgViewer("option", "zoom", 5); 48 | equal(tst.imgViewer("option", "zoom"), zmax, "zoom restricted by zoomMax"); 49 | tst.remove(); 50 | }); 51 | 52 | test( "zoomable option", 2, function() { 53 | var $img = $("#qunit-fixture img"); 54 | var zoom = false; 55 | var tst = $img.imgViewer({ 56 | zoomable: zoom 57 | }); 58 | equal(tst.imgViewer("option", "zoomable"), zoom, "set zoomable option in constructor"); 59 | zoom = true; 60 | tst.imgViewer("option", "zoomable", zoom); 61 | equal(tst.imgViewer("option", "zoomable"), zoom, "set zoomable option on built object"); 62 | tst.remove(); 63 | }); 64 | 65 | test( "dragable option", 2, function() { 66 | var $img = $("#qunit-fixture img"); 67 | var drag = false; 68 | var tst = $img.imgViewer({ 69 | dragable: drag 70 | }); 71 | equal(tst.imgViewer("option", "dragable"), drag, "set dragable option in constructor"); 72 | drag = true; 73 | tst.imgViewer("option", "dragable", drag); 74 | equal(tst.imgViewer("option", "dragable"), drag, "set dragable option on built object"); 75 | tst.remove(); 76 | }); 77 | 78 | 79 | }(jQuery)); 80 | -------------------------------------------------------------------------------- /docs/custom_onclick_callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - custom onClick callback example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

imgViewer Plugin - custom onClick callback example

18 |
19 | 20 |
21 |

22 | This example demonstrates the widget with a custom "onClick" callback. In this case clicking on the image will show the page pixel coordinates, image pixel coordinates and relative image coordinates (relative image coordinates range from 0,0 to 1,1 where 0,0 is the top-left corner and 1,1 is the bottom-right corner of the image) in a modal dialog. 23 |

24 |

25 | Zoom in and out using the mousewheel. Left mouseclick and drag to pan. On touch enable devices pinch gestures can be used to zoom in and out and tap and drag to pan around. 26 |

27 |

28 | Resize the window and try again 29 |

30 | 31 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /plugindocs/methods.adoc: -------------------------------------------------------------------------------- 1 | = Plugin Methods 2 | :toc: 3 | :toc-placement!: 4 | 5 | toc::[] 6 | 7 | == destroy - Destroys the plugin 8 | * No Arguments 9 | * No return 10 | 11 | == cursorToImg - Convert page pixel coordinates to relative image coordinate 12 | * Arguments: 13 | * pageX: x coordinate in pixel(page) coordinates 14 | * pageY: y coordiante in pixel(page) coordinates 15 | * Returns javascript object with relative image coordinates (relative image coordinates range from 0 to 1 16 | where 0,0 correspondes to the topleft corner and 1,1 the bottom right): 17 | * { x: relative x image coordinate, y: relative y image coordinate } 18 | * Returns null if the page coordinate is outside the image viewport. 19 | 20 | == getView - Get the relative image coordinates of the current view 21 | * Returns a javascript object with the relative image coordinates: 22 | * { top: minimum relative y coordinate, 23 | * left: minimum relative x coordinate, 24 | * bottom: maximum relative y image coordinate, 25 | * right: maximum relative x coordinate 26 | * } 27 | 28 | == imgToCursor - Convert relative image coordinate to a page pixel coordinate 29 | * Arguments: 30 | * relx: relative x image coordinate 31 | * rely: relative y image coordinate 32 | * Returns a javascript object with the page pixel coordinates: 33 | * { x: the x page pixel coordinate, y: the y page pixel coordinate } 34 | * Returns null if the relative image coordinates are not >=0 and <=1. 35 | 36 | == imgToView - Convert relative image coordinate to a viewport pixel location 37 | * Arguments: 38 | * relx: relative x image coordinate 39 | * rely: relative y image coordinate 40 | * Returns a javascript object with the viewport pixel coordinates: 41 | * { x: the x viewport pixel coordinate, y: the y viewport pixel coordinate } 42 | * Returns null if the relative image coordinates are not >=0 and <=1. 43 | 44 | == viewToImg - Convert a viewport pixel location to a relative image coordinate 45 | * Arguments: 46 | * viewX: x coordinate in viewport pixels 47 | * viewY: y coordinate in viewport pixels 48 | * Returns a javascript object with the relative image coordinates: 49 | * { x: relative x image coordinate, y: relative y image coordinate } 50 | * Returns null if the viewport pixel location is outside the zoomed image. 51 | 52 | == relposToImage - Convert relative image point to image pixel point 53 | * Arguments: 54 | * { x: relative x image coordinate, y: relative y image coordinate } 55 | * Returns a javascript object with the image pixel coordinates: 56 | * { x: the x image pixel coordinate, y: the y image pixel coordinate } 57 | * 58 | == isVisible - Is relative image coordinate visible 59 | * Arguments: 60 | * relx: relative x image coordinate 61 | * rely: relative y image coordinate 62 | * Returns 63 | * true or false 64 | 65 | == panTo - Pan to a relative image coordinates 66 | * Arguments: 67 | * relx: relative x image coordinate 68 | * rely: relative y image coordinate 69 | * Returns a javascript object with the relative image coordinates of the view centre after snapping the edges of the zoomed image to the view boundaries. 70 | * { x: view center relative x image coordinate, y: view center relative y image coordinate } 71 | * Returns null if the relative image coordinates are not >=0 and <=1 and the view is not changed. 72 | 73 | == update - Update the image display 74 | * No arguments 75 | * No return 76 | -------------------------------------------------------------------------------- /plugindocs/options.adoc: -------------------------------------------------------------------------------- 1 | = Plugin Options 2 | :toc: 3 | :toc-placement!: 4 | 5 | The widget options can be set at the time of creation: 6 | [source, javascript] 7 | var $img = $("#image1").imgViewer({ 8 | dragable: false, 9 | onClick: function( e, pos ) { 10 | $("#position").html("relx: " + pos.x + " rely: " + pos.y + " zoom: " + this.options.zoom() ); 11 | } 12 | }); 13 | 14 | or afterwards by: 15 | [source, javascript] 16 | $img.imgViewer("option", "zoomable", false); 17 | 18 | The current value of an option can be retrieved by: 19 | [source, javascript] 20 | $img.imgViewer("option", "zoomMax"); 21 | 22 | toc::[] 23 | == dragable - Controls if image will be dragable 24 | * Default: true 25 | * Example - to disable image dragging: 26 | 27 | [source, javascript] 28 | $("#image1").imgViewer("option", "dragable", false); 29 | 30 | == zoomStep - controls zoom change for each mousewheel click 31 | * Default: 0.1 32 | * Example: 33 | 34 | [source, javascript] 35 | $("#image1").imgViewer("option", "zoomStep", 0.05); 36 | 37 | == zoom - Get/Set the current zoom level 38 | * Default: 1 (ie the entire image is visible) 39 | * Example - to display the image magnified 3x: 40 | 41 | [source, javascript] 42 | $("#image1").imgViewer("option", "zoom", 3); 43 | 44 | == zoomMax - Get/Set the zoom limitage 45 | * Default: 0 (ie no limit on zoom) 46 | * Note: values less than 1 have no affect 47 | * Example - to restrict zoom to 3x or less: 48 | 49 | [source, javascript] 50 | $("#image1").imgViewer("option", "zoomMax", 3); 51 | 52 | == zoomable - Controls if image will be zoomable 53 | * Default: true 54 | * Example - to disable image zooming: 55 | 56 | [source, javascript] 57 | $("#image1").imgViewer("option", "zoomable", false); 58 | 59 | == onClick - mouseclick callback on the image 60 | * Default: $.noop 61 | * Note: within the callback function `this` refers to the imgViewer object 62 | * Callback Arguments: 63 | * e: the original click event object 64 | * Example - to display the relative image coordinate clicked (relative image coordinates range from 0 to 1 65 | where 0,0 correspondes to the topleft corner and 1,1 the bottom right): 66 | 67 | [source, javascript] 68 | $("#image1").imgViewer("option", "onClick", function(e) { 69 | var pos = this.cursorToImg( e.pageX, e.pageY); 70 | $("#click_position").html(e.pageX + " " + e.pageY + " " + pos.x + " " + pos.y); 71 | }); 72 | 73 | == onUpdate - Callback triggered after the image has been redisplayed 74 | * Default: $.noop 75 | * Note: within the callback function `this` refers to the imgViewer object 76 | * Callback Arguments: 77 | * None 78 | * Example - to display the relative image coordinate at the centre of the view: 79 | 80 | [source, javascript] 81 | $("#image1").imgViewer("option", "onUpdate", function() { 82 | var pos = { 83 | relx: this.vCenter.x/$(this.img).width(), 84 | rely: this.vCenter.y/$(this.img).height() 85 | }; 86 | $("#centre_position").html(pos.relx + " " + pos.rely); 87 | }); 88 | 89 | == onReady - Callback triggered when the plugin is fully initialised 90 | * Default: $.noop 91 | * Note: within the callback function `this` refers to the imgViewer object 92 | * Callback Arguments: 93 | * None 94 | * Example - to zoom and pan the image to a focal point on widget load: 95 | 96 | [source, javascript] 97 | $("#image1").imgViewer({ 98 | onReady: function() { 99 | this.options.zoom = 3; 100 | this.panTo(1,1); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /docs/multiple_viewers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - Multi-viewer example 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 38 | 39 |
18 |

imgViewer Plugin - multi-viewer example

19 | 20 |

No Container - Fixed Image Size

21 | 22 |

Click Position

23 | 24 |

No Container - Dynamic Width

25 | 26 |

Click Position

27 | 28 |

No Container - Fixed Size with Border and Padding

29 | 30 |

Click Position

31 | 32 |

Centred in a div - Dynamic Width

33 |
34 | 35 |

Click Position

36 |
37 |
40 | 41 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /docs/control_options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imgViewer Plugin - control options 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 33 | 34 |
18 |

imgViewer Plugin - control options

19 |
20 | 21 |
22 | 23 |

24 | Zoom: Maximum Zoom: 25 |
26 |

27 | This example demonstrates how the widget behaviour can be controlled by option settings. 28 |

29 |

30 | Zoom and drag can be enabled/disabled using the buttons.The maximum zoom level and the zoom can be set. In a custom onReady callback the intial zoom and center view are set using widget methods. A custom onUpdate callback is used to update the zoom value displayed. 31 |

32 |
35 | 36 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /dist/imgViewer.min.js: -------------------------------------------------------------------------------- 1 | var waitForFinalEvent=function(){var timers={};return function(callback,ms,uniqueId){if(!uniqueId){uniqueId="Don't call this twice without a uniqueId"}if(timers[uniqueId]){clearTimeout(timers[uniqueId])}timers[uniqueId]=setTimeout(callback,ms)}}();(function($){$.widget("wgm.imgViewer",{options:{zoomStep:.1,zoom:1,zoomMax:undefined,zoomable:true,dragable:true,onReady:$.noop,onClick:$.noop,onUpdate:$.noop},_create:function(){var self=this;if(!this.element.is("img")){$.error("imgviewer plugin can only be applied to img elements")}self.img=self.element[0];var $img=$(self.img);self.zimg=$("",{src:self.img.src}).appendTo("body").wrap("
");var $zimg=$(self.zimg);self.view=$(self.zimg).parent();var $view=$(self.view);self.vCenter={};self.drag=false;self.pinch=false;self.ready=false;$img.one("load",function(){self.ready=true;var width=$img.width(),height=$img.height(),offset=$img.offset();self.offsetPadding={top:parseInt($img.css("padding-top"),10),left:parseInt($img.css("padding-left"),10),right:parseInt($img.css("padding-right"),10),bottom:parseInt($img.css("padding-bottom"),10)};self.offsetBorder={x:Math.round(($img.outerWidth()-$img.innerWidth())/2),y:Math.round(($img.outerHeight()-$img.innerHeight())/2)};var vTop=offset.top+self.offsetBorder.y+self.offsetPadding.top,vLeft=offset.left+self.offsetBorder.x+self.offsetPadding.left;$view.css({position:"absolute",overflow:"hidden",top:vTop+"px",left:vLeft+"px",width:width+"px",height:height+"px"});$zimg.css({position:"relative",top:0+"px",left:0+"px",width:width+"px",height:height+"px","-webkit-tap-highlight-color":"transparent"});self.vCenter={x:width/2,y:height/2};self.update()}).each(function(){if(this.complete){$(this).trigger("load")}});self.render=false;$zimg.hammer();if(self.options.zoomable){self._bind_zoom_events()}if(self.options.dragable){self._bind_drag_events()}$zimg.on("tap",function(ev){ev.preventDefault();if(!self.dragging){var scoff=self._get_scroll_offset();ev.pageX=ev.gesture.center.x+scoff.x;ev.pageY=ev.gesture.center.y+scoff.y;self.options.onClick.call(self,ev)}});$(window).resize(function(){self._view_resize();waitForFinalEvent(function(){self._view_resize()},300,$img[0].id)});self._view_resize();self.options.onReady.call(self)},_get_scroll_offset:function(){var sx,sy;if(window.scrollX===undefined){if(window.pageXOffset===undefined){sx=document.documentElement.scrollLeft;sy=document.documentElement.scrollTop}else{sx=window.pageXOffset;sy=window.pageYOffset}}else{sx=window.scrollX;sy=window.scrollY}return{x:sx,y:sy}},_view_resize:function(){if(this.ready){var $view=$(this.view),$img=$(this.img),width=$img.width(),height=$img.height(),offset=$img.offset(),vTop=Math.round(offset.top+this.offsetBorder.y+this.offsetPadding.top),vLeft=Math.round(offset.left+this.offsetBorder.x+this.offsetPadding.left);this.vCenter.x*=$img.width()/$view.width();this.vCenter.y*=$img.height()/$view.height();$view.css({top:vTop+"px",left:vLeft+"px",width:width+"px",height:height+"px"});this.update()}},_bind_zoom_events:function(){var self=this;var $zimg=$(self.zimg);function doRender(){if(self.render){window.requestAnimationFrame(doRender);self.update()}}function startRenderLoop(){if(!self.render){self.render=true;doRender()}}function stopRenderLoop(){self.render=false}$zimg.on("mousewheel",function(ev){ev.preventDefault();var delta=ev.deltaY;self.options.zoom-=delta*self.options.zoomStep;self.update()});$zimg.on("touchmove",function(e){e.preventDefault()});$zimg.data("hammer").recognizers[1].options.enable=true;$zimg.on("pinchstart",function(){});$zimg.on("pinch",function(ev){ev.preventDefault();if(!self.pinch){var scoff=self._get_scroll_offset();self.pinchstart={x:ev.gesture.center.x+scoff.x,y:ev.gesture.center.y+scoff.y};self.pinchstartrelpos=self.cursorToImg(self.pinchstart.x,self.pinchstart.y);self.pinchstart_scale=self.options.zoom;startRenderLoop();self.pinch=true}else{self.options.zoom=ev.gesture.scale*self.pinchstart_scale;var npos=self.imgToCursor(self.pinchstartrelpos.x,self.pinchstartrelpos.y);self.vCenter.x=self.vCenter.x+(npos.x-self.pinchstart.x)/self.options.zoom;self.vCenter.y=self.vCenter.y+(npos.y-self.pinchstart.y)/self.options.zoom}});$zimg.on("pinchend",function(ev){ev.preventDefault();if(self.pinch){stopRenderLoop();self.update();self.pinch=false}})},_bind_drag_events:function(){var self=this;var $zimg=$(self.zimg);function doRender(){if(self.render){window.requestAnimationFrame(doRender);self.update()}}function startRenderLoop(){if(!self.render){self.render=true;doRender()}}function stopRenderLoop(){self.render=false}$zimg.on("mousedown",function(e){e.preventDefault()});$zimg.on("panstart",function(){});$zimg.on("panmove",function(ev){ev.preventDefault();if(!self.drag){self.drag=true;self.dragXorg=self.vCenter.x;self.dragYorg=self.vCenter.y;startRenderLoop()}else{self.vCenter.x=self.dragXorg-ev.gesture.deltaX/self.options.zoom;self.vCenter.y=self.dragYorg-ev.gesture.deltaY/self.options.zoom}});$zimg.on("panend",function(ev){ev.preventDefault();if(self.drag){self.drag=false;stopRenderLoop();self.update()}})},_unbind_zoom_events:function(){var self=this;var $zimg=$(self.zimg);$zimg.data("hammer").recognizers[1].options.enable=false;$zimg.off("mousewheel");$zimg.off("pinchstart");$zimg.off("pinch");$zimg.off("pinchend")},_unbind_drag_events:function(){var self=this;var $zimg=$(self.zimg);$zimg.off("pan");$zimg.off("panend")},destroy:function(){var $zimg=$(this.zimg);$zimg.unbind("click");$(window).unbind("resize");$zimg.remove();$(this.view).remove();$.Widget.prototype.destroy.call(this)},_setOption:function(key,value){switch(key){case"zoom":if(parseFloat(value)<1||isNaN(parseFloat(value))){return}break;case"zoomStep":if(parseFloat(value)<=0||isNaN(parseFloat(value))){return}break;case"zoomMax":if(parseFloat(value)<1||isNaN(parseFloat(value))){return}break}var version=$.ui.version.split(".");if(version[0]>1||version[1]>8){this._super(key,value)}else{$.Widget.prototype._setOption.apply(this,arguments)}switch(key){case"zoom":if(this.ready){this.update()}break;case"zoomable":if(this.options.zoomable){this._bind_zoom_events()}else{this._unbind_zoom_events()}break;case"dragable":if(this.options.dragable){this._bind_drag_events()}else{this._unbind_drag_events()}break;case"zoomMax":if(this.ready){this._view_resize();this.update()}break}},addElem:function(elem){$(this.view).append(elem)},isVisible:function(relx,rely){var view=this.getView();if(view){return relx>=view.left&&relx<=view.right&&rely>=view.top&&rely<=view.bottom}else{return false}},getView:function(){if(this.ready){var $img=$(this.img),width=$img.width(),height=$img.height(),zoom=this.options.zoom;return{top:this.vCenter.y/height-.5/zoom,left:this.vCenter.x/width-.5/zoom,bottom:this.vCenter.y/height+.5/zoom,right:this.vCenter.x/width+.5/zoom}}else{return null}},panTo:function(relx,rely){if(this.ready&&relx>=0&&relx<=1&&rely>=0&&rely<=1){var $img=$(this.img),width=$img.width(),height=$img.height();this.vCenter.x=relx*width;this.vCenter.y=rely*height;this.update();return{x:this.vCenter.x/width,y:this.vCenter.y/height}}else{return null}},imgToView:function(relx,rely){if(this.ready&&relx>=0&&relx<=1&&rely>=0&&rely<=1){var $img=$(this.img),width=$img.width(),height=$img.height();var zLeft=width/2-this.vCenter.x*this.options.zoom;var zTop=height/2-this.vCenter.y*this.options.zoom;var vx=relx*width*this.options.zoom+zLeft;var vy=rely*height*this.options.zoom+zTop;return{x:Math.round(vx),y:Math.round(vy)}}else{return null}},imgToCursor:function(relx,rely){var pos=this.imgToView(relx,rely);if(pos){var offset=$(this.img).offset();pos.x+=offset.left+this.offsetBorder.x+this.offsetPadding.left;pos.y+=offset.top+this.offsetBorder.y+this.offsetPadding.top;return pos}else{return null}},viewToImg:function(vx,vy){if(this.ready){var $img=$(this.img),width=$img.width(),height=$img.height();var zLeft=width/2-this.vCenter.x*this.options.zoom;var zTop=height/2-this.vCenter.y*this.options.zoom;var relx=(vx-zLeft)/(width*this.options.zoom);var rely=(vy-zTop)/(height*this.options.zoom);if(relx>=0&&relx<=1&&rely>=0&&rely<=1){return{x:relx,y:rely}}else{return null}}else{return null}},cursorToImg:function(cx,cy){if(this.ready){var $img=$(this.img),width=$img.width(),height=$img.height(),offset=$img.offset();var zLeft=width/2-this.vCenter.x*this.options.zoom;var zTop=height/2-this.vCenter.y*this.options.zoom;var relx=(cx-offset.left-this.offsetBorder.x-this.offsetPadding.left-zLeft)/(width*this.options.zoom);var rely=(cy-offset.top-this.offsetBorder.y-this.offsetPadding.top-zTop)/(height*this.options.zoom);if(relx>=0&&relx<=1&&rely>=0&&rely<=1){return{x:relx,y:rely}}else{return null}}else{return null}},relposToImage:function(pos){if(this.ready){var img=this.img,width=img.naturalWidth,height=img.naturalHeight;return{x:Math.round(pos.x*width),y:Math.round(pos.y*height)}}else{return null}},update:function(){if(this.ready){var zTop,zLeft,zWidth,zHeight,$img=$(this.img),width=$img.width(),height=$img.height(),zoom=this.options.zoom,zoomMax=this.options.zoomMax,half_width=width/2,half_height=height/2;zoom=zoomMax===undefined?zoom:Math.min(zoom,zoomMax);this.options.zoom=zoom;if(zoom<=1){zTop=0;zLeft=0;zWidth=width;zHeight=height;this.vCenter={x:half_width,y:half_height};this.options.zoom=1;zoom=1}else{zTop=Math.round(half_height-this.vCenter.y*zoom);zLeft=Math.round(half_width-this.vCenter.x*zoom);zWidth=Math.round(width*zoom);zHeight=Math.round(height*zoom);if(zLeft>0){this.vCenter.x=half_width/zoom;zLeft=0}else if(zLeft+zWidth0){this.vCenter.y=half_height/zoom;zTop=0}else if(zTop+zHeight", {"src": self.img.src}).appendTo("body").wrap("
"); 50 | var $zimg = $(self.zimg); 51 | // the container or viewport for the image view 52 | self.view = $(self.zimg).parent(); 53 | var $view = $(self.view); 54 | // the pixel coordinate of the original image at the center of the viewport 55 | self.vCenter = {}; 56 | // a flag used to decide if a mouse click is part of a drag or a proper click 57 | self.drag = false; 58 | self.pinch = false; 59 | // a flag used to check the target image has loaded 60 | self.ready = false; 61 | $img.one("load",function() { 62 | // get and some geometry information about the image 63 | self.ready = true; 64 | var width = $img.width(), 65 | height = $img.height(), 66 | offset = $img.offset(); 67 | // cache the image padding information 68 | self.offsetPadding = { 69 | top: parseInt($img.css('padding-top'),10), 70 | left: parseInt($img.css('padding-left'),10), 71 | right: parseInt($img.css('padding-right'),10), 72 | bottom: parseInt($img.css('padding-bottom'),10) 73 | }; 74 | /* 75 | * cache the image margin/border size information 76 | * because of IE8 limitations left and right borders are assumed to be the same width 77 | * and likewise top and bottom borders 78 | */ 79 | self.offsetBorder = { 80 | x: Math.round(($img.outerWidth()-$img.innerWidth())/2), 81 | y: Math.round(($img.outerHeight()-$img.innerHeight())/2) 82 | }; 83 | /* 84 | * define the css style for the view container using absolute positioning to 85 | * put it directly over the original image 86 | */ 87 | var vTop = offset.top + self.offsetBorder.y + self.offsetPadding.top, 88 | vLeft = offset.left + self.offsetBorder.x + self.offsetPadding.left; 89 | 90 | $view.css({ 91 | position: "absolute", 92 | overflow: "hidden", 93 | top: vTop+"px", 94 | left: vLeft+"px", 95 | width: width+"px", 96 | height: height+"px" 97 | }); 98 | // the zoom and pan image is position relative to the view container 99 | $zimg.css({ 100 | position: "relative", 101 | top: 0+"px", 102 | left: 0+"px", 103 | width: width+"px", 104 | height: height+"px", 105 | "-webkit-tap-highlight-color": "transparent" 106 | }); 107 | // the initial view is centered at the orignal image 108 | self.vCenter = { 109 | x: width/2, 110 | y: height/2 111 | }; 112 | self.update(); 113 | }).each(function() { 114 | if (this.complete) { $(this).trigger("load"); } 115 | }); 116 | /* 117 | * Render loop code during dragging and scaling using requestAnimationFrame 118 | */ 119 | self.render = false; 120 | /* 121 | * Event handlers 122 | */ 123 | $zimg.hammer(); 124 | 125 | if (self.options.zoomable) { 126 | self._bind_zoom_events(); 127 | } 128 | if (self.options.dragable) { 129 | self._bind_drag_events(); 130 | } 131 | $zimg.on("tap", function(ev) { 132 | ev.preventDefault(); 133 | if (!self.dragging) { 134 | var scoff = self._get_scroll_offset(); 135 | ev.pageX = ev.gesture.center.x + scoff.x; 136 | ev.pageY = ev.gesture.center.y + scoff.y; 137 | self.options.onClick.call(self, ev); 138 | } 139 | }); 140 | /* 141 | * Window resize handler 142 | */ 143 | 144 | $(window).resize(function() { 145 | self._view_resize(); 146 | waitForFinalEvent(function(){ 147 | self._view_resize(); 148 | }, 300, $img[0].id); 149 | 150 | }); 151 | self._view_resize(); 152 | 153 | self.options.onReady.call(self); 154 | }, 155 | /* 156 | * Return the window scroll offset - required to convert Hammer.js event coords to page locations 157 | */ 158 | _get_scroll_offset: function() { 159 | var sx,sy; 160 | if (window.scrollX === undefined) { 161 | if (window.pageXOffset === undefined) { 162 | sx = document.documentElement.scrollLeft; 163 | sy = document.documentElement.scrollTop; 164 | } else { 165 | sx = window.pageXOffset; 166 | sy = window.pageYOffset; 167 | } 168 | } else { 169 | sx = window.scrollX; 170 | sy = window.scrollY; 171 | } 172 | return {x: sx, y: sy}; 173 | }, 174 | /* 175 | * View resize - the aim is to keep the view centered on the same location in the original image 176 | */ 177 | _view_resize: function() { 178 | if (this.ready) { 179 | var $view = $(this.view), 180 | $img = $(this.img), 181 | width = $img.width(), 182 | height = $img.height(), 183 | offset = $img.offset(), 184 | vTop = Math.round(offset.top + this.offsetBorder.y + this.offsetPadding.top), 185 | vLeft = Math.round(offset.left + this.offsetBorder.x + this.offsetPadding.left); 186 | this.vCenter.x *=$img.width()/$view.width(); 187 | this.vCenter.y *= $img.height()/$view.height(); 188 | $view.css({ 189 | top: vTop+"px", 190 | left: vLeft+"px", 191 | width: width+"px", 192 | height: height+"px" 193 | }); 194 | 195 | this.update(); 196 | } 197 | }, 198 | /* 199 | * Bind events 200 | */ 201 | _bind_zoom_events: function() { 202 | var self = this; 203 | var $zimg = $(self.zimg); 204 | 205 | function doRender() { 206 | if (self.render) { 207 | window.requestAnimationFrame(doRender); 208 | self.update(); 209 | } 210 | } 211 | function startRenderLoop() { 212 | if (!self.render) { 213 | self.render = true; 214 | doRender(); 215 | } 216 | } 217 | function stopRenderLoop() { 218 | self.render = false; 219 | } 220 | 221 | $zimg.on("mousewheel", function(ev) { 222 | ev.preventDefault(); 223 | var delta = ev.deltaY ; 224 | self.options.zoom -= delta * self.options.zoomStep; 225 | self.update(); 226 | }); 227 | 228 | $zimg.on("touchmove", function(e) { 229 | e.preventDefault(); 230 | // e.stopPropagation(); 231 | }); 232 | $zimg.data("hammer").recognizers[1].options.enable = true; 233 | 234 | $zimg.on("pinchstart", function() { 235 | }); 236 | 237 | $zimg.on("pinch", function(ev) { 238 | ev.preventDefault(); 239 | if (!self.pinch) { 240 | var scoff = self._get_scroll_offset(); 241 | self.pinchstart = { x: ev.gesture.center.x+scoff.x, y: ev.gesture.center.y+scoff.y}; 242 | self.pinchstartrelpos = self.cursorToImg(self.pinchstart.x, self.pinchstart.y); 243 | self.pinchstart_scale = self.options.zoom; 244 | startRenderLoop(); 245 | self.pinch = true; 246 | } else { 247 | self.options.zoom = ev.gesture.scale * self.pinchstart_scale; 248 | var npos = self.imgToCursor( self.pinchstartrelpos.x, self.pinchstartrelpos.y); 249 | self.vCenter.x = self.vCenter.x + (npos.x - self.pinchstart.x)/self.options.zoom; 250 | self.vCenter.y = self.vCenter.y + (npos.y - self.pinchstart.y)/self.options.zoom; 251 | } 252 | }); 253 | 254 | $zimg.on("pinchend", function(ev) { 255 | ev.preventDefault(); 256 | if (self.pinch) { 257 | stopRenderLoop(); 258 | self.update(); 259 | self.pinch = false; 260 | } 261 | }); 262 | }, 263 | 264 | _bind_drag_events: function() { 265 | var self = this; 266 | var $zimg = $(self.zimg); 267 | function doRender() { 268 | if (self.render) { 269 | window.requestAnimationFrame(doRender); 270 | self.update(); 271 | } 272 | } 273 | function startRenderLoop() { 274 | if (!self.render) { 275 | self.render = true; 276 | doRender(); 277 | } 278 | } 279 | function stopRenderLoop() { 280 | self.render = false; 281 | } 282 | $zimg.on("mousedown", function(e) { 283 | e.preventDefault(); 284 | }); 285 | $zimg.on("panstart", function() { 286 | }); 287 | 288 | $zimg.on("panmove", function(ev) { 289 | ev.preventDefault(); 290 | if (!self.drag) { 291 | self.drag = true; 292 | self.dragXorg = self.vCenter.x; 293 | self.dragYorg = self.vCenter.y; 294 | startRenderLoop(); 295 | } else { 296 | self.vCenter.x = self.dragXorg - ev.gesture.deltaX/self.options.zoom; 297 | self.vCenter.y = self.dragYorg - ev.gesture.deltaY/self.options.zoom; 298 | } 299 | }); 300 | 301 | $zimg.on( "panend", function(ev) { 302 | ev.preventDefault(); 303 | if (self.drag) { 304 | self.drag = false; 305 | stopRenderLoop(); 306 | self.update(); 307 | } 308 | }); 309 | }, 310 | /* 311 | * Unbind events 312 | */ 313 | _unbind_zoom_events: function() { 314 | var self = this; 315 | var $zimg = $(self.zimg); 316 | $zimg.data("hammer").recognizers[1].options.enable = false; 317 | $zimg.off("mousewheel"); 318 | $zimg.off("pinchstart"); 319 | $zimg.off("pinch"); 320 | $zimg.off("pinchend"); 321 | }, 322 | 323 | _unbind_drag_events: function() { 324 | var self = this; 325 | var $zimg = $(self.zimg); 326 | $zimg.off("pan"); 327 | $zimg.off("panend"); 328 | }, 329 | 330 | /* 331 | * Remove the plugin 332 | */ 333 | destroy: function() { 334 | var $zimg = $(this.zimg); 335 | $zimg.unbind("click"); 336 | $(window).unbind("resize"); 337 | $zimg.remove(); 338 | $(this.view).remove(); 339 | $.Widget.prototype.destroy.call(this); 340 | }, 341 | 342 | _setOption: function(key, value) { 343 | switch(key) { 344 | case 'zoom': 345 | if (parseFloat(value) < 1 || isNaN(parseFloat(value))) { 346 | return; 347 | } 348 | break; 349 | case 'zoomStep': 350 | if (parseFloat(value) <= 0 || isNaN(parseFloat(value))) { 351 | return; 352 | } 353 | break; 354 | case 'zoomMax': 355 | if (parseFloat(value) < 1 || isNaN(parseFloat(value))) { 356 | return; 357 | } 358 | break; 359 | } 360 | var version = $.ui.version.split('.'); 361 | if (version[0] > 1 || version[1] > 8) { 362 | this._super(key, value); 363 | } else { 364 | $.Widget.prototype._setOption.apply(this, arguments); 365 | } 366 | switch(key) { 367 | case 'zoom': 368 | if (this.ready) { 369 | this.update(); 370 | } 371 | break; 372 | case 'zoomable': 373 | if (this.options.zoomable) { 374 | this._bind_zoom_events(); 375 | } else { 376 | this._unbind_zoom_events(); 377 | } 378 | break; 379 | case 'dragable': 380 | if (this.options.dragable) { 381 | this._bind_drag_events(); 382 | } else { 383 | this._unbind_drag_events(); 384 | } 385 | break; 386 | case 'zoomMax': 387 | if (this.ready) { 388 | this._view_resize(); 389 | this.update(); 390 | } 391 | break; 392 | } 393 | }, 394 | 395 | addElem: function(elem) { 396 | $(this.view).append(elem); 397 | }, 398 | /* 399 | * Test if a relative image coordinate is visible in the current view 400 | */ 401 | isVisible: function(relx, rely) { 402 | var view = this.getView(); 403 | if (view) { 404 | return (relx >= view.left && relx <= view.right && rely >= view.top && rely <= view.bottom); 405 | } else { 406 | return false; 407 | } 408 | }, 409 | /* 410 | * Get relative image coordinates of current view 411 | */ 412 | getView: function() { 413 | if (this.ready) { 414 | var $img = $(this.img), 415 | width = $img.width(), 416 | height = $img.height(), 417 | zoom = this.options.zoom; 418 | return { 419 | top: this.vCenter.y/height - 0.5/zoom, 420 | left: this.vCenter.x/width - 0.5/zoom, 421 | bottom: this.vCenter.y/height + 0.5/zoom, 422 | right: this.vCenter.x/width + 0.5/zoom 423 | }; 424 | } else { 425 | return null; 426 | } 427 | }, 428 | /* 429 | * Pan the view to be centred at the given relative image location 430 | */ 431 | panTo: function(relx, rely) { 432 | if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) { 433 | var $img = $(this.img), 434 | width = $img.width(), 435 | height = $img.height(); 436 | this.vCenter.x = relx * width; 437 | this.vCenter.y = rely * height; 438 | this.update(); 439 | return { x: this.vCenter.x/width, y: this.vCenter.y/height }; 440 | } else { 441 | return null; 442 | } 443 | }, 444 | /* 445 | * Convert a relative image location to a viewport pixel location 446 | */ 447 | imgToView: function(relx, rely) { 448 | if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) { 449 | var $img = $(this.img), 450 | width = $img.width(), 451 | height = $img.height(); 452 | 453 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 454 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 455 | var vx = relx * width * this.options.zoom + zLeft; 456 | var vy = rely * height * this.options.zoom + zTop; 457 | return { x: Math.round(vx), y: Math.round(vy) }; 458 | } else { 459 | 460 | return null; 461 | } 462 | }, 463 | /* 464 | * Convert a relative image location to a page pixel location 465 | */ 466 | imgToCursor: function(relx, rely) { 467 | var pos = this.imgToView(relx, rely); 468 | if (pos) { 469 | var offset = $(this.img).offset(); 470 | pos.x += offset.left + this.offsetBorder.x + this.offsetPadding.left; 471 | pos.y += offset.top + this.offsetBorder.y + this.offsetPadding.top; 472 | return pos; 473 | } else { 474 | return null; 475 | } 476 | }, 477 | /* 478 | * Convert a viewport pixel location to a relative image coordinate 479 | */ 480 | viewToImg: function(vx, vy) { 481 | if (this.ready) { 482 | var $img = $(this.img), 483 | width = $img.width(), 484 | height = $img.height(); 485 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 486 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 487 | var relx= (vx - zLeft)/(width * this.options.zoom); 488 | var rely = (vy - zTop)/(height * this.options.zoom); 489 | if (relx>=0 && relx<=1 && rely>=0 && rely<=1) { 490 | return {x: relx, y: rely}; 491 | } else { 492 | return null; 493 | } 494 | } else { 495 | return null; 496 | } 497 | }, 498 | 499 | /* 500 | * Convert a page pixel location to a relative image coordinate 501 | */ 502 | cursorToImg: function(cx, cy) { 503 | if (this.ready) { 504 | var $img = $(this.img), 505 | width = $img.width(), 506 | height = $img.height(), 507 | offset = $img.offset(); 508 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 509 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 510 | var relx = (cx - offset.left - this.offsetBorder.x - this.offsetPadding.left- zLeft)/(width * this.options.zoom); 511 | var rely = (cy - offset.top - this.offsetBorder.y - this.offsetPadding.top - zTop)/(height * this.options.zoom); 512 | if (relx>=0 && relx<=1 && rely>=0 && rely<=1) { 513 | return {x: relx, y: rely}; 514 | } else { 515 | return null; 516 | } 517 | } else { 518 | return null; 519 | } 520 | }, 521 | /* 522 | * Convert relative image coordinate to Image pixel 523 | */ 524 | relposToImage: function(pos) { 525 | if (this.ready) { 526 | var img = this.img, 527 | width = img.naturalWidth, 528 | height = img.naturalHeight; 529 | return {x: Math.round(pos.x*width), y: Math.round(pos.y*height)}; 530 | } else { 531 | return null; 532 | } 533 | }, 534 | /* 535 | * Adjust the display of the image 536 | */ 537 | update: function() { 538 | if (this.ready) { 539 | var zTop, zLeft, zWidth, zHeight, 540 | $img = $(this.img), 541 | width = $img.width(), 542 | height = $img.height(), 543 | // offset = $img.offset(), 544 | zoom = this.options.zoom, 545 | zoomMax = this.options.zoomMax, 546 | half_width = width/2, 547 | half_height = height/2; 548 | 549 | zoom = zoomMax === undefined ? zoom : Math.min(zoom, zoomMax); 550 | this.options.zoom = zoom; 551 | 552 | if (zoom <= 1) { 553 | zTop = 0; 554 | zLeft = 0; 555 | zWidth = width; 556 | zHeight = height; 557 | this.vCenter = { 558 | x: half_width, 559 | y: half_height 560 | }; 561 | this.options.zoom = 1; 562 | zoom = 1; 563 | } else { 564 | zTop = Math.round(half_height - this.vCenter.y * zoom); 565 | zLeft = Math.round(half_width - this.vCenter.x * zoom); 566 | zWidth = Math.round(width * zoom); 567 | zHeight = Math.round(height * zoom); 568 | /* 569 | * adjust the view center so the image edges snap to the edge of the view 570 | */ 571 | if (zLeft > 0) { 572 | this.vCenter.x = half_width/zoom; 573 | zLeft = 0; 574 | } else if (zLeft+zWidth < width) { 575 | this.vCenter.x = width - half_width/zoom ; 576 | zLeft = width - zWidth; 577 | } 578 | if (zTop > 0) { 579 | this.vCenter.y = half_height/zoom; 580 | zTop = 0; 581 | } else if (zTop + zHeight < height) { 582 | this.vCenter.y = height - half_height/zoom; 583 | zTop = height - zHeight; 584 | } 585 | } 586 | $(this.zimg).css({ 587 | width: width+"px", 588 | height: height+"px" 589 | }); 590 | 591 | var xt = -(this.vCenter.x - half_width)*zoom; 592 | var yt = -(this.vCenter.y - half_height)*zoom; 593 | $(this.zimg).css({transform: "translate(" + xt + "px," + yt + "px) scale(" + zoom + "," + zoom + ")" }); 594 | /* 595 | * define the onUpdate option to do something after the image is redisplayed 596 | * probably shouldn't pass out the this object - need to think of something better 597 | */ 598 | this.options.onUpdate.call(this); 599 | } 600 | } 601 | }); 602 | })(jQuery); 603 | -------------------------------------------------------------------------------- /docs/lib/imgViewer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * imgViewer 3 | * 4 | * 5 | * Copyright (c) 2013 Wayne Mogg 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | var waitForFinalEvent = (function () { 10 | var timers = {}; 11 | return function (callback, ms, uniqueId) { 12 | if (!uniqueId) { 13 | uniqueId = "Don't call this twice without a uniqueId"; 14 | } 15 | if (timers[uniqueId]) { 16 | clearTimeout (timers[uniqueId]); 17 | } 18 | timers[uniqueId] = setTimeout(callback, ms); 19 | }; 20 | })(); 21 | /* 22 | * imgViewer plugin starts here 23 | */ 24 | ;(function($) { 25 | $.widget("wgm.imgViewer", { 26 | options: { 27 | zoomStep: 0.1, 28 | zoom: 1, 29 | zoomMax: undefined, 30 | zoomable: true, 31 | dragable: true, 32 | onReady: $.noop, 33 | onClick: $.noop, 34 | onUpdate: $.noop 35 | }, 36 | 37 | _create: function() { 38 | var self = this; 39 | if (!this.element.is("img")) { 40 | $.error('imgviewer plugin can only be applied to img elements'); 41 | } 42 | // the original img element 43 | self.img = self.element[0]; 44 | var $img = $(self.img); 45 | /* 46 | * a copy of the original image to be positioned over it and manipulated to 47 | * provide zoom and pan 48 | */ 49 | self.zimg = $("", {"src": self.img.src}).appendTo("body").wrap("
"); 50 | var $zimg = $(self.zimg); 51 | // the container or viewport for the image view 52 | self.view = $(self.zimg).parent(); 53 | var $view = $(self.view); 54 | // the pixel coordinate of the original image at the center of the viewport 55 | self.vCenter = {}; 56 | // a flag used to decide if a mouse click is part of a drag or a proper click 57 | self.drag = false; 58 | self.pinch = false; 59 | // a flag used to check the target image has loaded 60 | self.ready = false; 61 | $img.one("load",function() { 62 | // get and some geometry information about the image 63 | self.ready = true; 64 | var width = $img.width(), 65 | height = $img.height(), 66 | offset = $img.offset(); 67 | // cache the image padding information 68 | self.offsetPadding = { 69 | top: parseInt($img.css('padding-top'),10), 70 | left: parseInt($img.css('padding-left'),10), 71 | right: parseInt($img.css('padding-right'),10), 72 | bottom: parseInt($img.css('padding-bottom'),10) 73 | }; 74 | /* 75 | * cache the image margin/border size information 76 | * because of IE8 limitations left and right borders are assumed to be the same width 77 | * and likewise top and bottom borders 78 | */ 79 | self.offsetBorder = { 80 | x: Math.round(($img.outerWidth()-$img.innerWidth())/2), 81 | y: Math.round(($img.outerHeight()-$img.innerHeight())/2) 82 | }; 83 | /* 84 | * define the css style for the view container using absolute positioning to 85 | * put it directly over the original image 86 | */ 87 | var vTop = offset.top + self.offsetBorder.y + self.offsetPadding.top, 88 | vLeft = offset.left + self.offsetBorder.x + self.offsetPadding.left; 89 | 90 | $view.css({ 91 | position: "absolute", 92 | overflow: "hidden", 93 | top: vTop+"px", 94 | left: vLeft+"px", 95 | width: width+"px", 96 | height: height+"px" 97 | }); 98 | // the zoom and pan image is position relative to the view container 99 | $zimg.css({ 100 | position: "relative", 101 | top: 0+"px", 102 | left: 0+"px", 103 | width: width+"px", 104 | height: height+"px", 105 | "-webkit-tap-highlight-color": "transparent" 106 | }); 107 | // the initial view is centered at the orignal image 108 | self.vCenter = { 109 | x: width/2, 110 | y: height/2 111 | }; 112 | self.update(); 113 | }).each(function() { 114 | if (this.complete) { $(this).trigger("load"); } 115 | }); 116 | /* 117 | * Render loop code during dragging and scaling using requestAnimationFrame 118 | */ 119 | self.render = false; 120 | /* 121 | * Event handlers 122 | */ 123 | $zimg.hammer(); 124 | 125 | if (self.options.zoomable) { 126 | self._bind_zoom_events(); 127 | } 128 | if (self.options.dragable) { 129 | self._bind_drag_events(); 130 | } 131 | $zimg.on("tap", function(ev) { 132 | ev.preventDefault(); 133 | if (!self.dragging) { 134 | var scoff = self._get_scroll_offset(); 135 | ev.pageX = ev.gesture.center.x + scoff.x; 136 | ev.pageY = ev.gesture.center.y + scoff.y; 137 | self.options.onClick.call(self, ev); 138 | } 139 | }); 140 | /* 141 | * Window resize handler 142 | */ 143 | 144 | $(window).resize(function() { 145 | self._view_resize(); 146 | waitForFinalEvent(function(){ 147 | self._view_resize(); 148 | }, 300, $img[0].id); 149 | 150 | }); 151 | self._view_resize(); 152 | 153 | self.options.onReady.call(self); 154 | }, 155 | /* 156 | * Return the window scroll offset - required to convert Hammer.js event coords to page locations 157 | */ 158 | _get_scroll_offset: function() { 159 | var sx,sy; 160 | if (window.scrollX === undefined) { 161 | if (window.pageXOffset === undefined) { 162 | sx = document.documentElement.scrollLeft; 163 | sy = document.documentElement.scrollTop; 164 | } else { 165 | sx = window.pageXOffset; 166 | sy = window.pageYOffset; 167 | } 168 | } else { 169 | sx = window.scrollX; 170 | sy = window.scrollY; 171 | } 172 | return {x: sx, y: sy}; 173 | }, 174 | /* 175 | * View resize - the aim is to keep the view centered on the same location in the original image 176 | */ 177 | _view_resize: function() { 178 | if (this.ready) { 179 | var $view = $(this.view), 180 | $img = $(this.img), 181 | width = $img.width(), 182 | height = $img.height(), 183 | offset = $img.offset(), 184 | vTop = Math.round(offset.top + this.offsetBorder.y + this.offsetPadding.top), 185 | vLeft = Math.round(offset.left + this.offsetBorder.x + this.offsetPadding.left); 186 | this.vCenter.x *=$img.width()/$view.width(); 187 | this.vCenter.y *= $img.height()/$view.height(); 188 | $view.css({ 189 | top: vTop+"px", 190 | left: vLeft+"px", 191 | width: width+"px", 192 | height: height+"px" 193 | }); 194 | 195 | this.update(); 196 | } 197 | }, 198 | /* 199 | * Bind events 200 | */ 201 | _bind_zoom_events: function() { 202 | var self = this; 203 | var $zimg = $(self.zimg); 204 | 205 | function doRender() { 206 | if (self.render) { 207 | window.requestAnimationFrame(doRender); 208 | self.update(); 209 | } 210 | } 211 | function startRenderLoop() { 212 | if (!self.render) { 213 | self.render = true; 214 | doRender(); 215 | } 216 | } 217 | function stopRenderLoop() { 218 | self.render = false; 219 | } 220 | 221 | $zimg.on("mousewheel", function(ev) { 222 | ev.preventDefault(); 223 | var delta = ev.deltaY ; 224 | self.options.zoom -= delta * self.options.zoomStep; 225 | self.update(); 226 | }); 227 | 228 | $zimg.on("touchmove", function(e) { 229 | e.preventDefault(); 230 | // e.stopPropagation(); 231 | }); 232 | $zimg.data("hammer").recognizers[1].options.enable = true; 233 | 234 | $zimg.on("pinchstart", function() { 235 | }); 236 | 237 | $zimg.on("pinch", function(ev) { 238 | ev.preventDefault(); 239 | if (!self.pinch) { 240 | var scoff = self._get_scroll_offset(); 241 | self.pinchstart = { x: ev.gesture.center.x+scoff.x, y: ev.gesture.center.y+scoff.y}; 242 | self.pinchstartrelpos = self.cursorToImg(self.pinchstart.x, self.pinchstart.y); 243 | self.pinchstart_scale = self.options.zoom; 244 | startRenderLoop(); 245 | self.pinch = true; 246 | } else { 247 | self.options.zoom = ev.gesture.scale * self.pinchstart_scale; 248 | var npos = self.imgToCursor( self.pinchstartrelpos.x, self.pinchstartrelpos.y); 249 | self.vCenter.x = self.vCenter.x + (npos.x - self.pinchstart.x)/self.options.zoom; 250 | self.vCenter.y = self.vCenter.y + (npos.y - self.pinchstart.y)/self.options.zoom; 251 | } 252 | }); 253 | 254 | $zimg.on("pinchend", function(ev) { 255 | ev.preventDefault(); 256 | if (self.pinch) { 257 | stopRenderLoop(); 258 | self.update(); 259 | self.pinch = false; 260 | } 261 | }); 262 | }, 263 | 264 | _bind_drag_events: function() { 265 | var self = this; 266 | var $zimg = $(self.zimg); 267 | function doRender() { 268 | if (self.render) { 269 | window.requestAnimationFrame(doRender); 270 | self.update(); 271 | } 272 | } 273 | function startRenderLoop() { 274 | if (!self.render) { 275 | self.render = true; 276 | doRender(); 277 | } 278 | } 279 | function stopRenderLoop() { 280 | self.render = false; 281 | } 282 | $zimg.on("mousedown", function(e) { 283 | e.preventDefault(); 284 | }); 285 | $zimg.on("panstart", function() { 286 | }); 287 | 288 | $zimg.on("panmove", function(ev) { 289 | ev.preventDefault(); 290 | if (!self.drag) { 291 | self.drag = true; 292 | self.dragXorg = self.vCenter.x; 293 | self.dragYorg = self.vCenter.y; 294 | startRenderLoop(); 295 | } else { 296 | self.vCenter.x = self.dragXorg - ev.gesture.deltaX/self.options.zoom; 297 | self.vCenter.y = self.dragYorg - ev.gesture.deltaY/self.options.zoom; 298 | } 299 | }); 300 | 301 | $zimg.on( "panend", function(ev) { 302 | ev.preventDefault(); 303 | if (self.drag) { 304 | self.drag = false; 305 | stopRenderLoop(); 306 | self.update(); 307 | } 308 | }); 309 | }, 310 | /* 311 | * Unbind events 312 | */ 313 | _unbind_zoom_events: function() { 314 | var self = this; 315 | var $zimg = $(self.zimg); 316 | $zimg.data("hammer").recognizers[1].options.enable = false; 317 | $zimg.off("mousewheel"); 318 | $zimg.off("pinchstart"); 319 | $zimg.off("pinch"); 320 | $zimg.off("pinchend"); 321 | }, 322 | 323 | _unbind_drag_events: function() { 324 | var self = this; 325 | var $zimg = $(self.zimg); 326 | $zimg.off("pan"); 327 | $zimg.off("panend"); 328 | }, 329 | 330 | /* 331 | * Remove the plugin 332 | */ 333 | destroy: function() { 334 | var $zimg = $(this.zimg); 335 | $zimg.unbind("click"); 336 | $(window).unbind("resize"); 337 | $zimg.remove(); 338 | $(this.view).remove(); 339 | $.Widget.prototype.destroy.call(this); 340 | }, 341 | 342 | _setOption: function(key, value) { 343 | switch(key) { 344 | case 'zoom': 345 | if (parseFloat(value) < 1 || isNaN(parseFloat(value))) { 346 | return; 347 | } 348 | break; 349 | case 'zoomStep': 350 | if (parseFloat(value) <= 0 || isNaN(parseFloat(value))) { 351 | return; 352 | } 353 | break; 354 | case 'zoomMax': 355 | if (parseFloat(value) < 1 || isNaN(parseFloat(value))) { 356 | return; 357 | } 358 | break; 359 | } 360 | var version = $.ui.version.split('.'); 361 | if (version[0] > 1 || version[1] > 8) { 362 | this._super(key, value); 363 | } else { 364 | $.Widget.prototype._setOption.apply(this, arguments); 365 | } 366 | switch(key) { 367 | case 'zoom': 368 | if (this.ready) { 369 | this.update(); 370 | } 371 | break; 372 | case 'zoomable': 373 | if (this.options.zoomable) { 374 | this._bind_zoom_events(); 375 | } else { 376 | this._unbind_zoom_events(); 377 | } 378 | break; 379 | case 'dragable': 380 | if (this.options.dragable) { 381 | this._bind_drag_events(); 382 | } else { 383 | this._unbind_drag_events(); 384 | } 385 | break; 386 | case 'zoomMax': 387 | if (this.ready) { 388 | this._view_resize(); 389 | this.update(); 390 | } 391 | break; 392 | } 393 | }, 394 | 395 | addElem: function(elem) { 396 | $(this.view).append(elem); 397 | }, 398 | /* 399 | * Test if a relative image coordinate is visible in the current view 400 | */ 401 | isVisible: function(relx, rely) { 402 | var view = this.getView(); 403 | if (view) { 404 | return (relx >= view.left && relx <= view.right && rely >= view.top && rely <= view.bottom); 405 | } else { 406 | return false; 407 | } 408 | }, 409 | /* 410 | * Get relative image coordinates of current view 411 | */ 412 | getView: function() { 413 | if (this.ready) { 414 | var $img = $(this.img), 415 | width = $img.width(), 416 | height = $img.height(), 417 | zoom = this.options.zoom; 418 | return { 419 | top: this.vCenter.y/height - 0.5/zoom, 420 | left: this.vCenter.x/width - 0.5/zoom, 421 | bottom: this.vCenter.y/height + 0.5/zoom, 422 | right: this.vCenter.x/width + 0.5/zoom 423 | }; 424 | } else { 425 | return null; 426 | } 427 | }, 428 | /* 429 | * Pan the view to be centred at the given relative image location 430 | */ 431 | panTo: function(relx, rely) { 432 | if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) { 433 | var $img = $(this.img), 434 | width = $img.width(), 435 | height = $img.height(); 436 | this.vCenter.x = relx * width; 437 | this.vCenter.y = rely * height; 438 | this.update(); 439 | return { x: this.vCenter.x/width, y: this.vCenter.y/height }; 440 | } else { 441 | return null; 442 | } 443 | }, 444 | /* 445 | * Convert a relative image location to a viewport pixel location 446 | */ 447 | imgToView: function(relx, rely) { 448 | if ( this.ready && relx >= 0 && relx <= 1 && rely >= 0 && rely <=1 ) { 449 | var $img = $(this.img), 450 | width = $img.width(), 451 | height = $img.height(); 452 | 453 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 454 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 455 | var vx = relx * width * this.options.zoom + zLeft; 456 | var vy = rely * height * this.options.zoom + zTop; 457 | return { x: Math.round(vx), y: Math.round(vy) }; 458 | } else { 459 | 460 | return null; 461 | } 462 | }, 463 | /* 464 | * Convert a relative image location to a page pixel location 465 | */ 466 | imgToCursor: function(relx, rely) { 467 | var pos = this.imgToView(relx, rely); 468 | if (pos) { 469 | var offset = $(this.img).offset(); 470 | pos.x += offset.left + this.offsetBorder.x + this.offsetPadding.left; 471 | pos.y += offset.top + this.offsetBorder.y + this.offsetPadding.top; 472 | return pos; 473 | } else { 474 | return null; 475 | } 476 | }, 477 | /* 478 | * Convert a viewport pixel location to a relative image coordinate 479 | */ 480 | viewToImg: function(vx, vy) { 481 | if (this.ready) { 482 | var $img = $(this.img), 483 | width = $img.width(), 484 | height = $img.height(); 485 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 486 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 487 | var relx= (vx - zLeft)/(width * this.options.zoom); 488 | var rely = (vy - zTop)/(height * this.options.zoom); 489 | if (relx>=0 && relx<=1 && rely>=0 && rely<=1) { 490 | return {x: relx, y: rely}; 491 | } else { 492 | return null; 493 | } 494 | } else { 495 | return null; 496 | } 497 | }, 498 | 499 | /* 500 | * Convert a page pixel location to a relative image coordinate 501 | */ 502 | cursorToImg: function(cx, cy) { 503 | if (this.ready) { 504 | var $img = $(this.img), 505 | width = $img.width(), 506 | height = $img.height(), 507 | offset = $img.offset(); 508 | var zLeft = width/2 - this.vCenter.x * this.options.zoom; 509 | var zTop = height/2 - this.vCenter.y * this.options.zoom; 510 | var relx = (cx - offset.left - this.offsetBorder.x - this.offsetPadding.left- zLeft)/(width * this.options.zoom); 511 | var rely = (cy - offset.top - this.offsetBorder.y - this.offsetPadding.top - zTop)/(height * this.options.zoom); 512 | if (relx>=0 && relx<=1 && rely>=0 && rely<=1) { 513 | return {x: relx, y: rely}; 514 | } else { 515 | return null; 516 | } 517 | } else { 518 | return null; 519 | } 520 | }, 521 | /* 522 | * Convert relative image coordinate to Image pixel 523 | */ 524 | relposToImage: function(pos) { 525 | if (this.ready) { 526 | var img = this.img, 527 | width = img.naturalWidth, 528 | height = img.naturalHeight; 529 | return {x: Math.round(pos.x*width), y: Math.round(pos.y*height)}; 530 | } else { 531 | return null; 532 | } 533 | }, 534 | /* 535 | * Adjust the display of the image 536 | */ 537 | update: function() { 538 | if (this.ready) { 539 | var zTop, zLeft, zWidth, zHeight, 540 | $img = $(this.img), 541 | width = $img.width(), 542 | height = $img.height(), 543 | // offset = $img.offset(), 544 | zoom = this.options.zoom, 545 | zoomMax = this.options.zoomMax, 546 | half_width = width/2, 547 | half_height = height/2; 548 | 549 | zoom = zoomMax === undefined ? zoom : Math.min(zoom, zoomMax); 550 | this.options.zoom = zoom; 551 | 552 | if (zoom <= 1) { 553 | zTop = 0; 554 | zLeft = 0; 555 | zWidth = width; 556 | zHeight = height; 557 | this.vCenter = { 558 | x: half_width, 559 | y: half_height 560 | }; 561 | this.options.zoom = 1; 562 | zoom = 1; 563 | } else { 564 | zTop = Math.round(half_height - this.vCenter.y * zoom); 565 | zLeft = Math.round(half_width - this.vCenter.x * zoom); 566 | zWidth = Math.round(width * zoom); 567 | zHeight = Math.round(height * zoom); 568 | /* 569 | * adjust the view center so the image edges snap to the edge of the view 570 | */ 571 | if (zLeft > 0) { 572 | this.vCenter.x = half_width/zoom; 573 | zLeft = 0; 574 | } else if (zLeft+zWidth < width) { 575 | this.vCenter.x = width - half_width/zoom ; 576 | zLeft = width - zWidth; 577 | } 578 | if (zTop > 0) { 579 | this.vCenter.y = half_height/zoom; 580 | zTop = 0; 581 | } else if (zTop + zHeight < height) { 582 | this.vCenter.y = height - half_height/zoom; 583 | zTop = height - zHeight; 584 | } 585 | } 586 | $(this.zimg).css({ 587 | width: width+"px", 588 | height: height+"px" 589 | }); 590 | 591 | var xt = -(this.vCenter.x - half_width)*zoom; 592 | var yt = -(this.vCenter.y - half_height)*zoom; 593 | $(this.zimg).css({transform: "translate(" + xt + "px," + yt + "px) scale(" + zoom + "," + zoom + ")" }); 594 | /* 595 | * define the onUpdate option to do something after the image is redisplayed 596 | * probably shouldn't pass out the this object - need to think of something better 597 | */ 598 | this.options.onUpdate.call(this); 599 | } 600 | } 601 | }); 602 | })(jQuery); 603 | -------------------------------------------------------------------------------- /docs/lib/hammer.min.js: -------------------------------------------------------------------------------- 1 | /*! Hammer.JS - v2.0.8 - 2016-04-23 2 | * http://hammerjs.github.io/ 3 | * 4 | * Copyright (c) 2016 Jorik Tangelder; 5 | * Licensed under the MIT license */ 6 | !function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance 1"); 140 | equal(tst.imgViewer("imgToCursor", 0.5, -0.5), null, "returns null for relative y image coordinate < 0"); 141 | equal(tst.imgViewer("imgToCursor", 0.5, 2.5), null, "returns null for relative x image coordinate > 1"); 142 | tst.remove(); 143 | start(); 144 | }).each(function() { 145 | if (this.complete) { $(this).load(); } 146 | }); 147 | }); 148 | 149 | asyncTest( "imgToCursor at 4x zoom",3, function() { 150 | var $img = $("#qunit-fixture img"); 151 | var zoom = 4; 152 | var tst = $img.imgViewer({zoom: zoom}); 153 | $img.load(function() { 154 | var width = $img.width(); 155 | var height = $img.height(); 156 | var offset = $img.offset(); 157 | var erelx = 0.5-0.5/zoom, 158 | erely = 0.5-0.5/zoom; 159 | var res = tst.imgViewer("imgToCursor", erelx, erely); 160 | ok(res.x === offset.left && res.y === offset.top, "top left corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + offset.left + " " + offset.top); 161 | erelx = 0.5+0.5/zoom; 162 | erely = 0.5+0.5/zoom; 163 | res = tst.imgViewer("imgToCursor", erelx, erely); 164 | ok(res.x === (offset.left+width) && res.y === (offset.top+height), "bottom right corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + (offset.left+width) + " " + (offset.top+height)); 165 | erelx = 0.5-0.25/zoom; 166 | erely = 0.5-0.25/zoom; 167 | res = tst.imgViewer("imgToCursor", erelx, erely); 168 | ok(res.x === (offset.left+width/4) && res.y === (offset.top+height/4), "internal point at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + (offset.left+width/4) + " " + (offset.top+height/4)); 169 | tst.remove(); 170 | start(); 171 | }).each(function() { 172 | if (this.complete) { $(this).load(); } 173 | }); 174 | }); 175 | 176 | asyncTest( "imgToCursor at 1x zoom with padding and border",1, function() { 177 | var $img = $("#qunit-fixture img"); 178 | var zoom = 1; 179 | $img.css({border: "30px solid #ccc", padding: "20px"}); 180 | var tst = $img.imgViewer({zoom: zoom}); 181 | $img.load(function() { 182 | var offset = $img.offset(); 183 | var erelx = 0.5-0.5/zoom, 184 | erely = 0.5-0.5/zoom; 185 | var res = tst.imgViewer("imgToCursor", erelx, erely); 186 | ok(res.x === offset.left+50 && res.y === offset.top+50, "top left corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + (offset.left+50) + " " + (offset.top+50)); 187 | tst.remove(); 188 | start(); 189 | }).each(function() { 190 | if (this.complete) { $(this).load(); } 191 | }); 192 | }); 193 | 194 | asyncTest( "imgToCursor at 4x zoom with padding and border",1, function() { 195 | var $img = $("#qunit-fixture img"); 196 | var zoom = 4; 197 | $img.css({border: "30px solid #ccc", padding: "20px"}); 198 | var tst = $img.imgViewer({zoom: zoom}); 199 | $img.load(function() { 200 | var offset = $img.offset(); 201 | var erelx = 0.5-0.5/zoom, 202 | erely = 0.5-0.5/zoom; 203 | var res = tst.imgViewer("imgToCursor", erelx, erely); 204 | ok(res.x === offset.left+50 && res.y === offset.top+50, "top left corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + (offset.left+50) + " " + (offset.top+50)); 205 | tst.remove(); 206 | start(); 207 | }).each(function() { 208 | if (this.complete) { $(this).load(); } 209 | }); 210 | }); 211 | 212 | 213 | asyncTest( "imgToView at 1x zoom",7, function() { 214 | var $img = $("#qunit-fixture img"); 215 | var tst = $img.imgViewer(); 216 | $img.load(function() { 217 | var width = $img.width(); 218 | var height = $img.height(); 219 | var res = tst.imgViewer("imgToView", 0.0, 0.0); 220 | ok(res.x === 0 && res.y === 0, "top left corner at 1x zoom is " + res.x + " " + res.y + " expected 0 0"); 221 | res = tst.imgViewer("imgToView", 1.0, 1.0); 222 | ok(res.x === width && res.y === height, "bottom right corner at 1x zoom is " + res.x + " " + res.y + " expected " + width + " " + height); 223 | res = tst.imgViewer("imgToView", 0.25, 0.25); 224 | ok(res.x === Math.round(width/4) && res.y === Math.round(height/4), "internal point at 1x zoom is " + res.x + " " + res.y + " expected " + Math.round(width/4) + " " + Math.round(height/4)); 225 | equal(tst.imgViewer("imgToView", -1.0, 0.5), null, "returns null for relative x image coordinate < 0"); 226 | equal(tst.imgViewer("imgToView", 2.0, 0.5), null, "returns null for relative x image coordinate > 1"); 227 | equal(tst.imgViewer("imgToView", 0.5, -0.5), null, "returns null for relative y image coordinate < 0"); 228 | equal(tst.imgViewer("imgToView", 0.5, 2.5), null, "returns null for relative x image coordinate > 1"); 229 | tst.remove(); 230 | start(); 231 | }).each(function() { 232 | if (this.complete) { $(this).load(); } 233 | }); 234 | }); 235 | 236 | asyncTest( "imgToView at 4x zoom",3, function() { 237 | var $img = $("#qunit-fixture img"); 238 | var zoom = 4; 239 | var tst = $img.imgViewer({zoom: zoom}); 240 | $img.load(function() { 241 | var width = $img.width(); 242 | var height = $img.height(); 243 | var erelx = 0.5-0.5/zoom, 244 | erely = 0.5-0.5/zoom; 245 | var res = tst.imgViewer("imgToView", erelx, erely); 246 | ok(res.x === 0 && res.y === 0, "top left corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected 0 0"); 247 | erelx = 0.5+0.5/zoom; 248 | erely = 0.5+0.5/zoom; 249 | res = tst.imgViewer("imgToView", erelx, erely); 250 | ok(res.x === width && res.y === height, "bottom right corner at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + width + " " + height); 251 | erelx = 0.5-0.25/zoom; 252 | erely = 0.5-0.25/zoom; 253 | res = tst.imgViewer("imgToView", erelx, erely); 254 | ok(res.x === Math.round(width/4) && res.y === Math.round(height/4), "internal point at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + Math.round(width/4) + " " + Math.round(height/4)); 255 | tst.remove(); 256 | start(); 257 | }).each(function() { 258 | if (this.complete) { $(this).load(); } 259 | }); 260 | }); 261 | 262 | asyncTest( "imgToView at 1x zoom with padding and border",1, function() { 263 | var $img = $("#qunit-fixture img"); 264 | $img.css({border: "30px solid #ccc", padding: "20px"}); 265 | var tst = $img.imgViewer(); 266 | $img.load(function() { 267 | var width = $img.width(); 268 | var height = $img.height(); 269 | var res = tst.imgViewer("imgToView", 0.25, 0.25); 270 | ok(res.x === Math.round(width/4) && res.y === Math.round(height/4), "internal point at 1x zoom is " + res.x + " " + res.y + " expected " + Math.round(width/4) + " " + Math.round(height/4)); 271 | tst.remove(); 272 | start(); 273 | }).each(function() { 274 | if (this.complete) { $(this).load(); } 275 | }); 276 | }); 277 | 278 | asyncTest( "imgToView at 4x zoom with padding and border",1, function() { 279 | var $img = $("#qunit-fixture img"); 280 | var zoom = 4; 281 | $img.css({border: "30px solid #ccc", padding: "20px"}); 282 | var tst = $img.imgViewer({zoom: zoom}); 283 | $img.load(function() { 284 | var width = $img.width(); 285 | var height = $img.height(); 286 | var erelx = 0.5-0.25/zoom; 287 | var erely = 0.5-0.25/zoom; 288 | var res = tst.imgViewer("imgToView", erelx, erely); 289 | ok(res.x === Math.round(width/4) && res.y === Math.round(height/4), "internal point at " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + Math.round(width/4) + " " + Math.round(height/4)); 290 | tst.remove(); 291 | start(); 292 | }).each(function() { 293 | if (this.complete) { $(this).load(); } 294 | }); 295 | }); 296 | 297 | asyncTest( "isVisible",9, function() { 298 | var $img = $("#qunit-fixture img"); 299 | var tst = $img.imgViewer(); 300 | $img.load(function() { 301 | ok(!(tst.imgViewer("isVisible", -0.1, 0.5)), "at 1x zoom returns false for relative x image coordinate < 0"); 302 | ok(!(tst.imgViewer("isVisible", 1.1, 0.5)), "at 1x zoom returns false for relative x image coordinate > 1"); 303 | ok(!(tst.imgViewer("isVisible", 0.5, -0.1)), "at 1x zoom returns false for relative y image coordinate < 0"); 304 | ok(!(tst.imgViewer("isVisible", 0.5, 1.5)), "at 1x zoom returns false for relative y image coordinate > 1"); 305 | ok(tst.imgViewer("isVisible", 0.0, 0.0), "at 1x zoom returns true for top left corner of view"); 306 | ok(tst.imgViewer("isVisible", 1.0, 1.0), "at 1x zoom returns true for bottom right corner of view"); 307 | ok(tst.imgViewer("isVisible", 0.5, 0.5), "at 1x zoom returns true for point inside view"); 308 | var zoom = 4; 309 | tst.imgViewer("option", "zoom", zoom); 310 | ok(!(tst.imgViewer("isVisible", 0.0, 0.0)), "at 4x zoom returns false for point outside view"); 311 | ok(tst.imgViewer("isVisible", 0.5, 0.5), "at 4x zoom returns true for point inside view"); 312 | tst.remove(); 313 | start(); 314 | }).each(function() { 315 | if (this.complete) { $(this).load(); } 316 | }); 317 | }); 318 | 319 | asyncTest( "panTo at 4x zoom",7, function() { 320 | var $img = $("#qunit-fixture img"); 321 | var zoom = 4; 322 | var tst = $img.imgViewer({zoom: zoom}); 323 | $img.load(function() { 324 | var erelx = 0.5-0.5/zoom, 325 | erely = 0.5-0.5/zoom; 326 | var res = tst.imgViewer("panTo", erelx, erely); 327 | ok(res.x === erelx && res.y === erely, "pan to internal location " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 328 | erelx = 1/(2*zoom); 329 | erely = 1/(2*zoom); 330 | res = tst.imgViewer("panTo", 0.0, 0.0); 331 | ok(res.x === erelx && res.y === erely, "pan to top left corner " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 332 | erelx = 1 - 1/(2*zoom); 333 | erely = 1 - 1/(2*zoom); 334 | res = tst.imgViewer("panTo", 1.0, 1.0); 335 | ok(res.x === erelx && res.y === erely, "pan to bottom right corner " + zoom + "x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 336 | equal(tst.imgViewer("panTo", -1.0, 0.5), null, "returns null for relative x image coordinate < 0"); 337 | equal(tst.imgViewer("panTo", 2.0, 0.5), null, "returns null for relative x image coordinate > 1"); 338 | equal(tst.imgViewer("panTo", 0.5, -0.5), null, "returns null for relative y image coordinate < 0"); 339 | equal(tst.imgViewer("panTo", 0.5, 2.5), null, "returns null for relative x image coordinate > 1"); 340 | tst.remove(); 341 | 342 | start(); 343 | }).each(function() { 344 | if (this.complete) { $(this).load(); } 345 | }); 346 | }); 347 | 348 | asyncTest( "viewToImg at 1x zoom",7, function() { 349 | var $img = $("#qunit-fixture img"); 350 | var tst = $img.imgViewer(); 351 | $img.load(function() { 352 | var width = $img.width(); 353 | var height = $img.height(); 354 | var res = tst.imgViewer("viewToImg", 0, 0); 355 | ok(res.x === 0 && res.y === 0, "top left corner at 1x zoom is " + res.x + " " + res.y + " expected 0 0"); 356 | res = tst.imgViewer("viewToImg", width, height); 357 | ok(res.x === 1 && res.y === 1, "bottom right corner at 1x zoom is " + res.x + " " + res.y + " expected 1 1"); 358 | res = tst.imgViewer("viewToImg", Math.round(width/4), Math.round(height/4)); 359 | ok(res.x === 0.25 && res.y === 0.25, "internal point at 1x zoom is " + res.x + " " + res.y + " expected 0.25 0.25"); 360 | equal(tst.imgViewer("viewToImg", -1, 0), null, "returns null for view x left of image"); 361 | equal(tst.imgViewer("viewToImg", 0, 2*width), null, "returns null for view x right of image > 1"); 362 | equal(tst.imgViewer("viewToImg", 0, -1), null, "returns null for view y above image"); 363 | equal(tst.imgViewer("viewToImg", 0, 2*height), null, "returns null for view y above image"); 364 | tst.remove(); 365 | start(); 366 | }).each(function() { 367 | if (this.complete) { $(this).load(); } 368 | }); 369 | }); 370 | 371 | asyncTest( "viewToImg at 4x zoom",3, function() { 372 | var $img = $("#qunit-fixture img"); 373 | var zoom = 4; 374 | var tst = $img.imgViewer({zoom: zoom}); 375 | $img.load(function() { 376 | var width = $img.width(); 377 | var height = $img.height(); 378 | var erelx = 0.5-0.5/zoom, 379 | erely = 0.5-0.5/zoom; 380 | var res = tst.imgViewer("viewToImg", 0, 0); 381 | ok(res.x === erelx && res.y === erely, "top left corner at 1x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 382 | erelx = 0.5+0.5/zoom; 383 | erely = 0.5+0.5/zoom; 384 | res = tst.imgViewer("viewToImg", width, height); 385 | ok(res.x === erelx && res.y === erely, "bottom right corner at 1x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 386 | erelx = 0.5-0.25/zoom; 387 | erely = 0.5-0.25/zoom; 388 | res = tst.imgViewer("viewToImg", Math.round(width/4), Math.round(height/4)); 389 | ok(res.x === erelx && res.y === erely, "internal point at 1x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 390 | tst.remove(); 391 | start(); 392 | }).each(function() { 393 | if (this.complete) { $(this).load(); } 394 | }); 395 | }); 396 | 397 | asyncTest( "viewToImg at 1x zoom with padding and border",1, function() { 398 | var $img = $("#qunit-fixture img"); 399 | $img.css({border: "30px solid #ccc", padding: "20px"}); 400 | var tst = $img.imgViewer(); 401 | $img.load(function() { 402 | var width = $img.width(); 403 | var height = $img.height(); 404 | var res = tst.imgViewer("viewToImg", Math.round(width/4), Math.round(height/4)); 405 | ok(res.x === 0.25 && res.y === 0.25, "internal point at 1x zoom is " + res.x + " " + res.y + " expected 0.25 0.25"); 406 | tst.remove(); 407 | start(); 408 | }).each(function() { 409 | if (this.complete) { $(this).load(); } 410 | }); 411 | }); 412 | 413 | asyncTest( "viewToImg at 4x zoom with padding and border",1, function() { 414 | var $img = $("#qunit-fixture img"); 415 | var zoom = 4; 416 | $img.css({border: "30px solid #ccc", padding: "20px"}); 417 | var tst = $img.imgViewer({zoom: zoom}); 418 | $img.load(function() { 419 | var width = $img.width(); 420 | var height = $img.height(); 421 | var erelx = 0.5-0.25/zoom; 422 | var erely = 0.5-0.25/zoom; 423 | var res = tst.imgViewer("viewToImg", Math.round(width/4), Math.round(height/4)); 424 | ok(res.x === erelx && res.y === erely, "internal point at 1x zoom is " + res.x + " " + res.y + " expected " + erelx + " " + erely); 425 | tst.remove(); 426 | start(); 427 | }).each(function() { 428 | if (this.complete) { $(this).load(); } 429 | }); 430 | }); 431 | 432 | }(jQuery)); 433 | 434 | 435 | --------------------------------------------------------------------------------