20210505のJavaScriptに関する記事は24件です。

Leafletで、GPSログ(GPX)を地図に表示する。Google マップ、OpenStreetMapなど。

Leafletで、GPSログ(GPX)を地図に表示を試した。 詳細は、以下。 https://2ndart.hatenablog.com/entry/2021/05/05/232759
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6でStripe Checkoutを実装 (開発環境/TESTモード)

背景 Stripe決済を初めて実装する機会があったので、備忘録がてらまとめる。 もし、Rails6でStripe決済を実装することになった駆け出しエンジニアがいたら是非参考にしてもらえると嬉しいです。 また、学習3~4ヶ月目のぺーぺーが書いた記事なので、理解が間違っている、リファクタリングなどあればなんなりと指摘していただけると幸いです。 Stripeには複数の決済手段があるが、今回は一番実装が簡単なCheckoutを実装した。 Checkoutとは?: Stripeが用意してくれている決済画面に遷移して決済を行う手段である。 実装内容 Stripe Checkout で決済機能を実装 決済完了時にユーザー情報を登録 支払完了後はWEBアプリに戻り、決済完了画面を表示 決済せずにStripe上で「戻る」をしたときに支払キャンセル画面を表示 Stripe側での設定 1. まずはStripeのサイトでアカウント作成 以下のリンクからアカウントを作成しましょう! Stripe公式サイト 2. ダッシュボード上の商品タブをクリックし、+ テスト商品を追加をクリック 3. 商品情報を入力 今回は一括払い、1,000円の商品を作成 4. 以下の4つのキーを使用 商品ページ内 詳細のID: prod_◯◯◯◯◯◯ 料金のAPI ID:price_◯◯◯◯◯◯◯◯◯◯ 開発者ページ内APIキーの標準キー 公開可能キー: pk_test_◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯…◯◯◯ シークレットキー: sk_test_◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯◯…◯◯◯ *第三者に漏洩しないようにご注意ください Rails側の手順 ■前提 deviseや必要なデータベースは作成済み。 今回、必要なデータベースは以下の感じ。 ユーザーテーブル: name, email, password, customer_id, plan_id + デフォルト プランテーブル: id, plan_name + デフォルト もしテーブルの作り方がわからない場合は、簡単にまとめてあるので以下を御覧ください! 参考:マイグレーションファイルの基本 ■Gemのインストール Gemfile gem 'stripe' # Stripeを実装するためのgem gem 'dotenv-rails' # Stripeの重要な情報を記述するためのgem bundle installして準備完了 実装 少し長いので、コントローラを機能2つに分けて解説します。 ■決済画面への遷移~決済完了ページ or 決済キャンセルページの実装 payments_controller.rb class PaymentsController < ApplicationController  #決済後の画面遷移 def new @session = Stripe::Checkout::Session.create({ payment_method_types: ['card'], line_items: [{ price: 'price_XXXXXXXXXXX', # 料金API-IDを記述 quantity: 1, }], mode: 'payment', success_url: request.base_url + '/payments/after_payment_register?session_id={CHECKOUT_SESSION_ID}', # 決済成功後の遷移先 cancel_url: request.base_url + '/payments/payment_cancel', #決済キャンセルした際の遷移先 }) end #決済前のキャンセルのアクション def payment_cancel end end Stripeでは、Railsに実装する際はStripe::Checkout::Session.◯◯◯のような形でセッションオブジェクトを使用してデータを取得します。 参考:Stripe API reference (Create) /payments/new.html.erb <script src="https://js.stripe.com/v3"></script> <script> var stripe = Stripe('pk_test_XXXXXXXXXXXXXX'); // ()内には公開可能キーを記述 stripe.redirectToCheckout({ sessionId: '<%= @session.id %>' }).then(function (result) { }); </script> 上記ではnewアクションに対するビューの設定です。つまり、決済画面を作っています。 newアクションで作ったインスタンス変数@session(料金、数量等の情報)を決済ページに持っていってくれます。 今回のケースだと payments/new というパスで決済ページにアクセスできます。 ← 戻るの箇所には元々はショップ名が入ります。 カーソルを持っていくと← 戻るに変わり、クリックするとキャンセル扱いとなり画面遷移します。 ダミーカード番号 4242 4242 4242 4242と その他の情報を適当に入力して「支払う」ボタンを押すと決済完了画面に遷移するはずです! ■決済完了後、データベースにユーザー情報を登録 まず、Stripeの決済は色々な情報を保有しています。欲しいデータが何なのか、事前に整理しておきましょう! どのようなデータが取れるのかは以下を参考にしてください。 参考:Stripe API reference 今回、ユーザー情報としてStripeから引っ張りたかったのは以下の通り。 -顧客ID(customer_id) -メールアドレス(email) -商品ID(product_id) payments_controller.rb class PaymentsController < ApplicationController before_action :user_plan_judgement, except: [:new, :payment_cancel] INITIAL_USER_NAME = "新規ユーザー" STRIPE_PRODUCT_ID = "prod_XXXXXX" # 商品IDを記述 #(中略) # 初期パスワードを乱数生成、初期ユーザー名を新規ユーザー、customer_id, email, plan_idをユーザーデータテーブルに登録 def after_payment_register generated_initial_password = Devise.friendly_token.first(8) stripe_user_data = Stripe::Checkout::Session.retrieve(params[:session_id]) @user = User.new( customer_id: stripe_user_data.customer, name: INITIAL_USER_NAME, email: stripe_user_data.customer_details["email"], password: generated_initial_password, plan_id: user_plan_judgement ) @user.save send_notification_email end private # 1.プランIDを取得 def user_plan_judgement stripe_plan_data = Stripe::Checkout::Session.list_line_items(params[:session_id]) if stripe_plan_data[:data][0][:price]["product"] == STRIPE_PRODUCT_ID plan_id = 1 end end end ざっくりですが、以下のことを実行しています。 1. ユーザーが決済したプランはなにかを判定 2. 1.を含めたユーザー情報を登録 1. ユーザーが決済したプランはなにかを判定 Stripe::Checkout::Session.list_line_itemsでStripeの商品IDと引っ張ってきたデータのproductデータが一致していれば plan_idに1を代入するという処理にしました。 現状あまり意味はないのですが、 プランの料金が変わったときでも同一のプランであることとして管理するために設定しました。 参考:Stripe API reference (line_items) 2. 1.を含めたユーザー情報を登録 Devise.friendly_token.first(8)でランダムな8桁のパスワードを設定 Stripe::Checkout::Session.retrieveでレトリーブしてきてくれます。 参考:Stripe API reference (retrieve) かわいいレトリーバー犬はこのretrieveから来ているらしいです。 狩った獲物をとってくる(回収してくる)意味らしいです。 (今回のPJの恩師から教えてもらいました。) 参考:語源由来辞典 なので、欲しいデータをretrieve(回収)してきてもらうわけです。 retrieveしてもらった獲物(データ)を獲物箱(=変数)へ代入します。 あとは見慣れたUser.newとuser.saveでユーザーデータを保存してユーザー登録も完了! その他、ビューを自分のお好みで整えて完成! ■完成形 コントローラ payments_controller.rb class PaymentsController < ApplicationController before_action :user_plan_judgement, except: [:new, :payment_cancel] INITIAL_USER_NAME = "新規ユーザー" STRIPE_PRODUCT_ID = "prod_XXXXXX" # 商品IDを記述 def new @session = Stripe::Checkout::Session.create({ payment_method_types: ['card'], line_items: [{ price: 'price_XXXXXXXXXXX', # 料金API-IDを記述 quantity: 1, }], mode: 'payment', success_url: request.base_url + '/payments/after_payment_register?session_id={CHECKOUT_SESSION_ID}', cancel_url: request.base_url + '/payments/payment_cancel', }) end def payment_cancel end def after_payment_register generated_initial_password = Devise.friendly_token.first(8) stripe_user_data = Stripe::Checkout::Session.retrieve(params[:session_id]) @user = User.new( customer_id: stripe_user_data.customer, name: INITIAL_USER_NAME, email: stripe_user_data.customer_details["email"], password: generated_initial_password, plan_id: user_plan_judgement ) @user.save send_notification_email end private def user_plan_judgement stripe_plan_data = Stripe::Checkout::Session.list_line_items(params[:session_id]) if stripe_plan_data[:data][0][:price]["product"] == STRIPE_PRODUCT_ID plan_id = 1 end end end ビュー /payments/new.html.erb <script src="https://js.stripe.com/v3"></script> <script> var stripe = Stripe('pk_test_XXXXXXXXXXXXXX'); // ()内には公開可能キーを記述 stripe.redirectToCheckout({ sessionId: '<%= @session.id %>' }).then(function (result) { }); </script> after_payment_register.html.erb  <p>決済してユーザー登録したぞ</p> <p>決済後に表示されるページだよ</p> <p>必要に応じてhtmlで文章記述</p> payment_cancel.html.erb <p>キャンセルされたときに表示されるページだよ</p> その他(ルーティング、env) routes.rb (中略) resources :payments, only: [:new] get '/payments/after_payment_register', controller: 'payments', action: 'after_payment_register' get '/payments/payment_cancel', controller: 'payments', action: 'payment_cancel' (中略) .env *ここはGithubにpushされないファイルなので、隠したい定数はここに書きます。 //以下はシークレットキー STRIPE_TEST_SECRET_KEY = sk_test_XXXXXXXXXXXXXXXXXX 終わりに 長くなりましたが、ご覧いただきありがとうございました! 今回、初めてStripe決済を実装する機会をいただくことができ、自分で実装した中で理解したことをまとめてみました。 Stripe::Checkout::Session.◯◯◯で自在にデータを取得できるのは本当に便利だなあと実感しました。 また、MVC、データの取得~保存、データベースの構築まで自分でやれたのでデータ関連の理解が深まったと感じます。 次回は本番環境の実装についてまとめられたらまとめてみようと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

checkboxでいい感じに、絞り込み検索してみた(Javascript備忘録)

はじめに ransackで、全件一覧表示を消してみた(備忘録)でも紹介していましたが、ransackというgemとJavaScriptを用いて、検索機能を実装した際に(勘違いして)いろいろ試したので、備忘録として残します。 【注意!】 ※JavaScriptの知識は、Progate1周程度。 ※ビュー&コードは、一部加工しています。(そのままコピペでは、動きません。) 環境 Rails 6.1.2 jquery 3.6.0 完成イメージ ・一覧テーブルがあり、2つのチェックボックスをクリックすると、絞り込み表示される  (データは、CSVファイルにあり。ransackで一覧表示&キーワード検索を実装。そこからチェックボックスで絞り込み表示させる) ・一覧ページ ・②をチェック後の一覧ページ 勘違いしたところ ①Railsで定義した変数をJavaScriptへ渡す データはCSVファイルに入っていて、ransack実装済みなのに 以下の内容を試していて、時間がかかる。 ・ransackで、全件一覧表示されているビューを消す ・gonを導入(Railsで定義した変数をJavaScriptへ渡すことができるGem) ・gonを用いて、実装 ②ransack検索とは別に、新たにJavaScriptでCSVファイルを読み込む ・JavaScriptで、CSVデータを読むこむ方法を調べる 大きくこの2つで、かなりの時間を費やしました アドバイス頂いた内容 行き詰まったので、メンターさんに度々アドバイスいただきました! このアドバイスのおかげで、軌道修正できました。 ①チェックボックスをクリックした際にコンソールに出力 ②チェックの入っている値を取得 ③チェックの入っている値のみ表示するようにする関数を作る ③が上手く実装できなかったので、さらに詳細なアドバイスを頂きました。 ④tableタグの中のtrタグに各行のデータが入っている。trタグを全て取得し,それぞれについて,値部分を取得して配列に含まれているかどうかを判定して表示・非表示を対応する trタグの中にデータがあるんだ!!と感動しました。(かなり知識浅く、すみません。) 苦戦したところ ③、④のtrタグの値を取得して、そこからの処理がどうすればよいのか?かなり苦戦しました。 (いろんなJavaScriptの記事を読みましたが、コードの理解が出来ず、1つ1つググりながらの実装だったので時間がかかりました。) 実装のポイント ①デフォルトで、チェックボックスに☑させる <label><input class="checkbox" type="checkbox" name="〇〇" value="①" checked> ①</label> checkedを入れることにより、デフォルトで☑できます。 ②チェックボックスをクリックすると処理が開始 $(".js-filter-form :checkbox").on("click", function(){ } ③チェックボックスに☑した値を配列に追加 //空の配列を作成 var checked = []; $(".js-filter-form :checked").each(function(){ // チェックボックスにチェックした値を配列に追加 checked.push($(this).val()); ④全てのtrを取得し処理 $('#list tbody tr').each(function(){ // html要素を取得 var txt = $(this).find("td:eq(0)").html(); // ①と②にチェックが入ると表示、チェックを外すと非表示できるよう条件分岐 if(txt.match(checked[0]) || (checked[1])){      //表示 $(this).show(); }else{      //非表示 $(this).hide(); } }); 最終コード(一部編集しております。Bootstrapは外しています。) index.html.erb <div> <h1>タイトル</h1>  <form class="js-filter-form :checkbox"> <label><input class="checkbox" type="checkbox" name="〇〇" value="①" checked> ①</label> <label><input class="checkbox" type="checkbox" name="〇〇" value="②" checked> ②</label> </form> <%= search_form_for @q do |f| %>    <%= f.label :title_cont, "タイトル" %> <%= f.search_field :title_cont %>    <%= f.label :content_cont, "キーワード" %>    <%= f.search_field :content_cont %> <%= f.submit "検索" %> <% end %> <div> <body> <div> <table id="list"> <thead> <tr> <th>ジャンル</th> <th>タイトル</th> </tr> </thead> <tbody> <% Railsの変数(複数形).each do |単数形| %> <tr> <td><%= 単数形.①②を示すジャンル %></td> <td><%= 単数形.タイトル %></td> </tr> <% end %> </tbody> </table> </div> </div> <script> $(".js-filter-form :checkbox").on("click", function(){ var checked = []; $(".js-filter-form :checked").each(function(){ // チェックボックスにチェックした値を配列に追加 checked.push($(this).val()); }); // 全てのtrを取得 $('#list tbody tr').each(function(){ var txt = $(this).find("td:eq(0)").html(); // ①と②にチェックが入ると表示、チェックを外すと非表示できるよう条件分岐 if(txt.match(checked[0]) || (checked[1])){ $(this).show(); }else{ $(this).hide(); } }); }); </script> </body> まとめ 実際に実装すると、想像以上に難しく感じました。 しかし、JavaScriptを学習できて、楽しかったです!(いろいろアプリ作ってみたいです。) 今回の反省点は、3つ。 ①「質問しないこと=自走力」ではないので、「何日も悩んでいることは、質問する」こと ②時間は有限なので、時間を区切ってスピード意識してすすめること ③メンターさんの時間を奪わないように、質問内容をはっきりすること 度々、質問に対応して頂いたメンターさんに感謝です。 コードについて、アドバイス等ございましたらコメント頂けると幸いです。 参考文献 jQueryで簡易的な検索機能(フィルター機能)を作る方法 jQueryAPI ドキュメント jQueryのhtml()で追加・取得・書き換えの方法まとめ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの繰り返し処理for文

JavaScriptの繰り返し処理for文について <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>繰り返し処理</title> </head> <body> <script src="js/loop.js"></script> </body> </html> for (var i = 1; i <=3; i++){ console.log(i); } 結果 まず変数iに1が代入され、console.logが実行されます。変数iは1なので1と実行されます。 次に増減式のi++が実行されます。この時にiに1が足されるので2になります。 次にループ継続条件式が実行されます。この時にiは2なので<=3が成り立ちそのまま継続されます。 これを繰り返して、4回目になると4<=3となり条件式が成り立たなくなるのでここで終了となります。 結果出力は1、2、3となります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで自動計算

自動計算機能の実装 自分用のメモです〜 正解かどうかは分かりません!! が、ひとまず動いたので残しておきます。 function calc (){ const itemPrice = document.getElementById("item-price"); itemPrice.addEventListener("keyup",()=>{ //取得した要素が入力されたら(keyup)イベント発火 tax = parseInt(itemPrice.value * 0.1); const addTax = document.getElementById("add-tax-price"); addTax.innerHTML = `${tax}円` //taxはアイテムの値段.valudの10%を小数点以下切り捨て //表示させる要素を取得して、innnerHTMLで上書き prof = (itemPrice.value) - (tax) const priceContent = document.getElementById("profit"); priceContent.innerHTML = prof }) } window.addEventListener('load',calc); めちゃくちゃ頭抱えて悩んでたけど、めちゃくちゃ基礎でした。。 精進します。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS 配列関連メモ

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

【入門】Vue.jsを試してみた

Vue.jsとは JavaScriptのフレームワークの1つで、効率よくフロントエンドの開発が行える。 学習コストが低い。 CDNでjsファイルを読み込むだけで使用することができる。 シンプルにコードを書くことができる。 DOM操作を自動に行う HTMLの要素とJSの値やイベントとの紐付けを自動で行ってくれる。 jQueryよりも容易にコーディングできる。 Hello World! まず、Vue.jsで「Hello World!」を表示してみましょう。 CDNでVueを読み込む <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"> Vueインスタンスの生成 new Vue({}) el: HTML側のid属性との対応づけをする。 data: 表示するデータなどを定義する。 index.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>Vue.js入門</title> </head> <body> <div id="app"> {{ message }} </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { message: "Hello World!" } }); </script> </body> </html> ディレクティブ ディレクティブとは、接頭辞v-が付いたVue.jsに用意された特別な属性のことである。 もっと詳しく言うと、DOM要素に対して何か実行するものをライブラリに伝達する。 Vue.jsではこのディレクティブで始まりディレクティブで終わるとも言うくらい重要な概念となる。 ※以下からソースコードはbody部のみを記載する。 v-bind HTML側のある要素の属性を動的に設定することができる。 index.html <body> <div id="app"> <p v-bind:class="error_class">エラーが発生しました。</p> <!- 省略記法 --> <p :class="error_class">エラーが発生しました。</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { error_class: "error" } }); </script> </body> v-on イベントリスナーを定義することで、Vueインスタンスのメソッドを呼び出すことができる。 index.html <body> <div id="app"> <p>{{ now }}</p> <button v-on:click="time">現在時刻を表示</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { now: "00:00:00" }, methods: { time: function() { var date = new Date(); this.now = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); } } }); </script> </body> v-if 条件分岐によって、要素の有無の切り替えができる。 isErrorがtrueならば、エラーが発生しました。が画面に表示される。 isErrorがfalseならば、エラーが発生しました。が画面に表示されない。 index.html <body> <div id="app"> <p v-if="isError">エラーが発生しました。</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var error = new Vue({ el: "#app", data: { isError: true } }); </script> </body> v-once Vue.jsによるレンダリング(画面描画)を一度のみ行うようにする。 Vueインスタンスの変数にアクセスするときはthis.変数名と記述する。 index.html <body> <div id="app"> <p v-once>{{ count }}回クリックされました</p> <button v-on:click="counter">カウントボタン</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue ({ el: '#app', data: { count: 0 }, methods: { counter: function(){ this.count++ } } }) </script> </body> v-for 配列であるデータの各要素を制御することができる。 拡張for文のような記法をv-forに記述する。 v-forを使用する場合は、v-bindのkeyプロパティの指定が必須である。 keyを指定しない場合は配列のindexがkey値に使用されるが、key として使用すべきではない。 → 配列内の要素の追加・削除や順序の変更が行われるとき、index値も変化してしまう。 index.html <body> <div id="app"> <button v-on:click="shuffle">シャッフル</button> <ul> <li v-for="prefecture in prefectures" :key="prefecture.id"> {{ prefecture.name }} </li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", data: { prefectures: [ { id:1, name: "福岡県" }, { id:2, name: "佐賀県" }, { id:3, name: "長崎県" }, { id:4, name: "大分県" }, { id:5, name: "熊本県" }, { id:6, name: "宮崎県" }, { id:7, name: "鹿児島県" } ] } }); </script> </body> v-model 入力情報とデータの双方向データバインディングを簡単に行うことができる。 もっと詳しく言うと、テキストボックスの値(value属性値)が変更されると、dataで持っているtextの値に自動的に反映される仕組みがある。 v-modelは属性の初期値を無視するという特徴があるため、dataに初期値を持たせるかカスタムディレクティブで独自に実装する必要がある。 index.html <body> <div id="app"> <p>{{ text }}</p> <input v-model="text"> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue ({ el: '#app', data: { text: '' } }) </script> </body> 算出プロパティ 算出プロパティとは、関数で算出したデータを返すことができるプロパティのこと。 conputedをvueインスタンスに記述する。 index.html <body> <div id="app"> {{ fullName }} </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: '#app', data: { firstName: 'Nobi', lastName: 'Nobita' }, computed: { fullName: function() { return this.firstName + ' ' + this.lastName } } }) </script> </body> 上記の例はmethodsでも同様なことができる。 index.html <body> <div id="app"> {{ getFullName() }} </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: '#app', data: { firstName: 'Nobi', lastName: 'Nobita' }, methods: { getFullName: function() { return this.firstName + ' ' + this.lastName } } }) </script> </body> conputed VS methods conputed methods 記法(呼び出し) () 不要 () 必要 キャッシュ される されない conputed 引数を伴う処理ができない。 キャッシュ機能あり(=指定されているデータに変化があれば、再実行される) methods 引数を使った処理が可能である。 キャッシュ機能なし(=毎度毎度再実行される) index.html <body> <div id="app"> // conputed <ol> <li>{{ randomNum }}</li> <li>{{ randomNum }}</li> <li>{{ randomNum }}</li> </ol> // methods <ol> <li> {{ getRandomNum() }}</li> <li> {{ getRandomNum() }}</li> <li> {{ getRandomNum() }}</li> </ol> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> <script> var app = new Vue({ el: "#app", computed: { randomNum: function () { console.log("computed"); return Math.random(); } }, methods: { getRandomNum: function () { console.log("methods"); return Math.random(); } } }); </script> </body> computedでは全て同じ値になるが、methodsは全て異なる値となる。 // computed 0.5562891327999486 0.5562891327999486 0.5562891327999486 // methods 0.10127439472454514 0.530424483259178 0.4696634887357811 computedではコンソールログには1度しかログが出ない。(1回しか呼ばれていない) computedはリアクティブデータ(dataオプションに設定されているデータ)が更新されない限り、実行されない!! 参考 https://jp.vuejs.org/ https://qiita.com/okerra/items/cda378436ed060e83a8c
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebGLのattributeで行列(mat3, mat4)を使用する

この記事では、WebGLのattributeで行列(mat3, mat4)を使用する方法について解説します。 具体的な使用例として、拡張機能のGeometry Instancingを使用しているときに個々のインスタンスの移動・回転・スケールを表す行列をattributeとして使用する場面を考えます。 サンプルでは、トーラスをGeometry Instancingで大量に描画し、各トーラスの移動・回転・スケールを行列形式のattributeでシェーダーに渡しています。 動くサンプルとソースコードは以下に置いてあります。 aadebdeb/SampleWebGlMatAttribute: Sample of matrix attributes in WebGL このサンプルでは、数学ライブラリとしてglMatrixを使用しています。 今回使用する頂点シェーダーは以下のようになります。インスタンスごとのモデル行列aInstanceMatrixと法線をオブジェクト座標からワールド座標に変換する行列aInstanceNormalMatrixが行列形式のattributeになります。 attribute vec3 aPosition; attribute vec3 aNormal; attribute mat4 aInstanceMatrix; // インスタンスのモデル行列 attribute mat3 aInstanceNormalMatrix; // インスタンスの法線モデル行列 attribute vec3 aInstanceColor; // インスタンスの色 uniform mat4 uViewProjectionMatrix; varying vec3 vColor; varying vec3 vWorldNormal; void main() { gl_Position = uViewProjectionMatrix * aInstanceMatrix * vec4(aPosition, 1.0); vColor = aInstanceColor; vWorldNormal = aInstanceNormalMatrix * aNormal; } JavaScript側では、最初にデータの生成を行います。ランダムに移動・回転・スケールを決定した行列(instanceMat)をインスタンスごとに生成して、その行列を一つの配列(instanceMatArray)にまとめます。その配列をもとにVBOを作成します。 const instanceMatArray = []; const instanceNormalMatArray = []; for (let i = 0; i < torusNum; ++i) { const instanceMat = mat4.create(); // Float32Array const rotation = quat.create(); quat.fromEuler(rotation, 360 * Math.random(), 360 * Math.random(), 360 * Math.random()); const translation = vec3.fromValues(50.0 * (2.0 * Math.random() - 1.0), 50.0 * (2.0 * Math.random() - 1.0), 50.0 * (2.0 * Math.random() - 1.0)); const scale = vec3.fromValues(0.5 + 1.5 * Math.random(), 0.5 + 1.5 * Math.random(), 0.5 + 1.5 * Math.random()); mat4.fromRotationTranslationScale(instanceMat, rotation, translation, scale); instanceMatArray.push(...instanceMat); const instanceNormalMat = mat3.create(); // Float32Array、法線モデル行列はモデル行列の逆行列の転置長列 mat3.fromMat4(instanceNormalMat, instanceMat); mat3.transpose(instanceNormalMat, instanceNormalMat); mat3.invert(instanceNormalMat, instanceNormalMat); instanceNormalMatArray.push(...instanceNormalMat); } const instanceMatVbo = createVbo(gl, new Float32Array(instanceMatArray)); const instanceNormalMatVbo = createVbo(gl, new Float32Array(instanceNormalMatArray)); 作成したVBOをattributeとして設定していきます。行列のattributeの場合は行列の各列ごとに設定を行います。getAttribLocationで取得したインデックスが行列の1列目用、その次のインデックスが2列目用、3つ目が3列目用...というようになります。さきほど生成したVBOの配列に合わせてvertexAttribPointerのstride引数とoffset引数を適切に設定します。 ここではaInstanceMatrixがmat4なので4列分、aInstanceNormalMatrixがmat3なので3列分の設定を行っています。 const extInstancedArray = gl.getExtension('ANGLE_instanced_arrays') function getAttribLocs(gl, program, names) { const locs = new Map(); names.forEach(name => locs.set(name, gl.getAttribLocation(program, name))); return locs; } const attribLocs = getAttribLocs(gl, program, ['aPosition', 'aNormal', 'aInstanceMatrix', 'aInstanceNormalMatrix', 'aInstanceColor']); gl.bindBuffer(gl.ARRAY_BUFFER, instanceMatVbo); gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix')); gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 1); gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 2); gl.enableVertexAttribArray(attribLocs.get('aInstanceMatrix') + 3); gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix'), 4, gl.FLOAT, false, 64, 0); // 64 = 16 * 4byte gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 1, 4, gl.FLOAT, false, 64, 16); gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 2, 4, gl.FLOAT, false, 64, 32); gl.vertexAttribPointer(attribLocs.get('aInstanceMatrix') + 3, 4, gl.FLOAT, false, 64, 48); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix'), 1); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 1, 1); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 2, 1); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceMatrix') + 3, 1); gl.bindBuffer(gl.ARRAY_BUFFER, instanceNormalMatVbo); gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix')); gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix') + 1); gl.enableVertexAttribArray(attribLocs.get('aInstanceNormalMatrix') + 2); gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix'), 3, gl.FLOAT, false, 36, 0); // 36 = 9 * 4byte gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix') + 1, 3, gl.FLOAT, false, 48, 12); gl.vertexAttribPointer(attribLocs.get('aInstanceNormalMatrix') + 2, 3, gl.FLOAT, false, 48, 24); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix'), 1); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix') + 1, 1); extInstancedArray.vertexAttribDivisorANGLE(attribLocs.get('aInstanceNormalMatrix') + 2, 1); 準備が終わったら、drawElementsInstancedANGLEで描画を実行します。 extInstancedArray.drawElementsInstancedANGLE(gl.TRIANGLES, torus.indices.length, gl.UNSIGNED_SHORT, 0, torusNum); この記事ではmat3、mat4のattributeを使用する方法について記述しました。 今回は使用しませんでしたが、mat2のattributeでも同様の方法が利用できると思います。 参考 javascript - WebGL: How to bind values to a mat4 attribute? - Stack Overflow LearnOpenGL - Instancing
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ネタ]復活の呪文を解析してみたよ

ゴールデンウィーク(2021-05)に、お勉強を兼ねて React + TypeScript で「ふっかつのじゅもん」の作成ページをGithub Pages 上に作りました(1の方です)。 じつは大昔に、復活の呪文の解析をした日記/作成できる CGI を公開していたことがあります。 (現在は、サイトが消滅して Internet Archive の中だけに存在する) その時のプログラム(+α)をもとに実装しました。 ついでなので、大昔に書いた「復活の呪文の解析日記」を少しリライトしたものも置いておきます。 ちなみに、ROMの解析などはしておらず、普通に遊んでいる範囲の情報をあつめて帰納的に推測したものです。 プログラムの不具合や実装の詳細などを挙動から推測するようなことは、エンジニアであればだれでも多少は行うことがあると思うので、何かの参考にはなるかと思います(かなり強引に関連付けてみた)。 日記に興味ない人は、ぐぐっと下にスクロールすると最後のあたりに復活の呪文の作成ページ(とソース)へのリンクがあります。 復活の呪文解析日記 【1997/06/08】呪文の解析 ふと思い立って、復活の呪文の解析を始めました。 とりあず、名前は「あ   」で始めて、何も持っていない状態で、復活の呪文を聞いてみました。 同じ状態でも何種類かの復活の呪文を聞くことが出来るのですが、実際にやってみると8種類までは聞くことが出来ました。 おさべつに はほわげげだど べうきさそ さには かこぶちな のへろぐぐぶい かこせつに つへむ けせいなの へごべううつに はほめよれ よごぜ こちおねふ みずいかかすち なのへむゆ むわげ ゆるへたと ねふれぎぎぜづ びあおけす けとね よわみてぬ ひまがごごぼえ くしたとね とまも るげやぬひ まじあおおとね ふみやりわ りじだ れぐもには ほざぼええさそ てぬひまも まれぎ 数字もキリがよいので、これで全部だろうと思って、解析を始めます。 まず気が付くのは、どの呪文も9文字目と 10 文字目が同じ文字だということです。 それに、 ある文字とその次の文字が4文字だけ離れている場合が多いということでしょうか。 それもある呪文で4文字だけ離れていると、他の呪文でも同じ場所は4文字だけ離れていることが多い。 同様に7文字離れていると、他の呪文の同じ場所でも7文字離れていることがほとんどのようです。 どうやら、文字と文字の差分にこそ情報が隠れているようですね。 しかし、ここで問題が一つ。 差分を抜き出すには文字コードが分からないといけないのですが、この場合はどうなっているんでしょう? まあ、差分から逆に求めればなんとかなりそうな気がするんですが……。 【1997/07/08】その2 名前は「あ   」で、持ち物はたいまつを一つだけの状態で呪文を聞いています。 おさべざぞ でぶしたたまも らろぐじだ じせつ かこぶごぜ づびさそそには はめよれぎ れおけ けせいぜづ びちまももれぎ ざぞでぶい ぶのへ こちおぢば ぼとめよよぜづ びあおけす けやり ゆるへげず ぢばこせせへむ ゆるがごぜ ごした よわみじだ どべすちちねふ みやりわげ わきさ るげやだど べてむゆゆわげ ずぢばぼえ ぼひま れぐもぞで ぶつみややじだ どべうきさ きめよ 【1997/07/09】コード表(勘) 復活の呪文で使われている 64 文字のコード表は、おそらく次の表のような感じでしょうか(呪文の入力画面からの推測)。 +0 +1 +2 +3 +4 +00 あ い う え お +05 か き く け こ +10 さ し す せ そ +15 た ち つ て と +20 な に ぬ ね の +25 は ひ ふ へ ほ +30 ま み む め も +35 や ゆ よ +38 ら り る れ ろ +43 わ +44 が ぎ ぐ げ ご +49 ざ じ ず ぜ ぞ +54 だ ぢ づ で ど +59 ば び ぶ べ ぼ これをもとに、前回・前々回の復活の呪文の差分を取ってみると……。 まずは、名前「あ   」アイテム無し 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 文字 け せ い な の - へ ご べ う う つ に - は ほ め よ れ - よ ご ぜ 文字コード(10進数) 08 13 01 20 24 - 28 48 62 02 02 17 21 - 25 29 36 37 41 - 37 48 52 1つ前の文字との差 -- 05 52 19 04 - 04 20 14 04 00 15 04 - 04 04 04 04 04 - 60 11 04 文字の差(8進数) 10 05 64 23 04 - 04 24 16 04 00 17 04 - 04 04 04 04 04 - 74 13 04 となりました。 最後の行は8進数です(文字は64種類、つまり 6bit なので、8進数の方がビット単位で見やすいと思ったため)。 最初のコード(1の位置)は、1文字目のコードをそのまま書いてあります。 残りのものも含めて、「文字の差(8進数)」の個所だけ表にしてみます。 まずは、アイテム無しのもの。 名前「あ   」アイテム無し(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 1つ目 04 06 64 23 04 - 04 04 16 04 00 07 04 - 04 04 04 04 04 - 74 13 04 2つ目 05 04 64 23 04 - 04 04 16 04 00 17 04 - 04 04 04 04 04 - 74 13 04 3つ目 10 05 64 23 04 - 04 24 16 04 00 17 04 - 04 04 04 04 04 - 74 13 04 4つ目 11 07 64 23 04 - 04 24 16 04 00 07 04 - 04 04 04 04 04 - 74 13 04 5つ目 44 04 64 63 04 - 04 04 16 04 00 07 04 - 04 04 04 04 04 - 74 13 04 6つ目 45 06 64 63 04 - 04 04 16 04 00 17 04 - 04 04 04 04 04 - 74 13 04 7つ目 50 07 64 63 04 - 04 24 16 04 00 17 04 - 04 04 04 04 04 - 74 13 04 8つ目 51 05 64 63 04 - 04 24 16 04 00 07 04 - 04 04 04 04 04 - 74 13 04 次は、名前「あ   」たいまつ1つだけ(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 1つ目 04 06 64 63 04 - 04 04 16 04 00 17 04 - 04 04 04 04 04 - 74 33 04 2つ目 05 04 64 63 04 - 04 04 16 04 00 07 04 - 04 04 04 04 04 - 74 33 04 3つ目 10 05 64 63 04 - 04 24 16 04 00 07 04 - 04 04 04 04 04 - 74 33 04 4つ目 11 07 64 63 04 - 04 24 16 04 00 17 04 - 04 04 04 04 04 - 74 33 04 5つ目 44 04 64 23 04 - 04 04 16 04 00 17 04 - 04 04 04 04 04 - 74 33 04 6つ目 45 06 64 23 04 - 04 04 16 04 00 07 04 - 04 04 04 04 04 - 74 33 04 7つ目 50 07 64 23 04 - 04 24 16 04 00 07 04 - 04 04 04 04 04 - 74 33 04 8つ目 51 05 64 23 04 - 04 24 16 04 00 17 04 - 04 04 04 04 04 - 74 33 04 大きな違いは、最後から2番目のコード(文字位置#19)。 このあたりに1番目のアイテムのデータが入っているのかな? 【1997/07/10】勇者「ああああ」 復活の呪文の文字と、名前の文字は別のコードですよね。 勝手な想像ですけど、名前のコードは「あ」が 0 で あ行~ま行・やゆよ・ら行・わをん・濁点・半濁点・ っゃゅょ-・数字の0~9・空白の順に並び、 最後の空白が 63 ( 10 進数 ) の様な気がします。 まあ、何の理由もないんですけどね。 というわけで、名前「ああああ」アイテム何もなし。 なひべおけ すちまもそひま もらろぐじ きした にはぶえく したほめせつに はほめよれ ぶうき のほいくし たやざぞめよれ ぎざぞでぶ つぬひ はむおさそ てらぜづゆごぜ づびあおけ へめよ ぜづへうき さそへむすのへ むゆるがご おこせ ぞばみかこ せつみやたとね ふみやりわ ぼおけ づぼやこせ つよずぢやりわ げずぢばぼ とのへ でべもけす ちゆじだもぐじ だどべうき ひみや そして、その差分。名前「ああああ」アイテムなし(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 1つ目 24 06 44 06 04 - 04 04 16 04 54 14 04 - 04 04 04 04 04 - 24 05 04 2つ目 25 04 44 06 04 - 04 04 16 04 54 04 04 - 04 04 04 04 04 - 24 05 04 3つ目 30 05 44 06 04 - 04 24 16 04 54 04 04 - 04 04 04 04 04 - 24 05 04 4つ目 31 07 44 06 04 - 04 24 16 04 54 14 04 - 04 04 04 04 04 - 24 05 04 5つ目 70 07 44 46 04 - 04 24 16 04 54 04 04 - 04 04 04 04 04 - 24 05 04 6つ目 71 05 44 46 04 - 04 24 16 04 54 14 04 - 04 04 04 04 04 - 24 05 04 7つ目 64 04 44 46 04 - 04 04 16 04 54 14 04 - 04 04 04 04 04 - 24 05 04 8つ目 65 06 44 46 04 - 04 04 16 04 54 04 04 - 04 04 04 04 04 - 24 05 04 【1997/07/11】120 bit 今回はいつもとは逆に、復活の呪文 ( 6 bit * 20 文字 = 120 bit ) にどんなデータが含まれているか、を考えます。 名前4文字 ( 6 bit * 4 ) 経験値 0 ~ 65535 ( 16 bit ) ゴールド 0 ~ 65535 ( 16 bit ) 武器 7種 ( 3 bit ) なし / たけざお / こんぼう / どうのつるぎ / てつのおの / はがねのつるぎ / ほのおのつるぎ / ロトのつるぎ 鎧 7種 ( 3 bit ) なし / ぬののふく / かわのふく / くさりかたびら / てつのよろい / はがねのよろい / まほうのよろい / ロトのよろい 盾 3種 ( 2 bit ) なし / かわのたて / てつのたて / みかがみのたて 薬草 0 ~ 6 ( 3 bit ) 鍵 0 ~ 6 ( 3 bit ) 道具 14 種 ( 4 bit * 8 ) なし / たいまつ / せいすい / キメラのつばさ / りゅうのうろこ / ようせいのふえ / せんしのゆびわ / ロトのしるし / おうじょのあい / のろいのベルト / ぎんのたてごと / しのくびかざり / たいようのいし / あまぐものつえ / にじのしずく 以上ですね。 全部合わせると 102 bit かな? ってことは、残りは 18 bit ( 3 文字分 )。 ……チェックコードが 18 bit もあるのかなぁ? 【1997/07/12】差分の見方 差分の見方ですが、04 とかのいつもあるデータはそのビットを XOR するのか、ビット自体を削除してしまうのか、その 04 自体を引くのか、またはそれらを組み合わせた計算をするのだと思います。 どうやっているのかは、まだ分かりませんが。 (なんとなく 特定の数字パターンを引いていくんじゃないかな) それと、同じ状態で聞ける8種類の復活の呪文で相違のあるビットはチェックコードかなにかなのでしょう。 最初の1文字目(#1)はどうやったらいいか分からないのでとりあえずおいといて。 チェックコードらしき箇所。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 コード 77 03 00 40 00 - 00 20 00 00 00 10 00 - 00 00 00 00 00 - 00 00 00 上記の数字の入っているビットが、チェックコード(かもしれない)ビットでしょう。 最初の1文字目がどうやって決まるかが全く分かりません。 が、ちょっとだけ気になることがあります。 8パターンの復活の呪文をならべると、9文字目(#9)が連続しているように思えることです。 上記の復活の呪文の9文字目(#9)を縦に見てください。 並べ替えると……。 名前「あ   」でアイテム無し:「うえおか」「ぎぐげご」 名前「あ   」でたいまつだけ:「せそたち」「もやゆよ」 名前「ああああ」でアイテム無し:「ぞだぢづ」「むめもや」 このように4つ連続した組が2つあるみたいじゃないですか? わかりにくいかな? 「あいうえおかきくけこ」の中の連続した4文字「うえおか」と、「がぎぐげご」の中の連続した4文字「ぎぐげご」になっている。 もしかすると、このあたりが解析の鍵になるかもしれません。 ただの偶然かもしれませんが。 【1997/07/13】勇者「あああ*」 少しだけ名前を変えて調べてみます。 名前「あああい」アイテム無し(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 文字 さ そ じ づ び - あ な も ら ぬ も ら - ろ ぐ じ だ ど - そ と ね 文字の差(8進数) 12 04 44 06 04 - 04 24 16 04 60 14 04 - 04 04 04 04 04 - 24 05 04 面倒なんで、1つだけ。 先日の差分の見方を参考にすると、変わった箇所は名前「ああああ」で 54 (8進数) 固定だった部分(#10)が 60(8進数) になっているってとこでしょうか。 固定の部分を引くと 仮定 すると 60 - 54 = 4 で (注:8進数です)、ビットが1つ立ったわけですね。 で、もう一つの 仮定 として「あ」のコードが 0 、「い」が 1 であれば、このビットこそが名前の4文字目の1番下のビットということでしょう。 続いて、勇者「あああう」アイテム無し(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 文字 み ゆ け そ て - ぬ ひ る が む が ご - ぜ づ び あ お - の ほ め 文字の差(8進数) 37 05 44 06 04 - 04 04 16 04 64 14 04 - 04 04 04 04 04 - 24 05 04 変わっている箇所は……やはり 54 固定の場所(#10)が 64 になっていますね。 64 - 54 = 10 で名前の4文字目の下から2番目のビットがここ、ということかな。 さらに続いて勇者「あああ 」アイテム無し(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 文字 ど べ も る が - ご お て ぬ ぬ ほ め - よ れ ぎ ざ ぞ - こ そ て 文字の差(8進数) 72 04 44 06 04 - 04 24 16 04 00 07 04 - 04 04 04 04 04 - 24 05 04 空白が 63 ( 10 進数 ) であるという 仮定 であれば、ここで変わるビットが、4文字目の全ビットということでしょう。 で、変わっている箇所は、例の 54 のところとその次のところ(#10~#11)です。 54 のところが 00 になっていますね。 00 - 54 = 24 ( 8 進数 で mod 64 ) です。 あれ? これは、さっきの名前の4文字目の下から2ビット目の位置の 仮定 と矛盾しますね。 ってことは、名前用の文字コードの 仮定 か、もしくは 54 を引くという 仮定 のどちらかが間違っているということ……。 4文字目を色々と変えてみたり、1~3文字目を色々と変えてみれば多少は分かるかもしれないが……うーん、先は長そうだ。 【1997/07/18】勇者「0000」 「大技林」(注:という本があった)の中の勇者「0000」の呪文を眺めていたら、差分が全て 04 の様に思えてきました。 早速実際に確認してみると…。 名前「0000」アイテム無し(文字の差(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 文字 お け す ち な - の へ む ゆ る が ご - ぜ づ び あ お - け す ち 文字の差(8進数) 04 04 04 04 04 - 04 04 04 04 04 04 04 - 04 04 04 04 04 - 04 04 04 はっ!本当に全部 04 だ! ということは、差分から4を引く・名前のコードは「0」が 0 かな……。 おけすちな のへむゆるがご ぜづびあお けすち かしたとね ふみやりわぢば ぼえくした とねふ けたとねふ みずぢばぼした とねふみや りわげ こそてぬひ まじだどべうき さそてぬひ まもら ゆろぐてぬ ひまもらろぐじ だどべうき さそて よれぎつに はほめよれぞで ぶいかこせ つには るぎざには ほざぞでぶこせ つにはほめ よれぎ れごぜのへ むぜづびあおけ すちなのへ むゆる で、差分。全部の数字から4を引いてあります。 名前「0000」アイテム無し(文字の差-4(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 1つ目 00 00 00 00 00 - 00 00 00 00 00 00 00 - 00 00 00 00 00 - 00 00 00 2つ目 01 02 00 00 00 - 00 00 00 00 00 10 00 - 00 00 00 00 00 - 00 00 00 3つ目 04 03 00 00 00 - 00 20 00 00 00 10 00 - 00 00 00 00 00 - 00 00 00 4つ目 05 01 00 00 00 - 00 20 00 00 00 00 00 - 00 00 00 00 00 - 00 00 00 5つ目 40 02 00 40 00 - 00 00 00 00 00 00 00 - 00 00 00 00 00 - 00 00 00 6つ目 41 00 00 40 00 - 00 00 00 00 00 10 00 - 00 00 00 00 00 - 00 00 00 7つ目 44 01 00 40 00 - 00 20 00 00 00 10 00 - 00 00 00 00 00 - 00 00 00 8つ目 45 03 00 40 00 - 00 20 00 00 00 00 00 - 00 00 00 00 00 - 00 00 00 差分から4を引いている。チェックビットも削除している。先頭1文字目も無視。 00 00 40 02 00 00 00 12 00 50 00 00 00 00 00 00 00 20 01 00 - 「ああああ」 00 00 40 02 00 00 00 12 00 54 00 00 00 00 00 00 00 20 01 00 - 「あああい」 00 00 40 02 00 00 00 12 00 60 00 00 00 00 00 00 00 20 01 00 - 「あああう」 00 00 40 02 00 00 00 12 00 64 00 00 00 00 00 00 00 20 01 00 - 「あああえ」 00 00 40 02 00 00 00 12 00 70 00 00 00 00 00 00 00 20 01 00 - 「あああお」 00 00 40 02 00 00 00 12 00 74 00 00 00 00 00 00 00 20 01 00 - 「あああか」 00 00 40 02 00 00 00 12 00 00 01 00 00 00 00 00 00 20 01 00 - 「あああき」 00 00 40 02 00 00 00 12 00 00 02 00 00 00 00 00 00 20 01 00 - 「あああぬ」 00 00 40 02 00 00 00 12 00 74 03 00 00 00 00 00 00 20 01 00 - 「あああ 」 00 00 60 17 00 00 00 12 00 74 03 00 00 00 00 00 00 20 01 00 - 「ああ  」 00 00 60 17 00 00 00 12 00 74 03 00 00 00 00 00 00 70 07 00 - 「あ   」 いろいろな名前 アイテム無し(文字の差-4(8進数))。 文字位置 #1 #2 #3 #4 #5 - #6 #7 #8 #9 #10 #11 #12 - #13 #14 #15 #16 #17 - #18 #19 #20 「ああああ」 00 00 40 02 00 - 00 00 12 00 50 00 00 - 00 00 00 00 00 - 20 01 00 「あああい」 00 00 40 02 00 - 00 00 12 00 54 00 00 - 00 00 00 00 00 - 20 01 00 「あああう」 00 00 40 02 00 - 00 00 12 00 60 00 00 - 00 00 00 00 00 - 20 01 00 「あああえ」 00 00 40 02 00 - 00 00 12 00 64 00 00 - 00 00 00 00 00 - 20 01 00 「あああお」 00 00 40 02 00 - 00 00 12 00 70 00 00 - 00 00 00 00 00 - 20 01 00 「あああか」 00 00 40 02 00 - 00 00 12 00 74 00 00 - 00 00 00 00 00 - 20 01 00 「あああき」 00 00 40 02 00 - 00 00 12 00 00 01 00 - 00 00 00 00 00 - 20 01 00 「あああぬ」 00 00 40 02 00 - 00 00 12 00 00 02 00 - 00 00 00 00 00 - 20 01 00 「あああ 」 00 00 40 02 00 - 00 00 12 00 74 03 00 - 00 00 00 00 00 - 20 01 00 「ああ  」 00 00 60 17 00 - 00 00 12 00 74 03 00 - 00 00 00 00 00 - 20 01 00 「あ   」 00 00 60 17 00 - 00 00 12 00 74 03 00 - 00 00 00 00 00 - 70 07 00 「あ」が 10 ( 10 進数 ) 、「い」が 11 、「う」が 12 といった感じかな。 【1997/07/19】名前 どうやら、後ろの方から差分を取った方が見やすいようですね。 いろいろな名前 アイテム無し(文字の差-4(8進数)・逆順) 文字位置 #20 #19 #18 - #17 #16 #15 #14 #13 - #12 #11 #10 #9 #8 #7 #6 - #5 #4 #3 #2 #1 「ああああ」 00 01 20 - 00 00 00 00 00 - 00 00 50 00 12 00 00 - 00 02 40 00 00 「あああい」 00 01 20 - 00 00 00 00 00 - 00 00 54 00 12 00 00 - 00 02 40 00 00 「あああう」 00 01 20 - 00 00 00 00 00 - 00 00 60 00 12 00 00 - 00 02 40 00 00 「あああえ」 00 01 20 - 00 00 00 00 00 - 00 00 64 00 12 00 00 - 00 02 40 00 00 「あああお」 00 01 20 - 00 00 00 00 00 - 00 00 70 00 12 00 00 - 00 02 40 00 00 「あああか」 00 01 20 - 00 00 00 00 00 - 00 00 74 00 12 00 00 - 00 02 40 00 00 「あああき」 00 01 20 - 00 00 00 00 00 - 00 01 00 00 12 00 00 - 00 02 40 00 00 「あああぬ」 00 01 20 - 00 00 00 00 00 - 00 02 00 00 12 00 00 - 00 02 40 00 00 「あああ 」 00 01 20 - 00 00 00 00 00 - 00 03 74 00 12 00 00 - 00 02 40 00 00 「ああ  」 00 01 20 - 00 00 00 00 00 - 00 03 74 00 12 00 00 - 00 17 60 00 00 「あ   」 00 07 70 - 00 00 00 00 00 - 00 03 74 00 12 00 00 - 00 17 60 00 00 「を   」 00 07 70 - 00 00 00 00 00 - 00 03 74 00 66 00 00 - 00 17 60 00 00 「ん   」 00 07 70 - 00 00 00 00 00 - 00 03 74 00 67 00 00 - 00 17 60 00 00 「っゃゅょ」 00 07 10 - 00 00 00 00 00 - 00 03 54 00 70 00 00 - 00 16 40 00 00 「゛゜  」 00 07 50 - 00 00 00 00 00 - 00 03 74 00 74 00 00 - 00 17 60 00 00 名前の1文字目はおそらく #8 の位置でしょう 2文字目は #20 の下 3bit と #19 の上 3bit 。 3文字目は #4 の下 4bit と #3 の上 2bit 。 4文字目は #11 の下 2bit と #10 の上 4bit 。 名前用のコードは以下のようになっているようです。 +0 +1 +2 +3 +4 +00 0 1 2 3 4 +05 5 6 7 8 9 +10 あ い う え お +15 か き く け こ +20 さ し す せ そ +25 た ち つ て と +30 な に ぬ ね の +35 は ひ ふ へ ほ +40 ま み む め も +45 や ゆ よ +48 ら り る れ ろ +53 わ を ん +56 っ ゃ ゅ ょ +60 ゛ ゜ ー   【1997/07/20】各データ(未確定版) 名前以外のデータも大体把握できました。 1ビットづつ変えた復活の呪文を聞いて変わったビットを確認して、というのを延々とやりました。 面倒なんで、結果だけ。 それと 8bit 毎に区切り直した方が見やすいようなので、そうなってます。 また、順番も逆になっています(末尾のバイトが先頭文字含む)。 アイテム2番目(4bit)+1番目(4bit) 鱗装備(1bit)+名前2文字目(6bit)+未使用(1bit) 経験値上(8bit) アイテム6番目(4bit)+5番目(4bit) 鍵(4bit)+薬草(4bit) 所持金上(8bit) 武器(3bit)+鎧(3bit)+盾(2bit) チェックコード(1bit)+未使用(1bit)+名前4文字目(6bit) アイテム8番目(4bit)+7番目(4bit) 名前1文字目(6bit)+未使用(1bit)+チェックコード(1bit) 所持金下(8bit) アイテム4番目(4bit)+3番目(4bit) チェックコード(1bit)+未使用(1bit)+名前3文字目(6bit) 経験値下(8bit) チェックコード(8bit) 鱗装備というのは、「りゅうのうろこ」を身に付けたことがあるかどうかです。 「未使用」といっているものが本当に未使用かは分かりません。 1bit 単位のチェックコードは8パターンある復活の呪文のキーじゃないかと思っています。 かなりのデータが分かり、なんとなくもう少しで復活の呪文の捏造ができるような気になりますが、実際には最後のそして最大の難関が手付かずのままのこってますね。 つまり、チェックコードの計算方法です。 【1997/07/22】チェックコード チェックコードの解析ですね。 とりあえず、一番シンプルな勇者「0000」でしょう。 経験値を1づつ取得して、復活の呪文を聞いてみました。 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 - EXP 0 00 00 00 00 00 00 00 00 00 00 00 00 00 01 61 - EXP 1 00 00 00 00 00 00 00 00 00 00 00 00 00 02 c2 - EXP 2 00 00 00 00 00 00 00 00 00 00 00 00 00 03 a3 - EXP 3 00 00 00 00 00 00 00 00 00 00 00 00 00 04 a5 - EXP 4 00 00 00 00 00 00 00 00 00 00 00 00 00 05 c4 - EXP 5 例の 8bit 毎に切り直した結果を 16 進数で表しています。 0x61 * 2 = 0xc2 ですが……偶然かな? それよりも 0x61 xor 0xc2 = 0xa3 の方が気になりますね(経験値 1, 2, 3 を比較)。 それに 0x61 xor 0xa5 = 0xc4 (経験値 1, 4, 5 を比較)。 どうやら、各 bit がある数値と括りついていてそれらの xor がチェックコードということかな? (経験値の下1bit目が 0x61 とくくりついて、下2bit目が 0xc2 とくくりついて、下3bit目が 0xa5 とくくりついている想定) ……いろいろ検証してみたが、どうやらそーらしい。 でも、 14 byte * 8 = 112 bit 分もマジックナンバーがあるの? おそらくはいくつかの数字から計算で求められるんじゃないかとおもいますが、それを調べるのは、 112 個のデータを集めないと……。 【1997/07/25】完成!!かな?(注:未完成です) ここ数日ひたすらチェックコードを解析していました。 で、結果。 一番左に書いてあるのが最上位ビット、一番右が最下位ビットと括りついたマジックナンバーです。 0x88 0xc4 0x62 0x31 0x08 0x84 0x42 0x21 0x98 0xcc 0xe6 0x73 0xa9 0xc4 0x62 0 0x5a 0xad 0xc6 0x63 0xa1 0xc0 0x60 0x30 0x38 0x9c 0x4e 0xa7 0xc3 0xf1 0x68 0xb4 0 0x68 0xb4 0x5a 0 0x06 0x83 0x51 0x20 0x10 0x08 0x84 0x42 0xa1 0x40 0xa0 0xf9 0xec 0xf6 0x7b 0xad 0xc6 0xe3 0x61 0x81 0xd0 0x68 0xb4 0xda 0x6d 0xa6 0xd3 0xb2 0xd9 0xfc 0xfe 0xff 0xef 0x67 0x23 0x34 0x1a 0x0d 0x96 0x4b 0x35 0x8a 0x45 0xaa 0xd5 0x7a 0x3d 0x8e 0x47 0xb3 0x49 0xa1 0x40 0xa0 0x50 0xa8 0xd4 0xea 0x75 0xa0 0 0x68 0xb4 0x5a 0xad 0xc6 0x63 0x7e 0xbf 0xcf 0xf7 0x6b 0xa5 0xc2 0x61 こんな感じでした。 0 が4か所ありますが、そこはチェックできていません。 それと 8byte 目の上から 2bit 目は王女の所のドラゴンを倒したかどうか、のようです。 同様に 10byte 目の下から 2bit 目はゴーレムを倒したかどうか、のようです。 ということは、ほかの未使用ビットも意味があるのかな。 それとなんとなく規則が見えそうで、でも良く分からないです。 上位 4bit と下位 4bit は別に見る。 下位 4bit は 0x08 → 0x0c → 0x06 → 0x03 → 0x09 → 0x04 → 0x02 → ?? の様に 1bit づつ 8 段階にシフトしているのかな。 上位 4bit は……同様にシフトしているけど 4 段階ってことなのか? でも違うところもあるなぁ。単純にミスだったりして。 ま、いいや。とりあえずここまでで CGI を作って公開しました。 (注:昔、公開していました) 【1998/02/07】戦士の指輪 2byte 目の一番下の bit は「戦士の指輪」を装備しているかどうかのようですね。 【1998/05/31】最後のフラグ 情報提供がありまして、最後のフラグは「死の首飾り」を入手したかどうかのようです。 また、チェックコードの求め方についても、判明しました。おそらく CRC を求めているようです。 (CRC-16-CCITT, 多項式は 0x1021) 例のマジックナンバーは、以下のようなコーディングで求める事ができます。 int cd = 0x8000; for (int i = 0; i < 15 * 8; i++) { if (cd & 0x8000) { cd = (cd << 1) ^ 0x1021; } else { cd <<= 1; } printf("0x%.2x\n", cd & 0xff); } 結果:(先頭の下のビットから順番に) 0x21 0x42 0x84 0x08 0x31 0x62 (以下略) マジックナンバーの完全版は、以下の通り。 0x88 0xc4 0x62 0x31 0x08 0x84 0x42 0x21 0x98 0xcc 0xe6 0x73 0xa9 0xc4 0x62 0x31 0x5a 0xad 0xc6 0x63 0xa1 0xc0 0x60 0x30 0x38 0x9c 0x4e 0xa7 0xc3 0xf1 0x68 0xb4 0xd0 0x68 0xb4 0x5a 0x2d 0x06 0x83 0x51 0x20 0x10 0x08 0x84 0x42 0xa1 0x40 0xa0 0xf9 0xec 0xf6 0x7b 0xad 0xc6 0xe3 0x61 0x81 0xd0 0x68 0xb4 0xda 0x6d 0xa6 0xd3 0xb2 0xd9 0xfc 0xfe 0xff 0xef 0x67 0x23 0x34 0x1a 0x0d 0x96 0x4b 0x35 0x8a 0x45 0xaa 0xd5 0x7a 0x3d 0x8e 0x47 0xb3 0x49 0xa1 0x40 0xa0 0x50 0xa8 0xd4 0xea 0x75 0xa0 0xd0 0x68 0xb4 0x5a 0xad 0xc6 0x63 0x7e 0xbf 0xcf 0xf7 0x6b 0xa5 0xc2 0x61 【2001/07/14】各データの最終版 相変わらず 8bit 毎に区切り直してあります(順番も逆になっています)。 アイテム2番目(4bit)+1番目(4bit) 鱗装備(1bit)+名前2文字目(6bit)+戦士の指輪装備有無(1bit) 経験値上(8bit) アイテム6番目(4bit)+5番目(4bit) 鍵(4bit)+薬草(4bit) 所持金上(8bit) 武器(3bit)+鎧(3bit)+盾(2bit) チェックコード(1bit)+見張りのドラゴンを倒したか(1bit)+名前4文字目(6bit) アイテム8番目(4bit)+7番目(4bit) 名前1文字目(6bit)+ゴーレムを倒したか(1bit)+チェックコード(1bit) 所持金下(8bit) アイテム4番目(4bit)+3番目(4bit) チェックコード(1bit)+死の首飾り入手済(1bit)+名前3文字目(6bit) 経験値下(8bit) チェックコード(8bit) 順番を戻して、6bit 毎つまり文字単位に切ると以下の通り。 制限のあるもの(チェックコードやアイテム、それに鍵・薬草など)を強調してあります。 また、5文字,7文字,5文字,3文字に区切って、間に(空白)と記述しておきます。 チェックコード下(6bit) 経験値4(4bit)+ チェックコード上(2bit) 名前3文字目の下(2bit)+経験値3(4bit) チェックコード(1bit)+死の首飾り入手済(1bit)+名前3文字目の上(4bit) アイテム4番目の下(2bit) + アイテム3番目(4bit) (空白) 所持金4(4bit)+ アイテム4番目の上(2bit) ゴーレムを倒したか(1bit)+ チェックコード(1bit) +所持金3(4bit) 名前1文字目(6bit) アイテム8番目の下(2bit) アイテム7番目(4bit) 名前4文字目の下(4bit)+ アイテム8番目の上(2bit) 盾(2bit)+ チェックコード(1bit) +見張りのドラゴンを倒したか(1bit)+名前4文字目の上(2bit) 武器(3bit)+鎧(3bit) (空白) 所持金2(6bit) 薬草(4bit) +所持金1(2bit) アイテム5番目の下(2bit) + 鍵(4bit) アイテム6番目(4bit) + アイテム5番目の上(2bit) 経験値2(6bit) (空白) 名前2文字目の下(3bit)+戦士の指輪装備有無(1bit)+経験値1(2bit) アイテム1番目の下(2bit) +鱗装備(1bit)+名前2文字目の上(3bit) アイテム2番目(4bit) + アイテム1番目の上(2bit) 「ふっかつのじゅもん」の作成ページ 上記の内容をもとに、あらためて「ふっかつのじゅもん」を作成できるページを作りました。 React のいろいろな部品を使って今風なUI/UXにしようかと思ったけど、そんなセンスもないのでとりあえず非常にシンプルな感じの画面になっています。 ソースはこちら (うまく動かなかったらごめんね) 各種項目を入力して呪文を作る方法と、呪文を入力して妥当性を確認する方法があります。 項目を入力して呪文を作る 「ふっかつのじゅもん」のページにいって、初期表示の画面が項目入力画面です。 (また、後述の呪文入力画面で左下の「項目を入力」ボタンを押すことで、画面遷移します) 4文字までで、名前を入力してください。 使用できる文字はひらがな(一部使えない文字があります)、数字、スペースのみです。 濁点、半濁点も一文字として数えます。 具体的な使える文字は以下の通りです(見えませんが、スペースも使えます)。 0123456789 あいうえお かきくけこ さしすせそ たちつてと なにぬねの はひふへほ まみむめも やゆよ らりるれろ わをん っゃゅょ ゛゜-  その他の項目を選択して「呪文を確認」をクリックすると、呪文が表示されます。 クリップボードにコピーするか、ツイッターにツイートすることができます。 呪文を入力して妥当性を確認 「ふっかつのじゅもん」のページにいって、左下の「呪文を入力」ボタンを押すと、呪文入力画面が表示されます。 呪文を20文字で入力してください。 呪文として使える文字はひらがなだけです(一部使えない文字があります)。 濁点・半濁点は、分離せずに1文字と数えます(例:「が」は1文字)。 単独の濁点・半濁点の文字は無効です(例:「か゛」は無効な文字を除外して「か」と判断されます)。 具体的な使える文字は以下の通りです。 あいうえお かきくけこ さしすせそ たちつてと なにぬねの はひふへほ まみむめも やゆよ らりるれろ わ がぎぐげご ざじずぜぞ だぢづでど ばびぶべぼ 見やすさのため、スペースを入力することもできます(呪文の文字数にはカウントしません) 右下の「呪文を確認」ボタンを押すと、呪文をチェックします。 語呂合わせの「ふっかつのじゅもん」を作りたい場合 呪文を入力する際、ひらがな以外に「?」を入力していると、呪文として使えるパターンが表示されます。 例:ふるいけや かわずとびこむ みずのおと ば?? 「?」は最大3つまで書けますが、3つ書くと処理に時間がかかるので注意してください。 また、特定の位置に特定の文字を並べると、例えば不正なアイテムを持っていることになって、残りの場所をどのように変えても有効な呪文にならないことがあります。 (あるいはMAX以上の薬草や鍵を持っていることになるケースもある) その場合には「n~m文字目のどこかを変更してね」といったメッセージが出ますので、修正してください。 また、上記メッセージがでなくても有効な呪文でないことがあります。 これは、呪文全体のチェックコード(内部的には CRC を使っています)での検証が通っていないものです。 呪文のどこかを修正すると有効な呪文になるはずなので、言い回しを変えられそうな部分に「?」を入れて、候補をしらべてください。 おもしろい語呂合わせのふっかつのじゅもんができたら、ツイッターなどで自慢しましょう。 既存の作品を見てみたい方は、以下の「奇妙な「復活の呪文」大辞典」までどうぞ(古い記事を掘り起こしたので、古いネタが多いです。時事ネタとか)。 github のマークダウンを github pages に置いただけなので、見た目がしょぼいのはご容赦を。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

x個の数字の要素を持つ配列からy番目に大きな要素を取得する

sortの使い方を意識しました いいやり方があるという方は、コメントにお願いいたします。 const x = [56, 23, 90, 1, 64, 37]; const y =4; function getNumber(x,y){ console.log(x[y-1]) } const descendingOrder=x.sort((a,b)=>b-a) getNumber(descendingOrder,y) //出力結果 //37
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カウントアップアプリを作って、props、stateの理解を深めた件

はじめに react入門いたしました。 reactの基礎である、props、stateについて カウントアップアプリを通じて学びました。 Props 親コンポーネントから子コンポーネントに値を渡す方法 データの属性によって記述の仕方が違います、文字列は""で囲む、数字は{}で囲む 子コンポーネントは引数として受け取る。受けつる際に{}で囲む 親コンポーネント const App = () => { return ( <> <section style={{ paddingTop: "50px" }}> <h1 className="ui container">カウントアプリ</h1> <div className="ui container" style={{ display: "flex" }}> <CountButton text="+1" num={1} /> <CountButton text="+2" num={2} /> <CountButton text="+3" num={3} /> <CountButton text="+4" num={4} /> <CountButton text="+5" num={5} /> </div> </section> </> ); }; text、numという変数で値を子コンポーネントに渡しております。 子コンポーネント const CountButton = (props) => { const [num, setNum] = useState(0); const onCountUp = () => { setNum(num + props.num); }; return ( <> <div className="count_box" style={{ marginRight: "20px" }}> <p style={{ fontSize: "20px" }}> Count<span style={{ fontSize: "24px" }}>{num}</span> </p> <button className="ui button blue" style={{ width: "100%" }} onClick={onCountUp} > {props.text} </button> </div> </> ); }; 子コンポーネントでは引数で(props)の値を受け取る {}で囲んでprops.変数名で値を表示する State 各コンポーネントが持つ状態 Stateが変更されると再レンダリングされる。 条件によって状態が変わる部分をStateで管理する import React, { useState } from "react"; const CountButton = (props) => { const [num, setNum] = useState(0); const onCountUp = () => { setNum(num + props.num); }; return ( <> <div className="count_box" style={{ marginRight: "20px" }}> <p style={{ fontSize: "20px" }}> Count<span style={{ fontSize: "24px" }}>{num}</span> </p> <button className="ui button blue" style={{ width: "100%" }} onClick={onCountUp} > {props.text} </button> </div> </> ); }; まずはuseStateをimportする const [num, setNum] = useState(0); 一つ目いにStateとして使う変数名(num)、二つ目にそのStateを変更するための関数名(setNum) useState(初期値)今回は0です。 setNum(num + props.num); Stateを更新する、numにprposの値分プラスする デモ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Lazy Loading】画面表示を速くする簡単な方法

はじめに フロントエンド強化月間の参加記事です。 概要 画像をたくさん使用するWebサイトでは画面の読込に時間がかかってしまうというパフォーマンスの課題を常に抱えています。 その課題の簡単な解消方法として「Lazy Loading」があります。 Lazy Loading とは Lazy Loading(遅延読込)はリソースを事前に読み込むのではなく、必要になった時にリソースを読み込む手法のことです。 要は、画像を表示するタイミングになった時に初めて画像を読み込みます。 Lazy Loading の効果 通常の画面読込ではブラウザの表示領域外の画像も事前に読み込んでいます。 これでは画面が表示されるまで時間がかかります。 Lazy Loading ではブラウザの表示領域外にある画像(破線の部分)を読み込まないことで画面の表示速度を向上させることができます。 実装 では、実際に実装してみましょう。 1. lazysizes.min.jsをダウンロードする lazysizes をGitHubから取得します。 2021/05現在の最新バージョンは5.3.1です。 2. lazysizes.min.jsをscriptタグで読み込む bodyの閉じタグの直前辺りでlazysizes.min.jsを読み込みます。 index.html <script src="lazysizes.min.js" async></script> </body> 3. imgタグのclass属性に"lazyload"を指定する class="lazyload" とすることで、lazysizes.min.jsに遅延読込の対象であると判断させます。 index.html <img class="lazyload" src="image.jpg"> 4. imgタグのsrc属性をdata-src属性に変更する data-src="image.jpg" とすると、画像が表示領域に差し掛かったタイミングでlazysizes.min.jsが画像を読み込みます。 index.html <img class="lazyload" data-src="image.jpg"> 以上でLazy Loading(遅延読込)の実装は完了です。 検証 Lazy Loadingによってどれくらいの変化があるか検証してみます。 検証方法 Lazy Loadingを(a)実装していないページと(b)実装しているページの2種類を用意し、 キャッシュを利用しないでページを読み込んだ際の読込完了時間の差異を計測します。 両方のページでは15枚ほど画像を読み込みます。 実装内容 index.html <body> <div> <img class="lazyload" data-src="img-001.jpg"> </div> <div> <img class="lazyload" data-src="img-002.jpg"> </div> <div> <img class="lazyload" data-src="img-003.jpg"> </div> <!-- 省略 --> <div> <img class="lazyload" data-src="img-013.jpg"> </div> <div> <img class="lazyload" data-src="img-014.jpg"> </div> <div> <img class="lazyload" data-src="img-015.jpg"> </div> <script src="lazysizes.min.js" async></script> </body> 検証結果 (a)実装していないページ すべて読み込み終えるのに1.47秒かかっています。 15枚近くある画像をすべて読み込んでいるせいか17回もリクエストが発生しており、6.1MBのリソースを取得しています。 LightHouseのスコア判定でも「Avoid enormous network payloads」が警告として現れています。 (b)実装しているページ すべて読み込み終えるのに0.584秒かかっています。 表示領域内の4つの画像のみを取得しているため、リソースが1.2MBに抑えられています。 ブラウザをスクロールして画像が表示領域に差し掛かった時、残りの画像が随時読み込まれます。 LightHouseのスコア判定でも「Avoid enormous network payloads」が現れていません。 つまり、パフォーマンスが改善していることがわかります。 結果 Lazy Loading は比較的、簡単に実装・導入できる上、画像をたくさん利用するWebサイトにおいてはパフォーマンスが改善されることが見込めます。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

便利ページ:WebSocketをサクッと触れるページを作ってみた

便利ページ:Javascriptでちょっとした便利な機能を作ってみた のシリーズです。 今回は、WebSocketをサクッと呼び出せるページを作ります。 最近のほとんどのブラウザには、WebSocket APIが実装されているため、それを使います。 もろもろは、GitHubに上げてあります。 Webページはこちらからも見ることができます。ユーティリティタブでWebSocketを選択してください。 また、今回からついで、ベースとしていたフレームワークをBootstrap 3.4.1からBootstrap 5に変更し、便利ページ内の各ページをVueのコンポーネントで構成するようにしました。 こうすることで、ページもメンテナンスしやすく、追加しやすくなりました。 もし、Bootstrap3.4.1との比較を見たい場合は以下を参照してください。  Bootstrap 3からBootstrap 5に移行してみた 画面はこんな感じです。 PWAにもしているので、アプリとして登録してあげてください。 Chromeを立ち上げなくても、いつでも便利ページを呼び出せますので。WindowsでもAndroidでもOKです。 ソースコード といっても、大したことはやっていないので、ソースコード(Vueコンポーネント)を示しておきます。 js/comp/comp_websocket.js const log_separator = "\n----------\n"; export default { mixins: [mixins_bootstrap], template: ` <div> <h2 class="modal-header">WebSocket</h2> <label class="title">url</label> <input type="text" class="form-control" v-model="ws_url" placeholder="wss://192.168.1.1:443/ws"> <div class="row"> <label class="col-auto title">protocols</label> <span class="col-auto"> <input type="text" class="form-control" v-model="ws_protocols"> </span> </div> <button class="btn btn-secondary" v-on:click="ws_open" v-if="!ws_socket">Open</button> <button class="btn btn-secondary" v-on:click="ws_close" v-else>Close</button> <br> status: {{ws_status_message}} <br><br> <div class="row"> <label class="col-auto title">message</label> <span class="col-auto"> <select class="form-select" v-model="ws_send_type"> <option value="text">text</option> <option value="binary">binary</option> </select> </span> </div> <input type="text" class="form-control" v-model="ws_send_message"> <button class="btn btn-secondary" v-on:click="ws_send" v-bind:disabled="!ws_socket">送信</button> <br><br> <textarea class="form-control col" id="ws_textarea" rows="10">{{ws_console_message}}</textarea> <button class="btn btn-secondary" v-on:click="console_clear">クリア</button> </div>`, data: function () { return { ws_socket: null, ws_url: "", ws_protocols: "", ws_console_message: "", ws_send_type: "text", ws_send_message: "", ws_status_message: "closed", } }, methods: { /* WebSocket */ ws_open: function(){ this.ws_close(); try{ this.ws_socket = new WebSocket(this.ws_url); this.ws_socket.binaryType = "arraybuffer"; this.ws_socket.onopen = (event) => { // console.log("websocket opened", event); this.console_log(this.make_console_state_message(event)); var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" }); this.ws_status_message = "opened from " + date_string; }; this.ws_socket.onclose = (event) =>{ // console.log("websocket closed", event); this.console_log(this.make_console_close_message(event)); this.ws_socket = null; this.ws_status_message = "closed"; }; this.ws_socket.onerror = (event) =>{ // console.log("websocket error", event); this.console_log(this.make_console_state_message(event)); }; this.ws_socket.onmessage = (event) => { // console.log("websocket message", event); if (typeof (event.data) == "string" || event.data instanceof String ){ this.console_log(this.make_console_input_message(event)); }else{ this.console_log(this.make_console_input_message(event)); } }; }catch(error){ alert(error); } }, ws_close: function(){ if (this.ws_socket) this.ws_socket.close(); }, ws_send: function(){ if( this.ws_send_type == "text" ){ this.ws_socket.send(this.ws_send_message); this.console_log(this.make_console_output_message("text", this.ws_send_message)); }else{ var data = hexStr2byteAry(this.ws_send_message); var array = new Uint8Array(data); this.ws_socket.send(array); this.console_log(this.make_console_output_message("binary", array)); } }, console_clear: function () { this.ws_console_message = ""; }, make_console_output_message: function (type, data) { var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" }); var message; if (typeof (data) == "string" || data instanceof String) { message = data; } else { // message = new Uint8Array(data).toString(); message = byteAry2hexStr(data); } return "[SEND] message " + type + " " + date_string + "\n" + message; }, make_console_state_message: function (event) { var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" }); return "[STATE] " + event.type + " " + date_string; }, make_console_close_message: function (event) { var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" }); return "[STATE] " + event.type + " code=" + event.code + " " + date_string; }, make_console_input_message: function(event){ var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" }); var message; var type; if (typeof (event.data) == "string" || event.data instanceof String) { message = event.data; type = "text"; } else { // message = new Uint8Array(event.data).toString(); message = byteAry2hexStr(event.data); type = "binary"; } return "[RECV] " + event.type + " " + type + " " + date_string + "\n" + message; }, console_log: function(message){ this.ws_console_message = message + log_separator + this.ws_console_message; } }, mounted: function(){ var elem_in = document.querySelector("#ws_textarea"); elem_in.style.height = 10; } }; function hexStr2byteAry(hexs, sep = '') { hexs = hexs.trim(hexs); if (sep == '') { var array = []; for (var i = 0; i < hexs.length / 2; i++) array[i] = parseInt(hexs.substr(i * 2, 2), 16); return array; } else { return hexs.split(sep).map((h) => { return parseInt(h, 16); }); } } function byteAry2hexStr(bytes, sep = '', pref = '') { if (bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); if (bytes instanceof Uint8Array) bytes = Array.from(bytes); return bytes.map((b) => { const s = b.toString(16); return pref + (b < 0x10 ? ('0' + s) : s); }).join(sep); } 要は、以下でWebSocketサーバに接続して、 this.ws_socket = new WebSocket(this.ws_url); 以下で、サーバに送信して、 this.ws_socket.send(this.ws_send_message); 以下で、サーバから受信する。 this.ws_socket.onmessage = (event) => { それだけです。 何かわかんなくなったり、パラメータで微調整したい場合は、以下を参考にしてください。 接続・切断・送信・受信の動きは、テキストエリアに逐一出力していますので、時系列で終えるようにしています。 WebSocket API (WebSockets)  https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API 終わりに HTTPSで立ち上げたWebサーバのJavascriptからHTTPのWebSocketサーバに接続するには、WebSocketサーバをHTTPS接続にする必要があります。 以下を参考に、Web Proxyサーバを立ち上げてください。  HTTPで立ち上げたWebSocketサーバに、HTTPSで接続するためのWeb Proxyの立ち上げ あと、こちらもご参考まで。  MMDダンスをM5Core2のディスプレイに表示する 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リモートチームの存在感を伝えるツール「Remote Faces」を作ったので使ってみてください

Remote Facesと言うアプリを開発しています。機能改良して一段落したのでアナウンスツイートしました。 続くツイートに書いてあるように、使われている技術スタックは、下記のようになっています。 TypeScript React Valtio Yjs IPFS PubSub 場合によって PeerJS 場合によって Electron 技術的特徴としては、データ共有のためにサーバを介さないことで、peer-to-peerとも呼ばれます。データは参加している人の間で直接通信されます。サーバがないので、実は誰が使っているかもわかりません。ぜひ使ってみたら感想などお聞かせいただけると嬉しいです。 アプリの機能としては、いくつかのものを参考にしています。 Remotty Spatial Chat Gather Town 最大の難点は使い方が慣れるまで難しいことでしょうか。改善していければと思います。 とりあえず、興味を持っていただけた場合は触っていただければと思います。1人で複数のChromeタブでも動作を試すことができます。 ツールはこちら: GitHubはこちら: 「React Fan」というコミュニティを立ち上げていて、そのSlackでもremote-facesに関する議論や質問ができますので、よろしければご参加ください。 詳しくは、下記のページをご参照ください。 React開発者向けオンラインサロン「React Fan」の入り口ページ Slackへの招待リンクも上記ページにあります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GeoJSONポリゴンの中心(緯度経度)を求める

シンプルにポリゴンの中心を求めたいときに。 const getCentroid = (geometry) => { var polygon = []; switch (geometry.type) { case "MultiPolygon": geometry.coordinates.forEach( (d, i) => { if (i == 0 || polygon.length < d[0].length) polygon = d[0]; }); break; case "Polygon": polygon = geometry.coordinates[0]; break; } return polygon.reduce( (a, b) => { return [a[0] + b[0] / polygon.length, a[1] + b[1] / polygon.length]; }, [0, 0] ); } geojsonのgeometryプロパティを渡す。 const geojson = { "type": "Feature", "properties": {}, "geometry": { "type": "Polygon", "coordinates": [ [ [138.7309,35.3628],[138.8079,35.1983],[139.0248,35.2248],[138.7309,35.3628] ] ] } } //中心地を求める const center = getCentroid(geo.geometry); // ->  [138.823625, 35.287175000000005]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Express JSでbody-parserがいらない理由: “bodyparser is deprecated”

使用環境 MacOS Catalina(10.15.7)、VScode(1.55.2)、Node JS(16.1.0)、Express JS(4.17.1)、body-parser(1.19.0) はじめに タイトル通りですが、最新版のExpress(2021年5月5日現在 npmからデフォルトでインストールできる最新版がv4.17.1)でbody-parserがいらない(=インストールしなくていい)ことがわかりました。 私は現在、下記のUdemyの人気コースでNode JSおよびExpress JSを学習しています。 The Complete 2021 Web Development Bootcamp (URL) https://www.udemy.com/course/the-complete-web-development-bootcamp そのチュートリアルにてnpmでbody-parserをインストールしreq.bodyを記述することによって、”クライアント経由でサーバーに送信されたデータを取得できる”ことを知りました。 $ npm install body-parser //jshint esversion:6 const express = require("express"); const bodyParser = require("body-parser"); const app = express(); const port = 3000 app.use(bodyParser.urlencoded({ extended: true })); //途中略 app.post("/", (req, res) => { var num1 = Number(req.body.num1); var num2 = Number(req.body.num2); var result = num1 + num2; res.send("The result of the calculation is " + result); }) //以下略 しかし、コードを書いている際に定義したbodyParserに予期せぬ訂正線が入り、以下の通り何やらおかしな説明が出てきました。 (チュートリアルではこんなの出てなかったよ!?涙) @deprecated というのは”非推奨の、反対の”という意味で、その下に「'body-parser'は非推奨です」とあるとおり、それが理由でこんな風に線が入っているみたいです。 ちなみにこれを無視しても問題なくそのままコードは実行できました。 でもこのまま放っておくのはなんか気持ち悪いですよね。 大丈夫です。ちゃんと解決策がありました。 解決策 StackOverFlowやMediumの記事など、いろいろ調べてみた結果、どうやらExpressのバージョン14.6.0以降ではbody-parserは標準搭載されているらしいです。 しつこいですが、要するに最新版のExpressをインストールしたらなら、あえてbody-parserをインストールする必要はないということです。 ではどうすればいいのかっていうと、この部分を $ app.use(bodyParser.urlencoded({extended: true})); 下記のように記述すれば済みます。 $ app.use(express.urlencoded({extended: true})); //Parse URL-encoded bodies ちなみに、.jason();を使う場合もこちらを app.use(bodyparser.json()); 下記の通り変更すればOKです。 app.use(express.json()); これに変更した上でも、req.bodyは問題なく使えます。 (なぜならExpressにbody-parserが標準搭載されているから) ってことで、body-parserさん、もうあなたは用済みです。バイバイ! $ npm uninstall body-parser constでbodyParserを定義したものも、もういらないのでこちらも消し去りましょう。 ちなみに、前述した完全なコードは以下の通りです。 //jshint esversion:6 const express = require("express"); //const bodyParser = require("body-parser"); ←もういらない const app = express(); const port = 3000 //app.use(bodyParser.urlencoded({extended: true})); ←bodyParserを下記の通りexpressに変更 app.use(express.urlencoded({ extended: true })); //途中略 app.post("/", (req, res) => { var num1 = Number(req.body.num1); var num2 = Number(req.body.num2); var result = num1 + num2; res.send("The result of the calculation is " + result); }) //以下略 最後に 前述したとおり、VS Codeからのメッセージを無視して、実行できちゃうんですが私はちょっとそれを無視するのが気持ち悪かったので、なんとかできないかなと検索を始めたら理由がわかりました。 スッキリできてよかったです。 恥ずかしながら、調べる前は、「まあ、動いているし無視していっかw」と思ってました。 こんな風に考えても解決しないことはすぐにググって解決していくスタイルが、結果的に学びが多くなる気がしたので、続けていこうと思います。(そしてこんな風に学んだことをアウトプットすることも。) 参考にしたサイトたち Medium Express JS— body-parser and why may not need it StackOverFlow bodyParser is deprecated express 4 Grepper “bodyparser is deprecated” Code Answer’s
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Expressでbody-parserがいらない理由: “bodyparser is deprecated”

使用環境 MacOS Catalina(10.15.7)、VScode(1.55.2)、Node JS(16.1.0)、Express JS(4.17.1)、body-parser(1.19.0) はじめに タイトル通りですが、最新版のExpress(2021年5月5日現在 npmからデフォルトでインストールできる最新版がv4.17.1)でbody-parserがいらない(=インストールしなくていい)ことがわかりました。 私は現在、下記のUdemyの人気コースでNode JSおよびExpressを学習しています。 The Complete 2021 Web Development Bootcamp (URL) https://www.udemy.com/course/the-complete-web-development-bootcamp そのチュートリアルにてnpmでbody-parserをインストールしreq.bodyを記述することによって、”クライアント経由でサーバーに送信されたデータを取得できる”ことを知りました。 $ npm install body-parser //jshint esversion:6 const express = require("express"); const bodyParser = require("body-parser"); const app = express(); const port = 3000 app.use(bodyParser.urlencoded({ extended: true })); //途中略 app.post("/", (req, res) => { var num1 = Number(req.body.num1); var num2 = Number(req.body.num2); var result = num1 + num2; res.send("The result of the calculation is " + result); }) //以下略 しかし、コードを書いている際に定義したbodyParserに予期せぬ訂正線が入り、以下の通り何やらおかしな説明が出てきました。 (チュートリアルではこんなの出てなかったよ!?涙) @deprecated というのは”非推奨の、反対の”という意味で、その下に「'body-parser'は非推奨です」とあるとおり、それが理由でこんな風に線が入っているみたいです。 ちなみにこれを無視しても問題なくそのままコードは実行できました。 でもこのまま放っておくのはなんか気持ち悪いですよね。 大丈夫です。ちゃんと解決策がありました。 解決策 StackOverFlowやMediumの記事など、いろいろ調べてみた結果、どうやらExpressのバージョン14.6.0以降ではbody-parserは標準搭載されているらしいです。 しつこいですが、要するに最新版のExpressをインストールしたらなら、あえてbody-parserをインストールする必要はないということです。 ではどうすればいいのかっていうと、この部分を $ app.use(bodyParser.urlencoded({extended: true})); 下記のように記述すれば済みます。 $ app.use(express.urlencoded({extended: true})); //Parse URL-encoded bodies ちなみに、.jason();を使う場合もこちらを app.use(bodyparser.json()); 下記の通り変更すればOKです。 app.use(express.json()); これに変更した上でも、req.bodyは問題なく使えます。 (なぜならExpressにbody-parserが標準搭載されているから) ってことで、body-parserさん、もうあなたは用済みです。バイバイ! $ npm uninstall body-parser constでbodyParserを定義したものも、もういらないのでこちらも消し去りましょう。 ちなみに、前述した完全なコードは以下の通りです。 //jshint esversion:6 const express = require("express"); //const bodyParser = require("body-parser"); ←もういらない const app = express(); const port = 3000 //app.use(bodyParser.urlencoded({extended: true})); ←bodyParserを下記の通りexpressに変更 app.use(express.urlencoded({ extended: true })); //途中略 app.post("/", (req, res) => { var num1 = Number(req.body.num1); var num2 = Number(req.body.num2); var result = num1 + num2; res.send("The result of the calculation is " + result); }) //以下略 最後に 前述したとおり、VS Codeからのメッセージを無視して、実行できちゃうんですが私はちょっとそれを無視するのが気持ち悪かったので、なんとかできないかなと検索を始めたら理由がわかりました。 スッキリできてよかったです。 恥ずかしながら、調べる前は、「まあ、動いているし無視していっかw」と思ってました。 こんな風に考えても解決しないことはすぐにググって解決していくスタイルが、結果的に学びが多くなる気がしたので、続けていこうと思います。(そしてこんな風に学んだことをアウトプットすることも。) 参考にさせていただいたサイトたち Medium Express JS— body-parser and why may not need it StackOverFlow bodyParser is deprecated express 4 Grepper “bodyparser is deprecated” Code Answer’s フロントエンド強化月間参加中!! またQiitaのフロントエンド強化月間にも参加中です! なんというタイミング...これを機にフロントエンド技術を一気に学んでいきます。 コチラ→Qiita フロントエンド強化月間
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発環境の準備とか

急な事態、、、 急な状況なので、HTML、CSS等を学んでいきます。Qitaの練習も兼ねています。 開発環境の準備1 開発環境を準備します。 ダウンロード 今回は簡単な内容ですのでテキストエディタでも充分なのですが、今回はVisual Studio CODEを利用します。普段使用しているVisual Studioとはちょっと違いますのでご注意ください。 上記リンク先よOSに応じたソフトをダウンロードし、インストールなどしてください。 ソフトの起動と日本語化 Visual Studio CODEを起動します。 おっと、英語メニューですね。 ここで逃げ出したい気持ちはよく分かるのですが、Visual Studio CODEはちょっとひと手間加えると日本語化できるらしいっす。 Visual Studio CODEの日本語化 アプリケーションウィンドウの右下、歯車のアイコンをクリックします。 その中にExtensions(機能拡張の意味)が表示されるのでクリックします 左上にExtensionの検索フィールドが出ますのでlanguageと入力してください Japanese Language Packが候補に出てきますので、そこのInstallボタンを押します Restart(再起動)するか聞いてきますので、Restartをクリックします。 日本語化されました 開発環境の準備2 XAMPPの準備 実行環境用として、XAMPPをダウンロード&インストールします。 XAMPPは車校みたいなもの 車校(東海地区では自動車学校のことをこう呼ぶ)では、まず敷地内で運転の練習をしてから路上教習に出ますよね。XAMPPはその車校みたいなもんです。 いきなりサイトを公開するんじゃなくてXAMPPの中でテストしてから本番サーバで公開という流れをとったりします。なので、XAMPPはみなさんのパソコンの中にサーバ(ローカルサーバ)を作ってテストする環境を作るツールです。 XAMPPダウンロード&インストール Windows環境が無いので、こちらを参考にインストールしてください。(外部サイトが立ち上がります) 余談 余談ですけどHTML、CSS、JavasScriptのテストであればXAMPPは無くてもいいかもしれません。パソコン内のブラウザ(Chromeとか)でテストできます。XAMPPを入れるのは、サーバが必用なPHPやSQLをいじってみたいからですね。 ここまでインストールしたら、次のステップにすすみます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】nuxt.config.jsに何が書かれているのか

nuxtの設定ファイル「nuxt.config.js」 nuxt.config.js では、下記のようにオプションを設定することができます。 export default { オプション: 値 } head このオプションで設定した値が、アプリケーション全体のメタタグに反映されます。 export default { head: { titleTemplate: '%s - Nuxt.js', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: 'Meta description' } ] } } metaタグって? 記事コンテンツの情報を検索エンジンやブラウザに伝えるための情報です。使用言語や文字エンコーディング、キーワードや説明文などについて記述しており、HTMLのhead要素内に配置されるタグのことです。 外部リソース読みこみもここで 第二引数以降に、詳細を指定できる。 async:非同期 defer:遅延 body:</body>の直前に追加 export default { head: { script: [ { src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js', async: true, defer: true, body: true } ] } } Headに指定できるオプション Nuxt.js公式 - head build ここで webpack や loaders など build 時の様々な設定を行うことができます。 import Sass from 'sass' import Fiber from 'fibers' build: { //略 loaders: { scss: { implementation: Sass, sassOptions: { fiber: Fiber, } } } } ここではサンプルとして、 Dart Sass という Sass のコンパイラの設定を書いてます。 fibers はコンパイル速度を向上させるためのライブラリです。 Nuxt.js公式 - build CSS グローバルに、全てのページで利用したい CSSファイルやCSSモジュールを指定します。 export default { css: ['~/assets/css/main.scss', '~/assets/css/animations'] } また、配列の2番目のように、 .css .scss といった拡張子を省いてもOKです。 plugins アプリケーションの初期化前にインポートしたい場合にここに記述。 plugins/ 配下に該当ファイルを置いて、ここに追加します。 引数を指定したい場合、オブジェクト形式で。 mode: 'client' ならクライアント側でのみファイルが読み込まれます。 export default { plugins: [ { src: '~/plugins/both-sides.js' }, { src: '~/plugins/client-only.js', mode: 'client' } { src: '~/plugins/server-only.js', mode: 'server' } ] } Nuxt.js公式 - plugins router nuxtで自動設定されたルートに変更を加えたいときに使います。 export default { router: { extendRoutes(routes, resolve) { routes.push({ name: 'Not Found', path: '*', component: resolve(__dirname, '~/pages/errors/404.vue'), }) } } } ルートを拡張したいときには、 extendRoutes() メソッドが準備されています。 Nuxt.js公式 - router srcDir このオプションで、nuxtアプリケーションの「ソースディレクトリ」を設定します。 export default { srcDir: 'app/' } この設定では、app/ 配下に components 、 pages、 plugins などの主要ファイル郡を設置するといった意味です。 Nuxt.js公式 - srcDir server アプリケーションのサーバー接続用の変数を nuxt.config.js 内に定義できます。 export default { server: { port: 8000, // デフォルト: 3000 host: '0.0.0.0', // デフォルト: localhost, timing: false } } server.timing オプションを有効にすると、サーバーサイドレンダリング中に経過した時間を計測するミドルウェアが追加され、'Server-Timing' としてヘッダーに追加されます。 Nuxt.js公式 - server 参考文献 Nuxt.js 公式 - nuxt.config Headに指定できるオプション
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】nuxt.config.jsに何が書かれているのか調べた

nuxtの設定ファイル「nuxt.config.js」 nuxt.config.js では、下記のようにオプションを設定することができます。 export default { オプション: 値 } head このオプションで設定した値が、アプリケーション全体のメタタグに反映されます。 export default { head: { titleTemplate: '%s - Nuxt.js', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: 'Meta description' } ] } } metaタグって? 記事コンテンツの情報を検索エンジンやブラウザに伝えるための情報です。使用言語や文字エンコーディング、キーワードや説明文などについて記述しており、HTMLのhead要素内に配置されるタグのことです。 外部リソース読みこみもここで 第二引数以降に、詳細を指定できる。 async:非同期 defer:遅延 body:</body>の直前に追加 export default { head: { script: [ { src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js', async: true, defer: true, body: true } ] } } Headに指定できるオプション Nuxt.js公式 - head build ここで webpack や loaders など build 時の様々な設定を行うことができます。 import Sass from 'sass' import Fiber from 'fibers' build: { //略 loaders: { scss: { implementation: Sass, sassOptions: { fiber: Fiber, } } } } ここではサンプルとして、 Dart Sass という Sass のコンパイラ(npmインストールしたパッケージ)の設定を書いてます。 fibers はコンパイル速度を向上させるためのものです。 Nuxt.js公式 - build CSS グローバルに、全てのページで利用したい CSSファイルやCSSモジュールを指定します。 export default { css: ['~/assets/css/main.scss', '~/assets/css/animations'] } また、配列の2番目のように、 .css .scss といった拡張子を省いてもOKです。 plugins アプリケーションの初期化前にインポートしたい場合にここに記述。 plugins/ 配下に該当ファイルを置いて、ここに追加します。 引数を指定したい場合、オブジェクト形式で。 mode: 'client' ならクライアント側でのみファイルが読み込まれます。 export default { plugins: [ { src: '~/plugins/both-sides.js' }, { src: '~/plugins/client-only.js', mode: 'client' } { src: '~/plugins/server-only.js', mode: 'server' } ] } Nuxt.js公式 - plugins router nuxtで自動設定されたルートに変更を加えたいときに使います。 export default { router: { extendRoutes(routes, resolve) { routes.push({ name: 'Not Found', path: '*', component: resolve(__dirname, '~/pages/errors/404.vue'), }) } } } ルートを拡張したいときには、 extendRoutes() メソッドが準備されています。 Nuxt.js公式 - router srcDir このオプションで、nuxtアプリケーションの「ソースディレクトリ」を設定します。 export default { srcDir: 'app/' } この設定では、app/ 配下に components 、 pages、 plugins などの主要ファイル郡を設置するといった意味です。 Nuxt.js公式 - srcDir server アプリケーションのサーバー接続用の変数を nuxt.config.js 内に定義できます。 export default { server: { port: 8000, // デフォルト: 3000 host: '0.0.0.0', // デフォルト: localhost, timing: false } } server.timing オプションを有効にすると、サーバーサイドレンダリング中に経過した時間を計測するミドルウェアが追加され、'Server-Timing' としてヘッダーに追加されます。 Nuxt.js公式 - server 参考文献 Nuxt.js 公式 - nuxt.config Headに指定できるオプション
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Docker開発環境でJavaScriptの変更がブラウザに反映されない問題の解決法

概要 Railsアプリの開発環境をDockerに移行したところ、JavaScriptの変更がブラウザに反映されなくなりました。その解決法についての備忘録です。 私自身、完全には理解できていないため、もしかしたら間違った説明があるかも知れません。 その際は、ご教示いただけましたら幸いです。 開発環境 macOS Big Sur バージョン 11.2.3 Ruby 2.6.5 Rails 6.0.3.4 MySQL 5.6.47 Docker上の開発環境でアプリを作成中です。 参照 ①結果としてこちらの質問者さんの方法で解決できました。 ②Dockerfileにyarnインストールの追記をしただけではうまく行かず、こちらの記事を参考にyarnのバージョンを指定する追記もしたところ、解決できました。 ③yarn公式サイトに掲載されているバージョンを指定したyarnインストール方法はこちらです。 解決方法 下記の①、②を追記し、docker-compose buildする。 これで、JavaScriptの変更がブラウザに反映されるようになりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsを理解するために

#概要 Vueの構成要素を理解するためにVueの構成要素とディレクティブ、算出プロパティ、メソッドについてわかりやすく記載しました。 Vue.jsの構成 基本的な、Vue.jsの構成についての復習です。 main.js let app = new Vue({ el:'#app', data:{ message:'こんにちわ!' } }); 1,インスタンス化 main.js let app =new Vue Vue.jsを起動するためには、Vueクラスをインスタンス化する必要があります。 letで宣言していますが、生成したインスタンスを後から参照する必要がなければ必要ありません。 2,動作オプション Vue(オプション)動作オプション HTMLから参照できる値を格納オブジェクトです。プロパティ名:値とします。 el:Vueの適用要素 data:データオブジェクト message:dataオブジェクトのmessageプロパティです。データは上記ではこんにちわです。 HTMLの構成 index.html <div id="app"> <h1>{{ message }}</h1> </div> id属性▶Vueの適用範囲です。 {{}}▶マスタッシュ構文。テンプレートからデータオブジェクトをにアクセスします。複雑な条件分岐などは出来ません。 Vuejsを理解するために ①ディレクティブ イベントを表します。[v-]から始まります。 マスタッシュ構文は式の値を埋め込むことが出来ません。 そこで、属性値の操作はv-bindを使用します。 main.js data:{ url:'https://qiita.com/akari_0618/items/5a9e9d565b47c5de2e4d' } データオブジェクトにurlプロパティを追加します。 index.html <div id="app"> <a v-bind:href="url">GO</a> </div> v-bind:属性名=値 ディレクティブ名と属性名はコロンで区切ります。 コロンの後方はディレクティブの引数です。 ②ブール属性 属性名を指定するだけで意味がある属性のことを論理属性またはブール属性といいます。これらの値を紐付けるためにはtrue,falseで表します。 index.html <input type="button" value="クリック" v-bind:disabled="flag"> main.js flag:false ③算出プロパティ 算出プロパティとは、既存のプロパティを演算した結果を取得するためです。 算出プロパティの元では、[this.プロパティ名]でアクセスすることができます。 定義側はメソッドですが、参照はプロパティです。 index.html <p>{{loEmail}}</p> main.js data:{ email:'suzuki-r.e @ddddd.ne.jp' }, computed:{ loEmail:function(){ return this.email.split('@')[0].toLowerCase(); } } *split(r) 正規表現が一致する場所で文字列を分割して配列にします。 ④メソッド 上記の算出プロパティはメソッドでも表すことが出来ます。 ただし、methodで表した場合は index.html <p>{{loEmail()}}</p> プロパティでは無いため、()で呼び出します。 methodとは、Vue内で扱える機能を定義する場所です。 他の関数から呼び出したり、HTML内で呼び出すことが出来ます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

google calendar上の予定をslack通知させて予定の守れるエンジニアになりたい!

背景 これは、チーム内のKPTでProblemの洗い出しをしているときに「自分最近作業に集中しちゃってmtg遅刻しちゃってるな」と感じたので、戒め & 自分の意識付けのために書いたチケットです。(チームのProblemじゃないじゃん、というところは目をつぶっていただいて) ただその意識も永久的に続くわけではないだろうなと思ったのでどうせならシステムで解決したい! ということで、 Google Calendarに上がっているmtg予定の1分前になったらslackにメンションを飛ばしてくれる仕組みを作りました。 実は... [5/5 14:20 追記] 知ってる方もいるかも知れませんが、slackにはgoogle calendarのインテグレーションが備わっており、 こちらを導入するだけで簡単に 1分前にslack通知してくれる 機能は実現できるのですが、 今回自作するに至った理由は、このスクショのように メンションの設定ができない フォーマットのカスタマイズができない このタイトルについてるリンクをカスタマイズできない(google calendarの予定に飛んでしまう) mtgの参加者やアジェンダなどの表示ができない といったことができず、mtgのリマインダとして利用するにはちょっと情報量不足・要件が満たせないと感じたためです。 もしこの情報量で十分だという方は黙ってこれを導入しちゃうのが一番だと思います。 special thx) https://twitter.com/sakichi01_/status/1389734107848921088?s=20 とりあえず成果物 ということで成果物はこちら。 インテグレーションで実現できなかったメンションやアジェンダ、参加者情報の記載が出来るようになりました。 以下のソースコードをgasに入れて、トリガーを1分毎に回しています。 const calendarId = 'calendarId@group.calendar.google.com'; const channel = '#channel_name'; const mention = '@sakas1231'; const username = '通知くん'; const icon_emoji = ':ghost:'; const incomingHookUrl = 'https://hooks.slack.com/services/webhook_url'; const tagName = 'notifired'; // slackリマインドさせる時刻 // e.g.) 1なら1分前に通知 const PRE_NOTI_MINUTES = 1; const isNotifiableEvent = event => !event.isAllDayEvent && !event.isNotifired; const buildEventObject = event => { // よしなにeventをparseしてプロパティを埋める const url = 'xxx'; const members = 'xxx'; return { isAllDayEvent: event.isAllDayEvent(), startTime: moment(event.getStartTime().getTime()), endTime: moment(event.getEndTime().getTime()), title: event.getTitle().replace('\n', ''), description: event.getDescription(), url, members, isNotifired: !!event.getTag(tagName), } }; const buildSlackBlock = eventObj => { const restTotalSeconds = eventObj.startTime.unix() - moment().unix(); const rest = { seconds: restTotalSeconds % 60, minutes: parseInt(restTotalSeconds / 60, 10), } return { channel, mention, icon_emoji, username, link_names: 1, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `${mention} あと *${rest.minutes}分${rest.seconds}秒* でmtgだよ` } }, { type: 'section', text: { type: 'mrkdwn', text: `*<${eventObj.url}|${eventObj.title}>*` } }, { type: 'section', text: { type: 'mrkdwn', text: `*説明:*\n${eventObj.description}` }, }, { type: 'section', fields: [ { type: 'mrkdwn', text: `*参加者:*\n${eventObj.members}` }, { type: "mrkdwn", text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}` }, ] } ] } }; const notification = event => { const jsonData = buildSlackBlock(event); const payload = JSON.stringify(jsonData); const options = { method : 'post', contentType : 'application/json', payload, muteHttpExceptions: true }; const res = UrlFetchApp.fetch(incomingHookUrl, options); return res.getResponseCode(); }; function main(){ // カレンダーから予定を取得する const cal = CalendarApp.getCalendarById(calendarId); const events = cal.getEventsForDay(new Date()); const now = moment().unix(); events.forEach(event => { const eventData = buildEventObject(event); // 通知する必要ないイベントの場合はcontinue if (!isNotifiableEvent(eventData)) { return; } // 通知時刻 const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix(); if (eventNotiTime > now) { return; } // ステータスコードが400未満でsetTag if (notification(eventData) < 400) { event.setTag(tagName, 'true'); } }); } 説明 const cal = CalendarApp.getCalendarById(calendarId); const events = cal.getEventsForDay(new Date()); Google App ScriptではGoogle CalendarのAPIを利用できるクラスが用意されており、それを利用しています。 events.forEach(event => { const eventData = buildEventObject(event); // 通知する必要ないイベントの場合はcontinue if (!isNotifiableEvent(eventData)) { return; } // 通知時刻 const eventNotiTime = eventData.startTime.clone().add(-1 * PRE_NOTI_MINUTES, 'm').unix(); if (eventNotiTime > now) { return; } // ステータスコードが400未満でsetTag if (notification(eventData) < 400) { event.setTag(tagName, 'true'); } }); それぞれの予定毎に通知対象となる(開始1分前)ような予定をピックアップしてslack通知させています。 通知が完了した予定には event.setTag(key, value) を用いてmetadataを付与することで 通知が完了したイベントの再送をさせない 通知ができなかったイベントは次のトリガーで通知を飛ばす ようにしています。 もっと正確にやるのであれば、 event.setTag(key, value) の部分でリトライ処理などを入れても良いかもしれません。 const notification = event => { const jsonData = buildSlackBlock(event); const payload = JSON.stringify(jsonData); const options = { method : 'post', contentType : 'application/json', payload, muteHttpExceptions: true }; const res = UrlFetchApp.fetch(incomingHookUrl, options); return res.getResponseCode(); }; slackのincoming webhookURLを用いてslack通知をさせています。 中身の構造作成は別関数でやっています。 UrlFetchApp.fetch の optionに muteHttpExceptions: true をつけるとエラー系のステータスコードも取得できるので有効にしています。 const buildSlackBlock = eventObj => { const restTotalSeconds = eventObj.startTime.unix() - moment().unix(); const rest = { seconds: restTotalSeconds % 60, minutes: parseInt(restTotalSeconds / 60, 10), } return { channel, mention, icon_emoji, username, link_names: 1, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: `${mention} あと *${rest.minutes}分${rest.seconds}秒* でmtgだよ` } }, { type: 'section', text: { type: 'mrkdwn', text: `*<${eventObj.url}|${eventObj.title}>*` } }, { type: 'section', text: { type: 'mrkdwn', text: `*説明:*\n${eventObj.description}` }, }, { type: 'section', fields: [ { type: 'mrkdwn', text: `*参加者:*\n${eventObj.members}` }, { type: "mrkdwn", text: `*時間:*\n${eventObj.startTime.format("HH:mm")} - ${eventObj.endTime.format("HH:mm")}` }, ] } ] } }; slack通知の構造を作成しています。 Block Kit Builderを用いると簡単に雛形の作成ができるのでおすすめです。 ちなみにしれっと今回は諸事情でmoment.jsを使用しましたが、moment.jsはレガシープロジェクトとして使用が非推奨となっていますのでその他のライブラリを使用するほうが適切です。 その後 まだ実運用で試してませんが、これでおそらく自分のmtg遅刻は解消されるはずです。 もし、mtg遅刻してしまうなんていう問題に同じく悩まされているのであればぜひお試しください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Marketoフォームのメールアドレス入力欄がフリーアドレスの場合に送信を制限する

Marketoフォームのメールアドレス入力欄がフリーアドレスの場合に送信を制限する 始めに Webサイトのフォーム入力には何かしらのノイズが混じることがあります。フォームの種類や目的によってノイズの種類は異なりますが、例えば以下のようなノイズがあります。 営業目的 競合他社による情報を探る目的 悪意のあるフォーム入力(サーバー負荷など) フリーアドレス(BtoBの場合) 「フリーアドレスでのフォーム入力」は、『リード獲得』の意味はありますが、ナーチャリングを考えたときに継続的なコミュニケーションにつながる可能性は低いため、ビジネス利用のメールアドレスを入力していただく必要があります。 コミュニケーションにつながる可能性を高めるためにも、フリーメールでのフォーム入力の場合はマーケティングオートメーション(以下、MA)によってフォーム入力完了後に再度フォーム入力を促す自動配信メールを行うことはありますが、2度目のフォーム入力が行われる可能性は低いのが現状です。すなわち、1度目のフォーム入力の際にビジネス利用のメールアドレスを入力してもらう必要があります。 これらを満たすためにMAの『Marketo』と、Googleタグマネージャ(以下、GTM)を利用した、Marketoフォームのメールアドレス入力欄がフリーアドレスの場合に送信を制限する(フォーム入力を完了させない)方法について、以下Marketo開発ブログを参考に実装を行いましたのでご紹介します。 ※ソースコードは以下Marketo開発者ブログを引用させていただきました。 Restrict Free Email Domains on Form Fill Out(公開日:2014年9月9日) https://developers.marketo.com/blog/restrict-free-email-domains-on-form-fill-out/ 実装手順 以下手順によりJavaScriptコード(以下、JS)をMarketoフォームへ実装することで、目的とする動作を実現します。 除外するメールアドレスを棚卸し JSコードの該当箇所にメールアドレスを記入 JSコードの該当箇所にフォーム送信拒否メッセージを記入 MarketoフォームにJSコードを反映 実装するJSコードは以下です。 .js <script> (function (){ // 2.以下[“▲▲”]に除外対象のメールアドレスを記入 var invalidDomains = ["@gmail.com","@yahoo.co.jp","@outlook.jp"]; MktoForms2.whenReady(function (form){ form.onValidate(function(){ var email = form.vals().Email; if(email){ if(!isEmailGood(email)) { form.submitable(false); var emailElem = form.getFormElem().find("#Email"); //3.以下にフォーム送信拒否メッセージを記入 form.showErrorMessage("フリーメールは登録できません", emailElem); }else{ form.submitable(true); } } }); }); function isEmailGood(email) { for(var i=0; i < invalidDomains.length; i++) { var domain = invalidDomains[i]; if (email.indexOf(domain) != -1) { return false; } } return true; } })(); </script> 1. 除外するメールアドレスを棚卸し どのテキストがメールアドレスに含まれていたらフォーム送信を制限するのか、制限対象とするメールアドレスの棚卸しを行います。 弊社ではフリーアドレスを除外したいため「@yahoo.co.jp」や「@gmail.com」などが該当のドメインです。 注意点として、アットマーク「@」を含まずに除外対象とした場合、ドメインパート(「@」以下のメールサーバーを表す箇所)によっては意図していないドメインも制限されてしまう可能性があります。逆にアットマーク「@」以下のドメインパートがサブドメイン.ドメインとなるパターンもあるため、注意が必要です。 2. JSコードの該当箇所にメールアドレスを記入 以下JSコードの箇所に、1.で棚卸しを行ったメールアドレスを、2重引用符「”」で括り、メールアドレスごとにカンマ「,」で区切ります。 var invalidDomains = ["@gmail.com","@yahoo.co.jp","@outlook.jp"]; ここで制限対象とするメールアドレスを指定します。 3. JSコードの該当箇所にフォーム送信拒否メッセージを記入 以下JSコードの箇所に、実際に送信ボタンを押下した際にメールアドレス入力欄に表示するメッセージを記入します。例えば「フリーメールは登録できません」や「会社利用のアドレスを入力してください」などです。 form.showErrorMessage("フリーメールは登録できません", emailElem); 4. MarketoフォームにJSコードを反映 Marketoのランディングページテンプレートに該当のJSコードを記述します。共通のランディングページテンプレートを使っており、該当箇所を限定させたい場合は、GTMで配信対象を限定することをおすすめします。 以下GTMでの設定手順になります。 GTMを開き、新規トリガーを作成 トリガータイプ「ページビュー」、トリガー発生場所「一部のページ」、対象ページを指定 ⇒ [ 保存 ] 新規タグを作成 タグの設定をクリックし「カスタムHTML」を選択 HTML記載箇所に、上記編集したを含むソースコードを記述 トリガーに先程作成したトリガーを指定 ⇒ [ 保存 ] ページ右上の「プレビュー」より、設定したスクリプトが動作するかを確認 ※プレビュー選択後、対象ページをリロードすることで、GTMより配信されているタグをプレビュー画面で確認できます。 プレビューを終了し、『公開』を行う 以上が設定手順です。 最後に 注意点として、ブラウザのJSを無効にすると動作しなくなります。また、ブラウザのキャッシュなどによってはJSが動作せずにフォームを通過する場合があります。 完璧な制御が難しいため、フォーム入力後に再度入力を促すなどの回避策を用意しておく必要があります。 不要なリード作成によるデータベースの圧迫を避けることもできますので、興味のあるMarketo導入済み企業様は実装してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む