20191230のReactに関する記事は5件です。

年末の断捨離アプリをFirebaseでhostingしてみた話(webアプリが少しずつ良くなっていく過程編)

Firebaseのhostingを使ってReactのSPAアプリをデプロイした話

です。
結論だけ話すとそんな感じです。(domainをとっていないことをお許しください)
https://dan-syari.web.app/#/

年末の短期休暇でアプリを1つ作ろうと考えていて、今回作りました。(開発期間は2日間)

今回作ったアプリについて

  • タイトル: 「Dan Syari」
  • 内容: 来年したい目標を3つ立て、それについて簡単な質問に答えるとその目標の中で重要なことがわかる
  • 技術:
    • React
    • Typescript ☆
    • React Hooks ☆
    • Reduxを使わない状態管理 (Context) ☆
    • Firebase (Hosting) ☆
    • Firestore ☆
    • Material UI ☆
    • SPA ☆

☆がついているものは著者は初めて使ったものです

洗い出して列挙してみると、結構挑戦してたことがわかります

本記事の対象者

  • 意気揚々と壮大なアプリを作ろうとして途中で断念してしまう人への解決方法
  • 最初から設計ばかり気にして、手が動かない人
  • 年末で暇すぎて、ゲームしたり漫画読んでたり、とにかく暇人なあなた!!

本記事で伝えたいこと

  • ちっぽけなところから初めて、少しずつ大きくしていくほうが楽しい
  • どんなアプリもデプロイしないと始まらない
  • 作っていくうちに設計のミスに気付いて後悔するけど悪いことばかりではない
    • 個人の失敗は、個人の開発や仕事での開発に活かせばいい!!

本記事の流れ

  • 開発の初期段階のラフ画から、デプロイして少しずつ現在(2019/12/30)の状態までを画像を比較することで追っていきます
  • 以下のような順番でいこうと思います
    • コーディング前のラフ画
    • 最初のデプロイ
    • 1日目の途中経過
    • 1日目の終わり
    • 2日目の途中経過
    • 2日目の終わり
    • 3日目のオプション
  • 年末なのに疲れたくないと思うので、軽くスワイプしてざっーと見るぐらいでいいので読んでみてください
    • 中堅の方達には初期の開発の時の気持ちを思い出していただけると思います
    • 開発初心者に近い人たちにとってはこうやって進めていけば続くのか!という1つの指標みたいな感じにしてもいいかもしれないです

コーディング前のラフ画

  • いきなりコーディングするのは避け、イメージだけでもいいので紙に書き起こしましょう
  • 仕事で開発する場合は、 FigmaSketch などのデザインツールを使うのもいいでしょう
    • 個人的にはFigmaを使っていますが、個人開発のハッカソンレベルのものであれば紙で十分なので、今回は紙です

トップページ
トップページ

質問ページ
質問ページ

結果画面
結果画面

最初のデプロイ

  • デプロイはFirebaseのHostingサービスを使いました
  • domainを取得しなければ基本的に無料で、コマンド一発でデプロイでき、FirestoreというDBも使うことができる最強のサービスです
    • ぜひ使いましょう(僕が学生の時に知っていたら本当によかった)
  • 最初のデプロイはトップページだけでした
    • 全く使い物にならないけど、デプロイって楽しいから何度でもしたくなります
    • そのうち、デプロイしたくて開発している自分に気付きます(☆ここ重要!!)

top

1日目の途中経過

  • 画像を見ればわかりますがほとんど進んでいません
  • Typescriptとか、React-routerとかほとんど経験がなかったので悪戦苦闘しました
  • ここら辺で、自分が作っているものは意味があるのかという自問自答タイムがありました
    • これを打破できたのは細かいcommitと、とりあえずデプロイするみたいなことをやっていたら手が動き始めたからです

top

  • 無限に質問が作れてしまうところからのスタートでした

question

1日目の終わり

  • 結果画面以外の簡単な実装が終了しました
  • ここからは修正があった部分だけ画像をつけます(枚数が増えてしまうので)

  • トップページのフォームの「断捨離る」ボタンを全てのフォームに記入していないと押せないようにしました(validation)

top form

  • 3つの目標に対してそれぞれ質問ページを作りました
    • ラフ画の時点では「はい・いいえ」でしたが、最終的に「0 ~ 10」で選択できるようにしました
      • グラフ作りやすいし、高い低いを Yes or No の2値で選ぶのなんか違うなと思ったからです

question

  • 悲しいことに1日目の終わりの時点では結果画面まで行かなかったです
  • この時点で開発開始から12時間経ち、ベッドへダイブしました
  • これぐらいは1時間で終わらせられるだろ!!というお気持ちの人もいると思いますが、今回の技術はほとんど初挑戦だったのでお許しください
    • 初めての技術はほとんど全部つまづくので温かい目でみていただけるといいですね
    • どんなエンジニアだって最初はこんなもんです。これから開発したい!と思っている初学者のかたも諦めずに頑張ってください!

result

2日目の途中経過

  • 各種質問に対する答えとして「0 ~ 10」をhtml標準の <input type="range"> を使って実装しました
  • 結果画面にグラフで3つの目標の結果画面を表示できるようにしました

question

  • 結果画面にrechartsを使って、
    • 2日目途中でこれだけ?と思われるかもしれませんが、Typescriptを初挑戦していて、型付けの部分でとんでもなく苦労しました
    • rechartsというグラフ描画用のpackageを、別のプロジェクトにjsで作成し、そこなら簡単に呼び出せるのに、なぜかTypescript(ts)で呼び出そうとすると映らず、一生描画できないのではないかと絶望しました
      • 結果的にTypescriptでは型付けのマッピングみたいなことをしたファイルがないと、型がわからず呼び出すことができていなかったです(@types/~ のpackageを追加しました)
    • 得意じゃない、英語の記事を漁ってでも意地でも答えを見つけに向かいました(公式ドキュメントとかは最初に読みましょう)

result

2日目の終わり

  • 2日目の途中の段階でやりたいことはほぼ終わったので、あとは微調整をしていくことができる状態になったので進捗が少し捗った
  • トップのフォーム部分の文字の色が気に食わなかったので修正した
    • 現状が良いのかという議論はあるけど、最初がダサすぎた

top form

  • Material UI を導入し、 Input とか Button あたりの既存のものを置き換えた
    • これだけで雰囲気出てくる
    • CSSフレームワーク大事(というかデザイナーさんは神)

question

  • 結果画面にグラフだけじゃなくて、それぞれの項目の可視化もしてみた
  • 星の大きさは「Dan Syari」的にいえば、あなたにとって大切なことを大きさで表している
    • 大きい星ほど、あなたにとって大切なことである
    • グラフを象限としてみたときに、第一、第二、第四、第三の順に大切なものになる
    • 重要なことがあなたにとって大切なことですよ、ということを伝えたいのだ

result 1

  • はっきり言って、こんなに文字があるとうざいなと、スクショしてみて強烈に感じた
    • 次の開発には生かしたい

result 2

象限

まとめ

  • さぁ、いかがだったでしょうか
  • 少しずつアプリが良くなっていくのが見えたのではないでしょうか
  • ぶっちゃけ、現時点でめっちゃおしゃれで、実用性があって、いろんな人に響くかときかれれば耳が痛くなるでしょう
    • でも、声を大にして言いたい、「最初はこんなもんだよ!」
    • 家族や、友達、知り合いに使ってもらってそれだけで作ってよかったって思えるのも本当に大事だと思います
    • エンジニアとして同じ技術を使い慣れて実装力を上げていくことも大事です
    • が、コンフォートゾーンから抜け出すこともときには大切です
    • 小さくてもダサくても良いから1年のうちに新しい技術に手を出してみること大事
  • だということが「Dan Syari」を使っていただけるとわかります
    • ぜひ試してみてください!!

https://dan-syari.web.app/#/

3日目のオプション

  • OGP設定
    • TwitterとかFacebookとかに貼ったときにでるサマリみたいなあれです
    • ぜひTwitterやFacebookに貼って、友達や知り合いに送ってみてください
  • ちょっとしたデザイン微修正(margin/paddingとか入力欄のwidthとか)
  • Firestoreの導入

top form

slack

  • めちゃくちゃダサいw
    • でも個人的には結構好き

使った技術の解説などは別の記事として作ろうと思います
少しでも面白いなと思った人や、年末の暇つぶしができて嬉しいなという人や、解説記事も読んでみたいなという人は「いいね!」してくださると嬉しいです(この記事にリンク追加したときに通知が飛ぶようになるので是非、「いいね!」しておくといいかもしれません)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのBDD勉強としてサービス作ってみた

TL;DR

  • フロントエンドBDDの勉強用に、簡単なサービス作った。
  • テスト書くの良い。
  • サービス公開とか色々本筋でないとこに結構引っかかった。
  • Javascript難しい。
  • フロントエンド難しい。

What's this?

最近業務でswiftを触っていたのもあり、勉強の方向性がフロントエンドに偏りつつある。
Reactは以前から学習に取り組み、簡易な実装経験があったが、
そもそも機能を実現することも重要だが、システムとして長期運用していく上で重要になるテストコードについても勉強したいと思ったので、じゃあReactでテスト書きつつシステム一個作って見よう!という試みで開始した。

作ったもの

image.png

サービス:https://timer-d73c3.web.app
GitHub:https://github.com/theMistletoe/RecoSta

起動中の経過時間を計測してるサービス。勉強時間・作業時間を記録することが目的。
エンジニアたるもの20h/weekは勉強するのは最低ラインとのことで、
私はどのくらいできているかな〜というのが気になった、ところから作ってみました。

syougakusei.png

使用したテストライブラリ

https://github.com/testing-library/react-testing-library

react-testing-libraryというライブラリで、Reactに置ける振る舞い駆動テストを実現するためのライブラリを使った。
swiftでもBDDをしていたこともあり、今後の潮流的にもBDD来てそうだなと思ったので使ってみた。
参考で詳しく書いてくださってる記事を載せています。

実際に書いてたテストコードが下記

describe("Main Page",  () => {

        it("get and display studytimes", async () => {
            const spy = jest.spyOn(axios, 'get').mockImplementation(() => {
                return {
                    data: [{date: '20191121', studytime: '2315'}, {date: '20320408', studytime: '444'}]
                }
            });

            const { getByText, getAllByTestId, getByPlaceholderText } = await render(<Main />);

            await waitForElement(() => getAllByTestId("studytime-list"));

            expect(firebase.auth().currentUser.getIdToken).toHaveBeenCalled();
            expect(spy).toHaveBeenCalledWith(`${process.env.REACT_APP_BACKEND_ENDPOINT}/api/v1/studytime`, 
            {headers: { authorization: `Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` }});

            expect(getByText("Total: You've studieds 2759 seconds!")).toBeInTheDocument();
            expect(getByPlaceholderText("Input Your Email Address")).toBeInTheDocument();

            expect(getByText("Now, You've studied")).toBeInTheDocument();
            expect(getByText("Date")).toBeInTheDocument();
            expect(getByText("Studied Times(s)")).toBeInTheDocument();
            expect(getByText("20191121")).toBeInTheDocument();
            expect(getByText("2315")).toBeInTheDocument();
            expect(getByText("20320408")).toBeInTheDocument();
            expect(getByText("444")).toBeInTheDocument();
        });
}

BDDっぽいところでいうと、
expect(getByText("20191121")).toBeInTheDocument();
みたいにHTMLの画面上に想定通りの文字や入力欄が存在するかどうかをテストコードで表現できる。
こうすることで、ユーザーからみた挙動に近い形で、テストコードをSpecとして表現できる。

システム構成

構成自体はJAMStack?のはず?です。

システム構成図 (1).png

Firebaseを中心に活用しつつ、バックエンドだけHeroku使ってる感じです。
Firebase HostingにReactのフロントエンドをデプロイしています。
バックエンドもNode.js(express)でさっくり書いています。

データストアはFireStoreというFirebaseのNoSQLを使用しています。エンティティ設計がちとめんどくさいですが、簡単なサービスを作るときには簡単にできて良い感じですね。

認証にもFirebase Authenticationを使用していて、メールアドレスによる認証を行なっています。
この辺の認証周りとかすごいめんどくさかった、、自分が理解できていないこともあるが、、、

そもそもFirebaseのmBaasの考え方的にバックエンドを挟む構成がいまいち向いていない感じがありますね。初めはバックエンド書かずにやろうとしたんですが、axiosのHTTPリクエストのテストがしたかったのと、よくわからなかったので逃げてしまった、、、

まとめ

テスト・BDDの勉強のためにサービス作りながら実際に適用できそうか勉強しながらやってみて、
BDD・TDDを実践しながら作ると、個人開発であっても実装に迷うことが少なくなり、方向性を見失うことはなかったように思う。
が、ここで早くなったと書かなかったのは、テストの書き方とか、Jestわからんとか、Mock効かねぇとか、テスト周りで実装に詰まることが格段に増え、すっごい時間がかかってしまった。

そもそもJavascriptへの理解が全然追いついていないとひしひしと感じた。バックエンドで使ってるような言語のスタンスで取り組むと、全然期待する挙動にならずオワタ...になる。

一旦ベースができたので、 実はテスト書けてないところとか、設計オワコンなところとか、 改善点を直しながら理解を深める活動ができればいいな、と思っている。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby/Rails でサーバ転職後数ヶ月で、TypeScript/React/Redux なチームで書けるようになるまでに参考にしたこと

夏に転職して、それまではrubyしか書いてこなかったのですが、
転職後はそれまで全く触ってこなかった TypeScript/React/Redux/Firebase なチームに入って開発できるようになるまでに参考になったものです。
もちろん、実際にはもっと他にも色んなものを参考にしています。
また、ここに書いたものも隅々まで読んだりしたわけではないのですが、振り返ってみて役に立ったなって思い出せるのを書いてみました。

公式サイト

その他のサイト

  • JSer.Info
    • slack にフィード流して見てる
    • twitter で見てる
  • JavaScript Primer
    • Promise/async/await のところは何度も見てる
  • TypeScript Deep Dive 日本語版
    • 本家の英語版も合わせてみる。
  • JavaScript Weekly
    • slack にフィード流して見てる
  • npm trends
    • npm の比較をするときに便利。
    • ある package をいれると、関連する package が出てくるので知らない package を知るのにも便利。

勉強会

  • JSConf
  • js community(xxx.js系)
  • GCP/Firebase系

slack

twitter

  • 各種公式アカウント系
  • 勉強会で発表してた人や情報を発信してくれてる人

まとめ

  • 公式ドキュメントは最高
    • API document はもちろんだが、guide や tutorial など、知りたいことのほとんどは公式に書いてある
  • それの理解の手助けのために、他の本やサイトを活用するのが良い
    • 手っ取り早く全体像や背景などを理解するには便利。
  • 最新の情報は、twitter で色んな人をフォローしていろんな記事流れてくるようにしてる
    • 定期的に勉強会にも参加する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FlaskとReact使ったTwitter認証をWebSocket使ってめっちゃ強引に行う

概要

reactにおける認証は下記のページなどで行っている人が複数人いる。
React Authentication with Twitter, Google, Facebook and Github
ReactでSPAを作り、Twitter認証(OAuth)でログインする。バックエンドはRails
ただ、自分は馬鹿なのでいまいちよくわからなかった(後、上記ページはサーバーサイドがexpressとかRailsでflaskは見つからなかった)。
そのため、小手先でめっちゃ強引な認証を書いた。

WebSocket使うのもReactのuseEffect使うのも、Flask使うのも初めてなので、参考程度にみてもらえると良い(後々、ちゃんとしたコードで書き直したい)

流れ

今回は「連携アプリ認証」のボタンを押すと連携が始まるようになっているので、そこの部分は適当に変えて欲しい。

  1. 認証を行うボタンをおす(js内handleClick関数)
  2. サーバーに"Twitter"の文字列が飛ぶ(js内handleClick関数)
  3. URLをサーバーが返す(Twitter認証用)(python内pipe関数)
  4. jsでURLで移動(js内ws.onmessage = function(e)内)
  5. Twitter認証画面
  6. 認証する
  7. クライアント側のページにredirect
  8. サーバーにauth_tokenとauth_verifierの情報がいく(useEffect内ws.onopenより)
  9. サーバーはtokenとverifierを使ってaccess_token_secretを作成する(python内user_timeline)
  10. Twitterから認証ユーザの最新のタイムライン一件を持ってくる(python内user_timeline)

認証などにはtweepyを用いている。

実際のコード

index.py
import os
import json
import sys
import tweepy
from flask import Flask, session, redirect, render_template, request, jsonify, abort, make_response
from flask_cors import CORS, cross_origin
from os.path import join, dirname
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler

app = Flask(__name__)
CORS(app)
#勝手に決める
app.secret_key = ""
#TwitterAppから持ってくる
CONSUMER_KEY = ""
CONSUMER_SECRET = ""

@app.route('/pipe')
def pipe():
   if request.environ.get('wsgi.websocket'):
       ws = request.environ['wsgi.websocket']
       while True:
            message = ws.receive()
            # print(message)
            #ボタンを押すとTwitterがwebsocketで送られるので送られたら発火
            if message == "Twitter":
                auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
                try:
                    # 連携アプリ認証用の URL を取得
                    redirect_url = auth.get_authorization_url()
                    session['request_token'] = auth.request_token
                except Exception as ee:
                    # except tweepy.TweepError:
                    sys.stderr.write("*** error *** twitter_auth ***\n")
                    sys.stderr.write(str(ee) + "\n")
                #websocketでurlを送り返す
                ws.send(redirect_url)
                ws.close()
                #return無いとエラーが出るため
                return redirect_url
            elif message != None:
                messages = json.loads(message)
                # print(messages)
                user_timeline(messages, ws)
def user_timeline(auths, ws):
    # tweepy でアプリのOAuth認証を行う
    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    verifier = str(auths["oauth_verifier"])
    # Access token, Access token secret を取得.
    auth.request_token['oauth_token'] = str(auths["oauth_token"])
    auth.request_token['oauth_token_secret'] = verifier
    try:
        access_token_secret = auth.get_access_token(verifier)
    except Exception as ee:
        print(ee)
        return ""

    print(access_token_secret)
    # tweepy で Twitter API にアクセス
    api = tweepy.API(auth)

    # user の timeline 内のツイートのリストを1件取得して返す
    for status in api.user_timeline(count=1):
        text = status.text
    # user の timeline 内のツイートのリストを1件取得して返す
    ws.send(text)
    ws.close()

def main():
    app.debug = True
    server = pywsgi.WSGIServer(("", 5000), app, handler_class=WebSocketHandler)
    server.serve_forever()

if __name__ == "__main__":
    main()
app.js
import React from 'react';
import './App.css';
import { useState, useEffect } from 'react';

function App() {
  const [flag, setFlag] = useState(false);
  const [userData, setUserData] = useState('');
  const [data, setData] = useState('');
  //webSocketとの通信
  const ws = new WebSocket('ws://localhost:5000/pipe');
  // レンダー前にwsがopenした後にurl内のverifierを返す
  useEffect(() => {
    ws.onopen = event => {
      if (userData == false && window.location.search.includes('verifier')) {
        setFlag(true);
        ws.send(getParam('oauth_verifier'));
      }
    };
    setUserData('true');
  });

  //url内の特定の要素を持ってくるためのコード
  function getParam(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, '\\$&');
    var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
      results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/\+/g, ' '));
  }

  // サーバー側からメッセージが送られてきた際に受け取り、関数を発動する(Twitterの認証用URLに飛ぶため)
  ws.onmessage = e => {
    console.log(e);
    if (e.data.includes('oauth_token')) {
      window.location.href = e.data;
    } else {
      console.log(e);
      setData(e.data);
    }
  };
  //クリックした時(今回は文字をbuttonをクリックしたらサーバーにTwitterのもじが送られる)
  function handleClick() {
    console.log('rest');
    ws.send('Twitter');
  }
  console.log(flag);

  //レンダー要素の切替
  let render = flag ? (
    <div>{data}</div>
  ) : (
    <button onClick={handleClick}>連携アプリ認証</button>
  );
  return <>{render}</>;
}

export default App;

正直、めっちゃ適当なコードなので参考にできればする程度がちょうどいいと思う(後、websocketのエラーが出る。closedの状態で通信を行ってしまっているためだと思うので修正できたら編集します)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TensorFlow.jsで手書き数字認識アプリを作ってみた

はじめに

サークルの講習会で、kivyとTensorFlow?を使って手書きの数字を認識するアプリを作っている先輩がいたので、これをReactでやってみたいなと思ったので、今回やってみました。このアプリを作る中で、こちらのサイトを非常に参考にさせていただきました。また、僕は機械学習についてあまり知識がないので、今回はpythonを全く書きません。機会があれば、モデルの作成からやってみたいと思います。

完成物

tegaki2.gif

インストールしたパッケージ

TensorFlow.js: https://www.tensorflow.org/js/
react-signature-canvas: https://www.npmjs.com/package/react-signature-canvas
material-ui: https://material-ui.com/

"@material-ui/core": "^4.8.1",
"@tensorflow/tfjs": "^0.12.6",
"react-signature-canvas": "^1.0.3",

※ tensorflow/tfjsは上記のバージョンに合わせてください。

ファイル構造

create-react-appコマンドで作成したプロジェクトをベースに話を進めます。以下のファイル構造は、編集または新規作成したファイルのみ書いています。

src-
   |-components
   |      |-Accuracy.js
   |      |-AccuracyTable.js
   |
   |-App.js
   |-App.css
components/Accuracy.js
import React from "react";

const Accuracy = props => {
  const { no, content } = props;

  return (
    <tr>
      <th>{no}</th>
      <td className="accuracy" data-row-index={`${no}`}>
        {content}
      </td>
    </tr>
  );
};

export default Accuracy;
components/AccuracyTable.js
import React from "react";
import Accuracy from "./Accuracy";

const AccuracyTable = () => (
  <table className="table">
    <thead>
      <tr>
        <th>数字</th>
        <th>精度</th>
      </tr>
    </thead>
    <tbody>
      <Accuracy no={0} content="-" />
      <Accuracy no={1} content="-" />
      <Accuracy no={2} content="-" />
      <Accuracy no={3} content="-" />
      <Accuracy no={4} content="-" />
      <Accuracy no={5} content="-" />
      <Accuracy no={6} content="-" />
      <Accuracy no={7} content="-" />
      <Accuracy no={8} content="-" />
      <Accuracy no={9} content="-" />
    </tbody>
  </table>
);

export default AccuracyTable;
App.js
import React from "react";
import "./App.css";
import * as tf from "@tensorflow/tfjs";
import SignatureCanvas from "react-signature-canvas";
import { Button } from "@material-ui/core";
import AccuracyTable from "./components/AccuracyTable";

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      is_loading: "is-loading",
      model: null,
      maxNumber: null,
      maxScore: null
    };
    this.onRef = this.onRef.bind(this);
    this.getImageData = this.getImageData.bind(this);
    this.getAccuracyScores = this.getAccuracyScores.bind(this);
    this.predict = this.predict.bind(this);
    this.reset = this.reset.bind(this);
  }

  componentDidMount() {
    tf.loadModel(
      "https://raw.githubusercontent.com/tsu-nera/tfjs-mnist-study/master/model/model.json"
    ).then(model => {
      this.setState({
        is_loading: "",
        model
      });
    });
  }

  onRef(ref) {
    this.signaturePad = ref;
  }

  getAccuracyScores(imageData) {
    const scores = tf.tidy(() => {
      const channels = 1;
      let input = tf.fromPixels(imageData, channels);
      input = tf.cast(input, "float32").div(tf.scalar(255));
      input = input.expandDims();
      return this.state.model.predict(input).dataSync();
    });
    return scores;
  }

  getImageData() {
    return new Promise(resolve => {
      const context = document.createElement("canvas").getContext("2d");
      const image = new Image();
      const width = 28;
      const height = 28;

      image.onload = () => {
        context.drawImage(image, 0, 0, width, height);
        const imageData = context.getImageData(0, 0, width, height);

        for (let i = 0; i < imageData.data.length; i += 4) {
          const avg =
            (imageData.data[i] +
              imageData.data[i + 1] +
              imageData.data[i + 2]) /
            3;
          imageData.data[i] = avg;
          imageData.data[i + 1] = avg;
          imageData.data[i + 2] = avg;
        }
        resolve(imageData);
      };

      image.src = this.signaturePad.toDataURL();
    });
  }

  predict() {
    this.getImageData()
      .then(imageData => this.getAccuracyScores(imageData))
      .then(accuracyScores => {
        const maxAccuracy = accuracyScores.indexOf(
          Math.max.apply(null, accuracyScores)
        );
        const elements = document.querySelectorAll(".accuracy");
        elements.forEach(el => {
          el.parentNode.classList.remove("is-selected");
          const rowIndex = Number(el.dataset.rowIndex);
          if (maxAccuracy === rowIndex) {
            el.parentNode.classList.add("is-selected");
          }
          el.innerText = Math.round(accuracyScores[rowIndex] * 1000) / 1000;
        });
        this.setState({
          maxNumber: maxAccuracy,
          maxScore: accuracyScores[maxAccuracy]
        });
        console.log(accuracyScores);
      });
  }

  reset() {
    this.setState({
      maxNumber: null
    });
    this.signaturePad.clear();
    const elements = document.querySelectorAll(".accuracy");
    elements.forEach(el => {
      el.parentNode.classList.remove("is-selected");
      el.innerText = "-";
    });
  }

  render() {
    let text = "数字を入力してください";
    if (this.state.maxNumber !== null) {
      if (this.state.maxScore > 0.999) {
        text = `この数字は確実に${this.state.maxNumber}です。`;
      } else if (this.state.maxScore > 0.9) {
        text = `この数字はほぼ間違いなく${this.state.maxNumber}です。`;
      } else if (this.state.maxScore > 0.5) {
        text = `この数字は多分${this.state.maxNumber}です。`;
      } else {
        text = `この数字は${this.state.maxNumber}かもしれないです。`;
      }
    }
    return (
      <div className="container">
        <h2>{text}</h2>
        <div className="canbas">
          <SignatureCanvas
            ref={this.onRef}
            minWidth={15}
            maxWidth={15}
            penColor="white"
            backgroundColor="black"
            canvasProps={{
              width: 420,
              height: 420,
              className: "sigCanvas"
            }}
            onEnd={this.predict}
          />
        </div>
        <div className="button">
          <Button variant="contained" onClick={this.reset}>
            reset
          </Button>
        </div>
        <AccuracyTable />
      </div>
    );
  }
}

export default App;
App.css
.container {
  margin-bottom: 120px;
  text-align: center;
}

.canbas {
  display: inline;
}

.button {
  display: block;
  margin-top: 20px;
  margin-bottom: 60px;
}

.table {
  display: inline;
  border-collapse: collapse;
  border-spacing: 0;
}

.table th, .table td {
  padding: 10px 0;
  width: 200px;
  text-align: center;
}

.table tr:nth-child(odd) {
  background-color: #eee;
}

.is-selected {
  color: red;
}

終わりに

PCでは、うまくいったのですが、スマホからアクセスするとうまくいきませんでした。今後の課題としては、スマホアプリ(Expo+ReactNative)にしてみたいなと思っています。あとは、機械学習を勉強してモデルの作成もいちからやってみたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む