プレゼンにリアルタイム投票機能をつけてみた話

学生LTと名古屋なんとか専門学校のコラボ企画でやったプレゼンです。
#作ったもの
プレゼン中にリアルタイム投票ができるようにする仕組みを作りました

具体的には、
参加者が指定のURLにあらかじめアクセスしておく。
↓
プレゼンと連動して参加者の端末に選択肢が表示される
↓
それをクリックするとリアルタイムで前の画面に結果が映る

##こんな感じ
参加者端末
![無題.png](https://qiita-image-store.s3.amazonaws.com/0/211349/4ba5c61f-f89a-a7d3-3227-40e57f0ac389.png)

スライド画面 - スライドはhtmlで作りました
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3231313334392f62313933353133372d363736302d303132332d326535662d3333613937633262616561322e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/211349/8a6bcc13-c401-dede-f0f1-c9dcb413d54a.png)

#技術的な話
node.jsとsocket.ioを用いて作りました。
![detail.png](https://qiita-image-store.s3.amazonaws.com/0/211349/33f84cd4-de81-e416-27b0-94e6b6e70c3a.png)
プレゼン画面と投票画面が同じindex.htmlで、かつ、Ctr+Bで簡単にホストが取れるという、セキュリティ的にかなり危ない感じです。
また、誰かが投票するたびに、全員の端末にその結果が転送される仕組みなので、大人数で行う場合には向いていません。
デモとしては面白いですが、実用化にはもう少し改良が必要です。

##サーバーサイド
expressとsocket.ioの非常にシンプルなつくりです

```server.js
var express=require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var port = process.env.PORT || 3000;

app.use(express.static('public'));

app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', function(socket){
  socket.on('question', function(ch){
    io.emit('question', ch);
  });
  socket.on('answer', function(ans){//選択肢を受け取ると
    io.emit('answer', ans);
  });
  socket.on('comment',function(com){//フィードバックにより、プレゼン後コメント機能を実装
    io.emit('comment',com)
  });
});

http.listen(port, function(){
  console.log('listening on *:' + port);
});
```

##クライアントサイド
<details>
<summary>htmlソース</summary><div>

```html
<!DOCTYPE html>
<html lang="en">

<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">
  <title>投票スライド</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
  <link rel="stylesheet" href="/css/slide.css">
  <link rel="stylesheet" href="/css/switch.css">
  <link rel="stylesheet" href="/css/style.css">

</head>

<body>
  <!--Ctr+bで登壇者モード-->
  <div id="switch_slides_div">
    <div></div>
    <div></div>
    <div></div>
  </div>
  <section id="top_slide">
    <h2>プレゼンをもっとアクティブに！</h2>
  </section>
  <section id="intro">
      <h1 class="top_left">自己紹介</h1>
      <img src="img/icon.jpg" style="position:absolute;top:0;right:0;margin:20px;width:15vw;" />
      <p class="align_left">
        名前:Moscwa<br />
        Twitter:@moscwa_<br />
        使える言語:Javascript / python / CSS（チューリング完全なのでプログラミング言語と信じてる！）/Unity/node.js<br />
        <br />
        誰得な開発をするのが好きな高校生。最近は機械学習とかもやってますが、基本WEB系フロントエンジニアです。<br />
        ニューラルネットワーク完全に理解した<br />
      </p>
    </section>
  <section id="slide1">
    <p>これまでのLTは登壇してる人→聞く人の一方的なプレゼンだった<br>
      確かに学生LTは登壇者と参加者の垣根は低いように思えるが、<br>もっとアクティブに参加してもいいじゃないか！</p>
  </section>
  <section id="slide2">
    <h3>と、いうわけで、<br>
      リアルタイムアンケートを取ろう！</h3>
    <p>(失敗したらすみません)</p>
  </section>
  <section id="slide3">
    <h3>お使いの端末でどうぞ！</h3>
    <h1>bit.ly/2Q3r6IJ</h1>
  </section>
  <section id="slide4">
    <h3>とりま、アンケート</h3>
    <h3>「あなたの使っているエディタは？」</h3>
  </section>
  <section id="slide5" class="vote">
    <h4>「あなたの使っているエディタは？」</h4>
    <canvas id="chart1" width="400" height="400"></canvas>
  </section>
  <section id="slide6" >
    <h3>こんな感じで簡単な投票システムが実装できます</h3>
  </section>
  <section id="slide6">
    <h3>Q:　パワポにそんな機能あるの？</h3>
    <h3 class="active">A:　実はppdxではなくhtmlです</h3>
  </section>
  <section id="slide7">
    <h2>どうやってるか？</h2>
    <h4>htmlでプレゼンを作る<br>
    ↓<br>
    node.js+websocketで通信<br>
    ↓<br>
    受け取ったデータをChart.jsで表示</h4>
  </section>
  <section id="slide7-2">
    <img id="detail" src="img/detail.png">
  </section>
  <section id="slide7">
      <h2>苦労したこと</h2>
      <p>・chart.jsで作られるチャートが異様にでかかった<br>
      ・GoogleCloudPlatformへのデプロイ</p>
  </section>
  <section id="slide8">
      <h2>質疑応答</h2>
  </section>
  <section id="slide9">
      <h3>時間が余ったのでアンケート取ります</h3>
  </section>
  <section id="slide10" class="vote">
      <h3>「あなたが一番好きなプログラミング言語は？」</h3>
      <canvas id="chart2" width="400" height="400"></canvas>
  </section>
  <section id="slide11" class="vote">
      <h3>プログラミング初めてからどれくらい？</h3>
      <canvas id="chart3" width="400" height="400"></canvas>
  </section>
  <section id="slide12" class="vote">
      <h3>Twitterやってる？</h3>
      <canvas id="chart4" width="400" height="400"></canvas>
  </section>
  <section id="slide13">
      <h3>やってる人、<br>
        @Moscwa_
        <br>フォローお願いします！</h3>
  </section>
  <section id="slide14" class="vote">
      <h3>彼女（彼氏）いる？</h3>
      <canvas id="chart5" width="400" height="400"></canvas>
  </section>
  <section id="slide16" >
      <h3>今後の展望</h3>
      <p>・質問をこの場で編集できるようにしたい<br>
      ・コメント機能とかつけたい<br>
      ・あわよくばppdxに代わる次世代のプレゼンに！</p>
  </section>
  <section id="slide16">
      <h2>Thank You For Your Time!</h2>
  </section>
  <section id="slide17">
    
  </section>
  <div id="vote">
    <h1 id="title"></h1>
    <div id="chc">
      <h1>待機中...</h1>
    </div>
    <div id="comment_div" class="input-field col s12">
      <input id="comment" type="text" class="validate">
      <label for="comment">コメントを流す</label> 
      <a id="send" class="waves-effect waves-light btn">send!</a>
    </div>
  </div>
  
  <script>
    var question_obj = {
      'slide5': ['使ってるエディタ', 'vim', 'emac', 'VSCode', 'atom', 'Sublime Text', 'その他'],
      'slide10': ['好きなプログラミング言語','C言語','Java / Kotlin','PHP / Ruby','Python','JS','Go','その他'],
      'slide11': ['プログラミング歴','半年未満','半年～1年','1年~2年','2年～5年','5年以上！'],
      'slide12':['twitter','やってる','やってない'],
      'slide14': ['恋人','いる','いない'],
    }
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
  <script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
  <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.min.js"></script>
  <script src="/js/main.js"></script>
  <script src="/js/slide.js"></script>
</body>
</html>
```
</div>
</details>

\<section>タグが一つのスライドのページです。
slide.jsでz-index と　displayを操作してクリックでページ遷移になります

<details><summary>main.js（投票機能）</summary><div>

```javascript
var chart_count = 0
var colors = ['red', 'pink', 'purple', 'blue', 'green', 'orange']
var socket = io();
var myChart;


socket.on('question', function (choices) {
    col = colors[Math.floor(Math.random() * colors.length)];
    chc.textContent = null;
    title.innerText = choices[0]
    chc_list = []
    ans_list = []
    for (i = 1; i < choices.length; i++) {
        elm = document.createElement('a')
        elm.innerText = choices[i]
        elm.classList.add('ch', 'waves-effect', 'waves-light', 'btn-large', col)
        elm.addEventListener('click', (e) => {
            socket.emit('answer', e.target.innerText)

            chc.innerHTML = "<h1>待機中...</h1>"
        })
        chc.appendChild(elm)
    }
    chc.classList.add(col, 'darken-3')
});


chc_list = []
ans_list = []
socket.on('answer', function (ans) {
    index = chc_list.indexOf(ans)
    if (index < 0) {
        chc_list.push(ans)
        ans_list.push(1)
    } else {
        ans_list[index] += 1
    }

    console.log(chc_list)
    console.log(ans_list)
    if (document.getElementById("chart" + chart_count)) {
        if (myChart) {
            //myChart.destroy()
        }
        Drow(document.getElementById("chart" + chart_count).getContext('2d'), 'sample', chc_list, ans_list, 'pie')
    }
});

function Drow(ctx, title, labels, data, type) {
    console.log(ctx)
    ctx.canvas.width = 500;
    ctx.canvas.height = 500;
    new Chart(ctx, {
        type: type,
        data: {
            labels: labels,
            datasets: [{
                label: title,
                data: data,
                backgroundColor: [
                    'rgba(244, 67, 54,.5)',
                    'rgba(139, 195, 74,.5)',
                    'rgba(103, 58, 183,.5)',
                    'rgba(33, 150, 243,.5)',
                    'rgba(255, 193, 7,1.5)',
                    'rgba(121, 85, 72,.5)',
                    'rgba(233, 30, 99,.5)',
                ]
            }]
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
        }
    })
}
socket.on('comment', function(comment){
    elm=document.createElement('p')
    elm.innerText=comment
    elm.classList.add('comment')
    document.body.appendChild(elm)
});


send.addEventListener('click', function () {
    socket.emit('comment', comment.value)
    comment.value = ""
});



document.addEventListener('keydown', function (e) {//投票画面→スライドの切り替え
    if (e.key == "b" && e.ctrlKey) {
        vote.style.display = "none"
        document.addEventListener('click', function () {
            main_func()
        })
        document.addEventListener('keydown', function (e) {
            if (e.key == "Enter") {
                main_func()
            } else if (e.key == "n") {
                switch_slide()
            } else if (e.key == "r") {
                demo_frame.src = demo_frame.src
            }
        })
    } else if (e.key == "q" && e.ctrlKey) {
        title = prompt('題名を入力')
        chc_list.push(title)
        while (true) {
            ch = prompt('選択肢を入力\nEで終了')
            if (ch == "E") {
                break
            }
            chc_list.push(ch)
        }
        
        canvas=document.querySelector("#"+current_slide.id+">canvas")
        if(!canvas){
            canvas = document.createElement('canvas')    
            canvas.width = "400"
            canvas.height = "400"
            current_slide.appendChild(canvas)
        }
        chart_count += 1
        canvas.id = "chart" + chart_count
        socket.emit('question', chc_list)
    }
})

```

</div></details>

質問内容が{スライドのid:[題名,選択肢1,選択肢2,選択肢3,・・・]}　の形でindex.htmlのscriptタグで保持されています
グラフ絵画にはchart.jsを使いました


<details>
<summary>slide.js</summary><div>

```javascript
var current_slide=top_slide
top_slide.style.display="flex";

(start=()=>{
  actions_lists=document.getElementsByClassName('actions_list')
  for(i=0;i<actions_lists.length;i++){
    for(j=0;j<actions_lists[i].children.length;j++){
      actions_lists[i].children[j].classList.add('action')
    }
  }
  setTimeout(()=>{
    top_slide.style.zIndex="150"
  })
})();

slides_list=document.getElementsByTagName('section')
for(i=0;i<slides_list.length;i++){
  slides_list[i].style.zIndex=100-i
}


main_func=()=>{
  action=document.querySelector("#"+current_slide.id+' .action')
  if(action){
    action.classList.remove('action')
    return;
  }
  if(current_slide.nextElementSibling.classList.contains('vote')){
    chart_count+=1
    socket.emit('question',question_obj[current_slide.nextElementSibling.id]);
  }
  simple_switch_slide()
}


switch_slides=document.querySelectorAll('#switch_slides_div > div')
switch_slide=()=>{
  current_slide.style.zIndex="150"
  current_slide.style.transform="rotate(-120deg)"
  for(i=0;i<switch_slides.length;i++){
    switch_slides[i].classList.add('active')
  }
  current_slide=current_slide.nextElementSibling
  current_slide.style.zIndex="110"
  setTimeout(()=>{
    for(i=0;i<switch_slides.length;i++){
      switch_slides[i].classList.remove('active')
    }
    current_slide.style.zIndex="150"
  },500)
}
simple_switch_slide=()=>{
  current_slide=current_slide.nextElementSibling
  current_slide.style.zIndex="150"
}

function execCopy(string){
  temp = document.createElement('div');
  temp.appendChild(document.createElement('pre')).textContent = string;
  s = temp.style;
  s.position = 'fixed';
  s.left = '-100%';
  document.body.appendChild(temp);
  document.getSelection().selectAllChildren(temp);
  result = document.execCommand('copy');
  document.body.removeChild(temp);
  return result;
}
```
</div></details>
画面クリックで次のスライドへ行く操作の実装

<details><summary>style.css</summary><div>

```css

html,body{
    position: absolute;
    width:100%;
    height:100%;
    margin:0;
    overflow: hidden;
  }
  section{
    z-index: 50;
    position: absolute;
    width:100%;
    height:100%;
    padding:5%;
    display: flex;
    flex-direction: column;
    justify-content:space-around;
    align-items: center;
    background-color:#f5f5f5;
    transition-duration: 500ms;
    text-align: center;
  }
  .horizonal{
    flex-direction: row;
  }
  h1{
    font-size: 8vw;
  }
  h2{
    font-size: 6vw;
  }
  h3{
    font-size: 5vw;
  }
  h4{
    font-size: 3vw;
    margin: 0;
  }
  .top_left{
    position: absolute;
    top:5%;
    left:5%;
  }
  
  p{
    font-size:2vw;
  }
  li{
    text-align: left;
    font-size: 2vw;
  }
  
  img.half{
    width:50%;
  }
  img.one_third{
    width: 33%;
  }
  img.quarter{
    width: 25%;
  }
  
  .align_left{
    width: 90%;
    text-align: left;
  }
  
  li,p{
    transition-duration: 300ms;
  }
  .action{
    opacity: 0;
  }
  
#vote{
    z-index: 200;
    position: absolute;
    top:0;
    left:0;
    width: 100%;
    height: 100%;
    background-color:#f5f5f5;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;
}
#chc{
    width:80vmin;
    height: 60%;
    display: flex;
    flex-direction: column;
    justify-content:space-around;
    align-items: center
}

#chc >a{
    width: 80%;
    height:13%;
    text-align: center;
    font-size: 150%;
}
#comment_div{
    width: 100%;
    height: 5%;
    display: flex;
    flex-direction: row
}

.comment{
    position: absolute;
    left:100%;
    animation: pass_comment 3s linear;
    font-size: 200%;
    z-index: 1000;
}

@keyframes pass_comment{
    to{
        transform: translateX(-120vw);
    }
}

#detail{
    width: 90%;
}
```
</div></details>
格別何か工夫をしてるわけではないです

#まとめ
セキュリティ的にもパフォーマンス的にも問題のあるスライドですが、そこのところさえ解決できれば、今後プレゼンの幅を広げる一つの手段になりうると思っています。
!