Homepage Animation Explanation
Given that adding comments directly to the javascript file used by the animation can only increase page load unnecessarily I found it better to comment on the code here.
Keep in mind that the contents below are written on the assumption that you have seen moonsong.js and want to know how it works, otherwise there is little point in reading any further.
As for why I have used javascript instead of css animations, the simplest answer is that I wanted to avoid sub-pixel rendering. An animated gif was off the cards as well because it would be slow to load and animated gifs have dumb framerate rounding issues.
I have written this animation script in the most performant way I can think of. It took a lot of work to get this running consistantly smooth everywhere. I have also written it for compatibility and to make full use of requestAnimationFrame for the browsers that support it. The problem with this is that none of this lends itself towards being readable.
Preparation
if(document.getElementById) { var moonsong=document.getElementById('moonsong'); if(typeof moonsong.style.overflow !== 'undefined') { if(typeof moonsong.style.left !== 'undefined') { ... } } }
This does some basic checks to ensure that the script cannot proceed unless basic support is met.
var dragon=document.getElementById('dragonl'); var cloud1=document.getElementById('cloud1l'); var cloud2=document.getElementById('cloud2l'); var cloud3=document.getElementById('cloud3l'); var cloud4=document.getElementById('cloud4l'); var dragonwing=document.getElementById('dragonwing');
Now we grab elements representing the dragon, the four cloud sections, and the dragon's wing in that order. By storing these into variables I can ensure that the browser doesn't have to search for the element on every frame of the animation.
var scrollprogress1 = 0; var scrollprogress2 = 0; var scrollprogress3 = 0; var scrollprogress4 = 0; var wingstage = 0; var scrolldragon=0; var realprog=13; var wingruns=0; var runs=0;
Now we create some variables for tracking the animation. "scrollprogress1" to "scrollprogress4" represents the horizontal scrolling position of all 4 cloud sections. "scrolldragon" represents the current frame of the dragon's up and down cycle, while "realprog" represents the vertical position of the dragon and "wingstage" represents the state of the dragon's wings. "runs" represents a cycle of frames for any animations that don't occur on a per frame basis (dragon animations), while "wingruns" allows us to halve the flapping rate. It's complicated... I'll explain it in further depth in the "animation frame" section.
var noticab=true; var cantr=false; if(document.all) { if(navigator.appName == 'Microsoft Internet Explorer') { if(navigator.platform == 'MacPPC') { noticab=false; moonsong.innerHTML='<img src="/images/moonsong561.gif">'; } } } if(noticab) { if(typeof navigator.vendor !== 'undefined') { if(navigator.vendor=='iCab') { if(parseInt(navigator.vendorSub+'')==3) { noticab=false; } } } if(typeof CSS !== 'undefined') { if(typeof CSS.supports !== 'undefined') { cantr=CSS.supports('transform','translateX(1px)'); } } ... }
Checks for IE5/Mac and iCab 3 to prevent rendering issues in those browsers. Also check if we can use css transforms.
if(noticab) { cloud1.style.width='960px'; cloud2.style.width='960px'; cloud3.style.width='960px'; cloud4.style.width='960px'; }
This allows the static fallback to not overflow on browsers that don't support overflow:hidden.
Timers
This determines when each frame of the animation should fire. The complexity of this is entirely for the sake of performance. But to put things simply, the part inside of if(cont) { ... } represents each animation frame and that code is meant to run around about every 0.046 seconds
var animeth=0; var datemeth=0; var datestart=0; var chkts=false; if(Date.now) { datemeth=1; } if(datemeth) { datestart=Date.now(); } else { datestart=(new Date()).getTime(); } if(window.requestAnimationFrame) { animeth=3; } else if(window.webkitRequestAnimationFrame) { animeth=4; } else if(window.mozRequestAnimationFrame) { animeth=5; } else if(window.oRequestAnimationFrame) { animeth=6; } else if(window.msRequestAnimationFrame) { animeth=7; }
"animeth" determines which timing function should be used for calling "scrollClouds" by checking what is supported by the browser, while "datemeth"does the same for timestamps. "chkts" comes into play during the first run of "scrollClouds". "datestart" is used for timestamp comparisons during requestAnimationFrame calls. A value of 0 for "animeth" represents setTimeout, while 1 represents requestAnimationFrame and 2 to 5 represent the prefixed variants of requestAnimationFrame. A value of 0 for "datemeth" uses (new Date()).getTime() to get the current timestamp, a value of 1 uses the more performant Date.now(), and finally a value of 2 uses the timestamp passed by requestAnimationFrame, although that isn't set here.
window.onload = function(){ switch(animeth) { case 3: window.requestAnimationFrame(scrollClouds); break; case 4: window.webkitRequestAnimationFrame(scrollClouds); break; case 5: window.mozRequestAnimationFrame(scrollClouds); break; case 6: window.oRequestAnimationFrame(scrollClouds); break; case 7: window.msRequestAnimationFrame(scrollClouds); break; default: window.setTimeout(sTTestA, 46); window.setTimeout('sTTestB()', 100); } };
This calls the initial run of "scrollClouds". setTimeout has the milliseconds per frame within the call, while requestAnimationFrame needs to handle that within "scrollClouds". The switch statement is used for performance reasons.
function sTTestA() { if(animeth<1) { animeth=1; scrollClouds(); } } function sTTestB() { if(animeth<1) { animeth=2; scrollClouds(); } }
This just checks against two different syntaxes for setTimeout and sees which one fires first if any.
function scrollClouds(e) { var datenow=0; var cont=1; if(animeth) { ... } if(cont) { ... } switch(animeth) { case 2: window.setTimeout('scrollClouds()', 46); break; case 3: window.requestAnimationFrame(scrollClouds); break; case 4: window.webkitRequestAnimationFrame(scrollClouds); break; case 5: window.mozRequestAnimationFrame(scrollClouds); break; case 6: window.oRequestAnimationFrame(scrollClouds); break; case 7: window.msRequestAnimationFrame(scrollClouds); break; default: window.setTimeout(scrollClouds, 46); } }
This function calls itself repeatedly to create a loop. "cont" determines if this call to scrollClouds represents a frame of the animation, this is always set to 1 (true) by default for setTimeout, but is only conditionally true for requestAnimationFrame, because unlike with setTimeout you will find that requestAnimationFrame needs to handle milliseconds per frame inside "scrollClouds" rather than within the timer call. "e" is the timestamp sent by requestAnimationFrame calls. The contents of "if(animeth)" (read as "if(animeth!=0)") handle the value of "cont" for requestAnimationFrame calls, while the contents of "if(cont)" handle each frame of the animation.
if(!chkts) { if(e) { if(typeof e == "number") { datemeth=2; datestart=e; } } chkts=true; }
During the first run of "scrollClouds" we will check if the timestamp is sent with requestAnimationFrame calls and if so we will set "datestart" to this value and will set "datemeth" to 2 so that we know to get our timestamps from the passed value from now on. This section of the code only needs to run once. The reason for this is that the value passed by parameter is a high resolution timestamp these days (although it wasn't when requestAnimationFrame was first implemented by browsers) and the javascript date functions are not, and so we can't rely on any other value for the initial timestamp but the value sent on first run.
if(datemeth==2) { datenow=e; } else if(datemeth==1) { datenow=Date.now(); } else { datenow=(new Date()).getTime(); } if(datenow-datestart>=46) { datestart=datenow; } else { cont=0; }
For requestAnimationFrame calls this compares the last timestamp recording in "datestart" to the current timestamp in "datenow" to know when the next frame of the animation should fire (via "cont"). If the animation frame will fire then the value of "datenow" will be saved to "datestart" in order to time the next frame.
switch(animeth) { case 1: window.requestAnimationFrame(scrollClouds); break; case 2: window.webkitRequestAnimationFrame(scrollClouds); break; case 3: window.mozRequestAnimationFrame(scrollClouds); break; case 4: window.oRequestAnimationFrame(scrollClouds); break; case 5: window.msRequestAnimationFrame(scrollClouds); break; default: window.setTimeout(scrollClouds, 46); }
At the end of the function the timer is called again.
Animation Frames
var sp1=scrollprogress1; var sp2=scrollprogress2; var sp3=scrollprogress3; var sp4=scrollprogress4; if(sp1>-304) {sp1-=16;} else {sp1=0;} if(sp2>-312) {sp2-=8;} else {sp2=0;} if(sp3>-316) {sp3-=4;} else {sp3=0;} if(sp4>-318) {sp4-=2;} else {sp4=0;} if(noticab) { if(cantr) { cloud1.style.transform='translateX('+sp1+'px)'; cloud2.style.transform='translateX('+sp2+'px)'; cloud3.style.transform='translateX('+sp3+'px)'; cloud4.style.transform='translateX('+sp4+'px)'; } else { cloud1.style.left=sp1+'px'; cloud2.style.left=sp2+'px'; cloud3.style.left=sp3+'px'; cloud4.style.left=sp4+'px'; } } else { cloud1.style.backgroundPosition=sp1+'px 0'; cloud2.style.backgroundPosition=sp2+'px 0'; cloud3.style.backgroundPosition=sp3+'px 0'; cloud4.style.backgroundPosition=sp4+'px 0'; } if(runs<3) { ... } else { ... } scrollprogress1=sp1; scrollprogress2=sp2; scrollprogress3=sp3; scrollprogress4=sp4;
The horizontal position of each cloud segment is first stored in a local variable (for performance reasons, because local variable access is faster), altered to reflect the movement for that frame, applied directly to the elements in question, and then at the end they are stored back into their global counterparts.
if(runs<3) { if(!wingruns) { if(wingstage) { dragonwing.style.backgroundPosition='0 0'; wingstage=0; } else { dragonwing.style.backgroundPosition='6px 0'; wingstage=1; } wingruns=1; } else { wingruns=0; } runs++; } else { ... runs=0; }
This is where things get complex. Every 1 of 4 frames (variable named "runs") the dragon will move, but every second frame (variable named "wingruns") of the remaining 3 out of 4 frames ("runs" again) will move the wing. The "wingstage" variable simply tracks if the wing is up or down.
Frame | Action |
---|---|
0 | None |
1 | Wing |
2 | None |
3 | Dragon |
4 | Wing |
5 | None |
6 | Wing |
7 | Dragon |
It is written this way so that the action of moving the dragon and moving the Dragon's wings can never happen at the same time. This ensures that no more than 5 elements will be animated at one time. It's a performance thing, just run with it.
The wing animation is just a matter of applying the image of the up position wing image over the down position wing image, and then hiding it in order to show the down position wing image underneath it. This is done by moving the down position wing image outside of the background area of the wing element.
dragon.style.top=realprog+'px'; if(scrolldragon>12) { if(scrolldragon<25) { scrolldragon++; realprog++; } else { scrolldragon=0; realprog=13; } } else { scrolldragon++; realprog--; }
Next up is the dragon's vertical position.
The variable "realprog" represents the vertical position of the dragon. This value has to move up and then down in a cycle. The current stage of this cycle and the overall direction is determined by "scrolldragon", which is used to determine the next value for "realprog". The value of "realprog" is applied to the dragon element as its vertical position (the css "top" property).
scrolldragon | realprog |
---|---|
0 | 13 |
1 | 12 |
2 | 11 |
3 | 10 |
4 | 9 |
5 | 8 |
6 | 7 |
7 | 6 |
8 | 5 |
9 | 4 |
10 | 3 |
11 | 2 |
12 | 1 |
13 | 0 |
14 | 1 |
15 | 2 |
16 | 3 |
17 | 4 |
18 | 5 |
19 | 6 |
20 | 7 |
21 | 8 |
22 | 9 |
23 | 10 |
24 | 11 |
25 | 12 |