Sinatra×Websocket×LINE×ペッパーズゴーストでホログラムを作ろう

## はじめに
本記事は[Life is Tech! Advent Calendar2023](https://qiita.com/advent-calendar/2023/life-is-tech)の17日目の記事です！

初めまして！関東のWebSコースメンターのみゅーらです！

今回初めてAdvent Calenderを書くにあたって非常に悩みました、というのもほかのメンターさんほどまだ深いところを紹介できる自信がなかったからです。

さぁ困った。僕にあるものはそんなに多くない！じゃあ僕を構成しているものは何だ！

1. 趣味のディズニー
1. 研究会で行っているメディアアート
1. Life is Tech!でやってるWebService

うーん、幅は広くて多趣味だけど全部深くまで話せるかというと、、、。

...そうだ！
これらを全部組み合わせよう！

ということで今回は
**`Sinatra × Websocket × LINE × ペッパーズゴースト`**
でちょっと面白いホログラムを作ってみようという記事でございます。

すこーし長い記事になるのでゆっくりとお楽しみください。

## いろんな大前提！
### 1. まずどんなのをつくるの
さてまず**Sinatra × Websocket × LINE × ペッパーズゴースト**って言ってもどんなものを作るのかわからない！
って言われるのが関の山だと思うのでまずはそこから。

今回作るのは
**LINEBotに送ったメッセージを自動でホログラムに投影するシステム**
です。難しそうでしょ？

特にホログラムなんかどうやればいいかわからない！
でも実は案外シンプルにそしてオールドな方法でできちゃうものなんです。

### 2. ペッパーズゴーストについて
さぁまずはホログラム要素であるペッパーズゴーストの解説から。
メディアアートの表現技法でもある「ペッパーズゴースト」。

その使用例と言われているものを見てみましょう。

まずはディズニーランドより**ホーンテッドマンションの舞踏会のシーン**！
![ホーンテッドマンションの舞踏会のシーン(著作権違反)](https://blogimg.goo.ne.jp/image/upload/f_auto,q_auto,t_image_sp_entry/v1/user_image/5f/82/03b970f7109f0e1e4794bd9d21d3d9da.jpg)
こんな感じで浮いてて透明なゴーストたち、いますよね。
こちらペッパーズゴーストです。ちゃんと幽霊だ。

次はこんな古いのではありません。(ただ仕組みが明言されてないのも事実)
ディズニーランドより**美女と野獣“魔法のものがたり”の王子の変身シーン**！

![美女と野獣の変身シーン](https://blogimg.goo.ne.jp/image/upload/f_auto,q_auto,t_image_sp_entry/v1/user_image/57/4b/71316f7293eca4fe454db218ef292fc1.jpg)

さぁ、結構すごい技術に思えてきたでしょう。
実はペッパーズゴーストは19世紀に生み出された技術であるにもかかわらず、今でも現役のメディアアートの表現技法のひとつなんです。

アイデアさえあれば今でも多くの人に感動を与えられちゃうんですね。

じゃあそんな技術どうやったらできるの！？
といいますと

![633416_0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/c88d627f-9e21-3988-8e7f-5eab03fa21a8.jpeg)
こうです。


斜めに透明なアクリル板(下敷きでもOK)を立てかけて、そこにipadなどで写したい画面を表示させて反射させるだけ。

実はすっごく簡単なんです。
これで一旦ペッパーズゴーストの説明はおしまい！

### 3. Websocketについて
じゃあ次に説明するのはWebSocketについて。

こちらはざっくり言っちゃうとクライアント側とサーバー側の両方から通信を行える通信方法のこと！
みんながよく使うHTTPとかはサーバー側→クライアント側の一方通行が基本なのでそこが大きな違い！

じゃあWebsocketってどんなことに使われるの？っていうと

`リアルタイムで更新が必要なもの！`

例えば

1. **リアルタイム通知**:
オンラインチャット、ライブストリーミング、オークションなど、ユーザーにリアルタイムな情報を提供する際に活用されます。
1. **リアルタイムデータ表示**:
リアルタイムなデータ更新が必要なダッシュボードやモニタリングアプリケーションで使用されます。
1. **マルチプレイヤーゲーム**:
リアルタイムのゲームプレイやゲーム内コミュニケーションで利用されることがあります。

などなど(ChatGPT調べ)！

一旦WebSocketはこの辺まで知っておけばOK!

### 4. つまり何をするの
今回やるのは
・Ipadに画面をうつしてそれをアクリル板に反射させてホログラムを作る
・反射させる画面にはLINEBotに送られてきたメッセージを表示する
・そのために送られてきたものをリロード無しで自動表示する(そのためのWebSocket)
・それの見た目を整える
といった感じ！

次の章からは実際に作っていきましょう！

### 5. 注意!
今回のこの記事はコードを詳しく解説するものとかじゃなくて作ってみよう！のノリなので結構解説はないに等しいかも！わからなかったら僕に直接聞いてみたりしてみてね！ちなみに記事最後に技術検証用のコードが入ったgithubのレポジトリのリンクが置いてあったりなかったり？？

## 技術検証 - LINE Messaging API
ここからはできることを一つ一つ確認していって最後の章で一気に組み合わせます！

まずはLINEMessagingAPIですが実はそれを使いやすくするための専用のgemファイルがあったりするんです。
そんなわけでgemfileがこちら！

```ruby
ruby '3.0.0'
source 'https://rubygems.org'

gem 'sinatra'
gem 'sinatra-contrib'
gem 'webrick'
gem "dotenv"
gem "line-bot-api"
```

なんと"line-bot-api"というgemが存在するんです！
ありがたい限り。。。

じゃあ後はapp.rbはどうかくの？といいますと

```app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?
Dotenv.load

def client
   @client ||= Line::Bot::Client.new{|config|
       config.channel_id = ENV["LINE_CHANNEL_ID"]
       config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
       config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
   } 
end

post '/callback' do
    body = request.body.read
    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
       error 400 do 'Bad Request' end 
    end
    
    events = client.parse_events_from(body)
    events.each do |event|
        if event.is_a?(Line::Bot::Event::Message)
            if event.type === Line::Bot::Event::MessageType::Text
                message = {
                    type:'text',
                    text: event.message['text']
                }
                client.reply_message(event['replyToken'], message)
            end
        end
    end
    
    "OK"
end
```

こんな感じで書いてあげるとLINEBotに送られてきたMessageをオウム返ししてくれるコードの出来上がり！

今回はLINEBotの挙動を書いてあげればいいだけなのでerbファイルはいりません！

LINEBotのアカウントの作り方や環境変数のセットの仕方は今回は省きます、[こちらの記事(ほかの方のもの)](https://www.mizucoffee.com/archives/1076)を参考にしてみてください！

また今回のコードを書くにあたってデバッグするためには外部からサイトにアクセスできるようにしなくちゃいけません。ということは毎回HerokuとかRailwayにデプロイしてからじゃないと試せないってこと、、、？

そんなことはありません。ngrokを使ってみましょう。
ngrokで一時的に公開状態にしてあげれば書いたそばから変更を適用できるのでめっちゃ楽です。その代わり閉じるごとにLINEBotのWebhookURLを書き換えなきゃいけないのは変わらないのでお気をつけを。

ちなみに作ったものはこんな感じになるよ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/84dc2528-e0de-8db8-6c0f-8a333b639b36.png)


## 技術検証 - Websocket
さぁ難題はこっちだった！！！
Websocket、かなり難しい部分がある、、、今回これのせいで一週間以上ロストした(._.)。

今回はfaye-websocketっていうgemを使って実装するよ！

まずgemfileはこんな感じ！

```ruby
source 'https://rubygems.org'

gem 'sinatra', '~> 2.1'
gem 'sinatra-contrib', '~> 2.1'
gem 'puma'
gem 'faye-websocket'
```
あれ？faye-websocket以外にも見慣れないgemが、、、

実はWebsocketを使うと普段使ってるWebrickが使えませぬ。そのため今回はpumaを使っていきます。

さぁ、あとはapp.rbとindex.erbだ、、、！

```app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?
require 'faye/websocket'

set :sockets, []

get '/' do
  erb :index
end

get '/websocket' do
  if Faye::WebSocket.websocket?(request.env)
    ws = Faye::WebSocket.new(request.env)

    ws.on :open do |event|
      settings.sockets << ws
    end

    ws.on :message do |event|
      settings.sockets.each do |socket|
        socket.send(event.data)
      end
    end

    ws.on :close do |event|
      ws = nil
      settings.sockets.delete(ws)
    end

    ws.rack_response
  end
end
```

```index.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Chat</title>
</head>
<body>
    <input type="text" name="message" placeholder="message">
    <button>送信</button>

    <div id="messages"></div>


    <script>
      if (location.protocol === 'https:') {
        var ws = new WebSocket('wss://' + location.host + '/websocket');
      } else {
        var ws = new WebSocket('ws://' + location.host + '/websocket');
      }

      ws.onopen = function() {
        console.log('connected');
      };

      ws.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = document.createElement('div');
        message.innerHTML = '<strong>' + data.message + '</strong> ';
        document.querySelector('#messages').appendChild(message);
      };

      ws.onclose = function() {
        console.log('disconnected');
      };

      document.querySelector('button').addEventListener('click', function() {
        var message = document.querySelector('input[name="message"]').value;
        ws.send(JSON.stringify({
          message: message
        }));
      });
    </script>
</body>
</html>
```
大体こんな感じ！
これを作るとどんなのが生まれるかというと～？？？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/c1e49090-c99a-71eb-df2d-be1f8a9b4ffa.png)

こんな感じでFormで送信した文字が表示されるよ！
これは同じタイミングで同じサイトを開いている人全員のところで**リロードをかけることなく**随時表示される！

もうほとんどいわゆるSNSって感じだね！(過言かもしれない)

さてこれで技術検証はおしまい！

実際に組み合わせていくよ！

# Sinatra×Websocket×LINEBotの実装
さてここからはもう組み合わせるだけなのでざっくりお見せします！！

まずgemfileがこんな感じ！
```ruby
ruby '3.0.0'
source 'https://rubygems.org'

gem 'sinatra', '~> 2.1'
gem 'sinatra-contrib', '~> 2.1'

#履歴用
gem 'sinatra-activerecord', '~> 2.0'
gem 'activerecord', '~> 6.1'
gem 'pg', '~> 1.2'
gem 'rake', '~> 13.0'

#websocket用
gem 'puma'
gem 'faye-websocket'

#LINEbot用
gem "dotenv"
gem "line-bot-api"
```

大体app.rbがこれ！
```app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/activerecord'
require 'sinatra/reloader' if development?
require './models'
Dotenv.load
require 'faye/websocket'
set :sockets, []

def client
   @client ||= Line::Bot::Client.new{|config|
       config.channel_id = ENV["LINE_CHANNEL_ID"]
       config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
       config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
   } 
end

post '/webhook' do
    body = request.body.read
    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
       error 400 do 'Bad Request' end 
    end
    
    events = client.parse_events_from(body)
    events.each do |event|
        if event.is_a?(Line::Bot::Event::Message)
            if event.type === Line::Bot::Event::MessageType::Text
                message = {
                    type:'text',
                    text: event.message['text']
                }
                client.reply_message(event['replyToken'], message)
                
                #履歴の作成
                History.create(text: event.message['text'])
                
                # WebSocketを通じて受信したメッセージを送信する
                settings.sockets.each do |socket|
                    socket.send(event.message['text'])
                end
            end
        end
    end
    
    "OK"
end

get '/websocket' do
  if Faye::WebSocket.websocket?(request.env)
    ws = Faye::WebSocket.new(request.env)

    ws.on :open do |event|
      settings.sockets << ws
    end

    ws.on :message do |event|
      settings.sockets.each do |socket|
        socket.send(event.data)
      end
    end

    ws.on :close do |event|
      ws = nil
      settings.sockets.delete(ws)
    end

    ws.rack_response
  end
end

get '/' do
    erb :index
end
```

うーんもうなんかすごいことになってるね。
でもまぁここまで読み進められた君ならわかるはず！

実はこっそり合体させるにあたってLINEで送られてきた文章をHistoryとして保存しています。

最後にindex.erbがこれ！
```index.html
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title>LINE×Websocket</title>
		<link rel="stylesheet" href="/assets/stylesheets/peppersghost.css">
	</head>
	<body>
		<div class="center">
			<div class="white-color">
				<div id="messages"></div>
			</div>
		</div>
	
	    <script>
	      if (location.protocol === 'https:') {
	        var ws = new WebSocket('wss://' + location.host + '/websocket');
	      } else {
	        var ws = new WebSocket('ws://' + location.host + '/websocket');
	      }
	
	      ws.onopen = function() {
	        console.log('connected');
	      };
	
	      ws.onmessage = function(e) {
	      	var text = e.data; 
			var message = document.createElement('h1'); 

			//以下2行を消せば複数のメッセージが表示可能
			var messagesDiv = document.querySelector('#messages');
			messagesDiv.innerHTML = '';
			
			message.innerHTML = '<strong>' + text + '</strong> ';
			document.querySelector('#messages').appendChild(message); 
	      };
	
	      ws.onclose = function() {
	        console.log('disconnected');
	      };
	    </script>
	</body>
</html>
```

あとは画面の背景を真っ黒にして、文字を白くして、文字を画面の真ん中にセットされるようにcssを書いてあげれば、、、？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/eec03c70-57e4-fa47-74b8-625ddcb0cec9.png)

完璧！！これで完成！！
あとはこれにJavaScriptとかで背景彩らせてあげもうおしゃれなサイトの出来上がり！
(でも今回は時間がないからここまで！)

あとはこれをデプロイしてあげてリンクを取れば完成、、！

# 完成品はこちら！
・Webサイトのリンクがこちら！
https://peppersghost-production.up.railway.app/

・LINEアカウントはこちら！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/26ea76e0-0aea-c83b-facb-ce86799344d8.png)

# 使い方
まずはipad(パソコンでもいいよ)とスマホを用意する！

そしたらipadでまずはサイトを開く！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/c0e76bc9-3ae3-ea7f-0c37-b0db9099c461.png)

こんな感じの真っ暗な画面になるはず！

その状態でスマホから上記のLINEを追加してメッセージを送ってあげる！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/2a5e4b97-2b31-6c24-3001-3136983f436f.png)

そうすると画面に文字が出るよ！

ちなみにサイトを開いているタイミングでほかのユーザーがメッセージを送ってきたら表示されちゃうからほかの人が傷つくような言葉は送らないでね！履歴とってるからね👀

# ペッパーズゴーストやってみよう
さぁあとはこれを透明なアクリル板に映すだけ、、、！

というわけで材料はこちら！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/f5014032-96c0-c60e-8d51-6c4eda7e3f32.png)

わぁ。本がいっぱいだね。しかも勉強系ばっかりだ。

次にパソコンとIpadをこうするのじゃ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/5db1cc15-554b-ad0f-01cf-380c428f3735.png)

IpadにはサイトをうつしててパソコンにはLINEがうつってるよ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/ea0ca1eb-c590-13a7-3b77-9451ceba3427.png)

次に本をこうするのじゃ。分厚い本は世界でたまーに役立つものの一つだね。

そして透明な下敷きをここに置くんじゃ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/efeeaf27-1be3-80e6-f603-3d00a5933f56.png)

じゃあ電気を消してIpadをこんな感じで上に載せて...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/1bb44838-378b-2976-95f7-734397bbab8b.png)

あとは電気を消してLINEにメッセージを送れば、、、！？！？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628924/20acfe06-e537-d3cc-9b83-7e8c6fd15911.png)

はい。ここで一つ悲しいお知らせがあります。
リンクとかのタブ欄が出てるのはまぁとりあえず許してほしいです。

それはそうと私失念しておりました。
これ上下を合わせると左右が反対になってしまいますね。
文章が見えることには見えるんですが、、、、。
左右真逆なので全く読めませんね。。

# Githubのリンクはこちら
・技術検証 - LINEMessagingAPI
https://github.com/Hayato1031/linebot_sinatra

・技術検証 - Websocket
https://github.com/Hayato1031/faye-websocket

・PeppersGhost-tutorial
https://github.com/Hayato1031/peppersghost-tutorial

# まとめ
今回はSinatra×Websocket×LINE×ペッパーズゴーストっていうごちゃまぜ記事を作ってみました！

あと一歩惜しかったりするサイトだけどこれ実は友達と一緒に使うとめっちゃ楽しいサイトなんです。ぜひ使うだけでも使ってみてくださいな。

これにあとJavaScriptで装飾したりすればどんどん進化するね、、！
それは次回記事を書くときとかに取っておきます！今回はあんまり時間がないのでここまで！

皆さんもぜひ自分のできることを最大限に掛け合わせるような作品を作っていってくださいませ🎉