user113581321
user113581321

Reputation: 137

setTimeout inside setTimeout inside a setInterval

I'm trying to make a string that will write itself letter by letter until completing the sentence, and the speed of appearing each letter is based on an input that varies from 1 to 10. At the end of the string, it will blink for 5 seconds until that an alien will appear. My idea was to create a setInterval to add the letters and when the counter added the array size it would return the final animation of the loop with the new setInterval call, and before it was called again it had already been cleared, and called again in a recursion by setTimout callback to maintain the infinite loop. But it's not reaching setTimout, why?

//script.js

const speedInput = document.getElementsByClassName('speed--input')[0];
const alien = document.getElementsByClassName('alien')[0];
const textDiv = document.getElementsByClassName('text')[0];
const textShow = document.getElementsByClassName('text--show')[0];

const textDB = 'We go to dominate the world.';
const textStr = '';

let count = 0;
let speed = speedInput.value * 100;

const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
  if (count < textDB.length) {
    textStr += textDB[count];
    textShow.innerHTML = textStr;
    textDiv.style.width = `${40 * count}px`;
    count++;
  } else {
    textShow.style.animation = 'bip 1s linear 1s infinite'
    return () => {
      setTimeout(() => {
        textShow.style.animation = '';
        textStr = '';
        count = 0;
        textShow.style.opacity = 0;
        alien.style.opacity = 1;

        setTimeout(() => {
          alien.style.opacity = 0;
          textShow.style.opacity = 1;
          textChangeHandle(count, textStr, textDB)
        }, 5000)
      }, 5000);
      clearInterval(textChangeHandle);
    }
  }
}, speed)

textChangeHandle(count, textStr, textDB);

//index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style/style.css">

  <title>Document</title>
</head>
<body>
  <div class="text">
    <p class="text--show"></p>
    <img class="alien" src="alien.png" alt="Aki é Jupiter karai">
  </div>

  <div class="speed">
    <span>Speed</span>
    <input class="speed--input" type="number" min="1" max="10" value="1">
  </div>

  <script src="script.js"></script>
</body>
</html>

//style.css

*,
*::after,
*::before {
  margin: 0;
  padding: 0;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
}

body {
  font-family: sans-serif;
  background-color: #3cd070;
}

.text {
  position: fixed;
  top: 50%;
  left: 50%;
  height: auto;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  font-size: 40px;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
}

.text .alien {
  opacity: 0;
  height: 600px;
  width: auto;
  margin-bottom: 50px;
  position: absolute;
}

.text .text--show {
  font-size: 40px;
  width: 100%;
  position: absolute;
  display: block;
  text-align: center;
  -webkit-animation: bip 1s linear 1s infinite;
          animation: bip 1s linear 1s infinite;
}

.speed {
  position: fixed;
  top: 90%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
  background-color: rgba(0, 0, 0, 0.2);
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  padding: 10px 20px;
  -webkit-box-align: center;
      -ms-flex-align: center;
          align-items: center;
  -webkit-box-pack: center;
      -ms-flex-pack: center;
          justify-content: center;
}

.speed .speed--input {
  border: 0;
  outline: 0;
  width: 40px;
  height: 25px;
  margin: 10px;
  text-align: center;
}

@-webkit-keyframes bip {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes bip {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

Upvotes: 2

Views: 164

Answers (3)

The Fool
The Fool

Reputation: 20547

There is also the animation frame api

It will run your function exactly before the next "natural" repaint of the browser.

Additionally and probably useful in your case, it will pass the current time in your callback, so you can take a time detail and animate according to this. That way you can get a smooth animation of equal length for everyone, even when the frame rate is inconsistent.

Consider the following snippet, which will basically cause an infinite loop.

function draw(now) {
   requestAnimationFrame(draw)
}

requestAnimationFrame(draw)

Now you only need to remember the time and take the time delta next time the function is called. If enough time has passed, you can write another character. (Normally you would change a value divided by the time delta, but since you have characters, they will be either there or not.

let speed = 300
let timePassedSinceLastChar = 0
let then = null

function draw(now){
  now *= 0.001;
  const deltaTime = now - then;
  then = now;
  timePassedSinceLastChar += deltaTime;
  if (timePassedSinceLastChar >= speed) {
     drawChar()
     timePassedSinceLastChar = 0
  }
  requestAnimationFrame(draw)
}

requestAnimationFrame(draw)

Furthermore, the requestAnimationFrame function returns the id of the requested frame. That allows to cancel the loop.

const frameId = requestAnimationFrame(draw)
cancelAnimationFrame(frameId)

So the final code could look something like this.

const textDB = 'We go to dominate the world.';
let charIndex = 0;
let frameId = null
let then = 0
let sinceDraw = 0
let speed = () => Math.random() * 0.4 + 0.05 // randomize the speed a bit
let $p = document.querySelector('p')

function draw(now) {
  // cancel on end of string
  if (charIndex >= textDB.length) {
    cancelAnimationFrame(frameId)
    console.log('done')
    return
  }

  // get the time delta
  now *= 0.001; // convert to seconds
  const deltaTime = now - then;
  then = now;
  sinceDraw += deltaTime

  // if its time to draw again, do so
  if (sinceDraw >= speed()) {
    let char = textDB[charIndex]
    $p.textContent += char
    charIndex++
    sinceDraw = 0
  }
  // request another frame
  frameId = requestAnimationFrame(draw)
}

// request the first frame
frameId = requestAnimationFrame(draw)
<p></p>

Upvotes: 2

user113581321
user113581321

Reputation: 137

I found the solution. With setInterval, the code was creating multiple instances of callbacks and overloading memory.

///script.js

const textEl = document.getElementById('text');
const inputEl = document.getElementById('input');
const alienEl = document.getElementById('alien');

const text = 'We go to dominate the world!';

let idx = 0;
let speed = 300 / inputEl.value;

writeText();

function writeText () {
  if(idx === 0) setTimeout(() => textEl.style.opacity = 1, speed)

  textEl.innerText = text.slice(0, idx);
  
  idx++;
  
  if (idx > text.length) {
    idx = 0;
    textEl.style.animation = 'bip 1s linear 1s infinite';
    setTimeout(() => {
      textEl.style.animation = ''
      textEl.style.opacity = 0; 
      setTimeout(() => {
        alienEl.style.opacity = 1;
        setTimeout(() => {
          alienEl.style.opacity = 0;
          setTimeout(writeText, speed);
        }, 5000)
      }, 1000);
    }, 5000)  
  } else {
    setTimeout(writeText, speed);
  }
}

inputEl.addEventListener('input', (e) => speed = 300 / e.target.value);

Upvotes: 0

Dan Mullin
Dan Mullin

Reputation: 4415

The issue is that in the else statement, you are returning a function that is never called.

return () => {
  setTimeout(() => {
    textShow.style.animation = '';
    textStr = '';
    count = 0;
    textShow.style.opacity = 0;
    alien.style.opacity = 1;

    setTimeout(() => {
      alien.style.opacity = 0;
      textShow.style.opacity = 1;
      textChangeHandle(count, textStr, textDB)
    }, 5000)
  }, 5000);
  clearInterval(textChangeHandle);
}

Just remove the return statment and call the setTimeout function directly.

const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
  if (count < textDB.length) {
    textStr += textDB[count];
    textShow.innerHTML = textStr;
    textDiv.style.width = `${40 * count}px`;
    count++;
  } else {
    textShow.style.animation = 'bip 1s linear 1s infinite'
    setTimeout(() => {
      textShow.style.animation = '';
      textStr = '';
      count = 0;
      textShow.style.opacity = 0;
      alien.style.opacity = 1;

      setTimeout(() => {
        alien.style.opacity = 0;
        textShow.style.opacity = 1;
        textChangeHandle(count, textStr, textDB)
      }, 5000)
    }, 5000);
    clearInterval(textChangeHandle);
  }
}, speed)

Upvotes: 0

Related Questions