Neuron Workout Solutions #6
Welcome to this sixth installment of Neuron Workout Solutions™, a series that answers the questions and challenges at the end of many chapters in the book. Boy, is this installment overdue. I’ve been meaning to write it for weeks, but life just didn’t seem to agree. Today we’ll address the challenges closing chapter 12: Effects. And with this, we open up the Neuron Workouts for the script.aculo.us part of the book.
Say we have half a dozen items (effects and custom code snippets) we want to queue up like pearls on a string. What’s the best option here? afterFinish callbacks or a custom queue? Why?
As soon as you need to queue up more than 2 or 3 effects and bits of code, I rather advocate using a custom queue. Not only does that make for an easier manipulation of the sequence (rescheduling, pausing, whatever…), it also avoid massive nesting (usually shown with massive indenting, too). We all found ourselves once or twice refining an increasingly-complex sequence, and winding up with something like this:
// UGLY CODE!!! ////////////////// Effect.A(options, { afterFinish: function() { // someCode… Effect.B(options, { afterFinish: function() { Effect.C(options, { afterFinish: function() { // someCode… } }), }); }); } });
Going with queues just seems nicer (especially for long effect chains), although using afterFinish for code interleaving is actually more concise than Effect.Event blocks that would, anyway, require such callbacks:
Effect.A({ …, queue: { scope: 'yourEffect', position: 'end' }, afterFinish: function() { // someCode… }}); Effect.B({ …, queue: { scope: 'yourEffect', position: 'end' }}); Effect.C({ …, queue: { scope: 'yourEffect', position: 'end' }, afterFinish: function() { // someCode… });
How would you queue two effects with a two-second pause between the end of the first effect and the beginning of the second one?
Just use a common queue and set the second effect’s delay. For instance:
new Effect.Scale('langLink', 200); new Effect.Scale('langLink', 50, { queue: 'end', delay: 2 });
If there were already ongoing effects and you wanted the first one to wait, you’d need to use queue: 'end' on the first one, too. Check out the demo page!
Write horizontal blind effects (Effect.BlindLeft and Effect.BlindRight).
Aaaaah, that’s the million-dollar one, right? The question keeps popping up on the support mailing list and in many places. Let’s look at how BlindUp and BlindDown are implemented. The ‘up’ version first, as it’s the simplest one:
Effect.BlindUp = function(element) { element = $(element); element.makeClipping(); return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, restoreAfterFinish: true, afterFinishInternal: function(effect) { effect.element.hide().undoClipping(); } }, arguments[1] || { }) ); };
OK, so what is Scripty doing here?
- First, we state we’re not interested in resizing the contents (that’s not what “blinds” effects do), and ensure the content won’t overflow once we start reducing the element’s surface: that’s what
makeClippingis for; it sets the element’soverflowCSS property tohidden. - Then scale, vertically only, from the current size (whatever it is) to zero.
- And some cleanup code when we’re done: hide the element, restore its
overflowCSS property’s previous value, and restore its original size. This is the sort of cleanup that makes it possible to hide and element withBlindUpand show it again withAppearorSlideDown, for instance.
We could very well transform this to suite BlindLeft. What’s changing here, basically? Well, instead of disabling horizontal resizing (using scaleX: false, we would need to disable vertical resizing. As BlindUp is graciously allowing us to override its default options set with our own, let’s try this out:
Effect.BlindLeft = function(element) { return Effect.BlindUp(element, Object.extend({ scaleX: true, scaleY: false }, arguments[1] || {})); };
First, a reminder about Blind… effects: for best results, you would need to avoid padding on the container; otherwise, size computation will mix up and you’ll start larger than you originally were. What you usually end up doing is wrap the whole contents inside another element, which has the proper padding, if you need any.
There’s only one specific gotcha with the code we just saw: you need to work around line wrapping. This is not an issue on vertical blinds, because text flows horizontally in browsers (so far, at least ;-)). But when blinding sideways, we change the container’s width, and its textual contents, if any, will wrap accordingly. For simple stuff, we can just get around with a white-space: nowrap CSS definition.
Sparing us that requirement, therefore enabling sideways blinds on random textual contents, would probably require an extra level of containment, much like SlideUp and SlideDown require, in order to play with internal positions. We’ll not dive into this here, but you’re welcome to share suggestions and demos in the comments!
So guess what?! It works! Check out the demo page!
The original BlindDown is a tad more tricky, but we don’t care: its code wouldn’t fit our BlindRight needs anyway. Because unlike up, left and down, a right-hand blind will not only resize the element, it will move it.
Basically, we need to synchronize two effects here:
- A
Moveto push the element to the right by its width - A
Scaleto reduce its size - We need to store the original position to restore it in the end, as we’re not supposed to keep the element moved aside…
It’s actually rather straightforward. Here we go:
Effect.BlindRight = function(element) { return new Effect.Parallel([ Effect.BlindUp(element, { scaleX: true, scaleY: false, sync: true }), new Effect.Move(element, { sync: true, x: $(element).getWidth(), y: 0 }) ], Object.extend(arguments[1] || {}, { afterFinishInternal: function() { element.setStyle({ left: left, top: top }); } })); };
Notice the afterFinishInternal callback, specifically designed for actual, internal callbacks in custom effects. This is also a nice example of Effect.Parallel in action for synchronized parallel execution.
Yay! That’s pretty short indeed. Check out the demo page for a live demo!
How could we turn any effect into a permanent loop?
Well, I wish we could just whip up a solution with a custom transition function; that would be an elegant way of doing it. However, that just won’t do: a transition essentially says which position to render instead of the original position (which always goes from zero to one in one smooth linear go). It does not change the effect’s duration, which means we can’t get “permanent loop” with a transition.
The trick below relies on the effects engine implementation up to version 1.8.1. It won’t work in the future “Scripty2,” although there’s another possible way there.
The idea is that the original position progression, maintained internally by the effects engine and not modifiable by callbacks, is based on the effect’s internal startOn and finishOn properties, as described around the end of the book’s effects chapter. But it doesn’t reinspect these properties all the time: it’s only based on their values at effect initialization time. However, the engine relies on these properties at every frame, which means that if we change the effect’s ending time post-initialization, we’ll get it to run positions way above one.
That sounds like this is an issue, but with the most common transitions, it’s not. Most rely on conversions through the trigonometric circle, which boils down to this: any position is “modulo 2:” from 0 to 1 goes forward, from 1 to 2 goes backward. Such transitions include sinoidal (the default one), flicker, wobble and spring.
Therefore, tricking any effect into becoming a permanent forth-and-back loop is as simple as changing its internal finishOn property to be in the infinite future, post-initialization. This could just mean passing the following callback in:
beforeSetup: function(effect) { effect.finishOn = Number.POSITIVE_INFINITY; }
Isn’t this great? On the demo page, you’ll be able to see a looped example of all other demos.
Disclaimer
I’ve had so little time to try and squeeze this through that I couldn’t test on IE6+ (works beautifully on Firefox 2+, Opera 9.22+ and Safari 2+. If there are any issues, please report them in the comments or through direct e-mail.
And that’s a wrap!
I’ll look forward to any questions, comments or wild guesses you may have. And stay tuned for the seventh installment!
No comments yet. Be the first.
Leave a reply



