├── .gitignore ├── src ├── assets │ ├── cp-paris-google-office-outside.jpg │ └── tiles │ │ ├── cp-paris-google-office-outside-0-0.jpg │ │ ├── cp-paris-google-office-outside-0-1.jpg │ │ ├── cp-paris-google-office-outside-1-0.jpg │ │ ├── cp-paris-google-office-outside-1-1.jpg │ │ └── README.md ├── css │ ├── topheman-panorama.css │ └── common.css ├── js │ ├── require.config.js │ ├── topheman-panorama │ │ ├── vendor │ │ │ ├── rAF.js │ │ │ └── device-motion-polyfill.js │ │ ├── require.plugins │ │ │ └── async.js │ │ ├── utils │ │ │ ├── blockedPopup.js │ │ │ ├── sensorsChecker.js │ │ │ └── deviceorientationHelper.js │ │ └── PanoramaManager.js │ ├── utils │ │ └── screenManager.js │ └── vendor │ │ └── require.js ├── googleAnalyticsScriptTag.html ├── demo.basic.html ├── test.bug.android.firefox.html ├── demo.deviceorientationhelper.html ├── demo.events.html ├── demo.controls.html ├── demo.custompanorama.html ├── README.md ├── demo.custompanorama.tiles.html └── index.html ├── grunt.options.default.json ├── RELEASESNOTES.md ├── package.json ├── LICENSE ├── README.md └── Gruntfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | /nbproject 3 | /.ftppass 4 | /grunt.options.json 5 | /node_modules 6 | /release 7 | -------------------------------------------------------------------------------- /src/assets/cp-paris-google-office-outside.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/PanoramaSensorsViewer/HEAD/src/assets/cp-paris-google-office-outside.jpg -------------------------------------------------------------------------------- /src/assets/tiles/cp-paris-google-office-outside-0-0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/PanoramaSensorsViewer/HEAD/src/assets/tiles/cp-paris-google-office-outside-0-0.jpg -------------------------------------------------------------------------------- /src/assets/tiles/cp-paris-google-office-outside-0-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/PanoramaSensorsViewer/HEAD/src/assets/tiles/cp-paris-google-office-outside-0-1.jpg -------------------------------------------------------------------------------- /src/assets/tiles/cp-paris-google-office-outside-1-0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/PanoramaSensorsViewer/HEAD/src/assets/tiles/cp-paris-google-office-outside-1-0.jpg -------------------------------------------------------------------------------- /src/assets/tiles/cp-paris-google-office-outside-1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/PanoramaSensorsViewer/HEAD/src/assets/tiles/cp-paris-google-office-outside-1-1.jpg -------------------------------------------------------------------------------- /src/assets/tiles/README.md: -------------------------------------------------------------------------------- 1 | Tiles generated with TilesGenerator, a little nodejs module I made, that I will release as soon as possible, once I stabilized it and documented it a little ... -------------------------------------------------------------------------------- /grunt.options.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "ftp-deploy": { 3 | "release" : { 4 | "host" : "ftp.example.com", 5 | "port" : 21, 6 | "dest" : "/path/to/directory/on/your/server" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/css/topheman-panorama.css: -------------------------------------------------------------------------------- 1 | #pano { 2 | position: absolute; 3 | top: 0px; 4 | left: 0px; 5 | border: 0px; 6 | width: 0px; 7 | height: 0px; 8 | display: block; 9 | } 10 | body{ 11 | margin:0px; 12 | } -------------------------------------------------------------------------------- /RELEASESNOTES.md: -------------------------------------------------------------------------------- 1 | RELEASES NOTES 2 | ============== 3 | 4 | #0.7.7 5 | 6 | * Updated Gruntfile.js (missing one html file for preprocessing) 7 | 8 | #0.7.6 9 | 10 | * fixed `options.disableTouchmove` (was broken on android phones on chrome and opera because of an update in blink). 11 | * For the moment, you may be able to move the panorama with a touchmove for 450ms until it comes back to the right place. 12 | * iOs devices are not impacted. -------------------------------------------------------------------------------- /src/js/require.config.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | baseUrl: "./js", 3 | // paths are analogous to old-school -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PanoramaSensorsViewer", 3 | "version": "0.7.7", 4 | "description": "Browse Streetview like if you were in it", 5 | "main": "grunt server", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/topheman/PanoramaSensorsViewer" 12 | }, 13 | "keywords": [ 14 | "mobile", 15 | "deviceorientation", 16 | "streetview" 17 | ], 18 | "author": "Christophe Rosset", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/topheman/PanoramaSensorsViewer/issues" 22 | }, 23 | "devDependencies": { 24 | "matchdep": ">=0.1.1", 25 | "grunt": ">=0.4.0", 26 | "grunt-contrib-connect": ">=0.1.2", 27 | "grunt-open": ">=0.2.0", 28 | "grunt-ftp-deploy": "~0.1.1", 29 | "grunt-contrib-copy": "~0.4.1", 30 | "grunt-contrib-clean": "~0.5.0", 31 | "grunt-preprocess": "~4.0.0", 32 | "grunt-git-rev-parse": "~0.1.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the license for PanoramaManager.js and its dependencies (only the ones copyrighted Christophe Rosset). It does not apply to requireJS, async, device-motion-polyfill or rAF. 2 | 3 | Copyright (C) 2013-2014 Christophe Rosset 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/js/topheman-panorama/vendor/rAF.js: -------------------------------------------------------------------------------- 1 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 2 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 3 | 4 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 5 | 6 | // MIT license 7 | 8 | (function() { 9 | var lastTime = 0; 10 | var vendors = ['ms', 'moz', 'webkit', 'o']; 11 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 12 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 13 | window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 14 | || window[vendors[x]+'CancelRequestAnimationFrame']; 15 | } 16 | 17 | if (!window.requestAnimationFrame) 18 | window.requestAnimationFrame = function(callback, element) { 19 | var currTime = new Date().getTime(); 20 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 21 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 22 | timeToCall); 23 | lastTime = currTime + timeToCall; 24 | return id; 25 | }; 26 | 27 | if (!window.cancelAnimationFrame) 28 | window.cancelAnimationFrame = function(id) { 29 | clearTimeout(id); 30 | }; 31 | }()); -------------------------------------------------------------------------------- /src/js/topheman-panorama/require.plugins/async.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | * RequireJS plugin for async dependency load like JSONP and Google Maps 3 | * Author: Miller Medeiros 4 | * Version: 0.1.1 (2011/11/17) 5 | * Released under the MIT license 6 | */ 7 | define(function(){ 8 | 9 | var DEFAULT_PARAM_NAME = 'callback', 10 | _uid = 0; 11 | 12 | function injectScript(src){ 13 | var s, t; 14 | s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = src; 15 | t = document.getElementsByTagName('script')[0]; t.parentNode.insertBefore(s,t); 16 | } 17 | 18 | function formatUrl(name, id){ 19 | var paramRegex = /!(.+)/, 20 | url = name.replace(paramRegex, ''), 21 | param = (paramRegex.test(name))? name.replace(/.+!/, '') : DEFAULT_PARAM_NAME; 22 | url += (url.indexOf('?') < 0)? '?' : '&'; 23 | return url + param +'='+ id; 24 | } 25 | 26 | function uid() { 27 | _uid += 1; 28 | return '__async_req_'+ _uid +'__'; 29 | } 30 | 31 | return{ 32 | load : function(name, req, onLoad, config){ 33 | if(config.isBuild){ 34 | onLoad(null); //avoid errors on the optimizer 35 | }else{ 36 | var id = uid(); 37 | //create a global variable that stores onLoad so callback 38 | //function can define new module after async load 39 | window[id] = onLoad; 40 | injectScript(formatUrl(name, id)); 41 | } 42 | } 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /src/css/common.css: -------------------------------------------------------------------------------- 1 | /** 2 | * PanoramaSensorsViewer - Copyright 2013-2014, Christophe Rosset (Topheman) - http://labs.topheman.com 3 | */ 4 | html{ 5 | font-size:75%; 6 | } 7 | body{ 8 | background-color: #EEE; 9 | font-family: Arial, "sans-serif"; 10 | } 11 | h1,h2,h3,h4,h5,h6{ 12 | color: #900000; 13 | } 14 | a{ 15 | color: #900000; 16 | } 17 | a:hover{ 18 | color: black; 19 | } 20 | dt{ 21 | font-weight: bold; 22 | float: left; 23 | width: 100px; 24 | clear: left; 25 | } 26 | dd{ 27 | float: left; 28 | width: 80%; 29 | margin-left:0px; 30 | } 31 | footer { 32 | font-size: 90%; 33 | clear:both; 34 | text-align: center; 35 | color: #62686A; 36 | } 37 | footer a { 38 | color: #62686A; 39 | } 40 | code{ 41 | font-size: 125%; 42 | } 43 | .summary{ 44 | float:left; 45 | margin-left: 5px; 46 | } 47 | .qr-code{ 48 | float:left; 49 | margin-bottom: 5px; 50 | } 51 | .summary ~ hr{ 52 | clear: both; 53 | } 54 | #forkongithub a{background:#000;color:#fff;text-decoration:none;font-family:arial, sans-serif;text-align:center;font-weight:bold;padding:5px 40px;font-size:1rem;line-height:2rem;position:relative;transition:0.5s;} 55 | #forkongithub a:hover{background:#900000;color:#fff;} 56 | #forkongithub a::before,#forkongithub a::after{content:"";width:100%;display:block;position:absolute;top:1px;left:0;height:1px;background:#fff;} 57 | #forkongithub a::after{bottom:1px;top:auto;} 58 | @media screen and (min-width:540px){ 59 | #forkongithub{position:absolute;display:block;top:0;right:0;width:160px;overflow:hidden;height:150px;} 60 | #forkongithub a{width:150px;position:absolute;top:35px;right:-62px;transform:rotate(45deg);-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);box-shadow:4px 4px 10px rgba(0,0,0,0.8);white-space:nowrap;} 61 | } 62 | #forkongithub a{ 63 | font-size: 1.3rem; 64 | } 65 | 66 | @media screen and (max-width:540px){ 67 | .qr-code, .non-mobile-infos{ 68 | display: none; 69 | } 70 | } -------------------------------------------------------------------------------- /src/demo.basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Basic 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test.bug.android.firefox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Bug test Android Firefox 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

PanoramaSensorsViewer

15 |

As explained here in the known bugs, I experienced on my Nexus 10 tablet, with firefox the following problem :

16 |

window.addEventLister("deviceorientation", handler, false) takes time to apply (sometimes it's needed to put the application in background, reload it, change tab, etc ... to force the connection to the gyroscope+accelerometer).

17 |

Here is a use case

18 | 23 |

24 |         
40 |     
41 | 
42 | 


--------------------------------------------------------------------------------
/src/demo.deviceorientationhelper.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |         PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / deviceorientationHelper
 5 |         
 6 |         
 7 |         
 8 |         
 9 |         
10 |         
11 |         
12 |     
13 |   
14 |       

deviceOrientationHelper

15 |

16 |         
17 |         
51 |     
52 | 


--------------------------------------------------------------------------------
/src/demo.events.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |         PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Events
 5 |         
 6 |         
 7 |         
 8 |         
 9 |         
10 |         
11 |         
12 |         
13 |     
14 |     
15 |         
16 | 17 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/demo.controls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Controls 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #PanoramaSensorsViewer 2 | 3 | Maybe you're a developer, so, no more #RTFM, you'll read this later ;-) so **go [straight to the API doc](https://github.com/topheman/PanoramaSensorsViewer/tree/master/src/#panoramasensorsviewer---api-doc)** ! (:wink) ... 4 | 5 | This library will let you make webapps that can browse Google Street View just by moving your mobile device and looking at the panorama like you were inside it. 6 | 7 | * Try [Topheman Street View](http://streetview.topheman.com/ "Topheman Street View"), a website I made based on PanoramaSensorsViewer 8 | * The examples included in this repository can also be [tested online](http://labs.topheman.com/PanoramaSensorsViewer "tested online") 9 | 10 | ##Intro 11 | 12 | PanoramaSensorsViewer takes advantage of Google Maps API v3 to display panoramas and takes care of all the tricky things such as : 13 | 14 | * Init a Google Street View panorama 15 | * At any position 16 | * Taking care of making the connection to the Google Maps API 17 | * Lets you have your custom panoramas (or anything else since you have full access to the Google Maps API) 18 | * Connecting your device's gyroscope+accelerometer to this panorama 19 | * Taking care of your device screen orientation (keeping the movements consistent whatever orientation you're on) 20 | 21 | ##Requirements 22 | 23 | For dependency management purposes (and also safe and simple script lazy load), PanoramaSensorsViewer was built using **RequireJS**, a module loader. 24 | 25 | For those of you who don't know **RequireJS**, I invite you to [check it out](http://requirejs.org/). Anyone who already use RequireJS shouldn't have any problem, however, this repo contains examples of how to use PanoramaSensorsViewer, if you want to make your own app, you can simply clone the repo ... 26 | 27 | PS : I also use RequireJS because it allows me to make builds with r.js via grunt (something you should also check out). For a next version, I'll try to ditch RequireJS via a build step (like the jQuery builder). 28 | 29 | ###RequireJS Setup 30 | 31 | You'll find the RequireJS config file here : [require.config.js](https://github.com/topheman/PanoramaSensorsViewer/blob/master/src/js/require.config.js). Nothing extraordinary here, only the declaration of the **async** plugin which is used to call google. 32 | 33 | If you moved the *topheman-panorama* folder, you can make an alias in this file so that RequireJS will find it. 34 | 35 | ###Grunt Setup (optional) 36 | 37 | Maybe like me you don't start a project without grunt anymore, so there is a Gruntfile.js and a package.json. 38 | 39 | The default task will launch you a server on localhost:9002 ... 40 | 41 | ##Known bugs 42 | 43 | * Chrome for Android : on some devices, the noise filter for the deviceorientation event can be pretty bad, so the image can be jumpy. 44 | * Firefox for Android : the noise filter is ok, however, the connection to the sensors (accelerometer+gyroscope) seems to be erratic - here is [the use case to reproduce the bug](http://labs.topheman.com/PanoramaSensorsViewer/test.bug.android.firefox.html). 45 | 46 | iOs works very well. -------------------------------------------------------------------------------- /src/js/utils/screenManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman/PanoramaSensorsViewer 3 | * 4 | * Released under MIT License : 5 | * https://github.com/topheman/PanoramaSensorsViewer/blob/master/LICENSE 6 | */ 7 | 8 | /** 9 | * Little util I made so that your panorama fits and resizes the window 10 | * You're free to use it. 11 | * Good point : when you resizes, it won't resize the panorama at each resize event. 12 | * 13 | * Just include it and launch the two methods when your panorama is initialized - see examples in the repo if necessary. 14 | */ 15 | 16 | (function (screenManager){ 17 | 18 | if (typeof define === 'function' && define.amd) { 19 | // AMD. Register as an anonymous module. 20 | define(screenManager); 21 | } else { 22 | // Browser globals 23 | window.screenManager = screenManager(); 24 | } 25 | 26 | })(function(){ 27 | 28 | var screenManager, 29 | _privateHelper, 30 | panoramaDiv, 31 | panoramaManager; 32 | 33 | _privateHelper = { 34 | panoramaFitToWindow: function(){ 35 | panoramaDiv.style.width = window.innerWidth; 36 | panoramaDiv.style.height = window.innerHeight; 37 | }, 38 | initResizeEvent: function(){ 39 | var self = this, 40 | timer = false; 41 | window.addEventListener('resize',function(e){ 42 | if(!timer){ 43 | timer = setTimeout(function(){ 44 | console.log('done resizing'); 45 | clearTimeout(timer); 46 | timer = false; 47 | if(panoramaManager.isInit()){ 48 | self.panoramaFitToWindow(); 49 | panoramaManager.resize(); 50 | } 51 | },800); 52 | } 53 | },false); 54 | } 55 | }; 56 | 57 | screenManager = { 58 | init: function(panoramaManager){ 59 | 60 | var panoramaDiv = panoramaManager.getPanoramaHtmlObject(); 61 | 62 | var panoramaFitToWindow = function(){ 63 | panoramaDiv.style.width = window.innerWidth+"px"; 64 | panoramaDiv.style.height = window.innerHeight+"px"; 65 | }; 66 | var initResizeEvent = function(){ 67 | var timer = false; 68 | window.addEventListener('resize',function(e){ 69 | if(!timer){ 70 | timer = setTimeout(function(){ 71 | console.log('done resizing'); 72 | clearTimeout(timer); 73 | timer = false; 74 | if(panoramaManager.isInit()){ 75 | panoramaFitToWindow(); 76 | panoramaManager.resize(); 77 | } 78 | },800); 79 | } 80 | },false); 81 | }; 82 | 83 | panoramaFitToWindow(); 84 | initResizeEvent(); 85 | } 86 | }; 87 | 88 | return screenManager; 89 | 90 | }); -------------------------------------------------------------------------------- /src/demo.custompanorama.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Custom panorama 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/js/topheman-panorama/utils/blockedPopup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all 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, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | * 23 | */ 24 | 25 | /** 26 | * We all have popup blockers like AdBlock, so in your application, when you need to provide a popup, 27 | * you can't be sure wether it has opened or been blocked. 28 | * This module provides you two events : 29 | * - isBlocked : so that you could notify your user to allow the popups for your site (for example) 30 | * - onUnblock : when your popup has opened correctly (maybe so that you remove a previous notification - or do nothing ...) 31 | * 32 | */ 33 | 34 | (function (blockedPopup){ 35 | 36 | if (typeof define === 'function' && define.amd) { 37 | // AMD. Register as an anonymous module. 38 | define(blockedPopup); 39 | } else { 40 | // Browser globals 41 | window.blockedPopup = blockedPopup(); 42 | } 43 | 44 | })(function(){ 45 | 46 | var blockedPopup; 47 | 48 | blockedPopup = { 49 | 50 | /** 51 | * 52 | * @param {window} popup 53 | * @param {Function} isBlockedCallback 54 | * @param {Function} isUnBlockedCallback 55 | */ 56 | isBlocked : function(popup, isBlockedCallback, isUnBlockedCallback){ 57 | console.log('blockedPopup.isBlocked()'); 58 | if (!popup){ 59 | console.log('no popup'); 60 | isBlockedCallback(); 61 | } 62 | else { 63 | popup.onload = function() { 64 | setTimeout(function() { 65 | if (popup.screenX === 0){ 66 | if(typeof isBlockedCallback === "function"){ 67 | isBlockedCallback(); 68 | } 69 | } 70 | else{ 71 | if(typeof isUnBlockedCallback === "function"){ 72 | isUnBlockedCallback(); 73 | } 74 | } 75 | }, 0); 76 | }; 77 | } 78 | }, 79 | 80 | /** 81 | * 82 | * @param {window} popup 83 | * @param {Function} callback 84 | */ 85 | onUnblock: function(popup, callback){ 86 | var check = (function(popup,callback){ 87 | return function(){ 88 | // console.log('check',popup); 89 | if(popup){ 90 | if(popup.screenX === 0){ 91 | setTimeout(check,10); 92 | } 93 | else{ 94 | if(typeof callback === "function"){ 95 | callback(); 96 | } 97 | } 98 | } 99 | else{ 100 | setTimeout(check,10); 101 | } 102 | }; 103 | })(popup,callback); 104 | check(); 105 | } 106 | 107 | }; 108 | 109 | return blockedPopup; 110 | 111 | }); -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | #PanoramaSensorsViewer - API doc 2 | 3 | I made a bunch of examples, this may be the best way to understand how PanoramaSensorsViewer works. [You can test them online here](http://labs.topheman.com/PanoramaSensorsViewer/). I also invite you to checkout the Google Maps API doc. 4 | 5 | Still, here is the API doc. 6 | 7 | ##Require PanoramaManager 8 | 9 | The very first thing you need to do is require the module `topheman-panorama/PanoramaManager`. Here is an example of how to do that (you may have a different way since RequireJS has different ways to load). 10 | 11 | ```html 12 | 13 | 14 | 15 | 22 | 23 | ``` 24 | 25 | ##PanoramaManager 26 | 27 | ###Summary 28 | 29 | Creates a JavaScript `PanoramaManager` instance linked to the `DOMObject` **passed in parameter**. At this moment, the instance does pretty much nothing until it is initialized by calling the method `init`. 30 | 31 | ###Constructor 32 | 33 | ```js 34 | 35 | var panoramaManager = new PanoramaManager(document.getElementById('pano')); 36 | 37 | ``` 38 | 39 | ###Methods 40 | 41 | ####.init(position,options) 42 | 43 | You can call the `.init` method as many times as you want, however, you need to keep in mind some events and callback options will only happen at the first time you call it, such as : 44 | 45 | * retrieving the Google Maps API 46 | * connecting to the gyroscope+accelerometer (ie `addEventListener("deviceorientation", ...)` ) 47 | 48 | This will init your panorama, filling the DOMObject you passed previously. 49 | 50 | #####parameters 51 | 52 | * `position` : 2 possibilities (see bellow when to use either one of them) 53 | * `Object` with latitude and longitude. Example : `{lat:37.869260, lon:-122.254811}` 54 | * `null` 55 | * `options` : this parameter is optional 56 | * `success` : `[Function]` `function(data){ /* scope is the panorama object on which you can call any Google Maps API */ }` 57 | * `error` : `[Function]` `function(error){ /* … */ }` 58 | * `pano` : `[String]` custom panorama id on which you wish to start your panorama (you'll need to provide a panoProvider callback) 59 | * `panoProvider` : `[Function]` custom panorama provider method for full custom panoramas (not linked with any Google Street View panoramas). You'll need to provide a panorama id as options.pano to specify which panorama you want to start with. [See example](https://github.com/topheman/PanoramaSensorsViewer/blob/master/src/demo.custompanorama.html). 60 | * `enableRemotetilt` : `[Boolean]` Will load the deviceorientation emulator if set at true (and if no sensors on your device) 61 | * `remotetiltIsBlocked` : `[Function]` Callback called if the emulator popup has been blocked by a popup blocker such as adBlock 62 | * `remotetiltIsUnblocked` : `[Function]` Callback called if the emulator popup has not been blocked by a popup blocker 63 | * `disableTouchmove` : `[Boolean|Function]` Disables the touchmove. If a function is given, this will be executed on touchmove. 64 | * `firefoxSensorsNotReady` : `[Function]` On some android devices, firefox mobile takes some time to add the event listener on the deviceorientation, this callback lets you spot when it happens to tell your user to reload for example 65 | * `googleApiKey` : `[String]` if you have a google API key and you let PanoramaSensorsViewer load the Google Maps API, you can specify your key here. (note that the API won't be loaded again if you already loaded it yourself). 66 | 67 | #####parameters advanced infos 68 | 69 | * in the `success` callback, you have access to all the Google Maps API that you can apply to your Google panorama which is the current scope `this`. 70 | * You can't use `pano` without `panoProvider` and only for panoramas which aren't connected to Google Street View (you'll be giving a `null` position in this case) ([see example](https://github.com/topheman/PanoramaSensorsViewer/blob/master/src/demo.custompanorama.html)). 71 | * If you want to connect your custom panorama, use the Google Maps API inside the `success` callback : `.registerPanoProvider()` like in [this example](https://github.com/topheman/PanoramaSensorsViewer/blob/master/src/demo.custompanorama.tiles.html). 72 | 73 | ####.isInit() 74 | 75 | returns `true` if the panorama is initialized 76 | 77 | ####.resize() 78 | 79 | triggers the resize event on the panorama (you would like to do that if you resize the DOMObject containing the panorama) -------------------------------------------------------------------------------- /src/js/topheman-panorama/utils/sensorsChecker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all 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, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | * 23 | */ 24 | 25 | /** 26 | * Mostly all recent browsers expose an api for deviceorientation and devicemotion events. 27 | * That doesn't mean the device you're on has sensors (accelerometer+gyroscope) to feed them. 28 | * So to check if the device has sensors, you can't rely on simple feature detection like 29 | * "ondeviceorientation" in window or "ondevicemotion" in window 30 | * 31 | * This module will let you check if there is really an accelerometer+gyroscope to rely on. 32 | */ 33 | 34 | (function (sensorsChecker){ 35 | 36 | if (typeof define === 'function' && define.amd) { 37 | // AMD. Register as an anonymous module. 38 | define(sensorsChecker); 39 | } else { 40 | // Browser globals 41 | window.sensorsChecker = sensorsChecker(); 42 | } 43 | 44 | })(function(){ 45 | 46 | var sensorsChecker, 47 | eventsMap = { 48 | "devicemotion" : { 49 | "event" : "DeviceMotionEvent", 50 | "handler" : function(e){ 51 | if(e.acceleration && e.acceleration.x !== null && e.acceleration.y !== null && eventsMap.devicemotion.support === false){ 52 | eventsMap.devicemotion.support = true; 53 | } 54 | }, 55 | support : false 56 | }, 57 | "deviceorientation" : { 58 | "event" : "DeviceOrientationEvent", 59 | "handler" : function(e){ 60 | if(e.beta !== null && e.gamma !== null && eventsMap.deviceorientation.support === false){ 61 | eventsMap.deviceorientation.support = true; 62 | } 63 | }, 64 | support : false 65 | } 66 | }, 67 | DEFAULT_DELAY = 500; 68 | 69 | sensorsChecker = { 70 | 71 | /** 72 | * 73 | * @param {String} event "devicemotion"|"deviceorientation" 74 | * @param {Function} success 75 | * @param {Function} failure 76 | * @params {options} options @optional 77 | * @config {Number} delay 78 | * @config {RegExp} userAgentCheck 79 | */ 80 | check: function(event, success, failure, options){ 81 | 82 | options = options ? options : {}; 83 | options.delay = options.delay ? options.delay : DEFAULT_DELAY; 84 | 85 | if(!eventsMap[event]){ 86 | throw new Error("Only devicemotion or deviceorientation events supported"); 87 | } 88 | if(typeof success !== "function"){ 89 | throw new Error("success callback missing"); 90 | } 91 | if(typeof failure !== "function"){ 92 | throw new Error("success callback missing"); 93 | } 94 | 95 | if(window[eventsMap[event].event]){ 96 | if(options && options.userAgentCheck && options.userAgentCheck instanceof RegExp && options.userAgentCheck.test(window.navigator.userAgent)){ 97 | success(); 98 | } 99 | else{ 100 | window.addEventListener(event, eventsMap[event].handler, false); 101 | setTimeout(function(){ 102 | window.removeEventListener(event, eventsMap[event].handler); 103 | if(eventsMap[event].support === true){ 104 | success(); 105 | } 106 | else{ 107 | failure(); 108 | } 109 | },options.delay); 110 | } 111 | } 112 | else{ 113 | failure(); 114 | } 115 | 116 | }, 117 | 118 | checkDevicemotion: function(success, failure, options){ 119 | this.check('devicemotion',success, failure, options); 120 | }, 121 | 122 | checkDeviceorientation: function(success, failure, options){ 123 | this.check('deviceorientation',success, failure, options); 124 | } 125 | 126 | }; 127 | 128 | return sensorsChecker; 129 | 130 | }); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman/PanoramaSensorsViewer 3 | * 4 | * Released under MIT License : 5 | * https://github.com/topheman/PanoramaSensorsViewer/blob/master/LICENSE 6 | */ 7 | 8 | /** 9 | * Tou know grunt ? It's great ! Two things : 10 | * - npm install (will install the node packages needed) 11 | * - npm install grunt-cli -g (if you don't have it already) 12 | * - grunt server (will start you a local server so that you can test) 13 | */ 14 | module.exports = function(grunt) { 15 | 16 | // Load Grunt tasks declared in the package.json file 17 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 18 | 19 | var PanoramaSensorsViewerVersion = require('./package.json').version, 20 | gruntSpecificOptions; 21 | 22 | try{ 23 | gruntSpecificOptions = grunt.file.readJSON('./grunt.options.json');//specific options meant to be split from the hard code of the GruntFile.js 24 | } 25 | catch(e){ 26 | grunt.log.error("No grunt.options.json file find (you will need it for ftp-deploy)"); 27 | } 28 | 29 | // Configure Grunt 30 | grunt.initConfig({ 31 | 32 | // grunt-contrib-connect will serve the files of the project 33 | // on specified port and hostname 34 | connect: { 35 | debug: { 36 | options: { 37 | port: 9002, 38 | hostname: "0.0.0.0", 39 | keepalive: true, 40 | base: "src" 41 | } 42 | }, 43 | release: { 44 | options: { 45 | port: 9002, 46 | hostname: "0.0.0.0", 47 | keepalive: true, 48 | base: "release" 49 | } 50 | } 51 | }, 52 | 53 | // grunt-open will open your browser at the project's URL 54 | open: { 55 | debug: { 56 | path: 'http://localhost:<%= connect.debug.options.port%>/' 57 | }, 58 | release: { 59 | path: 'http://localhost:<%= connect.release.options.port%>/' 60 | } 61 | }, 62 | 63 | //this part is for building 64 | clean: { 65 | //clean all before any build 66 | all: { 67 | src : ['release'] 68 | }, 69 | "after-build" : { 70 | src : [ 71 | 'release/googleAnalyticsScriptTag.html' 72 | ] 73 | } 74 | }, 75 | 76 | copy:{ 77 | release:{ 78 | files:[ 79 | {expand: true, cwd: 'src/', src: ['**'], dest: 'release/'} 80 | ] 81 | } 82 | }, 83 | 84 | "git-rev-parse": { 85 | panoramaSensorsViewerRevision: { 86 | options: { 87 | prop: 'PanoramaSensorsViewerRevision', 88 | number: 8 89 | } 90 | } 91 | }, 92 | 93 | //https://github.com/jsoverson/preprocess + https://npmjs.org/package/grunt-preprocess 94 | preprocess: { 95 | options:{ 96 | context:{ 97 | "PanoramaSensorsViewerVersion" : PanoramaSensorsViewerVersion, 98 | "PanoramaSensorsViewerRevision" : '<%= grunt.config.get("PanoramaSensorsViewerRevision") %>', 99 | "PanoramaSensorsViewerVersionAndRevision" : 'v'+PanoramaSensorsViewerVersion+' - r.<%= grunt.config.get("PanoramaSensorsViewerRevision") %>' 100 | } 101 | }, 102 | "process-html" : { 103 | files : { 104 | 'release/index.html' : 'src/index.html', 105 | 'release/demo.basic.html' : 'src/demo.basic.html', 106 | 'release/demo.controls.html' : 'src/demo.controls.html', 107 | 'release/demo.custompanorama.html' : 'src/demo.custompanorama.html', 108 | 'release/demo.custompanorama.tiles.html' : 'src/demo.custompanorama.tiles.html', 109 | 'release/demo.events.html' : 'src/demo.events.html', 110 | 'release/demo.deviceorientationhelper.html' : 'src/demo.deviceorientationhelper.html', 111 | 'release/test.bug.android.firefox.html' : 'src/test.bug.android.firefox.html', 112 | } 113 | } 114 | } 115 | 116 | }); 117 | 118 | if(gruntSpecificOptions){ 119 | grunt.config("ftp-deploy",{ 120 | release: { 121 | auth: { 122 | host: gruntSpecificOptions["ftp-deploy"].release.host, 123 | port: gruntSpecificOptions["ftp-deploy"].release.port, 124 | authKey: 'key1' 125 | }, 126 | src: 'release', 127 | dest: gruntSpecificOptions["ftp-deploy"].release.dest, 128 | exclusions: ['release/build.txt'] 129 | } 130 | }); 131 | grunt.registerTask('deploy', ['ftp-deploy:release']); 132 | } 133 | 134 | grunt.registerTask('server', ['open:debug', 'connect:debug']); 135 | grunt.registerTask('default', ['open:debug', 'connect:debug']); 136 | 137 | grunt.registerTask('getGitRevisionNumbers',['git-rev-parse']); 138 | 139 | grunt.registerTask('build', ['clean:all','copy:release','getGitRevisionNumbers','preprocess:process-html','clean:after-build']); 140 | grunt.registerTask('server-release', ['open:release', 'connect:release']); 141 | 142 | }; -------------------------------------------------------------------------------- /src/js/topheman-panorama/utils/deviceorientationHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all 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, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | * 23 | */ 24 | 25 | (function (deviceorientationHelper){ 26 | 27 | if (typeof define === 'function' && define.amd) { 28 | // AMD. Register as an anonymous module. 29 | define(deviceorientationHelper); 30 | } else { 31 | // Browser globals 32 | window.deviceorientationHelper = deviceorientationHelper(); 33 | } 34 | 35 | })(function(){ 36 | 37 | var deviceorientationHelper, 38 | _getDeviceorientationHandler, 39 | _eventToInfos, 40 | _mozScreenOrientationToWindowOrientation, 41 | deviceorientationHandler; 42 | 43 | _mozScreenOrientationToWindowOrientation = function(orientation){ 44 | var result;//@todo cache orientation 45 | switch(orientation){ 46 | case "portrait-primary" : 47 | result = 0; 48 | break; 49 | case "portrait-secondary" : 50 | result = 180; 51 | break; 52 | case "landscape-primary" : 53 | result = 90; 54 | break; 55 | case "landscape-secondary" : 56 | result = -90; 57 | break; 58 | default : 59 | result = 0; 60 | break; 61 | }; 62 | return result; 63 | }; 64 | 65 | _eventToInfos = function(e,infos){ 66 | switch(infos.orientation){ 67 | case 0 : 68 | infos.tiltLR = e.gamma; 69 | infos.tiltFB = e.beta; 70 | infos.direction = e.alpha; 71 | break; 72 | case 90 : 73 | infos.tiltLR = e.beta; 74 | infos.tiltFB = (-e.gamma >= 0 ? -e.gamma : (360 - e.gamma)); 75 | infos.direction = (e.alpha-90)%360; 76 | break; 77 | case -90 : 78 | infos.tiltLR = -e.beta; 79 | infos.tiltFB = e.gamma; 80 | infos.direction = (e.alpha+90)%360; 81 | break; 82 | case 180 : 83 | infos.tiltLR = -e.gamma; 84 | infos.tiltFB = -e.beta; 85 | infos.direction = e.alpha; 86 | break; 87 | }; 88 | }; 89 | 90 | _eventToInfosFirefox = function(e,infos){ 91 | switch(infos.orientation){ 92 | case 0 : 93 | infos.tiltLR = -e.gamma; 94 | infos.tiltFB = -e.beta; 95 | infos.direction = -e.alpha; 96 | break; 97 | case 90 : 98 | infos.tiltLR = -e.beta; 99 | infos.tiltFB = e.gamma; 100 | infos.direction = -(e.alpha+90)%360; 101 | break; 102 | case -90 : 103 | infos.tiltLR = e.beta; 104 | infos.tiltFB = -e.gamma; 105 | infos.direction = -(e.alpha+90)%360; 106 | break; 107 | case 180 : 108 | infos.tiltLR = e.gamma; 109 | infos.tiltFB = e.beta; 110 | infos.direction = -e.alpha; 111 | break; 112 | }; 113 | }; 114 | 115 | _getDeviceorientationHandler = function(callback){ 116 | //for android firefox mobile 117 | if(/Android.*(Mobile|Tablet).*Firefox/i.test(window.navigator.userAgent)){ 118 | return function(e,infos){ 119 | infos = { 120 | "orientation" : _mozScreenOrientationToWindowOrientation(window.screen.mozOrientation), 121 | "heading" : null, 122 | "absolute" : e.absolute 123 | }; 124 | _eventToInfosFirefox(e,infos); 125 | callback.call({},e,infos); 126 | }; 127 | } 128 | //for others 129 | else{ 130 | return function(e,infos){ 131 | infos = { 132 | "orientation" : window.orientation || 0, 133 | "heading" : e.compassHeading || e.webkitCompassHeading, 134 | "absolute" : e.absolute 135 | }; 136 | _eventToInfos(e,infos); 137 | callback.call({},e,infos); 138 | }; 139 | } 140 | }; 141 | 142 | deviceorientationHelper = { 143 | 144 | /** 145 | * @description adds a callback like you would do on deviceorientation event 146 | * but gives you a second argument so that you won't have to bother with the orientation of the device 147 | * @param {Function} callback function(e,infos){ ... } 148 | * @param {DeviceOrientationEvent} e 149 | * @param {Object} infos processed deviceorientation infos (according to orientation of the device) 150 | */ 151 | init: function(callback){ 152 | 153 | if(typeof deviceorientationHandler !== "undefined"){ 154 | throw new Error("deviceorientationHelper already initiated, please .destroy() it before reinitiate it"); 155 | } 156 | 157 | //init deviceorientationHandler 158 | deviceorientationHandler = _getDeviceorientationHandler(callback); 159 | window.addEventListener('deviceorientation',deviceorientationHandler,false); 160 | 161 | }, 162 | 163 | /** 164 | * @description destroys the handlers added at init 165 | */ 166 | destroy: function(){ 167 | 168 | if(typeof deviceorientationHandler === "undefined"){ 169 | throw new Error("deviceorientationHelper already destroy (or nether initiated), please .init() it before destroying it"); 170 | } 171 | 172 | window.removeEventListener('deviceorientation',deviceorientationHandler,false); 173 | 174 | } 175 | 176 | }; 177 | 178 | return deviceorientationHelper; 179 | 180 | }); -------------------------------------------------------------------------------- /src/demo.custompanorama.tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> / Custom panorama tiles 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PanoramaSensorsViewer <!-- @echo PanoramaSensorsViewerVersionAndRevision --> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Fork me on GitHub 15 |

PanoramaSensorsViewer

16 | QR Code to this page 17 |
18 | 19 |

To test the examples of this page on your mobile device, just snap the QR-Code on the left.

20 |

You should also check out Topheman Street View, an application based on PanoramaSensorsViewer.

21 |

Summary :

22 | 26 |
27 |
28 |

Getting started with PanoramaSensorsViewer

29 |

30 | Here is a list of demos using PanoramaSensorsViewer, so you could understand how to use the Google Maps API inside it.
31 | This will work as well on a desktop as on a mobile device (what you should do is test it on your mobile device and watch the code on your desktop).
32 | For each example, you will find : 33 |

34 |
    35 |
  • A link to the demo
  • 36 |
  • A link to the code on github
  • 37 |
  • A link to the Google Maps API doc
  • 38 |
39 |

Examples

40 |
41 |
Basic
42 |
43 | This is a basic demo like this one on the Google API doc, but with sensors. 44 | 48 |
49 |
Events
50 |
51 | Add Street View events to your panorama, inside the success callback of PanoramaManager.init. 52 | 56 |
57 |
Alter controls
58 |
59 | 60 | Alter the controls of your panorama via the setOptions method of Google Maps API for panorama, inside the success callback of PanoramaManager.init (zoom control and address will be hidden). 61 | 62 | 69 |
70 |
Custom panorama
71 |
72 | 73 | In the options, setup your own panoProvider and pano (like you would with Google Maps API) and get your own custom panorama with PanoramaSensorsViewer ! 74 | 75 | 81 |
82 |
Custom panorama tiles
83 |
84 | 85 | Connect your own custom panorama provider (with a tiled panorama) to the Google panoramas ! 86 | 87 | 96 |
97 |
98 |
99 |

API Doc

100 |

Check the API Doc on github.

101 |
102 |

Dependency utils that you can use standalone

103 |

PanoramaSensorsViewer ships some utils I made that can run in standalone (both non-AMD and AMD compatible). While you use PanoramaSensorsViewer, you don't have to worry about them but, it could come handy in some of your project.

104 |

blockedPopup.js

105 |

We all have popup blockers like AdBlock on our browser, so if your application ever needs popups (I know it's old) and make sure it's been authorized by your user, the little class will let you know.

106 |

See the code on the PanoramaSensorsViewer github repository

107 |

sensorsChecker.js

108 |

Mostly all recent browsers expose an api for deviceorientation and devicemotion events. That doesn't mean the device you're on has sensors (accelerometer+gyroscope) to feed them.

109 |

So to check if the device has sensors, you can't rely on simple feature detection like "ondeviceorientation" in window or "ondevicemotion" in window

110 |

This module will let you check if there is really an accelerometer+gyroscope to rely on.

111 |

See the code on the PanoramaSensorsViewer github repository

112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/js/vendor/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 2.1.6 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(ba){function J(b){return"[object Function]"===N.call(b)}function K(b){return"[object Array]"===N.call(b)}function z(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(J(n)){if(this.events.error&&this.map.isDefine||h.onError!==ca)try{e=k.execCb(c,n,b,e)}catch(d){a=d}else e=k.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!== 19 | this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&!this.ignore&&(r[c]=e,h.onResourceLoad))h.onResourceLoad(k,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete= 20 | !0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=l(a.prefix);this.depMaps.push(d);u(d,"defined",v(this,function(e){var n,d;d=this.map.name;var g=this.map.parentMap?this.map.parentMap.name:null,C=k.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,g,!0)})||""),e=l(a.prefix+"!"+d,this.map.parentMap),u(e,"defined",v(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})), 21 | d=m(q,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",v(this,function(a){this.emit("error",a)}));d.enable()}}else n=v(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=v(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];H(q,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),n.fromText=v(this,function(e,c){var d=a.name,g=l(d),i=Q;c&&(e=c);i&&(Q=!1);s(g);t(j.config,b)&&(j.config[d]=j.config[b]);try{h.exec(e)}catch(D){return w(B("fromtexteval", 22 | "fromText eval for "+b+" failed: "+D,D,[b]))}i&&(Q=!0);this.depMaps.push(g);k.completeLoad(d);C([d],n)}),e.load(a.name,C,n,j)}));k.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;z(this.depMaps,v(this,function(a,b){var c,e;if("string"===typeof a){a=l(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(P,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;u(a,"defined",v(this,function(a){this.defineDep(b, 23 | a);this.check()}));this.errback&&u(a,"error",v(this,this.errback))}c=a.id;e=q[c];!t(P,c)&&(e&&!e.enabled)&&k.enable(a,this)}));H(this.pluginMaps,v(this,function(a){var b=m(q,a.id);b&&!b.enabled&&k.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){z(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};k={config:j,contextName:b,registry:q,defined:r,urlFetched:V,defQueue:I,Module:$,makeModuleMap:l, 24 | nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.pkgs,c=j.shim,e={paths:!0,config:!0,map:!0};H(a,function(a,b){e[b]?"map"===b?(j.map||(j.map={}),S(j[b],a,!0,!0)):S(j[b],a,!0):j[b]=a});a.shim&&(H(a.shim,function(a,b){K(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=k.makeShimExports(a);c[b]=a}),j.shim=c);a.packages&&(z(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name, 25 | location:a.location||a.name,main:(a.main||"main").replace(ka,"").replace(fa,"")}}),j.pkgs=b);H(q,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=l(b))});if(a.deps||a.callback)k.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,f){function d(e,c,g){var i,j;f.enableBuildCallback&&(c&&J(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(J(c))return w(B("requireargs", 26 | "Invalid require call"),g);if(a&&t(P,e))return P[e](q[a.id]);if(h.get)return h.get(k,e,a,d);i=l(e,a,!1,!0);i=i.id;return!t(r,i)?w(B("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[i]}M();k.nextTick(function(){M();j=s(l(null,a));j.skipMap=f.skipMap;j.init(e,c,g,{enabled:!0});E()});return d}f=f||{};S(d,{isBrowser:A,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];if(-1!==f&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(Q=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,E?y.insertBefore(g,E):y.appendChild(g), 34 | M=null,g;if(ea)try{importScripts(d),b.completeLoad(c)}catch(l){b.onError(B("importscripts","importScripts failed for "+c+" at "+d,l,[c]))}};A&&O(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return s=L,u.baseUrl||(F=s.split("/"),s=F.pop(),ga=F.length?F.join("/")+"/":"./",u.baseUrl=ga),s=s.replace(fa,""),h.jsExtRegExp.test(s)&&(s=L),u.deps=u.deps?u.deps.concat(s):[s],!0});define=function(b,c,d){var h,g;"string"!==typeof b&&(d=c,c=b,b=null); 35 | K(c)||(d=c,c=null);!c&&J(d)&&(c=[],d.length&&(d.toString().replace(ma,"").replace(na,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(Q){if(!(h=M))R&&"interactive"===R.readyState||O(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return R=b}),h=R;h&&(b||(b=h.getAttribute("data-requiremodule")),g=G[h.getAttribute("data-requirecontext")])}(g?g.defQueue:U).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)}; 36 | h(u)}})(this); 37 | -------------------------------------------------------------------------------- /src/js/topheman-panorama/PanoramaManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2013-2014 Christophe Rosset - https://github.com/topheman/PanoramaSensorsViewer 3 | * 4 | * Released under MIT License : 5 | * https://github.com/topheman/PanoramaSensorsViewer/blob/master/LICENSE 6 | */ 7 | 8 | define(['topheman-panorama/utils/sensorsChecker','topheman-panorama/utils/deviceorientationHelper','topheman-panorama/vendor/rAF'],function(sensorsChecker,deviceorientationHelper,undefined){ 9 | 10 | var PanoramaManager, 11 | helper, 12 | panorama, //panorama google object 13 | sv, //streetView google object 14 | panoramaInitiated = false, 15 | currentPov = {heading:0,pitch:0}, 16 | currentPovChanged = false, 17 | headingPrecision = 0.1, 18 | pitchPrecision = 0.1, 19 | animate, 20 | helper; 21 | 22 | //animation loop - runs 60fps, update the panorama point of view only if it has changed 23 | animate = function(){ 24 | window.requestAnimationFrame(animate); 25 | //only render if the sensors have sense any change 26 | if(currentPovChanged === true){ 27 | panorama.setPov({heading:currentPov.heading*headingPrecision,pitch:currentPov.pitch*pitchPrecision}); 28 | currentPovChanged = false; 29 | } 30 | }, 31 | 32 | helper = { 33 | addSensorListeners : function(){ 34 | var self = this; 35 | deviceorientationHelper.init(function(e,infos){ 36 | //in portrait we have only 90deg 37 | var pitch = (infos.orientation === 0 || infos.orientation === 180) ? (2*infos.tiltFB-90) : (infos.tiltFB-90); 38 | self.updatePov({heading:-infos.direction,pitch:pitch}); 39 | }); 40 | }, 41 | updatePov: function(newPov){ 42 | newPov.heading = Math.floor(newPov.heading/headingPrecision); 43 | newPov.pitch = Math.floor(newPov.pitch/pitchPrecision); 44 | if(panoramaInitiated === true && (newPov.heading !== currentPov.heading || newPov.pitch !== currentPov.pitch)){ 45 | currentPov.heading = newPov.heading; 46 | currentPov.pitch = newPov.pitch; 47 | currentPovChanged = true; 48 | } 49 | } 50 | }; 51 | 52 | PanoramaManager = function(panoramaHtmlObject){ 53 | 54 | var panoramaDiv = panoramaHtmlObject, 55 | lastTouchstartTimeStamp = 0, 56 | touchmoveCurrently = false; 57 | 58 | panoramaInitiated = false; 59 | 60 | var prepareDisableTouchmove = function(options){ 61 | var touchmoveCallback, 62 | touchendCallback, 63 | disableTouchmoveCallback, 64 | isIOs = /(iPhone|iPad|iPod)/i.test(navigator.userAgent); 65 | if(("ontouchmove" in window) && options && options.disableTouchmove){ 66 | if(typeof options.disableTouchmove === "function"){ 67 | disableTouchmoveCallback = options.disableTouchmove; 68 | touchstartCallback = function(e){ 69 | lastTouchstartTimeStamp = e.timeStamp; 70 | }; 71 | touchmoveCallback = function(e){ 72 | if(isIOs || (e.timeStamp - lastTouchstartTimeStamp) > 450){ 73 | touchmoveCurrently = true; 74 | e.stopPropagation(); 75 | e.preventDefault(); 76 | disableTouchmoveCallback.call({},e); 77 | } 78 | }; 79 | touchendCallback = function(e){ 80 | if(touchmoveCurrently === true){ 81 | e.stopPropagation(); 82 | e.preventDefault(); 83 | disableTouchmoveCallback.call({},e); 84 | } 85 | touchmoveCurrently = false; 86 | }; 87 | } 88 | else { 89 | touchstartCallback = function(e){ 90 | lastTouchstartTimeStamp = e.timeStamp; 91 | }; 92 | touchmoveCallback = function(e){ 93 | if(isIOs || (e.timeStamp - lastTouchstartTimeStamp) > 450){ 94 | touchmoveCurrently = true; 95 | e.preventDefault(); 96 | } 97 | }; 98 | touchendCallback = function(e){ 99 | if(touchmoveCurrently === true){ 100 | e.preventDefault(); 101 | } 102 | touchmoveCurrently = false; 103 | }; 104 | } 105 | if(!isIOs){ 106 | panoramaDiv.addEventListener('touchstart',touchstartCallback,false); 107 | } 108 | panoramaDiv.addEventListener('touchmove',touchmoveCallback,true); 109 | panoramaDiv.addEventListener('touchend',touchendCallback,true); 110 | } 111 | }; 112 | 113 | var prepare = function(latLon,options){ 114 | 115 | var request, loadAllFunction; 116 | 117 | loadAllFunction = function() { 118 | sv = new google.maps.StreetViewService(); 119 | panorama = new google.maps.StreetViewPanorama(panoramaDiv); 120 | prepareDisableTouchmove(options); 121 | panorama.setVisible(false); 122 | init(latLon,options,true); 123 | //add the sensors listeners once and for all 124 | sensorsChecker.checkDeviceorientation(function(){ 125 | //hack for android firefox mobile that hasn't always deviceorientation events ready 126 | if(/Android.*(Mobile|Tablet).*Firefox/i.test(navigator.userAgent)){ 127 | sensorsChecker.checkDeviceorientation(function(){ 128 | helper.addSensorListeners(); 129 | animate(); 130 | panoramaInitiated = true; 131 | },function(){ 132 | if(typeof options !== "undefined" && typeof options.firefoxSensorsNotReady === "function"){ 133 | options.firefoxSensorsNotReady(); 134 | helper.addSensorListeners(); 135 | animate(); 136 | panoramaInitiated = true; 137 | } 138 | }); 139 | } 140 | else{ 141 | helper.addSensorListeners(); 142 | animate(); 143 | panoramaInitiated = true; 144 | } 145 | },function(){ 146 | if(options && options.enableRemotetilt === true){ 147 | require(['topheman-panorama/vendor/device-motion-polyfill','topheman-panorama/utils/blockedPopup'],function(undefined,blockedPopup){ 148 | blockedPopup.isBlocked(remoteTiltWindow,function(){ 149 | if(options && options.remotetiltIsBlocked && typeof options.remotetiltIsBlocked === "function"){ 150 | options.remotetiltIsBlocked(); 151 | } 152 | }); 153 | blockedPopup.onUnblock(remoteTiltWindow,function(){ 154 | if(options && options.remotetiltIsUnblocked && typeof options.remotetiltIsUnblocked === "function"){ 155 | options.remotetiltIsUnblocked(); 156 | } 157 | }); 158 | helper.addSensorListeners(); 159 | animate(); 160 | panoramaInitiated = true; 161 | }); 162 | } 163 | else{ 164 | //if no remotetilt -> no events added, though, we have to init the panorama (to be able to request info such as current position) 165 | panoramaInitiated = true; 166 | } 167 | },{ 168 | userAgentCheck: /(iPad|iPhone|Nexus|Mobile|Tablet)/i 169 | }); 170 | }; 171 | 172 | //if the api is present, dont reload it, only launch 173 | if(typeof google !== "undefined" && google.maps && google.maps.version && google.maps.version[0] == "3"){ 174 | console.log('goole maps API already loaded'); 175 | loadAllFunction(); 176 | } 177 | else{ 178 | request = "async!http://maps.google.com/maps/api/js?sensor=false"; 179 | if(options && options.googleApiKey){ 180 | request += "&key="+options.googleApiKey; 181 | } 182 | require([request],loadAllFunction); 183 | } 184 | }; 185 | 186 | var init = function(latLon,options,firstInit){ 187 | var position, processSVData, customPanoramaMode = false; 188 | if(typeof latLon === "undefined" || latLon === null){ 189 | if(!(typeof options !== "undefined" && options.panoProvider && typeof options.panoProvider === "function") && options.pano){ 190 | throw new Error("No latLon specified. If you want to create a custom panorama, you must provide in the init options the attributes : 'pano' and 'panoProvider'."); 191 | } 192 | customPanoramaMode = true; 193 | } 194 | else { 195 | if(!(latLon.lat && latLon.lon)){ 196 | throw new Error("Missing 'lat' or 'lon' attributes in first param"); 197 | } 198 | if(typeof options !== "undefined" && (options.panoProvider || options.pano) ){ 199 | throw new Error("Can't use either pano or panoProvider with a latLon specified, please use registerPanoProvider inside success callback (maybe for a next version)") 200 | } 201 | customPanoramaMode = false; 202 | } 203 | if(customPanoramaMode === false){ 204 | position = new google.maps.LatLng(latLon.lat, latLon.lon); 205 | processSVData = function(data, status){ 206 | if(status === google.maps.StreetViewStatus.OK){ 207 | //1rst set panorama location 208 | panorama.setPano(data.location.pano); 209 | //can be overloaded by options.success 210 | if(options && options.success && typeof options.success === "function"){ 211 | options.success.call(panorama,data); 212 | } 213 | if(panorama.getVisible() === false){ 214 | //sorry for the setTimeout but without it at first load, some of your options.success may still be their ... 215 | setTimeout(function(){ 216 | panorama.setVisible(true); 217 | },0); 218 | } 219 | } 220 | //if no panorama found, call the error callback if sepcified 221 | else{ 222 | if(options && options.error && typeof options.error === "function"){ 223 | options.error.call({},"No panorama found"); 224 | } 225 | else{ 226 | console.log("No panorama found"); 227 | } 228 | } 229 | }; 230 | 231 | sv.getPanoramaByLocation(position, 50, processSVData); 232 | } 233 | else{ 234 | //this is a temporary panoProvider where callbacks are added to map the api 235 | //this panoprovider is only called once and then replaced with the original 236 | var tmpPanoProvider = function(pano){ 237 | var data = options.panoProvider(pano); 238 | //can be overloaded by options.success 239 | if(options && options.success && typeof options.success === "function"){ 240 | options.success.call(panorama,data); 241 | } 242 | if(panorama.getVisible() === false){ 243 | //sorry for the setTimeout but without it at first load, some of your options.success may still be their ... 244 | setTimeout(function(){ 245 | panorama.setVisible(true); 246 | },0); 247 | } 248 | //end to refactor 249 | panorama.registerPanoProvider(options.panoProvider); 250 | return data; 251 | }; 252 | panorama.registerPanoProvider(tmpPanoProvider); 253 | panorama.setPano(options.pano); 254 | } 255 | }; 256 | 257 | /** 258 | * 259 | * @param {Object} latLon {lat:48.8534100,lon:2.3488000} 260 | * @params {Object} options @optional 261 | * @config {Function} success Method called after configure (if a panorama was found) : function(data){ ... } 262 | * @scope {google.maps.StreetViewPanorama} panorama 263 | * @param {google.maps.StreetViewService} data returned by google APIs about the panorama found 264 | * @config [Function} error Method called after init if no panorama was found : function(error){ ... } 265 | * @config {Boolean} enableRemotetilt If no accelerometer found on the device, you can launch an emulator 266 | * @config {Function} remotetiltIsBlocked Method called when the emulator popup was blocked by the browser 267 | * @config {Function} remotetiltIsUnblocked Method called when the emulator popup was released by the browser 268 | */ 269 | this.init = function(latLon,options){ 270 | if(this.isInit()){ 271 | init(latLon,options); 272 | } 273 | else{ 274 | prepare(latLon,options); 275 | } 276 | }; 277 | 278 | this.isInit = function(){ 279 | return panoramaInitiated; 280 | }; 281 | 282 | this.getCurrentPosition = function(){ 283 | if(panoramaInitiated === false){ 284 | throw new Error("Panorama needs to be init"); 285 | } 286 | return { 287 | lat : panorama.position.lat(), 288 | lon : panorama.position.lng() 289 | }; 290 | }; 291 | 292 | this.getCurrentPov = function(){ 293 | if(panoramaInitiated === false){ 294 | throw new Error("Panorama needs to be init"); 295 | } 296 | return panorama.getPov(); 297 | }; 298 | 299 | this.resize = function(){ 300 | if(panoramaInitiated === false){ 301 | throw new Error("Panorama needs to be init"); 302 | } 303 | google.maps.event.trigger(panorama, 'resize'); 304 | }; 305 | 306 | this.getPanoramaHtmlObject = function(){ 307 | return panoramaDiv; 308 | }; 309 | 310 | }; 311 | 312 | return PanoramaManager; 313 | 314 | }); -------------------------------------------------------------------------------- /src/js/topheman-panorama/vendor/device-motion-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DeviceMotion and DeviceOrientation polyfill 3 | * by Remy Sharp / leftlogic.com 4 | * MIT http://rem.mit-license.org 5 | * 6 | * Usage: used for testing motion events, include 7 | * script in head of test document and allow popup 8 | * to open to control device orientation. 9 | */ 10 | // (!window.DeviceOrientationEvent || !window.DeviceMotionEvent) && 11 | (function () { 12 | 13 | var div = document.createElement("div"), 14 | divStyle = div.style, 15 | evt; 16 | 17 | var polyfill = { 18 | motion: !window.DeviceMotionEvent, 19 | orientation: !window.DeviceOrientationEvent, 20 | transform: 'MozTransform' in divStyle ? 'MozTransform' : 21 | 'WebkitTransform' in divStyle ? 'WebkitTransform' : 22 | 'OTransform' in divStyle ? 'OTransform' : false 23 | }; 24 | 25 | divStyle[polyfill.transform] = 'rotate3d(0,0,0,0deg)'; 26 | if (!!divStyle[polyfill.transform]) polyfill.threeD = true; 27 | 28 | // Note - this isn't truely a polyfill, since we *always* go in and add 29 | // support. We can't stop the real events from firing, which is good, 30 | // but iPhone Emulator *says* it has device motion support, but in fact 31 | // doesn't - hence this truthy code :) 32 | 33 | // thankfully we don't have to do anything, because the event only fires on the window object 34 | if (polyfill.orientation || true) window.DeviceOrientationEvent = function () {}; 35 | if (polyfill.motion || true) window.DeviceMotionEvent = function () {}; 36 | 37 | try { 38 | // Standard DeviceOrientationEvent works in Firefox and Chrome 39 | evt = document.createEvent("DeviceOrientationEvent"); 40 | polyfill.prepareEvent = function( type, data ) { 41 | var isOrientation = type === "deviceorientation", 42 | deviceEvent = isOrientation ? 43 | "DeviceOrientationEvent": 44 | "DeviceMotionEvent", 45 | event = document.createEvent( deviceEvent ); 46 | 47 | isOrientation ? 48 | event["init" + deviceEvent]( type, true, true, 49 | data.alpha, 50 | data.beta, 51 | data.gamma, 52 | true 53 | ): 54 | event["init" + deviceEvent]( type, true, true, 55 | null, 56 | data.accelerationIncludingGravity, 57 | null, 58 | null 59 | ); 60 | 61 | if (isOrientation) { 62 | try { event.webkitCompassHeading = data.alpha; } catch (e) {} 63 | } 64 | 65 | return event; 66 | } 67 | } catch( e ) { 68 | // Fallback to HTMLEvents in Safari and Opera 69 | polyfill.prepareEvent = function( type, data ) { 70 | var event = document.createEvent( 'HTMLEvents' ), 71 | key; 72 | 73 | event.initEvent( type, true, true ); 74 | event.eventName = type; 75 | for ( key in data ) { 76 | event[key] = data[key]; 77 | } 78 | 79 | return event; 80 | } 81 | } 82 | 83 | var remoteTiltHost = 'remote-tilt.com'; 84 | 85 | // images - yes I do like to be self contained don't I?! :) 86 | var imageSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFsAAACpCAYAAABEbEGLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxODkwMDQ2NTNDNEYxMUUxQTYyOTk2M0QwMjU4OThGOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxODkwMDQ2NjNDNEYxMUUxQTYyOTk2M0QwMjU4OThGOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4OTAwNDYzM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4OTAwNDY0M0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+jKamsAAAAtNJREFUeNrs2t1t01AYgGG3dACPkA3qEcIlV5QJSCdAnaAwAWKCsgFs0HQChwmaS+6SThCOKwuigtP4J+ln9LzSRyWaRMmTE8fHcJLt3yRNkeY8U9VDmkWa+ZAPOktTptmYf84qzU29GDtXQG49111XM7xuUy3QfF/oa2DHAbeijwReABp8vjVh+zI8zFw4fBxv7q3qF1jdp1s7Qx2ut9UfZ0Gh+26BJ313dANXRDuvXg38xuf1NjrKoeTxMBKlq/rCzlCt01zWP0N0GujjtjzQ4y4iYS+DPJd8ZI/bCTtKHw7wmLNIJwBngbCn9QZgOcDZSF4/XqgzrUjY26ds0//xZPs0E2zYgg1bsGHDFuwRt2sHOcfTqSJruPi1C/s1t07dZg2XGxxGHLNhCzZswYYNW7BhCzZs2IINW7BhCzZs2IINW7BhwxZs2IINW7BhwxZs2IING7ZgwxZs2IING7ZgwxZs2LAFG7Zgw4Yt2LAFG7Zgw4Yt2LAFGzZswYYt2LAFGzZswYYt2LBhCzZswYYt2LBhCzZswYYNW7BhCzZs2IINW7BhCzZs2IINW7BhwxZs2IIdvbMdv7vG06lJF+yP3BxGYAs2bGcj8VukmadZb/1dkWaaJh/Li6hO8TaB57YGbSofwWvYjAH7psWiqd6QVWTsyMfsr2kuW9x+3vL2viDrlmmuOtzve/0mwW7RlydfhG36BLv9Cu3zqVjCbgf2kve3qRl5OezjtY6KPXnh+x/sMPIj4POa9oSOhr3YfnLRdlv3PV7YTfSdcBnwCX7uAF0E3apfbD/JWdAnOWsJvRrLp7TM4l6Meu4S6kXgi1C/V/XJk5VRRj1tqq953GV/X89+X/+MuhN+1/TLqIeTMU759BP5quEUZZqp7+WCN2l+7nNjK3zAFb3vt3sJr9X0/l9kM+g7Z1WfMT27az1puQ2uVvu5Q/JjD9mff/Hfq18CDADFyFpnRW9JPwAAAABJRU5ErkJggg=='; 87 | 88 | var imageBackSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFsAAACpCAIAAADLDtbcAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo3OTFCRTMwQTNDNjMxMUUxQTYyOTk2M0QwMjU4OThGOCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo3OTFCRTMwQjNDNjMxMUUxQTYyOTk2M0QwMjU4OThGOCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4OTAwNDZCM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4OTAwNDZDM0M0RjExRTFBNjI5OTYzRDAyNTg5OEY4Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+zWf2VgAABt9JREFUeNrsnU1PGlscxgHpi1aLtTGxpg0kdtWNpO7VTyAfoYmuXbmGha7ZsDep7knarmtSP4AJLo1NgwtvNCqUVI1vhfvIaei5/3mBgeHgZZ4nKcJwOGfO7/zfZmCm4VqtFnJWsVjcrSv0/1csFksmk3Nzc+7Nwk5ENjY2crlcf7DQNTo6mkql0ul0IpFolQgoLC4u9h8LoUxdzYnANJaWlkLB0PT09NbWFqzGkchqXaEgyQrlL5FAWYcLlD9EEDVmZmZCQRVibT6f/w8R4Oj7UOouEAGXP0QC6y+6kIy/f/+OJxH8Q90RCrxQi37+/PmeiKpKSQT68uULHqOGcTQtooUODg6wZmb2rVAomCOC3IYMhzzXxmd//vy5srKCYNftnVQoImbwZ7PZ9nAomuvr66Ky7J4iMEszkbzzOsoQkXg8bmAYWH7Pe2iViJlhOkzwCCLGMkDUzDDb29tv376FPXrNNZVK5du3byYTYtTYSMW6gOaBVyWREEUiJEIiJNLdXOP+9U3/KRwOuxGpVqtBIxKJRAQUeg3jSBs16+PHjwMy+bu7u5aIvHjxIiBEyuWyFQq9hnGEREiEREiEREiEREiEREiEREiEREiERCgSIRESIRESIRESIRESIRESIRESIRESIRGKREiEREiEREjEpDxflRaLxfSfAF9eXl5cXODJ+Pi43uzm5qZSqehbnj59OjIygsdI5H4Zbm9v8UHRRunZs2dDQ0O2o5+cnFg3os8nT57Y/jDZuhv+ExkcHBwbG2u8/PXrly0RfbYDAwOTk5PAIboaHR3Fp46Pj9GJvh0t9SF0lUql379/6yxev37t8iNtJ+h+eo247tZpMXXF43ErDiVM5s2bNzAKfSMW3KkrIGg8f/ToEXr2/TfrnolcXV3BFBsvsf5Os1XCauvTsNWrV6/0l5iq4+5G/u7wy5cvMfqDiKzn5+fCyF0ai3dBc29vT4QDrLNuJi7LrsMV5gkH8eWK/3aIeHIcseAIqAgEiMeiWcNThAfBJJ16E6Z3enqqIloPiAjHwZK6+EWLft6wf90vID2OujtUj+sRT47jSQIubMrl3QdERDiOj0SEFYjcqYdS4VDRaLSXRITj+Lh0Vr/QB9LHEuF5YmLClzuHtM8VjuNUR+kS8V/EhaZeg/ZwHD0YNcwEdd3+/j5qPxWMsV1EZdNEWqnNVFJsvc+BuoQxVqtVMa7qs2nBatRrsN/dCHK2fYp4oZDh0Vqw+pJ927SR4eFhYdtO5aOwZLQUM3SJjrYupioXWIoY8fDwEGH43bt3vSHy/PlzsYxOPiyinXtlKdYcEQTd2pYkVmuyvVTVHBERRFCD+hLVxDzx0po+un05cjtxBNWHsFhrVe5X6u2kmTkiVnNwCQ1N063erMVo3VUzaYeICKvA4TJt20whDl6g6+tr29oMcceK2/YEil91c7QNwxZL5GIgaqq6TcEKUFxaaxl0YjUQ5A4Upvi4iCbAKgpZdSKmlYrRfxuxLoV7FXB2diYsyHoOCUdJaGNdeTVt6+QB1K/I1RUisAKX9sigSLfubnV8fCyO4vSEKg5/VUtsLJVK3SDSTvYVRqG8RmzUMaHBjx8/xsfHxf0z1YnyxslkzFN00uBoa4ZHR0c4tBmqy8Vnu07Eqb5yP6OHJf2nLviLsgXgEIuPGXod9KKu3ttIJ3IPww9B/E6PREiEREiEREiEREiEREiEREiEREiEREiEIhESIRESIRESIRESIRESIRESIRESIRESoUiEREiEREiEREiEREiEREiEREikn2VzNYnvV6w8WNleGWZDRNxtpc99xHJVaVS8zf9LPtq0BSMrcw3VNLKa1PT09Pz8fCwWUy93d3e3t7fFbaOCQmRubi6TyeBRbAeOXC63uroaLK/58OHD1taWFUeofkNOkMK74nLpfiYCHOvr600tqGmbPiGSSCSy2WwrLVOpFNj1P5Hl5eXW3SGdTvc/Eay8J4OC+t9rPLX35caArNDaF3I/icjcb5pIsVj01N6XGxV78xrU0SbHQ5HuCZ9Xgh0eUvSAyNraWpcad65kMnlPBMHfJBSs+crKSistcdS3sbFhksjCwsL9n1qt9vHjxwGzwog1VxUKBXU7eGOamppSQ4fUn/fv3xuGsri4WC6XbXF8+vTJMA4Ig6rRw+rEKkx0ZmbGfKrDkcvs7GyjrodpbG5uYmfMV9L5fF49DzdONcNpl5aWAliDIIzqJx/C+sn31boChQOJ5evXr/qxRVh8HREoSxHWYX9cA8fe2dkxXKT0RJlMBjO1npoIO31lBWPJ5XLmg5yBcI44mk6nnY7Cw+5f4qGg2q2rD1jEYjFUpbYnd3X9K8AA/k9UkZpuQLkAAAAASUVORK5CYII='; 89 | 90 | var body = document.documentElement, // yep, it's not really the body folks 91 | height = 340, 92 | guid = 'b' + (+new Date).toString(32), 93 | src = getRemoteScript().src, 94 | ws; 95 | 96 | // if the url hash doesn't contain tiltremote (our key) then fireup the popup, else we are the popup 97 | if (window.location.hash.indexOf('tiltremote') === -1 && !window.remoteTilt) { 98 | initServer(src); 99 | } else { 100 | initRemote(); 101 | } 102 | 103 | function getRemoteScript() { 104 | var body = document.body, 105 | head = document.head || document.getElementsByTagName('head'), 106 | remoteScript = { src: '' }; // just in case 107 | if (body && body.lastChild.nodeName == 'SCRIPT') { 108 | remoteScript = body.lastChild; 109 | } else if (head && head.length && head.lastChild.nodeName == 'SCRIPT') { // it's in the head 110 | remoteScript = head.lastChild; 111 | } 112 | 113 | return remoteScript; 114 | } 115 | 116 | 117 | function connect(key) { 118 | var WebSocket = window.WebSocket || window.MozWebSocket; 119 | var ws = new WebSocket('ws://' + remoteTiltHost + '/listen/' + key); 120 | ws.onmessage = function (ev) { 121 | var deviceEvent = JSON.parse(ev.data); 122 | 123 | var event = polyfill.prepareEvent( deviceEvent.type, deviceEvent.data ); 124 | 125 | window.dispatchEvent(event); 126 | if (window['on' + event.type]) window['on' + event.type](event); 127 | }; 128 | ws.onclose = function () { 129 | setTimeout(function () { 130 | connect(key); 131 | }, 1000); 132 | }; 133 | ws.onopen = function () { 134 | console.log('connected to ' + key); 135 | }; 136 | } 137 | 138 | function initServer(src) { 139 | // old way didn't work. Shame, but I like the new way too :) 140 | // var remote = window.open('data:text/html,', 'Tilt', 'width=300,height=' + height); 141 | 142 | var key = ( src.match(/key=(.+?)\b/) || [,''] )[1]; 143 | 144 | var blocked = function () { 145 | if (key) { 146 | connect(key); 147 | } else { 148 | if (confirm("To use a real mobile device for motion events or you can't enable popups, select 'OK' to continue. Or, select 'cancel' and enable popups to use the mini remote.")) { 149 | // start the ajax madness 150 | var xhr = new XMLHttpRequest(); 151 | xhr.onreadystatechange = function() { 152 | if (xhr.readyState == 4) { 153 | if (xhr.status == 200 || xhr.status == 0) { 154 | var data = JSON.parse(xhr.responseText); 155 | connect(data.key); 156 | alert('Visiting http://' + remoteTiltHost + '/' + data.key + ' will remotely send motion events to this page'); 157 | } 158 | } 159 | }; 160 | xhr.open('GET', 'http://' + remoteTiltHost + '/getkey', true); 161 | xhr.send(null); 162 | } 163 | } 164 | }; 165 | 166 | //modif topheman - added global remoteTiltWindow var assignment to allow postMessage 167 | var remote = remoteTiltWindow = window.open(window.location.toString() + '#tiltremote', 'Tilt', 'width=300,height=' + height); 168 | 169 | //modif topheman removed the remote part (not to have the missleading connecting message in firefox) 170 | // stupid logic to detect if Chrome really did block the window 171 | // if (remote) { 172 | // remote.onload = function () { 173 | // setTimeout(function () { 174 | // if (remote.innerHeight <= 0) { 175 | // blocked(); 176 | // } 177 | // }, 10); 178 | // } 179 | // } else { 180 | // blocked(); 181 | // } 182 | 183 | } 184 | 185 | function renderRemote() { 186 | document.documentElement.innerHTML = ['Motion Emulator', 187 | '', 210 | '', 211 | '', 212 | '
', 213 | '
', 214 | '', 215 | '', 216 | '', 217 | '', 218 | '', 219 | '
', 220 | '
', 221 | '
', // end of controls 222 | '
', 223 | '
', 224 | '
', 225 | '
', 226 | '
', 227 | '
', 228 | 'south', 229 | '
', 230 | '' 231 | ].join(''); 232 | } 233 | 234 | function initRemote() { 235 | // because in Firefox you can't set the window.opener value 236 | var opener = window.opener; 237 | 238 | if (window.remoteTilt && !opener) { 239 | opener = window; 240 | } 241 | 242 | renderRemote(); 243 | 244 | var TO_RADIANS = Math.PI / 180, 245 | origBeta = 0, // used to work out the slider value 246 | orientation = { 247 | alpha: 180, 248 | beta: 0, 249 | gamma: 0 250 | }, 251 | accelerationIncludingGravity = { x: 0, y: 0, z: -9.81 }, 252 | down = false, // for mouse tracking 253 | last = { x: null, y: null }, 254 | pov = document.getElementById('pov'), 255 | sliders = { 256 | alpha: document.getElementById('alpha'), 257 | beta: document.getElementById('beta'), 258 | gamma: document.getElementById('gamma') 259 | }, 260 | preview = document.getElementById('preview'), 261 | motionValues = document.getElementById('motion'); 262 | 263 | 264 | window.update = function(updateSliders) { 265 | if ( polyfill.transform ) { 266 | preview.style[polyfill.transform] = 'rotateY('+ orientation.gamma + 'deg) rotate3d(1,0,0, '+ (origBeta*-1) + 'deg)'; 267 | preview.parentNode.style[polyfill.transform] = 'rotate(' + (180-orientation.alpha) + 'deg)'; 268 | } 269 | 270 | for (var key in orientation) { 271 | document.getElementById('o' + key.substring(0, 1)).value = parseFloat(orientation[key].toFixed(2)); 272 | if (key == 'beta') { 273 | sliders.beta.value = origBeta; 274 | } else { 275 | sliders[key].value = orientation[key]; 276 | } 277 | } 278 | 279 | motionValues.value = [ 280 | accelerationIncludingGravity.x.toFixed(2), 281 | accelerationIncludingGravity.y.toFixed(2), 282 | accelerationIncludingGravity.z.toFixed(2) 283 | ].join(' / '); 284 | 285 | }; 286 | 287 | function fire(event) { 288 | if (opener) { 289 | if (opener['on' + event.type]) opener['on' + event.type](event); 290 | opener.dispatchEvent(event); 291 | if (opener != window) window.dispatchEvent(event); 292 | } 293 | } 294 | 295 | function fireDeviceOrientationEvent() { 296 | var event = polyfill.prepareEvent( "deviceorientation", orientation ); 297 | fire(event); 298 | } 299 | 300 | function fireDeviceMotionEvent() { 301 | var event = polyfill.prepareEvent( "devicemotion", {accelerationIncludingGravity: accelerationIncludingGravity} ); 302 | fire(event); 303 | } 304 | 305 | function fireMotionEvents() { 306 | // if (polyfill.orientation) 307 | fireDeviceOrientationEvent(); 308 | // if (polyfill.motion) 309 | fireDeviceMotionEvent(); 310 | } 311 | 312 | function getOrientationValue(value, type) { 313 | value *= 1; 314 | if (type == 'beta') { 315 | // parseFloat + toFixed avoids the massive 0.00000000 and infinitely small numbers 316 | accelerationIncludingGravity.z = parseFloat( (Math.sin( (TO_RADIANS * (value - 90))) * 9.81).toFixed(10) ); 317 | accelerationIncludingGravity.y = parseFloat((Math.sin( (TO_RADIANS * (value - 180))) * 9.81).toFixed(10)); 318 | origBeta = value; 319 | value = parseFloat((Math.sin(value * TO_RADIANS) * 90).toFixed(10)); 320 | } else if (type == 'gamma') { 321 | accelerationIncludingGravity.x = parseFloat( (Math.sin( (TO_RADIANS * (value - 180))) * -9.81).toFixed(10) ); 322 | } 323 | return value; 324 | } 325 | 326 | function oninput(event) { 327 | var target = event.target; 328 | if (target.nodeName == 'INPUT') { 329 | orientation[target.id] = getOrientationValue(target.value, target.id); 330 | if (!event.manual) fireMotionEvents(); 331 | } 332 | } 333 | 334 | function manualUpdate() { 335 | for (var key in sliders) { 336 | oninput({ manual: true, target: { id: key, nodeName: 'INPUT', value: sliders[key].value } }); 337 | } 338 | fireMotionEvents(); 339 | } 340 | 341 | // simple fake wobble 342 | function startShake() { 343 | shake = setInterval(function () { 344 | sliders.alpha.value = parseFloat(sliders.alpha.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 345 | sliders.beta.value = parseFloat(sliders.beta.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 346 | sliders.gamma.value = parseFloat(sliders.gamma.value) + (Math.random() * (Math.random() < 0.5 ? 1 : -1) * 0.05); 347 | manualUpdate(); 348 | }, 100); 349 | } 350 | 351 | function stopShake() { 352 | clearInterval(shake); 353 | } 354 | 355 | setTimeout(function () { 356 | // remove other styles 357 | [].forEach.call(document.styleSheets, function (style) { 358 | if (style.title != 'protect') { 359 | style.ownerNode.parentNode.removeChild(style.ownerNode); 360 | } 361 | }); 362 | }) 363 | 364 | /** begin event hooks */ 365 | 366 | // body !== body - it's the documentElement 367 | body.addEventListener('input', oninput, false); 368 | 369 | pov.addEventListener('mousedown', function (event) { 370 | down = true; 371 | last.x = event.pageX; 372 | last.y = event.pageY; 373 | event.preventDefault(); 374 | }, false); 375 | 376 | body.addEventListener('mousemove', function (event) { 377 | if (down) { 378 | var dx = (last.x - event.pageX)// * 0.1, 379 | dy = (last.y - event.pageY); // * 0.1; 380 | last.x = event.pageX; 381 | last.y = event.pageY; 382 | sliders.gamma.value -= dx; 383 | // sliders.gamma.value = Math.sin( (TO_RADIANS * (sliders.gamma.value) / 180 384 | sliders.beta.value -= dy; 385 | manualUpdate(); 386 | } 387 | }, false); 388 | 389 | body.addEventListener('mouseup', function (event) { 390 | down = false; 391 | }, false); 392 | 393 | body.addEventListener('click', function (event) { 394 | var target = event.target; 395 | if (target.nodeName == 'BUTTON') { 396 | if (target.id == 'flat') { 397 | sliders.alpha.value = 180; 398 | sliders.beta.value = 0; 399 | sliders.gamma.value = 0; 400 | } 401 | 402 | manualUpdate(); 403 | } else if (target.id == 'wobble') { 404 | if (target.checked) startShake(); 405 | else stopShake(); 406 | } 407 | }, false); 408 | 409 | // eat our own dog food 410 | window.addEventListener('devicemotion', function () { 411 | // translate x, y, z back to orientation... ::sigh:: 412 | // ...or just let the orientation event do the work 413 | }, false); 414 | window.addEventListener('deviceorientation', function (event) { 415 | if (event.alpha !== null) orientation.alpha = event.alpha; 416 | if (event.beta !== null) orientation.beta = event.beta; 417 | if (event.gamma !== null) orientation.gamma = event.gamma; 418 | update(); 419 | }, false); 420 | 421 | //added by topheman for bombs 422 | window.addEventListener('keydown',function(e){ 423 | var origin = window.location.origin; 424 | if(e.keyCode === 32){ 425 | window.postMessage('bomb',origin); 426 | } 427 | }); 428 | //end added by topheman for bombs 429 | 430 | update(); 431 | } 432 | 433 | })(); 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | --------------------------------------------------------------------------------