- 投稿日:2021-01-01T23:02:08+09:00
Vue.jsで非同期処理関数を作る方法
皆さんこんにちは!!!
今回はVue.jsで非同期処理関数を作る方法をご紹介します!
めちゃめちゃ簡単なので、ぜひ学習の参考にしてください。
それでは説明していきます。
Promiseを用いて関数を作成
今回はTimeoutの関数を非同期処理で行います。
Vue.jsで非同期処理関数を作成する際は、宣言時にPromiseを利用するのではなく、返り値として利用します。
App.vue<script> export default{ methods: { sleep(mesc) { return new Promise((resolve) => { setTimeout(resolve, mesc) }) } }, } </script>エラー対処
Vue2.x系を使っている方は下記のようなエラーが出ると思います。(出ない方はスルーして結構です)
error: npm i core-js/fn/promiseこのエラーの対処の仕方は、僕が書いた記事core-jsの依存関係エラー(core-js/~/~)対処法を参考にして頂くと、恐らく解決できます。解決できない場合はコメント欄にてご報告をお願いします。
以上、「Vue.jsで非同期処理関数を作る方法」でした!
めちゃめちゃ簡単♪
良ければ、LGTM、コメントお願いします。
また、何か間違っていることがあればご指摘頂けると幸いです。
他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!
Thank you for reading
- 投稿日:2021-01-01T19:21:23+09:00
Spring Boot + Java + PostgreSQL でDBに画像ファイルをアップロード&画面表示
はじめに
ユーザや商品等のテーブルに画像の一緒に保存したくなったので、データベース上に保存できないかどうか色々と調べた結果何とかその方法がわかったので備忘録としての残しておきます。誰かの参考になればと思います。
完成イメージ
以下必要となるソースコードです
これの応用でウェブアプリケーションに画像の実装ができました:)
CREATE TABLE files ( id SERIAL PRIMARY KEY, name VARCHAR, type VARCHAR, data BYTEA -- ポイント1: 保存したい写真のデータ型をBYTEA(byte array)にする );@Entity @Table(name = "files") @Getter @Setter @NoArgsConstructor public class FileDB { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private String id; private String name; private String type; @Lob // ポイント2: @Lobと@Typeを以下のようにつける(@Lobはサイズが大きいデータのカラムにつけるみたい。@Typeがないと「bigintのデータが出力されてますよ」的なエラーが出る @Type(type = "org.hibernate.type.BinaryType") @Column(name = "data") private byte[] data; public FileDB(String name, String type, byte[] data) { this.name = name; this.type = type; this.data = data; } }<body><!-- ポイント3: formタグに「enctype="multipart/form-data"」追記 --> <form th:action="@{/portfolio/file/upload}" method="POST" enctype="multipart/form-data"> <input type="file" name="file" id="file"> <button type="submit">UPLOAD</button> </form> <div> <p th:text="${message}"></p> <img th:src="${'data:image/png;base64,'+image}" alt=""> </div> </body>@Controller @RequestMapping("/portfolio/file") public class FileController { @Autowired private FileStorageService storageService; @GetMapping(value="/index") public String index(Model m) { String path = ""; m.addAttribute("path", path); return "file_upload"; } @PostMapping("/upload") public String uploadFile(@RequestParam("file") MultipartFile file, Model m) { String message = ""; try { FileDB savedFile = storageService.store(file); byte[] bytes = savedFile.getData(); // ポイント4: Base64.getEncoder().encodeToString(bytes)でbyteをStringにして、Viewに渡す String image = Base64.getEncoder().encodeToString(bytes); message = "Uploaded the file successfully: " + file.getOriginalFilename(); m.addAttribute("message", message); m.addAttribute("image", image); return "file_upload"; } catch (Exception e) { message = "Could not upload the file: " + file.getOriginalFilename() + "!"; m.addAttribute("message", message); return "file_upload"; } } }@Service public class FileStorageService { @Autowired private FileDBRepository fileDBRepository; public FileDB store(MultipartFile file) throws IOException { String fileName = StringUtils.cleanPath(file.getOriginalFilename()); FileDB FileDB = new FileDB(fileName, file.getContentType(), file.getBytes()); return fileDBRepository.save(FileDB); } }
- 投稿日:2021-01-01T16:31:02+09:00
(Java、JavaScript、Python)文字列処理の比較
Java、JavaScript、Pythonの処理比較
最近JavaScriptとPythonの勉強を始めました。
学んだことの整理として、Java、JavaScript、Pythonの3言語の処理比較を記事にしていこうと思います。
今回は文字列処理です。文字列処理の比較一覧
処理内容 Java JavaScript Python 文字列長 length() length len(文字列) 一致比較 equals(比較文字)
equalsIgnoreCase(比較文字)==比較演算子
===比較演算子==比較演算子 検索 indexOf(検索文字列)
lastIndexOf(検索文字列)
contains(検索文字列)
startsWith(検索文字列)
endsWith(検索文字列)
indexOf(検索文字列)
lastIndexOf(検索文字列)
includes(検索文字列)
startsWith(検索文字列)
endsWith(検索文字列)find(検索文字列)
rfind(検索文字列)
in式
not in式
startswith(検索文字列)
endswith(検索文字列)単純結合 +演算子
concat(結合文字列)
StringBuilder.append(結合文字列)+演算子
concat(結合文字列,...)+演算子 空白除去 trim()
strip()trim()
trimLeft()
trimRight()strip(削除対象文字)
lstrip(削除対象文字)
rstrip(削除対象文字)大文字小文字変換 toUpperCase()
toLowerCase()toUpperCase()
toLowerCase()upper()
lower()
capitalize()
title()
swapcase()
切り出し substring(開始index,終了index)
charAt(index)substring(開始index,終了index)
charAt(index)
slice(開始index,終了index)
substr(開始index,文字数)
文字列[開始index:終了index] 繰り返し repeat(繰返し数) repeat(繰返し数) *演算子 置換 replace(前文字,後文字)
replaceFirst(前文字,後文字)
replaceAll(前文字,後文字)
replace(前文字,後文字)
replaceAll(前文字,後文字)replace(前文字,後文字)
re.sub(前文字,後文字,文字列)
re.subn(前文字,後文字,文字列)
translate(式)区切りで結合 String.join(区切り文字,結合文字列...) 結合文字配列.join(区切り文字) 区切り文字.join(結合文字配列) 区切りで分割 split(区切り文字)
split(区切り文字) split(区切り文字)
rsplit(区切り文字)
splitlines()
re.split(区切り文字)任意成型 String.format(成型式,文字列...) テンプレートリテラル→
`xxx${変数名}xxx`成型式.format(変数や式...)
f'xxx{変数や式}xxx'
形成式 % (変数や式,...)追記
- 一致比較
- JavaのequalsIgnoreCase()は大文字小文字を無視した一致比較を行います。
- ===演算子は型を含めた厳密な比較を行います。'123'===123 →false
- 検索
- 一致箇所の文字indexを返す:indexOf()、lastIndexOf()、find()、rfind()
- true/falseを返す:contains()、includes()、in式、not in式
- 先頭文字の一致判定、true/falseを返す:startsWith()
- 末尾文字の一致判定、true/falseを返す:endsWith()
- 空白除去
- いずれの言語も半角空白、タブ、改行が除去対象になります。
- Javaのstrip()は上記に加え、全角空白も除去対象になります。
- Pythonの関数は引数に削除対象文字指定可、省略時は空白・タブ・改行が除去対象になります。
- 大文字小文字変換
- Pythonは他の2つよりも細かい変換関数が存在します。capitalize()→最初の文字を大文字に他は小文字に変換、title()→単語最初の文字を大文字に他は小文字に変換、swapcase()→大文字小文字を入れ替え
- 切り出し
- substring()は終了indexを省略可能で、その場合は文字列の末尾を終了indexとします。
- JavaScriptの切り出しsubstring()とslice()は通常の使用範囲では差異はありませんが、引数がイレギュラーな場合に結果が異なります。
- 開始index>終了indexの場合、substring()は自動で開始終了を入替え、slice()は空文字を返します。
- 負数指定の場合、substring()は引数を0として扱い、slice()は「文字列長-引数」として扱います。
- Pythonは関数ではなく「文字列[start:end]」という書式のスライスを使用します。start,endいずれか省略可能、「:」省略時はcharAt()のように1文字を取得します。
- 置換
- Java:replace()はマッチした全文字置換、replaceFirst()マッチした最初の文字置換。
- JavaScript:replace()はマッチした最初の文字置換、replaceAll()はマッチした全文字置換、ただしreplace()も正規表現オプション「g」で全文字置換可能。
- Python:replace()は第3引数にsplit()のように最大置換数指定可。
- Python:translate()は複数ケースの置換が可能。ただし置換元文字は1文字であること。置換の組み合わせを「str.maketrans()」で指定します。「"abCdefghIk".translate(str.maketrans({'C':'c', 'I':'ij'}))」
- 正規表現が使用できるのは各言語で以下の通りです。
- Java:replaceFirst()、replaceAll()
- JavaScript:replace()、replaceAll()
- Python:re.sub()、re.subn()、subnは置換処理された文字列+変換した個数のリスト(正確にはタプル)を返します。
- 区切りで分割
- split()はどの言語でも第2引数に最大分割数が指定可能です。
- Java、JavaScriptのsplit()は正規表現使用可能です。JavaScriptにおいては空白区切りは正規表現で「/\s/」と指定します。
- Pythonはreモジュールのre.split()が正規表現使用可能です。
- Pythonのsplit()は引数省略可能で、省略時は空白・タブ・改行が分割対象となります。
- Pythonのrsplit()は第2引数に最大分割数指定したとき右からカウントするのがsplit()との差異になります。
- Pythonのsplitlines()は改行で分割します。OS依存の改行にも対応しています。
- 任意成型
- Pythonはいくつかの書式があります。format()は下記サンプル参照。
- 1つは%演算子:sei='山田' mei='太朗'のとき、print('名前:%s %s' % (sei,mei))
- 1つはf文字列:print(f'名前:{sei} {mei}')
サンプル
Java
public class StringSample { public static void main(String[] args) { StringSample obj = new StringSample(); obj.executeSample(); } public void executeSample() { String str = " 山田,太朗,Yamada,Taro,M,19950827,25才,東京都千代田区神田佐久間町2-XX-XX〇〇荘 "; // 空白除去 String strKuhaku1 = str.trim(); // 区切りで分割 String[] strBun1 = strKuhaku1.split(","); // 単純結合 String resName = new StringBuilder().append(strBun1[0]).append(strBun1[1]).toString(); // 大文字小文字変換 String strOoko1 = strBun1[2].toUpperCase(); String strOoko2 = strBun1[3].toUpperCase(); // 単純結合 String resAlpha = strOoko1.concat(" ").concat(strOoko2); // 一致比較 String resSex; if (strBun1[4].equals("M")) { resSex = "男性"; } else { resSex = "女性"; } // 切り出し String strKiri1 = strBun1[5].substring(0, 4); String strKiri2 = strBun1[5].substring(4, 6); String strKiri3 = strBun1[5].substring(6); // 区切りで結合 String resBirth = String.join("/", strKiri1, strKiri2, strKiri3); // 置換 String resAge = strBun1[6].replace("才", "歳"); // 文字列長 String resAddress = strBun1[7]; if (16 < strBun1[7].length()) { resAddress = strBun1[7].substring(0, 16); // 単純結合 resAddress = resAddress + "(略)"; } // 検索 String resMessage1 = ""; String resMessage2 = ""; if (strBun1[7].startsWith("東京都")) { resMessage1 = "首都です。"; } if (strBun1[7].contains("千代田区")) { resMessage2 = "東京駅があります。"; } // 任意成型 String res1 = String.format("%s(%s)様 %s %s生(%s)", resName, resAlpha, resSex, resBirth, resAge); String res2 = String.format("%s %s%s", resAddress, resMessage1, resMessage2); // 繰り返し String line = "-".repeat(64); System.out.println(line); System.out.println(res1); System.out.println(res2); System.out.println(line); } }JavaScript
<html> <head> <script> function executeSample() { let str = " 山田,太朗,Yamada,Taro,M,19950827,25才,東京都千代田区神田佐久間町2-XX-XX〇〇荘 "; // 空白除去 let strKuhaku1 = str.trim(); // 区切りで分割 let strBun1 = strKuhaku1.split(","); // 単純結合 let resName = strBun1[0] + strBun1[1]; // 大文字小文字変換 let strOoko1 = strBun1[2].toUpperCase(); let strOoko2 = strBun1[3].toUpperCase(); // 単純結合 let resAlpha = strOoko1.concat(" ", strOoko2); // 一致比較 let resSex; if (strBun1[4] == "M") { resSex = "男性"; } else { resSex = "女性"; } // 切り出し let strKiri1 = strBun1[5].substring(0, 4); let strKiri2 = strBun1[5].substring(4, 6); let strKiri3 = strBun1[5].substring(6); // 区切りで結合 let resBirth = [strKiri1, strKiri2, strKiri3].join("/"); // 置換 let resAge = strBun1[6].replace("才", "歳"); // 文字列長 let resAddress = strBun1[7]; if (16 < strBun1[7].length) { resAddress = strBun1[7].substring(0, 16); // 単純結合 resAddress = resAddress + "(略)"; } // 検索 let resMessage1 = ""; let resMessage2 = ""; if (strBun1[7].startsWith("東京都")) { resMessage1 = "首都です。"; } if (strBun1[7].includes("千代田区")) { resMessage2 = "東京駅があります。"; } // 任意成型 let res1 = `${resName}(${resAlpha})様 ${resSex} ${resBirth}生(${resAge})`; let res2 = `${resAddress} ${resMessage1}${resMessage2}`; // 繰り返し let line = "-".repeat(64); console.log(line); console.log(res1); console.log(res2); console.log(line); } </script> </head> <body onload="executeSample()"> <h1>Qiita記事</h1> <h2>文字列処理</h2> </body> </html>Python
str1 = " 山田,太朗,Yamada,Taro,M,19950827,25才,東京都千代田区神田佐久間町2-XX-XX〇〇荘 " # 空白除去 strKuhaku1 = str1.strip() # 区切りで分割 strBun1 = strKuhaku1.split(",") # 単純結合 resName = strBun1[0] + strBun1[1] # 大文字小文字変換 strOoko1 = strBun1[2].upper() strOoko2 = strBun1[3].upper() # 単純結合 resAlpha = strOoko1 + " " + strOoko2 # 一致比較 resSex = "" if strBun1[4] == "M": resSex = "男性" else: resSex = "女性" # 切り出し strKiri1 = strBun1[5][0:4] strKiri2 = strBun1[5][4:6] strKiri3 = strBun1[5][6:] # 区切りで結合 resBirth = "/".join([strKiri1, strKiri2, strKiri3]) # 置換 resAge = strBun1[6].replace("才", "歳") # 文字列長 resAddress = strBun1[7] if 16 < len(strBun1[7]): resAddress = strBun1[7][:16] # 単純結合 + resAddress = resAddress + "(略)" # 検索 resMessage1 = "" resMessage2 = "" if strBun1[7].startswith("東京都"): resMessage1 = "首都です。" if "千代田区" in strBun1[7]: resMessage2 = "東京駅があります。" # 任意成型 res1 = "{}({})様 {} {}生({})".format(resName, resAlpha, resSex, resBirth, resAge) res2 = "{} {}{}".format(resAddress, resMessage1, resMessage2) # 繰り返し * line = "-" * 64 print(line) print(res1) print(res2) print(line)出力結果
---------------------------------------------------------------- 山田太朗(YAMADA TARO)様 男性 1995/08/27生(25歳) 東京都千代田区神田佐久間町2-X(略) 首都です。東京駅があります。 ----------------------------------------------------------------
- 投稿日:2021-01-01T15:46:56+09:00
html, javasript, jquery とSakura Editor
初めに
Sakura EditorでHTMLページを保存する。
Sakura Editorのダウンロード
https://sourceforge.net/projects/sakura-editor/
HTMLページを保存する
<html> <head> <title>タイトル</title> </head> <body id="main-id"> <div>サンプルテキスト</div> </body> </html>jQueryの追加
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>jQueryのロード(HTMLを読み込んでからjQueryの処理を開始)
<script> $(document).ready(function(){ //マインオブジェクト var $main = $("#main-id"); //divを追加 var $div = $("<div>").html("サンプルテキスト2"); //親オブジェクトに子オブジェクトを追加 $main.append($div); }); </script>以上
- 投稿日:2021-01-01T11:27:08+09:00
【JavaScript】非同期処理について 2〜非同期処理のチェーン、コールバックヘル〜
※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。
本記事は下記の記事の続きとなります。
【JavaScript】非同期処理について1〜タスクキュー、コールスタック、イベントループ〜非同期処理のチェーンとは
非同期処理のチェーンとは、一つの非同期処理が終了した後に、次の非同期処理を開始し、チェーンのようにどんどんつなげていく処理のことです。
今この様な非同期処理を行うプログラムがあるとします。
function sleep() { setTimeout(function () { console.log(0); }, 1000); }1000ミリ秒後に0を出力するプログラムです。
非同期処理をつなげるには、内部でさらに非同期処理を呼び出してあげます。
function sleep() { setTimeout(function () { console.log(0); sleep(); //1000ミリ待機後、関数sleep自体が呼び出される。 }, 1000); }上記のプログラムでは、呼び出された関数sleepが、さらにsleep関数を呼び出し無限ループしてします。
そこで引数として関数sleepを呼び出す関数を渡してあげます。
function sleep(callback) { setTimeout(function () { console.log(0); callback(); }, 1000); } sleep(function () { sleep( function(){} ) });この様にすることで、非同期処理を繋げることができました。
※二回目の関数sleepには何もしない処理をコールバックに渡しています。話が脱線しますが、下記のプログラムだとエラーになります。
function sleep(callback) { setTimeout(function () { console.log(0); callback(); }, 1000); } sleep( sleep( function(){} ));理由は、引数として渡される値が、「関数」か、「関数の戻り値」かの違いです。
正しい方のプログラムでは引数としてfunction () { sleep( function(){} ) }無名関数自体を渡していますが、エラープログラムでは
sleep( function(){} )引数で中で、先にsleep関数を実行させちゃっています。
console.log(function () { sleep( function(){} ) }); console.log(sleep( function(){} ));上記のプログラムを実行してみたらわかりますが、正しいプログラムの出力結果は[Function (anonymous)]で、エラープログラムの方の結果は、「undefined」となります。(関数sleep自体は戻り値を返すプログラムではないので)
よって、エラーバージョンのプログラムは下記を実行していることになります。
function sleep(callback) { setTimeout(function () { console.log(0); callback(); }, 1000); } sleep( sleep( undefined ));もちろん、引数に渡したundefinedは、関数ではないので、下記の様なエラーが出力されます。
TypeError: callback is not a function //本来、仮引数callbackは、関数を受け取るが、それ以外が渡って来てるよという意味話を戻します。
非同期処理の結果を次の非同期処理に渡すこともできます。
function sleep(callback, n) { setTimeout(function () { console.log(n); n++ callback(n); }, 1000); } sleep(function (n) { sleep(function () { }, n) } , 0);少し分かりづらいですが、一回目のsleepでは、第二引数に0を渡しています。
内部処理でnは1加算され、その結果をコールバック関数に渡しています。
↓ こういうことfunction (1) { sleep(function () { }, 1) }二回目のsleepは、一回目の処理結果nを受けて、その結果を出力します。結果は
0 1となります。
では同じように関数sleepを五回つなげてみます。
function sleep(callback, n) { setTimeout(function () { console.log(n); n++ callback(n); }, 1000); } sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { }, n); }, n); }, n); }, n); }, n); }, 0);この様に、引数の中に再帰的に、sleepを呼び出す関数とnを受け渡していきます。これだとあまりにもヘルが過ぎるのでインデントをすると
function sleep(callback, n) { setTimeout(function () { console.log(n); n++ callback(n); }, 1000); } sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { sleep(function (n) { }, n); }, n); }, n); }, n); }, n); }, 0);これだと多少見やすくなりましたが、それでも分かりづらいですよね。
この様に、コールバックのネストが深くなっていきコードの見通しが悪くなっていくことを「コールバックヘル」と呼びます。このような、コールバックヘルを回避するために 「Promise」 という処理があります。
- 投稿日:2021-01-01T06:11:32+09:00
[連載]スーパーマリオ的なゲームをjavascriptで作ってみる 初級編 〜6章〜 テキがいてこそ
本連載について
- プログラミング初心者がスーパーマリオ的なゲームを作成するのに情報をまとめたものです
- 不明点や不備あれば、なんでもコメントいただけると大変嬉しいです!!!
より良いものにしたいので!- 一番最初の連載はこちらから確認お願いします!
▼ゲームイメージ
▼目次
1章 準備する
2章 簡単なページ作ってみる
3章 画像を動かしてみる
本章の概要
やっぱりゲームといえば、主人公以上に重要な役割を担うと言っても過言ではないのが”敵”の存在ですよね!
ってことで、敵を登場させたいと思います
- 本章の内容は大きく4ステップです
- その1 〜とりあえず登場させるの巻〜
- その2 〜あったたらだめよの巻〜
- その3 〜ジャンプで踏みつければOKの巻〜
- その4 〜増殖させようの巻〜
- 各ステップごとに実際のソースをQiita上に記載しています
- 上記と同じくソースの実態を保存しているgitのリポジトリも記載しています
リンクにアクセスして実際のソースをダウンロードすることができます
ぜひダウンロードして動かしながら試してみてください!その1 〜とりあえず登場させるの巻〜
ゴール
- とりあえず動く敵を登場させる
前提
- 画像は作っておく必要があります
- 着地判定や、ブロックから落ちたら自由落下する部分などは、既存ロジックを流用して実現します
やること
- 敵の画像ファイルを作成し、配置します ([参考]ドッド絵を作成する)
- 敵の位置を算出します
- 敵の画像を描画します
- サンプルソースでは以下のファイル構成としています
└┬─ src ── ... │ └─ images ┬─ character-01 ── ... └─ character-02 ── base.png └─ ground-01 ── ...実装内容
index.js...(省略) // 敵の情報のパラメータ宣言 & 初期化 var enemyX = 550; var enemyY = 0; var enemyIsJump = true; var enemyVy = 0; ...(省略) // 画面を更新する関数を定義 (繰り返しここの処理が実行される) function update() { // 画面全体をクリア ctx.clearRect(0, 0, 640, 480); // アップデート後の敵の座標 var updatedEnemyX = enemyX; var updatedEnemyY = enemyY; // 敵は左に固定の速度で移動するようにする updatedEnemyX = updatedEnemyX - 1; // 敵の場合にも、主人公の場合と同様にジャンプか否かで分岐 if (enemyIsJump) { // ジャンプ中は敵の速度分だけ追加する updatedEnemyY = enemyY + enemyVy; // 速度を固定分だけ増加させる enemyVy = enemyVy + 0.5; // ブロックを取得する const blockTargetIsOn = getBlockTargetIsOn( enemyX, enemyY, updatedEnemyX, updatedEnemyY ); // ブロックが取得できた場合には、そのブロックの上に立っているよう見えるように着地させる if (blockTargetIsOn !== null) { updatedEnemyY = blockTargetIsOn.y - 32; enemyIsJump = false; } } else { // ブロックの上にいなければジャンプ中の扱いとして初期速度0で落下するようにする if ( getBlockTargetIsOn(enemyX, enemyY, updatedEnemyX, updatedEnemyY) === null ) { enemyIsJump = true; enemyVy = 0; } } // 算出した結果に変更する enemyX = updatedEnemyX; enemyY = updatedEnemyY; // 敵の画像を表示 var enemyImage = new Image(); enemyImage.src = "../images/character-02/base.png"; ctx.drawImage(enemyImage, enemyX, enemyY, 32, 32); ...(省略) }※ 実際のソースコードは こちら からダウンロードできます
説明
- 主人公の座標算出のために行っていることとほぼ同じことを実施してあげればOKです
- キー入力による制御がないため、
固定で左方向に移動する
&&ブロックの境でも進んでそのまま落下する
ものとしています- だいぶ煩雑になってきてしまいましたね、、本当はメソッド切り出しとかすべきなのですが本章ではこのまま進んでしまいます
- とりあえず、敵が動く状態までで、当たり判定はその2で行います
CodePenのサンプル
See the Pen mario-game-tutorial-01-06-01 by taku7777777 (@taku7777777) on CodePen.
その2 〜あったたらだめよの巻〜
ゴール
- 敵にあたったら、ゲームオーバーとなるようにする
前提
- あたり判定は、簡易的に更新後の座標のみを用いて行います (着地判定のように、更新前の座標は用いない)
やること
- 更新後の主人公の座標と敵の座標から当たり判定を行います
- あたっていたら、ゲームオーバーとすします
実装内容
index.js...(省略) // 画面を更新する関数を定義 (繰り返しここの処理が実行される) function update() { ...(省略) // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある if (!isGameOver) { // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする var isHit = isAreaOverlap(updatedX, updatedY, 32, 32, updatedEnemyX, updatedEnemyY, 32, 32); if (isHit) { // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える isGameOver = true; vy = -10; } } ...(省略) } ...(省略) /** * 2つの要素(A, B)に重なる部分があるか否かをチェックする * 要素Aの左上の角の座標を(ax, ay)、幅をaw, 高さをahとする * 要素Bの左上の角の座標を(bx, by)、幅をbw, 高さをbhとする */ function isAreaOverlap(ax, ay, aw, ah, bx, by, bw, bh) { // A要素の左側の側面が、Bの要素の右端の側面より、右側にあれば重なり得ない if (bx + bw < ax) { return false; } // B要素の左側の側面が、Aの要素の右端の側面より、右側にあれば重なり得ない if (ax + aw < bx) { return false; } // A要素の上側の側面が、Bの要素の下端の側面より、下側にあれば重なり得ない if (by + bh < ay) { return false; } // B要素の上側の側面が、Aの要素の下端の側面より、上側にあれば重なり得ない if (ay + ah < by) { return false; } // ここまで到達する場合には、どこかしらで重なる return true; }※ 実際のソースコードは こちら からダウンロードできます
説明
- 2つの要素が重なる部分があるかチェックするメソッド(isAreaOverlap)を追加します
- 重なりがあるか否かのチェックは、重ならない可能性を排除してって、最後まで排除されなかったらもう重なっているしかないよね。って感じのロジックにしています
- 主人公と敵の更新後の座標を特定し終えた後に、主人公と敵の座標とで重なりがあるかチェックします
- 重なりがある場合にはゲームオーバーとします
CodePenサンプル
See the Pen mario-game-tutorial-01-06-02 by taku7777777 (@taku7777777) on CodePen.
その3 〜ジャンプで踏みつければOKの巻〜
ゴール
- ジャンプして敵を踏みつけたら、敵をやっつけられるようにする
前提
- 厳密には敵の上側の側面からあたった場合に敵を倒すことができる、とすべきであるがここでは簡易的にジャンプ中かつ下向きに進んでいる状態で敵にあたったら敵を倒すことができるとします(実際にはほぼ問題なし)
やること
- 当たり判定を行った後に、ジャンプ中 && 下向きに進んでいた場合には敵を倒す(見えなくする && あたり判定の対象から除外する)ようにします
実装内容
index.js...(省略) // 画面を更新する関数を定義 (繰り返しここの処理が実行される) function update() { ...(省略) // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある if (!isGameOver) { // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする var isHit = isAreaOverlap(updatedX, updatedY, 32, 32, updatedEnemyX, updatedEnemyY, 32, 32); if (isHit) { if (isJump && vy > 0) { // ジャンプしていて、落下している状態で敵にぶつかった場合には // 敵を消し去る(見えない位置に移動させる)とともに、上向きにジャンプさせる vy = -7; enemyY = 500; } else { // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える isGameOver = true; vy = -10; } } } ...(省略) }※ 実際のソースコードは こちら からダウンロードできます
説明
-7
とか、マジックナンバー入っちゃっていますが、ここでは一旦無視で、、、動かしてみてそれっぽく動く値を入れていますCodePenサンプル
See the Pen mario-game-tutorial-01-06-03 by taku7777777 (@taku7777777) on CodePen.
その4 〜増殖させようの巻〜
ゴール
- 敵を量産する
前提
- 初期画面表示時に、複数の敵を登場させるようにします。
- 時間立つごとにどんどん敵が増えてく、みたいな感じにしてもよいのですが、制御めんどうなのでここでは予め敵を配置しておくようにします
やること
- 複数の敵の情報を保持できるようにします
- 初期位置を決めます
- 座標の計算・あたり判定・敵の描画を敵ごとに実施するようにします
実装内容
index.js...(省略) // 敵の情報のパラメータ宣言 & 初期化 var enemies = [ { x: 550, y: 0, isJump: true, vy: 0 }, { x: 750, y: 0, isJump: true, vy: 0 }, { x: 300, y: 180, isJump: true, vy: 0 }, ]; ...(省略) // 画面を更新する関数を定義 (繰り返しここの処理が実行される) function update() { // 画面全体をクリア ctx.clearRect(0, 0, 640, 480); // 敵情報ごとに、位置座標を更新する for (const enemy of enemies) { // アップデート後の敵の座標 var updatedEnemyX = enemy.x; var updatedEnemyY = enemy.y; var updatedEnemyInJump = enemy.isJump; var updatedEnemyVy = enemy.vy; ...(省略、ほぼ既存ロジックと同じ) // 算出した結果に変更する enemy.x = updatedEnemyX; enemy.y = updatedEnemyY; enemy.isJump = updatedEnemyInJump; enemy.vy = updatedEnemyVy; } ...(省略) // すでにゲームオーバーとなっていない場合のみ敵とのあたり判定を行う必要がある if (!isGameOver) { // 敵情報ごとに当たり判定を行う for (const enemy of enemies) { // 更新後の主人公の位置情報と、敵の位置情報とが重なっているかをチェックする var isHit = isAreaOverlap(x, y, 32, 32, enemy.x, enemy.y, 32, 32); if (isHit) { if (isJump && vy > 0) { // ジャンプしていて、落下している状態で敵にぶつかった場合には // 敵を消し去る(見えない位置に移動させる)とともに、上向きにジャンプさせる vy = -7; enemy.y = 500; } else { // ぶつかっていた場合にはゲームオーバーとし、上方向の初速度を与える isGameOver = true; vy = -10; } } } } ...(省略) // 敵情報ごとに当たり判定を行う for (const enemy of enemies) { ctx.drawImage(enemyImage, enemy.x, enemy.y, 32, 32); } ...(省略) }※ 実際のソースコードは こちら からダウンロードできます
説明
- 以下の4点において、複数データを扱えるようにします
1. 複数の敵の情報を保持できるように、変数の持ち方を変える(配列で保持するようにする)
2. 敵座標の計算を敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)
3. 主人公との当たり判定も敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)
4. 敵の描画を敵ごとに行うようにする (for文を用いて繰り返し処理するようにする)CodePenのサンプル
See the Pen mario-game-tutorial-01-06-04 by taku7777777 (@taku7777777) on CodePen.
終わりに
お疲れまです!!!
初級編は一旦このへんで...
これ以上は、ちゃんとリファクタしながら進めてあげないとソースがカオスになってしまうので、
(もうだいぶカオスですが)
次回以降はtypescriptを用いてリファクタしながら進めます。
導入のしやすさをとってjavascriptを使ってましたが、
実際には圧倒的にtypescriptのほうが開発楽なので、typescriptに頼っていきます!!ここまで来たあなたら、typescriptの利用はメリットしかないはずです!!!!!!!!!
- 投稿日:2021-01-01T03:38:51+09:00
js namespace sample
JavaScriptの名前空間の実装例
sample.jsvar test = test || {}; test = (function () { function callAlert() { alert('test'); } return { callAlert: callAlert }; })(); test.callAlert();参考
https://qastack.jp/programming/881515/how-do-i-declare-a-namespace-in-javascript
https://teratail.com/questions/83675
- 投稿日:2021-01-01T02:29:43+09:00
【CSSは左手系】スマホの姿勢をCSSで反映させるいくつかの方法と考え方【センサは右手系】
はじめに
前回の記事でDeviceOrientation Eventの値をグラフにリアルタイム表示してみました。
今回はそのDeviceOrientation EventとOrientation Sensor APIを使って、スマホの姿勢をCSSで反映させる方法を検討します。だいぶ「予備知識」の項目が長くなってしまったので、処理だけ見たければここまで飛んでください。
予備知識
センサーの座標系
スマホの画面を上に、画面の頭を北に向けて地面に置いたとき、
- 東西にX軸(東に向かって正)
- 南北にY軸(北に向かって正)
- 上下にZ軸(空に向かって正)
となります。
また、原点から各軸の正の方向を見て時計回りを、正の回転とします。
いわゆる右手系です。
(例の右手の法則をかたどって親指がX、人差し指がY、中指がZとなる。右手で親指を軸の正の方向に向けて軸を掴むと、残りの指が正の回転方向を指す。)
出典:Orientation Sensor(日本語訳)
出典:Orientation and motion data explained - Developer guides | MDN
出典:DeviceOrientation Event Specification (日本語訳)CSSの座標系
ご存知の方も多いでしょうが、CSSでは画面の
- 左右がX軸(右に向かって正)
- 上下がY軸(下に向かって正)
となっています。さらに3Dを表現する場合は、画面に垂直なZ軸(視点側に向かって正)を加えます。
そうすると、いわゆる左手系の形になります。
(例の左手の法則をかたどって、親指がX、人差し指がY、中指がZとなる。 )See the Pen CSS coordinate system by Arakaki Tokyo (@arakaki-tokyo) on CodePen.
左手系の回転方向は右手系の逆回りとなります。
すなわち、各軸の正の方向から原点を見て時計回りを、正の回転とします。
(左手で親指を軸の正の方向に向けて軸を掴むと、残りの指が正の回転方向を指す。)例えば上図でのY軸は
rotateZ(90deg)
としてますが、これはZ軸を中心にして、正の方向(視点)から原点(画面)を見て時計回りに90度回転させることを意味しています。
同様にZ軸はrotateY(-90deg)
ですが、これはY軸を中心にして、正の方向(画面の下)から原点(画面の上)を見て反時計回りに90度回転させています。ここで重要なのは、Y軸を中心とした回転の方向がセンサーとCSSで見かけ上一致することです。
センサでは原点(画面中心)から正の方向(画面上部)を見て時計回りが正の方向
CSSでは正の方向(画面下部)から原点(画面上部)を見て時計回りが正の方向これは、CSSの座標系がセンサーの座標系のY軸の方向を反対にした形となっているためです。
X軸とZ軸では、軸の方向は同じで回転方向が逆になります。DeviceOrientation Eventで取得できる値
DeviceOrientationEvent
?またはDeviceOrientationAbsoluteEvent
?では姿勢に関してオイラー角の情報、すなわち以下3つの値を取得できます。
- alpha: Z軸での回転。0~360(北が0)
- beta: X軸での回転。-180~180(水平が0)
- gamma: Y軸での回転。-90~90(水平が0)
ref.
ここで意識しなければならないのは、上記の回転が適用される順番です。
- 画面を上にして地面に置いて、北を向けた状態から
- Z軸を中心に$\alpha$度回転
- 回転後のX'軸を中心に$\beta$度回転
- 回転後のY''軸を中心に$\gamma$度回転
というZXYの順番で回転させると考えます。この順番も後々重要になります。
DeviceOrientation Event Specification (日本語訳)4.1. deviceorientation イベントちなみに、オイラー角表現による回転の順番についてはジンバルをイメージすると理解しやすいと思います。
下記動画が大変分かりやすかったです。
ジンバルロックとは?ジンバルロックを回避する方法をわかりやすく解説します【Maya作業画面】 - YouTubeOrientation Sensor APIで取得できる値
AbsoluteOrientationSensor
?またはRelativeOrientationSensor
?では、クォータニオンの情報を取得できます。
具体的には、$(V_x, V_y, V_z)$で表せる単位ベクトルを軸に角度$\theta$だけ回転させた場合、下記4つの値が取得されます。
$[V_x\times\sin\frac{\theta}{2}\ ,\ V_y\times\sin\frac{\theta}{2}\ ,\ V_z\times\sin\frac{\theta}{2}\ ,\ \cos\frac{\theta}{2}]$本記事ではクォータニオンについて上記定義ぐらいに留め、深入りしないことにします。(筆者がよく分かってない)
以下の記事がとても詳しく書かれているので、興味のある方はぜひ。
クォータニオン (Quaternion) を総整理! ~ 三次元物体の回転と姿勢を鮮やかに扱う ~ - QiitaCSSのtransformで変換関数が評価される順番
突然ですが、下の牛さんをYZ平面のY軸上に立たせたいとしたら、どのように回転させればよいでしょうか。
例によって見やすいように視点を移動した風にしてますが、各軸はCSSの座標系を表すものとします。See the Pen 210105002 by Arakaki Tokyo (@arakaki-tokyo) on CodePen.
上記のセンサーで回転が適用される順番と同様に考えれば、
- Z軸を中心に90度回転
- 回転後のX'軸を中心に−90度回転
と動かせばよさそうです。
これを実現するCSSは以下のようになります。#cow2 { transform: rotateZ(90deg) rotateX(-90deg); }See the Pen 210105003 by Arakaki Tokyo (@arakaki-tokyo) on CodePen.
期待通りの結果になりました。
ところで、上記transform
のプロパティに記述した変換関数は、本当にrotateZ()
→rotateX()
の順番に評価されたのでしょうか?
というのも、上ではセンサーのオイラー角と同様に考えてみましたが、以下の順番で動かすと考えることもできます。すなわち、
- X軸を中心に−90度回転
- 元のZ軸を中心に90度回転
こうして考えた場合、変換関数は
rotateX()
→rotateZ()
の順番に(つまり右から左に)評価されたことになります。
同じ処理なのに考え方によって順番が逆になるので困惑してしまいますが、結論を言うと内部の処理としては後者の考え方が適切です。(次節で確認します。)が、あくまでも"考え方"としてであれば、状況に応じて分かりやすいように捉えればいいんじゃないかと思います。例えば、
- 回転されるオブジェクトの立場になって、自分自身の軸を中心に回転していくなら左から右に
- 画面から見ている立場として、画面の軸を中心に回転させてくなら右から左に
または、すでにいくつかの変換関数が適用されている状態でさらに回転させたい場合、
- オブジェクトの軸で回転を追加したいなら右端に追記
- 画面の軸で回転を追加したいなら左端に追記
といった感じで。
余談ですが上の回転は以下の方法でも実現可能です。
#cow3 { transform: rotateY(-90deg) rotateZ(90deg); } #cow4 { transform: rotateX(-90deg) rotateY(-90deg); }See the Pen 21010504 by Arakaki Tokyo (@arakaki-tokyo) on CodePen.
各軸回りに回転させて姿勢を特定するという方法では、回転させる軸の順番で複数のパターンがありえます。
逆に言えば、各軸で回転させる角度が分かっていても、回転させる軸の順番によって結果が変わってしまいます。
オイラー角の説明の際に順番を意識しなければならないと述べたのはこれが理由です。CSSのmatrix3dについて
3次元の変換を4×4の行列で表現したものです。?
変換行列については検索すれば数多の情報が出てきますが、例えばなんとなく雰囲気を感じたければこちらだったり、そもそも行列とは?なんで変換?という方はこちらが分かりやすかったです。
(筆者も文系出身の行列未経験者だったので、今回色々調べて勉強になりました)さて、今回はそんな行列の中でも回転行列に注目します。
x軸、y軸、z軸周りの回転を表す回転行列は、それぞれ以下のようになります。?R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\ \,\\ R_y(\theta) = \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\ \,\\ R_z(\theta) = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}この回転行列ですが、今回対象とするセンサーの右手座標系でも、CSSの左手座標系でも共通です。
例えば点$(1,0,0)$をZ軸で90度回転させると点$(0,1,0)$となります。これはZ軸の正の方向から見て、センサーの右手系の座標では反時計回り、CSSの左手系の座標では時計回りとなっていて、各座標系での正の回転方向と一致しています。
他の軸についても同様で、つまり特定の座標系で完結している限り、回転行列自体は回転の方向には関与しないのです。それでは回転行列をCSSに適用するべくmatrix3dの形にしてみましょう。今回はJSで以下の関数を定義しました。
const d2r = deg => deg/180 * Math.PI; function rotateX(deg) { rad = d2r(deg); return [ 1, 0, 0, 0, 0, Math.cos(rad), Math.sin(rad), 0, 0, - Math.sin(rad), Math.cos(rad), 0, 0, 0, 0, 1 ] } function rotateY(deg) { rad = d2r(deg); return [ Math.cos(rad), 0, - Math.sin(rad), 0, 0, 1, 0, 0, Math.sin(rad), 0, Math.cos(rad), 0, 0, 0, 0, 1 ] } function rotateZ(deg) { rad = d2r(deg); return [ Math.cos(rad), Math.sin(rad), 0, 0, - Math.sin(rad), Math.cos(rad), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] } function dot(m1, m2) { // const retval = [Array(4), Array(4), Array(4), Array(4)]; const retval = Array(16); for (i = 0; i < 4; i++) { for (j = 0; j < 4; j++) { let c = 0; for (k = 0; k < 4; k++) { // c += m1[i][k] * m2[k][j]; c += m1[i * 4 + k] * m2[k * 4 + j]; } // retval[i][j] = c; retval[i * 4 + j] = c; } } return retval; }
d2r()
は度数法から弧度法への変換、dot()
は行列の積を返します。当初は2次元配列で扱おうとしていたものを1次元配列に直した名残がありますが、やはり2次元配列で考えたほうが分かりやすいですね。
さて、注目してほしいのはrotateX()
、rotateY()
、rotateZ()
で$\sin\theta$のマイナス符号が入れ替わっている点です。
なぜこうなるのかと言うと、matrix3dは4×4行列の16個の値を列優先データ順(column-major order)で表現したものだからです。matrix3d() = matrix3d( <number>#{16} )
specifies a 3D transformation as a 4x4 homogeneous matrix of 16 values in column-major order.
CSS Transforms Module Level 2(太字筆者)
matrix3d()
関数は16の値で指定します。列優先の順で記述します。
matrix3d(a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, c3, d3, a4, b4, c4, d4)
matrix3d() - CSS: カスケーディングスタイルシート | MDN(太字筆者)これは単にデータ保持形式を意味していて、以下のa〜d行1〜4列の行列の、列ごとに連続して値を保持しますよ〜ということのようです。
ref. 列優先(Column-major),行優先(Row-major)は複数ある - Qiita\begin{bmatrix} a1 & a2 & a3 & a4 \\ b1 & b2 & b3 & b4 \\ c1 & c2 & c3 & c4 \\ d1 & d2 & d3 & d4 \\ \end{bmatrix} → [a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, c3, d3, a4, b4, c4, d4]で、この列優先で並べた配列の要素を4つずつで改行すると転置行列に見えるので、ちょうど$\sin\theta$のマイナス符号が入れ替わっているような姿となっている訳です。
ここで注意喚起しておきたいことがあります。
上では「特定の座標系で完結している限り、回転行列自体は回転の方向には関与しない」と述べました。
しかし今回は、右手系のセンサーの回転を左手系のCSSに適用させようという試みです。Y軸の回転方向は見かけ上一致しますが、X軸とZ軸では反対方向に回転するので、その分を変換しなければなりません。
具体的には、センサーのZ軸で$\alpha$度したらCSSのZ軸では$-\alpha$度回転させることになります。(X軸でも同様)
これを回転行列に当てはめると以下のように転置行列となり、上で定義した関数と同じように見えてしまいます。R_z(-\alpha) = \begin{bmatrix} \cos(-\alpha) & -\sin(-\alpha) & 0 & 0 \\ \sin(-\alpha) & \cos(-\alpha) & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} \cos\alpha & \sin\alpha & 0 & 0 \\ -\sin\alpha & \cos\alpha & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}実装の考え方は様々あるかと思いますが、今回定義した
rotateX()
、rotateY()
、rotateZ()
の各関数は与えられた角度の回転行列を列優先の順で記述した配列を返すだけであり、座標系の変換については考慮していません。
これを混同してしまうとドツボにはまる(はまってしまった…)ので、何卒ご注意ください。
回転行列$R(\theta)$の列優先データ順での表現と、逆回転を表す行列$R(-\theta)$は、どちらも転置行列$R^\mathsf{T}(\theta)$となる。混同しないよう注意。
それではこの関数でmatrix3dを使ってみましょう。
再度牛さんに登場してもらって、先程と同じ回転をさせるには以下のようにします。$("cow3").style.transform = `matrix3d(${dot(rotateX(-90),rotateZ(90))})`;See the Pen 21010505 by Arakaki Tokyo (@arakaki-tokyo) on CodePen.
回転行列の積も回転行列になるわけですが、ここでも順番が重要になります。
rotateX(-90)
の回転行列にrotateZ(90)
の回転行列を掛ける場合、
- X軸を中心に-90度回転
- 元のZ軸を中心90度回転
という順番で回転させる行列を表します。
この順番には見覚えがありますね。そうです、CSSでのtransform: rotateZ(90deg) rotateX(-90deg);
という記述で変換関数を右から評価したと考える場合と一致します。
先だって「変換関数は右から評価されると考えるのが適切」と述べましたが、以下仕様に沿って詳しく確認してみます。まず、
transform
プロパティに記述された各変換関数は、左から右に(from left to right)post-multiplyされます。Multiply by each of the transform functions in transform property from left to right
ref.CSS Transforms Module Level 2(太字筆者)
Post-multiply all <transform-function>s in <transform-list> to transform.
ref. CSS Transforms Module Level 2(太字筆者)
post-multiply
post-multipliedTerm A post-multiplied by term B is equal to A · B.
ref. CSS Transforms Module Level 1いきなり難解な表現になりましたが、要は
transform: A B C;
という記述があったとき、$(C \times (B \times A))$として計算するということです。これは結合法則によって$((C \times B) \times A)$と同等です。
(ここらへん、ドキュメント読んでもはっきりと確信が持てませんでした…解釈違い等あればご指摘ください?)そして計算後の値が一つの
matrix3d()
(またはmatrix()
)にまとめられることになります。A <transform-list> for the computed value is serialized to either one <matrix()> or one <matrix3d()> function by the following algorithm
CSS Transforms Module Level 2(引用文中の"following algorithm"というのが上述の処理)回転についてさらに掘り下げると、各軸回りの回転は
rotate3d()
に置き換え可能です。rotateX() = rotateX( [ <angle> | <zero> ] )
same as rotate3d(1, 0, 0, <angle>).rotateY() = rotateY( [ <angle> | <zero> ] )
same as rotate3d(0, 1, 0, <angle>).rotateZ() = rotateZ( [ <angle> | <zero> ] )
same as rotate3d(0, 0, 1, <angle>), which is a 3d transform equivalent to the 2d transform rotate(<angle>).
CSS Transforms Module Level 2さらに
rotate3d(x, y, z, α)
は以下の行列に変換されます。\begin{bmatrix} 1-2 \cdot(y^2 + z^2)\cdot sq & 2\cdot(x\cdot y\cdot sq - z\cdot sc) & 2\cdot(x\cdot z\cdot sq - y\cdot sc) & 0\\ 2\cdot(x\cdot y\cdot sq + z\cdot sc) & 1-2 \cdot(x^2 + z^2)\cdot sq & 2\cdot(y\cdot z\cdot sq - x\cdot sc) & 0\\ 2\cdot(x\cdot z\cdot sq - y\cdot sc) & 2\cdot(y\cdot z\cdot sq + x\cdot sc) & 1-2 \cdot(x^2 + y^2)\cdot sq & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\ \,\\ sc = \sin(\frac{\alpha}{2})\cdot\cos(\frac{\alpha}{2}) \\ sq = \sin^2(\frac{\alpha}{2})CSS Transforms Module Level 2 | #Rotate3dDefined
この行列に
rotateZ(θ)
、すなわちrotate3d(0, 0, 1, θ)
を当てはめてみます。\begin{bmatrix} 1-2 \cdot sq & -2\cdot sc & 0 & 0\\ 2\cdot sc & 1-2 \cdot sq & 0 & 0\\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} = \begin{bmatrix} 1-2 \cdot \sin^2\frac{\theta}{2} & -2\cdot \sin\frac{\theta}{2}\cdot\cos\frac{\theta}{2} & 0 & 0\\ 2\cdot \sin\frac{\theta}{2}\cdot\cos\frac{\theta}{2} & 1-2 \cdot \sin^2\frac{\theta}{2} & 0 & 0\\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\ \,\\ = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0\\ \sin\theta & \cos\theta & 0 & 0\\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} \\はい、高校数学の教科書を引っ張り出して2倍角の公式と半角の公式を駆使すると
見覚えのある回転行列になりました。これはrotateX()
でもrotateY()
でも同様です。
以上より、transform: rotateZ(90deg) rotateX(-90deg);
は結局$ R_x(-90)\cdot R_z(90)$を計算した行列を列優先でmatrix3d()
として変換したものとなり、上のJSのスクリプトはその処理を再現しただけという事になるわけです。
【コメント受け訂正・追記】
以上より、transform: rotateZ(90deg) rotateX(-90deg);
は列優先でmatrix3d()
にシリアライズするという意味での転置行列 $ R_x^\mathsf{T}(-90^\circ)\cdot R_z^\mathsf{T}(90^\circ)$ を計算したものとなり、上のJSのスクリプトはその処理を再現しただけという事になるわけです。なお、転置行列の性質として以下のようなものがあります。
$ (AB)^\mathsf{T} = B^\mathsf{T}A^\mathsf{T}$
ref. 転置行列の基本的な4つの性質と証明 | 高校数学の美しい物語CSSでは
matrix3d()
に変換するときだけ列優先に(すなわち転置行列に)すればよいので、上のtransform
プロパティについて実装上は$ (R_z(90^\circ)\cdot R_x(-90^\circ))^\mathsf{T} $として処理しても同様の結果が得られます。
このように考えたほうが分かりやすいですかね。結局"考え方"としては前述の通り状況に応じて考えればよくて、matrix3d()
を直接扱うような場合にはここでの記述を踏まえて考えるというように切り分ければよいと思います。
【追記ここまで】1. DeviceOrientation Event
予備知識編がだいぶ長くなってしまいました…。
以下ではサクサク実装例を見ていきます。1_1. CSSの回転関数を使う
素直に取得した値で各軸回りに回転させます。
センサーでは以下の順番で回転させるのでした(再掲)。
1. Z軸を中心に$\alpha$度回転
2. 回転後のX'軸を中心に$\beta$度回転
3. 回転後のY''軸を中心に$\gamma$度回転これをCSSで考えると、
transform
プロパティで左から右に考えるのでした。(考え方)
さらに、センサーの座標系とCSSの座標系では、X軸とZ軸で回転方向が逆になるのでした。(CSSの座標系)という訳で、以下のようなコードになります。
window.addEventListener("deviceorientation", ev => { document.getElementById("cube1_1").style.transform = `rotateX(90deg) rotateZ(${- ev.alpha}deg) rotateX(${- ev.beta}deg) rotateY(${ev.gamma}deg)`; })最初の
rotateX(90deg)
は視点を横からにした風の操作です。
そのままの状態では上(センサーのZ軸の正の方向)から見た視点となるので、スマホを手に持って画面を見る視点になるようにしました。
デモではサイコロの1がスマホの画面となります。(以下同)1_2. 行列計算する
上でやっていることをJSで再現するだけです。
window.addEventListener("deviceorientation", ev => { const matrix3d = dot(dot(rotateY(ev.gamma), rotateX(-ev.beta)), rotateZ(-ev.alpha)); document.getElementById("cube1_2").style.transform = `rotateX(90deg) matrix3d(${matrix3d})`; })2. RelativeOrientationSensor
2_1. populateMatrixメソッドを使う(失敗)
OrientationSensorインターフェースには
populateMatrix()
というメソッドがあります。
- ref.
const options = { frequency: 15, referenceFrame: 'device' }; const sensor1 = new RelativeOrientationSensor(options); sensor1.start(); sensor1.addEventListener('reading', function (ev) { const targetMatrix = new Float32Array(16); sensor1.populateMatrix(targetMatrix); document.getElementById("cube2_1").style.transform = `rotateX(90deg) matrix3d(${targetMatrix}) `; }実際の動きを見ると、CSS的にオイラー角を
transform: rotateY(-γ) rotateX(-β) rotateZ(-α);
と記述した場合と同じ回転になりました。
行列を転置してみたりY軸で反転させてみたりしましたがダメ…(原因は次節)2_2. populateMatrixメソッドの処理を実装
上のリンク先でも載っている計算を実装してみます。
const x = this.quaternion[0]; const y = this.quaternion[1]; const z = this.quaternion[2]; const w = this.quaternion[3]; const matrix2 = (function (x, y, z, w) { return [ x * x - y * y - z * z + w * w, 2 * (x * y + z * w), 2 * (x * z - y * w), 0, 2 * (x * y - z * w), -x * x + y * y - z * z + w * w, 2 * (y * z + x * w), 0, 2 * (x * z + y * w), 2 * (y * z - x * w), -x * x - y * y + z * z + w * w, 0, 0, 0, 0, 1 ] })(x, -y, z, -w); document.getElementById("cube2_2").style.transform = `rotateX(90deg) matrix3d(${matrix2}) `;例によって列優先、若干一部の計算が異なるのは $ x^2 + y^2 + z^2 + w^2 = 1$ を使うかどうかの違いです。
実際の計算にあたってはy
とw
をマイナス倍してますが、y
はY軸反転だから、w
は回転が逆方向だからです。センサーから取得されたクォータニオンでは、ベクトルの先から原点を見て反時計回りが正の回転っぽい。(右手系)
右手系では回転方向は反時計回りを正の向きとして測る
クォータニオン (Quaternion) を総整理! ~ 三次元物体の回転と姿勢を鮮やかに扱う ~ - Qiita対してCSSの
rotate3d
は原点を見て時計回りが正の回転です。(左手系)the rotation is clockwise as one looks from the end of the vector toward the origin.
CSS Transforms Module Level 2クォータニオンから行列に変換する式をよく見ると、
w
をマイナス倍すれば転置行列になることが分かるかと思います。上でpopulateMatrixメソッドをそのまま使ってダメだったのは、こういった操作ができないからですね。
行列の演算でどうにかできるのかなぁ…2_3. CSSのrotate3d()を使う
クォータニオンって実はほぼほぼrotate3d()の引数と一致するんですよね。
定義再掲$(V_x, V_y, V_z)$で表せる単位ベクトルを軸に角度$\theta$だけ回転させた場合、下記4つの値が取得されます。
$[V_x\times\sin\frac{\theta}{2}\ ,\ V_y\times\sin\frac{\theta}{2}\ ,\ V_z\times\sin\frac{\theta}{2}\ ,\ \cos\frac{\theta}{2}]$x, y, zは定数倍してるだけなので方向を表す上では関係なさそう、なので$\cos\frac{\theta}{2}$からθを計算すればよいだけです。
document.getElementById("cube2_3").style.transform = `rotateX(90deg) rotate3d(${x}, ${-y}, ${z}, ${2 * Math.acos(-w)}rad)`;3. 方角の絶対値
ここまでで紹介したコードは全て、方角(alpha値)を相対値を取得します。
以下の方法で(デバイスが対応していれば)絶対値を取得できます。
CSSへの反映はすでに紹介した方法です。3_1. DeviceOrientAtionabsolute Event
window.addEventListener("deviceorientationabsolute", ev => { document.getElementById("cube3_1").style.transform = `rotateX(90deg) rotateZ(${- ev.alpha}deg) rotateX(${- ev.beta}deg) rotateY(${ev.gamma}deg) `; })3_2. AbsoluteOrientationSensor
const sensor2 = new AbsoluteOrientationSensor(options); sensor2.start(); sensor2.addEventListener('reading', function (ev) { const x = this.quaternion[0]; const y = this.quaternion[1]; const z = this.quaternion[2]; const w = this.quaternion[3]; document.getElementById("cube3_2").style.transform = `rotateX(90deg) rotate3d(${x}, ${-y}, ${z}, ${2 * Math.acos(-w)}rad)`; })終わりに
センサーやらCSSの3Dやら行列計算やら、初めてづくしで調べながら書いたのでなかなか大変でした…
至らない点も多々あるかと思うので、間違いやご指摘などコメントいただければ嬉しいです!余談ですが回転行列の仕組みを理解しようと、これも初めてGeoGebraを使ってみました。
回転行列 – GeoGebra
ノンプログラミングでこれだけ多機能、強力なビジュアライズができるウェブアプリがあるのかと感動しました。すごい。
- 投稿日:2021-01-01T01:46:04+09:00
打倒レンダリングブロック!(JavaScript編)
はじめに
CSSやJavaScriptなどの読み込み方法(記述)によってはレンダリングを妨げることとなります。
本記事ではJavaScriptの読み込み(記述)に焦点を当てて、レンダリングブロックの対策について説明します。レンダリングブロック対策の基本はHTMLをとにかく速く出力することです。
この前提をもとに、対策方法について説明できればと思います。レンダリングの基本
ブラウザにおけるレンダリングの流れは大まかに以下のようになります。
- HTMLをパース。
- DOMツリーを構築する。
- CSSのマークアップからCSSOMツリーを構築する。
- JavaScriptを実行する。
- DOMとCSSOMを組み合わせてレンダリングツリーを構成する。
- 各ノードのビューポート内での正確な位置とサイズをレイアウトする。
- 各ノードを画面に描画する。
ページにアクセスしてからデータを読み込んで描画するまでの処理手順をクリティカルレンダリングパスといいます。このフローを最適化することがレンダリングブロックへの対策となり、JavaScriptの読み込み方ひとつで最適化できます。
では具体的にどういったことに気をつければ良いのか、3つに分けて紹介します。
- 推奨パターン
- アンチパターン
- ケースバイケース
推奨パターン
async・defer属性で読み込み
記述に属性がない場合、途中でリソースを見つけるとパースを中断しリソースの読み込み・実行を行います。(これが所謂レンダリングブロックです。)
async・defer属性を付けることで非同期で(HTMLのパース/DOM構築を阻害することなく)読み込めます。
ただし非同期で行われるのは読み込みのみで、リソースの実行はパースが中断されます。async・deferの違いはJavaScriptの実行タイミングにあります。
async
:リソースが読み込まれた直後に実行されます。
defer
:HTMLパース完了後(DOMContentLoadedの直前)に実行されます。async属性の特徴
<head> <script src="foo.js" async></script> <script src="bar.js" async></script> </head> <body>...</body>
- 非同期で読み込まれたあと即座に実行されます。読み込まれたものから実行されるため順番が担保されません。
- トラッキングコード/計測タグなど、他のJavaScriptに依存しないファイルの読み込みに使用。
</body>
直前の読み込みでは非同期の利点が活かされないため、<head>
内で読み込むこと。defer属性の特徴
<head> <script src="foo.js" defer></script> <script src="bar.js" defer></script> </head> <body>...</body>
- 非同期で読み込まれ、HTMLパース完了後(DOMContentLoadedの直前)に実行されます。記載している順番に実行されます。
- DOMに依存する、他のJSに依存するファイルの読み込みに使用。
</body>
直前の読み込みでは非同期の利点が活かされないため、<head>
内で読み込むこと。アンチパターン
インラインJS
<head> <script> setTimeout(function() {document.body.appendChild(document.createElement("div")}, 1000); </script> </head> <body> <p>テキスト1</p> <script> setTimeout(function() {document.body.appendChild(document.createElement("div")}, 1000); </script> <p>テキスト2</p> </body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。- HTML文書のサイズを肥大化させてしまう。
- ブラウザのローカルキャッシュが効かない。
属性無しで読み込み
<head> <script src="foo.js"></script> </head> <body> <p>テキスト</p> </body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。
</body>
の直前で読み込み<head>...</head> <body> <p>テキスト</p> <script src="foo.js" async></script> </body>
推奨しない理由
<script>
タグ以降のDOMの構築をブロックしてしまう。- async・defer属性を付けても、パース終了間近に読み込みを開始するため非同期の利点が活かされない。
async・defer属性を両方指定
<head> <script src="foo.js" async defer></script> </head> <body>...</body>
推奨しない理由
- syncに対応していないブラウザはdeferで読み込む記述方法だが、現在そんなブラウザはない。
link rel preloadで読み込み
<head> <script src="foo.js"></script> <link rel="preload" as="script" href="bar.js"> </head> <body>...</body>
推奨しない理由
- async・deferで代替できる。
- ブラウザに依存する記述(IE・FireFoxでは使えない)。
ケースバイケース
Script-injectedでの読み込み
Script-injectedは外部リソース読み込みのソースコードを出力するインラインJSです。(以下の記述を参照)
Script-injected 実行前
<head> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-XXXXXX');</script> </head> <body>...</body>Script-injected 実行後
<head> <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX"></script> </head> <body>...</body>
推奨しない理由
- 非同期読み込みを実現できるが、インラインJSのため基本は使用しない。
- Google Tag Managerなどはこのような仕組みだが、計測イベントのタイミングがシビアでなければ外部ファイルにしてasync・defer属性での読み込みも検討するとベター。
まとめとポイント
- async・defer属性を使用して
<head>
内で読み込む。(deferは実行順序を保証するが、asyncは保証しない)- インラインJSを記述しない。
- Google Tag Managerなどトラッキングコード/計測タグはケースバイケース。
- JavaScriptは複数ファイルに分けずに出来る限り結合・圧縮する。(肥大化しすぎるのも問題なので適宜調整する)
参考URL
フロントエンドのパフォーマンスを徹底解説!ブラウザの気持ちで理解するHTML/Javascript/CSSの話
https://techblog.raccoon.ne.jp/archives/53180280.htmlrel=”preload”を極めるために必要な2種類のプリロード機能
https://techblog.raccoon.ne.jp/archives/1575956867.htmlJavaScript ファイルの読み込みと実行のタイミングを調べる(async, defer)
https://misc.laboradian.com/js-async/1/