Live2D WebGLをES6に書き直し

今更ながらES6を勉強したので、Live2D WebGL版をES6にかき直してみた。
<img width="400" alt="screenshot.png" src="https://qiita-image-store.s3.amazonaws.com/0/15028/ba180d60-7db1-168d-8343-6fb164c74f1b.png">

# 開発環境
Live2D WebGL SDK2.1のSimpleプロジェクト

今回使ったものは以下の通り。まだ勉強中なので色々と間違ってるかも。
それにしてもソースがすっきりして素晴らしいっ！

・const/let
・class構文
・Promise
・arrow関数
・Fetch API

# HTML部分

```html:simple.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Live2D Simple</title>
</meta>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=4.0">
</meta>
</head>
<body>
<canvas id="glcanvas" style="border:dashed 1px #CCC"></canvas>
<!-- Live2D Library -->
<script src="../../lib/live2d.min.js"></script>
<!-- User's Script -->
<script src="src/Simple.js"></script>
</body>
</html>
```

# javascript部分

```js:Simple.js
'use strict';

const CANVAS_SIZE = 512;        // canvasサイズ
const CANVAS_ID = 'glcanvas';   // canvasID
// Live2D モデル設定
const MODEL_DEF = {
    "type":"Live2D Model Setting",
    "name":"haru",
    "model":"assets/haru/haru_02.moc",
    "textures":[
        "assets/haru/haru_02.1024/texture_00.png",
        "assets/haru/haru_02.1024/texture_01.png",
        "assets/haru/haru_02.1024/texture_02.png",
    ]
};


window.onload = () => {
    let Live2DModel = new Simple(CANVAS_SIZE, CANVAS_ID, MODEL_DEF);
};


class Simple{
    /*
     * コンストラクタ
     * @param {number} canSize
     * @param {string} canId
     */
    constructor(canSize, canId, modelDef){
        // Live2Dモデルのインスタンス
        this.live2DModel = null;
        // アニメーションを停止するためのID
        this.requestID = null;
        // モデルの初期化が完了したら true
        this.initLive2DCompleted = false;
        // WebGL Image型オブジェクトの配列
        this.loadedImages = [];
        // Live2D モデル設定
        this.modelDef = modelDef;
        // Live2Dの初期化
        Live2D.init();
        // canvasオブジェクトを取得
        this.canvas = document.getElementById(canId);
        this.canvas.width = this.canvas.height = canSize;
        // コンテキストを失ったとき
        this.canvas.addEventListener("webglcontextlost", (e) => {
            console.error("context lost");
            this.initLive2DCompleted = false;
            // アニメーションを停止
            let cancelAnimationFrame =
                window.cancelAnimationFrame ||
                window.mozCancelAnimationFrame;
            cancelAnimationFrame(this.requestID);
            e.preventDefault();
        }, false);
        // コンテキストが復元されたとき
        this.canvas.addEventListener("webglcontextrestored", (e) => {
            console.error("webglcontext restored");
            this.initLoop(this.canvas);
        }, false);
        // Init and start Loop
        this.initLoop(this.canvas);
    }

    /***
     * WebGLコンテキストを取得・初期化。
     * Live2Dの初期化、描画ループを開始。
     * @param {canvas} canvas
     */
    initLoop(canvas){
        // WebGLのコンテキストを取得する
        let para = {
            premultipliedAlpha : true,
    //        alpha : false
        };
        this.gl = this.getWebGLContext(canvas, para);
        if (!this.gl) {
            console.error("Failed to create WebGL context.");
            return;
        }
        // OpenGLのコンテキストをセット
        Live2D.setGL(this.gl);
        // mocファイルからLive2Dモデルのインスタンスを生成
        const modelBuffer = fetch(this.modelDef.model).then(res => {
            return res.arrayBuffer();
        }).then(arrayBuffer => {
            this.live2DModel = Live2DModelWebGL.loadModel(arrayBuffer);
        });
        // テクスチャの読み込み
        let promises = [];
        for(let i = 0; i < this.modelDef.textures.length; i++){
            promises[i] = this.loadTextureImage(i);
        }
        // 全部テクスチャロードしたら次の処理へ
        Promise.all(promises).then(() => {
            this.tick();
        });
    }

    /***
     * テクスチャを読み込みPromiseを返します。
     * @param {Number} i
     * @returns {Promise}
     */
    loadTextureImage(i) {
        this.loadedImages[i] = new Image();
        return new Promise((resolve, reject) => {
            this.loadedImages[i].addEventListener('load', (e) => {
                resolve(this.loadedImages[i]);
            });
            this.loadedImages[i].src = this.modelDef.textures[i];
        });
    }

    /***
     * ループ処理
     */
    tick(){
        this.draw(); // 1回分描画
        let requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
            this.requestID = requestAnimationFrame(this.tick.bind(this));
    }

    /***
     * 描画処理
     */
    draw(){
        // 描画エリアをクリア
        this.gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
        // Live2D初期化
        if(!this.live2DModel)
            return; //ロードが完了していないので何もしないで返る
        // ロード完了後に初回のみ初期化する
        if(!this.initLive2DCompleted){
            this.initLive2DCompleted = true;
            // 画像からWebGLテクスチャを生成し、モデルに登録
            for(let i = 0; i < this.loadedImages.length; i++){
                //Image型オブジェクトからテクスチャを生成
                let texName = this.createTexture(this.gl, this.loadedImages[i]);
                this.live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
            }
            // テクスチャの元画像の参照をクリア
            this.loadedImages = null;

            // 表示位置を指定するための行列を定義する
            let s = 2.0 / this.live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
            let matrix4x4 = [
                s, 0, 0, 0,
                0,-s, 0, 0,
                0, 0, 1, 0,
               -1, 1, 0, 1
            ];
            this.live2DModel.setMatrix(matrix4x4);
        }

        // キャラクターのパラメータを適当に更新
        let t = UtSystem.getUserTimeMSec() * 0.001 * 2 * Math.PI; //1秒ごとに2π(1周期)増える
        let cycle = 3.0; //パラメータが一周する時間(秒)
        // PARAM_ANGLE_Xのパラメータが[cycle]秒ごとに-30から30まで変化する
        this.live2DModel.setParamFloat("PARAM_ANGLE_X", 30 * Math.sin(t/cycle));
        this.live2DModel.setParamFloat("PARAM_EYE_R_OPEN", 1 * Math.sin(t/cycle));
        this.live2DModel.setParamFloat("PARAM_EYE_L_OPEN", 1 * Math.sin(t/cycle));
        // Live2Dモデルを更新して描画
        this.live2DModel.update(); // 現在のパラメータに合わせて頂点等を計算
        this.live2DModel.draw();   // 描画
    }

    /***
     * WebGLのコンテキストを取得する
     * @param {canvas} canvas
     */
    getWebGLContext(canvas){
        let NAMES = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
        let param = {
            alpha : true,
            premultipliedAlpha : true
        };

        for(let i = 0; i < NAMES.length; i++){
            try{
                let ctx = canvas.getContext(NAMES[i], param);
                if(ctx) return ctx;
            }
            catch(e){}
        }
        return null;
    }

    /***
     * Image型オブジェクトからテクスチャを生成
     * @param {gl} gl
     * @param {Image} image
     * @returns {Texture}
     */
    createTexture(gl, image){
        let texture = gl.createTexture(); //テクスチャオブジェクトを作成する
        if (!texture){
            console.error("Failed to generate gl texture name.");
            return -1;
        }

        if(this.live2DModel.isPremultipliedAlpha() == false){
            // 乗算済アルファテクスチャ以外の場合
            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
        }
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);	//imageを上下反転
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D , texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
        gl.generateMipmap(gl.TEXTURE_2D);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return texture;
    }
};
```

# 参考URL
・[お疲れさまXMLHttpRequest、こんにちはfetch](http://qiita.com/tomoyukilabs/items/9b464c53450acc0b9574)

# モーション再生版も修正
**==== 2017/02/26追記 ====**
ちなみに以下記事のモーション切り替え版も修正してみた。
モーションファイル読み込みもfetchさせたら上手くいきました。
[Live2D WebGL版のモーション切替する](http://qiita.com/naotaro0123/items/47e0d0663c9023370950)

```js:simple.js
'use strict';

const CANVAS_SIZE = 512;        // canvasサイズ
const CANVAS_ID = 'glcanvas';   // canvasID
var MODEL_PATH = "assets/haru/";
var MODEL_DEFINE = {
    "type":"Live2D Model Setting",
    "name":"haru",
    "model": MODEL_PATH + "haru_01.moc",
    "textures":[
        MODEL_PATH + "haru_01.1024/texture_00.png",
        MODEL_PATH + "haru_01.1024/texture_01.png",
        MODEL_PATH + "haru_01.1024/texture_02.png",
    ],
    "motions":[
        MODEL_PATH + "motions/idle_00.mtn",
        MODEL_PATH + "motions/shake_00.mtn",
        MODEL_PATH + "motions/tapBody_05.mtn",
    ],
};

window.onload = () => {
    let Live2DModel = new Simple();
};


class Simple{
    /*
     * コンストラクタ
     */
    constructor(){
        // Live2Dモデルのインスタンス
        this.live2DModel = null;
        // アニメーションを停止するためのID
        this.requestID = null;
        // モデルの初期化が完了したら true
        this.initLive2DCompleted = false;
        // WebGL Image型オブジェクトの配列
        this.loadedImages = [];
        // モーション
        this.motions = [];
        // モーション管理マネジャー
        this.motionMgr = null;
        // モーション番号
        this.motionnm = 0;
        // モーションチェンジ
        this.motionchange = false;
        // Live2D モデル設定
        this.modelDef = MODEL_DEFINE;

        // Live2Dの初期化
        Live2D.init();

        // canvasオブジェクトを取得
        this.canvas = document.getElementById(CANVAS_ID);
        this.canvas.width = this.canvas.height = CANVAS_SIZE;
        // コンテキストを失ったとき
        this.canvas.addEventListener("webglcontextlost", (e) => {
            console.error("context lost");
            this.initLive2DCompleted = false;
            // アニメーションを停止
            let cancelAnimationFrame =
                window.cancelAnimationFrame ||
                window.mozCancelAnimationFrame;
            cancelAnimationFrame(this.requestID);
            e.preventDefault();
        }, false);
        // コンテキストが復元されたとき
        this.canvas.addEventListener("webglcontextrestored", (e) => {
            console.error("webglcontext restored");
            this.initLoop(this.canvas);
        }, false);
        // Init and start Loop
        this.initLoop(this.canvas);
    }

    /***
     * WebGLコンテキストを取得・初期化。
     * Live2Dの初期化、描画ループを開始。
     * @param {canvas} canvas
     */
    initLoop(canvas){
        // WebGLのコンテキストを取得する
        let para = {
            premultipliedAlpha : true,
    //        alpha : false
        };
        this.gl = this.getWebGLContext(canvas, para);
        if (!this.gl) {
            console.error("Failed to create WebGL context.");
            return;
        }
        // OpenGLのコンテキストをセット
        Live2D.setGL(this.gl);

        // mocファイルからLive2Dモデルのインスタンスを生成
        const modelBuffer = fetch(this.modelDef.model).then(res => {
            return res.arrayBuffer();
        }).then(arrayBuffer => {
            this.live2DModel = Live2DModelWebGL.loadModel(arrayBuffer);
        });
        // テクスチャの読み込み
        let promises = [];
        for(let i = 0; i < this.modelDef.textures.length; i++){
            promises[i] = this.loadTextureImage(i);
        }
        // 全部テクスチャロードしたら次の処理へ
        Promise.all(promises).then(() => {
            this.loadMotionFile();
            this.tick();
        });
    }

    /***
     * テクスチャを読み込みPromiseを返します。
     * @param {Number} i
     * @returns {Promise}
     */
    loadTextureImage(i) {
        this.loadedImages[i] = new Image();
        return new Promise((resolve, reject) => {
            this.loadedImages[i].addEventListener('load', (e) => {
                resolve(this.loadedImages[i]);
            });
            this.loadedImages[i].src = this.modelDef.textures[i];
        });
    }

    /***
     * モーションを読み込みます
     */
    loadMotionFile(){
        // モーションのロード
        for(var i = 0; i < this.modelDef.motions.length; i++){
            fetch(this.modelDef.motions[i]).then(res => {
                return res.arrayBuffer();
            }).then(arrayBuffer => {
                this.motions.push(Live2DMotion.loadMotion(arrayBuffer));
            });
        }
        // モーションマネジャーのインスタンス化
       this.motionMgr = new L2DMotionManager();

        // マウスクリックイベント
        this.canvas.addEventListener("click", e => {
            this.motionchange = true;
            if(this.motions.length - 1  > this.motionnm){
                this.motionnm++;
            }else{
                this.motionnm = 0;
            }
        }, false);
    }

    /***
     * ループ処理
     */
    tick(){
        this.draw(); // 1回分描画
        let requestAnimationFrame =
            window.requestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.msRequestAnimationFrame;
            this.requestID = requestAnimationFrame(this.tick.bind(this));
    }

    /***
     * 描画処理
     */
    draw(){
        // 描画エリアをクリア
        this.gl.clearColor( 0.0 , 0.0 , 0.0 , 0.0 );
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
        // Live2D初期化
        if(!this.live2DModel)
            return; //ロードが完了していないので何もしないで返る
        // ロード完了後に初回のみ初期化する
        if(!this.initLive2DCompleted){
            this.initLive2DCompleted = true;
            // 画像からWebGLテクスチャを生成し、モデルに登録
            for(let i = 0; i < this.loadedImages.length; i++){
                //Image型オブジェクトからテクスチャを生成
                let texName = this.createTexture(this.gl, this.loadedImages[i]);
                this.live2DModel.setTexture(i, texName); //モデルにテクスチャをセット
            }
            // テクスチャの元画像の参照をクリア
            this.loadedImages = null;

            // 表示位置を指定するための行列を定義する
            let s = 2.0 / this.live2DModel.getCanvasWidth(); //canvasの横幅を-1..1区間に収める
            let matrix4x4 = [
                s, 0, 0, 0,
                0,-s, 0, 0,
                0, 0, 1, 0,
               -1, 1, 0, 1
            ];
            this.live2DModel.setMatrix(matrix4x4);
        }

        // モーションが終了していたら再生する
        if(this.motionMgr.isFinished() || this.motionchange == true ){
            this.motionMgr.startMotion(this.motions[this.motionnm], 0);
            this.motionchange = false;
            console.info("motion:" + this.motionnm);
        }
        // モーション指定されていない場合は何も再生しない
        if(this.motionnm != null){
            // モーションパラメータの更新
            this.motionMgr.updateParam(this.live2DModel);
        }

        // Live2Dモデルを更新して描画
        this.live2DModel.update(); // 現在のパラメータに合わせて頂点等を計算
        this.live2DModel.draw();   // 描画
    }

    /***
     * WebGLのコンテキストを取得する
     * @param {canvas} canvas
     */
    getWebGLContext(canvas){
        let NAMES = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
        let param = {
            alpha : true,
            premultipliedAlpha : true
        };

        for(let i = 0; i < NAMES.length; i++){
            try{
                let ctx = canvas.getContext(NAMES[i], param);
                if(ctx) return ctx;
            }
            catch(e){}
        }
        return null;
    }

    /***
     * Image型オブジェクトからテクスチャを生成
     * @param {gl} gl
     * @param {Image} image
     * @returns {Texture}
     */
    createTexture(gl, image){
        let texture = gl.createTexture(); //テクスチャオブジェクトを作成する
        if (!texture){
            console.error("Failed to generate gl texture name.");
            return -1;
        }

        if(this.live2DModel.isPremultipliedAlpha() == false){
            // 乗算済アルファテクスチャ以外の場合
            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 1);
        }
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);  //imageを上下反転
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D , texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
        gl.generateMipmap(gl.TEXTURE_2D);
        gl.bindTexture(gl.TEXTURE_2D, null);
        return texture;
    }
};
```