WebGLでエモいパーティクルを飛ばして音楽を再生する！

本記事は、<a href="https://qiita.com/advent-calendar/2018/webgl" target="_blank">WebGL Advent Calendar 2018</a> の 19 日目の記事となります。

#はじめに

普段から私はYoutubeを見まくっています。

主に洋楽の激しい感じのジャンルの音楽(Post-hardcore、Pop punk、Djentとか)を聞くことが多いのですが、いわゆる激しい感じのジャンルのバンド界隈が良く使っているMVがあるのです。

共感して頂けた方は、もれなく私と音楽の趣味が合うでしょう。

まずはそのMVがどんなものか見てもらいましょう。
以下に参考例を上げました。

Bring Me The Horizon - Avalanche
https://youtu.be/EM6LGEfFmn0

Issues - Rank Rider
https://youtu.be/NCUclCLdxZ0

Whisperer - Confide
https://youtu.be/-qcgbtE2SvY

このような、アニメーションを織り交ぜながらcdジャケットを全面に出しているMVです。

今回、このようなMVを"ビジュアルMV"と呼ぶことにします。
（正しい呼び方がわかる方は教えていただきたい。。）

#まずは完成物

まずは出来たものを見てもらったほうが早いかと思います。
以下よりご覧頂ければと思います。

![main.png](https://qiita-image-store.s3.amazonaws.com/0/174125/4bbb77ce-7ba4-a9be-e284-b68c650317a4.png)

<a href="https://m0nch1.github.io/visual-mv/" target="_blank">particle music player🌟</a>

ちなみに「ラウドロック」は和製英語らしいですね。。知らなかった。。

>ラウドロック(Loud rock)は、ヘヴィメタルやハードコアなどから派生したロックのジャンルの一種。類似するジャンルのミクスチャー・ロックやモダン・ヘヴィネスなどと同様に和製英語である。

>*出典: フリー百科事典『ウィキペディア（Wikipedia）』*

この「particle music player🌟」はブラウザに音声ファイルをドロップすることで再生可能となっております。

この先の記事をノリノリで見て頂く為にも、是非ともお好きな曲を聴きながら記事を読み進めて頂ければと思います。

**※音声ファイルをドロップ後、再生ボタンを押すと１曲ループで再生されます。
※音量には十分にご注意ください。**

#"ビジュアルMV"っぽさとは何か

作成するにあたり、例のようなMVの
どのような要素が*｢あ～それっぽい｣*という共感を生み出すのか、冷静に分析。
~~(分析するほどのことではない。)~~

結果、**以下4点**が重要なのではないかと考えました。

**1.CDのジャケットと曲名をかっこよく登場させる**
**2.オブジェクトをユラユラさせる**
**3.パーティクルを飛び散らかす**
**4.オーディオビジュアライザを表示させる**

それぞれの実装について、部分的にコードを紹介したいと思います。

##1.CDのジャケットと曲名をかっこよく登場させる

**ファーストインパクト**は重要です。

今回は仮のCDジャケットと仮の曲名を左右からフェードインさせ、フェードイン後、中央に縦線を出現させるシンプルなものです。

これは**CSSアニメーション**で実装しています。

```css:style.css

#lp{
    animation: left-in 3s ease-in-out;
}

#lp::after{
    box-sizing: inherit;
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    transform-origin: center;
    border-right: 4px solid white;
    visibility: hidden;
    animation: line-fade 2s ease 3s ;
}

#cap{
    margin: 0 auto;
    animation: right-in 3s ease-in-out;
}

@keyframes left-in {
    0%{ left: -700px; }
    100%{ left: 0; }
}

@keyframes right-in {
    0%{ right: -700px; }
    100%{ right: 0; }
}

@keyframes line-fade {
    0%{ transform: scale3d(1,0,1); visibility: visible; }
    100%{ transform: scale3d(1,1,1); visibility: visible;}
}

```
**摘要**
・アニメーション時間はCDジャケットと曲名部分を3秒で表示、中央縦線はanimation-fill-modeのforwardsを指定し、CDジャケットが表示されたらそこから2秒かけて縦線を表示。
・中央の縦線はCDジャケット側の要素に疑似要素（::after）を使って表示。

##2.オブジェクトをユラユラさせる

"ビジュアルMV"は、基本的にはCDジャケット、曲名などのオブジェクトがユラユラしています。

この浮遊感がより一層"ビジュアルMV"っぽさを出しています。

ユラユラについてもCSSアニメーションで表現しています。

```css:style.css

#lp{
    animation: left-in 3s ease-in-out, horizontal 1.5s ease-in-out infinite alternate;
}

#cap{
    margin: 0 auto;
    animation: right-in 3s ease-in-out, horizontal 1.5s ease-in-out infinite alternate;
}

.lpimg{
    position: absolute;
    top: 50%;
    left: 50%;
    width: 50%;
    margin: 0 auto;
    animation: vertical 1s ease-in-out infinite alternate;
}

@keyframes horizontal {
    0% {transform:translateX( -8px); }
  100% { transform:translateX(  0px); }
}

@keyframes vertical {
    0% { transform:translate(-50%,-50%); }
  100% { transform:translate(-50%,-55%); }
}

```
**摘要**
・アニメーション`horizontal`でまずは右左にユラユラ
・CDジャケットだけはもっとランダムにユラユラしてほしいので、中の要素`.lpimg`にさらに縦方向のアニメーション`vertical`を指定。
・CDジャケットのユラユラを少しでもランダム化するため、ループ時間をずらして設定。

##3.パーティクルを飛び散らかす

このステップだけで、"ビジュアルMV"っぽさを格段とはね上げることが出来ます。
今回WebGLのアドベントカレンダーに参加させて頂いているので、ある意味この部分が本題になります。

パーティクルを飛ばすには、WebGLの力が必要不可欠です。

一切WebGL関連の技術をやったことが無かったので、かの有名なWebGLのライブラリthree.jsを使用することにしました。

以下サイト様に本当にお世話になりました。
[Three.js入門サイト - ICS MEDIA](https://ics.media/tutorial-three/index.html)

###Three.jsの導入

まずはthree.jsのスクリプトを読み込み、描画領域である`Canvas要素`を作成しておきます。

```html:particle-1.html
<!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">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js"></script>
    <title>ガイド線表示</title>
</head>
<body>
    <canvas id="myCanvas"></canvas>
</body>
```

###ガイド線を引く

**[🌟DEMO1を見る](https://m0nch1.github.io/visual-mv/particle-1.html)**

まずはパーティクルを作りやすい環境を作りましょう。
とりあえず`canvas要素`に**ガイド線**を表示させます。

```javascript:index.js

window.addEventListener('load', init);

      function init(){
        // Canvas未サポート対応
        if (!window.HTMLCanvasElement) return;
        
        // サイズを指定
        const width = window.innerWidth;
        const height = window.innerHeight;

        // レンダラーを作成
        const renderer = new THREE.WebGLRenderer({
          alpha: true,
          canvas: document.querySelector('#myCanvas')
        });
        
        renderer.setClearAlpha(0);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(width, height);

        // シーンを作成
        const scene = new THREE.Scene();

        // カメラを作成
        const camera = new THREE.PerspectiveCamera(70, width / height, 1, 6000);
        camera.position.set(0, 500, 1500);
        camera.lookAt(scene.position);
        
        //ガイド線
        let gridHelper = new THREE.GridHelper(width, 5); // サイズ, 分割数
        scene.add(gridHelper);
        
        let axisHelper = new THREE.AxisHelper(width); // サイズ
        scene.add(axisHelper);

        renderer.render(scene, camera);

      }
```

###自由に動けるカメラを作成

**[🌟DEMO2を見る](https://m0nch1.github.io/visual-mv/particle-2.html)**

Three.jsには**カメラを抑制する`OrbitControls`**というものが存在しているらしいのですが、ライブラリ本体には含まれていないようです。

まずはHTML側で以下スクリプトを読み込むよう指定します。

```html:particle-2.html
<script src="https://cdn.rawgit.com/mrdoob/three.js/master/examples/js/controls/OrbitControls.js"></script>
```
そしてJavascriptには以下記述を追加します。

```javascript:index.js
// コントロールカメラ
let controlCamera = new THREE.PerspectiveCamera(35, width / height, 1, 6000);
controlCamera.position.set(0, 500, 1600);
let controls = new THREE.OrbitControls(controlCamera);
  
tick();

function tick() {
renderer.render(scene, controlCamera);
requestAnimationFrame(tick);
}

```

ガイド線表示の時は`renderer.render(scene, camera);`を指定していましたが、
今回は新しく作成したカ自由に動けるカメラでレンダリングするため、`renderer.render(scene, controlCamera);`としています。

これでカメラをぐりぐりと動かすことが出来ます！

###ただの正方形を表示する

**[🌟DEMO3を見る](https://m0nch1.github.io/visual-mv/particle-3.html)**

作りやすい環境が整ったところで、まずは**簡単なオブジェクト**を表示します。

```javascript:index.js
let geometry = new THREE.BoxGeometry( 200, 200, 200 );
let material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
let cube = new THREE.Mesh( geometry, material );
scene.add( cube );
```
Three.jsには`ジオメトリ`と`マテリアル`というものがあります。

すごく簡単に言うと、以下になります。

**ジオメトリ＝「形状」
マテリアル＝「表面の仕上げ方法」**

例：「ジオメトリは球体で、マテリアルはマットな感じ」というイメージ。

今回は立方体なので、ジオメトリには`BoxGeometry`を指定しています。
他にも、球体は`SphereGeometry`、平面は`PlaneGeometry`、ドーナツ型は`TorusGeometry`などなど。。。

詳しくは以下を参照ください。

**[three.js-documentation](https://threejs.org/docs/#api/en/core/Geometry)**

###パーティクルを作成する

**[🌟DEMO4を見る](https://m0nch1.github.io/visual-mv/particle-4.html)**

立方体はできました。
が、今回作成するのはパーティクル。

そもそもパーティクルとはなんだ。。
→今回は**｢薄っぺらい四角い紙みたいなやつ｣**を大量に飛び散らかすことにしました。

###立方体を薄っぺらい四角い紙へ

```javascript:index.js
let geometry = new THREE.PlaneGeometry( 200, 200 );
let material = new THREE.MeshBasicMaterial( {color: 0xffff00, side: THREE.DoubleSide} );
let plane = new THREE.Mesh( geometry, material );
scene.add( plane );
```

せっかく作った立方体を薄っぺらい紙へ変更しました。

###パーティクルを動かす

**[🌟DEMO5を見る](https://m0nch1.github.io/visual-mv/particle-5.html)**

パーティクルは飛び散らなければなりません。
まずはパーティクルを動かせるようにしましょう。

今回は下から上に散っていく、**パウダースノー系パーティクル**を想定しています。

```javascript:index.js

tick();

function tick() {
  plane.rotation.y += 0.1;
  plane.rotation.x += 0.1;
  plane.rotation.z += 0.1;

  plane.position.x += 1;
  plane.position.y += 1;
  plane.position.z += 1;

  renderer.render(scene, controlCamera);
  requestAnimationFrame(tick);
}

```

座標の移動と、ジオメトリの回転を`関数tick`の中に組み込みました。

これをrequestAnimationFrameで延々と再描画しています。

パウダースノーとはほど遠い。。

###パーティクルを量産する

**[🌟DEMO6を見る](https://m0nch1.github.io/visual-mv/particle-6.html)**

パーティクルはそれなりに大量に飛び散らなくてはなりません。
ひとまず場所はランダムに、大量表示させてみます。

```javascript:index.js
const X_SIZE = window.innerWidth;
const Y_SIZE = window.innerHeight;
const LENGTH = 80;
const PLANE_SCALE = 10;
const plane = [];

for(let i=0; i<LENGTH; i++){
        let geometry = new THREE.PlaneGeometry( PLANE_SCALE, PLANE_SCALE );
        let material = new THREE.MeshBasicMaterial({
                color: 'black', 
                side: THREE.DoubleSide, 
                opacity: 0.7,
                transparent: true
                });
        plane[i] = new THREE.Mesh( geometry, material );

        plane[i].position.x = X_SIZE * (Math.random() - 0.5);
        plane[i].position.y = Y_SIZE * (Math.random());
        plane[i].position.z = X_SIZE * (Math.random() - 0.5);
        scene.add( plane[i] );
}

function random(min, max) {
    let rand = Math.floor((min + (max - min + 1) * Math.random()));
    return rand;
}
        
tick();

function tick() {
        for(let i=0; i<LENGTH; i++){
              plane[i].rotation.y += (Math.random()*0.1);
              plane[i].rotation.x += (Math.random()*0.1);
              plane[i].rotation.z += (Math.random()*0.1);

              plane[i].position.x += (random(-5, 5)*0.1);
              plane[i].position.y += 2.5;
              plane[i].position.z += (random(-5, 5)*0.1);
         }
          renderer.render(scene, controlCamera);
          requestAnimationFrame(tick);
        }
```

こんな感じで何とかできました。

`変数LENGTH`でパーティクルの数を指定し、forループで作成したパーティクルの一つ一つを`配列plane`に格納。

黄色だと見づらかったのでパーティクルの色を黒色に変更し、サイズも調整しています。

`関数tick`内で各パーティクルの座標とジオメトリの回転を指定しています。
ランダマイズするため、`Math.random()`を使用しています。

「０～１０の間」みたいな最小値、最大値を指定したランダマイズをしたかったので`関数random`を作成し、
座標の移動に使用しています。

###~~タイムリープ~~ループさせる

**[🌟DEMO7を見る](https://m0nch1.github.io/visual-mv/particle-7.html)**

大量にできましたが、表示したそばから上に飛んでいって戻ってきません。。
次は、上がるところまで上がったらまた下に戻るようにします。

```javascript:index.js

const width = window.innerWidth;
const height = window.innerHeight;

function tick() {
    for(let i=0; i<LENGTH; i++){
        plane[i].rotation.y += (Math.random()*0.1);
        plane[i].rotation.x += (Math.random()*0.1);
        plane[i].rotation.z += (Math.random()*0.1);

        plane[i].position.x += (random(-5, 5)*0.1);
        plane[i].position.y += 2.5;
        plane[i].position.z += (random(-5, 5)*0.1);

        if (plane[i].position.y > height) {
            plane[i].position.x = X_SIZE * (Math.random() - 0.5);
            plane[i].position.y = 0;
            plane[i].position.z = X_SIZE * (Math.random() - 0.5);
        }
    }
}
```

先ほどまでの`関数tick`内のループ処理に、条件分`if (plane[i].position.y > height)`を追加しました。

`変数height`にはwindowの高さを入れています。
これでy座標がwindowの高さを超えたら0にリセットし、永遠に下から上へパーティクルが上がるようになります。

###もっとランダム性が必要

**[🌟DEMO8を見る](https://m0nch1.github.io/visual-mv/particle-8.html)**

今のままではただ単に下から上への移動のみ。

もっと竜巻みたいに回転したりして欲しかったのでひと工夫。
マウス位置によってカメラを回転させることで、あたかもパーティクルが舞っているかのようにしました。

```javascript:index.js
// コントロールカメラ
// let controlCamera = new THREE.PerspectiveCamera(35, width / height, 1, 6000);
// controlCamera.position.set(0, 500, 1600);
// let controls = new THREE.OrbitControls(controlCamera);

let rot = 0; 
let mouseX = 0; 

document.addEventListener("mousemove", (event) => {mouseX = event.pageX;});

tick();

function tick() {

    const targetRot = (mouseX / window.innerWidth) * 360;
    rot += (targetRot - rot) * 0.01;
    const radian = rot * Math.PI / 180;
    
    camera.position.x = 1000 * Math.sin(radian);
    camera.position.z = 1000 * Math.cos(radian);
    
    camera.lookAt(new THREE.Vector3(0, 0, 0));

    for(let i=0; i<LENGTH; i++){
        plane[i].rotation.y += (Math.random()*0.1);
        plane[i].rotation.x += (Math.random()*0.1);
        plane[i].rotation.z += (Math.random()*0.1);

        plane[i].position.x += (random(-5, 5)*0.1);
        plane[i].position.y += 2.5;
        plane[i].position.z += (random(-5, 5)*0.1);

        if (plane[i].position.y > height) {
                plane[i].position.x = X_SIZE * (Math.random() - 0.5);
                plane[i].position.y = 0;
                plane[i].position.z = X_SIZE * (Math.random() - 0.5);
        }
    }

    renderer.render(scene, camera);
    requestAnimationFrame(tick);
}

}

```

マウス位置によってカメラ位置を変更するため、自動移動カメラとはおさらばです。
コメントアウトします。

イベントリスナー`mousemove`で、マウスを移動している時のみ座標を取得するようにしています。

そして`関数tick`の中でマウス位置によってカメラを動的に回転させています。

これでパーティクルは完成です！

##4.オーディオビジュアライザを表示させる

最後にオーディオビジュアライザです。

当初は適当に棒をアニメーションさせるだけにしようと思っていたのですが、せっかく曲を流せるんだから、**リアルにオーディオビジュアライザが表示できないか**考えました。

###Web Audio APIの登場

ここで登場するのが[**Web Audio API**](https://developer.mozilla.org/ja/docs/Web/API/Web_Audio_API)。

Web Audio APIを使うことで、音声ファイルにエフェクト効果を追加したり、音声ファイルの波形情報を取得することができます。

Web Audio APIには音声ファイルを音源をとして利用するための`AudioNode`というものがあります。

AudioNodeは、**スネア1発のような効果音系に適した`AudioBufferSourceNode`**と、**楽曲１曲丸ごとのような長い音声ファイルに適した`MediaElementAudioSourceNode`**があります。

今回は１曲丸ごとを流すことが目的のため、**`MediaElementAudioSourceNode`**を使用していきます。

```javascript:custom.js
let source, animationId;
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext;

let analyser = audioContext.createAnalyser();
analyser.fftSize = 128;
analyser.connect(audioContext.destination);

let gainNode = audioContext.createGain();
gainNode.connect(analyser); 

let audioTag = document.getElementById('usermusic');
let pausebtn = document.getElementById('mplaypause')

source = audioContext.createMediaElementSource(audioTag);
source.connect(gainNode);

function handleDragOver(e) {
  e.stopPropagation();
  e.preventDefault();
  e.dataTransfer.dropEffect = 'copy';
}

function execDrop(e) {
  e.stopPropagation();
  e.preventDefault();

  audioContext.resume().then(() => {
    console.log('Playback resumed successfully');
  });
    
    pausebtn.setAttribute('src' , 'img/Orion_play.png');
    
    let files = e.dataTransfer.files;
    let mimecheck = files[0].type;
    
    if (mimecheck.startsWith('audio')) {
      pausebtn.style.display = 'inline';
      document.getElementById('output').innerHTML = files[0].name;
      let audiourl = URL.createObjectURL(files[0]);
      audioTag.setAttribute('src' , audiourl);
      document.querySelector('body').style.backgroundImage = 'url(img/landscape.jpg)'; 
      document.getElementById('lpimg').setAttribute('src' , 'img/lpimg.jpg');
    }
  }
  
  let isPlay = false;
  
  pausebtn.addEventListener('click', function(event){
    if (!isPlay) {
      audioTag.play();
    pausebtn.setAttribute('src' , 'img/Orion_pause.png');
  } else {
    audioTag.pause();
    pausebtn.setAttribute('src' , 'img/Orion_play.png');
  }
  isPlay = !isPlay;
});

let canvas        = document.getElementById('visualizer');
let canvasContext = canvas.getContext('2d');
canvas.setAttribute('width', analyser.frequencyBinCount * 10);

render = function(){
  let spectrums = new Uint8Array(analyser.frequencyBinCount);
  analyser.getByteFrequencyData(spectrums);
　
  canvasContext.clearRect(0, 0, canvas.width, canvas.height);
  for(let i=0, len=spectrums.length; i<len; i++){
    canvasContext.fillStyle = 'rgba(255, 255, 255, 0.7)';
    if (i%3 === 0){
      canvasContext.fillRect(i*10, 80, 4, spectrums[i]/6);
      canvasContext.fillRect(i*10, 70, 4, 10);
      canvasContext.fillRect(i*10, 70, 4, -(spectrums[i]/6));
    } else if (i%5 === 0) {
      canvasContext.fillRect(i*10, 80, 4, spectrums[i]/2);
      canvasContext.fillRect(i*10, 70, 4, 10);
      canvasContext.fillRect(i*10, 70, 4, -(spectrums[i]/2));
    } else {
      canvasContext.fillRect(i*10, 80, 4, spectrums[i]/5);
      canvasContext.fillRect(i*10, 70, 4, 10);
      canvasContext.fillRect(i*10, 70, 4, -(spectrums[i]/5));
    }
  }
  animationId = requestAnimationFrame(render);
};
```

オーディオビジュアライザも`canvas要素`で描画しています。

今回は、`body要素`にイベントハンドラ`ondragover`と`ondrop`を追加しブラウザ上に音声ファイルをドロップできるようにしています。

```html:index.html
<body id="DropFrame" ondragover="handleDragOver(event);" ondrop="execDrop(event);">
```
Web Audio API開発に役立ったのが**Firefoxのデベロッパーツール**です。
Web Audio エディターなるものがあります。

`analyser.connect(audioContext.destination);`や、`gainNode.connect(analyser);`のように、用途ごとのノードを接続して行くのですが、この繋がりを可視化したり、そのノードをミュートしたりといった操作が可能です。

設定方法などは以下を参照ください。
[Web Audio エディター](https://developer.mozilla.org/ja/docs/Tools/Web_Audio_Editor)

#もっといろんな曲を"ビジュアルMV"化したい。

ここまでで、一応目標としていたものは形になりました。

が、どうせならもっといろんな曲を"ビジュアルMV"化したい。。

そこで、**各種音楽サービスのWeb API**は活用できないか考えました。

※当初はSoundCloudを使う予定でしたが、現在SoundCloudのWeb APIサービスの新規登録はストップしているようで断念。。(2018/12/18現在)

##Spotify Web API

![spospo.png](https://qiita-image-store.s3.amazonaws.com/0/174125/9706bdbc-392c-5fb2-1bc7-f94e45c8701b.png)

ユーザー数や、曲数はもちろんですが、Web API関連も充実してそうだったので、Spotify Web APIを活用することに決めました。

※youtube musicもそのうちWeb APIを提供してくれそうな気がしますね。
(きっとプレミアム会員限定に違いない。)

**結果、プレミアム(有料)会員限定の機能にはなってしまいましたがSpotifyのURLから曲を再生でき、且つジャケット画像と背景画像もURLから指定した曲のものへ変更できるようになりました。**

これでどんな曲もPost-hardcore風の"ビジュアルMV"化をすることが出来ます。

クリスマスといえばこの曲、*Bing Crosbyの「White Christmas」*もこんなにカッコよくなります。

![white christmas.png](https://qiita-image-store.s3.amazonaws.com/0/174125/4a4625c6-3f4b-7d1d-8f41-b810cbd5e439.png)


###Spotify Web APIを使う

Spotify Web APIを使うにあたって必要な手順をかなり端折って説明します。

①[Spotifyに登録](https://www.spotify.com/jp/)

Spotify未登録の方は上記より登録出来ます。
ちなみに無料会員でもWeb APIの作成自体は出来るみたいです。

②[developerサイト](https://developer.spotify.com/)にアクセスし、ログインします。

③Appを作成

developerサイトの[「DASHBOARD」](https://developer.spotify.com/dashboard/login)からアプリを作成します。
`My New App`をクリックし、アプリを作成します。

クリックしたらアプリの詳細が聞かれるので、ポチポチして登録して行きましょう。

![sptfy1.png](https://qiita-image-store.s3.amazonaws.com/0/174125/16b90224-dc5d-cfc4-cfcd-ac19c7ef8be2.png)

Appを作成したら以下のような画面になります。

ここで、作成したアプリの`ID`を確認することが出来ます。

さらに、「EDIT SETTING」をクリックしてアプリの`Redirect URI`などを設定しましょう。

この`ID`や`Redirect URI`は後で使用することになります。

![spyfy2.png](https://qiita-image-store.s3.amazonaws.com/0/174125/70a9a9ff-afd6-d0a0-fe9f-1f4b395c71e8.png)

##Spotify Web API実装

以下サイト様を参考に作成しました。

[Spotify Web Playback SDK Template](https://spotify-web-playback.glitch.me/)

HTML側、`</body>`前とかで以下のスクリプトを読み込んでおきます。

```html:index.html
<script src="https://sdk.scdn.co/spotify-player.js"></script>
```

まずは**トークン発行のためのログイン画面**実装です。

```javascript:custom.js
window.onSpotifyWebPlaybackSDKReady = () => {
  
  let login_sp = document.getElementById('sptfy');
  let mtrigger = document.querySelector('#trigger');

  login_sp.addEventListener('click', function(event){
    
    const authEndpoint = 'https://accounts.spotify.com/authorize';
    const clientId = '285998fe3500467bb715878d0a767dbf';
    const redirectUri = 'https://m0nch1.github.io/visual-mv/';
    const scopes = [
      'streaming',
      'user-read-private',
      'user-modify-playback-state',
      'app-remote-control'
    ];
    
    pausebtn.setAttribute('src' , 'img/Orion_pause.png');
    pausebtn.style.display = 'inline';
    
    var hash;

    if (!_token){
      mtrigger.checked = false;
      hash = window.location.hash
      .substring(1)
      .split('&')
      .reduce(function (initial, item) {
        if (item) {
          let parts = item.split('=');
          initial[parts[0]] = decodeURIComponent(parts[1]);
        }
        return initial;
      }, {});
      _token = hash.access_token;
    } else {
      window.location.hash = '';
    }

    if (!_token) {
      window.location = `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes.join('%20')}&response_type=token&show_dialog=true`;
    }
  });
}
```

これでSpotifyマークのボタンをクリックしたときに、
トークンが無い状態であれば`window.location`でWeb API認証の画面にリダイレクトすることが出来ます。

さらに、URLインポートボタンがクリックされた時に指定されているURLから曲の情報を取得し、プレイヤーを作成します。

```javascript:custom.js
  let spbtn = document.getElementById('sp_btn');
  spbtn.addEventListener('click', function(event){
    
    audioTag.removeAttribute('src');
    mtrigger.checked = false;
    
    let spurl = document.getElementById('sp_url').value;
    spurl = 'spotify:track:' + spurl.slice(-22);
    
    let player = new Spotify.Player({
      name: 'particle music player🌟',
      getOAuthToken: callback => {
        callback(_token);
      },
      volume: 1
    });

    player.addListener('ready', ({ device_id }) => {
      console.log('Ready with Device ID', device_id);
  
      const play = ({
        spotify_uri,
        playerInstance: {
          _options: {
            getOAuthToken,
            id
          }
        }
      }) => {
        getOAuthToken(_token => {
          fetch(`https://api.spotify.com/v1/me/player/play?device_id=${id}`, {
            method: 'PUT',
            body: JSON.stringify({ uris: [spotify_uri] }),
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${_token}`
            },
          });
        });
      };
  
      play({
        playerInstance: player,
        spotify_uri: spurl,
      });
  
    });
    
    player.connect();

    player.on('player_state_changed', state => {
      if (!state.paused){
        login_sp.style.display = 'none';
        animationId = requestAnimationFrame(sprender);
      } else {
        login_sp.style.display = 'inline';
        animationId = requestAnimationFrame(render);
      }
      document.getElementById('lpimg').setAttribute('src' , state.track_window.current_track.album.images[0].url);
      let srcpath = document.getElementById('lpimg').getAttribute('src');
      document.querySelector('body').style.backgroundImage = 'url(' + srcpath + ')'; 
      document.getElementById('output').innerHTML = state.track_window.current_track.name;
    });
    
    pausebtn.addEventListener('click', function(event){
      
      let statechk = pausebtn.getAttribute('src');
      
      if (statechk === 'img/Orion_play.png') {
        player.resume().then(() => {
          pausebtn.setAttribute('src' , 'img/Orion_pause.png');
        });
      } else {
        player.pause().then(() => {
          pausebtn.setAttribute('src' , 'img/Orion_play.png');
        });
      }
    });
    
  });
```
これで晴れて入力されたURLから曲を再生することが出来ます！

###URLから再生する際の注意点
インポートするURLは「トラック」のURLでないと流れません。。
「アルバム」のURL等はインポートしても何も起きません。

`楽曲のタイトル上で右クリック`→`曲のリンクをコピー`でトラックのURLを取得できます。

![sptfy4.png](https://qiita-image-store.s3.amazonaws.com/0/174125/44d5b7d2-2f10-5097-9566-b3fdd1e5e971.png)

###Web Playback SDKがスゴい

今回の使っているのは、**[｢Web Playback SDK｣](https://developer.spotify.com/documentation/web-playback-sdk/reference/)**というAPIです。

この仕様はbeta版のようです。

実は、ブラウザ上で[ブラウザのSpotify](https://open.spotify.com/browse/featured)を開いておき、そこから先ほどの要領でURLをコピーして｢particle music player🌟｣側で再生するとブラウザのSpotify側も動作が連動していることがわかります。

**1度接続されれば、ブラウザのSpotify側で曲を変更すると｢particle music player🌟｣側もその曲へと変わってくれます。**

*official髭男dism*の*｢stand by you｣*のような激オシャレソングですらも、
まさにPost-hardcoreの"ビジュアルMV"みたいな感じになって素晴らしいです！！

![stand by you.png](https://qiita-image-store.s3.amazonaws.com/0/174125/8c45c8c8-0615-8dcc-bc69-e2324b1c0cd2.png)

#まとめ

今年、初めてアドベントカレンダーに参加させて頂きました。
いろんなナレッジを参考にしたので、コードのリファクタリングが必要な箇所が多々あります。。

WebGLとしてはthree.jsでパーティクルを飛び散らかすだけでしたが、これを機にWebGLにのめり込んでいきたいなと思います。

どうせだから音楽の波形やリズムデータなどを取得してそれに合わせて描画を変えるとか、次はやってみたいです。

Spotify Web APIも無限の可能性を感じました。

次の目標は海外系バンドによくある"リリックビデオ"をWebで再現することです。
以下、海外系バンドによくある"リリックビデオ"の例です。

Mayday Parade - "Somebody That I Used To Know" feat. Vic Fuentes (Punk Goes Pop 5)
https://youtu.be/ftE9uSPpecs

JavaScriptもっと理解しないとな。。

plus ultra！！