├── .gitignore ├── package.json ├── COPYING ├── index.html ├── readme.md ├── index.js └── client ├── host └── index.html ├── assets ├── js │ ├── host.js │ └── device.js └── css │ └── site.css └── device └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "device-orientation-websockets", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "express": "^4.16.4", 8 | "socket.io": "^2.1.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Device Orientation - Websockets 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 | 23 | 24 |

Click here if you aren't redirected

25 | 26 | 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Device Orientation - Websockets 2 | 3 | ## Installation 4 | 5 | 1. Install dependencies: 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | 2. Run app: 12 | 13 | ``` 14 | node index.js 15 | ``` 16 | 17 | 3. Open web pages: 18 | 19 | - Device: [http://localhost:7006/](http://localhost:7006/) 20 | - Host: [http://localhost:7006/host/](http://localhost:7006/host/) 21 | 22 | 23 | ## Post 24 | 25 | - [https://f90.co.uk/labs/device-orientation-websockets/](https://f90.co.uk/labs/device-orientation-websockets/) 26 | 27 | ## Example 28 | 29 | - [http://orangespaceman.github.io/device-orientation-websockets](http://orangespaceman.github.io/device-orientation-websockets) 30 | (Note, this is the device view only, it must be run via Node to see the websockets in action) 31 | 32 | Copyright © 2018 Me 33 | This work is free. You can redistribute it and/or modify it under the 34 | terms of the Do What The Fuck You Want To Public License, Version 2, 35 | as published by Sam Hocevar. See the COPYING file for more details. 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var http = require('http').Server(app); 4 | var io = require('socket.io')(http); 5 | var port = process.env.PORT || process.env['app_port'] || 7007; 6 | 7 | app.get('/', (req, res) => { 8 | res.sendFile(__dirname + '/client/device/index.html'); 9 | }); 10 | 11 | app.get('/host', (req, res) => { 12 | res.sendFile(__dirname + '/client/host/index.html'); 13 | }); 14 | 15 | app.use(express.static(__dirname + '/client/')); 16 | 17 | var host = io.of('/host'); 18 | var client = io.of('/device'); 19 | 20 | client.on('connection', socket => { 21 | socket.on('name', name => { 22 | host.emit('add', { 23 | id: socket.id, 24 | name 25 | }); 26 | }); 27 | socket.on('orientation', orientation => { 28 | host.emit('orientation', { 29 | id: socket.id, 30 | orientation 31 | }); 32 | }); 33 | socket.on('disconnect', () => { 34 | host.emit('remove', { 35 | id: socket.id 36 | }); 37 | }) 38 | }); 39 | 40 | http.listen(port); 41 | -------------------------------------------------------------------------------- /client/host/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Device Orientation - Websockets 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |

24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /client/assets/js/host.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // handle sockets 4 | var socket = io("/host"); 5 | socket.on("add", addScene); 6 | socket.on("orientation", updateScene); 7 | socket.on("remove", removeScene); 8 | 9 | // content node to add scenes to 10 | var wrapperEl = document.querySelector(".Wrapper"); 11 | 12 | // scene template to duplicate 13 | var sceneTemplateEl = document.querySelector(".Scene"); 14 | 15 | // object containing all active scenes 16 | var scenes = {}; 17 | 18 | function addScene(data) { 19 | scenes[data.id] = {}; 20 | 21 | var sceneEl = sceneTemplateEl.cloneNode(true); 22 | sceneEl.classList.remove("isTemplate"); 23 | wrapperEl.appendChild(sceneEl); 24 | scenes[data.id].sceneEl = sceneEl; 25 | 26 | var deviceEl = sceneEl.querySelector(".Device"); 27 | scenes[data.id].deviceEl = deviceEl; 28 | 29 | var nameEl = sceneEl.querySelector(".NameText"); 30 | nameEl.textContent = data.name; 31 | } 32 | 33 | function updateScene(data) { 34 | if (scenes[data.id]) { 35 | var deviceEl = scenes[data.id].deviceEl; 36 | deviceEl.style.transform = 37 | "rotateX(" + data.orientation.beta + "deg) " + 38 | "rotateY(" + data.orientation.gamma + "deg) " + 39 | "rotateZ(" + data.orientation.alpha + "deg)"; 40 | } 41 | } 42 | 43 | function removeScene(data) { 44 | if (scenes[data.id]) { 45 | wrapperEl.removeChild(scenes[data.id].sceneEl); 46 | delete scenes[data.id]; 47 | } 48 | } 49 | 50 | })(); -------------------------------------------------------------------------------- /client/device/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Device Orientation - Websockets 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |

41 | α 42 | 43 |

44 |

45 | β 46 | 47 |

48 |

49 | γ 50 | 51 |

52 |

53 | α' 54 | 55 |

56 |

57 | β' 58 | 59 |

60 |

61 | γ' 62 | 63 |

64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /client/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: #506080; 17 | font-family: Georgia, serif; 18 | height: 100%; 19 | margin: 0; 20 | width: 100%; 21 | } 22 | 23 | /* Wrapper */ 24 | 25 | .Wrapper { 26 | height: 100vh; 27 | width: 100vw; 28 | overflow: hidden; 29 | 30 | display: flex; 31 | align-items: stretch; 32 | justify-content: space-between; 33 | flex-wrap: wrap; 34 | } 35 | 36 | body.isHost .Wrapper::after { 37 | content: ""; 38 | flex: auto; 39 | } 40 | 41 | /* 3d */ 42 | 43 | .Scene { 44 | flex: 1 0 33%; 45 | } 46 | 47 | .Scene.isTemplate { 48 | display: none; 49 | } 50 | 51 | .DeviceContainer { 52 | background: rgba(0,0,0,0.1); 53 | box-shadow: 0 0 25px 0 rgba(0,0,0,0.1); 54 | position: relative; 55 | margin: 0 auto; 56 | width: 75%; 57 | height: 100%; 58 | perspective: 1000px; 59 | transform: translate(0) scale(0.6); 60 | } 61 | 62 | .Device { 63 | width: 100%; 64 | height: 100%; 65 | transform-style: preserve-3d; 66 | transform: scale(2.8); 67 | transform-origin: center center; 68 | transition: transform 1s; 69 | } 70 | 71 | .Device.hasName { 72 | transform: scale(1); 73 | } 74 | 75 | .Device.hasInited { 76 | transition: none; 77 | } 78 | 79 | body.isHost .Device { 80 | transform: scale(1) rotateX(30deg) rotateY(30deg) rotateZ(-30deg); 81 | transition: none; 82 | } 83 | 84 | .Device > * { 85 | position: absolute; 86 | } 87 | 88 | .Device-front { 89 | width: 100%; 90 | height: 100%; 91 | background: #f90; 92 | transform: translateX(0) rotateY(0deg) translateZ(15px); 93 | 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | } 98 | 99 | .Device-back { 100 | width: 100%; 101 | height: 100%; 102 | background: #fc0; 103 | transform: translateX(0) translateZ(-15px); 104 | } 105 | 106 | .Device-left { 107 | width: 30px; 108 | height: 100%; 109 | background: #f60; 110 | transform: translateX(-15px) rotateY(-90deg); 111 | backface-visibility: hidden; 112 | } 113 | 114 | .Device-right { 115 | width: 30px; 116 | height: 100%; 117 | background: #f60; 118 | transform: rotateY(90deg); 119 | backface-visibility: hidden; 120 | right: -15px; 121 | } 122 | 123 | .Device-top { 124 | width: 100%; 125 | height: 30px; 126 | background: #f30; 127 | transform: rotateX(90deg) translateZ(15px); 128 | backface-visibility: hidden; 129 | } 130 | 131 | .Device-bottom { 132 | width: 100%; 133 | height: 30px; 134 | background: #f30; 135 | transform: rotateX(-90deg); 136 | backface-visibility: hidden; 137 | bottom: -15px; 138 | } 139 | 140 | 141 | /* Name form */ 142 | 143 | .Name { 144 | text-align: center; 145 | width: 100%; 146 | padding: 20px; 147 | } 148 | 149 | .Name-label { 150 | display: block; 151 | font-size: 8px; 152 | margin-bottom: 5px; 153 | opacity: 1; 154 | transition: opacity 1s; 155 | } 156 | 157 | .Device.hasName .Name-label { 158 | opacity: 0; 159 | } 160 | 161 | .Name-input { 162 | background: none; 163 | border: none; 164 | border-bottom: 1px dashed #036; 165 | color: #036; 166 | font-family: Georgia, serif; 167 | font-size: 20px; 168 | margin-bottom: 5px; 169 | padding: 10px; 170 | text-align: center; 171 | width: 100%; 172 | 173 | transition: border-bottom-color 1s; 174 | } 175 | 176 | .Device.hasName .Name-input { 177 | border-bottom-color: transparent; 178 | } 179 | 180 | .Name-input:focus { 181 | outline: none; 182 | } 183 | 184 | .Name-button { 185 | border: none; 186 | color: #fff; 187 | background: #036; 188 | font-family: Georgia, serif; 189 | font-size: 8px; 190 | padding: 2px 10px; 191 | opacity: 1; 192 | transition: opacity 1s; 193 | } 194 | 195 | .Device.hasName .Name-button { 196 | opacity: 0; 197 | } 198 | 199 | /* host */ 200 | 201 | .NameText { 202 | font-size: 32px; 203 | } 204 | 205 | 206 | /* debug */ 207 | 208 | .Debug { 209 | display: flex; 210 | justify-content: space-around; 211 | position: fixed; 212 | top: 0; 213 | right: 0; 214 | left: 0; 215 | background: rgba(255, 255, 255, 0.4); 216 | padding: 0.2rem; 217 | z-index: 3; 218 | font-size: 0.75rem; 219 | text-align: left; 220 | opacity: 0; 221 | } 222 | 223 | .Debug.isDisplayed { 224 | opacity: 1; 225 | transition: opacity 1s; 226 | } 227 | 228 | .Debug-block { 229 | margin: 0; 230 | } -------------------------------------------------------------------------------- /client/assets/js/device.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // device orientation - default to portrait 4 | var isLandscape = false; 5 | var isRotatedClockwise = false; 6 | 7 | // device el 8 | var device = document.querySelector('.Device'); 9 | 10 | // name form els 11 | var form = document.querySelector(".Name"); 12 | var formInput = document.querySelector(".Name-input"); 13 | var formButton = document.querySelector(".Name-button"); 14 | 15 | // debug els 16 | var debugEl = document.querySelector('.Debug'); 17 | var debugAlphaEl = document.querySelector('.Debug-value--alpha'); 18 | var debugBetaEl = document.querySelector('.Debug-value--beta'); 19 | var debugGammaEl = document.querySelector('.Debug-value--gamma'); 20 | var debugAlphaModifiedEl = document.querySelector('.Debug-value--alphaModified'); 21 | var debugBetaModifiedEl = document.querySelector('.Debug-value--betaModified'); 22 | var debugGammaModifiedEl = document.querySelector('.Debug-value--gammaModified'); 23 | 24 | // socket io connection 25 | var socket = window.io ? io('/device') : null; 26 | 27 | // reduce number of orientation events sent to websockets 28 | var debounceDeviceOrientationEvent = debounce(handleDeviceorientationEvent, 10); 29 | 30 | // init 31 | function init() { 32 | formInput.focus(); 33 | calculateDeviceOrientation(); 34 | form.addEventListener("submit", saveName); 35 | device.addEventListener("transitionend", initWebsockets); 36 | window.addEventListener('orientationchange', handleOrientationChange); 37 | } 38 | 39 | // recalculate values based on major device rotation 40 | // (e.g. landscape to portrait or vice versa) 41 | // allow time for the screen layout to readjust first 42 | function handleOrientationChange() { 43 | setTimeout(function() { 44 | calculateDeviceOrientation(); 45 | }, 500); 46 | } 47 | 48 | // calculate whether the device is landscape or portrait 49 | function calculateDeviceOrientation(e) { 50 | isLandscape = 51 | document.documentElement.clientHeight < document.documentElement.clientWidth; 52 | isRotatedClockwise = window.orientation === -90; 53 | } 54 | 55 | // save name on form submit 56 | // kick off animation 57 | function saveName(e) { 58 | e.preventDefault(); 59 | var name = formInput.value.trim(); 60 | if (name.length > 0) { 61 | device.classList.add('hasName'); 62 | debugEl.classList.add('isDisplayed'); 63 | formButton.disabled = true; 64 | formInput.readOnly = true; 65 | socket.emit("name", name); 66 | } 67 | } 68 | 69 | // init websockets on css transition end 70 | function initWebsockets() { 71 | device.classList.add('hasInited'); 72 | window.addEventListener("deviceorientation", debounceDeviceOrientationEvent); 73 | } 74 | 75 | // update display on device orientation 76 | function handleDeviceorientationEvent(e) { 77 | var alpha = normaliseAlpha(e); 78 | var beta = normaliseBeta(e); 79 | var gamma = normaliseGamma(e); 80 | 81 | emit(alpha, beta, gamma); 82 | render(alpha, beta, gamma); 83 | debug(alpha, beta, gamma, e); 84 | } 85 | 86 | function emit(alpha, beta, gamma) { 87 | if (socket) { 88 | socket.emit("orientation", { 89 | alpha: alpha, 90 | beta: beta, 91 | gamma: gamma 92 | }); 93 | } 94 | } 95 | 96 | // update device 97 | function render(alpha, beta, gamma) { 98 | device.style.transform = 99 | "rotateX(" + beta + "deg) " + 100 | "rotateY(" + gamma + "deg) " + 101 | "rotateZ(" + alpha + "deg)"; 102 | } 103 | 104 | function normaliseAlpha(e) { 105 | var alpha; 106 | if (!isLandscape) { 107 | if (e.beta > 90) { 108 | alpha = e.alpha - 90; 109 | } else { 110 | alpha = -e.alpha + 90; 111 | } 112 | } else { 113 | if (isRotatedClockwise) { 114 | if (Math.abs(e.beta) > 90) { 115 | alpha = e.alpha; 116 | } else { 117 | alpha = -e.alpha; 118 | } 119 | } else { 120 | if (Math.abs(e.beta) > 90) { 121 | alpha = e.alpha; 122 | } else { 123 | alpha = -e.alpha; 124 | } 125 | } 126 | } 127 | return alpha; 128 | } 129 | 130 | function normaliseBeta(e) { 131 | var beta; 132 | if (!isLandscape) { 133 | beta = (-e.beta + 90); 134 | } else { 135 | if (isRotatedClockwise) { 136 | beta = (-e.gamma + 90); 137 | } else { 138 | beta = 90 + e.gamma; 139 | } 140 | } 141 | return beta; 142 | } 143 | 144 | function normaliseGamma(e) { 145 | var gamma; 146 | if (!isLandscape) { 147 | gamma = e.gamma; 148 | } else { 149 | if (isRotatedClockwise) { 150 | if (Math.abs(e.beta) > 90) { 151 | gamma = e.beta; 152 | } else { 153 | gamma = (-e.beta); 154 | } 155 | } else { 156 | if (Math.abs(e.beta) > 90) { 157 | gamma = (-e.beta); 158 | } else { 159 | gamma = e.beta; 160 | } 161 | } 162 | } 163 | return gamma; 164 | } 165 | 166 | function debug(alpha, beta, gamma, e) { 167 | debugAlphaEl.textContent = Math.round(e.alpha); 168 | debugBetaEl.textContent = Math.round(e.beta); 169 | debugGammaEl.textContent = Math.round(e.gamma); 170 | debugAlphaModifiedEl.textContent = Math.round(alpha); 171 | debugBetaModifiedEl.textContent = Math.round(beta); 172 | debugGammaModifiedEl.textContent = Math.round(gamma); 173 | } 174 | 175 | // https://davidwalsh.name/javascript-debounce-function 176 | // Returns a function, that, as long as it continues to be invoked, will not 177 | // be triggered. The function will be called after it stops being called for 178 | // N milliseconds. If `immediate` is passed, trigger the function on the 179 | // leading edge, instead of the trailing. 180 | function debounce(func, wait, immediate) { 181 | var timeout; 182 | return function() { 183 | var context = this, args = arguments; 184 | var later = function() { 185 | timeout = null; 186 | if (!immediate) func.apply(context, args); 187 | }; 188 | var callNow = immediate && !timeout; 189 | clearTimeout(timeout); 190 | timeout = setTimeout(later, wait); 191 | if (callNow) func.apply(context, args); 192 | }; 193 | } 194 | 195 | init(); 196 | 197 | })(); 198 | --------------------------------------------------------------------------------