Noodl：Canvasノードを使ってJavascriptライブラリをいろいろと実装する

## はじめに
この記事はNoodlもくもく会｜GWアドベントカレンダーの4日目の記事となります。
昨年末からNoodlの記事を書かせていただいている@macoleが担当します。
今回の記事ではNoodl V2のCanvasノードを使って、色々なJavaScript　Libraryを実装してみます。

## 概要
1章はCanvasノードとJavascriptノードの接続について説明します。
2章以降では各種ビジュアル系ライブラリの事例を紹介しています。2章以降は独立しているため、気になる部分だけをリファレンス的に活用ください。

### 目次

1. Noodl上でのCanvasの書き方
2. Chart.jsによるグラフ表現
3. Pixi.jsによる2D描画表現
4. Babylon.js/Three.jsによる3D表現
5. QRCode生成

### サンプルデモ・サンプルデータの利用
 [Canvasデモ](https://macole.github.io/Noodl2_CanvasDemo/index.html)
>・[Chart.js](https://macole.github.io/Noodl2_CanvasDemo/index.html?device=ChartJS)
>・[Pixi.js](https://macole.github.io/Noodl2_CanvasDemo/index.html?device=PixiJS)
>・[QRCode.js](https://macole.github.io/Noodl2_CanvasDemo/index.html?device=QRCodeJS)
>・[Babylon.js](https://macole.github.io/Noodl2_CanvasDemo/index.html?device=BabylonJS)
>・[Three.js](https://macole.github.io/Noodl2_CanvasDemo/index.html?device=ThreeJS)

### GitHub上のサンプルデータと構築手順
ソースを公開しています。[Github](https://github.com/macole/Noodl)プロジェクトを対象PCにクローンし/Noodl2_CanvasDemoをNoodlプロジェクトにインポートしてください。
プロジェクトには5種類デモがコンポーネントとして作成してあり、5つのデバイスとして登録しています。
実行時には実行したいdeviceを選択してください。

![image572.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/a1e2b369-1f4a-eac6-7503-38d63c6cb5ca.png)
**図　デモはComponentとして作成している**

## 1. Noodl上のCanvas操作
Noodl上で外部ライブラリを使ってCanvasに描画するときは下記の3連コンボが基本形になります。

![image603.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/fbdf4cd5-d5c8-ecb8-e2c6-f89e8fb1eba0.png)
**図 Canvasを使う基本形**

ScriptDownloaderノードで外部JSライブラリを取得し、
ScriptDownloaderで取得したJSライブラリをJavaScriptノードで使います。
出力先となるCanvasを知るためにCanvasのDocumentElementをJavascriptのinputにつなぎます。

Javascriptノードには下記内容を記載します

1. inputsにdomElement(canvas)を定義する
2. 外部から取得したJSライブラリにcanvasのdomElementやcontextを渡す
3. 各JSライブラリを使う

```js:Canvasを使う基本Javascript
define({
    //Javascriptノードの入力ポート定義,入力の名前と型
    inputs: {
        // 入力例:'型',
        // 使用可能な型は'number', 'string', 'boolean', 'color' and 'signal',
        // 上記以外の型もProject内の入出力であれば使えますが外部と連携する場合は上記以外使えません
        mySignal: 'signal',
        DOM: 'domelement',
        scriptLoaded: "string",
    },
    // Javascriptノードの出力ポート,出力名と型
    outputs: {
        // 出力例:'型',
    },
    // すべてのsignal inputには、対応する名前を持つ独自の関数が必要です
    // 入力で信号(signal)が受信されると実行されます
    mySignal: function (inputs, outputs) {
        // ...
        var canvas = inputs.DOM;

    },
    // この関数はinputが変更されたときに呼び出されます
    change: function (inputs, outputs) {
        // ...
    }
})
```

## 2. Chart.jsによるグラフ表現
グラフ描画ライブラリ[Char.js](https://www.chartjs.org/)を使ってCanvasにグラフを描きます。
Chart.js自体の説明は[こちら](https://qiita.com/Haruka-Ogawa/items/59facd24f2a8bdb6d369)などを確認ください

![ChartJS.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/fd0a69b2-2fe4-c231-609c-e01e320af00f.gif)
**図 グラフの描画例**

### ノードの接続
外部ライブラリはCDN経由で取得するか、NoodlのProjectフォルダにコピーして使います。ここではCDNを使って読込んでみます。
ScriptDownloaderのExternal scripts>script0に`https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js`を記入します。
>**NoodlのProjectフォルダに置いた場合は`./chart.bundle.js`のような形で指示します。**

Canvasへの描画部分は1章で紹介した基本の3連コンボを使っています。NoodlらしくするためCanvasの上にNoodlノードでImageを重ねていますが、なくてもかまいません。
![image571.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/cdcaa2c2-f276-c3d6-ebe0-aae335328dc8.png)
**図 Noodlのノード構造**

### Javascriptノード解説
例として折れ線グラフのJavascriptソースを示します。
JavascriptはCanvasの取得をNoodl向けに変えていますが、それ以外はChart.jsサンプルそのものです。
修正点はcanvasにIDを振ってElementを取得していた部分をinputで取得したdomElement(canvas)に書き換えています。

```js:修正点
//変更前
var canvas = document.getElementById("myChart");

//変更後 
var canvas = inputs.DOM;
```

```js:Javascriptノードの中身
define({
    inputs: {
        mySignal: 'signal',
        DOM: 'domelement',
        scriptLoaded: "string",
    },
    outputs: {
    },
    mySignal: function (inputs, outputs) {
        var canvas = inputs.DOM;
        var myLineChart = new Chart(canvas, {
            type: 'line',
            data: {
                labels: ['8月1日', '8月2日', '8月3日', '8月4日', '8月5日', '8月6日', '8月7日'],
                datasets: [
                    {
                        label: '最高気温(度）',
                        data: [35, 34, 37, 35, 34, 35, 34, 25],
                        borderColor: "rgba(255,0,0,1)",
                        backgroundColor: "rgba(0,0,0,0)"
                    },
                    {
                        label: '最低気温(度）',
                        data: [25, 27, 27, 25, 26, 27, 25, 21],
                        borderColor: "rgba(0,0,255,1)",
                        backgroundColor: "rgba(0,0,0,0)"
                    }
                ],
            },
            options: {
                title: {
                    display: true,
                    text: '気温（8月1日~8月7日）'
                },
                scales: {
                    yAxes: [{
                        ticks: {
                            suggestedMax: 40,
                            suggestedMin: 0,
                            stepSize: 10,
                            callback: function (value, index, values) {
                                return value + '度'
                            }
                        }
                    }]
                },
            }
        });
    },
    change: function (inputs, outputs) {
    }
})
```

また、JavascriptノードではJavascriptを直接入力するほかに、外部ファイルとして読み込むことも可能です。
外部ファイルとすることで下記のようなメリットがあります。
-  再利用しやすくなる
-  Javascriptの内容を外部から編集・確認しやすくなる(エディタを使って編集したり、githubで履歴管理がしやすい)
-  ファイル名を書き換えることで、別のJavascriptファイルと差し替えることができる

反対にデメリットとしては
- Noodl内から直接確認できなくなる
- 実行時にファイル読込が生じるため若干遅くなる
開発時にJavascript側の実装が多い場合、再利用が多い場合は便利ですが、実行速度を気にする場合はNoodl内で書いたほうが有利です。
ケースバイケースで使い分けましょう。

## 3. Pixi.jsによる2D描画表現
2D描画ライブラリライブラリ[Pixi.js](https://www.pixijs.com/)を使ってCanvasに2Dアニメーションを表示します。
Pixi.js自体の説明は[こちら](https://qiita.com/owo_hayachi/items/0c46c5821f449ad68080)などを確認ください。

![PixiJS.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/2a394f85-597b-8e42-1bc9-a2299add1b93.gif)

**図 3Dインタラクションデモの例**

### ノードの接続
外部ライブラリはCDN経由で取得するか、NoodlのProjectフォルダにコピーして使います。ここではCDNを使って読込んでみます。
ScriptDownloaderのExternal scripts>script0に`https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.2.3/pixi.min.js`をセットします。
Canvasへの描画部分は1章で紹介した基本の3連コンボを使っています。NoodlらしくするためCanvasの上にDragノードImageノードを重ねています。
![image575.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/31d782b0-6756-6862-265a-dba509c58890.png)
**図.Noodlのノード構造**

### Javascriptノード解説
JavascriptはCanvasの取得部分をNoodl向けに変えていますが、それ以外は元のPixi.jsサンプルそのものです。
修正点はcanvasにIDを振ってElementを取得していた部分をinputで取得したdomElement(canvas)に書き換えています。

```js:修正点
//変更前
//もとのPixi.jsではPixiアプリケーションを生成してドキュメントのBodyに追加していた
let app = new PIXI.Application({ backgroundColor:0x000000 });
document.body.appendChild(app.view);

//変更後
//NoodlではCanvaをinputsから取得して、PixiアプリケーションのViewに代入している
var canvas = inputs.DOM;
let app = new PIXI.Application( { view:canvas,backgroundColor: 0x000000f});
```
※ちなみにNoodlでもCanvasを使わず、'document.body.appendChild(app.view);'とすることでWindowに直接描画することもできます。

```js:Javascriptノードの中身
define({
    inputs: {
        mySignal: 'signal',
        DOM: 'domelement',
        scriptLoaded: "string",
    },
    outputs: {
    },

    mySignal: function (inputs, outputs) {
        var canvas = inputs.DOM;
        let app = new PIXI.Application( { view:canvas,backgroundColor: 0x000000});

        //表示をする画像の数
        let totalSprites = 100;
        //パーティクルの設定
        let sprites = new PIXI.particles.ParticleContainer(totalSprites, {
            scale: true,
            position: true
        });

        //ステージに反映
        app.stage.addChild(sprites);
        // 複数の画像を設定
        let hayachi = [];
        //画像分をtotalSprites回数分ループ
        for (let i = 0; i < totalSprites; i++) {
            // 画像の設定
            let dude = PIXI.Sprite.fromImage('imgs/star.png');
            // 画像の中心地をきめる
            dude.anchor.set(0.5);
            // 画像のサイズのランダムにきめる
            dude.scale.set(0.01 + Math.random() * 0.1);
            // ランダムに散らす
            dude.x = Math.random() * app.screen.width;
            dude.y = Math.random() * app.screen.height;
            // ランダムな速度を0?2の間で作ります。
            dude.speed = (200 + Math.random() * 0.5) * 0.5;
            // 生成された画像の配列をpushして簡単にアクセスできるようにします
            hayachi.push(dude);
            // ループのデータを反映
            sprites.addChild(dude);
        }
        //アニメーションの初期値値
        let tick = 0;
        //ステージの高さ
        const HEIGHT = 620;

        app.ticker.add(function () {
            // 画像を繰り返し処理して位置を更新する
            for (let i = 0; i < hayachi.length; i++) {
                //配列を代入
                let dude = hayachi[i];
                //横の位置
                dude.x += (Math.random() - 0.5) * 1 * dude.scale.x;
                //縦の位置
                dude.y += dude.scale.y * dude.speed;
                //画面の一番下に行った時縦の位置をリセット、横の位置をランダムに配置
                if (dude.y > HEIGHT) {
                    dude.y = -10;
                    dude.x += (Math.random() - 0.5) * 1 * dude.scale.x;
                }
            }
            // ティッカーを増やす
            tick += 0.1;
        });

        //------テキストの描画-----//
        // スタイルを指定
        let styleBig = new PIXI.TextStyle({
            fontFamily: 'Comfortaa',
            fontSize: 60,
            fill: '#ffffff',
            lineJoin: 'round',
            stroke: '#1099bb',
            strokeThickness: 30,
            align: 'center',
            wordWrapWidth: 1000,
            wordWrap: true
        });
        let styleSmall = new PIXI.TextStyle({
            fontFamily: 'Comfortaa',
            fontSize: 40,
            fill: '#ffffff',
            lineJoin: 'round',
            stroke: '#1099bb',
            strokeThickness: 30,
            align: 'center',
            wordWrapWidth: 1000,
            wordWrap: true
        });

        // スタイルを反映
        let textBig = new PIXI.Text('Pixi.js on Noodl', styleBig);
        let textSmall = new PIXI.Text('Particle animation', styleSmall);
        // テキストの位置を指定
        textBig.x = 200;
        textBig.y = 200;
        textSmall.x = 200+50;
        textSmall.y = 200+100;
        // ステージに表示させる
        app.stage.addChild(textBig);
        app.stage.addChild(textSmall);
    },
    change: function (inputs, outputs) {
    }
})
```

## 4. Babylon.jsによる3Dモデルの操作
3D描画ライブラリ[Babylon.js](https://www.babylonjs.com/)を使い、Canvasに3Dインタラクションを実装します。
Babylon.jsは3Dゲームエンジンとして3D描画にとどまらず衝突計算や特殊効果、サウンド、管理ツールなど豊富な機能があります。
今回のデモは[こちら](https://qiita.com/il-m-yamagishi/items/fa0460830de7a17a8f89)を参考としました。他にも[こちら](https://doc.babylonjs.com/examples/)のサンプルなどいろいろと確認してください。

![BabylonJS.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/2d3bd512-7e70-b02b-f89d-b01271d44397.gif)

**図 3Dインタラクションデモの例**

### ノードの接続
外部ライブラリはCDN経由で取得するか、NoodlのProjectフォルダにコピーして使います。ここではCDNを使って読込んでみます。
ScriptDownloaderのExternal scripts>script0に`https://preview.babylonjs.com/babylon.js`をセットします。
Canvasへの描画部分は1章で紹介した基本の3連コンボを使っています。NoodlらしくするためCanvasの上にNoodlノードでImageを重ねています。

![image613.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/e3b7fe68-8ec3-5cfc-cb5f-4f439aaf12bc.png)
**図.Noodlのノード構造**

### Javascriptノード解説
```js:修正点
//変更前のBabylon
const canvas = document.getElementById('renderCanvas');
const engine = new BABYLON.Engine(canvas);

//変更部分
var canvas = inputs.domElement;
var engine = new BABYLON.Engine(canvas, true); 
```

```js:Javascriptノードの中身
﻿define({
	inputs:{
	    mySignal:'signal',
	    domElement:'domelement',
	    scriptLoaded: "string",
	},
	outputs:{
	    id:'string',
	},
	mySignal:function(inputs,outputs) {
        var canvas = inputs.domElement;
        var engine = new BABYLON.Engine(canvas, true); 
        function createScene() {
            // 新しいシーンオブジェクトを作成する
            const scene = new BABYLON.Scene(engine);
            // 自由移動出来るカメラを生成
            // ドラッグで視点回転、矢印キーで移動
            const camera = new BABYLON.FreeCamera('camera1', new BABYLON.Vector3(0, 5,-10), scene);
            // カメラの向きを座標0地点にする
            camera.setTarget(BABYLON.Vector3.Zero());
            // canvas 要素をクリックやドラッグなどで操作出来るようにする
            camera.attachControl(canvas, false);
            // 照明を追加
            var light = new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0,1,0), scene);
            // 球体メッシュを生成
            var sphere = BABYLON.Mesh.CreateSphere('sphere1', 16, 2, scene);
            // 少し上に持ち上げる
            sphere.position.y = 1;
            // 地面メッシュを生成
            var ground = BABYLON.Mesh.CreateGround('ground1', 6, 6, 2, scene);
            return scene;
        }
        const scene = createScene();
        // 描画ループ関数を定義する
        engine.runRenderLoop(() => {
            scene.render();
        });
        // リサイズ処理
        window.addEventListener('resize', () => {
            engine.resize();
        });
    }
})
```
## 4. Three.jsによる3Dアニメーション
Babylon.jと同じく3D描画ライブラリ[Three.js](https://threejs.org/)を使って、Canvasに3Dアニメーションを実装します。
Babylon.jsほど機能はありませんがドキュメントは色々とあるので、自分で理解しながら実装していけます。
参考にさせていただいたサイトは[こちら](https://ics.media/tutorial-three/)です。

![ThreeJS3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/975c73f4-aa57-4f04-1491-61e43ce8ace4.gif)

**図 3Dアニメーションデモの例**

### ノードの接続
外部ライブラリはCDN経由で取得するか、NoodlのProjectフォルダにコピーして使います。ここではCDNを使って読込んでみます。
ScriptDownloaderのExternal scripts>script0に`https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js`
Canvasへの描画部分は1章で紹介した基本の3連コンボを使っています。NoodlらしくするためCanvasの上にNoodlノードでDragノードImageノードを重ねています。

![image614.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/1f226b75-9faf-4280-2183-8085d4ccc600.png)
**図 Noodlのノード構造**

### Javascriptノード解説

```js:修正点
//変更前
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('#myCanvas')

//変更部分
const renderer = new THREE.WebGLRenderer({
canvas: inputs.DOM
```

```js:Javascriptノードの中身
define({
	inputs:{
	    mySignal:'signal',
	    DOM:'domelement',
	    scriptLoaded:'string',
	},
	
	outputs:{
	},
	
	mySignal:function(inputs,outputs) {
        const width =window.width;//画面サイズの取得
        const height =window.height;//画面サイズの取得

        // レンダラーを作成
        const renderer = new THREE.WebGLRenderer({
          canvas: inputs.DOM
        });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(width, height);
        renderer.setClearColor(0xf9f9f9, 1.0);

        // シーンを作成
        const scene = new THREE.Scene();
        scene.fog = new THREE.Fog(0xf9f9f9, 200, 300);

        // カメラを作成
        const camera = new THREE.PerspectiveCamera(45, width / height);

        // マテリアルを作成する
        const material = new THREE.SpriteMaterial({
          map: new THREE.TextureLoader().load('imgs/shimizu.png')
        });
        // フォグ（霞）を有効にする
        material.fog = true;

        // ビルボードを作成
        for (let i = 0; i < 1000; i++) {
          const sprite = new THREE.Sprite(material);
          // ランダムな座標に配置
          sprite.position.x = 500 * (Math.random() - 0.5);
          sprite.position.y = 100 * Math.random() - 40;
          sprite.position.z = 500 * (Math.random() - 0.5);
          // 必要に応じてスケールを調整
          sprite.scale.set(10, 10, 10);

          scene.add(sprite);
        }

        // 地面を作成
        const plane = new THREE.GridHelper(300, 10, 0x888888, 0x888888);
        plane.position.y = -40;
        scene.add(plane);

        tick();

        // 毎フレーム時に実行されるループイベントです
        function tick() {
          // カメラの自動移動
          camera.position.x = 100 * Math.sin(Date.now() / 2000);
          camera.position.z = 100 * Math.cos(Date.now() / 2000);
          camera.position.y = 50 * Math.sin(Date.now() / 1000) + 60;
          camera.lookAt(new THREE.Vector3(0, 0, 0));

          // レンダリング
          renderer.render(scene, camera);
          requestAnimationFrame(tick);
        }

	},
	
	change:function(inputs,outputs) {
	}
})

```
## 5. QRCode生成
[QRCode.js](http://davidshimjs.github.io/qrcodejs/)を使ってCanvasにQRコードを生成します。
こちら[JavaScriptでQRコードを生成・解析](https://qiita.com/dojyorin/items/78c22be5411ad68cdff9)を参考にしています。
また、ライブラリを使わない場合こちら[JavaScriptで実装してみるQRコードジェネレータ](https://qiita.com/bellbind/items/b80698ceddebb6dfe4aa)が参考になります。

![QRCode.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/3fd63fef-fac2-deb2-88f6-fd7007807156.gif)

**図 QRコード生成の例**

### ノードの接続
外部ライブラリはCDN経由で取得するか、NoodlのProjectフォルダにコピーして使います。ここではCDNを使って読込んでみます。
ScriptDownloaderのExternal scripts>script0に`https://cdn.jsdelivr.net/npm/qrcode@latest/build/qrcode.min.js`

NoodlのTextInputノードから入力されたテキストを受取って、JavascriptノードでQRコードを生成。
生成したQRコードをcanvasに描画しています。
右側にGroupノードでボタンを作成し、ボタンが押されたときにsignalを飛ばしてJavascriptを実行しています。

![image616.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192442/09058f16-f1e4-9231-77c0-4da89fdcd4a6.png)
**図.Noodlのノード構造**

### Javascriptノード解説

```js:QRdocd生成部分
define({
    inputs: {
        mySignal: 'signal',
        DOM: 'domelement',
        scriptLoaded: "string",
        text: "string",
    },

    outputs: {
    },

    mySignal: function (inputs, outputs) {
        var canvas = inputs.DOM;
       
        function writeQr(canvas, data) {
            return new Promise((res, rej) => {
                QRCode.toCanvas(canvas, data, {
                    margin: 2,
                    scale: 2
                }, (err, tg) => !err ? res(tg) : rej(err));
            });
        }
        writeQr(canvas, inputs.text);
    },
    change: function (inputs, outputs) {
    }
})
```

## まとめ
今回はCanvasを使った描画について説明しました。
NoodlではJavascriptを使ってobnizやmicrobitなどIoT機器と接続したり。
MQTTデータを受取って値をグラフ化するなどデータとビジュアルを接続できるので、
次はIoT機器を使ってNoodl画面を操作したいと思っています。

参考
- [Noodl v2.0.2でa-frameを表示させる方法について](https://www.crossroad-tech.com/entry/noodl202-a-frame)
- [HTML5 Canvas Tutorial](https://www.html5canvastutorials.com/)