20211205のJavaScriptに関する記事は30件です。

【JavaScript】変数宣言の使い分けについて(var let const)【初学者の疑問点を簡潔に解説】

はじめに  本記事は、プログラミングの学習を始めて1ヶ月の初学者が、学習を進めていて疑問に思った点について調べた結果を備忘録も兼ねてまとめたものです。  そのため、記事の内容に誤りが含まれている可能性があります。ご容赦ください。  間違いを見つけた方は、お手数ですが、ご指摘いただけますと幸いです。 今回の疑問点  今回の疑問点は、   JavaScriptにおける変数宣言の使い分け    です。   最近、JavaScriptを勉強し始め、上記の点については一度まとめておいた方が良いと感じたので、記事にすることにしました。 疑問点についての解説 結論  varは現在は、ほとんど使用されていない。  letは再代入が必要なときに使用し、constは定数的に扱う場合に使用する。 それぞれの変数宣言の違いと特徴  それぞれの再宣言、再代入、スコープについてまとめます。 再宣言 再宣言:一度宣言した変数名と同名の変数を再度宣言すること var → 可能。 let → Uncaught SyntaxErrorが出る。不可。 const → Uncaught SyntaxErrorが出る。不可。 再代入 再代入:変数に値を代入した後に別の値を代入すること(変数の中身だけを変える) var → 可能。 let → 可能。 const → Uncaught TypeErrorが出る。不可。 スコープ スコープ:変数の有効範囲 グローバルスコープ:どこからでも参照可能 関数スコープ(ローカルスコープ):関数の中でのみ有効 ブロックスコープ(ローカルスコープ):ブロック()や{}の中でのみ有効 var → グローバルスコープと関数スコープの変数を宣言可能。 let → ブロックスコープの変数を宣言可能。 const → ブロックスコープの変数を宣言可能。 まとめ 最後にポイントをまとめます。 var let const 再宣言 ○ × × 再代入 ○ ○ × スコープ グローバル・関数 ブロック ブロック  上記の表から、varはスコープも広く一見便利そうに見えますが、スコープが広いことで、意図しない参照等からエラーが発生する可能性を高めてしまうため、現在はあまり使用されていません。  letとconstでは、letが再代入可能、constでは不可であるため、 letを再代入が必要な場合に、 constは定数的な扱いをする場合に使用します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの数値型

2進数の世界で動いている汎用計算機が処理する数値は、整数か浮動小数点数の2つに分けられます。C言語の型ではintやdoubleです。それぞれの型はメモリ上でのビット長が決まっており、32bitや64bitです。 言語によっては数値の精度を必要ならいくらでも伸ばしたりできるような任意精度演算 という演算システム(実際上は利用可能なメモリ容量に制限される)による演算が可能な言語もあります。 ここではJavaScriptで数値がどのように扱われているのかを記述します。 JavaScriptの数値は1種類 JavaScriptにの数値型は1種類しかありません。 JavaScriptでは整数・浮動小数点数の区別がなくすべての数値が 64ビットの浮動小数(double型) のみです。 以下のようなプログラムでは一見整数を扱ってるように見えますが、これはすべて小数としてデータを扱ってることになります。 const x = 1 const y = 2 console.log(x + y) //3 double型でも52ビットをはみ出ない限り、整数は厳密に計算されます。 この出力は 3と表示されますが、内部的には 3.0 === 3 と言うことになります。 IEEE 754 倍精度浮動小数点数 JavaScriptの数値型は IEEE 754 倍精度浮動小数点数 ( double )です。 これは64ビットの浮動小数点数表現の規格の一つです。doubleによる浮動小数点数の表現は広く使われており、コンピューターの小数表現のスタンダードです。 計算機の世界では有限個のデータしか扱えないので、プログラムで表す数値の精度には限界があります。 以下のよく知られた例を見てみましょう console.log(0.1 + 0.2) 特に意識しなければ上記の例は 0.3 と出力されそうですが、実際には 0.30000000000000004 と出力されるはずです。 こういう例もある console.log(0.3 === 0.1 + 0.2) /* 出力:false */ なぜこのようなことになるかと言うと、0.1 や 0.2という数が切りの悪い数なので正確に表現することができないのが原因です。 0.1 は2進数で表すと 0.00011001100... と無限小数になり、double型で表現できる一番近い値に丸められます。 丸められた結果を10進数に戻すと 0.1000000000000000055511151231257827021181583404541015625 というような値になります。 0.2も同様に丸められ10進数に戻すと以下のような値になります。 0.200000000000000011102230246251565404236316680908203125 0.1 + 0.2 の計算結果は以下の通りです。 0.3000000000000000444089209850062616169452667236328125 これは 0.1 、0.2 の丸められた誤差、 0.1 + 0.2 の加算で発生した丸めの誤差が計算結果に蓄積されているということです。これは、JavaScriptが実際に計算するのは 正確な 合計です。 プログラムで0.3 と書いた時点でもすでに誤差は発生しています。 コンピューターは実際の 0.3 に最も近いdouble型で表現な可能な数(↓)で解釈されます。 0.299999999999999988897769753748434595763683319091796875 これは 0.3をdoubleで表現しようとすると負の方向に誤差が発生しますが、 0.1 と 0.2 ともに正の誤差を持っており、これらが蓄積した 0.1 + 0.2 は 0.3 から離れるため、0.3 ( double型で表現可能な 0.3 に最も近い値)からずれてしまいます。このズレはビット表現ではわずか1ビット。0.3 にちょうど1ビットです。これは 0.3 にちょうど1ビット分の値である $2^{-54}$ を足すと0.1 + 0.2になります。 // これはtrueと出力される console.log(0.3 + 2 ** (-54) === 0.1 + 0.2) ここで言いたいことは、このような挙動はJavaScriptに関わらず 浮動小数点数(IEE754)の仕様 であり、 コンピューターで小数を表現しようとする限り避けようのないものであると言うことです。 JavaScriptの数値精度は52ビット JavaScriptの数値は整数・浮動小数点数で区別がないことは冒頭で説明しましたが、 これは 整数だろうと小数だろうとすべてdoubleで表現している ということです。 つまり JavaScriptの整数の精度も52ビット ということになります。 整数と小数を区別する他のプログラミング言語では整数の精度は32ビットだったり64ビットだったりするので、それに比べるとかなり中途半端に感じると思います。ただそれらの言語と比べるとJavaScriptでの整数はdoubleであることに由来する 下の桁から大雑把になっていく という挙動をすることがあります。 整数の精度が52ビットということは、 $0$から$2^{53}-1$ までの整数は正確に解釈できることを意味します。これで重要なのは、ここで正確に表現できるというのは 1つ違う隣の整数と区別できる 事を意味します。以下の例は$2^{53}-1$が両隣の数と区別できることを表しています。 console.log(2 ** 53 - 2 === 2 ** 53 -1) // faise (2 ** 53 - 2 === 2 ** 53 -1は違うもとの解釈される console.log(2 ** 53 - 1 == 2 ** 53) // false (2 ** 53 - 1 == 2 ** 53は違うもと解釈される NaN と Infinity、+0 と- 0 JavaScriptには NaN と Infinity という特別な数値が存在します。 NaNはNot a Numberの略で有ることは知られていますが、JavaScriptでは数値の一種なので型を調べると以下のように数値型となります。 console.log(typeof NaN) // number 0 / 0 という計算をするとNaNとなります。 NaNの面白い挙動は NaN === NaN とするとfalseとなるところです。 NaN < NaN 等のNaN含む比較演算子は全部falseになります。ある数値をNaNか判定したい場合は isNaN を使うといいでしょう。 Infinity も同様で、これは「無限大」を表現する特別な数値で、IEEE 754由来です。無限大には正の無限大、負の無限大があります。 doubleには +0 と -0 という2種類の0が存在し、JavaScriptでも2種類の0が確認できます。(通常の0は正の +0 となります)。+0 と -0 を === で比較してもtrueとなるのが特徴です。 このようなJavaScriptにおいて使われている等値比較演算子である === もIEEE 754の影響をうけています。 ES2015以降では、IEEE 754の影響を排除した等値比較の手段として Object.is という関数が用意されてるようです まとめ ここではJavaScriptの数値型について書いてみました。 ポイントはJavaScriptの数値型は整数と浮動小数点数を区別せず、 IEEE 754倍精度浮動小数点数 で表す点です。 その結果整数が52ビットの精度をもっており、浮動小数点数特有の 0.3 !== 0.1 + 0.2 のような挙動が現れます。どの挙動がJavaScript特有の物でどの挙動がIEEE 754由来の話なのかをよく考えてコーディングするといいでしょう。 参考文献 https://qntm.org/notpointthree
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next2D NoCode ToolのFilterを利用する

フィルターは、グラフィックエフェクトの一種で、ぼかし、ベベル、グロー、ドロップシャドウなど、豊富な視覚効果をDisplayObjectに適用して表示できます。 事前の技術共有事項 フィルター機能はメモリを大量に消費するので、使い過ぎには十分注意してください。 特に広範囲にフィルターを適用した場合、レンダリング処理が重くなります。 但し、座標(x,y)変更であればキャッシュしたフィルターが適用されるためレンダリング負荷は発生しません。ですが、拡大縮小、回転、色の変化もしくはフィルターの値に変更が行われた場合、フレーム毎に再描画をします。 この特性を理解頂き、効果的にご利用頂ければと思います。 DropShadow シャドウアルゴリズムは、ぼかしフィルターで使用するのと同じボックスフィルターに基づいています。ドロップシャドウのスタイルには複数のオプションがあり、内側シャドウ、外側シャドウ、ノックアウトモードなどがあります。 before after 設定値 プロパティ 型 初期値 効果 distance number 4 オフセットの距離。 angle number 45.0 角度を指定。(0~360 単位:度) color number or string 0x000000 カラーを指定。(e.g.: "red", "#990000", 0xff0000) alpha number 1.0 カラーの透明度を指定。(0.0~1.0) blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) strength number 1 インプリントやスプレッドの長さ。(0 ~ 255) quality number 1 フィルタを適用する回数(1~15) inner boolean false 内部ドロップシャドウなら true、外部ドロップシャドウなら false を指定。 knockout boolean false ノックアウト効果の有無 hideObject boolean false オブジェクトを非表示にするなら true を指定。 動作サンプル Blur ぼかし効果は、イメージの細部をぼかします。ソフトフォーカスがかかっているように見えるぼかしから、半透明ガラスを通してイメージを見るようにかすんで見えるガウスぼかしまで作成できます。このフィルターのqualityプロパティを低く設定すると、ソフトフォーカスがかかっているように見えるぼかしになります。qualityプロパティを高く設定すると、ガウスぼかしフィルターに似たものになります。 before after 設定値 プロパティ 型 初期値 効果 blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) quality number 1 フィルタを適用する回数(1~15) 動作サンプル Glow グローのスタイルには複数のオプションがあり、内側グロー、外側グロー、ノックアウトモードなどがあります。グローフィルターは、distanceプロパティとangleプロパティを0に設定したドロップシャドウフィルターによく似ています。 before after 設定値 プロパティ 型 初期値 効果 color number or string 0x000000 カラーを指定。(e.g.: "red", "#990000", 0xff0000) alpha number 1.0 カラーの透明度を指定。(0.0~1.0) blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) strength number 1 インプリントやスプレッドの長さ。(0 ~ 255) quality number 1 フィルタを適用する回数(1~15) inner boolean false 内部ドロップシャドウなら true、外部ドロップシャドウなら false を指定。 knockout boolean false ノックアウト効果の有無 動作サンプル Bevel ボタンなどのオブジェクトにベベル効果を適用すると 3 次元的に表現されます。 異なるハイライトカラー、シャドウカラー、ベベルのぼかし量、ベベルの角度、ベベルの配置、ノックアウト効果を使用して、ベベルの外観をカスタマイズできます。 before after 設定値 プロパティ 型 初期値 効果 distance number 4 オフセットの距離。 angle number 45.0 角度を指定。(0~360 単位:度) highlightColor number or string 0x000000 カラーを指定。(e.g.: "red", "#990000", 0xff0000) highlightAlpha number 1.0 カラーの透明度を指定。(0.0~1.0) shadowColor number or string 0x000000 カラーを指定。(e.g.: "red", "#990000", 0xff0000) shadowAlpha number 1.0 カラーの透明度を指定。(0.0~1.0) blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) strength number 1 インプリントやスプレッドの長さ。(0 ~ 255) quality number 1 フィルタを適用する回数(1~15) type string "inner" ベベルの種類。 knockout boolean false ノックアウト効果の有無 動作サンプル GradientGlow グラデーショングローとは、制御可能なカラーグラデーションによるリアルな輝きです。グラデーショングローは、オブジェクトの内側エッジや外側エッジの周囲、またはオブジェクトの上に適用できます。 before after 設定値 プロパティ 型 初期値 効果 distance number 4 オフセットの距離。 angle number 45.0 角度を指定。(0~360 単位:度) colors array null カラーを配列で指定。(e.g.: ["red", "#990000", 0xff0000]) alphas array null 透明度を配列で指定。(e.g.: [0.0, 0.5, 1.0]) ratios array null 配分を配列で指定。(e.g.: [0.0, 128, 255]) blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) strength number 1 インプリントやスプレッドの長さ。(0 ~ 255) quality number 1 フィルタを適用する回数(1~15) type string "outer" グローの種類。 knockout boolean false ノックアウト効果の有無 動作サンプル GradientBevel グラデーションベベルは、オブジェクトの外側、内側、または上側が斜めになったエッジであり、グラデーションカラーで強調されます。斜めのエッジによってオブジェクトが 3 次元に見えます。 before after 設定値 プロパティ 型 初期値 効果 distance number 4 オフセットの距離。 angle number 45.0 角度を指定。(0~360 単位:度) colors array null カラーを配列で指定。(e.g.: ["red", "#990000", 0xff0000]) alphas array null 透明度を配列で指定。(e.g.: [0.0, 0.5, 1.0]) ratios array null 配分を配列で指定。(e.g.: [0.0, 128, 255]) blurX number 4 水平方向ぼかし量を指定。(0~255 単位:ピクセル) blurY number 4 垂直方向ぼかし量を指定。(0~255 単位:ピクセル) strength number 1 インプリントやスプレッドの長さ。(0 ~ 255) quality number 1 フィルタを適用する回数(1~15) type string "outer" グローの種類。 knockout boolean false ノックアウト効果の有無 動作サンプル 如何だったでしょうか? 今回はエフェクト効果を見ていただきたかったので、サンプル動画を多めに投稿しました。 この他にも、ColorMatrixFilter、ConvolutionFilter、DisplacementMapFilterとあるのですが、NoCode Tool内ではまだ利用ができない為、説明を割愛しました。 Next2D Playerから直接コードから利用は可能ですので、ご興味があればAPI Docsをご覧ください。 もし、この記事を見て、興味が出た方はこちらか動作確認ができます。 NoCode Tool 明日はタイムライン機能の紹介ができればと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ファイルのdropイベントが発火しないときに見るメモ

drag and drop の実装をしたいとき dragenter、dragleave は発火するのに drop だけが発火しない事象に遭遇した。 ブラウザデフォルトの 画像をブラウザ上で表示する機能をキャンセルする必要があった // ファイルドロップしたときに ブラウザデフォルトの 画像をブラウザ上で表示する機能をキャンセル const htmlReplaceCancel = (e) => { e.preventDefault() return false } document.addEventListener('dragover', htmlReplaceCancel) document.addEventListener('drop', htmlReplaceCancel)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JSでフォトシャワーを自作してみた

動機 ・結婚式でみんながとった写真すぐに共有できたらなあ ・いい感じにフォトシャワーみたいなもの作ってみよかなあ とりあえずコードを貼ってみる ・これをコピペしてimage_list内の写真を変更すればとりあえずこんな感じで写真が上から下に降りてくるみたいな感じのことができるはず、、(写真なのでわかりにくいですが) photo_shower.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wedding_photo_shower</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <style> .photo-shower-container { position: relative; height: 100vh; /* コンテナの高さ */ width: 100%; /* コンテナの横幅 */ overflow: hidden; /* コンテナからはみ出した要素を隠す */ background-image: linear-gradient(to right, #FFECD2 0%, #FCB69F 100%); } /* 写真のスタイル */ .picture { position: absolute; animation: animate-picture 10s linear; } .pic { max-width: 600px; max-height: 600px; } /* 写真のアニメーション */ @keyframes animate-picture { 0% { top: 0; opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { opacity: 0; top: 100vh; } } .content { position: relative; } </style> </head> <body> <main class='content'> <div class='photo-shower-container'></div> </main> </body> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> window.addEventListener('DOMContentLoaded', () => { let image_list = [ 'image_cat/cat1.jpeg', 'image_cat/cat2.jpeg', 'image_cat/cat3.jpeg', 'image_cat/cat4.jpeg', 'image_cat/cat5.jpeg', 'image_cat/cat6.jpeg', 'image_cat/cat7.jpeg', 'image_cat/cat8.jpeg', 'image_cat/cat9.jpeg', 'image_cat/cat10.jpeg' ]; // コンテナを指定 const section = document.querySelector('.photo-shower-container'); let field = 'right'; // 左右交互に写真を表示させるため let count = 0; // 写真を生成する関数 function createPic() { const pictureEl = document.createElement('span'); pictureEl.className = 'picture'; let image = document.createElement('img'); image.className = 'pic'; image.src = image_list[count]; image.style.borderRadius = '20px'; // 写真の傾き具合を-30〜30degでランダムに let min = 0 ; let max = 60 ; let rotate_num = Math.floor( Math.random() * (max + 1 - min) ) + min - 30; image.style.transform = 'rotate(' + rotate_num + 'deg)'; // 写真の長辺の長さを指定 const minSize = 400; const maxSize = 600; let size = Math.random() * (maxSize + 1 - minSize) + minSize; // ここで写真のむきを取得。 getPictureDirection(image.src) // dataにはwidthかheight。長い方がstringで入っている。 .then((data) => { if(data === 'width') { image.style.width = size + 'px'; image.style.height = 'auto'; } else { image.style.height = size + 'px'; image.style.weight = 'auto'; } pictureEl.appendChild(image); // 左右の順番に写真が表示されるように if (field === 'right') { pictureEl.style.left = Math.random() * 300 + 'px'; field = 'left'; } else { pictureEl.style.left = Math.random() * 475 + 475 + 'px'; field = 'right' } section.appendChild(pictureEl); }) .catch((err) => { console.log("error", err); }); // 一定時間が経てば写真を消す setTimeout(() => { pictureEl.remove(); }, 10000); count += 1; } // 写真を生成する間隔をミリ秒で指定 setInterval(function () { createPic() }, 2500); }); // 写真を読み込んでから向きの判定 const loadImg = function(src){ return new Promise(function(resolve, reject){ const image = new Image(); image.src = src; image.onload = function(){ resolve(image); } image.onerror = function(error){ reject(error); } }); } // 写真の向きを取得 let getPictureDirection = function getDirection(src) { return new Promise(function(resolve, reject){ loadImg(src) //読み込みが完了した(画像が設定された)Imageオブジェクトを受け取って処理 .then(function(res){ if (res.width > res.height) { resolve('width'); } else { resolve('height'); } }) //読み込みエラー時の処理 .catch(function(error){ console.log(error); }); }); } </script> 補足 ・結婚式では自分のmacと会場のスクリーンをつなぐ感じだったので、自分のmacでいい感じに表示されればよいかなというcssです。 ・写真の大きさは最初はmax-heightとmax-widthを指定するだけだったのですが、いまいちだったので、ちゃんと写真の向きを取得してから、maxの値を決めました。写真をロードするのをまたないといけなかったので、ここでコードが結構増えてしまった。。 ・実際に会場でリハーサルをしたときに写真が見にくかったので、本番は背景を黒にしました。実際表示してみないと分からない部分だなあと思いました。 ・ひとまず結婚式は良い感じだったのでよし!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DenoからFirebaseを使う

新しいJavaScriptランタイムであるDenoと、今年8月にリリースされたFirebase JavaScript SDK v9の組み合わせを試してみたいと思います。 なお、以下のコードはdeno deployでも動作するはずです。 Firebase JavaScript SDKとFirebase Admin Node.js SDK Firebaseの機能を利用するためのSDKには、クライアントサイド向けであるFirebase JavaScript SDKとサーバーサイド向けであるFirebase Admin Node.js SDKが存在します。両者の特徴は以下の通りです。 JavaScript SDK Admin Node.js SDK 実行環境 ブラウザ Node.js 特権 なし あり 提供方法 npm/CDN npm Denoはブラウザ互換のランタイムなので、ブラウザ向けに作られているFirebase JavaScript SDKを利用します。 ちなみに、(v9ではなく)v8を利用する方法を解説した[ドキュメント](https://deno.com/deploy/docs/tutorial-firebase#concepts)でも、ブラウザ向けSDKの利用が推奨されています。 Firebase JavaScript SDKをimportする Firebase JavaScript SDKは有名ライブラリだけあって、複数のCDNからimportできます。 しかし、そのままimportしただけでは動作しないこともあります。各CDNを試してみたところ、以下のようになりました。 動作 型チェック https://esm.sh/firebase@9.6.0 × ○ https://cdn.skypack.dev/firebase@9.6.0 × × https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js ○ × esm.sh以外のCDNはJavaScriptを配信するため、型チェックが効きません(全てany型になる)。 一方、esm.shとskypack.devはバンドル前のファイルを配信するため、エラーが発生します(※)。 ※esm.shのエラーについて firebase SDKを使うためにはfirestore等のSDKも同時にimportする必要があるのですが、そのままimportするとバージョン違いのapp.tsが2種類importされてしまいます。(画像参照) app.tsの中にはfirebaseの初期化コードがあるため、適切な初期化が行われず、error: Uncaught Error: Service firestore is not availableというエラーが発生します。 このエラーはimportするタイミングによって発生したりしなかったりするので、気を付けたほうがいいと思います。(発生原因を特定するのに大分かかりました) そこで、型チェックにはesm.shを利用し、実行にはwww.gstatic.comを利用するという方法を使います。コードは以下のようになります。 // ※型チェックのために、@deno-tyesにesm.shのURLを指定する // @deno-types="https://cdn.esm.sh/v58/firebase@9.6.0/app/dist/app/index.d.ts" import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js"; // @deno-types="https://cdn.esm.sh/v58/firebase@9.6.0/database/dist/database/index.d.ts" import { getDatabase } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-database.js"; ※ // @deno-typesに使うURLの取得方法について https://esm.sh/firebase@9.6.0/app にアクセスすると、型チェック用のURLがx-typescript-typeヘッダーに入っています(画像参照)。そのURLをimport文の上に// @deno-types="<url>"の形で追加することで、型チェックのみにesm.shを使うことができます。 実行する firestore firestoreからデータを取得するサンプルです。問題なく実行できました。 // @deno-types="https://cdn.esm.sh/v57/firebase@9.4.1/app/dist/app/index.d.ts" import { initializeApp } from "https://www.gstatic.com/firebasejs/9.4.1/firebase-app.js"; // @deno-types="https://cdn.esm.sh/v58/firebase@9.4.1/firestore/dist/firestore/index.d.ts" import { doc, getDoc, getFirestore, } from "https://www.gstatic.com/firebasejs/9.4.1/firebase-firestore-lite.js"; const firebaseConfig = JSON.parse(Deno.env.get("FIREBASE_CONFIG")!); const app = initializeApp(firebaseConfig); const db = getFirestore(app); const docSnap = await getDoc(doc(db, "graphList", id)); Realtime Database Realtime Databaseからデータを取得するサンプルです。 // @deno-types="https://cdn.esm.sh/v58/firebase@9.6.0/app/dist/app/index.d.ts" import { deleteApp, initializeApp, } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js"; // @deno-types="https://cdn.esm.sh/v58/firebase@9.6.0/database/dist/database/index.d.ts" import { get, getDatabase, ref, // enableLogging, } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-database.js"; const firebaseConfig = JSON.parse(Deno.env.get("FIREBASE_CONFIG")!); // このコメントアウトを解除すると詳細なログが表示される // enableLogging(console.log); const app = initializeApp(firebaseConfig); const db = getDatabase(app); const docSnap = get(ref(this.#resources.db, `graphList/${id}`)); await deleteApp(app) // これを呼ばないと実行が終わらない このコードをそのまま実行すると、Error: Client is offline.というエラーが発生します。最初はなんのこっちゃと思っていたのですが、enableLoggig()を呼んで詳細なログを出した結果、window.locationへのアクセスでエラーが出ているようでした。 そのため実行時には--locationフラグを付けて実行し、window.locationを設定する必要があります。(参考:#12969) > deno run --allow-net --location http://foo.com mod.ts 更に、そのままでは永遠に実行が終わらないため、処理の終了時にはdeleteApp()関数を呼んで接続を切断する必要があります。 サーバー等で利用する場合は、「一定時間接続が無かったら切断」のような処理を入れる必要があると思われます。 ※記事執筆時点では、deleteApp()関数を呼んでも実行が終了しません。どうやらsetTimeout()が永遠に再帰呼び出しされてしまっている箇所があるようで、この件についてはhttps://github.com/firebase/firebase-js-sdk/issues/5783 で対応待ちです。 セキュリティルール firebaseではデータベースの操作に対してセキュリティルールを設定することができます。 本来であればサーバー側ではセキュリティルールに関係なくアクセスできるAdmin SDKを使うのですが、今はブラウザ向けSDKを使っているのでセキュリティルールを考慮する必要があります。 もし書き込み権限を許可したい場合、「全て許可」してしまうと、外部からデータを操作し放題になってしまいます。 これを防ぐためには、Firebase Authorizationを使用し、 セキュリティルールで、管理者アカウント以外の操作を禁止する サーバー上では、管理者アカウントにログインしてからDBに接続する といった制御を行う必要があります。 この辺は自分でも未検証なので、v8を使用した公式のexampleを参照してください。 (Deno.env.get()を使用してemailとpasswordを取得し、メール認証しているのが分かると思います。) まとめ 時間が無くて不完全ではありますが、Denoでfirebaseを動作させることができました。 注意するポイントとしては、 Admin SDKではなくブラウザ向けのSDKを使う esm.shやskypack.devからimportするとエラーが発生することがあるので、gstatic.comからimportする セキュリティルールに注意する という所に気を付けて使う必要があります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

forEachの中でasync awaitは使えない(コピペ例)

forEachの中で非同期処理で詰まったので備忘録として書く エラー例 console.log("before_forEach", this.datas); this.datas.forEach(async(data) => { //非同期処理 }); console.log("after_forEach", this.datas); 成功例 console.log("before_forEach", this.datas); for (let data of this.datas){ //非同期処理 }); console.log("after_forEach", this.datas); forで書き直せば、処理順はうまくいくはず! 参考にしたandより詳しい記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【HTML/JavaScript】buttonタグでリンクをする方法

buttonタグとは? <button type="button">ボタン</button> ※buttonタグはデフォルトでsubmitになるので、buttonをformタグで囲っておりsubmitしない場合には、「type="button"」をつけましょう。 方法1 buttonタグでリンクをする方法 <button type="button" onclick="loction.href='URL'">ボタン</button> 具体例 <button type="button" onclick="location.href='https://qiita.com/solidstate'">Qiitaマイページ</button> buttonタグでリンクをする方法(別タブで開く) <button type="button" onclick="window.open('URL', '_blank')">ボタン</button> 具体例 <button type="button" onclick="window.open('https://qiita.com/solidstate', '_blank')">Qiitaマイページ</button> 方法2 <a class="button" href="https://qiita.com/solidstate">Qiitaマイページ</a> .button { display: inline-block; color: #333; padding: 5px; border: 1px solid #ccc; background-color: #f2F2f2; text-decoration: none; }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ツールチップの表示・非表示を切り替えるためにstopPropagationを使った例

この記事について ツールチップを表示・非表示にする実装で詰んでたので忘れないようにメモ 文字だけだと分かりづらいので挙動イメージは下の方でcodepenにまとめておいた ツールチップ表示・非表示の仕様 ツールチップ内の「×」を押すと消える 「?」を押すと表示される ツールチップの外をクリックすると消える ツールチップが開くのは「?」を押したアイテムのみ NG挙動 連続で「?」をクリックしても反応しない ツールチップ内をクリックするとツールチップが消えてしまう See the Pen Untitled by kena-nk (@kena-nk) on CodePen. コードはこんな感じ function Base() { const [question, setQuestion] = React.useState(0); const toggleDescription = (e, name) => (question === name ? setQuestion(null) : setQuestion(name)); const closeDescription = () => { if (question !== null) setQuestion(null); }; return ( <div className="container" onClick={() => closeDescription()}> <div className="list"> <div className="item"> <div>項目1</div> <div className="wrapper"> <div className="far fa-question-circle" onClick={(e) => toggleDescription(e, 1)} /> {question === 1 && ( <div className="description"> <div>項目1の説明が入るよ</div> <div className="far fa-times-circle" onClick={(e) => toggleDescription(e, 1)} /> </div> )} </div> </div> ~~省略~~ </div> </div> ); } OK挙動 連続で「?」をクリックしても反応しない → 解決 ツールチップ内をクリックするとツールチップが消えてしまう → 解決 See the Pen ツールチップOK挙動 by kena-nk (@kena-nk) on CodePen. コードはこんな感じ function Base() { const [question, setQuestion] = React.useState(0); const toggleDescription = (e, name) => { e.stopPropagation(); // e.stopPropagation();追加したよ return question === name ? setQuestion(null) : setQuestion(name); }; const closeDescription = () => { if (question !== null) setQuestion(null); }; return ( <div className="container" onClick={() => closeDescription()}> <div className="list"> <div className="item"> <div>項目1</div> <div className="wrapper"> <div className="far fa-question-circle" onClick={(e) => toggleDescription(e, 1)} /> {question === 1 && ( // onClick={(e) => e.stopPropagation()}追加したよ <div className="description" onClick={(e) => e.stopPropagation()}> <div>項目1の説明が入るよ</div> <div className="far fa-times-circle" onClick={(e) => toggleDescription(e, 1)} /> </div> )} </div> </div> ~~省略~~ </div> </div> ); } 今回の原因 一番外側のDOMに仕掛けてるcloseDescription()がクリックのたびに実行されていることが問題点だった。 本当は内側のDOMごとに仕掛けているtoggleDescription()が発火されてほしいが、親のイベント実行が邪魔してうまくいってないという感じだった。 流れに起こすとこんな感じ↓ 1. 項目1の「?」クリック 2. toggleDescription() が発火して question に 1 がセットされる 3. closeDescription() が発火するが question は null ではないので何も起こらない 4. 項目2の「?」クリック 5. toggleDescription() が発火して question に 2 がセットされる 6. closeDescription() が発火して question は値がセットされている為 question に null セットされる なので今回は親の実行を阻止するstopPropagation()を仕掛けることで回避した。 まとめ イベント発火阻止系はpreventDefault()しか使ったことがなく色々疎かったので使う機会が来てよかった。めちゃめちゃ有難い関数・・! これ以外にも親のイベントを阻止する方法などあればどなたか教えてください?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack5で環境構築

はじめに TypeScriptの環境をローカルに作るためにwebpackが必要だった。 Udemyの一番人気の教材を購入したが、versionが古い・非推奨のパッケージの使用・必要のないパッケージのインストール等 不満が溜まったので、返金処理してインターネットの大海原へ冒険に出た。 webpackって何?状態の方は記事下部の参考サイトを訪問してみてください。 前提と使用したパッケージのversion Webpackを使ってjsとscssをバンドルする程で書いていく。 node/npmは入っている前提 環境 Mac Book Air M1 VSCode node v16.13.0 npm 8.1.0 使用パッケージを羅列 "css-loader": "^6.5.1", "sass": "^1.44.0", "sass-loader": "^12.3.0", "style-loader": "^3.3.1", "webpack": "^5.64.4", "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.6.0" 環境を構築する手順 1. 開発用ディレクトリを作成する mkdir webpack-prod 2. npmの初期化 npm init npm init -y 3. 関連パッケージのインストール npm i -D webpack webpack-cli webpack-dev-server sass-loader sass style-loader css-loader 4. webpack.config.jsの作成と設定 webpack.config.jsの内容 const path = require('path'); // developmentかproductionかを指定 const MODE = "development"; //ソースマップの利用有無 const enabledSourceMap = MODE === 'development'; module.exports = { mode: MODE, entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js' }, devServer: { static: { directory: path.join(__dirname, 'dist') }, open: true }, module: { rules: [ { test: /\.scss/, use: [ "style-loader", { loader: "css-loader", options: { // オプションでCSS内のurl()メソッドの取り込みを禁止する url: false, // ソースマップを有効にする sourceMap: enabledSourceMap, //謎 importLoaders: 2 } }, { loader: 'sass-loader', options: { //ソースマップの有効化 sourceMap: enabledSourceMap } } ] }, { test: /\.(jpe?g|png|gif|svg|ico)$i/, //画像をBase64として取り込む webpack5から搭載で url-loaderの代用 //画像を埋め込まず任意のフォルダに保村する場合 asset/resource type: 'asset/inline' } ] } } Tisp 着目して欲しいのは deveServerのところ contentBase?が使えなくなってます webpack-dev-serverの起動コマンドも変更されたみたい npx webpack serveで動きます scssで画像を読み込む際、今までならurl-loaderが必要でしたが、webpack5から必要なくなったみたい コンパイル名sassはdart-sassです。名前ややこしいですね。 node-sassは非推奨です。近頃サポートも切れます。 参考サイト ics.media こちらのサイトは定期的にブラッシュアップして最新の状態に保っているみたいです。 最高 終わりに udemyでwebpackを学習し始めて2日停滞してしまいました。書籍にも言えることですが、versionの確認は重要ですね。 react学習に購入したudemyもローダーの記述方法が変わっていたりしました・・・ フロントエンドの技術の移り変わりは早いと聞きますが、もう体感してる笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクトで配列を埋めようとしてarray.fill()を使ったらハマった

経緯 行列計算しようとして二重配列を使おうとした際、new Array(10)とかで先に必要サイズを確保してからarray.fill(new Float32Array())とし,他の行列の値をコピーした際に結果がおかしくなった。 const array = new Array(3); array.fill(new Float32Array(4)) fillってプリミティブ型にしか使ったことなかったのでちょっとハマってしまった。 結論 当たり前と言えば当たり前なのだが、fill(new Hoge)とした際に生成されるインスタンスは1つで、それが全ての列にシャローコピーされていたからであった。 同じインスタンスを参照してるため、一番最後に代入された値が全ての行に入っていた。 fillではなく普通にループでnew Hoge()を逐一生成してやれば問題ない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webの勉強はじめてみた その9 ~JavaScriptとCSS編 簡単なアニメーション~

N予備校の「プログラミング入門 Webアプリ」をアーカイブ動画とともに受講しています。 今回は第一章13節「はじめてのCSS」と14節「CSSを使ったプログラミング」を受講しました。 メモ 1. marginは要素の外側、paddingは要素の内側 2. CSSではclass属性を使うことが多い 3. 条件文はできるだけシンプルに 練習問題: JavaScriptとCSSでアニメーション <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title> CSS を使ったプログラミング </title> <link rel="stylesheet" href="css-programming.css"> </head> <body> <h1 id="header" class="face">CSS を使ったプログラミング</h1> <script src="animation.js"></script> </body> </html> .face{ /*color: darkblue; */ color: red; } .back{ /*color: lightgray;*/ color: red; /*opacity: 0.4;*/ } //header 取得 let header = document.getElementById('header'); //角度 let degree = 0; //透明度 let opacity = 0; //加算フラグ let isPositive = true; //headerを回転させる function rotateHeader(){ //6°ずつ回転させる degree += 6; //裏を向いた瞬間にclassを変更する //361° と 1° は同じなので、360で割った余りが現在の角度になる。 degree %= 360; //表を向いているとき:0 以上 90 未満 か 270 以上 360 未満 /*if((0 <= degree && degree < 90) || (270 <= degree && degree < 360)){ //表 header.className = 'face'; }else{ //裏を向いているとき:90 以上 270 未満 header.className = 'back'; }*/ //↓↓ 条件をシンプルに if(90 <= degree && degree < 270){ header.className = 'back'; }else{ header.className = 'face'; } //回転 header.style.transform = 'rotateX(' + degree +'deg)'; //header.style.transform = 'rotate3d(1, 1, 1, ' + degree +'deg)'; //透明度の設定 if(isPositive){ opacity += 0.1; if(opacity >= 1){ //加算フラグを消す isPositive = false; } }else{ opacity -= 0.1; if(opacity <= 0){ //加算フラグを立てる isPositive = true; } } header.style.opacity = opacity; } //20ミリ秒ごとに実行してアニメーションさせる //setInterval(rotateHeader, 20); setInterval(rotateHeader, 100); 今回はh1タグ内の文字列を回転させる、というもの。 あと追加で透明度も変えてます。 degree %= 360 のあたり、自力では全然思いつかないので、ただすごいなぁと感心するばかりでした。 まとめ アニメーションって手段は色々あるけれど、どれも個人的にすごく感動を覚えます。 あとはもっと頭を柔らかくしていきたい。 そういえば次はCSS編です、と言いながらメインはがっつりJavaScriptでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】複数のaタグリンクを無効化する方法

こんにちは。ikuraです。 最近、寒くなってきたので1日に11時間も布団にもぐってしまうようになってしまいました。(夏は8時間程度) 寒いのが好きではないので早く4月にならないかと思っております 余談はさておき、仕事でJavaScriptを触る機会がありましてオンライン画面のリンクを無効化する処理を書きました。 ネットで「JavaScript aタグリンク無効化」と調べると色々実装方法が出てきたのですが、動的にリンクを無効化する処理がネット上にあまりなかったため記事を書こうと思い立ちました。 pointerEventsの概要 私はaタグリンクを無効化するのに、「pointerEvents」を使いました。 poiterEventsの詳しい内容は、以下のリンクをご参照ください。 Pointer events - Web API | MDN pointer-eventsの本当の意味 - Qiita 「pointer-events」は、基本的にcssのプロパティとして使用されているためJavaScriptではあまり使われないようです。。 pointerEventsの実装方法 1.「getElementsByTagName('a')」でタグ名"a"を取得する。 2.取得したaタグの件数文for分を回す。 3.「.style.pointerEvents = 'none';」でaタグ要素を無効化する。 という流れになります。 aタグ要素を有効化する場合は、「auto」を使用します。 index.html <html lang="en"> <head> <script src="a_index.js"></script> </head> <body> <a href="https://qiita.com/">Qiitaへ1</a> <a href="https://qiita.com/">Qiitaへ2</a> <a href="https://qiita.com/">Qiitaへ3</a> <a href="https://qiita.com/">Qiitaへ4</a> <a href="https://qiita.com/">Qiitaへ5</a> <input type="button" value="ここ押すとリンクが無効化される" onclick="invalidLink()"> </body> </html> a_index.js function invalidLink() { let elements = document.getElementsByTagName('a'); for (let i = 0; i < elements.length; i++) { elements[i].style.pointerEvents = 'none'; } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで簡易な定数のグルーピング(ちょっとしたリファクタリングの話)

はじめに こんにちは、運動通信社でバックエンドメインに携わっている Takeya です。 フロントエンドもちょくちょく実装してます。 今回はES6の書式でよく忘れるのでメモしておきたいと思います。 背景 当時の実装方針や歴史的な経緯もあって、ライブ配信ページの定数宣言クラスがこんな感じになってました。 class Config { static get BEFORE() { return 0; } static get PLAYING() { return 1; } static get END() { return 2; } // ... 略 (ライブ配信状態と関係ない各種定数が並列に書かれてる) } この3つはライブ配信状態を表している定数なので、グルーピングして LIVE_STATUS.BEFORE って参照したいですね。 結論 今回に関しては、なんのことはなく、定数っぽいキー名にしたオブジェクトを返せばいいかなと。 static get LIVE_STATUS() { return { BEFORE: 0, PLAYING: 1, END: 2, }; } これで Config.LIVE_STATUS.BEFORE で参照できます。 (※Javaとかなら enum で解決な気がしますが、JavaScriptには enum がないです。 Symbol を使えば enum っぽくできるみたいですが、今回はそこまで考えが至らず) 理想はどうすべきなのか? おそらく理想は、クラス設計を見直して、あるべきクラスに定数を移管する、のがいいのかなと思います。(Javaっぽく考えると LIVE_STATUS 自体を ValueObject としてクラス化すべきな気がします) しかしながら、 結構規模があるので設計見直しは工数的に現実的でない。 一部のライブ配信ページは Vue.js に移行し始めているので、Vueとの相性を考えた設計を検討した方が良さそう。 ちょっと可読性が気になったという話なので、手軽な修正で可読性が上がればOK。 と思って今回の実装を落とし所にしました。 今回の落とし所だと、 jsdoc でObject型の中身の型を表現できないみたいで、Lint系ツールなど型チェックには弊害があるかもしれません。 リファクタリングは工数(納期)とできることのバランスをとっていくのが難しいところです。 今回は非常にちょっとした話でしたが、少しずつリファクタリングを進めています、っていう一例でした。 おまけ enum に関して、 TypeScriptなら enum をサポートしてたので軽く調べると、 enum じゃなくて union 型を使おう、ってなってるようです。 色々クセがありますね。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

luma.glでWebGLに入門する

この記事は、MIERUNE Advent Calendar 2021の5日目の記事です。 はじめに        ____      /      \    /  _ノ  ヽ、_  \   / o゚((●)) ((●))゚o \  シェーダーやりたいお… |     (__人__)    |   \     ` ⌒´     /        ____      /      \    /  _ノ  ヽ、_  \   /  o゚⌒   ⌒゚o   \  でも生のWebGL操作するのつらいお… |     (__人__)    |     \     ` ⌒´     /        ____      /⌒  ⌒ \    /( ●)  (● ) \   /::::⌒(__人__)⌒:::\   だからluma.glでやるお!  |     |r┬-|     |   \     `ー'´     / 本記事のゴール luma.glで簡単なアニメーションを作成します。 WebGLについて WebGLはブラウザからGPUにアクセスするためのJavaScript向けAPIです。GPUへの実際の命令はGLSLという言語で記述します。シェーダーと呼ばれたりします。シェーダーはJavaScript上では単なる文字列で、実行時にコンパイルされます。 このWebGL、なかなか低レベルなAPIとなっていて、ちょっと図形を描画するだけでもかなりのコード量になります。イメージ的には、DOMを生JSで操作する感じで結構つらい。WebGLももうちょっと高レベルなAPIでラップして欲しい訳です。 luma.glを使おうと思った理由 WebGL、というかブラウザで3Dとなると、デファクトスタンダードのライブラリとしてThree.jsがあります。普通はこれを使っておくのがよいでしょう。今回はWebGL自体の学習が目的なので、もう少しWebGLに近い低レベルなAPIがいいなと思い、そこで採用したのがluma.glです。 またもうひとつ、Three.jsはWebGL2で使えるようになるTransform Feedbackに未対応らしいことも他の選択肢をさがす動機でした。luma.glは対応しています。 luma.glの特徴 あのdeck.glの裏側で動いているライブラリです。ドキュメントによれば、luma.glのAPIでも低レベル・中レベル・高レベルに分かれているようです。主に中・高レベルのAPIを操作することになるでしょう。モジュールごとに程度の差はありますが、まさにWebGLのAPIをラップするものです。WebGLほどめんどうくさくなく、でも同じような文脈でAPIを使う感じです。 ちなみにTypeScript対応は未完了です(2021-11)。 luma.glを使ってみる インストール 中レベルAPIの@luma.gl/webglと、高レベルAPIの@luma.gl/engineを使います。 npm install @luma.gl/webgl @luma.gl/engine アニメーションループ WebGLの描画の土台、いわゆるアニメーションループは、@luma.gl/engnineが便利なクラスAnimationLoopを提供しています。 import { AnimationLoop } from '@luma.gl/engine'; const loop = new AnimationLoop({ // @ts-ignore onInitialize({gl}) { // ...頂点やシェーダーの初期化 return { hoge, fuga }; }, // @ts-ignore onRender({gl, hoge, fuga...}) { // ...フレームごとの処理 }, }); loop.start(); // 描画開始 クラス名、関数名から、使い方は明快です。 onInitialize()が返す値をonRender()の引数として受け取れるのがポイントです。 この状態では(そもそもサンプルコードとして不完全ですが)、頂点情報を全く定義していないので、画面には何も表示されません。頂点情報(Model)の定義、アニメーション描画定義を行う必要があります。 Model定義 luma.glでは、頂点やシェーダーをひとまとめにするModelクラスが定義されています。 とりあえず三角形を描画するための頂点とシェーダーを定義してみます。 AnimationLoopでは、Modelは初期化時onInitialize()で定義します(return { model}とし、onRender()に渡します)。 import { AnimationLoop, Model } from '@luma.gl/engine'; import { Buffer } from '@luma.gl/webgl'; // 以上略 onInitialize: function ({ gl }) { const positions = [0.0, 0.6, 0.6, -0.6, -0.6, -0.6]; // 頂点の定義:[p1x, p1y, p2x, p2y, p3x, p3y] const positionBuffer = new Buffer(gl, new Float32Array(positions)); const model = new Model(gl, { // Vertex Shader vs: ` attribute vec2 position; varying vec2 fPosition; void main() { fPosition = position; gl_Position = vec4(position, 0.0, 1.0); } `, // Fragment Shader fs: ` varying vec2 fPosition; void main() { gl_FragColor = vec4(fPosition, length(fPosition), 1.0); } `, attributes: { position: positionBuffer, }, vertexCount: positions.length / 2, // 頂点の数 }); return { model }; // onRender()へ渡す }, // 以下略 Modelをアニメーションループで描画 描画はonRender()で定義します。 画面をお掃除 -> 頂点バッファを操作 -> 描画 を毎フレーム行うことで、アニメーションが実現します。 import { AnimationLoop, Model } from '@luma.gl/engine'; import { Buffer, clear } from '@luma.gl/webgl'; // 以上略 onRender({ gl, model }) { clear(gl, { color: [0, 0, 0, 1] }); // 画面を黒一色で初期化 model.draw(); // onInitialize()で定義したModelを描画 }, // 以下略 この時点で、画面に三角形が表示されるようになります。 しかしアニメーションループとか、毎フレーム描画とか言いながら、微動だにしない三角形だけが表示されても面白くありません。 アニメーションさせてみる 頂点を操作するには、時間の推移を用いる方法や、前フレームの頂点から次フレームの頂点を計算する方法があります(きっとこれ以外にもありますが、私がやったことあるのがこれら)。後者はこの記事で紹介するにはヘヴィなので、前者の方法で三角形を動かしてみます。 フレームごとに、Modelの時間情報を更新する 結論のコードだけ。 // @ts-ignore onRender({ gl, model }) { clear(gl, { color: [0, 0, 0, 1] }); const time = performance.now(); // JavaScriptで時間情報を取得 model.setUniforms({ time }); // modelに時間情報を付与 model.draw(); }, これでModelにフレーム単位で新たな時間情報が与えられます。 次に、Model側でそれを受け取る準備をします。 時間情報を用いた描画 時間ごとに頂点を動かしてみましょう。頂点の移動にはいろいろありますが、今回は回転させてみます。WebGLの世界では、頂点はベクトルであり、ベクトルの変換に行列を使用できます。今回は回転行列なるものを利用して、三角形を回してみます。 // 以上略 const model = new Model(gl, { vs: ` uniform float time; // setUniforms()から値を受け取る attribute vec2 position; varying vec2 fPosition; // 回転行列: Z軸に対してラジアンでいうrだけ回転 mat2 rot(float r) { float cr = cos(r); float sr = sin(r); return mat2( cr, sr, -sr, cr ); } void main() { fPosition = position; gl_Position = vec4(rot(time * 0.001) * position, 0.0, 1.0); // 回転行列を用いてpositionベクトルを座標変換 } `, // 以下略 timeの値のまま回転させると余りにも回転速度が速いので、ちょうど良いくらいに値を小さくしています。以上のコードで、三角形が回転します。 上記のサンプルコードはすべてこちらに置いてあります 終わりに 私みたいな、シェーダーやってみたいけどWebGLは記述量が多くてつらい、という方には本記事の内容が刺さるかもしれません。 WebGLと聞くとThree.jsを使いがちですし、単にブラウザで3Dモデルを表示するなら、最も手早い選択肢でしょう。ただ、シェーダー自体を学びたいとか、WebGLに近いレイヤーを触りたい場合には、luma.glはおすすめできる選択肢だと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】ボタンを押したらデータ値をとる&色が変わる方法(アプリ開発アウトプット)

はじめに こんにちは! 今回は【Nuxt.js】ボタンを押したら値をとる&色が変わる方法についてアウトプットしていきます! 前提 ・Nuxtの新規プロジェクト作成が既に済んでいる ・vue.jsの基礎学習が済んでいる ・tailwindcssが扱える 対象 ・真偽値を使用しての開発を学びたい方 ・診断アプリケーションを作りたい方 実装 template <button class="text-2xl my-6 mx-10 py-6 px-6 border-2 border-red-600 rounded-full h-28 w-28 flex items-center justify-center" :class="answers.q1 ? 'bg-red-200' : ''" @click="answer('q1',true)"> YES </button> <button class="text-2xl my-6 mx-10 py-6 px-6 border-2 border-blue-600 rounded-full h-28 w-28 flex items-center justify-center" :class="answers.q1 === false ? 'bg-blue-200' : ''" @click="answer('q1',false)"> NO </button> script data() { return { answers: { q1: null }, }; }, methods: { answer(questionNumber, bool) { this.answers[questionNumber] = bool; }, } 解説 上記のコード、下記の写真のようにYESとNOのボタンがあります。 ・YESを押したらtrueをデータ値としてとる。背景を赤色にする。 ・NOを押したらfalseをデータ値としてとる。背景を青色にする。 ということがしたいです。 初期値はnull。からの状態。 【YESのボタン】 @click="answer('q1',true)" YESボタンをクリックしたとき、script部分answerにq1はtureというデータ値が入ります。 それをthis.answers[questionNumber] = bool;と記述し、q1=trueと処理が働き、決定付けられます。 :class="answers.q1 ? 'bg-red-200' : ''" この記述の'bg-red-200' : ''この部分は'左辺(tureの時)' : 右辺(falseの時)という意味になります。よって全体を訳すとYESボタンを押すとデータ値にtrueが入る。answers.q1のデータ値がtrueの時YESボタンの背景カラーは赤色に変わるという意味になり、処理が実行されている。 【NOのボタン】 まず最初に、初期値はnullとお話ししました。boolにnullと定義してしまうと、性質的な問題で「nullはfalsyな値なので、if文の条件式ではfalseとなる」と処理してしまう働きがあります。 なので上記同様に'': 'bg-red-200'と記述してしまうと、 初期状態から色が入ってしまうことになります。(nullだが、falseの影響が出てしまう) なので'bg-blue-200' : ''というように記述しなければなりません。 NOボタンの場合は:class="answers.q1 === false(answer.q1はfalseと定義)と記述してしまいます。 これで初期値はnullではなくfalseなので、NOボタンをクリックしたら反対のtrueになることになります。 背景色も'bg-blue-200' : ''と記述しているので、初めから青く染まっているのではなく、しっかりクリックしてから青く染まるようになりました。 しかしこのままではYESもtrue、NOもtrueとデータをとってしまうので、@click="answer('q1',false)"と書き加えることで、NOボタンを押したら、answer.q1のデータににfalseが入るという処理法に塗り替えました! NOボタンの処理をまとめるとボタンを押したらtrueになるのだが、データとして入るのはfalseであるということになります! 最後に 今回はボタンを押したらデータ値をとる&色が変わる方法についてアウトプットしました。 今回の記事を読んで質問、間違い等あればコメント等頂けると幸いです。 今後ともQiitaにてアウトプットしていきます! 最後までご愛読ありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazorチートシート

Blazorで使われる記述を簡単にまとめてみました。 データバインド 変数のバインド <p>@value</P> inputタグへのバインド <input @bind-value="<変数名>" /> @bind-<バインド先の属性名> <input @bind="<変数名>" /> バインド先の属性名を省略した場合は、デフォルトの属性のバインドされる。 バインドタイミング <input @bind="<変数名>" @bind:event="oninput" /> 即時反映される。 <input @bind="<変数名>" @bind:event="onchange" /> 入力確定時、Enter押下かフォーカスが外れたとき スタイルへのバインド <div @bind-style="fontsize" @bind-style:event="onchange" >フォントサイズを変更</div> <div style="font-size: @(fontSize)pt" >フォントサイズを変更</div> 数値入力 <input type="number" @bind="<変数名>" /> 項目選択 <select @bind="<変数名>"> @foreach(var item in items) { <option value="@item.Id">@item.Name</option> } </select> チェックボックス <input type="checkbox" @bind="<変数名>" /> コンポーネント関連 パラメーター付きの子コンポーネントの呼び出し Razor <Component Name="Name" Year="2021" /> C# //コンポーネント側のプロパティに[Parameter]をつけておく [Parameter] public string Name { get; set; } [Parameter] public int Year { get; set; } 親コンポーネントからリストの要素として呼ばれる際は、 [Parameter] Public RednerFragment ChildContent {get; set;} //を作成しておく 属性スプラッティングでコンポーネントにデータを渡す @foreach (var item in Items) { Var attrs = new Dictionary<string, object>() { { "Name", item.Name }, { "Value", item.Value }, } <Component @attributes="attrs" /> //使わない場合 <Component Name=@item.Name Value=@item.Value /> } C#とJavascriptの連携 C#からJavascriptを呼び出す JSRuntimeをインジェクトする。 @page "/index" @inject IJSRuntime JSRuntime; Javascriptの関数を定義する <script> window.jsFunctions = { changeValue: function (value, id) { $(`#${id}`).text(value); } } </script> C#からJavascriptの関数を呼び出す private async void Method() { await JSRuntime.InvokeVoidAsync("jsFUnctions.changeValue", "value", "id"); } JavascriptからC#の処理を呼び出す C#の処理 @code { [JSInvokable] public static void SomeMethod(string value) //staticである必要がある。 { Console.WriteLine(value); return; } } JavascriptからC#のメソッドを呼び出す DotNet.invokeMethod("<アセンブリ名>", "SomeMethod", "value"); コンポーネント間のデータバインド 親から子へのデータバインド 子コンポーネントに [Parameter] public EventCallBack<string> XXXXChanged { get; set; } のような形でバインドするプロパティ名 + Changedのイベントを作成する。 親コンポーネントからの呼び出し <ChildComponent @bind-XXXX="XXXX" /> 子から親へのデータバインド [Parameter] public EventCallBack<double> XXXXChanged {get; set;} private async void clickSomething() { await resultChanged.InvokeAsync(XXXX); } 子のイベントを親に伝える 子コンポーネント実装 [Parameter] public EventCallback<string> OnResultChanged {get; set;} private async void SomeMethod() { await OnResultChanged.InvkeAsync(XXXX); } 親コンポーネントの実装 <ChildComponent OnResultChanged="@ChangeResult" /> @code { private void ChangeResult(string result) { this.XXX = result; } } イベント クリックイベント <button @onclick="onSubmit">登録</button> ラムダ式の場合 <button @onclick="@(() => Message = "Some Message…")">登録</button> KeyPressイベント <input type="text" @bind="Text" @onkeypress="OnKeyPress" /> マウスムーブイベント <rect @onmousemove="OnMouseMove"></rect> ルーティング ページパラメーター @page "/param/{param1}/{param2}" 初期化 初期化処理(同期) protected override void OnInitialized() { ... } 初期化処理(非同期) protected override async Task OnInitializedAsync() { await ... }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Symbolでの安全なトークン交換の方法と各方法のメリット,デメリットのまとめ(と有効期限の延長方法)

概要 仮想通貨ではトークン同士の交換がよく行われます.しかし,交換する相手が必ずしも信頼できるとは限りません.通常の送金トランザクションだけで交換しようとした場合,こちらがトークンを送っても相手が持ち逃げしてしまう可能性があります. これを防ぐためには,通常の送金トランザクション以外でトークンを交換する必要があります.Symbolではモザイク(トークン)同士を安全に交換したいに利用できるトランザクションとしては次のようなものがあります. トランザクション 連署の最大猶予期間 必要な署名の回数 不成立時のペナルティ アグリゲートコンプリート 6時間(アナウンス前) 2(作成者),1(連署者) 無(TX手数料も0) アグリゲートボンデッド 約2日間 1 10XYM シークレットロック,プルーフ 約365日 2(連署者は1も可) 有効期限満了までモザイクロック モザイクの交換を行いたい場合はこのようなトランザクションを生成していれば良いのですが,各トランザクションによって有効期間も連署に必要な署名の数も変わってきます.これらをまとめてみました. 今回はAが7XYMを,Bが5Xembook.tomatoを交換したいと想定してこの記事を書いていきます. アグリゲートコンプリート アグリゲートコンプリートは事前にオフライン環境でトランザクションのペイロードを共有できる場合に利用できます. トランザクションのペイロード(内容)のみメールやQRコードなどで転送して連署を行うことで成立させることができます.Symbolのネットワークを利用しないため,トランザクションの署名に失敗してもTX手数料などの手数料が一切かからないところが非常に強いです. アグリゲートコンプリートを使った場合の例 手順 1.Aがアグリゲートコンプリートを生成する const xym = require('symbol-sdk'); const netwotkType = xym.NetworkType.MAIN_NET;//Xembook.tomatoを送金したいのでMAIN_NET const epochAdjustment = '';//epchAdjustmentを入れる,http://(ノードのFQDN):3000/network/propertiesで取得可能 const currencyMosaicId = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能 const networkGenerationHash = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能 const feemultiplier = 100;//早く送金したいなら100,遅くても良い場合は10がおすすめ(2021/12/02現在) const a_Account = xym.Account.createFromPrivateKey( '*********************************', networkType, ); const a_publicAccount = xym.PublicAccount.createFromPublicKey( '*****************', networkType ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( '****************', networkType ); const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6) ); const tomatoMosaic = new xym.Mosaic( '310378C18A140D1B',//モザイクID310378C18A140D1B xym.UInt64.fromUint(5*1)//送金量*可分性 ); const atobTx = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), b_publicAccount.address,//受取人アドレス [xymMosaic],//モザイク xym.PlainMessage.create('AtoB'),//メッセージ netwotktype,//ネットワークタイプ ) const btoaTX = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), a_publicAccount.address,//受取人アドレス [tomatoMosaic],//モザイク xym.PlainMessage.create('BtoA'),//メッセージ netwotktype,//ネットワークタイプ ); const aggregateTx = xym.AggregateTransaction.createComplete( xym.Deadline.create(epochAdjustment,6),//有効期限,アグリゲートコンプリートは6時間後のものまで受け入れられる [ atobTx.toAggregate(a_publicAccount), btoaTX.toAggregate(b_publicAccount), ], netwotktype, [], ).setMaxFeeForAggregate(feemultiplier,1); const signedTx = a_Account.sign( aggregateTx , networkGenerationHash, ); console.log(signedTx.payload);//このペイロードが必要 2.Bに生成したアグリゲートコンプリートのペイロードを伝え,連署させる const b_Account = xym.Account.createFromPrivateKey( '*********************************', networkType, ); const payload = '';//Aから伝えられたpayload const cosignature = xym.CosignatureTransaction.signTransactionPayload( b_Account , payload, networkGenerationHash, ); console.log(JSON.stringfy(cosignature));//連署が生成される,これをAに伝える 3.AはBの連署を使ってアグリゲートコンプリートを完成させ,ネットワークにアナウンスする cpnst node = '';//ノードのURLを入れる,例 http://api-peer.xym-node.com:3000 const repo = new xym.RepositoryFactoryHttp(node); const transactionHttp = repo.createTransactionRepository(); const cosigJson = JSON.parse("*************");//連署のJSONをparseする const congnature = new xym.CosignatureSignedTransaction(//CosignatureSignedTransactionに戻す cosigJson.parentHash, cosigJson.signature, cosigJson.signerPublicKey, new xym.UInt64([cosigJson.version.lower,cosigJson.version.higher]), ); const complete = a_account.signTransactionGivenSignatures(//連署を使って完成させる aggregateTx,[congnature], networkGenerationHash ); transactionHttp.announce(complete ).subscribe(//ネットワークにアナウンスする (x) => console.log(x), (err) => console.error(err), ); console.log(node+'/transactionStatus/'+complete.hash);//ステータス確認ができる 全てを一括実行するサンプル const xym = require('symbol-sdk'); var networkGenerationHash =''; var epochAdjustment = ''; var repo; var transactionHttp; var currencyMosaicId; var node; var networkType = xym.NetworkType.MAIN_NET; var feemultiplier; console.log("***************************************************************"); console.log(Date()); console.log("***************************************************************"); //送信元アカウント const a_account = xym.Account.createFromPrivateKey( '****************************', networkType, ); const b_account = xym.Account.createFromPrivateKey( '****************************', networkType, ); (async()=>{ node = 'http://api-peer.xym-node.com:3000'; repo = new xym.RepositoryFactoryHttp(node); transactionHttp = repo.createTransactionRepository(); await getInfo(); const data = await aggregateCompleteOffline(); const cosigStr = JSON.stringify(data.cosignature); const cosigJson = JSON.parse(cosigStr); const co = new xym.CosignatureSignedTransaction( cosigJson.parentHash, cosigJson.signature, cosigJson.signerPublicKey, new xym.UInt64([cosigJson.version.lower,cosigJson.version.higher]), ); const complete = a_account.signTransactionGivenSignatures(data.transaction, [co],networkGenerationHash); announceTX(complete); })(); async function aggregateCompleteOffline(){ const a_publicAccount = xym.PublicAccount.createFromPublicKey( 'D77668C77A26F6979ED2D0C5EF99E0A1FFA7A988601E40E82B9A25861A6B2D12', networkType ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( 'B9198633C7EC0BED5D79319C5B07748D42A2206ACEB75A485A9070CFA0993F35', networkType ); const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000)//送金量*10^(可分性)(xymは6) ); const tomatoMosaic = new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6) ); const atobTx = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), b_publicAccount.address,//受取人アドレス [xymMosaic],//モザイク xym.PlainMessage.create('AtoB'),//メッセージ networkType,//ネットワークタイプ ) const btoaTX = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), a_publicAccount.address,//受取人アドレス [tomatoMosaic],//モザイク xym.PlainMessage.create('BtoA'),//メッセージ networkType,//ネットワークタイプ ); const aggregateTx = xym.AggregateTransaction.createComplete( xym.Deadline.create(epochAdjustment,6),//有効期限,アグリゲートコンプリートは6時間後のものまで受け入れられる [ atobTx.toAggregate(a_publicAccount), btoaTX.toAggregate(b_publicAccount), ], networkType, [], ).setMaxFeeForAggregate(feemultiplier,1); const signedTx = a_account.sign(aggregateTx,networkGenerationHash);//payloadを作成する console.log(signedTx); //payloadからトランザクションを作る const newTx = xym.TransactionMapping.createFromPayload(signedTx.payload); console.log(newTx); const coAgg = xym.CosignatureTransaction.signTransactionPayload(//作成したpayloadに連署しておく b_account, signedTx.payload, networkGenerationHash); console.log(coAgg); return {transaction:newTx,cosignature:coAgg} } function announceTX(signed){ console.log("送信トランザクションハッシュ: "+signed.hash); console.log(node+'/transactionStatus/'+signed.hash); transactionHttp.announce(signed).subscribe( (x) => console.log(x), (err) => console.error(err), ); } async function getInfo(){ feemultiplier = 10; epochAdjustment = await repo.getEpochAdjustment().toPromise(); networkGenerationHash = await repo.getGenerationHash().toPromise(); currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,"")); } アグリゲートボンデッド アグリゲートコンプリートでは連署を手動で集めましたが,アグリゲートボンデッドでは連署をSymbolのネットワークを介して集めることができます.この方法はSymbolのネットワークで連署要求ができる他,公式ウォレットにも直接送金要求を送ることができ,アグリゲートコンプリートよりも連署が行いやすくなっています. また,有効期限も最大2日に設定できるためアグリゲートコンプリートよりも連署までの有効時間が長くなります. さらに,必要な連署を全て集めると自動でトランザクションが実行されるため,アグリゲートコンプリートのように集めた連署を使ってトランザクションを完成させる手間を省くことができます.連署を集める機能に関してはアグリゲートボンデッドの方が便利です. アグリゲートボンデッドを使った場合の例 アグリゲートボンデッドの利用の際はアナウンスを行ったアカウントから10XYMが最大2日間ロックされます.成功すると返還されますが,失敗した場合はネットワークのハーベスト報酬に回され帰ってきません.10XYMのロックは結構痛いので,アグリゲートボンデッドはトランザクションが確実に連署される場合に利用した方が良いでしょう. 手順 1.Aがアグリゲートボンデッドを生成する const xym = require('symbol-sdk'); const netwotkType = xym.NetworkType.MAIN_NET;//Xembook.tomatoを送金したいのでMAIN_NET const epochAdjustment = '';//epchAdjustmentを入れる,http://(ノードのFQDN):3000/network/propertiesで取得可能 const currencyMosaicId = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能 const networkGenerationHash = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能 const feemultiplier = 100;//早く送金したいなら100,遅くても良い場合は10がおすすめ(2021/12/02現在) function createAggregateBonded(){ const netwotktype = xym.NetworkType.TEST_NET; const a_publicAccount = xym.PublicAccount.createFromPublicKey( '54B24D5FCA1EDB2CA9B73424752CDD6BECE9FFCD14F83125599B51A03E2E6942', networkType ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( '9C4BE4A67607DA47992DD3D881B67E2553955121854D2947CE51A5159CC32154', networkType ); const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6) ); const tomatoMosaic = new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'), xym.UInt64.fromUint(5*1)//送金量*可分性 ); const atobTx = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), b_publicAccount.address,//受取人アドレス [xymMosaic],//モザイク xym.PlainMessage.create('AtoB'),//メッセージ netwotktype,//ネットワークタイプ ) const btoaTX = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), a_publicAccount.address,//受取人アドレス [tomatoMosaic],//モザイク //xym.PlainMessage.create(""), xym.PlainMessage.create('BtoA'),//メッセージ netwotktype,//ネットワークタイプ ); const aggregateTx = xym.AggregateTransaction.createBonded( xym.Deadline.create(epochAdjustment,48),//有効期限,アグリゲートボンデッドは48時間後まで受け入れられる [ atobTx.toAggregate(a_publicAccount), btoaTX.toAggregate(b_publicAccount), ], netwotktype, [], ).setMaxFeeForAggregate(100,1); const signedTx = senderAccount.sign(aggregateTx,networkGenerationHash); return signedTx; } 2.アグリゲートボンデッドのハッシュを使ってハッシュロックをアナウンスする function createHashLock(signedTx){ const lockXym = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(10 * 1000000)//10XYMロック ); const hashLockTx = xym.HashLockTransaction.create( xym.Deadline.create(epochAdjustment), lockXym, xym.UInt64.fromUint(2 * 2880),//最大2日(2*2880ブロック)留め置きできる signedTx, networkType, ).setMaxFee(feemultiplier); const signed = senderAccount.sign(hashLockTx,networkGenerationHash); return signed; } 3.ハッシュロックが承認されるまで待機し,アグリゲートボンデッドをアナウンスする (async()=>{ const node = 'http://api-peer.xym-node.com:3000'; const wsEndpoint = node.replace('http', 'ws') + "/ws"; const nsHttp = new xym.NamespaceHttp(node); const listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket); const repo = new xym.RepositoryFactoryHttp(node); const transactionHttp = repo.createTransactionRepository(); const receiptRepo = repo.createReceiptRepository(); const transactionService = new xym.TransactionService(transactionHttp,receiptRepo); websocketConnection(); const aggTx =createAggregateBonded(); const hashLockTx = createHashLock(aggTx); //ハッシュロックが承認されてからアグリゲートボンデッドを投げる const res = await transactionService.announceHashLockAggregateBonded(hashLockTx,aggTx,listener).toPromise(); })(); function websocketConnection(){ listener.open().then(() => { listener.newBlock() .subscribe(block =>{ console.log("ブロック高: "+block.height); },err=>{ console.log("失敗しました"); }); }); } 4.Bがネットワークのパーシャルキャッシュから情報を取得し連署,アナウンスする const networkType = xym.MAIN_NET; const b_account = xym.Account.createFromPrivateKey( '************************', networkType, ); var node; var wsEndpoint; var nsHttp; var listener; var repo; var transactionHttp; (async()=>{ node = 'http://api-peer.xym-node.com:3000'; wsEndpoint = node.replace('http', 'ws') + "/ws"; nsHttp = new xym.NamespaceHttp(node); listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket); repo = new xym.RepositoryFactoryHttp(node); transactionHttp = repo.createTransactionRepository(); websocketConnection(); function websocketConnection(){ listener.open().then(() => { listener.newBlock() .subscribe(block =>{ console.log("ブロック高: "+block.height); signAllPartialTransaction(b_account); },err=>{ console.log("失敗しました"); }); }); } async function signAllPartialTransaction(account){ const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise(); for (var data of transactions.data) { const cosignatureTx = xym.CosignatureTransaction.create(data); const signedTx = account.signCosignatureTransaction(cosignatureTx); transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe( (x) => console.log(x), (er) => console.error(er), ); } } 全てを一括実行するサンプル const xym = require('symbol-sdk'); const WebSocket = require('ws'); var networkGenerationHash =''; var epochAdjustment = ''; var repo; var transactionHttp; var receiptRepo; var currencyMosaicId; var node; var networkType = xym.NetworkType.MAIN_NET; var feemultiplier; var nsHttp; var wsEndpoint; var listener; var transactionService; const a_publicAccount = xym.PublicAccount.createFromPublicKey( '************', networkType ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( '*************', networkType ); console.log("***************************************************************"); console.log(Date()); console.log("***************************************************************"); //送信元アカウント const a_account = xym.Account.createFromPrivateKey( '******', networkType, ); const b_account = xym.Account.createFromPrivateKey( '******', networkType, ); (async()=>{ node = 'http://api-peer.xym-node.com:3000'; nsHttp = new xym.NamespaceHttp(node); wsEndpoint = node.replace('http', 'ws') + "/ws"; listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket); repo = new xym.RepositoryFactoryHttp(node); transactionHttp = repo.createTransactionRepository(); receiptRepo = repo.createReceiptRepository(); transactionStatusRepo = repo.createTransactionStatusRepository(); transactionService = new xym.TransactionService(transactionHttp,receiptRepo); websocketConnection(); await getInfo(); const aggTx = await createAggregateBonded(); const hashLockTx = createHashLock(aggTx); const res = await transactionService.announceHashLockAggregateBonded(hashLockTx,aggTx,listener).toPromise(); console.log(res); })(); async function signAllPartialTransaction(account){//partial状態であれば全て署名するので注意 const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise(); for (var data of transactions.data) { const cosignatureTx = xym.CosignatureTransaction.create(data); const signedTx = account.signCosignatureTransaction(cosignatureTx); transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe( (x) => console.log(x), (er) => console.error(er), ); } } function websocketConnection(){ listener.open().then(() => { listener.newBlock() .subscribe(block =>{ console.log("ブロック高: "+block.height); signAllPartialTransaction(b_account); },err=>{ console.log("失敗しました"); }); }); } async function createAggregateBonded(){ const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7 * 1000000)//送金量*10^(可分性)(xymは6) ); const tomatoMosaic = new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6) ); const atobTx = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), b_publicAccount.address,//受取人アドレス [xymMosaic],//モザイク xym.PlainMessage.create('AtoB'),//メッセージ networkType,//ネットワークタイプ ) const btoaTX = xym.TransferTransaction.create( xym.Deadline.create(epochAdjustment), a_publicAccount.address,//受取人アドレス [tomatoMosaic],//モザイク xym.PlainMessage.create('BtoA'),//メッセージ networkType,//ネットワークタイプ ); const aggregateTx = xym.AggregateTransaction.createBonded( xym.Deadline.create(epochAdjustment,48),//有効期限,アグリゲートボンデッドは2日後まで受け入れられる [ atobTx.toAggregate(a_publicAccount), btoaTX.toAggregate(b_publicAccount), ], networkType, [], ).setMaxFeeForAggregate(100,1); const signedTx = a_account.sign(aggregateTx,networkGenerationHash);//payloadを作成する return signedTx; } function createHashLock(signedTx){ const lockXym = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(10 * 1000000)//10XYMロック ); const hashLockTx = xym.HashLockTransaction.create( xym.Deadline.create(epochAdjustment), lockXym, xym.UInt64.fromUint(2 * 2880),//最大2日(2*2880ブロック)留め置きできる signedTx, networkType, ).setMaxFee(feemultiplier); const signed = a_account.sign(hashLockTx,networkGenerationHash); return signed; } async function getInfo(){ feemultiplier = 100; epochAdjustment = await repo.getEpochAdjustment().toPromise(); networkGenerationHash = await repo.getGenerationHash().toPromise(); currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,"")); } シークレットロック,プルーフ 交換したい当事者同士が同じシークレットを使用してシークレットロックを行い,プルーフをアナウンスすることで交換が成立します.交換が不成立の場合(プルーフトランザクションがアナウンスされない場合)ロックした分のモザイクが有効期限が切れるまで変換されません. シークレットロックを使った場合の例 シークレットプルーフを使った場合の例(アグリゲートコンプリートで一括実行の場合) なお,この方法ではアトミックスワップと同じ仕組みを利用しているため他のチェーンが対応していれば,他のチェーンの通貨との交換(アトミックスワップ)が可能です.交換が失敗した(他方がシークレットロックを行わなかった,片方のシークレットロックの期限が切れてしまった)場合,ロックした期間だけモザイクが変換されないデメリットがあります. 手順 1.Aがシークレット,プルーフのペアを生成する const random = crypto.randomBytes(20); const proof = random.toString('hex');//Bが正しくシークレットロックを行うまで絶対に伝えてはならない console.log('Proof:', proof); const hash = sha3_256.create(); const secret = hash.update(random).hex().toUpperCase();//Bに伝える console.log('Secret:', secret); 2.Aが生成されたシークレットを使ってシークレットロックトランザクションを生成し,アナウンスする const networkType = xym.MAIN_NET; const a_account = xym.Account.createFromPrivateKey( '************************', networkType, ); const b_address = xym.Address.createFromRawAddress( '************************', networkType, ); const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6) ); const atob= xym.SecretLockTransaction.create( xym.Deadline.create(epochAdjustment), xymMosaic, xym.UInt64.fromUint((10 * 24 * 3600) / 30), // 10日間(28800ブロック)ロックする xym.LockHashAlgorithm.Op_Sha3_256, secret, b_address , networkType, ); const atobSigned= a_account.sign( atob, networkGenerationHash, ); transactionHttp.announce(atobSigned).subscribe( (x) => console.log(x), (err) => console.error(err), ); } 3.BがAと同じシークレットを使ってシークレットロックトランザクションを生成する ここでは,ロックがAが生成したハッシュロックよりも早く期限切れになるように設定する必用があります.Aが期限切れになったあとにもハッシュロックがされていると,AはBのモザイクを取り出せるのにBはAのモザイクを取り出せないといった状態になってしまいます. 今回はAがシークレットロックを行ってから1日以内にこのトランザクションを生成すると仮定し,ロック期間を1日短い9日で設定します const secret = '';//Aが生成したものと同じものを使う const networkType = xym.MAIN_NET; const b_account = xym.Account.createFromPrivateKey( '************************', networkType, ); const a_address = xym.Address.createFromRawAddress( '************************', networkType, ); const tomatoMosaic = new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'),//モザイクID310378C18A140D1B xym.UInt64.fromUint(5*1)//送金量*可分性 ); const btoa = xym.SecretLockTransaction.create( xym.Deadline.create(epochAdjustment), tomatoMosaic , xym.UInt64.fromUint((9 * 24 * 3600) / 30), // 9日間(28800ブロック)ロックする xym.LockHashAlgorithm.Op_Sha3_256, secret, a_address, networkType, ); const btoaSigned= b_account.sign( btoa, networkGenerationHash, ); transactionHttp.announce(btoaSigned).subscribe( (x) => console.log(x), (err) => console.error(err), ); } 4.AはBが同じシークレットを用いてシークレットロックトランザクションを発行したことを確認し,自分宛のシークレットプルーフトランザクションをアナウンスする. Bの行ったトランザクションがファイナライズされてから行うようにしてください. const btoaPloof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, a_account.address, proof, networkType, ).setMaxFee(feemultiplier); const btoaPloofSigned = a_account.sign(btoaPloof, networkGenerationHash); transactionHttp.announce(btoaPloofSigned ).subscribe( (x) => console.log(x), (err) => console.error(err), ); 5.BはAがアナウンスしたシークレットプルーフトランザクションからproofを取得し,自分宛のシークレットプルーフトランザクションをアナウンスする.(署名をAが行えば代行することが出来ます) const atobPloof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, b_account.address, proof, networkType, ).setMaxFee(feemultiplier); const atobPloofSigned = b_account.sign(atobPloof , networkGenerationHash); transactionHttp.announce(atobPloofSigned ).subscribe( (x) => console.log(x), (err) => console.error(err), ); Aがシークレットプルーフトランザクションを一括実行してあげる場合(手数料は現状あまり変わらないので,こちらの方が良いと思います) const btoaPloof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, a_account.address, proof, networkType, ); const atobPloof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, b_address, proof, networkType, ); const aggregateTx = xym.AggregateTransaction.createComplete( xym.Deadline.create(epochAdjustment,6), [ btoaPloof.toAggregate(a_account.publicAccount), atobPloof.toAggregate(a_account.publicAccount), ], netwotktype, [], ).setMaxFeeForAggregate(feemultiplier,1); const aggSined= a_account.sign(aggregateTx , networkGenerationHash); transactionHttp.announce(aggSined).subscribe( (x) => console.log(x), (err) => console.error(err), ); 全てを一括実行するサンプル(ファイナライズ等の考慮は全くしていません,ご注意ください) const xym = require('symbol-sdk'); const crypto = require('crypto'); const sha3_256 = require('js-sha3').sha3_256; const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); var networkGenerationHash =''; var epochAdjustment = ''; var node = ""; var repo; var transactionHttp; var currencyMosaicId; var networkType; var feemultiplier; console.log("***************************************************************"); console.log(Date()); console.log("***************************************************************"); var networkType = xym.NetworkType.MAIN_NET; const a_account = xym.Account.createFromPrivateKey( '***************', networkType, ); const b_account = xym.Account.createFromPrivateKey( '***************', networkType, ); const a_publicAccount = xym.PublicAccount.createFromPublicKey( 'D77668C77A26F6979ED2D0C5EF99E0A1FFA7A988601E40E82B9A25861A6B2D12', networkType ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( 'B9198633C7EC0BED5D79319C5B07748D42A2206ACEB75A485A9070CFA0993F35', networkType ); (async()=>{ node = 'http://api-peer.xym-node.com:3000'; repo = new xym.RepositoryFactoryHttp(node); transactionHttp = repo.createTransactionRepository(); await getInfo(); const data = await makeSecretLock(); await makeSecretLockb(data.secret); await _sleep(40000); createSecretProofTransaction(data.secret,data.proof); })(); async function makeSecretLock(){ //atob生成 const xymMosaic = new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7 * 1000000)//送金量*可分性(xymは6) ); const random = crypto.randomBytes(20); const proof = random.toString('hex'); console.log('Proof:', proof); const hash = sha3_256.create(); const secret = hash.update(random).hex().toUpperCase(); console.log('Secret:', secret); const atob = xym.SecretLockTransaction.create( xym.Deadline.create(epochAdjustment), xymMosaic, xym.UInt64.fromUint(10 * (24 * 3600) / 30), xym.LockHashAlgorithm.Op_Sha3_256, secret, b_publicAccount.address, networkType, ).setMaxFee(feemultiplier); const atobSigned = a_account.sign( atob, networkGenerationHash, ); await announceTX(atobSigned); return {"secret":secret,"proof":proof}; } async function makeSecretLockb(secret) {//ノード接続後の処理 //atob生成 const tomatoMosaic = new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6) ); const btoa = xym.SecretLockTransaction.create( xym.Deadline.create(epochAdjustment), tomatoMosaic, xym.UInt64.fromUint(9* (24 * 3600) / 30), xym.LockHashAlgorithm.Op_Sha3_256, secret, a_publicAccount.address, networkType, ).setMaxFee(feemultiplier); const btoaSigned = b_account.sign( btoa, networkGenerationHash, ); await announceTX(btoaSigned); } async function getInfo(){ feemultiplier = 10; epochAdjustment = await repo.getEpochAdjustment().toPromise(); networkGenerationHash = await repo.getGenerationHash().toPromise(); currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,"")); } function createSecretProofTransaction(secret, proof) { const btoaProof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, a_account.address, proof, networkType, ); const atobProof = xym.SecretProofTransaction.create( xym.Deadline.create(epochAdjustment), xym.LockHashAlgorithm.Op_Sha3_256, secret, b_publicAccount.address, proof, networkType, ); const aggregateTx = xym.AggregateTransaction.createComplete( xym.Deadline.create(epochAdjustment, 6), [ btoaProof.toAggregate(a_account.publicAccount), atobProof.toAggregate(a_account.publicAccount), ], networkType, [], ).setMaxFeeForAggregate(100, 1); const aggSined = a_account.sign(aggregateTx, networkGenerationHash); announceTX(aggSined); } async function announceTX(signed) { console.log("送信トランザクションハッシュ: " + signed.hash); console.log(node + '/transactionStatus/' + signed.hash); transactionHttp.announce(signed).subscribe( (x) => console.log(x), (err) => console.error(err), ); } 番外編 有効期限を延長する これまで有効期限について書いてきましたが,これらは未来のトランザクション(未来のdeadLine)のトランザクションを生成することで期限を延長することができます.アグリゲートボンデッド以外のトランザクションであれば有効期限が6時間になっているため,6時間毎の未来のトランザクションを生成することで有効期限を無限に伸ばしてあげることができます.アナウンスの際はdeadLineにあったTXに連署し,実行する必要があります. この方法ではトランザクションを複数回投げてしまうことができるので対策が必要になります.具体的には,トランザクションを一度きりしか成立しないように組んであげる必要があります. 例1 使い捨てアドレスにモザイクを入れておく この方法では使い捨てアドレス(カッコ良く言えばコントラクトアドレスと言えるのでしょうか...)にモザイクを入れ,口座の残高の範囲で取引を制限します.一度取引を成立させれば資産が移動するため以降のトランザクションは成立させることができなくなります. const a_account = xym.Account.createFromPrivateKey( ******, networkType, ); const b_account = xym.Account.createFromPrivateKey( '******', networkType, ); const a_publicAccount = xym.PublicAccount.createFromPublicKey( '8E9765F3471D5E7AEA39620F0998C25C96CEFA24F78980F31B55604FAF7C8868', networkType, ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( 'CCECC77ABE531E7038A7D4E416BB99739642B56014034F1C45B1BD157803C105', networkType, ); const Txs = deadLineTest(7);//6時間*7回=42時間の有効期限のトランザクションを生成 var signedTxs = []; for(tx of Txs){ const singed = a_account.sign(tx,networkGenerationHash); const onetimeCosign = xym.CosignatureTransaction.signTransactionPayload(//onetimeアドレスの連署 onetimeAccount, singed.payload, networkGenerationHash ); signedTxs.push({"signed":singed,"cosign":onetimeCosign}); } function futureDeadlineTx(num){ const now = Date.now(); var Txs = []; for(i=6;i<num*6;i+=6){ //現在のUNIXミリ秒-Symbolが誕生したUNIX秒*1000 + 6の倍数 const deadline = new xym.Deadline(now - epochAdjustment * 1000 + 60*60*i*1000); console.log(deadline); Txs.push(createFutureTransaction(deadline)); } return Txs; } function createFutureTransaction(futureDeadline){ //使い捨てアドレスからb const onetimetob = xym.TransferTransaction.create( futureDeadline, b_publicAccount.address, [new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000) )], xym.PlainMessage.create('atob'), networkType, ); //bからa const btoa = xym.TransferTransaction.create( futureDeadline, a_publicAccount.address, [new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'), xym.UInt64.fromUint(5) )], xym.PlainMessage.create('btoa'), networkType, ); //手数料をAに負担させるために空トランザクションを作る const feeTx = xym.TransferTransaction.create( futureDeadline, a_publicAccount.address, [new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(0) )], xym.PlainMessage.create(''), networkType, ); const aggregateTx = xym.AggregateTransaction.createComplete( futureDeadline, [ onetimetob.toAggregate(onetime_publicAccount), btoa.toAggregate(b_publicAccount), feeTx.toAggregate(a_publicAccount), ], networkType, [], ).setMaxFeeForAggregate(feemultiplier,2); return aggregateTx; } Bは通常のアグリゲートコンプリート同様,連署してトランザクションを返却します.Aが再署名する余裕を持たせるため,状況に応じて複数のトランザクションに連署して返した方が良いと思います. この方法はワンタイムアドレスのモザイクを一度きりしか行えない量に制限することで取引が複数回行われないようになっています.ただし,モザイクを再入金することで再度行える状態にすることも可能です.第三者にモザイクを入金され,トランザクションを複数回実行されないようにしたい場合にはアドレス制限をかけて入金されないようにするなどの対策を取る必要があります. ワンタイムアドレスに送金したモザイクをトランザクションが実行する前に自分のアドレスに戻すことで,取引の中止を行うことも可能です. 例2 ランダムに生成したアドレスに対してアドレス制限を行う この方法ではアドレス制限を混ぜることで複数回実行しようとすると'Failure_RestrictionAccount_Invalid_Modification'が発生し無効になります.仕組み上,ワンタイムアドレスを利用する必要はありません.ただし,アドレス制限を手動で解除した場合は再実行が可能です.また,同じアドレスにアドレス制限を加えていくことになるためアドレス制限の枠を使い切ると使用できなくなります.ワンタイムアドレスがアドレス制限を行えば枠の問題は無いのですが,この場合はワンタイムアドレスの秘密鍵を確実かつ安全に破棄することが必要になります. const randomAccount = xym.Account.generateNewAccount(networkType); const a_account = xym.Account.createFromPrivateKey( ******, networkType, ); const b_account = xym.Account.createFromPrivateKey( '******', networkType, ); const a_publicAccount = xym.PublicAccount.createFromPublicKey( '8E9765F3471D5E7AEA39620F0998C25C96CEFA24F78980F31B55604FAF7C8868', networkType, ); const b_publicAccount = xym.PublicAccount.createFromPublicKey( 'CCECC77ABE531E7038A7D4E416BB99739642B56014034F1C45B1BD157803C105', networkType, ); const Txs = deadLineTest(7);//6時間*7回=42時間の有効期限のトランザクションを生成 var signedTxs = []; for(tx of Txs){ const singed = a_account.sign(tx,networkGenerationHash); const onetimeCosign = xym.CosignatureTransaction.signTransactionPayload(//onetimeアドレスの連署 onetimeAccount, singed.payload, networkGenerationHash ); signedTxs.push({"signed":singed,"cosign":onetimeCosign}); } function futureDeadlineTx(num){ const now = Date.now(); var Txs = []; for(i=6;i<num*6;i+=6){ //現在のUNIXミリ秒-Symbolが誕生したUNIX秒*1000 + 6の倍数 const deadline = new xym.Deadline(now - epochAdjustment * 1000 + 60*60*i*1000); console.log(deadline); Txs.push(createFutureTransaction(deadline)); } return Txs; } function createFutureTransaction(futureDeadline){ //使い捨てアドレスからb const onetimetob = xym.TransferTransaction.create( futureDeadline, b_publicAccount.address, [new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(7*1000000) )], xym.PlainMessage.create('atob'), networkType, ); //bからa const btoa = xym.TransferTransaction.create( futureDeadline, a_publicAccount.address, [new xym.Mosaic( new xym.MosaicId('310378C18A140D1B'), xym.UInt64.fromUint(5) )], xym.PlainMessage.create('btoa'), networkType, ); //アドレス制限を行う const addressRestrictionTx = xym.AccountAddressRestrictionTransaction.create( futureDeadline, xym.AddressRestrictionFlag.BlockOutgoingAddress, [randomAccount.address], [], networkType, ); const aggregateTx = xym.AggregateTransaction.createComplete( futureDeadline, [ atob.toAggregate(a_publicAccount), btoa.toAggregate(b_publicAccount), addressRestrictionTx.toAggregate(a_publicAccount), ], networkType, [], ).setMaxFeeForAggregate(feemultiplier,1); return aggregateTx; } その他にもアドレスに事前にリンクさせたマルチシグを解除したり,ランダム生成したワンタイムアカウントのアドレス制限を混ぜるなど(プログラム実行後に秘密鍵を破棄)色々なやり方があると思います.しかし,どの方法でも条件をクリアさえしてしまえばトランザクションの再実行ができてしまいます.個人的には,ワンタイムアドレスの秘密鍵が漏れても被害が少なくなるように作るのが良いと思います. この記事を書いてから思ったのですがアグリゲートボンデッドでもおそらく同じことが可能です.アグリゲートボンデッドで作ればAが署名を完成させる手間を省くことができますし,有効期間も2日なので必要な未来のトランザクションの量が1/8に減少します.署名したいときにBがハッシュロックを行い,ボンデッドをBがアナウンスして連署すればおそらく可能とおもいます.アグリゲートコンプリートを使うよりも断然便利ですし,アグリゲートボンデッドを使用する上で最大のネックとなっていた10XYMのロック問題も解決します.もし可能であればアグリゲートボンデッドを使うべきだと思います(今回は記事が長くなったので未検証です) 未来のトランザクションをノードにアナウンスした際の挙動について 未来のトランザクションをノードは受け入れない仕様になっているため(deadlineが6時間後までのトランザクション)未来のトランザクションをノードに送り付けると,’Failure_Core_Future_Deadline’となり失敗します.そして,その未来のトランザクションを送ったノードでは有効なdeadlineになっても受け入れなくなります.このため,未来のトランザクションは有効になる前にノードに送らないように注意する必要があります.有効になる前に送ってしまった場合は他のノードにアナウンスしてあげることで正常にアナウンスすることが可能です. 番外編2 署名済みトランザクション自体をチェーンに書き込む 連署のためにはトランザクションを相手にメールなどの様々な手段で送ってあげる必要がありますが,署名済みトランザクション自体は公開しても害のあるものではないのでチェーンにそのまま書き込んでしまうことで署名を伝送する手間を省くことができます. const signed = JSON.stringify(signedTransaction);//署名済みトランザクションを文字列化 const transferTransaction = xym.TransferTransaction.create( xym.Deadline.crate(epochAdjustment), targetAddress, [new xym.Mosaic( currencyMosaicId, xym.UInt64.fromUint(0) )], xym.PlainMessage.create(signed), networkType, ).setMaxFee(feemultiplier); const payloadTransferTransactionSigned= a_account.sign( transferTransaction, networkGenerationHash, ); トランザクションによってはpayloadが長くなり,transferTransactionでは納めきれなくなると思います.その場合はアグリゲートトランザクションを使って文字数を増やすと良いと思います. この方法は平文で署名済トランザクションを公開しているため誰でもトランザクションを見ることができます.このため,いたずらで署名済みトランザクションをノードに投げられルト,アナウンスが出来なくなる可能性があります.簡単な暗号化をおこなうか,外部にrestを公開しないapiノードを建ててそのノードからアナウンスすることで対応は可能だと思います.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material UI のTextField とReact Hook Form の組み合わせでフォームのひらがなをカタカナに変換

フォーム入力でひらがなでの入力をカタカナに自動変換する機能をMaterial UI のTextField とReact Hook Form のバリデーションを組み合わせて実装したかったのでしてみました。 ひらがなをカタカナに変換する機能部分実装 カタカナとひらがなの UTF-16 コードポイントの値を比較すると、0x60 だけずれているとのことで、0x60 という値を、ひらがなに足せばカタカナに、カタカナから引けばひらがなに変換することができるとのこと。 export const hiraToKata = (string: string): string => { return string.replace(/[\u3041-\u3096]/g, (ch: string) => String.fromCharCode(ch.charCodeAt(0) + 0x60), ); }; 参考:文字列内のひらがなとカタカナを変換する フォーム部分の実装 Material UI やReact Hook Form のドキュメントを行き来しながらなんとか実装できました。 <TextField/> のonBlur に入力された値をカタカナに変換してsetValue してあげるとフォームからフォーカスが外れたときにカタカナに変換できるようになりました。 import { TextField } from '@mui/material'; import { Controller, useForm } from 'react-hook-form'; import { hiraToKata } from 'util/hiraToKata'; export const CustomForm = () => { const { formState: { errors }, control, setValue, } = useForm({ mode: 'onBlur', criteriaMode: 'all', }); return ( <Controller name='lastName_kana' control={control} rules={{ required: '入力が必要です' }} defaultValue='' render={({ field }) => ( <TextField {...field} variant='outlined' label='セイ' error={errors?.lastName_kana && true} helperText={errors?.lastName_kana && errors?.lastName_kana.message} onBlur={(e) => setValue('lastName_kana', hiraToKata(e.target.value))} /> )} /> ); }; React Hook Form であれこれ詰まってますが何とかかんとかやってます。 もっといい方法がありましたらぜひぜひご教授ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESP32をJavascriptでLチカする:console.logをリモートで参照する

前回の投稿「ESP32をJavascriptでLチカする」に引き続き、ESP32でJavascriptを動かします。 今回は、デバッグログをリモートで参照できるようにします。 ESP32でJavascriptを動かせるようになったのはいいですが、console.logの出力は相変わらずSerial出力でした。 リモートで確認できるように、log.ioを使います。 log.ioについては、以下を参照してください。  log.ioでログをブラウザでリアルタイムモニタリングする ソースコードもろもろは、前回同様以下に上げてあります。 随時、修正、拡張しています。 poruruba/QuickJS_ESP32 log.ioサーバを立ち上げる 以下の記事を参考に、log.ioサーバを立ち上げます。  log.ioでログをブラウザでリアルタイムモニタリングする npm install -g log.io mkdir ~/.log.io vi ~/.log.io/server.json log.io-server log.ioにコンソール出力する main.cppの以下の部分に立ち上げたlog.ioのサーバとポート番号を指定します。 main.cpp const char *LOGIO_HOST = "【Log.ioのホスト名】"; // log.ioのホスト名 const unsigned short LOGIO_PORT = 6689; // log.ioのポート番号 次に、log.ioをブラウザから開きます。 あとは、Javascriptで以下のように有効化して、console.logで出力すると、その内容がlog.ioサーバに送信されます。 main.js console.enableLogio(true); console.log("hello”); こんな感じで、ログが表示されます。 stream名はMACアドレスにしてあります。 console.logではなく、console.infoなどで呼び出すと、source名がinfoなどで届きます。 これで、わざわさESP32を仮想COMポートなどとしてつなぐ必要がなくなります。 (追加)SDの追加 log.ioとは関係ありませんが、ついでに、SDにも対応しておきました。JavascriptからSDを操作できるようにしておきました。 APIは以下を参照してください。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Processing 2021】 #p5js で #MediaPipe (JavaScript版)を使った高精度な認識を利用する

この記事は、2021年の Processing のアドベントカレンダー(@Adventar) の 15日目の記事です。 あと 2021年のアドベントカレンダーについて、Qiita の p5.js のアドベントカレンダー もあったので、そちらは以下の別記事を登録しているので、よろしければこちらもご覧いただけると幸いです。 【p5.js 2021】Magenta.js の MusicVAE を使った音作りを試す(p5.js Web Editor上で扱う) - Qiita 【p5.js 2021(2つ目)】 Leap Motion(leap.js)を p5.js Web Editor上(JavaScript)で扱う - Qiita この記事の内容 内容は、p5.js(Processing を JavaScript で扱えるライブラリというような感じのもの)と、Googleさんが提供している MediaPipe(その中の JavaScript版)を組み合わせる話です。 あれこれ説明を書いてしまっているので、「とりあえず実装の中身だけ知りたい」とか、「今回の内容をサクッと体験したい」・「実装した例を見たい」という場合は、目的によって、記事内の以下のいずれかの項目からお読みください。 MediaPipe のサンプルの構成(MediaPipe Hands を例に) p5.js と組み合わせるために簡素化する サンプルを体験する MediaPipe + p5.js で試作したもの(試作の事例集) 記事に出てくるライブラリ等 p5.js とは? p5.js とは、以下の公式ページの説明を引用すると、「クリエイティブ・コーディングのための JavaScriptライブラリ」です。 クリエイティブ・コーディングの説明や例について、気になる方は以下をご覧ください。  ●趣味としてのクリエイティブ・コーディング|deconbatch|note   https://note.com/deconbatch/n/n6e6dbcb2b452 技術的なところでは、p5.js は描画系ライブラリの 1つとして紹介されたりもします。 ライブラリを用いなくても、HTML+JavaScript の構成で canvasタグと素の JavaScript を使った描画を行うことはできますが、それを、より便利にしてくれるもの、というのがざっくりなイメージです。 そして、WebGL を用いた 3D系の描画も扱いやすくしてくれる仕組みがあります。 MediaPipe とは? MediaPipe は、Googleさんが提供している仕組みで、今回扱う JavaScript版では、主に人を対象にした認識処理(機械学習ベースの処理)を利用できるものです。 JavaScript版以外にも、以下のとおり複数の開発言語に対応していて、開発言語によって使える機能・使えない機能があったりします。 JavaScript版は、「Googleさんが提供する機械学習ライブラリの JavaScript版、TensorFlow.js」で動いているので、ブラウザ上で動作します。 以下の公式ページ内のリンクから、ブラウザ上で体験できるデモページに行って、簡単に体験することもできます。  ●MediaPipe in JavaScript - mediapipe   https://google.github.io/mediapipe/getting_started/javascript.html デモページを開くと、例えば以下のような処理結果を、実際に確認できます(要カメラ)。 顔全体だけでなく、顔の中の複数の場所の位置を検出 手の指先や手首など、手の中の複数の場所の位置を検出 画像中の人の領域と人ではない背景などの領域とを分離する(ための領域の情報を得られる) いわゆる姿勢推定ができる(顔や手だけでなく、肩・腰・足など全身の複数の箇所の位置を検出できる) 特定の物体の 3D空間内でのおおよその位置(それを囲うボックスの情報)を得られる なぜ MediaPipe を使いたいのか? ここで書いた JavaScript版の MediaPipe でできることの一部は、別の JavaScriptライブラリでも対応できるものがあります。p5.js とセットでよく用いられるもので ml5.js が有名どころであったりもします。 自分も、実際に MediaPipe以外の画像認識系ライブラリを p5.js と組み合わせて、お試しをしたことも何度もあります。 「なぜ、MediaPipe なのか?」というところは、その精度が優れているから、というのがあります。 例えば、画像からの手の認識で、「手が画面端に近いと認識しづらい」・「開いた手が真正面に向いてると良く認識するが、手を傾けたりすると認識しづらくなる」等ということがありますが、MediaPipe はそのあたりがダントツで優秀なイメージです。 ちなみに、手の認識の話だと「MediaPipe の仕組みは MediaPipe Hands」、他のライブラリだと「ml5.js の Handpose」、「Handtrack.js」などを p5.js と組み合わせて試し、使い比べていました。 「MediaPipe と p5.js で実際に何ができるか?」という話は、この記事の最後(「おわりに」の後の部分)に、自分が組み合わせて試したものの一部を掲載しますので、よろしければそちらをご参照ください。 (精度についても、その動画でおおよそ分かる部分があるかと思います) MediaPipe と p5.js を組み合わせる MediaPipe のサンプルの構成(MediaPipe Hands を例に) MediaPipe と p5.js を組み合わせる話をする前に、MediaPipe 単体での構成を補足します。 いくつもある中の「MediaPipe Hands」を例にして、話を進めていきます。その際、簡単化のために、同時に検出できる手の数を 1つのみという制約をつけて進めていきます(機能としては2つよりも多くの手を同時に検出できます)。 基本的な実装については、公式ページの「JavaScript Solution API」の部分をもとにします。 具体的には、ライブラリの読み込みと、JavaScript の実装が以下のようになっています。 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> </head> <body> <div class="container"> <video class="input_video"></video> <canvas class="output_canvas" width="1280px" height="720px"></canvas> </div> </body> </html> HTML について、bodyタグ部分は「videoタグとcanvasタグ」という、カメラ入力+描画を扱うもので、よく見かける構成です。 <script type="module"> const videoElement = document.getElementsByClassName('input_video')[0]; const canvasElement = document.getElementsByClassName('output_canvas')[0]; const canvasCtx = canvasElement.getContext('2d'); function onResults(results) { canvasCtx.save(); canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); canvasCtx.drawImage( results.image, 0, 0, canvasElement.width, canvasElement.height); if (results.multiHandLandmarks) { for (const landmarks of results.multiHandLandmarks) { drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {color: '#00FF00', lineWidth: 5}); drawLandmarks(canvasCtx, landmarks, {color: '#FF0000', lineWidth: 2}); } } canvasCtx.restore(); } const hands = new Hands({locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }}); hands.setOptions({ maxNumHands: 2, modelComplexity: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5 }); hands.onResults(onResults); const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({image: videoElement}); }, width: 1280, height: 720 }); camera.start(); </script> JavaScript の実装は、主に以下の処理で構成されています。 MediaPipe Hands の設定 パラメータ設定 カメラでの画像取得 カメラのパラメータ設定 画像処理部への受け渡し カメラ画像に検出したキーポイント等の描画(緑や赤の丸や線のところ)を重畳表示 カメラ画像の描画 キーポイント等の重畳描画 (キーポイントの座標データの保持) p5.js と組み合わせるために簡素化する p5.js と組み合わせるにあたり、重複した部分が出てくるので、それを削ります。 上記の HTML と JavaScript を扱う際、p5.js側で受け持てば良さそうなものは削ってしまうという方針です。 また、手の上に重畳しているキーポイント等の情報(赤や緑の丸や線)は、デバッグ用にあっても良いかもしれないですが、最終的には不要になるので削ってしまいます。 その結果、以下のような役割分担となります。 MediaPipe Hands で担当 MediaPipe Hands のパラメータ設定 カメラでの画像取得 カメラのパラメータ設定 画像処理部への受け渡し p5.js で担当 カメラ画像の描画 キーポイントの座標情報を使った処理(手の位置に応じた処理をする座標計算) カメラ画像上への図形等の重畳描画 HTML について ここから、p5.js の実装の話も入ってくるのですが、プログラムを p5.js Web Editor の上で作っていきます。 まずは、そこでデフォルトで準備されている index.html に手を加えていきます。 主にやることは、「MediaPipe Hands用のライブラリの読み込み」と「MediaPipe で使うための videoタグの追加」です (手のキーポイント検出までは、MediaPipe Hands側の処理で担ってもらう、という切り分けが良さそうだったため)。 <!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> <link rel="stylesheet" type="text/css" href="style.css" /> <meta charset="utf-8" /> </head> <body> <video class="input_video"></video> <script src="sketch.js"></script> </body> </html> JavaScript について JavaScript については、以下の内容にしました。 元から改変している部分や、お試しのために設定した内容などは、コメントを入れています。 const isFlipped = true; let keypointsHand = []; const videoElement = document.getElementsByClassName("input_video")[0]; videoElement.style.display = "none"; function onHandsResults(results) { keypointsHand = results.multiHandLandmarks; } const hands = new Hands({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; }, }); hands.setOptions({ selfieMode: isFlipped, maxNumHands: 1, // 今回、簡単化のため検出数の最大1つまでに制限 modelComplexity: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5, }); hands.onResults(onHandsResults); const camera = new Camera(videoElement, { onFrame: async () => { await hands.send({ image: videoElement }); }, width: 1280, height: 720, }); camera.start(); let videoImage; function setup() { const canvas = createCanvas(500, 400); videoImage = createGraphics(320, 180); } function draw() { clear(); background("rgba(100, 100, 255, 0.2)"); videoImage.drawingContext.drawImage( videoElement, 0, 0, videoImage.width, videoImage.height ); push(); if (isFlipped) { translate(width, 0); scale(-1, 1); } displayWidth = width; displayHeight = (width * videoImage.height) / videoImage.width; image(videoImage, 0, 0, displayWidth, displayHeight); pop(); if (keypointsHand.length > 0) { console.log(keypointsHand); // 結果を得る const indexTip = keypointsHand[0][8]; console.log(indexTip); ellipse(indexTip.x * displayWidth, indexTip.y * displayHeight, 10); } } 今回作ったものを実行すると、手の 21箇所のキーポイントの情報が得られます。 そして、その中の人差し指の先の位置座標を使って、その位置に円を描画します。この後にあるリンクをたどることで、実際にブラウザ上で体験できるものも用意しています。 「drawingContext」を用いる部分について 今回、「あらかじめ用意した videoタグ + MediaPipe側のカメラ処理で得られた画像を p5.js で取得する」のは、少しだけ対応が必要です。 具体的には、以下の Qiita の記事で書いた「drawingContext を用いる方法」で対応可能です。  ●p5.js の処理で既存の videoタグに表示されたカメラ画像を取り込む【drawingContext を利用】 - Qiita   https://qiita.com/youtoy/items/ad9372a1aa895b1a0fb1 サンプルを体験する 今回の記事に書いたもの 今回、p5.js Web Editor上で作ったものを、以下で実際に体験いただけます。 カメラに手をかざすと、人差し指の先に小さな円がついてくる、という簡素な内容になります。 ●p5.js Web Editor | 【2021年のProcessingのアドベントカレンダー】MediaPipeとの組み合わせ  https://editor.p5js.org/toyota_ref/sketches/7caxAKaEp 別の事例 また、今回の内容をさらに発展させ、某マンガ・アニメの悪役が使う呪文っぽものを体験できる、というコンテンツを試作しました。以下より、情報の参照や体験を行っていただけます。 ●yo-to/magic-mediapipe_hands_p5js  https://github.com/yo-to/magic-mediapipe_hands_p5js ●某アニメの悪役が使う「とっておきの手品」っぽい禁呪が使える(気がするかもしれない)Webアプリ V2 | ProtoPedia  https://protopedia.net/prototype/2734 おわりに MediaPipe + p5.js を組み合わせた実装をいろいろやる中で、できるだけシンプルにプログラムを組む、ということを模索してきました。 まずは、「これがベストかは分からないけれど、かなりコンパクトになっただろう」と思える状態になったので、アドベントカレンダー登録も絡めて、記事化してみました。 MediaPipe + p5.js で試作したもの(試作の事例集) 以下は、試作したものが動作している様子の動画を、ひたすら掲載していきます。 また、動画サムネイルが大量に並んだ表示になるのを避けるため、一部を除いて動画表示部分を折りたたんでいます。 「★ 動画の表示部分を折りたたんでいます (クリックすると展開できます)」と書かれた以下の部分をクリックすると、隠れていた動画が表示されます MediaPipe Hands(手の認識) 【1】 手の動きで音を奏でる 【2】 某アニメの悪役の禁呪を使える感じがする(かもしれない)やつ 【3】 カメラ映像の中にカメラ映像を表示 =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= 【4】 p5.js の WEBGLモードとの組み合わせを試すためのもの(ボックスを回転表示させる) =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= 【5】 MediaPipe + p5.js の組み合わせを最初に試したもの(透過された矩形を動的に描画) =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= MediaPipe Holistic(姿勢推定) 【6】 ネコ画像で VTuber っぽい何か MediaPipe Selfie Segmentation(背景と人の分離) 以下のデモ映像で、画面上部に 3つほど小さいウィンドウがありますが、これは見た目に分かりやすくするためにデバッグ用情報を残しているものです。 左から順に「人物領域の情報(マスクとして使うもの)」⇒「カメラ画像そのまま」⇒「カメラ映像にマスク処理をしたもの(人物のみ表示させたもの)」となっています。 【7】 人と背景の間での描画1(バーチャル背景的な表示や、p5.js の動的な表示) =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= 【8】 人と背景の間での描画2(p5.js の動的な表示) =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= MediaPipe Face Mesh 【9】 目と口の動き(位置・開閉)と目の虹彩の動き(位置)を利用した描画 顔の中のたくさんの点を検出できる「MediaPipe Facemesh」が、目の中の領域での「虹彩検出」ができるようになったという機能追加があった際、それを試すために作ったものです(目の位置の白い円は、目の中の虹彩n位置・動きに連動しています)。 =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= MediaPipe で試作したもの(p5.js は使っていない事例) 以下は p5.js は絡まないですが、MediaPipe の JavaScript版をブラウザ上で使っている形の試作です。 【10】 手で 6台のロボットトイ(toio)を操る ロボットトイの toio をブラウザから制御しており、そのきっかけの部分に MediaPipe を使いました。 toio とブラウザの通信は、Web Bluetooth API という仕組みを活用して実装しています。 =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)================================= MediaPipe 以外のものを p5.js と組み合わせた事例 【11】 Handtrack.js を使ったもの(物理演算エンジンとの組み合わせ) =================================   ★ 動画の表示部分を折りたたんでいます   (クリックすると展開できます)=================================
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQuery(jQuery UI)selectableとsortableを使って複数の要素をまとめて並び替えできる機能を作る

背景 これまでsortableは使って単一項目の並び替え機能は実装していたが、複数選択できて尚且つそれらをまとめて並び替えできる機能を実装。 sortableを使っていたので同じjQuery UIのselectableを組み合わせて実現させたが、見た目の調整とか含めた参考記事が全然見つけられなかったので自分用にメモ。 変更前のコード(単一項目のみの並び替え) div.targetsの子要素div.itemを並び替える機能。 ほんと単純に、並び替えたいエレメントに対してsortableを使っているだけ。 $("div.targets").sortable({items: "div.item")}; オプションとか色々あるけど、これだけで対象項目をドラック&ドロップして並び替えができるようになる。 すごい便利。。。 とりあえず機能だけを実装する場合 seletableで複数項目を選択できるようにする selectableを使うと、選択した項目のクラスに"ui-selected"が付与される。 選択した項目をsortableの並び替え対象にするために、cancelオプションで".ui-selected"を指定すること。 $("div.targets").selectable({ cancel: ".ui-selected" }); sortableを使ってまとめて並び替え selectableで複数項目を選択できるようになったので、それらをまとめて並び替えるためにsortableにhelperオプションとstopイベントを追加で設定。 .sortable({ items: "div.item", helper: function(e, item) { let selected_all = item.parent().children('.ui-selected').clone(); item.data('multidrag', selected_all).siblings('.ui-selected').remove(); return $('<div>').append(selected_all); }, stop: function(e, ui) { let selected = ui.item.data('multidrag'); ui.item.after(selected); ui.item.remove(); }, }); 機能だけであればこれで終わり 上の2つのコードにメソッドチェーンを使ってあげれば、div.itemの複数並び替えはできるようになっているはず。 $("div.targets").selectable({ cancel: ".ui-selected" }) .sortable({ items: "div.item", helper: function(e, item) { let selected_all = item.parent().children('.ui-selected').clone(); item.data('multidrag', selected_all).siblings('.ui-selected').remove(); return $('<div>').append(selected_all); }, stop: function(e, ui) { let selected = ui.item.data('multidrag'); ui.item.after(selected); ui.item.remove(); }, }); 見た目も調整する場合 今回の並び替えの機能面には影響ないけど、その他の機能向けや見た目の調整用に設定を追加したのでついでに記載。 selectableの追加設定 ■追加内容 ・selectedイベントの追加 ・unselectedイベントの追加 ・cancelオプションの設定追加 並び替え以外で、選択された項目をまとめて操作するために、選択されたらチェックボックスにチェックor選択が外れたらチェックボックスのチェックも外すような処理も入れています。 selectedイベントで選択時の処理、unselectedイベントで選択が外れた時の処理を設定。(ここの記事を参考にしました) 操作トリガー用のdiv.buttonをcancelオプションに追加しています。 $("div.targets").selectable({ cancel: "div.button, .ui-selected", selected: function(e, ui) { $(ui.selected).find('input[name="checkbox[]"]').prop("checked", true); }, unselected: function(e, ui) { $(ui.unselected).find('input[name="checkbox[]"]').prop("checked", false); }, }); sortableの追加設定 ■追加内容 ・itemsやtoleranceなどのオプションを追加 ・helperオプションの返り値を変更 オプションの一番上のitemsからtoleranceは書いてある通りだったので割愛。(参考記事) ドラック中に選択した項目が全てヘルパーに表示されちゃってダサかったので、代表の1項目だけを表示されるように変更。 helperオプションで代表の1項目を定義 let selected_one = item.clone(); して、 定義したselected_oneをhelperオプションの返り値に設定することで、ドラック中はこれだけが表示されるようになった。 .sortable({ items: "div.item", cursor: "pointer", cursorAt: {top: 80, left: 90}, containment: "div.imgs", tolerance: "pointer", helper: function(e, item) { let selected_all = item.parent().children('.ui-selected').clone(); let selected_one = item.clone(); item.data('multidrag', selected_all).siblings('.ui-selected').remove(); return $('<div>').append(selected_one); }, stop: function(e, ui) { let selected = ui.item.data('multidrag'); ui.item.after(selected); ui.item.remove(); }, }); 最終的なコード $("div.targets").selectable({ cancel: "div.button, .ui-selected", selected: function(e, ui) { $(ui.selected).find('input[name="checkbox[]"]').prop("checked", true); }, unselected: function(e, ui) { $(ui.unselected).find('input[name="checkbox[]"]').prop("checked", false); }, }) .sortable({ items: "div.item", cursor: "pointer", cursorAt: {top: 80, left: 90}, containment: "div.imgs", tolerance: "pointer", helper: function(e, item) { let selected_all = item.parent().children('.ui-selected').clone(); let selected_one = item.clone(); item.data('multidrag', selected_all).siblings('.ui-selected').remove(); return $('<div>').append(selected_one); }, stop: function(e, ui) { let selected = ui.item.data('multidrag'); ui.item.after(selected); ui.item.remove(); }, });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ネットで知り合った仲間と農家の3代目の困りごとをFirebaseで解決した話

はじめに ※この記事はfreee APIで業務を楽しく便利にハックしよう!アドベントカレンダー2021の6日目の記事として投稿しています。 はじめまして。今年の1月からプログラミングの勉強をしているものです。 今回は、ネットで知り合った仲間とともに(顔もフルネームも知らないもの同士で)半年間、開発を行い、農家の3代目の困りごとを解決するアプリを作った話をしたいと思います。 きっかけ 私は技術力を身につけたいと思い、今年の1月〜4月ごろまで独学で勉強を進めていました。しかし次第に写経に飽き、このままUdemyなどで写経をして学習するより実際になにか動くものを作るほうが楽しいだろうなと思うようになりました。 また、せっかくなら1人でやるより誰かと一緒に開発する方がもっと楽しいだろうなと思い、勇気を出してネットで仲間を募り、アプリのチーム開発を始めてみることにしました。 募集したメンバーのうちの1人が農業をテーマになにか取り組みたいという希望があったため、農業の課題を解決するアプリを作ることにしました。 農家限定のSNS,直販のプラットフォームなど、いろんなアプリのアイデアが出ましたが、ど素人がいきなり、多くのユーザーがいる前提で価値がでるアプリを作るというのは、資金面でまあ難しいだろうということで、まずは目の前にいる1人を救えるアプリを作ろうという決断にいたりました。 目の前にいる「1人」を決める 農家のために作ると決めたものの、農家と関わりがあったのが私くらいしかいなかったため、「目の前にいる1人」の選定は私が大学時代に農業体験に毎年行っていた農家さん(専業農家)にすることになりました。 さっそく、「農家向けアプリを作りたいので、いいものができたら、使ってもらえないか」と打診したところ、合意が取れ、アプリ開発のためのヒアリングがスタートしました。 想定ユーザーの抱えている困りごとの洗い出し 2代目の社長から世代交代をして、頑張っているたくやさん(専業農家3代目30代男性)が、当時、抱えていたことは以下のようなことでした。 現在は直販のお客さんはみなFAXで注文してくるので、それをみて、注文内容を確認している。 以前は親戚のSEだった叔父が作ってくれたシステムがあったが、消費税の値上げに伴い使えなくなってしまった。 今、顧客を管理するものがないので、毎年くれるリピーターからの注文も、毎度宛名を手入力してヤマトの送り状を作成している。 注文や発送の一覧を管理しているものがないので、ヤマトの営業所に、野菜を持って行って、ヤマトの担当者が伝えてくれる確定情報をもとに、はじめて今日、自分が何件、誰に発送するのかを把握する。 請求管理は親父の担当で、通帳を渡してもらえないので、入金漏れがあったかどうかもわからない。 以前、会社が潰れそうになったときに、前職(食品メーカーの営業時代)の得意先のスーパーに何度も頭を下げて野菜を置かせてもらった。その得意先から入金があった時に、真っ先にお礼が言いたいのに、親父が銀行の通帳をもっていて渡してくれなかった。いつ入金があったのかわからなかった。だからすぐにお礼が言えなかった。それが今でも涙が出るほど一番悔しい。 と私に、教えてくれました。 アプリの要件を整理する たくやさんの悩みを解決するために、検討したアプリの要件は下記としました。 ユーザーはアプリで顧客を管理でき、freeeの取引先とも連携できる ユーザーはアプリで注文情報を管理でき、freeeの請求書とも連携できる ユーザーはアプリで受注〜発送〜請求〜入金までを管理でき、freeeの消し込み情報と連携できる ユーザーは登録した宛名情報をもとに、ヤマトで野菜を送る際に必要な送り状を作成できる ユーザーはヤマトの伝票情報をアプリに反映することで、配達状況を確認できる 技術選定 Next.jsをフル活用して、Vercelにデプロイすることも考えましたが、今回ははじめてのアプリ開発であったこともあり、freee社がテンプレートを出していたFirebaseを選択しました。 複雑になりがちな認証周りはFirebase Authのカスタムトークン認証とfreeeSDKを使い実装でき、バックエンドはすでにあるサンプルを参考に実装できたので、かなり効率的に開発が行えたかなと思います。 一方で下記の要望がチームにあったので、フロント部分はテンプレートを使わず自分たちで実装しました。 React/Next.jsを使って書いてみたい Firebase v9を使って軽量化してみたい useSWRを使ってfreeeAPIコールの最適化をしてみたい chakraUIを使いたい 400,401のエラーをハンドリングしたい Firestoreからのダイレクトな書き込み処理を行いたい Firestore rulesで権限を制御したい アプリ構成 アプリ構成は下記のブログを参考に、↓こんな感じで進めました。 なんとかできあがったアプリを納品 そんなこんなで、迎えた納品日。 夜中2時までみんなで頑張り最後まで直せるバグをひたすら直し、8/14にアプリを納品しました。喋ってる間に寝そうになりましたが、文化祭の前日みたいなノリでとっても楽しい時間でした!! 総勢、200件近いチケットを消化し、なんとか、繁忙期であるとうもろこしの収穫時期の前にアプリの納品を間に合わせることができました。 自分たちのアプリで業務がハックされた瞬間 そして、数日後、作ったアプリで管理されたお届け先情報をもとに、たくやさんが収穫したとうもろこしが各チームメンバーの家に届きました。 アプリを使って届いたとうもろこし。箱を見ただけで泣ける! しかも産地直送だから超うまい!!! 2人目のユーザーに価値を届けるために 我々の学習目標は見事、達成! たくやさんからも、「とても管理がしやすくなった。これ他の人にもぜひ提供したほうがいいよ!」というお褒めの言葉をもらうことができました。 その後チームのこれからについて、みなで検討した結果、2ndステップとして、アプリを世の中にリリースすることを目標に開発を再開することに決めました。 2ndステップではリリースを強く意識して、より高い品質にしていくためにQAエンジニアを置き、テストの自動化などに取り組んだり、さらにメンバーを募集して、10人体制での開発を行っていきました。 そして迎えた2021年12月。 今年の1月にhtml/cssを学びはじめた駆け出しエンジニア集団のアプリ開発のそのコミット数は3000、JIRAのチケット数は400枚を突破しました。 現在、何人かの現役エンジニアにレビューをしてもらっている最中で世にリリースできる日もそう遠くないかもしれません。 チーム開発での学び 開発プロセスとしては、JIRA・miro・Githubを使って、アジャイル開発で進めました。 スプリントは1週間単位とし、月〜金の朝7:00〜をデイリースクラム、土曜の朝7:00〜をレトロスペクティブ、日曜の朝7:00〜をスプリントプランニングとしました。 5月下旬から開発を開始し、毎日、バーンダウンチャートを追っかけながら進めましたが、かなり試行錯誤でありました。 なんたって全員がプログラミング初心者・ようやくReactでTodoアプリを模写できるレベル。 当時のバーンダウンチャートは驚くほどガタガタでした。笑 綺麗な右肩下がりのバーンダウンチャートを描けるチームになるために取り組んだことを3つほど紹介します。 各メンバー何かしらの役割を持つ 限られた人がチームを引っ張るのでなく、すべての領域で一から全員議論をするでもなく、各メンバーがそれぞれの役割(スクラムマスター・広報・チーム活性化・コード品質・デザインなど)を持ち、その領域の最終意思決定権を持つようにしました。これにより、限られた時間で議論が発散的にならず、集中して行えたように思います。 チームの決め事は定期的に見直す 当初のデイリースクラムは6:10からのスタートでした。これは、朝4時から勉強しているメンバーが2人、朝7時くらいまで起きていてそれから寝るメンバーが1人いたからです。 しかしその後、メンバーが入れ替わり、朝が苦手なメンバーが増えた時には、開催を22:00からにしました。一度決めたからといってそのままにせず、何事も今いるメンバーの最適解を模索することを心がけました。現在は、7:00に落ち着いています。 チームを活性化させ続けるために卒業制度(出戻り自由)をもうける ネット上のみで出来上がったこの脆く儚い関係は、オフラインの飲み会や学業・仕事の予定にはどうしても勝てません。チーム立ち上げ当初は、「今はちょっと忙しいけど落ち着いたら対応できます。」という言葉を信じ、忙しくなってしまったメンバーを待ち続けていました。が、待てど待てど戻ってこないメンバー。いつしか音信不通になってしまい、悲しい気持ちとともに、お任せしたタスクは宙ぶらりん。チームの士気も少し下がってしまいました。このままではチームが空中分解してしまうという危機感から、週5日以上(2日間は休んでOK)の出席を求めるチームガイドラインをもうけるとともに、2週連続でガイドラインを守れない場合には卒業を促し、幽霊部員を作らないようにしました。これにより、けじめもついて、チームとして卒業生を気持ちよく送り出すこともできるし、学習意欲の高いメンバーを新たに募集するということができるようになりチームのベロシティも維持できるようになりました。 これ以外にもチーム運営に苦戦することもいくつかありましたがその全てが学びの連続でした。また総勢19人のメンバーがこのチーム開発に関わり、この活動をきっかけに次々と仲間がエンジニアとしての転職に成功していく様子を目の当たりにし、チームを立ち上げた身として本当に嬉しい気持ちになり、色々な経験を提供してくれた仲間に本当に感謝の気持ちでいっぱいです。 最後に freeeAPIはリファレンスが充実していて開発環境もすぐに手に入るしAPIを叩くのを学ぶのに非常によかったと思います。(ヤマトは最後までB2やAPIを試せる開発環境を手に入れることができなかった...泣) しかし、現役エンジニアにとっては簡単なことでも私たち初学者にはフロントエンドの技術習得についてキャッチアップが大変な部分も多くありました。そんなとき、先人たちが残してくれたものが非常に役に立ちましたので、最後におすすめ講座一覧をご紹介して終わりたいと思います。 これからプログラミングを始めて、freeeAPIを使ったアプリ開発をしてみたいという方はぜひ参考にしてみていただけると幸いです。 おすすめ講座一覧 NO 講座名 1 VS Codeの使い方講座!日本語化する拡張機能のインストールも紹介!byしまぶーさん 2 無料かつ最速でプログラミングを学ぶ!ドットインストールの効率的な使い方! byしまぶーさん 3 Git:はじめてのGitとGitHub by清透さん 4 HTML講座 byしまぶーさん 5 CSS講座 byしまぶーさん 6 JavaScript講座 byしまぶーさん 7 モダンJavaScript講座 byしまぶーさん 8 モダンJavaScriptの基礎から始める挫折しないためのReact入門 byじゃけぇさん 9 React入門 byあべちゃん 10 今更解説するReactHooks入門講座 byあべちゃん 11 Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版 byじゃけぇさん 12 Firebase入門 by前田さん 13 Firebase入門 byあべちゃん 14 Firebase設計 bymogaさん 15 EsLint/Prettier ※サロン限定公開 byしまぶーさん 16 Next.js ※サロン限定公開 byしまぶーさん 17 TypeScript ※サロン限定公開 byしまぶーさん
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テス値

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

【初心者向け】npmについてよく知らない人のための教科書 ~npm installは何をおこなっているのか~

はじめに 対象はnpmを学習し始めた人、npmへの理解が浅い人となります。 本記事は、npm(node package manager)に苦手意識があった筆者が克服するために勉強した備忘録です。 開発環境 Apple M1 Big Sur 11.6 Node.js 16.13.0 npm 8.1.0 npmとは こちらについては、【初心者向け】NPMとpackage.jsonを概念的に理解するをお読みください。 「パッケージとは何か」や「package.jsonとpackage-lock.jsonの違い」など、とても理解がしやすいようにまとめられております。 いつかこんな素敵な記事を書いてみたいです。 npm installとは パッケージをインストールするコマンドです。 package.jsonのdependenciesとdevDependenciesに書かれているパッケージをインストールする npm installのエイリアスは、 npm i または npm add です。 まれにエイリアスを使って、解説している記事があり、「このコマンド初めて見たかも?」と思ったものはエイリアスの可能性が高いです。 また、公式を見てみると、他のコマンドにもエイリアスがたくさんあることがわかります。 dependenciesとは 本番環境で必要なパッケージが書かれている dependenciesにパッケージを追加したいときは、 npm install <package-name> --save-prod --save-prodを、-Pとしても可能 であるが、「--save-prod」はデフォルトなため npm install <package-name> と、「--save-prod」を省略できます。 devDependenciesとは 開発環境やテストに必要なパッケージが書かれている devDependenciesにパッケージを追加したいときは npm install <package-name> --save-dev --save-devを、-Dとしても可能 さいごに わからないことがあった時に、Qiitaやドキュメント以外の記事を漁るクセなおしたい。 まず最初に目を通すべきは、やはりドキュメントですね! 参考・おすすめ記事 「『package.json』って何?って時に少し調べた時のノート」 https://overworker.hatenablog.jp/entry/2020/09/27/232236 「package-lock.jsonってなに?」 https://qiita.com/sugurutakahashi12345/items/1f6bb7a372b8263500e5 「package.json のチルダ(~) とキャレット(^)」 https://qiita.com/sotarok/items/4ebd4cfedab186355867 「【いまさらですが】package.jsonのdependenciesとdevDependencies」 https://qiita.com/chihiro/items/ca1529f9b3d016af53ec 「npmパッケージのvulnerability対応フロー」 https://qiita.com/riversun/items/7f1679509f38b1ae8adb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでcommit時にaddされているファイルにのみリントとフォーマットを実行する

何をするのか タイトル通りで、Next.jsでcommit時にステージング(git add)されているファイルにのみnext lint --fixとprettier --wrightを実行します。 ググれば日本語の記事でもやり方は沢山出てくるのですが、意外と落とし穴があるので今回はその詳細を書いていこうと思います。 ※大前提として初めからeslintが導入されている11以降のバージョンを使用しましょう。 よくあるやり方 huskyとlint-stagedを入れて.husky/_/pre-commitとpackage.jsonで以下のように設定して、commitする。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint --dir src", "lint:fix": "next lint --fix --dir src", "format": "prettier --write ./src/**/*", "prepare": "husky install" }, "dependencies": { } "devDependencies": { }, "lint-staged": { "*.(js|ts)?(x)": [ "yarn format", "yarn lint:fix" ] } } これの何が問題かというと、これだと該当する全てのファイルにリントとフォーマットが効いてしまいます。 今回はステージングされているファイルにのみ反映したいです。 やり方 「よくあるやり方」の問題点ですが、.lintstagedrc.jsを使ってコマンドを動的に生成する事で解決します。 ただその前にそこまでの一通りの手順を記載いたします。 また今回はJavaScript Standard Styleを適用したいと思います。(ここはお好みで何でも良いです) パッケージの導入 ※全部一気にやってもいいですが、今回はわかりやすく分けてインストールしています。 まずNext.jsのプロジェクトが作成し終わったら、そのディレクトリで、 yarn add --dev eslint-config-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node を実行して、JavaScript Standard Styleに必要なパッケージをインストールします。 次にコードフォーマッターのprettierと、eslintとprettierのルールの競合を回避してくれるeslint-config-prettierをインストールします。 yarn add --dev prettier eslint-config-prettier そして最後にcommitやpushの前に特定の処理を挟んでくれるhuskyと、ステージングされているファイルの処理を行うためのlint-stagedをインストールします。 yarn add --dev husky lint-staged パッケージの設定 次にeslintとhuskyの設定を行います。 まずeslintrc.jsonを以下のように編集します。 { "extends": ["standard", "next", "prettier"] } このstandardとprettierはそれぞれJavaScript Standard Styleとeslint-config-prettierの設定です。 またこの時の注意点ですがスタイルのルールはnextより前に、フォーマッターのルールはnextより後に書きます。 次にhuskyの設定を行います。 以下のコマンドを実行してhuskyを初期化します。 npx husky-init && yarn ↑が実行されると、自動でルートディレクトリに.husky/_/pre-commitが作成されるので、そのファイルを以下のように書き換えます。 #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn lint-staged 最後にlint-stagedの設定を行います。 ルートディレクトリに.lintstagedrc.jsを作成して、以下の内容を書き込みます。 module.exports = { '*.(js|ts)?(x)': filenames => { const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) return [ `prettier --write ${rawFilePaths.join(' ')}`, `next lint --fix --file ${rawFilePaths.join(' --file ')}` ] } } 以上でインストール&設定は完了です。 これでgit commitすると、自動でステージングされているファイルにリントとフォーマットが効くようになりました。 .lintstagedrc.jsについて まず.lintstagedrc.jsが何をやっているのかですが、returnで返される配列のコマンドをcommitの前に実行してくれます。 つまり単純に module.exports = { '*.(js|ts)?(x)': filenames => [ 'yarn format', 'yarn lint:fix' ] } のように書くと、ステージングされたファイルにjs/jsx/ts/tsxが存在すると、yarn formatとyarn lintが実行されます。 ただしこれでは「よくあるやり方」で書いたように全てのファイルにリントとフォーマットが反映されてしまいます。 そのためステージングされているファイルにのみ反映するコマンドを引数filenamesを使用して生成する必要があります。 コード解説 このfilenamesですが、こいつはステージングされているファイルの絶対パスを列挙した配列であり、中身は以下のようになっています。 [ 'C:/Users/username/Desktop/my_project/.lintstagedrc.js', 'C:/Users/username/Desktop/my_project/next.config.js', 'C:/Users/username/Desktop/my_project/src/pages/members/index.tsx' ] この配列をprocess.cwd(ここではC:/Users/username/Desktop/my_projectが出力される)を使ってプロジェクトルートまでを.に置換します。 const rawFilePaths = filenames.map(file => { const cwd = process.cwd() const isMatchingPathFormat = file.includes(process.cwd()) const myCwd = isMatchingPathFormat ? cwd : cwd.replace(/\\/g, '/') const rawFilePath = file.replace(myCwd, '.') return rawFilePath }) [ './.lintstagedrc.js', './next.config.js', './src/pages/members/index.tsx' ] あとはこの配列を使ってお好みのコマンドが生成できます。 例えば上記のprettier --write ${rawFilePaths.join(' ')}であれば、prettier --write ./.lintstagedrc.js ./next.config.js ./src/pages/members/index.tsxとなります。 注意点 ※isMatchingPathFormatとmyCwdですが、windowsの場合process.cwd()の出力がC:\Users\username\Desktop\my_projectのようにスラッシュがバックスラッシュになってしまう事があるのでそれの調整をしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ノンプログラマーがSymbolブロックチェーンでゲームを作ってみた

この記事は nem Advent Calendar 2021 5日目の記事です。 はじめに プログラム経験ゼロでも、Symbolというブロックチェーンを利用すれば、 Web環境とテキストメモだけで、ブロックチェーンゲームが作れます。 はじめてのプログラムはSymbolブロックチェーンから、 冬休みの学びに、是非お楽しみください。 自己紹介 日常生活でプログラムに一切触れない 仕事をしつつ nembear(ねむぐま) という名前で、暗号通貨Symbol界隈で開発以外の活動をしております。 そんな私でも、ほぼコピペでSymbolを利用した独自のブロックチェーンゲームを作ることが出来たので、皆さんにも紹介します。 本記事で作るブロックチェーンゲーム ブロックチェーンを利用したスロットゲームを作ります。3つの数字が揃えば、「symbol:xym」というトークンが送信される仕組みです。このトークンの送信の部分にSymbolのブロックチェーンを利用します。この記事を読めば、このゲームだけでなくあなたのオリジナルブロックチェーンゲームをほぼコピペで作成することが出来るようになります。 ※アドカレ用にnembearトークンがもらえるバージョンを作りました。とても簡単なゲームですので是非、遊んでください。 ↓↓↓↓↓↓ 必要な環境とスキル 【環境】 ・インターネット接続環境 ・Google Chromeブラウザ ・テキストメモ 【スキル】 ・コピー&ペースト 開発までの流れ 1.SymbolWalletを手に入れる(約5分) 2.Symbol送金プログラムを書いて体験する(約20分) 3.ボタンを押すとSymbol送金されるWebアプリ作成(約10分) 4.スロットゲームにSymbol送金を組み合わせる(自由時間) 5.完成(自由時間) ※イメージ 1.スロットに利用するコインを保管する金庫を作ります。 2.コインの保管と金庫からコインを送信する仕組みを学びます。 3.2で学んだ仕組みを利用して、簡単なアプリを作ります。 4.上記までの学びを利用して、スロットゲームに組み込みます。 5.実際に遊んでみましょう! Symbolブロックチェーンゲーム作成方法 1.SymbolWalletを手に入れる(約5分) ここではスロットに利用するコインを保管する"金庫"を作ります。 その"金庫"となるものが「SymbolWallet」です。これは暗号資産Symbol:XYMを保管するために最もポピュラーでセキュリティの高いWalletとなります。 テキストと動画で同じ内容の作成方法を説明します。お好みに合わせて選択してください。 ■テキストでの説明はこちらから ※ポイント:リンク先の「項目3-4」では必ず「TESTNET」を選択してください。 ■動画での説明はこちらから ※ポイント:動画内のSymbolWalletはこちらからダウンロードしてください。 ↓↓↓ https://github.com/symbol/desktop-wallet/releases また、2021/12/04 にSymbolTESTNETがリセットされた影響で、以下の操作をする必要があります。(下記ツイ内での推奨されているノードURLでもうまくいかない時は「tomato.xym-node.com:3000」をご利用ください。) 2.Symbol送金プログラムを書いて体験する(約20分) ここでは、コインの保管と金庫からコインを送信する仕組み を学びます。 前項目で作成した「金庫:SymbolWallet」に、ゲーム内で利用するコインを保管します。そして保管したコインを送信するところまでを、プログラムで体験してもらいます。ここで言うコインとはSymbolのトークンである「XYM」です。(テスト用のXYMの為、購入する必要はありません。) ここからSymbolブロックチェーンに触れることになりますが、すべてコピペで体験出来ます。リラックスしてお進みください。Symbolがいかに簡単で誰にでも使いやすいものであるかを感じてもらえますと幸いです。 ※ポイント:本項目は @nem_takanobu さんの記事を参考に実施しております。以下のリンク記事を開いて動画をご覧ください。 3.ボタンを押すとSymbol送金されるWebアプリ作成(約10分) ここでは、項目2で学んだ仕組みを利用して、簡単なアプリを作ります。 先ほどはコンソール画面にコードを打ち込んで送金を体験しました。操作は簡単ではありましたが、毎回何度もあの操作を繰り返すのは大変ですよね?ですので、送信までの動作をボタン1つにまとめて、ボタンを押せばSymbolブロックチェーンを利用して送信される装置を作ってみましょう。 3-1.ボタンを押すと動作が発生するベースを作る ■準備するもの →テキストエディタ(メモ帳でOK) ■手順 1.以下のコードをすべてテキストエディタにコピペし、保存する。 2.保存したテキストの名前を「test.html」に変更する。 3.「test.html」をダブルクリックする。 早速、以下のコードをテキストエディタにコピペしましょう。 test.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>nembear nemAdventCalendar2021</title> </head> <body> <p>button click</p> <input type="button" value="button" id="btn"> <script> function butotnClick() { alert('Ok'); } let button = document.getElementById('btn'); button.addEventListener('click', butotnClick); </script> </body> </html> 時々質問で、↑のコードが良く分からない。という話を聞きますが、私も良く分かりません。私たちはエンジニアではないので、読める必要は無いのです。上記のサンプルも「html ボタン クリック」でWeb検索し、1番最初に上がってきた記事のサンプルを使ってます。深く考えるのはあとで、先ずは実践していきましょう。 次に、コピペしたテキストの名前を「test.html」として保存します。 「test.html」をダブルクリックしてください。 こんな画面が出てきたと思います。 「button」をクリックしてみましょう。 「Ok」のPOPが表示されました。おめでとうございます。これで、ボタンを押すと何かしら動作する(これなら「Ok」のPOPが出る)装置が完成しました。次の項目で、この装置にSymbolブロックチェーンの送金部分を投入してみましょう。 3-2.ボタンを押すとSymbol:XYMが送信されるアプリを作る ■準備するもの →テキストエディタ(メモ帳でOK) ■手順 1.以下のコードをすべてテキストエディタにコピペし、保存する。 2.保存したテキストの名前を「test2.html」に変更する。 3.「test2.html」をダブルクリックする。 ※ポイント:先ほど作った「test.html」の中身を確認すると、以下の部分がどうやら「Ok」POPが出るための呪文のようです。(試しに、この'Ok'を他の言葉に変えると、変えた内容のPOPが出ます。) function butotnClick() { alert('Ok'); } つまり、ボタンを押すと、「 alert('Ok'); 」の処理が動くということです。だとしたらば、項目2で実施したSymbol送金のコードをすべてここに書き込めば、ボタン押すだけで送金出来るのではないでしょうか? 早速、以下のコードをテキストエディタにコピペしましょう。 ※ポイント:項目2の動画で一緒に入力していたコードをそのまま利用してます。秘密鍵とアドレスだけ、あなたが作成したSymbolWalletのものを入力してください。 test2.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>nembear nemAdventCalendar2021</title> </head> <body> <!-- 設定値の定義とライブラリのインポート&ライブラリのインスタンス生成 --> <script src="https://xembook.github.io/nem2-browserify/symbol-sdk-1.0.1.js"></script> <script language="JavaScript"> NODE = 'http://tomato.xym-node.com:3000'; GENERATION_HASH = '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155'; EPOCH_ADJUSTMENT = 1616694977; nem = require("/node_modules/symbol-sdk"); </script> <p>button click</p> <input type="button" value="button" id="btn"> <script> function butotnClick() { // アカウントの登録(自分のWalletの秘密鍵を入れてください) alice = nem.Account.createFromPrivateKey('あなた自身のSymbolWalletの秘密鍵を入力', nem.NetworkType.TEST_NET); console.log(alice); // トランザクション作成(自分のWalletのアドレスを入れてください) tx = nem.TransferTransaction.create( nem.Deadline.create(EPOCH_ADJUSTMENT), nem.Address.createFromRawAddress("あなたのSymbolアドレスを入力"), [new nem.Mosaic(new nem.MosaicId('091F837E059AE13C'), nem.UInt64.fromUint(1000000))], nem.PlainMessage.create('nemAdventCalendar2021'), nem.NetworkType.TEST_NET, nem.UInt64.fromUint(100000) ); console.log(tx); // 署名 signedTx = alice.sign(tx, GENERATION_HASH); console.log(signedTx); // 署名したトランザクションをネットワークにアナウンス new nem.TransactionHttp(NODE) .announce(signedTx) .subscribe((x) => console.log(x), (err) => console.error(err)); alert('Ok'); } let button = document.getElementById('btn'); button.addEventListener('click', butotnClick); </script> </body> </html> 次に、コピペしたテキストの名前を「test2.html」として保存し、ダブルクリックして表示させてください。 「test.html」と同様の表示内容です。ですが、ボタンを押した後の処理が大きく異なります。あなたのWalletを開いたままで、ボタンを押してください。 「チャキン」という音と共に、あなたのWalletに1XYMが送金されていると思います。おめでとうございます。これであなたは、この世のどんなコードにも、Symbolブロックチェーンを繋げることが出来ました。これは嘘ではありません。”javascript”フリーコード等で検索して出てきた世界中のコードに、Symbolブロックチェーンを繋げることが出来るのです。では、次の章でそれを実践してみましょう。 ※送信されていない場合は以下の確認をしてください。 1.すべてコピー&ペーストしているか? 2.秘密鍵とアドレスを正しく記入しているか? 3.Web上で開いている「test2.html」が更新されているか?(F5を押す) 4.スロットゲームにSymbol送金を組み合わせる では早速、3章の最後に言った、”javascript”フリーコード等で検索して出てきた世界中のコードに、Symbolブロックチェーンを繋げることが出来るを証明したいと思います。今回はスロットゲームなので、「javascript スロットゲーム」で検索して出てきたサンプルコードを利用します。 早速出てきた「programmercollege」さんのサンプルコードを利用します。 以下のコードをコピペし、「slot1.html」という名前で保存、起動してください。 slot1.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>slot nemAdventCalendar2021</title> </head> <body> <h1>slot nemAdventCalendar2021</h1> <script language="JavaScript"> var time1; var time2; var time3; // START function slot() { start1(); start2(); start3(); } //各ランダム値を表示 function start1() { document.getElementById("dat1").value = Math.floor(Math.random() * 2); time1 = setTimeout(start1, 10); } function start2() { document.getElementById("dat2").value = Math.floor(Math.random() * 2); time2 = setTimeout(start2, 10); } function start3() { document.getElementById("dat3").value = Math.floor(Math.random() * 2); time3 = setTimeout(start3, 10); } // STOP function stop1() { clearTimeout(time1); } function stop2() { clearTimeout(time2); } function stop3() { clearTimeout(time3); const textbox = document.getElementById("dat1"); const inputValue1 = textbox.value; console.log(inputValue1); const textbox2 = document.getElementById("dat2"); const inputValue2 = textbox2.value; console.log(inputValue2); const textbox3 = document.getElementById("dat3"); const inputValue3 = textbox3.value; console.log(inputValue3); if (inputValue1 === inputValue2 && inputValue3 === inputValue1) { alert('Win'); } else { alert('Lose'); } } </script> <style> table, tr, td { border-style: none; } </style> <div style="background-color : rgb(214, 82, 82); padding : 20px;"> <table> <tr> <td colspan="3"> <input type="button" value="START" onClick="slot()" style="width:100%;"> </td> </tr> <tr> <td> <input type="text" id="dat1" value="0" style="text-align:center;font-size:30px"> </td> <td> <input type="text" id="dat2" value="0" style="text-align:center;font-size:30px"> </td> <td> <input type="text" id="dat3" value="0" style="text-align:center;font-size:30px"> </td> </tr> <tr> <td> <input type="button" value="STOP" onClick="stop1()" style="width:100%;"> </td> <td> <input type="button" value="STOP" onClick="stop2()" style="width:100%;"> </td> <td> <input type="button" value="STOP" onClick="stop3()" style="width:100%;"> </td> </tr> </table> </div> </script> </body> </html> 早速立ち上げてみましょう。 こんな感じで、startを押すと数字が変化し(便宜上0,1だけにしてます)、すべて揃えば「Win」揃わなかったら「Lose」のPOPが出力されます。 3章で学んだことを思い出してください。前回は「Ok」のPOPが表示されるきっかけで送金コマンドを登録してましたよね?ということは、今回も「Win」POPが表示されるきっかけの箇所にSymbol送金コマンドを張り付けるだけではないでしょうか。イメージではもう完成してませんか?Symbolって簡単すぎますよね。 それでは、ここからは是非、ご自身で書いてみてほしいですが、どうしてもうまくいかない場合は以下のサンプルコードをご利用ください。 ※ポイント:秘密鍵とアドレスだけ、あなたが作成したSymbolWalletのものを入力してください。 slot2.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>slot nemAdventCalendar2021</title> </head> <body> <h1>slot nemAdventCalendar2021</h1> <!-- 設定値の定義とライブラリのインポート&ライブラリのインスタンス生成 --> <script src="https://xembook.github.io/nem2-browserify/symbol-sdk-1.0.1.js"></script> <script language="JavaScript"> NODE = 'http://tomato.xym-node.com:3000'; GENERATION_HASH = '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155'; EPOCH_ADJUSTMENT = 1616694977; nem = require("/node_modules/symbol-sdk"); var time1; var time2; var time3; // START function slot() { start1(); start2(); start3(); } //各ランダム値を表示 function start1() { document.getElementById("dat1").value = Math.floor(Math.random() * 2); time1 = setTimeout(start1, 10); } function start2() { document.getElementById("dat2").value = Math.floor(Math.random() * 2); time2 = setTimeout(start2, 10); } function start3() { document.getElementById("dat3").value = Math.floor(Math.random() * 2); time3 = setTimeout(start3, 10); } // STOP function stop1() { clearTimeout(time1); } function stop2() { clearTimeout(time2); } function stop3() { clearTimeout(time3); const textbox = document.getElementById("dat1"); const inputValue1 = textbox.value; console.log(inputValue1); const textbox2 = document.getElementById("dat2"); const inputValue2 = textbox2.value; console.log(inputValue2); const textbox3 = document.getElementById("dat3"); const inputValue3 = textbox3.value; console.log(inputValue3); if (inputValue1 === inputValue2 && inputValue3 === inputValue1) { // アカウントの登録(自分のWalletの秘密鍵を入れてください) alice = nem.Account.createFromPrivateKey('自分のWalletの秘密鍵を入れてください', nem.NetworkType.TEST_NET); console.log(alice); // トランザクション作成(自分のWalletのアドレスを入れてください) tx = nem.TransferTransaction.create( nem.Deadline.create(EPOCH_ADJUSTMENT), nem.Address.createFromRawAddress("自分のWalletのアドレスを入れてください"), [new nem.Mosaic(new nem.MosaicId('091F837E059AE13C'), nem.UInt64.fromUint(1000000))], nem.PlainMessage.create('nemAdventCalendar2021'), nem.NetworkType.TEST_NET, nem.UInt64.fromUint(100000) ); console.log(tx); // 署名 signedTx = alice.sign(tx, GENERATION_HASH); console.log(signedTx); // 署名したトランザクションをネットワークにアナウンス new nem.TransactionHttp(NODE) .announce(signedTx) .subscribe((x) => console.log(x), (err) => console.error(err)); alert('Win'); } else { alert('Lose'); } } </script> <style> table, tr, td { border-style: none; } </style> <div style="background-color : rgb(214, 82, 82); padding : 20px;"> <table> <tr> <td colspan="3"> <input type="button" value="START" onClick="slot()" style="width:100%;"> </td> </tr> <tr> <td> <input type="text" id="dat1" value="0" style="text-align:center;font-size:30px"> </td> <td> <input type="text" id="dat2" value="0" style="text-align:center;font-size:30px"> </td> <td> <input type="text" id="dat3" value="0" style="text-align:center;font-size:30px"> </td> </tr> <tr> <td> <input type="button" value="STOP" onClick="stop1()" style="width:100%;"> </td> <td> <input type="button" value="STOP" onClick="stop2()" style="width:100%;"> </td> <td> <input type="button" value="STOP" onClick="stop3()" style="width:100%;"> </td> </tr> </table> </div> </script> </body> </html> 5.完成(遊んでみよう!) アナタが作った、Symbolブロックチェーンゲーム。早速遊んでみましょう。数字が揃うと、XYMが送金されていることが分かります。 私が言っていた”javascript”フリーコード等で検索して出てきた世界中のコードに、Symbolブロックチェーンを繋げることが出来るが嘘でないことが証明されたのではないでしょうか??例えば、シューティングゲームがあって、敵を倒した時?10,000点を超えた時?レースゲームで勝利した時?想像するだけで何でも出来そうな気になりますし、実際に出来ます。あなたの想像力にSymbolはコストゼロで寄り添います。力を貸してくれます。どうぞ、来年はこの新たな武器を手にエンジョイしてください! ハッカソン情報(開発イベント) Symbol/NEMを世の中に広める活動をしているNPO法人NEMTUS内で「ハッカソン」が開催される予定です。誰でも参加出来て、ビギナーにも賞が出るという噂です。是非ここでの学びで挑戦してはいかがでしょうか? 終わりに いかがでしたでしょうか?ブロックチェーンがとても近い存在に感じられたのではないでしょうか?Symbolはそのように設計されてます。世界中の誰もが利用できるブロックチェーンです。ごく一部の人間だけが使えるものではありません。 あなたも今日からSymbolを利用する側になりました。ブロックチェーンでアプリケーションを作る側になりました。とても素晴らしいことです。ブロックチェーンは投資するだけのものではないことが理解できたと思います。 私はそんなSymbolで活動する方々へ、支援を続けております。 アナタの活躍に支援する日が来ることを楽しみにしております。 【こころから感謝を込めて】シンくまノードは設立してから丁度半年が経ちました。そしてこの半年間で以下の支援を実施出来ました。✅支援回数:24回(計47名)✅総支援額:104,129XYMすべての委任者の皆さんに感謝いたします?※シンくまノードはこちら↓https://t.co/Ctry4cncU2#Smbol #XYM https://t.co/o8uIIDG8kS— アドカレ12/5 元祖ねむぐま (@nembear) November 23, 2021 Symbolはいつでもあなたのそばに。 素敵な旅と幸運を。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js 2021(2つ目)】 Node.js での UDP・TCP通信をシンプルに試す(2021年12月)

この記事は、2021年の Node.js のアドベントカレンダー の 10日目の記事です。 内容は、以下の記事を書く中で使った「zigsim-ws」の中で利用されている「dgram」が気になって調べたり試したりしたことを、記事として書いた形です。 ●【IoTLT 2021】 ZIG SIM から送られるデータを p5.js Web Editor上で活用してみる - Qiita  https://qiita.com/youtoy/items/caca41a68ab3bff6ffa6 zigsim-ws のプログラムと UDP通信 zigsim-ws は、プロトタイピングに役立つスマホアプリの「ZIG SIM」を、ブラウザ(HTML+JavaScript のプログラム)との間で通信させる時に利用するものです。 軽く補足をすると、ZIG SIM がデータを送る時に用いる通信が UDP・TCP で、ブラウザ上の JavaScript のプログラムでは直接は扱えないものになるので、それらの間で通信できるようにするためには仲介役が必要です。zigsim-ws は、その仲介役として UDP 通信をブラウザ上で動く JavaScript が扱える WebSocket の通信に変換する役割を担います。 ●zigsim-ws/index.js at master · acrylicode/zigsim-ws  https://github.com/acrylicode/zigsim-ws/blob/master/index.js そのプログラムは、以下のような実装になっています。 const dgram = require('dgram'); const WebSocket = require('ws'); function init(updPort = 50000, wsServerPort = 8080) { const updServer = dgram.createSocket('udp4'); const wss = new WebSocket.Server({ port: wsServerPort }); console.log(`websocket server listening on port: ${wsServerPort}`) wss.on('connection', (ws) => { console.log(`New client connected to the websocket. Number of clients: ${wss.clients.size}`) }); updServer.on('error', (err) => { console.log(`upd server error:\n${err.stack}`); updServer.close(); }); updServer.on('message', (msg) => { wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(msg.toString()) } }); }); updServer.on('listening', () => { const address = updServer.address(); console.log(`udp server listening on port: ${address.port}`); }); updServer.bind(updPort); } module.exports.init = init; 冒頭の 2行目の部分は、WebSocketサーバーを動かすためのものを読み込んでいますが、1行目で「dgram」を読み込んでいます。 dgram について見てみる npm のページを見て、さらに情報をチェックしようと思って以下にアクセスしました。  ●dgram - npm   https://www.npmjs.com/package/dgram そうすると、以下をのような記載が...(deprecated になってる...) そして、さらに調査をしていきました。 dgram についてさらにチェック 先ほどの npm のページの記載をあらためて見てみると 「As it's a core Node module, we will not transfer it ...」 と書かれていました。 さらに、Node.js の公式情報を合わせて見てみると、Node.js自体の機能の話で「UDP/datagram sockets」というものが出てきました。 ●UDP/datagram sockets | Node.js v17.2.0 Documentation  https://nodejs.org/api/dgram.html 結論を書くと、先ほどの zigsim-ws は、こちらの Node.js内のものを呼んでいたようです。 (※ 自分が zigsim-ws のプログラムを実行していたフォルダ等を見ると、node_modulesフォルダ以下に dgram のフォルダがなかった) そして、Node.js での UDP通信の話を見ていくと、以下の内容も見つかりました。 こちらの記事でも、Node.js に元から入っていそうな dgram の話をされています。 ●Node.jsでUDP通信する - Re: note  https://hikoleaf.hatenablog.jp/entry/2019/06/08/235753 Node.js で UDP通信を実行する それでは、実際に Node.js での UDP通信を、自分の環境で実行してみます。 2者間の単純な通信 先ほど、調べて出てきたと書いていたサイトを見てみると、2者間でのシンプルな通信の例が掲載されていました。 そこで、まずはこの例をそのまま利用します。 1点、元の記事で $ npm install dgram を実行する手順が書いてありますが、これは不要だと思われます。 (これをやってしまうと、上記で deprecated となってたやつを読み込んでしまいそうな予感...) 以下の2つのプログラムを用意し、2つのターミナルを開いて、それぞれで 1つずつのプログラムを実行すると、定期的にデータのやりとりが行われている様子が確認できました。 const dgram = require('dgram'); const PORT_A = 3002; const HOST_A = '127.0.0.1'; const PORT_B = 3003; const HOST_B = '127.0.0.1'; const socket = dgram.createSocket('udp4'); socket.on('listening', () => { const address = socket.address(); console.log('UDP socket listening on ' + address.address + ":" + address.port); }); socket.on('message', (message, remote) => { console.log(remote.address + ':' + remote.port +' - ' + message); socket.send(message, 0, message.length, PORT_B, HOST_B, (err, bytes) => { if (err) throw err; }); }); socket.bind(PORT_A, HOST_A); const dgram = require('dgram'); const PORT_A = 3002; const HOST_A ='127.0.0.1'; const PORT_B = 3003; const HOST_B ='127.0.0.1'; const socket = dgram.createSocket('udp4'); var count = 0; setInterval(() => { count++; const data = Buffer.from(String(count)); socket.send(data, 0, data.length, PORT_A, HOST_A, (err, bytes) => { if (err) throw err; }); }, 500); socket.on('message', (message, remote) => { console.log(remote.address + ':' + remote.port +' - ' + message); }); socket.bind(PORT_B, HOST_B); これらを実行すると、一方から一定間隔で他方へカウントアップする文字列を送り、それを受信した側はエコーバックするという動作が確認できました。 2つのプログラム間での UDP通信! https://t.co/PHpipZ0fZs pic.twitter.com/2evFUu7Um8— you (@youtoy) December 5, 2021 そういえば、●● over UDP という形のものを使うことはよくあったけど、UDP を直接扱うようなことはこれまでなかったかも。 Node.js で TCP通信を実行する この後は、UDP通信を行うプログラムに手を入れて何かやろうかとも思ったのですが、ふと「TCP はどんな感じなんだろう?」という考えが頭をよぎりました。 そして、記事を検索したところ、以下のものが出てきました。 非常にシンプルなプログラムが掲載されていて、試してみるのにちょうど良さそうです。 ●Node.jsでTCP通信する - Re: note  https://hikoleaf.hatenablog.jp/entry/2019/06/09/131620 UDP に関する記事と同様に $ npm install net を実行する手順が書いてありますが、これは不要な感じが。 const net = require('net'); const server = net.createServer(socket => { socket.on('data', data => { console.log(data + ' from ' + socket.remoteAddress + ':' + socket.remotePort); socket.write('server -> Repeating: ' + data); }); socket.on('close', () => { console.log('client closed connection'); }); }).listen(3000); console.log('listening on port 3000'); const net = require('net'); const client = net.connect('3000', 'localhost', () => { console.log('connected to server'); client.write('Hello World!'); }); client.on('data', data => { console.log('client-> ' + data); client.destroy(); }); client.on('close', () => { console.log('client-> connection is closed'); }); そのまま動かしてみたら、以下のように通信が行えました。 1度データを送信すると、送信側は終了する挙動のようだったので、何度か送信をやってみています。 先ほどの例をそのまま動かしただけ、なのだけど、Node.js で TCP! pic.twitter.com/osz9wifMfQ— you (@youtoy) December 5, 2021 そういえば、●● over TCP という形のものを...(以降、省略) おわりに 既存のサンプルを動かしただけではありますが、プログラムをざっくり見てみると、Node.js ではかなりシンプルな実装で UDP や TCP の通信を扱えるんだな、というのを感じました。 たしか、「M5Stack系の UIFlow で、UDP を扱うブロックがあったな」とか思い浮かんだので、それとつないでみるとか、何かこれを使ったデバイス/アプリ/サービス間通信を試せたら、とか思っています。 たしかあったよな...、と思って確認したら、やはり!#UIFlow でソケット通信カテゴリと、その中の UDP。#M5Stack pic.twitter.com/c2fYvyB70D— you (@youtoy) December 5, 2021
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5.jsの関数まとめ part.4 preloadとloadImage()

この記事は Qiita p5js アドベントカレンダー5日目の記事です。 これはなに 書籍『Generative Design with p5.js』P_1_2_2_01に登場する関数について理解を深める記事です。 今回はpreload()とloadImage()。 preload() setupやdrawと同じ、p5での描画を組み立てる関数(Structure)です。 setupよりも前に実行されるのが特徴。 リファレンスより Called directly before setup(), the preload() function is used to handle asynchronous loading of external files in a blocking way. If a preload function is defined, setup() will wait until any load calls within have finished. Nothing besides load calls (loadImage, loadJSON, loadFont, loadStrings, etc.) should be inside the preload function. If asynchronous loading is preferred, the load methods can instead be called in setup() or anywhere else with the use of a callback parameter. setup()の直前に呼び出されるpreload()関数は、外部ファイルの非同期読み込みをブロック方式で処理するために使用されます。preload関数が定義されている場合、setup()は内部のloadコールが終了するまで待機します。ロードコール(loadImage、loadJSON、loadFont、loadStringsなど)以外のものは、プリロード関数内に入れるべきではありません。非同期のロードが必要な場合は、loadメソッドをsetup()の中やその他の場所でコールバックパラメータを使用して呼び出すことができます。(DeepL翻訳) preloadの中で呼ばれる関数は基本的にload系に限られているようですね。 この中でなんでもかんでもしていいよというわけではなさそうです。 Type preload(): void; structureに分類される関数なので返り値はとくになし。 loadImage() 画像を読み込むためのloadImageについても触れたい思います。 名前の通り、画像データを読み込むために使用される関数です。 リファレンスより The path to the image should be relative to the HTML file that links in your sketch. Loading an image from a URL or other remote location may be blocked due to your browser's built-in security. 画像のパスは、スケッチにリンクしているHTMLファイルからの相対パスでなければなりません。URLやその他の遠隔地からの画像の読み込みは、ブラウザの内蔵セキュリティによってブロックされることがあります。(DeepL翻訳) 相対パスで読み込む必要があるよ。ということくらいですかね。 上記は一部抜粋ですが、他には setup内でも呼べるけど読み込みが遅れたりするからpreload内で実行してね とか base64も読み込み可能だよ ということが書かれています。 ちなみにloadImage()はpreloadの中で呼ばれることが推奨されていますが、どこからでも呼べる関数です。 P_1_2_2_01では数字を入力するごとにloadImageを走らせる処理が書かれていました。 (ふと使ってる画像全てをpreload内で一度に呼び出した方が、画像変更時に遅延がなくなるのでは...と思いました。試してないけど。) Type loadImage(path: string, successCallback?: (p1: Image) => any, failureCallback?: (p1: Event) => any): Image; 引数 path 画像パス successCallback / failureCallbac 成功時・失敗時に実行できる関数を指定できるようです。 返り値はImageという型で、p5内で扱えるイメージオブジェクトのようです。 試してみる P_1_2_2_01のplaygroundにある画像を利用してみます。 preload内でloadImage()し、image()で画像を表示。 var img; function setImage(loadedImageFile) { img = loadedImageFile; } function preload() { // load完了後、setImageで画像をimgに格納する loadImage('data/pic1.jpg', setImage); } function setup() { createCanvas(600, 600); image(img, 0, 0) } image()の第一引数にImageを渡すだけで表示ができます。 第二、第三引数は画像の起点となるx, y座標です。 せっかくなのでload失敗時のEventを使う関数も作りましょう。 loadImageの第三引数には失敗時の関数、noticeErrorを渡しました。 またパスのdataをdaaにして読み込みに失敗させます。 const noticeError = (event) => { console.log(event) } function preload() { loadImage('daa/pic1.jpg', setImage, noticeError); } 画像の読み込みに失敗し、Eventの中身を取得することができました。 // console Event {isTrusted: true, type: "error", target: null, currentTarget: null, eventPhase: 0…} どんな時に使えそうか P_1_2_2_01は画像をバラバラに分解し、明度や彩度で色分けしたものを描画するというものです。 またそれがぐにゃぐにゃ動いたりしたら面白いんじゃないか・・といろいろ想像が働きます。 既存のものから無限に新しいものが作れるのは面白いですよね。 画像やそういったメディアを使うタイミングは多くあると思います。 下の画像と上で使用した画像は同じもの。 色分けをして並び替えをすることでグリッチかつピクセルアートのようにも見えますね!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホの加速度センサーを使ってWebでリアルタイム同期する歩数計を作る

スマートフォンには多くのセンサーが搭載されており、それらを活用した様々なアプリがあります。 今回は、加速度センサーを使って複数のスマートフォンで取得した歩数をリアルタイムにモニタリングできるようなWebアプリを作成しました。 参考にさせていただいた記事 3軸デジタル加速度センサーを使用したフル機能の歩数計の設計 ビデオチャットアプリを作る(WebRTC+Node.js+Socket.IO) 使用した技術 - node.js (express v.4.17.1) - socket.io (v.4.3.2) - chart.js (v.3.6.0) つくったもの Github: https://github.com/Ogijun2018/SyncroWalk スマートフォンの加速度センサーを用いて、ログインしている各デバイスの歩数の情報をデスクトップ画面で表示するWebアプリを作成しました。 Webで実装しているためiOS・Androidそれぞれでの実装の必要がなく、加速度センサーが搭載されていればどの端末でも使えるアプリになっています。 リアルタイムに歩いている様子が更新されるのを見ると、離れていても一緒に歩いている感が出て運動を頑張れるかな?と思い作成しました。 Webでリアルタイム更新できる歩数計を作るにあたって、実装のポイントは二つありました。 データ送信におけるリアルタイム性 歩数計の精度 データ送信におけるリアルタイム性 今回は「一緒に運動している感」を出すために、歩数が遅延なくリアルタイムに更新されるシステムを作ることを目標としました。 スマートフォンから取得できるデバイスの角度や加速度の値は毎秒数十回入ってくるため、後述するデータの平滑化を行ったとしても通常のクライアントサーバー方式のシステムでは無理があるだろうと思われます。 そのため、今回は低遅延でデータの送信ができるP2P方式のWebRTCを採用しました。 WebRTCというと映像や音声のストリームデータの送受信をするときに使用される技術な感じがしますが、もちろん独自データの送受信も行うことができます。 データの送受信は以下のような方法で行いました。 作成したブラウザにアクセス WebSocket (Socket.io) を利用し、シグナリングサーバとの接続を確立 サーバとの接続が確立されたら、生成されたidをトークンとしてクライアントに返す 画面表示(iOSの場合は、明示的にデバイスの加速度の取得許可ダイアログを設定) デバイスの傾き/加速度情報を取得し送信する クライアントから取得された傾き/加速度情報に、③で生成されたトークン情報を付与してサーバへ送信 サーバに上がった自分以外の全クライアントの情報を他のデバイスで取得 client.js let g_mapRtcPeerConnection = new Map(); const g_socket = io.connect(); const IAM = { token: null, }; // ② WebSocketを利用し、シグナリングサーバと接続 --------------------------------- g_socket.emit("join", {}); g_socket.on("connect", () => { console.log("Socket Event : connect"); }); // ③ server.jsから生成されたトークンを受け取る ----------------------------------- g_socket.on("token", (data) => { IAM.token = data.token; }); // ④ iOSの場合は明示的に加速度の取得許可ダイアログを設定 ---------------------------- // 参考: https://qiita.com/nakakaz11/items/a9be602874bd54819a18 function ClickRequestDeviceSensor() { DeviceOrientationEvent.requestPermission() .then(function (response) { if (response === "granted") { window.addEventListener("deviceorientation", deviceOrientation); $("#sensorrequest").css("display", "none"); } }) .catch(function (e) { console.log(e); }); DeviceMotionEvent.requestPermission() .then(function (response) { if (response === "granted") { window.addEventListener("devicemotion", deviceMotion); $("#sensorrequest").css("display", "none"); } }) .catch(function (e) { console.log(e); }); } if (window.DeviceOrientationEvent) { if ( DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === "function" ) { $("body").css("display", "none"); var banner = '<div id="sensorrequest" onclick="ClickRequestDeviceSensor();"> <p>センサー有効化</p> </div>'; $("body").prepend(banner); } else { window.addEventListener("deviceorientation", deviceOrientation); } } if (window.DeviceMotionEvent) { if ( DeviceMotionEvent.requestPermission && typeof DeviceMotionEvent.requestPermission === "function" ) { } else { window.addEventListener("devicemotion", deviceMotion); } } // ------------------------------------------------------------------------ // ⑤ デバイスの傾き/加速度情報を取得し送信 ------------------------------------- function deviceMotion(e) { e.preventDefault(); let ac = e.acceleration; deviceMotionData.x = ac.x; deviceMotionData.y = ac.y; deviceMotionData.z = ac.z; sendDeviceInfo(); } function deviceOrientation(e) { e.preventDefault(); deviceOrientationData.gamma = e.gamma; deviceOrientationData.beta​= e.beta; deviceOrientationData.alpha ​= e.alpha; sendDeviceInfo(); } // ------------------------------------------------------------------------ // ⑥ クライアントから取得された傾き/加速度情報に、 // ③で生成されたトークン情報を付与してサーバへ送信 ------------------------- function SendDeviceInfo() { // メッセージをDataChannelを通して相手に直接送信 g_mapRtcPeerConnection.forEach((rtcPeerConnection) => { rtcPeerConnection.datachannel.send( JSON.stringify({ type: "message", data: { deviceMotionData, deviceOrientationData }, from: IAM.token, }) ); }); } ... // ⑦ サーバに上がった自分以外の全クライアントの情報を他のデバイスで取得 function setupDataChannelEventHandler(rtcPeerConnection) { rtcPeerConnection.datachannel.onmessage = (event) => { let objData = JSON.parse(event.data); if ("message" === objData.type) { // 受け取ったデバイスの情報を表示 let element = getRemoteChatElement(objData.from); element.innerHTML = `加速度情報: ${objData.data.deviceMotionData}, 傾き情報: ${objData.data.deviceOrientationData}`; } ... }; } // -------------------------------------------------------------------------- ... server.js const express = require("express"); const http = require("http"); const socketIO = require("socket.io"); const app = express(); const server = http.Server(app); const io = socketIO(server); // ③ サーバとの接続が確立されたら、生成されたidをトークンとしてクライアントに返す ------- io.on("connection", (socket) => { const token = socket.id; io.to(socket.id).emit("token", { token: token }); ... }); // -------------------------------------------------------------------------- ... シグナリングサーバーによる自動シグナリングの処理や、チャットからの離脱、STUN Serverの対応をしている部分は省略しています。 詳しい実装は こちら(ビデオチャットアプリを作る(WebRTC+Node.js+Socket.IO))がとても参考になります。 歩数計の精度 上述の通り、スマートフォンを使用した歩数計はほとんどが加速度センサーを用いた実装になっています。 今回も同様に加速度センサーを用いた実装を行いますが、単純な加速度の上下を読み取るだけでは以下のような問題が起きてしまいます。 ほんの少しの揺れ/歩行とは違う動きでも一歩と認識されてしまう デバイスの向きは状況によって違うため、特定の軸の加速度変化を見ても反応しない時がある しきい値が高すぎると小さい一歩では反応しない これらを解決するために、こちら(3軸デジタル加速度センサーを使用したフル機能の歩数計の設計)の記事を参考にして歩数計アルゴリズムを作成し、歩数計の精度を高めました。 歩数計アルゴリズム 上図はスマートフォンをポケットに入れた状態で歩行した際の加速度変化のグラフです。 この図ではz軸において歩行の一歩を踏み出した時に大きく加速度が変化していることが確認できます。 ただし、例えばスマートフォンをバッグの中に入れている時、デバイスが地面に対して垂直に立っているような形だとz軸ではなくy軸が大きく変化するはずであり、必ずしも1つの軸の情報のみを見れば良いというわけではありません。 そのため、3軸すべてにおける加速度のピーク値検出と動的に閾値を決める必要があります。 平滑化 まずは、データの瞬間的な変化をなくすために平滑化を行います。 今回は4個のデータが入ってくるまでデータの更新を止め、4個目が入ってきた時にその平均を算出することで平滑化を行います。 動的しきい値 平滑化されたデータを基に、具体的にどこのラインを超えたら歩行していると判断するのかを決定します。 (図参考: 3軸デジタル加速度センサーを使用したフル機能の歩数計の設計) 3軸の加速度の最大値と最小値を50サンプルごとに継続的に更新し、その平均値を動的しきい値(THRESHOLD)とします。 次の50サンプルについてはその前に計算したしきい値を使用して歩行が行われているかを決定します。 そして、有効な一歩かどうかを判断するのには「現在の加速度」と「その一つ前に入力された加速度」との差を見ることで実現します。 歩行時に1歩を踏み出す動作は、加速度曲線が動的しきい値をまたいでおり、加速度の傾きが負であるとき と定義できます。 これらを実際に実装したコードが以下になります。 client.js let filterData = { x: 0, y: 0, z: 0 }; let axis_result = { x: 0, y: 0, z: 0 }; let axis_new = { x: 0, y: 0, z: 0 }; let axis_old = { x: 0, y: 0, z: 0 }; let deviceMotionData = { x: 0, y: 0, z: 0 }; // 動的しきい値のサンプル数 const THRESHOLD = 50; // 歩数 let stepCount = 0; let sampleCount = 0; let filterCount = 0; // 3軸それぞれの最小値に100, 最大値に-100を初期値で入れることで // データ入力後すぐに更新できるようにする let acXmin, acYmin, acZmin = 100; let acXmax, acYmax, acZmax = -100; // しきい値 let dcX, dcY, dcZ; // 加速度変化の最も大きい軸 let thresholdLevel = 0; function deviceMotion(e) { e.preventDefault(); let ac = e.acceleration; let acg = e.accelerationIncludingGravity; if (filterCount < 3) { // 3軸のデータそれぞれの4データ分の平均を計算し平滑化する filterCount++; filterData.x += ac.x; filterData.y += ac.y; filterData.z += ac.z; return; } else { filterCount = 0; axis_result = { x: filterData.x / 4, y: filterData.y / 4, z: filterData.z / 4, }; filterData = { x: 0, y: 0, z: 0 }; } // 3軸それぞれの加速度の最小値・最大値を計算する acXmin = Math.min(axis_result.x, acXmin); acYmin = Math.min(axis_result.y, acYmin); acZmin = Math.min(axis_result.z, acZmin); acXmax = Math.max(axis_result.x, acXmax); acYmax = Math.max(axis_result.y, acYmax); acZmax = Math.max(axis_result.z, acZmax); sampleCount++; // サンプル数が50を超えたらしきい値を変更する if (sampleCount > THRESHOLD) { sampleCount = 0; // 3軸の加速度の最小値・最大値からしきい値を決定する dcX = (acXmax - acXmin) / 2; dcY = (acYmax - acYmin) / 2; dcZ = (acZmax - acZmin) / 2; // 初期化 acXmax = acYmax = acZmax = -100; acXmin = acYmin = acZmin = 100; } // 現在の加速度のベクトルを計算 let resultVector = Math.sqrt( Math.pow(axis_result.x, 2) + Math.pow(axis_result.y, 2) + Math.pow(axis_result.z, 2) ); // 動的精度はとりあえず1.0の固定値とする if (resultVector > 1.0) { // 加速度変化が1.0以上あった場合、axis_oldとaxis_newを更新する axis_old = { x: axis_new.x, y: axis_new.y, z: axis_new.z }; axis_new = { x: axis_result.x, y: axis_result.y, z: axis_result.z }; } else { // そうでない場合はaxis_oldのみを更新する axis_old = { x: axis_new.x, y: axis_new.y, z: axis_new.z }; return; } // 3軸の中でどれが一番加速度変化が大きかったかで条件分岐 let abs_x_change = Math.abs(axis_result.x); let abs_y_change = Math.abs(axis_result.y); let abs_z_change = Math.abs(axis_result.z); if (abs_x_change > abs_y_change && abs_x_change > abs_z_change) { // X軸が一番大きかった場合 thresholdLevel = dcX; if (axis_old.x > thresholdLevel && thresholdLevel > axis_new.x) { // 加速度の傾きが負 かつ しきい値を跨いでいる場合は1歩と判定 stepCount++; document.getElementById("step").innerHTML = stepCount; SendDeviceInfo(); } } else if (abs_y_change > abs_x_change && abs_y_change > abs_z_change) { thresholdLevel = dcY; // Y軸が一番大きかった場合 if (axis_old.y > thresholdLevel && thresholdLevel > axis_new.y) { stepCount++; document.getElementById("step").innerHTML = stepCount; SendDeviceInfo(); } } else if (abs_z_change > abs_x_change && abs_z_change > abs_y_change) { thresholdLevel = dcZ; // Z軸が一番大きかった場合 if (axis_old.z > thresholdLevel && thresholdLevel > axis_new.z) { stepCount++; document.getElementById("step").innerHTML = stepCount; SendDeviceInfo(); } } } 感度が高すぎる場合は、if (resultVector > 1.0) の部分を動的に設定・固定値を高くしたり、時間窓を設定して特定の間隔での加速度変化のみを歩行と判断する(遅すぎる・速すぎる加速度変化を除外する)ことでより精度をあげることができます。 まとめ 歩数計のアルゴリズムは調べてもごく単純な実装だったり、なかなか情報がでてこず自分で実装してみましたが、意外と歩行を検知するだけでも大変なんだなぁという感想です。 実際に使用してみると、手でデバイスを持っている場合や前に進む歩行をしている場合はしっかりと反応していましたが、足踏みではなかなか反応しませんでした。 足踏みだと加速度変化が歩行に比べて小さくなってしまい、反応できないみたいですね。 ジャイロセンサーを使用して試してみましたが、今度は反応が良すぎるなどの問題が発生したので、さまざまなシチュエーションでの歩行に対応できるようにすることは今後の課題です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む