0

I have a class test whose background-color I want to flip between lime and green faster and faster.

For that, I'm using a for loop variable and passing it to a function containing a setTimeout(), but it's not working.

(This is not a duplicate question. The said "original" is about a simple setTimeout() whereas this question is about a setTimeout() within a for loop. I understand that the answers on that question might indirectly answer mine, but the questions themselves aren't the same)

$(document).ready(function() {

  for (var i = 0; i < 20; i++) {
    delay(i);
    $(".test").css('background-color', 'lime');
  }

});

function delay(i) {
  setTimeout(function() {
    $(".test").css('background-color', 'green');
  }, 1000 - 50 * i);
}
.test {
  width: 300px;
  height: 300px
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="test"></div>
  • 1
    What are you expecting to happen? You set `.test` to lime in a loop which takes about a nanosecond to run. So the element's background is lime. Then the first timeout elapses and changes it to green. And it stays green until the next timeout, at which point it is changed to green. – Heretic Monkey Apr 16 '18 at 21:12
  • I called the `delay()` function from within the for loop, so I was expecting the statement in the loop where the color is changed to `lime` would be executed after the `setTimeout()` had completed. – Somesh Thakur Apr 16 '18 at 21:17
  • Possible duplicate of [Sleep in JavaScript - delay between actions](https://stackoverflow.com/questions/758688/sleep-in-javascript-delay-between-actions) – Heretic Monkey Apr 16 '18 at 21:28
  • I think that one is about a simple `setTimeout`.. It doesn't directly answer my question.. – Somesh Thakur Apr 17 '18 at 04:52
  • I'm sorry but none of the answers here are really helpful. They either just don't work or are overkill (in my opinion). I just nested the `setTimeout()` inside a `setInterval()` and it's working. I'm sorry if my question wasn't clear enough. Thanks to everyone for your time! :) – Somesh Thakur Apr 17 '18 at 08:47

6 Answers6

1

The problem is the loop executes faster than the timeout. setTimeout function basically says execute the given function after a certain time. The for loop you created there will continue without waiting for the code inside the setTimeout function to be executed, In other words your code producing 20 functions that will be executed in the future.

There are many way to produce the functionality you need. To keep it simple and solve it you should create two functions instead:

$(document).ready(function() {

  for (var i = 0; i < 20; i++) {
    delay_lime(i);
    delay_green(i+1);
  }

});

function delay_green(i) {
  setTimeout(function() {
    $(".test").css('background-color', 'green');
  }, 1000 - 50 * i);
}

function delay_lime(i) {
  setTimeout(function() {
    $(".test").css('background-color', 'lime');
  }, 1000 - 50 * i);
}
Eden Reich
  • 387
  • 2
  • 7
1

Try this way:

for(var i=0;i<20;i++)
{
  delay(i);
}

function delay(i) {
  setTimeout(function() {
      if (i%2 == 0) {
        $(".test").css('background-color', 'green');      
      } else {
        $(".test").css('background-color', 'lime');
      }

  }, 1000 - 50 * i);
}
Petr Lazarev
  • 3,102
  • 1
  • 21
  • 20
  • $(".test").css('background-color', ['green', 'lime'][i%2]) – Raith Apr 16 '18 at 21:24
  • Note that retaining the original maths from the poster (1000 - 50 * i) retains one of his mistakes and will produce a regular flashing effect rather than an increasing one, as requested. – Raith Apr 16 '18 at 21:26
1

try this: here is a example example

$(document).ready(function() {

  delay();

  var start = 0;

   delay(start);

   function delay(start) {

       setTimeout(function() {

           if(start == 0 ){
              $(".test").css('background-color', 'green');
              start = 1;
            }else{
               $(".test").css('background-color', 'red');
                start = 0;
            }

           delay(start);

        }, 100);
    }

});
Klodian
  • 633
  • 6
  • 18
0

If you want to use a for loop, you should turn its containing function into an async function and await promises that resolve at the desired time:

const delay = (i) => new Promise(resolve => {
  setTimeout(resolve, 1000 - 50 * i);
});
function changeToGreen() {
  $(".test").css('background-color', 'green');
}
function changeToLime() {
  $(".test").css('background-color', 'lime');
}
(async () => {
  for (var i = 0; i < 20; i++) {
    await delay(i);
    changeToLime();
    await delay(i);
    changeToGreen();
  }
})();
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Using async and await to convert this into a synchronous solution is interesting, but I think it'd be better to improve the original setTimeout solution - especially given the level of knowledge the OP seems to demonstrate. Do you think this is easier to understand?! – Raith Apr 16 '18 at 21:28
0

Your loop doesn't wait for any of the timeouts to occur, it runs through and queues up the events which will fire at the relevant intervals.

However, whilst doing so it sets the background color to lime a number of times.

After the loop has finished, the queued intervals start firing, and they set the background color to green a number of times.

But the colours do not alternate as the code execution is not in the order you expect.

Also, the multiple calls to setInterval queue the events to be fired after the specified delay. The code does not wait for the allotted time and then fire the next one. So your could of 1000 - 50 * i actually queues the latest event first, and so on until it queues the event that will actually fire first. Does that make sense? It will be more intuitive for you to set these in the order that they will fire. You could achieve the reducing delay by incrementing the timeout by a variable which reduces, e.g.

time = 1000;
delay = 1000;
setTimeout (blah, time);
time += delay;
delay -= 50;
setTimeout (blah, time);
// etc.

You could achieve an alternating effect by setting alternate intervals to be green and lime. For that a simple toggle variable would help.

color = 1;

color = 1 - color; // toggles between 0 and 1
useColor = ["lime", "green"][color];

I shan't rewrite your entire program for you, but I can assist more if you have specific questions. The best way to learn is to do.

Raith
  • 528
  • 3
  • 8
  • This is a rather nasty example, but it demonstrates a single timed function which enqueues itself, alternates the colour, and limits the final flash rate. https://jsfiddle.net/xpvt214o/137312/ – Raith Apr 16 '18 at 21:51
0

There is a slight misunderstanding about the way timeouts work in the example code. Timeouts are asynchronous, meaning that they execute out of the normal order of execution. As a result, the lime green is shown immediately, and then at various times later the background is repeatedly changed to green; although, the only time the change is noticed is the first time as changing from green to green has no effect.

setTimeout creates a task, JavaScript in a browser is single threaded and will execute tasks through a task scheduler.

Using 1000 - 50 * i from 0 to 19 in the approach shown in the question will result in timeouts being scheduled for execution. First at 1000, then at 950, etc. However, they are all scheduled at the exact same time. So there is no difference scheduling them in forward or reverse order as the only relevant metric used is the time. Essentially the result is that every 50 milliseconds, the background color is set to green in this example.

Unfortunately, tasks that get executed in the browser are not executed exactly on time, and using this will aim at 50 milliseconds per call, but due to Operating System scheduling and depending on the system in use the result could be wildly different.

This could have been done with an interval just as easily, where the interval used was 50 milliseconds (although it would still suffer from the aforementioned OS issue). That said, there is no acceleration being used there. A better approach here, since we are dealing with animation (the colors flashing) would be to instead use requestAnimationFrame MDN.

requestAnimationFrame will attempt to run your code at 60 frames per second, or roughly 16.6 milliseconds per frame (1000 milliseconds / 60 frames).

Given that the goal was acceleration, a rate could be put in place to ramp the flashing.

// Cache the constructed jQuery object for element with class "test"
var testCache = $('.test');

// Create a set of colors to use in the flashing
var colors = ['lime','green'];

// Use a variable for a switch between the two colors
var colorSwitch = 0;

// Keep track of how many times the color has flashed
var i = 0;

// Used for tracking the start of an animation sequence
var start;

// In order to facilitate acceleration, use a function for 
// determining the time between flashes,
// used an offset x^2 line at (20,16) with a 2x width
// y = 1/2(x-19)^2 - 19x + 16
var ft = t => 0.5*(t-19)*(t-19) - (t-19) + 16;

// This function will be called every 16.6 milliseconds
// by requestAnimationFrame, the timestamp is automatically injected
(function flashAccel(timestamp){

 // Loop control to ensure only 20 flashes occur
 if(i >= 20) return;

 // Track the start of the timing for the animation sequence
 start = start || timestamp;

 // This is the milliseconds since the last sequence was updated
 var elapsed = timestamp - start;

 // Check to see if enough time has elapsed based on the acceleration 
 // function's value and the current value, if it has then update the view
 if( elapsed > ft(i) ){
 
     // Swaps between 0 and 1
     colorSwitch = 1 - colorSwitch;

     // Selects 0 or 1 indexed color
     var color = colors[colorSwitch];
     testCache.css('background-color',color);

     // Update metrics
     i++;
     start = timestamp;
 }

 // Request the function to be called again in roughly 16.6 milliseconds
 window.requestAnimationFrame(flashAccel);

})()
.test {
  width: 300px;
  height: 300px
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="test"></div>
Travis J
  • 81,153
  • 41
  • 202
  • 273