├── assets ├── img │ ├── bg-static.png │ └── bg-parallax.png ├── js │ ├── parallax.js │ └── device-orientation.js └── css │ └── site.css ├── COPYING ├── readme.md └── index.html /assets/img/bg-static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/device-orientation-vertical/master/assets/img/bg-static.png -------------------------------------------------------------------------------- /assets/img/bg-parallax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/device-orientation-vertical/master/assets/img/bg-parallax.png -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Device Orientation - Vertical 2 | 3 | ## Post 4 | 5 | - [https://f90.co.uk/labs/device-orientation-vertical/](https://f90.co.uk/labs/device-orientation-vertical/) 6 | 7 | ## Example 8 | 9 | - [http://orangespaceman.github.io/device-orientation-vertical](http://orangespaceman.github.io/device-orientation-vertical) 10 | 11 | Copyright © 2018 Me 12 | This work is free. You can redistribute it and/or modify it under the 13 | terms of the Do What The Fuck You Want To Public License, Version 2, 14 | as published by Sam Hocevar. See the COPYING file for more details. 15 | -------------------------------------------------------------------------------- /assets/js/parallax.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // el: bg image to scroll 4 | var bgEl; 5 | 6 | // int: remember last offset so we only reposition on scroll 7 | var lastOffset = 0; 8 | 9 | // method called on page load to init all behaviour 10 | function load() { 11 | initElements(); 12 | update(); 13 | } 14 | 15 | // find all DOM elements 16 | function initElements() { 17 | bgEl = document.querySelector('.Background-parallax'); 18 | } 19 | 20 | function update() { 21 | updateScroll(); 22 | requestAnimationFrame(update); 23 | } 24 | 25 | function updateScroll() { 26 | var newOffset = window.pageYOffset || 0; 27 | if (newOffset !== lastOffset) { 28 | var offsetMultiplier = 0.4; 29 | var offset = Math.round(newOffset * offsetMultiplier); 30 | bgEl.style.transform = 'translate3d(0, ' + offset + 'px, 0)'; 31 | lastOffset = newOffset; 32 | } 33 | } 34 | 35 | // init 36 | document.addEventListener('DOMContentLoaded', load); 37 | })(); 38 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Device Orientation - Vertical scroll 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |

Top

25 |
26 |
27 |

Middle

28 |
29 |
30 |

Bottom

31 |
32 |
33 |
34 |
35 | 36 |
37 |

38 | α 39 | 40 |

41 |

42 | β 43 | 44 |

45 |

46 | γ 47 | 48 |

49 |

50 | β' 51 | 52 |

53 |

54 | top 55 | 56 |

57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /assets/css/site.css: -------------------------------------------------------------------------------- 1 | /* elements */ 2 | 3 | *, 4 | *:before, 5 | *:after { 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | font-size: 16px; 11 | height: 100%; 12 | width: 100%; 13 | } 14 | 15 | body { 16 | background: #345; 17 | font-family: Georgia, serif; 18 | height: 100%; 19 | margin: 0; 20 | width: 100%; 21 | } 22 | 23 | body.has-deviceOrientation { 24 | overflow: hidden; 25 | } 26 | 27 | /* Wrapper */ 28 | 29 | .Wrapper { 30 | color: #fff; 31 | text-align: center; 32 | margin: 0 auto; 33 | } 34 | 35 | body.has-deviceOrientation .Wrapper { 36 | overflow: hidden; 37 | } 38 | 39 | /* Wrapper canvas - used to position background images */ 40 | 41 | .Wrapper-canvas { 42 | position: relative; 43 | margin: 0 auto; 44 | } 45 | 46 | /* Wrapper content - constrain proportions */ 47 | 48 | .Wrapper-content { 49 | max-width: 400px; 50 | margin: 0 auto; 51 | padding-left: 1rem; 52 | padding-right: 1rem; 53 | position: relative; 54 | z-index: 2; 55 | } 56 | 57 | /* Backgrounds - position below content */ 58 | 59 | .Background { 60 | position: absolute; 61 | bottom: 0; 62 | left: 0; 63 | right: 0; 64 | top: 0; 65 | overflow: hidden; 66 | z-index: 1; 67 | } 68 | 69 | .Background-static, 70 | .Background-parallax { 71 | background-image: url(../img/bg-static.png); 72 | position: absolute; 73 | left: 0; 74 | right: 0; 75 | top: 0; 76 | bottom: 0; 77 | background-position: center; 78 | background-size: contain; 79 | z-index: 2; 80 | transform: translate3d(0, 0, 0); 81 | } 82 | 83 | .Background-parallax { 84 | background-image: url(../img/bg-parallax.png); 85 | } 86 | 87 | /* Blocks */ 88 | 89 | .Block { 90 | display: flex; 91 | align-items: center; 92 | justify-content: center; 93 | min-height: 100vh; 94 | } 95 | 96 | .Block-message { 97 | font-size: 4rem; 98 | text-shadow: 3px 3px 1px rgba(255, 255, 255, 0.5); 99 | } 100 | 101 | 102 | /* debug */ 103 | 104 | .Debug { 105 | display: none; 106 | justify-content: space-around; 107 | position: fixed; 108 | top: 0; 109 | right: 0; 110 | left: 0; 111 | background: rgba(255, 255, 255, 0.4); 112 | padding: 0.2rem; 113 | z-index: 3; 114 | font-size: 0.75rem; 115 | text-align: left; 116 | } 117 | 118 | .Debug-block { 119 | margin: 0; 120 | } 121 | 122 | body.has-deviceOrientation .Debug { 123 | display: flex; 124 | } 125 | -------------------------------------------------------------------------------- /assets/js/device-orientation.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // assume device doesn't report orientation unless proven otherwise 4 | // this allows us to update styles and behaviour only if supported 5 | var hasDeviceOrientationInited = false; 6 | 7 | // els: elements to scroll 8 | // for vertical scroll, some devices support `document.documentElement` 9 | // while others use `document.body` so to be safe we support both 10 | var scrollVerticalEl; 11 | var scrollVerticalElAlt; 12 | 13 | // initial values for beta/gamma, to base later calculations on 14 | var initialBeta = 0; 15 | 16 | // el: the wrapper element surrounds all page content 17 | var wrapperEl; 18 | var wrapperHeight = 0; 19 | 20 | // height of the screen 21 | var screenHeight = 0; 22 | 23 | // device orientation - default to portrait 24 | var isLandscape = false; 25 | var isRotatedClockwise = false; 26 | 27 | // int: store previous top value 28 | var lastTop; 29 | 30 | // debounced function for listening to resize events 31 | var resizeDebounceFunction = debounce(handleOrientationChange, 10); 32 | 33 | // debug els 34 | var debugAlphaEl; 35 | var debugBetaEl; 36 | var debugGammaEl; 37 | var debugBetaModifiedEl; 38 | var debugTopEl; 39 | 40 | // method called on page load to init all behaviour 41 | function load() { 42 | initElements(); 43 | calculateCanvasDimensions(); 44 | calculateDeviceOrientation(); 45 | initScroll(); 46 | initDebug(); 47 | } 48 | 49 | // find all DOM elements 50 | function initElements() { 51 | scrollVerticalEl = document.documentElement; 52 | scrollVerticalElAlt = document.body; 53 | wrapperEl = document.querySelector('.Wrapper'); 54 | } 55 | 56 | // gather canvas dimensions, to be used later in calculations 57 | function calculateCanvasDimensions() { 58 | wrapperHeight = wrapperEl.offsetHeight; 59 | wrapperWidth = wrapperEl.offsetWidth; 60 | screenHeight = document.documentElement.clientHeight; 61 | } 62 | 63 | // calculate whether the device is landscape or portrait 64 | function calculateDeviceOrientation(e) { 65 | isLandscape = 66 | document.documentElement.clientHeight < document.documentElement.clientWidth; 67 | isRotatedClockwise = window.orientation === -90; 68 | } 69 | 70 | // set initial scroll position 71 | function initScroll() { 72 | var top = wrapperHeight; 73 | updateScrollPosition(top); 74 | } 75 | 76 | // update scroll position 77 | function updateScrollPosition(top) { 78 | scrollVerticalEl.scrollTop = top; 79 | scrollVerticalElAlt.scrollTop = top; 80 | } 81 | 82 | // further initialisation logic from first device orientation event 83 | // 84 | // browsers report that they support device orientation 85 | // even when they don't contain a giroscope, 86 | // so for the first device orientation event, 87 | // set up site to support them 88 | function initDeviceOrientation() { 89 | var body = document.querySelector('body'); 90 | body.classList.add('has-deviceOrientation'); 91 | hasDeviceOrientationInited = true; 92 | 93 | // with the addition of a new class on the body element, 94 | // styles may now be different for devices that support device orientation, 95 | // so re-evaluate dimensions 96 | calculateCanvasDimensions(); 97 | 98 | window.addEventListener('resize', resizeDebounceFunction); 99 | 100 | // Disable scrolling by touch 101 | wrapperEl.ontouchmove = function (e) { 102 | e.preventDefault(); 103 | e.stopPropagation(); 104 | } 105 | } 106 | 107 | // recalculate values based on major device rotation 108 | // (e.g. landscape to portrait or vice versa) 109 | function handleOrientationChange() { 110 | // allow time for the screen layout to readjust first 111 | setTimeout(function() { 112 | calculateDeviceOrientation(); 113 | calculateCanvasDimensions(); 114 | 115 | // the initial values will need to be reassessed 116 | initialBeta = null; 117 | }, 500); 118 | } 119 | 120 | // update scroll position based on orientation change event 121 | function handleOrientationEvent(event) { 122 | if (!hasDeviceOrientationInited) { 123 | initDeviceOrientation(); 124 | } 125 | 126 | // store initial values for later use 127 | if (!initialBeta) { 128 | initialBeta = calculateBeta(event); 129 | } 130 | 131 | // calculate orientation 132 | // need to switch beta/gamma if device is in landscape mode 133 | var beta = calculateBeta(event); 134 | 135 | // calculate scroll position from orientation 136 | var top = calculateVerticalScroll(beta, event); 137 | 138 | // update scroll 139 | updateScrollPosition(top); 140 | 141 | // store top value 142 | lastTop = top; 143 | 144 | debug(event, beta, top); 145 | } 146 | 147 | // calculate beta based on device orientation 148 | // and fix range values accordingly 149 | function calculateBeta(event) { 150 | if (isLandscape) { 151 | if (isRotatedClockwise) { 152 | return normaliseGammaClockwiseRotation(event.gamma); 153 | } else { 154 | return normaliseGammaAntiClockwiseRotation(event.gamma); 155 | } 156 | } else { 157 | return normaliseBeta(event.beta); 158 | } 159 | } 160 | 161 | // convert beta from [-180,180] to [0,360] 162 | // and make it increase consistently 163 | // rather than jump at the half-way point 164 | // 165 | // raw beta values start in the range: 166 | // 0 (face up) [--> 1 ] 167 | // 90 (horizontal) [--> 90 ] 168 | // 179 (almost face down) [--> 179 ] 169 | // -179 (almost face down inverted) [--> 181 new value] 170 | // -90 (horizontal inverted) [--> 270 new value] 171 | // -1 (almost face up inverted) [--> 359 new value] 172 | function normaliseBeta(beta) { 173 | if (beta < 0) { beta = 360 + beta; } 174 | if (beta > 270) { beta = 0; } 175 | return beta; 176 | } 177 | 178 | // convert gamma from [-90,90] to [0,180] 179 | // and make it increase consistently 180 | // rather than jump at the half-way point 181 | // 182 | // raw gamma values start in the range: 183 | // below the horizon, -90 (close to horizon) down to 0 (face up) 184 | // above the horizon, 90 (close to horizon) down to 0 (face down) 185 | // 186 | // -1 (face up) [--> 179 new value] 187 | // -89 (just below horizon) [--> 91 new value] 188 | // 89 (just above horizon) [--> 89 ] 189 | // 1 (almost face down) [--> 1 ] 190 | function normaliseGammaClockwiseRotation(gamma) { 191 | if (gamma < 0) { gamma = 180 - Math.abs(gamma); } 192 | return gamma; 193 | } 194 | 195 | // convert gamma from [-90,90] to [0,180] 196 | // and make it increase consistently 197 | // rather than jump at the half-way point 198 | // 199 | // raw gamma values start in the range: 200 | // below the horizon, 90 (close to horizon) down to 0 (face up) 201 | // above the horizon, -90 (close to horizon) down to 0 (face down) 202 | // 203 | // 1 (face up) [--> 179 new value] 204 | // 89 (just below horizon) [--> 91 new value] 205 | // -89 (just above horizon) [--> 89 new value] 206 | // -1 (almost face down) [--> 1 new value] 207 | function normaliseGammaAntiClockwiseRotation(gamma) { 208 | if (gamma > 0) { gamma = 180 - gamma; } 209 | if (gamma < 0) { gamma = Math.abs(gamma); } 210 | return gamma; 211 | } 212 | 213 | // calculate new vertical scroll position 214 | // convert beta value to a value within page height range 215 | function calculateVerticalScroll(beta, event) { 216 | var currentBeta = beta; 217 | var maxBeta; 218 | 219 | // starting angle below horizon, max angle is directly above 220 | if (initialBeta <= 90) { 221 | maxBeta = 140; 222 | 223 | // starting angle above horizon, max angle reduced 224 | } else if (initialBeta <= 180) { 225 | if (!isLandscape) { 226 | maxBeta = initialBeta + 90; 227 | 228 | // cap landscape max at 180 as gamma values don't go above this 229 | // and it avoids complex maths... 230 | } else { 231 | maxBeta = 180; 232 | } 233 | 234 | // starting angle above head, max angle reduced and capped 235 | } else if (initialBeta > 180) { 236 | maxBeta = 250; 237 | } 238 | 239 | // lock to top when moving beyond max angle 240 | if (currentBeta > maxBeta) { 241 | if (!isLandscape) { 242 | currentBeta = maxBeta; 243 | } else { 244 | if ( 245 | (isRotatedClockwise && event.gamma > 0) || 246 | (!isRotatedClockwise && event.gamma < 0) 247 | ) { 248 | currentBeta = initialBeta; 249 | } else { 250 | currentBeta = maxBeta; 251 | } 252 | } 253 | } 254 | 255 | // lock to bottom when moving below initial angle 256 | if (currentBeta < initialBeta) { 257 | if (!isLandscape) { 258 | currentBeta = initialBeta; 259 | } else { 260 | 261 | // phone is currently above horizon 262 | if (Math.abs(event.beta) > 90) { 263 | 264 | // if the phone started above the horizon 265 | if (initialBeta > 90) { 266 | 267 | // lock to the top if if has gone beyond 180 degrees 268 | if ( 269 | (isRotatedClockwise && event.gamma > 0) || 270 | (!isRotatedClockwise && event.gamma < 0) 271 | ) { 272 | currentBeta = maxBeta; 273 | 274 | // lock to the bottom if it is just below the initial value 275 | } else { 276 | currentBeta = initialBeta; 277 | } 278 | 279 | // the phone started below the horizon, lock to bottom 280 | } else { 281 | currentBeta = maxBeta; 282 | } 283 | 284 | // phone is currently below horizon, lock to bottom 285 | } else { 286 | currentBeta = initialBeta; 287 | } 288 | } 289 | } 290 | 291 | // generate a value for the page scroll: 292 | // map the current beta from somewhere between its initial and max value 293 | // to somewhere between the top and bottom of the page 294 | var top = mapRange(currentBeta, initialBeta, maxBeta, wrapperHeight - screenHeight, 0); 295 | 296 | // if the top value has changed from last time 297 | if (lastTop && top !== lastTop) { 298 | 299 | // if the top value has increased or decreased by more than this value, 300 | // smooth the transition to reduce the visible jump 301 | // the higher the number, the less likely that any adjustment is needed 302 | var movementLimit = 5; 303 | 304 | // adjustment value to apply to smooth the transition 305 | // the closer to 0, the quicker the transition 306 | // the closer to 1, the slower the transition 307 | var scrollAdjustment = 0.9; 308 | 309 | // if we are scrolling down the page at too high a rate, adjust 310 | if (!isLandscape && top > lastTop && top - movementLimit > lastTop) { 311 | top = top - ((top - lastTop) * scrollAdjustment); 312 | 313 | // if we are scrolling up the page at too high a rate, adjust 314 | } else if (!isLandscape && top < lastTop && top + movementLimit < lastTop) { 315 | top = top + ((lastTop - top) * scrollAdjustment); 316 | } 317 | } 318 | 319 | return Math.round(top); 320 | } 321 | 322 | function initDebug() { 323 | debugAlphaEl = document.querySelector('.Debug-value--alpha'); 324 | debugBetaEl = document.querySelector('.Debug-value--beta'); 325 | debugGammaEl = document.querySelector('.Debug-value--gamma'); 326 | debugBetaModifiedEl = document.querySelector('.Debug-value--betaModified'); 327 | debugTopEl = document.querySelector('.Debug-value--top'); 328 | } 329 | 330 | function debug(event, beta, top) { 331 | debugAlphaEl.textContent = Math.round(event.alpha); 332 | debugBetaEl.textContent = Math.round(event.beta); 333 | debugGammaEl.textContent = Math.round(event.gamma); 334 | debugBetaModifiedEl.textContent = Math.round(beta); 335 | debugTopEl.textContent = top; 336 | } 337 | 338 | // map a value from, one [min-max] range to another [min-max] range 339 | function mapRange(value, fromMin, fromMax, toMin, toMax) { 340 | return (value - fromMin) * (toMax - toMin) / (fromMax - fromMin) + toMin; 341 | } 342 | 343 | // https://davidwalsh.name/javascript-debounce-function 344 | // Returns a function, that, as long as it continues to be invoked, will not 345 | // be triggered. The function will be called after it stops being called for 346 | // N milliseconds. If `immediate` is passed, trigger the function on the 347 | // leading edge, instead of the trailing. 348 | function debounce(func, wait, immediate) { 349 | var timeout; 350 | return function() { 351 | var context = this, args = arguments; 352 | var later = function() { 353 | timeout = null; 354 | if (!immediate) func.apply(context, args); 355 | }; 356 | var callNow = immediate && !timeout; 357 | clearTimeout(timeout); 358 | timeout = setTimeout(later, wait); 359 | if (callNow) func.apply(context, args); 360 | }; 361 | } 362 | 363 | // init listeners 364 | document.addEventListener('DOMContentLoaded', load); 365 | window.addEventListener('orientationchange', handleOrientationChange); 366 | window.addEventListener('deviceorientation', handleOrientationEvent); 367 | 368 | })(); 369 | --------------------------------------------------------------------------------