diff --git a/controller/index.html b/controller/index.html index 8975042..124b203 100644 --- a/controller/index.html +++ b/controller/index.html @@ -6,6 +6,7 @@ + diff --git a/controller/js/app.js b/controller/js/app.js index 165ee41..aea0c19 100644 --- a/controller/js/app.js +++ b/controller/js/app.js @@ -28,6 +28,8 @@ var APP = { var rotation = new THREE.Euler( 0, 0, 0, 'YXZ' ); var rotQuat = new THREE.Quaternion(); + var tweenInProgress = false; + this.load = function( json ) { renderer = new THREE.WebGLRenderer( { @@ -126,19 +128,173 @@ var APP = { }; - this.setManualOrientation = function( alpha, beta, gamma ) { + this.setManualOrientation = ( function() { - var _x = THREE.Math.degToRad( beta || 0 ); - var _y = THREE.Math.degToRad( alpha || 0 ); - var _z = THREE.Math.degToRad( gamma || 0 ); + var _q = new THREE.Quaternion(); - euler.set( _x, _y, -_z, 'YXZ' ); + return function( alpha, beta, gamma ) { - // Apply provided deviceorientation values to controller - controls.object.quaternion.setFromEuler( euler ); - controls.object.quaternion.multiply( worldQuat ); + var _x = THREE.Math.degToRad( beta || 0 ); + var _y = THREE.Math.degToRad( alpha || 0 ); + var _z = THREE.Math.degToRad( gamma || 0 ); - } + euler.set( _x, _y, -_z, 'YXZ' ); + + // Apply provided deviceorientation values to controller + _q.setFromEuler( euler ); + _q.multiply( worldQuat ); + + controls.object.quaternion.copy( _q ); + + }; + + } )(); + + this.playback = ( function() { + + var source, destination; + var _this; + + var _a0, _b0, _g0; + + return function( data ) { + + _this = this; + + // Store original device orientation values + _a0 = deviceOrientation.alpha; + _b0 = deviceOrientation.beta; + _g0 = deviceOrientation.gamma; + + var frameNumber = 0; + + // Tween through each of our animation frames + data.frames.reduce( function( chain, frame ) { + // Add these actions to the end of the promise chain + return chain.then( function() { + if ( frameNumber > 0 ) { + sendMessage( + window.parent, { + 'action': 'updateActiveFrame', + 'data': frameNumber + } + ); + } + frameNumber++; + + if ( frame.type === 0 ) { // SET + return _this.set( frame ); + } else { // ANIMATION + return _this.tween( frame ); + } + } ); + }, Promise.resolve() ).then( function() { + // Rollback to original device orientation values + window.setTimeout( function() { + sendMessage( + window.parent, { + 'action': 'resetTimeline' + } + ); + + _this.setManualOrientation( _a0, _b0, _g0 ); + }, 1000 ); + } ); + + }; + + } )(); + + this.set = ( function() { + + var _this; + + var waitTime, playTime; + + return function( frame ) { + + _this = this; + + var setPromise = new Promise( function( resolve, reject ) { + + waitTime = frame.offset * 1000; + playTime = frame.duration * 1000; + + window.setTimeout( function() { + + _this.setManualOrientation( frame.data.alpha, frame.data.beta, frame.data.gamma ); + + window.setTimeout( function() { + resolve(); // this Promise can never reject + }, playTime ); + + }, waitTime ); + + } ); + + return setPromise; + + }; + + } )(); + + this.tween = ( function() { + + var source, destination; + var _this; + + var waitTime, playTime; + + return function( frame ) { + + _this = this; + + var tweenPromise = new Promise( function( resolve, reject ) { + + tweenInProgress = true; + + source = { + alpha: deviceOrientation.alpha || 0, + beta: deviceOrientation.beta || 0, + gamma: deviceOrientation.gamma || 0 + }; + + destination = {}; + + if ( frame.data.alpha !== source.alpha ) destination.alpha = frame.data.alpha; + if ( frame.data.beta !== source.beta ) destination.beta = frame.data.beta; + if ( frame.data.gamma !== source.gamma ) destination.gamma = frame.data.gamma; + + waitTime = frame.offset * 1000; + playTime = frame.duration * 1000; + + var throwError = window.setTimeout( function() { + tweenInProgress = false; + reject(); + }, waitTime + 200 ); + + var tween = new TWEEN.Tween( source ) + .delay( waitTime ) + .to( destination, playTime ) + .onStart( function() { + window.clearTimeout( throwError ); + } ) + .onUpdate( function() { + _this.setManualOrientation( this.alpha, this.beta, this.gamma ); + } ) + .onComplete( function() { + tweenInProgress = false; + resolve(); + } ) + .start(); + + } ); + + return tweenPromise; + + }; + + } )(); this.updateScreenOrientation = function( data ) { @@ -229,6 +385,10 @@ var APP = { delta: time - prevTime } ); + if ( tweenInProgress ) { + TWEEN.update( time ); + } + controls.update(); // *** Calculate device orientation quaternion (without affecting rendering) @@ -238,7 +398,7 @@ var APP = { camQuat.inverse(); // Derive Tait-Bryan angles from calculated device orientation quaternion - deviceOrientation.setFromQuaternion( camQuat ); + deviceOrientation.setFromQuaternion( camQuat, 'YXZ' ); // Calculate required emulator screen roll compensation required var rollZ = rotation.setFromQuaternion( controls.object.quaternion, 'YXZ' ).z; diff --git a/controller/js/controls.js b/controller/js/controls.js index 2f79859..116d41d 100644 --- a/controller/js/controls.js +++ b/controller/js/controls.js @@ -16,6 +16,8 @@ window.addEventListener( 'load', function() { player.setSize( window.innerWidth, window.innerHeight ); } ); + var currentScreenOrientation = 0; + // Listen for device orientation events fired from the emulator // and dispatch them on to the parent window window.addEventListener( 'deviceorientation', function( event ) { @@ -30,6 +32,7 @@ window.addEventListener( 'load', function() { 'beta': event.beta, 'gamma': event.gamma, 'absolute': event.absolute, + 'screen': currentScreenOrientation, 'roll': event.roll } }, @@ -61,6 +64,8 @@ window.addEventListener( 'load', function() { 'rotateScreen': function( data ) { player.updateScreenOrientation( data ); + currentScreenOrientation = ( 360 - data.totalRotation ) % 360; + if ( window.parent ) { sendMessage( window.parent, { @@ -69,6 +74,9 @@ window.addEventListener( 'load', function() { url.origin ); } + }, + 'playback': function( data ) { + player.playback( data ); } }; diff --git a/controller/js/third_party/tween.min.js b/controller/js/third_party/tween.min.js new file mode 100644 index 0000000..5f0bf2f --- /dev/null +++ b/controller/js/third_party/tween.min.js @@ -0,0 +1,2 @@ +// tween.js v.0.15.0 https://github.com/sole/tween.js +void 0===Date.now&&(Date.now=function(){return(new Date).valueOf()});var TWEEN=TWEEN||function(){var n=[];return{REVISION:"14",getAll:function(){return n},removeAll:function(){n=[]},add:function(t){n.push(t)},remove:function(t){var r=n.indexOf(t);-1!==r&&n.splice(r,1)},update:function(t){if(0===n.length)return!1;var r=0;for(t=void 0!==t?t:"undefined"!=typeof window&&void 0!==window.performance&&void 0!==window.performance.now?window.performance.now():Date.now();rn;n++)E[n].stop()},this.delay=function(n){return s=n,this},this.repeat=function(n){return e=n,this},this.yoyo=function(n){return a=n,this},this.easing=function(n){return l=n,this},this.interpolation=function(n){return p=n,this},this.chain=function(){return E=arguments,this},this.onStart=function(n){return d=n,this},this.onUpdate=function(n){return I=n,this},this.onComplete=function(n){return w=n,this},this.onStop=function(n){return M=n,this},this.update=function(n){var f;if(h>n)return!0;v===!1&&(null!==d&&d.call(t),v=!0);var M=(n-h)/o;M=M>1?1:M;var O=l(M);for(f in i){var m=r[f]||0,N=i[f];N instanceof Array?t[f]=p(N,O):("string"==typeof N&&(N=m+parseFloat(N,10)),"number"==typeof N&&(t[f]=m+(N-m)*O))}if(null!==I&&I.call(t,O),1==M){if(e>0){isFinite(e)&&e--;for(f in u){if("string"==typeof i[f]&&(u[f]=u[f]+parseFloat(i[f],10)),a){var T=u[f];u[f]=i[f],i[f]=T}r[f]=u[f]}return a&&(c=!c),h=n+s,!0}null!==w&&w.call(t);for(var g=0,W=E.length;W>g;g++)E[g].start(n);return!1}return!0}},TWEEN.Easing={Linear:{None:function(n){return n}},Quadratic:{In:function(n){return n*n},Out:function(n){return n*(2-n)},InOut:function(n){return(n*=2)<1?.5*n*n:-.5*(--n*(n-2)-1)}},Cubic:{In:function(n){return n*n*n},Out:function(n){return--n*n*n+1},InOut:function(n){return(n*=2)<1?.5*n*n*n:.5*((n-=2)*n*n+2)}},Quartic:{In:function(n){return n*n*n*n},Out:function(n){return 1- --n*n*n*n},InOut:function(n){return(n*=2)<1?.5*n*n*n*n:-.5*((n-=2)*n*n*n-2)}},Quintic:{In:function(n){return n*n*n*n*n},Out:function(n){return--n*n*n*n*n+1},InOut:function(n){return(n*=2)<1?.5*n*n*n*n*n:.5*((n-=2)*n*n*n*n+2)}},Sinusoidal:{In:function(n){return 1-Math.cos(n*Math.PI/2)},Out:function(n){return Math.sin(n*Math.PI/2)},InOut:function(n){return.5*(1-Math.cos(Math.PI*n))}},Exponential:{In:function(n){return 0===n?0:Math.pow(1024,n-1)},Out:function(n){return 1===n?1:1-Math.pow(2,-10*n)},InOut:function(n){return 0===n?0:1===n?1:(n*=2)<1?.5*Math.pow(1024,n-1):.5*(-Math.pow(2,-10*(n-1))+2)}},Circular:{In:function(n){return 1-Math.sqrt(1-n*n)},Out:function(n){return Math.sqrt(1- --n*n)},InOut:function(n){return(n*=2)<1?-.5*(Math.sqrt(1-n*n)-1):.5*(Math.sqrt(1-(n-=2)*n)+1)}},Elastic:{In:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),-(r*Math.pow(2,10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i)))},Out:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),r*Math.pow(2,-10*n)*Math.sin(2*(n-t)*Math.PI/i)+1)},InOut:function(n){var t,r=.1,i=.4;return 0===n?0:1===n?1:(!r||1>r?(r=1,t=i/4):t=i*Math.asin(1/r)/(2*Math.PI),(n*=2)<1?-.5*r*Math.pow(2,10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i):r*Math.pow(2,-10*(n-=1))*Math.sin(2*(n-t)*Math.PI/i)*.5+1)}},Back:{In:function(n){var t=1.70158;return n*n*((t+1)*n-t)},Out:function(n){var t=1.70158;return--n*n*((t+1)*n+t)+1},InOut:function(n){var t=2.5949095;return(n*=2)<1?.5*n*n*((t+1)*n-t):.5*((n-=2)*n*((t+1)*n+t)+2)}},Bounce:{In:function(n){return 1-TWEEN.Easing.Bounce.Out(1-n)},Out:function(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375},InOut:function(n){return.5>n?.5*TWEEN.Easing.Bounce.In(2*n):.5*TWEEN.Easing.Bounce.Out(2*n-1)+.5}}},TWEEN.Interpolation={Linear:function(n,t){var r=n.length-1,i=r*t,u=Math.floor(i),o=TWEEN.Interpolation.Utils.Linear;return 0>t?o(n[0],n[1],i):t>1?o(n[r],n[r-1],r-i):o(n[u],n[u+1>r?r:u+1],i-u)},Bezier:function(n,t){var r,i=0,u=n.length-1,o=Math.pow,e=TWEEN.Interpolation.Utils.Bernstein;for(r=0;u>=r;r++)i+=o(1-t,u-r)*o(t,r)*n[r]*e(u,r);return i},CatmullRom:function(n,t){var r=n.length-1,i=r*t,u=Math.floor(i),o=TWEEN.Interpolation.Utils.CatmullRom;return n[0]===n[r]?(0>t&&(u=Math.floor(i=r*(1+t))),o(n[(u-1+r)%r],n[u],n[(u+1)%r],n[(u+2)%r],i-u)):0>t?n[0]-(o(n[0],n[0],n[1],n[1],-i)-n[0]):t>1?n[r]-(o(n[r],n[r],n[r-1],n[r-1],i-r)-n[r]):o(n[u?u-1:0],n[u],n[u+1>r?r:u+1],n[u+2>r?r:u+2],i-u)},Utils:{Linear:function(n,t,r){return(t-n)*r+n},Bernstein:function(n,t){var r=TWEEN.Interpolation.Utils.Factorial;return r(n)/r(t)/r(n-t)},Factorial:function(){var n=[1];return function(t){var r,i=1;if(n[t])return n[t];for(r=t;r>1;r--)i*=r;return n[t]=i}}(),CatmullRom:function(n,t,r,i,u){var o=.5*(r-n),e=.5*(i-t),a=u*u,f=u*a;return(2*t-2*r+o+e)*f+(-3*t+3*r-2*o-e)*a+o*u+t}}},"undefined"!=typeof module&&module.exports&&(module.exports=TWEEN); diff --git a/emulator/css/timeline.css b/emulator/css/timeline.css new file mode 100644 index 0000000..7142d00 --- /dev/null +++ b/emulator/css/timeline.css @@ -0,0 +1,33 @@ +#timeline { + line-height: 1; + position: fixed; + display: none; + bottom: 0; + left: 0; + padding: 10px; +} +#timeline ul { + font-size: 13px; + display: inline-block; + margin: -0.2em auto 0.2em; + -webkit-filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .25)); + -moz-filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .25)); + -ms-filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .25)); + -o-filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .25)); + filter: drop-shadow(0 1px 5px rgba(0, 0, 0, .25)); + -ms-transform: translateZ(0); + -o-transform: translateZ(0); + -moz-transform: translateZ(0); + -webkit-transform: translateZ(0); + transform: translateZ(0); +} +div.frame { + display: block; + font-family: Helvetica, Arial, sans-serif; + font-size: 11px; + height: 27.5px; + padding: 8px 20px; + margin: 0 10px; + background-color: #383636; + color: #cccccc; +} diff --git a/emulator/index.html b/emulator/index.html index 2912d90..d7d875a 100755 --- a/emulator/index.html +++ b/emulator/index.html @@ -7,12 +7,16 @@ doe - The Device and Screen Orientation Emulator - - + + + + + + @@ -50,10 +54,8 @@
  • a: - - b: - - g: + b: + g:
  • @@ -76,6 +78,35 @@ +
    + + + + +
    + diff --git a/emulator/js/emulator.js b/emulator/js/emulator.js index fe1076c..d3ec429 100644 --- a/emulator/js/emulator.js +++ b/emulator/js/emulator.js @@ -1,6 +1,6 @@ -function startEmulator() { +var selfUrl = new URL( window.location ); - var selfUrl = new URL( window.location ); +function startEmulator() { var controller = document.querySelector( 'iframe#controller' ); var emulatorMenu = document.querySelector( '#emulator' ); @@ -76,7 +76,14 @@ function startEmulator() { var currentRotation = currentScreenOrientation == 0 ? 360 : currentScreenOrientation; - updateScreenOrientation( currentRotation - 90, true ); + // Post message to self to update screen orientation + postMessage( JSON.stringify( { + 'action': 'updateScreenOrientation', + 'data': { + 'totalRotation': currentRotation - 90, + 'updateControls': true + } + } ), selfUrl.origin ); } ); @@ -112,102 +119,29 @@ function startEmulator() { } ); - // Add keyboard shortcuts to switch in-emulator device type - $( document ).on( 'keyup', function( e ) { - switch ( e.keyCode ) { - /*case 49: - $( '[data-device="iphone"]' ).trigger( 'click' ); - break; - case 50: - $( '[data-device="android"]' ).trigger( 'click' ); - break; - case 51: - $( '[data-device="tablet"]' ).trigger( 'click' ); - break; - case 52: - $( '[data-device="ipad"]' ).trigger( 'click' ); - break;*/ - case 32: - case 56: - case 82: - $( '.rotate' ).trigger( 'click' ); - break; - } - } ); - - var currentScreenOrientation = 360; - - function updateScreenOrientation( requestedScreenOrientation, updateControls ) { - - // Calculate rotation difference - var currentRotation = currentScreenOrientation == 0 ? 360 : currentScreenOrientation; - - var rotationDiff = currentRotation - requestedScreenOrientation; - - // Update controller rendering - sendMessage( - controller, { - 'action': 'rotateScreen', - 'data': { - 'rotationDiff': -rotationDiff, - 'totalRotation': requestedScreenOrientation, - 'updateControls': updateControls - } - }, - selfUrl.origin - ); - - // Notify emulated page that screen orientation has changed - sendMessage( - deviceFrame, { - 'action': 'screenOrientationChange', - 'data': 360 - requestedScreenOrientation - }, - selfUrl.origin - ); - - if ( ( ( currentRotation / 90 ) % 2 ) !== ( ( requestedScreenOrientation / 90 ) % 2 ) ) { - - $( 'button[data-rotate=true]' ).each( function() { - width = $( this ).attr( 'data-viewport-width' ); - height = $( this ).attr( 'data-viewport-height' ); - $( this ).attr( 'data-viewport-width', height ); - $( this ).attr( 'data-viewport-height', width ); - if ( $( this ).hasClass( 'active' ) ) { - $( this ).trigger( 'click' ); - } - } ); - - } - - screenOrientationEl.textContent = ( 360 - requestedScreenOrientation ) % 360; - - // Update current screen orientation - currentScreenOrientation = requestedScreenOrientation; - - } - var orientationAlpha = document.querySelector( 'input#orientationAlpha' ); var orientationBeta = document.querySelector( 'input#orientationBeta' ); var orientationGamma = document.querySelector( 'input#orientationGamma' ); + var currentScreenOrientation = 360; + var userIsEditing = false; - function onUserIsEditingStart(e) { + function onUserIsEditingStart( e ) { userIsEditing = true; } - function onUserIsEditingEnd(e) { - var alpha = parseFloat(orientationAlpha.value, 10); - var beta = parseFloat(orientationBeta.value, 10); - var gamma = parseFloat(orientationGamma.value, 10); + function onUserIsEditingEnd( e ) { + var alpha = parseFloat( orientationAlpha.value, 10 ); + var beta = parseFloat( orientationBeta.value, 10 ); + var gamma = parseFloat( orientationGamma.value, 10 ); // Fit all inputs within acceptable interval alpha = alpha % 360; - if(beta < -180) beta = -180; - if(beta > 180) beta = 180; - if(gamma < -90) gamma = -90; - if(gamma > 90) gamma = 90; + if ( beta < -180 ) beta = -180; + if ( beta > 180 ) beta = 180; + if ( gamma < -90 ) gamma = -90; + if ( gamma > 90 ) gamma = 90; sendMessage( controller, { @@ -223,13 +157,13 @@ function startEmulator() { } - function stopUserEditing(e) { + function stopUserEditing( e ) { userIsEditing = false; } - function stopUserEditingKey(e) { + function stopUserEditingKey( e ) { var keyCode = e.which || e.keyCode; - if (keyCode !== 13) { + if ( keyCode !== 13 ) { return true; } // Force blur when return key is pressed @@ -237,81 +171,28 @@ function startEmulator() { target.blur(); } - orientationAlpha.addEventListener('focus', onUserIsEditingStart, false); - orientationAlpha.addEventListener('change', onUserIsEditingEnd, false); - orientationAlpha.addEventListener('keypress', stopUserEditingKey, false); - orientationAlpha.addEventListener('blur', stopUserEditing, false); + orientationAlpha.addEventListener( 'focus', onUserIsEditingStart, false ); + orientationAlpha.addEventListener( 'change', onUserIsEditingEnd, false ); + orientationAlpha.addEventListener( 'keypress', stopUserEditingKey, false ); + orientationAlpha.addEventListener( 'blur', stopUserEditing, false ); - orientationBeta.addEventListener('focus', onUserIsEditingStart, false); - orientationBeta.addEventListener('change', onUserIsEditingEnd, false); - orientationBeta.addEventListener('keypress', stopUserEditingKey, false); - orientationBeta.addEventListener('blur', stopUserEditing, false); + orientationBeta.addEventListener( 'focus', onUserIsEditingStart, false ); + orientationBeta.addEventListener( 'change', onUserIsEditingEnd, false ); + orientationBeta.addEventListener( 'keypress', stopUserEditingKey, false ); + orientationBeta.addEventListener( 'blur', stopUserEditing, false ); - orientationGamma.addEventListener('focus', onUserIsEditingStart, false); - orientationGamma.addEventListener('change', onUserIsEditingEnd, false); - orientationGamma.addEventListener('keypress', stopUserEditingKey, false); - orientationGamma.addEventListener('blur', stopUserEditing, false); + orientationGamma.addEventListener( 'focus', onUserIsEditingStart, false ); + orientationGamma.addEventListener( 'change', onUserIsEditingEnd, false ); + orientationGamma.addEventListener( 'keypress', stopUserEditingKey, false ); + orientationGamma.addEventListener( 'blur', stopUserEditing, false ); var screenOrientationEl = document.querySelector( '#screenOrientation' ); var actions = { - 'connect': function( data ) { - - var urlHash = selfUrl.hash; - - // Tell controller to start rendering - sendMessage( - controller, { - 'action': 'start' - }, - selfUrl.origin - ); - - // If any deviceorientation URL params are provided, send them to the controller - if ( urlHash.length > 6 ) { - var coords = urlHash.substring( 1 ); - try { - var coordsObj = JSON.parse( coords ); - - if ( ( coordsObj.length === 3 || coordsObj.length === 4 ) && ( coordsObj[ 0 ] || coordsObj[ 1 ] || coordsObj[ 2 ] ) ) { - - sendMessage( - controller, { - 'action': 'setCoords', - 'data': { - 'alpha': coordsObj[ 0 ] || 0, - 'beta': coordsObj[ 1 ] || 0, - 'gamma': coordsObj[ 2 ] || 0 - } - }, - selfUrl.origin - ); - - // Use 4th parameter to set the screen orientation - if ( coordsObj[ 3 ] ) { - var requestedScreenOrientation = coordsObj[ 3 ] * 1; - if ( requestedScreenOrientation / 90 > 0 && requestedScreenOrientation / 90 < 4 ) { - - if ( deviceFrame.contentWindow.screenFrame.isLoaded ) { - updateScreenOrientation( 360 - requestedScreenOrientation, false ); - } else { - deviceFrame.contentWindow.screenFrame.addEventListener( 'load', function() { - updateScreenOrientation( 360 - requestedScreenOrientation, false ); - }, false ); - } - - } - } - - } - } catch ( e ) {} - } - - }, 'newData': function( data ) { // Print deviceorientation data values in GUI - if (!userIsEditing) { + if ( !userIsEditing ) { orientationAlpha.value = printDataValue( data.alpha ); orientationBeta.value = printDataValue( data.beta ); orientationGamma.value = printDataValue( data.gamma ); @@ -337,24 +218,68 @@ function startEmulator() { deviceFrame.style.webkitTransform = deviceFrame.style.msTransform = deviceFrame.style.transform = 'rotate(' + ( roll - currentScreenOrientation ) + 'deg) scale(' + scaleFactor + ')'; }, - 'updatePosition': function( data ) { + 'updateScreenOrientation': function( data ) { - window.setTimeout( function() { - var hashData = [ - parseFloat( orientationAlpha.value, 10 ), - parseFloat( orientationBeta.value, 10 ), - parseFloat( orientationGamma.value, 10 ), ( 360 - currentScreenOrientation ) % 360 - ].join( ',' ); + var requestedScreenOrientation = data.totalRotation % 360; + var updateControls = data.updateControls; - selfUrl.hash = '#[' + hashData + ']'; + // Calculate rotation difference + var currentRotation = currentScreenOrientation == 0 ? 360 : currentScreenOrientation; - replaceURL( selfUrl ); - }, 100 ); + var rotationDiff = currentRotation - requestedScreenOrientation; + + // Update controller rendering + sendMessage( + controller, { + 'action': 'rotateScreen', + 'data': { + 'rotationDiff': -rotationDiff, + 'totalRotation': requestedScreenOrientation, + 'updateControls': updateControls + } + }, + selfUrl.origin + ); + + // Notify emulated page that screen orientation has changed + sendMessage( + deviceFrame, { + 'action': 'screenOrientationChange', + 'data': 360 - requestedScreenOrientation + }, + selfUrl.origin + ); + + if ( ( ( currentRotation / 90 ) % 2 ) !== ( ( requestedScreenOrientation / 90 ) % 2 ) ) { + + $( 'button[data-rotate=true]' ).each( function() { + width = $( this ).attr( 'data-viewport-width' ); + height = $( this ).attr( 'data-viewport-height' ); + $( this ).attr( 'data-viewport-width', height ); + $( this ).attr( 'data-viewport-height', width ); + if ( $( this ).hasClass( 'active' ) ) { + $( this ).trigger( 'click' ); + } + } ); + + } + + screenOrientationEl.textContent = ( 360 - requestedScreenOrientation ) % 360; + + // Update current screen orientation + currentScreenOrientation = requestedScreenOrientation; }, 'lockScreenOrientation': function( data ) { - updateScreenOrientation( ( 360 - data ) % 360, true ); + // Post message to self to update screen orientation + postMessage( JSON.stringify( { + 'action': 'updateScreenOrientation', + 'data': { + 'totalRotation': 360 - data, + 'updateControls': true + } + } ), selfUrl.origin ); $( 'button.rotate' ).prop( "disabled", true ).attr( "title", "Screen Rotation is locked by page" ); $( 'i', 'button.rotate' ).addClass( 'icon-lock' ).removeClass( 'icon-rotate-left' ); diff --git a/emulator/js/timeliner.js b/emulator/js/timeliner.js new file mode 100644 index 0000000..4aade22 --- /dev/null +++ b/emulator/js/timeliner.js @@ -0,0 +1,379 @@ +function EmulatorTimeline( alpha, beta, gamma, screen ) { + + var _frames = []; + + this.import = function( frames ) { + _frames = frames; + }; + + this.getAll = function( index ) { + return _frames; + }; + + this.get = function( index ) { + return _frames[ index ]; + }; + + this.set = function( obj, duration, offset, index ) { + + var frame = { + 'type': 0, // FIX TO POSITION + 'duration': duration || 1, + 'offset': offset || 0, + 'data': obj || {} + }; + + if ( index !== undefined && index !== null ) { + // Update an existing animation frame + _frames[ index ] = frame; + } else { + // Append a new animation frame + _frames.push( frame ); + } + + return this; + + }; + + this.length = function() { + return _frames.length; + }; + + this.animate = function( obj, duration, offset, index ) { + + var frame = { + 'type': 1, // ANIMATE TO POSITION + 'duration': duration || 1, + 'offset': offset || 0, + 'data': obj || {} + }; + + if ( index !== undefined && index !== null ) { + // Update an existing animation frame + _frames[ index ] = frame; + } else { + // Append a new animation frame + _frames.push( frame ); + } + + return this; + + }; + + this.start = function() { + + sendMessage( + controller, { + 'action': 'playback', + 'data': { + 'frames': _frames + } + }, + selfUrl.origin + ); + + return this; + + }; + + // Set initial device orientation frame data + var data = { + 'alpha': alpha || 0, + 'beta': beta || 0, + 'gamma': gamma || 0, + 'screen': screen || 0 + }; + + this.set( data, 1, 0 ); + +}; + +EmulatorTimeline.prototype = { + + 'constructor': EmulatorTimeline, + + // Static method + 'compress': function( framesOriginal ) { + var framesCompressed = []; + + for ( var i = 0, l = framesOriginal.length; i < l; i++ ) { + framesCompressed.push( [ + framesOriginal[ i ].type, + framesOriginal[ i ].duration, + framesOriginal[ i ].offset, + framesOriginal[ i ].data.alpha, + framesOriginal[ i ].data.beta, + framesOriginal[ i ].data.gamma, + framesOriginal[ i ].data.screen + ] ); + } + + return framesCompressed; + }, + + 'uncompress': function( framesCompressed ) { + var framesOriginal = []; + + for ( var i = 0, l = framesCompressed.length; i < l; i++ ) { + var data = { + 'type': framesCompressed[ i ][ 0 ], + 'duration': framesCompressed[ i ][ 1 ], + 'offset': framesCompressed[ i ][ 2 ], + 'data': { + 'alpha': framesCompressed[ i ][ 3 ], + 'beta': framesCompressed[ i ][ 4 ], + 'gamma': framesCompressed[ i ][ 5 ], + 'screen': framesCompressed[ i ][ 6 ] + } + }; + + framesOriginal.push( data ); + } + + return framesOriginal; + } + +}; + +// Create a new emulator timeline +var timeline = new EmulatorTimeline( 0, 0, 0, 0 ); + +var activeFrameIndex = 0; +var _lastData = {}; + +var actions = { + 'connect': function( data ) { + + $( 'button#play' ).on( 'click', function() { + $( 'button[data-frame-number=0]' ).removeClass( 'charcoal' ).addClass( 'asphalt active' ); + + // Disable play button for duration of animation + $( 'button#play' ).attr( 'disabled', 'disabled' ); + + $( 'button[data-frame-number=0]' ).trigger( 'click' ); + + timeline.start(); + } ); + + $( 'body' ).on( 'click', 'button[data-frame-number]', function() { + + activeFrameIndex = $( this ).attr( 'data-frame-number' ); + + var activeFrame = timeline.get( activeFrameIndex ); + + sendMessage( + controller, { + 'action': 'setCoords', + 'data': activeFrame.data + }, + selfUrl.origin + ); + + // Post message to self to update screen orientation + postMessage( JSON.stringify( { + 'action': 'updateScreenOrientation', + 'data': { + 'totalRotation': ( 360 - activeFrame.data.screen ) % 360, + 'updateControls': false + } + } ), selfUrl.origin ); + + $( 'button.active[data-frame-number]' ).removeClass( 'asphalt active' ).addClass( 'charcoal' ); + $( 'button[data-frame-number=' + activeFrameIndex + ']' ).removeClass( 'charcoal' ).addClass( 'asphalt active' ); + + } ); + + $( 'button#clearTimeline' ).on( 'click', function() { + + // Trash all frame buttons except the first one + $( 'button[data-frame-number]' ).not( ':first' ).remove(); + + var startFrame = timeline.get( 0 ); + // Reset timeline + timeline.import( [ startFrame ] ); + + // Focus the first frame + $( 'button[data-frame-number=0]' ).trigger( 'click' ); + + } ); + + $( 'button#addNewFrame' ).on( 'click', function() { + + var newFrameId = timeline.length(); + + if ( timeline.get( newFrameId ) == undefined ) { + + // Use last known device orientation values to initialize new animation frame + var data = { + alpha: printDataValue( _lastData.alpha ), + beta: printDataValue( _lastData.beta ), + gamma: printDataValue( _lastData.gamma ), + screen: _lastData.screen + }; + + timeline.animate( data, 2, 0 ); + + } + + // Create and add a new frame button to GUI + $( '#frame-group' ).append( + $( '
  • ' ).append( + $( '' ) + .attr( 'data-frame-number', newFrameId ) + .text( newFrameId + 1 ) + ) + ); + + // Highlight new frame + $( 'button[data-frame-number=' + newFrameId + ']' ).trigger( 'click' ); + + // Don't allow more than 20 frames + if ( newFrameId >= 19 ) { + + $( 'button#addNewFrame' ).attr( 'disabled', 'disabled' ); + + } + + } ); + + // Tell controller to start rendering + sendMessage( + controller, { + 'action': 'start' + }, + selfUrl.origin + ); + + var urlHash = selfUrl.hash; + + // Parse provided JSON animation hash object (if any) + if ( urlHash && urlHash.length > 3 ) { + // Remove leading '#' + var jsonBase64 = urlHash.substring( 1 ); + + // Base64 decode this data + var jsonStr = window.atob( jsonBase64 ); + + try { + var json = JSON.parse( "{\"d\": " + jsonStr + " }" ); + + // 'Unzip' the data + var frames = EmulatorTimeline.prototype.uncompress( json.d ); + + if ( frames && frames.length > 0 ) { + + // Create the correct number of animation frame buttons + for ( var i = 1; i < frames.length; i++ ) { + $( 'button#addNewFrame' ).trigger( 'click' ); + } + + // Import json as the emulator timeline + timeline.import( frames ); + + // Focus the first frame + $( 'button[data-frame-number=0]' ).trigger( 'click' ); + + // Update onscreen coords + sendMessage( + controller, { + 'action': 'setCoords', + 'data': { + 'alpha': frames[ 0 ].data.alpha || 0, + 'beta': frames[ 0 ].data.beta || 0, + 'gamma': frames[ 0 ].data.gamma || 0 + } + }, + selfUrl.origin + ); + + // Post message to self to then update controller screen orientation + postMessage( JSON.stringify( { + 'action': 'updateScreenOrientation', + 'data': { + 'totalRotation': ( 360 - frames[ 0 ].data.screen ) % 360, + 'updateControls': false + } + } ), selfUrl.origin ); + } + } catch ( e ) { + console.log( e ); + } + + } + + $( '#timeline' ).css( { + 'display': 'block' + } ); + + }, + 'newData': function( data ) { + var _data = { + alpha: printDataValue( data.alpha ), + beta: printDataValue( data.beta ), + gamma: printDataValue( data.gamma ), + screen: data.screen + } + + if ( _data.alpha == _lastData.alpha && _data.beta == _lastData.beta && _data.gamma == _lastData.gamma && _data.screen == _lastData.screen ) return; + + // If this data applies to the first frame or a screen orientation change + // is being observed then use .set instead of .animate! + if ( activeFrameIndex === 0 || _data.screen !== _lastData.screen ) { + timeline.set( _data, _data.screen !== _lastData.screen ? 2 : 1, 0, activeFrameIndex ); + } else { + timeline.animate( _data, 2, 0, activeFrameIndex ); + } + + _lastData = _data; // store for next loop + }, + 'updatePosition': function( data ) { + window.setTimeout( function() { + // 'Zip' the current timeline data + var framesCompressed = EmulatorTimeline.prototype.compress( timeline.getAll() ); + + // Stringify the data object + var jsonStr = JSON.stringify( framesCompressed ); + + // Base64 encode the data object + var jsonBase64 = window.btoa( jsonStr ); + + // Replace current URL hash with compressed, stringified, encoded data! + selfUrl.hash = '#' + jsonBase64; + replaceURL( selfUrl ); + }, 150 ); + }, + 'updateActiveFrame': function( data ) { + + $( 'button[data-frame-number=' + ( data || 0 ) + ']' ).trigger( 'click' ); + + // Rotate screen orientation to correct value + postMessage( JSON.stringify( { + 'action': 'updateScreenOrientation', + 'data': { + 'totalRotation': ( 360 - timeline.get( data ).data.screen ) % 360, + 'updateControls': false + } + } ), selfUrl.origin ); + + }, + 'resetTimeline': function( data ) { + + $( 'button[data-frame-number=0]' ).trigger( 'click' ); + + // Re-enable play button + $( 'button#play' ).removeAttr( 'disabled' ); + + } +}; + +window.addEventListener( 'message', function( event ) { + + if ( event.origin != selfUrl.origin ) return; + + var json = JSON.parse( event.data ); + + if ( !json.action || !actions[ json.action ] ) return; + + actions[ json.action ]( json.data ); + +}, false );