Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JQuery animate - any way to "stall" the complete trigger?

Imagine I have a viewport with a defined width and height. I clone an element out of view on the right side, and fly it across the viewport at a random y position with code like this:

    ...
    .css({
        'left': BASE_NODE_INIT_LEFT_PX,
        'top' : rand(BASE_NODE_INIT_TOP_MIN_PX, BASE_NODE_INIT_TOP_MAX_PX) + 'px',
        'width': _width + 'px',
        'height': _height + 'px',
    })
    .animate({
        left: BASE_NODE_ANIMATE_DISTANCE_X_PX
    }
    ...

Simple enough animation. Here's the fun part: on each step I would like to apply some physics to this element.

Something like this:

        ...
        step:function(currentStep){
            if($(this).data('animating-wind') !== true)
            {
                $(this).data('animating-wind',true);
                var wind = (rand(1,2) == 2) ? '+=' + rand(1, 100) + 'px' : '-=' + rand(1, 100) + 'px';
                $(this).animate({
                    'top': wind,
                    'text-indent': wind,
                },{
                    duration: 500,
                    complete: function(){
                        $(this).data('animating-wind',false);
                    },
                    queue: false
                });
            }
        }
        ...

Basically what happens is instead of flying straight across from right to left, it slows down, speeds up, and raises and dips randomly, just as I'm intending.

The problem I am facing is that sometimes the "wind" is strong enough so that the element is still visible on the viewport when the number of steps has reached the total calculated steps, and it'll just disappear (which happens in my complete:function(){ ...$(this).remove(); ...}

Obviously what's happening is that JQuery thinks the animation is complete because it calculated the steps and said "I'm animating this object this many pixels over this many milliseconds over this many steps - it's there". But the wind animation is outside the animation queue so this animation process is none-the-wiser.

What I would like is to begin the animation and just keep animating until the element has exited the viewport in any direction - once it is no longer visible, either from its initial flight path or from the wind blowing it off screen, I'll detect it and trigger the onComplete callback. The only thing I can think of is to put this animation inside a function and in the complete:function(){} make a recursive call to itself, then inside the step:function(){} I'd do the "visible in the viewport" check and call stop(true,false) if true. This just seems like a very expensive check - running that calculation 100+ times in 400-1200ms might make the animation choppy so I'm looking for a more elegant solution.

TL;DR: How do I keep animating something so long as it hasn't reached its destination yet due to additional animations being set on it?

UPDATE: I applied @mu is too short's idea and came up with this:

/** 
 * Create a new clone of the master div for user interaction. Position it 
 * randomly off the canvas, and animate it across at a random speed.
 */

function spawn_target() {
    spawn_timeout = setTimeout(function() {
        var bonus = (rand(1, BONUS_ODDS) == BONUS_ODDS) ? ' ' + BONUS_CLASS : '';
        var _img_src = (bonus.length > 0) ? BONUS_NODE_IMG_SRC : BASE_NODE_IMG_SRC;
        var _width = $('#' + BASE_NODE_ID).width();
            _width = Math.floor(rand(_width / 3, _width));
        var _height = _width;
        var _framerate = 10 - Math.floor(_height/10);
        var _clone = $('#' + BASE_NODE_ID).clone().addClass(CLONE_CLASS + bonus).attr('id', CLONE_ID).appendTo('#' + CANVAS_ID).css({
            'left': BASE_NODE_INIT_LEFT_PX,
            'top': rand(BASE_NODE_INIT_TOP_MIN_PX, BASE_NODE_INIT_TOP_MAX_PX) + 'px',
            'width': _width + 'px',
            'height': _height + 'px',
        })

        $(_clone).children('img').attr('src', _img_src);

        /**
         * Handle the actual flight across the viewport 
         * @param object _this The clone we are animating manually
         * @return object pointer This contains the interval so we can clear it later
        */
        function fly(_this) {

            var animating_wind = false;
            var pointer = {
                timer_id: null
            };
            pointer.timer_id = setInterval(function() {

                // Get rid of the node if it is no longer visible in the viewport
                if (!$(_this).is(':onviewport')) {
                    clearInterval(pointer.timer_id);
                    $(_this).remove();
                    adj_game_data(GAME_DATA_LIVES_ID, -1);
                    check_lives();
                    spawn_target();
                }

                // Move node along with slight realism
                var distance = rand(FLY_DISTANCE_MIN, FLY_DISTANCE_MAX);
                $(_this).css('left', '-=' + distance + 'px');

                // Only trigger the wind animation when it is idle
                if (animating_wind !== true) {
                    animating_wind = true;

                    // Setup the wind physics
                    var _wind_power = rand(WIND_POWER_MIN, WIND_POWER_MAX);
                    var _wind_dir = (rand(1, 2) == 2) ? '+=' : '-=';

                    // Override wind direction to keep node inside viewport
                    if(($(_this).offset().top + $(_this).height() + _wind_power) > CANVAS_HEIGHT)
                    {
                        _wind_dir = '-=';
                    }
                    if(($(_this).offset().top - _wind_power) < CANVAS_TOP_BUFFER_PX)
                    {
                         _wind_dir = '+=';   
                    }

                    var _wind = _wind_dir + _wind_power + 'px';  

                    // Apply the wind physics and don't let the node break the top or bottom
                    // boundaries of the viewport
                    $(_this).animate({
                        'top': _wind
                    }, {
                        duration: WIND_ANIMATION_DURATION,
                        complete: function() {
                            animating_wind = false;
                        },
                        queue: false
                    });
                }
            }, _framerate);

            return pointer;
        }
        $(_clone).data('interval', fly(_clone));

    }, rand(SPAWN_TARGET_TIMEOUT_MIN, SPAWN_TARGET_TIMEOUT_MAX));
}

Here's what I have so far: http://jsfiddle.net/AlienWebguy/DmtQ7/embedded/result/

I like this idea though it seems like JQuery should have something built-in, which is what I was hoping for. Now, instead of $(obj).stop() I have to clearInterval($(obj).data('interval').timer_id);. *scratches head...

like image 891
AlienWebguy Avatar asked Nov 14 '22 19:11

AlienWebguy


1 Answers

This is what I think you should be doing (pseudo-JavaScript):

function animate_it($thing) {
    var wind     = false;
    var timer_id = setInterval(function() {
        // Compute the movement for this step.
        // Then factor in the wind effect and update the `wind` variable.
        // Then apply the movement to `$thing`.
        if(it_is_off_the_page($thing))
            clearInterval(timer_id);
    }, n);
}

If there is anything in the inner computation that is computed over and over again, precompute it at the animate_it level and let the closure close over it. You would want to play with it a bit to figure out what n should be.

Just do the animation by hand so that you're not fighting with jQuery's animate function and stop the animation when you know it is done. This also allows you to include a direction and strength for the wind (so that your animated object could go backwards) pretty much for free.

You might want to get the timer_id back to the outside world so that you could stop the whole thing from the outside. In that case, you'd want to fake a pointer with a one element object:

function animate_it($thing) {
    var wind    = false;
    var pointer = { timer_id: null };
    pointer.timer_id = setInterval(function() {
        // Compute the movement for this step.
        // Then factor in the wind effect and update the `wind` variable.
        // Then apply the movement to `$thing`.
        if(it_is_off_the_page($thing)) {
            clearInterval(pointer.timer_id);
            pointer.timer_id = null;
        }
    }, n);
    return pointer;
}

var timer = animate_it($thing);

// And then later, to shut it down...
if(timer.timer_id)
    clearInterval(timer.timer_id);

You'd probably want a "it is gone" callback on animate_it if you had a lot of animated objects, that would let you keep your external list of timers clean and cruft free.


The problem with trying to do this with animate is that animate computes the number of steps at the beginning and then you can change that. You need a variable number of steps but animate doesn't care (it cares even less than a honey badger).

like image 95
mu is too short Avatar answered Dec 09 '22 23:12

mu is too short