hotate-issimoの日記

超絶マイペースに自己研鑽に励む

JavaScriptで1文字ずつ表示するやつ

こういうの↓。タイプライター風というらしい。

f:id:hotate-issimo:20190517221433g:plain
タイプライター風gif
こんな感じで作ってみた。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript">
	window.onload = function(){
		typewriter('はたらけど\nはたらけど猶わが生活楽にならざり\nぢつと手を見る', 'main');
	}
	
	//**************************************************
	// 文字列を1文字ずつ表示
	// text : 表示する文字列
	// id : 対象要素
	//**************************************************
	function typewriter(text, id){
		var texts = text.split('\n');
		var target = document.getElementById(id);
		var index = 0;
		var i = 0;
		
		var intervalID = setInterval(type, 100);
		
		function type(){
			if(i < texts.length && index < texts[i].length){
				target.insertAdjacentHTML('beforeend', texts[i].charAt(index++));
			} else if(index >= texts[i].length) {
				i++;
				if(i == texts.length){
					// 文字列を表示し終えたらsetIntervalを無効にして終了
					clearTimeout(intervalID);
				} else {
					// 次の行へ
					target.insertAdjacentHTML('beforeend', '<br>');
					index = 0;
				}
			} 
		}
	}
</script>
</head>
<body>
	<div id="main"></div>
</body>
</html>

type関数を作成し1文字表示したり改行したり。そしてsetIntervalを利用して待ち時間100msごとにtype関数を繰り返し呼び出すことで1文字ずつ表示する処理を実現。

しかしおそらく、setIntervalではなくPromiseというのを利用するのが今時の書き方?存在自体初めて知ったので、きちんと調べて書き換えてみたいと思う。

以下、失敗の記録。関数部分だけ記載。
◆初版

function typewriter(text, id){
	var target = document.getElementById(id);
	var index = 0;
	var write = '';
	
	var intervalID = setInterval(type, 100);
	
	function type(){
		if(index < text.length){
			write += text.charAt(index++);
			target.innerHTML = write;
		} else if(index >= text.length) {
			// 文字列を表示し終えたらsetIntervalを無効にして終了
			clearTimeout(intervalID);
		}
	}
}

f:id:hotate-issimo:20190517223701g:plain
初版の実行結果
改行されないんですけどぉ!
HTMLとして表示するから、改行は<br>にしてあげないと駄目だった。

◆第2版

function typewriter(text, id){
	text = text.replace(/\n/g, '<br>');
	var target = document.getElementById(id);
	var index = 0;
	var write = '';
	
	var intervalID = setInterval(type, 100);
	
	function type(){
		if(index < text.length){
			write += text.charAt(index++);
			target.innerHTML = write;
		} else if(index >= text.length) {
			// 文字列を表示し終えたらsetIntervalを無効にして終了
			clearTimeout(intervalID);
		}
	}
}

f:id:hotate-issimo:20190517224438g:plain
第2版の実行結果
\nを<br>に置換。
replaceメソッドは第1引数を正規表現で書いてあげると全置換してくれるようになる。 /置換元/g という書き方。詳しくは『グローバルマッチ』で検索!
…で、一瞬<が見えるのがとても気になる。

◆第3版

function typewriter(text, id){
	if(text.match('\n')){
		var texts = text.split('\n');
		for(var i = 0 ; i < texts.length; i++){
			typewriter(texts[i], id);
			document.getElementById(id).insertAdjacentHTML('beforeend', '<br>');
		}
		return;
	}
	var target = document.getElementById(id);
	var index = 0;
	
	var intervalID = setInterval(type, 100);
	
	function type(){
		if(index < text.length){
			target.insertAdjacentHTML('beforeend', text.charAt(index++));
		} else if(index >= text.length) {
			// 文字列を表示し終えたらsetIntervalを無効にして終了
			clearTimeout(intervalID);
		}
	}
}

f:id:hotate-issimo:20190517230019g:plain
第3版の実行結果
改行ごとに区切って文字列を表示すれば良いんでしょ、という発想。1行分表示したら改行して次の処理へ。
今まではinnnerHTMLだったけれどinsertAdjacentHTMLに変更。innnerHTMLでは都度全文更新している状態だったが、insertAdjacentHTMLにすることで既存の文章+追加する文字という状態になった。こんな書き方もあるんだ…。
実行結果は愉快なことに。一番最初に改行され、まず動くtypewriter(texts[0], id);が待ち時間の間にtypewriter(texts[1], id);が動き、待ち時間の間にtypewriter(texts[2], id);が動き…ということみたい。これが非同期処理というやつかぁ。

で、修正したのが冒頭のソース。
setIntervalでループしているので、呼び出した先のtype関数でもろもろ制御することに。
あとは、第3版では改行有無で処理分けていたけれど分ける必要なかったよね、とか細々修正。

ひとまず完成したから満足!