1

I'm building a simple Simon Says/Memory app and am trying to use jQuery to animate some divs where the user repeats the pattern back. The part I'm getting stuck on is lighting up the divs in sequence.

My initial thought was to use an array [1,4,2,3,1,4] that matches up with the id's of specific divs

HTML


<div class="container">
  <div id="1" class="red square"></div>
  <div id="2" class="yellow square"></div>
  <div id="3" class="blue square"></div>
  <div id="4" class="green square"></div>
</div>

CSS


.square{
  width: 200px;
  height: 50px;
  opacity: 0.2;
}

.brighten {
  opacity: 1;
}

From here, I'd loop through the array and do something like:

for( var i = 0; i < arr.length; i++){
  var element = $("#"+arr[i]);

  element.addClass('brighten');

  //pause for a half second

  element.removeClass('brighten');
}

being a JS/jQuery novice I'm struggling because the divs are being brighten and unbrighten all at once.

I looked at using jQuery animate() but when I use the following code

for (var i = 0; i < arr.length; i++) {
  $("#" + arr[i]).animate({
    opacity: 1,
  }, 500, function() {
    // Animation complete.
    $("#" + arr[i]).animate({
      opacity: 0.2,
    }, 500, function() {
        // Animation complete.
    });
  });
}

all of the divs are being highlighted at once rather than in sequence.

isherwood
  • 58,414
  • 16
  • 114
  • 157
shartshooter
  • 1,761
  • 5
  • 19
  • 40

4 Answers4

1

The loop runs, and completes, right now, it doesn't wait for your animations, they all start immediately and finish in half a second.

If you want to "stagger" the animation, you have to add a delay that grows with each iteration

for (var i = 0; i < arr.length; i++) {
  $("#" + arr[i]).stop(true, true).delay(i*500).animate({
    opacity: 1
  }, 500, function() {
      $(this).animate({
          opacity: 0.2,
      }, 500, function() {
            // Animation complete.
      });
  });
}
adeneo
  • 312,895
  • 29
  • 395
  • 388
  • I made a mistake in my original submission. I need the opacity to drop back down to 0.2 from 1 which is why I have the second callback. – shartshooter Jan 28 '16 at 21:11
  • @user3195487 - I just removed the second animation, as it was doing the same as the first, and made no sense. I've added back in again now that you've changed it. – adeneo Jan 28 '16 at 21:28
  • this is working except for one critical piece. If the array goes `[1,2,3,3,3,3] then the div maintains opacity of 1 for the last four iterations. How do I make sure that opacity moves back to 0.2 before coming back up to 1? – shartshooter Jan 28 '16 at 21:34
  • If the array goes like that, you're animating the `$('#3')` four times, and you have an ID starting with a number, which can cause issues. – adeneo Jan 28 '16 at 21:40
  • I'd suggest trying a `stop()`, made an edit, see if that works – adeneo Jan 28 '16 at 21:41
  • No dice, updating the ID to a string didn't help either - https://jsfiddle.net/bg146guf/1/ – shartshooter Jan 28 '16 at 23:01
1

The reason for your issue is that the FOR loop you're using doesn't wait for the first light to brighten/unbrighten before moving along to the next light. You'll need to find a way to chain these behaviors. Since jQuery animate doesn't use a true Promise structure (a discussion for another time), you might be better using the callbacks more effectively. Consider this:

var sequence = [1,4,2,3,1,4];
var currentIndex = 0;

// start first light
$('#' + sequence[currentIndex]).addClass('brighten');

// prepare to dim and move to next light
pauseBeforeNextLight();

/**
 * pauses, then turns off one light and turns on the next.
 */
function pauseBeforeNextLight() {
    window.setTimeout(function() {
        var $oldLight = $('#' + sequence[currentIndex]);
        $oldLight.removeClass('brighten');

        if (currentIndex >= sequence.length) {
            // we've reached the end of the sequence. Discontinue
            return;
        }

        currentIndex++; // same as currentIndex = currentIndex + 1

        // turn on the next light
        var $newLight = $('#' + sequence[currentIndex]);
        $newLight.addClass('brighten');

        // leave the new light on for 0.5s, then next. This is INSIDE the
        // timeout, so it WON'T happen immediately like in a for-loop.
        pauseBeforeNextLight() 
    }, 500);
}

This will force the program to NOT run the next light until the previous light is dimmed. In this example, the first light will dim at the exact time that the next light is brightened, then 0.5s between changes.

Jason Spradlin
  • 1,407
  • 9
  • 14
  • Were we in more advanced javascript, I'd recommend using jQuery's promise library (or preferably an even better js promise library) – Jason Spradlin Jan 28 '16 at 21:14
  • This works well except for one problem. If part of the sequence repeats then the class won't be removed, the div will stay highlighted for the entire period. - https://jsfiddle.net/4w03jkyp/1/ – shartshooter Jan 28 '16 at 23:09
  • Excellent point. A fadeIn/fadeOut should be in order. Or CSS3 transitions – Jason Spradlin Jan 29 '16 at 17:10
1

Try using Array.prototype.slice() to make copy of original array, Array.prototype.shift() , recursive call to an IIFE

var arr = [1, 4, 2, 3];

(function animate(elem) {
  var el = elem.shift();
  if (el) {
  $("#" + el).animate({
    opacity: 1
  }, 500, function() {
    $(this).animate({opacity:0.2}, 500, animate.bind(null, elem))
  })
  }
}(arr.slice(0)));
.square {
  width: 200px;
  height: 50px;
  opacity: 0.2;
  display:inline;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<div class="container">
  <div id="1" class="red square">1</div>
  <div id="2" class="yellow square">2</div>
  <div id="3" class="blue square">3</div>
  <div id="4" class="green square">4</div>
</div>

I'm looking to use the function you've created to iterate through several loops. Without repeating code, how could I do that?

Try returning jQuery .promise() from animate function, using recursion to call animate on each item within arrays

var arr = [1, 4, 2, 3];
var arr2 = [2, 3, 2, 3];
var arr3 = [4, 3, 2, 3];
var arrays = [arr, arr2, arr3];
var a = arrays.slice(0);

function animate(elem) {
  var el = elem.shift();
  if (el) {
    // added `return` 
    return $("#" + el).animate({
      opacity: 1
    }, 500, function() {
      return $(this).animate({
        opacity: 0.2
      }, 500)
    // return jQuery promise object from `animate` function
    }).promise().then(animate.bind(null, elem))
  } 
}; //Need to iterate through `arrays` if possible. 
(function cycle() {
  return animate(a.shift())
  // recursively call `cycle` for each item within copy of `arrays`: `a`
  // if `a.length` is not `0`
  .then(a.length && cycle)
}())
.square {
  width: 200px;
  height: 50px;
  opacity: 0.2;
  display: inline;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<div class="container">
  <div id="1" class="red square">1</div>
  <div id="2" class="yellow square">2</div>
  <div id="3" class="blue square">3</div>
  <div id="4" class="green square">4</div>
</div>

jsfiddle https://jsfiddle.net/ry40t266/9/

guest271314
  • 1
  • 15
  • 104
  • 177
  • this works really well. I'm noticing that if I use this function within a loop it's causing some issues. Any thoughts on this - https://jsfiddle.net/ry40t266/ – shartshooter Jan 28 '16 at 22:56
  • @user3195487 A loop is not needed, recursion is used to iterate `arr` until no items remain within `arr` https://jsfiddle.net/ry40t266/1/ , see updated post – guest271314 Jan 28 '16 at 23:24
  • sorry for not making clear. I'm looking to use the function you've created to iterate through several loops. Without repeating code, how could I do that? I've added some comments here - https://jsfiddle.net/ry40t266/6/ – shartshooter Jan 29 '16 at 04:02
0

You can also try to use CSS3 features:

<!DOCTYPE html>
<html>
<head>
<style> 
#para1{
    width: 100px;
    height: 100px;
    background-color: yellow;
    -webkit-animation-name: example; /* Chrome, Safari, Opera */
    -webkit-animation-duration: 1s; /* Chrome, Safari, Opera */
    animation-name: anime1;
    animation-duration: 1s;
}

/* Chrome, Safari, Opera */
@-webkit-keyframes anime1 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}

/* Standard syntax */
@keyframes anime1 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}

#para2 {
    width: 100px;
    height: 100px;
    background-color: green;
    -webkit-animation-name: example; /* Chrome, Safari, Opera */
    -webkit-animation-duration: 5s; /* Chrome, Safari, Opera */
    animation-name: anime2;
    animation-duration: 5s;
}

/* Chrome, Safari, Opera */
@-webkit-keyframes anime2 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}

/* Standard syntax */
@keyframes anime2 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}

#para3 {
    width: 100px;
    height: 100px;
    background-color: red;
    -webkit-animation-name: example; /* Chrome, Safari, Opera */
    -webkit-animation-duration: 10s; /* Chrome, Safari, Opera */
    animation-name: anime3;
    animation-duration: 10s;
}

/* Chrome, Safari, Opera */
@-webkit-keyframes anime3 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}

/* Standard syntax */
@keyframes anime3 {
    from {opacity: 0;}
    to {opacity: 0.9;}
}
</style>
</head>
<body>


<div id="para1"></div>
<p>Text</p>
<div id="para2"></div>
<p>Text</p>
<div id="para3"></div>


</body>
</html>

this generates a 'stoplight' that changes opacity from top to bottom without any JS to begin with. You just change the duration of your effect by using animation-duration attribute. Note: This example does not work in Internet Explorer 9 and earlier versions.

Leo Skhrnkv
  • 1,513
  • 16
  • 27