異なるGCPアカウント間でFirebaseのプロジェクトを移行する

モチベーション

最近Webアプリ作る時には専らFirebaseを使うことが多くなりました。 画面ぽちぽちで簡単に環境が作成できるので、気付いたらプロジェクトが量産されてます。 そのほとんどは何を目的としたアプリなのかも忘れ去られる運命なのですが、なかには法人化してまでちゃんと作り込みたいサービスが出てきました。

しかし、プライベートのGoogleアカウントでプロジェクトを作成したおかげで、請求が個人の方に来て面倒になる懸念が出てきました。(無料枠があってまだ請求は来てませんが)

加えて、他のGCPサービスを使う時や開発メンバーが増えた時のことも考えると、法人アカウントでプロジェクトを持っておいた方が良い気がします。

そこで今回は異なるGCPアカウント間でFirebaseのプロジェクトの移行を行ったので、備忘録として残しておきます。

やること

  • Firebaseの新規プロジェクトを作成
  • Authenticationのデータ移行
  • Firestoreのデータ移行
  • Storageのデータ移行
  • Functionsのデプロイ

注意点

以降の作業では、移行元⇆移行先のプロジェクト切り替え操作は省くので、ご了承ください。GCPアカウントが違うので、

firebase logout
firebase login
firebase use [project-name]

などの切り替えが発生してます。

Firebaseの新規プロジェクトを作成

ここから頑張る https://console.firebase.google.com/

Authenticationのデータ移行

firebase.google.com

事前に移行元のAuthentication画面から、パスワードハッシュパラメータを確認しておきます。

# ユーザーをexport
firebase auth:export accounts.json

# ユーザーをimport
firebase auth:import accounts.json --hash-algo [algorithmの値] --hash-key [base64_signer_keyの値] --salt-separator [base64_salt_separatorの値] --rounds [roundsの値] --mem-cost [mem_costの値]

Firestoreのデータ移行

firebase.google.com

事前にFirestoreをプロビジョニングしておきます。

GCPアカウントを跨ぐので、移行元のバケットに対する読み取り権限を付与する必要があることに注意です。

# Firestoreの全コレクションをStorageにexport
gcloud firestore export gs://[SOURCE_BUCKET] --async

# exportしたコレクションをimport
gcloud firestore import gs://[SOURCE_BUCKET]/[EXPORT_PREFIX] --async

Storageのデータ移行

cloud.google.com

  • 事前にStorageをプロビジョニングした後に、Data Transferのコンソール画面上で移行。
  • 移行元のStorageを参照しているリンクの更新。(Firestoreのドキュメントに点在してるかも)

Functionsのデプロイ

# 環境変数をセット
firebase functions:config:set app.apikey="hogehoge"

# ソースコードをデプロイ
firebase deploy --only functions

2019年を振り返る

(WPから記事を移行してきました @2020/04/24)

一年のまとめ記事を書こうと思いつつ、ついに紅白歌合戦が始まったので慌てて筆をとりました。 今年は転職したり海外行ったり、思い返せばいろいろなことがあった年だったので、それぞれのイベントのことを思い返しながら言葉に残しておこうと思います。

1月

落合さんの個展へ

にしざか’s Instagram profile post: “落合さんの個展へ 目的やルールが決められた世界で自分を最適化してくのも楽しいけど、何にも縛られずにアートを通して自己表現するのもよいのかなと思ったり。 #質量への憧憬 #sehnsucht_nach_masse”

光と影を自在に操った写真にデジタルの中に質量感を感じてドラマチックな印象を受けました。

また、アーティストとしての落合さん作品に触れ、他人を評価を気にせず好きなように発信し続けることや、決められたフレームにおいてゲーム感覚で上位を目指してルールに最適化されることの面白さを感じました。

2月

台湾旅行

にしざか’s Instagram post: “連休を使って台湾へ 夜市の屋台で食を楽しみ、太魯閣で時間の流れが生み出した自然の彫刻に圧倒され、雨の九份に趣を感じた、そんな旅。 日本寒い。 #taiwan #taipei #taroko #jiufen”

連休を使って研究室メンバーと台湾に行ってきました。過去にも行ったことがあったのですが今回はタロコ国立公園にも足を運び、自然の彫刻美を感じてきました。

それにしても台湾に行くと独特な匂いが街や屋台や飲食店に充満している気がして、、たぶん肉髭って食べ物なんだけどちょっと苦手です笑

3月

Quantum Summit

https://www.q-summit.net/

量子コンピュータのカンファレンスに参加しました。

スパコンを上回る性能!既存の暗号技術が通用しなくなる危険性がある!など騒がれている量子技術ですが、実際のところどんな仕組みでどんな分野に応用が期待されていてその実装はどのくらいの段階なのかということを、研究者やスタートアップの聞いてきました。

その話では、完全にノイズがなくエラー訂正機能が実装された汎用量子コンピュータが開発されるまでには数十年かかるため、現段階ではノイズがありスケールしない量子コンピュータ(NISQ)が現実的な落とし所とされているとのことです。その上でビジネス的な応用の面では、まず古典コンピュータと量子コンピュータを両立して実装されて運用されていきます。

(参考)量子コンピュータの現在とこれから

会社の卒業パーティーでWebARを使った企画をやる

nszknao.hatenablog.com

前職のIBMはそれなりに大きいIT企業だったので、テックを使った楽しい企画をやりたい思いで作りました。

7月

IBM卒業

にしざか on Instagram: “ありがたいことに、たくさんの友人から送別していただき、家がお花畑です💐 都内にはいるんでこれからも仲良くしてやってください 新天地でもやったるでー”

2017年に入社して2年ちょっと勤めたIBMを退職しました。

SI業界はWeb系ベンチャーなどと二項対立的に比較されて批判を受けることも多いですが、決められたモノを決められた期間に決められたリソースの中でデリバリーするという意味では勉強になりました。それでも止めることを決めたのは、SIはどこまでいっても客商売なのでプロダクトの評価軸が外部(=お客さん)にあることが僕の中にあるモノづくりをやりにくくさせたことが大きいです。

それにこれは組織でやっていく以上しょうがない部分もあるのですが、社内での評価が自分の上司やPMに基づいていて、ユーザーからFBが反映されにくい点です。エンジニアとしてプロダクトを作る側の人間としては、自分の評価はプロダクトを使ってくれているユーザーに直接してもらいたいし、それによって収入も決まってくる方が作り甲斐があります。

9月

日本一の星空を見に阿智村

note.com

年初に祖父の田舎で満点の星空をみて感動したのが忘れられず、山の斜面に寝転がりながら頭の中を真っ白にしてボーッと星を眺めるやつやりたい!との思いで友人を誘って行きました。車買って毎週末でも見に行きたいし星をきれいに撮るためのカメラも欲しい。

11月

統計検定2級を取得

Kaggleが強い人になるためデータを見る目を養うきっかけとして統計検定を受けました。 ビッグデータの時代では統計的検定など意味ないなど言われることもありますが、実際の現場ではまだまだ分析に使える有益な情報が少なく、欲しいアウトプットを得るためにあわててデータを取り始める必要がある場面にも出会すことも多いです。データの有用性を検証するためにも統計的な知識は必要だし、機械学習の周辺技術を習得するときの理解を深めるためにも検定をとっておきました。

白金鉱業の勉強会参加

note.com

運営人に知り合いがたくさんいたので参加させていただきました。エンジニアが勉強会に参加するのはやっぱ大事だと気付かされた。

まとめ

今年は転職したり勉強会に参加したりワクワクするビジネスを知人と考えたり、自分にとっては準備の年でした。なりたい自分になるために必要な要素が見えてきた気がします。来年の抱負とかはまた年初に書こうと思いますが、その分来年は挑戦の年にするためとりあえずメディアでの発信を増やしていきます。

この振り返り記事を書いてて思ったのですが、何かイベントや心境の変化や考えをまとめたい時は、noteでもTwitterでもInstagramでもいいのでメディアに投稿しておくと年末に振り返りやすいですね。

手軽にメモを登録できて、リマインドしてくれるLINE BOTを作りました

日常生活の中でふと浮かんだアイデアや疑問を逃したくない

そんな思いから、今回紹介するLINE BOT『MEMOリマインドくん』を作成することにしました。

前田裕二さんの著書1に影響を受け、毎日何らかの目的を持って、あらゆる情報に対して毛穴むき出し状態でいるためにメモを取り始めた僕ですが、その方法として今まではiPhoneのメモアプリを使ってました。

iPhoneのメモアプリは毎日のメモ記録には向かないのでは

このメモアプリですが、会議の議事録のようにその場で完結する情報を手軽にまとめるといった使用法であれば問題ないと思います。しかし、毎日ストックされるメモを記録する場所としては使いにくさを感じていました。例えば、 - ただただメモを追記していくだけだと毎回一番下までスクロールするのが大変 - かといって日付ごとにフォルダを分けてたら、その作業自体が面倒だし、なにより時間が経つほどに過去メモの参照が困難になる そこで、今回のようなユースケースに沿って作られた別のアプリを落として使おうかとも思いましたが、メモアプリ自体種類がたくさんありすぎて選別に時間がかかるくらいなら、欲しいものは自分で作っちゃおう精神で突き進むことにしました。

LINE BOTとして作るメリット

それではどうやってオリジナルのメモアプリを実現しようかと考えるわけですが、毎日のメモをだらだらと投稿できて、おまけに記録済みのメモを手軽に振り返ることができたらいいなぁ、と妄想したときに思いついたのがLINE BOTです。

そのメリットもいくつか挙げられます。 - ユーザーは日付を意識することなく、ただLINEすればいい - ノンアクションでその日のメモを振り返ることができる - 毎日のメモデータがDBに蓄積されるので、今後ダッシュボードを作るなど応用が効く LINEってスマホ持ってる現代の日本人ならだいたいがインストールしていますし(家族や周りの友人を見た体感)、わざわざネイティブアプリ作るほどの手間やコストもかけたくないと思っていた僕には最適な実現方法です。

ちなみに、利用イメージはこんな感じです。 (テスト開発中のスクショなので、変なレスがありますが無視してください、、)

利用イメージ

ユーザーは、メモしたいことを思いついたらその場で投稿しておきます。すると、その日の終わりにメモの一覧をリマインドしてくれるイメージです。 まだまだシンプルなアプリですが、今後運用の中でアップデートしていきます。

こちらに友達追加のリンクも貼っておくので、どんなものかちょっとでも気になってくれましたら、ぜひ友達追加してあげてください!

LINE友達追加のリンク

実現する機能

機能は至ってシンプルです↓ - 投稿されたメモを登録する - 23:59に一日のメモをリマインドする 今後、実際に使っていく中で欲しい機能が増えたら加えていきたいと思います。

開発で利用したツール・環境

クラウドGoogle Cloud Platform(GCP)を使っています。 - Cloud Functions サーバーレスの実行環境。今後、データ分析もしたいので言語はPythonを選択。 - Firestore NoSQLのデータベース - Cloud Scheduler バッチ処理 - Source Repositories ソースレポジトリ管理 + CI 最近は専らFirebaseで開発することが多いので、GCPを使いこなしたい欲が高く、データが溜まってきたら話題のAutoMLなど試してみたいこともあり、今後のことも考えてクラウドGCPを選択しました。

前提

  • LINE Developersアカウントを発行済み
  • GCP上でプロジェクトを作成済み

設計+実装

システム全体の構成をまとめたダイアグラム

システムダイアグラム

いざ実装

ソースコードはこちらに置きました → githubリポジトリ

main.py にCloud Functionsの関数を3つ作成しました。かっこの中が呼ばれる関数名です。 - ユーザーから投稿されたメモをDBに登録して、完了メッセージを送信(webhook) - 一日の終わりに、その日に投稿されたメモをリマインドするバッチ(remind) - LINEのアクセストークンを更新するバッチ(renew) それでは、それぞれの実装に関して具体的に解説していきます! ソースコードは、説明に必要な部分だけ掻い摘んで記載します。

ユーザーから投稿されたメモをDBに登録して、完了メッセージを送信

処理フロー 1. ユーザーからのメッセージ投稿をCloud Functions側にWebhookで通知 まず、Cloud FunctionsにHTTPトリガーの関数を作成し、発行されたURLをLINE Developersの基本チャネル設定→Webhook URLに登録します。2LINE側から接続確認して有効性を確認しておきます。

Cloud Functionsのコードはブラウザからも編集できますが、ローカルで編集してデプロイする場合は、以下のコマンドで!3

gcloud beta functions deploy webhook --entry-point webhook --trigger-http --runtime python37 --region asia-northeast1 --env-vars-file ../.env --max-instances=60

とりあえずデフォルトのコードをローカルにコピーして、デプロイできるか試してみるといいと思います。

あ、それと、シークレットキーなどは環境変数として .env ファイルにまとめて、コードと一緒にデプロイできるようにしました。publicリポジトリにpushしないように注意!

  1. LINEから送られてきたリクエストかどうか検証 ここからコードの中身に入っていきます。 まずはリクエストの中身を検証。あらかじめLINE側で設定された署名をヘッダから読み取って、ハンドラで検証してます。この辺りはLINEのリファレンスに書いてあるもののコピペ。
handler = WebhookHandler(os.environ['LINE_CHANNEL_SECRET'])

def webhook(request):
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        return "Invalid signature. Please check your channel access token/channel secret.", 400

    return 'OK'

気をつけなければいけないのは、このコードを加えると、1では通っていた接続確認が通らなくなることがあるそうです。4実際にぼくも、アプリからは正常に応答するにも関わらずエラーを吐くようになりました。接続確認では、Tokenの整合性までは確認していないために起こるようです。

  1. メモの内容をFirestoreに登録して、完了メッセージを送信 ユーザからメッセージが送信されたイベントを検知して発火します。 DB登録の流れはすごいシンプル。

rootのmemosコレクションにドキュメントを作成 ↓ 作成したmemoドキュメントのrefをusersに登録

データの不整合を防ぐためにTransaction処理で実装しています。

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message_sent_time = datetime.datetime.fromtimestamp(event.timestamp/1000)

    batch = db.batch()
    # create memo
    memo_ref = db.collection('memos').document()
    batch.set(memo_ref, {
        "userId": event.source.user_id,
        "text": event.message.text,
        "created_at": message_sent_time
    })
    # if exists user
    user_ref = db.collection('users').document(event.source.user_id)
    if not user_ref.get().exists:
        batch.set(user_ref, {
            "created_at": message_sent_time
        })
    # if exists today's memo
    user_memo_ref = user_ref.collection('memos').document(message_sent_time.strftime('%Y-%m-%d'))
    if not user_memo_ref.get().exists:
        batch.create(
            user_ref.collection('memos').document(message_sent_time.strftime("%Y-%m-%d")),
            { memo_ref.id: memo_ref }
        )
    else:
        batch.set(
            user_memo_ref,
            { memo_ref.id: memo_ref },
            merge=True
        )
    batch.commit()
    # completion message
    access_token = db.collection('config').document('access_token').get().to_dict()['access_token']
    line_bot_api = LineBotApi(access_token)
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text="登録しました。")
    )

一日の終わりに、その日に投稿されたメモをリマインドするバッチ

処理フロー

  1. GCP内からの適切なリクエストかどうか認証 こちらはバッチ実行ですが、念の為リクエストの検証をしています。

こちらは、Cloud Schedulerから送られるリクエストにService Account情報を紐づけることで実現可能です。紐づけを行うと、Service Account自体がリクエストを行ったかのように振舞います。

具体的には、HTTP リクエストの Authorization ヘッダに Service Account の情報を含んだ OpenID Connect の ID Token が渡ってくるので、その ID Token を検証します。^5

def _auth(bearer_token):
    token = bearer_token.split(' ')[1]
    # Verify and decode the JWT. `verify_oauth2_token` verifies
    # the JWT signature, the `aud` claim, and the `exp` claim.
    claim = id_token.verify_oauth2_token(token, transport.requests.Request())

    if claim['email'] != os.environ['GCP_APP_ENGINE_DEFAULT_SERVICE_ACCOUNT']:
        raise ValueError('Wrong service account.')

def remind(request):
    # Auth
    try:
        # Get the Cloud Scheduler-generated JWT in the "Authorization" header.
        bearer_token = request.headers.get('Authorization')
        _auth(bearer_token)
    except Exception as e:
        return 'Invalid token', 400
        
    # この後に処理が始まる、、、
  1. その日に投稿されたメモ一覧を整形して、ユーザーに送信 userドキュメントのmemosコレクションに、日付ごとのメモをまとめたDB設計にしているので、該当日のメモをgetしてよしなにテキスト整形しています。
def remind(request):
    # Auth
    try:
        # Get the Cloud Scheduler-generated JWT in the "Authorization" header.
        bearer_token = request.headers.get('Authorization')
        _auth(bearer_token)
    except Exception as e:
        return 'Invalid token', 400

    access_token = db.collection('config').document('access_token').get().to_dict()['access_token']
    line_bot_api = LineBotApi(access_token)

    today = datetime.datetime.now().strftime('%Y-%m-%d')
    for user_snap in db.collection('users').stream():
        today_memo_snap = db.collection('users').document(user_snap.id).collection('memos').document(today).get()
        if today_memo_snap.exists:
            remind_memo = ''
            today_memo_dict = today_memo_snap.to_dict()
            for memo_ref in today_memo_dict.values():
                remind_memo += memo_ref.get().to_dict()['text']
            # remind
            line_bot_api.push_message(
                user_snap.id,
                TextSendMessage(text=remind_memo)
            )

    return 'OK'
  1. Cloud Schedulerに、ジョブを登録 Authヘッダーに、OIDCトークンを追加するのを忘れずに。 このジョブは毎日23:59に起動させます。

LINEのアクセストークンを更新するバッチ

こちらはユーザーには見えない内部の処理ですが、上記の全ての機能で必要になるアクセストークンを管理するので重要です。

LINEのアクセストークンは、発行されてから30日間で失効してしまう5ので、DB上で動的に管理しています。30日以内に更新できればいいので、余裕を持って20日ごとにバッチ処理が走るようにスケジュールさせました。

処理フロー

  1. GCP内からの適切なリクエストかどうか認証 上記と同一処理なので割愛

  2. LINEのREST APIを使ってトークンを更新 POSTした結果を使ってDB更新しているだけです。

def renew(request):
    # Auth
    try:
        # Get the Cloud Scheduler-generated JWT in the "Authorization" header.
        bearer_token = request.headers.get('Authorization')
        _auth(bearer_token)
    except Exception as e:
        return 'Invalid token', 400

    payload = {
        'grant_type': 'client_credentials',
        'client_id': os.environ['LINE_CHANNEL_ID'],
        'client_secret': os.environ['LINE_CHANNEL_SECRET']
    }
    res = requests.post(
        '<https://api.line.me/v2/oauth/accessToken>',
        data=payload,
    )
    body = res.json()
    body['updated_at'] = datetime.datetime.now()
    db.collection('config').document('access_token').update(body)

    return 'OK'

運用する上で便利なTips

ソースコード管理にSource Repositoriesを使ってプチCI

ダイアグラム図の中にも書いたのですが、Source RepositoriesからCloud Functionsへの自動デプロイが可能です。 運用フローに組み込めば、コマンドによる手動デプロイの必要がなくなります!

LINE BOTGCP上で作ってみた感触

  • Cloud Functionsはまだベータ版(2019年8月現在)6と言うこともあり、Python実装のドキュメントがそこまで充実してない(Node.js実装の方が多少はましかも)
  • Dockerコンテナとかk8sとかがっつり使って開発したい(願望)、という気持ちになる
  • GCPのコンソール画面、わりと好き

今後の課題、妄想

  • 間違えて投稿したメモを取り消す機能を追加したい(LINEのMessaging API見てもメッセージの削除イベントってのが無いから、やり方は考えないといけないかも)
  • ゲーミフィケーション要素があると、より継続しそう(メモするたびに得点が加算!)
  • メモを記録、リマインドするだけではなくて、抽象化→転用の記録もうまくシステムに組み込みたい

参考

パーティー企画でブラウザで動くWebARアプリを作った話

先日、会社の組織の卒業パーティがあったので、IT企業ならではテック企画を!と思い作成したアプリについて、機能や要件決定で考慮したことを共有します。

まずは完成形

ARマーカーにかざすと画像とテキストが飛び出してきます。
加えて、スクリーンショットを撮って保存することもできるアプリとなっています。
(飛び出る写真はサンプル用に差し替えてます)

開発で利用したツール・環境

  • AR.js
  • A-Frame
  • Firebase(Hosting/Storage)

動作環境

今回のデモでは、下記の環境で動作することを確認しています。

古いバージョンのOSや、ここに挙げた以外の環境では動作しない可能性がありますので、あらかじめご了承ください。

今回の要件

参加者みんなに楽しんでもらえて、記憶に残る企画にしたいと思いで決めました。

  • ターゲットが、テンションの上がっているパーティー参加者
  • 短期間でAndroid/iOSに対応させるため、ブラウザで動くWebアプリ
  • 思い出に残るパーティーになる

ちなみに、Unityでの実装も考えましたが、ARCore/ARKit周りの習得までに時間がかかると判断して除外しました。

作成した機能

以上に挙げた要件を満たすために決めた機能と、考えたことを少し詳細に述べます。

  • マーカー型ARアプリ
    長くなってしまうので詳細は下に書きましたが、今回はARマーカーを使ったアプリにしました。

  • 思い出の写真がコンテンツとして飛び出る
    卒業していく参加者が、楽しかった過去の瞬間を思い返して欲しかったので、時間の経過+その瞬間に感じていた感情を体感できる情報媒体として写真を選択しました。

  • 写真が撮れる
    思い出に残るパーティーになる、という要件を満たすためにも大事にしている機能です。コンテンツが飛び出て、その場で「わーすごい!」と思ってもらえるだけでもちろん嬉しいのですが、その瞬間を持ち帰ってもらい、振り返ることができたら良いなと思い作りました。

マーカー型AR? or マーカーレス型AR?

作成した機能、の項目でも触れたのですが、今回ARマーカーを使った仕様になるまでの経緯をご説明します。
ARアプリには、大きく分けて以下の実現方法が存在します1

  • 位置情報AR: GPSなどを利用し、特定の位置に情報を表示
  • マーカー型AR: マーカーを利用し、マーカー上に情報を表示
  • マーカーレス型AR: 画像認識を利用し、特定の物体に情報を表示

今回は会場のみで使うことを想定しているので、マーカー型AR or マーカーレス型ARに候補が絞られます。
当初は、会場で参加者にロゼット(胸につける花の形をしたリボンの勲章)を配布していたので、それを認識させようと思いマーカーレス型で考えていたのですが、調べているとマーカーレスをWebで実現するためには、既存のSafariChromeとは別のビューアーをインストールしなければならないことがわかりました。具体的には以下が挙げられます。

それぞれのビューアーに対応するためには利用するライブラリが異なるし、なにより参加者に追加アプリをインストールさせる手間が増えてしまうのはよろしくない、ということでマーカーレス型も却下されます。
そして最後に残るマーカー型での実現方法を調べると、、実際に作成している方がいました!2しかもSafari/Chromeに対応している!ということでマーカー型ARで作成することにしました。

いざ実装

ソースコードはこちらに置きました → github

マーカーの作成

まずマーカーの作成ではAR.js Marker Trainingを利用しました。
マーカーを作るときは以下のような制約がある3ので、注意していただきたいと思います。

  • 正方形であること
  • 基本は黒枠、内部の白領域の割合が、1 : 2 : 1になります
  • 枠線は変更可能ですが、一般的に黒枠と白領域の割合は、3 : 14 : 3が最低ラインになります
  • 黒枠でマーカーの検出を行ない、白領域内のパターンに応じてマーカーを判別します
  • 白領域内のパターンは、点対称・線対称を避け、デザイン部分に細い線を用いないことが求められます

実際に作成したマーカーはこちらです。なるべくシンプルに白黒だけの文字にしました。 ARマーカーサンプル

マーカーの認識機能

重要な部分だけ抜粋して解説させていただきます。

<!-- ARコンテンツを定義する -->
<a-scene embedded arjs='sourceType: webcam; debugUIEnabled: false; detectionMode: mono_and_matrix; trackingMethod: best; matrixCodeType: 3x3;' vr-mode-ui="enabled: false">
  <!-- 日本語テキストを描画できる用にフォントを用意 -->
  <a-assets>
    <a-asset-item id="font" src="font/mp.json">
  </a-assets>

  <!-- マーカーを定義(画像+テキスト) -->
  <a-marker type="pattern" url="marker/young.patt">
    <a-image src="img/new-employee-orientation.png" position="0 0 0" rotation="90 0 180"></a-image>
    <a-entity text-geometry="value: 懐かしの入社式; font: #font; size: 0.1;" position="-1 1 1" rotation="-90 0 0" width="5" height="3" material="color: #3cb371;"></a-entity>
  </a-marker>

  <!-- カメラプレビューを用意する要素 -->
  <a-entity camera></a-entity>
</a-scene>

a-から始まる要素が、A-Frame/AR.jsにより提供される要素で、基本的にa-sceneの中でAR用のコンテンツを定義しています。

日本語対応するときには注意が必要

A-Frameでテキストを描画するときはa-textを使うことが多いのですが、デフォルトではマルチバイト文字に未対応なのでちょっと工夫が必要です。
私の場合はaframe-text-geometry-componentを利用した方法で対応しました。4

写真が撮れるカメラ機能

こちらは、Qiitaに有益情報5がありましたので、そのまま利用させていただきました。説明のためHTMLだけ載せておきます。

<div class="ui">
  <!-- 撮った画像をプレビューするための`img`要素 -->
  <img id="snap">
  <!-- 削除ボタン -->
  <a href="#" id="delete-photo" title="Delete Photo" class="disabled"><i class="material-icons">delete</i></a>
  <!-- 撮影ボタン -->
  <a href="" id="take-photo" title="Take Photo"><i class="material-icons">photo_camera</i></a>
  <!-- ダウンロードボタン -->
  <a href="#" id="download-photo" download="selfie.png" title="Save Photo" class="disabled" target="_blank"><i class="material-icons">file_download</i></a>
</div>

スクショするために大事な要素は、コードの中にある4つです。ユーザーの操作フローは以下のような流れ。

  1. 撮影ボタンでスクショを撮る
  2. プレビューされるので、ダウンロード/削除を選ぶ
  3. 削除の場合は、プレビューが消えるだけ。ダウンロードの場合は、別タブで画像が表示されるのでブラウザの機能で画像保存

いざデプロイ

今回はバックエンドの処理は皆無なのでホスティングするだけですが、最近流行り?のFirebaseを選択しました。公開までの方法は詳細に説明してくださっている方6がいるので割愛します。

今後の課題

ブラウザ対応の確認、くらいしかテストというテストをしなかったので、当日利用してもらったユーザーからいくつかフィードバックをもらいました。

  • ARコンテンツが出てきているんだけどチカチカする
    フレームレートなどを考慮するべきだったのかもしれないけれど、この辺りはまだ現時点で理解できてない部分

  • テキストが画面から切れてしまっている
    端末の画面サイズに合わせて、描画位置を決めるようにするべきだった

おわりに

この記事では、卒業パーティーの企画で作成したARアプリを紹介しました。アプリをインストールすることなしにその場で手軽にARを楽しめるという利点がうまく働いたのか、当日は多くの参加者に使っていただき、反響も大きい企画となりました←
歓送迎会の多いシーズンなので、面白いテック企画を立ち上げたいという方の参考になれば幸いです!

参考