Tuesday, October 15, 2013

Handling Window Re-sizing in the Browser

It's time to do a little house-keeping maintenance on our game template.  I know, that doesn't sound as exciting as player movement and dynamic lighting, but it is just as important.  What we are looking to do in this post is learn how to handle when the user suddenly changes the browser window size.  This happens quite frequently, so we need to have code in place to quickly re-size our game graphics too.  Otherwise, our viewport will get stretched out of proportion or it might get cut off so the user cannot see the whole picture.  Let's take a look at how to keep the browser view centered and in proportion.

  Every webpage on the internet has a global 'window' object that contains the entire webpage.  The 'window' is like the trunk of a tree and our game is somewhere out on the branches.  Everything on our webpage is connected back to the 'window' object.  The window object is created for us when we open a browser, and it comes with some helpful functions.  One of these is called 'addEventListener()'.  This helper function listens for events such as mouse clicks, window re-sizing, etc..  When it detects that one of these user events has happened, it performs a function of your choosing.  This way, your webpage will become interactive and responsive to the user's actions.  

Here's how to create an eventListener on the window object of our webpage:  
window.addEventListener('resize', onWindowResize, false); 

We type 'window' which refers to the browser window object, then a period ( . ) which is about to call a function on that object, then 'addEventListener();'  You'll notice that this function takes 3 parameters inside its parentheses ( ). The first, 'resize', is telling the browser what we want to listen for - in this case, a browser window re-size by the user. The second parameter, 'onWindowResize' , is the name of our function that will be executed if a 'resize' is detected.  Don't worry about the third parameter which is set to 'false'.  This is an optional parameter that we can leave out, but the W3C advises to put it in anyway to be compatible with older browsers.  

We will define our 'onWindowResize' function now. When the browser detects that a re-size has happened, it will look for a function called 'onWindowResize' and execute whatever code is inside that function.  So at the bottom of our program, we will add the definition of this 'onWindowResize' function.  Here's how the definition looks: 
function onWindowResize(){
   camera.aspect = window.innerWidth / window.innerHeight;
   camera.updateProjectionMatrix();
   renderer.setSize( window.innerWidth, window.innerHeight );   
}

Think of the above code snippet as a 'design' for a function.  This code does nothing on its own - it only provides the instructions of what to do if it is called.  You can also tell that this is just a design by looking at the final curly brace ( } ) by itself on the very last line.  Notice it does not have a semi-colon ( ; ) after it.  So it's not really an action statement that just happens on its own.  This function has to be called by something else.  And that something else is our 'addEventListener' that we just created inside the window object.  When addEventListener detects a browser re-size, it will find this 'onWindowResize' function and execute all the code inside of its curly braces.  
Let's take a look at each line of code inside this function.  The first line accesses the 'aspect' property of the 'camera' that we created at the top of the program.  Remember 'aspect' means the ratio of width to height of your computer/device screen.  This first line fills the 'aspect' property with the new width and height of the browser window, now that the user has suddenly changed them.  
The next line calls a function on our camera object called 'updateProjectionMatrix()' .  Without getting too technical, this helper function recalculates the camera's viewing frustum (a sort of pyramid with the pyramid top at your eye location, the sides extending outward from you, and the pyramid base located back in the distance where you are looking).
The final line should look familiar.  It is an exact copy of our initial renderer set-up code.  This call just refreshes everything so that the final image is correct.

And that's it!  Now that we have everything we need to handle re-sizing, let's add this code back into our game template.  Here's how the final tempGame.js file looks now.  Copy and Paste the following code and then save it as 'tempGame.js' , overwriting the old one:   

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false); 

var cubeGeometry = new THREE.CubeGeometry(20,20,20);
var cubeMaterial = new THREE.MeshLambertMaterial({ color: 'rgb(0,255,0)' });
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);

var sphereGeometry = new THREE.SphereGeometry(5);
var sphereMaterial = new THREE.MeshBasicMaterial({ color: 'rgb(255,255,0)' });
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);

var light = new THREE.PointLight( 'rgb(255,255,255)', 1, 0 );
scene.add(light);

var sunRiseFlag = true;
var sunHeight = 0;

camera.position.y = 40;
camera.position.z = 160;

cube.rotation.x = 0.4;
cube.rotation.y = 0.6;

animate();

function animate(){
   
   requestAnimationFrame(animate);

   if(sunRiseFlag == true){
      sunHeight = sunHeight + 1;
   }

   if(sunRiseFlag == false){
      sunHeight = sunHeight - 1;
   }

   if(sunHeight > 150){
      sunHeight = 150;
      sunRiseFlag = false;
   }

   if(sunHeight < 0){
      sunHeight = 0;
      sunRiseFlag = true;
   }

   light.position.set(50,sunHeight,50);
   sphere.position = light.position;

   renderer.render( scene, camera );

}


function onWindowResize(){
   camera.aspect = window.innerWidth / window.innerHeight;
   camera.updateProjectionMatrix();
   renderer.setSize( window.innerWidth, window.innerHeight );   
}

Once you have saved it, go back to your example01.html file and open with your Chrome browser.  Now while the Sun is moving up and down, try to drag the window edges of your browser around.  Try minimizing, maximizing, and squashing the dimensions of the browser window.  No matter what you do now, our code will efficiently handle these window 're-sizes' and correctly display our game graphics.  Pretty handy!

Next up... Timing on different users' machines with the Three.js Clock.