3
\$\begingroup\$

I have two divs as seen in this JSFiddle. To recreate the problem, keep clicking on the Gray container till both divisions touch each other.

The purpose is to execute a certain function once the green div touches the red one. The problem is that, this step callback as seen from the following code and demo, gets called multiple times. It is not something that I want to happen.

One method that would fix the problem is to keep a certain flag variable which turns True once step is executed. But I'm not sure that's enough as there needs to be a way to break out of the step callback.

I want to know:

  1. If there is a better/efficient way to detect collision of two divs than using jQuery Collision.

  2. How to stop the step callback after its executed once.

HTML:

<div class="container">
    <div class="test"></div>
    <div class="menu"></div>
</div>

JavaScript:

$(document).ready(function () {
    var hit_list;
    $(".container").click(function () {

        $(".menu").stop().animate({
            left: "+=100px"
        }, {
           duration: 300,
           complete: function () {
               $(".menu").animate({
                   left: "0"
               }, 800);
           },
           step: function(){
               //Test for collision
               hit_list = $(".menu").collision(".test");
               if (hit_list.length != 0) {
                   $(".container").append("Welcome Earthling");
               }
           }
         });
     });
 });
\$\endgroup\$

5 Answers 5

6
\$\begingroup\$

In response to not triggering the message repeatedly, I would store the previous check for a collision so you know if a collision has started or ended.

If there is a collision but your previous check didn't show a collision, then you have a new collision and you can trigger the event. However, if the previous check did show a collision, don't fire the callback since it has already been fired.

I would also recommend using custom jQuery events. This allows you to separate out the callback from the animation code.

Below I've adapted your code to trigger a custom event when you detect a collision has started or ended. Here's the updated javascript (also available as a modification of your fiddle):

$(document).ready(function () {

    $(".menu").data("collision", false);
    $(".menu").on("collision_start", function () {
        $(".container").append("Welcome Earthling");
    });

    $(".container").click(function () {

        $(".menu").stop().animate({
            left: "+=100px"
        }, {
            duration: 300,
            complete: function () {
                $(".menu").animate({
                    left: "0"
                }, 800);
            },
            step: function () {
                //Test for collision
                var hit_list = $(this).collision(".test");
                var current_collision = hit_list.length != 0;

                // compare this to what we last observed
                var changed_collision = current_collision != $(this).data("collision");

                if (changed_collision) {
                    // save this new observation for future comparisons
                    $(this).data("collision", current_collision);

                    // trigger an event
                    $(this).trigger("collision_" + (current_collision ? "start" : "stop"));
                }
            }
        });
    });
});
\$\endgroup\$
4
\$\begingroup\$

You can do this:

// Let's make it a jQuery plugin so each element acts independently
$.fn.collider = function () {
    return $.each(this, function () {

        // a flag when done
        var done = false;

        // Cache a few values
        var container = $(this);
        var menu = container.find('.menu');
        var menuWidth = menu.width();
        var test = container.find('.test');
        var testLeft = test.position().left;

        container.on('click', function () {
            // prevent click when already hit
            if(done) return;

            menu.stop().animate({
                left: "+=100px"
            }, {
                duration: 300,
                complete: function () {
                    menu.animate({
                        left: "0"
                    }, 800);
                },
                step: function () {
                    // We can just check the right of the green with the left of the red
                    if (!done && menu.position().left + menuWidth >= testLeft) {
                        done = true;
                        console.log('done!');
                    }
                }
            });
        });
    });
}

$(function () {
    $('.container').collider();
});
\$\endgroup\$
2
  • \$\begingroup\$ How well will it scale for detecting on all four sides? As in adding more conditions will affect the animation smoothness in any way? \$\endgroup\$
    – md1hunox
    Commented Feb 25, 2014 at 8:03
  • \$\begingroup\$ @vineetrok In the context of your demo, you only used one side. \$\endgroup\$
    – Joseph
    Commented Feb 25, 2014 at 8:10
3
\$\begingroup\$

Interesting question,

there is no support in browsers for collision events, so you have to check every so often, which feels wrong. But that's the way it is.

There is no way to stop calling step but you can put of course an if statement to prevent Welcome Earthling from being shown multiple times:

       step: function(){
           //Test for collision
           if(!hitList.length)
           {
               hitList = $menu.collision( $test );
               if (hitList.length != 0) {
                   $(".container").append("Welcome Earthling");
               }
           }
       }

I did change hit_list to hitList, lowerCamelCase FTW! I also assume you would cache $('.menu') and $('.test'). And finally I assume here that you would initialize hitList to [].

Another way to approach this is to not use a step function at all, simply keep checking for a collision separately, once the collision has occurred, stop checking. You would use a function similar to this:

function detectMenuTestCollision() 
{
  var hit_list = $(".menu").collision(".test");
  if(hit_list.length) 
  {
    $(".container").append("Welcome Earthling");
  }
  else
  {
    setTimeout(detectMenuTestCollision, 250);    
  }
}

Of course, you ought to cache those jQuery lookups ;)

Finally, you can look at the code of the collision plugin here, I think this code is pretty tight already, you could simplify this down to your needs, but I think that would fall in the realm of premature optimization.

\$\endgroup\$
2
\$\begingroup\$

I didn't know the jQuery collision code, thanks to konijn for posting.

Even the best universal library can be optimized for a specific case. Another issue if it is worth the effort or not.

In your case, one common optimization approach can work: move all the dimensions from one object to the other. Then, you only need to check if the first object (that is a dimensionless point) is inside the other.

Lets say that you have 2 squares, of 10px width and height each. To check if they collide, you can check if a point in the center of the first square collides with an square of dimension 20px.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ You're pretty active for a new user, you should come visit us in the chat room sometime! :) \$\endgroup\$
    – syb0rg
    Commented Feb 26, 2014 at 23:13
2
\$\begingroup\$

You can add a flag - this makes it only show "Welcome Earthling" once, but does not stop step.

$(document).ready(function () {
    var hit_list;
    var col_flag = 0;
    $(".container").click(function () {
        $(".menu").stop().animate({
            left: "+=100px"
        }, {
            duration: 300,
            complete: function () {
                $(".menu").animate({
                    left: "0"
                }, 800);
            },
            step: function(){
                // Test for collision if collision_flag is not tripped
                if(col_flag == 0){
                    hit_list = $(".menu").collision(".test");
                    if (hit_list.length != 0) {
                        $(".container").append("Welcome Earthling");
                        col_flag = 1;
                    }
                }
            }
        });
    });
});
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.