tag:blogger.com,1999:blog-75806373406389690872023-07-17T22:01:02.007-07:00Another Reason for JavaScriptMaking note of the sometimes trivial, sometimes trite thoughts on all of our favorite interpreted language.Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.comBlogger6125tag:blogger.com,1999:blog-7580637340638969087.post-85857020146047435032009-02-10T09:30:00.001-08:002009-02-10T09:35:53.985-08:00Recession Blues...So I'm back for more blogging after almost a year of silence. The company I had been working for was a victim of the recession, so I'm out on the street selling apples. <br /><br />On the upside, while they were not amenable to me blogging about coding, they're not here to push me around anymore, so I'm back. <br /><br />Since then I've learned quite a bit about both JavaScript as a language and the frameworks that have matured around it. As a result, I will be positing some of that wisdom here - until some new employer decides that they want to keep what I know a secret...<br /><br />Til then, let the knowledge flow!Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0tag:blogger.com,1999:blog-7580637340638969087.post-9083609198963069652008-02-25T10:01:00.000-08:002008-02-26T20:40:36.356-08:00JavaScript Animation - Part 3 - The Code So Far<p>In the last <a href="http://blog.anotherreason.com/2008/02/javascript-animation-part-1-simple.html">JavaScript Animation post</a>, we left off with code for the AnimationManager class. So far we can do is call addMovement, specifying a direction, the total number of pixels to move, and the number of pixels to move during each "frame". Then, when startAnimation is called, the manager will automatically daisy chain calls to the Window object's setInterval method. When the element specified in each movement instruction has reached it's destination, the next movement instruction is executed. Not bad for a start. But this doesn't yet allow us to do two rather useful things. <br /></p><ul><br /> <li>Move in two directions at once, i.e., diagonally or along a curve.</li><br /> <li>Move two elements at once.</li><br /></ul><br />But before we add new functionality, let's go over what we have so far.<br /><p></p><br /><p><br />A quick note before we begin. I prefer to use the object literal syntax for class declaration. In case you aren't familiar with it, all it means is that instead of adding member variables and functions to the hidden <code>prototype</code> object one at a time...<br /></p><pre>Foo.prototype.someMethod = function(){ ... };<br />Foo.prototype.some_int = 1;</pre><br />I add them all at once...<br /><pre>Foo.prototype = {<br /> some_int : 1,<br /> someMethod : function(){ ... }<br />}</pre><br />Not that different, but it is easier to read, is a bit more compact (fewer characters=smaller files), and <b>much</b> easier to type since you don't have to constantly be writing out <code>prototype</code>. Now there is a downside: since most of the JavaScript IDE plugins I've seen are not able to parse class structures when written this way, so they can't help in outlining the class for quick navigation. For example, the "Outline" view in Eclipse becomes useless, so you might want to stick with the more verbose style if that will slow you down too much.<br /><p></p><br /><p><br />The <code>AnimationManager</code> class begins with a few member variable declarations.<br /><br /></p><pre>function AnimationManager(){<br /> this.initialize();<br />}<br />AnimationManager.prototype = {<br /><br />movements : undefined,<br />intervals : undefined,<br />current_movement_index : undefined,</pre><br /><br />The first two are arrays that will hold the movements we ask the manager to perform and the interval ids returned by the various calls to the Window object's <code>setInterval</code> method They are not initialized when declared, because though the AnimationManager class will most often be instantiated as a <a href="http://en.wikipedia.org/wiki/Singleton_pattern" target="_blank">Singleton</a>, you may want more than one at some point and we don't want them all sharing the same array. (I will talk more about this later). So instead they are initialized<br />in a method called, you guessed it, initialize.<br /><p></p><br /><br /><pre>initialize : function(){<br /><br /> this.movements = new Array();<br /> this.intervals = new Array();<br />},</pre><br /><p><br />To those of you familiar with the <a target="_blank" href="http://prototypejs.org/">Prototype JavaScript library</a>, the initialize method is essentially a replacement for the classes constructor method. So when you call<br /><pre>Class.create({ // class definition using object literal syntax })</pre><br />the <code>initialize()</code> method will be called as soon as the class is instantiated. <br />But since I am not using Prototype here, we will accomplish more or less the same thing by calling <code>initialize()</code><br />in the contructor.<br /></p><br /><p><br />So on to the method that tells <code>AnimationManager</code> what to do. Since there are no private methods in JavaScript, we can't hide the rest of the class, but this is really the only method aside from <code>startAnimation</code> and <code>stopAnimation</code> that another class or script would ever have to access. (At least until we start adding functionality later.) The <code>addMovement</code> method takes four arguments,<br /></p><ul><br /><li><code>element</code> - the element we want to move around,</li><br /><li><code>direction</code> - the direction (up, down, left, right) we want <code>element</code> to move,</li><br /><li><code>pixel_count</code> - how many pixels to move it,</li><br /><li><code>increment</code> - how many pixels to move it from frame to frame of the animation.<br /></li></ul><br />The first three are self-explanatory, but increment might be a little confusing if you still haven't grasped the whole "frame" thing.More on that in a bit. For now, all we need to know is that these four values are put together into an anonymous object and stored (in order) along with any other instructions the manager may receive. But what about the other two variables being stored in the object - <code>start_x</code> and <code>start_y</code>? What are they for? We'll get to that in a moment. <br /><p></p><br /><pre>addMovement : function( element, direction, pixel_count, increment ){<br /><br />if( element ){<br /> <br /> this.movements.push( {<br /><br /> element : element,<br /> start_x : 0,<br /> start_y : 0,<br /> direction : direction,<br /> pixel_count : pixel_count,<br /> increment : increment<br /><br /> });<br /><br />}else{<br /> // element doesn't exist<br /> return false;<br />}<br />},</pre><br /><br /><p><br />Next we have <code>nextMovement</code>, which takes no arguments, but knows a couple of things about its containing class.<br /></p><pre>nextMovement : function(){<br /><br /> if( this.current_movement_index < this.movements.length ){<br /><br /> var movement = this.movements[this.current_movement_index];<br /> movement.start_x= this.findPosX( movement.element );<br /> movement.start_y = this.findPosY( movement.element );<br /> <br /> ... <br /></pre><br /><br />The member variable <code>current_movement_index</code> is simply that, an index of the movement currently being executed. The list that it is associated with is <code>movements</code>, the array we stored all our movments in as they were added by an outside script or class. The first line of <code>nextMovement</code> asks if there are still movements left to be executed. If so, we go on to calculate and store the values for <code>start_x</code> and <code>start_y</code>. These are simply the starting x and y positions (in pixels) for the element we are about to start moving. These are used to check how much progress has been made with each successive call of <code>executeMovement</code>. Again, more about them later. Next we come to one of the more confusing parts of the whole project. It's actually not that complicated once you get the hang of it, but it can present a bit of a conceptual hurdle.<p></p><br /><p><br />When you call a JavaScript function, the variable scope of that function is determined not by its declaration, but by its caller. So even if we create the class <code>Foo</code> and declare and define the method <code>bar()</code> as a member function, we cannot guarantee that, when it is called, a <code>this</code> pointer inside the scope of the method will actually point to an instance of the class <code>Foo</code>. In practice, this really only applies to callbacks. <br />Here is an example.<br /><br /></p><pre>function Foo(){};<br />Foo.prototype = {<br /> class_name : "Foo",<br /> bar : function(){<br /> alert( "bar belongs to " + this.class_name );<br /> }<br />}<br /><br />var foo = new Foo();<br />some_button.onclick = foo.bar;<br /></pre> <br />When <code>some_button</code> is clicked, you will get the following alert message,<br /><pre>bar belongs to undefined</pre><br />Why? Because when <code>some_button</code> is clicked, and it calls <code>Foo</code>'s <code>bar</code> method, <code>bar</code> is executed inside <code>some_button</code>'s scope, and <code>some_button</code> does not have a member variable named <code>class_name</code>. Basically <code>some_button</code> has stolen and co-opted <code>bar()</code>, and made it it's own.<p></p><br /><p><br />But what, you may ask, does this have to do with the <code>AnimationManager</code>? Well let's have a look. First we will write the method in the same way as we did the <code>Foo.bar()</code> example. <br /></p><br /><pre>...<br /> <br /> this.intervals[this.current_movement_index] = window.setInterval( function(){<br /> <br /> if( !this.executeMovement( this, new Array() ) ){<br /> <br /> window.clearInterval( this.intervals[this.current_movement_index] );<br /> this.current_movement_index++;<br /> this.nextMovement( this, new Array() );<br /> }<br /><br /> }, 30 );<br /><br /> return true;<br /> }<br /><br />},</pre><br /><p><br />This looks like a fairly straightforward example of a recursive function call. Each time <code>setInterval</code> is called, we check to see if <code>executeMovement</code> is through with its work, and if it is, we clear the interval and queue up the next movment. But run it and you won't get past the second line. The problem is <code>this</code>. Inside the anonymous function that will be run over and over by <code>setInterval</code>, the <code>this</code> pointer refers to the anonymous function object we created, which has no idea about what <code>executeMovement</code> or <code>intervals</code> might mean. It would simply tell the interpreter that they are <code>undefined</code>. <br /></p><br /><p><br />Now you might suggest pointing <code>setInterval</code> to a member function instead, but this would still result in the same error, only it would happen further up the call stack. In the same way <code>some_button</code> stole the scope of <code>bar()</code> away from <code>Foo</code>, so too would <code>Window.setInterval</code> steal the member function's scope away from <code>AnimationManager</code>. <br /></p><br /><p><br />So how do we deal with <code>this</code>? We need to maintain scope or we'll have to put the entire functionality of <code>AnimationManager</code> inside <code>setInterval</code>. The solution turns out to be an otherwise obscure native function called <code>apply()</code>. <code>apply</code> is a member of the native JavaScript <code>function</code> class, so any <code>function</code> object can use it. It takes two arguments.<br /></p><br /><ul><br /> <li><code>thisArg</code> - the object you want <code>this</code> to resolve to,</li><br /> <li><code>arguments</code> - an array of arguments passed to the function you are calling.<br /></li></ul><br />So going back to the <code>Foo.bar</code> example, if we use <code>apply()</code>, it would look like this.<br /><pre>function Foo(){};<br />Foo.prototype = {<br /> class_name : "Foo",<br /> bar : function(){<br /> alert( "bar belongs to " + this.class_name );<br /> }<br />}<br /><br />var foo = new Foo();<br />some_button.onclick = function(){<br /> Foo.prototype.bar.apply( foo, new Array() );<br />}</pre><br />Now if we click on <code>some_button</code>, the alert will say,<br /><pre>bar belongs to Foo</pre><br /><p><br />So applying (pardon the pun) the same principle to <code>AnimationManager</code>, we can write it like this.<br /></p><pre> ... <br /><br /> <b>var ani_mgr = this;</b><br /> this.intervals[this.current_movement_index] = window.setInterval( function(){<br /> <br /> if( !AnimationManager.prototype.executeMovement.<b>apply</b>( ani_mgr, new Array() ) ){<br /> <br /> window.clearInterval( ani_mgr.intervals[ani_mgr.current_movement_index] );<br /> ani_mgr.current_movement_index++;<br /> AnimationManager.prototype.nextMovement.<b>apply</b>( ani_mgr, new Array() );<br /> }<br /><br /> }, 30 );<br /> }<br /><br />},</pre><br />Now when <code>executeMovement</code> is called, the <code>this</code> pointer refers to the instance of <code>AnimationManager</code> we assigned to the local variable <code>ani_mgr</code>. We solve the same problem inside the anonymous function by substituting <code>ani_mgr</code> for <code>this</code>.<br /></p><br /><p><br />Notice that we set the time between executions of <code>setInterval</code> to 30ms, as per the formula mentioned in the<br /><a target="_blank" href="http://blog.anotherreason.com/2008/02/javascript-animation-part-1-simple_18.html">first post</a> in the series.<br /></p><br /><p><br />Next we have <code>executeMovement</code>. It acts mainly as a router. It determines the direction of the movement being executed, and makes a call to the appropriate member function. If the element is to be moved to the right, it calls - you guessed it - moveRight. This is done using a simple <code>switch-case</code> enclosure, but to make things a bit more readable and yet avoid using cpu intensive string comparisons, I created a series of static member variables that can be used kind of like an Enumeration. The variable declarations are at the end of the class declaration, since static members are not generally declared as a part of the <code>prototype</code> object, and so do not have to be associated with a particular instance of the class.<br /></p><br /><pre>executeMovement : function(){<br /><br />var movement = this.movements[this.current_movement_index];<br />var element = movement.element;<br /><br />if( element ){<br /><br /> switch( movement.direction ){<br /><br /> case AnimationManager.UP:<br /> return this.moveUp( movement );<br /> break;<br /><br /> case AnimationManager.RIGHT:<br /> return this.moveRight( movement );<br /> break;<br /><br /> case AnimationManager.DOWN:<br /> return this.moveDown( movement );<br /> break;<br /><br /> case AnimationManager.LEFT:<br /> return this.moveLeft( movement );<br /> break;<br /><br /> default: return false;<br /> }<br />}else{<br /><br /> console.info( "no element for executeMovement" );<br />}<br /><br /><br />},</pre><br /><p><br />Next up we have the machinery of the <code>AnimationManager</code>.<br /></p><ul><br /> <li><code>moveUp</code> - moves the element up</li><br /> <li><code>moveRight</code> - moves the element right</li><br /> <li><code>moveDown</code> - moves the element down</li><br /> <li><code>moveLeft</code> - moves the element left</li><br /></ul><br /><pre><br /><br /><br />moveUp : function( movement ){<br /><br />var destination = movement.start_y - movement.pixel_count;<br />var current_y = this.findPosY( movement.element );<br /><br />if( movement.element && destination < current_y ){<br /> movement.element.style.top = ( current_y - movement.increment ) + "px";<br /> return true;<br />}else<br /> return false;<br /><br /><br />},<br /><br />moveRight : function( movement ){<br /><br />var destination = movement.start_x + movement.pixel_count;<br />var current_x = this.findPosX( movement.element );<br /><br />if( movement.element && current_x < destination ){<br /> movement.element.style.left = ( current_x + movement.increment ) + "px";<br /> return true;<br />}else<br /> return false;<br />},<br /><br /><br />moveDown : function( movement ){<br /><br />var destination = movement.start_y + movement.pixel_count;<br />var current_y = this.findPosY( movement.element );<br /><br />if( movement.element && destination > current_y ){<br /> movement.element.style.top = ( current_y + movement.increment ) + "px";<br /> return true;<br />}else<br /> return false;<br />},<br /><br />moveLeft : function( movement ){<br /><br />var destination = movement.start_x - movement.pixel_count;<br />var current_x = this.findPosX( movement.element );<br /><br />if( movement.element && destination < current_x ){<br /> movement.element.style.left = ( current_x - movement.increment ) + "px";<br /> return true;<br />}else<br /> return false;<br /><br />},</pre><br />I ordered them this way because they match the clockwise directional order you see in CSS classes: top, right, bottom, left. Better to be consistent than confused.<br /><p></p><br /><p><br /> All four direction methods function in the same way: check the current position of the element against the place it's supposed to end up. If it hasn't reached it's destination yet move it <code>increment</code> pixels. The destination is determined based on the direction the element is supposed to move. If its moving to the right, <code>executeMovement</code> calls <code>moveRight</code>. Moving to the right means moving in a positive direction along the x-axis, so we can simply add the the <code>pixel_count</code> provided by <code>addMovement</code> to the element's <code>start_x</code>. So,<br /><br /></p><pre>var destination = movement.start_x + movement.pixel_count;</pre><br /><br />To get the <code>current_x</code> position of the element, we use <code>findPosX</code>, which is based on the work of Peter-Paul Koch over at <a href="http://quirksmode.org/" target="_blank">quirksmode.org</a>. It is a cross-browser method for getting the pixel position of any element on the page. It is a little slow for work like this because it has to deal with the quirks inherent to the various browsers, but it works well, so I'm not about to mess with it at the moment. We'll go over the code for it later. <pre>var current_x = this.findPosX( movement.element );</pre><br />Now that <code>moveRight</code> still has work to do, it goes ahead and does it.<br /><pre><br /><br />if( movement.element && current_x < destination ){<br /> movement.element.style.left = ( current_x + movement.increment ) + "px";<br /> return true;<br />}else<br /> return false;<br />},</pre><br />When the moving is done, the method returns <code>true</code>. Back in <code>nextMovement</code>, it tests the return value of the move to determine whether that move should be repeated,<br />or whether the next move should be executed. So returning <code>true</code> means do it again, while returning <code>false</code> means the element has reached it's destination and<br /><code>current_movement_index</code> should be incremented by one before calling <code>nextMovement</code> again.<br /><p></p><br /><br /><p><br />Next we come to the on/off switch for <code>AnimationManager</code>. After adding movements, we have to call <code>startAnimation</code> to get things actually moving. It simply sets<br /><code>current_movement_index</code> to 0 (just in case we've already run through some movements), and gets things started by calling <code>nextMovement</code>. If at some point we want<br /> to stop on a dime, we can call <code>stopAnimation</code>, which will make sure any and all <code>setInterval</code> calls are cleared using <code>clearInterval</code>. <br /></p><pre><br /><br />startAnimation : function(){<br /><br /><br />this.current_movement_index = 0;<br /><br /><br />this.nextMovement();<br />},<br /><br /><br />stopAnimation : function(){<br /><br /> for( var i=0; i<this.intervals.length; i++ ){<br /><br /> window.clearInterval( this.intervals[i] );<br /><br /> }<br /><br />},</pre><br /><p></p><br /><br /><p><br />Finnally, we have the Peter-Paul Koch-based methods for finding the x and y position of an element. Actually what it finds is the <code>left</code> and <code>top</code> positions of an element, but they are effectively the same thing. I won't go into how he does it, because you should have a look at <a href="http://quirksmode.org/">his site</a> anyway. Its an invaluable resource for web developers. Check it out.<br /></p><pre>findPosX : function( element ){<br /><br /> var curleft = 0;<br /><br /> if(element.offsetParent){<br /> while(1){<br /> curleft += element.offsetLeft;<br /> if(!element.offsetParent)<br /> break;<br /> element = element.offsetParent;<br /> }<br /> }else if(element.x){<br /> curleft += element.x;<br /> }<br /> return curleft;<br />},<br /><br /><br />findPosY : function( element ){<br /><br /> var curTop = 0;<br /> if(element.offsetParent)<br /> while(1){<br /> curTop += element.offsetTop;<br /> if(!element.offsetParent)<br /> break;<br /> element = element.offsetParent;<br /> }<br /> else if(element.y)<br /> curTop += element.y;<br /> return curTop;<br />}</pre><br /><p></p><br /><p><br />Finally we have the static values for <code>UP</code>, <code>DOWN</code>, <code>LEFT</code> and <code>RIGHT</code>. Nothing mysterious here, but this technique comes in handy<br />for making your code more human readable. Any time you can easily replace numbers with words you should, because a wise man once said, we read our code far more often than we write it.<br /></p><pre>AnimationManager.UP = 0;<br />AnimationManager.RIGHT = 1;<br />AnimationManager.DOWN = 2;<br />AnimationManager.LEFT = 3;</pre><br /><p></p><br /><br /><p><br /> So that's what we have so far. But what's missing? Some were mentioned at the start.<br /><br /> </p><ul><br /> <li>Move in multiple directions simultaneously</li><br /> <li>Move more than one element at a time</li><br /> </ul><br /><br /> But there are a few more I can think of.<br /> <ul><br /> <li><b>Pause and Play</b> - Currently, stopping the animation and starting it again will reset the whole thing. We need to be able to pause everything, then start again from were it was.</li><br /> <li><b>Dynamically change movement instructions</b> - If we're going to make this interactive, it can't just have a bunch of hard coded instructions that can't be changed once it gets moving.<br /> We need to be able to modify the instructions based on user interactions.</li><br /> <li><b>Test for collisions</b> - Flash allows us to test whether two things on the stage are in contact with one another. We need to be able to do this with just JavaScript, HTML and CSS.</li><br /> </ul><br /> So next time we will make some changes and some additions to <code>AnimationManager</code>, as well as create some new wrapper and helper classes that will make the new functionality<br /> effective, efficient and scalable.<br /><p></p>Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0tag:blogger.com,1999:blog-7580637340638969087.post-19765012066211187152008-02-20T20:03:00.000-08:002008-02-20T21:26:41.834-08:00The Future of Web ApplicationsWhile <a href="http://en.wikipedia.org/wiki/Mathematica">Mathematica</a> is not an application most people have even heard of, let alone used, its latest incarnation is a prime example of how client-side applications can utilize more powerful machines elsewhere on the net to do the heavy lifting while still offering a user experience every bit as rich as any desktop app.<br /><br />For years, <a href="http://en.wikipedia.org/wiki/Stephen_Wolfram">Stephan Wolfram's</a> Mathematica has been the leading desktop application for conjuring all manner of mathematical wizardry. It does everything from factoring polynomials to graphing differential equations to your freshman algebra homework. The only problem is that for more complex tasks, it can make thick black smoke spew from the ears of the average laptop. That means it won't do the college students much good unless they happen to have a kick-ass rig at their disposal. The idea behind <a href="http://www.wolfram.com/products/webmathematica/index.html">webMathematica</a> is to bring the desktop version's true power to those without access to the necessary hardware.<br /><br />But one might ask how an application that can generate thousands of floating-point-operations per second would possibly run smoothly inside a browser. The answer turns out to be: do the actual math somewhere else.<br /><br />Here's how it works. Set up your formulas, datasets and such, and when the calculations are ready to begin, all the info is sent via XMLHttpRequests to a server where all the actual math is forwarded to machines specifically designed to handle the task. The response is then rendered via JavaScript, CSS and HTML.<br /><br />Very cool.Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0tag:blogger.com,1999:blog-7580637340638969087.post-79422323892981979262008-02-20T07:28:00.000-08:002008-02-20T08:05:26.161-08:00Cross Site XHR in Firefox 3Had a look at <a href="http://ejohn.org/blog/cross-site-xmlhttprequest/">John Resig's blog</a>, (the author of <a href="http://jquery.com/">JQuery</a>), and he has written up the docs for the new cross-site version of the <a href="http://developer.mozilla.org/en/docs/Cross-Site_XMLHttpRequest">XMLHttpRequest object</a> that will make it's debut in <a href="http://wiki.mozilla.org/Firefox3">Firefox 3</a>.Its actually quite simple in its implementation. The resource being accessed by the XHR simply adds an "Access-control" header specifying the domains permitted access.<br /><br />Normally of course, an XHR can only make requests of the domain it came from. So if the page you are reading came from "blog.anotherreason.com", if I include a a script with an XHR, it can only make requests of that domain. Seems a pain at first, since it might be nice to grab info from an external rss feed or some such thing. But that would also mean that any scripts on the page that originate somewhere else - like an adsense script or a hit counter - could make requests of "blog.anotherreason.com", which presents problems. A malicious script could wreak havoc on my web server by making thousands of requests, (DoS attack), or could start throwing various common method names at it like deleteAccount(1) or deleteUser(1). If I am not really diligent about handling authentication on the server-side, things could go really badly. So only scripts that originate on the domain "blog.anotherreason.com" can access the other resources on that domain because one would assume that I would not write a script to mess up my own web server.<br /><br />But the new cross-site XHR that will be available with Firefox 3 is a little different because I can now specify an "Access-control" header in a resource, the XHR can get at things it was previously denied by design. So if i publish an RSS feed of this blog, and put the following php code in the page that serves it up...<br /><br /><pre><? header('Access-Control: allow <*>'); ?><br /> <? <span style="color:green;">//serve up some rss data here</span> ?><br /></pre><br />...anyone can create a script that updates from my RSS feed without any page reloads. Pretty cool eh?<br /><br />Still wary about giving other people access to my server under any circumstances, but then again, a DoS attack can come from a bot just as easily as a script in a web page, so I will have to look into this some more.Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0tag:blogger.com,1999:blog-7580637340638969087.post-65990275577077512782008-02-18T11:13:00.000-08:002008-02-23T10:39:53.263-08:00Javascript Animation - Part 2 - Multiple Vectors<p>Yes, I know. "Multiple Vectors" looks very complicated and quite pretentious. But it is the most succinct way to describe what I am going to tackle in this post. If you have a better way to describe it in a couple of words, please let me know. That said, it's really not a complex concept, but it takes a bit of work to get one's mind around it, and I figure I should get your juices going early in the process.</p>So here's the problem.<br /><p>In the <a href="http://blog.anotherreason.com/2008/02/javascript-animation-part-1-simple_18.html" target="_blank">last entry</a> on the subject, I described the nature of the project - to do as much animation as possible without using anything except HTML, CSS and JavaScript. In other words, no Flash. (And certainly no SilverLight. Someday I will be including SVG in the mix, but not yet.) The first problem was getting an element to move around fast enough to appear as smooth, realistic movement, but slow enough for the eye and the brain to percieve it as motion and not an instantaneous change of location. We can do this by using JavaScript's native <a href="http://www.w3schools.com/htmldom/met_win_setinterval.asp">setInterval</a> method to make incremental changes over time, instead of all at once. A pause of 40ms or so between executions of our <i>moveElement</i> function does the job. Each execution moves the element one pixel to the right, and 100 iterations later we use the JS native method <a href="http://www.w3schools.com/htmldom/met_win_clearinterval.asp">clearInterval</a> to bring things to a stop. </p>To check whether to continue or to stop, we simply test whether we have moved the element the desired number of pixels, and then call <i>clearInterval...</i><br /><pre><br />if( current_x > dest_x )<br />window.clearInterval( interval_id );</pre><br />Simple enough. But what if we want to now move it up 50 pixels? Your first instinct might be to simply daisy chain some setInterval calls like so...<br /><pre><br /><br />function moveRight(){<br />if( current_x > dest_x ){<br />window.clearInterval( interval_id );<br />interval_id = window.setInterval( moveUp, 30 );<br />}else<br />// move the element to the right...<br />}<br /><br /><br />function moveUp(){<br />if( current_y < dest_y )<br />window.clearInterval( interval_id );<br />else<br />// move the element up<br />}<br /></pre><br />This will certainly work, but this will very quickly become very difficult to maintain. A better way to handle this would be to create a class capable of moving an element around on the page with a minimal amount of effort on the developer's part. What we're looking for here is a JS animation framework.<p><br /></p><p><u><b>The AnimationManager Class</b></u></p><br /><p>We begin with a very basic manager class to handle requests to move an element around on the page. So what do we need this class to do? First we need to give it the id attribute of an element on the page. Simple enough. Then we need to give it instructions on what we want to do with that element. Not as simple.</p>For the purposes of this post, we will need the class to take a series of instructions about how to move an element an arbitrary number of pixels along the X or Y axis. So here's what we would like to be able to type something like the following to move a target element in a 100x100 square, 5 pixels at a time.<br /><pre><br /><br />var target = $( "target-div" ); // prototype element selector shortcut<br />var animation_mgr = new AnimationManager();<br /> animation_mgr.addMovement( target, AnimationManager.RIGHT, 100, 5 );<br /> animation_mgr.addMovement( target, AnimationManager.DOWN, 100, 5 );<br /> animation_mgr.addMovement( target, AnimationManager.LEFT, 100, 5 );<br /> animation_mgr.addMovement( target, AnimationManager.UP, 100, 5 );<br /> animation_mgr.startAnimation();<br /></pre><br /><p>Obviously it would be even simpler to call <i>moveInASquareShape</i> or something like that, and perhaps we might do that one day, but not right now. What we are looking for at the moment is some way to chain together a series of movements in an intuitive way. Here's what I came up with. Have a look, and I'll explain it all in the next post. So stay tuned!</p><br /><pre><br />function AnimationManager(){}<br />AnimationManager.prototype = {<br /><br />movements : undefined,<br />intervals : undefined,<br />current_movement_index : 0,<br /><br />initialize : function(){<br /><br /><br /> this.movements = new Array();<br /> this.intervals = new Array();<br /><br />},<br /><br /><br />addMovement : function( element, direction, pixel_count, increment ){<br /><br /> if( element ){ <br /> <br /> this.movements.push( {<br /><br /> element : element,<br /> start_x : 0,<br /> start_y : 0,<br /> direction : direction,<br /> pixel_count : pixel_count,<br /> increment : increment<br /> <br /> });<br /> <br /> }else{<br /> // element doesn't exist<br /> return false;<br /> }<br />},<br /><br />nextMovement : function(){<br /><br /> if( this.current_movement_index < this.movements.length ){<br /> <br /> var movement = this.movements[this.current_movement_index];<br /> var start_x = this.findPosX( movement.element );<br /> var start_y = this.findPosY( movement.element );<br /> movement.start_x = start_x;<br /> movement.start_y = start_y;<br /> var ani_mgr = this;<br /> <br /> this.intervals[this.current_movement_index] = window.setInterval( function(){<br /><br /> if( !AnimationManager.prototype.executeMovement.apply( ani_mgr, new Array() ) ){<br /> <br /> window.clearInterval( ani_mgr.intervals[ani_mgr.current_movement_index] );<br /> ani_mgr.current_movement_index++;<br /> AnimationManager.prototype.nextMovement.apply( ani_mgr, new Array() );<br /> }<br /> <br /> }, 30 );<br /><br /> return true;<br /> }<br /><br />},<br /><br /><br />executeMovement : function(){<br /><br /> var movement = this.movements[this.current_movement_index];<br /> var element = movement.element;<br /><br /> if( element ){<br /><br /> switch( movement.direction ){<br /> <br /> case AnimationManager.UP:<br /> return this.moveUp( movement );<br /> break;<br /> <br /> case AnimationManager.RIGHT:<br /> return this.moveRight( movement );<br /> break;<br /> <br /> case AnimationManager.DOWN: <br /> return this.moveDown( movement );<br /> break;<br /><br /> case AnimationManager.LEFT: <br /> return this.moveLeft( movement );<br /> break;<br /> <br /> default: return false;<br /> }<br /> }else{<br /> <br /> console.info( "no element for executeMovement" );<br /> }<br /><br /><br />},<br /><br /><br /><br />moveUp : function( movement ){<br /><br /> var destination = movement.start_y - movement.pixel_count;<br /> var current_y = this.findPosY( movement.element ); <br /><br /> if( movement.element && destination < current_y ){<br /> movement.element.style.top = ( current_y - movement.increment ) + "px";<br /> return true;<br /> }else<br /> return false;<br /><br /><br />},<br /><br />moveRight : function( movement ){<br /><br /> var destination = movement.start_x + movement.pixel_count;<br /> var current_x = this.findPosX( movement.element );<br /><br /> if( movement.element && current_x < destination ){<br /> movement.element.style.left = ( current_x + movement.increment ) + "px";<br /> return true;<br /> }else<br /> return false;<br />},<br /><br /><br />moveDown : function( movement ){<br /><br /> var destination = movement.start_y + movement.pixel_count;<br /> var current_y = this.findPosY( movement.element );<br /><br /> if( movement.element && destination > current_y ){<br /> movement.element.style.top = ( current_y + movement.increment ) + "px";<br /> return true;<br /> }else<br /> return false;<br />},<br /><br />moveLeft : function( movement ){<br /><br /> var destination = movement.start_x - movement.pixel_count;<br /> var current_x = this.findPosX( movement.element );<br /><br /> if( movement.element && destination < current_x ){<br /> movement.element.style.left = ( current_x - movement.increment ) + "px";<br /> return true;<br /> }else<br /> return false;<br /><br />},<br /><br /><br /><br /><br />startAnimation : function(){<br /><br /><br /> this.current_movement_index = 0;<br /><br /><br /> this.nextMovement();<br />},<br /><br /><br />stopAnimation : function(){<br /><br /> for( var i=0; i<this.intervals.length; i++ ){<br /> <br /> window.clearInterval( this.intervals[i] );<br /> <br /> }<br /><br />},<br /><br /><br />findPosX : function( element ){<br /><br /> var curleft = 0;<br /> <br /> if(element.offsetParent){<br /> while(1){<br /> curleft += element.offsetLeft;<br /> if(!element.offsetParent)<br /> break;<br /> element = element.offsetParent;<br /> }<br /> }else if(element.x){<br /> curleft += element.x;<br /> }<br /> return curleft;<br />},<br /><br /><br />findPosY : function( element ){<br /><br /> var curTop = 0;<br /> if(element.offsetParent)<br /> while(1){<br /> curTop += element.offsetTop;<br /> if(!element.offsetParent)<br /> break;<br /> element = element.offsetParent;<br /> }<br /> else if(element.y)<br /> curTop += element.y;<br /> return curTop;<br />}<br /><br /><br /><br /><br /><br />})<br /><br />AnimationManager.UP = 0;<br />AnimationManager.RIGHT = 1;<br />AnimationManager.DOWN = 2;<br />AnimationManager.LEFT = 3;<br /><br /><br /><br /></pre><br /><p>We can test this by placing a test element on the page, adding a few movements, and adding a couple of buttons to trigger the startAnimation() and stopAnimation() methods of the AnimationManager.<br /><br />NOTE: Be sure you have the AnimationManager.js file in the proper place.</p><br /><pre><br /> <html><br /> <head><br /> <script language='JavaScript' type='text/javascript' src='AnimationManager.js'></script><br /> <script language='JavaScript'><br /> var ani_mgr = new AnimationManager();<br /> function onWindowLoad(){<br /> <br /> var target = document.getElementById( "target-div" );<br /> ani_mgr.addMovement( target, AnimationManager.RIGHT, 100, 5 );<br /> ani_mgr.addMovement( target, AnimationManager.DOWN, 100, 5 );<br /> ani_mgr.addMovement( target, AnimationManager.LEFT, 100, 5 );<br /> ani_mgr.addMovement( target, AnimationManager.UP, 100, 5 );<br /><br /> }<br /><br /> </script><br /> <style><br /> #target-div{<br /> <br /> background-color: blue;<br /> color: white;<br /> height: 100px;<br /> width: 100px;<br /> position: absolute;<br /> top: 50px;<br /> left: 50px;<br /> margin: auto auto;<br /> <br /> }<br /> </style><br /> </head><br /> <body><br /> <input type="button" onclick="ani_mgr.startAnimation()" value="Start" style="display:inline"/><br /> <input type="button" onclick="ani_mgr.stopAnimation()" value="Stop" style="display:inline" /><br /> <div id="target-div">Target Div</div><br /> </body><br /> </html><br /><br /></pre>Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0tag:blogger.com,1999:blog-7580637340638969087.post-82986305559399168232008-02-18T11:00:00.000-08:002008-02-19T09:30:43.343-08:00Javascript Animation - Part 1 - The Simple Stuff<p>So I've started work on some new GUI widgets, and realized that many of them would be much easier to do in Flash. But unless I planned hook into the Flash player via fscommand or the getURL method, things would get very heavy very fast. An entirely Flash-based interface is, in my humble opinion, a waste of bandwidth and creates as many problems as it solves - not the least of which is maintainability and scalability. Rebuilding and deploying a large Flash-app is often not a trivial matter, and because the result is a binary, all or nothing file, one cannot easily turn features on and off to suit either the environment or the user's preferences. So with those concerns in mind, I wanted to see what I could do using only JavaScript, CSS and HTML, (and a little JSON and/or XML thrown in for data transfer purposes).<!--break--><br /></p><p>As soon as I started working on the main class that would handle the creation, display and animation of a few simple DIV blocks of content, I realized that I would have to recreate the kind of frame-based system that Flash uses. But since HTML and Javascript were not really intended to be an animation platform, there were a number of conceptual and practical hurdles to overcome. The one I decided to tackle first was to undermine one of the DOM's greatest strengths, and use a more obscure feature to rebuild it to my specs.</p><br /><p><span style="font-size:x-small;"><strong>The "onpropertychange" Problem</strong></span></p><p>The ability to change HTML element properties using Javascript, and have those changes be reflected instantly on the screen, is one of the most basic tasks a script can perform, and one of the strengths of the system. So overriding that behavior is neither trivial nor without consequence.</p><br /><p>For example, if I want to animate an object's position on the screen using Javascript, I have to keep in mind that the human eye sees things at around 24 frames-per-second, meaning that anything faster will not even register. So if I try to do this...</p><br /><pre><br /> for( var i=0; i<100; i++ ) <br /> some_element.style.left = ( findPosX( some_element ) + 1 ) + "px"; <br /><p style="color: green; text-indent:5pt;"><br />/*<br /> NOTE: The method findPosX gets the X position of an element<br /> regardless of the browser. I won't reproduce it here,<br /> but you can get it at <a href="http://quirksmode.org/">QuirksMode.org</a>, a great site for all<br /> things cross-browser.<br />*/<br /></p><br /></pre><br /><p>...the element will simply vanish and reappear instantly 100 pixels to the right . Not exactly a smooth animation. The problem is that each time the loop is executed, and the <i style="font-size: 8pt;">onpropertychange</i> event is fired for the element we are moving, and the render engine moves the element to the newly specified position - in this case, one pixel to the right. But since the loop executes in a few hundredths of a second, the eye can't see what's happening during each iteration. It is in fact, worse than that, because unless the refresh rate of the screen you are using is fast enough to actually render them all, most of the iterations never even make it to your eye.</p><p>So how do we insure that things will happen at a rate that the eye can actually perceive? The answer to make sure each iteration is separated by enough time for both the screen to render the change and your eye to perceive it. That's where setInterval comes in handy.</p><br /><p style="font-weight: bold;">Using setInterval to slow things down a bit</p>The native JavaScript method <span style="font-style: italic;">setInterval</span> requires two arguments: a function to execute over and over again, and the number of milliseconds to wait between executions. So for example, I can declare a function that will move the element one pixel to the right...<br /><pre> function moveElement(){<br /> some_element.style.left = ( findPosX( some_element ) + 1 ) + "px";<br /> } <br /></pre><br /><p>...and then call that function once a second using setInterval.</p><br /><pre> var interval_id = window.setInterval( moveElement, 1000 ); <span style='color:green'>// 1000 milliseconds = 1 second</span> </pre><br /><br /><p>Note how setInterval returns a unique id we can use to clear the interval using, you guessed it, clearInterval.</p><br /><pre> window.clearInterval( interval_id );</pre><br /><p>Now the whole point is to make the element move at a rate slow enough to perceive, but fast enough to appear smooth - i.e., as though the element were a real thing moving across our field of vision, so we have to speed things up just a bit. Since 24 frames per/second is the standard for film, that's what we'll use. So if our formula is as follows:</p><br /><pre>24 frames per/second = 1 frame every 1/24th of a second <br />1/24 seconds = 0.04 seconds <br />0.04 seconds = 40 milliseconds <br /></pre><br /><p>So at the very minimum, we want to move the element one pixel every 40 milliseconds. But setting the interval to 40ms is, in general, not going to do the job because there are a lot of other factors at work.</p><br /><p style="font-weight: bold;">Keep everything, not just the animation, moving smoothly</p>JavaScript is not a threaded language. Most browsers these days do create a thread for the JavaScript interpreter to execute in, but everything that happens in that thread must wait for it's turn. As a result, more than the specified number of milliseconds may pass between executions of our <i>moveElement</i><p> method if some other JavaScript code is executed at the same time, e.g., while a user interacts with another element that uses JavaScript to change some property or another. Moreover, the browser will often pause the JavaScript thread to give priority to give some other more important thread a chance - like the one that makes HTTP requests, the one that creates context menus, etc. With this in mind, we are tempted to drop the number of milliseconds between executions down to 1, so we can be sure that the animation does not appear choppy. After all, it would seem that smooth and speedy is better than slow and choppy. But there is a downside. If we ask for the function to be executed every millisecond, we are effectively requesting the interpreter to make everything else a lower priority - including user interaction. So if by clicking the "Do Something" button, we begin our animation AND create and send an XMLHttpRequest, the request process will not have a chance to do its thing until after the animation has completed. The axiom "form follows function" holds just as true in the development of web interfaces as it does in the architecture of buildings, so we do not want to make the user wait for some important functionality just so we can be sure that we have a photorealistic animation. Better to compromise a bit and keep it at 30ms or so. That way other processes have a chance to do their thing.</p><br /><p>So here is the code so far. Note that we check to see if the element has moved the correct number of pixels, and if it has, we stop execution. (Please ignore the ugly global variables. We'll get to making this prettier later on.)</p><br /><pre> var interval_id; <br /> var some_element; <br /> var start_position; <br /> var end_position; <br /> <br /> start_animation_button.onclick = function(){ <br /> <br /> <span class='code-comment'>// THE ABSOLUTELY POSITIONED ELEMENT WE WANT TO MOVE</span><br /> some_element = document.getElementById( "some-element" ); <br /><br /> <span class='code-comment'>// THE ELEMENT'S X POSITION</span><br /> start_position = findPosX( some_element );<br /> <br /> <span class='code-comment'>// where we want it to end up: 100 pixels to the right.</span><br /> end_position = start_position + 100; <br /><br /> <span class='code-comment'>// move the element one pixel (at least) once every 30ms</span><br /> interval_id = window.setInterval( moveElement, 30 ); <br /> } <br /><br /> <span class='code-comment'>// the method to execute repeatedly</span><br /> function moveElement(){ <br /> <br /> <span class='code-comment'>// where it is at the moment</span><br /> var current_left = parseInt( some_element.style.left ); <br /><br /> <span class='code-comment'>// test if we've reached the destination - if so, stop executing the method</span><br /> if( current_left >= end_position ) <br /> window.clearInterval( interval_id ); <br /> } <br /><br /></pre><br /><br /><p>When the "start_animation_button" is clicked, we initialize a pointer to the element we want to move, and determine the start and end positions. With these three variables, we can test the current position of the element during each execution of the method, and stop execution when the element has moved the specified number of pixels to the right.</p><br /><p> </p><br /><p>In the next entry, I will talk more about how to best manage multiple changes to an element during each "frame".</p>Russ Methliehttp://www.blogger.com/profile/08209519048437626579noreply@blogger.com0