【チュートリアル】HTML/CSS/JSだけで作る音楽アプリ
こんにちは、Kosukeeeです。
今回はHTML/CSS/JSだけを使って音楽アプリ(瞑想アプリ)を作成していきたいと思います。
フロントエンドの開発を始めたばかりの方やJSを使ってウェブアプリを作成してみたいという方に参考になるように分かりやすく説明していきたいと思います。
まず、これからどういったものを作るのかイメージしていただくためにこちらの動画を確認してみてください。
また下記のリンクから実際に動いているものを触ってみることもできます。
Chromeなどのブラウザ開発者ツールにモバイルエミュレータがあるのでそちらでスマートフォンの表示状態にしてご確認ください。
Githubのレポジトリはこちらにあるのでソースコードを確認いただけます。
また画像や音のmp3データなどはこのレポジトリからそのままご利用ください。
実装する機能の洗い出し
今回の音楽アプリです実装する機能としては
- プレーヤーの再生ボタンをクリックすると音楽を再生する
- 再生している状態でもう一度クリックすると音楽を止める
- 再生中に経過した時間に応じて周囲の円周の色のついた部分が進んでいく
- 再生中に経過した時間に王子て残り時間がカウントダウンする
- リスト一覧の中から別の曲を選択するとプレーヤーの背景画像、再生する音楽、再生時間、タイトルを変更する
機能としては非常にシンプルですが実際に実装してみると考慮しなくてはいけない状態の変化などがありJSの基礎を理解する上でも参考になると思います。
それでは始めていきましょう。
実装していくパーツ(コンポーネント)を確認する
フロントエンドの業界ではアプリケーションの各パーツのことをコンポーネント(要素、部品)と呼びます。
なのでここでもコンポーネントと呼んでいきます。
これから実装していく必要のあるコンポーネントを確認してみましょう。
イメージとしては小さい単位のコンポーネントが集まり中規模のコンポーネントを構成して、さらに中規模のコンポーネントが集まり一つのアプリケーションになるといった感じでしょうか。
コンポーネント一つ一つは独立した存在で他の場所にも転用できるように設計するのが重要です。
(例えばデザイン上の変更がありtitleコンポーネントをplayer-timerコンポーネントの上部に配置したいという要望に耐えられるようにするため)
ベースを作成する
ディレクトリの構成は以下になります。
- imgフォルダ
- soundsフォルダ
- svgフォルダ
- app.js
- index.html
- style.css
img、soundsとsvgはGithubのレポジトリからファイルをコピーしてください。
(実際のレポジトリには上記のファイル以外にcomposer.jsonとindex.phpが入っていますがこのアプリには関係ないので無視していただいて大丈夫です。)
それではまずはindex.html、style.css、app.jsを作成してください。
ターミナルを使える方は以下のコマンドで作成しましょう。
touch index.html style.css app.js
ターミナルの使い方がわからない方はエディタ上でファイルを作成してください。
CSSのスタイリングに関しては説明を省くのでレポジトリからコードをコピペしてください。
HTML/CSSパートの作成
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta http-equiv='X-UA-Compatible' content='ie=edge'>
<title>瞑想アプリ</title>
</head>
<body>
</body>
</html>
次にindex.htmlからCSSとJSファイルを読み込みます。
ウェブパフォーマンスの観点からCSSはheadタグの中で読み込み、JSはbodyタグの一番最後に配置します。
<head>
<link rel='stylesheet' href='./style.css'>
</head>
...
<body>
...
<script src='./app.js'></script>
</body>
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta http-equiv='X-UA-Compatible' content='ie=edge'>
<link rel='stylesheet' href='./style.css'>
<title>瞑想アプリ</title>
</head>
<body>
<script src='./app.js'></script>
</body>
</html>
次にCSSファイルにコードをコピペします。
style.css
まず大きな構成としてはplayerコンポーネントとselectionコンポーネントに別れています。
(省略して<body>タグの内側だけ書いていきます)。
<body>
<div class="player">
</div>
<div class="selection">
</div>
<script src='./app.js'></script>
</body>
それではまずはplayerコンポーネントの中を作っていきましょう。
まず音楽を再生するのでaudioタグで配置する必要があります。曲が連続して再生されるようloop属性もつけておきます。
<body>
<div class="player">
<audio class="song" src="sounds/ocean_sunset.mp3" loop></audio>
</div>
<div class="selection">
</div>
<script src='./app.js'></script>
</body>
次に曲の再生ボタンとタイマーのコンポーネントです。
タイマーの部分は下地になる白のサークルとその上を移動するグレーのサークルの2種類を配置する必要があります。
ちなみにsvgをアニメーションさせる場合はsvgタグで配置しますが、再生ボタンのように単に画像として使用するのであればimgタグで配置して問題ありません。
<body>
<div class="player">
<audio class="song" src="sounds/ocean_sunset.mp3" loop></audio>
<div class="player-timer">
<!-- タイマーの下地になるサークル -->
<svg class="player-timer-circle player-timer-track-circle" width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="110" cy="110" r="108.5" stroke="white" stroke-width="3"/>
</svg>
<!-- タイマーの移動する部分のサークル -->
<svg class="player-timer-circle player-timer-moving-circle" width="220" height="220" viewBox="0 0 220 220" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="110" cy="110" r="108.5" stroke="#B7B7B7" stroke-width="3"/>
</svg>
<!-- 再生ボタン -->
<img src="svg/play.svg" alt="" class="player-timer-btn" />
<h3 class='timeDisplay'>0:00</h3>
</div>
</div>
<div class="selection">
</div>
<script src='./app.js'></script>
</body>
この状態でページを確認してみましょう。
index.htmlファイルをそのままブラウザにドラッグアンドドロップするとページが表示されます。
playerコンポーネントの箇所が出来上がりました。
それでは次にselectionコンポーネントを実装していきます。
selectionコンポーネントは大きく2つに分けられます。
titleコンポーネント:現在再生されている曲の情報を表示
soundListコンポーネント:他の曲のリスト一覧を表示
まずはtitleコンポーネントを配置します。
先程用意したselectionクラスのdivの中に配置します。
<div class="selection">
<span class="selection-bar"></span>
<div class="title">
<p class="title-headline">南国のせせらぎ ~フィジー~</p>
<p class="title-description">
海辺の音, Sound of Sea
</p>
</div>
</div>
次にsoundListコンポーネントですが、子要素であるsoundList-itemの構造について説明します。
soundListには「data-sound」「data-image」「data-time」というものがついています。見たことがないかと思いますが、これは今回のウェブアプリの実装でJSから必要になるものなので特別に用意したdata属性と呼ばれるものです。
data属性というのはdata-***という形でいくらでもつけることができ、その中に任意の値を配置することができます。
今回のウェブアプリですとリストの中から曲を選択したら曲のファイルのパスと背景画像と曲の長さを更新する必要があるのでdata属性を使って持たせておきます。
data-sound:曲のmp3ファイルのパス
data-image:背景画像のパス
data-time:曲の長さ
<div
class="soundList-item"
data-sound="sounds/morning_forest.mp3"
data-image="img/road.jpg"
data-time="120"
>
<img src="img/road.jpg" alt="" class="soundList-image" />
<div class="soundList-text">
<span class="soundList-title">爽やかな鳥の声 ~山中湖~</span>
<span class="soundList-description">Morning birds, Lake Yamanaka</span>
</div>
</div>
それらをtitleコンポーネントの下部に配置します。
<div class="selection">
<span class="selection-bar"></span>
<div class="title">
<p class="title-headline">南国のせせらぎ ~フィジー~</p>
<p class="title-description">
海辺の音, Sound of Sea
</p>
</div>
<div class="soundList">
<div
class="soundList-item"
data-sound="sounds/morning_forest.mp3"
data-image="img/road.jpg"
data-time="120"
>
<img src="img/road.jpg" alt="" class="soundList-image" />
<div class="soundList-text">
<span class="soundList-title">爽やかな鳥の声 ~山中湖~</span>
<span class="soundList-description">Morning birds, Lake Yamanaka</span>
</div>
</div>
<div
class="soundList-item"
data-sound="sounds/ocean_sunset.mp3"
data-image="img/ocean.jpg"
data-time="60"
>
<img src="img/ocean.jpg" alt="" class="soundList-image" />
<div class="soundList-text">
<span class="soundList-title">南国のせせらぎ ~フィジー~</span>
<span class="soundList-description">海辺の音, Sound of Sea</span>
</div>
</div>
<div
class="soundList-item"
data-sound="sounds/night_forest.mp3"
data-image="img/night-forest.jpg"
data-time="180"
>
<img src="img/night-forest.jpg" alt="" class="soundList-image" />
<div class="soundList-text">
<span class="soundList-title">夜の森 ~群馬県沼田~</span>
<span class="soundList-description">Sounds of insects, Star in the forest</span>
</div>
</div>
</div>
</div>
それではもう一度ページを確認してみましょう。
全てのコンポーネントが配置されているのを確認できたらHTMLのパートは完成です。
それでは次にJSのパートに移りましょう。
JSパートの作成
まずapp.jsの中でappという関数を定義します。
この後のコードはこの中に記述していきます。
const app = () => {
}
app();
まずはHTMLの必要な要素を定数と変数の中に格納します。
outlineという定数にはcircleタグが代入されていてoutlineLengthという定数ではcircleのgetTotalLengthプロパティにアクセスしています。これでcircleの長さを取得できます。
またcurrentTimeではaudioタグが代入されているsong定数からcurrentTimeプロパティにアクセスして再生した際の経過した時間を取得しています。
残りの再生時間を計算してminutesとsecondsに代入するのですが、currentDurationからcurrentTimeを引いて残りの時間(leftDuration)を算出します。
そしてminutesには60で割って分数を算出し、Math.floorを使って小数点以下を切り捨てます(秒数を60秒で割ると分数を算出できます)。
secondsには60で割った後の余りが代入されます。
曲を再生する機能の実装
次に再生ボタンをクリックしたら曲を再生する機能を実装します。
再生ボタンはplayBtnに代入されているのでplayBtnがクリックされたらcheckPlaying関数を呼ぶという形しています。
checkPlaying関数には現在選択されている曲であるsongを引数に渡しています。
この関数で曲の再生中か停止中かという状態をチェックしてそれぞれに応じて曲(song)のコントロールします。
このmapCheckPlayingEventをDOMContentLoadedが呼ばれた時(ページが読み込まれた時)に呼ぶようにします。
DOMContentLoadedはHTMLドキュメントが読み込まれ解析された後に呼ばれます。
次にcheckPlaying関数を実装します。
曲の再生状態をチェックして止まっていればsong.play()で再生して、ボタンの画像を変更します。再生されていればsong.pause()で曲を止めて、同様に画像を変更しています。
この状態でページを確認してみましょう。
再生ボタンをクリックしてみて曲が再生されてボタンの画像が切り替わるかどうかをみてください。
音楽アプリの形になってきましたね。
ただこのままですと曲の再生時間を過ぎても再生し続けてしまいます。
仮にcurrentDurationを5(秒)に変更してみましょう。
この状態で再生ボタンを押すと5秒経過した後も曲が流れ続けています。
これをcurrentDurationの秒数に応じて止まるように修正します。
曲の終わりをチェックする関数をcheckSongEndingとします。
この関数では経過時間であるcurrentTimeと曲の長さであるcurrentDurationを比較して経過した時間が曲の長さを超えたら
- 曲を止める
- 経過した秒数を0に戻す
- 停止ボタンを再生ボタンに変更する
ということをしています。
このcheckSongEndingをmapCheckPlayingEventに配置します。
曲の経過に応じて移動するsvgの実装
次に曲の経過に応じて移動するsvgの円周の実装です。
今はまだsvgの緑の部分がそのまま見えています。
曲をスタートする前は白い部分だけを表示して経過した時間に応じて緑の部分が進んでいくようにしたいと思います。
この関数をupdatePlayerByTimeUpdateとします。
audioタグには曲の再生中に一定の間隔で呼ばれるtimeupdateというイベンがあるのでそれを利用します。
この関数の中ですることは時間の経過に応じて
- 円周の緑の部分を更新する
- 残り時間の表示を更新する
という2つです。
なのでそれぞれを
- updateCircle(円周の部分を更新する)
- updateTimerText(残り時間の表示を更新する)
に分けて実装します。
それではupdateCircle関数から実装します。
この関数ですることは全体の曲の長さに対する現在の経過した時間の割り合いを計算してその長さだけ円周のsvgを進めるということです。
なので
- 現在の経過した時間の比率 : currentTime / currentDuration
- その比率を円周の長さに対する比率に置き換える
- 円周の全体の長さから求めた経過した時間分をひく
円周の色のついた部分はstrokeDashoffsetで表示しています。なので求めた経過した時間をそのまま代入するのではなくて、円周全体の長さから引いた残りを代入する必要があります。
計算自体はシンプルですが少しややこしいので気をつけてください。
次にupdateTimerTextを実装します。
経過した時間はsong.currentTimeで求まるのでそこから全体の長さ(leftDuration)から 経過した時間(currentTIme)を引いて残りの時間を計算します。
そこから「分」と「秒」が求まるのでその値をsetTimerText関数に渡します。
setTimerText関数は受け取ったminutesとsecondsをtimeDisplayのtextContentプロパティに代入して上書きします。
一点気をつけるのは「秒」の部分が10秒以下である場合は先頭に「0」をつけるロジックを追加している点です。
もう一度先ほどのupdatePlayerByTimeUpdate関数を確認しますが、if/elseで条件分岐しています。
これは別の曲を選択した際に円周の動いた分を元の状態に戻す必要があるため、songのsrc属性をチェックして最後に呼ばれたsongのsrc属性と異なる場合(別の曲が選択された)はsetCircleStroke関数を呼んで円周部分をデフォルトに戻しています。
setCircleStroke関数自体は2つのことをしています。
strokeDasharrayでストロークの間隔を定義して、strokeDashoffsetで緑の部分を外に押し出しています。
別の曲を選択した際のデータの更新
別の曲を選択した際にはデータを更新する必要があります。
「別の曲を選択」というのは曲をクリックした時なのでまずはclickイベントをそれぞれの曲のリストに連携する必要があります。
この関数をmapClickEventToSoundBtnとして実装します。
まずsoundsには全ての曲がNodeListの形で代入されているのでforEachでループしてclickイベントを設定します。
そして別の曲を選択した時にすることは
- 曲のsrcと背景画像を変更する (setPlayerSong関数)
- 曲のタイトルと説明文を変更する (setPlayerText関数)
- 曲の残り時間を変更する (setTimer関数)
の3つになります。
それぞれを実装していきましょう。
setPlayerSong関数は引き数に曲のパスであるsongDataと背景画像のパスであるimageUrlを受け取るようにしています。
そしてクリックしたリスト一覧にはdata属性に曲のパスと画像のパスを収めています。
そのためクリックした時にthisからdata-imageとdata-soundを取得してそれを引き数として渡しています。
そしてそれをsongのsrcとplayerのbackgroundImageに大入しています。
さらにplayBtnの画像を再生ボタンにしています。
次にsetPlayerText関数です。
この関数はtitleとdescriptionを引き数に取り、それぞれテキストを上書きしています。
titleとdescriptionはクリックした要素の子要素にあるのでquerySelectorで取得できます。
最後にsetTimer関数です。
この関数はminutesとsecondsを引き数に取りそのまま残り時間を上書きします。
最後にこれらの関数をsetDefaultPlayerという関数で包みます。
そしてDOMContentLoadedでHTMLが読み込まれてパースされた後にこの関数を呼ぶことでその配下の関数も呼ばれるようにしています。
これで実装は終わりになります。
機能自体は非常にシンプルですがロジックや状態の変化なども考慮するとコードが自然と複雑になってきます。
その時に一つ一つの関数を機能ごとに切り分けたり、関数名を見ただけでどんな処理をしているのかをイメージできるようにすると後から見返した時に混乱しにくくなります。
ただ実際には今回のように上から順番に実装してるというよりは何度も行ったり来たりしてコードを書き直していくうちに最終的にこのような形にしています。
なので色々試行錯誤しながらコードを書きなしていくと良いと思います。
今回はこれで終わりになります。
最後までお読みいただきありがとうございました。