Fade golf

2010-06-19 11:33:32

There probably dozens of Javascript scripts out there that do some kind of fade effect. Some are solo, most are embedded in some kind of complicated script. My experience is that the ease function gets bigger and more complicated as you try to make it more generic.

Last year Thomas Fuchs presented Emile at Fronteers 2009, which was basically a 50 line generic ease script. But because it was trying to be generic, it had still had quite some bloat and incompatibilities with certain setups.

So for this project I was asked to create a fade. The project did not have a generic library that could do this so I had to decide how to do it for just this single occurrence. Even Emile was useless to me and felt like too much of code to just do this.

So that led me to creating a simple function and reinventing the wheel, tailored to my needs (that project). In fact, it (eventually) led to a 146 byte function who's features are put down below.

I really tried my best to get it down to "140 chars", but I got stuck at 144 (when not giving it a name), and that's already with a little cheating :p Let's get down to it.

The original function:

Code: (Javascript)
var fade = function(e, out){
if (!e) return debug("el not found");
if (!e.o) e.o = 0;
if (e.t) clearInterval(e.t);
e.t = setInterval(function(){
if (out) {
if (e.o > 0) {
e.o -= 1/50;
e.style.opacity = e.o;
} else {
clearInterval(e.t);
e.t = 0;
e.opacity = e.o = 0;
}
} else {
if (e.o <= 1) {
e.o += 1/50;
e.style.opacity = e.o;
} else {
clearInterval(e.t);
e.t = 0;
e.opacity = e.o = 1;
}
}
}, 10);
}

About 500 bytes, including whitespace. A simple naive implementation of fadeIn and fadeOut, with canceling a running action and picking up where the last action was stopped. It does use expandos (augment DOM elements), which is not considered a best practice.

The short version:

Code: (Javascript)
function f(e,b,c,o){c=clearInterval;(o=e.o)?c(e.t):o=e.o=0;e.t=setInterval(function(){e.style.opacity=e.o=(o+=(b?-1:1)/49)<0||o>1?!b|c(e.t):o},9)}

Doesn't look the same, but will do the same. The rest of the post will explain what is going on. I'll also produce versions of the function if some features are left out. The + are ups, the - are downs.

First, the feature list:
+ Has a name (this takes up two bytes yknow)
+ Fade an element in
+ Fade an element out
+ Will stop a previous fade on this element if one is going on (only from this script, obviously)
+ Will continue from the current opacity as set by this script (will not read current style but an expando)
+ Does not use eval (literally nor "cloaked")
+ Allos to enter falsy and truesy (is that even a word), without requiring strict boolean type, for second parameter
+ Uses no other libraries than the ones supplied by the browser (no JQuery, MooTools, etc)
+ Stays within the Ecmascript 5 (or below) realm. No lambda cheating :)
- Uses expandos, because they are shorter and I'm okay with the ramifications
- Only works on browsers that support the opacity style (so basically anything non-IE). Support for IE is trivial, but probably doubles the size.
- Does not allow you to customize speed/duration (trivial to add though, one byte for speed and two bytes for duration)
- Requires e to at least allow to apply the dot operator (e.x) or will fail (does not check e at all)

Now let's tear apart the function.

Code: (Javascript)
/**
* Fade a DOM element in or out
* @param e DOM element
* @param b boolean Fade out?
*/
function f(e,b,c,o){
c=clearInterval;
(o=e.o)?c(e.t):o=e.o=0;
e.t=setInterval(function(){
e.style.opacity=e.o=
(o+=(b?-1:1)/49)<0||o>1
?
!b|c(e.t)
:
o
}
,9)
}

Some generic notes.

The function always calls clearInterval, even if it could determine that the timer is not running. I'm assuming that the timer id's returned by setInterval are unique across one session. I quickly searched but found no conclusive evidence to this on the web. Still, I'm positive this is the case.

Semi-colons are optional in Javascript. However, you only have a choice between a semi-colon and a return character. The return character is two bytes (CL RF) while the semi-colon is one. On Unix/Mac systems this would be equally as long by the way, but no matter. Semi-colon is safer on all platforms. You don't need a semi-colon (or return) right before a closing bracket though.

The function literals are required, as well as their full fingerprint function(){}. This is syntactically required. You can obviously give a string to setInterval but that would break the eval feature. More on that later. Likewise, the function could be created using the function constructor, but that's eval as well (and probably longer).

The expandos are used to prevent the hassle of looking up current style (which is ridiculous bloat) and being able to stop a fade action between individual calls of the function without polluting the global scope.

Having said that..

Code: (Javascript)
function f(e,b,c,o){

It has to have a name, a handle by which we can call it. This is two bytes. The function keyword can't be cached or saved or shortened, it's a language construct keyword.

The parameters are a way of creating variables more quickly. The result is equivalent (at least in our code!) in speed and execution. The e and b are actual parameters, c and o are caches.

Code: (Javascript)
c=clearInterval;

Since we are using clearInterval twice in the script and it's such a long word, we cache it here. There are no scoping issues as clearInterval always acts on the global scope, regardless. This could also be seen as some optimization, since it prevents global scope lookup to clearInterval (but this was never a reason ;)).

Code: (Javascript)
(o=e.o)?c(e.t):o=e.o=0;

Ah the first wtf. First we cache the current value of e.o, which is the opacity of the element. If this function never ran, e.o will (probably..) be undefined. We need to check for this because we are incrementing this value later and we want to prevent a NaN.

The value is cached because it's used so much throughout the function that the overhead is compensated (takes four extra bytes to create but saves two bytes per usage).

The parenthesis are (sadly) required because the ternary operator (?:) has precedence over assignment, meaning it would otherwise store the result of the ternary operator in o, which we don't want.

The ternary operator checks whether o (e.o) is falsy. If not, clearInterval is called on e.t, regardless of whether e.t exists (which it probably should since e.o exists) and is still running. Calling clearInterval with bogus values has no visual side effects.

Otherwise e.o was either undefined or 0 (or some other falsy) and we need to set it to a number. We need to store that in o as well. Earlier versions used to OR the value (5|0==5, undefined|0===0), but that proved to be longer in this construction.

Note that clearTimeout is only called if e.o evaluates to a non-falsy. The proof that this is okay is this: this function will either finish with rounded values (0 or 1), or it will be stopped mid-way in which case it will be between 0 and 1. Ergo, it can never be bigger than 1 or smaller than 0. Hence, when the value is falsy, it is 0 or undefined. If 0, the timer will have been stopped. If undefined, the timer never ran in the first place and e.t is undefined. Either way, no need to run clearTimeout on it.

So, now we have an o and e.o which is a value between 0 and 1 inclusive and we are positive that whatever previous fade effect might have been running, will be stopped now.

Code: (Javascript)
e.t=setInterval(function(){

Nothing to see here really. We cache the timer id in the e.t expando of the DOM element. This is because we want to be able to cancel this fade in a next call. The other way around that is to create variables under closure or something, which takes up more space :) No space will be won by caching the value in a new variable t. The overhead is not worth it.

Code: (Javascript)
e.style.opacity=e.o=

A dreadfully long line, but required nevertheless. Set the opacity and the e.o expando to the new value for this iteration (the value comes from the next lines, obviously).

Code: (Javascript)
(o+=(b?-1:1)/49)<0||o>1

This is a tricky one. We first store the new value in o and then check whether it will exceed the 0 or 1 bound. The parenthesis are once again required because otherwise o would (also) become the result of the ternary operator that follows.

Here is where the second parameter determines the direction of fading. I'm aware that booleans cast to zero and one, but I've not been able to create something shorter than b?-1:1. The parenthesis are again, required, otherwise the division would only happen if b is false.

So o becomes the next value of opacity. After that it simply checks o<0||o>1. Earlier versions would also check the second parameter (b&&o>1||!b&&<0) but I realized that, since we assume this function will only exit with bounded rounded values, o will never start below or above the bounds. Hence checking for the direction parameter b is not really needed (and saves some bytes :)).

Code: (Javascript)
?
!b|c(e.t)
:
o

So this ternary checks whether o is out of bounds or not. If not out of bounds, it will simply return o (last line in this snippet). Otherwise it will round the value in a devious manner.

We assume b to be a boolean-ish value, but we aren't enforcing it to be boolean (could save us another byte right here). That's why we first invert the value with the exclamation mark, always producing a boolean.

Next we apply the binary OR operator using the result of clearInterval, stopping the timer at the same time. The result of clearInterval is always undefined.

The final step is sheer magic. Well okay, maybe not. Both operands of the OR will be casted to number. Boolean becomes 0 or 1 and undefined becomes 0. x|0 is always x and OR always produces an integer. In other words, it produces a one if b is false (fade in) and zero if b is true (fade out).

In all fairness, I have a feeling that if you switch the meaning of the second parameter (true means fade in), things will turn out all right just the same, without sacrificing the "falsy truesy" condition, because it is casted to number anyways. This would save one byte (the invert).

Code: (Javascript)
}
,9)
}

The remaining bytes are trivial. Note that the semi-colons are left out (saving two bytes) because they are auto inserted when encountering a closing bracket.

I use a 9ms interval because that's the largest single digit. I've tried leaving out the last parameter to setInterval, but that makes setInterval only running the code once in Firefox (weird bug or expected behavior?).

And that's it, 146 bytes at your service :)

Shorter versions that sacrifice certain conditions:

Code: (Javascript)
function f(e,b,t,o){(o=e.o)?0:o=e.o=0;t=setInterval(function(){e.style.opacity=e.o=(o+=(b?-1:1)/49)<0||o>1?!b|clearInterval(t):o},9)}

Unable to cancel previous fade: 133 bytes

Code: (Javascript)
function f(e,b,c,o){c=clearInterval;(o=e.o)?c(e.t):o=e.o=0;e.t=setInterval(function(){e.style.opacity=e.o=(o+=b/49)<0||o>1?!b-1|c(e.t):o},9)}

Requiring the second parameter to be -1 for fade out and 1 for fade in: 141 bytes

Code: (Javascript)
function f(e,o,t){o=0;t=setInterval(function(){e.style.opacity=(o+=1/49)>1?clearInterval(t)||1:o;},9)}

Super naive fade in 0 to 1: 102 bytes

Code: (Javascript)
function f(e,o,t){o=1;t=setInterval(function(){e.style.opacity=(o-=1/49)<0?clearInterval(t)||0:o;},9)}

Super naive fade out 1 to 0: 102 bytes

Code: (Javascript)
function f(e){a=e;o=1;t=setInterval('a.style.opacity=(o-=1/49)<0?clearInterval(t)||0:o',9)}

Eval naive fade out using globals: 91 bytes

Code: (Javascript)
// e in global scope should be the element
o=1, t=setInterval('e.style.opacity=(o-=1/49)<0?clearInterval(t)||0:o',9)

Eval naive snippet fade out using globals: 73 bytes

Hmmm, that last two actually made me shiver :p

Anyways, if anyone can shorten the original function, without sacrificing any of the (ups of) the listed conditions (hope I didn't forget any ;)), please let me know.

Hope it helps you, or at least amused you :)

PS. golf is from Code Golf, where it is the objective to get a certain snippet of code in the least amount of characters, just like strokes in golf.