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>
.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;
}
Basically we need two <div>
:
- The first with class name
cs-instructions
is the typewriter machine which has aposition: relative;
property with this background image - And the second with class name
cs-instructions-inner
is the typewriter paper onposition: absolute;bottom:190px;
with this background image which is repeat on y axis. We add aoverflow-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>
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);
}
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>
);
}
}
Our component look like <Typewriter mb={'mb-4'} nextStep={this.nextStep} setScroll={this.setScroll} mystring="National treasure stolen from Paris."></Typewriter>
-
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 aref
attribute because we need to access and interact withDOM 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 - componentDidMount(): we launch the recursive typeWriter() function when the component is mounted.
-
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 themyDiv ref
. And when the recursive function is breaking out we execute another functionthis.props.nextStep()
and also we stop the sound effectthis.pauseSound()
. - playSound() pauseSound() sure we add and handle sound effect.
- 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>
);
}
}
-
Constructor(): we declare a state with a
step
init to 0 and we have anextStep()
function which increase the number of steps. We have also thesetScroll
function and theref instructions
. - JSX: Steps are inside conditionals brackets, maybe we need to refactor the code but it's works!
Part 5: The result
Top comments (1)
đ This is wonderfully satisfying