├── .gitignore ├── demo └── index.html ├── LICENSE ├── README.md ├── style.css └── photoTilt.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 John Tregoning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Photo Tilt 2 | ========= 3 | 4 | HTML5 clone of the photo tilt feature/gesture/ux found in Facebook's Paper app. 5 | 6 | Basic Usage 7 | ----- 8 | PhotoTilt (image url, container node) 9 | ``` 10 | var photoTilt = new PhotoTilt({ 11 | url: 'photo.jpg', 12 | lowResUrl: 'lowRes.jpg', //optional it will load lowRes 1st if available 13 | maxTilt: 18, //optional, defaults to 20 14 | container: document.body //optional, defaults to body 15 | reverseTilt: false //optional, defaults to false 16 | }); 17 | ``` 18 | Note: The speed of the tilt can be tweaked by updating the transform transtion speed in the CSS file. 19 | 20 | Demo 21 | ---- 22 | You can find a working example [here](http://s3.jt.io/tilt/index.html) (make sure you test this on a device with a triaxial/accelerometer like a phone/tablet). 23 | 24 | More 25 | ---- 26 | Blog post with extra information [here](http://jt.io/2014/photo-tilt/). 27 | 28 | TODO 29 | ---- 30 | 31 | * add option for [full screen mode](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode) 32 | * implement [lockOrientation](https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation) (only works in FF) 33 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | overflow:hidden; 4 | height: 100%; 5 | width: 100%; 6 | padding: 0; 7 | margin: 0; 8 | background-color: #000; 9 | } 10 | 11 | .mask { 12 | position: absolute; 13 | top:0; 14 | left: 0; 15 | right: 0; 16 | bottom: 0; 17 | overflow: hidden; 18 | } 19 | 20 | .mask img { 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | } 25 | 26 | .tilt-bar { 27 | position: fixed; 28 | bottom: 30px; 29 | left: 10px; 30 | right: 10px; 31 | height: 1px; 32 | z-index: 1; 33 | background-color: rgba(255,255,255,0.3); 34 | } 35 | 36 | .tilt-indicator { 37 | background-color: rgba(255,255,255,0.8); 38 | height: 1px; 39 | position: absolute; 40 | } 41 | 42 | .mask img, 43 | .tilt-indicator { 44 | -webkit-transition: -webkit-transform 0.2s linear; 45 | -moz-transition: -moz-transform 0.2s linear; 46 | -ms-transition: -ms-transform 0.2s linear; 47 | transition: transform 0.2s linear; 48 | 49 | -webkit-backface-visibility: hidden; 50 | -moz-backface-visibility: hidden; 51 | -ms-backface-visibility: hidden; 52 | backface-visibility: hidden; 53 | 54 | -webkit-perspective: 1000; 55 | -moz-perspective: 1000; 56 | -ms-perspective: 1000; 57 | perspective: 1000; 58 | 59 | -webkit-transform: translatez(0); 60 | -moz-transform: translatez(0); 61 | -ms-transform: translatez(0); 62 | transform: translatez(0); 63 | } 64 | 65 | .disable-transitions .mask img, 66 | .disable-transitions .tilt-indicator { 67 | -webkit-transition: none; 68 | -moz-transition: none; 69 | -ms-transition: none; 70 | transition: none; 71 | } 72 | 73 | .is-resizing { 74 | visibility: hidden; 75 | } -------------------------------------------------------------------------------- /photoTilt.js: -------------------------------------------------------------------------------- 1 | var PhotoTilt = function(options) { 2 | 3 | 'use strict'; 4 | 5 | var imgUrl = options.url, 6 | lowResUrl = options.lowResUrl, 7 | container = options.container || document.body, 8 | latestTilt = 0, 9 | timeoutID = 0, 10 | disableTilt, 11 | viewport, 12 | imgData, 13 | img, 14 | imgLoader, 15 | delta, 16 | centerOffset, 17 | tiltBarWidth, 18 | tiltCenterOffset, 19 | tiltBarIndicatorWidth, 20 | tiltBarIndicator, 21 | config; 22 | 23 | config = { 24 | maxTilt: options.maxTilt || 20, 25 | twoPhase: options.lowResUrl || false, 26 | reverseTilt: options.reverseTilt || false 27 | }; 28 | 29 | window.requestAnimationFrame = window.requestAnimationFrame || 30 | window.mozRequestAnimationFrame || 31 | window.webkitRequestAnimationFrame || 32 | window.msRequestAnimationFrame; 33 | 34 | var init = function() { 35 | 36 | var url = config.twoPhase ? lowResUrl : imgUrl; 37 | 38 | generateViewPort(); 39 | 40 | preloadImg(url, function() { 41 | 42 | img = imgLoader.cloneNode(false); 43 | generateImgData(); 44 | imgLoader = null; 45 | 46 | if (config.twoPhase) { 47 | 48 | preloadImg(imgUrl, function() { 49 | img.src = imgLoader.src; 50 | imgLoader = null; 51 | }); 52 | 53 | } 54 | 55 | render(); 56 | addEventListeners(); 57 | 58 | }); 59 | 60 | }; 61 | 62 | var updatePosition = function() { 63 | 64 | var tilt = latestTilt, 65 | pxToMove; 66 | 67 | if (tilt > 0) { 68 | tilt = Math.min(tilt, config.maxTilt); 69 | } else { 70 | tilt = Math.max(tilt, config.maxTilt * -1); 71 | } 72 | 73 | if (!config.reverseTilt) { 74 | tilt = tilt * -1; 75 | } 76 | 77 | pxToMove = (tilt * centerOffset) / config.maxTilt; 78 | 79 | updateImgPosition(imgData, (centerOffset + pxToMove) * -1); 80 | 81 | updateTiltBar(tilt); 82 | 83 | window.requestAnimationFrame(updatePosition); 84 | 85 | }; 86 | 87 | var updateTiltBar = function(tilt) { 88 | 89 | var pxToMove = (tilt * ((tiltBarWidth - tiltBarIndicatorWidth) / 2)) / config.maxTilt; 90 | setTranslateX(tiltBarIndicator, (tiltCenterOffset + pxToMove) ); 91 | 92 | }; 93 | 94 | var updateImgPosition = function(imgData, pxToMove) { 95 | setTranslateX(img, pxToMove); 96 | }; 97 | 98 | var addEventListeners = function() { 99 | 100 | if (window.DeviceOrientationEvent) { 101 | 102 | var averageGamma = []; 103 | 104 | window.addEventListener('deviceorientation', function(eventData) { 105 | 106 | if (!disableTilt) { 107 | 108 | if (averageGamma.length > 8) { 109 | averageGamma.shift(); 110 | } 111 | 112 | averageGamma.push(eventData.gamma); 113 | 114 | latestTilt = averageGamma.reduce(function(a, b) { return a+b; }) / averageGamma.length; 115 | 116 | } 117 | 118 | }, false); 119 | 120 | window.requestAnimationFrame(updatePosition); 121 | 122 | } 123 | 124 | window.addEventListener('resize', function() { 125 | 126 | container.classList.add('is-resizing'); 127 | window.clearTimeout(timeoutID); 128 | 129 | timeoutID = window.setTimeout(function() { 130 | 131 | generateViewPort(); 132 | container.innerHTML = ""; 133 | render(); 134 | container.classList.remove('is-resizing'); 135 | 136 | }, 100); 137 | 138 | }, false); 139 | 140 | }; 141 | 142 | var setTranslateX = function(node, amount) { 143 | node.style.webkitTransform = 144 | node.style.MozTransform = 145 | node.style.msTransform = 146 | node.style.transform = "translateX(" + Math.round(amount) + "px)"; 147 | }; 148 | 149 | var render = function() { 150 | 151 | var mask, 152 | tiltBar, 153 | resizedImgWidth, 154 | tiltBarPadding = 20; 155 | 156 | mask = document.createElement('div'); 157 | mask.classList.add('mask'); 158 | 159 | img.height = viewport.height; 160 | resizedImgWidth = (imgData.aspectRatio * img.height); 161 | 162 | delta = resizedImgWidth - viewport.width; 163 | centerOffset = delta / 2; 164 | 165 | tiltBar = document.createElement('div'); 166 | tiltBar.classList.add('tilt-bar'); 167 | tiltBarWidth = viewport.width - tiltBarPadding; 168 | 169 | tiltBarIndicator = document.createElement('div'); 170 | tiltBarIndicator.classList.add('tilt-indicator'); 171 | 172 | tiltBarIndicatorWidth = (viewport.width * tiltBarWidth) / resizedImgWidth; 173 | tiltBarIndicator.style.width = tiltBarIndicatorWidth + 'px'; 174 | 175 | tiltCenterOffset = ((tiltBarWidth / 2) - (tiltBarIndicatorWidth / 2)); 176 | 177 | updatePosition(); 178 | 179 | if (tiltCenterOffset > 0) { 180 | disableTilt = false; 181 | tiltBar.appendChild(tiltBarIndicator); 182 | mask.appendChild(tiltBar); 183 | container.classList.remove('disable-transitions'); 184 | } else { 185 | disableTilt = true; 186 | latestTilt = 0; 187 | container.classList.add('disable-transitions'); 188 | } 189 | 190 | mask.appendChild(img); 191 | container.appendChild(mask); 192 | 193 | }; 194 | 195 | var generateViewPort = function() { 196 | 197 | var containerStyle = window.getComputedStyle(container, null); 198 | 199 | viewport = { 200 | width: parseInt(containerStyle.width, 10), 201 | height: parseInt(containerStyle.height, 10) 202 | }; 203 | 204 | }; 205 | 206 | var generateImgData = function() { 207 | 208 | imgData = { 209 | width: imgLoader.width, 210 | height: imgLoader.height, 211 | aspectRatio: imgLoader.width / imgLoader.height, 212 | src: imgLoader.src 213 | }; 214 | 215 | }; 216 | 217 | var preloadImg = function(url, done) { 218 | 219 | imgLoader = new Image(); 220 | imgLoader.addEventListener('load', done, false); 221 | imgLoader.src = url; 222 | 223 | }; 224 | 225 | init(); 226 | 227 | return { 228 | getContainer: function(){ 229 | return container; 230 | } 231 | } 232 | 233 | }; --------------------------------------------------------------------------------