20210910のJavaScriptに関する記事は25件です。

Firebaseのバージョンによるimport記法

こちらにまとめてみました。 Ver7以降を対象にしてあります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryのclickがiPhoneで発火しないと思ったらasyncが原因だった話

はじめに こんにちは。ハリモグラ(Tachyglossus aculeatus)です。 こう見えてカモノハシと同じく原始的な哺乳類の仲間で、よくハリネズミに似ていると言われます。 ふだんはオーストラリアの草原でのんびりと地中の虫を探しながら過ごしていますが、 ひょんなことからweb屋さんの世界に興味を持ち、自分のサイトを作っています。 今回、サイトを作っていたらある問題にドツボにハマってしまったので、 誰かが同じ轍を踏まないようにQiitaに投稿することにしました。 初投稿です。よろしくお願いいたします。 環境 メインPC:Windows 10 Home ver.10.0.18363.1556(Docker Desktop, VSCode) メイン携帯:iOS 14.3 (iPhone 12 Pro Max) サブ携帯:Android 11 (UMIDIGI A9 Pro) 結論 目的:モバイル専用フッターをクリック/タップしたらメニューを表示するようにしたい 事案:jQueryのClickイベントで発火するプログラムがiPhone実機でのみ動作しない 原因:asyncによりプラグインを遅延読み込みしていたため、プラグインが定義した関数が読み込めずにJS全体が止まっていた 教訓:asyncは使わない。せめてdeferを使う。webインスペクタは大事。ある事象を疑うときは、検索する前にそれを内包する上位の事象があればそれを先に検証する。 経過 では改めて経緯を説明していきましょう。 私は、スマホ版webサイトのユーザーエクスペリエンス(?)を向上させたいと思い、 フッターのボタンを押したら横からメニューがシュッっと出てくるやつ(語彙力) を実装することにしました。 私はjavascript(jQuery)はほとんど無学なので、 初心者用のサイト 1 を参考にさせてもらいながらコツコツと作りました。 その仕組みは単純で、まずメニューを見えない場所に指定するためのクラスAと、 表示されているときの座標を指定したクラスBを準備して、 メニューにはあらかじめクラスAを指定しておきます。 そしてボタンが押されたらメニューにクラスBを付与するようにします。 そうすると、CSSの働きによってメニューはAの指定位置からBの指定位置までシュッと動きます。 これをプログラムで表現すると以下のようになります。 CSS #nav { position: fixed; z-index: 2; top: 0; height: 100vh; transition: all cubic-bezier(0.165, 0.84, 0.44, 1) 0.8s; overflow: auto; right: -100%; } #nav .active { right: 0; } JavaScript $(function(){ $(".button").click(function(){ $("#nav").toggleClass('active'); }); }); HTMLは省略しますが、buttonがメニューを開閉するボタン、navがメニューです。 CSS3が無かった昔はなんかもっと複雑なことをして実現していた気がしますが、 技術の進歩は素晴らしいもので最近はたったこれだけでスライドメニューを実装できます(すごい) さて、さっそくこれをモバイル環境から確認してみましょう。 手元のスマホで開発中のページを確認したい場合、 コマンドラインからipconfigと叩いて、 出てきたIPアドレスのうちデフォルトゲートウェイが存在するイーサネットのIPアドレスをスマホに入力します。 (PCとスマホが同じネットワークに接続されている必要があります) PCとAndroidでは無事に動作しました。しかし、iPhoneではボタンを押しても反応がありません。 試しに、alert('動けー'); を追記してみてもアラートが出てこないので、 そもそも $(".button").click(function(){}); がiPhoneでのみ発火していないのだという発想に至りました。 そこで、「jquery click iphone 動かない」等で検索してみると、次の解決策に行きつきました。 clickメソッドでは動かない。on("click touchstart")メソッドを使うべし。 親要素にdocumentやbodyを指定していると動かない。対象の親要素をピンポイントで指定すべし。 クリックする対象に cursor: pointer; を指定すべし。 いずれも、引用元のサイトだけでなく複数のサイト 2345 で見られた知見です。 また、②に関しては真逆のことを言っているサイトも散見されます。 iOSのバージョンの違いによるものでしょうか。 ということで実際にいろいろやってみましたが、うまく動きません。 ①は実装するとPCやAndroidで挙動が不安定になってしまう上にiPhoneでは動かず、 また②と③を実装してみても、うんともすんとも動きません。 そもそも③なんておまじないレベルだと思うのですが、本当に効果があるのでしょうか……。 などと訝しがっても仕方ないので、さんざん悩んだあげく別の角度から検証することにしました。 逆に、どこからならiPhone上でjavascriptが動作するのか。 まず$(document).ready(function(){alert("hoge");});を試してみますが、反応がありません。 なるほどそもそもjQueryが動いていないんだな。 念のためwindow.onload = function(){alert("fuga");};も試したところ、これも動きません。 jQueryどころかjavascriptそのものが動いていない。しかもiPhoneだけ。 これはiPhone上のブラウザでなんらかのFatal Errorが発生しているのだと思い、 どうにかしてそのエラーの内容を見たいと思いました。 それで「iphone webインスペクタ」で調べてみると、 iPhone→設定app→Safari→詳細→webインスペクタのトグルをONにすることによって、 パソコン上からwebインスペクタを確認できるとの情報が。 ただしMacのみ。な、なんだってー Windowsでもwebインスペクタを見る方法はないのかと、すがる思いで検索してみると、 ていねいに解説している個人ブログ様 6 を見つけました。ありがたや~ その方法は、まず管理者権限でWindows Powershellを開きます。 scoopをインストールします。scoopはCentOSでいうところのyumです。 WindowsPowershell $ Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force $ iwr -useb get.scoop.sh | iex Gitをインストールします。 WindowsPowershell $ scoop install git アプリをインストールできるように設定します。 WindowsPowershell $ scoop bucket add extras iOS WebKit Debug Proxy 7 をインストールします。 WindowsPowershell $ scoop install ios-webkit-debug-proxy Node.jsをインストールします。 WindowsPowershell $ scoop install nodejs npmでRemoteDebug iOS WebKit Adapter 8 をインストールします。 WindowsPowershell $ npm install remotedebug-ios-webkit-adapter -g PCにiPhoneを有線接続し、下記コマンドでアダプターを起動します(停止するときはCtrl+C)。 警告が出てきたらすべて許可を選択します。 WindowsPowershell $ remotedebug_ios_webkit_adapter --port=9000 Google Chromeを開き、 chrome://inspect/#devices にアクセスします。 Discover network targetsの隣にあるConfigure...をクリックします。 localhost:9000 を追加してDoneをクリックします。(設定ページは開いたまま) iPhone上のSafariで確認したいサイトを開きます。 するとGoogle Chromeで開いている設定ページの「Remote Target」に表示しているサイト名が表示されます。 クリックするとそのサイトのリモートインスペクタが起動します。 iPhone上のSafariで一度ページをリロードします。 エラーが出てきた! なになに、「Uncaught TypeError: $(...).slidein is not a function」……。 slideinという関数が見つかりませんと言っていますね。そんなのjQueryにあったっけ? 検索してみると2012年に作られた古いプラグインがヒットしました。 自分のサイトに戻ってHTMLのヘッダーを確認してみると、確かにそのプラグインをロードしている。 HTML <head> <script type="text/javascript" src="./js/jquery.slidein.js" acync></script> </head> ん? よく見ると「async」というオプションがくっついてる。 これは確か、遅延読み込み(非同期ローディング)のためのオプションだったはず。 はっ。もしかして、プラグイン本体を後に読み込んでいるため関数が迷子になっているのでは!? 事件は現場で起きているんじゃない! ヘッダーで起きているんだ! ということでasyncを取ったらあっさりiPhoneでも動きました。やったー asyncは、ページの読み込みとは別に遅延して外部ファイルを読み込むオプションですが、 その際に読み込むファイルの順番は考慮されないそうです。9 (へー知らなかった) 順番に、かつ非同期に読み込みたい場合はdeferというオプションを使います。 ある外部ファイルに関数が定義されていて、 その関数を別のファイルで使っている場合は読み込む順番も考慮しないといけないわけですね。 そして、AndroidやPCではその辺はうまいこと忖度されて読み込まれていたのでエラーは出ないものの、 iPhoneにおいては狂った順番のままロードしてしまうため、エラーが出ていたと……。 そのせいで初歩的な不具合であるにも関わらず今までずっと気づかずにいました。 (いままでiPhone実機で確認していないことがバレてしまった) 普通に検索するとjQueryの記法やiPhoneの仕様に関する知見が出てくるので、 よもやヘッダーに原因があるとは思いもしませんでした。 *  *  * 以下は私見というか蛇足です。 最近の検索エンジンは優秀なので、 あいまいなキーワードを入力してもそれっぽい答えに容易にたどり着くことができます。 しかしその「それっぽい答え」が自分にとって都合の良い情報だったりすると、 そもそも検索キーワードという前提が誤っているかもしれないという可能性に対して盲目になりがちです。 そして「それっぽい答え」が実は役に立たない情報であったとしても、 有用性の無さに気づくのはなかなか容易ではありません。(縋るものがそれしかないからです) 私は、プログラミングにおける不具合の特定は、 土の中に埋め込まれた宝物を探すようなものだと思っています。 あるいは、最近だと「アキネイターのようなもの」といったほうが適切かもしれません。 地面全体を原因(宝物)があるかもしれない可能性の総体として捉えたとき、 ある部分だけをピンポイントに掘って宝物を探すのは、あまり効率的とは言えません。 ではどうするべきなのか。 今回の場合は、まずjQueryのclickメソッドがうまく働かないということにまず気づきました。 しかしそのあと、jQueryそのものがうまく働いていないことに気づき、 さらにjavascriptそのものがうまく働いていないことに気づきました。 clickメソッドはjQueryの動作を前提とし、jQueryはjavascriptの動作を前提としているので、 この場合は javascriptがjQueryを内包し、jQueryがclickメソッドを内包しています。 より下位に内包された可能性は、それ自体に問題がある場合にかぎらず、 それを含む上位の可能性にエラーが起きたときにも正常に動かなくなることが間々あります。 しかしもしも上位に原因があったとしたら、下位にいくら原因を求めても解決することはありません。 したがって、より広い面積の可能性から検証していったほうが合理的であると言えます。 可能性を切り分けて、こっちには確実に宝物は無いという確信のもと、 残っている可能性をどんどん狭くしていくわけです。 今回の案件は、clickメソッドではなく、まずそれを含むjavascriptそのものの動作から疑っていれば ここまで難航することもなかったと思います。その意味で、とても良い教訓になりました。 生粋のプログラマーにしてみれば当たり前すぎてなんてことのない話なのかもしれませんが、 私と同じような初学者にはこういった概念も意外と時短のために大切だったりするのではないでしょうか。 ここまでお読みいただき、ありがとうございました。 動くwebデザインアイデア帳 ↩ iOS javascript clickイベントが効かないにはまる。 - かもメモ ↩ 【iOS】実機のiOSだとクリックイベントが動作しない件でハマった【Safari】 - Qiita ↩ iOS12以前のmobile Safariではwindow、document、bodyにclickイベントを設定しても発火しない - みかづきブログ・カスタム ↩ iOS(iPhone Safari)でclickイベントが発生しない場合の対処方法 - てらこや.work ↩ WindowsでiOSのSafariで開いたページをデバッグする - Web活 ↩ A Dev Tools proxy for iOS devices - Github ↩ Debug Safari and WebVies on iOS - Github ↩ <script> タグに async / defer を付けた場合のタイミング - Qiita ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コントローラーで定義した値をJSに受け渡す

開発環境 OS:macOS Big Sur 11.2.2 Ruby:2.6.5 Ruby on Rails:6.0.0 テキストエディタ:Visual Studio Code つまづいたこと 投稿の詳細画面から非同期通信でDB更新をしたいと考えたときに、更新に必要な値を更新アクションを定義しているコントローラーに渡せず詰まった。 コントローラーからビューファイルに値を渡すときはインスタンス変数を定義するが、JavaScriptへはどうしたらいいのだろう?と思い調べてみた。gonというgemの導入により実現できるらしい。 実践したこと gonは簡単にJSファイルにデータを受け渡すために作られたgemらしい。(と、いうことは難しい手順を踏めば使わなくてもできるということか・・・) 使い方は、 ①まずgemファイルにgonを定義 gem 'gon' ②gemをインストール % bundle install ③includeの記述を追加 下記の通り、headのapplication.js読み込みより前に追加する application.html.erb <head> ~ <%= include_gon %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> ~ </head> これで準備は完了。あとは、 ④受け渡したい値を「gon.〜」という名前で変数定義する recipes_controller.rb def show # JSに渡す変数を定義 gon.receipe_id = @recipe.id end ⑤受け取ったJSファイルで使用 clip.js // clipコントローラーへのリクエスト const XHR = new XMLHttpRequest(); const URL = `/recipes/${gon.receipe_id}/clips` XHR.open('POST', URL, true); XHR.responseType = "json"; XHR.send(); 以上。意外と簡単にできる。 gonのその他の機能について せっかくなので公式ドキュメントを見ていたら、`「gon.watch.〜」という指定の仕方を見つけた。 どうやら値の一定間隔での置き換え・リフレッシュに使う模様。 今回は利用しなかったが、更新したDBのカウントを定義して、リアルタイムでカウント表示したりというときに使えそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dialogタグを使ってモーダルウィンドウ

html5のdialogタグを使ってモーダルウィンドウ実装するとどうなるのか試してみたかった。 HTML クラス構文使って以下の通りに。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> </head> <body> <dialog id="dialog"> <p class="message"></p> <button>OK</button> <button>CANCEL</button> </dialog> <button id="btn1">btn1</button> <button id="btn2">btn2</button> <script> class Modal { constructor(dialog){ if (dialog.tagName!=='DIALOG') throw 'target is not DIALOG' if (dialog.classList.contains('modal-instance')) throw 'target is used' dialog.classList.add('modal-instance') this.dialog = dialog this._close = e => e.target.tagName==='BUTTON' && dialog.close(e.target.textContent) this._after = e => this.fn(e.target.returnValue, ...this.args) dialog.addEventListener('click', this._close, false) dialog.addEventListener('close', this._after) } open(msg='', fn, ...args){ if (typeof fn!=='function') return this.dialog.querySelector('.message').textContent = msg this.dialog.showModal() this.fn = fn this.args = args } } const modal = new Modal(document.getElementById('dialog')); const afterModal = (rv, btnId, a, b) => { if (rv==='CANCEL') return console.log(rv) switch (btnId) { case 'btn1': console.log(btnId, a+b) break case 'btn2': console.log(btnId, a*b) break } } document.getElementById('btn1').addEventListener('click', e => { modal.open('btn1 question', afterModal, 'btn1', 5, 5) }, false); document.getElementById('btn2').addEventListener('click', e => { modal.open('btn2 question', afterModal, 'btn2', 5, 5) }, false); </script> </body> </html> javascript解説 class Modal { constructor(dialog){ //利用したいDIALOGを指定 if (dialog.tagName!=='DIALOG') throw 'target is not DIALOG' //既に利用されているか確認 if (dialog.classList.contains('modal-instance')) throw 'target is used' dialog.classList.add('modal-instance') this.dialog = dialog //ダイアログ内のボタンを押した時の処理 //ボタンの文字列を取得 this._close = e => e.target.tagName==='BUTTON' && dialog.close(e.target.textContent) //閉じた後の処理。指定の関数を実行 this._after = e => this.fn(e.target.returnValue, ...this.args) dialog.addEventListener('click', this._close, false) dialog.addEventListener('close', this._after) } open(msg='', fn, ...args){ //引数1: ダイアログ内で表示するメッセージ //引数2: 閉じた後に実行する関数。引数=(押したボタンの文字列, ...引数3) //引数3以降: 引数2の関数で利用する引数 if (typeof fn!=='function') return this.dialog.querySelector('.message').textContent = msg this.dialog.showModal() this.fn = fn this.args = args } } //以降、使用例 const modal = new Modal(document.getElementById('dialog')); const afterModal = (rv, btnId, a, b) => { if (rv==='CANCEL') return console.log(rv) switch (btnId) { case 'btn1': console.log(btnId, a+b) break case 'btn2': console.log(btnId, a*b) break } } document.getElementById('btn1').addEventListener('click', e => { modal.open('btn1 question', afterModal, 'btn1', 5, 5) }, false); document.getElementById('btn2').addEventListener('click', e => { modal.open('btn2 question', afterModal, 'btn2', 5, 5) }, false); 終わり もっと上手いやり方ありそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5.js と物理演算エンジン「Matter.js」の組み合わせをお試し

以下のツイートの動画で、「円同士がぶつかったり、地面みたいなところの上を転がったり」という部分の仕組みの話です。 先ほどツイートしてた、#p5js と Matter.js を組み合わせて物理演算エンジンに入門してみた話の続き。取り急ぎ Handtrack.js を使った実装を追加して、カメラ映像からの手の認識と組み合わせた機能を追加してみた! pic.twitter.com/ScPjDsLv6K— you (@youtoy) September 8, 2021 物理演算エンジン/物理エンジンとか、英語では Physics engine と呼ばれたりするものになります。 ●物理演算エンジン - Wikipedia  https://ja.wikipedia.org/wiki/%E7%89%A9%E7%90%86%E6%BC%94%E7%AE%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3 今回、JavaScript で使えるライブラリの話になるのですが、いくつかあるライブラリの中の「Matter.js」を扱います。それを、p5.js の描画処理と組み合わせて使います。 今回の内容 今回の記事で扱うのは、冒頭で掲載していた動画の中でも使われている仕組みで、具体的には以下のようなものです。 #p5js と Matter.js を組み合わせて、物理演算エンジンに入門してみた! pic.twitter.com/7hWITEx4ad— you (@youtoy) September 8, 2021 冒頭の動画は、「画像認識で手が開いている状態を認識できたら、その手の部分から円が出現する」というものでしたが、こちらは「マウスのドラッグ操作をすると、ドラッグした軌跡の上に円が出現する」というものになります。 p5.js と Matter.js の組み合わせたプログラムの参照元 最初は、何か参考にできそうなものがあればとググってみるところから始めたのですが、その結果、以下の動画にたどり着きました。 ●5.17: Introduction to Matter.js - The Nature of Code - YouTube  https://www.youtube.com/watch?v=urR596FsU68 スキップしつつ動画の内容をざっくり見ていくと、Matter.js を p5.js と組み合わせて使っているのが確認できました。 そして、動画の説明欄を見ていると「Source Code for the all Video Lessons という記載とリンク」を見つけることができました。 そのリンク先はこちらです。 ソースコードを探す そのリポジトリの中にソースコードがありそうだったので、動画のタイトル「5.17: Introduction to Matter.js - The Nature of Code」の一部である「Introduction to Matter.js」をキーワードにして検索をかけてみました。 その結果が以下です。 その検索結果の一番上に README.md のファイルが表示されていますが、そのファイルが置かれている階層へ移動してみました。 そして、ファイルの一覧を見ると sketch.js も置いてあります。   中はこのような内容になっていました。 sketch.js // Daniel Shiffman // http://codingtra.in // http://patreon.com/codingtrain // Code for: https://youtu.be/uITcoKpbQq4 // module aliases var Engine = Matter.Engine, // Render = Matter.Render, World = Matter.World, Bodies = Matter.Bodies; var engine; var world; var circles = []; var boundaries = []; var ground; function setup() { createCanvas(400, 400); engine = Engine.create(); world = engine.world; //Engine.run(engine); boundaries.push(new Boundary(150, 100, width * 0.6, 20, 0.3)); boundaries.push(new Boundary(250, 300, width * 0.6, 20, -0.3)); } // function keyPressed() { // if (key == ' ') { // } // } function mouseDragged() { circles.push(new Circle(mouseX, mouseY, random(5, 10))); } function draw() { background(51); Engine.update(engine); for (var i = 0; i < circles.length; i++) { circles[i].show(); } for (var i = 0; i < boundaries.length; i++) { boundaries[i].show(); } } 他に box.js、boundary.js といったファイルもありました。 box.js // Daniel Shiffman // http://codingtra.in // http://patreon.com/codingtrain // Code for: https://youtu.be/uITcoKpbQq4 function Circle(x, y, r) { var options = { friction: 0, restitution: 0.95 }; this.body = Bodies.circle(x, y, r, options); this.r = r; World.add(world, this.body); this.show = function() { var pos = this.body.position; var angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); stroke(255); fill(127); ellipse(0, 0, this.r * 2); pop(); }; } boundary.js // Daniel Shiffman // http://codingtra.in // http://patreon.com/codingtrain // Code for: https://youtu.be/uITcoKpbQq4 function Boundary(x, y, w, h, a) { var options = { friction: 0, restitution: 0.95, angle: a, isStatic: true }; this.body = Bodies.rectangle(x, y, w, h, options); this.w = w; this.h = h; World.add(world, this.body); console.log(this.body); this.show = function() { var pos = this.body.position; var angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); noStroke(); fill(0); rect(0, 0, this.w, this.h); pop(); }; } あとは index.html の中身を確認して、ライブラリの依存関係を確認しておきます。 上記の JavaScript のファイルを読み込む部分などもありましたが、依存しているライブラリは「p5.js と Matter.js のみ」となりそうです。 <script language="javascript" type="text/javascript" src="https://cdn.jsdelivr.net/npm/p5@1.3.1/lib/p5.min.js"></script> <script language="javascript" type="text/javascript" src="libraries/matter.js"></script> オンライン環境(p5.js Web Editor)で動かしてみる 必要そうなもの一式がそろったので、これをオンラインの開発・実行環境である「p5.js Web Editor」で動かしてみようと思います。 index.html に関する内容 p5.js Web Editor のデフォルトの index.html に関しては、p5.sound.min.js を削除し、matter.min.js を追加しました。 matter.min.js は、現時点の最新版を CDN から読み込んでくるようにしました。 index.html <!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://cdnjs.cloudflare.com/ajax/libs/matter-js/0.17.1/matter.min.js"></script><!-- 追加 --> <link rel="stylesheet" type="text/css" href="style.css"> <meta charset="utf-8" /> </head> <body> <script src="sketch.js"></script> </body> </html> sketch.js に関する内容 sketch.js については、元々 3つに分かれていた処理を 1つにまとめてしまいました。 元の 3つに分割されたままで使っても良かったのですが、後で全体を眺めて読んでいきたかったので 1つにしてしまいました(全体の行数がそれほど多くないのもあり)。 そのまま動かしてみた後、色の設定の部分などは少しだけ変更を加えてみています。 sketch.js let Engine = Matter.Engine, World = Matter.World, Bodies = Matter.Bodies; let engine; let world; let circles = []; let boundaries = []; let ground; function setup() { createCanvas(400, 400); engine = Engine.create(); world = engine.world; boundaries.push(new Boundary(150, 100, width * 0.6, 20, 0.3)); boundaries.push(new Boundary(250, 300, width * 0.6, 20, -0.3)); } function mouseDragged() { circles.push(new Circle(mouseX, mouseY, random(5, 10))); } function draw() { background(180); Engine.update(engine); for (let i = 0; i < circles.length; i++) { circles[i].show(); } for (let i = 0; i < boundaries.length; i++) { boundaries[i].show(); } } function Boundary(x, y, w, h, a) { let options = { friction: 0, restitution: 0.95, angle: a, isStatic: true, }; this.body = Bodies.rectangle(x, y, w, h, options); this.w = w; this.h = h; World.add(world, this.body); console.log(this.body); this.show = function () { let pos = this.body.position; let angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); noStroke(); fill(0, 100, 200); rect(0, 0, this.w, this.h); pop(); }; } function Circle(x, y, r) { let options = { friction: 0, restitution: 0.95, }; this.body = Bodies.circle(x, y, r, options); this.r = r; World.add(world, this.body); this.show = function () { let pos = this.body.position; let angle = this.body.angle; push(); translate(pos.x, pos.y); rotate(angle); rectMode(CENTER); strokeWeight(1); stroke(255); fill(0, 0, 80); ellipse(0, 0, this.r * 2); pop(); }; } style.css に関する内容 style.css は特に変更は加えていません。 実行する あとは、p5.js Web Editor の実行ボタンを押して動かせば OK です。 実行後は、画面上でマウスのドラッグ操作をしてみましょう。 そうすると、ドラッグをした軌跡にあたる部分から、円がたくさん出現して画面内を落ちていきます。 そして、円の落下・円同士の衝突など、画面内の物体の動きは、物理演算エンジンがうまく処理をしてくれているのも確認できました。 まとめ 今回、サンプルとなるものをほぼそのまま利用して、p5.js と物理演算エンジン「Matter.js」を組み合わせた処理に入門してみました。 その後、p5-matterという、「p5.js と Matter.js を組み合わせるのを、より簡単にしてくれるライブラリ」があるのを知ったので、Matter.js をさらに使っていきつつ、こちらも試していければと思っています。 ●p5-matter by pzp1997  http://palmerpaul.com/p5-matter/ ●pzp1997/p5-matter: Seamlessly integrate matter.js with p5.js  https://github.com/pzp1997/p5-matter 【追記】 p5-matter に関する記事の公開 p5-matter に関する記事を書きました。 ●p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【概要編】 - Qiita  https://qiita.com/youtoy/items/a0e0da2da4c3acf66a7b ●p5-matter を使って p5.js での物理演算エンジン(Matter.js)の利用を簡単化する【活用編1】 - Qiita  https://qiita.com/youtoy/items/7fa6f6e6df2cf60133e6
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

microCMSを使い超爆速でCMS機能を実装してみる【PHP/JS】

久しぶりです。最近JAMStackとかが流行ってる流れでヘッドレスCMSが使われつつあるので実際に使って爆速でCMS機能を実装してみたので実装例とかを紹介したいと思います。 初心者はもちろん非エンジニアでもわかるレベルで解説・紹介します。 デモ:https://0115765.com/samples/php/MicroCMS_List.php microCMSとは microCMSは純国産のヘッドレスCMSです。 ヘッドレスCMSとは表示する部分を排除したCMS(コンテンツ管理システム)のことです。 通常WordPressなどの一般的なCMSはフロントエンドとバックエンドのセットで構築されます。WordPressはテーマなどを使って見える部分を整備しますよね。 一方ヘッドレスCMSは表示する部分(=フロントエンド)を提供せず自分で見える部分を作るので自由度が格段に増します。(公式サイトより) そしてmicroCMSはコンテンツをmicroCMS側に保管するためコンテンツの保守管理が楽です。 ヘッドレスCMSの利点・欠点 利点としては以下のことが挙げれられます。 必要な部分だけ導入できる 自由な言語・フレームワークを使うことができる バックエンドを構築する必要が無いためフロントエンド部分の作成に専念できる つまり「WordPressの導入するまでもないけどCMS機能ほしいなぁー!」レベルの一部分に導入したい時、大活躍ですね。 また欠点はこんな感じです。 一から見える部分を作るのでコンテンツ表示部を作成・改変するにはそれなりの言語的な知識を要する もちろんフロントエンド部分は自分で書かないといけないのでHTML/JSとかの言語やVueなりReactなりのフレームワークの知識も必要になってきます。逆に言えばエンジニアならもってこいのやつなんですね~ microCMSの無料プラン 無料プランと有料プランが用意されており無料プランはこんな感じです。 データ転送量:100GB API数:10個 メディア最大容量:40MB コンテンツ数上限:10000件 →個人のポートフォリオやや中小企業のコーポレートサイト・オウンドメディアには十分ですね。その他の比較は公式サイトを見てください。 実際に作ってみる それでは実際に作ってみましょう。 コンテンツ(記事)を作成する アカウント作成 以下リンクからサインアップ出来ます。無料プランならクレカ登録もないので勝手に請求…なことにはならないのでご安心を。 ログインしたら新規サービスを作成します。サービス名とサービスIDを任意の名前で設定します。 プラン選択で無料のヤツにします。 次にAPI基本情報を入力します。今回はNewsというAPI名にしました。 ブログ的な感じにしたいためリスト形式を選択。 最後にAPIスキーマを作成します。今回はタイトルと本文を追加しました。 更新日・投稿日などは元から入ってます 記事を書く それではコンテンツを作成していきましょう。右上の作成から作れます。 ここはWordPressのクラシックエディターと全く同じ操作で記事を書けます。 斜め字・太字・下線・取り消し線はもちろん文字色・背景色・コード・画像・YouTube埋め込みもできます! 作成が完了したら公開をクリックして保存しましょう。That's All!それでは見える部分を作っていきましょう! 表示部分を作る 本来はJAMStackやSSG(Static Site Generatin)を活用した環境などで使われることが多い(むしろそっちメイン)ですが今回は純粋なPHPのみ・JavaScriptのみで実装してみましたので参考にしてみてください。 APIをfetchなりcUrlなりで取得して加工するだけなので超簡単です! まぁNuxtJSで作ってNetlifyでホスティングする…みたいな記事しか無いしドキュメントもそれしか無いのであんまやる人がいないんでしょう… ※JAMStackとは…Netlifyが開発した、クライアントサイドJavaScript・再利用可能なAPI・マークアップ(markup)の3つをベースとしたクラウドネイティブなウェブ開発アーキテクチャである。(Wikiより) 【PHP】実装例 PHPで計100行以内で実装してみたのでコード例を紹介します。 Bladeを使いたかったからLaravelでやろうと思ったところですがバニラのPHPを使いました。 記事表示部分 デモ:https://0115765.com/samples/php/MicroCMS_Content.php?id=w65rp7nbzn 各記事のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]/[記事ID]です。 クエリパラメータを使ってIDを認識してそれを元にcURLでAPIを叩いてHTMLとして加工します。 MicroCMS_Content.php <?php $id = $_GET['id']; // クエリパラメータを取得 if (isset($id) == true) { GetContent($id); } else { echo 'Set query parameters'; } function GetContent($id) { // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news/' . $id); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: ***************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); ShowHtml( $result['title'], $result['createdAt'], $result['updatedAt'], $result['body'] ); } // コンテンツ表示部 function ShowHtml($title, $createAt, $updatedAt, $body) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>更新日:' . $updatedAt . '</p><br>'; echo '<article style="background-color: aliceblue;">本文:<br>' . $body . '</article>'; } 記事一覧 デモ:https://0115765.com/samples/php/MicroCMS_List.php 記事一覧のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]です。そこにAPIキーをGETで渡すだけです。 MicroCMS_List.php <?php // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: **************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); foreach ($result['contents'] as $contents) { $url = 'https://0115765.com/samples/php/MicroCMS_Content.php?id=' . $contents['id']; ShowHtml( $contents['title'], $contents['createdAt'], $url ); } function ShowHtml($title, $createAt, $url) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>URL: <a href="' . $url . '">' . $url . '</a></p><hr>'; }; 5分でできるかよとか言われそうですがcURLコマンドをPHPコードに変換してくれるサービスがあるので簡単なんですね~あとはjson_decodeしてHTMLに入れるだけ。 動作確認 https://0115765.com/samples/php/MicroCMS_List.php にアクセスしたら記事一覧が表示されます。 各リンクをクリックすると該当IDの作成した記事が表示されます。ちゃんと出来てますね。 LightHouseっていうChrome系ブラウザに搭載しているWebサイトの速度などを評価するヤツで測定してみましたがSpeedは100点になっており速度面でも問題なさそうでした。 ※今回やってませんが一応XSS対策もしておいてくださいね。 とりまできました。 【JavaScript】実装例 PHPで十分だけど一応JavaScriptでも作ってみました。Vueなどのフレームワークを使わずVanillaのJavaScriptを使ったので勘違いなく… ちなみにこっちのほうが簡単です。 Content.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>Content</title> </head> <body> <main> <h2 id='title'></h2> <p id='date'></p> <article id='body'></article> </main> <script> // 取得 fetch('https://***********.microcms.io/api/v1/news/w65rp7nbzn', { headers: { 'X-API-KEY': '****************************************' } }) .then(res => res.json()) .then(json => { document.getElementById('title').innerHTML = 'タイトル:' + json.title; document.getElementById('date').innerHTML = '作成日:' + json.createdAt; document.getElementById('body').innerHTML = '本文' + json.body; }) </script> </body> </html> 確認してみても大丈夫でした。 注意事項 バニラのJSで作成するとAPIキーが丸見えになるのでそこだけ注意してください。デモが無いのもそういうわけです。 なのでサーバーサイドやSSGで開発することが必須になります! 総括 超爆速で作ることができて正直感動しております() 最近有名なので是非使ってみてください! Twitter@ichii731 | ポートフォリオ あ、ちなみにAPIは記事管理画面で直接投げて確認できますよ~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

microCMSを使い5分でCMS機能を実装してみる【PHP/JS】

久しぶりです。最近JAMStackとかが流行ってる流れでヘッドレスCMSが使われつつあるので実際に使って爆速でCMS機能を実装してみたので実装例とかを紹介したいと思います。 初心者はもちろん非エンジニアでもわかるレベルで解説・紹介します。 デモ:https://0115765.com/samples/php/MicroCMS_List.php microCMSとは microCMSは純国産のヘッドレスCMSです。 ヘッドレスCMSとは表示する部分を排除したCMS(コンテンツ管理システム)のことです。 通常WordPressなどの一般的なCMSはフロントエンドとバックエンドのセットで構築されます。WordPressはテーマなどを使って見える部分を整備しますよね。 一方ヘッドレスCMSは表示する部分(=フロントエンド)を提供せず自分で見える部分を作るので自由度が格段に増します。(公式サイトより) そしてmicroCMSはコンテンツをmicroCMS側に保管するためコンテンツの保守管理が楽です。 ヘッドレスCMSの利点・欠点 利点としては以下のことが挙げれられます。 必要な部分だけ導入できる 自由な言語・フレームワークを使うことができる バックエンドを構築する必要が無いためフロントエンド部分の作成に専念できる つまり「WordPressの導入するまでもないけどCMS機能ほしいなぁー!」レベルの一部分に導入したい時、大活躍ですね。 また欠点はこんな感じです。 一から見える部分を作るのでコンテンツ表示部を作成・改変するにはそれなりの言語的な知識を要する もちろんフロントエンド部分は自分で書かないといけないのでHTML/JSとかの言語やVueなりReactなりのフレームワークの知識も必要になってきます。逆に言えばエンジニアならもってこいのやつなんですね~ microCMSの無料プラン 無料プランと有料プランが用意されており無料プランはこんな感じです。 データ転送量:100GB API数:10個 メディア最大容量:40MB コンテンツ数上限:10000件 →個人のポートフォリオやや中小企業のコーポレートサイト・オウンドメディアには十分ですね。その他の比較は公式サイトを見てください。 実際に作ってみる それでは実際に作ってみましょう。 コンテンツ(記事)を作成する アカウント作成 以下リンクからサインアップ出来ます。無料プランならクレカ登録もないので勝手に請求…なことにはならないのでご安心を。 ログインしたら新規サービスを作成します。サービス名とサービスIDを任意の名前で設定します。 プラン選択で無料のヤツにします。 次にAPI基本情報を入力します。今回はNewsというAPI名にしました。 ブログ的な感じにしたいためリスト形式を選択。 最後にAPIスキーマを作成します。今回はタイトルと本文を追加しました。 更新日・投稿日などは元から入ってます 記事を書く それではコンテンツを作成していきましょう。右上の作成から作れます。 ここはWordPressのクラシックエディターと全く同じ操作で記事を書けます。 斜め字・太字・下線・取り消し線はもちろん文字色・背景色・コード・画像・YouTube埋め込みもできます! 作成が完了したら公開をクリックして保存しましょう。That's All!それでは見える部分を作っていきましょう! 表示部分を作る 本来はJAMStackやSSG(Static Site Generatin)を活用した環境などで使われることが多い(むしろそっちメイン)ですが今回は純粋なPHPのみ・JavaScriptのみで実装してみましたので参考にしてみてください。 APIをfetchなりcUrlなりで取得して加工するだけなので超簡単です! まぁNuxtJSで作ってNetlifyでホスティングする…みたいな記事しか無いしドキュメントもそれしか無いのであんまやる人がいないんでしょう… ※JAMStackとは…Netlifyが開発した、クライアントサイドJavaScript・再利用可能なAPI・マークアップ(markup)の3つをベースとしたクラウドネイティブなウェブ開発アーキテクチャである。(Wikiより) 【PHP】実装例 PHPで計100行以内で実装してみたのでコード例を紹介します。 Bladeを使いたかったからLaravelでやろうと思ったところですがバニラのPHPを使いました。 記事表示部分 デモ:https://0115765.com/samples/php/MicroCMS_Content.php?id=w65rp7nbzn 各記事のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]/[記事ID]です。 クエリパラメータを使ってIDを認識してそれを元にcURLでAPIを叩いてHTMLとして加工します。 MicroCMS_Content.php <?php $id = $_GET['id']; // クエリパラメータを取得 if (isset($id) == true) { GetContent($id); } else { echo 'Set query parameters'; } function GetContent($id) { // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news/' . $id); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: ***************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); ShowHtml( $result['title'], $result['createdAt'], $result['updatedAt'], $result['body'] ); } // コンテンツ表示部 function ShowHtml($title, $createAt, $updatedAt, $body) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>更新日:' . $updatedAt . '</p><br>'; echo '<article style="background-color: aliceblue;">本文:<br>' . $body . '</article>'; } 記事一覧 デモ:https://0115765.com/samples/php/MicroCMS_List.php 記事一覧のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]です。そこにAPIキーをGETで渡すだけです。 MicroCMS_List.php <?php // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: **************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); foreach ($result['contents'] as $contents) { $url = 'https://0115765.com/samples/php/MicroCMS_Content.php?id=' . $contents['id']; ShowHtml( $contents['title'], $contents['createdAt'], $url ); } function ShowHtml($title, $createAt, $url) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>URL: <a href="' . $url . '">' . $url . '</a></p><hr>'; }; 5分でできるかよとか言われそうですがcURLコマンドをPHPコードに変換してくれるサービスがあるので簡単なんですね~あとはjson_decodeしてHTMLに入れるだけ。 動作確認 https://0115765.com/samples/php/MicroCMS_List.php にアクセスしたら記事一覧が表示されます。 各リンクをクリックすると該当IDの作成した記事が表示されます。ちゃんと出来てますね。 LightHouseっていうChrome系ブラウザに搭載しているWebサイトの速度などを評価するヤツで測定してみましたがSpeedは100点になっており速度面でも問題なさそうでした。 ※今回やってませんが一応XSS対策もしておいてくださいね。 とりまできました。 【JavaScript】実装例 PHPで十分だけど一応JavaScriptでも作ってみました。Vueなどのフレームワークを使わずVanillaのJavaScriptを使ったので勘違いなく… ちなみにこっちのほうが簡単です。 Content.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>Content</title> </head> <body> <main> <h2 id='title'></h2> <p id='date'></p> <article id='body'></article> </main> <script> // 取得 fetch('https://***********.microcms.io/api/v1/news/w65rp7nbzn', { headers: { 'X-API-KEY': '****************************************' } }) .then(res => res.json()) .then(json => { document.getElementById('title').innerHTML = 'タイトル:' + json.title; document.getElementById('date').innerHTML = '作成日:' + json.createdAt; document.getElementById('body').innerHTML = '本文' + json.body; }) </script> </body> </html> 確認してみても大丈夫でした。 注意事項 バニラのJSで作成するとAPIキーが丸見えになるのでそこだけ注意してください。デモが無いのもそういうわけです。 なのでサーバーサイドやSSGで開発することが必須になります! 総括 超爆速で作ることができて正直感動しております() 最近有名なので是非使ってみてください! Twitter@ichii731 | ポートフォリオ あ、ちなみにAPIは記事管理画面で直接投げて確認できますよ~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

microCMSを使い5分でCMS機能を実装してみる【PHP/JS】

久しぶりです。最近JAMStackとかが流行ってる流れでヘッドレスCMSが使われつつあるので実際に使って爆速でCMS機能を実装してみたので実装例とかを紹介したいと思います。 初心者はもちろん非エンジニアでもわかるレベルで解説・紹介します。 デモ:https://0115765.com/samples/php/MicroCMS_List.php microCMSとは microCMSは純国産のヘッドレスCMSです。 ヘッドレスCMSとは表示する部分を排除したCMS(コンテンツ管理システム)のことです。 通常WordPressなどの一般的なCMSはフロントエンドとバックエンドのセットで構築されます。WordPressはテーマなどを使って見える部分を整備しますよね。 一方ヘッドレスCMSは表示する部分(=フロントエンド)を提供せず自分で見える部分を作るので自由度が格段に増します。(公式サイトより) そしてmicroCMSはコンテンツをmicroCMS側に保管するためコンテンツの保守管理が楽です。 ヘッドレスCMSの利点・欠点 利点としては以下のことが挙げれられます。 必要な部分だけ導入できる 自由な言語・フレームワークを使うことができる バックエンドを構築する必要が無いためフロントエンド部分の作成に専念できる つまり「WordPressの導入するまでもないけどCMS機能ほしいなぁー!」レベルの一部分に導入したい時、大活躍ですね。 また欠点はこんな感じです。 一から見える部分を作るのでコンテンツ表示部を作成・改変するにはそれなりの言語的な知識を要する もちろんフロントエンド部分は自分で書かないといけないのでHTML/JSとかの言語やVueなりReactなりのフレームワークの知識も必要になってきます。逆に言えばエンジニアならもってこいのやつなんですね~ microCMSの無料プラン 無料プランと有料プランが用意されており無料プランはこんな感じです。 データ転送量:100GB API数:10個 メディア最大容量:40MB コンテンツ数上限:10000件 →個人のポートフォリオやや中小企業のコーポレートサイト・オウンドメディアには十分ですね。その他の比較は公式サイトを見てください。 実際に作ってみる それでは実際に作ってみましょう。 コンテンツ(記事)を作成する アカウント作成 以下リンクからサインアップ出来ます。無料プランならクレカ登録もないので勝手に請求…なことにはならないのでご安心を。 ログインしたら新規サービスを作成します。サービス名とサービスIDを任意の名前で設定します。 プラン選択で無料のヤツにします。 次にAPI基本情報を入力します。今回はNewsというAPI名にしました。 ブログ的な感じにしたいためリスト形式を選択。 最後にAPIスキーマを作成します。今回はタイトルと本文を追加しました。 更新日・投稿日などは元から入ってます 記事を書く それではコンテンツを作成していきましょう。右上の作成から作れます。 ここはWordPressのクラシックエディターと全く同じ操作で記事を書けます。 斜め字・太字・下線・取り消し線はもちろん文字色・背景色・コード・画像・YouTube埋め込みもできます! 作成が完了したら公開をクリックして保存しましょう。That's All!それでは見える部分を作っていきましょう! 表示部分を作る 本来はJAMStackやSSG(Static Site Generatin)を活用した環境などで使われることが多い(むしろそっちメイン)ですが今回は純粋なPHPのみ・JavaScriptのみで実装してみましたので参考にしてみてください。 APIをfetchなりcUrlなりで取得して加工するだけなので超簡単です! まぁNuxtJSで作ってNetlifyでホスティングする…みたいな記事しか無いしドキュメントもそれしか無いのであんまやる人がいないんでしょう… ※JAMStackとは…Netlifyが開発した、クライアントサイドJavaScript・再利用可能なAPI・マークアップ(markup)の3つをベースとしたクラウドネイティブなウェブ開発アーキテクチャである。(Wikiより) 【PHP】実装例 PHPで計100行以内で実装してみたのでコード例を紹介します。 Bladeを使いたかったからLaravelでやろうと思ったところですがバニラのPHPを使いました。 記事表示部分 デモ:https://0115765.com/samples/php/MicroCMS_Content.php?id=w65rp7nbzn 各記事のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]/[記事ID]です。 クエリパラメータを使ってIDを認識してそれを元にcURLでAPIを叩いてHTMLとして加工します。 MicroCMS_Content.php <?php $id = $_GET['id']; // クエリパラメータを取得 if (isset($id) == true) { GetContent($id); } else { echo 'Set query parameters'; } function GetContent($id) { // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news/' . $id); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: ***************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); ShowHtml( $result['title'], $result['createdAt'], $result['updatedAt'], $result['body'] ); } // コンテンツ表示部 function ShowHtml($title, $createAt, $updatedAt, $body) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>更新日:' . $updatedAt . '</p><br>'; echo '<article style="background-color: aliceblue;">本文:<br>' . $body . '</article>'; } 記事一覧 デモ:https://0115765.com/samples/php/MicroCMS_List.php 記事一覧のAPIエンドポイントはhttps://[サービス名].microcms.io/api/v1/[API名]です。そこにAPIキーをGETで渡すだけです。 MicroCMS_List.php <?php // cUrlでAPIを叩く $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'https://***************.microcms.io/api/v1/news'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); $headers[] = 'X-Api-Key: **************************************'; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); curl_close($ch); // 取得したら表示 $result = json_decode($response, true); foreach ($result['contents'] as $contents) { $url = 'https://0115765.com/samples/php/MicroCMS_Content.php?id=' . $contents['id']; ShowHtml( $contents['title'], $contents['createdAt'], $url ); } function ShowHtml($title, $createAt, $url) { echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; echo '<h2>タイトル:' . $title . '</h2>'; echo '<p>作成日:' . $createAt . '</p>'; echo '<p>URL: <a href="' . $url . '">' . $url . '</a></p><hr>'; }; 5分でできるかよとか言われそうですがcURLコマンドをPHPコードに変換してくれるサービスがあるので簡単なんですね~あとはjson_decodeしてHTMLに入れるだけ。 動作確認 https://0115765.com/samples/php/MicroCMS_List.php にアクセスしたら記事一覧が表示されます。 各リンクをクリックすると該当IDの作成した記事が表示されます。ちゃんと出来てますね。 LightHouseっていうChrome系ブラウザに搭載しているWebサイトの速度などを評価するヤツで測定してみましたがSpeedは100点になっており速度面でも問題なさそうでした。 ※今回やってませんが一応XSS対策もしておいてくださいね。 とりまできました。 【JavaScript】実装例 PHPで十分だけど一応JavaScriptでも作ってみました。Vueなどのフレームワークを使わずVanillaのJavaScriptを使ったので勘違いなく… ちなみにこっちのほうが簡単です。 Content.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>Content</title> </head> <body> <main> <h2 id='title'></h2> <p id='date'></p> <article id='body'></article> </main> <script> // 取得 fetch('https://***********.microcms.io/api/v1/news/w65rp7nbzn', { headers: { 'X-API-KEY': '****************************************' } }) .then(res => res.json()) .then(json => { document.getElementById('title').innerHTML = 'タイトル:' + json.title; document.getElementById('date').innerHTML = '作成日:' + json.createdAt; document.getElementById('body').innerHTML = '本文' + json.body; }) </script> </body> </html> 確認してみても大丈夫でした。 注意事項 バニラのJSで作成するとAPIキーが丸見えになるのでそこだけ注意してください。デモが無いのもそういうわけです。 なのでサーバーサイドやSSGで開発することが必須になります! 総括 超爆速で作ることができて正直感動しております() 最近有名なので是非使ってみてください! Twitter@ichii731 | ポートフォリオ あ、ちなみにAPIは記事管理画面で直接投げて確認できますよ~
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GASでプログラミング入門 ~応用編 Vol.1~

社内サークルにてエンジニアから非エンジニアの方向けにプログラミングを教えるという活動を行っています。 今回はその教材応用編の第1弾です。 今まではGASというよりもJavaScriptの基礎文法とアルゴリズムの組み立て方の基礎を学んできたので、本記事以降ではGASを活用した応用編に入っていきたいと思います。 JavaScriptについてある程度知見のある方は本記事から読んでいただいて大丈夫ですが、もしプログラミングに入門して基礎固めをしたいという方がいましたら、下記にVol.1~10までのリンクとそれぞれの記事に含まれる内容を表にしましたので、参考にしていただけますと幸いです。 記事 内容 GASでプログラミング入門 Vol.1 変数 GASでプログラミング入門 Vol.2 逐次・反復・分岐 GASでプログラミング入門 Vol.3 各種演算子 GASでプログラミング入門 Vol.4 関数 GASでプログラミング入門 Vol.5 関数の戻り値・コメント GASでプログラミング入門 Vol.6 let・var・const GASでプログラミング入門 Vol.7 配列 GASでプログラミング入門 Vol.8 連想配列 GASでプログラミング入門 Vol.9 null・break・論理演算子 GASでプログラミング入門 Vol.10 switch・代入演算子 前回の演習問題の解答例 (1). 下記のような出力になるプログラムを作成して下さい。 なお、閏年の日数は考慮しないものとします。 ※switch文を上手く活用しましょう。 1月の日数は31日です。 2月の日数は28日です。 3月の日数は31日です。 4月の日数は30日です。 5月の日数は31日です。 6月の日数は30日です。 7月の日数は31日です。 8月の日数は31日です。 9月の日数は30日です。 10月の日数は31日です。 11月の日数は30日です。 12月の日数は31日です。 解答例コード function myFunction() { for (let month = 1; month <= 12; month++) { let day = 0; switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: day = 31; break; case 2: day = 28; break; case 4: case 6: case 9: case 11: day = 30; break; } console.log(month + "月の日数は" + day + "日です。"); } } (2). 下記のような出力になるプログラムを作成して下さい。 1〜12月の合計日数は365日です。 1〜12月の平均日数は30.416666666666668日です。 解答例コード function myFunction() { let total = 0; for (let month = 1; month <= 12; month++) { let day = 0; switch (month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: day = 31; break; case 2: day = 28; break; case 4: case 6: case 9: case 11: day = 30; break; } total += day; } console.log("1〜12月の合計日数は" + total + "日です。"); console.log("1〜12月の平均日数は" + (total / 12) + "日です。"); } なお解答例はあくまで例なので、必ずしも上記のようになっていないといけないわけではありません。 スプレッドシートのセルをGASから読み込む スプレッドシートのセルをGASから読み込むには下記の手順が必要になります。 読み込む対象のスプレッドシートを取得する。 読み込む対象のスプレッドシート内のシートを取得する。 読み込む対象のセルを取得する。 現在開いているスプレッドシート内のシートを取得する場合 上記の1~2で、現在GASのコードを書いているスプレッドシート内の開いているシートを取得するコードはこんな感じになります。 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート) const sheet = spreadsheet.getActiveSheet(); 外部のスプレッドシートを指定する場合 スプレッドシートのID指定 スプレッドシートURLの/d/xxxxxxxxx/editのxxxxxxxxxがIDになりますので、こちらを指定して、外部のスプレッドシートを開くことも可能です。 // 読み込む対象のスプレッドシートを取得する。 const spreadsheet = SpreadsheetApp.openById('xxxxxxxxx'); スプレッドシートのURL指定 // 読み込む対象のスプレッドシートを取得する。 const spreadsheet = SpreadsheetApp.openById('https://docs.google.com/spreadsheets/d/xxxxxxxxx/edit'); スプレッドシート内のシートを指定する場合 シート名で指定する場合 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();// 読み込む対象のスプレッドシート内のシートを取得する。 const sheet = spreadsheet.getSheetByName('シート1'); シートの並び順の数値で指定する場合 同一スプレッドシート内に含まれる複数シートが配列に格納されているので、シートの並び順に従って、左から右に向かって配列内の数値を指定します。 配列なので、先頭要素は0から始まります。 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();// 読み込む対象のスプレッドシート内のシートを取得する。 const sheet = spreadsheet.getSheets()[0]; 単一セルの場合 まずはスプレッドシートの単一セルを読み込む手順です。 ※以降は基本的に現在開いているスプレッドシート内のセル操作を対象とします。 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート) const sheet = spreadsheet.getActiveSheet(); // 指定するセルの範囲(A1)を取得 const range = sheet.getRange('A1'); // 値を取得 const value = range.getValue(); 上記の例ではスプレッドシートのA1セルの内容を読み取っています。 複数セルの場合 複数セルを読み込む際は、getRange関数に最大で4つの引数を渡します。 それぞれの引数の意味は下記になっています。 引数 説明 第一引数 読み込み時に始点にする行番号。先頭行は1から始まる。 第二引数 読み込み時に始点にする列番号。先頭列は1から始まる。 第三引数 読み込む行数。省略可でデフォルトは1。 第四引数 読み込む列数。省略可でデフォルトは1。 少々ややこしいのは、多くのプログラミング言語において、配列を扱う際の先頭インデックスは0でしたが、スプレッドシートやExcelなどの表計算アプリケーションでは多くの場合、先頭を1からカウントすることが多いので、ごっちゃにならないように注意が必要です。 複数セルの値を順番に取り出す(2次元配列について) 複数セルの値を順番に取り出すにはgetRange関数の戻り値からgetValues関数を呼び出して、2次元の配列にアクセスすることで可能になります。 2次元配列というと少し難しく感じるかもしれませんが、基本的にはスプレッドシートの表をそのままプログラムの配列に対応させたものとして、捉えていただくと良いかもしれません。 今までに扱った配列は1次元配列であり、先頭から何番目の要素という形でアクセスしていました。 let nums = [1, 2, 3, 4]; console.log(nums[0]); // 1 console.log(nums[1]); // 2 console.log(nums[2]); // 3 console.log(nums[3]); // 4 2次元配列では下記のようになります。 let nums = [[1, 2, 3, 4],[5, 6, 7, 8]]; console.log(nums[0][0]); // 1 console.log(nums[0][1]); // 2 console.log(nums[1][0]); // 5 console.log(nums[1][1]); // 6 要素へのインデックスを二つ続けて書くことで碁盤の目のどこにアクセスするか?を決定しています。 つまり、XY座標で表すと、nums[y][x]というような形で、Yで行番号をXで列番号を指定することになります。 ここでもまたややこしいのですが、getRange関数に指定する行と列の番号は先頭を1からだったのに対し、取得後のセルの値は配列なので先頭は0からになるという点です。 非常にややこしいですが、混乱しないようにしてください。 セルを縦方向に読み込む場合 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート) const sheet = spreadsheet.getActiveSheet(); // 指定するセルの範囲(A1~A3)を取得 const range = sheet.getRange(1, 1, 3, 1); // 値を取得 const values = range.getValues(); // 値を出力 for (let i = 0; i < values.length; i++) { for (let j = 0; j < values[i].length; j++) { console.log(values[i][j]); } } 終点とする列番号を1にして、終点の行番号は3になっているので、セルを縦方向に読み込みたい場合は上記のような指定を行います。 セルを横方向に読み込む場合 // 読み込む対象のスプレッドシートを取得する。(現在開いているスプレッドシート) const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); // 読み込む対象のスプレッドシート内のシートを取得する。(現在開いているスプレッドシート内のシート) const sheet = spreadsheet.getActiveSheet(); // 指定するセルの範囲(A1~C1)を取得 const range = sheet.getRange(1, 1, 1, 3); // 値を取得 const values = range.getValues(); // 値を出力 for (let i = 0; i < values.length; i++) { for (let j = 0; j < values[i].length; j++) { console.log(values[i][j]); } } 終点とする列番号を3にして、終点の行番号は1になっているので、セルを横方向に読み込みたい場合は上記のような指定を行います。 上記プログラムを実行する際の注意点 上記プログラムGASで実行しようとすると、初回実行時には下記のような確認画面が表示されます。 権限を確認を押し、GASで使用するGoogleアカウントの確認や、GASで実行する権限などの確認画面が表示されるので、それらで適切な権限を設定しないと実行できませんので、ご注意ください。 ※個人のGoogleアカウントなら特に問題ないかと思いますが、組織でGoogleアカウントを利用している場合は管理者の方に承認をいただくなどが必要になるかもしれません。 演習問題 (1). スプレッドシートのA1セルに氏名、B1セルに出身、C1セルに趣味、D1セルに一言メッセージを入力しておき、入力したセルをGASのコード内で取得して、下記のような自己紹介を表示するプログラムを作成してください。 氏名:鈴木一郎 出身:東京都 趣味:ドライブ 一言:GASを頑張ってマスターする! (2). スプレッドシートのA2セルに10、A3セルに5を入力しておき、入力したセルをGASのコード内で取得して、下記のような出力になるようにプログラムを作成してください。 計算結果:2 まとめ いかがでしたでしょうか? 今回はGASからスプレッドシート内のセルを取得することができるようになりましたので、今までと比べると大分業務効率化する為のGAS活用例がぼんやりとはイメージが付いてきたのではないでしょうか? 次回は任意の値をセルに出力する方法を解説する予定ですので、引き続きお付き合いいただけると幸いです。 それではまた次の記事でお会いしましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript/TypeScriptにおけるimport/exportについての個人的な方針

JavaScript/TypeScriptにおけるimport/exportについて、個人的な方針を考えてみました。 デフォルトエクスポートは使わない 値や型をエクスポートするときは、常に名前付きエクスポートを使います。 デフォルトエクスポートを使っている foo.ts export default function foo (): void {} main.ts import foo from './foo' 名前付きエクスポートを使っている foo.ts export function foo (): void {} main.ts import { foo } from './foo' なぜなら、名前付きエクスポートにはデフォルトエクスポートに勝るいくつものメリットがあるからです。 Avoid Export Default - TypeScript Deep Dive 日本語版 なぜ default export を使うべきではないのか? - LINE ENGINEERING 詳しくは上記を参照していただくといいかと思いますが、自分が特によいと感じるのは以下の点です。 インポート時の名前を統一できる インポート時にエディタで補完が効く ただし、Next.jsやNuxt.jsなどのフレームワークがデフォルトエクスポートを要求している場合など、デフォルトエクスポートを使わなければならないケースにおいてはその限りではありません。 1つのファイルにおける役割は1つ デフォルトエクスポートが1つのファイルに1つしか存在できないのに対し、名前付きエクスポートは複数存在できます。 そうなると役割がまったく異なる複数の値や型をエクスポートすることもできるわけですが、1つのファイルにおける役割は1つに留めます。 1つのファイルに複数の役割が存在する foobar.ts export function foo (): void {} export function bar (): void {} main.ts import { foo, bar } from './foobar' 役割ごとにファイルが分割されている foo.ts export function foo (): void {} bar.ts export function bar (): void {} main.ts import { foo } from './foo' import { bar } from './bar' なぜなら、1つのファイルに複数の役割を持たせるとメンテナビリティが低くなるからです。 ただし、1つのファイル内に複数の名前付きエクスポートが存在すること自体は、それらの役割が同じものであればかまいません。 たとえば、エクスポートしたい関数の引数の型もエクスポートする場合などがあてはまります。 foo.ts export interface Options { bar: boolean } export function foo (options: Options): void {} main.ts import { foo, Options } from './foo' const options: Options = { bar: true } foo(options) 参考リンク エクスポートとインポート
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LiteXLoader用のプラグインを自作する

はじめに 前回はBDSサーバーでプラグインを導入するための準備としてLiteXLoader等を導入しました まだ見てない方はこちらから Minecraft統合版公式サーバーでプラグインを使う方法【LiteXLoader】 今回はLiteXloader(以下LXL)用のプラグインを自作してみよう!っていう記事です 言語はJavascriptを使います(Luaは知らねぇ) エディタは何でも良いですが、自分はVisual Studio Codeを使ってます VSCode用のプラグインでLXLDevHelperというものあるみたいですが言語が中国語でプラグインなので翻訳しようがないので使ってません。 多すぎるので一部のみの紹介となります。公式ドキュメントに全ての情報が乗っているので気になる人は是非 対象 Javascript触ったことがある人(プログラミングにある程度慣れている人)向け 文字を表示してみよう 1.適当になディテクトリにjavaScriptファイルを作りましょう 2.そのファイルにlog("Hello! LiteXLoader Plugin!")を入力してBDSのPluginsディレクトリにコピーして起動 3.BDS起動時にHello! LiteXLoader Plugin!と表示されるはずです! おまけ:色付きの文字を表示する colorLog(red,"Hello!") と入力すると赤い文字でコンソールに出力されます! 他にもblueやgreen等が指定できます 自作コマンドを登録してみよう コマンドの登録にはnc.regPlayerCmd()を使います。 mc.regPlayerCmd("hello","say hello!",function(pl,args){ pl.tell("Hello!"); 解説 第1パラメーター(helloの部分)は実際に入力するコマンド(スラッシュは除く) 第2パラメーター(say hello!)は実際にプレイヤーがコマンドを打ったときに表示されるコマンドの説明(日本語でもOK) 第3パラメーター(function...)は実際に実行するコールバック関数 関数にはコマンドを実行したプレイヤーオブジェクトとコマンドの引数が格納された配列を渡すことができます 第4パラメーター(オプション)として実行できるパーミッションを指定することができます 0(デフォルト)→オペレーター以外でも使用することができる 1→オペレーターでないと実行することができない おまけ コンソールコマンドを登録する mc.regConsoleCmd("hello","say hello!",function(args){ log("Hello!"); regPlayerCmd関数と大体同じですが、コンソール用なのでPlayerオブジェクトがありません 例:オウム返しするコマンドを作ってみる mc.regConsoleCmd("oumu","引数の言葉をオウム返しします",oncmd_oumu) function oncmd_oumu(pl,args){ if(args.length == 1){ pl.tell(args[0]) }else{ pl.tell("引数を指定してください") } } プレイヤーに関する情報を扱う プレイヤーオブジェクトについて LXLにはプレイヤーオブジェクトというものがあり、いろいろなもので取得することができます プレイヤーオブジェクトを使って以下のようなことができます プレイヤーの名前の取得 座標の取得 xuidの取得 パーミッションの取得 ゲームモードの取得 体力の取得 スニーク状態の取得 などなど・・・ プレイヤーオブジェクトを取得する方法 コマンドのコールバック関数の引数やイベントリスナーのコールバック関数の引数などから取得することが多いと思いますが、それ以外からも取得できます プレイヤー名やXUIDから取得する var Player = mc.getPlayer("プレイヤーの名前やXUID") 全てのオンラインプレイヤー一覧 var Players = mc.getOnlinePlayers() この関数の返り値は全てのプレイヤーのプレイヤーオブジェクトが入った配列なので1人ずつ何かしたいときは配列を1つずつ処理していく必要があります for (const player of players) { player.tell("Hey!") } プレイヤーオブジェクトの機能 各種情報を取得する方法は公式ドキュメントの表見たほうがいいと思います 公式ドキュメント プレイヤーがオペレーターかどうか オペレーター以外は処理を変えたい場面とかに使えます if(Player.isOP()){ log("OP!") }else{ log("not OP!") } プレイヤーをキック player.kick() これでキックできますが、 player.kick("サーバーからキックされました")と引数に文字列を渡すとその文字列が切断されたクライアントに表示されます また、kick()とdisconnect()は同じらしいです 特定のプレイヤーにメッセージを送信する player.tell("hello!") 前の解説でも使っているやつです。 第2引数にメッセージのタイプを指定できるみたいです tell()でもsendtext()でも同じっぽい 全プレイヤーにメッセージを送信する アナウンスとかに使えそう mc.broadcast("ハロー!") こちらもtellと同様、第2引数にタイプを設定できるみたいです プレイヤーをTPさせる `player.teleport(100,200,-300) 指定した座標にプレイヤーがテレポートします プレイヤーをキルする player.kill() キルするだけ プレイヤーのデバイス情報を取得する var device = player.getDevice() この返り値のDeviceオブジェクトにはこんな使い方ができます device.ip //IPアドレス device.avgPing //Ping(ms) device.avgPacketLoss //パケットロス(%) device.os //OS(iOSやAndroidやWindows10など) device.clientId //識別ID(?) ゲームイベントの取得 LXLにはイベントリスナーというものを使ってゲームに関するあらゆるイベントをキャッチして指定の関数を実行することができます。 リスナーの使い方 mc.listen("イベント名",callback) イベント名はこれから紹介するイベントを文字列として指定します callbackは呼び出される関数です。 イベントによっては引数が取得できるものがあります。 プレイヤーに関するイベント "onPreJoin" プレイヤーが参加するときに呼び出されます 引数はプレイヤーオブジェクトを渡すことができます "onJoin" onPreJoinと違うところはこのイベントはクライアントの読み込みが終わってからイベントが呼び出されます 引数はプレイヤーオブジェクトを渡すことができます "onLeft" プレイヤーが退出するときに呼び出されます 引数はプレイヤーオブジェクトを渡すことができます "onRespawn" プレイヤーがリスポーンするときに呼び出されます 引数はプレイヤーオブジェクトを渡すことができます "onPlayerDie" プレイヤーが死亡したときに呼び出されます 引数はプレイヤーオブジェクトを渡すことができます "onPlayerCmd" プレイヤーがコマンドを実行したときに呼び出されます 引数はプレイヤーオブジェクトと実行したコマンドの文字列を渡すことができます "onChat" プレイヤーがチャットを送信したときに呼び出されます 引数はプレイヤーオブジェクトと送信したチャットの文字列を渡すことができます エンティティに関するイベント "onMobDie" Mobが殺害されたときに呼び出されます 引数には死んだエンティティオブジェクトと死んだ原因となったエンティティオブジェクトを渡すことができます。 プレイヤーが死亡するとonPlayerDieイベントに加えこのイベントも実行されるので注意が必要 "onExplode" エンティティによって爆発が起きた場合に実行されます 引数には爆発の原因となったエンティティオブジェクトと爆発した座標を渡すことができます。 その他のイベント "onServerStarted" サーバーの起動が完了したときに実行されます "onConsoleCmd" サーバーコンソールでコマンドが実行されたときに呼びだされます。 引数には実行されたコマンドの文字列を渡すことができます。 フォームを使ったGUIの作り方 Hive鯖とかによくあるアレです 適当なフォームを送信してみる player.sendModalForm("タイトル","説明","ボタン1","ボタン2",callback) このようにPlayerオブジェクトに対応したクライアントにフォームが送信されます。 callback用の関数では引数として(player, result)と受け取ることができ、 resultには整数型としてOKが1、キャンセルが0、フォームをキャンセル(多分右上の☓を押したとき?)はNullが入っています ボタンの数を増やしたり画像を表示させたりする方法 player.sendSimpleForm("タイトル","説明",["ボタン1", "ボタン2", "ボタン3"],["image1.png", "image2.png", "image3.png"],callback) ボタンも文字列が入った配列と、表示させる画像のパスが入った配列を引数に渡してあげることでできます。 (多分BDSルートディレクトリからの相対パス指定だと思います) callback用の関数では引数として(player, id)と受け取ることができ、 idは整数型でクリックしたボタンの番号が入っています。 0から始まり、キャンセルはnullです。 クリックしたボタンを取得するサンプル player.sendSimpleForm("タイトル","説明",["ボタン1", "ボタン2", "ボタン3"],["image1.png", "image2.png", "image3.png"],form_callback) function form_callback(player, id){ if (id == 0){ player.tell("ボタン1") }else if (id == 1){ player.tell("ボタン2") }else if (id == 2){ player.tell("ボタン3") }else{ player.tell("キャンセル") } } ドロップダウンリスト等の項目を使った高度なフォームの作り方 自分もあまり使う機会なくてまだ試せてません・・・ ドキュメントによるといろいろ指定できるみたいです。 システム系 ファイルに書き込んだり読み込む ファイルの読み込み file.readFrom(path) BDSルートディレクトリから相対パスで指定します。 読み取った文字列が返り値として返されます。 書き込み file.writeTo(path,text) pathは相対パスで指定して、textは書き込む内容を文字列型で指定します 書き込み先のファイルが存在しない場合には自動的に生成され、存在する場合は上書きされます フォルダを作成する file.createDir(dir) HTTP GETリクエストの送信 network.httpGet(url,callback) urlにはGETするアドレスを文字列型で指定します。 callbackには(status, result)を取得でき、statusはHTTPステータスコード、resultはGETした結果が入っています。 おわりに 使いそうな関数はここで紹介しましたが、他にもNBTデータを操作したりすることもできるようです。 LXLプラグインはGithubではあまり公開されておらず、中国の掲示板のMineBBSというところに殆どのプラグインが公開されているようです(DLには謎の金塊?が必要らしくあまり良くわかってない) 最近だとLiteLoaderBDSもLXLもGithubの更新頻度が高く日々新機能が追加されていっています。 なので公式ドキュメントも覗いてみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue Router】動的ルートマッチングによる遷移前後で同じコンポーネントを使用する場合の注意点

実行環境 macOS 10.15.7 (19H1217) Vue.js 2.6.14 Vue Router 3.5.2 前提 以下のようにuserの詳細ページについてのルーティングを行っているとします。 App.vue ... <router-view /> ... router.js const router = new VueRouter({ routes: [ { path: '/user/:id', component: User } ] }) 問題点 例えば,/user/1から/user/2へURLが遷移した場合,どちらのURLでもUserコンポーネントが表示されます。 しかし,遷移前後のコンポーネントが同じ場合,そのコンポーネントはrerenderされません。 この例の場合は,Userコンポーネントは/user/2へ遷移した時にはrerenderされないということです。 解決策 前述の問題点を解消するためには,次のようにします。 App.vue ... <router-view :key="$route.fullPath" /> ... Vue.jsでは,keyの値が変わる度にその要素はrenderされる仕様になっています。 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザ上でプレビューを表示せずにカメラキャプチャする方法

javascript を使うとウェブブラウザからカメラにアクセスして画像をキャプチャできる。が、多くのサンプルプログラムではプレビュー表示(カメラから取り込んだままの画像の表示)があったり、さらにその画像を取り込むためのキャンバスエリアがあったりするので、画面のレイアウト上で問題になることがある(なにか悪い目的で使うわけではない。ブラウザ上に必ず警告なりアイコンなりが出るし)。では、プレビュー表示なしにキャプチャができるか?というと、出来ますという話。 ポイント <video> タグの領域を隠すのに css で display: none を指定する そうすると画像が取り込まれなくなる(画像が更新されなくなる)ブラウザがあるので、play() を呼び出す 画像の取り出しのための canvas は createElement() で作成するが appendChild() しない 具体的には以下のような感じ。Mac の Chrome と Safari で動作確認。 navigator.mediaDevices.getUserMedia({ video: true, audio: false }) .then(stream => { video.srcObject = stream; video.play(); // これがないと video タグを不可視にしたときに止まるブラウザがある setInterval(copyFrame, 30); // copyFrame 関数を 30msec に1度呼び出す }).catch(err => alert(`${err.name} ${err.message}`)); }, false); プログラム例 動作の様子とソースコード全体は https://shiura.com/html5/aa2.html から見てみてください。取り込まれた画像を以下のようにアスキーアートで表示するプログラムになっています。説明はコメントでソースコード中に書いてあります。 参考資料 このプログラムを作成するにあたり、以下の記事を参考にしました。 [iOS11]WebRTCでカメラアクセス&ピクセル取得する HTML5のWebRTCでPCに接続されたカメラ映像をウェブブラウザー上に表示してコマ画像を保存したい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Typescriptを使う理由

javascriptには限界がある 1.値が自由すぎる javascriptのプログラムでは、使用する値のタイプをあまり意識しない。この変数はどんなタイプの値が入っててどんな用途に使うものかをきちんと理解していない。 2.関数も自由すぎる 変数のスコープが関数ないなのかグローバルかと言ったところも大雑把。使い終わったのに残っている変数があるとerrorの元になる 3オブジェクトも自由すぎる 多くの他の言語ではクラスを定義してこれをコピーしてオブジェクトを作っているがjavascriptでは、クラスがあまり浸透していない 1.2.3これらの要素はいずれもerrorの原因になりかねず非常に安全ではない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

'name' は非推奨です。ts(6385)

某UdemyでJavaScriptの講座を見ていて、初歩的コードをVSCodeで入力していた。 let name = 'Tim'; function hello(name){ console.log('HELLO' + name); } hello(name); ☝エディタの画面上では、最後のhello()の引数に取り消し線が表示されている。そして下のような注意書きが表示された。 'name' は非推奨です。ts(6385) lib.dom.d.ts(18305, 5): この宣言はここで非推奨とマークされました。 const name: void @deprecated 利用できるクイックフィックスはありません 動作自体は問題ないが非推奨だと怒られている。 JavaScriptについて、仕事で使っているのに体系的な知識がない私は、なんです……?これ…と同じ言葉で検索をかけた。下記の英語サイトと同じ現象の模様。 https://www.reddit.com/r/vscode/comments/ltonp7/name_is_deprecated_ts6385/ windowオブジェクトにすでにある変数nameをグローバルスコープで新たに定義しているので怒られているらしい。 回避方法としてはnameのスコープを限定するかnameをもっと固有の名称に変更するか、TypeScriptで記述する、など。 実務ではまず起きない現象だが気になったのでメモ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js 値の変更がビューに検知されない問題

Vue.jsでの実務が多くなってきて、ビュー側が変更されない問題が多すぎたので、解消方法一旦まとめました。 公式ドキュメントにもVue2で検知されない変更のパターンはまとめられています 参考リンク → Vue 2 での変更検出の注意事項 オブジェクトの場合 公式ドキュメント Vue では、すでに作成されたインスタンスに対して新しいルートレベルのリアクティブなプロパティを動的に追加することはできません。 data: { user: { name: "taro", age: 52, }, }, methods: { changeAge: function(age){ this.user.age = age; // これだと検知されない }, } 解決方法 Vue.set使う Vue.setを使用するとネストされたプロパティに対して、リアクティブな変更が可能になる methods: { changeAge: function(age){ this.$set(this.user, 'age', age); }, } Object.assign()でオブジェクトごとマージしてしまう MDN Web Docs Object.assign() メソッドは、すべての列挙可能な自身のプロパティの値を、 1 つ以上のコピー元オブジェクトからコピー先オブジェクトにコピーするために使用されます。変更されたコピー先オブジェクトを返します。 methods: { changeAge: function(age){ const newUser = {...this.user}; newUser.age = age; this.user = Object.assign({}, this.user, newUser); }, } 配列の場合 公式ドキュメント Vue は、配列における次の変更は検知できません: 1. インデックスと一緒にアイテムを直接セットする場合、例えば vm.items[indexOfItem] = newValue 2. 配列の長さを変更する場合、例えば vm.items.length = newLength data: { numbers: [1, 2, 3, 4, 5], }, methods: { changeNumbers: function(number, index){ this.numbers[index] = number; // これだと検知されない }, } 解決方法 要素を追加する場合 オブジェクトに関しては、Vue.setで変更しましたが、追加する要素を追加する場合は通常通り、pushで良いらしいです 破壊的メソッド↓であれば、Vueが監視してくれます sort() splice() push() shift() unshift() reverse() methods: { changeNumbers: function(number){ this.numbers.push(number); }, } 要素の値を置き換える場合 splice使って置き換えてあげるとリアクティブな変更になります。 splice(変更したいindex, 変更する要素数, 変更後の要素); methods: { changeNumbers: function(number){ this.numbers.splice(index, 1, number); }, } まとめ オブジェクト・配列のリアクティブな変更の加え方をまとめてみました この辺の問題もVue3では解決されているらしいので、あまり需要な情報だったかもしれないですね、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ウェブサイト作成用備忘録・18号:three.jsで似たようなオブジェクトをまとめて生成し、後で個別にアニメーションさせたい【コピペでプレビュー】

タイトル通りの目標を実現させようとした結果、最終的にこのようなサンプルが完成しました。 学習が進むたびにプレビューのコードがどんどん長くなってそろそろ邪魔になってきたので、今回は折りたたみ機能を活用してみました! 【コピペでプレビュー】 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- jquery --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <!-- TweenMax --> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script> <!-- three.js --> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r127/three.min.js"></script> <!-- OrbitControls.js --> <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r109/examples/js/controls/OrbitControls.js"></script> </head> <body style="margin: 0;"> <!-- 3Dオブジェクト --> <div id="3dCanvas" style="position: fixed; z-index: 0;"></div> <!-- /3Dオブジェクト --> <!-- HTMLパート --> <div style="position: fixed; bottom: 0; right: 50%; transform: translateX(50%); height: 20%; display: flex; justify-content: center; z-index: 1;"> <div style="border-radius: 25px; box-sizing: border-box; padding: 1em 0; min-width: 350px; width: 99%; max-width: 450px; height: 99%; background-color: rgba(0,0,0,.7); display: flex; flex-flow: wrap; align-items: center; text-align: center; font-size: calc(.5rem + 2vmin); color: #ffffff;"> <!-- 入力パート --> <button class="" id="up" style="margin: 0 auto; padding: 5px 0; width: 10em;" type="button">↑</button> <div style="width: 100%;"> <button class="" id="left" style="margin: auto; padding: 5px 0; width: 10em; vertical-align: middle;" type="button">←</button> <button class="" id="right" style="margin: auto; padding: 5px 0; width: 10em; vertical-align: middle;" type="button">→</button> </div> <button class="" id="down" style="margin: 0 auto; padding: 5px 0; width: 10em;" type="button">↓</button> <!-- /入力パート --> </div> </div> <!-- /HTMLパート --> <script> jQuery(document).ready(function () { // カメラ用変数 let controls; let camera; let near = 0.1; let fov = 30; let far = 100; let target = new THREE.Vector3(0, 1, 0); let cameraX = 0; let cameraY = 10; let cameraZ = 5; // ページの読み込みを待つ $(window).on("load", function () { // レンダラーを作成 const canvas = document.getElementById("3dCanvas"); const renderer = new THREE.WebGLRenderer({ alpha: true, }); $("#3dCanvas").append(renderer.domElement); function cameraResize() { // カメラを作成 camera = new THREE.PerspectiveCamera(); // カメラコントローラーを作成 controls = new THREE.OrbitControls(camera, canvas); // レンダラーのサイズ設定(高画質対応) renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); // カメラ設定 camera.near = near; camera.fov = fov; camera.aspect = window.innerWidth / window.innerHeight; camera.far = far; // カメラポジション camera.position.set(cameraX, cameraY, cameraZ); // 原点方向を見つめる camera.lookAt(target); // カメラコントローラー制御 // 自動回転 controls.autoRotate = true; // 自動回転速度 controls.autoRotateSpeed = 1; // 垂直回転制限 controls.maxPolarAngle = 1.55; controls.minPolarAngle = 0; // パン移動無効化 controls.enablePan = false; // カメラを登録 camera.updateProjectionMatrix(); } // レンダラー・カメラの初期設定 cameraResize(); // リサイズ時にレンダラー・カメラの再設定 $(window).resize(function () { fov = camera.fov; cameraX = camera.position.x; cameraY = camera.position.y; cameraZ = camera.position.z; cameraResize(); }); // シーンを作成 const scene = new THREE.Scene(); // 床を作成 const planeSize = 5; const planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize); const normalMaterial = new THREE.MeshLambertMaterial({ color: 0x333333, }); const planeMesh = new THREE.Mesh(planeGeometry, normalMaterial); planeMesh.position.set(0, -10, 0); planeMesh.rotation.x = Math.PI * -.5; scene.add(planeMesh); // 柱を9本作成 const pillarSize = 0.5; const pillarGeometry = new THREE.BoxGeometry(pillarSize, 0.1, pillarSize); const pillarMaterial = new Array(9); for (var n = 0; n < 9; n++) { pillarMaterial[n] = new THREE.MeshLambertMaterial({ color: 0x333333, transparent: true, opacity: 1, }); } let pillarMesh = new Array(9); let pillar_n = 0; for (var z = 0; z < 3; z++) { for (var x = 0; x < 3; x++) { pillarMesh[pillar_n] = new THREE.Mesh(pillarGeometry, pillarMaterial[pillar_n]); pillarMesh[pillar_n].position.set((x - 1) * 1, -10, (z - 1) * 1); scene.add(pillarMesh[pillar_n]); TweenMax.to(pillarMesh[pillar_n].scale, 1, { delay: pillar_n / 3, y: 100, }); TweenMax.to(pillarMesh[pillar_n].position, 1, { delay: pillar_n / 3, y: -5, }); pillar_n++; } } function pillar_loop() { TweenMax.to(pillarMaterial, .1, { opacity: 0.5, }); if (pillar_n > 8) { pillar_n = 0; } else if (pillar_n < 0) { pillar_n = 8; } TweenMax.to(pillarMaterial[pillar_n], .5, { opacity: 1, }); } pillar_loop(); // 柱を個別にアニメーション $("#up").click(function () { TweenMax.to(pillarMesh[pillar_n].scale, .5, { y: "+=10", }); TweenMax.to(pillarMesh[pillar_n].position, .5, { y: "+=0.5", }); }); $("#right").click(function () { pillar_n++; pillar_loop(); }); $("#left").click(function () { pillar_n--; pillar_loop(); }); $("#down").click(function () { TweenMax.to(pillarMesh[pillar_n].scale, .5, { y: "-=10", }); TweenMax.to(pillarMesh[pillar_n].position, .5, { y: "-=0.5", }); }); // 環境光源を作成 const ambientlight = new THREE.AmbientLight(0xFFFFFF, 0.5); scene.add(ambientlight); // 平行光源を作成 const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.5); directionalLight.position.set(0, 1, 0.5); scene.add(directionalLight); // ループアニメーション設定 function tick() { requestAnimationFrame(tick); // カメラコントローラーを更新 controls.update(); // レンダリングを更新 renderer.render(scene, camera); } tick(); }); }); </script> </body> </html> 今回工夫した箇所 1・配列の中にメッシュやマテリアルを格納する事で、for文でまとめて生成した後に、配列の中で変更したい部分だけを後から個別に操作できるようになりました。(例えばこの部分とか) // 柱を9本作成 const pillarSize = 0.5; const pillarGeometry = new THREE.BoxGeometry(pillarSize, 0.1, pillarSize); const pillarMaterial = new Array(9); for (var n = 0; n < 9; n++) { pillarMaterial[n] = new THREE.MeshLambertMaterial({ color: 0x333333, transparent: true, opacity: 1, }); } let pillarMesh = new Array(9); let pillar_n = 0; for (var z = 0; z < 3; z++) { for (var x = 0; x < 3; x++) { pillarMesh[pillar_n] = new THREE.Mesh(pillarGeometry, pillarMaterial[pillar_n]); pillarMesh[pillar_n].position.set((x - 1) * 1, -10, (z - 1) * 1); scene.add(pillarMesh[pillar_n]); TweenMax.to(pillarMesh[pillar_n].scale, 1, { delay: pillar_n / 3, y: 100, }); TweenMax.to(pillarMesh[pillar_n].position, 1, { delay: pillar_n / 3, y: -5, }); pillar_n++; } } 注意点・このプレビューを作った時点では、配列の全ての要素に順番に処理を行うforEach文の方が良いかと思ったのですが、メッシュを格納する前の空配列の段階だとforEach文が機能しなかった為、まとめて生成する時はfor文の繰り返し処理で配列にメッシュやマテリアルを格納して、一度格納した後に配列全ての要素を同時に操作する場合はforEach文を使用すると楽だと思います。 2・ボックスジオメトリを地面から伸ばして柱の様な外観にしようとした際、大きさだけを変更するとオブジェクト中央を基準に上下に向かって伸びていく挙動だった為、大きさの変更と同時にY軸の高さを調整しています(大きさと高さの比率については…目分量で頑張りました) …今回はこれだけですね。 手隙の時間で色々勉強してはいるのですが、一定の期間毎にアウトプット出来る範囲をまとめていくと、単体では薄味な内容になってしまいますね。 自分用の備忘録ですが、誰かの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】ホイスティング(巻き上げ)について

はじめに アウトプットが苦手な自分が練習の一環として記事を書いてみる。 第六回目です。 今回はJavaScript独特の挙動であるホイスティング(巻き上げ)について書いていきます。 ホイスティング(巻き上げ)とは? ホイスティングとは 変数や関数の宣言は常にコードの先頭で行われたことにする という挙動のことです。 以下を例に見ていきます。 function a() { console.log('aが実行されました') } a(); // 実行結果 aが実行されました 上記は「関数を定義」しその後「関数の実行」を行っているので結果はaが実行されましたが出力されました。 ここまでは問題ないと思います。 では次は「関数を定義」と「関数の実行」の順番を入れ替えるとどうなるでしょうか。 a(); function a() { console.log('aが実行されました') } // 実行結果 aが実行されました 結果は順番を入れ替えてもaが実行されましたが出力されました。 プログラムは基本的に上から順に処理が行われるためエラーになりそうですが、問題なく動作しています。 これがホイスティングによる変数や関数の宣言は常にコードの先頭で行われたことにする挙動です。 変数を使ったホイスティング ホイスティングにはもう一つ特徴があります。 それは定義のみが巻き上げられ、初期化は巻き上げられないというものです。 次の例を見ていきます。 function a() { console.log(x); var x = 0; } a(); // 実行結果 undefined 一見「0」が出力されそうですが、結果はundefinedとなりました。 これは宣言のみが巻き上げられ、コンソールログ出力時の段階では初期化が行われないため結果「undefined」が出力されました。 以下のように初期化処理の後にコンソールログを出力をすることで「0」が出力されます。 function a() { console.log(x); // undefined var x = 0; console.log(x); // 0 } a(); よくありそうなミス 最後にグローバル変数を出力しようとしたが、意図せずブロックスコープ内に同名の変数を宣言してしまった場合、以下のようになります。 var x = 0; function a() { console.log(x); var x = 10; } a(); // 実行結果 undefined 結果はundefinedになりました。 これはブロックスコープ内の宣言が巻き上げられたことで参照する変数がブロックスコープ内の変数「x」となってしまいました。 そして「x」は初期化されていないため、0を出力するつもりが結果undefinedが出力されてしまったのです。 まとめ 以上になります。 ホイスティングはJavaScript特有の挙動なので意識していないと予期せぬ挙動になってしまいます。 対策としては変数の宣言はなるべく先頭に行うのが良いと思います。 参考になれば幸いです。 参考サイト https://developer.mozilla.org/ja/docs/Glossary/Hoisting
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React]chart.jsで縦軸に小数点を表示させない方法[TypeScript]

 はじめに デフォルトのままだと、データがない場合に、 なぜか縦軸が小数点表示されてしまう現象が発生しました。 縦軸が[人]なのに、小数点付くとおかしいので、修正しました。 やり方 以下付け足すだけです。 yAxes: [ { ticks: { beginAtZero: true, userCallback: function (label: any) { if (Math.floor(label) === label) { return label; } }, }, }, ], 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

yarnの処理でEACCESエラーが出たときの対処法

参考 エラー内容 EACCESのpermission deniedエラーが出ました。 FATAL EACCES: permission denied, mkdir '/home/xx/Documents/xxx-toC/frontend/node_modules/.cache/nuxt' ╭──────────────────────────────────────────────────────────────────────────────╮│ ││ ✖ Nuxt Fatal Error ││ ││ Error: EACCES: permission denied, mkdir ││ '/home/xx/Documents/xxx-toC/frontend/node_modules/.cache/nuxt' ││ │╰──────────────────────────────────────────────────────────────────────────────╯ error Command failed with exit code 1. 対処法 node modules のcacheへの権限がなさそうなので、そこに権限を与えました。 sudo chmod 777 node_modules/.cache/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhoneでも全画面表示させたい、んだ。

はじめに Fullscreen API に対応していない iPhoneで、どうしても全画面で表示させたい時がある。 特に landscape時の上部バーは、普通のサイトではスクロールすると隠れてくれるので邪魔に感じることはない。しかし、パノラマやVR等のコンテンツを表示する場合は邪魔に感じてしまいます、よね。 PWAにしてホーム画面に追加で解決はする、するのかもしれない、、がそうじゃない、んだ。解決はするかもなのだが、わざわざホーム画面に追加したりはしなかったりとか、するよね。 こんな時、個人的に好きなこがいる、krpanoである。 上下にスワイプして上部バーを隠してくれる。 他のコンテンツでもコレをやりたい、んだ。 やってみよう サンプルコード index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, viewport-fit=cover" /> <style > * { margin:0; padding:0; } html { height:100%; } body { height:100%; font-family:Lato, sans-serif; color:#fff; line-height:1.8; } #fs { overflow:hidden; position:absolute; z-index:2; display:none; justify-content:center; align-items:center; width:100%; height:calc( 100vh + 1px ); background-color:RGBA(0,0,0,0.8); touch-action:pan-y } #container { overflow:hidden; position:relative; z-index:1; top:0; width:100%; height:100%; background-color:#b1dff7; touch-action:none; } @media ( orientation:landscape ){ #container { height:100vh; } } </style> </head> <body> <div id="container"> <p style="position:absolute;left:5px;top:5px;width:40px;height:40px;background-color:#fff;"></p> <p style="position:absolute;right:5px;top:5px;width:40px;height:40px;background-color:#fff;"></p> <p style="position:absolute;right:5px;bottom:5px;width:40px;height:40px;background-color:#fff;"></p> <p style="position:absolute;left:5px;bottom:5px;width:40px;height:40px;background-color:#fff;"></p> </div> <script> let _ua = navigator.userAgent.toLowerCase(), _container = document.querySelector('#container'); if ( (/iphone/.test(_ua)) && !(/crios|edgios/.test(_ua)) ){ var _fs = document.createElement('div'); _fs.id = 'fs'; document.body.insertBefore(_fs, _container); _fs.innerHTML = '&#8593;&#8595;&nbsp;SWIPE'; document.addEventListener( 'DOMContentLoaded', fs_display ); window.addEventListener( 'resize', fs_display ); function fs_display() { if ( window.orientation == 0 || ( window.orientation != 0 && screen.width - window.innerHeight <= 20 ) ){ _fs.style.display = 'none'; } else if ( screen.width - window.innerHeight > 20 ) { _fs.style.display = 'flex'; } } } else { _container.style.height = '100%'; } </script> </body> </html> 構造 HTML <body> <div id="fs">↑↓&nbsp;SWIPE</div> <div id="container"> // Your contents. </div> </body> CSS html, body { height:100%; } #fs { overflow:hidden; position:absolute; z-index:2; display:none; width:100%; height:calc( 100vh + 1px ); touch-action:pan-y } #container { overflow:hidden; position:relative; z-index:1; width:100%; height:100%; touch-action:none; } @media ( orientation:landscape ){ #container { height:100vh; } } html, body, div#container を height:100%、div#fs を height:calc( 100vh + 1px ) とし、landscape時の div#container を height:100vh とします。 landscape時、コンテンツの高さを 100vh とし div#fs の高さを +1pxすることによって、上下スワイプによる上部バーの移動がよりスムーズに処理されます。 Android等において、下部バーに要素が隠れないよう iPhone Safari 以外は js にて height:100% に戻します。 div#fs, div#container を overflow:hidden とし、div#fsは touch-action:pan-y、div#contanerは touch-action:none とし制限をかけます。 制御 JS let _ua = navigator.userAgent.toLowerCase(), _container = document.querySelector('#container'); if ( (/iphone/.test(_ua)) && !(/crios|edgios/.test(_ua)) ){ var _fs = document.createElement('div'); _fs.id = 'fs'; document.body.insertBefore(_fs, _container); _fs.innerHTML = '&#8593;&#8595;&nbsp;SWIPE'; document.addEventListener( 'DOMContentLoaded', fs_display ); window.addEventListener( 'resize', fs_display ); function fs_display() { if ( window.orientation == 0 || ( window.orientation != 0 && screen.width - window.innerHeight <= 20 ) ){ _fs.style.display = 'none'; } else if ( screen.width - window.innerHeight > 20 ) { _fs.style.display = 'flex'; } } } else { _container.style.height = '100%'; } div#fs は iPhone Safari のみDOM生成します。 landscape時に div#fs を表示し、上下スワイプで上部バーが隠れると同時にウィンドウリサイズイベントがトリガーされ div#fs がdisplay:noneとなります。 まとめ Safari以外は、試行錯誤を繰り返した結果、、未対応となっております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでページ内リンクを実装する

react-scrollのインストール https://www.npmjs.com/package/react-scroll npm install --save-dev react-scroll [使用例] //test.tsx import {Link as Scroll} from "react-scroll" export default function Layout():JSX.Element { return ( <div> <header> <nav> <Scroll to="skills" smooth={true} duration={600} offset={-30}>skills</Scroll> </nav> </header> <main> <div id="about">ここにコンテンツを書く</div> <div id="skills">ここにコンテンツを書く</div> <div id="values">ここにコンテンツを書く</div> <div id="future">ここにコンテンツを書く</div> </main> </div> )} option解説 to="遷移先のid名" smoothをつけるとスムーズに移動するようになる。 durationは移動時間 offsetで到着位置をずらせる //[応用例] export default function Layout() { {/* map関数で取り出してタグを生成する。 */} const navItem = ["about", 'skills', 'values', 'future'] return ( <div> <header> <nav> {/* map関数で取り出してタグを生成する。 */} <div className="space-x-6 md:space-x-8"> {navItem.map((item, index) => { return (<Scroll to={`${item}`} className=" uppercase" smooth={true} duration={600} key={index} offset={-30}>{item}</Scroll>) })} </div> </nav> </header> <main> <div id="about">ここにコンテンツを書く</div> <div id="skills">ここにコンテンツを書く</div> <div id="values">ここにコンテンツを書く</div> <div id="future">ここにコンテンツを書く</div> </main> </div> )}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでページ内遷移を実装する

react-scrollのインストール https://www.npmjs.com/package/react-scroll npm install --save-dev react-scroll [使用例] //test.tsx import {Link as Scroll} from "react-scroll" export default function Layout():JSX.Element { return ( <div> <header> <nav> <Scroll to="skills" smooth={true} duration={600} offset={-30}>skills</Scroll> </nav> </header> <main> <div id="about">ここにコンテンツを書く</div> <div id="skills">ここにコンテンツを書く</div> <div id="values">ここにコンテンツを書く</div> <div id="future">ここにコンテンツを書く</div> </main> </div> )} option解説 to="遷移先のid名" smoothをつけるとスムーズに移動するようになる。 durationは移動時間 offsetで到着位置をずらせる [応用例] //test.tsx export default function Layout():JSX.Element { //ここでnavバーのコンテンツを定義する。 const navItem = ["about", 'skills', 'values', 'future'] return ( <div> <header> <nav> //map関数で取り出してタグを生成する。 <div className="space-x-6 md:space-x-8"> {navItem.map((item, index) => { return (<Scroll to={`${item}`} className=" uppercase" smooth={true} duration={600} key={index} offset={-30}>{item}</Scroll>) })} </div> </nav> </header> <main> <div id="about">ここにコンテンツを書く</div> <div id="skills">ここにコンテンツを書く</div> <div id="values">ここにコンテンツを書く</div> <div id="future">ここにコンテンツを書く</div> </main> </div> )}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.js実践編(メッセージボード編)

※node.js超入門ノートの続きになります。 モデルの実装 以下のコマンドを実行します。 npx sequelize-cli model:generate --name Board --attributes userId:integer, message:string アソシエーションの設定 作成したファイルを修正します。 models/board.js // 従モデル 'use strict'; module.exports = (sequelize, DataTypes) => { const Board = sequelize.define('Board', { userId: { type: DataTypes.INTEGER, validate: { notEmpty: { msg: "利用者は必須です。" } } }, message: { type: DataTypes.STRING, validate: { notEmpty: { msg: "メッセージは必須です。" } } } }, {}); Board.associate = function(models) { Board.belongsTo(models.User); // 従モデル }; return Board; }; 以下も修正します。 models/user.js User.associate = function(models) { User.hasMany(models.Board); // 主モデル }; 以下のコマンドでマイグレーションを行います。 npx sequelize-cli db:migrate ログイン処理 ログイン処理を追記します。 routes/users.js router.get('/login', (req, res, next) => { var data = { title:'Users/Login', content: '名前とパスワードを入力してください。' } res.render('users/login', data); }); router.post('/login', (req, res, next) => { db.User.findOne({ where:{ name: req.body.name, pass:req.body.pass, } }).then(usr => { if (usr != null) { req.session.login = usr; let back = req.session.back; if (back == null){ back = '/'; } res.redirect(back); } else { var data = { title: 'Users/Login', content:'名前かパスワードに問題があります。再度入力して下さい。' } res.render('users/login', data); } }) }); 以下のファイルを作成します。 routes/boards.js const express = require('express'); const router = express.Router(); const db = require('../models/index'); const { Op } = require("sequelize"); const { route } = require('./users'); const pnum = 10; // ログインのチェック function check(req, res) { if (req.session.login == null) { // ログイン後に戻る値 req.session.back = '/boards'; res.redirect('/users/login'); return true; } else { return false; } } // トップページ router.get('/', (req, res, next) => { res.redirect('/boards/0'); }); router.get('/:page',(req, res, next) => { if (check(req, res)){ return }; const pg = req.params.page * 1; db.Board.findAll({ offset: pg * pnum, limit: pnum, order: [ ['createdAt', 'DESC'] ], include: [{ model: db.User, required: true }] }).then(brds => { var data = { title: 'Boards', login: req.session.login, content: brds, page: pg } res.render('boards/index', data); }); }); // メッセージフォームの送信処理 router.post('/add', (req, res, next) => { if (check(req, res)){ return }; db.sequelize.sync() .then(() => db.Board.create({ userId: req.session.login.id, message: req.body.msg }) .then(brd => { res.redirect('/boards'); }) .catch((err) => { res.redirect('/boards'); }) ) }); // 利用者のホーム router.get('/home/:user/:id/:page', (req,res, next) => { if (check(req, res)){ return }; const id = req.params.id * 1; const pg = req.params.page * 1; db.Board.findAll({ where: {userId: id}, offset: pg * pnum, limit: pnum, order: [ ['createdAt', 'DESC'] ], include: [{ model: db.User, required: true }] }).then(brds => { var data = { title: 'Boards', login: req.session.login, userId: id, userName: req.params.user, content: brds, page: pg } res.render('boards/home', data); }); }); module.exports = router; app.jsに組み込みます。 app.js var boardsRouter = require('./routes/boards'); app.use('/boards', boardsRouter); テンプレート作成 以下のファイルを作成します。 views/users/login.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p><%- content %></p> <form action="/users/login" method="post"> <div class="form-group"> <label for="name">NAME</label> <input type="text" name="name" id="name" class="form-control"> </div> <div class="form-group"> <label for="pass">PASSWORD</label> <input type="text" name="pass" id="pass" class="form-control"> </div> <input type="submit" value="ログイン" class="btn btn-primary"> </form> <p class="mt-4"><a href="/boards">&lt;&lt; Top へ戻る</a> <a href="/users/add">アカウントの作成&gt;&gt;</a></p> </div> </body> </html> views/boards/index.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p class="h4">Wlcome to <%= login.name %>.</p> <form action="/boards/add" method="POST"> <div class="row"> <div class="col-10"> <input type="text" name="msg" class="form-control"> </div> <input type="submit" value="送信" class="btn btn-primary col-2"> </div> </form> <table class="table mt-5"> <% for(let i in content) { %> <%- include('data_item', {val: content[i]}) %> <% } %> </table> <ul class="pagination justify-content-center"> <li class="page-item"> <a href="/boards/<%= page - 1 %>" class="page-link">&lt;&lt; prev</a> </li> <li class="page-item"> <a href="/boards/<%= page + 1 %>" class="page-link">Next &gt;&gt;</a> </li> </ul> </div> </body> </html> views/boards/home.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html"> <title><%= title %></title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="/stylesheets/style.css" /> </head> <body class="container"> <header> <h1 class="display-4"> <%= title %> </h1> </header> <div role="main"> <p class="h4"><%= userName %>'s messages.</p> <table class="table mt-5"> <% for(let i in content) { %> <%- include('data_item', {val: content[i]}) %> <% } %> </table> <ul class="pagination justify-content-center"> <li class="page-item"> <a href="/boards/home/<%= userName %>/<%= userId %>/<%= page - 1 %>" class="page-link">&lt;&lt; prev</a> </li> <li class="page-item"> <a href="/boards/home/<%= userName %>/<%= userId %>/<%= page + 1 %>" class="page-link">Next &gt;&gt;</a> </li> </ul> </div> <div class="text-left"> <a href="/boards">&lt;&lt; Top.</a> </div> </body> </html> views/boards/data_item.ejs <% if (val != null) { %> <tr class = "row"> <th class="col-2"> <a class="text-dark" href="/boards/home/<%=val.User.name %>/<%= val.userId %>/0"> <%= val.User.name %> </a> </th> <td class="col-7"><%= val.message %></td> <% var d = new Date(val.createdAt); var dstr = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds(); %> <td class="col-3"><%= dstr %></td> </tr> <% } %> 結果 ログイン画面 メッセージボード画面 ユーザーのホーム画面
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

愛しのjQueryでぬるぬる動くスネークゲームを作る

実物:https://mogamoga1024.github.io/SnakeGame/ 矢印キーで移動 スペースキーで一時停止 IE11でも動作可能 PC上以外での動作は未想定 ちなみに頭が体に当たってもゲームオーバーにはなりません。 前書き Vanilla.js(生JS) + jQueryの構成です。 ES6やaltJSやp5.jsやVue.js、Babel、Polyfillなどは使いません。 Vanilla.js(生JS)とjQueryが嫌いな人は見ない方がいいでしょう。 裏テーマはIE11対応です。 ソースはこちら:https://github.com/mogamoga1024/SnakeGame 描画 SVGタグを利用します。以上。 SceneManager + Scene ゲームの状態(以後、Sceneと呼ぶ)としてスタート、ゲーム中、ゲームオーバーなどがあり、Sceneごとに画面レイアウト、キー操作が異なります。 Sceneが切り替わったときに、例えばキー操作を切り替えるとして共通のキー操作メソッド内部で○○Sceneは○○をする。△△Sceneは△△をするといった風に、Sceneごとに処理を分岐するのはif文がだらだらと長くなり可読性、保守性が下がります。 だらだらした分岐 $(window).keydown(function(event) { if (scene === "start") { startSceneKeyDownEvent(event); } else if (scene === "playing") { playingSceneKeyDownEvent(event); } else if (scene === "game over") { gameOverSceneKeyDownEvent(event); } else if (scene === "pause") { pauseSceneKeyDownEvent(event); } else if (scene === "hogehoge") { hogehogeSceneKeyDownEvent(event); } // 略 }); それを避けるためにSceneはオブジェクトとし、描画処理、キー操作などは個々のSceneオブジェクトで管理するようにし、Sceneの切り替えはSceneManagerオブジェクトに任せるようにします。 Sceneオブジェクトはstartメソッドで初期表示処理、updateメソッドで1フレーム単位の描画を想定しています。 scene.js function Scene() {} Scene.prototype.start = function($canvas) {}; Scene.prototype.update = function($canvas) {}; Scene.prototype.keydown = function(keyCode) {}; Scene.prototype.keyup = function(keyCode) {}; sceneManager.js const SceneManager = (function() { const $window = $(window); const $svg = $("svg"); let timer = null; let currentScene = new Scene(); window.GAME_FIELD_WIDTH = $svg.width(); window.GAME_FIELD_HEIGHT = $svg.height(); $window.keydown(function(e) { return currentScene.keydown(e.keyCode); }); $window.keyup(function(e) { return currentScene.keyup(e.keyCode); }); function canvasToCenter() { $svg.css("top", ($window.height() - GAME_FIELD_HEIGHT) / 2); $svg.css("left", ($window.width() - GAME_FIELD_WIDTH) / 2); }; canvasToCenter(); $window.resize(function() { canvasToCenter() }); return { start: function(scene, _shouldUpdate) { const shouldUpdate = (_shouldUpdate === undefined) ? true : _shouldUpdate; currentScene = scene; currentScene.start($svg); clearInterval(timer); if (shouldUpdate) { timer = setInterval(function() { currentScene.update($svg); }, 1000 / 60); } } }; })(); おまけ index.html index.html <!DOCTYPE html> <html> <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>SVG de スネークゲーム</title> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style> body { margin: 0; padding: 0; } #svg-container { position: relative; } svg { position: absolute; background-color: whitesmoke; } </style> </head> <body> <div id="svg-container"> <svg width="1100" height="600" xmlns="http://www.w3.org/2000/svg"></svg> </div> <script src="js/common/common.js"></script> <script src="js/common/position.js"></script> <script src="js/common/regular4nPolygoDegreee.js"></script> <script src="js/constants/keyCodeConstants.js"></script> <script src="js/feed/feed.js"></script> <script src="js/feed/feeder.js"></script> <script src="js/snake/snake.js"></script> <script src="js/scene/scene.js"></script> <script src="js/scene/sceneManager.js"></script> <script src="js/scene/gameStartScene.js"></script> <script src="js/scene/gamePlayScene.js"></script> <script src="js/scene/gameOverScene.js"></script> <script src="js/main.js"></script> </body> </html> 使い方としてはSceneオブジェクトを継承し独自のSceneオブジェクトを作成し、SceneManager.startメソッドでSceneを切り替えます。具体例として実際のプログラムを張っておきます。 gameStartScene.js function GameStartScene() {}; GameStartScene.prototype = Object.create(Scene.prototype); GameStartScene.prototype.constructor = GameStartScene; GameStartScene.prototype.start = function($canvas) { $canvas.empty(); const text1 = document.createElementNS("http://www.w3.org/2000/svg", "text"); const $text1 = $(text1); $canvas.append(text1); $text1.attr({ "text-anchor": "middle", "x": GAME_FIELD_WIDTH / 2, "y": GAME_FIELD_HEIGHT * 2 / 5, "font-size": 50 }); $text1.text("SVG de スネークゲーム"); const text2 = document.createElementNS("http://www.w3.org/2000/svg", "text"); const $text2 = $(text2); $canvas.append(text2); $text2.attr({ "text-anchor": "middle", "x": GAME_FIELD_WIDTH / 2, "y": GAME_FIELD_HEIGHT * 3 / 5, "font-size": 30 }); $text2.text("press any key"); }; GameStartScene.prototype.keyup = function(keyCode) { if (keyCode === KEY_CODE.F5) return; if (keyCode === KEY_CODE.F12) return; SceneManager.start(new GamePlayScene()); }; main.js SceneManager.start(new GameStartScene(), false); GamePlayScene GameStartScene、GameOverSceneは単純なのでスルーしますが、GamePlaySceneはスネークゲームの根幹なので解説します。コードは次の通りですが、真面目に見なくていいです。 gamePlayScene.js function GamePlayScene() { this.snake = null; this.feeder = null; this.feedList = []; this.canLoop = true; this.feedMaxCount = Feed.UNTIL_NOURISH_COUNT; this.firstKeyCode = null; this.secondeKeyCode = null; }; GamePlayScene.prototype = Object.create(Scene.prototype); GamePlayScene.prototype.constructor = GamePlayScene; GamePlayScene.prototype.start = function($canvas) { $canvas.empty(); const feedCanvas = document.createElementNS("http://www.w3.org/2000/svg", "g"); $canvas.append(feedCanvas); this.feeder = new Feeder($(feedCanvas)); const snakeCanvas = document.createElementNS("http://www.w3.org/2000/svg", "g"); $canvas.append(snakeCanvas); this.snake = new Snake($(snakeCanvas)); } GamePlayScene.prototype.update = function() { if (this.canLoop === false) return; if (this.firstKeyCode !== null) { this.snake.headAngleChangeByKeyCode(this.firstKeyCode); } if (this.snake.isHittingWall()) { SceneManager.start(new GameOverScene(this.snake.eatCount), false); return; } if (this.feedList.length === 0) { for (let i = 0; i < this.feedMaxCount; i++) { this.feedList.push(this.feeder.sowFeed(this.snake)); } } for (let i = this.feedList.length - 1; i >= 0; i--) { const feed = this.feedList[i]; if (this.snake.canEatFeed(feed)) { this.snake.eatFeed(feed); this.feedList.splice(i, 1); } } this.snake.move(); }; GamePlayScene.prototype.keydown = function(keyCode) { if (keyCode === KEY_CODE.SPACE) { this.canLoop = !this.canLoop; return; } if (keyCode !== this.firstKeyCode) { this.secondeKeyCode = this.firstKeyCode; } this.firstKeyCode = keyCode; }; GamePlayScene.prototype.keyup = function(keyCode) { if (keyCode === KEY_CODE.SPACE) return; if (keyCode === this.firstKeyCode) { this.firstKeyCode = this.secondeKeyCode; this.secondeKeyCode = null; } else if (keyCode === this.secondeKeyCode) { this.secondeKeyCode = null; } }; startメソッドは画面の生成、updateメソッドはメインとなるスネークゲームの処理、keydownメソッドとkeyupメソッドはキーの入力を制御しています。 updateメソッド内部はだいたいこんな感じのフローとなっています。 ちなみに、プロパティのfirstKeyCode、secondeKeyCodeは 上キーを押して蛇を上へ移動させる。 上キーを押したまま右キーを押して蛇を右へ移動させる。 右キーを離すと蛇が上へ移動する。 という挙動を実現させるために存在しています。 Snake ソースは長いので読まなくていいです。 描画、キー入力による方向転換、移動、壁、餌のあたり判定処理を行っています。 snake.js function Snake($canvas) { this.headPosition = new Position(100, 100); this.tailPosition = this.headPosition.clone(); this.radius = 25; this.eyeRadius = 5; this.eatCount = 0; this.headDegree = new Regular4nPolygonDegree(25); this.speed = 5; this.trace = []; this.trace.push(this.headPosition.clone()); this.scale = 1; const snake = document.createElementNS("http://www.w3.org/2000/svg", "path"); $canvas.append(snake); this.$snake = $(snake); const leftEye = document.createElementNS("http://www.w3.org/2000/svg", "circle"); $canvas.append(leftEye); this.$leftEye = $(leftEye); const rightEye = document.createElementNS("http://www.w3.org/2000/svg", "circle"); $canvas.append(rightEye); this.$rightEye = $(rightEye); this.$snake.attr({ "stroke": "green", "stroke-linecap": "round", "stroke-linejoin": "round", "fill": "none", "stroke-opacity": 0.8 }); this.$leftEye.attr("fill", "black"); this.$rightEye.attr("fill", "black"); this.draw(); } Snake.prototype.draw = function() { this.updateTailPosition(); let d = "M" + this.headPosition.x + "," + this.headPosition.y; if (this.eatCount === 0) { d += "L" + this.headPosition.x + "," + this.headPosition.y; } else { const lastTraceIndex = this.trace.length - 1; if (this.trace.length === 1) { d += "L" + this.tailPosition.x + "," + this.tailPosition.y; } else { d += "L" + this.trace[0].x + "," + this.trace[0].y; } for (let i = 0; i < lastTraceIndex; i++) { if (i === lastTraceIndex - 1) { d += "L" + this.tailPosition.x + "," + this.tailPosition.y; break; } else { d += "L" + this.trace[i + 1].x + "," + this.trace[i + 1].y; } } } this.$snake.attr({ "stroke-width": this.radius * this.scale * 2, "d": d }); const eyeCenterDistance = (this.radius - this.eyeRadius) * this.scale * 0.9; const headRadian = this.headDegree.toRadian(); const leftEyeX = this.headPosition.x + eyeCenterDistance * Math.cos(headRadian + Math.PI / 6); const leftEyeY = this.headPosition.y + eyeCenterDistance * Math.sin(headRadian + Math.PI / 6); this.$leftEye.attr({ "cx": leftEyeX, "cy": leftEyeY, "r": this.eyeRadius * this.scale }); const rightEyeX = this.headPosition.x + eyeCenterDistance * Math.cos(headRadian - Math.PI / 6); const rightEyeY = this.headPosition.y + eyeCenterDistance * Math.sin(headRadian - Math.PI / 6); this.$rightEye.attr({ "cx": rightEyeX, "cy": rightEyeY, "r": this.eyeRadius * this.scale }); }; Snake.prototype.headDegreeChangeByKeyCode = function(keyCode) { let rotationDirection; if (keyCode === KEY_CODE.UP_ARROW) { rotationDirection = this.findRotationDirection( this.headDegree.DEGREE_90, this.headDegree.DEGREE_270 ); } else if (keyCode === KEY_CODE.DOWN_ARROW) { rotationDirection = this.findRotationDirection( this.headDegree.DEGREE_270, this.headDegree.DEGREE_90 ); } else if (keyCode === KEY_CODE.LEFT_ARROW) { rotationDirection = this.findRotationDirection( this.headDegree.DEGREE_0, this.headDegree.DEGREE_180 ); } else if (keyCode === KEY_CODE.RIGHT_ARROW) { rotationDirection = this.findRotationDirection( this.headDegree.DEGREE_180, this.headDegree.DEGREE_0 ); } else { return; } if (rotationDirection !== 0) { this.headDegree.shift(rotationDirection); this.trace.unshift(this.headPosition.clone()); } }; Snake.prototype.findRotationDirection = function(start90nDegree, end90nDegree) { if (this.headDegree.equals90nDegree(start90nDegree) || this.headDegree.equals90nDegree(end90nDegree)) { return 0; } if (this.headDegree.existIn90nDegreeRange(start90nDegree, end90nDegree)) { return 1; } return -1; }; Snake.prototype.isHittingWall = function() { if ( this.headPosition.x - this.radius <= 0 || this.headPosition.y - this.radius <= 0 || this.headPosition.x + this.radius >= GAME_FIELD_WIDTH || this.headPosition.y + this.radius >= GAME_FIELD_HEIGHT ) { return true; } return false; }; Snake.prototype.canEatFeed = function(feed) { return this.headPosition.distance(feed.position) <= this.radius + feed.radius; }; Snake.prototype.move = function() { const headRadian = this.headDegree.toRadian(); this.headPosition.x += this.speed * Math.cos(headRadian); this.headPosition.y += this.speed * Math.sin(headRadian); this.draw(); }; Snake.prototype.updateTailPosition = function() { if (this.eatCount === 0) return; const snakeLength = 2 * this.radius * this.eatCount; let tmpSnakeLength = 0; let joint = this.headPosition; let index = 0; while (true) { const nextJoint = this.trace[index]; const preTmpSnakeLength = tmpSnakeLength; tmpSnakeLength += joint.distance(nextJoint); if (tmpSnakeLength === snakeLength) { this.tailPosition.x = nextJoint.x; this.tailPosition.y = nextJoint.y; break; } else if (tmpSnakeLength > snakeLength) { const remainingSnakeLenght = snakeLength - preTmpSnakeLength; if (joint.x === nextJoint.x) { this.tailPosition.x = joint.x; if (joint.y < nextJoint.y) { this.tailPosition.y = joint.y + remainingSnakeLenght; } else { this.tailPosition.y = joint.y - remainingSnakeLenght; } } else { const absC = nextJoint.distance(joint); const a = nextJoint.x - joint.x; const b = nextJoint.y - joint.y; this.tailPosition.x = joint.x + (a / absC) * remainingSnakeLenght; this.tailPosition.y = joint.y + (b / absC) * remainingSnakeLenght; } break; } if (index === this.trace.length - 1) { break; } joint = nextJoint; index++; } if (index < this.trace.length - 1) { this.trace.splice(index + 1); } }; Snake.prototype.eatFeed = function(feed) { feed.eaten(this); }; おまけ position.js position.js function Position(x, y) { this.x = x; this.y = y; } Position.prototype.distance = function(position) { const dx = this.x - position.x; const dy = this.y - position.y; return Math.sqrt(dx * dx + dy * dy); }; Position.prototype.clone = function() { return new Position(this.x, this.y); }; updateTailPositionメソッドは頭の座標と移動経路からしっぽの座標を求めて更新しています。 eatFeedメソッドは餌を食べたときの処理を餌のほうのオブジェクトで処理したいのでこういう書き方をしています。例えば、餌Aは体を1伸ばす。餌Bは逆に体を縮める。餌Cはスピードを上げる。餌Dは蛇の色を変えるみたいな感じです。 移動方向を制御しているheadDegreeプロパティのRegular4nPolygonDegreeオブジェクトは次で解説します。 Regular4nPolygonDegree このクラスは蛇の進行方向を管理します。 よーするにこーいうことです。 ソースは見なくていいです。 regular4nPolygonDegree.js function Regular4nPolygonDegree(n) { this.DEGREE_0 = 0; this.DEGREE_90 = n; this.DEGREE_180 = n * 2; this.DEGREE_270 = n * 3; this.N = n * 4; this.index = 0; this.centralAngle = Math.PI / (n * 2); } Regular4nPolygonDegree.prototype.shift = function(direction) { if (direction > 0) { this.index = (this.index + 1) % this.N; } else if (direction < 0) { this.index = (this.index - 1 + this.N) % this.N; } }; Regular4nPolygonDegree.prototype.equals90nDegree = function(degree) { if (this.isDEGREE_XX(degree) === false) { throw new Error("オブジェクトで定義されているDEGREE_XXを引数を利用すること。"); } return this.index === degree; }; Regular4nPolygonDegree.prototype.existIn90nDegreeRange = function(startDegree, endDegree) { if (this.isDEGREE_XX(startDegree) === false || this.isDEGREE_XX(endDegree) === false) { throw new Error("オブジェクトで定義されているDEGREE_XXを引数を利用すること。"); } if (startDegree < endDegree) { return startDegree < this.index && this.index < endDegree; } else if (startDegree > endDegree) { return startDegree < this.index || this.index < endDegree; } return false; }; Regular4nPolygonDegree.prototype.isDEGREE_XX = function(degree) { if ( degree === this.DEGREE_0 || degree === this.DEGREE_90 || degree === this.DEGREE_180 || degree === this.DEGREE_270 ) { return true; } return false; }; Regular4nPolygonDegree.prototype.toRadian = function() { return this.index * 2 * Math.PI / this.N; }; Regular4nPolygonDegree.prototype.convertRegular4nPolygon = function(n) { const radian = this.toRadian(); Regular4nPolygonDegree.call(this, n); this.index = Math.round(radian / this.centralAngle) % this.N; }; 予防線 突っ込まれそうなので先手を打ちますが、 Regular4nPolygonDegree.prototype.isDEGREE_XX = function(degree) { return degree === this.DEGREE_0 || degree === this.DEGREE_90 || degree === this.DEGREE_180 || degree === this.DEGREE_270; }; と書いてもいいです。 Feeder + Feed Feedはまんま餌です。 eatenメソッドはゲームバランスを調整するときに適当につけたパラメータが残っています。 許せんって人は定数化でもしてください。 feed.js function Feed($canvas, feeder, x, y) { this.position = new Position(x, y); this.feeder = feeder; this.radius = this.feeder.feedRadius; const feed = document.createElementNS("http://www.w3.org/2000/svg", "circle"); $canvas.append(feed); this.$feed = $(feed); this.$feed.attr({ "cx": this.position.x, "cy": this.position.y, "r": this.radius, "fill": "brown" }); } Feed.UNTIL_NOURISH_COUNT = 10; Feed.prototype.eaten = function(snake) { this.$feed.remove(); snake.eatCount++; if (snake.eatCount % Feed.UNTIL_NOURISH_COUNT === 0) { if (snake.speed < 10) { snake.speed += 0.5; const n = snake.headDegree.N / 4 - 2; if (n > 0) { snake.headDegree.convertRegular4nPolygon(n); } } if (snake.radius * snake.scale > 15) { snake.scale *= 0.99; this.feeder.feedRadius *= 0.99; } } if (snake.trace.length > 0) { snake.trace[snake.trace.length - 1].x = snake.tailPosition.x; snake.trace[snake.trace.length - 1].y = snake.tailPosition.y; } }; Feederは餌をまきます。 feeder.js function Feeder($canvas) { this.$canvas = $canvas; this.feedRadius = 20; } Feeder.prototype.sowFeed = function(snake) { let x, y, snakeRotationRadius; do { x = Math.floor(Math.randomInt(GAME_FIELD_WIDTH + 1 - this.feedRadius * 2) + this.feedRadius); y = Math.floor(Math.randomInt(GAME_FIELD_HEIGHT + 1 - this.feedRadius * 2) + this.feedRadius); snakeRotationRadius = snake.speed / Math.sqrt(2 * (1 - Math.cos(snake.headDegree.centralAngle))) + snake.radius; } while ( y < -x + snakeRotationRadius || y < x - GAME_FIELD_WIDTH + snakeRotationRadius || y > x + GAME_FIELD_HEIGHT - snakeRotationRadius || y > -x + GAME_FIELD_WIDTH + GAME_FIELD_HEIGHT - snakeRotationRadius ); return new Feed(this.$canvas, this, x, y); }; おまけ common.js common.js // 0以上max未満の整数の乱数 Math.randomInt = function(max) { return Math.floor(Math.random() * max); }; sowFeedメソッドは蛇が餌を食べれる範囲に餌をまきます。 よーするにこーいうことです。 終わり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む