- 投稿日:2019-12-09T23:40:19+09:00
create-react-appを使わずにReactの環境構築をして周辺ツールとかを理解する
Reactに入門したのはいいもののcreate-react-appでプロジェクトを作成すると無駄なファイルが多かったり、環境構築を丸投げしてるのでちょっと色々まずいなと思い、
create-react-appを使わずに環境構築をしていこうと思います。ドキュメントを見ると学習用やちょっと試したいときに最適である的なことが書いてありますね。
以下はこの記事での手順をまとめたスクリプトです。npmが入っている前提ですが、実行ディレクトリにプロジェクトが生成されると思います。
最終的なスクリプト
PROJECT_NAME=myapp echo $PROJECT_NAME mkdir $PROJECT_NAME cd $PROJECT_NAME cat <<EOF > package.json { "scripts": { "dev": "webpack-dev-server --open" } } EOF npm i react react-dom webpack webpack-cli npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom} mkdir dist mkdir src cat <<EOF > dist/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> my-app </title> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html> EOF cat <<EOF > src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; const App = () => <div> hello word </div> ReactDOM.render(<App/>, document.getElementById("root")) EOF cat <<EOF > tsconfig.json { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports" :true } } EOF cat <<EOF > webpack.config.js const path = require('path'); module.exports = { mode: "development", entry: './src/index.tsx', module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: [ '.tsx', '.ts', '.js' ], }, devtool: 'inline-source-map', devServer: { contentBase: './dist', }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), } }; EOF
実行後は以下のようになると思います。
├── dist │ └── index.html ├── package-lock.json ├── package.json ├── src │ └── index.tsx ├── tsconfig.json └── webpack.config.js必要なモジュール
- React/ReactDOM
- webpack
- ソースコードを1つのファイルにまとめるツール。v4 から cli も必須になった。
- webpack-dev-server
- Hotリロード機能。
- トランスパイラ
- babel, ts-loaderとか
- classとかJSXとかトランスパイルしてJavaScriptコードを生成する。
npmでいれる。
npm i react react-dom webpack webpack-clitypescriptを使う場合は型定義とかも
npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom}作成するファイル
- index.html
- index.js
- package.json
- ts-config.json
- webpack.config.js
index.html
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>my-app</title> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html>htmlファイル。webpackでbundleしたファイルを読み込む。
index.ts
src/index.tsx
import React from "react" import ReactDOM from "react-dom" const App = () => <div> hello word </div> ReactDOM.render(<App />, document.getElementById("root"))エントリーファイル。Hello Worldだけ。
tsconfig.json
tsconfig.json
{ "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports": true } }とりあえず公式のと同じです。適宜追加していきます。
webpack.config.js
webpackの設定ファイルです。初見ででかいconfigファイルを見るとビビってしまいますが、最低限必要なのはそこまで多くないです。
https://webpack.js.org/guides/typescript/
https://webpack.js.org/concepts/
webpack.config.js
const path = require("path") module.exports = { mode: "development", entry: "./src/index.tsx", module: { rules: [ { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, devtool: "inline-source-map", devServer: { contentBase: "./dist", }, output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, }mode(dev or prod)と入出力パスとloaderとresolveなんで簡単ですね。
この部分はwebpack-dev-serverの設定です。
devtool: "inline-source-map", devServer: { contentBase: "./dist", },https://webpack.js.org/guides/development/#using-webpack-dev-server
package.json
npm init -y
みたいにすると雛形が生成されますが、最低限ファイルがあればいいようです。echo "{}" > package.jsonnpm i react, react-domあとオプションが微妙ですね。
--save-dev | -D
でdevDependencyに追加
--save | -S
で両方とかですかね??npxはローカルにインストールしたパッケージを実行できる(正確ではないかも)
npx webpack-dev-server --open
npm script
に追記して{ "scripts": { "start": "webpack-dev-server --open" } }npm run startで実行できるようになる。
あとは適当にテストとかlinterとかいれるだけ。
...用語とか言葉とかあやふやなところ適宜修正します。
その他
git
echo "node_modules" > .gitignore git init git add . git commit -m "create project"styled-components
npm i styled-components npm i @types/styled-components
- 投稿日:2019-12-09T23:26:05+09:00
InternetExplorerを拒否するサイトの作り方
InternetExplorer の対応が面倒くさい
私は,HTML・JavaScript の初心者で現在勉強中なのですが,普段は Google Chrome と Edge で動作確認をしており自分の作ったサイトを IE で見てみるとデザインが酷いことになっていました。一応(少なくとも Chrome と Edge では)レスポンシブ対応にしたはずなのですが,色々大きさが変なことになっていました。
ええい!面倒くさい!IE を拒否してしまえ!!ということで,IE を拒否する仕様にしました。
ただし,IE を拒否するとサイトのアクセス数が激減したりユーザーの満足度が下がる可能性があるので注意が必要です。
方法
方法としては,JavaScript を使って,IE でアクセスするとサイトが IE に対応していない旨が書かれたページにリダイレクトされ,Google Chrome のインストールを促すというものです。
IE 判定
IE_boycott.jsvar userAgent = window.navigator.userAgent.toLowerCase(); if(userAgent.indexOf('msie') != -1 || userAgent.indexOf('trident') != -1) { window.location.replace("do_not_use_ie.html"); }これを HTML の中に書き込むか,読み込むかします。
userAgent で IE かどうかを判定し,IE なら強制的に do_not_use_ie.html にリダイレクトしています。
開発者ツールを使って userAgent を書き換えられたらリダイレクト回避されてしまいますが,そんなことをするような人はとっくに別のブラウザに乗り換えているでしょう(たぶん)。
リダイレクト先
正直リダイレクト先は何でもよいですし,直前まで見ていたところに戻すこともできますが,一応メッセージを表示します。
do_not_use_ie.html<h1>Boycott Internet Explorer</h1> <p> このサイトは,Internet Explorer向けには作られていません。Internet Explorerでは,このサイトのレイアウトが最悪な状態になるでしょう。これは,Internet Explorerを考慮せず,最新のブラウザ向けにこのサイトが作られている為です。<br> 正しい状態でこのサイトを閲覧したい場合は他の最新のWEBブラウザを使用してください。このサイトでは,Internet Explorerを完全に切り捨てます。 </p> <p> はっきり言ってしまいましょう。Internet Explorerは,<strong>時代遅れ</strong>です。開発したMicrosoftも使用中止を呼び掛けています。<br> しかし,Internet Explorerを使っている人がなんと多いことか。そのおかげでWEBデザイナーの人たちが非常に苦労し,迷惑しているんです。最新の技術に対応していない時代遅れでも表示できるようにと。<br> 本来,これは無駄な労力です。Internet Explorerを使う人が少しでも減れば,彼らが苦労せずに済みます。しかも,あなたも最新の技術で最高のネットサーフィンができます。<br> Internet Explorerを使い続けることは,デメリットしかないのです。 </p> <p> この際ですから,あなたのせいで苦労している人たちのために,以下のリンクから最新のカッコいいWEBブラウザ「Google Chrome」(無料!シェア率トップ!)をダウンロードしてしまいましょう!!あなたもWEBデザイナーもWin Winです。 </p> <p> <a href="https://www.google.com/chrome/" target"_blank">あなたのせいで困っている人を助ける!</a> </p>超手抜きです。もはや DOCTYPE 宣言もしていません...。適宜書き換えてください。
一番下の「あなたのせいで困っている人を助ける!」というリンクを押すと,Google Chrome のダウンロードページに飛びます。
↓実際の画面です(プライバシー保護のため画像に一部モザイクをかけています)。
手抜き過ぎてなんか怪しいサイトに見えますね...。ということで,IE を拒否するサイトができました。めでたし,めでたし。
- 投稿日:2019-12-09T23:02:31+09:00
案件を受注するとパトランプが回るようにした話
はじめに
この記事はmohikanz Advent Calendar 2019の9日目の記事になります。
昨日はlivaさんの2019年にやったことでした。
https://liva.hatenablog.com/entry/mohikanz-advent-calendar-2019-08この記事はツッコミどころたくさんですが、ネタなのでご容赦いただけるとありがたいです。
動機
ある日会社の営業さんが「受注が決まった時にもっと盛り上がる感じがほしい。そうすればもっと仕事取ってくるしみんなの給料も上がるのに!(意訳)」と言いだしました。困ったことがあればテクノロジーの力でなんとかするのがエンジニアの務め。ここは一肌脱いであげましょう。
構成
弊社では型落ちのMacでオフィスの環境音楽を流しているので、そこにパトランプをつなげてファンファーレっぽい音も流すことにしました。
パトランプのコントロールはちょいオーバースペックですが家で眠っているArduinoがあったのでそれを使うことにします。
Arduinoからの給電だと電力が足らないようなので、外部電源とリレーを利用します。きっとこんな感じMac --- Arduino --- リレー --- パトランプ | 外部電源 ------準備
まずは材料集め。パトランプとリレー、電源を調達します。
パトランプはコレ
12/24V 兼用 フラッシュ ストロボ LED 警告灯 回転灯
リレーはコレ
5V 1チャンネルリレーモジュール
電源はコレ※ジャンパ線が刺さるアダプタが付いてて良きでした
スイッチング式 ACアダプター 12V 1Aパトランプを組み立てる
今回リレーというものを初めて使ってみました。リレーは電気でON/OFFできるスイッチのようなものらしいです。
【参考URL】
リレーをスイッチとして使ってみよう下の画像のように配線して、Arduinoの13番ピンがHIGHの時にパトランプ点灯、LOWで消灯という感じにします。
Macからパトランプを回す
Arduinoはシリアル通信で13番ピンの上げ下げをコントロールするようにします。
Arduinoのスケッチはこんな感じです(汚くてごめんなさい)sketchvoid setup() { // put your setup code here, to run once: pinMode(13, OUTPUT); Serial.begin(9600); while (!Serial) { ; //シリアル通信ポートが正常に接続されるまで抜け出さない } } void loop() { // put your main code here, to run repeatedly: if (Serial.available() > 0) { // read the incoming byte: int incomingByte = Serial.read(); if (incomingByte == 10) { // do nothing } else if (incomingByte == 49) { digitalWrite(13, HIGH); } else { digitalWrite(13, LOW); } } }おもむろに出てくる49は文字コードで"1"かどうか判定してます。
10は改行なので無視します(雑でごめんなさい)
先程のスケッチを流し込んでArduinoを起動したらMacからcat /dev/cu.usb-xxxx & echo 1 | tee /dev/cu.usb-xxxxこんな感じで叩くとパトランプが光ります。わーい。
消す時は1以外を送ってあげれば消えます。受注からパトランプへの流れ
とりあえず光らせることは出来たので、受注 -> パトランプの流れを考えてみます。
大体こういう時ってエッジ側から受注の状態を定期的に監視するのがセオリーなのではと思うのですが、今回は違う方法でやってみたかったので考えてみたところWeb Pushを使う方法を思いつきました。
VAPIDというやつを使えばFCMを使わなくてもプッシュを飛ばせるらしく、受信した後は任意のJSを動かせるので(※後でハマる)パトランプを光らせるのにもってこいです!
【参考URL】
そこそこ小さくまとめたVAPID Web Push通知デモ(Node.js)弊社では案件の状態をSalesforceで管理しているので
1. Salesforce上で案件が受注になる
2. webhook的なやつでPush送信Lambdaをキックする
3. ServiceWorkerでシリアル通信してパトランプする
でイケそうです。Web Push
大体上記の参考URLどおりで通知は受け取れると思います。
今回は相手が一人なので色々乱暴にやりました(ごめんなさい)
ポイントは
- generateVAPIDKeysを一度叩いてメモっておき
- プッシュの購読やServiceWorkerはlocalhostで登録(参考URLのvapid_demo.jsの辺り)
辺りでしょうか。
Lambdaのスクリプトはこんな感じです。handler.js'use strict'; const webPush = require('web-push'); webPush.setVapidDetails( 'mailto:hoge@fuga.piyo', // 第一引数は'mailto:~'というフォーマットでないとだめらしい 'xxx', // メモったVAPIDKeysのpublicKey key 'yyy' // VAPIDKeysのprivate key ); module.exports.push = async (body, context) => { try { await webPush.sendNotification(body, JSON.stringify({ title: 'Web Push!', })); } catch (err) { console.log(err); } return { statusCode: 200, }; }API Gatewayを通してsubscriptionの中身をLambdaに投げると無事Web Pushが届きます。ヨシ!
JSでシリアル通信する
調べてみるとWebUSBというのを使えばシリアル通信できるっぽい。
【参考URL】
WebUSBことはじめ実装してみると確かにJSでパトランプが回ります(毎度汚くてごめんなさい)
serial_demo.js(async _ => { let device; try { device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341, productId: 0x0043 }]}); const device = devices[0]; await device.open(); await device.selectConfiguration(1); await device.claimInterface(1); var result = await device.transferOut(4, Uint8Array.of(0x00, 0x00, 0xff, 0x00, 0xff, 0x00)); console.log(result); } catch (err) { console.log(err); } })(); btnSerialTestON.onclick = async evt => { try { const devices = await navigator.usb.getDevices(); console.log(devices); const device = devices[0]; await device.open(); await device.selectConfiguration(1); await device.claimInterface(1); var result = await device.transferOut(4, Uint8Array.of(0x31, 0x0A)); console.log(result); } catch (err) { // No device was selected. } };あとはServiceWorkerでシリアル通信する処理を呼び出してやれば良い、はずでした…
Web PushからはWebUSBに触れない
そのまんまなのですが、ServiceWorkerの中ではnavigator.usbがnullになります。
このままじゃパトランプが回せない…せっかくプッシュは届くのに…
なんとかしてプッシュを契機にできないかと考えたところ、こんなことを思いつきました。
Web Pushの通信自体をトリガーにすれば良いのでは…!(良い子は真似しないでください)tcpdumpで監視
プッシュを受け取る時の通信をtcpdumpで監視すると、Web Pushは
f188.1e100.net
を含むホストや、名前解決できないホストの5228ポートから飛んでくることがわかりました。
ここまで来たらもう引き返せません。
プッシュの通信(ぽいもの全て)が来たらパトランプを回します。fanfare.sh#!/bin/sh if [ ! -c /dev/cu.usb-xxxx ]; then exit; fi killall cat cat /dev/cu.usb-xxxx & echo "rootのパスワード" | sudo -S tcpdump -i en0 -Alvvp | awk ' BEGIN { run="echo 1 | tee /dev/cu.usb-xxxx; afplay /somewhere/fanfare.mp4; echo 0 | tee /dev/cu.usb-xxxx"; } /(.*.5228|f188.1e100.net).*length (510|280)/ { print | run; close(run); }'【参考URL】
Filtering output of tcpdump and running script when string is found in realtime
ファンファーレっぽい音はafplayコマンドで流します。音と光が揃うと派手めで良きです。↓
↓
↓
↓ 光るので念の為ご注意を
↓
↓
↓
↓
↓
↓
↓まとめ
良い子は真似しないでください。
- 投稿日:2019-12-09T23:00:03+09:00
Three.js の世界へようこそ! 3歩でわかる お手軽3Dプログラミング
Qiita の Advent Calendar も盛り上がっているようですが,本記事は社内の Advent Calendar 用に書きました。
1人でも多くの方に3Dプログラミングの魅力について知って頂けたら嬉しいです。とはいえ,私も専門家ではないので初心者ながら頑張って書いていきたいと思います。3Dに馴染みのない方にもなるべく抵抗がないよう,行列やベクトル,内積・外積のような数学的な話は省いています。というか,私もあんまり分かってません(汗
そういった知識がなくてもある程度なら3Dプログラミングができてしまうのが Three.js の魅力でもあります。Three.js とは
ブラウザ上での 3D 描画を簡単に扱えるようにしてくれる JS ライブラリです。
公式サイト
https://threejs.org/入門サイト(おすすめ)
https://ics.media/tutorial-three/登場人物たちを見てみよう!
~ Three.js の世界に登場する人たちをご紹介するコーナー ~
見てみる
Scene / シーン
- 世界に存在するオブジェクトを格納するところ
- 基準となる座標系を持つ(ワールド座標)
Mesh / メッシュ
- 世界に存在するモノ
- シーンが舞台ならメッシュは役者
- Geometry(形状)と Material(質感)で出来ている
- オブジェクト個々の座標を持つ(ローカル座標)
Geometry / ジオメトリー
- モノの形状を表現するもの
- Box, Circle, Ring, Sphere など様々な形状が用意されている
- vertices(頂点), faces(面)の配列から自由な形状を作ることもできる
Material / マテリアル
- モノの質感を表現するもの
- ライトの影響あり or なし,光沢感のあり or なし,などの種類がある
- 面に Texture(画像)を貼ることもできる
Light / ライト
- 世界を照らす光
- 面の反射や影などに影響を与える
- 平行光源や点光源など,種類によってオブジェクトへの影響が異なる
Camera / カメラ
- 世界を観る者
- 透視投影や平行投影など,種類によって見え方が異なる
- メッシュとライトとカメラの向きや位置関係などでどんな絵になるか決まる
- カメラを動かすことで世界の中での移動や向きの変更を表現できる
Renderer / レンダラー
- 世界を描きだす者
- カメラから見たシーンを1枚の写真のように画像にしてくれる
- アニメーションしたい場合は一定間隔でレンダーする
- WebGLRenderer を使えば,WebGL で GPU を使ったレンダリングができる
Canvas / キャンバス
- 世界の外に結果を伝える者
- HTML要素
- Renderer が描画した画像は Canvas を通して HTML の上に表示される
- HTML側にも座標系がある(スクリーン座標系)
その他
簡単 3 Step で体験してみよう!
~ サンプルを使って Three.js の世界を体験できるコーナー ~
Step1: 物体を表示する
まずは,シンプルな立方体を表示してみましょう。
See the Pen three.js sample step 1 by dsudo (@dsudo) on CodePen.
※ スマホの場合は,Result をクリックしたあと,右下の Rerun をクリックすると結果が表示されます。
こちら からもご覧いただけます。
解説
0.事前に Three.js ライブラリを読み込んでおく必要があります。HTML の Script タグに追加すれば OK です。今回は CDN から取得することにします。
<script src="https://unpkg.com/three@0.111.0/build/three.js"></script>1.HTML に Canvas要素を追加しておき,JavaScript側で取得します。ここに Three.js で描いた結果を表示していきます。ついでに Canvas の幅と高さも取っておきます。これも後ほど使います。
<canvas id="main"></canvas>const canvas = document.querySelector("#main"); const width = canvas.clientWidth; const height = canvas.clientHeight;2.立方体の設置場所としてシーンを作成します。ここから Three.js のオブジェクトを扱っていきます。Three.js のオブジェクトは
THREE
というモジュールに入っています。const scene = new THREE.Scene();3.次に,表示する立方体(メッシュ)を作成します。立方体を表現する
BoxGeometry
と ライトを使わずに発色できるMeshNormalMaterial
を使っていきます。作成したメッシュに位置を設定してシーンに追加します。※今回は話を簡単にするためにライトは使用しません。const box = new THREE.Mesh( new THREE.BoxGeometry(64, 64, 64), new THREE.MeshNormalMaterial() ); box.position.set(0, 0, 0); scene.add(box);4.続いて,カメラを作成します。今回は
PerspectiveCamera
を使います。これは透視投影と言って,人間の目で 3D 世界を見た時と近い遠近感のある見た目にしたいときに使用します。ほかに平行投影(正投影)といってどこからみても同じ大きさに見えるカメラもあります。
視野角,アスペクト比,手前の閾値,奥の閾値を設定します。アスペクト比の計算に Canvas の幅と高さを使います。位置を適当に設定し,lookAt
で注視点を指定します。注視点はカメラがどこを向いているかです。立方体のポジションを指定していて,カメラが立方体の中心に向いている状態になります。(BoxGeometry
の場合,ローカル座標の原点が図形の中心になります。どこが原点になるかは図形によって異なります)const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000); camera.position.set(200, 100, 300); camera.lookAt(scene.position);5.とうとうレンダラーの登場です。もちろん
WebGLRenderer
を使用します。レンダリング結果を表示する Canvas を指定します。指定せずに内部で生成させることもできますが,レンダラーに問い合わせせずに扱えるようにしておいた方が何かと便利なので明示的に指定しています。
antialias: true
を設定しておくと物体の輪郭が滑らかに表示されます。サイズは Canvas の幅と高さを設定します。clearColor
は背景色です。透過させることもできます。pixelRatio
は HiDPI(高画素密度モニター)のときにぼやけるの防ぐらしいです。たしかに指定しないとぼやけました。const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setSize(width, height); renderer.setClearColor(0xd0f0d0); renderer.setPixelRatio(window.devicePixelRatio);6.いよいよレンダリングです。シーンとカメラを指定して,レンダラーの
render
メソッドを実行します。renderer.render(scene, camera);無事に立方体が表示されました?
Step2: 物体を回転する
静止画を表示するだけでは面白くないので,続いて先ほどの立方体を回転させてみましょう。
See the Pen three.js sample step 2 by dsudo (@dsudo) on CodePen.
こちら からもご覧いただけます。
解説
レンダラーの設定までは先ほどと同じです。(あ,微妙に背景色だけ変えてあります)
先ほどは1度レンダリングだけで終わってしまいましたが,アニメーションするためには一定間隔でレンダリングし続けなければなりません。そこで,
renderer.render(scene, camera);
の部分を次のように変更します。const animate = () => { // next frame requestAnimationFrame(animate); // rotate const sec = Date.now() / 1000; box.rotation.x = sec * (Math.PI / 4); box.rotation.y = sec * (Math.PI / 4); box.rotation.z = sec * (Math.PI / 4); // render renderer.render(scene, camera); }; animate();まずは
animate
関数を定義し,requestAnimationFrame
を使って,自分を呼び出すことで永遠に再帰処理をしています。その中でrenderer.render
を呼び出すことで,レンダリングを繰り返し行うことを実現しています。
requestAnimationFrame
はブラウザでアニメーションするときに使う関数で,タブがフォアグラウンドの場合は,およそ 60回/秒 ですが,バックグラウンドの場合は,リソースの消費を抑えるためにより少ない呼び出しになります。物体を動かさないとアニメーションしているか分からないので,
Date.now()
から取得した値を使ってbox.rotation.x/y/z
を設定することで少しずつ向きを変化させています。この辺の設定をいじると色んな動きが試せてStep3: 物体を自在に回転する
クルクル回ったのはいいけど,ただ見てるだけというのも退屈ですね。
Three.js の魅力の1つはインタラクティブな操作に対応させることができるところです。
最後はユーザーの入力(マウスやタッチパッド)で立方体を動かしてみましょう。See the Pen three.js sample step 3 by dsudo (@dsudo) on CodePen.
こちら からもご覧いただけます。
使い方
マウス
ドラッグ:回転
ホイール:拡大・縮小
CTRL+ドラッグ:移動タッチパッド
1本指で動かす:回転
2本指で広げる:拡大
2本指で狭める:縮小
2本指でスライド:移動※ 右下の Rerun ボタン:リセット(CodePen の機能)
解説
まず,立方体を動かしているというのはウソですw
実際にはカメラが動いています。カメラが立方体の周りを回っているので,見ている人からは立方体が回転しているように見えます。コペルニクス的な何かですね。
OrbitControls
というカメラ制御を使用します。こちらは Three.js の本体のライブラリに含まれないため,追加でライブラリを読み込む必要があります。<script src="https://unpkg.com/three@0.111.0/examples/js/controls/OrbitControls.js"></script>これまでのオブジェクトに加えて,
OrbitControls
を追加します。enableDamping
はデフォルト false ですが,true を設定すると,カメラの移動や回転がマウスを離したあとにピタっとは止まらず,なだらかに余韻を残したような動きになります。1var controls = new THREE.OrbitControls(camera, canvas); controls.enableDamping = true;先ほどの
animate
との違いですが,物体を自動的に動かす必要はないので,box.rotation.x/y/z
を設定していた部分を削除しています。代わりにcontrols.update
を呼び出していますが,こちらはenableDamping
かautoRotate
を true に設定している場合は必要になります。デフォルト (false) のままであれば不要です。const animate = () => { // next frame requestAnimationFrame(animate); // required if controls.enableDamping or controls.autoRotate are set to true controls.update(); // render renderer.render(scene, camera); }; animate();あと,地味に Canvas がアクティブになって枠線が強調されてしまうので,CSS に
outline: none;
を追加してエフェクトを無効化します。ということで,簡単に3Dプログラミングができましたね?
おしまい
どうでしたか?3Dの魅力を体験して頂けたでしょうか?
私も5年ぶりに Three.js を触ってみましたが,やっぱり3Dは楽しいですね!もっと色んなことを知りたい方は公式のドキュメントやサンプルをご覧頂くとさらに奥深い Threejs の世界へと誘われること請け合いです。
https://threejs.org/examples/#webgl_animation_cloth本記事を書くにあたってコチラのサイトにお世話になりました。とても分かりやすくて参考になります。
https://ics.media/tutorial-three/それから,Three.js Advent Calendar もあるので要チェックですね。
https://qiita.com/advent-calendar/2019/threejs
カメラが真上と真下を向いたときにそれ以上いかなくなるのは,ジンバルロックという現象を防ぐために制御してるためだと思われます。この言葉を覚えておくと,どっちを向いたらいいか分からなくなったときなどに「ジンバルロックが起きたんだな」といった 3D ギャグが使えるようになります。 ↩
- 投稿日:2019-12-09T22:43:57+09:00
three.jsで一筆書きのlineを作る
作ったもの
アイディアを考え中 ??#webgl #threejs pic.twitter.com/o3sWjJXNkj
— yukidoke (@snowdoke) March 7, 2018初めに
Three.js Advent Calendar 2019の10日目の記事です。
今回、ご紹介するのはSVGをJSONに変換して、一筆書きのlineを作ろうというものです。
少し古くなってしまうのですが、
three.js r92
blender 2.79
を使用しています。まず、blenderでSVGをインポートします。
ファイル > インポート > .svgその際、インポートしたSVGはカーブになっていますので、SVGを選択した状態でAlt + Cを押して、メッシュに変換してください。
次は、インポートしたSVGをJSONにエクスポートします。
ファイル > エクスポート > .json
デフォルトでは、JSONのエクスポートはないため、アドオンを追加する必要があり、こちらの記事が参考になります。3Dデータ(Blender)をThree.jsで表示するまでの方法
JSONをエクスポートする際のオプションですが今回は頂点情報のみを使用するので、
Geometry
タイプ: geometry
index Buffer: なし
エクスポートするもの 頂点にのみチェックを入れる、でやっています。コード
頂点座標を配列に追加する関数を書きます。
function addVertexBuffer(geometry, index, x, y, z){ var index = index*3; var array = geometry.attributes.position.array; array[index] = x; array[index+1] = y; array[index+2] = z; };描くlineの設定を書きます。
function drawDynamicLine(){ var geometry = new THREE.BufferGeometry(); // JSONの読み込み var request = new XMLHttpRequest(); request.open("GET", "think.json", false); request.send(null); var json_l = JSON.parse(request.responseText); var positions = new Float32Array(maxpoints * 3); geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); var material = new THREE.LineBasicMaterial({color: 0xffd700}); var line = new THREE.Line(geometry, material); var points = new Float32Array(json_l["vertices"]); for(var i=0; i<maxpoints; i++){ var x = points[i*3+0]; var y = points[i*3+1]; var z = points[i*3+2]; addVertexBuffer(line.geometry, i, x, y, z); } line.position.z = 0.3; scene.add(line); }繰り返す関数を書きます。
function animate() { line.geometry.setDrawRange( 0, drawCount ); if(drawCount > maxpoints) drawCount = maxpoints; drawCount += 2; line.geometry.attributes.position.needsUpdate = true; requestAnimationFrame(animate); renderer.render(scene, camera); }Sceneなどを設定します。
function setup() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 100); var ambientLight = new THREE.AmbientLight(0xffffff); scene.add(ambientLight); var directionalLight = new THREE.DirectionalLight(0xffffff); directionalLight.position.set(1, 1, 1).normalize(); scene.add(directionalLight); renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); drawDynamicLine(); } setup(); animate();最後に
個人の感想ですが、lineが時間の経過とともに描かれていくのは、とても面白いです。
僕が作ったものでは、lineがZ軸上は移動していないので、Z軸も移動させるとまた違うものができてくると思います。
- 投稿日:2019-12-09T22:33:33+09:00
年末まで毎日webサイトを作り続ける大学生 〜52日目 JavaScriptで神経衰弱ゲームを作る〜
はじめに
こんにちは!@70days_jsです。
今日は神経衰弱を作りました。
全部クリアすると、なんと!
最後にとっておきの画像を見ることができます!
(大声では言えないですが、思わず興奮してしまう画像です...)ぜひやってみてください。
ちなみにバグもありまして、素早く連続でクリックすると何枚もカードが開けてしまうのでご了承ください。直す時間がありませんでした。
今日は52日目。(2019/12/9)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day52.html
やったこと
ほぼ全てJavaScriptで作りました。一部cssのアニメーションも使っています。
のでhtmlは何もなし↓<body></body>
JavaScript長いですが一応全て載せます↓
window.onload = function() { createCardWrapper(); //wrapperを作る createCard(); //cardを作る }; let divWrapper; //一番大枠のdiv let countDisplay; let countDisplaySpan = document.getElementById("countDisplaySpan"); let card = [ { 0: "" }, { 1: "" }, { 2: "" }, { 3: "" }, { 4: "" }, { 5: "" }, { 6: "" }, { 7: "" }, { 8: "" }, { 9: "" }, { 10: "" }, { 11: "" }, { 12: "" }, { 13: "" }, { 14: "" }, { 15: "" } ]; //それぞれのcardにつけるid let image = ["1", "2", "3", "4", "5", "6", "7", "8"]; //cardの画像名 let imageNumber = 0; let beforeCard = ""; //前にクリックしたcard let id; //クリックしたcardのid(=key) let countClick = 2; //1回目か2回目かの判断 let goal = 0; //全て当てたかどうかの判断 function clicked(e) { open(e); //cardクリック時のモーション successOrFailure(e); } //cardクリック時のモーション function open(e) { id = e.target.id; e.target.classList.add("click-none"); e.target.classList.add("card-rotate"); delayImageDisplay(1000).then(function() { e.target.style.backgroundImage = "url(day52/" + card[id][id] + ".png)"; e.target.classList.remove("card-rotate"); }); } function successOrFailure(e) { if (countClick % 2 == 0) { //1回目 countClick++; countDisplaySpan.innerHTML = countClick - 2 + ", 奇数, 前回のカード" + beforeCard + ",ポイント: " + goal; beforeCard = id; } else { //2回目、かつ成功 if (card[beforeCard][beforeCard] === card[id][id]) { console.log("success"); countDisplaySpan.innerHTML = countClick - 2 + ", 偶数, 前回のカード" + beforeCard + ",ポイント: " + goal; beforeCard = id; countClick++; goal += Number(card[id][id]); if (goal === 36) { setTimeout(goaal, 2000); } } else { //2回目、かつ失敗 e.target.classList.add("card-rotate"); countClick++; countDisplaySpan.innerHTML = countClick - 2 + ", 偶数, 前回のカード" + beforeCard + ",ポイント: " + goal; delayImageDisplay(1000) .then(function() { e.target.classList.remove("card-rotate"); }) .then(function() { let before = document.getElementById(beforeCard); beforeCard = id; setTimeout(function() { before.style.backgroundImage = "url(day52/day52_card.png)"; e.target.style.backgroundImage = "url(day52/day52_card.png)"; console.log(before); console.log(e.target); e.target.classList.remove("card-rotate"); e.target.classList.remove("click-none"); before.classList.remove("click-none"); }, 1000); }); } } } //image配列の順番をシャッフルする function shuffle() { for (var i = image.length - 1; i > 0; i--) { let random = Math.floor(Math.random() * (i + 1)); let tmp = image[i]; image[i] = image[random]; image[random] = tmp; } } //一番大枠のwrapperを作成する関数 function createCardWrapper() { countDisplay = document.createElement("div"); countDisplaySpan = document.createElement("span"); countDisplaySpan.setAttribute("id", "countDisplaySpan"); countDisplay.innerHTML = "クリック回数: "; countDisplay.appendChild(countDisplaySpan); document.body.appendChild(countDisplay); divWrapper = document.createElement("div"); divWrapper.setAttribute("id", "divWrapper"); document.body.appendChild(divWrapper); } //cardを16枚作る関数 function createCard() { shuffle(); //画像をシャッフルする for (var i = 0; i < card.length; i++) { let div = document.createElement("div"); let key = Object.keys(card[i]); //a,b..pと順番に入る div.setAttribute("id", key); card[i][key] = image[imageNumber]; //cardと画像が結びついた(画像は2枚ずつ) div.setAttribute("class", "card"); // div.style.backgroundImage = "url(day52/" + image[imageNumber] + ".png)"; if (imageNumber >= 7) { imageNumber = 0; shuffle(); //画像をシャッフルする } else { imageNumber++; } divWrapper.appendChild(div); div.addEventListener("click", clicked); } console.log(card); } //画像の表示を遅らせるための関数 function delayImageDisplay(delay) { return new Promise(function(resolve) { setTimeout(resolve, delay); }); } function goaal() { goalDiv = document.createElement("div"); goalImg = document.createElement("img"); goalDiv.innerHTML = "おめでとうございます!"; goalImg.setAttribute("class", "goalImg"); goalDiv.setAttribute("class", "goalDiv"); goalImg.setAttribute("src", "day52/secret.jpg"); goalDiv.appendChild(goalImg); document.body.appendChild(goalDiv); }cssも一応全て載せておきます。↓
body { margin: 0; height: 100vh; display: flex; justify-content: center; align-items: center; flex-direction: column; } img { max-width: 100%; max-height: 100%; } #divWrapper { width: 600px; height: auto; background-color: rgba(80, 80, 200, 0.3); display: flex; justify-content: center; align-items: center; flex-wrap: wrap; } .card { display: inline-block; width: 130px; height: 130px; background-color: rgba(50, 50, 50, 0); /* border: solid 1px black; */ margin: 1%; background-image: url("day52/day52_card.png"); background-size: cover; } .card:hover { opacity: 0.2; } .card-rotate { animation: rotate; animation-duration: 1s; } @keyframes rotate { 0% { transform: rotateY(0deg); } 100% { transform: rotateY(92deg); } } .click-none { pointer-events: none; } .goalDiv { position: absolute; background-color: black; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; color: white; flex-direction: column; font-size: 3em; } .goalImg { display: inline-block; }肝になる部分
ちょっと今回は長いので大事なところだけ説明していきます。
let card = [
...この変数↑は配列ですが、その中身はhash形式で情報を保存しています。↓
{ 0: "" },
{ 1: "" },
{ 2: "" },
...神経衰弱のカードにはそれぞれidが割り振られており、このhashのキー(0~15)はそのidと全て対応しています。
このhashはのちにvalueに1~8の値を持ちます。
この数字は画像の名前と一致しており、それぞれ2回ずつhashに割り当てることで対になる2枚のカードを実現しています。
これがhashのvalueに入る数字です。↓let image = ["1", "2", "3", "4", "5", "6", "7", "8"]; //cardの画像名
このままでは順番通りに画像が入ってしまうので、image配列の中身をシャッフルします。↓
function shuffle() {
for (var i = image.length - 1; i > 0; i--) {
let random = Math.floor(Math.random() * (i + 1));
let tmp = image[i];
image[i] = image[random];
image[random] = tmp;
}
}promiseを使う
ちなみに、今回promiseも少し使ってみました。
やっぱりまだ理解しきれていないですが。↓function delayImageDisplay(delay) {
return new Promise(function(resolve) {
setTimeout(resolve, delay);
});
}function open(e) {
id = e.target.id;
e.target.classList.add("click-none");
e.target.classList.add("card-rotate");
delayImageDisplay(1000).then(function() {
e.target.style.backgroundImage = "url(day52/" + card[id][id] + ".png)";
e.target.classList.remove("card-rotate");
});ただ、しっかり順番通りに動いてくれているので、なんとなく便利だということは分かりました。
いずれちゃんと理解しようと思います。ゴールページについて
神経衰弱を全てやり終えたら思わず興奮してしまう画像を用意しています。
変数を用意します。↓
let goal = 0;
カードが当てることができたら、goal変数に画像の値分(0~8)数値を足します。↓
goal += Number(card[id][id]);
画像はは0~8なので、全て足し合わせると36になります。36になると関数を実行するようにしています。↓
if (goal === 36) {
setTimeout(goaal, 2000);
}
} else {...関数の中身です。secret.jpgが例の画像です。↓
function goaal() {
goalDiv = document.createElement("div");
goalImg = document.createElement("img");
goalDiv.innerHTML = "おめでとうございます!";
goalImg.setAttribute("class", "goalImg");
goalDiv.setAttribute("class", "goalDiv");
goalImg.setAttribute("src", "day52/secret.jpg");
goalDiv.appendChild(goalImg);
document.body.appendChild(goalDiv);
}secret画像は国宝級の画像を用意しました。
皆さんもぜひ試してみてください!(アプリでは全て見れます)↓
感想
ちょっと今日は説明が意味不明すぎることになっていると思います。
申し訳ありません。というのも、手当たり次第に作っていたら色々と複雑になってしまい、全てを言語化していたら膨大な量になってしまうと判断したからです。
今後は最初にきちんと設計して、なるべくシンプルなコードを書けるように尽力します最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。
参考
- アイコン素材ダウンロードサイト「icooon-mono」 | 商用利用可能なアイコン素材が無料(フリー)ダウンロードできるサイト | 6000個以上のアイコン素材を無料でダウンロードできるサイト ICOOON MONO
- パブリックドメインQ:著作権フリー画像素材集
画像を使用させていただきました。
- 投稿日:2019-12-09T21:37:54+09:00
クリスマスの聖地を人工衛星から捉える
Ateam Lifestyle Advent Calendar 2019 の12日目は株式会社エイチームライフスタイルでWebエンジニアをしている @water_resistant が担当します。
はじめに
12月といえばクリスマスがあり、街中光がキラキラして楽しくなる季節ですね。
ただクリスマスは1日しかないので、どうせなら一番キラキラしている場所(聖地と定義)で楽しみたいかと思います。
今回はそんな人のためにIT技術を使って文字通り最高にキラキラしている場所を探していきます。使ったツール
今回は一番キラキラしている場所を探すために、Google Earth Engineを使いました。
GoogleEarthEngineとは
公式ページの紹介をみてみます
Google Earth Engineは、衛星画像と地理空間データセットの数ペタバイトのカタログと惑星規模の分析機能を組み合わせて、科学者、研究者、開発者が変化を検出し、傾向を地図化し、地球表面の差異を定量化できるようにします
(Google翻訳済)つまり衛星画像をデータソースとして様々な分析をできるプラットフォームというわけですね。
今回はこのEarthEngineをキラキラスポット探索の為に利用します。GooleEarthEngineを使う準備
SingUpフォームにメールアドレスなどの情報と使用用途を記載するだけです。
今回の目的には「明かりをたくさん使っている=電気を沢山使っている場所を調査する」ためと記述。
1日程度時間が空いてから登録完了のメールがきました。
これを受け取った後でないとAPIを呼び出しても認証エラーになります。↓
実はこれだけで衛星画像へのアクセスや加工が簡単にできるようになります。
すごいですね。今回は触るのが初めてということもあるので、APIではなくIDEからおこなってみます。
今回使用する人工衛星データ
IDEを表示できたので、キラキラスポットを探すために利用するデータを探します。
DataCatalogで「light」と検索をしたところ、いくつかデータを発見。
今回の用途に適してそうなデータ↓
VIIRS Stray Light Corrected Nighttime Day/Night Band Composites Version 1
- 衛星名称
- 使用機材
- Visible/Infrared Imager and Radiometer Suite(VIIRS) / マルチチャンネルイメージャ・放射計
規約についても問題なさそうなのでこれに決定。
NOAAのデータ、情報、および製品は、配信方法に関係なく、著作権の対象ではなく、その後の一般の使用に対する制限はありません。一度取得すると、それらは合法的に使用できます。前述のデータはパブリックドメインにあり、使用と配布に制限なく提供されています。
(Google翻訳済)データを絞り込むための事前準備
キラキラスポットを探すためには、市区町村(スポット)で区切られたデータで絞り込む必要があります。
そのために行政区域データをダウンロードしてEarthEngineに取り込みます。規約的にも問題なく信用できそうなデータが政府統計GISデータダウンロードにあったので今回はこれを利用。
(いい感じのサイトが他にありましたが 規約的に今回の用途では無理そうなので断念)あとはデータをダウンロードしてからEarthEngineのIDEからアップロードするだけです。
データが多すぎるのも困るので今回は愛知県のデータのみアップロードします。 (文字コードのみ注意)
これでプログラムから「区画区切りデータ」を利用することができるようになりました。
実際にデータを取得する
公式ドキュメント などを見ながらコードを書いていく
今回は2014年~2018年の年別データを取得して、光量が多いところや増加傾向のところをキラキラスポットとする。
// 衛星データのBAND: avg_radから日付を指定してImageCollection取得 var y_2014 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMCFG').select('avg_rad').filter(ee.Filter.date('2014-12-01T00:00:00+09:00','2014-12-01T23:59:59+09:00')).median(); var y_2015 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMCFG').select('avg_rad').filter(ee.Filter.date('2015-12-01T00:00:00+09:00','2015-12-01T23:59:59+09:00')).median(); var y_2016 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMCFG').select('avg_rad').filter(ee.Filter.date('2016-12-01T00:00:00+09:00','2016-12-01T23:59:59+09:00')).median(); var y_2017 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMCFG').select('avg_rad').filter(ee.Filter.date('2017-12-01T00:00:00+09:00','2017-12-01T23:59:59+09:00')).median(); var y_2018 = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMCFG').select('avg_rad').filter(ee.Filter.date('2018-12-01T00:00:00+09:00','2018-12-01T23:59:59+09:00')).median(); // IDE上のMap表示のオプション var nighttimeVis = {min: 0.0, max: 60.0, opacity: 0.8}; Map.setCenter(136.8809966,35.174377); Map.setZoom(10); // IDE上のMapにデータを表示 Map.addLayer(y_2014.clip(aichi_pref_shape), nighttimeVis, '[YEAR] 2014'); Map.addLayer(y_2015.clip(aichi_pref_shape), nighttimeVis, '[YEAR] 2015'); Map.addLayer(y_2016.clip(aichi_pref_shape), nighttimeVis, '[YEAR] 2016'); Map.addLayer(y_2017.clip(aichi_pref_shape), nighttimeVis, '[YEAR] 2017'); Map.addLayer(y_2018.clip(aichi_pref_shape), nighttimeVis, '[YEAR] 2018'); // データ集約方法定義 var reducers_fnc = ee.Reducer.mean().combine({ reducer2: ee.Reducer.minMax(), sharedInputs: true }).combine({ reducer2: ee.Reducer.median(), sharedInputs: true }); // データ集約 function reduce_collection(image) { return image.reduceRegions({ collection: aichi_pref_shape, reducer: reducers_fnc, scale: 500 }) } // データ出力タスク定義 (GoogleDrive出力) function export_table(table, description) { return Export.table.toDrive({ collection: ee.FeatureCollection(table), folder: 'EarthEngineExport', description: description, selectors: (["KEY_CODE", "CITY", "CITY_NAME", "S_NAME", "JINKO", "SETAI", "mean", "max", "min"]) }) } // 定義ファイルを参考 // https://www.e-stat.go.jp/gis/statmap-search/data?datatype=2&serveyId=A002005212015&downloadType=1 // データ出力タスク呼び出し export_table(reduce_collection(y_2014), 'stray_light_2014'); export_table(reduce_collection(y_2015), 'stray_light_2015'); export_table(reduce_collection(y_2016), 'stray_light_2016'); export_table(reduce_collection(y_2017), 'stray_light_2017'); export_table(reduce_collection(y_2018), 'stray_light_2018');上記コードを実行すると、愛知県区画に絞られたデータが結果に出てきます。↓
あとはIDEの「Task Run」からデータ出力用の関数を実行します。
GoogleDriveに格納されているのを確認できました。↓
IDEはグラフィカルに見ることができるのがいいですが、定量的に測る際は適切ではないので今回はCSV出力してそこでデータを比較することにしました。
いざデータ加工
出力されたCSVファイルを結合して年別推移を見られるようにしたのが下のキャプチャです。
CAGRでソートしてみたところ、「岡崎市 宮石町」が2016年から4倍跳ね上がっていたので調べてみます。
該当住所をGoogleMapで調べたところ、エリア内にSA(サービスエリア)を見つけました。↓
NEXCO中日本、2016年2月13日15時にオープンする新東名「岡崎SA」
なるほど面白いですね。ただCAGRランキング上位を見ても、私が求めているキラキラとは少し違うので、別の観点「過去5年間の平均値」で見ていきます。
上位5件はこうなりました。
- 中区 錦3丁目
- 中区 新栄町1丁目
- 中区 栄4丁目
- 一宮市 丹陽町三ツ井字平山
- 中区 新栄町2丁目
中区 錦3丁目 中区 新栄町1丁目 中区 栄4丁目 一宮市 丹陽町三ツ井字平山 中区 新栄町2丁目 「栄」や「錦」が出ることは予想してましたが、、 4位は完全に予想外です。何か変な建物が見えますが...。
(実は一宮は4位だけでなく、それ以降もそれなりにでてきます。)ただひょっとしたらキラキラスポット(聖地)の答えがここにあるかもしれないと思ったので少し探ります。
衛星の分解能が400m-800mで、区間分割がうまく行かずに明るくなっていると思われるので
該当の住所をEarthEngine側で拡大してみましょう。IC(インターチェンジ)近くが無茶苦茶明るい様子。
確かに豪華絢爛なお城が多いですもんね。
知りたくなかった。まとめ
GoogleEarthEngineは、水温や気温を始めとして凄く沢山のデータがあります。
例えば下のようなデータについては図書館に行かずともPCの前に居ながら出すことができます。
- 森林がどれだけ減っているのか
- 気温/海温はどれだけ変動しているのか
使い方は簡単ながらも、非常に多くを知ることができるので、自分の視点で地球を一度見てみるのも面白いかと思います。
(本来はその用途のツールです)あとクリスマスに向けてのキラキラスポット(聖地)調査は全くもって役に立たなかった。
Ateam Lifestyle Advent Calendar 2019 の 13日目は、 @poncoがお送りします!
私のネタエントリとは違う、しっかりした記事を書いてきてくれることでしょう!そんな”挑戦”を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
今回のコード
今回作成したデータやコードへのリンクを貼っておきます。
(EarthEngineはアカウント登録してないと見れないです。)参考・データ元
- 投稿日:2019-12-09T21:00:05+09:00
React初心者がHookを使ってTo Doリストを作ったらこうなった
目次
- 初めに
- 完成品
- 前提条件
- 各コンポーネントについて
- Appコンポーネント
- Headerコンポーネント
- Formコンポーネント
- ToDoListコンポーネント
- 最終的なコード
- 参考
初めに
React Hooks Advent Calendar 2019 10日目の記事を書かせていただきました。
まだReactを勉強し始めて間もないですが、やはりTo Doリストは一度作っておくべきだろうと思って色々Qiitaを漁っていてあることに気が付きました。
「React Hookを使って実装している人、ほとんどいないな...」と(私が見てないだけだと思います)。
そこで私はReact Hookの一つ、useStateを使ってTo Doリストを作りました。変数名とか結構ガバガバだと思います。
React初心者ですがお叱りなどあったら甘んじて受け入れる所存です。よろしくお願いします。※React Hooks Advent Calendar 2019 7日目の@daishiさんの記事では私とは別のやり方でTo Doリスト作ってますね。
完成品
完成品がこちらです。Netlifyでデプロイもしてます。→ To Do List Used By Hooks
私はブラウザはなんとなくVivaldi使っているのでVivaldiでアクセスした画面になります。スタイルはほぼ全部
Bootstrap 4
に投げました。コードはGitHubに投げました。
https://github.com/koralle/todo-usedby-hook
前提条件
- Windows 10
- node.js v12.13.1
- yarn 1.19.2
- Bootstrap 4
- create-react-app 3.2.0
各コンポーネントについて
App
を親コンポーネントにして、画面上からHeader
、Form
、ToDoList
で構成しました。Appコンポーネント
return
の中の不自然に見える<div>
タグはBootstrap
であれこれするためにこうなってます...App.jsimport React, {useState} from 'react'; import './App.css'; import Header from './Header'; import Form from './Form' import ToDoList from './ToDoList' const App = () => { // ToDoリストのStateをtoDoListと定義 const [toDoList, setToDoList] = useState([]); // toDoListに項目を追加 const addToDoList = (Title, Content) => { setToDoList(toDoList.concat({"title": Title, "content": Content})); } // toDoListの項目を削除 const deleteToDoList = (index) => { setToDoList(toDoList.filter(item => toDoList[index] !== item)); } return ( <div> <Header headerTitle="To Do List Used By Hooks"/> <div> <div> <Form add={addToDoList}/> <ToDoList list={toDoList} delete={deleteToDoList}/> </div> </div> </div> ); } export default App;Headerコンポーネント
header
タグをrenderしているだけなので,あまり書くことないです。Header.jsimport React from 'react'; import './Header.css' const Header = (props) => { return ( <header> <h1>{props.headerTitle}</h1> </header> ); } export default Header;Formコンポーネント
このコンポーネント、できればステートレスにしたかったんですけど上手くいきませんでした。
AppコンポーネントからaddToDoList()
をpropsとして引き渡してます。Form.jsimport React, {useState} from 'react'; import './Form.css' const Form = (props) => { const [toDoTitle, setToDoTitle] = useState(""); const [toDoContent, setToDoContent] = useState(""); // Titleフォームの状態の制御 const handleToDoTitleInputChange = (e) => { setToDoTitle(e.target.value); } // Contentフォームの状態の制御 const handleToDoContentInputChange = (e) => { setToDoContent(e.target.value); } // 入力フォームのクリア const resetInputField = () => { setToDoTitle(""); setToDoContent(""); } // 項目の追加を確定 const callAddToDoList = (e) => { e.preventDefault(); props.add(toDoTitle,toDoContent); resetInputField(); } return ( <form> <div> <div> <span>Title</span> </div> <input type="text" onChange={handleToDoTitleInputChange} value={toDoTitle} /> </div> <div> <div> <span>Detail</span> </div> <textarea onChange={handleToDoContentInputChange} value={toDoContent} ></textarea> </div> <div> <button type="submit" onClick={callAddToDoList} > ADD </button> </div> </form> ); } export default Form;ToDoListコンポーネント
AppコンポーネントのStateである
toDoList
と項目を削除する関数deleteToDoList()
をpropsとして引き渡しています。ToDoList.jsimport React from 'react'; import './ToDoList.css' const ToDoList = (props) => { // AppコンポーネントのStateであるtoDoListをpropsとして受け取って // mapでループする const toDoListItems = props.list.map( (item, i) => { return ( <div key={i} > <div> <h5>Title: {item.title}</h5> <p>Content: {item.content}</p> <button onClick={() => props.delete(i)} > Delete </button> </div> </div> ); } ); return ( <div> <h1>Your Tasks: {props.list.length}</h1> <div> {toDoListItems} </div> </div> ); } export default ToDoList;最終的なコード
上のコードに
Bootstrap4
でスタイルを付ける為にclassNameをつけ足していった結果が以下のコードになります。
(CSSファイルは割愛しました。)./public/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <title>To Do List used by Hooks</title> </head> <body> <div id="root"></div> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> </body> </html>App.jsimport React, {useState} from 'react'; import './App.css'; import Header from './Header'; import Form from './Form' import ToDoList from './ToDoList' const App = () => { const [toDoList, setToDoList] = useState([]); const addToDoList = (Title, Content) => { setToDoList(toDoList.concat({"title": Title, "content": Content})); } const deleteToDoList = (index) => { setToDoList(toDoList.filter(item => toDoList[index] !== item)); } return ( <div className="toDo-app"> <Header headerTitle="To Do List Used By Hooks"/> <div className="toDo-app-body container"> <div className="toDo-main"> <Form add={addToDoList}/> <ToDoList list={toDoList} delete={deleteToDoList}/> </div> </div> </div> ); } export default App;Header.jsimport React from 'react'; import './Header.css' const Header = (props) => { return ( <header className="toDo-header"> <h1>{props.headerTitle}</h1> </header> ); } export default Header;Form.jsimport React, {useState} from 'react'; import './Form.css' const Form = (props) => { const [toDoTitle, setToDoTitle] = useState(""); const [toDoContent, setToDoContent] = useState(""); const handleToDoTitleInputChange = (e) => { setToDoTitle(e.target.value); } const handleToDoContentInputChange = (e) => { setToDoContent(e.target.value); } const resetInputField = () => { setToDoTitle(""); setToDoContent(""); } const callAddToDoList = (e) => { e.preventDefault(); props.add(toDoTitle,toDoContent); resetInputField(); } return ( <form className="toDo-form"> <div className="toDo-form-title input-group"> <div className="input-group-prepend"> <span className="input-group-text">Title</span> </div> <input type="text" className="form-control shadow" onChange={handleToDoTitleInputChange} value={toDoTitle} /> </div> <div className="toDo-form-detail input-group"> <div className="input-group-prepend"> <span className="input-group-text">Detail</span> </div> <textarea className="form-control shadow" aria-label="Detail" onChange={handleToDoContentInputChange} value={toDoContent} ></textarea> </div> <div className="toDo-form-add"> <button className="btn btn-success" type="submit" onClick={callAddToDoList} > ADD </button> </div> </form> ); } export default Form;ToDoList.jsimport React from 'react'; import './ToDoList.css' const ToDoList = (props) => { const toDoListItems = props.list.map( (item, i) => { return ( <div key={i} className="card toDo-item"> <div className="card-body"> <h5 className="card-title">Title: {item.title}</h5> <p className="card-text">Content: {item.content}</p> <button className="btn btn-danger" onClick={() => props.delete(i)} > Delete </button> </div> </div> ); } ); return ( <div className="toDo-List"> <h1>Your Tasks: {props.list.length}</h1> <div> {toDoListItems} </div> </div> ); } export default ToDoList;参考
To Doリストのベースはこちらの記事で学ばせてもらいました。
Formコンポーネントを作る際にかなり参考にしました。
(2020年のフロントエンドマスターになりたければこの9プロジェクトを作れで紹介されていました)。
- 投稿日:2019-12-09T20:31:43+09:00
jQueryプラグインの実践
jQueryプラグインの実践をしてみた
こんにちは。ちょっとずつ学習したことを実践して投稿します。
やったこと
jQueryとjavaScriptを使って、
1. 画像をクリックしたら、そのフォントサイズを表示する。
2. ボタンをクリックしたらテキストを取得し表示するコード
部分的にコードを抜粋します。
index.htmlに画像などいろいろと要素を埋め込んでいきます。
jquery.showsize.jsの方へ画像をクリックした際の動作を埋め込んでいき、外部ファイルとして取得します。index.html<button type="button" name="button"> <p><img src="img/hoge.jpg"></p> <div id="cat1-button"><p id="text-a">click</p></div> </button> <button type="button" name="button"> <p><img src="img/hogehoge.jpg"></p> <div id="cat2-button"><p id="text-b">click</p></div> </button> <button type="button" name="button"> <p><img src="img/hogehogehoge.jpg"></p> <div id="cat3-button"><p id="text-c">click</p></div> </button> <button type="button" name="button"> <p><img src="img/hogehogehogehoge.jpg" data-size="30"></p> <div id="cat4-button"><p id="text-d">click</p></div> </button> </div> </ul> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> <script src="jquery.showsize.js"></script> <script> $(function(){ $('img').showsize({ }); }); { document.getElementById("cat1-button").onclick = function() { document.getElementById("text-a").innerHTML = "hoge"; }; } { document.getElementById("cat2-button").onclick = function() { document.getElementById("text-b").innerHTML = "hogehoge"; }; } { document.getElementById("cat3-button").onclick = function() { document.getElementById("text-c").innerHTML = "hogehogehoge"; }; } { document.getElementById("cat4-button").onclick = function() { document.getElementById("text-d").innerHTML = "hogehogehogehoge"; }; } </script> </div>jquery.showsize.js;(function($) { $.fn.showsize = function(options) { var elements = this; elements.each(function() { var opts = $.extend({}, $.fn.showsize.defaults, options, $(this).data()); $(this).click(function() { var msg = $(this).width() + ' x ' + $(this).height(); $(this).wrap('<div style="position:relative;"></div>'); var div = $('<div>') .text(msg) .css('position', 'absolute') .css('top', '0') .css('background', 'black') .css('color', getRandomColor()) .css('font-size', opts.size + 'px') .css('opacity', opts.opacity) .css('padding', '2px'); $(this).after(div); }); }); return this; }; function getRandomColor() { var colors = ['white', 'skyblue', 'orange', 'green']; return colors[Math.floor(Math.random() * colors.length)]; } $.fn.showsize.defaults = { size: 10, opacity: 0.9 }; })(jQuery);後記
複数のscriptが続く際には、{}で括るという初歩的なことが大事だと痛感しました。
このコードをサンプルとしてご自由にご利用ください。
- 投稿日:2019-12-09T20:20:23+09:00
はじめてぶらうざののべるげーむをつくってみたの!
はじめてぶらうざののべるげーむをつくってみたの!
自分でノベルゲーを作りたくなったので、モチベ維持のためにもQiita記事にしました。
ゆくゆくは簡単にブラウザノベルゲームを作れるソフトでも作りたいなと思ってるので、その下調べ的ななにがしですね。Qiita記事は初心者なので、なんか書いた方がいいこととかあれば教えてくだせぇ。
それではさっそく行きましょう。Chapter 0.使用言語
- HTML
- CSS
- JavaScript(JQuery)
Chapter 1.とりあえずタイトル画面つくるかのぅ…。
タイトル画面がないと始まりません。
1.仮でとりあえず作っていきます。
html<body> <h1>ノベルゲーム!</h1> <a href="">続きから</a> <a href="">最初から</a> <a href="">環境設定</a> </body>css*{ text-decoration:none; color:black; } h1{ background-color:rgba(255,0,0,.3); } a{ display:block; background-color:rgba(0,255,0,.3); }必要なものはこれくらいでしょうか。
これをもとにCSSでデザインを作っていきますが、上の通りbackground-color:rgba(??,??,??,.3);
などと背景色をつけるとどの要素がどこにいるかわかりやすいですね。2.タイトルとメニューは中央寄せにしたいですね。
そこで、
text-align:center;
をh1とaに対して指定します。
また、それぞれのwidthを10emにしました。
この時点では、h1要素とa要素の中身は中央寄せになりましたが、a要素自体は左に寄っています。これを真ん中に持ってくるためにdisplay:flex;
を召喚します。html<div id="felxcontainer"> <h1>ノベルゲーム!</h1> <a href="">続きから</a> <a href="">最初から</a> <a href="">環境設定</a> </div>css#flexcontainer{ display:flex; flex-flow: column nowrap; align-items:center; position:absolute; top:0;bottom:0;left:0;right:0; }3.背景がなんか寂しいなぁ…。
というわけでネットのフリー画像を背景に指定。
cssbody{ background:url("image/TopImage.jpg"); margin:0; padding:0; position:absolute; top:0; bottom:0; left:0; right:0; }全体的に上に寄っているので、h1とaの親要素(#flexcontainer)のcssに、
justify-content:center;
を追加します。すると以下の通り全体が真ん中に来ます。
4.あとはこまごま調整して…
html<body> <div id="flexcontainer"> <h1>ノベルゲーム!</h1> <div> <a href="">続きから</a> <a href="">最初から</a> <a href="">環境設定</a> </div> </div> </body>css*{ text-decoration:none; color:black; } body{ background:url("image/TopImage.jpg"); margin:0; padding:0; position:absolute; top:0; bottom:0; left:0; right:0; } #flexcontainer{ display:flex; flex-flow: column nowrap; align-items:center; justify-content: center; position:absolute; top:0;bottom:0;left:0;right:0; } #flexcontainer div{ border:2px solid white; border-radius:5px; } h1{ margin:0; color:white; font-size:11vw; text-align: center; } a{ color:white; display:block; text-align:center; font-size:2.5vw; line-height:2em; width:10em; } a:hover{ background-color:rgba(255,255,255,.5); }根気が残っていれば次回以降はゲームの中身の部分を作っていく予定です。
参考サイト
- PAKUTASO(ここのフリー画像を使いました。)
- 投稿日:2019-12-09T19:29:02+09:00
無名再帰 (JavaScript)
Z コンビネータ?いえ、知らない子ですね。1
const fix = f => f = f((...x) => f(...x));使い方
// 5 の階乗 console.log("fact 5 =", fix(rec => n => n < 1 ? 1 : n * rec(n - 1))(5) ); // > fact 5 = 120再帰するのに、一々変数に入れたり、
function
にしたりしたくない時に。以下応用例
トランポリン
トランポリンと組み合わせてスタック溢れを回避してみる。
トランポリン自体の解説は先人がいらっしゃるので、そちらに丸投げします。// トランポリン化 const trampoline = f => (...x) => { for (let y = f(...x); ; y = y()) if (typeof y !== 'function') return y; }; // 0 から n の合計 const sum = trampoline(fix(rec => (n, s = 0) => n < 1 ? s : () => rec(n - 1, s + n) )); console.log("sum 100000 =", sum(100000)); // > sum 100000 = 5000050000メモ化
// メモ化, 1 引数のみ const memo = f => ((map, setter) => x => map.has(x) ? map.get(x) : setter(map, x, f(x))) (new Map(), (m, k, v) => (m.set(k, v), v)); // メモ化再帰でフィボナッチ数 const fib = fix(rec => memo( n => n < 2 ? n : rec(n - 1) + rec(n - 2) )); // ローカルなメモにするなら const fibL = n => fix(rec => memo( n => n < 2 ? n : rec(n - 1) + rec(n - 2) ))(n); console.log("fib 50 =", fib(50)); // > fib 50 = 12586269025
冗談はさておき、素直に Z コンビネータ実装してしまうと最適化が悲しいことになることが多いので。 ↩
- 投稿日:2019-12-09T18:06:42+09:00
意外と間違えるVue.jsのバージョン確認方法
- 投稿日:2019-12-09T16:49:31+09:00
Firefox で mozCaptureStream() すると音が出なくなる
WEB ページ上で Video や Audio を扱うことができる HTMLMediaElement があり、
captureStream()
メソッドを使用することでメディアをキャプチャできます。Chrome 等では実際に音をスピーカー等から再生しながらキャプチャできるのですが、Firefox ではキャプチャすると音が出なかった (内部的には再生されていてキャプチャはできる) ため、メモしておきます。
もともと Firefox では
captureStream()
の実装が仕様に追いついておらず、moz
ベンダー接頭辞が付いていますが、MDN にはこの音が出なくなる仕様に関しては書かれていませんでした。参考「HTMLMediaElement.captureStream() - Web API | MDN」
1. 再現コード
Chrome 77 と Firefox 71 で動作確認。
(Chrome ではセキリティの制限で index.html を file スキームで開くと cross-origin data としてブロックされるため、
php -S 0.0.0.0:8080
などでローカルサーバーを立てるなどしてください。)index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>mozCaptureStream() バグ</title> <script src="main.js"></script> </head> <body> <video id="video" src="video.webm" controls></video> <input type="button" value="mozCaptureStream()" onclick="TEST.captureStream();"> </body> </html>main.js(()=>{ const TEST = {}; TEST.captureStream = () => { const videoElement = document.getElementById('video'); return ( 'captureStream' in videoElement ) ? videoElement.captureStream() : videoElement.mozCaptureStream(); }; window.TEST = TEST; })();これはバグとして Bugzilla に投げられていました。
参考「1178751 - Calling mozCaptureStream on an HTMLMediaElement should not destroy the AudioSink」
2. 解決策
理想的なことを言えば Firefox のコードを修正すべきですが、ここでは JavaScript 側で対処します。
main.js (Firefox 対応版)(()=>{ const TEST = {}; TEST.captureStream = () => { const videoElement = document.getElementById('video'); let stream; if ( 'captureStream' in videoElement ) { stream = videoElement.captureStream(); } else if ( 'mozCaptureStream' in videoElement ) { stream = videoElement.mozCaptureStream(); // ★ const audioContext = new AudioContext(); const mediaStreamSource = audioContext.createMediaStreamSource(stream); mediaStreamSource.connect(audioContext.destination); // } else { console.error('Unsupported: captureStream()'); } return stream; }; window.TEST = TEST; })();
mozCaptureStream()
するとHTMLMediaElement
に接続されているAudioContext
で音声が再生できなくなる。- 別途新しく
AudioContext
を作成する。- キャプチャーしたストリームからオーディオソースを作成し、
AudioContext.destination
1 に接続する。キャプチャーしたストリームは、既に音量
HTMLMediaElement.volume
を反映しているので、GainNode
に接続するような記述は不要です。ただし、上記の対処法では他のバグ 1443511 の影響を受けます。
参考「1443511 - A small delay occurs when the volume is changed on Soundcloud」(「Soundcloud でボリュームが変更されると、わずかな遅延が発生します」2 )
- 投稿日:2019-12-09T16:45:57+09:00
CSSサブグリッドで真のフレキシブルなレイアウトを実現する方法
2019年12月3日、Firefox71がリリースされ、このバージョンから CSS Subgrid が使用できるようになりました。
CSS Subgridは2016年来からW3CのWorking Draftによって勧告されていましたが、今回のFirefoxのアップデートによって遂にユーザーへ提供できるようになりました。
この記事では、CSS Subgridの何が我々をワクワクさせ、どのようにインタフェースの実装を変化させてくれるのかについて紹介したいと思います。CSS Subgrid とは
CSS Gridのおさらい
CSS Subgridの説明の前に、簡単にCSS Gridのおさらいをします。
CSS Gridとは、2次元レイアウトをCSSを用いて簡潔に組むための仕組みを指します。
任意の要素にdisplay: grid;
を適用することで、以下の画像のように対象の要素はグリッドコンテナーとして、グリッドコンテナーの子要素はグリッドアイテムとして扱われます。
<ul style="display: grid;"> <!-- グリッドコンテナー --> <li>ぺんぎん</li> <!-- グリッドアイテム --> <li>あざらし</li> <!-- グリッドアイテム --> <li>らっこ</li> <!-- グリッドアイテム --> </ul>CSS Gridの問題点
CSS Gridを用いることで、グリッドアイテムの高さを柔軟に揃えることが可能となりました。
では、我々の戦いはGridの登場によって終わりを迎えたのでしょうか。そんなことはありません。グリッドアイテムの中の要素に目を向けるとどうでしょう。
グリッドアイテムの中には画像やテキスト等の複数の要素を設ける必要があります。これらのテキストはコンテンツによって変化するため、実装時には様々な高さへ変化することを想定しなくてはいけません。しかしながら、グリッドアイテムは以下の画像のように、中の要素の高さまでは揃えることができません。
CSS Subgridの登場
この問題を解決してくれるのがCSS Subgridです。
詳細な実装方法は後述しますが、グリッドアイテム要素に対してgrid-template-columns: subgrid;
やgrid-template-rows: subgrid;
を指定することで、CSS Subgridを有効化させることができます。
有効化させることで、グリッドアイテムの中の要素をサブグリッドアイテムとして扱うことができ、アイテム毎に高さを可変させることが可能となります。以下の例ではSubgridを用いてタイトル部分の高さを可変させています。Subgrid未適用時は高さがコンテンツ毎に異なっているのに対し、Subgrid適応時は高さが統一されてるかと思います。
未適用時 適用時 (Firefox 71) Subgridを自在に操ることで、真のフレキシブルなレイアウトを簡潔に実現することが可能となります。
対応環境 (2019/12/07現在)
現在はFifefox 71~ のみ対応しています。
Subgrid | Can I Use...より使い方
サンプル
See the Pen Sample of CSS Subgrid by oreo (@oreo) on CodePen.
(Firefox 71~で確認してみてください。)
実装方法
Subgridは以下のソースコードで対応することが可能です。
<ul class="gridContaienr"> <li class="gridItem"> <img src="https://via.placeholder.com/150" alt=""> <h3>タイトル</h3> <p>テキスト</p> <a href="http://example.com/">リンク</a> </li> <li class="gridItem"> <img src="https://via.placeholder.com/150" alt=""> <h3>タイトル</h3> <p>テキスト</p> <a href="http://example.com/">リンク</a> </li> <li class="gridItem"> <img src="https://via.placeholder.com/150" alt=""> <h3>タイトル</h3> <p>テキスト</p> <a href="http://example.com/">リンク</a> </li> </ul>.gridContainer { display: grid; grid-template-columns: repeat(3, minmax(200px, 1fr)); grid-gap: 1em; } /* 以下の記述を追加 */ .gridContainer .gridItem { display: grid; grid-row: span 4; grid-template-rows: auto auto 1fr auto; /* fallback for non-supported browsers */ grid-template-rows: subgrid; }まずは
.gridItem
へdisplay: grid;
を指定することでグリッドコンテナーとして扱う必要があります。
同時に、grid-template-rows: subgrid;
を指定することで、グリッドコンテナーをサブグリッド化することができます。こちらの例ではSubgridに対応していないブラウザ用のフォールバックとして、
grid-template-rows: auto auto 1fr auto;
を指定しています。
可変させる要素と固定させる要素を明示することで、Subgridに対応していないブラウザでも最低限のレイアウトを担保できるので、未サポートブラウザが多い現在はこちらの記述も必須でしょう。終わりに
CSS SubgridはFlexやGridだけでは実現できなかったレイアウトを簡潔に実現できる力を秘めていると考えられます。
現在はFirefoxのみが対応していますが、今後より多くのメジャーなブラウザへの実装され、ユーザーへ提供できるようになることを願ってやみません。参考サイト
- 投稿日:2019-12-09T16:40:47+09:00
【AfterEffects×Web】作った動画素材をWebで使い隊!!
はじめに
僕の暇つぶしは動画を作ることで、
日頃から「動画とプログラミングで何かできないかな・・・」
って考えてた時にとあるライブラリを発見しました。その名は・・・・・「Lottie」
本日はこのライブラリを使ってAeで作った動画をブラウザに表示させる方法を紹介していきます。
Lottieとは
Lottieとは、Airbnbから登場したWeb、iOS、 Android、React Native対応のアニメーションライブラリです。
After Effectsで表示できるアニメーションをリアルタイムでレンダリングし、簡単にアニメーションを作成することができます。・公式ページ: https://airbnb.design/lottie/
使い方
まずはBodymovinをインストールします。
(補足:BodymovinとはAeの拡張機能であり、Aeで作ったアニメーションデータをJSONファイルとして書き出してくれる優れものです)・github: https://github.com/airbnb/lottie-web
・Creative Cloud: https://exchange.adobe.com/creativecloud.details.12557.bodymovin.html色々インストール方法はありますが、僕はAdobeストアからインストールしました。
インストールしたらAeの拡張機能にBodymovinが追加されます。
Bodymovinを起動させると作った動画の名前が表示されます。
以下の画像だとComp1と表示されてます。
次に歯車マークのSettingを押すとずらっと設定の項目が表示されます。
項目の詳細下記のような感じです。
Split : json ファイルを指定した秒数で分割する Glyphs: フォントをシェイプ化する(基本的にこれを選択でOK) Hidden : 非表示レイヤーも出力する Guides : ガイドレイヤーを出力する Extra Comps : エクスプレッション外部ファイル読み込み? Assets : 画像ファイル出力の詳細設定を行える Standalone : json を含んだ js ファイルを出力する Demo : テスト/確認用の HTML を出力する AMD : XML を出力するGlyphsにチェックを入れSaveを押します。
Comp1を選択しディレクトリの場所が設定できたら、
Renderを押すと・・・data.jsonというファイルが作成されます!これで準備はOK!
ブラウザに表示させよう
<body> <div class="lottie"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.5.3/lottie.min.js"></script> <script src="js/sample.js"></script> </body>表示させる要素にclassまたはidをつける。
LottieのCDNを記述する。
(CDNはgithubに掲載されてます)HTMLの記述はこれだけ!
JSの記述は以下になります。
bodymovin.loadAnimation({ container: document.querySelector('.lottie'), renderer: 'svg', //'svg' / 'canvas' / 'html'の中から選ぶことができます。 loop: true, //ループ再生するのかを選びます(true/false) autoplay: true, //自動再生するかどうかを選択します(true/false) path: 'json/data.json' //jsonまでのパスを記述します。出力したjsonの名前は適宜変える必要があります。 });querySelectorでclassを取ってきてます。
あと今回はSVGでrenderしてほしいので設定はSVGで記述しています。
loopなど不要であればfalseにしてあげると一回だけしか再生されないでお好みでどうぞ。
この設定が出来たら完成!!ブラウザを開くと、、あら不思議!!
作った動画が表示されています。
JSとかCSSでゴリッゴリにコーディングせずとも、
動画素材さえあれば簡単にブラウザに落とし込めます!素敵!!メリットとデメリット
メリット
・簡単に複雑なアニメーションが作れる
・JSONデータを使用するのでGIFなどと比べるとかなり軽い
・SVGでレンダリングされるので拡大しても綺麗デメリット
・現状実現できない動作はある
(使えないエフェクト多すぎて暴れた)
・画像をそのまま使うことが出来ないので使用する場合はAi等でパス化しないといけない
・結局色んなツールを使うことになるまとめ
サクッとアニメーションを作るにはめちゃくちゃいいツールではないかなと思いました。
また、現状まだサポートしていないエフェクトはありますが、使えるエフェクトがまだまだ増えていくみたいなので期待してます!
- 投稿日:2019-12-09T16:39:25+09:00
フロントエンド2年目のエンジニアが再帰処理を必要にかられて使えるようになったお話
パーソンリンクアドベントカレンダー 10日目 & Qiita初投稿です!
エンジニアになって2年目のhanetukiです。
現在は、フロントエンドメインで開発して飯を食べてます。そんな私の備考録です。出会いは突然に
[ { name: "タカシ", children: [ { name: "ミツル" }, { name: "ナツキ", children: [ { name: "アイナ" }, { name: "ナオキ" } ] }, { name: "ヨツハ" }, { name: "フタバ", children: [ { name: "リク" } ] } ] }, { name: "ソラ", children: [ { name: "ハナ" } ] } ]上記のような階層構造を処理する際に、ユニークになるkeyが一つも存在しないので何か付与する必要がありました。
ユニークになるkey無いと、対象となる値の更新や参照が困難になる為です。JavaScriptでオブジェクトデータを扱う(登録・参照・更新・削除)
(...こんな感じのことをやるのにユニークになるkeyが必要だった)そのときたどり着いたのが親子関係がはっきりしているパス情報を作成することでした。
よしメソッド作ろう!と意気込んだものの...階層構造のネストがどこ続いていなかったりどこまでも続いていたり、
この手のJSONを処理するのは初めてでした。(入れ子 尽きるまで object js)検索
我ながら頭の悪そうな検索ワードでGoogle先生に尋ねたところ
再帰関数 なるものに出会いました。再帰関数ってなんなんだ??
再帰関数を学ぶと、どんな世界が広がるかつまりはこうだ、
// 関数をconstで定義する const func = function(count) { console.log(`console: ${count}`); if (count) { // 関数の中でconstで定義したfuncを呼び出す。 func(count--); } }; // 初期値を引数に渡す func(10); // 結果 // console: 10 // console: 9 // console: 8 // console: 7 // console: 6 // console: 5 // console: 4 // console: 3 // console: 2 // console: 1 // console: 0関数の中でその関数を呼び出すことでループをすることができるそうだ。スゲぇー
着手してみた
早速コードを書いて、usersの各値にpathを埋め込んでいきましょう。
// 上記のJSONを変数で定義します。。 const users = [ { name: "タカシ", children: [ { name: "ミツル" }, { name: "ナツキ", children: [ { name: "アイナ" }, { name: "ナオキ" } ] }, { name: "ヨツハ" }, { name: "フタバ", children: [ { name: "リク" } ] } ] }, { name: "ソラ", children: [ { name: "ハナ" } ] } ] // 関数funcを定義 const func = (users, path=[]) => { users.forEach((user, i) => { // userにpathを追加 user.path = [...path, i]; if (user.children) { // user.childrenがあれば関数を再び呼び出す func(user.children, user.path); } }); }; func(users); console.log(users); // 結果 // [ // { name: "タカシ", // path: [ 0 ], // children: [ // { // name: "ミツル", // path: [ 0, 0 ] // }, // { name: "ナツキ", // path: [ 0, 1 ], // children: [ // { // name: "アイナ", // path: [ 0, 1, 0 ], // }, // { // name: "ナオキ", // path: [ 0, 1, 1 ], // } // ] // }, // { // name: "ヨツハ", // path: [ 0, 2 ], // }, // { // name: "フタバ", // path: [ 0, 3 ], // children: [ // { // name: "リク", // path: [ 0, 3, 0 ] // } // ] // } // ] // }, // { // name: "ソラ", // path: [ 1 ], // children: [ // { // name: "ハナ", // path: [ 1, 0 ] // } // ] // } // ]※図解
タカシ, path: [ 0 ]
┗ ミツル, path: [ 0, 0 ]
┗ ナツキ, path: [ 0, 1 ]
┗ アイナ, path: [ 0, 1, 0 ]
┗ ナオキ, path: [ 0, 1, 0 ]
┗ ヨツハ, path: [ 0, 2 ]
┗ フタバ, path: [ 0, 3 ]
┗ リク, path: [ 0, 3, 0 ]
ソラ, path: [ 1 ]
┗ ハナ, path: [ 1, 0 ]引数にusersの配列とそれまでのpathを渡して各userごとにpathの埋め込んでいくという作業を再帰的に行なっています。
pathに関してはforEachのindexをaddする形で使っています。
user.path = [...path, i];
は引数pathを スプレッド構文 を利用して展開して利用しています。
スプレッド構文はシンタックスシュガーです。// スプレッド構文を用いない場合 user.path = path; user.path.push(i); // スプレッド構文を用いた場合 user.path = [...path, i];この二つは同じ結果になります。詳しくは、@Nossaさんがまとめてくれている記事がありますのでそちらをご覧ください。
最後に
再帰関数をある程度利用することでjsを扱う上での視野が広がった気がします。
また、記事に間違いや不明な点があれば遠慮なくご指摘いただけますと幸いです✧。٩(ˊωˋ)و✧*。明日は、@kuwakuwakuwaさん担当です!
- 投稿日:2019-12-09T16:34:40+09:00
最小構成のWebXR AR
WebXR AR Paint Advent Calendar3日目です(チガウ)。
前回のエントリでWebXR AR Paintのソースコードについて雑に説明しました。ただ、改めて見ると、WebXRにとってはあまり本質ではない部分の説明が長くなってしまっていて混乱させるだけだったかもしれません。ということで、今回WebXR AR Paintのコードの
TubePainter
関連のコードをばっさり削って最小構成にしてみました。https://technohippy.github.io/webxr-ar-sample/
画面をタップすると、その時点での端末の位置と向きにあわせて円錐が表示されます。
<!DOCTYPE html> <html lang="en"> <head> <title>three.js ar</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <link type="text/css" rel="stylesheet" href="main.css"> </head> <body> <script type="module"> import * as THREE from './jsm/three.module.js'; import { ARButton } from './jsm/ARButton.js'; let camera, scene, renderer; let controller; init(); animate(); function init() { // 1. レンダラーのalphaオプションをtrueにする renderer = new THREE.WebGLRenderer({antialias: true, alpha: true}); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // 2. レンダラーのXR機能を有効にする renderer.xr.enabled = true; document.body.appendChild(renderer.domElement); // 3. AR表示を有効にするためのボタンを画面に追加する document.body.appendChild(ARButton.createButton(renderer)); scene = new THREE.Scene(); let light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1); light.position.set(0, 1, 0); scene.add(light); camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20); window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }, false); // 4. XRセッションにアクセスするためコントローラーを取得する controller = renderer.xr.getController(0); controller.addEventListener('selectend', () => { controller.userData.isSelecting = true; }); } function makeArrow(color) { const geometry = new THREE.ConeGeometry(0.03, 0.1, 32) const material = new THREE.MeshStandardMaterial({ color: color, roughness: 0.9, metalness: 0.0, side: THREE.DoubleSide }); const localMesh = new THREE.Mesh(geometry, material); localMesh.rotation.x = -Math.PI / 2 const mesh = new THREE.Group() mesh.add(localMesh) return mesh } function handleController(controller) { if (!controller.userData.isSelecting) return; const mesh = makeArrow(Math.floor(Math.random() * 0xffffff)) // 5. コントローラーのposition, rotationプロパティを使用して // AR空間内での端末の姿勢を取得し、メッシュに適用する mesh.position.copy(controller.position) mesh.rotation.copy(controller.rotation) scene.add(mesh) controller.userData.isSelecting = false } function animate() { renderer.setAnimationLoop(render); } function render() { handleController(controller); renderer.render(scene, camera); } </script> </body> </html>要するに以下のような処理を追加すればARモードを実装できます。
- レンダラーのalphaオプションをtrueにする
- レンダラーのXR機能を有効にする
- AR表示を有効にするためのボタンを画面に追加する
- XRセッションにアクセスするためコントローラーを取得する
- コントローラーのposition, rotationプロパティを使用してAR空間内での端末の姿勢を取得し、メッシュに適用する
割と簡単にARアプリが実装できて楽しいので、ぜひ試してみてください。
- 投稿日:2019-12-09T16:23:23+09:00
現在地座標をWebページの地図の上に表示する
学園祭に限らずイベントではマップが大事です。当然ながらWebサイトにも載せることになるでしょう。
まあそのまま画像のマップを載せてもいいですが、せっかくプログラミングができるのだから色々工夫してみたいものです。というわけで、今回はマップ上に現在位置を表示してみます。実装例
Geolocation API
JavascriptのGeolocation APIを用いれば、現在の位置情報の緯度と経度を取得できます。
navigator.geolocation.watchPosition((pos) => { // 処理 }, (err) => { // エラー処理 }, { enableHighAccuracy: true, maximumAge: 0 });詳しい仕様はGeolocation.watchPosition() - Web API | MDNを見てほしいですが、ざっくり説明すると、watchPositionは順に「成功時のコールバック」「エラー時のコールバック」「オプション」の3つを引数に取る関数で、オプションのプロパティは以下のものがあります。
プロパティ名 説明 enableHighAccuracy
位置情報の精度を良くする。少しバッテリーが犠牲になる。 timeout
今回は省略。タイムアウト時間(ミリ秒)。 maximumAge
一度位置情報を取得したあと、その位置情報を維持する時間(ミリ秒) 成功時のコールバック関数の引数
pos
の中のオブジェクトpos.coords
に位置情報のデータが入っています。pos.coords
の中身のプロパティには以下のようなものがあります。
プロパティ名 説明 coords.latitude
緯度 coords.longitude
経度 coords.accuracy
精度。単位はm。間違っててもまあ半径この範囲にはいるだろうというもの。地図アプリで現在地に薄く出るあの薄い円。 coords.heading
方角。北を0度として時計回り。単位は度。 ここから緯度と経度が取得できます。
座標変換
緯度経度を、地図上の座標に変換します。
tl; dr
基準として地図上の二点を適当に選ぶ。ただし、計算を簡単にするために、両者のy座標は等しくなるように取る。
2点の地図上の座標を$(x_1,y_1),(x_2,y_1)$、緯度と経度を$(a_1,b_1),(a_2.b_2)$とすると、緯度経度が$(a,b)$である任意の点の地図上の座標$(x,y)$は、
$$
\left(
\begin{array} \\
x \\
y \\
\end{array}
\right)
=L
\left(
\begin{array} \\
\cos \theta & -\sin \theta \\
\sin \theta & \cos \theta \\
\end{array}
\right)
\left(
\begin{array} \\
(b-b_1)\cos a_1 \\
a-a_1 \\
\end{array}
\right)
+
\left(
\begin{array} \\
x_1 \\
y_1 \\
\end{array}
\right)
$$
ただし、
$$
L=\frac{x_2-x_1}{\sqrt{(a_2-a_1)^2+((b_2-b_1)\cos a_1)^2}} \\
\theta=\mathrm{atan2}(a_1-a_2, (b_2-b_1)\cos a_1)
$$
ちなみに$\mathrm{atan2}$は、
$\theta=\mathrm{atan2}(r\sin \theta, r\cos \theta)$ ($r$は任意の実数)
を満たす関数で、Javascriptにも実装されています。詳しくはMath.atan2() - JavaScript | MDNを見てください。証明
緯度経度の直交座標化
冷静に考えるとわかるのだが、経度1度あたりの距離は、緯度によって変わる(緯度1度あたりの距離はどこでも一定)。
経度1度あたりの長さ$lon$を、緯度一度あたりの長さ$lat$で表すと、
$$
lon = 経度\times\cos(緯度)\times lat
$$
である。簡単な図形の問題。
ちなみに、この計算式を用いて書いた世界地図の図法をサンソン図法と呼ぶ。この地図を見るとわかるが、基準(緯度経度ともに0度)から遠く離れたところほど歪んでしまう。
そこで、地図プログラムでは基準を0度ではなく、マップの内部のどこか(例えば東京大学では安田講堂や1号館)にすることで歪みを最小限にする。
以降、緯度1度あたりの長さを1緯度長と呼ぶことにする。座標の変換
座標の変換のために、地図上の1本のベクトルについて基底の変換を考える。簡単のため、地図の座標軸に平行な基準を考える。例えば、東京大学の駒場1号館の一番下の辺にしよう。
1号館の左下の地図上の座標を$(x_1,y_1)$、緯度経度を$(a_1,b_1)$とし、右下の地図上の座標を$(x_2,y_1)$、緯度経度を$(a_2,b_2)$とする。
この2つの点の地図上の$y$座標は等しいことに注意する。また、$x$方向が経度であることにも注意する。
すると、
$$
A
\left(
\begin{array} \\
(b_2 - b_1)\cos(a_1) \\
a_2 - a_1
\end{array}
\right)
=
\left(
\begin{array} \\
1 & 0 \\
0 & 1 \\
\end{array}
\right)
\left(
\begin{array} \\
x_2 - x_1 \\
0
\end{array}
\right) (Aは行列)
$$
ただし、$\cos(a_2) \approx \cos(a_1)$として$b_2\cos(a_2) - b_1\cos(a_1) \approx (b_2 - b_1)\cos(a_1)$とした。
$A$はよくわからないので、これを求めていきたい。ここで、緯度長の座標系は直交座標であり、1度あたりの長さを揃えてあるから、
$$
A=
L
\left(
\begin{array} \\
\cos \theta & -\sin \theta \\
\sin \theta & \cos \theta
\end{array}
\right)
(L \in \mathbb{R})
$$
と書ける(回転と定数($L$)倍)。代入して、計算すると、
$$
L
\left(
\begin{array} \\
(b_2 - b_1)\cos(a_1)\cos \theta - (a_2-a_1)\sin \theta \\
(b_2 - b_1)\cos(a_1)\sin \theta + (a_2-a_1)\cos \theta
\end{array}
\right)
=
\left(
\begin{array} \\
x_2 - x_1 \\
0
\end{array}
\right)
$$
$L$はベクトルの長さの変化の割合なので(回転では大きさが変わらないため)、
$$
L=\frac{x_2 - x_1}{\sqrt{(a_2 - a_1)^2 + ((b_2 - b_1)\cos(a_1))^2}}
$$
行列の下側の式より
$$
\tan\theta= -\frac{a_2 - a_1}{(b_2 - b_1)\cos(a_1)}
$$
よって、三角関数の公式より
$$
\cos\theta=\pm\frac{(b_2 - b_1)\cos(a_1)}{\sqrt{(a_2 - a_1)^2 + ((b_2 - b_1)\cos(a_1))^2}} \\
\sin\theta=\mp\frac{a_2-a_1}{\sqrt{(a_2 - a_1)^2 + ((b_2 - b_1)\cos(a_1))^2}} \\
(複号同順)
$$
となるが、行列の上の式に$L$も含めて代入すると、符号が決まり、コサインがプラス、サインがマイナスである。ゆえに、
$$
\theta=\mathrm{atan2}(a_1-a_2, (b_2-b_1)\cos a_1)
$$
これにて$A$が求められた。
緯度経度をマップの座標系に変換するときは、次のような手順を踏めば良い。
緯度経度を$(a, b)$とすると
1. $((b-b_1)\cos(a_1), a-a_1)^\mathsf{T}$に左から$A$をかける
2. $(x_1, y_1)^\mathsf{T}$を足す実装
Javascriptで実装する場合は、行列の掛け算だけ人間が自力でやってしまい、三角関数周りなどはすべて
Math
オブジェクトの関数を使って計算すると良いと思います。
実際に表示するのはCSSとか別の領域なのでここでは割愛します。まとめ
とにかく基底変換ってやつですね。大学生の皆さんは練習にどうぞ()。
- 投稿日:2019-12-09T16:16:10+09:00
ReactFire v2 alphaを試してみる(Firestore編)
はじめに
前回の記事では、react-firebase-hooksを試したのですが、12月6日のReact Day Berlinでは、ReactFireのプレゼンがありました。こちらはとても先進的で期待できます。
本記事では、Firestoreの部分を移植してみたいと思います。
ReactFireとは
ReactFire v1は2016年5月のリリースを最後に、その後deprecateされ、Firebase JS SDKを直接使うようアナウンスされています。それが2019年7月よりv2として再度開発しているようです。
https://github.com/FirebaseExtended/reactfire
現在、masterブランチがv2になっています。
READMEには、
Status: Alpha. ReactFire is meant for React Concurrent Mode, which is only available in experimental React builds.
と書いてあるのですが、ざっとコードを見たところ、Suspenseを使っているだけで、Concurrent Modeでなくても動きそうです。
コーディング
import React, { Suspense, useState } from "react"; import ReactDOM from "react-dom"; import "firebase/firestore"; import { FirebaseAppProvider, useFirestoreCollectionData, useFirestore } from "reactfire"; const firebaseConfig = { apiKey: "...", authDomain: "...", databaseURL: "...", projectId: "...", storageBucket: "...", messagingSenderId: "...", appId: "..." }; const TodoList = () => { const firestore = useFirestore(); const todosRef = firestore().collection("todos"); const values = useFirestoreCollectionData(todosRef, { idField: "id" }); return ( <ul> {values.map(value => ( <li key={value.id}>{value.title}</li> ))} </ul> ); }; const NewTodo = () => { const [title, setTitle] = useState(""); const [pending, setPending] = useState(false); const firestore = useFirestore(); const add = async () => { setTitle(""); setPending(true); try { await firestore() .collection("todos") .add({ title }); } finally { setPending(false); } }; return ( <div> <input value={title} onChange={e => setTitle(e.target.value)} /> <button type="button" onClick={add}> Add </button> {pending && "Pending..."} </div> ); }; const App = () => { return ( <FirebaseAppProvider firebaseConfig={firebaseConfig}> <Suspense fallback={<div>Loading...</div>}> <div> <TodoList /> <NewTodo /> </div> </Suspense> </FirebaseAppProvider> ); }; const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);動きました。nextではだめで、canaryを使う必要がありました。
おわりに
ドキュメントがまだ追いついていなくて、ソースコードを読まないといけなかったです。
- 投稿日:2019-12-09T16:14:46+09:00
FirebaseとReactとReduxで多機能チャットを実装しよう【環境準備編】
FirebaseとReactとReduxで多機能チャットを実装しよう【環境準備編】
記事を順次書いていきます。
リンク集
- 環境準備編 ←イマココ
- チャットルーム選択編
- メッセージ機能編
- チャットルーム設定編
- デモページ
- github(公開準備中)TL;DR
FirebaseとReactを使って、メッセージを送り合う簡単なチャットアプリの解説記事はいくつか見かけますよね。
だけど、実務で使うチャットアプリならもっと複雑なはず。
そこで以下の7機能を持つチャットアプリを実装しました。
- 個人チャット
- グループチャット
- 新規チャットルーム作成
- チャットルームの削除
- チャットルームから退出
- チャットルームメンバー変更(追加/削除)
- チャットルーム名とアイコンの設定
デザインは、イケてるアプリ風にできるMaterial-uiにお任せした。
FirebaseとReactで開発するための環境構築
Firebase、React、Reduxを使って開発していくための環境を構築します。
React関連モジュールのインストール
Reactの環境をイチから構築するのは面倒です。
そこでcreate-react-appを使います。create-react-app
以下のコマンドでcreate-react-appしてください。
npx create-react-app react-chat※npmでグローバルインストールしたcreate-react-appは非推奨になりました。
過去にインストールしたことがある場合は、以下コマンドでアンインストールしておきましょう。npm uninstall -g create-react-app"react-chat"というフォルダが作成されます。
Reactの開発環境が作られています。(簡単すぎて感激)npm startしてブラウザ画面が表示されることを確認しましょう。
npm startその他モジュールのインストール
Reduxとmaterial-uiに必要なモジュールをインストールします。
npm install --save redux react-redux redux-actions immer @material-ui/core @material-ui/iconsFirebaseとの紐付け
Firebaseとの紐付けを行なっていきます。
1. Firebaseプロジェクトの作成
Firebase Consoleからプロジェクトを作成します。
今回は"react-chat"という名前にしました。
Analyticsを使うかどうか聞かれますが、どちらでも良いです。
使う場合はGoogle Analyticsのアカウントが必要になります。次へ次へと進めてプロジェクトを作成。
プロジェクトが作成されたら、</>マークをクリックしてWebアプリを追加しましょう。
アプリ名はなんでも良いです。「このアプリのFirebase Hostingも設定します。」にチェック。
次へ次へと進めてデプロイ完了させてください。
※モジュールインストールやログインなど基本的なところは割愛。
公式ドキュメントが詳しいです。2. configファイルの作成
まず、Firebaseモジュールをインストールします。
npm install firebase --saveFirebaseコンソールから[歯車ボタン] > [プロジェクトと設定] > [全般]の下部 [Firebase SDK snippet] > [構成]をチェック
で以下のような構成をコピーできます。firebaseフォルダとconfig.jsファイルを作成してペーストしてください。
src/firebase/config.jsexport const firebaseConfig = { apiKey: "YOUR-API-KEY", authDomain: "YOUR-PROJECT-ID.firebaseapp.com", databaseURL: "https://YOUR-PROJECT-ID.firebaseio.com", projectId: "YOUR-PROJECT-ID", storageBucket: "YOUR-PROJECT.appspot.com", messagingSenderId: "xxxxxxxxxx", appId: "x:xxxxxxxxxxx:web:xxxxxxxxxxxxxxxxxx", measurementId: "G-XXXXXXXXX" };index.jsファイルを作成して、設定をexportします。
src/firebase/index.jsimport firebase from 'firebase'; import { firebaseConfig } from './firebase/config.js'; export const firebaseApp = firebase.initializeApp(firebaseConfig); export const database = firebaseApp.database();3. Firebase init
initする前に、デフォルトリソースロケーションを選択しましょう。
(これ設定しておかないと、後から怒られます。)コンソールで
firebase init
を実行します。どのプロダクトを使うか選択。
今回はDatabase, Hosting, Storageの3つです。
プロジェクトの選択では、[Use an existing project] > [react-chat]を選択。
基本はEnterで進めていきます。
public directoryは"build"に変更してください。
また、single-page appとして設定しましょう。
これでFirebaseの設定は完了です。
4. Firebase Hostingにデプロイ
ビルドしてデプロイします。
npm run buildfirebase deployFirebase Hostingの公開先URLにアクセスして
npm start
の時と同じ画面が表示されることを確認しましょう。次はFirebaseとReactとReduxで多機能チャットを実装しよう【メッセージ編】です。
(現在編集中)参考記事
- 投稿日:2019-12-09T16:06:52+09:00
fs.stat を Promise 化して複数のファイルの stat を一気に取る。
書いてから思ったのですが、下から読んだ方がいいかも。
Promise 以前の状況
例えば node.js で 'x' という名前のファイルの stat を取得したい場合
sample.jsconst fs = require( 'fs' ) fs.stat( 'x' , ( er, stat ) => { if ( er ) console.error( er ) else console.log( stat ) } )という風にやってたと思います。
ファイルが複数の場合
複数のファイル、例えば 'x', 'y' という名前のファイルの stat を取得してから何かやりたいような場合、時間のかかる逐次処理でよければ
sample.jsconst fs = require( 'fs' ) const stats = [] fs.stat( 'x' , ( er, stat ) => { if ( er ) console.error( er ) else { stats.push( stat ) fs.stat( 'y' , ( er, stat ) => { if ( er ) console.error( er ) else { stats.push( stat ) console.log( stats ) } } ) } } )並行にしたい場合はちょっと苦しいテクニックを使って
sample.jsconst fs = require( 'fs' ) const stats = [ null, null ] fs.stat( 'x' , ( er, stat ) => { if ( er ) console.error( er ) else { stats[ 0 ] = stat if ( stats[ 1 ] ) console.log( stats ) } } ) fs.stat( 'y' , ( er, stat ) => { if ( er ) console.error( er ) else { stats[ 1 ] = stat if ( stats[ 0 ] ) console.log( stats ) } } )のようにする必要がありました。ちょっと大変です。特にファイルの数が増えたりしたら。
Promise 以降
fs.stat を Promise 化すると上のような大変さがなくなります。
Promise 化
util.promisify を使う
sample.jsconst fs = require( 'fs' ) const { promisify } = require( 'util' ) promisify( fs.stat )( 'x' ).then( _ => console.log( _ ) ).catch( _ => console.error( _ ) )素でやる
util.promisify がある今となってはもうやることはないと思いますが、参考までに。
sample.jsconst fs = require( 'fs' ) new Promise( ( rs, rj ) => fs.stat( 'x' , ( er, stat ) => er ? rj( er ) : rs( stat ) ) ).then( _ => console.log( _ ) ).catch( _ => console.error( _ ) )ファイルが複数の場合
Promise.all を使う
Promise の配列を作って Promise.all に渡してやります。配列の中のすべての Promise が解決されるか、どれかがリジェクトされるまで待ちます。
sample.jsconst fs = require( 'fs' ) const { promisify } = require( 'util' ) Promise.all( [ 'x', 'y' ].map( _ => promisify( fs.stat )( _ ) ) ).then( _ => console.log( _ ) ).catch( _ => console.error( _ ) )関数化してみる
sample.jsconst fs = require( 'fs' ) const { promisify } = require( 'util' ) const Stats = files => Promise.all( files.map( _ => promisify( fs.stat )( _ ) ) ) Stats( [ 'x', 'y', 'z' ] ).then( _ => console.log( _ ) ).catch( _ => console.error( _ ) )最後に
Promise.all を使わない手はありませんね!
- 投稿日:2019-12-09T15:40:16+09:00
Sortable.jsを使ってスパイダーソリティアを作ってみた
はじめに
現在SIerからWebエンジニアへの転職活動中で、ポートフォリオの一つとして
私の好きなゲームであるスパイダーソリティアをつくってみることにしました。
(スパイダーソリティアのルールはこちらから)ソース:Git Repo
プレイ:こちらから
※レスポンシブ対応はしていません(PCのみ)イメージ
終わりに
- 作成期間は約1か月ほどかかりました
- アルゴリズムや、一からのモノ作りを学ぶことができました
- SortableJSは結構いろいろな動作をさせることができました
途中ほんとうに挫折しかけましたがなんとか完成させることができたのでとても良かったです!
<使用技術>
HTML/CSS/JavaScript/SortableJs/Bootstrap/JQuery etc<参考にしたサイト>
【jQueryUIを使わずにドラッグ&ドロップを実装したい】Javascriptライブラリ「Sortable」を使ってみた
- 投稿日:2019-12-09T15:32:36+09:00
YouTubeの英語字幕をディクテーションできるChrome拡張機能を作る(海外ドラマで英語学習がしたい!)
ZOZOテクノロジーズ #4 Advent Calendar 2019 の10日目です。
昨日は@pakioさんの「Laravel + Vue + Vuetifyでbundle sizeの削減を試みてみる」でした。なんと先日、英単語の日本語訳とレベルを同時に取得でき、しかもローカル環境だけで動く素晴らしいJavaScriptライブラリに出会いました! この記事ではそのライブラリを使ってChrome拡張機能を作っていきます。
作っているもの
YouTubeの英語字幕をディクテーション(リスニング)できるChrome拡張機能「YouTube-Dictation」
github.com/tippy3/youtube-dictation
わかりにくいスクショで恐縮です。
つまりは動画の下に字幕を出して、ユーザーが英単語をディクテーションするということです。実際に作った流れ
Chrome拡張機能はJavaScript, HTML, CSSで開発します。めちゃくちゃ簡単です。
1 manifest.json
まずは各ファイルを用意し、エントリーポイントとなる
manifest.json
を作ります。{ "name": "YouTube-Dictation", "short_name": "YouTube-Dictation", "description": "YouTubeで楽しく英語学習。好きな動画の英語字幕でディクテーション(リスニング)ができる拡張機能です。", "version": "1.0", "icons": { "128": "images/128.png" }, "content_scripts": [{ "matches": ["https://*.youtube.com/*", "http://*.youtube.com/*"], "run_at": "document_end", "css": ["css/main.css"], "js": ["js/superagent-master/superagent.js", "js/underscore-master/underscore.js", "js/javascript-lemmatizer-master/js/lemmatizer.js", "js/kantan-ej-dictionary-master/kantan-ej-dictionary.js", "js/main.js"] }], "web_accessible_resources": [ "js/javascript-lemmatizer-master/dict/*.json" ], "permissions": [ "storage" ], "manifest_version": 2 }概要をざっくり説明すると、
content_scriptsは表示しているWebページに挿入されるJavaScriptです。表示中のDOMを操作したり、キーイベントを追加したりすることができます。他にも拡張機能のアイコンを押されたときに実行されるpage_actionなどがあります。
今回はcontent_scriptsでYouTubeのURLにマッチしたときにCSSとJavaScriptを読み込みます。JavaScriptは依存関係のあるライブラリを順に読み込み、最後に自分の処理を書く
main.js
を読み込みます。"run_at"でWebページの読み込み後に実行するようにしています。また、ライブラリにJSONを使うものがあったのでweb_accessible_resourcesにファイル名を書きました。これを書かないとJavaScriptからJSONにアクセスできません。また、Web Storageを使うライブラリがあったのでpermissionsに"storage"を入れました。
ここから
main.js
をガリガリ書いていきます。2 UIを出力する
このやり方は完全に自己流なのですが、JavaScriptでDOMをappendChildしてUIを出力します。なんか頭の悪い感じもしますが動くのでヨシ!
let html = document.createElement("div"); html.id = "yt-dictation-container"; html.innerHTML = '<p id="yt-dictation-text">YouTube-Dictation</p>...長いので省略'; html.addEventListener('click', clickEvent); document.body.appendChild(html);3 AjaxでAPIを叩き字幕を取得する
AjaxするためのライブラリsuperagentでJSONを取得します。使い方はjQueryの.ajax()とほぼ同じです。
function requestCCList(){ SUPERAGENT .get(YOUTUBE_CC_API) .accept('json') .query({ type: 'list', v: video_id }) .end(responseCCList); } function responseCCList(err, res){ // 略。下記参照 }実際には、まずは字幕一覧を取得し、もし英語の字幕があったらその字幕を取得する、という2段構えになっています。
また、字幕を取得後、行ごとの配列を作成し、さらに単語ごとの配列を作成しています。4 ディクテーションする英単語を選ぶ
この章がこの記事の本編です。はじめは英単語をランダムに出題していたのですが、ユーザーのレベルに合わせた英単語を出題したいと思いました。そこで使ったのが以下のライブラリです。
- kantan-ej-dictionary
- 英単語の和訳とSVLレベルを取得できるとても素晴らしいライブラリ
- javascript-lemmatizer
- 英単語の原型を取得できるとても素晴らしいライブラリ
英単語のレベル分けには、アルクによるSVL12000(重要英単語12000語を1から12までの12段階にレベルわけしたもの)を採用し、それを和訳とともに取得できるライブラリkantan-ej-dictionaryを使ってみました。
しかし、複数形や過去形だとkantan-ej-dictionaryにヒットしないため、原型でヒットしなかった場合はjavascript-lemmatizerを使って英単語を原型に戻し、再度検索しました。
なんとこれらライブラリはいずれもローカルだけで動きます。しゅばらしい。。。
あとは取得した英単語のレベルを使い、最もユーザーのレベルにあった英単語を出題します。
実際のコードが下記です。英単語ごとにループを回し、以下の処理を行います。let candidates = []; // 問題にする単語の候補(単語のインデックス番号が入る) let min_diff = 99; cc.words.forEach((word,index)=>{ const original_word = removeSymbol(word); let result = kanten_dictionary[original_word]; // まずは英単語を辞書で検索 if(!result || !result.svl_level){ // 辞書にヒットしなかった場合、英単語を原型に戻す const lemma = LEMMATIZER.lemmas( original_word ); if(lemma.length==0){ return false; // 原型に戻せなかった場合 }else{ result = lemma[0][0]; } result = kanten_dictionary[result]; // 原型で辞書を検索 if(!result || !result.svl_level){ return false; // 原型でも辞書にヒットしなかった場合 } } // 単語のレベルとユーザーのレベルの差を求める const diff = Math.abs( result.svl_level - USER_SVL_LEVEL ); if( diff<min_diff || min_diff==99 ){ // よりレベルの近い単語が見つかった場合、その単語を候補にする min_diff = diff; candidates = []; candidates.push(index); }else if( diff==min_diff ){ // レベルの差が同じ単語が見つかった場合、その単語を候補に入れる candidates.push(index); } });あとは出題する英単語をinputタグに変え、動画の再生時間に合わせて字幕をUIに出力します。入力の判定処理やキーイベントなども書いて、、、完成!!!せず、本日を迎えてしまいました。
実は一旦ストアへリリースしたのですが、
- このテキトーなUIを改善しなきゃなぁ
- ユーザーのレベルは決め打ちじゃなくて動的に決めたいなぁ
- 英単語の和訳も表示したいなぁ
- 間違えた単語をセーブして復習できるようにしたいなぁ
などの欲が出た結果、このアドベントカレンダーに間に合わなくなりました。ごぺんなさい?
ここからはストアへの公開についてです。
Chromeウェブストアへの公開方法
Q: ストアへの登録は難しいですか?
A: 面倒くさいですが難しくはありません。1 まずはデベロッパー登録($5)
無料で公開する場合もデベロッパー登録が必要です。氏名・住所・電話番号等を入力し、登録料5ドルをクレジットカードで支払います。登録料は1回払えばOKです。
2 作った拡張機能を登録
拡張機能の名前や説明、販売する国や金額を入力していきます。途中で縦横比別にサムネイル画像を登録していくのですが、これがとても面倒くさかったです。アイコンやスクリーンショット含めて5種類も画像を作らされました。
3 Googleによる審査を待つ
無料公開でもGoogleによる審査があります。私の場合は公開申請後1日以内に通ることが多かったです。
これでようやく公開!(してないけど)
記事本文はここまでです。謝辞
共同開発者の@motonari728に深く感謝申し上げます。また一緒に食事でも行きましょう。
もっとChrome拡張機能について知りたくなったら(参考文献)
QiitaにはChrome拡張機能に関する記事がたくさんあります。はじめに読むべき2本をオススメとして紹介します。
- Chrome拡張開発 超概要(特に主な拡張の種類の章が分かりやすいです)
- Chrome 拡張機能のマニフェストファイルの書き方(他にもググるとたくさん出てきます)
次回予告
明日は@252525さんです。お楽しみに!
最後までお読みいただきありがとうございました。
- 投稿日:2019-12-09T15:06:57+09:00
JavaScriptで,文字列のNull or 空判定
- 投稿日:2019-12-09T14:36:10+09:00
【Vue.js】とあるテキストエディタのライブラリ選定の変遷
アドベントカレンダーでもなんでもないです。
とあるプロジェクトでテキストエディタの開発に1年以上に渡って携わりました。
その中でのライブラリ選定にあたり紆余曲折がありましたので記します。要件
- シンプルにテキスト入力ができる
- データとしてプレーンテキストを取り出せる
- コピー&ペーストなどができる
- 文字装飾は一切不要
- 画像を挿入できる
この画像挿入が鬼門となります。
画像挿入を満たすため、WYSIWYGエディタライブラリを模索することとなります。version.1 tiptap
https://github.com/scrumpy/tiptap
以下の理由からtiptapを採用しました
- 要件を満たせる機能郡
- Vueライブラリである
生じた問題
実際にリリースしてみると、特定の環境や操作において以下のような問題が発生してしまいました。
- ペースト時に連続する半角スペースが1つになる
- ペースト時に全角スペースが半角スペースになる
- ペースト時に改行が消える
- 約1万文字を超えるとクラッシュする
主にiOSで多くの異常が見られました。
その他にも全角文字や改行コードの入力に関するハックが必要な事象が多く発見されました。基本的に変換不要な英数字の基本的な入力しかサポートされてないように思われます。
version.2α
ライブラリの再選定
https://github.com/codex-team/editor.js
https://github.com/Microsoft/roosterjs
https://prosemirror.net/
https://draftjs.org/version.2 quill
いくつかのライブラリを発見したものの、採用実績やフレームワークとの相性を加味した結果次のQuillを採用しました。
- 試験実装において前回の不具合は発生しない
- Vueライブラリではないものの、導入が容易
生じた問題
今回はversion.1における不具合を踏まえQuality Assuranceを通過したものの、新たな特定の環境や操作に依存した不具合が発生してしまいました。
- HTML形式のデータをペーストすると一部不要なスタイルの適用や改行の増加が見られる(Wordなど)
- 特定の環境・特定の位置でbackspaceするとクラッシュする
- 見た目上編集できるがインスタンス内にはデータが存在しないことがある
特にIEで問題が頻発することとなります。
クリップボード上のHTMLの加工処理の複雑さからこちらの想定するもとのプレーンなテキストをペーストをするのが困難です。
改行に関しては特に<p>
タグをコピーしていた場合、ブラウザによりテキスト+改行でとしてデータが取得されるために改行の増加が見られましいた。
また、内部の特殊なデータ構造の複雑さ・イベント処理の複雑さからオーバーライドしての処理なども困難です。version.3α エディタ自前実装の検討
これまでの不具合を勘案し、エディタライブラリの自作という選択肢を考慮したものの以下の理由から断念することとなりました。
- メンテナーがいなくなる懸念(少人数チームのため)
contenteditable
のブラウザ差異を埋める実装の困難さ- WYSIWYGを作り込むのは機能過多かつ工数不足
version3. そして現在
以上を考慮し、
contenteditable
を捨てることとなりました。
結局いくついたのは<textarea>
です。。。抜粋.vue<div v-for="(t, index) in texts" :key="index"> <div v-if="t.image"> <img :src="t.image.src" :alt="t.image.alt" @click="deleteImage(index)"> </div> <div v-else> <textarea v-model="t" :index="index" :ref="`text${index}`"> </div> </div>これに各種カーソル移動や変更のハンドラを追加したものが完成形となりました。
教訓
- 過多な機能は誤動作を巻き起こす
- ブラウザ差異の想定されるAPI(contenteditableのような)はライブラリでも解決されない場合がある
- 全角入力は想定されてない場合がある
- iOSとIEは魔境
Simple is best.
- 投稿日:2019-12-09T11:55:42+09:00
【必読】React開発で役に立つプラクティス8選
以下は、jsmanifestさんの記事、8 Useful Practices for React Apps You Should Knowの日本語訳です。
【必読】Reactアプリで役に立つプラクティス8選(8 Useful Practices for React Apps You Should Know)
mediumで私を見つけてくださいね。
Reactはとても多くのステージへの変化を行ってきました。そしてそれらは、必ず私達を驚かせます。
最初、私達はmixinsを使って、インターフェースを作ったり管理したりしていました。そして次にクラスコンポーネントというコンセプトが到来し、現在は、Reactでの私達の開発方法を変えるreact hooksです。
他になにか素晴らしいことを知っていますか? 例えば、Reactで使える、アプリをより良くするのに役立つ巧みなトリックです。
この記事では、全てのReactデベロッパーが知っておくべき8つの高度なトリックを紹介します。このリストの全てがあなたにとって目新しいものではないと思いまが、最低でも1つ、便利なトリックを見つけることを願っています。
これが、あなたが知っておくべきreactで使える8つのトリックです。
1. 文字列でReact要素を生成する(Create react elements with strings)
まず1つ目は、HTML DOMタグを表すシンプルな文字列でReact DOM要素を作る方法です。
例えば、変数に
div
という文字列を代入することで、Reactコンポーネントを作ることができます。import React from 'react' const MyComponent = 'div' function App() { return ( <div> <h1>Hello</h1> <hr /> <MyComponent> <h3>I am inside a {'<div />'} element</h3> </MyComponent> </div> ) }Reactは、
React.createElement
を呼び、与えられた文字列を使って、内部で要素を生成します。これってクールでしょ?Material-UIのようなコンポーネントライブラリでよく使われます。コンポーネントのルートnodeを決める
component
propsを宣言できます。function MyComponent({ component: Component = 'div', name, age, email }) { return ( <Component> <h1>Hi {name}</h1> <div> <h6>You are {age} years old</h6> <small>Your email is {email}</small> </div> </Component> ) }このように使用することができます。
function App() { return ( <div> <MyComponent component="div" name="George" age={16} email="george@gmail.com"> </div> ) }カスタムコンポーネントを渡すこともできます。
function Dashboard({ children }) { return ( <div style={{ padding: '25px 12px' }}> {children} </div> ) } function App() { return ( <div> <MyComponent component={Dashboard} name="George" age={16} email="george@gmail.com"> </div> ) }2. Error Boundariesを使う(Use Error Boundaries)
JavaScriptでは、私達はほとんどのエラーを
try/catch
ー発生したエラーを"catch"できるコードのブロックー で対応しています。エラーがcatchブロックに補足された時、私達はアプリがクラッシュするのを避けることができます。例はこのようなものです。
function getFromLocalStorage(key, value) { try { const data = window.localStorage.get(key) return JSON.parse(data) } catch (error) { console.error } }Reactは結局ただのJavaScriptなので、エラーをキャッチしてハンドルできると考えるでしょう。しかし、Reactの性質上、コンポーネント内でのJavScriptエラーは内部のstateを破壊し、将来のレンダーにおいてcryptic errors(不可解なエラー)を引き起こします。
このような理由で、ReactチームはError Boundariesを紹介しています。そして全てのReactデベロッパーがそれらを知っておくべきです。そうすれば自らのReactアプリで使用することができます。
Error Boundaries登場以前のエラー発生の問題点は、これらの不可解なエラーが発生した時、Reactはそれらに対処したり、リカバリーする方法を私達に与えていなかったことです。ですので、私達は皆Error Boundariesが必要なのです。
Error Boundariesは、Reactコンポーネントです。コンポーネントツリーのどこででもエラーをキャッチすることができ、ログ出力し、クラッシュしたコンポーネントツリーの代わりにフォールバック用UIを表示させることができます。Error Boundariesは、レンダー中、ライフサイクルメソッド内、そしてError Boundaries以下に存在する全体のツリーのコンストラクタ内でエラーをキャッチします(そしてこれが、私達のアプリのトップのどこかでError Boundariesを宣言しレンダーする理由です)。
class ErrorBoundary extends React.Component { constructor(props) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true } } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo) } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1> } return this.props.children } }そして、普通のコンポーネントと同じように使用できます。
<ErrorBoundary> <MyWidget /> </ErrorBoundary>3. 前回の値を保持する(Retain Previous Values)
propsやstateを更新している時、あなたは
React.useRef
を使ってそれらの前回の値を保持することができます。例えば、1つの配列のアイテムの、現在と前回の変更を追跡するために、前回の値が割り当てられる
React.useRef
と、現在の値が割り振られるReact.useState
を使用することができます。function MyComponent() { const [names, setNames] = React.useState(['bob']) const prevNamesRef = React.useRef([]) React.useEffect(() => { prevNamesRef.current = names }) const prevNames = prevNamesRef.current return ( <div> <h4>Current names:</h4> <ul> {names.map((name) => ( <li key={name}>{name}</li> ))} </ul> <h4>Previous names:</h4> <ul> {prevNames.map((prevName) => ( <li key={prevName}>{prevName}</li> ))} </ul> </div> ) }これは正しく動作します。なぜなら
React.useEffect
はレンダーが完了した後に実行されるからです。
setNames
が実行される時、コンポーネントは再度レンダーを行い、prefNamesRef
は前回の値を保持します。なぜなら、前回のレンダーから見てReact.useEffect
が最後に実行されるコードだからです。
そしてuseEffect
内でprevNamesRef.current
に値が再び代入されるので、次回のレンダー時には前回のnamesの値を保持します。4. 値チェックにReact.useRefを使う(Use React.useRef for flexible non-stale value checks)
React Hooksが登場する以前、ComponentがDOMにマウントされた後に、dataのフェッチなどの処理を行いたい場合、私達は
componentDidMount
というクラスコンポーネントのStaticメソッドを使用していました。React Hooksが発表された時、それはすぐにコンポーネントを書く最も人気の手段となりました。コンポーネントがアンマウントされた後にstateがセットされるのを防ぐために、コンポーネントがマウントされたかを監視したい時、私達はこのようにするでしょう。
import React from 'react' import axios from 'axios' class MyComponent extends React.Component { mounted = false state = { frogs: [], error: null, } componentDidMount() { this.mounted = true } componentWillUnmount() { this.mounted = false } async fetchFrogs = (params) => { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params }) if (this.mounted) { this.setState({ frogs: response.data.items }) } } catch (error) { if (this.mounted) { this.setState({ error }) } } } render() { return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => <li key={frog.name}>{frog.name}</li> )} </ul> </div> ) } }React Hooksに統合後、Hooksは
componentDidMount
を持っていません。そして、アンマウント後に起こったstateアップデートによるメモリリークのコンセプトは未だにhooksに当てはまります。しかし、React Hooksを使ったcomponentDidMount
に似た方法はReact.useEffect
を使用することです。なぜなら、それはコンポーネントがレンダリングされた後に実行されるからです。もしあなたがReact.useRef
を使って、マウントされた値を割り当てるなら、以下のクラスコンポーネントの例で同じことができます。import React from 'react' import axios from 'axios' function MyComponent() { const [frogs, setFrogs] = React.useState([]) const [error, setError] = React.useState(null) const mounted = React.useRef(false) async function fetchFrogs(params) { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params, }) if (mounted.current) { setFrogs(response.data.items) } } catch (error) { if (mounted.current) { setError(error) } } } React.useEffect(() => { mounted.current = true return function cleanup() { mounted.current = false } }, []) return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => ( <li key={frog.name}>{frog.name}</li> ))} </ul> </div> ) }リレンダーをせず最新の変更をトラッキングするもう一つ別の例は、以下のように、
React.useMemo
と併せて使用することです。(source)function setRef(ref, value) { // Using function callback version if (typeof ref === 'function') { ref(value) // Using the React.useRef() version } else if (ref) { ref.current = value } } function useForkRef(refA, refB) { return React.useMemo(() => { if (refA == null && refB == null) { return null } return (refValue) => { setRef(refA, refValue) setRef(refB, refValue) } }, [refA, refB]) }もしref propsが変更され、定義されたら、新たな関数を作ります。
つまり、Reactは古いフォークされたrefをnull、新しいフォークされたrefを現在のrefで呼ぶことを意味します。そしてReact.useMemo
が使われているので、refの変数たちはrefA
またはrefB
のref propsが変更されるまで記憶されます。ナチュラルなクリーンアップが起こるのです。5. 他の要素に依存したカスタム要素にReact.useRefを使う(Use React.useRef for customizing elements that depend on other elements)
React.useRef
にはいくつか役に立つユースケースがあります。例えば、React.useRef
自体をref propsに割り当てるなどです。function MyComponent() { const [position, setPosition] = React.useState({ x: 0, y: 0 }) const nodeRef = React.useRef() React.useEffect(() => { const pos = nodeRef.current.getBoundingClientRect() setPosition({ x: pos.x, y: pos.y, }) }, []) return ( <div ref={nodeRef}> <h2>Hello</h2> </div> ) }もし
div
要素の座標位置を取得したいなら、この例が役に立ちます。しかし、アプリケーションのどこかに存在する別の要素の位置をアップデートしたかったり、それに応じて条件ロジックを変更したり適応する場合、ベストな方法はref callback function pattern
を使うことです。callback function patternを使用すると、ReactコンポーネントまたはHTML DOM要素を第一引数として受け取ります。下の例は、コールバック関数である
setRef
がどこでref
propsに適応されるかを示すシンプルなものです。直接React.useRef
をDOM要素に割り当てるのとは対象的に、setRef
の内側では必要なことは何でもできるということがわかります:const SomeComponent = function({ nodeRef }) { const ownRef = React.useRef() function setRef(e) { if (e && nodeRef.current) { const codeElementBounds = nodeRef.current.getBoundingClientRect() // Log the <pre> element's position + size console.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`) ownRef.current = e } } return ( <div ref={setRef} style={{ width: '100%', height: 100, background: 'green' }} /> ) } function App() { const [items, setItems] = React.useState([]) const nodeRef = React.useRef() const addItems = React.useCallback(() => { const itemNum = items.length setItems((prevItems) => [ ...prevItems, { [`item${itemNum}`]: `I am item # ${itemNum}'`, }, ]) }, [items, setItems]) return ( <div style={{ border: '1px solid teal', width: 500, margin: 'auto' }}> <button type="button" onClick={addItems}> Add Item </button> <SomeComponent nodeRef={nodeRef} /> <div ref={nodeRef}> <pre> <code>{JSON.stringify(items, null, 2)}</code> </pre> </div> </div> ) }6. 高階コンポーネント(Higher Order Components)
プレインなJavaScriptで強力な再利用性を持つ関数を作るよくあるパターンは高階関数です。Reactも結局はJavaScriptなので、内部で高階関数を使用することができます。
再利用性のあるコンポーネントには、高階コンポーネントとというトリックが使えます。
高階コンポーネントとは、あなたがコンポーネントを引数にして、返り値としてコンポーネントを返す関数を持っているときのことです。高階関数がロジックを抽象化し、他の関数間で共有されるように、高階コンポーネントもコンポーネントをロジックから切り離し、他のコンポーネント間で共有することができます。つまり、たくさんの再利用可能なコンポーネントを採用し、アプリケーション内で実際に繰り返し使うことが可能だということなのです。
これは高階コンポーネントの例です。このスニペットでは、高階コンポーネントであるwithBorderはカスタムコンポーネントを引数として取り込み、隠されたミドルレイヤーコンポーネントを返します。そして、親がこの高階コンポーネントをレンダーするとき、これはコンポーネントとして呼ばれ、ミドルレイヤーコンポーネントからから渡されたpropsを受け取ります。
import React from 'react' // Higher order component const withBorder = (Component, customStyle) => { class WithBorder extends React.Component { render() { const style = { border: this.props.customStyle ? this.props.customStyle.border : '3px solid teal', } return <Component style={style} {...this.props} /> } } return WithBorder } function MyComponent({ style, ...rest }) { return ( <div style={style} {...rest}> <h2>This is my component and I am expecting some styles.</h2> </div> ) } export default withBorder(MyComponent, { border: '4px solid teal', })7. Render Props
React内で使う私のお気に入りのトリックの一つは、render prop patternです。複数のコンポーネントでコードを共有するという問題を解決する点において、これは高階コンポーネントに似ています。Render propsは、レンダーに必要なものを全て差し戻すことが目的の関数をはっきりとさせます。
Render props expose a function who's purpose is to pass back everything the outside world needs to render its children.Reactでコンポーネントをレンダーする最もベーシックな方法は、このようなものでしょう。
function MyComponent() { return <p>My component</p> } function App() { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return ( <MyComponent fetching={fetching} fetched={fetched} fetchError={fetchError} frogs={frogs} /> ) }Render Propsを使う場合は、その子をレンダーするpropは慣例的にrenderと呼ばれます。
function MyComponent({ render }) { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return render({ fetching, fetched, fetchError, frogs, }) }この例では、
MyComponent
はrender prop componentとして参照するコンポーネントの例です。なぜなら、MyComponent
はpropとしてのrender
を期待していますし、子をレンダーするためのそれを実行するからです。
これはReactにおいて、とてもパワフルなパターンです。renderのコールバックを通して引数として、共有されたstateとdataを渡すことができるのです。複数のコンポーネントにおいて、そのコンポーネントはリレンダーされ、再利用されることが可能になります。function App() { return ( <MyComponent render={({ fetching, fetched, fetchError, frogs }) => ( <div> {fetching ? 'Fetching frogs...' : fetched ? 'The frogs have been fetched!' : fetchError ? `An error occurred while fetching the list of frogs: ${fetchError.message}` : null} <hr /> <ul style={{ padding: 12, }} > {frogs.map((frog) => ( <li key={frog.name}> <div>Frog's name: {frog.name}</div> <div>Frog's age: {frog.age}</div> <div>Frog's gender: {frog.gender}</div> </li> ))} </ul> </div> )} /> ) }8. Memoize
Reactデベロッパーとして知っておくべき最も大事なことの一つは、
React.memo
のようなコンポーネントのパフォーマンス最適化です。それを知っておくと、無限ループのようなとてもひどいエラーを防ぐことができます。Reactアプリにおいて、Memoization(メモ化)を行う、いくつかの方法があるので是非読んでみてください。
締め
ここでこの投稿は終わりです!この記事があなたにとって価値があることを願います。
Mediumも見てくださいね。
感想
まず、今回の記事は翻訳が難しかったです。英語もさることながら、自分のReact Hooksの理解が浅いなと実感しました。
逆にReact HooksやよりReactの理解が進めば、これらのトリックはとても役に立ちそうだと思いました。間違い等見つけましたら教えていただけるとありがたいです。
- 投稿日:2019-12-09T10:49:51+09:00
テスタビリティの観点でコードを書くこと
本記事は、サムザップ #2 Advent Calendar 2019 の12/9の記事です。
はじめに
テスタビリティ(Testability, テスト容易性)とはテストがどれだけ実行しやすいか、どれだけ効果的かを表す度合いです。
テスタビリティが高いとより不具合を見つけやすく、コードの品質を高めることができます。
テスタビリティの特徴としてはJames Bachがあげたものがあります。
テスタビリティが高いコードを書くことは、それ自体がコードの品質を高めることになる場合があります。参照透過性を例に説明していきたいと思います。
参照透過性をもつ関数はテスタビリティが高いと言われています。
参照透過性を壊すものの一部を挙げると以下があります。
- 環境変数
- グローバル変数
- 現在時刻を返す関数
- 乱数生成機
これらを含む関数は実行する環境やタイミングなどによって返す値が異なり、テスタビリティの低い関数になることがあります。
例
例として、様々な敵をランダムに生成し返す関数
createRandomEnemy()
を考えます。
この関数は呼ばれる度に95%の確率でノーマル敵、5%の確率でレア敵を返すという仕様だとして、
以下のように書いたとします。const createRandomEnemy = () { const probability = Math.random(); if (probability > 0.05) { return new nomalEnemy(); // ノーマル敵を表すオブジェクト } return new rareEnemy();// レア敵を表すオブジェクト }仕様通りの動作をするか確認するには、以下のように対象の関数を何万回も呼び出した上で5%程度の範囲内かどうかを調べるテストが考えられます。
しかし、これでは確率的に成功/失敗するテストになってしまいます。test('レアな敵の出現チェック.', () => { enemies = createEnemies(50000); // たくさん敵を生成する. 内部でランダムに敵を生成する関数を呼び出している const resut = aggregateEnemies(enemies); // どの敵がどれだけ生成されたかを集計する関数(略) expect(resut.rareEnemyWeight).toBeWithinRange(0.0499, 0.0501); // レア敵が5±0.1%程度で生成されているかのアサーション }); function createEnemies(count) { const enemies = []; _.times(count) { enemies.push(createRandomEnemy()); } return enemies; }関数内で行っていた乱数生成
Math.random()
をやめ、
createRandomEnemy()
に外から乱数を与えるようにし参照透過性をもたせることで、これを防ぐことができます。
つまり、createRandomEnemy()
の関数呼び出しの結果が、引数probability
にのみ依存するようにします。const createRandomEnemy = (probability) { if (0.05 < probability) { return new nomalEnemy(); } return new rareEnemy(); }こうすることで、createRandomEnemyについては何度実行しても同じ結果となります。
test('レアな敵の出現チェック.', () => { const enemy = createRandomEnemy(0.01); expect(enemy.type).toEqual(RARE); });まとめ
参照透過性を保つことはコードを理解しやすくし、テスタビリティを高め、不具合を減らす上で役立ちます。
簡単な工夫で対処できる場合は、テスタビリティの観点をもってコードを書くことをおすすめします。明日は @tomokazu_kurosawa さんの記事です。
- 投稿日:2019-12-09T10:33:13+09:00
A-FRAME: 物理演算でボーリングっぽい動きを実現してみる10(ピンに複合シェイプの基本形状を複数適用する)
- 投稿日:2019-12-09T09:35:47+09:00
create-react-appを用いて,React/JSXのコンパイル環境を構築する
はじめに
create-react-app
を用いて,コンパイル環境を構築する例を備忘録としてまとめます.環境
- Windows 10 Home
- npm 6.12.1
- Git bashでコマンド実行
Reactのツールでビルド
React/JSXを使用するときは,ソースコードをあらかじめコンパイルしておくのが一般的です.
ここでは,React/JSXのコンパイル環境を整える方法を紹介します.React/JSXのコンパイル環境を作る
create-react-app
というアプリを使ってコンパイル環境を構築します.
これはFacebookが用意しているアプリです.以下のコマンドで
create-react-app
をグローバルインストールします.$npm install -g create-react-app次に,このツールを利用したプロジェクト(sample1)を作成します.
作業ディレクトリの直下にsample1
というフォルダが生成されます.$create-react-app sample1 Creating a new React app in C:\Users\Let-create-ReactApp\src\ch2\sample1. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts with cra-template... ・・・略・・・ We suggest that you begin by typing: cd sample1 npm start Happy hacking!次に,
sample1
のディレクトリに移動して,npm start
と入力してアプリを起動します.//作業ディレクトリを表示 $pwd C:\Users\Let-create-ReactApp\src\ch2\ //sample1というフォルダが直下にあることを確認 $ls sample1/ $ch sample1 $npm start > sample1@0.1.0 start C:\Users\Let-create-ReactApp\src\ch2\sample1 > react-scripts start ・・・略・・・ Compiling... Compiled with warnings. ./src/App.js Line 2:8: 'logo' is defined but never used no-unused-vars Search for the keywords to learn more about each warning. To ignore, add // eslint-disable-next-line to the line before. Compiling... Compiled successfully!Windowsのネイティブ環境で実行している場合には,Webブラウザが起動し,Reactのひな型が以下のように表示されます.
create-react-app sample1
を実行するとき,カレントディレクトリに以下のフォルダが生成されます.
node_modules
: インストールされたNode.jsのモジュールが存在src
:コンパイル前のソースコードが存在public
:index.htmlのひな型となるファイルが存在
src
ディレクトリにあるファイルを編集して保存すると,ファイルの変更を自動的に検知して,Webブラウザに反映できます.
そのため,リアルタイムで実行結果を確認して,ファイルの編集を行うことができます.
src
の中のApp.js
を下記のように編集して保存すると,自動的にWebブラウザが更新され,以下のような画面が表示されます.App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <h1>こんにちは</h1> </div> ); } export default App;プログラムが完成し,開発したプログラムをWeb上に公開するためには,以下のコマンドを実行します.
$npm run buildこれにより,
sample1
フォルダ直下にbuild
というフォルダが生成されます.
そのフォルダ以下に各種ファイルが圧縮した状態で生成されます.
ローカル環境でうまくビルドされたかを確認するには,Webサーバが必要になります.
以下のようにコマンドを実行してserve
コマンドをインストールします.そしてserve -s build
を実行すると,Webサーバが起動するので,指示されたURLにWebブラウザでアクセスすると,実行状態を確認できます.$npm install -g serve $serve -s build INFO: Accepting connections at http://localhost:5000Webブラウザで
http://localhost:5000
でアクセスします.まとめ
create-react-app
で環境を整えると,リアルタイムで変更が反映され,エラー表示もわかりやく,リソース様にビルドするのも容易です.