DEV Community

Cover image for ReactJS typewriter effect inspired by Carmen Sandiego 1991 MS-DOS
Julien Verkest
Julien Verkest

Posted on

ReactJS typewriter effect inspired by Carmen Sandiego 1991 MS-DOS

Spoiler: the result

In the early 90's I used to play at Where In The World Is Carmen Sandiego game. Now I'm older, I don't play anymore but I decide to make a clone of the 1991 MS-DOS version with ReactJS.

Part 1: HTML and CSS

<div class="cs-instructions">
  <div class="cs-instructions-inner">
      <p>****** FLASH ******</p>
      <p>National treasure stolen from Paris.</p>
      <p>The treasure has been identified as the elevator from the Eiffel Tower.</p>
      <button type="button" onClick="nextStep();">Click to continue</button> 
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
.cs-instructions {
  background-image: url('https://i.imgur.com/1woliuF.png');
  background-size: contain;
  background-position: bottom left;
  background-repeat: no-repeat;
  position: relative;
  z-index: 1;
  height:342px;
  width: 467px;
} 

.cs-instructions-inner {
  background-color:#fff;
  background-image: url('https://i.imgur.com/6aOej4c.jpg');
  background-size: contain;
  background-position: bottom left;
  background-repeat:repeat-y;
  font-size:14px;
  line-height:30px;
  padding:10px 30px 10px 30px;
  margin:10px 0 0 38px; 
  min-height:120px;
  width:330px;
  overflow-y:hidden;
  position: absolute;
  color:#000;
  z-index: 2;
  bottom:190px;
}
Enter fullscreen mode Exit fullscreen mode

Basically we need two <div>:

  • The first with class name cs-instructions is the typewriter machine which has a position: relative; property with this background image bg typewriter
  • And the second with class name cs-instructions-inner is the typewriter paper on position: absolute;bottom:190px; with this background image which is repeat on y axis. bg paperbg paperbg paper We add a overflow-y:hidden to prevent the instructions scrolling.

Part 2: Typewriter and autoScroll VanillaJS

<div class="cs-instructions">
  <div class="cs-instructions-inner">
      <button type="button" onClick="launch();">Click to start the case</button> 
      <div class="typewriter"> </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

We add a typewriter div where the text will be display. The button have a onClick event which call a launch function.

autoScroll = () => {
    const el = document.getElementsByClassName('cs-instructions-inner');
    el.scrollTo(0, el.scrollHeight);
  }

  typeWriter = (string, i=0) => {
    if (i < string.length) {
      document.getElementsByClassName('typewriter')[0].innerHTML += string.charAt(i); // display character one by one
      i++;
      autoScroll;
      setTimeout(() => typeWriter(string, i), 40); // speed
    }
  }

 launch = () => {
   var string = '*** FLASH ***';
   string += 'National treasure stolen from Paris.'; 
   string += 'The treasure has been identified as the elevator from the Eiffel Tower.';
   string += 'Female suspect reported at the scene of the crime. Your assignement: Track the thief from Paris to her hideout and arrested her!';
   typeWriter(string);
 } 

Enter fullscreen mode Exit fullscreen mode

At the bottom we have the launch() function. We store text on a string variable and pass it into the recursive typeWriter() function with two parameters: string and index which is initialized to 0. We break out of recursive function when the index i of the string is equal at the length of the string. Inside the condition we call autoScroll() function to ensure we always are at the bottom of the div cs-instructions-inner.

Preview at this step

Part 3: ReactJs Typewriter component with sound effect

class Typewriter extends React.Component {
  constructor(props){
    super(props);
    this.myDiv = null;
    this.mySound = null;
    this.typewriter = element => {
      this.myDiv = element;
    }; 
    this.setmySound = element => {
      this.mySound = element;
    };

    this.playSound = this.playSound.bind(this);
    this.pauseSound = this.pauseSound.bind(this);
  }

  playSound() {
    if (this.mySound && (this.mySound.duration === 0 || this.mySound.paused)) {
      this.mySound.play();
    }
  } 

  pauseSound() { 
    if(this.mySound){
      this.mySound.pause();
    }
  }

  typeWriter = (s, i=0) => {
    if (this.myDiv && i < s.length) {
      this.myDiv.innerHTML += s.charAt(i);
      i++;
      if(i>5) { 
        this.props.setScroll(); 
      }
      this.playSound();
      setTimeout(() => this.typeWriter(s, i), 40);
    }
    else {
       this.pauseSound();
       if(this.props.nextStep){
        this.props.nextStep();
       } 
    }
  }

  componentDidMount() {
    if(this.myDiv) {
      setTimeout(() => this.typeWriter(this.props.mystring), 200);
    }
  }

  componentWillUnmount() {
    this.pauseSound();
  }

  render() {
    return (
      <div className={this.props.mb}> 
        <audio autoPlay preload="auto" ref={this.setmySound} src="https://freesound.org/data/previews/138/138049_2423928-lq.mp3" type="audio/mpeg" > </audio>
        <span ref={this.typewriter}> </span>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Our component look like <Typewriter mb={'mb-4'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="National treasure stolen from Paris."></Typewriter>

  1. JSX: we pass a margin-bottom mb props at the div wrapper and inside it we have an <audio>tag and a <span> tag. Both of them have a ref attribute because we need to access and interact with DOM nodes.We use the callback refs which gives more fine-grain control over when refs are set and unset. See more at the official documentation https://reactjs.org/docs/refs-and-the-dom.html
  2. componentDidMount(): we launch the recursive typeWriter() function when the component is mounted.
  3. Typewriter(s, i=0) It's not a pure function lol. Lot of things are realized. First the string is passed by the this.props.mystring props. Inside typeWriter() function we execute the autoScroll(), we play the sound and we insert characters one by one on the myDiv ref. And when the recursive function is breaking out we execute another function this.props.nextStep() and also we stop the sound effect this.pauseSound().
  4. playSound() pauseSound() sure we add and handle sound effect.
  5. componentWillUnmount() we stop the sound when the component is unmounted.

Part 4: ReactJs Handle steps component

class App extends React.Component {
  constructor(){
    super();
    this.state = {
      step: 0
    }
    this.myDiv = null;
    this.instructions = element => {
      this.myDiv = element;
    };
    this.setScroll = this.setScroll.bind(this);
  }

  setScroll() {
    this.myDiv.scrollTo(0, this.myDiv.scrollHeight);
  }

  nextStep = () => this.setState({step: this.state.step + 1});

  render(){
    return(
      <div className="wrapper">
        <div className="cs-instructions">
          <div className="cs-instructions-inner" ref={this.instructions}>
            {
              this.state.step === 0
              ? <button onClick={() => {this.nextStep()}}>CLICK TO START THE CASE</button>
              : ''
            }

            {
              this.state.step > 0 
              ? <Typewriter mb={'mb-4'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="****** FLASH ******"></Typewriter>
              : ''
            }

            {
              this.state.step > 1 
              ? <Typewriter mb={'mb-4'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="National treasure stolen from Paris."></Typewriter>
              : ''
            }

            {
              this.state.step > 2 
              ? <Typewriter mb={'mb-0'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="The treasure has been identified as the elevator from the Eiffel Tower."></Typewriter>
              : ''
            }


            {
              this.state.step > 3 
              ? <button className="mb-4" onClick={() => {this.nextStep()}}>Click to continue</button>
              : ''
            }
            {
              this.state.step > 4 
              ? <Typewriter mb={'mb-0'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="Female suspect reported at the scene of the crime. Your assignement: Track the thief from Paris to her hideout and arrested her!"></Typewriter>
               : ''
            }

             {
              this.state.step > 5 
              ? <button className="mb-4" onClick={() => {this.nextStep()}}>Click to continue</button>
              : ''
            }

            {
              this.state.step > 6 
              ? <Typewriter mb={'mb-0'} setScroll={this.setScroll} mystring="You must apprehend the thief by Sunday, 5 pm. Good luck ! "></Typewriter>
               : ''
            }
          </div>
        </div>
        <div className="credits">Typewriter effect realised with ReactJS inspired by Carmen Sandiego 1991 MS-DOS <br/><a href="https://www.julien-verkest.fr/" target="_blank">Julien Verkest Šī¸ april 2019 </a> </div>
       </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Constructor(): we declare a state with a step init to 0 and we have a nextStep() function which increase the number of steps. We have also the setScroll function and the ref instructions.
  2. JSX: Steps are inside conditionals brackets, maybe we need to refactor the code but it's works!

Part 5: The result

Top comments (1)

Collapse
 
dance2die profile image
Sung M. Kim

😁 This is wonderfully satisfying