CHIRIMENとPixi.JSでスクリーンセーバーを3時間で作った話

さて、Advent Calendarですよ！

**めりーくりすまーす！！！**

![いつもの謎画像](http://img.dfm.lrv.jp/wifitng/xmas.jpg)

※時間がないので[WIFI-TNGとESP-WROOM-02で始めるWIFI Arduino](http://qiita.com/tadfmac/items/17448a2d96bd56373a66)使い回しですw

# 今度は何?

たぶん皆さん同様、年末進行中！そんな中！！

「明日までに何か展示するものを作らなきゃならない！！！」

まじかよ！

ということが、前日の夕方に決定！アホか！
ということが、先週ありました。（いつもそんな感じではあるw）

- 営業目的の社内展示会用。私は技術サポートの仕事をやってるので出来る感をアピールする必要がある！
- 時間がない！徹夜しちゃったら翌日説明員する体力ない！！
- 手元にCHIRIMENがある！昨日までにLチカは出来た。というか、別の場所でCHIRIMENを使った気合いの展示も行われている。よし！コバンザメ作戦だ！
- 前後左右のブースではVRとかドローンとか技術力を駆使した展示が盛りだくさん！違いを出さなければ！

→CHIRIMENを使った「何か」を作ることにしました。

# CHIRIMENとは?

![CHIRIMEN](http://img2.dfm.lrv.jp/screensaver/chirimen.jpg)

CHIRIMENは、[MozOpenHardプロジェクト](http://mozopenhard.mozillafactory.org/)で開発が進められている、[Firefox OS](https://www.mozilla.org/ja/firefox/os/2.0/)搭載のボードです。以前KDDIがイベントとかで配ってた[Open Web Board](http://opensource.kddi.com/owb/)と似ていますが、豊富なGPIOピンが搭載されているのが特徴です。

JavaScriptでGPIO制御できる、こうしたボードは結構あります。[Tessel](https://tessel.io/)や、[konashi](http://konashi.ux-xu.com/)が有名ですね。

CHIRIMENの特徴は何と言っても「中でブラウザが動いてる」ということ。
JavaScriptだけでなく、HTML5の機能がフルで使えるんですね！

詳細は[MozOpenHard CHIRIMEN](http://mozopenhard.mozillafactory.org/)を見てください。

# Lチカ（3時間の前に終わってたこと）

Lチカは、GPIOポートにLEDを繋いでHIGHとLOWを繰り返し出力するとできます。
Arduinoとかと一緒です。

CHIRIMENのアプリは、HTML/CSS/JavaScriptで作るWebアプリなので、LチカもJavaScriptからやる感じになります。

下記を参考にしました。

[test-gpio](https://github.com/MozOpenHard/examples/tree/master/test-gpio)

## IOポートの情報

CHIRIMENのIOポート情報は下記にありました。

[CHIRIMEN pin config.xlsx](https://github.com/MozOpenHard/CHIRIMEN-tools/blob/master/CHIRIMEN%20pin%20config.xlsx)

> こちらのリポジトリ整理中らしいので、URL変わるかもです。

今回は、下記ピンを使うことにしました。

|ピン番号|GPIOポート番号|
|:-----|:-----|
|CH1-10|199|
|CH1-11|244|
|CH1-12|243|
|CH1-13|246|
|CH1-14|245|


## Lチカプログラム

というわけで書いてみました。

```index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blink</title>
</head>
<body>
<div>Lチカ</div>
<script src="app.js"></script>
</body>
</html>
```

```app.js
var pnums = [199,244,243,246,245];
for(var i= 0;i< pnums.length; i++){
  navigator.mozGpio.export(pnums[i]);
  var start = new Date();
  while((new Date()-start)<100);
}
var val,c = 0;
setInterval(function(){
  val ^= 1;  
  navigator.mozGpio.setValue(pnums[c],val);
  if(val == 0){
    c=(c+1)%pnums.length;
  }
},100);
```

[こんな感じになりました!](https://www.instagram.com/p/_WxkphHCr0/)

# スクリーンセイバーへの道

最初はあまりの時間のなさにLチカを展示しようと思いましたが、、、、さすがにそれは。

せっかくHTML5が動くボードなので、画面を見せたい。
しかし！時間がない！

> クリスマスシーズンなので、サンタとか出てくればいいだろ！

というわけで、クリスマスっぽいスクリーンセイバーを作ることにしました。

![スクリーンセイバー](http://img2.dfm.lrv.jp/screensaver/ss.png)

こんなの！
Lチカ付き！（←あまりコラボ感はないw）

CHIRIMENに操作デバイスを作る暇はないので、スマホと連携して操作する感じにします。

![リモコンつき](http://img2.dfm.lrv.jp/screensaver/block.png)

最終的につくるのは下記３つになります。

1. スクリーンセイバー（CHIRIMENにインストールするWebアプリ）
2. WebSocket 中継サーバー（node.jsで作る）
3. コントローラー（スマホからアクセスするWebサイト）

作ったコードはこちら！[tadfmac/screensaver](https://github.com/tadfmac/screensaver)

さて、頑張ります。

# 1. スクリーンセイバー

## 画像を集める！

クリスマスっぽさは、画像で！

"サンタ　イラスト フリー" とかでググって、背景透明っぽいのを収集しまくります。

※著作権とかはご注意を！

## 画像を動かす!

画像を動かせば、スクリーンセイバーの出来上がり！
[Pixi.JS](http://www.pixijs.com/)を使って動かしました。

公開からずいぶん時間が経ってしまいましたが、Pixi.JSに興味のある方は下記も参考にしてください。

- [簡単アニメーション！Pixi.jsを触ってみる！（1）テキストを動かしてみる](http://qiita.com/tadfmac/items/3684489e133320cd9d75)
- [簡単アニメーション！Pixi.jsを触ってみる！（2）画像を動かしてみる](http://qiita.com/tadfmac/items/d63df0a51ff9a3b55aa1)

上記記事のような感じで画像を動かしました。

## 手抜きリモコン対応

WebSocketに対応する部分を作ります。
時間が無いので、慣れた[poorws](https://github.com/tadfmac/poorws?files=1)を使います。

これで、`ws.onmessage()`で受け取ったメッセージにより、画像を出したりするようにします。

## ソース

こんな感じになりました。

```index.html
<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
<title>XMAS Screen Saver</title>
<style>
*{
    padding:0px;
    margin:0px;
}
#status{
  position:fixed;
  bottom:0px;
  right:0px;
  font-size:18px;
  background-color:black;
  color:white;
}
</style>
</head>
<body>
<div id="status">Connecting...</div>
<div id="pixiview"></div>
<script src="pixi.min.js"></script>
<script src="poorws.js"></script>
<script src="jquery-2.0.3.min.js"></script>
<script src="app.js"></script>
</body>
</html>
```

JSはこんな感じで。
(毎度の事ですが、勢いで書いてるので関数化すら出来ていませんw)

```app.js
// ws
var host = "ws://xxx.net:xxxx";
var ws = new poorws(host);
var status = 0;

ws.onStatusChange = function(sts){
  status = sts;
  if(sts == 0){
    $("#status").text("Connecting...");
  }else if(sts == 1){
    $("#status").text("Connected!");
  }else if(sts == 2){
    $("#status").text("Disconnecting...");
  }else if(sts == 3){
    $("#status").text("Re-Connecting...");
  }
};

ws.onOpen = function(e){
  ws.send("master");
};

ws.onMessage = function(mes){
  if(mes.data == "master ack"){
    console.log("master ack received");
  }else{
    if(mes.data == "clear"){
      clearSprites();      
    }else if(mes.data == "snow"){
      if(snow == true){
        removeSnow();
      }else{
        addSnow();
      }
    }else{
      addSprite(getRandomInt(0,fruitsnames.length));
    }
  }
};

function getRandomInt(min, max) {
  return Math.floor( Math.random() * (max - min + 1) ) + min;
}

// Animation
var stage = new PIXI.Stage(0x000000);
var width = window.innerWidth;
var height = window.innerHeight;
var renderer = PIXI.autoDetectRenderer(width, height, {autoResize: true});
document.getElementById("pixiview").appendChild(renderer.view);

function initWall(){
  var wall = PIXI.Texture.fromImage('./img/wall.jpg');
  var wsprite = new PIXI.Sprite(wall);
  wsprite.position.x = width / 2;
  wsprite.position.y = height / 2;
  wsprite.anchor.x = 0.5;
  wsprite.anchor.y = 0.5;
  stage.addChild(wsprite);
}

initWall();

var xmascontainer = new PIXI.Container();
stage.addChild(xmascontainer);

// 画像からスプライトオブジェクトを作る
var sprites = [];

var fruitsnames = [
'./img/bell1.png',
'./img/santa1.png',
'./img/santa2.png',
'./img/santa3.png',
'./img/tonakai1.png',
'./img/tonakai2.png',
'./img/tree1.png',
'./img/tree2.png'
];

function addSprite(cnt){
  var ftex = PIXI.Texture.fromImage(fruitsnames[cnt%fruitsnames.length]);
  var fsprite = new PIXI.Sprite(ftex);
  fsprite.position.x = Math.random() * width;
  fsprite.position.y = Math.random() * height;
  fsprite.anchor.x = 0.5;
  fsprite.anchor.y = 0.5;
  fsprite.speed = Math.random();
  sprites.push(fsprite);
  xmascontainer.addChild(fsprite);
  blinkStart();
}

function clearSprites(){
  for(var i=0;i < sprites.length; i++){
    xmascontainer.removeChild(sprites[i]);  
  }
}

var snow = false;

requestAnimationFrame(animate);

function animate(){
  requestAnimationFrame(animate);
  for(var cnt=0;cnt <sprites.length;cnt++){
    sprites[cnt].rotation += (sprites[cnt].speed / 10);
    sprites[cnt].position.x += (sprites[cnt].speed * 10);
    if(sprites[cnt].position.x > (width + 100)){
      sprites[cnt].position.x = -200;
      sprites[cnt].position.y = Math.random() * height;
      sprites[cnt].speed = Math.random();
    }
  }
  if(snow == true){
    for(cnt=0;cnt <MAX_SNOW;cnt++){
      var scale = snowimgs[cnt].scale.x;
      snowimgs[cnt].position.x += scale * (Math.random() - 0.5) * 4;
      snowimgs[cnt].position.y += (scale * 3) + 1;
      if(snowimgs[cnt].position.y > 1024){
        snowimgs[cnt].position.y = -10;
      }
    }
  }
  renderer.render(stage);
}

// snow
var texture = PIXI.Texture.fromImage('img/snow2.png');
var MAX_SNOW = 300;
var snowimgs = [];

for(var cnt=0;cnt < MAX_SNOW;cnt ++){
  snowimgs.push(new PIXI.Sprite(texture));
  snowimgs[cnt].position.x = Math.random() * width;
  snowimgs[cnt].position.y = Math.random() * height;
  snowimgs[cnt].anchor.x = 0.5;
  snowimgs[cnt].anchor.y = 0.5;
  var base = Math.random();
  snowimgs[cnt].alpha = (base/2) + 0.4;
  snowimgs[cnt].scale.x = base/2;
  snowimgs[cnt].scale.y = base/2;
}

function addSnow(){
  snow = true;
  for(var cnt=0;cnt < MAX_SNOW;cnt ++){
    stage.addChild(snowimgs[cnt]);
  }
}

function removeSnow(){
  snow = false;
  for(var cnt=0;cnt < MAX_SNOW;cnt ++){
    stage.removeChild(snowimgs[cnt]);
  }
}

// resizeing
var resizeTimer = false;
$(window).resize(function() {
    if (resizeTimer !== false) {
        clearTimeout(resizeTimer);
    }
    resizeTimer = setTimeout(function() {
      width = window.innerWidth;
      height = window.innerHeight;
      renderer.resize(width, height);
    }, 200);
});

// Lチカ (For CHIRIMEN)
if(navigator.mozGpio){
  var pnums = [199,244,243,246,245];
  for(var i= 0;i< pnums.length; i++){
    navigator.mozGpio.export(pnums[i]);
    delay(100);
  }
  var val,c = 0;
  var timer = null;

  function blinkStart(){
    if(timer){
      clearInterval(timer);
    }
    timer = setInterval(function(){
      val ^= 1;  
      navigator.mozGpio.setValue(pnums[c],val);
      if(val == 0){
        c=(c+1)%pnums.length;
      }
    },100);
  }

  function blinkStop(){
    if(timer){
      clearInterval(timer); 
    }
    timer = null;
  }

  function delay(millisec){
    var start = new Date();
    while((new Date()-start)<millisec);
  }

  function toggle(){
    if(timer){
      blinkStop();
    }else{
      blinkStart();
    }
  }
  blinkStart();
}

```

# 2. WebSocket 中継サーバー

WenSocketを中継するサーバーです。

[websockets/ws](https://github.com/websockets/ws)を使って、下記サーバーを立ち上げておきます。

```bridge.js
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: xxxx}); 
var connections = [];
var masterconn = null;

wss.on('connection', function (ws) {
  connections.push(ws);
  ws.on('close', function () {
    connections = connections.filter(function (conn, i) {
      return (conn === ws) ? false : true;
    });
    if(ws == masterconn){
      masterconn = null; 
    }
  });
  ws.on('message', function (message,flags) {
    if(flags.binary){
      for(var cnt1=0;cnt1 < message.length; cnt1++){
        console.dir("bynarydata_"+cnt1+":"+message[cnt1]);
      }
      if((masterconn != null)&&(masterconn.readyState == 1)){
        masterconn.send(message);
      }
    }else{
      console.log("message received: "+message);
      if(message == "ping"){
         ws.send("ack");
      }else if(message == "master"){
         masterconn = ws;
         ws.send("master ack");
      }else{
         if((masterconn != null)&&(masterconn.readyState == 1)){
           masterconn.send(message);
         }
      }
    }
  });

  // polling
  setInterval(function(){
    connections.forEach(function (con, i) {
      if(con.readyState == 1){
        con.ping("p");
      }else{
        console.log("ping failed:"+i);
      }
    });
  },5000);
});
```

## 仕組み

スクリーンセイバーアプリ（CHIRIMEN）から、`"master"`というメッセージを送っておきます。

WebSocketサーバーでは、他のコネクションから送られたメッセージを`"master"`に転送します。

これだけ！！

# 3. リモコン

最後はリモコンです。リモコンもWebアプリ。

ボタン押したらWebSocketを送るだけ！
こちらも、[poorws](https://github.com/tadfmac/poorws?files=1)を使います。

```index.html
<!DOCTYPE html>
<html>
<head>
<title>Controller</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8;"/>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
</head>
<body>
<button id="send">PUSH!</button>
<button id="clear">clear</button>
<button id="snow">snow</button>
<div id="status">Connecting...</div>
<script src='jquery-2.0.3.min.js'></script>
<script src='poorws.js'></script>
<script>
var host = "ws://xxx.net:xxxx";
var ws = new poorws(host);
var status = 0;
ws.onStatusChange = function(sts){
  status = sts;
  if(sts == 0){
    $("#status").text("Connecting...");
  }else if(sts == 1){
    $("#status").text("Connected!");
  }else if(sts == 2){
    $("#status").text("Disconnecting...");
  }else if(sts == 3){
    $("#status").text("Re-Connecting...");
  }
};
$(function(){
  $("#send").bind({
    "touchstart mousedown":function(e){
      ws.send("test");
    }
  });
  $("#clear").bind({
    "touchstart mousedown":function(e){
      ws.send("clear");
    }
  });
  $("#snow").bind({
    "touchstart mousedown":function(e){
      ws.send("snow");
    }
  });
});
</script>
</body>
</html>
```

簡単！

# まとめ

展示会というと、まだ出したことの無い人は下記のように考えがちです。
私もそうでした。ちょっと前までは。

- 展示会なんてのに出すのは、凄く頑張ってる凄い人ばかり。自分は違う
- モノを作ってるけど、まだ途中。まだ完成してない。出さない方がいいのでは？

しかし！実際は違います！
だいたい、展示会とかに出してる人は下記のように考えてます。

- やっつけ仕事もネタに出来ればOK！
- 完成？そのうちにね！

展示会での反応を貰って、どんどん良くするのです。
なので、何でもいいから出しましょう。叩かれましょう！ネタにしましょう！

楽しいですよ！！ ←これがいいたい