20210606のJavaScriptに関する記事は28件です。

JavaScriptで範囲指定乱数生成

はじめに JavaScript(TypeScript)で乱数を作る時、いつも迷うのでここにメモしておく やりたいこと 最小値(min)以上、最大値(max)以下の乱数を取得する。 random.js const rand = (min, max) => { return (Math.floor(Math.random() * (max - min + 1)) + min); }; random.ts const rand = (min: number, max: number): number => { return (Math.floor(Math.random() * (max - min + 1)) + min); };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで体温計の数字を画像認識してGoogleスプレッドシートに書き込ませたかった。

作りたいもの 体温計の液晶に表示された体温を、PCのWEBカメラで読み取って、Googleスプレッドシートに書き込みます。 WEBカメラが撮影するのは「画面に体温計が映ったら」をトリガーにして自動化します。 なぜ作ろうと思ったのか 最近は会社指示等で毎朝検温される方も多いかと思いますが、私は以前から起き抜けの体温を測ってその日の体調とともにGoogleスプレッドシートに記入し続けていました。この手動による記入をなくせないかなぁ、というのが今回の課題です。 ちなみに普通に「Bluetooth対応の体温計」なんて当たり前にあるだろうと思っていたのですが、価格.comで調べても「BLE(Bluetooth Low Energy) 内蔵体温計 UT-201BLE」という1機種しか出てこず、エー・アンド・デイという私の知らない会社だったのと、接続が不安定という口コミがあったので購入は見合わせました。 パルスオキシメーターですらBluetooth接続製品がわりとあるのにもっと身近な体温計にないのは不思議です。電池持ちの問題? 医療機器で参入障壁が高くて黙ってて売れてるから世間の需要をキャッチしてバージョンアップする気がない?(ぉぃ さて、通信方式はBluetoothでなくてもいいから、信頼できるメーカーから出てないのかな、と調べていくとオムロンの「音波通信体温計 MC-6800B けんおんくん」というものがありました。が、専用アプリを立ち上げるという手間が増えている気がします。 そもそも現状はGoogleスプレッドシートに体温の他に寝起きの体調や、気温も記入しているので専用アプリに入力するというのはデータの使い道が狭まります。専用アプリからcsv書き出しできれば良さそうですが……。 あとここまで気にされる方は少数派であろうという自覚はあるのですが、今まで小数点以下2位で取得してきたデータが3年分あるのに、これからは小数点以下1位になるのはなんかいやです。(理系っぽいことをまったく理系っぽくない理由で言う理系) ところで「けんおんくん」というネーミングは一昔前のシステムっぽくて良いので勝手に拝借して今回のシステムは「けんおんくんGS」としましょう。 処理の概要 以下のような処理でいきます。 1.Teachable Machineで体温計の数値を読み取れる写真を100枚以上撮影し、学習データを作成する。(写真データAとする) 2.WEBページを表示して、動画撮影を開始する。 3.ml5.jsを使って、写真データAと合致したときに画像をcanvasに取得する。(画像Bとする) 4.画像BをTesseract.jsに投入して、数字を読み取る。(数字Cとする) 5.数字CをGoogleスプレッドシートに書き込む。 今回、「【勝手企画】Twilioオンラインコンテストに応募しちゃおうハッカソン」に参加するためにTwilioの必要バージョンであるnode.jsのバージョンを切り替えできるように変更したら開発環境が壊れました( ノД`)シクシク… なので今回できているのは3.までです。。。 追記)環境直りました(/・ω・)/ つくりかた Teachable Machineに「体温計の液晶面を見せている画像」と「体温計がない背景」の2パターンで学習させます。一致する場合の返り値は「Thermometer」とし、それ以外は「None」とします。 ここでトラブル。「体温計」として認識されるのは以下の赤で囲んだ画像でした。アイ・アム・体温計マン。 それどころか電灯をつけるだけで体温計と認識されてしまいます。 対比先の画像「None」をまったく同じ画像にしすぎました。 なので「None」の画像には人間が映ったり、電灯をつけたり消したりしてデータを追加しました。 これで以下のように割と正確に認識するようになりました。以下はConsoleに画像認識結果を吐いてるところです。 課題 「処理の概要」の4.と5.がまだです。 また、以下のように体温計をかざしたあとに移動しようとした自分が静止画として写っています。ある程度認識するとはいえ、体温計だけを認識するのは厳しそう。となると、「最初に体温計を検知してから3秒後に1回だけ静止画を撮影する」(3秒は人間が体温計をかざしてカウントする運用)などが良さそうでしょうか。 あと、体温計の数字が反転して映っているので、読み取れるように再反転させる必要もありますね。。。あれ、結構大変そうだな。。。 開発中に書いた記事 環境 ml5.js Teachable Machine Tesseract.js コード thermometer.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta content="width=device-width, initial-scale=1.0" name="viewport"> <title>Thermometer</title> </head> <body> <h1>Temperature to GoogleSpreadSheet</h1> <div id="app"> <p>1.体温計を検知したら静止画として撮影します。</p> <p>2.撮影した画像から体温を読み取ります。</p> <p>3.読み取った体温をGoogleスプレッドシートに入力します。</p><video autoplay="" height="240" id="myvideo" style="border: 3px solid deepskyblue;" width="320"></video> <canvas height="240" id="mycanvas" style="border: 3px solid pink;" width="320"></canvas><img height="240" id="myimg" src="" style="border: 3px solid green;" width="320"><br> <p>4.体温計の自動判別がうまく動かないときは手動で撮影もできます。</p><button onclick="video2canvas2img()">撮影</button> </div> <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"> </script> <script> // 作成したモデルのURL const imageModelURL = 'https://teachablemachine.withgoogle.com/models/KMTRCuFmI/'; let classifier; // 撮影ボタンクリックでvideoタグ→canvasタグ→imgタグへとその瞬間のフレーム(静止画)データを移してゆく function video2canvas2img() { // videoタグ、canvasタグを取得 const video = document.getElementById('myvideo'); const canvas = document.getElementById('mycanvas'); // canvasへ描画するための「コンテキスト」を取得 const context = canvas.getContext('2d'); // コンテキストに対してvideoのデータを書き込むことで、canvasへ反映 context.drawImage(video, 0, 0); // imgタグを取得 const img = document.getElementById('myimg'); // imgのsrc属性に、canvasの中身から変換したデータURL(URLっぽいが実体は生データ)をセット img.src = canvas.toDataURL(); } async function main() { // カメラからの映像取得 // 映像や音声が使えるデバイスが確定するまで時間がかかるためawaitを使う const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { width: 320, height: 240 }, }); // IDが"myvideo"であるDOMを取得 const video = document.getElementById('myvideo'); // videoにカメラ映像をセット // myvideoなvideo要素のsrcObject(映像オブジェクトを入れるところ)にデータ(メディアストリーム)をセットする video.srcObject = stream; // 自作モデルのロード classifier = ml5.imageClassifier(imageModelURL + 'model.json', video, () => { // ロード完了 console.log('Model Loaded!'); }); // 分類処理を連続的に行います(YOLOと同じ)。 function onDetect(err, results) { if (results[0].label==='thermometer'){ console.log('体温計を検知しました!'); video2canvas2img(); }else{ console.log('serching...'); } classifier.classify(onDetect); } classifier.classify(onDetect); } // カメラ映像をビデオタグに表示する main(); </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amplify ConsoleでNext.jsアプリをホスティング

先日、Amplify ConsoleでNext.jsのSSRアプリがホスティングできる機能が発表された。 公式ブログ Host a Next.js SSR app with real-time data on AWS Amplify 実際はSSR, SSGのどちらかをpackage.jsonから判断するらしい。 Deploy and host server-side rendered apps with Amplify When you deploy a Next.js app, Amplify inspects the app's build script in the package.json file to detect whether the app is SSR or SSG. Next.jsを触ったことがなかったので、サンプルアプリをAmplify Consoleで動かしてみた。基本的に上記のブログに沿ってやってみて、時折上記の公式ドキュメントを参照する。自分が再度試したくなった時にコピペでできるのが理想。 注意点 いずれも上記のドキュメントに書いてある。 - SSRはマニュアルデプロイには対応していないのでレポジトリ連携が必要 - Next.js10には対応していないので、version9に落とした方がいい。10もできはするらしいがフルサポートではないとのこと。 ローカルでサンプルアプリを動かす〜CodeCommitへのpushまで Amazon Linux 2のEC2インスタンスを起動。接続はVSCodeのRemote Developmentで。便利。 CodeCommitのロール作成とかの準備 Nodeを入れる ここから公式ブログの内容に入っていく。npx create-next-appをするとサンプルアプリが出来上がる。 ただし、Next.jsのバージョンが10なので9に落とさないといけない。公式ではないがこちらの記事も参考になり、バージョンを真似させてもらった。サンプルアプリの package.jsonを一部以下のように書き換える。 "dependencies": { "next": "9.5.5", "react": "16.12.0", "react-dom": "16.12.0" } この状態でnpm installしてダウングレード -> npm start。 pages/index.jsのImageという機能がバージョン10からの機能らしくエラーを吐いたので、関連する行を安直に消して再度npm startしたら動いた。 5. push git init git add . git commit -m "comment" git push --set-upstream レポジトリのURL master ホスティング 上記のブランチをAmplify Consoleに接続する。コンソールから新しいアプリを作って上記のブランチを選択するとビルドの設定などが自動生成される。 デプロイに使用するロールを選択する必要があり、今まで使っていた既存のロールを選択したところAccess Deniedのエラーが出てビルドに失敗した。 2021-06-05T04:43:22.686Z [INFO]: Starting SSR Build... 2021-06-05T04:44:29.259Z [INFO]: AccessDenied AdministratorAccessの権限でロールを新規作成してそっちを使ったら成功した。 できたものを確認。 大きな特徴は以下。 CloudFrontが自分のアカウントにできる Lambda@Edgeがサーバーの役割を果たして処理を行う。ドキュメントにも記載あり。そのため、console.log()はLambda@Edgeのログとして出力される (これはLambda@Edgeの特徴) Lambda@Edgeはus-east-1にできるが、CloudWatchログはアクセスされたエッジロケーションがあるリージョンにできる。日本からの場合には大体ap-northeast-1をみとけば良さそう。公式ではないがこの記事に救われた。全く知らなかった。 ちょっと処理足した 処理をどこに書けばいいかわからないので、とりあえずテンプレート化したい。先程の外部記事も参考にして、npx create-next-appでできたコードを以下のようにちょこっと編集すると好きな処理をかけるようになる。 index.js import Head from 'next/head' import styles from '../styles/Home.module.css' import Link from "next/link"; export default function Home() { return ( <div className={styles.container}> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <h1 className={styles.title}> Welcome to <a href="https://nextjs.org">Next.js!</a> </h1> <p> <Link href="/handler"> <a>処理するボタン</a> </Link>{" "} </p> </main> </div> ) } 以下を新規作成。getServerSideProps()は文字列を返す決まりなのか?とりあえず試してうまくいったのは以下だけど、JSONをそのまま返した場合にList側でどういういじり方ができるのかはわからない。 handler.js // import AWS from "aws-sdk"; const List = ({data}) => { return( <> list: {data} </> ) } export const getServerSideProps = async () => { const res = // 何かしらJSONが返ってくる処理 const data = JSON.stringify(res); return {props:{data}}; } export default List; まとめ Lambda@Edgeで処理していた Next.jsのサンプルアプリをホスティングして、適当な処理がかけるボタンを置きました。 おまけ: 認証に関して 僕は今回試していないが、withSSRContext()を使えばCognitoの認証情報をサーバー側で使える。これはAmplifyのドキュメントに書いてあり、普通のReactアプリをホスティングするときとほぼ同様の操作で認証情報を使えそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

scrollMagic スクロールでふわっと要素が出るやつ

ScrollMagicを使ってふわっと要素を出す方法 スクロールと連動させて何かをしたいならScrollMagic一択かなと自分は思っています。 自分が忘れないための備忘録 scrollMagicの公式のデモサイトはこちら https://scrollmagic.io/ サクッとデモだけ見たい人 サクッと実装だけみたい人は下記より見てください CodePenのリンク https://codepen.io/xhisashix/pen/YzZLJqY ディレクトリの構成 . ├── css │ └── style.css ├── index.html └── js └── scrollMagic.js CDNの読み込み bodyの閉じたタグの上に記載 CDNのリンクに付いては上記のScrollMagicのデモサイトのページにも記載があります。 index.html <script src="//cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.5/ScrollMagic.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.5/plugins/debug.addIndicators.min.js"></script> サンプルHTML htmlはこれだけ index.html <div class="section"> <div class="box"></div> <div class="box"></div> <div class="box"></div> </div> CSS style.css .section { display: flex; flex-wrap: wrap; justify-content: flex-start; margin: 0 auto; max-width: 980px; width: 100%; } .section .box { margin-top: 20px; margin-left: 20px; width: 200px; height: 200px; background: #eee; opacity: 0; transition: 0.5s; transform: translateY(15%) rotate(-45deg); } .section .box:nth-child(odd) { transform: translateY(-15%) rotate(45deg); } .section .box.active { opacity: 1; transform: translateY(0); } js 特定のクラスがついてるものずべてにアニメーションを適応點せたかったので 今回は下記のような、実装を行いました。 scrollMagic.js class ScrollFadeIn { constructor() { let box = document.querySelectorAll('.box:not(.active)'); console.log(box); if (box.length === null) { return; } let controller = new ScrollMagic.Controller(); for (let i = 0; i < box.length; i++) { let scene = new ScrollMagic.Scene({ triggerElement: box[i], triggerHook: 'onEnter', reverse: false, offset: 150, }) .addIndicators() .addTo(controller); scene.on('enter', () => { box[i].classList.add('active'); }); } } } new ScrollFadeIn(); ちょっと説明 querySelectorAll 条件に該当する要素をすべて取得する let box document.querySelectorAll('.box:not(.active)'); console.log(box); // ログの結果 NodeList(3) [div.box, div.box, div.box] 上記を使ってあとはループを回すだけ for (let i = 0; i < box.length; i++) { let scene = new ScrollMagic.Scene({ triggerElement: box[i], triggerHook: 'onEnter', reverse: false, offset: 150, }) .addIndicators() .addTo(controller); scene.on('enter', () => { box[i].classList.add('active'); }); } イベント発火のタイミング triggerHook: 'onEnter', onEnter onCenter onLeave クラスの追加 triggerElementが特定の位置に来たときクラスを付与 このクラスをつけアニメーションはCSSで調整を行っている scene.on('enter', () => { box[i].classList.add('active'); }); 最後に vue-scrollmagicは使ったことがあり、なんとなく理解していたが、 scrollMagicを使うのが初めてだったため、忘れないための備忘録として書かせてもらいました。 誰かの参考になれば幸いです 最後まで読んでいただきありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript's newest one is ES6

ES6 is newest version of JavaScript.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでSPAを実現するためのVue Router

まず、SPAとは シングルページアプリケーションの略 必要な部分のみ置き換えて画面遷移させるアプリケーション設計 画面遷移の際にページ全体ではなく、必要な部分だけを遷移させるところがポイント 通常のページ遷移 SPAのページ遷移 (Vuexで作った素材 示している意味は違うけど、なんか通じそうだったから使ってみた) Vue Routerのインストール 既存プロジェクト配下に移動した上で下記を実行 vue add router ページ遷移時URLの部分にハッシュを使用しないヒストリーモードを使うかどうかの選択をする (ファイルに少し変更を加えるため、ファイル編集後にGitコミットしていない場合、インストールしても良いか警告が表示される) Use history mode for router? (Requires proper server setup for index fallback in production) こちらを yesか Noを選択後、インストールされる インストール完了後、Vue Routerを使用することができる 参考文献 基礎から学ぶVuejs
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mockを使ったNode.js(Express)のAPIテスト

はじめに Jest・SupertestでAPIテストを作成した際に躓いたので、その実装の備忘録を残しておく。 この記事では時に、より実践的なテストにするためにはmockを用いるべきであるがどのようにmock化してWeb APIがcallされないようにするか?という部分について書き残す。 ※mockを用いず、ただ単にnode.js(Express)のAPIテスト(End Pointテスト)を実行できるようするための方法は、Jest・Supertestを使用したnode.js(Express)のAPIテスト app.listen()はエラーになるを参照。 以下で実装したテストの実行結果(GitHub Actionsの結果)は以下。 https://github.com/yuta-katayama-23/travel-app/runs/2757301572 なぜmock化する必要があるか? Node.js(Express)でServer Sideを構築する場合、外部のWeb APIを実行しデータを取得することも多くある。その際に、UnitTestやAPIテストで実際に通信を行ってしまうと、テストとして動作が不安定になるなどの問題が発生してしまうため。 詳細はこちらを参照。 外部のWeb APIの実行をmock化してAPIテストする ここで言うAPIテストはEnd Pointテストで、実際に期待されるEnd Pointがありそこから結果が返ってくるか?をテストする。 (実際には以下で取り上げるテスト内容もテストしているとも言えるが・・・。) この場合、Node.jsのmoduleをmock化し外部のWeb APIを実行させないようにする+supertestを用いてテストを実装できる。 テスト対象のコードは以下。 app.js const express = require('express'); const app = express(); /* Middleware */ app.use(express.urlencoded({ extended: false })); app.use(express.json()); // Cors for cross origin allowance const cors = require('cors'); app.use(cors()); // Initialize the main project folder app.use(express.static('dist')) // dotenv const dotenv = require('dotenv') dotenv.config(); const axios = require('axios').default; // config for get countries and cities const axiosConfig = { baseURL: 'https://api.countrystatecity.in/v1/', timeout: 2500, headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` } } app.get('/allCountries', async (req, res) => { try { const countries = await axios.get('countries', axiosConfig) res.status(200).send({ countries: countries.data }) } catch (error) { errorHandler(res, error) } }) const errorHandler = (res, error) => { if (error.response) { res.status(500).send(error.response.data) } else { res.status(500).send({ error: error.message }) } } module.exports = app; テストコードは以下。 test.js const request = require('supertest') const app = require('../../../src/server/app') const axios = require('axios') jest.mock('axios') // ここでNode.jsのmodule(今回はaxios)をmock化している describe('axiosをmock化&supertestでrequestを飛ばしてテスト', () => { it('/allCountries', async () => { const resp = { data: [{ name: 'test' }] }; axios.get.mockResolvedValue(resp); // mock化したmoduleの実行結果を定義している https://jestjs.io/docs/mock-function-api#mockfnmockresolvedvaluevalue const res = await request(app).get('/allCountries') expect(res.status).toEqual(200) expect(res.body.countries[0].name).toEqual('test') }) }) ※テストコードの解説・注意事項  ・jest.mock(module)はrequire/importのスコープに記載する   jest公式にも以下のように書いてある通り、jest.mock(module)はrequire/importのスコープに記載しないとダメ。 Note: In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement. 注: 適切にモックするために、Jest は jest.mock('moduleName') が require/import ステートメントと同じスコープにある必要があります。 route-mock.test.js // 省略 const axios = require('axios') describe('axiosをmock化&supertestでrequestを飛ばしてテスト Get Endpoints (mocking)', () => { it('/allCountries', async () => { jest.mock('axios') // ←このような実装はエラーになる // 省略 }) }) APIのエンドポイントがcallされた時に実行される処理をテスト ここで言うAPIテストはAPIのcall時に実行される処理のテストの事。 End Pintが存在するか?よりはそのEnd Pointをcallした時に実行される処理が期待される挙動であるか?を検証する。 この場合、単純にエイリアス(関数)を呼び出してテストしてあげればよいが、そのテストを実行できるようにNode.js(Express)のソースコードの構成もテストできる形にする必要がある。 プロジェクトの構成のリファクタリング まず、Node.js(Express)の構成だがMVCモデルに則り以下のようにする。 src ・・・ └── server ├── app.js ├── controllers │ └── controller.js ├── routes │ └── router.js └── server └── server.js すると、単純にエイリアス(関数)であるコードを書く事ができるので、テストを実行する際にはこの関数を呼び出すだけでいい。 controller.js const axios = require('axios').default; // dotenv const dotenv = require('dotenv') dotenv.config(); // config for get countries and cities const axiosConfig = { baseURL: 'https://api.countrystatecity.in/v1/', timeout: 2500, headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` } } const allCountries = async (req, res) => { try { const countries = await axios.get('countries', axiosConfig) res.status(200).send({ countries: countries.data }) } catch (error) { errorHandler(res, error) } } const errorHandler = (res, error) => { if (error.response) { res.status(error.response.status).send({ error: error.response.data, errorMsg: error.message }) } else { res.status(500).send({ errorMsg: error.message }) } } module.exports = { allCountries, }; 実際のテスト対象のソースコード全体は以下を参照。 テストコードの実装 上記で関数化できたのでテストコードは以下のようになる。 (テスト対象のコードは上記のallCountries関数。) test.js const { allCountries } = require('../../../src/server/controllers/controller') const axios = require('axios') jest.mock('axios') describe('axiosはmock化&関数のテストとしてテスト', () => { it('/allCountries', async () => { const resp = { data: [{ name: 'not use superttest' }] }; axios.get.mockResolvedValue(resp); const req = {} const res = { status: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis() } await allCountries(req, res) expect(res.status.mock.calls[0][0]).toBe(200) expect(res.send.mock.calls[0][0].countries[0].name).toEqual('not use superttest') }) }) ※テストコードの解説 jest.fn().mockReturnThis()axiosのresponseがres.status().send()というようなメソッドチェーンで使われるのでそれ自身(this)を返してあげる必要があるので.mockReturnThis()でthisを返すようにしている1 mock.calls[0][0]呼び出されたモックに対してmock.calls[n][m]で、n番目の引数に対してm回目の呼び出しで指定された引数を取り出すという意味で、今回は引数1つ&呼び出した回数も1回なので、mock.calls[0][0]に結果が可能されている上記のコードで言えば、res.status(200).send({ countries: countries.data })というテスト対象のコードが、mock化したそれぞれの関数(status: jest.fn()…とsend: jest.fn()…)に対し、・.status(200)で1回呼び出されその時の引数は200なのでres.status.mock.calls[0][0]で200が取り出せる・.send({ countries: countries.data })で1回呼び出されその時の引数は{ countries: countries.data }なのでres.send.mock.calls[0][0]で{ countries: [{ name: 'not use superttest' }] }が取り出せるという事。2 参考文献 https://jestjs.io/docs/manual-mocks#mocking-node-modules https://jestjs.io/ja/docs/mock-functions#%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%A2%E3%83%83%E3%82%AF https://www.agent-grow.com/self20percent/2019/03/25/only-express-and-jest-testing/ https://tech.bitbank.cc/lets-test-by-jest/ https://jestjs.io/ja/docs/mock-functions#%E3%83%A2%E3%83%83%E3%82%AF%E3%81%AE%E5%AE%9F%E8%A3%85 ↩ https://jestjs.io/ja/docs/mock-functions#mock-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MediaPipeで魔方陣を描く!?

ちまたで、MediaPipeが流行っているようで、私も触ってみました。 PCに接続したWebCamの映像をMediaPipeに通すことで、人のポーズが検出できます。 そこで、マウスが押されてから離されるまで、人の腕から指の部分をトレースして、軌跡を映像に重ね合わせてみました。 さあ、手にマウスをもって魔方陣を軌跡で描いてみましょう。 ちなみに、マウスはM5StickCを使っています。バッテリとBLEが付いているので、手にもって軌跡を描きながら、マウスクリックができるので、(多少は)軌跡を描きやすくなります。 ソースコードもろもろを以下に上げておきました。 poruruba/MagicPipe_Tracing  https://github.com/poruruba/MagicPipe_Tracing Webページの作成 MediaPipeにいくつかあるSolutionのうち、Poseを使いました。 以下のページに書いてあるサンプルソースを改造します。  https://google.github.io/mediapipe/solutions/pose#javascript-solution-api 結果がこちら。 html/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js" crossorigin="anonymous"></script> </head> <body> <div class="container" style="position: relative;"> <video class="input_video" style="position: absolute; visibility: hidden"></video> <canvas class="video_canvas" width="1280px" height="720px" style="position: absolute;"></canvas> <canvas class="output_canvas" width="1280px" height="720px" style="position: absolute;"></canvas> </div> </body> <script type="module"> const videoElement = document.getElementsByClassName('input_video')[0]; const canvasElement = document.getElementsByClassName('output_canvas')[0]; const videoCanvasElement = document.getElementsByClassName('video_canvas')[0]; const canvasCtx = canvasElement.getContext('2d'); const videoCtx = videoCanvasElement.getContext('2d'); let mouse_pressed = false; let prev_xy = { x : -1, y : -1 }; let draw_cleared = true; const NUM_OF_DATA = 3; window.onmousedown = (event) =>{ mouse_pressed = true; }; window.onmouseup = (event) => { mouse_pressed = false; }; window.onmouseout = (event) =>{ mouse_pressed = false; } let x_ary = []; let y_ary = []; function reset_position(){ for( var i = 0 ; i < NUM_OF_DATA ; i++ ){ x_ary[i] = -1; y_ary[i] = -1; } return { x: -1, y: -1 }; } function push_positioin(x, y){ for( let i = 0 ; i < NUM_OF_DATA - 1 ; i++ ){ x_ary[NUM_OF_DATA - 1 - i] = x_ary[NUM_OF_DATA - 1 - i - 1]; y_ary[NUM_OF_DATA - 1 - i] = y_ary[NUM_OF_DATA - 1 - i - 1]; } x_ary[0] = x; y_ary[0] = y; let sum_x = 0, sum_y = 0; let i = 0; for( i = 0 ; i < NUM_OF_DATA ; i++ ){ if (x_ary[i] < 0 || y_ary[i] < 0) break; sum_x += x_ary[i]; sum_y += y_ary[i]; } if( i == 0 ) return { x: -1, y: -1 }; else return { x: sum_x / i, y: sum_y / i }; } function onResults(results) { canvasCtx.save(); videoCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height); canvasCtx.strokeStyle = '#800'; canvasCtx.fillStyle = '#800'; canvasCtx.lineWidth = 10; if( results.poseLandmarks[13].visibility >= 0.7 && results.poseLandmarks[11].visibility >= 0.7 && prev_xy.x < 0 && prev_xy.y < 0 && !draw_cleared){ var w = results.poseLandmarks[13].x - results.poseLandmarks[11].x; var h = results.poseLandmarks[13].y - results.poseLandmarks[11].y; var fire = (Math.abs(w / h) < 0.5) && (h < 0.0) && mouse_pressed; console.log(fire); if( fire ){ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); draw_cleared = true; mouse_pressed = false; } } if (results.poseLandmarks[19].visibility >= 0.7 && results.poseLandmarks[15].visibility >= 0.7 & mouse_pressed) { var x1 = results.poseLandmarks[19].x; var y1 = results.poseLandmarks[19].y; var x2 = results.poseLandmarks[15].x; var y2 = results.poseLandmarks[15].y; var x = x1 + (x1 - x2); var y = y1 + (y1 - y2); var pos = push_positioin(x, y); if( prev_xy.x >= 0 && prev_xy.y >= 0 ){ canvasCtx.beginPath(); canvasCtx.moveTo(prev_xy.x * 1280, prev_xy.y * 720); canvasCtx.lineTo(pos.x * 1280, pos.y * 720); canvasCtx.stroke(); draw_cleared = false; } prev_xy = pos; }else{ prev_xy = reset_position(); } canvasCtx.restore(); } const pose = new Pose({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`; }}); pose.setOptions({ modelComplexity: 1, smoothLandmarks: true, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, selfieMode: true, }); pose.onResults(onResults); const camera = new Camera(videoElement, { onFrame: async () => { await pose.send({image: videoElement}); }, width: 1280, height: 720 }); reset_position(); camera.start(); </script> </html> ポイントだけ解説します。 〇映像をセルフィー視点に変更 映像をそのまま表示すると、自分が描いた軌跡が左右反転してしまいます。 そこで、セルフィーのように、映像を左右反転させます。 html/index.html <video class="input_video" style="position: absolute; visibility: hidden"></video> ・・・ selfieMode: true, として、左右反転させた映像とし、オリジナルの映像は非表示にしています。 〇指の位置を検出 ポーズの体の部品の位置は以下のインデックスのポイントで分かります。 https://google.github.io/mediapipe/images/mobile/pose_tracking_full_body_landmarks.png 以下の部分です。 ポイント15から19へのベクトルの2倍の長さの先に、指先がある想定としました。 if (results.poseLandmarks[19].visibility >= 0.7 && results.poseLandmarks[15].visibility >= 0.7 & mouse_pressed) { var x1 = results.poseLandmarks[19].x; var y1 = results.poseLandmarks[19].y; var x2 = results.poseLandmarks[15].x; var y2 = results.poseLandmarks[15].y; var x = x1 + (x1 - x2); var y = y1 + (y1 - y2); var pos = push_positioin(x, y); if( prev_xy.x >= 0 && prev_xy.y >= 0 ){ canvasCtx.beginPath(); canvasCtx.moveTo(prev_xy.x * 1280, prev_xy.y * 720); canvasCtx.lineTo(pos.x * 1280, pos.y * 720); canvasCtx.stroke(); draw_cleared = false; } prev_xy = pos; }else{ prev_xy = reset_position(); } 〇軌跡を移動平均化する そのまま軌跡にすると、ノイズが大きくギザギザするため、3点の移動平均にしました。 function reset_position(){ function push_positioin(x, y){ 〇腕を上げてマウスクリックで軌跡クリア そのまま軌跡を描き続けていると、軌跡で埋まってしまうので、軌跡をクリアするポーズを決めました。 ポイント11から13へのベクトルが上を向いた状態でマウスクリックした場合にクリアするようにしました。 if( results.poseLandmarks[13].visibility >= 0.7 && results.poseLandmarks[11].visibility >= 0.7 && prev_xy.x < 0 && prev_xy.y < 0 && !draw_cleared){ var w = results.poseLandmarks[13].x - results.poseLandmarks[11].x; var h = results.poseLandmarks[13].y - results.poseLandmarks[11].y; var fire = (Math.abs(w / h) < 0.5) && (h < 0.0) && mouse_pressed; console.log(fire); if( fire ){ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); draw_cleared = true; mouse_pressed = false; } } M5StickCをBLEマウス化 PCにつないでいるマウスを使ってもいいのですが、ポーズの手が検出しにくくなるため、手に持ちやすいM5StickCをBLEマウス化しました。 以下を使わせていただきました。 以下、M5StickC側のソースコードです。 Esp32_Button/src/main.cpp #include <M5Stickc.h> #include <BleMouse.h> BleMouse bleMouse; #define BTN_WAIT 100 bool btnA_pressed = false; bool btnB_pressed = false; bool ble_connected = false; void setup() { // put your setup code here, to run once: M5.begin(); Serial.begin(115200); Serial.println("Starting BLE work!"); pinMode(M5_LED, OUTPUT); digitalWrite(M5_LED, HIGH); bleMouse.begin(); } void loop() { M5.update(); // put your main code here, to run repeatedly: if (bleMouse.isConnected()) { if (!ble_connected) ble_connected = true; if (M5.BtnA.isPressed() && !btnA_pressed) { btnA_pressed = true; digitalWrite(M5_LED, LOW); bleMouse.press(MOUSE_LEFT); Serial.println("Pressed LEFT"); delay(BTN_WAIT); } else if (M5.BtnA.isReleased() && btnA_pressed) { btnA_pressed = false; digitalWrite(M5_LED, HIGH); bleMouse.release(MOUSE_LEFT); Serial.println("Released LEFT"); delay(BTN_WAIT); } if (M5.BtnB.isPressed() && !btnB_pressed) { btnB_pressed = true; bleMouse.press(MOUSE_RIGHT); Serial.println("Pressed RIGHT"); delay(BTN_WAIT); } else if (M5.BtnB.isReleased() && btnB_pressed) { btnB_pressed = false; bleMouse.release(MOUSE_RIGHT); Serial.println("Released RIGHT"); delay(BTN_WAIT); } }else{ if (ble_connected){ ble_connected = false; digitalWrite(M5_LED, HIGH); } } } platformio.iniは以下の通りです。 ESP32_Button/platformio.ini [env:m5stick-c] platform = espressif32 board = m5stick-c framework = arduino monitor_speed = 115200 upload_port = COM5 monitor_port = COM5 lib_deps = t-vk/ESP32 BLE Mouse@^0.3.1 m5stack/M5StickC@^0.2.0 終わりに 実際に軌跡を描いているところを録画してアップしてもよかったのですが、ちょっと恥ずかしかったので、遠慮しました。。。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

あなたはどのアイドルグループの顔に近いの?AIであなたの顔分析~アイドル組み分け帽子~

この記事を読んでいるということは当然あなたはアイドルが大好きですね! アイドルの写真を見せられた時にこの子はあのグループだね!なんて見分けるのは簡単すぎる!!・・・たぶんw なので、自分の顔はあのグループ顔!と判断してくれるAIを作りました! さすがに自分顔がどのグループに近いかなんて考えたことなかったので面白いし、推しているグループならさらに親近感がわくのではないかと思い作成にいたりました。 アイドル組み分け帽子(ハリ〇タ??) 今回はカメラを起動し映像をAIにかけて判断してくれるWebサイトを作成しました! こちらで公開しているのでぜひ使ってみてください! 使い方は簡単! 自分の顔を映してロード完了まで待つだけ! あとは判定ボタンを押すと結果が表示されます! ちなみに自分顔は乃木坂でしたw 怒涛の画像集めと幸せな画像選定 まずAIに学習させるために各アイドルグループの画像を集めなくてはなりませんでした。 そこで今回活用したのがGoogleCromeの拡張機能のImage Downloader! とにかく多くのアイドルグループと厳密な選定(欲望)をしたかったので一番手っ取り早いこちらにしました。 使い方に関しては簡単ですが記事にしたのでこちらを参照ください。 そして至福の画像選定ですが以下の条件で選定しました。 条件 理由 単体で映っているもの 複数人いると判断できないと思ったため なるべく顔がアップのもの 画像の顔で判断してもらうため 宣材写真を必ず入れる  アイドル本人がよく使う画像のため 上記の選定をもとに1グループあたり120枚の画像を取得しました。 Teachable Machineにアイドルのお勉強 次に集めた画像で学習データの作成です。 今回、Teachable Machineを使用して学習データを作りました。 画像を各グループごとにアップロードし「モデルをトレーニングする」ボタンを押下するだけで簡単に学習データを作成することができました! お勉強させたアイドルグループの一覧とメンバー選定はこちら! 見た目がよさげで簡単にWebアプリの作成 まずはメインとなるJSです。 const imageModelURL = 'モデルのURL'; let classifier; const app = new Vue({ el: '#app', data: { modelState: 'モデルロード中...', result: '---------', }, async mounted() { // カメラからの映像取得 const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true, }); // IDが"myvideo"であるDOMを取得 const video = document.getElementById('myvideo'); // videoにカメラ映像をセット video.srcObject = stream; // 自作モデルのロード classifier = ml5.imageClassifier(imageModelURL + 'model.json', video, () => { // ロード完了 app.modelState = 'ロード完了!'; console.log('Model Loaded!'); }); }, methods: {  //ボタンクリックされたら判定開始 onButtonClicked: async function () { const yourFaceResults = await classifier.classify(); app.result = yourFaceResults[0].label; console.log('あなたの顔は' + app.result); }, }, }); HTMLとCSSですが1から作るととても大変だと思いHTMLのテンプレートを使用しました。 サンプルにはなかったTwitterのボタン部分だけ載せておきます。 <a id="twitter_button" class="twitter-share-button" data-text="アイドル顔分け帽子の結果!!あなた顔は・・・" data-show-count="false"></a> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> もっとこうしたかった!! 今回できなかったところを書き留めておきます。 ・画像・映像の顔だけで判断したかった。 ・Twitterボタンの機能に結果を自動ではめ込みたかった。 映像の顔切り出しはでBodyPix試そうとしたもののうまくいかず、時間をかけすぎてFace-Apiは試せずと今回のしくみになりました。 Twitterボタンのボタンはjsでメソッドを組んだのですがうまく作動せずサイトのタブ名がツイートに組み込まれるというところで断念。 終わりに 機械学習は自分には手の届かないものと考えていましたがすごく簡単にモデルを作成できました。 また今回Twitterにて判断してほしいグループを募集しました。 ご返答いただいた方本当にありがとうございました。 もし、このグループも入れてほしいというのがございましたらコメントで教えてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

InDesign スクリプト テキストフレームを配置(構造の選択された属性の値のページに要素の内容の)

構造の選択された属性の値のページに要素の内容のテキストフレームを配置するスクリプトは、これで良いのかな・・・? /* 構造の選択された属性の値のページに要素の内容のテキストフレームを配置 更新 2021/06/06 */ // アプリ指定 #target "indesign"; // スクリプト名 var scriptName = "テキストフレームを配置(構造の選択された属性の値のページに要素の内容の)"; //スクリプト動作指定(一つのアンドゥ履歴にする及びアンドゥ名の指定) app.doScript(function () { // ダイアログ var dialogueFlg = confirm("構造の選択された属性の値のページに要素の内容のテキストフレームを配置します。" + "\r" + "正数の値の入った属性を選択して実行してください。", "", scriptName); // Noの場合 if (dialogueFlg == false) { // スクリプトを終了 exit(); } // 選択されているオブジェクト selectObjects = app.activeDocument.selection; // 変更された要素名数 var changeElementNameNumber = 0; // 配置ページ番号 var placePageNumber; // 配置数 var placeNumber = 0; // 選択の数だけ繰り返す for(var i = 0; i < selectObjects.length; i++){ // 選択が属性の場合 if (selectObjects[i].constructor.name == "XMLAttribute"){ // ストーリーのコンストラクタ名を取得する関数を使い結果がXmlストーリーの場合 if(getStoryConstructorName(selectObjects[i]) == "XmlStory"){ // 属性の値が正数の場合 if(selectObjects[i].value.match(/^[1-9][0-9]*$/g) != null){ // 配置ページ番号に入れる placePageNumber = selectObjects[i].value; // 配置ページ番号のページが存在する場合 if(app.activeDocument.pages.item(placePageNumber - 1).isValid == true){ // Xml要素のテキストフレームを 配置ページ番号のページにページのサイズで配置 selectObjects[i].parent.placeIntoFrame(app.activeDocument.pages.item(placePageNumber - 1),app.activeDocument.pages.item(placePageNumber - 1).bounds); // 配置数を追加 placeNumber++; } } } } } // 結果表示 alert("選択数 " + selectObjects.length + "\r" + "配置数 " + placeNumber, scriptName); //スクリプト動作指定(一つのアンドゥ履歴にする及びアンドゥ名の指定)の続き }, ScriptLanguage.JAVASCRIPT, [scriptName], UndoModes.ENTIRE_SCRIPT, scriptName); /* ストーリーのコンストラクタ名を取得する関数、引数(オブジェクト)の宣言 */ function getStoryConstructorName(anyObject) { // parentStoryプロパティが存在する場合 if (anyObject.hasOwnProperty("parentStory") == true) { // parentStoryがある場合 if (anyObject.parentStory) { // ストーリーのコンストラクタ名を戻す return anyObject.parentStory.constructor.name; } // スプレッドの場合 } else if (anyObject.constructor.name == "Spread") { // 抜ける return; } // オブジェクトの階層を一つ上げて再帰関数 return getStoryConstructorName(anyObject.parent); } /* ストーリーのコンストラクタ名を取得する関数の宣言終了 */ たとえば 以下の様なXml(文字コードはUTF-8)をInDesignの構造に読み込み 属性を選択して(属性の選択を補助するスクリプトあり)使用します。 1つの要素を複数配置する事は出来ません。 複数配置したい場合は、リンク等で配置するのが良いのかな・・・? オブジェクトスタイルパネルのパネルメニューのデフォルトテキストフレームスタイルを 設定しておくとそのスタイルでテキストフレームが作られます。 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Root> <猫の名前 placePage="1">こはく</猫の名前> <猫の名前 placePage="2">ぼんた</猫の名前> <猫の名前 placePage="3">くろい</猫の名前> </Root>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptを使った実用的な時差の計算

LineBotを使って仕事の課題解決を行えるアプリケーションを作っている過程で、LINEBotにはない機能をWebアプリケーションを使って補完することを考えました。 その機能は時差を計算する機能なのですが、これをJavaScriptを使って実現したいと思い、以前の記事で、時差計算について自分なりに考え、コードを記載してみたところ、ありがたいことに、もっとDRYなコードがあることを教えていただきました。 自分が作りたかった時差計算をしてくれるサイトについて、時差計算機能が完成したのですが、教えて頂いた内容を踏まえて、JavaScriptで改めて時差を計算する方法について記事にしてみました。 開発環境等 macOS BigSur ruby-2.6.5 Rails 6.0.3.6 Bootstrap 時差計算搭載サイト 実際に使用するとこんな感じです。↓ 時刻の入力はHTMLのdatatime-localを使用して、ユーザーに選択させようとしたのですが、PC版では、秒数までの入力があるのに対して、スマートフォン版では、分までしかなく、秒数まで入力させる時差計算がしたかったので、秒数を入力する欄を別に設けて、その値をDataオブジェクトに変えて計算させる仕様にしています。 計算元になる時刻表示は常に、bottomに固定されており、setIntervalでリアルタイム表示になっています。 実際のコード 見た目はBootStrapを使用して作成しました。(body部分だけ) <body> <div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto"> <h1 class="display-4 text-center">Time Deference</h1> <p class="lead text-center">このサイトでは、防犯カメラ等の時差を確認することができます。</p> </div> <div> <div class="card-header fixed-bottom text-center"> <h3 id="RealTimeArea1"class="card-title"></h3> </div> <div class="container" id="card-area-1"> <div class="card mb-3 shadow-sm text-center"> <div class="card-body"> <div class="container-fluid"> <div class="container-count text-right"> <h4 id = "innerCount-1" ></h4> </div> <h5><label for="InputDateTime">現時表示の時刻を選択</label></h5> <input type="datetime-local" id="InputDateTime-1" class="shadow-sm" style= "width: 200px;"> <input type="number" id="InputSecondTime-1" class="shadow-sm" placeholder="秒数を入力" style= "width: 120px;" min="0" max="59"><label>秒</label> <button type="button" class="btn-time-deference btn-primary btn-sm" id="check-current-time-1">確認</button> <h3 id="RealTimeArea1"class="card-title"></h3> <h2 id="TimeDeference-1"></h2> <h5><label for="InputDateTimePass-1">再生中の表示時刻を選択</label></h5> <input id="InputDateTimePass-1" type="datetime-local" class="shadow-sm" style= "width: 200px;"> <input id="InputSecondTimePass-1" type="number" class="shadow-sm" placeholder="秒数を入力" style= "width: 120px;" min="0" max="59"><label>秒</label> <button type="button" class="btn-correction-time btn-primary btn-sm" id="check-correction-time-1">確認</button> <h2 id ="CorrectionTime-1"></h2> <div class="text-center"> <button class="m-2 btn btn-outline-success add-button" style="width: 200px;" id="add-btn-1">追加</button> </div> </div> </div> </div> </div> </div> </div> </body> リアルタイム表示 window.addEventListener('load', () => { let set2digits = function digits (num) { let digit; if(num < 10) {digit = "0" + num;} else {digit = num;} return digit; } let timeNow = function realtime () { const nowTime = new Date(); const nowYear = nowTime.getFullYear(); const nowMonth = nowTime.getMonth() + 1; const nowDay = nowTime.getDate(); const nowHour = set2digits(nowTime.getHours()); const nowMin = set2digits(nowTime.getMinutes()); const nowSec = set2digits(nowTime.getSeconds()); const dayOfWeek = nowTime.getDay(); const dayOfWeekStr = [ "日", "月", "火", "水", "木", "金", "土" ][dayOfWeek] ; const realTime = "現在:" + nowYear + "年" + nowMonth + "月" + nowDay + "日"+ "(" + dayOfWeekStr + ")" + nowHour + ":" + nowMin + ":" + nowSec; document.getElementById("RealTimeArea1").innerHTML = realTime; }; setInterval(timeNow, 1000); }); これで、現在時刻をリアルタイムで表示することができます。 こちらのサイトを参考にさせていただきました。 表示させる形は、0000年00月00日(曜日)00:00:00といった形です。 set2digitsでは、表示させる時刻が1桁の場合は、0を結合させる処理をしています。 時差計算処理 window.addEventListener('load', () => { function timeInputElement(){ const timeAreaCount = document.querySelectorAll(".container").length; const timeInput = document.getElementById(`InputDateTime-${timeAreaCount}`); const timeInputSecond = document.getElementById(`InputSecondTime-${timeAreaCount}`); return{ timeAreaCount: timeAreaCount, timeInput: timeInput, timeInputSecond: timeInputSecond }; }; function getInputSecond (secondElement) {if (Number(secondElement.value) < 10){ return secondElement.value.padStart(2, 0); }else{ return secondElement.value }}; function timeDeference(timeDeferenceElement){ const nowDate = new Date(); const inputSecond = getInputSecond(timeDeferenceElement.timeInputSecond); const InputDate = new Date(timeDeferenceElement.timeInput.value + ":" + inputSecond); const sign = nowDate > InputDate ? 1 : 0; const diff = Math.abs(nowDate - InputDate); const times = new Date(diff).toUTCString().match(/(\d+):(\d+):(\d+)/); const days = Math.floor(diff / 86400000); return { timeDeference: `${String(days).padStart(2, '0')}日${times[1]}時間${times[2]}分${times[3]}秒${['早い', '遅れ'][sign]}`, setTime: diff }; }; let setTime; document.addEventListener('click', event =>{ let setCount = document.querySelectorAll(".container").length if (event.target.closest(`#check-current-time-${setCount}`)){ let timeDeferenceElement = timeInputElement(); let innerTime = timeDeference(timeDeferenceElement) setTime = innerTime; document.getElementById(`TimeDeference-${timeDeferenceElement.timeAreaCount}`).innerHTML = innerTime.timeDeference; } }); function timeInputPassElement(){ const timeAreaCount = document.querySelectorAll(".container").length; const correctionTimeInput = document.getElementById(`InputDateTimePass-${timeAreaCount}`); const correctionTimeInputSecond = document.getElementById(`InputSecondTimePass-${timeAreaCount}`); return{ timeAreaCount: timeAreaCount, correctionTimeInput: correctionTimeInput, correctionTimeInputSecond: correctionTimeInputSecond }; }; function correctionTime(setTime, correctionTimeElement){ const setDate = setTime.setTime const inputSecondCorrection = getInputSecond(correctionTimeElement.correctionTimeInputSecond); const InputCorrectionDate = new Date(correctionTimeElement.correctionTimeInput.value + ":" + inputSecondCorrection).getTime(); const checkStr = setTime.timeDeference function parseTime(resultTime){ let set2digits = function digits (num) { let digit; if(num < 10) {digit = "0" + num;} else {digit = num;} return digit; } const correctionYear = resultTime.getFullYear(); const correctionMonth = resultTime.getMonth() + 1; const correctionDay = resultTime.getDate(); const correctionHour = set2digits(resultTime.getHours()); const correctionMin = set2digits(resultTime.getMinutes()); const correctionSec = set2digits(resultTime.getSeconds()); return "修正:" + correctionYear + "年" + correctionMonth + "月" + correctionDay + "日" + correctionHour + ":" + correctionMin + ":" + correctionSec; }; if (checkStr.indexOf("早い") !== -1){ resultTime = new Date(InputCorrectionDate - setDate); return parseTime(resultTime); }else{ resultTime = new Date(InputCorrectionDate + setDate); return parseTime(resultTime); }; }; document.addEventListener('click', event =>{ let setCount = document.querySelectorAll(".container").length if (event.target.closest(`#check-correction-time-${setCount}`)){ if (setTime.timeDeference !== null){ let correctionTimeElement = timeInputPassElement(); document.getElementById(`CorrectionTime-${setCount}`).innerHTML = correctionTime(setTime, correctionTimeElement); }} }); }); これで、現在時刻を基準にして、ユーザーが任意に指定した時刻に対する時差計算を行うことができます。 私は、時差計算ができる要素を「追加ボタン」を押すことで、追加させ、元の要素に入力した値をそのまま残しておけるような機能を実装させたかったので、要素が増えるたびに、HTMLで記載したidの枝番が増えるようにしています。 追加ボタンを押すとこんな感じです↓ 時差計算機能のコード解説 実際に時差計算をしてくれるコードはこちらです。 function timeDeference(timeDeferenceElement){ const nowDate = new Date(); const inputSecond = getInputSecond(timeDeferenceElement.timeInputSecond); const InputDate = new Date(timeDeferenceElement.timeInput.value + ":" + inputSecond); const sign = nowDate > InputDate ? 1 : 0; const diff = Math.abs(nowDate - InputDate); const times = new Date(diff).toUTCString().match(/(\d+):(\d+):(\d+)/); const days = Math.floor(diff / 86400000); return { timeDeference: `${String(days).padStart(2, '0')}日${times[1]}時間${times[2]}分${times[3]}秒${['早い', '遅れ'][sign]}`, setTime: diff }; }; datatime-localで取得した時刻は分までしかないため、別で設けた秒数を入力できるinputタグのnumberで最小値と最大値を設けて、datatime-localで取得した値と結合させ、Dateオブジェクトを取得することで、PC版でもスマートフォン版でもこのサイトから秒数までの時差計算ができるようにさせています。 時差の計算結果を現在時刻より00日00時間00分00秒遅い/早いで表示させたかったので、計算する時刻が現在時刻の値より大きいか否かで0「遅い」1「早い」の値を定数signに格納させています。 Math.abs()関数を使用することで、計算結果の絶対値を取得することができるため、現在時刻よりも早くても、遅くても、時差式の計算結果をそのまま利用できるようにしています。 const times = new Date(diff).toUTCString().match(/(\d+):(\d+):(\d+)/); 定数timesに時差計算で出した時差diffの時、分、秒の部分を配列として格納させて、後から自由に取り出せるようにしています。 ちなみに、times[0]は、"00:00:00"というmatchメソッドで指定した正規表現/(\d+):(\d+):(\d+)/にマッチした値の全体部分が格納さます。 この正規表現の意味は//で囲った部分が正規表現であることを意味し、\dは0-9の整数を意味しており、直後に+を付すことで1回以上の繰り返しを意味しています。 つまり、new Date(diff).toUTCString()によって、"Thu, 01 Jan 1970 00:00:00 GMT"こんな感じの文字列が返ってきますが、.match(/(\d+):(\d+):(\d+)/)によって、00:00:00の部分だけを配列として返すことができるというわけです。 matchメソッドはキャプチャグループを含むすべての一致するものを含む配列を返します。 一致するものがない場合は null を返します。 Math.floor()関数は、計算結果の小数点を切り捨てた値を返してくれます。 const days = Math.floor(diff / 86400000)によって、時差の日数を出すことができます。 86400000ですが、Dateオブジェクトによる算術演算子はミリ秒で計算結果を返します。 よって、このミリ秒を日単位まで繰り上げるために日数 = 1000 * 60 * 60 * 24 (86400000)となっているわけです。 時差計算によって導き出した値の結果をreturnで呼び出し元に返しています。 この時に、計算結果を2つの値timeDeferenceとsetTimeで返していますが、setTimeは2つ目のcorrectionTime関数で使うためです。 時差計算を元に、更に、その時差計算結果を次の任意の時間の修正値に使えるようにしています。 ${String(days).padStart(2, '0')}日${times[1]}時間${times[2]}分${times[3]}秒${['早い', '遅れ'][sign]} についてですが、daysは、この時点で、number型として定義されているため、後のtimesや「早い」「遅い」は全てstring型になっており、daysも文字列型にするためにString()で文字列に変換しています。 また、padStart()は任意の固定長文字列を返すメソッドなので、daysを文字列にしなければなりません。 padStart(2, '0')によって、2桁の固定長文字列を返すことになり、daysの値が1桁の場合は、0が追加されて表示されることになります。 自分が記事にした内容について、コメントを頂き、自分の学習をさらに深めていくことができました。 長文記事になり、わかりにくい点が多々あったと思いますが、自分の理解をアウトプットしてみたので、何かご指導いただけることがありましたらコメントいただけるとありがたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptで実装するKuwaharaフィルタ

はじめに みなさんはKuwaharaフィルタをご存知でしょうか。 Kuwaharaフィルタは画像処理フィルタの一種で、物体の輪郭を残したまま画像のノイズを除去することが可能です。一方で、写真を絵画っぽく変換可能な画像処理フィルタとして話題になりました。 今回はこのKuwaharaフィルタの処理を、クライアントサイドのブラウザで実行可能なようにJavascriptで実装してみました。 デモサイトも作ってみたので、試してみて下さい。 アルゴリズム Kuwaharaフィルタのアルゴリズムの紹介です。(Wikipedia参照) あるピクセルの輝度値$I(x,y)$を決めるにあたり、まずは周辺$2a+1 $の正方形を4つの領域に分割し$Q_i(x,y)$を定義します。 ※$a$はフィルタ大きさ、$\times$はデカルト積を意味する Q_i(x,y) = \left\{ \begin{array}{ll} [x,x+a] \times [y,y+a] & if \; i = 1 \\ [x-a,x] \times [y,y+a] & if \; i = 2 \\ [x-a,x] \times [y-a,y] & if \; i = 3 \\ [x,x+a] \times [y-a,y] & if \; i = 4 \\ \end{array} \right. 図で表すとこんな感じです。 図から分かるように各領域は重複しています。 そして、フィルタ適用後の輝度値は、各領域の平均値$m_i(x,y)$と標準偏差$\sigma_i(x,y)$を使って、標準偏差が最も大きい領域の平均値として与えられます。 \Phi(x,y) = m_i(x, y) where \: i=arg \: min_j \: \sigma_j(x,y) つまり、あるピクセルの値を、周辺の4つの領域の中で最も均質な領域の平均値で置き換えるということです。基本的にフィルタ大きさ$a$が大きいほどより抽象的な画像となります。 計算量 上記のアルゴリズムを愚直に実装しようとした場合、各ピクセルに対する計算量が$2\times4(a+1)^2$ですので、画像一辺のピクセル数を$n$とすると、画像1枚で$8n^2(a+1)^2$の計算量となります。 フィルタサイズ$a$が大きい場合は$O(n^4)$の計算量になります。仮に画像サイズが$1000\times1000$だとすると$10^{12}$回ほどの演算が必要で、Intel Core i5の動作周波数が3.50GHzとかなので、計算に数分かかってしまいます。 そこで、計算量を削減するために2次元累積和を利用します。 累積和とは、配列に対して前処理を行うことで高速に区間和を求めることができるアルゴリズムです。 2次元の場合は、ある位置より左上にある全ての要素の和を算出した配列を事前に求めておいて、これを使って領域の和を算出します。 こちらの記事が非常に分かりやすいので引用させて頂きますが、以下の図のように二次元累積和を利用することで、画像内の黄色区間の和を$O(1)$で算出できます。 一方でKuwaharaフィルタでは、領域内の輝度値の平均値だけでなく標準偏差も算出する必要があります。ですが、標準偏差は以下のように式変形できるので、二乗和と和の累積和の配列さえ事前に用意しておけば標準偏差も同様に計算量$O(1)$で算出できます。 ※画像の各ピクセルの輝度値(0~255)は非負で、$\sigma$が最小のとき$\sigma^2$も最小です \sigma= \frac{1}{n} \sum_{i=1}^{n} (x_i - \bar{x} )^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - \frac{1}{n} \sum_{i=1}^{n} 2x_i \bar{x} + \frac{1}{n} \sum_{i=1}^{n} (\bar{x})^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - 2(\bar{x})^2 + (\bar{x})^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - \frac{1}{n^2}\Bigl(\sum_{i=1}^{n}x\Bigr)^2 以上より、二次元累積を利用することでKuwaharaフィルタの計算量は、事前計算を含めても$O(n^2)$となり、$1000\times1000$の画像でも計算量が$10^6$回程度で1秒以内で計算することが可能です。 実装コード 実装コードを以下はGithubレポジトリにあります。ぜひ使ってみてください。 ※Webサイトで使用する際の留意点 1. サイズの大きい画像を使う場合はブラウザのメモリ制限にご注意下さい。Chromeの場合は、$5000\times5000$程度の画像で限界でした。画像処理前のリサイズが良いと思います。 2. Javascriptはシングルスレッドですので、処理中はレンダリングが止まってしまいます。処理に時間がかかる場合は、リサイズするか画像処理部分をWeb Workerでマルチスレッド化するのが良いかと思います。 終わりに 今回は、ブラウザで動作する画像処理を実装してみました。次回は、ブラウザ側(JS)で動作する深層学習のWebアプリに挑戦してみようと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KuwaharaフィルタをJavascriptで実装してみた

はじめに Kuwaharaフィルタは画像処理フィルタの一種で、物体の輪郭を残したまま画像のノイズを除去することが可能です。また、少し前に写真を絵画っぽく変換できる画像処理フィルタとして話題になりました。 今回はこのKuwaharaフィルタの処理を、クライアントサイドのブラウザで実行可能なようにJavascriptで実装してみました。こちらのWebアプリで実際に処理を試すことが出来ます。 アルゴリズム Kuwaharaフィルタのアルゴリズムの紹介です。(Wikipedia参照) あるピクセルの輝度値$I(x,y)$を決めるにあたり、まずは周辺$2a+1 $の正方形を4つの領域に分割し$Q_i(x,y)$を定義します。 ※$a$はフィルタ大きさ、$\times$はデカルト積を意味する Q_i(x,y) = \left\{ \begin{array}{ll} [x,x+a] \times [y,y+a] & if \; i = 1 \\ [x-a,x] \times [y,y+a] & if \; i = 2 \\ [x-a,x] \times [y-a,y] & if \; i = 3 \\ [x,x+a] \times [y-a,y] & if \; i = 4 \\ \end{array} \right. 図で表すとこんな感じです。 図から分かるように各領域は重複しています。 そして、フィルタ適用後の輝度値は、各領域の平均値$m_i(x,y)$と標準偏差$\sigma_i(x,y)$を使って、標準偏差が最も大きい領域の平均値として与えられます。 \Phi(x,y) = m_i(x, y) where \: i=arg \: min_j \: \sigma_j(x,y) つまり、あるピクセルの値を、周辺の4つの領域の中で最も均質な領域の平均値で置き換えるということです。基本的にフィルタ大きさ$a$が大きいほどより抽象的な画像となります。 計算量 上記のアルゴリズムを愚直に実装しようとした場合、各ピクセルに対する計算量が$2\times4(a+1)^2$ですので、画像一辺のピクセル数を$n$とすると、画像1枚で$8n^2(a+1)^2$の計算量となります。 フィルタサイズ$a$が大きい場合は$O(n^4)$の計算量になります。仮に画像サイズが$1000\times1000$だとすると$10^{12}$回ほどの演算が必要で、Intel Core i5の動作周波数が3.50GHzとかなので、計算に数分かかってしまいます。 そこで、計算量を削減するために2次元累積和を利用します。 累積和とは、配列に対して前処理を行うことで高速に区間和を求めることができるアルゴリズムです。 2次元の場合は、ある位置より左上にある全ての要素の和を算出した配列を事前に求めておいて、これを使って領域の和を算出します。 こちらの記事が非常に分かりやすいので引用させて頂きますが、以下の図のように二次元累積和を利用することで、画像内の黄色区間の和を$O(1)$で算出できます。 一方でKuwaharaフィルタでは、領域内の輝度値の平均値だけでなく標準偏差も算出する必要があります。ですが、標準偏差は以下のように式変形できるので、二乗和と和の累積和の配列さえ事前に用意しておけば標準偏差も同様に計算量$O(1)$で算出できます。 ※画像の各ピクセルの輝度値(0~255)は非負で、$\sigma$が最小のとき$\sigma^2$も最小です \sigma= \frac{1}{n} \sum_{i=1}^{n} (x_i - \bar{x} )^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - \frac{1}{n} \sum_{i=1}^{n} 2x_i \bar{x} + \frac{1}{n} \sum_{i=1}^{n} (\bar{x})^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - 2(\bar{x})^2 + (\bar{x})^2\\ =\frac{1}{n} \sum_{i=1}^{n} x_i^2 - \frac{1}{n^2}\Bigl(\sum_{i=1}^{n}x\Bigr)^2 以上より、二次元累積を利用することでKuwaharaフィルタの計算量は、事前計算を含めても$O(n^2)$となり、$1000\times1000$の画像でも計算量が$10^6$回程度で1秒以内で計算することが可能です。 実装コード 実装コードを以下はGithubレポジトリにあります。 ※ブラウザのメモリ制限が有り、Chromeの場合は$5000\times5000$程度の画像で限界でした。画像処理前のリサイズが良いと思います。 ※Javascriptはシングルスレッドで、処理中はレンダリングが止まってしまうので、処理に時間がかかる場合はWeb Workerでマルチスレッド化するのが良いかと思います。 参考 ・Kuwahara filterとかいう明らかに日本人の名前な画像フィルターに出会い、試してみたらすごかったので紹介する。 ・累積和を何も考えずに書けるようにする!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

作ったゲームを他言語化(英語のみ)してみる

実装するゲーム 前に作ったHTML5のゲームに実装してみます。 テキスト量も程よかったです。 実装方法 言語用のテキストをjsonで準備する。 assets ∟gameText.json 構造は「ja(日本語)」、「en(英語)」で分けてそれぞれの言語選択で振り分けるできるようにした。 { "ja":{ "TitleScene":{ "menu":{ "menu1": "モンスターさくせい" } } }, "en":{ "TitleScene":{ "menu":{ "menu1": "Creat monster", } } } } 出来上がったもの 英語はgoogle翻訳でかなりアバウトに行っています。 振り分けスクリプトの実装 ブラウザの設定をjavascriptで検知できるのでリソースを読み込むsceneスクリプトに記述します。 まずjsonの読み込み this.load.json('gameText', 'assets/gameText.json'); jsonを読み込んでから振り分けしたかったのでリソースの読み込み完了後に振り分けを追加。 Phaser.jsでシーンを跨いでのデータ保持する場合は //データを格納する変数を作る this.registry.set('変数', '値'); //呼び出し var hoge = this.registry.list.変数; //変更 this.registry.list.変数 = "hogehoge"; が用意されているので、こちらを使用して保存しておく。 this.load.on('complete', () => { let gameText = this.cache.json.get('gameText'); this.registry.set('gameText', gameText); /*英語チェック*/ // 最優先の言語だけ取得 var lang = (window.navigator.languages && window.navigator.languages[0]) || window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage; if(lang == 'ja'){ this.registry.set('lang', 'ja'); this.registry.set('gameTextLang', this.registry.list.gameText.ja); }else{ this.registry.set('lang', 'en'); this.registry.set('gameTextLang', this.registry.list.gameText.en); } this.progress.destroy();//プログレスバーの削除 this.scene.start('TitleScene');//タイトルシーンに移動 }); 参考 チェック方法 を参考に chromeの設定>詳細設定>言語 三点リーダーから「下に移動」、「トップに移動」で優先順を選択して 振り分けられるかチェックします。 テキスト部分を書き換えていく ボタンのテキストを直で指定していたのをjsonの要素に書き換えていく。 this.btnMakeMonster = this.add.text( ~~省略~~ this.registry.list.gameTextLang.TitleScene.menu.menu1,//←'モンスター作成'を差し替え ~~省略~~ ); 切り替えの実装 アクセス時の判定以外でも、ゲーム内の設定として切り替えられるようにします。 小さいですがタイトルの右上に設置 ボタンを選択後に保存していた言語の変更して、シーンを読み直します。 SelectLanguage(_selectLang){ this.registry.list.lang = _selectLang;//選択した言語(ja or enが入る) if(_selectLang == 'ja'){ this.registry.list.gameTextLang = this.registry.list.gameText.ja; }else{ this.registry.list.gameTextLang = this.registry.list.gameText.en; } this.scene.restart(); } メニュー以外の動的なテキスト 日本語:「10連勝」 英語:「WIN 10」 みたいにしたい時に number = "1"; //日本語 text = number + "連勝"; //英語 text = "WIN" + number; 英語だと変数の順番を変更しなければならない。 無理矢理というか思いつかなかったので、jsonには前後のテキストを準備。 "ja":{ "number_text1": "", "number_text2": "連勝" }, "en":{ "number_text1": "WIN", "number_text2": "" } 記述は //日本語or英語 number = "1"; text = number_text1 + number + number_text2; //日本語:「1連勝」 //英語:「WIN1」 のように挟んで誤魔化した... テキストありの画像部分 テキストありの画像だったのはテキストに変更 タイトル部分は画像を2つ用意して切り替え(画像の切り替えはタイトルのみ) 地味に面倒ですが、タイトル部分の変更はテンションが上がります。 最後に 他の言語を追加する OGPの切り替え したときにどんどんややこしい実装になりそうな気がしています。 ベストプラクティスがわからず、こんな実装になりました。 英語化を思い立ってやってみましたが8時間ほどかかりました。 日本語のみを想定していたため、元のコードがいじりづらかった... ゲームを作るときは元から他言語化を想定している実装するのが良さそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

作ったゲームを多言語化(英語のみ)してみる

実装するゲーム 前に作ったHTML5のゲームに実装してみます。 テキスト量も程よかったです。 実装方法 言語用のテキストをjsonで準備する。 assets ∟gameText.json 構造は「ja(日本語)」、「en(英語)」で分けてそれぞれの言語選択で振り分けるできるようにした。 { "ja":{ "TitleScene":{ "menu":{ "menu1": "モンスターさくせい" } } }, "en":{ "TitleScene":{ "menu":{ "menu1": "Creat monster", } } } } 出来上がったもの 英語はgoogle翻訳でかなりアバウトに行っています。 振り分けスクリプトの実装 ブラウザの設定をjavascriptで検知できるのでリソースを読み込むsceneスクリプトに記述します。 まずjsonの読み込み this.load.json('gameText', 'assets/gameText.json'); jsonを読み込んでから振り分けしたかったのでリソースの読み込み完了後に振り分けを追加。 Phaser.jsでシーンを跨いでのデータ保持する場合は //データを格納する変数を作る this.registry.set('変数', '値'); //呼び出し var hoge = this.registry.list.変数; //変更 this.registry.list.変数 = "hogehoge"; が用意されているので、こちらを使用して保存しておく。 this.load.on('complete', () => { let gameText = this.cache.json.get('gameText'); this.registry.set('gameText', gameText); /*英語チェック*/ // 最優先の言語だけ取得 var lang = (window.navigator.languages && window.navigator.languages[0]) || window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage; if(lang == 'ja'){ this.registry.set('lang', 'ja'); this.registry.set('gameTextLang', this.registry.list.gameText.ja); }else{ this.registry.set('lang', 'en'); this.registry.set('gameTextLang', this.registry.list.gameText.en); } this.progress.destroy();//プログレスバーの削除 this.scene.start('TitleScene');//タイトルシーンに移動 }); 参考 チェック方法 を参考に chromeの設定>詳細設定>言語 三点リーダーから「下に移動」、「トップに移動」で優先順を選択して 振り分けられるかチェックします。 テキスト部分を書き換えていく ボタンのテキストを直で指定していたのをjsonの要素に書き換えていく。 this.btnMakeMonster = this.add.text( ~~省略~~ this.registry.list.gameTextLang.TitleScene.menu.menu1,//←'モンスター作成'を差し替え ~~省略~~ ); 切り替えの実装 アクセス時の判定以外でも、ゲーム内の設定として切り替えられるようにします。 小さいですがタイトルの右上に設置 ボタンを選択後に保存していた言語の変更して、シーンを読み直します。 SelectLanguage(_selectLang){ this.registry.list.lang = _selectLang;//選択した言語(ja or enが入る) if(_selectLang == 'ja'){ this.registry.list.gameTextLang = this.registry.list.gameText.ja; }else{ this.registry.list.gameTextLang = this.registry.list.gameText.en; } this.scene.restart(); } メニュー以外の動的なテキスト 日本語:「10連勝」 英語:「WIN 10」 みたいにしたい時に number = "1"; //日本語 text = number + "連勝"; //英語 text = "WIN" + number; 英語だと変数の順番を変更しなければならない。 無理矢理というか思いつかなかったので、jsonには前後のテキストを準備。 "ja":{ "number_text1": "", "number_text2": "連勝" }, "en":{ "number_text1": "WIN", "number_text2": "" } 記述は //日本語or英語 number = "1"; text = number_text1 + number + number_text2; //日本語:「1連勝」 //英語:「WIN1」 のように挟んで誤魔化した... テキストありの画像部分 テキストありの画像だったのはテキストに変更 タイトル部分は画像を2つ用意して切り替え(画像の切り替えはタイトルのみ) 地味に面倒ですが、タイトル部分の変更はテンションが上がります。 最後に 他の言語を追加する OGPの切り替え したときにどんどんややこしい実装になりそうな気がしています。 ベストプラクティスがわからず、こんな実装になりました。 英語化を思い立ってやってみましたが8時間ほどかかりました。 日本語のみを想定していたため、元のコードがいじりづらかった... ゲームを作るときは元から多言語化を想定している実装するのが良さそうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firefoxのアドオン審査をスムーズに通過するために気をつけること

気をつけること・・・というか、実際に指摘されたことのまとめです。 拡張のターゲットを明確にする Firefoxのアドオンは用途があまりに限定的だと弾かれてしまうようです。どういう人を対象としたアドオンなのかを明確に記述すると良いようです。アドオンをストアで公開する時は、開発した機能をある程度一般向けに公開したい時だと思いますので、それを素直に書けばよいと思われます。 テスト方法を記述する 実際にどうやって使うのか、その方法を記述しないと、「テスト方法がわからないから教えてくれ」と怒られます。審査担当者宛のホワイトボードという所にテスト方法を書くと良いようです。すべての機能の厳密なテストを行っているわけではなさそうですが。 third-party製ライブラリは改変せず使用する jQueryなどのライブラリを含めるときは、改変せず使用する必要があります。リネームもおそらく不可で、怒られます。また、使用したバージョンのソースコードが閲覧できるURLを合わせて提出する必要があります。詳しくは公式ドキュメントを参照してください。 ページに要素を追加するときは、危険なコードが埋め込まれないよう対策する 特にユーザーに入力させたものなどを追加するときなど、innerHTMLなどを対策無しで使ってしまうと、悪意あるスクリプトを挿入される恐れがあります。それを防ぐために、innerText等を用いるか、DOMPurifyのようなライブラリを使用して、挿入する要素を安全に保つ必要があります。jqueryを使う際はhtml()に直接DOMを書くのではなく、多少面倒でも要素を生成し、appendなどのjqueryのメソッドを用いるのが安全です。 おまけ アドオンの説明などは日本語での記述が可能ですが、審査レビューは全て英語なので、おそらく審査してるのは日本人ではないと思われます。自動翻訳などを使っていると思いますので、主語と述語のはっきりした翻訳向きの文章にすると、心象がちょっとよくなる・・・かもしれません。答案用紙はきれいに書け程度の話ですが。 また何か怒られたら追記します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Javascript】Strings(3)学習ノート

初めに Stringについて学習した内容のoutput用記事です。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 ※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。 前回の記事: https://qiita.com/redrabbit1104/items/d68a8f79420a2a0320a6 https://qiita.com/redrabbit1104/items/75ff729fff8c51f97e86 文字列の分割 split()メソッドを使い、文字列を分割し配列に格納します。 const bank = "Mitsubishi UFJ Bank"; const splitBank = bank.split(" "); console.log(splitBank); //(3) ["Mitsubishi", "UFJ", "Bank"] splitBankは配列なので、変数を3つ用意すればそれぞれ分割代入できます。 const [companyName1, companyName2, companyName3] = splitBank; console.log(companyName1); //Mitsubishi console.log(companyName2); //UFJ console.log(companyName3); //Bank 文字列の結合 join()メソッドで文字列を結合できます。対象になる文字列は配列の形になっている必要があります。 const joinBank = [ companyName1.toUpperCase(), companyName2, companyName3.toUpperCase(), ].join("?"); console.log(joinBank); //MITSUBISHI?UFJ?BANK これを応用すればsplit()メソッドで文字列を分割し、for of文を使って最初の文字だけを大文字にし結果を返す関数を作れます。関数として定義するとどんな文字列でも最初の文字を大文字にすることができます。 const friendNames = "john messy louis jessy"; const uppercaseFirstLetter = (name) => { const names = name.split(" "); //名前を" "で分割し,names配列に格納 const results = []; //結果を格納する for (const eachName of names) { results.push(eachName[0].toUpperCase() + eachName.slice([1])); } console.log(results.join(" ")); //配列のresultsにある値をjoinメソッドで結合 }; uppercaseFirstLetter(friendNames); //John Messy Louis Jessy また、名前を大文字にするところは、replace()メソッドで次のように書き換えることもできます。 //前略 for (const eachName of names) { results.push(eachName.replace(eachName[0], eachName[0].toUpperCase())); } //攻略 文字列を補う 文字列の全体の数を決め、残りの足りない文字数を指定した文字で補うることもできます。文字列の先頭から埋めていくか、末尾から埋めるかによってpadStart()かpadEnd()メソッドを使います。 console.log(friendNames.padStart(30, "+")); //++++++++john messy louis jessy console.log(friendNames.padEnd(30, "+")); //john messy louis jessy++++++++ そして、次のようにメソッドチェーンとして繋げることもできます。この場合、合計文字数は40文字になります。 console.log(friendNames.padStart(30, "+").padEnd(40, "+")); //++++++++john messy louis jessy++++++++++ これを応用すれば、クレジットカードで下桁4文字を除いた残りの部分を*にすることもできます。 const asteriskCardNumber = (card) => { const strCard = card + ""; const showCardNumber = strCard.slice(-4); console.log(showCardNumber.padStart(strCard.length, "*")); }; asteriskCardNumber(12340499030223); //**********0223 asteriskCardNumber("49892839048594"); //**********8594 文字列の繰り返し repeat()メソッドで文字列を数分だけ繰り返し表示できます。 const repeatMessage = "起きなさい!"; console.log(repeatMessage.repeat(10)); //起きなさい!起きなさい!起きなさい!起きなさい!起きなさい!起きなさい!起きなさい!起きなさい!起きなさい!起きなさい! テンプレートリテラルで次のように繰り返したい絵文字? を関数で定義して使ってみると面白いです。 const cars = (num) => { console.log(`There are ${num} cars ${"?".repeat(num)} on the road.`); }; cars(5); //There are 5 cars ????? on the road. 参考サイト https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/join https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/padStart https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js+Firebaseでメールフォームを作成する

はじめに Vue.jsで制作したSPAにFirebaseFunctionsを用いて簡単にメールフォームを設置できました。 制作にあたって、こちらの記事を参考にさせていただきました。 適用したVue.jsのアプリはこちら↓ 1. FirebaseFunctionsの追加とメール通知処理の実装 こちらの記事の  1. プロジェクトの追加  2. Firebase functionでのメール通知処理の実装 を参考にさせていただきました。 Functionsを使用するにあたって有料枠にする必要がありますが、従量課金制度なので大量にメールが送られてこない限り、無料で使うことができます。 また、2.のconfig内の値はFirebaseプロジェクトコンソールのcdnチェックボックス内に記載されています。 2. ContactFormコンポーネントの作成 component配下でContact.vueファイルを作成します。 Contact.vue <template> <div class="contact"> <h1>Contact</h1> <v-container> <v-form ref="form" v-model="contactFormValidation.valid" lazy-validation > <v-text-field v-model="contactForm.name" :rules="contactFormValidation.nameRules" label="name" required ></v-text-field> <v-text-field v-model="contactForm.email" :rules="contactFormValidation.emailRules" label="email" required ></v-text-field> <v-textarea v-model="contactForm.contents" :rules="contactFormValidation.contentsRules" label="contents" required ></v-textarea> <v-btn :loading="contactForm.loading" :disabled="!contactFormValidation.valid" @click="sendMail()" color=primary block large >submit </v-btn> </v-form> </v-container> <v-snackbar v-model="snackBar.show" :color="snackBar.color" bottom right :timeout="6000" > {{snackBar.message}} </v-snackbar> </div> </template> <script> import { functions } from '@/plugins/firebase.js' export default { data: () => ({ contactForm: { name: '', email: '', contents: '', loading: false }, contactFormValidation: { valid: false, nameRules: [v => !!v || '必須項目です'], emailRules: [ v => !!v || '必須項目です', v => /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(v) || 'メールアドレスが正しくありません' ], contentsRules: [v => !!v || '必須項目です'] }, snackBar: { show: false, color: '', message: '' } }), methods: { sendMail: function () { if (this.$refs.form.validate()) { this.contactForm.loading = true const mailer = functions.httpsCallable('sendMail') mailer(this.contactForm) .then(() => { this.formReset() this.showSnackBar( 'success', 'お問い合わせありがとうございます。送信が完了しました。' ) }) .catch(err => { this.showSnackBar( 'error', '送信に失敗しました。時間をおいて再度お試しください。' ) console.log(err) }) .finally(() => { this.contactForm.loading = false }) } }, showSnackBar: function (color, message) { this.snackBar.message = message this.snackBar.color = color this.snackBar.show = true }, formReset: function () { this.$refs.form.reset() } } } </script> 空欄だったり、メールアドレスが正しくないと送信できないようにvalidationを設けました。 特にメールアドレスのvalidationは下記の記事で詳しく解説しています。 また、App.vueにContact.vueを読み込ませることも忘れずに行いましょう。 3. メールサーバの設定・デプロイ 先ほどと同様にこちらの記事の  4. メールサーバーの設定・デプロイ を参考にさせていただきました。 さいごに Firebaseを使うとサーバサイドの処理をほとんど記述することなく、メールフォームを実装できるので驚きました。 v-modelでの双方向バインディングで入力情報を常にチェックできるのでVue.jsを用いてよかったと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザバックでのページ表示時処理

ブラウザバックで動くJSの処理について質問されて、正確な情報を伝えられなかったのがきっかけで掘り下げました。 ページ表示時に動くJS処理は何があるか? イベント 内容 DOMContentLoaded HTML解析完了時 load 全リソースの読み込み完了時(キャッシュの場合は対象外) pageshow 全リソースの読み込み完了時(キャッシュの場合も対象) 参考:ブラウザ関連のJavaScript - Qiita ちなみに呼ばれる順番は、①DOMContentLoaded → ②load → ③pageshowです。 各ブラウザでのブラウザバックで動くJS処理 確認方法 A.html ↓ リンク押下 B.html ↓ ブラウザバック A.html ←このタイミングで動くページ表示時処理 キャッシュから表示された場合 Firefox、Safariは、キャッシュから復元されるとDOMContentLoaded, loadが動かない。 リクエストはキャッシュから復元されているので送らない。 キャッシュされないように指定した場合(cache-control: no-cacheをレスポンスに指定) Firefoxは、キャッシュから復元されたかの有無でDOMContentLoaded, loadが動いたり・動かなかったりする。 (2021年06月06日時点) Safariは、cache-control: no-cacheを指定してもキャッシュから復元してしまう。 【Safari】no-store がついていてもbfcacheが有効になる - UGA Boxxx pageshowイベント ブラウザバックで安定して呼ばれるのは、pageshowイベント。 pageshowイベントの引数でPageTransitionEventを受け取ります。 PageTransitionEvent.persistedのtrue or falseでキャッシュから表示されているのかを判定できます。 window.addEventListener('pageshow', function(event) { if (event.persisted) { console.log('キャッシュから表示'); } else { console.log('新しいページを受信して表示'); } }); Window: pageshow イベント - Web API | MDN PageTransitionEvent.persisted - Web APIs | MDN ブラウザバックに関連するキーワード bfcache bfcacheは、Back Forward Cacheのこと。 Firefox 1.5では、訪問したページ全体をJavaScriptの状態も含めてメモリ内にキャッシュすることで、ページ間の戻る、進む時のページロードが不要にしている。 Using Firefox 1.5 caching - Mozilla | MDN HTTPヘッダ cache-control: no-cache レスポンスのHTTPヘッダにcache-control: no-cacheを指定するとブラウザにキャッシュさせたくないことを指定できる。 no-store:  レスポンスをキャッシュに保存することはできません。他のディレクティブを設定することもできますが、最近のブラウザーではレスポンスがキャッシュされることを防ぐために必要なディレクティブはこれだけです。 Cache-Control - HTTP | MDN まとめ 現状はブラウザバック後でも、動かしたいJSの初期処理がある場合はpageshowイベントを使う方が望ましい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】ローディング画面を実装する

はじめに vue-loading-templateモジュールを用いて、Vue.jsのアプリにローディング画面を実装しました Vue.jsのアプリにはVuetifyを使用しています 適用したVue.jsのアプリはこちら↓ . vue-loading-templateのインストール $ npm add vue-loading-template src/App.vue App.vue <template> <v-app id="inspire" :style="{background: $vuetify.theme.dark}" >   <!-- ローディング --> <Loading v-show="isLoading"></Loading> <v-main :style="{width:'90%', margin: '3vh auto'}" v-show="!isLoading" > <!-- 省略 --> </v-main> <v-footer color="grey darken-2" padless v-show="!isLoading" > <v-col class="text-center white--text" cols="12" > {{ new Date().getFullYear() }} — <strong>Komekami</strong> </v-col> </v-footer> </v-app> </template> <script> // Loadingコンポーネントの読み込み import Loading from '@/components/Loading' export default { data: () => ({ isLoading: true, drawer: null, items: [ { title: 'Top', icon: 'mdi-view-dashboard', link: '/' }, { title: 'Profile', icon: 'mdi-account', link: '/profile' }, { title: 'Gallery', icon: 'mdi-image', link: '/gallery' }, { title: 'Contact', icon: 'mdi-forum', link: '/contact' } ], right: null }), // Loading切り替え処理 mounted () { setTimeout(() => { this.isLoading = false }, 1000) }, components: { Loading } } </script> Point ローディング画面コンポーネントをLoadingとして読み込んでいます 変数isLoadingを用いてtrueかfalseかでローディング画面の表示/非表示を行っています。 mountedを使用し切り替えを行っています。 mountedはDOMの作成完了時にisLoading=falseとなり、ローディング画面が非表示になります。 メイン部(v-main)とフッター部(v-footer)は変数isLoadingがfalseで描画(v-show)されます。 src/components/Loading.vue ローディング画面のコンポーネントです。 Loading.vue <template> <div v-show="isLoading"> <div class="fullview"> <div class="loading-spacer"></div> <vue-loading type="beat" color="#304686" :size="{ width: '100px', height: '100px' }" > </vue-loading> </div> </div> </template> <script> import { VueLoading } from 'vue-loading-template' export default { name: 'isLoading', data () { return { isLoading: true } }, components: { VueLoading } } </script> <style> .fullview { width: 100%; height: 100%; background:black; position: fixed; top: 0; left: 0; } .loading-spacer { height: 40%; } </style> Point VueLoadingをインポート <vue-loading>内でLoadingの各種設定を行っています 今回はローディングの動き方をtype="beat"にしました 他にも色々typeがあるのでvue-loading-templateをチェックしてみてください さいごに モジュールをインストールすることで簡単にローディング画面を実装することができました。 SPAは初回読み込み時に時間がかかりがちなので、ローディング画面を実装するとサイト訪問者のストレスも軽減されるのではないかと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack + Babel を使ってIE対応したつもりが、アロー関数が残っていてIEで実行できなかった件

あるプロジェクトで、webpackでVue.jsの環境構築をしました。 Babelを導入しトランスパイルの設定もしたので、Internet Explorer (IE11) でも動くだろうと思っていました。 しかし、実際にIE11で確認してみると、Vue.jsで作ったアプリケーションが全く動かない。。 コンソールを見てみると、JavaScriptのエラーが出ていました。 なんでだろうと思い、コンパイル後のファイルを覗いてみると、、 app.js (()=>{ ... })(); いや、アロー関数残ってるやないかーい!! IE11はいまだにアロー関数に対応してないので、これだとエラーになってしまいます。 うまくいかなかったときのwebpackの設定 当初、webpackの設定ファイルの中身はこんな感じでした。 webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { entry: { app: './src/app.js', }, output: { filename: '[name].js', path: __dirname + '/dist' }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new VueLoaderPlugin() ], resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, } } ちゃんとBabelインストールして設定したのに、何がいけないの? って感じでした。 Babelの設定の問題だと思い、いろいろ試し、何時間も格闘しました。 が、原因はBabelではありませんでした。 改良後のwebpackの設定 改良後のwebpackの設定ファイルはこんな感じです! webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { entry: { app: './src/app.js', }, output: { filename: '[name].js', path: __dirname + '/dist', environment: { arrowFunction: false } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new VueLoaderPlugin() ], resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, } } さて、みなさん、何が変わったかお気づきでしょうか? なんと、たった3行追加しただけです! environment: { arrowFunction: false } この3行をoutputの中に入れただけです! よく見ると、webpackの公式ドキュメントに、さらっと書いてあります。さらっと。。 https://webpack.js.org/configuration/output/#outputenvironment こんなのわかるわけないじゃないか!! 上記の設定をすることで、webpackがアロー関数を出力しないよう制御できるみたいです。 結果、コンパイル後のファイルを見てみると… app.js !function(){ ... }(); やった! やっと関数がfunctionになったぞ! IEで確認してみると、、 無事に動きました!! めでたしめでたし みなさんも、Babel導入してちゃんと設定したのにアロー関数が残っているなーと思ったら、上記設定を試してみてください。 僕みたいに無駄に格闘して消耗する被害者が減ることを願っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザを使った Python Web アプリケーションのテスト

はじめに 最近趣味で Python の Web フレームワーク(Django や Flask みたいなやつ)を開発しているのですが,その際 Cookie や CORS などのテストを行う必要が出てきました.その類のテストは Fetch API などのブラウザの JavaScript 用 API を使用したテストであるため,通常の Python を実行して行うユニットテストではなく,ブラウザに何らかの方法で JavaScript を実行させてテストする必要があります.このようなテスト方法は(おそらくあるとは思うのですが)ググってもあまり良い前例が見つからなかったので,色々試行錯誤してある程度形になったものをここにまとめたいと思います. 注意点 今回は Web フレームワークとして私が個人開発しているものを使用しますが,他のものを使用したとしても流れとしては同じですので,そこらへんは工夫してみてください. 用語について 以下ではサーバーという用語を使用しますが,物理的なマシンのことではなく,実行可能なサーバーアプリケーションという意味で使用するので注意してください(この辺の用語のニュアンスは極めて曖昧で厄介なので困ります). テスト方法 登場人物 このテストには3つの登場人物がいます: ブラウザ(ユーザーエージェント;ここでテストが実行される) Web サーバー(ブラウザへ HTML 等を提供) App サーバー(Python で書かれた Web アプリケーション;テストしたいアプリケーション) ※ 場合によっては 2, 3 は同一のアプリケーションとして実行されるものもありますが,今回は別々であるとします. 上図の矢印では以下のような通信が発生しています: Web サーバーからブラウザへ HTML や関連ファイルが送信される 1 で送信された HTML 内のスクリプトが実行され,App サーバーへテスト用のリクエストを行う 今回はこのような登場人物と通信が行われるテストを,出来る限り簡単に実行できる方法(例えば1つのコマンド)について述べます. マルチプロセス このテストでの登場人物の数をプロセス数と一致させるのであれば,このテストを行う際には3つのプロセスが同時に実行されている必要があります.ということで,マルチプロセスでテストを実行する方法を取ることにします.今回はブラウザに関しては Python 内で直接的に操作することせず,標準ライブラリである webbrowser を使ってブラウザを起動するところまでで留めようと思います. QUnit QUnit は比較的簡単に導入できる JavaScript テストフレームワークです.私は JavaScript の世界にあまり詳しくないので,導入が楽で助かりました.この記事では QUnit の使い方については述べませんが,公式ドキュメントを見ればある程度理解できると思います. テストの流れ 以上のテスト方法をまとめると,テストは次のような手順で行います: 子プロセスで App サーバーを起動 子プロセスで Web サーバーを起動 親プロセスで Web サーバーのテスト用エントリーポイントを指定しブラウザを起動 ブラウザが Web サーバーからテスト用エントリーポイントを受け取る テスト用スクリプトが順に読み込まれテストが実行される ここで,テスト用エントリーポイントとはテスト用の JavaScript を埋め込んだ HTML ファイルのことです.また,上述したようにテスト用スクリプトでは QUnit を使用してテストを行います. 実装 実装の方法は様々ですが,ここではその一例を示します. ディレクトリ構造 アプリケーションの規模によってプロジェクトのディレクトリ構造は様々ですが,ここでは簡単のために以下のように設定します. |- web/ | |- tests/ | | |- bye.test.js | | |- hello.test.js | |- index.html |- app.py |- main.py App サーバー App サーバーは Python で書かれた実際にテストしたい Web アプリケーションです.ここでは,一例として適当に実装します(実際には認証や複雑な API ロジックが実装されるかと思います).エンドポイントは以下の2つです: /hello (GET, POST) /bye (GET) 実装自体は自作のフレームワークで行っておりますが,Flask などの Web フレームワークを使ったことがある方であれば,コードを見ればなんとなく内容を理解できると思います.内部ロジックは特に難しいことはしておらず,短いバイナリデータを送信したり,JSON データを受信したりしているだけです.この記事の目的は,このアプリケーションをブラウザを使ってテストすることなので,これ以上の説明は割愛します. app.py from bamboo import ( BinaryApiData, HTTPStatus, JsonApiData, WSGIApp, WSGIEndpoint, ) from bamboo.sticky.http import ( add_preflight, allow_simple_access_control, set_cache_control, data_format, ) app = WSGIApp() class TestRequest(JsonApiData): account_id: str email_addr: str age: int @app.route("hello") @add_preflight( allow_methods=["GET", "POST"], allow_origins=[], add_arg=False, ) class HelloEndpoint(WSGIEndpoint): @set_cache_control(no_cache=True) @data_format(input=None, output=None) def do_GET(self) -> None: self.send_body(b"Hello, Client!") @data_format(input=TestRequest, output=None) def do_POST(self, req: TestRequest) -> None: print(req.dict) self.send_only_status(HTTPStatus.ACCEPTED) @app.route("bye") class ByeEndpoint(WSGIEndpoint): @set_cache_control(no_cache=True) @allow_simple_access_control(add_arg=False) @data_format(input=None, output=BinaryApiData) def do_GET(self) -> None: self.send_body(b"Bye, Client!") テスト用 Web サーバー こちらはブラウザさんに実行してもらうテストを実装する必要があります.そのためには,とりあえずブラウザのエントリーポイントとしての HTML を用意し,そこに埋め込むテスト用のスクリプトを書けば OK です.つまり今回の場合は,最低限用意すべきファイルは web/index.html --> テスト実行用のエントリーポイント web/tests/*.js --> 実際に実行したいテスト用スクリプト ということになります(テスト用スクリプトの名前は何でもいいです). テスト実行用エントリーポイント まずはテスト実行用のエントリーポイントを用意します.こちらの実装はそのプロジェクトがどういうものであるかによって異なりますが,今回のように単純に通信のみのテストを行いたい場合には,以下のようなテンプレの HTML ファイルを用意するだけで事足ります. web/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Browser tests</title> <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.15.0.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <!-- QUnit --> <script src="https://code.jquery.com/qunit/qunit-2.15.0.js"></script> <!-- Test scripts --> <script src="./tests/hello.test.js"></script> <script src="./tests/bye.test.js"></script> <!-- テスト用のスクリプトが増えればここに追加する --> </body> </html> テスト用スクリプト 次に実際にブラウザさんに実行してもらうテスト用スクリプトを用意します.このスクリプトの実装では,上述したように QUnit を使用します.QUnit の具体的な使用方法については公式ドキュメントに譲りますが,基本的なテストの流れは「リクエスト --> レスポンスの検証」の流れになるでしょう. テスト用スクリプトのファイルの分割方法については任意ですが,個人的にしっくりした分割方法は「1つのエンドポイントにつき1つのテストスクリプト」に基づいた分割方法です.これについては好みの問題なので,色々試行錯誤してみるといいかもしれません. まずは「/hello」への通信テストを行うためのテストスクリプトを書きます. web/tests/hello.test.js QUnit.module("/hello"); const uriHello = "http://localhost:9000/hello"; QUnit.test("GET", (assert) => { const done = assert.async(); fetch(uriHello, { method: "GET" }) .then((res) => { assert.true(res.ok); res.text() .then((body) => { assert.equal(body, "Hello, Client!"); }) .catch((err) => { throw err; }); }) .catch((err) => { throw err; }) .finally(() => { done(); }); }); QUnit.test("POST", (assert) => { const done = assert.async(); const data = { account_id: "hogehoge", email_addr: "hogehoge@hoge.com", age: 99, }; fetch(uriHello, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }) .then((res) => { assert.true(res.ok); }) .catch((err) => { throw err; }) .finally(() => { done(); }); }); 次に「/bye」への通信テストを行うためのテストスクリプトを書きます. web/tests/bye.test.js QUnit.module("/bye"); const uriBye = "http://localhost:9000/bye"; QUnit.test("GET", (assert) => { const done = assert.async(); fetch(uriBye, { method: "GET" }) .then((res) => { assert.true(res.ok); res.text() .then((body) => { assert.equal(body, "Bye, Client!"); }) .catch((err) => { throw err; }); }) .catch((err) => { throw err; }) .finally(() => { done(); }); }); テストランナー 最後にテストランナー(全てのテストを実行するためのスクリプト)を実装します.上述したように,今回はブラウザ,Web サーバー,App サーバーを使ってテストを行うので,マルチプロセスでテストを実行します.具体的には,2つの子プロセスを生成し,そこで Web サーバーと App サーバーを起動します.起動後,親プロセス側で Web サーバーの URI を指定してブラウザを起動させ,そのブラウザ上でテストを実行します.今回は特別なサードパーティ製のライブラリは使用せず,標準ライブラリのみで実装しています. main.py from http.server import SimpleHTTPRequestHandler from multiprocessing import Process from pathlib import Path import socket from socketserver import ( BaseRequestHandler, BaseServer, TCPServer, ) import time import typing as t import webbrowser from bamboo import WSGITestExecutor from app import app HOST_WEB = "localhost" PORT_WEB = 8000 HOST_APP = "localhost" PORT_APP = 9000 DIR_WEB = str(Path(__file__).absolute().parent / "web") SLEEP_TIME_SEVER_INIT = 0.05 class WebHTTPRequestHandler(SimpleHTTPRequestHandler): """ドキュメントルートを指定するためのリクエストハンドラ""" def setup(self) -> None: super().setup() self.directory = DIR_WEB class WebServer(TCPServer): """アドレスを再利用するために改造した TCPServer""" def __init__( self, server_address: t.Tuple[str, int], RequestHandlerClass: t.Callable[..., BaseRequestHandler], bind_and_activate: bool = True, ) -> None: BaseServer.__init__( self, server_address, RequestHandlerClass, ) self.socket = socket.socket( self.address_family, self.socket_type, ) # この部分が TCPServer と異なる self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if bind_and_activate: try: self.server_bind() self.server_activate() except: self.server_close() raise def run_app_server() -> None: """App サーバーを起動""" WSGITestExecutor.debug(app, HOST_APP, PORT_APP) def run_web_server() -> None: """Web サーバーを起動""" with WebServer( (HOST_APP, PORT_WEB), WebHTTPRequestHandler, ) as server: server.serve_forever() def run_servers(sleep_server_init: float) -> t.Tuple[Process, Process]: """2つのサーバーを子プロセスで起動""" ps_app = Process(target=run_app_server) ps_app.start() ps_web = Process(target=run_web_server) ps_web.start() # サーバー起動までスリープ(サーバーの起動時間によって調整) time.sleep(sleep_server_init) return (ps_app, ps_web) def main() -> None: """テストを実行""" ps_app, ps_web = run_servers(SLEEP_TIME_SEVER_INIT) webbrowser.open_new(f"http://{HOST_WEB}:{PORT_WEB}") try: while True: time.sleep(10000) except KeyboardInterrupt: print() finally: ps_app.terminate() ps_web.terminate() ps_app.join() ps_web.join() ps_app.close() ps_web.close() if __name__ == "__main__": main() 実行 main.py を実行することでテストを実行できます: $ python main.py テストを実行するとブラウザが起動され,無事成功すると下図のように表示されます.もし失敗すればその原因が表示されます(ブラウザのコンソールも見た方がいいです). ブラウザを閉じても上記のコマンドを実行したターミナルはブロックされたままですが,それは今回の実装では Python 側がブラウザが閉じたことを認識出来ないためです.Ctrl-C を押してテストの実行を終了させましょう. おわりに 本記事は Python で実装した Web アプリケーションをブラウザの JavaScript 用 API を使用してテストする手法(我流)についてまとめてみました.最終的にテストが1つのコマンドに収まったので,個人的には最小限これでいいかなという感じです.ただ,Python で扱えるブラウザ操作用のツールとして Playwright for Python などがあるので,これらを使って同じようなことが出来るのならそちらを採用した方が良いかもしれません.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React チュートリアル発展 追加課題

React公式チュートリアル追加課題の実装に関してまとめます。前回の記事で関数コンポーネント化を行った後の続きです。まだの方は先に確認して頂ければと思います。 https://qiita.com/nishiwaki_ff/items/d60f2ba346521610775b 追加実装の流れ 公式チュートリアルのタイムトラベル機能実装まで完了(https://ja.reactjs.org/tutorial/tutorial.html#wrapping-up) モジュール化 関数コンポーネントに書き換え 追加課題の実装 ⇦この記事 追加課題1 解説 課題1の内容は、履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示するです。この課題を実装するには次の2つのことを実現する必要があります。 着手の位置をstateで保持する (col, row) のフォーマットで表示する 着手の位置や、それを表示する履歴情報を持っているのはGameコンポーネントです。では順番にGameコンポーネントに修正を加えます。 着手の位置をstateで保持する ボードが要素9つの配列になってるので、そのインデックスを着手の位置として保持しようと思います。最終的に(col, row) のフォーマットで表示する必要があるものの、インデックスから計算で導き出すことができるので、インデックスのみをstateに保持させます。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null // 着手の位置を保持する為pointを追加 } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i // pointにインデックスを代入 } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move + '(' + step.point + ')' : // 確認の為着手位置を表示 'Go to game start'; // 以下変更無し ここまで修正を終えるとこのような結果になると思います。あとは表示を修正すれば完成です。 (col, row) のフォーマットで表示する Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; // colを算出 const row = (step.point / 3 + 1) | 0; // rowを算出 const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; // 指定のフォーマットに修正 const desc = move ? goToMove : 'Go to game start'; // 1行に修正 return ( <li key={move}> <button onClick={() => jumpTo(move)}>{desc}</button> </li> ); }); // 以下変更無し colとrowを算出した後、(col, row) のフォーマットで表示させています。rowに関してはビット論理和を用いて小数点以下を切り捨てています。ビット論理和の代わりにMath.floor関数を使うことも可能です。 以上で課題1は終了です。このような結果になります。 追加課題2 解説 課題2の内容は、着手履歴のリスト中で現在選択されているアイテムをボールドにするです。この課題を実装するには次の2つのことを実現する必要があります。 履歴の表示をボールドにする 現在選択されているアイテムのみをボールドにする 履歴の表示をボールドにする まずは着手履歴リストのbuttonタグにclassを追加します。 Game.js const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={'bold'} // class追加 > {desc} </button> </li> ); }); // 以下変更無し 追加したclassに対応する記述をcssに追加します。 index.css /* 追加 */ .bold { font-weight: bold; } 履歴の表示が全てボールドになります。 現在選択されているアイテムのみをボールドにする 最後に現在選択されているアイテムのみボールドに修正していきます。現在選択されているアイテムを判定するにはstemNumberが使えます。 Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} // 現在選択されているアイテムだけclassNameをboldにする className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); // 以下変更無し 以上で課題2が終了です。 追加課題3 解説 課題3の内容は、Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換えるです。この課題は繰り返し文を利用して実装します。まずは一部だけfor文で書き換えます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} /> ); }; const maxCol = 3; const rowBoard1 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 0 + col; rowBoard1.push(renderSquare(index)); } return ( <div> <div className="board-row"> {rowBoard1} </div> <div className="board-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="board-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </div> ); }; export default Board; rowBoard1の箇所を追加及び修正しました。しかしこの時点で警告が出ます。 keyが必要なので追加します。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} // key追加 /> ); }; // 以下変更無し これで警告も消えたので先程と同様にfor文で書き換えていきます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const maxCol = 3; const rowBoard1 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 0 + col; rowBoard1.push(renderSquare(index)); } const rowBoard2 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 1 + col; rowBoard2.push(renderSquare(index)); } const rowBoard3 = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * 2 + col; rowBoard3.push(renderSquare(index)); } return ( <div> <div className="board-row"> {rowBoard1} </div> <div className="board-row"> {rowBoard2} </div> <div className="board-row"> {rowBoard3} </div> </div> ); }; export default Board; 明らかに冗長なのでさらにfor文を追加してネストさせます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; rowBoard.push(renderSquare(index)); } squareBoard.push( <div className="board-row"> {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; これでほぼ完成ですが、ここで再び警告が出ます。 先程と同様にkeyを追加すると警告が消えます。 Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; rowBoard.push(renderSquare(index)); } squareBoard.push( <div className="board-row" key={'row-' + row} // key追加 > {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; 以上で課題3が終了です。 追加課題4 解説 課題4の内容は、着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加するです。次の順番で実装していきます。 現在は昇順で表示されているので、降順で表示させる 昇順と降順を判定するためのstateを追加 ボタンを追加し、ボタン押下で並べ替えができるようにする 降順で表示させる 降順で表示させるにはreverse()メソッドを用いてmovesの順序を入れ替えることで実現できます。 Game.js const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); moves.reverse(); // movesの順序入れ替え // 以下変更無し 昇順と降順を判定するためのstateを追加 Game.js const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); // state追加 const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); // stateの値で昇順と降順を判定し条件分岐 if (movesOrder) { moves.reverse(); } // 以下変更無し ボタンを追加し、ボタン押下で並べ替えができるようにする まずはボタンを追加します。 Game.js return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> {/* ボタン追加 */} <button> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); // 以下変更無し ボタンを追加しましたが、まだ押下しても何も変化しません。最後にボタンを押下した時にstateの値が変わるようイベントを追加します。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> {/* イベント追加 */} <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }; export default Game; 以上で課題4が終了です。 追加課題5 解説 課題5の内容は、どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトするです。次の順番で実装していきます。 calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする 3つのマスの情報を用いて該当箇所をハイライトする calculateWinner関数で勝利者と一緒に勝利につながった3つのマスの情報を返すようにする calculateWinner関数の中身とそれを呼び出していた箇所を修正します。 Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares).winner || squares[i]) { // winnerを参照するよう修正 return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const winner = calculateWinner(current.squares).winner; // winnerを参照するよう修正 const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; // 勝者とマスの情報をもつオブジェクトを用意 const result = { winner: null, winLine: [] } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; // 勝者をresultに代入 result.winLine = result.winLine.concat(lines[i]); // 勝利に繋がったマスの情報をresultに代入 } } return result; // resultを返す }; export default Game; 3つのマスの情報を用いて該当箇所をハイライトする calculateWinner関数から返るマスの情報をまずはBoardコンポーネントに渡します。 Game.js const current = history[stepNumber]; const {winner, winLine} = calculateWinner(current.squares); // winnerとwinLineを分割代入 const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } return ( <div className="game"> <div className="game-board"> {/* propsにwinLineを追加 */} <Board squares={current.squares} onClick={i => handleClick(i)} winLine={winLine} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); // 以下変更無し これでBoardコンポーネントでwinLineが参照できるようになります。次にBoardコンポーネントを修正します。propsで受け取ったwinLineにindexが含まれるかどうか確認し、結果をSquareコンポーネントに渡します。 Board.js const Board = (props) => { const renderSquare = (i, isHighlight) => { // isHighlightでハイライト有無の情報を受け取る return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} // props追加 isHighlight={isHighlight} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; const isHighlight = props.winLine.includes(index); // winLineにindexが含まれるかどうか確認 rowBoard.push(renderSquare(index, isHighlight)); } squareBoard.push( <div className="board-row" key={'row-' + row} > {rowBoard} </div> ); } // 以下変更無し これでSquareコンポーネントでハイライト有無の情報が参照できるようになります。その情報を用いてハイライトするようSquareコンポーネントを修正します。 Square.js const Square = (props) => { const className = props.isHighlight ? 'square-highlight' : 'square'; return ( <button className={className} onClick={props.onClick}> {props.value} </button> ); }; export default Square; 最後に対応するcssを追加して完了です。 index.css /* 追加 */ .square-highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } 追加課題6 解説 課題6の内容は、どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示するです。次の順番で実装していきます。 calculateWinner関数で引き分けかどうかの情報も返すようにする 引き分けの場合、引き分けのメッセージを表示する calculateWinner関数で引き分けかどうかの情報も返すようにする Game.js const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; const result = { winner: null, winLine: [], isDraw: false // 追加 } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; result.winLine = result.winLine.concat(lines[i]); } } // 勝者が決まっておらず、かつマスが全て埋まっている時が引き分けになる if (result.winner === null && !squares.includes(null)) { result.isDraw = true; } return result; }; export default Game; 引き分けの場合、引き分けのメッセージを表示する Game.js const current = history[stepNumber]; const {winner, winLine, isDraw} = calculateWinner(current.squares); // isDrawも参照できるように書き換え const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } // 引き分けだった場合はstatusを変更 if (isDraw) { status = 'Draw'; } // 以下変更無し これで引き分けの場合はDrawのメッセージが表示されるようになります。 まとめ 以上で全ての実装が終了です。 最後に最終結果のコードがこちらです。 index.js import ReactDOM from 'react-dom'; import './index.css'; import Game from './Game.js'; ReactDOM.render(<Game />, document.getElementById("root")); Game.js import {useState} from 'react'; import Board from './Board'; const Game = () => { const [history, setHistory] = useState( [ { squares: Array(9).fill(null), point: null } ] ); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXIsNext] = useState(true); const [movesOrder, setMovesOrder] = useState(false); const handleClick = (i) => { const copyHistory = history.slice(0, stepNumber + 1); const current = copyHistory[copyHistory.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares).winner || squares[i]) { return; } squares[i] = xIsNext ? "X" : "O"; setHistory( copyHistory.concat( [ { squares: squares, point: i } ] ) ); setStepNumber(copyHistory.length); setXIsNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXIsNext((step % 2) === 0); }; const current = history[stepNumber]; const {winner, winLine, isDraw} = calculateWinner(current.squares); const moves = history.map((step, move) => { const col = step.point % 3 + 1; const row = (step.point / 3 + 1) | 0; const goToMove = 'Go to move #' + move + '(' + col + ', ' + row + ')'; const desc = move ? goToMove : 'Go to game start'; return ( <li key={move}> <button onClick={() => jumpTo(move)} className={move === stepNumber ? 'bold' : ''} > {desc} </button> </li> ); }); if (movesOrder) { moves.reverse(); } let status; if (winner) { status = "Winner: " + winner; } else { status = "Next player: " + (xIsNext ? "X" : "O"); } if (isDraw) { status = 'Draw'; } return ( <div className="game"> <div className="game-board"> <Board squares={current.squares} onClick={i => handleClick(i)} winLine={winLine} /> </div> <div className="game-info"> <div>{status}</div> <button onClick={() => {setMovesOrder(!movesOrder)}}> {'ASK⇔DESK'} </button> <ol>{moves}</ol> </div> </div> ); }; const calculateWinner = (squares) => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; const result = { winner: null, winLine: [], isDraw: false } for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { result.winner = squares[a]; result.winLine = result.winLine.concat(lines[i]); } } if (result.winner === null && !squares.includes(null)) { result.isDraw = true; } return result; }; export default Game; Board.js import Square from './Square'; const Board = (props) => { const renderSquare = (i, isHighlight) => { return ( <Square value={props.squares[i]} onClick={() => props.onClick(i)} key={'index-' + i} isHighlight={isHighlight} /> ); }; const squareBoard = []; const maxRow = 3; const maxCol = 3; for (let row = 0; row < maxRow; row++) { const rowBoard = []; for (let col = 0; col < maxCol; col++) { const index = maxCol * row + col; const isHighlight = props.winLine.includes(index); rowBoard.push(renderSquare(index, isHighlight)); } squareBoard.push( <div className="board-row" key={'row-' + row} > {rowBoard} </div> ); } return ( <div> {squareBoard} </div> ); }; export default Board; Square.js const Square = (props) => { const className = props.isHighlight ? 'square-highlight' : 'square'; return ( <button className={className} onClick={props.onClick}> {props.value} </button> ); }; export default Square; index.css body { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } .board-row:after { clear: both; content: ""; display: table; } .status { margin-bottom: 10px; } .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square__highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; } .bold { font-weight: bold; } .square-highlight { background: yellow; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで現実世界の文字を読み取る方法を調べてみた

機械学習のml5.jsについて学んだので何かを作ろうと思い、普通の温度計のデジタル表示された数字をで読み取るものを作ろうと思い調べました。 ml5.jsが対応しているモデルから探す まずは学びたてのml5.jsで探してみますが……うーん、なさげ、でしょうかね……。「OCR」「character」「number」で検索しましたが見つからず。カメラに映った文字を読み取る機能は需要的にもメジャーどころかと思っていたのですが……。 Teachable Machineでそれぞれの体温の写真を撮って学習させればいけるとは思いますが手間がかかりすぎて本末転倒ですし、なにより狙った体温にできたらお前は人間ではない。 Google先生に訊く 以下のサイトによくまとまっていて助かりました。そこで知ったライブラリについて実装記事などをさらに調べていきます。 tesseract.js デモ用サイトを見る感じなかなかテンションあがる感じです。 「Tesseract.js can run either in a browser and on a server with NodeJS.」とのこと。 ちなみに「人が書いた文字を認識したいときはOCRではなく手書き認識(Hand written recognition)と呼ぶ」という知見を得ました。・・・デジタル表示された数字を読み取りたい場合は? tessedit_char_whitelist で読み取る文字列をホワイトリストで指定できる よく缶コーヒーについてる、数字10桁くらいを手入力するのが面倒なので自動化してみた、という実装例です。これは私も面倒で結局応募してないので素敵です。 文字列ホワイトリストに数字をそれぞれ入れれば認識精度上がるかな。 Tesseractの対応形式は広く、Canvas要素をそのまま投入可能なのも便利なところです。 色々試されています。今回のサンプルコードに使わせてさせていただきます。(- -)(_ _)ペコリ ocrad.js MyScript 有料。 まとめ 評判と実装例の情報量からtesseract.jsを試してみましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Map Reactを簡単に試す

完成形 Google Map React でも紹介されているGoogle Map React。いくつか過去記事がある。 今回使うのは以下。 npm install --save google-map-react サンプル Main.js import React, { Component } from 'react'; import GoogleMapReact from 'google-map-react'; const AnyReactComponent = ({ text }) => <div>{text}</div>; class SimpleMap extends Component { static defaultProps = { center: { lat: 35.66, // 緯度経度 lng: 139.74 }, zoom: 15 }; render() { return ( // Important! Always set the container height explicitly <div style={{ height: '100vh', width: '100%' }}> <GoogleMapReact bootstrapURLKeys={{ key:'{API キーをここに}' }} defaultCenter={this.props.center} defaultZoom={this.props.zoom} > <AnyReactComponent lat={35.667345081692176} lng={139.7401442420512} text="アークヒルズはここ" /> </GoogleMapReact> </div> ); } } export default SimpleMap; トラブルシューティング 依存関係の解決 npm install --save --legacy-peer-deps google-map-react APIキー このページでは Google マップが正しく読み込まれませんでした。JavaScript コンソールで技術情報をご確認ください。 「Google Maps JavaScript API」で「有効にする」ことで、このエラーを抑止して、利用できるようになります。 You must enable Billing on the Google Cloud Project at https://console.cloud.google.com/project/_/billing/enable Learn more at https://developers.google.com/maps/gmp-get-started Zoomとは Zoom は 0 to 18。 ということで超簡単にできた!という以上メモ書きだが、なにがしか参考になればさいわいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ボタンを押した際に値を取得する方法

これも備忘として書いておきます。 test2.html <html> <head> <meta charset="utf-8" /> </head> <body> <button id="button1" name="baka" value="500" onclick="cal()">計算実行</button> <script type="text/javascript"> //ボタン押下時実行 function cal(){ alert(document.getElementById("button1").value*100); } </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptを久しぶりに触ってみました。

html文書にJavaScriptを埋め込んで処理する時の記載方法を備忘で記載しておきます。 次に試したいのは、与えている変数をブラウザから取得する方法です。 test.html <html> <head> <meta charset="utf-8" /> </head> <body> <h1>線</h1> <canvas id="line" width="100" height="100"></canvas> <h1>四角</h1> <canvas id="rectangle" width="100" height="100"></canvas> <h1>円</h1> <canvas id="circle" width="100" height="100"></canvas> <script type="text/javascript"> //読み込み時に実行する onload = function() { /* 線を引く */ var line_canvas = document.getElementById("line"); var line_ctx = line_canvas.getContext("2d"); line_ctx.beginPath(); // 開始位置に移動する line_ctx.moveTo(20, 20); // 線を引く line_ctx.lineTo(80, 80); line_ctx.closePath(); line_ctx.stroke(); /* 四角を描く */ var rect_canvas = document.getElementById("rectangle"); var rect_ctx = rect_canvas.getContext("2d"); rect_ctx.beginPath(); // 四角を描く rect_ctx.strokeRect(20, 20, 60, 60); /* 色の付いた円を書く */ var cir_canvas = document.getElementById("circle"); var cir_ctx = cir_canvas.getContext("2d"); // 塗りつぶす色を指定する cir_ctx.fillStyle = 'rgb(0, 255, 0)'; cir_ctx.beginPath(); // 円を描く位置を決める cir_ctx.arc(50, 50, 40, 0, Math.PI * 2, false); // 実際に円を書く cir_ctx.fill(); } </script> </body> </html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

toio をブラウザ上から簡単制御! p5.js用ライブラリの p5.toio の始め方(p5.js Web Editor上からの利用)

いろいろ前置きや説明を書いているため、「早速試していきたい!」という方は 「p5.js Web Editor で p5.toio を使う」の部分までは飛ばしてしまって、読んでいただけると良いかもしれません。 はじめに この記事は、ブラウザ上から toio を制御する際に、プログラミングの手間をかなり削減してくれる p5.toio に関する記事です。 p5.js Web Editor というオンライン上の環境を使って使う方法を、簡単に紹介します(※ さらに今回の内容の続きの記事も、別途書ければと思ってます)。 自分はこれまで Web Bluetooth API を使った toio用のプログラムを作ってきたのですが、最近ツイートした以下の内容は p5.toio を使って作ってみました。 @tetunori_lego さんが公開されている p5.toio を活用して、1台の円状に動く #toio を残り 5台が追いかけるプログラムを作った!2台バージョンは、ずいぶん前に Web Bluetooth API で自力で実装してたけど、p5.toio を使ったら toio の台数は増えたのにソースコードがめちゃくちゃ短くなった!#p5js pic.twitter.com/J6SqTkSzLO— you (@youtoy) June 1, 2021 この時、以前の Web Bluetooth API を使ったプログラム(例えば、2台での追跡を含むプログラムを作っていました)より格段に短い行数のプログラムになりました。この便利さを、ぜひ p5.js Web Editor の使い方と合わせて紹介したいと思い、この記事を書きました。 余談ですが、↓こちらが 2台で追跡を行う処理を含む Web Bluetooth API を使ったものです。 さらにもう1作品、機械学習 Teachable Machine と開発者向けマット(仮)の組み合わせです。音や声で 2台の #toio を操作します。卓上ベル、「追跡開始」の人声、ハンドベルの音、そして、踏切の音でtoio が動き出したり、追跡が始まったり、止まったりします。#おうちでロボット開発 #toiotomo pic.twitter.com/rTWwueY9ZN— you (@youtoy) May 17, 2020 今回のゴール この記事では、toio 1台だけがあれば試せるプログラムを p5.js Web Editor で作成し、動作確認をするところまで行います。 また、toio用のプログラムだけでなく p5.js Web Editor の使い方も補足しながら進めていきます。 今回の話に出てくるものの補足(toio以外) 今回の記事の内容から考えると、おそらくはこの記事を見る方は「toio とは?」という説明は不要だと思われるため、「p5.toio」と「p5.js」・「p5.js Web Editor」についてだけ補足をしようと思います。 p5.toio とは? p5.toio に関する説明は、作者である @tetunori_lego さんの以下の記事を見ていただくのが良いです。p5.toio を作られた経緯なども書かれています。  ●p5.toioのα版リリースについて - Qiita   https://qiita.com/tetunori_lego/items/23642e50129d934876e5 上記の記事にも書かれている、公式ページはこちらです。  ●toio lib for p5.js | p5.toio   https://tetunori.github.io/p5.toio/ p5.js とは? p5.js は、電子アートとビジュアルデザインのためのプログラミング言語と言われている Processing を JavaScript で扱えるようにしたライブラリです。2D・3D の描画を簡単に行うための処理がいろいろ準備されています。 他の説明として、 https://p5js.jp/ に書かれた説明も掲載しておきます。 p5.js Web Editor とは? p5.js Web Editor は、上記の p5.js を使ったプログラムをオンライン上で書くことができ、さらに作ったプログラムをオンラインに保存しておくこともできたりする環境です(保存のためにはアカウントの作成が必要なのですが、プログラムを書いて動かすだけならアカウントは不要)。 ちなみに、p5.js Web Editor の URL にアクセスすると、以下のような画面が表示されます。 デフォルトで以下のようなプログラムが書かれていて、ここに JavaScript のプログラムを書いていきます。 function setup() { createCanvas(400, 400); } function draw() { background(220); } p5.js Web Editor で p5.toio を使う それでは、p5.js Web Editor で p5.toio を使ったプログラミングを行っていきます。 p5.toio のライブラリを読み込ませる JavaScript のプログラムを書いていく前に、 p5.js Web Editor で p5.toio を使えるようにするための準備をしておきましょう。 まずは以下の部分をクリックしてください。 そうすると、左に「index.html」・「sketch.js」・「style.css」の 3つのファイルのリストが表示されます。デフォルトでは sketch.js が選ばれているので、index.html をクリックしてください。 index.html をクリックすると、JavaScript のプログラムが書かれていた部分(sketch.js の内容が書かれていた部分)が index.html の内容に変わります。 そこで <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script> という p5.sound.min.js を含む行があるので、その後に <script src="https://tetunori.github.io/p5.toio/dist/0.5.0/p5.toio.min.js"></script> という行を追加してください。以下は、記事執筆時点で上記の行追加の対応を行った後の index.html です。 <!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script> <script src="https://tetunori.github.io/p5.toio/dist/0.5.0/p5.toio.min.js"></script> <link rel="stylesheet" type="text/css" href="style.css" /> <meta charset="utf-8" /> </head> <body> <script src="sketch.js"></script> </body> </html> 行の追加が終わったら、以下の ①⇒② を順番にクリックして最初の状態に戻してしまってください。 toio を動かすプログラムを書く 上にリンクを書いた @tetunori_lego さんの Qiita の記事や p5.toio の公式ページの説明を見つつ、以下の処理を行うプログラムを書いてみます。 toio との接続 toio で音を鳴らす toio をその場で回転させる(モーターの制御) 上記の回転動作を停止させる 以下が JavaScript のプログラムです。 const gCubes = []; function setup() { createCanvas(400, 400); } function draw() { background(220); } function mouseClicked() { P5tCube.connectNewP5tCube().then((cube) => { gCubes.push(cube); cube.turnLightOn("white"); }); } function keyPressed() { switch (keyCode) { case LEFT_ARROW: gCubes[0]?.playMelody([ { note: 0x3c, duration: 0x1e }, { note: 0x3e, duration: 0x1e }, { note: 0x40, duration: 0x1e }, ]); break; case RIGHT_ARROW: gCubes[0]?.move(70, -70, 0); break; case DOWN_ARROW: gCubes[0]?.stop(); break; default: break; } } それでは、まずはこのプログラムを動かしてみましょう。 ここで上記を実行する手順を記載してから、その後にプログラムに関する補足を書いていきます。 書いたプログラムを実行する プログラムを実行するには、画面左上の「▶︎ 再生ボタン」を押してください。 その後は、JavaScript のプログラムで書いた「toio との接続」・「音を鳴らす」・「モーターを動かす・止める」といった処理を、マウスのクリック操作・矢印キーの押下で行っていきます。 toio との接続 toio とブラウザの接続に使われる Web Bluetooth API は、プログラムが勝手にデバイスとの接続を行うことができず、ユーザのアクションが必ず必要になります。 その部分について、今回の実装では @tetunori_lego さんのサンプルと同じように、マウスクリックをトリガーにして toio のスキャンを行う仕組みにしました。 以下の枠で囲まれている部分、「プレビュー」のエリアをマウスでクリックしてください(ちなみに、色がついていない部分でも OK です)。 そうすると、以下のような表示が出てくるので、以下の画像の ①⇒② という順番でクリックをしてください。 この操作を行うと toio とのペアリングは完了です。 プログラムで「toio との接続時に、裏のランプを白色で点灯させる」という処理を入れているので、ランプを見ることで処理が完了したことを確認できます。 toio で音を鳴らす・toio のモーターを動かす 上記の toio との接続を終えた後、キーボードの矢印キーを押すことで、以下の動作をさせる処理を実行できます。 左矢印キーを押す toio から音を鳴らす 右矢印キーを押す toio がその場でずっと回転し続ける(モーター制御) 下矢印キーを押す 上記の「右矢印キー押下」で実行したモーター制御を止める 実際に動作をさせた時の様子を、以下に掲載してみます。 p5.toio に関する記事に掲載した #toio 用のプログラムを動作させた時の様子。 pic.twitter.com/6e9vLCtvYg— you (@youtoy) June 6, 2021 動画のとおり、うまく動作させられました。 プログラムに関する補足 プログラムを実行して動作を試していただく手順を書き終えたところで、次にプログラムの補足を書いていきます(上に掲載した Qiita の記事を参照していただく部分もあります)。 toio との接続・ランプの点灯 この部分は、 @tetunori_lego さんが以下に掲載されていたプログラムを使って書きました。 1. Cubeと接続しよう 使用例 1: ランプを点灯する 音を鳴らす 音を鳴らす指示の部分は、 @tetunori_lego さんが以下に掲載されていたプログラムを使っています。 使用例 2: MIDI メロディーを再生する その音を鳴らし始めるトリガーになる処理は、p5.js のキー押下を取得する仕組みを用いました。 以下の p5.js の公式ドキュメントに書かれているもので、押下されたキーの判定には keyCode === LEFT_ARROW) という、左矢印キーに対応する値を指定しています。  ●reference | keyPressed()   https://p5js.org/reference/#/p5/keyPressed その場で回転させる・動きを止める 回転(モーターの制御)と動きの停止について、toio の制御に関しては p5.toio の公式リファレンスで以下に該当するものを用いています。 stop() のほうは、処理を実行するとモーター制御を停止するというもので、パラメーター指定等はありません。 move() のほうは、パラメータとして「左側のモーターのスピード指定」・「右側のモーターのスピード指定」・「モーターを動かす時間の長さ(単位はミリ秒)」の 3つがあり、今回はそれぞれ「70、 -70、 0(※ずっと動き続ける)」という値を使いました。 【公式リファレンス】 move 【公式リファレンス】 stop さらに補足 プログラムの中の「gCubes[0]?.【move等】」という部分の「?.」について、詳細には触れませんが少しだけ補足します(自分用のメモも兼ねて)。 これは「オプショナルチェイニング演算子」 というもので、今回のプログラムでの動作的には「toio との接続がない状態で、toio を制御するための move 等の処理を実行しても、エラーは発生させない(処理としては undefined を返す)」というものです。 詳しく知りたい方は以下のページなどをご覧ください。  ●Optional chaining (?.) - JavaScript | MDN   https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Optional_chaining p5.js Web Editor で便利な操作 最後に、p5.js Web Editor の操作の中で、プログラムを書くときに便利と思われる機能を 1つだけ紹介して説明は終わりにします。 コードの整形 画面上部のメニューから「編集 ⇒ コード整形」を選んでいくか、ショートカットキー(※ 以下の画像で書かれているのは Mac用のものになっています)を押すと、ソースコードのインデント等を自動的に整えてくれる機能があります。 おわりに 今回は p5.toio を使って、p5.js Web Editorの上に書いた toio用のプログラムに関する話を書きました。そして、そのプログラムは、toio 1台だけを使って実行できるものでした。 冒頭に掲載したような複数台を制御するプログラムについては、今回の記事の中で触れられてないので、それについて別途記事を書ければと思っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む