2

I am trying to make a game with HTML, CSS and JS which involves a typewriting animation. I want a custom function which when called, typewrites some text. However, there is a problem when using the function multiple times. The function combines the text from the different strings which are inputted. in the function. Please help me find a solution to this. My code is similar to the W3 Schools example code with a few changes.

var txt, speed, i;
function typeWriter() {
  if (i < txt.length) {
document.getElementById("text").innerHTML += txt.charAt(i);
i++;
setTimeout(typeWriter, speed);
  }
}
function write(whatToWrite, howFast) {
  i = 0;
  txt = whatToWrite;
  speed = howFast;
  typeWriter();
}

//There is a problem when called multiple times
write("hello", 100);
write("world", 100);
html{
  background-color: #0a1c08;
  color: #ffffff;
  font-size: 20px;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>The Game of Destiny</title>
    <link href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap" rel="stylesheet">
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <p id="text"></p>
  </body>
</html>
<script src="typewriter.js"></script>
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62

2 Answers2

0

Like the first comment said,setTimeout doesn't block, and the next write is executed immediately. To prevent this behaviour, you need to block the execution somehow. One way would be to use a callback in the timeout function, another to await a Promise. A good explanation can be found in this post.

To solve the problem, I used a custom sleep function, which blocks the execution as long as the typing animation goes. It is not perfect and could use some more refinement, but it works.

var speed;
function typeWriter(i, txt) {
 if (i < txt.length) {
    document.getElementById("text").innerHTML += txt.charAt(i);
    i++;
    setTimeout(() => typeWriter(i, txt), speed);
  }
}

async function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
};

async function wrapper() {
  let word = "hello";
  speed = 100;
  await typeWriter(0, word);
  await sleep(word.length * speed);

  word = "world";
  speed = 200;
  await typeWriter(0, word);
}
wrapper()
html{
  background-color: #0a1c08;
  color: #ffffff;
 font-size: 20px;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>The Game of Destiny</title>
    <link href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap" rel="stylesheet">
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
  <p id="text"></p>
  </body>
</html>
<script src="typewriter.js"></script>
Gh05d
  • 7,923
  • 7
  • 33
  • 64
0

setTimeout doesn't block, and the next write is executed immediately after the first write has printed the first character. Then i will be reset to 0, and only the second call of write executes correctly.

This can be fixed by using promises, or simply by chaining the words to type in write function, like this:

var txt = '',
  i = 0,
  pad = document.getElementById('text'),
  speed;
function typeWriter() {
  if (i < txt.length) {
    pad.insertAdjacentText('beforeEnd', txt.charAt(i));
    i++;
    setTimeout(typeWriter, speed);
  }
}

function write(whatToWrite, howFast) {
  txt += whatToWrite;
  speed = howFast;
  typeWriter();
}

write('hello', 100);
write(' world', 100);
html {
  background-color: #0a1c08;
  color: #ffffff;
  font-size: 20px;
}
<p id="text"></p>

Notice, that txt and i are now initialized at the top scope instead of write function. I've also defined the element to type to outside of the function, and used insertAdjacentText instead of innerHTML. These changes make the code a bit lighter to run, specifically if you're going to type very long texts to the paragraph.

Teemu
  • 22,918
  • 7
  • 53
  • 106