- 投稿日:2020-01-18T20:03:35+09:00
[Github Actions]githubのリリースの公開に合わせてtwitterで通知
前回の記事(webサイトの更新の表示をgithubのreleaseで管理)の中で書いたgithubのreleaseに合わせてツイートをするGithub Actionsについて書きます。
概要
githubの任意のリポジトリのreleaseが公開されたタイミングでツイートをするGithub Actionsを作っていきます。
GitHub Marketplace
https://github.com/marketplace/actions/tweet-trigger-publish-releaseリポジトリ
https://github.com/mugi111/tweet-trigger-release作り方
↓↓↓作り方の参考にしたページです↓↓↓
LGTMすると現場猫が「ヨシ!」してくれるGitHub Actionsをつくった + Tips今回はこちらのjavascriptのテンプレートを使用して作っていきました。
githubにログインをしてリポジトリのページに行くと、緑色の
Use this template
というボタンが見えるので
いつも通りの見慣れたページに移動するので、ここから自分のリポジトリを作成していきます。
テンプレートに加えて使うものとしてものとして今回はtwitterのパッケージをインストールします
npmnpm i twitter --save
yarnyarn add twitter作りは単純でtwitterのAPIを使用してツイートをPOSTするだけです。
client
の宣言やツイート本文の指定で使われているcore.getInput(' xxx ')この記述で後述する
.github/workflow
直下のyamlファイルから値を取得してきます。下がコードの全文です。
index.jsconst core = require('@actions/core'); const Twitter = require('twitter'); // most @actions toolkit packages have async methods async function run() { try { const client = new Twitter({ consumer_key: core.getInput('consumer_key'), consumer_secret: core.getInput('consumer_secret'), access_token_key: core.getInput('access_token_key'), access_token_secret: core.getInput('access_token_secret') }); client.post('statuses/update', {status: core.getInput('tweet_body')}, (error) => { if (!error) { console.log('Succeeded!'); } else { console.log('Couldnt tweet.'); } }); } catch (error) { core.setFailed(error.message); } } run()yamlファイルの中身
この部分で
release
のpublished
をトリガーにすることを定義しています。on: release: types: [published]あとは実行環境や使用するActions、入力する値を設定します。
build: runs-on: ubuntu-latest steps: - uses: mugi111/tweet-trigger-release@v1.2 with: consumer_key: ${{ secrets.CONSUMER_KEY }} consumer_secret: ${{ secrets.CONSUMER_SECRET }} access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} tweet_body: "更新しました!"使い方
以下のyamlファイルを
.github/workflow
の直下においておきます。tweet-trigger-release.ymlname: "tweet-trigger-release" on: release: types: [published] jobs: build: runs-on: ubuntu-latest steps: - uses: mugi111/tweet-trigger-release@v1.2 with: consumer_key: ${{ secrets.CONSUMER_KEY }} consumer_secret: ${{ secrets.CONSUMER_SECRET }} access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} tweet_body: "更新しました!"ここの
secrets.XXX
を設定するためにSettingsのSecretsタブからtwitterのDeveloperページから取得したtokenやkeyを登録します。
Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ ※2019年8月時点の情報
https://qiita.com/kngsym2018/items/2524d21455aac111cdeeあとは
"更新しました!"
となっているtweet_body
を書き換えることでツイートの本文を設定して完了です。おわり
- 投稿日:2020-01-18T19:48:15+09:00
便利ページ:データURLを生成する
「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」のシリーズものです。
HTMLでは、スキームが先頭についた URL で、コンテンツ制作者は小さなファイルをインラインで文書に埋め込むことができる「データURL」というのがよく使われています。
こんな感じです。
data:[<mediatype>][;base64],<data>(参考) データURL : MDN
https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URIs(たいしたことはやってませんが)便利ページ に追加しておきました。
いつもの通りGitHubにも上げてあります。
https://github.com/poruruba/utilities参考までに、以下にデモとしてアクセスできるようにしてあります。「バイナリファイル」のタブを選択してみてください。
https://poruruba.github.io/utilities/start.jsbinary_open: function(e){ var file = e.target.files[0]; var reader = new FileReader(); reader.onload = (theFile) =>{ this.binary_data = new Uint8Array(theFile.target.result); this.binary_type = file.type ? file.type: 'application/octet-stream'; if( this.binary_type.startsWith('text/')) this.binary_text = decoder.decode(this.binary_data); this.binary_dataurl = "data:" + this.binary_type + ";base64," + bufferToBase64(this.binary_data); this.binary_change(); }; reader.readAsArrayBuffer(file); // reader.readAsBinaryString(file); // reader.readAsDataURL(file); },以下、抜粋です。
this.binary_dataurl = "data:" + this.binary_type + ";base64," + bufferToBase64(this.binary_data);以上
- 投稿日:2020-01-18T19:00:41+09:00
JavaScript参考サイト(個人的メモ)
JavaScript初心者向け参考記事
2017年から始めるjavascript勉強ノート
ES2015(ES6) 入門
Google流JavaScriptにおけるクラス定義の実現方法(ES6以前)
※引用{ES6でクラスが導入されほとんどのブラウザがサポートしている2019年現在、ここで書かれている手法を直接使用することはないでしょう。 }[JavaScript] 猿でもわかるクロージャ超入門 まとめ
直ぐに忘れるので備忘録
即時関数 (function(){..})();
- 投稿日:2020-01-18T19:00:13+09:00
【Angular】画面真っ白。Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html" がコンソールに表示
1. エラー詳細
Angularアプリをherokuでデプロイして、
Build succeeded!
されたけど
実際開いてみたら画面真っ白。
そして、コンソールにエラーメッセージが表示されていました。Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec.
モジュール読み込み失敗。サーバーはJavaScriptじゃない"text/html"のMIME型を返却。
2. 環境
macOS Mojave 10.14.6 Angular CLI: 8.3.22 Node: 12.10.0 Angular: 8.2.14tsconfig.json"compilerOptions": { //JavaScirptのバージョン "target": "es2015", },3. 解決策
index.htmlにあったscriptタグのtype属性を
text/javascript
に変更index.html<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>sample-app</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="styles.f88876c1aa2c513e53bf.css"></head> <body> <app-root></app-root> <!-- scriptタグのtype="module"をすべて type="text/javascript"に変更 --> <script src="runtime-es2015.edb2fcf2778e7bf1d426.js" type="text/javascript"> </script> <script src="runtime-es5.edb2fcf2778e7bf1d426.js" nomodule defer></script> <script src="polyfills-es5.6696c533341b95a3d617.js" nomodule defer></script> <script src="polyfills-es2015.2987770fde9daa1d8a2e.js" type="text/javascript"></script> <script src="main-es2015.bb16c3796b9468f94a5b.js" type="text/javascript"></script> <script src="main-es5.bb16c3796b9468f94a5b.js" nomodule defer></script></body> </html>変更後、再度デプロイしてみたところCSSも含めてすべて表示されるようになりました。
4. 参考サイト
- 投稿日:2020-01-18T18:36:31+09:00
graphql clientはApolloよりfetchでやった方がいいのではないかという件
リポジトリ
https://github.com/gqlkit-repos/gqlkit-store
graphql client storeプラグインです。
これを、各フレームワークのcontextに突っ込んでやろうという魂胆
reduxはreact、vuexはvueという感じですが
クロスフレームワークでかつgraphqlの為の
client&storeというのを作ってみました。
nuxt.jsでは検証済resolverファーストアーキテクチャのgraphql client設計
. ├── index.js └── resolvers ├── Mutation │ ├── createStaff.js │ └── deleteStaff.js ├── Query │ └── staffs.js ├── cache.js └── client.jsたったのこれだけ
と言いたいところですが
これを例えば、nuxtで使うなら、nuxtのpluginsディレクトリに突っ込むので
一見、まぁまぁな規模感あるように見えます。というか、そもそもなのですが
クライアント側のvuexとかreduxだとかのストアが抱えている課題って
もうストアはストアで別個のフレームワークと捉えて設計した方が良いレベルではないかと思うので
これはなんか正解路線なのでは?と思っています。ちなみにQueryとMutation以下のjsファイルはデモ用のファイルです。
queryのコードの見た目も、mutationのコードの見た目もほぼ同じ
上のディレクトリツリーのstaffs.jsはこんな感じ
staffs.jsimport client from '../client' import cache from '../cache' import gql from 'graphql-tag' export const demand = gql` query { staffs { id name age sex } } ` export default async variables => { let re if (!cache.has('staffs').value()) { const { staffs } = await client.req(demand) cache.set('staffs', staffs).write() re = staffs } else { re = cache.get('staffs').value() } return re }mutationにあたるcreateStaff.jsはこんな感じ
mutationはrefetchしたいモジュールファイルをrefetchという名前で読み込みます。
そんでもってqueryでもmutationでも
クライアントツールはclient.req(demand, variables)
(variablesが必要ない場合は省略)createStaff.jsimport client from '../client' import cache from '../cache' import gql from 'graphql-tag' import refetch from "../Query/staffs"; export const demand = gql` mutation($name: String!, $age: Int!, $sex: String!) { createStaff(name: $name, age: $age, sex: $sex) { id name age sex } } ` export default async variables => { const res = await client.req(demand, variables) const staff = res.createStaff const staffs = cache.get('staffs').value() staffs.push(staff) cache.set('staffs', staffs).write() return refetch() }クライアントツールは21行のコードでqueryもmutationもOK!
fetchで書くとこんなシンプルになるとは正直、驚きました。
ただ、今後subscription導入のことも考えると
もうちょっと大きいファイルになる可能性はあります。
まだ、subscriptionは未経験なので。。。client.jsconst GQLKIT_SERVER_END_POINT = process.env.GQLKIT_SERVER_END_POINT || 'http://localhost:4000/query' const method = 'POST' const headers = { 'Content-Type': 'application/json', Accept: 'application/json' } export default { async req(demand, variables) { const res = await fetch(GQLKIT_SERVER_END_POINT, { method, headers, body: JSON.stringify({ query: demand.loc.source.body, variables }) }) const { data } = await res.json() return data } }キャッシュはどうするか?
キャッシュは完全切り分けの方針です。
Apolloのapiはキャッシュと密結合ですが
resolverファーストなgraphql clientアーキテクチャでは
キャッシュは完全別扱いにして
好きなin memory dbを使えば良いという方針で考えます。
resolverを書いて解決すれば良いじゃないか!という方針です。僕は今のところlowdbを使っています。
ここはお好みで好きなもの使ってよしです。cache.jsimport low from 'lowdb' import Memory from 'lowdb/adapters/Memory' const cache = low(new Memory()) cache.defaults({}).write() export default cacheまとめ
apolloがなぜ、あれほどまでに複雑なアーキテクチャを取っているのか
僕の知らない何らかの理由でああなっている可能性も大なのでこれがベストアンサーだなんて到底言い切る事はできないです。
これはあくまで、1アイデアというものです。あと、jsのオブジェクトをgormっぽい書き方で読み書き出来るような
ツールをご存知の方おられましたら教えて頂けると嬉しいです。サーバーとクライアントで違うノウハウというのを最小限に抑えたい
ということで
graphqlサーバーのアーキテクチャを真似たプラグインを作ってみたという話でした。
- 投稿日:2020-01-18T17:56:31+09:00
Firebase 静的サイト(html CSS JavaScript)をデプロイ
Firebaseにてデプロイの記録
【使用環境】
html
CSS
JavaScript
macbook<次回の時短の為に記録>
Firebaseにて登録、 (googleアカウント必要)ログイン
新しいプロジェクトフォルダをFirebase HP内で作成する。
ここからターミナルで
今回は下記②の設定のために public ディレクトリ 配下にデプロイアプリ(html CSS JavaScript img等)を配置
URL > ルートパスの為
デプロイにGitHubは必須ではないが、
git add ←必要なら
git commit ←必要なら
push ←必要なら
① firebase-tools をインストール
(npm がインストールされている前提) $ npm install -g firebase-tools $ npm -v $ firebase -V $ firebase loginアプリディレクトリへ移動後、
①初期化
$ firebase initPCの矢印キーで Hosting: を選択。 (色が変わる。かつ●へ)
エンター
firebaseのHPで作成した
アプリを関連付けるプロジェクトを選択するよう求められる
Use an existing project を選んで エンター
HPで作成したプロジェクト一覧がでる。
今回のものを選択し、エンター(画像はありません)
②
ディレクトリのパスを聞かれるのでデフォルト (/public) のまま エンター
Configure as a single-page app (rewrite all urls to /index.html)? (y/N)
他も動かすため、推奨 Nを選択File public/index.html already exists. Overwrite?
作成したindex.htmlが上書きされるので 推奨 Nを選択complete!
初期化が完了
デプロイ
そのままの階層で
$ firebase deploy↑ ここの下に URLがある。
デプロイ完了
(修正後の再デプロイは、$ firebase deployだけで↑は不要)
2回目以降の事象
$ firebase login >Error: Server Error. certificate has expiredその為、
firebase-toolsをupdateする必要がある
$ sudo npm i -g firebase-tools $ firebase loginログインできた
- 投稿日:2020-01-18T17:01:15+09:00
DOMにおける「Illegal Invocation」エラーの一例とその原因・対処法
DOMに関わるプログラムを書いていると、「Illegal Invocation」エラーに遭遇することがあります。例えば、次のようにすると
q
の呼び出しでエラーが発生します。const q = document.querySelector; q("body"); // Uncaught TypeError: Illegal invocationちなみに、「Illegal Invocation」とはGoogle Chromeのエラーメッセージであり、Firefoxは次のようにもうちょっと親切なエラーメッセージを出力します。エラーメッセージというのは基本的に仕様で定められておらず処理系の裁量があるところですから、エラーの意味が分からなくて詰まったら別のブラウザで試してみるのもひとつの手です。
TypeError: 'querySelector' called on an object that does not implement interface Document.Google Chromeの「Illegal Invocation」はいろいろな場合に表示されるメッセージなので、全てに通用する汎用的な原因・対処法というのはありません。この記事で扱うのは冒頭のように、「呼び出しやすいようにメソッドを別の変数に入れた」場合に発生するエラーです。
Twitterでこのエラーで詰まっている方がいたのですが、「Illegal Invocation」で検索しても出てくる既存の記事がどれも微妙だったのでこの記事を用意しました。
エラーの原因
このエラーの原因をひとことで言うならば「
this
が違う」ということです。DOMの関数は、正しいthis
で呼び出さないといけません。JavaScriptは、オブジェクトのメソッド内では
this
を用いて自分自身を参照することができます。const obj = { prop: 100, method() { console.log(this.prop); } }; obj.method(); // 100 が表示されるこの例では、
obj.method()
という呼び出しをすると100
が表示されます。これは、obj.method
の中で使われているthis
がobj
を指すからです。しかし、このプログラムを次のように変えるとうまく動作しません。const obj = { prop: 100, method() { console.log(this.prop); } }; const func = obj.method; func(); // undefined が表示される(strictモード内ならエラーが発生)
obj.method
を別の変数func
に入れてfunc
を呼び出したら、結果が変わってしまいました。これは、obj.method
の呼び出し方が変わったことによりobj.method
の中のthis
が変わったからです1。このように、関数内での
this
がどうなっているかは、その関数をどのように呼び出したかによって決まります。foo.bar()
というメソッド呼び出しの記法を用いて呼んだ場合は、関数bar
内でのthis
の値はfoo
です。つまり、foo.bar()
というメソッド呼び出しの記法は、ただ単に「foo.bar
に入っている関数を呼び出す」という意味なのではなく、「this
をfoo
として、foo.bar
を呼び出す」とおいう意味のプログラムだったのです。冒頭の
document.querySelector
についても同じことが言えます。このquerySelector
メソッドは、this
がdocument
の状態で呼び出さないといけません。よって、「this
をdocument
として呼び出す」というプログラムを書かないと正しく呼び出すことができません。
const q = document.querySelector
としたあとにq()
を呼び出した場合は、q
はたしかにdocument.querySelector
と同じであるものの、q()
として呼び出してもthis
がdocument
になりません。これが「Illegal Invocation」エラーの原因でした。ここで、Firefoxのエラーメッセージを振り返ってみましょう。
TypeError: 'querySelector' called on an object that does not implement interface Document.日本語にすると「
querySelector
がDocument
インターフェースを実装していないオブジェクト上で呼ばれました」となります。「~上で」という表現がわかりにくいですが、これはthis
が何かということを意味しています。つまり、これは「this
がDocument
インターフェースを実装したオブジェクトではない」ということに文句を言っているのです。まさに上で解説した通りのことが起きていることになります。個人的には、エラーメッセージに「this
」というワードを含めてくれればもっと親切になるのにと思わないでもありませんが。エラーの対処法
以上で、エラーの原因は分かりました。次は、これに対する対処法を説明します。基本的には、問題が「
this
が正しくない」ことだったのですから、対処法は「正しいthis
で呼ぶ」しかありません。その方法は色々あります。最もベーシックな方法は、常にメソッド呼び出しの記法を使うことです。
document.querySelector("body")
の形で呼べば、この記法は「this
をdocument
としてメソッドを呼ぶ」という意味ですから正しいthis
で呼ぶことができます。しかし、これでは冒頭のコードの目的を達成したとは言えません。冒頭のコードの目的は、これを
q("body")
という短いコードで呼べるようにすることでした。これを達成する方法のひとつは、次のようにすることです。const q = (query) => document.querySelector(query); q("body"); // エラーが出ない!これは元のプログラムをη変換しただけのように見えますが、
document.querySelector
を呼び出すときはあくまでメソッド呼び出しの記法を使われているのがポイントです。これならquerySelector
を呼び出すときのthis
がちゃんとdocument
になっているため、エラーにはなりません。別の方法はbindを使って次のようにする方法です。
const q = document.querySelector.bind(document); q("body"); // エラーが出ない!
bind
をこのように使う場合、「this
がすでに固定されている関数」を作ることができます。この場合変数q
に入っているのは、「this
がdocument
に固定されたdocument.querySelector
」です。よって、q("body")
のようにメソッド記法を使わずに呼び出した場合でも、bind
の効果によってdocument.querySelector
はthis
がdocument
の状態で呼び出されるため、エラーにはなりません。もうひとつ別の方法としては、callメソッド(またはapplyメソッド)を用いる方法もあります。これは関数オブジェクトが持つメソッドで、「
this
を明示的に指定して呼び出す」ということが可能になります。const q = document.querySelector; q.call(document, "body"); // エラーが出ないこれは長いので、短く書くという目的に使われることは無いと思いますが、
call
やapply
はthis
の値も含めて関数呼び出しを詳細に制御したい場合に重宝します。上級編:エラーを仕様で確かめる
ところで、「
document.querySelector
はthis
がdocument
の状態で呼び出さないとエラーになる」というのは不親切にも思えますが、実はちゃんと仕様書に明記してあり、ブラウザはそれに従っているだけです。そこで、この仕様がどのように定義してあるのかを確かめましょう。
ということで、まずDOM仕様書をチェックしましょう。
querySelector
はParentNodeインターフェースの中に定義してあります。仕様にはこのように定義が書かれています。interface mixin ParentNode { // (省略) Element? querySelector(DOMString selectors); [NewObject] NodeList querySelectorAll(DOMString selectors); }; Document includes ParentNode;なんとなく、
querySelector
はParentNode
インターフェースが持つメソッドであり、引数はDOMString
型(JavaScriptでいうただの文字列です)の引数selectors
ひとつであり、返り値はElement?
型(Element
またはnull
を意味します)であることが分かります。実は、この記法はWebIDLと呼ばれるものです。WebIDLはインターフェース・メソッド等を定義するための標準化された言語であり、Web関連のさまざまな仕様書におけるインターフェース定義記法を統一するために作成されました。仕様書の書き方を定義するメタ仕様という感じです。
そして、今回目的としている挙動の根拠を見つけるためには、WebIDL仕様書を紐解かなければなりません。つまり、「
Element? querySelector(DOMString selectors);
と書かれていたらどのような挙動のメソッドになるのか」という定義をWebIDL仕様書で調べることになります。現在Webを支配しているのはJavaScript (ECMAScript) ですから、WebIDLにはECMAScript bindingという章があり、「WebIDLで定義されたメソッドはJavaScriptではどういう挙動をするのか」ということが厳密に仕様化されています。
いきなり核心に迫りますが、3.6.7 Operationsという節でこの構文に対する仕様が定義されています。最初の段落を引用します(強調は筆者)。
For each unique identifier of an exposed operation defined on the interface, there exist a corresponding property. Static operations are exposed of the interface object. Regular operations are exposed on the interface prototype object, unless the operation is unforgeable or the interface was declared with the [Global] extended attribute, in which case they are exposed on every object that implements the interface.
強調されている点が今回関係のある部分です。上記の記法によれば、実は
querySelector
はregular operationに分類されます。よって、querySelector
はinterface prototype objectに実体が存在することになります。今回querySelector
はParentNode
というmixinインターフェース上に定義されていますが、Document includes ParentNode;
という宣言がありますから、querySelector
はDocument
インターフェース上に宣言されたものであると見なされます。よって、ここで言うinterface prototype objectとはDocument.prototype
のことです。// true が表示される console.log(document.querySelector === Document.prototype.querySelector);となると、
Document.prototype.querySelector
にどのような関数が設定されるのかがポイントになります。これは、仕様書を少し読み進めると出てくるcreate an operation functionアルゴリズムで定義されています。このアルゴリズムは「関数を作る」という複雑な操作を自然言語で表現しているためややこしいのですが、一部を抜粋すると、Document.prototype.querySelector
を実行した際にはまず以下のステップが実行されます。If target is an interface, and op is not a static operation:
- Let esValue be the this value, if it is not null or undefined, or realm’s global object otherwise. (This will subsequently cause a TypeError in a few steps, if the global object does not implement target and [LenientThis] is not specified.)
- If esValue is a platform object, then perform a security check, passing esValue, id, and "method".
- If esValue does not implement the interface target, throw a TypeError.
- Set idlObject to the IDL interface type value that represents a reference to esValue.
ここではtargetとopという2つの変数が存在しており、それぞれinterfaceとoperationです。これらはJavaScriptの値ではなく、WebIDLにおけるインターフェース・オペレーションといった概念です。言うなればこれらはWebIDLのASTノードのことであると見なせます。ここでは
target
はquerySelector
の実装先であるDocument
インターフェースを表すものであり、op
は`Element? querySelector(DOMString selectors);
という定義そのものであると考えられます。実際にこのステップを実行すると、まず1により、
this
の値が取得されesValueに入ります。document.querySelector("body")
のように呼び出した場合はesValueに入るのはdocument
であり、正しくない方法で呼び出した場合はesValueにグローバルオブジェクトが入ります。2にはsecurity checkという文言が登場しますが、これは今回は関係ありません。ちなみに、security checkの実態はHTML仕様書にあり、これは他のオリジンのブラウジングコンテキストに対して許可されていない操作をしたら
SecurityError
が発生するという仕様を定めています。3が一番のポイントです。ここでは、esValueがtargetをimplementしないならば、TypeErrorを発生させると定義されています。esValueは
this
の値で、targetはDocumentインターフェースのことでしたから、ここで問題のチェックが行われていることになります。ちゃんとthis
の値をdocument
にして呼び出さなかった場合、esValueはDocumentインターフェースをimplementしていませんから、TypeErrorが発生することになります。なお、ここで「implementする」というのがどういう意味なのかが気になるかと思います。これは3.7 Platform objects implementing interfacesで以下のように定義されています。
An ECMAScript value value implements an interface interface if value is a platform object and the inclusive inherited interfaces of value.[[PrimaryInterface]] contains interface.
つまり、value.[[PrimaryInterface]]にそのインターフェース(もしくはそれを継承したインターフェース)が入っているかどうかでチェックしています。[[PrimaryInterface]]というのはインターナルスロットです。これも仕様書用語ですが、インターナルスロットについては筆者が最近書いた別の記事で詳しく解説しています。これはWebIDLによって新たに定義されているインターナルスロットです。
なお、このような方式になっているということは、オブジェクトのプロトタイプをごまかしても無意味だということを意味しています。次のような「偽Doocument」を作ってもごまかすことはできずにTypeErrorが発生します。
const fakeDocument = Object.create(Document.prototype); fakeDocument.querySelector = document.querySelector; fakeDocument.querySelector("body"); // TypeErrorが発生まとめ
この記事では、DOMのメソッドを正しい
this
で呼び出さなかったことに起因する「Illegal Invocation」エラーを解説し、その対処法もあわせて説明しました。ポイントは、メソッド呼び出しのfoo.bar()
という記法はただ関数を呼び出すだけでなく、「呼び出された関数の中のthis
を指定する」という機能も併せ持っているという点です。DOMに限らず、多くのメソッドは正しくthis
を指定しないと期待した通りに動作しません2。これにより、「メソッドを別の変数に入れてそれを呼び出す」ということをした場合はメソッド呼び出し記法を使っていないため正しいthis
がセットされず、エラーとなるのでした。対処法としては、第一に「ちゃんとメソッド呼び出し記法を使って呼び出す」が上げられます。また、
Function.prototype.bind
を使ってthis
が常に固定された関数を作る方法もありました。冗長ですが、Function.prototype.call
でthis
を明示的に指定する方法もあります。この記事の後半では上級者向けのコンテンツとして、「
this
が正しくないとTypeErrorが発生する」という挙動がちゃんと仕様書において定義されていることを確かめました。
この呼び出し方の場合、strictモードでは
this
はundefined
になり、それ以外の場合はthis
はグローバルオブジェクト(globalThis)になります。 ↩最近は
class
宣言の中でプロパティ宣言とアロー関数を使ってメソッドを宣言する流派もあり、その場合はアロー関数によってthis
が固定されるためどのように呼び出しても期待通りのthis
となります。 ↩
- 投稿日:2020-01-18T16:17:58+09:00
Javascript + CSS で複数行テキストを省略する
実現したいこと
jQueryを使わずに複数行テキスト末尾に「...」をつけたい。
よくある処理なのに忘れがちなので備忘録。TL;DR
参考
クラスメソッドさんの記事を土台にVanilla JSに書き換えました。
クラスメソッドさん、いつもありがとうございます。
https://dev.classmethod.jp/ria/string-replace-css-and-jquery/コード
index.html<div class="wrap"> <p class="ellipsis"> あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、 うつくしい森で飾られたモリーオ市、 郊外のぎらぎらひかる草の波。 </p> </div>ellipsis.css.ellipsis { width: 296px; height: 48px; font-size: 16px; overflow: hidden; }ellipsis.jsconst target = document.getElementsByClassName("ellipsis")[0]; let text = target.innerHTML; const clone = target.cloneNode(true); clone.style.visibility = "hidden"; clone.style.position = "absolute"; clone.style.overflow = "visible"; clone.style.width = `${target.clientWidth}px`; clone.style.height = "auto"; target.insertAdjacentElement("afterend", clone); while((text.length > 0) && (clone.clientHeight > target.clientHeight)) { text = text.substr(0, text.length - 1); clone.innerHTML = `${text}...`; } target.innerHTML = clone.innerHTML; clone.parentNode.removeChild(clone);解説
▶︎何もしていない状態
ellipsis.css.ellipsis { width: 296px; font-size: 16px; }▶︎高さを
height: 48px;
で指定し、要素からはみ出したテキストをoverflow: hidden;
で隠すellipsis.css.ellipsis { width: 296px; height: 48px; font-size: 16px; overflow: hidden; }▶︎JSの処理を追加
テキストの<p>
要素を複製して、直後に追加ellipsis.jsconst target = document.getElementsByClassName("ellipsis")[0]; let text = target.innerHTML; const clone = target.cloneNode(true); clone.style.visibility = "hidden"; // display: "none"だとclientHeightが取得できない clone.style.position = "absolute"; clone.style.overflow = "visible"; clone.style.width = `${target.clientWidth}px`; clone.style.height = "auto"; target.insertAdjacentElement("afterend", clone);▶︎複製した要素の
height
が元の要素のheight
を下回るまで文字を減らしていくellipsis.jswhile((text.length > 0) && (clone.clientHeight > target.clientHeight)) { text = text.substr(0, text.length - 1); clone.innerHTML = `${text}...`; }▶︎複製した要素のテキストを元の要素に代入、複製要素は不要なので削除
ellipsis.jstarget.innerHTML = clone.innerHTML; clone.parentNode.removeChild(clone);
- 投稿日:2020-01-18T16:04:15+09:00
初心者による jQuery 基礎
jQueryの基本的な構文
jQueryで行われる処理の流れは、
1. $()関数で命令の対象となるHTML要素をjQueryオブジェクトに変換 2. そのjQueryオブジェクトに対して独自メソッドを呼び出して変更を加えるjQueryの基本的な構文は、
sample.js//jQueryオブジェクト.変更する命令 $(HTML要素).メソッド('引数', '引数')また、HTMLを最後まで読み込んだ時点で実行するために、jQueryは以下のようになる。
sample.js$(function){ //HTMLがロードされたのちに実行する処理 }onメソッド
onメソッド構文は次のように書く
sample.json('イベントタイプ', 'イベントハンドラ')ちなみに、thisプロパティは「onメソッドに指定してあるイベントが発生した要素」が格納される。
thisを使うことで、1. 処理のパフォーマンスが上がる 2. コードの汎用性を持たせられる そして一番重要なのが、 3. $()関数に複数のセレクタを指定した場合にthisを使うことで処理の切り分けをしてくれるというメリットがある。
イベントタイプ
主なイベントタイプは、
mouseover : 要素にマウスが乗った時 mouseout : 要素からマウスが離れた時 mousedown : 要素上でクリックボタンが押されたとき mouseup : 要素上でクリックボタンが離れた時 mousemove : 要素上でマウスが動かされたとき click : 要素がクリックされたとき dbclick : 要素がダブルクリックされたとき keydown : 要素にフォーカスした状態で、キーボードのキーが押されたとき keyup : 要素にフォーカスした状態で、キーボードのキーが離れた時 focus : 要素にフォーカスが当たった時 blur : 要素からフォーカスが離れた時 change : 入力内容が変更されたとき resize : 要素がリサイズされたとき scroll : 要素がスクロールされたときイベントハンドラ
sample.jsfunction() { //任意の処理 }たとえば、
sample.js$('#type').on('mouseover', function(){ $('#type').css('color', '#ebc000') })のように使う。
メソッドチェーン
メソッドチェーンとは、「メソッドを鎖(チェーン)のようにつなげて記述し、実行するプログラム手法」です。書き方は、
sample.js$('セレクタ').メソッドA().メソッドB().メソッドC()...たとえば、
sample.js$(function(){ $('#type') .on('mouseover', function(){ $('#type').css({ color: '#ebc000', backgroundColor: '#ae5e9b' }) .on('mouseout', function(){ $('#type').css({ color: '', backgroundColor: '' }) }) }) })なお、実行の順番が結果に関係しないメソッドについては、順番を変えても同様の結果となる。
このメソッドチェーンのメリットは、
- $()関数の記述が一回で済むため可読性が上がる。
- プログラムの処理速度が上がる。
stopメソッド
実行中のアニメーションを途中でキャンセルするにはstop()メソッドを使います。
sample.jsstop( true | false )参考資料
「jQuery最高の教科書」
- 投稿日:2020-01-18T15:08:32+09:00
初心者による JavaScriptの基礎
概要
JavaScriptの基本的な書き方や特徴をメモとして箇条書きのように書いていく。なにかあれば順次追加していく
文のルール
javascriptの文のルールとして、
- 文の末尾にセミコロンをつけることを推奨している。
- 大文字/小文字は厳密に区別される。
文字リテラル
文字リテラルは(')や(")で囲む必要があるが、ふたつ使いたい場合は、
sample.js'He's a doctor!' -> 悪い例 "He's a doctor!" -> よい例ただしエスケープ処理をすれば、大丈夫である。
sample.js'He\'s a doctor'テンプレート文字列
テンプレート文字列を利用することで、以下のような文字列表現が可能になる。
- 文字列への変数の埋め込み
- 複数行にまたがる文字列
sample.jslet name = '斉藤' let str = `こんにちは${name}さん。今日も天気いいですね!` console.log(str)分割代入(配列)
分割代入とは、配列/オブジェクトを分解し、配下の要素/プロパティ値を個々の変数に分解するための構文です。
sample.js//従来のやり方 let data = [1, 2, 3] let x1 = data[0] let x2 = data[1] let x3 = data[2] //分割代入 let data = [1, 2, 3] let [x1, x2, x3] = dataとてもすっきりしている。
分割代入(オブジェクト)
オブジェクトのプロパティを変数に分解することもできます。
sample.jslet book { title: 'javascript', price: 270 } //分割代入 let {price, title, memo='なし'} = book複雑な分解も可能です。
1. 入れ子となったオブジェクトを分解する
sample.jslet book = { title: 'javascript', price: 270, other: { author: '斉藤', logo: 'logo.jpeg' } } //分割代入 let {title, price, other: {author}} = book2. 変数の別名を指定する
sample.jslet book { title: 'javascript', price: 270 } //分割代入(名前変更) let {price: value, title: name} = bookショートカット演算(短絡演算)
sample.jsif(x === 1) {console.log("Hello!")} x === 1 && console.log("Hello!")上の二式は同じ挙動をする。if文を使わなくても簡単に同じことができる。よく使う印象。
for...of命令
「配列など」を順番に列挙するための一つの手段がfor...of命令だ。
配列などというのは、for...of命令は、Arrayオブジェクトやイテレーターやジェネレーターなども処理できるためである。これらを総称して列挙可能なオブジェクトという。sample.jsfor(仮変数 of 列挙可能なオブジェクト){ //コード }for...inに対してfor...ofは値を順に列挙するのが特徴だ。
- 投稿日:2020-01-18T15:06:29+09:00
ExpoアプリでのStripe決済を整理+Paymentsを使ってみる
はじめに
直近の記事でWebViewを使用することによってExpoアプリをEjectせずにStripeを導入する方法を確認しました。
ここではもう一つの選択肢として、iOSにおいてEjectが必須であるPayments APIの方をまとめてみます。Compatibilityを整理
なかなかややこしいので、まずはWebViewでの実装を含めて、各OS、機能で何ができて何ができないのかを整理しておきます。
checkout.js in WebView
Expoでの動作 → iOS/Android共に❌
サーバー側実装 → 不要
動的な金額設定 → 可
PSD2 → 不明(未準拠?)Stripeの
checkout.js
は、カード入力フォームをWebページ上にモーダルで表示させて決済を導入できるライブラリです。
現在はレガシー扱いになっていて、下記のドキュメントのようにStripeがホスティングするページへリダイレクトさせる実装へと変更することが推奨されています。Checkout migration guide
https://stripe.com/docs/payments/checkout/migration今から導入する際には当然選択肢には入らないと思いますが、StripeをExpoで使用する際のライブラリとしてexpo-stripe-checkoutという一実装があり、こちらはこの
checkout.js
をWebViewで使用しています。
ちょっと試してみたところ、現在このcheckout.js
はブラウザがWebViewやスマホであると認識された場合に別タブでチェックアウト画面を開くような処理になっていて、うまく動作しませんでした。Elements + Payment Intents in WebView
Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 必要
動的な金額設定 → 可
PSD2 → 準拠可Stripe Elementsはカード情報などの入力フォームを独自に作成するためのWeb向けAPIです。
ユーザーが入力したカード情報などに開発者がタッチできないよう設計されており、欧州の決済サービス指令(PSD2)に準拠したセキュアな決済フローを柔軟に実装できるようになっています。
Elementsの場合はPayment Intentという決済機能を使うのですが、これはサーバー側の実装が併せて必要になります。Expoで使用する際はこちらの記事に簡単な例を載せたので、参照してください。
ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む
https://qiita.com/mildsummer/items/f95fd53864be6f14e3b0Checkout + Sessions in WebView
Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 必要(+静的HTMLホスティング)
動的な金額設定 → 可
PSD2 → 準拠可現在のCheckoutは、
redirectToCheckout
メソッドによってStripeがホスティングする決済フォームへリダイレクトする機能になっています。
通常はSessionをサーバー側で作成してからsessionIdを指定することでリダイレクトする感じになります。
そのほか、WebViewで使用する場合もコールバック先のHTMLはWeb上のどこかに用意する必要があります。
詳しくはこちらの記事の1.2.を参照してください。
ejectなしのExpoアプリにStripe決済をWebViewを使って埋め込む(Checkout編)
https://qiita.com/mildsummer/items/616677286e79cb8f8f75Checkout クライアント専用組み込み in WebView
Expoでの動作 → iOS/Android共に⭕️
サーバー側実装 → 不要(静的HTMLホスティングは必要)
動的な金額設定 → 不可(SKU・個数を指定)
PSD2 → 準拠可現状、Expoアプリに導入できるStripeの機能のなかで唯一サーバー側の実装が不要なパターンです。
ただし、SKUを指定する形になるため動的に金額を設定することはできません。これはセキュリティ上の制限ということだと思います。
Expoでの実装例はこちらを参照してください。Expo Payments + Charge
Expoでの動作 iOS Android Managed / Expo Client ❌ ⭕️(英語のみ) Bare / Expo Client ❌ ⭕️(英語のみ) Bare / Standalone ⭕️ ⭕️ サーバー側実装 → 必要
動的な金額設定 → 可
PSD2 → 未準拠WebViewを使わず、Stripeのネイティブアプリ用ライブラリを使用したパターンです。
ExpoからはPayments API(expo-payments-stripe)を使用します。
Stripeの各OSライブラリをReact Nativeで使用できるようにしたものがtipsi-stripeで、これをExpoで使用できるようにしたものがexpo-payments-stripeと二重の依存関係になっているため、最新のバージョンが反映されるまでになかなか時間がかかりそうだなという懸念がありますが、一応使用できます。
ただし2020年1月現在のところPSD2に準拠したPaymentIntentsを使用できないため、欧州での使用が想定される場合は注意が必要です。
PSD2に準拠する必要がある場合は、Expoのフォーラムでも話題に上がっているので随時チェックしてみてください。
Tipsi-stripe SCA compliant Payment
https://forums.expo.io/t/tipsi-stripe-sca-compliant-payment/27422表に書いた通り、iOSではBare Workflowでスタンドアロンビルドした状態のみで使用できます。
AndroidではEjectせずExpo Clientで使用できるのですが、今のところUI文言の設定にはリソースファイル(XML)の修正が必要で、故にカード入力UIを日本語に対応したい場合はeject/Bare Workflow化が必須です。では、このPaymentsを導入する流れを説明していきます。
まずPaymentsを使ってみる
サーバー側の実装
ExpoのPaymentsではユーザーの入力したカード情報をトークン化する機能のみ使用できます。
このトークンはStripe Chargeの作成時にsource
オプションとして指定するものです。このCharge作成の処理はサーバー側で行います。
上述したWebViewの記事と同じようにFirebase Cloud Functionsにデプロイするとしたら例えば以下のような感じになります。functions/index.jsconst functions = require('firebase-functions'); const app = require('express')(); const stripe = require('stripe')('sk_test_...'); // 秘密鍵 const cors = require('cors'); const bodyParser = require('body-parser'); app.use(require('body-parser').text()); app.use(cors()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.post('/createCharge', async (req, res) => { const { token } = req.body; const result = await stripe.charges.create({ amount: 2000, // 金額 currency: 'jpy', // 通貨単位 source: token, // トークンを渡す description: 'test charge', // 説明 }); res.json(result); }); exports.api = functions.https.onRequest(app);WebViewの記事と合わせて簡単に
https.onRequest
でexpressを使用していますが、勿論ちゃんとhttpsCallable
やFirestoreとの連携などすることも可能です。また、Firebase Cloud Functionsを使用する際に注意が必要な点があります。
Payment Intentsを使用した
1. サーバー側で決済情報を作成 → 2. クライアント側でカード情報と紐付けて決済完了
のパターンと違ってChargeの場合は
1. クライアント側でカード情報を作成(トークン化) → 2. サーバー側で決済
という流れになるので、サーバー側で冪等性を保証しないと同じカード情報を使って二重に決済されるということが容易に起こってしまいます。
詳しくはこちらのk-boyさんの記事が参考になります。stripeをfirebaseで使うときidempotency_keyをつけよう
https://qiita.com/k-boy/items/6d8ce83084a0f49ab0d2アプリ側を作成
ボタンをタップしたらカード入力フォームを表示するような画面を作ります。
expo-payments-stripe
をインストールします。$ npm install --save expo-payments-stripeJS側はシンプルにApp.jsのみでこんな感じに。
App.jsimport React, { Component } from 'react'; import { StyleSheet, Text, ActivityIndicator, TouchableOpacity, View } from 'react-native'; import { PaymentsStripe as Stripe } from 'expo-payments-stripe'; export default class App extends Component { state = { loading: false, succeeded: false }; /** * カード入力画面を表示しCharge作成 */ charge = async() => { this.setState({ loading: true }); try { await Stripe.setOptionsAsync({ publishableKey: 'pk_test_...' // 公開鍵 }); const params = { // Only iOS support this options smsAutofillDisabled: true, requiredBillingAddressFields: 'full', prefilledInformation: { billingAddress: { name: 'Hanako Yamada', line1: 'line1', line2: 'line2', city: 'Yokohama', state: 'Kanagawa', country: 'JP', postalCode: '2440000', email: 'test@test.com', } } }; const source = await Stripe.paymentRequestWithCardFormAsync(params); const result = await fetch('https://[リージョン名]-[プロジェクト名].cloudfunctions.net/api/createCharge', { // 先ほどのAPIを叩く method: 'POST', headers: { 'Content-type': 'application/json' }, body: JSON.stringify({ token: source.tokenId }) }); this.setState({ loading: false, succeeded: result.status === 200 }); } catch (e) { console.log(e); this.setState({ loading: false }); } }; /** * Chargeをキャンセル */ cancel = () => { this.setState({ isCharge: false }); }; render() { const { loading, succeeded } = this.state; return ( <View style={styles.container}> <Text style={styles.title}>支払いのテスト</Text> <TouchableOpacity onPress={this.charge} disabled={succeeded || loading}> <View style={[ styles.button, succeeded && styles.succeededButton ]} > {loading && <ActivityIndicator color="#ffffff" style={styles.buttonIndicator} />} <Text style={[styles.buttonText, succeeded && styles.succeededButtonText]} > {succeeded ? '支払いが完了しました' : '支払い'} </Text> </View> </TouchableOpacity> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, height: '100%', width: '100%', backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center' }, title: { position: 'relative', fontSize: 24, fontWeight: 'bold', marginBottom: 24 }, modal: { justifyContent: 'flex-end', margin: 0 }, modalInner: { height: '70%', backgroundColor: '#ffffff' }, button: { position: 'relative', width: 240, height: 50, borderRadius: 25, justifyContent: 'center', alignItems: 'center', backgroundColor: 'orange' }, succeededButton: { borderColor: 'orange', borderWidth: 2, backgroundColor: 'transparent' }, buttonText: { fontSize: 18, fontWeight: 'bold', color: '#ffffff' }, succeededButtonText: { color: 'orange' }, buttonIndicator: { position: 'absolute', top: 0, left: 0, width: 50, height: 50, justifyContent: 'center', alignItems: 'center' } });
paymentRequestWithCardFormAsync
に渡すオプションはiOSのみ有効です。住所などの入力を必須としたり、UIの色を変更できたりします。まずはこれをejectせずAndroidで確認してみます。ボタンをタップすると、
カード入力画面がモーダルで表示されました。
テストカードを入力し、
先ほど用意したサーバー側の処理を含めて成功したら、ダッシュボードを確認。
無事決済が完了しています。Bare WorkflowでPaymentsを確認
さて、ここまでは簡単なのですが、前述した通りiOSでは完全にejectが必須、あるいは最初からBare Workflowで使用することになります。また、Androidのカード入力画面を日本語化するためにもejectが必要になります。
ということで、ここからはBare WorkflowやExpoKitを使用したReact Nativeプロジェクトを前提とします。
それに伴ったビルドの流れやデバッグなどはこの記事の要旨から外れるので割愛するとして、iOSでの動作確認・設定項目、Androidでの言語設定の方法を説明していきます。iOSで確認
Expo Clientで表示すると
setOptionsAsync
の時点でTypeError: Cannot read property 'init' of undefined
というエラーが発生すると思いますが、eject後、スタンドアロンビルドしてXCodeからビルドして確認するとすんなり使用できます。ボタンをタップすると、
カード入力画面が立ち上がります。住所欄にはpaymentRequestWithCardFormAsync
のオプションで渡した内容が表示されています。
テストカードを入力し、「Done」をタップすると、
問題なく決済ができました。
UIの色を変更
iOSのカード入力UIの色はオプションで変更することができます。
App.jsconst params = { // Only iOS support this options smsAutofillDisabled: true, requiredBillingAddressFields: 'full', prefilledInformation: { billingAddress: { name: 'Hanako Yamada', line1: 'line1', line2: 'line2', city: 'Yokohama', state: 'Kanagawa', country: 'JP', postalCode: '2440000', email: 'test@test.com', } }, theme: { // UIの色を変更 primaryBackgroundColor, secondaryBackgroundColor, primaryForegroundColor, secondaryForegroundColor, accentColor, errorColor } }; const source = await Stripe.paymentRequestWithCardFormAsync(params);とりあえず
accentColor
をブランドカラー的な色に変更するだけでも十分かなと思います。
日本語対応
上の画像のようにデフォルトではUIが英語になっているので、これを日本語に変更してみます。
tipsi-stripe
の下記のissueを参考にします。
https://github.com/tipsi/tipsi-stripe/issues/97iOSではXCodeのプロジェクト設定→「info」タブ→「Localizations」を開き、
1.Use Base Internationalization
をチェック(通常チェックされているはず)
2. 「+」で「Japanese」を選択し、「Finish」住所入力無しの場合
住所入力を必要としない場合は
requiredBillingAddressFields
を指定せず、App.jsconst params = { smsAutofillDisabled: true, theme: { accentColor: 'orange' } }; const source = await Stripe.paymentRequestWithCardFormAsync(params);Androidの設定
日本語対応
Androidでのカード入力画面は
node_modules/expo-payments-stripe/android/src/main/res/
内のリソースをandroid/app/src/main/res/
内のリソースで上書きするような形で設定が可能です。
上記のissueにacro5piano氏が紹介している例の通りにandroid/app/src/main/res/values/strings.xml
を以下のように変更してみます。android/app/src/main/res/values/strings.xml<resources> <string name="app_name">ここはアプリ名</string> <string name="gettipsi_card_number">カード番号</string> <string name="gettipsi_save">保存</string> <string name="gettipsi_card_cvc">確認番号(CVC)</string> <string name="gettipsi_google_pay_unavaliable">この端末では、 Google Pay はご利用になれません。</string> <string name="gettipsi_user_cancel_dialog">キャンセルしました。カードは追加されていません。</string> <string name="gettipsi_card_enter_dialog_title">カード番号を入力して下さい</string> <string name="gettipsi_card_enter_dialog_positive_button">完了</string> <string name="gettipsi_card_enter_dialog_negative_button">キャンセル</string> <string name="gettipsi_card_number_label">カード</string> <string-array name="gettipsi_currency_array"> <item>通貨 (任意)</item> <item>指定しない</item> <item>円</item> </string-array> </resources>ビルドして確認すると、このように文言が指定したものに変更できました。
色の変更など
colors.xml
を追加し、アクセントカラーを変更してみます。android/app/src/main/res/values/colors.xml<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorAccent">#FFA500</color> </resources>ボタンの色が変わります。iOSと違って、カード画像の色は変わりませんでした。
PNG画像を使っているので動的な変更は難しそうです。
同じ要領でandroid/app/src/main/res/drawable/stp_card_form_front.png
の画像を変更してみると、UIに反映されます。
tipsi-stripe関連で発生したエラーとその対応(Android)
ビルド時に
Could not resolve com.github.tipsi:CreditCardEntry
というエラーが発生しました。
JitPackというサービスが必要なようなので、build.gradle
に追加します。android/build.gradle// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { buildToolsVersion = "28.0.3" minSdkVersion = 21 compileSdkVersion = 28 targetSdkVersion = 27 supportLibVersion = "28.0.0" } repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { mavenLocal() google() jcenter() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url "$rootDir/../node_modules/react-native/android" } maven { // Android JSC is installed from npm url("$rootDir/../node_modules/jsc-android/dist") } maven { url 'https://www.jitpack.io' } // この行を追加 } } task wrapper(type: Wrapper) { gradleVersion = '4.7' distributionUrl = distributionUrl.replace("bin", "all") }終わり
以上です。ユースケースに従って実装方法を検討してみてください。
- 投稿日:2020-01-18T14:10:18+09:00
初心者による Array/Map/Setオブジェクト
Arrayオブジェクト
Arrayオブジェクトは、配列型の値を扱うためのオブジェクトで、配列に対して要素の追加/削除、結合、並べ替えを行うための機能を提供する。配列を生成するには原則、配列リテラルを利用する。
sample.jslet array = []のように定義する。
Arrayオブジェクトの主なメンバー
Arrayオブジェクトで利用できるメンバーは以下のようなものがある。
length : 配列のサイズ isArray(obj) : 指定したオブジェクトが配列にあるか toString() : 「要素 , 要素 , ...」の形式で文字列に変換 toLocaleString() : 配列を文字列に変換(区切り文字はロケールによって変化) indexOf(elem[, index]) : 指定した要素に合致した最初の要素のキーを取得(indexは検索開始位置) lastIndexOf(elem[, index]) : 指定した要素に合致した最後の要素のキーを取得(indexは検索開始位置) entries() : すべてのキー/値を取得 keys() : すべてのキーを取得 values() : すべての値を取得 concat(ary) : 指定配列を現在の配列に連結 join(del) : 配列内の要素を区切り文字delで連結 slice(start[, end]) : start + 1 ~ end番目の要素の抜出し splice(start, cnt[, rep[, ...]]) : 配列内の start + 1 ~ start + cnt番目の要素をrep...で置き換える from(alike, [map[, this]]) : 列挙可能なオブジェクトを配列に変換する of(e1...) : 可変長引数を配列に変換 copyWithin(target, start[, end]) : start + 1 ~ end番目の要素をtarget + 1番目からの位置にコピー fill(val, start[, end]) : 配列内のstart + 1 ~ end番目の要素をvalで置き換え pop() : 配列末尾の要素を取得、削除 push(data1[, data2, ...]) : 配列末尾に要素を追加 shift() : 配列先頭の要素を取得し、削除 unshift(data1[, data2, ...]) : 配列先頭に要素を追加 reverse() : 逆順に並べ替え sort([fnc]) : 要素を昇順に並べ替え //コールバック関数を使ったメソッド forEach(fnc[, that]) : 配列内の要素を関数fncで順に処理 map(fnc[, thst]) : 配列内の要素を関数fncで順に加工 every(fnc[, that]) : 配列内のすべての要素が条件fncに合致するか some(fnc[, that]) : 配列内のいずれかの要素が条件fncに合致するか filter(fnc[, that]) : 条件fncで合致した要素だけで配列を生成 find(fnc[, that]) : 関数fncが初めてtrueを返した要素を返す findIndex(fnc[, that]) : 関数fncが初めてtrueを返した要素のインデックス番号を取得 reduce(fnc[, init]) : 隣同士の要素を左から右へ関数fncで処理して単一の値にする reduceRight(fnc[, init]) : 隣同士の要素を右から左へ関数fncで処理して単一の値にするかなり多くのメソッドがあるが、どれもC++などにもライブラリで備えてあるものである。違いとしては、Arrayオブジェクトはスタック/キューの区別がないことが挙げられる。コールバック関数を使ったメソッドは使用頻度が高い印象。
Mapオブジェクト
Mapオブジェクトは、キー/値のセット、いわゆる連想配列を管理するためのオブジェクトです。
sample.js//mapの定義 let m = new Map() //アクセス for(let [key, value] of m){ console.log(key) console.log(value) }Mapオブジェクトの主なメンバー
Mapオブジェクトで利用できるメンバーは以下のようなものがある。
sample.jssize : 要素数 set(key, val) : キー/値valの要素を追加。重複時は上書き。 get(key) : 指定したキーの要素を取得 has(key) : 指定したキーの要素があるのかを判定 delete(key) : 指定したキーの要素を削除 clear() : すべての要素を削除 keys() : すべてのキーを取得 values() : すべての値を取得 entries() : すべてのキー/値を取得 forEach(fnc[, that]) : マップ内の要素を関数fncで順に処理Setオブジェクト
Setオブジェクトは、重複しない値の集合を管理するためのオブジェクトです。
sample.js//Setの定義 let s = new Set() //アクセス for(let val of s){ console.log(val) }Setオブジェクトの主なメンバー
Setオブジェクトで利用できるメンバーは以下のようなものがある。
sample.jssize : 要素数 add(val) : 指定した値を追加 has(key) : 指定した値の要素があるのかを判定 delete(key) : 指定した値の要素を削除 clear() : すべての要素を削除 values() : すべての値を取得 entries() : すべてのキー/値を取得 forEach(fnc[, that]) : マップ内の要素を関数fncで順に処理参考資料
山田祥寛様 「javascript本格入門」
- 投稿日:2020-01-18T12:04:35+09:00
クライアントサイドだけで日本語PDFを出力したときの文字化け・改行不具合をpdfmake最新版で再ビルドして直した話
はじめに
- 常用文字にサブセット化を行ったものを id:naoa_yさんが作ってくれているのでそれを使っても良い
- ビルド後のファイル(vfs_fonts.js)で 5MB -> 2.3MBぐらいに減ってる
- ただしbuildが5年前なのでpdfmakeのバージョンが0.1.20と古い
- 最新は0.1.63 (released this on 11 Dec 2019)
- 上記のpdfmake@0.1.20だと日本語化したときの文字改行(word wrap)が適切にうごかない&改行されたとき文字化けする不具合があった
- 私の場合、可変長の文字を動的に組み込む必要があり、自動改行はどうしても必要だった。1行に収まる短文のみであれば問題ないかと
- 最新だとなおるかもしれないと、pdfmakeを最新にして使いたかったので、buildし直した。結論、治った!
- Vueと書いてるけど、フォント差し替えるまでは共通だとおもう
- @watameさんの記事を参考にしました。ありがとうございます。
TL;DR
- このリポジトリにある、
build/pdfmake.min.js
,build/vfs_fonts.js
を使えばよいです。2つ合わせると10MB程度になります- https://github.com/yazashin/pdfmake/tree/0.1.63-ja
- フォントサイズを減らしたい・違うフォントにしたいって人は下記を読みましょう
日本語対応pdfmakeをビルドする
fork&cloneする
- https://github.com/bpampuch/pdfmake
- よしなに自分のリポジトリへ
cloneした後branchを0.1系に切り替える
- "This is unstable master branch for version 0.2.x, for stable version 0.1.x see branch 0.1."とのこと
日本語フォントを入手する
- ここは好み・用途によるとおもいますが、私のケースではこのフォントがマッチしました
- https://ipafont.ipa.go.jp/#jp
- IPAexゴシックのみ利用(等幅フォント)
- メリデメを考えた時、メリットが勝ったのでこれを選択
- 用途として、珍しい漢字もつかわれるのでサブセット化したとき文字化けの発生が怖い
- pdfmakeとvfs_fontsで合計して10MBぐらいになる
- pdfmake.min.js 1.2MB / vfs_fonts.js 8.1MB
- 印刷ページごとに都度読み込みタイプだと辛い印象がある
- vueのようなSPAでindex.htmlにscriptタグでglobalに登録してしまえば、大きな問題にはならないって考えて進めた
- ぶっちゃけ最近はネット回線高速なので、SPAで初回ロード時に1回発生する10MB程度ならほとんど気にならないと思う
- 安定したらServiceWorkerのキャッシュ対象にしてしまうのもありだとおもう
- SEO気にする必要がない業務系サービスなのでこの判断ができている。っていう前提があります。
- むしろSEO気にするサービスで、SPA & PDF生成が必要って、かなりニッチだとはおもうけどw
フォントを入れ替える
vfs_fonts.jsの更新
- buildFontsを実行
$ yarn install .... success Saved lockfile. ✨ Done in 216.37s. $ ./node_modules/.bin/gulp buildFonts [10:45:48] Using gulpfile ~/workspace/pdfmake/gulpfile.js [10:45:48] Starting 'buildFonts'... [10:45:48] Finished 'buildFonts' after 189 ms
- pdfmakeへフォントを反映
$ yarn run build ... WARNING in webpack performance recommendations: You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application. For more info visit https://webpack.js.org/guides/code-splitting/ [10:50:16] Finished 'build' after 23 s ✨ Done in 24.49s.
- warning出てるけど無視して進める
vfs_fonts.jsの確認
Vue(typescript)で確認
- 今回つくったpdfmake & vfs_fonts は、npmとして登録はせず、publicなフォルダに静的jsとして利用する
- npmにするとdeploy時にciでビルドするとき、フォントのビルドから必要になるとか、そこらへん面倒でこうした
- vue-cliのversionによって構成かわりますが、
VUE_PROJECT_DIR/public/static-js/
配下においたindex.htmlに登録
- ポイントして、
defer
を忘れずにつけましょう。10MB近いjsの読み込みでHTMLのパースが止まってしまいUXに影響がでますindex.html... ~~~ ... <div id="app"></div> <!-- built files will be auto injected --> </body> <script defer src="/static-js/pdfmake.min.js"></script> <script defer src="/static-js/vfs_fonts.js"></script> </html>Vueの実装
- Vueで使うときはこんなかんじ。手打ちなので間違ってるかもしれません。
pdf.vuedeclare var pdfMake: any; @Component({}) export default class PrintPdf extends Vue { createPdf() { pdfMake.fonts = { IPA_gothic: { normal: 'ipaexg.ttf', bold: 'ipaexg.ttf', italics: 'ipaexg.ttf', bolditalics: 'ipaexg.ttf', } }; const defaultStyle = 'IPA_gothic'; const docDefinition = { content: 'Hello 日本語!', defaultStyle: { font: defaultStyle } } pdfMake.createPdf(docDefinition).print(); } }Thanks
- 投稿日:2020-01-18T12:03:32+09:00
じゃんけんプログラムを改変してた
- 投稿日:2020-01-18T11:55:21+09:00
Node.js の基礎とそのフレームワーク Express
Node.js とは何なのか
Node.js とは、簡単にいうと JavaScript をサーバーサイドで実行させてくれる存在です。フロントエンドも、バックエンドも1つの言語で実行でき、WEBサービス、スマートフォンアプリ、IoT関連の開発にも使用することができるみたいです。
インストール
まず Node.jsをインストールしていない人は、[Node.js公式サイト](https://nodejs.org/en/) から、LTS版をダウンロードしてインストール。
『node』 でJavaScriptを実行させる
コマンドラインでindex.jsが格納されているフォルダにpwdで移動し、そのフォルダ内のindex.jsを実行する。
node index.jsnodeだけでEnter押すと、JavaScriptコンソールで色々なコードを試せるようになるみたい。やめるときは .exit と入力するか、control + C を2回押すと終了します。
Node.js のいろんなAPI
Node.jsには様々な機能があり、その一覧が公式サイトのdocumentationで見れます。
https://nodejs.org/dist/latest-v12.x/docs/api/APIの1つ。 『File System』
FileSystemは、ローカルファイルとかにアクセスすることができるAPI。
今回は、Node.js の File System を使用して、ローカルファイルのコピーを使用します。
FileSystemを読み込む
qiita.js//jshint esversion:6 const fs = require("fs");※ const とは、var のようなもの。しかし、var が後から変更できるのに対して、constは後から変更できることができず、一度データを格納(代入?)するとずっとそのまま。
※コメントがないと、"const" is available in ES6 というエラーメッセージが出てきますが、コメントで//jshint esversion:6と記載することでエラーメッセージを回避することができます。
ファイルのコピーを作成する
qiita.js//jshint esversion:6 const fs = require("fs"); fs.copyFileSync("file.txt","copyfile.txt"); //fs.copyFileSync("コピーするファイル","コピーされたもののファイル名");※ file.txtのコピーを作成して、copyfile.txtを作成します。って意味。 ちなみに、すでにcopyfile.txtという名前のファイルが存在していた場合、上書き保存されるので注意。
コマンドラインで、実行する
node qiita.js※今回は、qiita.js というファイルにJavaScriptコードを記載していたので、そのファイルをコマンドライン上で実行する。
Expressフレームワーク
node.js をより使いやすくしたのがExpress フレームワークらしい。めっちゃざっくりだけど。
Expressのインストール
retrieved from Express.jp 公式サイト
https://expressjs.com/ja/starter/installing.html最後のところは『 npm install express 』だけでOKみたい。
Express をjsファイルで使用できるようにする
requireでExpressをロードし使用できるようにします。ついでにexpress()もappに格納します。
qiita.jsconst express = require("express"); const app = express();Expressでサーバーを作成する
ここが割と理解に苦しんだところ。
手順
========================
(JavaScript上)
1.まずExpressをロード
2.アクセスされた時のRespondを考える(ページ別)
3.listenでサーバーを指定
(コマンドラインにて)
4.node server.js で実行
(ブラウザ上にて)
5.http://localhost:3000/と入力し、サーバーに接続
6.respondの処理が実行される
(コマンドラインにて)
7.control + C で終了する========================
これが一応の流れで、下記が実際のコード。
server.js// jshint esversion:6 // expressをロード const express = require("express"); const app = express(); // localhost:3000 にアクセスされた時のRespond app.get("/",function(request, respond){ //ここでconsole.log(request);とするとrequestの内容みれる respond.send("Hello World"); }); // localhost:3000/profile にアクセスされた時のRespond app.get("/profile", function(request, respond){ respond.send("My name is Kibinag0. I'm from Japan."); }); //listenで待ち受け状態にする app.listen(3000, function(){ console.log("Server started on port 3000"); });・Request
ブラウザ?からサーバーにリクエストされる情報。
・Respond
サーバーにリクエストが来た時に、反応して実行する処理。
ちなみにサーバーにはスレッドモデル(Apache等)とイベントループ(Node.js等 シングルスレッドともいう)という種類があり、それぞれの違いはこちらが分かりやすかったため、参照。
https://dotinstall.com/lessons/basic_nodejs/26202・listen()
listenで待ち受け状態にする。このlistenがあることによってrequestを受け取ることができるようになる。
『res.sendFile』を使用してHTMLファイルでRespondする
今までは、"Hello world"とかいう文字列でRespondしていましたが、今回はres.sendFileを使用してHTMLファイルでRespondします。
server.js// リクエストがあったら、index.htmlファイルをrespondする app.get("/", function(req, res){ res.sendFile(__dirname + "/index.html"); });(__dirname + "/index.html")は、dirnameをちゃんと取得して、そのindex.htmlがどのディレクトリに格納されているのかを教えてあげる必要があるんですね。
body-parserを使用してformデータを取得する
HTMLファイルでFormを使用して、ユーザーにデータを入力してもらう。そして、そのデータを使用して何か処理を行う場合の方法です。
事前にbody-parserをインストール
npm install body-parserHTMLでフォームを作成
index.html<h1>Calculator</h1> <form class="" action="/" method="post"> <input type="text" name="num1" placeholder="First Number"> <input type="text" name="num2" placeholder="Second Number"> <button type="submit" name="submit">Calculate</button> </form>このフォームをサーバー側で
calculator.js// jshint esversion:6 // ロードする const express = require("express"); const bodyParser = require("body-parser"); const app = express(); // body-parserの使用 app.use(bodyParser.urlencoded({extended: true})); // index.html でrespondする app.get("/",function(req,res){ res.sendFile(__dirname + "/index.html"); }); // index.htmlのフォームでpostされた部分 app.post("/",function(req, res){ // bodyの中のnum1, num2を取得する var num1 = Number(req.body.num1); var num2 = Number(req.body.num2); var result = num1 + num2; res.send("The result of the calculation is " + result); }); app.listen(3000);Udemy AngelaさんのWEB DEVELOPMENT COURSE 『206. Processing Post requests with body-parser』より
サーバー更新自動化『nodemon』のすすめ
ちなみに、上記だとserver.jsファイルを更新するたびにコマンドラインでサーバー終了して、もう一回立ち上げることになります。それって結構めんどくさいですよね。
そんな人のために、Udemy講師のAngelaさんが役立つツールnodemonを教えてくれました。
Angelaさんの講義はこちら→ Udemy Bootcamp web developmentnodemonを使うと、server.jsファイルを上書き保存するたびに検知してくれて、自動でサーバーを更新し、反映させてくれる優れものです。
インストール
インストールの仕方はコマンドラインで
npm install -g nodemonと記載するだけ。
※permission error が出たら
sudo npm install nodemon -gで対応。
使い方
nodemon server.jsとコマンドラインで記載すると、server.jsファイルの更新を自動反映してくれます。
以上 Node.jsとExpress。
結構難しいかったので、追加、修正を随時していきます。。
- 投稿日:2020-01-18T11:28:35+09:00
初めての Azure Bot Service - Sample code と local test 編 -
はじめに
今回は Azure Bot Service を使用して連続した対話に対応できる Bot を作成します.
Azure Bot Service を初めて使用する人を(できる限り)想定してます.
(熟練の方々からしたら退屈するかと思います.)また,今回は Bot の sample を local test するところまでで
Azure への Deploy については次回行います.(訂正や指摘などあれば,温かく教えていただけると助かります.)
私の開発環境
- PC : surface laptop2 (windows 10)
- Editor : VS Code
PC に関しては何でも大丈夫です.
Editor に関しては 本記事を参考にされる場合は VS Code をご使用ください.
(後程お話しますが, Azure への deploy 時に VS Code の Extension を使用しています)Azure Bot Service の概要
Azure Bot Service とは,Microsoft 社のクラウドサービスである Azure のサービスの1つです.
公式の document は以下をご参考ください.Azure Bot Service のリソースを作成すると,セットで Azure App Service のリソースが作成されます.App Service とは同じく Azure のサービスの 1 つで,Web Application を作成することの出来るサービスです.
Bot の基本
まずは,Bot が初めての方,Auzre Bot Service に初めて触れる方のためにいくつかの用語や実装法の紹介をします.
専門用語
Azure Bot Service を使用する上で参考になる用語をいくつか先にご紹介しておきます.
- channel
- Bot への通話手段.窓口のようなイメージ.
- User は channel を通じて Bot App と会話をします.
- Teams や LINE, Slack,Web chat などがあります.
- channel によって仕様が異なるため,実際の Bot 開発では channel が何かも大事.
- connecter
- channel と Bot アプリを繋ぐ.
- 実は Azure Bot Service の実態はこの部分(だと思われる)
- Bot App
- 実際に処理を行う部分.
- 実態としては,一般的な Web App と変わらない
- Azure Bot Service の場合は App Service に相当します
ご存知の方もいらっしゃるかもしれませんが,Azure Bot Service 自体は Bot Application ではありません.channel を通してきたメッセージを Bot へと伝える connecterの役割を担っています.
Bot の実装方法
Bot アプリを作成する場合,Line や Slack などの channel ではなく,実装の観点で話すと2つの実装方法があります.
- function 型
- AWS lambda や Azure Function などの Serverless Function を活用した場合
- 非常にシンプルな作り
- Function を使用した回数だけ課金対象になるので使用頻度によっては非常に安価
- Server 型
- 今回のAzure Bot Service(厳密には Web App Service)のような場合
- serverを起動している間機能する
- State(変数の値など)を保持することができる
Azure Bot Service を使用した場合は,下の server 型になります.
function 型では リクエスト(Bot の場合は Bot への呼びかけ)が発生する度に function が実行されます.一方,server 型では server が起動している間のみ応答を返します. これらの特性の大きな違い(の 1 つ)は server側で変数の値などを保持してステートフルな会話が可能なことです.
ステートフルな会話とは簡単に言うと一連の流れを記憶した状態で会話ができるということです.
Serverless Function であれば,基本的には 「1 Question : 1 Answer」 であるのに対して,ステートフルな実装であれば「 n Question : m Answer」と複数の情報から返答を返すことができます.
とりあえずここでは,ざっくりと下図のようなイメージだけで大丈夫です.
(私も厳密な定義は微妙です(笑))sample の実行
それでは実際に sample を動かしつつ確認していきます.
sample コードの取得
以下の Github から sample コードを取得します.
Azure Bot Service の sample Code (Github)
今回は連続した会話フローのsample (Node.js版) を使用します.
先ほど取得したコードのうち,以下の directory を開いてください./BotBuilder-Samples/samples/javascript_nodejs/05.multi-turn-prompt/少しコードを見てみましょう.
プログラムを実行すると index.js が実行されます.
まずは index.js をみてみます.一部抜粋したものを記載しておきます.index.js// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. const restify = require('restify'); const path = require('path'); // Import required bot services. // See https://aka.ms/bot-services to learn more about the different parts of a bot. const { BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } = require('botbuilder'); // Import our custom bot class that provides a turn handling function. const { DialogBot } = require('./bots/dialogBot'); const { UserProfileDialog } = require('./dialogs/userProfileDialog'); // Read environment variables from .env file const ENV_FILE = path.join(__dirname, '.env'); require('dotenv').config({ path: ENV_FILE }); /********** 中略 **********/ // Define the state store for your bot. // See https://aka.ms/about-bot-state to learn more about using MemoryStorage. // A bot requires a state storage system to persist the dialog and user state between messages. const memoryStorage = new MemoryStorage(); // Create conversation state with in-memory storage provider. const conversationState = new ConversationState(memoryStorage); const userState = new UserState(memoryStorage); // Create the main dialog. const dialog = new UserProfileDialog(userState); const bot = new DialogBot(conversationState, userState, dialog); // Create HTTP server. const server = restify.createServer(); server.listen(process.env.port || process.env.PORT || 3978, function() { console.log(`\n${ server.name } listening to ${ server.url }.`); console.log('\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator'); console.log('\nTo talk to your bot, open the emulator select "Open Bot"'); }); // Listen for incoming requests. server.post('/api/messages', (req, res) => { adapter.processActivity(req, res, async (context) => { // Route the message to the bot's main handler. await bot.run(context); }); });前半部分では必要な module を import したり,
sample プログラムの別の js ファイルでインスタンスを作成しています.HTTP server を create しているところでは,PORT の設定なんかもしていますね.
また,最後の部分で post する際のエンドポイントを設定しています.
今回はしませんが,Proactive なメッセージ(push 通知など)を実装する際には,
同様の方法で Proactive 用のエンドポイントを用意することもあります.続いて,実際の会話フローの制御をしているプログラムを確認しましょう.
/dialogs/userProfileDialog.js
を開いてください.一部抜粋したものを記載しておきます.userProfileDialog.jsconstructor(userState) { super('userProfileDialog'); this.userProfile = userState.createProperty(USER_PROFILE); // 典型的な会話のパターンを用意してくれている this.addDialog(new TextPrompt(NAME_PROMPT)); this.addDialog(new ChoicePrompt(CHOICE_PROMPT)); this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT)); this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator)); // ここで連続する会話を追加している this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [ this.transportStep.bind(this), this.nameStep.bind(this), this.nameConfirmStep.bind(this), this.ageStep.bind(this), this.confirmStep.bind(this), this.summaryStep.bind(this) ])); this.initialDialogId = WATERFALL_DIALOG; }
TextPrompt
やNumberPrompt
はその名の通り,テキストや数値入力などの典型的な会話のパターンを実装するために用意されています.そして,抜粋した code の後半部分にある
WatarfallDialog
に様々な dialo を add することで,add した dialog が一連の会話として扱われます.ここで add されている dialog は抜粋部分より後ろの sample code で上記の Prompt を継承して実装されたものです.local test
まずは Azure に deploy(ざっくり言うとプログラムを Azure 上で動作させる) せずに,local で動作テストできるようにします.以下のリンクより Bot Framework Emulator の exe ファイルを取得して実行します.
ちょっと分かりにくいですが,ページ中頃にある以下の画像ようなファイル群から自分に適したものを入手してください.
上 4 つのどれかで大丈夫なはずです.
Bot Framework Emulator が起動できること(アプリが実行できて window が開くこと)を確認したら,sample program のある場所で command prompt やら Power Shell やらのコマンドツールを開きましょう.何かしらのコマンドツールを開けたら,
npm install
とnpm start
を実行してください.すると,以下のような画面になります.
この画面になり,Bot App が Listen 状態になったことを確認したら,
先ほどの Bot Framework Emulator を実行してOpen Bot
を押してください.
そして,URL の欄にhttp://lovalhost:3978/api/messages
と入力します.
これは sample code で index.js の最後に書かれているserver.post
のエンドポイントです.
このポイントに http post リクエストを送ることで Bot と会話します.こんな感じですね!入力したら Connect してください.
無事にsample を実行できたら,何か話しかけてみましょう.
下の gif のように会話ができると思います.ちなみに,Bot を停止させたい時は shell の画面で
ctr+c
を押してください.次の内容
少し長くなりそうなので今回はここまでで終了とします.
もう1回くらいは書こうと思うので,予定している内容を書いておきます.
- Dialog の分岐
- 今回は決まった会話内容でしたが,次回はもう少し複雑に会話を分岐させます.
- Azure への Deploy
- 今回は localtest で終わりましたが,次回は実際に Azure に Deploy します.
- channel の設定
- とりあえず Teams との連携でも目指します.
(よくよく考えたら今回まだ Azure Bot Service 使ってない説)
- 投稿日:2020-01-18T09:32:51+09:00
`let`にあんまり知られてなさそうな性質が結構あった話
前書き
Afor (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 100); } // 5 5 5 5 5Bfor (let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 100); } // 0 1 2 3 4この2つのコード、
var
とlet
の違いとしてそこそこ有名なものだと思います。この動作自体に文句はありませんし、自分もそういうもんだと思って使っていたのですが・・・
よくよく考えるとBのコード上では
console.log(i)
はlet
で宣言された変数i
を参照しているlet
で宣言された変数i
は、for
文によってインクリメントされているconsole.log(i)
が実行されるのは、for
ループが完了した後であるという処理になっているため、Aのコードと同じ結果になるはずです。
MDNにあるような
let
の性質だけでは、
この部分の説明がつかなくて気になってしまったというのが発端で、
色々試していた+調べていたらそこそこな量になったのでまとめました。ただ、知られてなさそうな性質として気になったのが全て
for
と組み合わせた時に表れる性質なので
実用性はそんなにないと思います。よく知られているであろう性質
ブロックスコープである
{ let a = 1; console.log(a); // 1 } console.log(a); // Uncaught ReferenceError: a is not defined
let
宣言するとスコープがブロック内に制限されるやつです。どこのサイトでも載ってる基本中の基本。
同一ブロック内での再宣言不可
{ let a = 1; let a = 2; } // Uncaught SyntaxError: Identifier 'a' has already been declared
let
宣言すると、同一スコープで再宣言ができません。
これはvar
での再宣言でも同じくエラーになります。{ let a = 1; { let a = 2; console.log(a); } console.log(a); } // 2 1のように、ブロック内の別ブロックであれば
再宣言ではなく新規の変数宣言扱いになるので、エラーは出なくなります。これも基本。
for
文のループブロックは、それぞれが別のブロックスコープになるfor (let i = 0; i < 5; i++) { let a = i; console.log(a); } // 1 2 3 4 5仮に1つの
for
文における全ループブロックが単一のスコープであれば
let a
での再宣言でエラーが発生しているはずですが、
エラーは発生していないのでそういうことなのです。この辺りまでは調べればそこそこ出てきます。
巻き上げ的な挙動はあるが、(巻き上げが問題になるようなコードであれば)エラーになる
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization let a;undefined の値で始まる var 変数と異なり、 let 変数は定義が評価されるまで初期化されません。変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。
定義を評価というのが具体的に何を指すのかちょっと曖昧な気もしますが
console.log(a); // ひとつ上のコードとはちょっと違い、変数が存在しないエラー // Uncaught ReferenceError: a is not definedlet a; console.log(a); // undefinedという挙動から考えると、
let
宣言された行に到達=定義を評価と考えて良さそうです。
宣言自体は巻き上がっておらず、ブロック開始時点で宣言前の準備が行われている、という感じですね。いずれにしても、このエラーが発生するようなコードには構造上の問題があると考えた方が良いと思います。
これを知ってたら中級くらい? 実務上で遭遇することは結構ありそう。
あんまり知られてなさそうな性質
本題です。
冒頭にも書きましたが、以下はfor
文との組み合わせの際に表れる性質です。「
for
文の存在するブロックスコープ」と「for
ループブロックスコープ」の間に、見えないブロックスコープが存在する(ような挙動をする){ // スコープ① for (let i = 0; i < 10; i++) { // スコープ② i++; console.log(i); } console.log(i); } // Uncaught ReferenceError: Cannot access 'i' before initializationエラーが出ていることから、
for
文の初期化部はスコープ①ではないことが分かります。仮にスコープ②であったとしたら、各ループブロック内での
let i
宣言はブロックごとに個別の変数になるので、
i++
でループ数が減少する説明がつきません。なのでイメージとしては
{ { let i; for (i = 0; i < 10; i++) { i++; console.log(i); } } console.log(i); }のようなコードの挙動が近しいです。
※このコードはあくまでイメージです。
後述する別の性質を破壊してしまうので、使ってはいけません。
for
文において、ある条件下でのみ「別の変数の参照(のような挙動)」が起こる発端となった挙動の原因です。
なぜこうなるのか
それらしい記載のあるサイトがあまりなかったのですが
つまり、for (let...)ループが複数回実行され、そのループにクロージャーが含まれている場合(話している猫の例のように)、すべてのクロージャーが同じループ変数をキャプチャするのではなく、各クロージャーがループ変数の異なるコピーをキャプチャします。
とのこと。
発生する条件は異なりますが、関数内で
arguments
という変数が自動で使えるようになっていたり、
状況に応じたオブジェクトがthis
に束縛されているのに近しいイメージでしょうか。確かに
for (let i = 0; i < 5; i++) { setTimeout(() => { i *= 2; console.log(i); }, 100); } // 0 2 4 6 8と、各クロージャ内での
i
は、それぞれ別のi
として存在しているように見えます。これなら確かにBの挙動になるのも頷けるのですが・・・
色々試してみる
色々なコードを試してみるうちに、この説明もちょっと違っている気がしてきました。
(5年前の記事なので、当時とパーサの解釈が違う可能性はありますが)即時関数
for (let i = 0; i < 5; i++) { (() => { i++; console.log(i); })(); } console.log('end'); // 1 3 5 endクロージャ内の処理ではあるものの、普通に
for
文の初期化部のlet i
を参照しちゃっているようです。関数定義+同期実行
for (let i = 0; i < 5; i++) { function f() { i++; console.log(i); }; f(); } console.log('end'); // 1 3 5 endやってることは即時関数と変わらないので、まあこうなるよなといった印象。
Promise
のexecutor
for (let i = 0; i < 5; i++) { new Promise(resolve => { i++; console.log(i); }); } console.log('end'); // 1 3 5 end
Promise
と言えどexecutor
は同期的に実行されるので、まあこうなるよなといった印象。
Promise.then
for (let i = 0; i < 5; i++) { new Promise(resolve => { resolve(); }) .then(() => { i++; console.log(i); }); } console.log('end'); // end 1 2 3 4 5動いた。
ほぼ即時的な処理であっても非同期処理であればいいんだろうか?
setTimeout
のdelay = 0
for (let i = 0; i < 5; i++) { setTimeout(()=>{ i++; console.log(i); }, 0); } console.log('end'); // end 1 2 3 4 5こっちも
Promise.then
と同じような処理だしやはり動いた。
setInterval
for (let i = 0; i < 5; i++) { let c = 0; const p = setInterval(()=>{ i += 10; console.log(i); if (c++ === 1) { clearInterval(p); } }, 0); } console.log('end'); // end 10 11 12 13 14 20 21 22 23 24同一の関数を複数回実行しても、ちゃんとそれぞれのクロージャごとに個別の
i
を参照していそうここまでくると、クロージャが非同期処理として実行されることが条件のようにも思えますが・・・
クロージャを
for
文の全ループの完了後に実行const a = []; for (let i = 0; i < 5; i++) { a.push(function() { i++; console.log(i); }); } console.log('end'); for (let j = 0; j < a.length; j++) { a[j](); } // end 1 2 3 4 5非同期処理ではないですが、同期処理としての順番を変えたら参照する変数が変わっているようです。
念のため、外部スコープの配列に格納しているせい、という可能性もあるので・・・
const a = []; for (let i = 0; i < 5; i++) { a.push(function() { i++; console.log(i); }); a[a.length - 1](); } console.log('end'); // 1 3 5 end
let i
参照に戻っているので、特に関係はなさそうです。新たな参照先が生成されるタイミング
クロージャがそれぞれ個別の
i
を生成するタイミングも調べておきます。for (let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 10); i++; } // 1 3 5ちょっとわかりづらいですが、
let i = 0
(0)- クロージャ定義 (0)
- ループ内最下部の
i++
(1)for
文の更新部のi++
(2)- クロージャ定義 (2)
- ループ内最下部の
i++
(3)for
文の更新部のi++
(4)- クロージャ定義 (4)
- ループ内最下部の
i++
(5)for
文の更新部のi++
(6)for
文終了 (6)()内は そのときの
i
の値です。どうやら、クロージャが定義された瞬間の
i
ではなく、
クロージャ定義後のi++
が反映された状態のi
が新たな参照先変数の値になっているようです。結論
ということで、Bのような動作をする理由は、
1.
for
文の初期化部で変数がlet
宣言されている(仮にlet i
とする)
2.for
ループブロック内で関数(クロージャ)が定義されている (仮にf
とする)
3.f
の内部から、i
が参照されている
4.for
ループ処理が完了した後にf
が実行されているの全ての条件を満たした場合
f
内で参照しているi
の値が、「f
実行時のi
の値」ではなく「f
が定義されたスコープの末端に到達した時点でのi
の値」に束縛されるという性質に当てはまっているコードだから、ということなのではないかと思います。
ちなみに条件の
1
についてですが、let
での宣言をfor
文の初期化部以外にしてしまうとlet i; for (i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 10); } // 5 5 5 5 5と、クロージャごとに個別の参照先にならなくなります。
あくまでfor
文の初期化部でlet
宣言した時のみ、の性質ですね。起こりえる問題
for (let i = 0, finished = 0; i < 5; i++) { setTimeout(() => { if (++finished === 5) { console.log('finished'); } }, Math.random() * 100); }
for
文の初期化部は独立したスコープになっている、という性質を考えると
for
ループ内からのみアクセスできる変数が欲しいみたいなときに、
for
文の初期化部に宣言しちゃうことがあるかもしれません。上のコードでは、
finished
を「完了した非同期処理の数を表すカウンタ」のつもりで定義しています。
ですが、この性質のせいでfinished
もクロージャごとに個別な存在になってしまい、let finished
はいつまで経っても5になりません。初期化部で複数の変数を宣言するコードは割と見かけるので、意外とハマる可能性はあるかも?
余談
ずっと
for
文で説明してきましたが、for-in
やfor-of
でも同様の挙動になります。また
for-in
とfor-of
では、let
の代わりにconst
を使用することができますが、
この場合もやはり同様の挙動になるようです。まとめ
なんだか
let
の性質というより、for
文の性質になってるような気がしつつ・・・。
javascript
にもまだまだ知らないこと沢山だなー。
- 投稿日:2020-01-18T01:49:44+09:00
色空間を 3D で可視化した Web アプリで色への理解を深める
0. はじめに
色に関して「ちゃんと知りたい・理解したい」と思っている方、実は多かったりしないでしょうか。
私は以前、下記のようなもやもやした気持ちを抱いていました。
- 色が見える根本的な仕組みを知りたいけど、なんか難しそう
- 「RGB」とかって、なんとなく分かるけどよく知らない
- 「彩度」と「明度」という言葉をたまに聞くけど、ぶっちゃけよく分からない(でも日常生活は困らない)
- 資料作成やデザイン関連作業で、色をどうやって選んだらいいか分からない・なかなか出したい色にたどり着けずイライラする
本記事では、上記のような色に関する素朴な疑問の解決(もしくは解決への糸口)を可能な限り目指して、色及び色空間に関する基礎的な内容をご紹介したいと思います。
背景としては、この年末年始に下記動画のような 色空間を 3D で可視化する Web アプリケーション ―ColorSpace― を趣味で開発したこともあり、開発の目的だった「色空間の直感的な理解」を一人でも多くの人にしていただけたら嬉しい、という想いがあります。
本記事はまず基礎的な内容として、「1. 色が見える仕組み」に関してざっくりふれ、色の表現方法である「2. 表色系と色空間」に関する解説をし、web やソフトウェアの世界で一般的と思われる RGB と HSV 色空間に関して説明いたします。最後に「3. アプリケーションの説明」にて今回開発したものの背景・概要を解説いたします。
「1. 色が見える仕組み」「2. 表色系と色空間」は特に基礎的な内容であるため、画像処理に関わっている方など色に十分詳しい方はターゲットとしてはいませんが、「3. アプリケーションの説明」はもしかしたらお楽しみ頂けるかもしれません。
また、「色選びが難しい理由」に関してはちょっとだけ触れますが、配色と呼ばれる色選び自体に関しては本記事では触れません…配色関連の書籍は数多くあるので、それらを参照していただければと思います。Web アプリ閲覧にあたっての注意事項
Web アプリ閲覧にあたってはちょっとした注意事項があります ①スマホ・タブレットでもキレイに表示されるよう実装していますが、 GPU をゴリゴリ使うために消費電力が大きく、外出中はバッテリーの持ち時間が危うくなるかもしれません。 出先では少し覗くに留まり、もし気に入っていただけたら、お家に帰った後に お茶を飲みながらスマホ充電しつつ見るか、電源に接続された PC 等で ゆっくり眺めて頂ければと思います… ②最近のスマホ・PC・タブレット等の端末であれば、ほぼ問題なくぬるぬる滑らかに動作しますが、 端末の GPU の性能によっては、カクカク動いたりレスポンスが悪くて宇宙空間を漂うような 無重力感(というか無力感)を感じることもあります。すみません。上記2つの問題への対応として、GPU の負荷が小さくなる LITE バージョンも実装しました。
あくまで推奨は通常版ですが、あまりにもレスポンスが悪い場合には LITE バージョンを試していただければ少しはマシになるかもしれません。1. 色が見える仕組み
我々が色を見ることができるのは、物体に反射している光を目がキャッチし感知しているためです。光がない暗闇では我々は色を感じ取れません。つまり、目に届く光によって我々は物の色を知覚します。
光は波で出来ており1、我々が普段見ている太陽光や照明光は無数の様々な波長の光を含んでいます。その中で、我々が明るさや色を感じることが出来る波長の光は可視光線 (Visible Light) と呼ばれます。可視光線は波長の範囲が大体決まっていて、その範囲を外れた波長のものは我々人間の目には見えず、明るさも色も感じられません。そこいら中に飛んでいるはずの電波が全く目に見えないのと同様(光は電波と同じく電磁波の一種です)、太陽光の中に含まれる赤外線や紫外線も我々には見ることが出来ません2。
可視光線を、連続した波長ごとの成分に分解させたものをスペクトル3と呼び、虹やスマホに反射した光など、日常生活でも見ることができます4。可視光線がカラフルに分解されて見えるこの現象は、光は波長の違いによって別々の色に見える、ということを示しており、「可視光線上のそれぞれの波長の光」は特定の色に紐付いている、ということが分かります。例えば 400nm の波長の光は紫色、600nm の波長の光はオレンジ色、といった具合です。下の図は、スペクトルとして現れる色を波長と共に示した概略図で、波長と色のひも付きを示しています。図上の右端はおおよそ 760nm の波長の光で、この波長の光は濃い赤色に見えるということが読み取れます5。一つだけの波長からなる光を単波長光と言いますが、単波長光は特定の色に決まることから単色光 (monochromatic light) とも呼びます。
(出典:"LED Wavelength Vs. LED Color Temperature" from Fireflier Lighting Web site)スペクトル上に見ることのできる単波長光だけが我々の見える全ての色をカバーするわけではありません。例えば、白をはじめとしてピンクや深い緑色などは上図の通り、スペクトル上には見えません。しかしそれらの色も、スペクトル上のたった 3 つの色だけで全てを作り出せるのではないか、とトーマス・ヤングが仮説を唱えました。
今現在ではそれがほぼ事実であることが分かっています。人間の視細胞には三種類あってそれぞれ異なる波長の光刺激に強く反応し、それらの刺激の混ぜ合わせによって見える色が決まる、という仕組みが明らかになっています。そしてヤングが唱えたように 3 つの単波長光でほぼ全ての色を表現できることも分かっています6。それら 3 つの単波長光の色が「光の三原色 (three primary colors) 」と言われる赤・緑・青( R/G/B )であり、三原色だけで我々の見えるほぼ全ての色は再現可能なのです7。
なお、様々な物がそれぞれ異なる色に見えるのは、物体によって、どの波長の成分を反射しやすくどの成分を吸収しやすいのか、という特性が異なっているためです。例えば、白い物体は可視光線の中のほとんどの波長の光を反射し、黒いものは可視光線をほぼ全て吸収する8、赤い物体は赤に近い波長のものだけをよく反射する、といった感じです。
以上が、我々が色を見ている仕組みです。
まとめ
- 人間は光を感知することで色が見える
- 光は無数の波長の成分からなり、目に見える範囲の光を可視光線という
- 可視光線の中のそれぞれの波長の光は特定の色と紐づく
- 全ての色は 3 つの波長成分だけからほぼ再現できる
- それらの 3 つは三原色と呼ばれる赤・緑・青である
2. 表色系と色空間
表色系とはなにか
前章の通り、人間に見える色は、赤・緑・青の三色の強さの混ぜ合わせによってほぼ表現可能なことが示されていますが、その事実を元に「それぞれの色をどのように表すか」ということを定めたルールを表色系 ( color system ) と言います。読んで字の如くです。ちょっと固く言うと、表色系とは任意の色をどのような形式で定量的に表すか、という体系のことです。
表色系には大きく分けて混色系と顕色系の二種類があります。
1. 混色系 (color mixing system)
赤・緑・青の三原色といった知覚の仕組みを元にした成分を定めて、それらの混ぜ合わせにより定量的に表す表色系です。RGB 表色系の他、XYZ 表色系、CMYK 表色系などがあります。
混色系は色の見える原理に根ざした体系であるために、カメラやディスプレイなど色を再現する機器で扱うための形式に適しています。カメラでの色の入力は、人間の視覚を再現したような形で R/G/B のそれぞれの光の強さを記録するように作られていますし、カラーテレビやカラーディスプレイでの出力では、ディスプレイ上の各ピクセルを R/G/B の光の強さを合成することにより、出力すべき色を再現して表示します9。
2.顕色系 (color appearance system)
「明るさ」「色合い」「鮮やかさ」といった、見た目の感覚として表現されうる属性を軸として、それらの属性値の組み合わせで表す表色系です。「色の三属性」と呼ばれる「色相」「明度」「彩度」の三属性で各色を表すマンセル表色系が代表的です。
顕色系は、人間が色を選んだり比較したりする際に有用です。別名カラーオーダーシステムと呼ばれるように、色を人間が自然に感じられるように順序良く並べた (order した) もので、色を探しやすく選びやすいように作られています。例えば、同じ赤系列でちょっとくすんだ色を表したい、といった場合や、「この赤と同じくらいの明るさ・鮮やかさの青系統の色が欲しい」といった際に数値としてわかりやすい表現となります。
色空間とはなにか
色空間 (color space) というのは、表色系で定められたルールによって導き出される全て要素からなる色の表現範囲のようなものです10。「RGB 色空間」といえば RGB 表色系によって具現化される全ての要素を含む集合です。
つまり表色系と色空間は、ほぼ同様の概念であると言えますが11、ソフトウェアなど工学関連分野では表色系よりも色空間という言葉がよく使われる気がします12。以下では、色空間としてソフトウェア関連では最も身近な RGB 色空間と HSV 色空間の 2 つを取り上げて説明いたします。
なお、表色系や色空間周りでは、「CIE-RGB 色空間」など CIE が頭につく語が多く登場しますが、CIE は国際照明委員会 (International Commission on Illumination13) のことで、現在広く用いられている RGB 表色系と XYZ 表色系を定めた標準化団体です。CIE が頭に付いているものは CIE が標準化したもの、という意味なので、ざっくりした説明のみの本記事では CIE 系であるか否かは特には触れません。
RGB 色空間
色の三原色である R/G/B の三種類の値の組み合わせで色を表現します。全てが 0 のときは光がない状態を意味して黒を表し、全てが最大値のときには白となります14。
(出典:"File:Synthese+.svg" from Wikimedia Commons)RGB 色空間は、人間の知覚に基づいた定量化方法であるために色を再現するための機器で扱いやすいという混色系のメリットが最大の長所で、画像処理などで画像データを扱う際は RGB のまま扱うことが多いです。
しかし、人間には R/G/B の値を色として直感的に捉えることが難しい、というデメリットがあります。普段我々の生活の中である色を見たときに、赤・緑・青の刺激が混ぜ合わさった状態のものを脳が認識しており、分割して感じているわけではないためです。例えば、鮮やかなピンク色を再現するためには、赤・緑・青をどれくらいの強さで混ぜ合わせたらいいのか、をすぐに思いつける人は少ないのではないでしょうか。また、現在見ている色を、色合いを変えずに明るくしたい、といった場合に、R/G/B のそれぞれの値をどのように変化させたらいいかも、直感的には分かりづらいかと思います15。
資料作成などで色を選ぶ際、下記のような R/G/B のそれぞれの値を変化させるものを触ったことのある人も多いかと思いますが「なんだか扱いにくい」と感じるのは、仕組み上自然なことだと思います。
HSV 色空間
HSV 色空間に代表される顕色系は、RGB 色空間等の混色系の問題点である「数値の直感的な理解が難しい」という問題を解決します。
HSV 色空間は、「色の三属性」と呼ばれる、見た目の感覚として表現可能な属性を軸とする顕色系の代表選手、マンセル表色系に近い表現方法です。色相 (Hue)、彩度 (Saturation)、明度 (Value または Brightness) の三属性の値の組み合わせによって表現します。
(出典:"HSV color solid cone.png" from Wikimedia Commons)色相は、「赤系統」や「青系統」といった色味で、色相環と呼ばれる色合いを円状に並べて表記されることが多いです。
彩度と明度は分かりにくいのですが、「彩度は色の鮮やかさ」「明度は明るさ(そのままですが)」と考えると分かりやすいです。彩度が高いものはビビッドカラーと言われる原色に近い色で、彩度が 0 のものは、モノトーンと呼ばれる白・黒・グレーです。つまりビビッドな色から彩度を落とすと、段々とモノトーンの色に近づいていきます。重要なポイントは、「同じくらいの明るさに感じるものでも、色の鮮やかさが異なるものがある」という点で、下の図を見ていただくと、明るさをほぼ保ったまま、色を鮮やかなものからくすんだ色まで変化させることが可能なことがわかるかと思います。厄介な点は、彩度と明度は単純に切り分けることが出来ず、明度を落としたときに彩度の取りうる幅も小さくなる、という点です。例えば黒は、彩度が 0 のものしか存在し得ません。下図において、下の方にいくと横幅いっぱいに黒くなるのはそのためで、立体として表す場合には円柱でなく円錐で表現されることが多いのも同様の理由です。独立していて欲しいはずの属性間に関連がある、という点が、この色空間の難点であるように個人的には思います。
なお、HSV (Hue/Saturation/Value) と HSB (Hue/Saturation/Brightness) は同じものを指します。HSL (Hue/Saturation/Lightness or Luminance) と HLS (Hue/Lightness or Luminance/Saturation) も同じものを指します。HSV と HSL は似ていますが若干異なります。紛らわしいですね… さらに紛らわしいことを言うと HSI (Hue/Saturation/Intensity) は HSL/HLS と HSV/HSB を全て含めたことを指します。HSI は色空間の理論の説明では出てきますが実用上にはあまり登場しないため、個人的には HSV/HSB と HSL/HLS は違うということだけ押さえておけばよいかと思っています。
まとめ
- 表色系とは色を表す体系のこと
- 色空間とは色を表した時の表現方法とそれが包括する範囲のこと
- RGB 色空間とは R/G/B の強さで色を表す、混色系の代表方式
- 原理に根ざしたため便利であるが、人間には分かりづらい
- HSV 色空間とは 色相/彩度/明度で色を表す、顕色系の代表方式
- 感覚を元にしているため分かりやすいが、明度と彩度などは若干理解しにくい
- 内部では R/G/B 等に変換して使用される
3. アプリケーションの説明
開発の背景
今まで見てきたように色を定量的に表す際、RGB 色空間では赤と緑と青の強さ、HSV 色空間では色相と彩度と明度という 3 つの数値を用います。その他にもここでは挙げていない様々な色空間がありますが、ほとんどの場合、一つの色は 3 つの数値の組み合わせ(三次元ベクトル)として表現されます16。ここで大事なのは 「色は基本的には一次元もしくは二次元では表現しきれない」 という事実です。つまり、「一本の数直線」(一次元)や「座標平面」(二次元)で表現することは本質的に不可能だということです。
前章でも若干触れましたが、資料の作成やデザイン関連の作業で色を選ぶ際、なんとなく難しさや違和感を感じる方も多いのではないかと思います。RGB のバーで探しにくいのは上述の通り自然なことと思われますが、HSV 形式のカラーピッカーも私はなんだか探しにくい、と感じてきました。その原因は、本来三次元である色空間を、無理やり一次元のバーと二次元の平面で表現しているために、空間全体を俯瞰することができず規則性も直感的に把握することが難しいためではないかと考えています。
そのため、「三次元空間上に色を秩序正しくキレイに並べてみたら、色全体を俯瞰できていい感じになるのでは…」、と思い試してみたのが今回開発したアプリケーションです。
少し調べてみても同様のことをやっている人はほとんどいなかったため17、実際に自分で見てみたく Three.js を初めて触りながら作ってみました18。色空間に対する直感的な理解が深まるような可視化を目指していたので、このアプリケーションを通じて色に対する理解が深まる方が少しでもいれば幸いです。
あとは、実際に開発したものをソースコードと共に公開したら、転職活動の際に話のネタになるかなーと思ったり…
詳細
今回開発したアプリケーションは、色空間を 3DCG として可視化したものです。3DCG のインタラクティブな操作を通じて、人間が知覚可能な色の範囲を俯瞰しつつ、色と色の関係性や混ざり具合などを具体的に把握することを目指しています。
前章の RGB、HSV 色空間の色の並びを可視化し、様々な角度・距離で見てみたり、所望の色の RGB 値を確認したりできます。
特に個人的におすすめな点は、RGB, HSV の任意の軸の値で絞り込んだものを表示できる、という点です。例えば、RGB の赤の強い値を固定した時に G/B の値の変化でどのように色が変化するか、というのを軸を絞り込んで見たことができたり、HSV の最も明度が高い分類となる色の群は、RGB 色空間では立体の外側に来る、ということを確認したりできます。
具体的な操作方法は、ドラッグやスクロール、ピンチイン / ピンチアウトなどを試していただければ直感的に把握できるようにしてみているので、詳細な説明は省略いたします19。
一つだけ中々発見できないかもしれない操作として「視点の平行移動」があり、「右クリック + ドラッグ」「Ctrl + ドラッグ」「二本指でドラッグ」のいずれかで可能な操作です。また、実装上の技術的な詳細等に関しては本記事の趣旨ではないので割愛いたしますが、もし需要があれば記事を書きたいと思います。ソースコードは全てこちらの GitHub レポジトリで公開しています20。
アプリケーションに関する想定問答
- RGB と HSV を切り替えた時、対応しているはずのキューブで若干 RGB の値が異なっている?
- RGB と HSV の両表現とも「なるべく網羅的に色を表示する」「位置関係に応じた色の配置にしている」ため、対応キューブの色が若干異なってしまっています。すみません。
- 全部の色は表現できてなくない?
- はい、現在表示しているのは代表的と算出した 512 色のみです。「全部の色の数」は、色の表現形式によりますが、近年最も一般的に使われる、R/G/B 各チャンネルで 8bit (256 階調) ずつデータを保持する形式だと $256^3$ で 16,777,216 色(!)あります。アニメーションなどを使用して、「一つのキューブがクリックされた際にその近辺の色を表示しドリルダウンしていく」等の手法によって全ての色を探せるようにする方法は考えられますが、需要が謎なのでそこまでやってみていません。もしその機能も欲しい方がいたら実装するかもしれませんので、ご要望有りましたら GitHub の issue に書いて頂くか本記事のコメントにご記載下さい。
- 何の役に立つの?
- 多分ほとんど役に立つことはないです。すみません。
- 当初はデザイナーさんとかが色選びの時に使えるようなツールに出来たら嬉しかったのですが、おそらくそのようなレベルには程遠いのではないかと思います(そもそもデザイナーさんの色選びの作業プロセスを知らない…)
- さらに、美しく見えることも重視していて光や影の表示をいい感じになるように調整しているので21、「厳密に色を選びたい」という想いには応えられそうにありません。
- もしこのアプリケーションを通じて色空間に対する直感的な理解が深まる方が 1 人でもいるならば、開発者としてはもうそれで十分満足です。
- 個人的には、このアプリケーションで三次元空間上の浮遊物体を眺めつつグリグリ動かしたり、キューブをクリックしてくるくる回してみたり、色々な軸の値で絞り込んでみたり、キューブ群の浮遊しているど真ん中に突撃しにいってそこからの様々な眺めを愛でてみたりするだけで、十分気持ちよかったり楽しかったりします。
4. おわりに
色というのは、日常的なものでありながらも直感的に把握しづらい事実があったり、とても奥深くとても面白いものです。
本記事の目的通り、これをきっかけに少しでも色に対しての理解が深まったり今まで以上の興味が湧いたり、色の世界は面白い・キレイだ、などと感じて頂ける方がいれば幸いです。上でも書いたとおり、今回私が開発したものはほとんど実際の役に立つものではないと思います。ただ、今回の開発では、色を網羅的にかつ秩序正しく並ばせてみただけで、こんなにも美しいものが現れた、という事実を知れて私はとても嬉しかったです。色覚は個人差も大きく、皆が皆同じように見えているわけではないという事実はありながらも、我々の過ごす色彩に溢れているこの世界は、本当に美しく素晴らしいものだと改めて思いました。
駄文ながら最後までお付き合い頂きありがとうございました。
参考文献・参考サイト
書籍
色彩工学入門 -定量的な色の理解と活用- (ほげほげ著)
研究者の方が書かれた色に関する科学に関しての学術書です。とても深い内容でありながらわかりやすく書かれています。色彩に関してしっかりと勉強したい、深く知りたい、詳しくなりたいという方向けです。
ディジタル画像処理[改訂新版]
画像処理全般に関して、かなりの内容を網羅している書籍です。その上、浅かったりいい加減な記述はほぼありません。色の理論・扱いは、画像処理では必須なこともあり、第3章「画像の性質と色空間」にほどよくまとめられています。この書籍は画像処理関連に携わるエンジニアの方の多くが一度は目を通しているのではないかと思いますし、画像処理に興味のある方にはオススメの一冊です。
色彩検定 公式テキスト 3級編
色彩検定という資格試験のテキストです。学術的な難しい話はなく、数式等が苦手な方でもとても読みやすいながら色に対しての理解は深まる一冊だと思います。ちなみに私は色彩検定の資格は毎年取りたいと思いつつ、受けそびれてしまっています…
参考サイト
three.js
Three.js の公式サイトです。やはり開発時に最も頼りになるのは公式ドキュメントです。
Three.js 入門サイト (ICS media 内)
Three.js に関する初歩的な内容がとてもわかりやすくまとまっています。大変お世話になりました。
Three.jsのパフォーマンスTips (Kabuku 社開発者ブログ)
Three.js での負荷軽減、パフォーマンス最適化に関して、Three.js 作者自らのツイートを解説付きで紹介しています。こちらのサイトもお世話になりました。
厳密ではありません。実際は波でありかつ粒でもあります。このような、全く直感的でない光の性質は実験によって証明されています。気になる方は量子力学関連の書籍を参照して下さい。 ↩
紫外線が見える人も一部いるみたいです。なんということでしょう。 ↩
「スペクトル」という語は、光のスペクトル以外でも使われるため本文脈でのスペクトルは正確には分光スペクトルと言います。例えば音を周波数成分ごとに分解したものなどもスペクトルであり、周波数スペクトルなどと呼ばれます。 ↩
虹のスペクトルは、空気中のしずくにより光が若干複雑に分散されたものです。 ↩
この状態で両端に現れるのは、可視光線の中で最も波長が長い赤のものと、最も波長が短い紫のものです。赤外線、紫外線は、これらの波長の外にあることからそう呼ばれます。 ↩
実際は 3 つの単波長光の混ぜ合わせだけでは全ての色は再現できません。3 つの視細胞が感知する波長の範囲には重なりがあることにより、一部の領域において赤の成分を負の値にしなければ表現できない色が存在します。しかし、線形代数のちょっとした計算で基底を変えることにより、全ての色を基底の正の値のみで表現できることが可能となります。それを色空間としたものが XYZ 色空間であり、XYZ 色空間では全ての色を表現可能であるため、色の値の情報伝達手段として内部的に扱われることが多いです。ただし、基底となる波長は実際に目に見えない色となってしまうこともあり、我々の目に触れる範囲では RGB が使われることが多いです。 ↩
逆に言えば、人間は「赤」「緑」「青」と人間が名付けた色の波長しか感知できないために、それらの組み合わせで作られる色だけしか見えない、ということです。他の生物では 4 種類の視細胞で色を感知する動物もおり、例えばモンシロチョウの羽を紫外線センサーを使って見ると、人間には見えない模様が見え、モンシロチョウにはその模様で雄雌の区別ができるという話もあります。しかし、それらの動物に見えている 4 種類の原色が混ざりあった色の景色はどうやっても人間には想像できません。 ↩
黒いものが光に当たると温まりやすいのは、光を吸収する割合が他の色のものと比べて大きいためです。 ↩
これらの機器は「どのようにしたら人間が同じと感じるように色を記録・伝達・再現することができるか」という目的で作られているため、人間の知覚を再現するような仕組みであるのは当然のことなのですが、それは人間だけを対象とした仕組みであることは興味深い事実です。すなわち、実物とそれを撮影した写真や映像は人間にはほぼ同じものとして見えていても、紫外線が見えるような人間と知覚特性の異なる動物には全く別の色合いに見えている可能性があります。 ↩
厳密な定義はとても難しい用語なので、間違いではないわかりやすい表現を頑張って捻出しています。表色系と違う点は、色空間は数学的な意味での代数構造としての空間、つまり色を多次元ベクトルとして捉えた場合の N 次元のベクトル空間のニュアンスが強い点だと思っています(が、私も専門ではないので確かではありません…)。 ↩
実際、英語では表色系を表す color system と色空間を表す color space は同義として扱われています。 ↩
理由として(完全に私見ですが)、工学系では色をベクトルとして扱うことが多く線形変換など数学的な処理を頻繁に行うために、代数系としての空間という語が馴染みやすいからではないかと思っています。 ↩
元はフランス語で "Commission internationale de l'éclairage" としていたため CIE という略語となっています。 ↩
厳密には、白に関しても「どういった色を白とするか」を決める必要があります。 ↩
訓練すればどれくらいの比率で R/G/B を混ぜれば任意の色を作れるかわかるようになるのかもしれません。例えば料理人は「酸味をもうちょっと足したほうが味のバランスがよくなりそう」という想像ができるようになるのと同じで、「こういう色が欲しい場合には青と緑を 2:5 の割合くらいで足せばよい」となるかもしれません…そもそも画家さんは自分で色を混ぜて好きな色を作れてそうですし… ↩
「基本的に」と書いたのは、透過チャネルを加えた四次元で表現されることも多いためです。 ↩
今回じっくり調べてみたら、既に類似のものがありました…見つけてしまってちょっぴり凹みましたが、以前に見つけていたら今回自分は作らなかったかもしれず、逆に今まで発見していなくてよかった、という気持ちにもなりました。 ↩
実は最初は、某企業に応募した際に出された課題の中の「自由課題」を選択して開発し、今回作り直した感じです。 ↩
説明書的なドキュメントを作るのが面倒というのもありますが、アプリ自体が取り扱い方を説明してくれるようなソフトウェア(いわゆるアフォーダンスに溢れているようなもの)を作れたらいいなーと個人的には常に思っています。自分の作ったアプリで「これは何が表示されているのですか?」とよく質問され、その度に「まだまだだなー」と思う毎日です… ↩
Three.js はガチ初心者なので実装として拙い部分は多々あるかと思います。効率の良い書き方・一般的な実装方法などご指摘ありましたら頂けるととても嬉しいです。 ↩
光や影の表示に無駄にこだわっているために、一部端末で激重だったり消費電力が激しかったりします… ↩
- 投稿日:2020-01-18T00:15:54+09:00
webサイトの更新の表示をgithubのreleaseで管理
TD;LR
webサイトの更新履歴をgithubのリリースで管理したら便利だった
releaseの更新に合わせてツイートをするgithub actionsを作ったreleaseで管理
よくウェブページについている下みたいな更新履歴を作ろうと思ったけど
20xx/0x/xx xxxxxの内容を更新 20xx/0x/xx xxxxxを追加 20xx/0x/xx xxxxxを更新ファイルにまとめたりして一々追記していくのが嫌だったのでgithubの機能を使いました。
release機能
releaseはgithubの機能でリリースノートや添付ファイルを付けてユーザーに公開する事ができます。
ここのreleases
と書いてある部分から作成でき、
今までの履歴がこのように表示されます。こんな感じでピッタリな機能だったので今回の更新履歴の実装に使ってみることにしました。
releaseの取得
releaseの取得にはgithubのrest APIクライアントのoctokit/restを使用しました。
npm install @octokit/rest
上のnpmコマンドでパッケージのインストールをしたあとはコードを書いていくだけです。
import Octokit from "@octokit/rest"; let releaseLog = [] const octokit = new Octokit();取得したreleaseを入れておくリストとOctokitの宣言を済ませたら。
octokit.repos.listReleases({ owner: "mugi111", repo: "my-profile-page", }).then((res) => { if (res.status === 200) { // 整形 } })
owner
でユーザー名repo
でリポジトリ名を指定してlistRelease
でreleaseの一覧を取得します。
一覧が返ってきたら自分の使いやすいように整形して最初に宣言したreleaseLog
に入れておきます。あとはループさせたりして表示させるだけです。
おわり
更新履歴に追加したいところでrelease作成すれば自動的にページに反映されるようになるので楽になった。
githubからの取得も意外と簡単だった。
github actionsについては次のでちゃんと書きます。更新履歴を実装したページとリポジトリ
https://th-mg-profile.netlify.com/
https://github.com/mugi111/my-profile-page