5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaScript for Robotics or HardwareAdvent Calendar 2015

Day 20

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

Last updated at Posted at 2015-12-20

さて、Advent Calendarですよ!

めりーくりすまーす!!!

いつもの謎画像

※時間がないのでWIFI-TNGとESP-WROOM-02で始めるWIFI Arduino使い回しですw

今度は何?

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

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

まじかよ!

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

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

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

CHIRIMENとは?

CHIRIMEN

CHIRIMENは、MozOpenHardプロジェクトで開発が進められている、Firefox OS搭載のボードです。以前KDDIがイベントとかで配ってたOpen Web Boardと似ていますが、豊富なGPIOピンが搭載されているのが特徴です。

JavaScriptでGPIO制御できる、こうしたボードは結構あります。Tesselや、konashiが有名ですね。

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

詳細はMozOpenHard CHIRIMENを見てください。

Lチカ(3時間の前に終わってたこと)

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

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

下記を参考にしました。

test-gpio

IOポートの情報

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

CHIRIMEN pin config.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);

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

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

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

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

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

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

スクリーンセイバー

こんなの!
Lチカ付き!(←あまりコラボ感はないw)

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

リモコンつき

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

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

作ったコードはこちら!tadfmac/screensaver

さて、頑張ります。

1. スクリーンセイバー

画像を集める!

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

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

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

画像を動かす!

画像を動かせば、スクリーンセイバーの出来上がり!
Pixi.JSを使って動かしました。

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

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

手抜きリモコン対応

WebSocketに対応する部分を作ります。
時間が無いので、慣れたpoorwsを使います。

これで、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を使って、下記サーバーを立ち上げておきます。

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を使います。

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!
  • 完成?そのうちにね!

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

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

5
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?