Typewriter effect in JavaScript for dialogues

160 Views Asked by At

Here's my dialogue system for the Entity class in Entity.js

 drawDialogue(context, element) {

     // Draw the dialogue box and face graphic
      context.drawImage(this.sprite.dialogueBox, 0, 90);
      context.drawImage(this.sprite.faceset, 6, 103);
    
      // Create a container for the text
      const dialogueContainer = document.createElement('div');
      dialogueContainer.className = 'dialogue-container';
  
      // Create a text element for the dialogue
      const textElement = document.createElement('p');


      // Create a container for the text
      const nameContainer = document.createElement('div');
      nameContainer.className = 'name-container';
  
      // Create a text element for the dialogue
      const nameElement = document.createElement('p');
      nameElement.innerText = this.displayName;
      nameElement.className = 'name-text';
      
      //...

      textElement.className = 'dialogue-text';
      textElement.innerText = this.text;

      // // Add the text to the dialogue container
      dialogueContainer.appendChild(textElement);
      element.appendChild(dialogueContainer);

      // // Add name to the dialogue name container
      nameContainer.appendChild(nameElement);
      element.appendChild(nameContainer);
    }
  }

I am only trying to add a typewriter effect to the textElement.innerText and nothing else. I am struggling to do so as I am not sure how to approach it and how to append each letter to the dialogue text container one by one.

This system works perfectly fine, as the dialogue is displayed correctly and with the correct text when the player interacts with this entity instance, however, I am trying to add a typewriter effect to it.

Although, it may really seem to implement a typewriter effect an RPG game like mine, I am using a game loop which updates the state of the game at a constant frame rate, this method is directly called in the game loop as seen in the following code block:

 Loop() {
    const gameLoop = () => {
       
        //Check if any entity is interacting with the player
        Object.values(entities).forEach(entity => {
          //If an entity is interacting...
          if (entity.interacting) {
            //Display the dialogue
            entity.drawDialogue(this.context, this.element);
          }
        });

       //Move on to the next frame
       requestAnimationFrame(gameLoop);
    };

    //Call the game loop method again
    gameLoop();
  }

As you can see, I am calling this function when the player interacts with an NPC.

Do you have any approaches that I can pursue ensuring that my game is able to manage dialogues using a typewriter effect flawlessly? (similarly to that of Pokémon FireRed)

I have tried creating a different class for typewriter text which prints each letter one by one, but it didn't seem to work unfortunately hence why I am asking about this issue on Stack Overflow.

3

There are 3 best solutions below

0
Thomas Frank On

Here's a fairly generic solution:

async function typeWriteTextToElement(cssSelectorOrElement, text, delayPerCharMs = 50) {
  let sleep = ms => new Promise(res => setTimeout(res, ms));
  let element = cssSelectorOrElement;
  element = typeof element === 'string' ? document.querySelector(element) : element;
  for (let i = 1; i <= text.length; i++) {
    element.innerText = text.slice(0, i);
    await sleep(delayPerCharMs);
  }
}

// Test
document.body.innerHTML = '<div style="padding:10px 20px"></div>'.repeat(6);
for (let i = 1; i <= 6; i++) {
  typeWriteTextToElement(`div:nth-child(${i})`,
    'Hi there! How do you like typewriters?', i * 20, i);
}

0
kennarddh On

Simple typewritter animation example

const targets = document.querySelectorAll('.typewritter')

const parseIntHelper = (str) => {
  const result = parseInt(str, 10)

  return Number.isNaN(result) ? undefined : result
}

targets.forEach(target => {
  const text = target.getAttribute('data-text')
  const charDelayString = target.getAttribute('data-char-delay')
  const endDelayString = target.getAttribute('data-end-delay')

  const charDelay = parseIntHelper(charDelayString, 10)
  const endDelay = parseIntHelper(endDelayString, 10)

  const type = (init, text, index, charDelay = 50, endDelay = 1000) => {
    const isEnd = text.length - 1 === index

    if (!init && index === 0)
      target.textContent = ""

    target.textContent += text[index]

    setTimeout(() => {
      type(false, text, isEnd ? 0 : index + 1, charDelay, endDelay)
    }, isEnd ? endDelay : charDelay)
  }

  type(true, text, 0, charDelay, endDelay)
})
<p class="typewritter" data-text="Hello world, Typing..."></p>
<p class="typewritter" data-text="Hello world2, Typing2..."></p>
<p class="typewritter" data-text="Foo, Bar, Foo, Bar"></p>
<p class="typewritter" data-text="Test, This is typewriter"></p>
<p class="typewritter" data-text="Test 2 seconds end delay" data-end-delay="2000"></p>
<p class="typewritter" data-text="Test 100 ms char delay" data-char-delay="100"></p>
<p class="typewritter" data-text="Test 0 ms char delay" data-char-delay="0"></p>

0
TK421 On

A typewriter effect that uses Promises / array.forEach() / setTimeout. Asynchronous and as condensed as I could make it. Enjoy!

async function typeText(elId, text) {
  let promise = Promise.resolve();

  text.split('').forEach(el => {
    promise = promise.then(() => new Promise(resolve => {
      document.getElementById(elId).innerHTML += el;
      setTimeout(resolve, 125);
    }));
  });

  await promise;
}

typeText("terminalText1", "Hello, my name is { NPC #1 }, what can I do for you?").then(()=> console.log("Terminal 1 typing complete!"));

typeText("terminalText2", "Hello, my name is { NPC #2 }, what quest would you like to start?").then(()=> console.log("Terminal 2 typing complete!"));
.terminal {
    width: 300px;
    height: 65px;
    background: #000;
    color: #fff;
    margin: 5px auto;
    box-sizing: border-box;
    padding: 15px;
    border-radius: 5px;
}

.cursor {
    margin-left: 3px;
  border-right: 0.2em solid #fff;
}

.typing {
  animation: blink-caret 0.75s step-end infinite;
}

@keyframes blink-caret {
  from,
  to {
    border-color: transparent;
  }
  50% {
    border-color: #fff;
  }
}
<div class="terminal">
    <span id="terminalText1"></span>
    <span class="cursor typing"></span>
</div>

<div class="terminal">
    <span id="terminalText2"></span>
    <span class="cursor typing"></span>
</div>