- 投稿日:2021-08-15T22:26:01+09:00
履歴書の年表アプリ
まえがき こんにちは、今回も練習の一環で自作物を作りました。 お題は「履歴書で使用する年表を計算してくれるアプリ」です。 言葉で説明されてもわかりにくいと思うので、とりあえず完成品をお見せします。 この様に生年月日を入力すると、いつ入学して、いつ卒業したのかの一覧を作ってくれる簡素なアプリです。 自分で計算するとなると面倒くさいですよね?少なくとも前回の閏年判定アプリよりは使えますね! 小学校入学・卒業なんて履歴書に書かないとは思いますが… 今回も制作の記録をここに残します。 制作時間は12時間です。 もくじ 1.目的 2.結果 3.制作の流れ 4.ふりかえり 5.更新版 2021/8/17更新 1. 目的 このアプリのメインである履歴年表の処理自体は非常に簡単だと思います。 生まれた年から足し算をして、その結果を画面に表示させるだけなのだから。 難しそうなのは4月2日の判定と入力制限だと思います。 特に入力制限に関して、31日がない月と、2月の閏年は厄介そうな問題だと思いました。 ということで今回の目的は、キッチリと入力制限が施された実用的なアプリを作ることを目的にします。 2. 結果 完成品は以下のような見た目です。 1: 「なし」のラジオボタンをチェックした状態で履歴年表作成を押すと高校卒業以降は表示されません。 2: 「専門・短大」のラジオボタンをチェックした状態で履歴年表作成を押すと専門・短大の年表が表示されます。 3: 「4年制大学」のラジオボタンをチェックした状態で履歴年表作成を押すと4年制大学の年表が表示されます。 4: 値が入力されたいない状態で履歴年表作成を押すと、未入力であると注意されます。 5: 存在しない月日の状態で履歴年表作成を押すと、値が不正であると注意されます。 6: 閏年を判定します。閏年以外は2月29日を存在しない月日と判断します。 3. 制作の流れ 「1.目的」でも触れましたが、このアプリで厄介そうな”4月2日の判定と入力制限(31日がない月と、2月の閏年)”の問題を中心に話を進めます。 そして、後に判明した「プルダウンで未入力だった時の判定」について最後に話したいと思います。 4月2日の判定 入力制限 プルダウンで未入力だった時の判定 また、今回は製作途中の変数名などは日本語です。これは私の英語力の問題です。 完成時には英語に直しています。 4月2日の判定 日本では4月1日生まれか、4月2日生まれかで学年が変わります。 つまり、4月1日生まれなら生年から6年を足せば小学校入学ですが、4月2日生まれなら7年を足さないといけません。 入力された値が4月1日よりも値が大きければ、7年を足し、小さければ6年を足す。 という処理になると思います。 ということで以下のように月を100倍し、その後月と日の値を足して401より大きかどうか判定します。 (後で気が付きましたがmonthStatasの渡す値は最初から100倍にしててもよさそうですね…) const monthStatas = 4; const dayStatas = 2; const 月 = Number(monthStatas) * 100; const 日 = Number(dayStatas); let 月日 = 月 + 日; let 自分の履歴; const 履歴 = function(set){ let 履歴配列 = []; if(set > 401){ 履歴配列.push(7); return 自分の履歴 = 履歴配列; }else{ 履歴配列.push(6); return 自分の履歴 = 履歴配列; } } console.log(履歴(月日)); ”自分の履歴”という配列を使用しているのは、完成版では表示させる際にインデックスで指定して値を表示させたいからです。 出力結果 ちなみに月日が401の時は以下のように出力されます。 大丈夫そうですね。 入力制限 31日がない月と、2月の閏年の問題… より具体的に考えると 2月30日、2月31日、4月31日、6月31日、9月31日、11月31日 それと閏年でない場合は2月29日、以上の7日の入力制限をする必要があります。 順番としては、まず閏年の判定を行い、その後閏年ならば上記の6日を除外、閏年でない場合は上記の6日に加えて2月29日も除外します。 閏年の判定 前に書いた記事”『閏年判定アプリ』を作った”にてコメントで教えていただいた方法で判定しようと思います。 if( new Date(生年, 1, 29).getDate() === 29 ){ console.log("閏年です"); } console.log("閏年ではない"); これはnew Date()に変数yearと2月の29番目を設定し、それが存在する年の場合は29を返すのでtrueになります。 他にも方法がありますので。詳しくは 閏年の判定方法について をご参照ください。 閏年の場合、閏年でない場合の判定 閏年の場合と閏年でない場合で除外する配列を作り、includes()で判定しようと思います。 const 閏年の時の除外日 = [230, 231, 431, 631, 931, 1131]; const 閏年でないの時の除外日 = [229, 230, 231, 431, 631, 931, 1131]; 入力された月日が、閏年なら上の配列、閏年でなければ下の配列を参照して一致する数字列があるかどうか確認します。 function 入力制限(生年, 月日){ if( new Date(生年, 1, 29).getDate() === 29 ){ if(閏年の時の除外日.includes(月日)){ return; //閏年で、なおかつ存在しない月日 }else{ return; //閏年で、存在する月日 } }else{ if(閏年でないの時の除外日.includes(月日)){ return; //閏年でなく、なおかつ存在しない月日 }else{ return; //閏年でなく、存在する月日 } } } この判定が正しいのかどうかを1990年~2000年にかけて境界値分析を行い、問題ないことを確認しました。 プルダウンで未入力だった時の判定 今回、プルダウンメニューを使用しており、見栄えの関係上、一番上の数値はハイフンにしています。 そして、このハイフンを選択した状態でボタンを押された時 「注意!未入力の箇所があります。」と表示されるようにしたいと思います。 一番簡単だと思ったのは、ハイフンの箇所に設定する値(value)をInfinityにするという方法です。 19<select name="num" id = "pulldownYear"> <option value=Infinity>-</option><!--年--> <!--以下中略--> <select name="month" id = "pulldownMonth"><!--月--> <option value=Infinity>-</option> <!--以下中略--> <select name="day" id = "pulldownDay"><!--日--> <option value=Infinity>-</option> <!--以下省略--> こうすることで、JavaScriptで以下のような判定ができます。 if(!(生年 + 月 + 日 === Infinity)){} この様な強引な判定ができます。3つの値のどれか一つでもInfinityが含まれていたらInfinityになります。 ORを使うほうが正しいと思いますが… 4. ふりかえり 今回は「3.制作の流れ」にて触れてはいませんでしたが、try{}catch{}を使うなど色々教科書に書かれていたことを実際に導入することができて楽しかったです。制作の目的としていたキッチリと入力制限が施された実用的なアプリを作ることは満足いく結果でした。次はオブジェクト指向を用いた、アプリケーションの自作に挑戦してみたいです。 ここまで読んでいただきありがとうございました。 最後に、(CSSは特に面白いことはしてないので…)HTMLとJavaScriptを公開します。 もっと綺麗なコードが書けるよう精進していきます。 <!DOCTYPE html> <html> <head> <meta charset = "UTF-8"> <title>自分の履歴年表作成アプリ</title> <link rel = "stylesheet" href = "styles.css"> </head> <body> <!--全体の親要素---> <div class = "relative"> <!--プルダウンメニュー親要素---> <div class = "pulldownM"> <!--0~99までの年数用プルダウンメニュー---> 19<select name="num" id = "pulldownYear"> <option value=Infinity>-</option> <!--中略---> <option value="99">99</option> </select>年 <!--1~12までの月数用プルダウンメニュー---> <select name="month" id = "pulldownMonth"> <option value=Infinity>-</option> <!--中略---> <option value="12">12</option> </select>月 <!--1~31までの日数用プルダウンメニュー---> <select name="day" id = "pulldownDay"> <option value=Infinity>-</option> <!--中略---> <option value="31">31</option> </select>日 </div> <!--注意書き表示用要素---> <div id = "cautionId"> </div> <!--履歴年表作成ボタン要素---> <div class = buttonM> <div id = "button">履歴年表作成</div> </div> <!--履歴年表表示用要素---> <div class = "historyList"> <div id = "output1">----</div> <div id = "output2">----</div> <div id = "output3">----</div> <div id = "output4">----</div> <div id = "output5">----</div> <div id = "output6">----</div> <div id = "output7">----</div> <div id = "output8">----</div> </div> <!--ラジオボタン用要素---> <div class = "ridiobuttonM"> <input type="radio" name="q2" id = "radioA" checked> なし <input type="radio" name="q2" id = "radioB" > 専門・短大 <input type="radio" name="q2" > 4年制大学 </div> </div> <script src = main.js></script> </body> </html> //履歴年表作成ボタンを押したらイベントスタート const RirekiNenpyouSakusei = document.getElementById('button'); RirekiNenpyouSakusei.addEventListener('click', () => { //プルダウンメニューの3つの値 const yearStatas = document.getElementById("pulldownYear"); const monthStatas = document.getElementById("pulldownMonth"); const dayStatas = document.getElementById("pulldownDay"); //プルダウンメニューの3つの値の前処理 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) * 100; const dayN = Number(dayStatas.value); //100倍した月の値と、日の値を足す let monthDayN = monthN + dayN; //閏年の時(上)と閏年でない時(下)の除外する値 const NonDayLyear = [230, 231, 431, 631, 931, 1131]; const NonDayNotLyear = [229, 230, 231, 431, 631, 931, 1131]; //結果を表示させる際に使用する大域変数 let myHistory; //入力制限用関数 function InputLimit(year, monthDay){ if(!(year + monthDay === Infinity)){//プルダウンメニューでハイフンが選択されているかどうかの判定 if( new Date(year, 1, 29).getDate() === 29 ){//閏年の判定 if(NonDayLyear.includes(monthDay)){//閏年の時に除外する値の判定 const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); }else{ const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(monthDay); } }else{ if(NonDayNotLyear.includes(monthDay)){//閏年でない時に除外する値の判定 const cautionB = document.getElementById("cautionId"); cautionB.textContent = `値が不正です。`; return Sakuzyo(); }else{ const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(monthDay); } } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } const history = function(set){//大域変数myHistoryに配列を設定するための関数 let historyArray = []; if(set > 401){//4月1日以降であるかどうかを判定 historyArray.push(yearStatasN + 7); historyArray.push(yearStatasN + 13); historyArray.push(yearStatasN + 16); historyArray.push(yearStatasN + 19); historyArray.push(yearStatasN + 21); historyArray.push(yearStatasN + 23); return myHistory = historyArray; }else{ historyArray.push(yearStatasN + 6); historyArray.push(yearStatasN + 12); historyArray.push(yearStatasN + 15); historyArray.push(yearStatasN + 18); historyArray.push(yearStatasN + 20); historyArray.push(yearStatasN + 22); return myHistory = historyArray; } } InputLimit(yearStatasN, monthDayN);//入力制限用関数の呼び出し try{//入力制限用関数で処理がはじかれた際にエラーを出してしまうのでそれを捕えるためのtry..catch... const pa1 = document.getElementById("output1"); pa1.textContent = `小学校入学${myHistory[0]}年4月`; const pa2 = document.getElementById("output2"); pa2.textContent = `小学校卒業${myHistory[1]}年3月`; const pa3 = document.getElementById("output3"); pa3.textContent = `中学校入学${myHistory[1]}年4月`; const pa4 = document.getElementById("output4"); pa4.textContent = `中学校卒業${myHistory[2]}年3月`; const pa5 = document.getElementById("output5"); pa5.textContent = `高校入学${myHistory[2]}年4月`; const pa6 = document.getElementById("output6"); pa6.textContent = `高校卒業${myHistory[3]}年3月`; const radiocheckA = document.getElementById('radioA'); const radiocheckB = document.getElementById('radioB'); if(radiocheckA.checked){//ラジオボタンの値によって表示を変化させる const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; return; }else if(radiocheckB.checked){ const pa7 = document.getElementById("output7"); pa7.textContent = `専門・短大入学${myHistory[3]}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `専門・短大卒業${myHistory[4]}年3月`; return; }else{ const pa7 = document.getElementById("output7"); pa7.textContent = `大学入学${myHistory[3]}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `大学卒業${myHistory[5]}年3月`; return; } }catch(err){ console.log(`出力部分エラー:${err.message}`);//特に意味はない } }); const Sakuzyo = () => {//入力制限用関数ではじかれた際に出力済みの結果を消去する const pa1 = document.getElementById("output1"); pa1.textContent = `----`; const pa2 = document.getElementById("output2"); pa2.textContent = `----`; const pa3 = document.getElementById("output3"); pa3.textContent = `----`; const pa4 = document.getElementById("output4"); pa4.textContent = `----`; const pa5 = document.getElementById("output5"); pa5.textContent = `----`; const pa6 = document.getElementById("output6"); pa6.textContent = `----`; const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; } 5. 更新版 コメントで日付を扱うときはDateオブジェクトを使用したほうが良い。というアドバイスを頂きました。 それを受けて、「入力制限」と「4月2日の判定」の箇所をDateオブジェクトに置き換えましたので、その更新内容を発表します。 なお、Dateオブジェクト置き換えに伴い、「閏年の判定」はする必要がなくなりました。 入力制限 まず新しく入力された値をDateオブジェクトに代入した変数setDateを用意しました。 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) - 1; const dayN = Number(dayStatas.value); let setDate = new Date(yearStatasN, monthN, dayN); それを入力値の月(monthN)と比較してfalseならば、閏年の場合も含めた存在しない月日となります。 InputLimit(hantei, setDate, monthN); function InputLimit(HT, dateT, month){ if(!(HT === Infinity)){ if(dateT.getMonth() === month){//その月日が存在するかどうかを判定する const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(dateT); }else{ const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } この方法は JavaScriptで簡単に日付が有効かチェックする方法 の記事を参考にしました。 これはDateオブジェクトに値を入力したとき、その月日が存在しない場合は翌月にズレるという特性を利用しています。 4月2日の判定 以下のように判定方法をDateオブジェクトに置き換えただけです。 if( new Date(yearStatasN, monthN, dayN) > new Date(yearStatasN, 3, 1)){} ふりかえり 苦手だったDateオブジェクトから逃げていましたが、今回の課題を消化したことでDateオブジェクトに対し理解が深まりました。 アドバイスありがとうございました! 以下に更新したJavaScriptを載せます。 //履歴年表作成ボタンを押したらイベントスタート const RirekiNenpyouSakusei = document.getElementById('button'); RirekiNenpyouSakusei.addEventListener('click', () => { //プルダウンメニューの3つの値 const yearStatas = document.getElementById("pulldownYear"); const monthStatas = document.getElementById("pulldownMonth"); const dayStatas = document.getElementById("pulldownDay"); //プルダウンメニューの3つの値の前処理 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) - 1; const dayN = Number(dayStatas.value); //入力された値をDateオブジェクトに代入 let setDate = new Date(yearStatasN, monthN, dayN); //プルダウンメニューでハイフンが選択されているかどうかの判定用定数 const hantei = yearStatasN + monthN + dayN; let myHistory = []; function InputLimit(HT, dateT, month){ if(!(HT === Infinity)){//プルダウンメニューでハイフンが選択されているかどうかの判定 if(dateT.getMonth() === month){//存在する月日かどうかを判定 const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(dateT); }else{ const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } //大域変数myHistoryに配列を設定するための関数 const history = function(set){ const myHistoryPush = () =>{ myHistory.push(set.setFullYear(set.getFullYear() + 6)); myHistory.push(set.setFullYear(set.getFullYear() + 3)); myHistory.push(set.setFullYear(set.getFullYear() + 3)); myHistory.push(set.setFullYear(set.getFullYear() + 2)); myHistory.push(set.setFullYear(set.getFullYear() + 2)); } if( new Date(yearStatasN, monthN, dayN) > new Date(yearStatasN, 3, 1)){ myHistory.push(set.setFullYear(set.getFullYear() + 7)); myHistoryPush(); return output(myHistory); }else{ myHistory.push(set.setFullYear(set.getFullYear() + 6)); myHistoryPush(); return output(myHistory); } } InputLimit(hantei, setDate, monthN);//入力制限用関数の呼び出し function output(MH){//出力用 try{ const pa1 = document.getElementById("output1"); pa1.textContent = `小学校入学${new Date(MH[0]).getFullYear()}年4月`; const pa2 = document.getElementById("output2"); pa2.textContent = `小学校卒業${new Date(MH[1]).getFullYear()}年3月`; const pa3 = document.getElementById("output3"); pa3.textContent = `中学校入学${new Date(MH[1]).getFullYear()}年4月`; const pa4 = document.getElementById("output4"); pa4.textContent = `中学校卒業${new Date(MH[2]).getFullYear()}年3月`; const pa5 = document.getElementById("output5"); pa5.textContent = `高校入学${new Date(MH[2]).getFullYear()}年4月`; const pa6 = document.getElementById("output6"); pa6.textContent = `高校卒業${new Date(MH[3]).getFullYear()}年3月`; const radiocheckA = document.getElementById('radioA'); const radiocheckB = document.getElementById('radioB'); if(radiocheckA.checked){//ラジオボタンの値によって表示を変化させる const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; return; }else if(radiocheckB.checked){ const pa7 = document.getElementById("output7"); pa7.textContent = `専門・短大入学${new Date(MH[3]).getFullYear()}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `専門・短大卒業${new Date(MH[4]).getFullYear()}年3月`; return; }else{ const pa7 = document.getElementById("output7"); pa7.textContent = `大学入学${new Date(MH[3]).getFullYear()}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `大学卒業${new Date(MH[5]).getFullYear()}年3月`; return; } }catch(err){ console.log(err); } } }); const Sakuzyo = () => {//入力制限用関数ではじかれた際に出力済みの結果を消去する const pa1 = document.getElementById("output1"); pa1.textContent = `----`; const pa2 = document.getElementById("output2"); pa2.textContent = `----`; const pa3 = document.getElementById("output3"); pa3.textContent = `----`; const pa4 = document.getElementById("output4"); pa4.textContent = `----`; const pa5 = document.getElementById("output5"); pa5.textContent = `----`; const pa6 = document.getElementById("output6"); pa6.textContent = `----`; const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; } 参考文献:初めてのJavaScript
- 投稿日:2021-08-15T22:26:01+09:00
履歴書の年表アプリ 2021/8/17更新
まえがき こんにちは、今回も練習の一環で自作物を作りました。 お題は「履歴書で使用する年表を計算してくれるアプリ」です。 言葉で説明されてもわかりにくいと思うので、とりあえず完成品をお見せします。 この様に生年月日を入力すると、いつ入学して、いつ卒業したのかの一覧を作ってくれる簡素なアプリです。 自分で計算するとなると面倒くさいですよね?少なくとも前回の閏年判定アプリよりは使えますね! 小学校入学・卒業なんて履歴書に書かないとは思いますが… 今回も制作の記録をここに残します。 制作時間は12時間です。 もくじ 1.目的 2.結果 3.制作の流れ 4.ふりかえり 5.更新版 2021/8/17更新 1. 目的 このアプリのメインである履歴年表の処理自体は非常に簡単だと思います。 生まれた年から足し算をして、その結果を画面に表示させるだけなのだから。 難しそうなのは4月2日の判定と入力制限だと思います。 特に入力制限に関して、31日がない月と、2月の閏年は厄介そうな問題だと思いました。 ということで今回の目的は、キッチリと入力制限が施された実用的なアプリを作ることを目的にします。 2. 結果 完成品は以下のような見た目です。 1: 「なし」のラジオボタンをチェックした状態で履歴年表作成を押すと高校卒業以降は表示されません。 2: 「専門・短大」のラジオボタンをチェックした状態で履歴年表作成を押すと専門・短大の年表が表示されます。 3: 「4年制大学」のラジオボタンをチェックした状態で履歴年表作成を押すと4年制大学の年表が表示されます。 4: 値が入力されたいない状態で履歴年表作成を押すと、未入力であると注意されます。 5: 存在しない月日の状態で履歴年表作成を押すと、値が不正であると注意されます。 6: 閏年を判定します。閏年以外は2月29日を存在しない月日と判断します。 3. 制作の流れ 「1.目的」でも触れましたが、このアプリで厄介そうな”4月2日の判定と入力制限(31日がない月と、2月の閏年)”の問題を中心に話を進めます。 そして、後に判明した「プルダウンで未入力だった時の判定」について最後に話したいと思います。 4月2日の判定 入力制限 プルダウンで未入力だった時の判定 また、今回は製作途中の変数名などは日本語です。これは私の英語力の問題です。 完成時には英語に直しています。 4月2日の判定 日本では4月1日生まれか、4月2日生まれかで学年が変わります。 つまり、4月1日生まれなら生年から6年を足せば小学校入学ですが、4月2日生まれなら7年を足さないといけません。 入力された値が4月1日よりも値が大きければ、7年を足し、小さければ6年を足す。 という処理になると思います。 ということで以下のように月を100倍し、その後月と日の値を足して401より大きかどうか判定します。 (後で気が付きましたがmonthStatasの渡す値は最初から100倍にしててもよさそうですね…) const monthStatas = 4; const dayStatas = 2; const 月 = Number(monthStatas) * 100; const 日 = Number(dayStatas); let 月日 = 月 + 日; let 自分の履歴; const 履歴 = function(set){ let 履歴配列 = []; if(set > 401){ 履歴配列.push(7); return 自分の履歴 = 履歴配列; }else{ 履歴配列.push(6); return 自分の履歴 = 履歴配列; } } console.log(履歴(月日)); ”自分の履歴”という配列を使用しているのは、完成版では表示させる際にインデックスで指定して値を表示させたいからです。 出力結果 ちなみに月日が401の時は以下のように出力されます。 大丈夫そうですね。 入力制限 31日がない月と、2月の閏年の問題… より具体的に考えると 2月30日、2月31日、4月31日、6月31日、9月31日、11月31日 それと閏年でない場合は2月29日、以上の7日の入力制限をする必要があります。 順番としては、まず閏年の判定を行い、その後閏年ならば上記の6日を除外、閏年でない場合は上記の6日に加えて2月29日も除外します。 閏年の判定 前に書いた記事”『閏年判定アプリ』を作った”にてコメントで教えていただいた方法で判定しようと思います。 if( new Date(生年, 1, 29).getDate() === 29 ){ console.log("閏年です"); } console.log("閏年ではない"); これはnew Date()に変数yearと2月の29番目を設定し、それが存在する年の場合は29を返すのでtrueになります。 他にも方法がありますので。詳しくは 閏年の判定方法について をご参照ください。 閏年の場合、閏年でない場合の判定 閏年の場合と閏年でない場合で除外する配列を作り、includes()で判定しようと思います。 const 閏年の時の除外日 = [230, 231, 431, 631, 931, 1131]; const 閏年でないの時の除外日 = [229, 230, 231, 431, 631, 931, 1131]; 入力された月日が、閏年なら上の配列、閏年でなければ下の配列を参照して一致する数字列があるかどうか確認します。 function 入力制限(生年, 月日){ if( new Date(生年, 1, 29).getDate() === 29 ){ if(閏年の時の除外日.includes(月日)){ return; //閏年で、なおかつ存在しない月日 }else{ return; //閏年で、存在する月日 } }else{ if(閏年でないの時の除外日.includes(月日)){ return; //閏年でなく、なおかつ存在しない月日 }else{ return; //閏年でなく、存在する月日 } } } この判定が正しいのかどうかを1990年~2000年にかけて境界値分析を行い、問題ないことを確認しました。 プルダウンで未入力だった時の判定 今回、プルダウンメニューを使用しており、見栄えの関係上、一番上の数値はハイフンにしています。 そして、このハイフンを選択した状態でボタンを押された時 「注意!未入力の箇所があります。」と表示されるようにしたいと思います。 一番簡単だと思ったのは、ハイフンの箇所に設定する値(value)をInfinityにするという方法です。 19<select name="num" id = "pulldownYear"> <option value=Infinity>-</option><!--年--> <!--以下中略--> <select name="month" id = "pulldownMonth"><!--月--> <option value=Infinity>-</option> <!--以下中略--> <select name="day" id = "pulldownDay"><!--日--> <option value=Infinity>-</option> <!--以下省略--> こうすることで、JavaScriptで以下のような判定ができます。 if(!(生年 + 月 + 日 === Infinity)){} この様な強引な判定ができます。3つの値のどれか一つでもInfinityが含まれていたらInfinityになります。 ORを使うほうが正しいと思いますが… 4. ふりかえり 今回は「3.制作の流れ」にて触れてはいませんでしたが、try{}catch{}を使うなど色々教科書に書かれていたことを実際に導入することができて楽しかったです。制作の目的としていたキッチリと入力制限が施された実用的なアプリを作ることは満足いく結果でした。次はオブジェクト指向を用いた、アプリケーションの自作に挑戦してみたいです。 ここまで読んでいただきありがとうございました。 最後に、(CSSは特に面白いことはしてないので…)HTMLとJavaScriptを公開します。 もっと綺麗なコードが書けるよう精進していきます。 <!DOCTYPE html> <html> <head> <meta charset = "UTF-8"> <title>自分の履歴年表作成アプリ</title> <link rel = "stylesheet" href = "styles.css"> </head> <body> <!--全体の親要素---> <div class = "relative"> <!--プルダウンメニュー親要素---> <div class = "pulldownM"> <!--0~99までの年数用プルダウンメニュー---> 19<select name="num" id = "pulldownYear"> <option value=Infinity>-</option> <!--中略---> <option value="99">99</option> </select>年 <!--1~12までの月数用プルダウンメニュー---> <select name="month" id = "pulldownMonth"> <option value=Infinity>-</option> <!--中略---> <option value="12">12</option> </select>月 <!--1~31までの日数用プルダウンメニュー---> <select name="day" id = "pulldownDay"> <option value=Infinity>-</option> <!--中略---> <option value="31">31</option> </select>日 </div> <!--注意書き表示用要素---> <div id = "cautionId"> </div> <!--履歴年表作成ボタン要素---> <div class = buttonM> <div id = "button">履歴年表作成</div> </div> <!--履歴年表表示用要素---> <div class = "historyList"> <div id = "output1">----</div> <div id = "output2">----</div> <div id = "output3">----</div> <div id = "output4">----</div> <div id = "output5">----</div> <div id = "output6">----</div> <div id = "output7">----</div> <div id = "output8">----</div> </div> <!--ラジオボタン用要素---> <div class = "ridiobuttonM"> <input type="radio" name="q2" id = "radioA" checked> なし <input type="radio" name="q2" id = "radioB" > 専門・短大 <input type="radio" name="q2" > 4年制大学 </div> </div> <script src = main.js></script> </body> </html> //履歴年表作成ボタンを押したらイベントスタート const RirekiNenpyouSakusei = document.getElementById('button'); RirekiNenpyouSakusei.addEventListener('click', () => { //プルダウンメニューの3つの値 const yearStatas = document.getElementById("pulldownYear"); const monthStatas = document.getElementById("pulldownMonth"); const dayStatas = document.getElementById("pulldownDay"); //プルダウンメニューの3つの値の前処理 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) * 100; const dayN = Number(dayStatas.value); //100倍した月の値と、日の値を足す let monthDayN = monthN + dayN; //閏年の時(上)と閏年でない時(下)の除外する値 const NonDayLyear = [230, 231, 431, 631, 931, 1131]; const NonDayNotLyear = [229, 230, 231, 431, 631, 931, 1131]; //結果を表示させる際に使用する大域変数 let myHistory; //入力制限用関数 function InputLimit(year, monthDay){ if(!(year + monthDay === Infinity)){//プルダウンメニューでハイフンが選択されているかどうかの判定 if( new Date(year, 1, 29).getDate() === 29 ){//閏年の判定 if(NonDayLyear.includes(monthDay)){//閏年の時に除外する値の判定 const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); }else{ const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(monthDay); } }else{ if(NonDayNotLyear.includes(monthDay)){//閏年でない時に除外する値の判定 const cautionB = document.getElementById("cautionId"); cautionB.textContent = `値が不正です。`; return Sakuzyo(); }else{ const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(monthDay); } } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } const history = function(set){//大域変数myHistoryに配列を設定するための関数 let historyArray = []; if(set > 401){//4月1日以降であるかどうかを判定 historyArray.push(yearStatasN + 7); historyArray.push(yearStatasN + 13); historyArray.push(yearStatasN + 16); historyArray.push(yearStatasN + 19); historyArray.push(yearStatasN + 21); historyArray.push(yearStatasN + 23); return myHistory = historyArray; }else{ historyArray.push(yearStatasN + 6); historyArray.push(yearStatasN + 12); historyArray.push(yearStatasN + 15); historyArray.push(yearStatasN + 18); historyArray.push(yearStatasN + 20); historyArray.push(yearStatasN + 22); return myHistory = historyArray; } } InputLimit(yearStatasN, monthDayN);//入力制限用関数の呼び出し try{//入力制限用関数で処理がはじかれた際にエラーを出してしまうのでそれを捕えるためのtry..catch... const pa1 = document.getElementById("output1"); pa1.textContent = `小学校入学${myHistory[0]}年4月`; const pa2 = document.getElementById("output2"); pa2.textContent = `小学校卒業${myHistory[1]}年3月`; const pa3 = document.getElementById("output3"); pa3.textContent = `中学校入学${myHistory[1]}年4月`; const pa4 = document.getElementById("output4"); pa4.textContent = `中学校卒業${myHistory[2]}年3月`; const pa5 = document.getElementById("output5"); pa5.textContent = `高校入学${myHistory[2]}年4月`; const pa6 = document.getElementById("output6"); pa6.textContent = `高校卒業${myHistory[3]}年3月`; const radiocheckA = document.getElementById('radioA'); const radiocheckB = document.getElementById('radioB'); if(radiocheckA.checked){//ラジオボタンの値によって表示を変化させる const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; return; }else if(radiocheckB.checked){ const pa7 = document.getElementById("output7"); pa7.textContent = `専門・短大入学${myHistory[3]}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `専門・短大卒業${myHistory[4]}年3月`; return; }else{ const pa7 = document.getElementById("output7"); pa7.textContent = `大学入学${myHistory[3]}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `大学卒業${myHistory[5]}年3月`; return; } }catch(err){ console.log(`出力部分エラー:${err.message}`);//特に意味はない } }); const Sakuzyo = () => {//入力制限用関数ではじかれた際に出力済みの結果を消去する const pa1 = document.getElementById("output1"); pa1.textContent = `----`; const pa2 = document.getElementById("output2"); pa2.textContent = `----`; const pa3 = document.getElementById("output3"); pa3.textContent = `----`; const pa4 = document.getElementById("output4"); pa4.textContent = `----`; const pa5 = document.getElementById("output5"); pa5.textContent = `----`; const pa6 = document.getElementById("output6"); pa6.textContent = `----`; const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; } 5. 更新版 コメントで日付を扱うときはDateオブジェクトを使用したほうが良い。というアドバイスを頂きました。 それを受けて、「入力制限」と「4月2日の判定」の箇所をDateオブジェクトに置き換えましたので、その更新内容を発表します。 なお、Dateオブジェクト置き換えに伴い、「閏年の判定」はする必要がなくなりました。 入力制限 まず新しく入力された値をDateオブジェクトに代入した変数setDateを用意しました。 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) - 1; const dayN = Number(dayStatas.value); let setDate = new Date(yearStatasN, monthN, dayN); それを入力値の月(monthN)と比較してfalseならば、閏年の場合も含めた存在しない月日となります。 InputLimit(hantei, setDate, monthN); function InputLimit(HT, dateT, month){ if(!(HT === Infinity)){ if(dateT.getMonth() === month){//その月日が存在するかどうかを判定する const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(dateT); }else{ const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } この方法は JavaScriptで簡単に日付が有効かチェックする方法 の記事を参考にしました。 これはDateオブジェクトに値を入力したとき、その月日が存在しない場合は翌月にズレるという特性を利用しています。 4月2日の判定 以下のように判定方法をDateオブジェクトに置き換えただけです。 if( new Date(yearStatasN, monthN, dayN) > new Date(yearStatasN, 3, 1)){} ふりかえり 苦手だったDateオブジェクトから逃げていましたが、今回の課題を消化したことでDateオブジェクトに対し理解が深まりました。 アドバイスありがとうございました! 以下に更新したJavaScriptを載せます。 //履歴年表作成ボタンを押したらイベントスタート const RirekiNenpyouSakusei = document.getElementById('button'); RirekiNenpyouSakusei.addEventListener('click', () => { //プルダウンメニューの3つの値 const yearStatas = document.getElementById("pulldownYear"); const monthStatas = document.getElementById("pulldownMonth"); const dayStatas = document.getElementById("pulldownDay"); //プルダウンメニューの3つの値の前処理 const yearStatasN = Number(yearStatas.value) + 1900; const monthN = Number(monthStatas.value) - 1; const dayN = Number(dayStatas.value); //入力された値をDateオブジェクトに代入 let setDate = new Date(yearStatasN, monthN, dayN); //プルダウンメニューでハイフンが選択されているかどうかの判定用定数 const hantei = yearStatasN + monthN + dayN; let myHistory = []; function InputLimit(HT, dateT, month){ if(!(HT === Infinity)){//プルダウンメニューでハイフンが選択されているかどうかの判定 if(dateT.getMonth() === month){//存在する月日かどうかを判定 const erase = document.getElementById("cautionId"); erase.textContent = ``; return history(dateT); }else{ const cautionA = document.getElementById("cautionId"); cautionA.textContent = `値が不正です。`; return Sakuzyo(); } }else{ const cautionC = document.getElementById("cautionId"); cautionC.textContent = `注意!未入力の箇所があります。`; return Sakuzyo(); } } //大域変数myHistoryに配列を設定するための関数 const history = function(set){ const myHistoryPush = () =>{ myHistory.push(set.setFullYear(set.getFullYear() + 6)); myHistory.push(set.setFullYear(set.getFullYear() + 3)); myHistory.push(set.setFullYear(set.getFullYear() + 3)); myHistory.push(set.setFullYear(set.getFullYear() + 2)); myHistory.push(set.setFullYear(set.getFullYear() + 2)); } if( new Date(yearStatasN, monthN, dayN) > new Date(yearStatasN, 3, 1)){ myHistory.push(set.setFullYear(set.getFullYear() + 7)); myHistoryPush(); return output(myHistory); }else{ myHistory.push(set.setFullYear(set.getFullYear() + 6)); myHistoryPush(); return output(myHistory); } } InputLimit(hantei, setDate, monthN);//入力制限用関数の呼び出し function output(MH){//出力用 try{ const pa1 = document.getElementById("output1"); pa1.textContent = `小学校入学${new Date(MH[0]).getFullYear()}年4月`; const pa2 = document.getElementById("output2"); pa2.textContent = `小学校卒業${new Date(MH[1]).getFullYear()}年3月`; const pa3 = document.getElementById("output3"); pa3.textContent = `中学校入学${new Date(MH[1]).getFullYear()}年4月`; const pa4 = document.getElementById("output4"); pa4.textContent = `中学校卒業${new Date(MH[2]).getFullYear()}年3月`; const pa5 = document.getElementById("output5"); pa5.textContent = `高校入学${new Date(MH[2]).getFullYear()}年4月`; const pa6 = document.getElementById("output6"); pa6.textContent = `高校卒業${new Date(MH[3]).getFullYear()}年3月`; const radiocheckA = document.getElementById('radioA'); const radiocheckB = document.getElementById('radioB'); if(radiocheckA.checked){//ラジオボタンの値によって表示を変化させる const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; return; }else if(radiocheckB.checked){ const pa7 = document.getElementById("output7"); pa7.textContent = `専門・短大入学${new Date(MH[3]).getFullYear()}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `専門・短大卒業${new Date(MH[4]).getFullYear()}年3月`; return; }else{ const pa7 = document.getElementById("output7"); pa7.textContent = `大学入学${new Date(MH[3]).getFullYear()}年4月`; const pa8 = document.getElementById("output8"); pa8.textContent = `大学卒業${new Date(MH[5]).getFullYear()}年3月`; return; } }catch(err){ console.log(err); } } }); const Sakuzyo = () => {//入力制限用関数ではじかれた際に出力済みの結果を消去する const pa1 = document.getElementById("output1"); pa1.textContent = `----`; const pa2 = document.getElementById("output2"); pa2.textContent = `----`; const pa3 = document.getElementById("output3"); pa3.textContent = `----`; const pa4 = document.getElementById("output4"); pa4.textContent = `----`; const pa5 = document.getElementById("output5"); pa5.textContent = `----`; const pa6 = document.getElementById("output6"); pa6.textContent = `----`; const pa7 = document.getElementById("output7"); pa7.textContent = `----`; const pa8 = document.getElementById("output8"); pa8.textContent = `----`; } 参考文献:初めてのJavaScript
- 投稿日:2021-08-15T21:49:41+09:00
Web制作の学習で使用したサイト・サービス
はじめに Web制作の学習で使用した無料サイト・サービスの自分用メモ 随時更新☺ フロントエンド Progate 言わずとしれたサービス jsFiddle ブラウザ上でHTML/CSS/JavaScriptのコード実装と動作チェックができる JavaScrip実装時に、()の付け忘れとかあるとエラー出してくれるから超便利◎ VSCodeの代わりに使える!(^^)! 仕事ですぐに使えるTypeScript フューチャー株式会社(Future Corporation)が公開してるドキュメント サーバーサイド・API Talend API Tester - Free Edition Chromeブラウザの拡張機能で、簡単にAPIのGET/POSTリクエストが送信できる。 JSON Server JSONを返すだけのサーバー API作成前にサクっとJSONレスポンス~画面に表示するところを作りたいときに使った DB Airtable Googleスプレッドシートっぽく使える簡易データベース DBにデータ登録できるフォームやAPIがデフォルトで準備されていて超便利◎ 参考サイト ● Next.js で Airtable を簡易データベースとして使ってみるよ
- 投稿日:2021-08-15T20:38:59+09:00
[TypeORM]Entity metadata for Post#category was not found.
概要 TypeORMでエラーが表示されてマイグレーションができない "Entity metadata for Post#category was not found. Chech if you specified a correct entity object and if it's connection in the connection options." 原因 jsconfig.jsonに作成したEntityを追加していなかった 対処 jsconfig.jsonに作成したEntityのファイルパスを記入 ※TypeScriptの場合はtsconfig.json 追記内容 "include": [ "./entity.post.js", "./entity.category.js" ],
- 投稿日:2021-08-15T20:36:33+09:00
MapLibre GL JSで時系列コントロールを扱う[maplibre-gl-temporal-control]
TL;DR MapLibre GL JSで時系列表示をするためのプラグインmaplibre-gl-temporal-controlを開発したよ npm install mapligre-gl-temporal-controlで使えるよ maplibre-gl-temporal-control examples RasterTiles: https://kanahiro.github.io/maplibre-gl-temporal-control/raster.html VectorTiles: https://kanahiro.github.io/maplibre-gl-temporal-control/vector.html usage npm install maplibre-gl-temporal-control const map = new maplibregl.Map(mapOptions) // anyLayerはmaplibreレイヤーのオブジェクト map.addLayer(anyLayer1_1) map.addLayer(anyLayer1_2) map.addLayer(anyLayer2_1) map.addLayer(anyLayer2_2) map.addLayer(anyLayer3_1) map.addLayer(anyLayer3_2) // その他任意の数のレイヤー import TemporalControl from 'maplibre-gl-temporal-control'; // アニメ表示の各フレームを定義 const temporalFrames = [ { title:'frame1', // 時系列コントロールに表示されるタイトル layers:[anyLayer1_1, anyLayer1_2] // 1フレームに表示したいレイヤーの配列 }, title:'frame2', layers:[anyLayer2_1, anyLayer2_2] }, { title:'frame3', layers:[anyLayer3_1, anyLayer3_2] }, // 任意の数のフレーム ] const temporalControl = new TemporalControl(temporalFrames, { interval: 100, // 1フレームあたりの表示秒数、ms単位 position: 'top-left', performance: true // アニメ表示のパフォーマンスを向上させるフラグ }); map.addControl(temporalControl); ポイント Mapに追加したレイヤーオブジェクトと同じ内容のオブジェクトでフレームを生成しましょう(同じインスタンスである必要はない) performance: trueの場合、描画パフォーマンス向上のため、本来ゼロとする透過度を0.000000000000000000001とする(ベクター限定) このオプションは通常の用途では設定不要です、もし大量のベクターデータの描画が遅かったりしたら試してみてください 技術メモ TypeScriptで開発 CSSをファイルで定義すると、importしたりめんどうくさいので、すべてコード上で定義 同じような理由で、再生ボタンなどのアイコンはすべてbase64文字列として保持、ファイルサイズが小さいので容量増加のデメリットは無視
- 投稿日:2021-08-15T20:01:41+09:00
GoogleAPIs spreadsheetAPI 置換API
ぱっと調べてやってる人がいなかったので、備忘録として載せておきます。 fields meaning find 検索単語 replacement 置き換え単語 matchCase 検索で大文字と小文字が区別される場合はTrue matchEntireCell 検索値がセル全体と一致する必要がある場合はTrue searchByRegex 正規表現にする場合はtrue 正規表現ルールはこちら includeFormulas 検索に数式を含むセルを含める必要がある場合はTrue。数式を含むセルをスキップする場合はFalse。 sheetId シートID allSheets すべてのシートに適応するか range 範囲 JSON_representation.json { "find": string, "replacement": string, "matchCase": boolean, "matchEntireCell": boolean, "searchByRegex": boolean, "includeFormulas": boolean, // Union field scope can be only one of the following: "range": { object (GridRange) }, "sheetId": integer, "allSheets": boolean // End of list of possible types for union field scope. } find_replace_example.ts import { google, sheets_v4, Auth, Common } from 'googleapis' async execAPI(spreadsheetId: string, key: string, value: string) { const auth = await this.authentication(SpreadSheetExchangeScope) const sheets = google.sheets({ version: 'v4', auth }) /* batchUpdateのOption */ const request = { spreadsheetId: spreadsheetId, resource: { requests: [ { findReplace: { find: `\${${key}}`, replacement: value, allSheets: true } } ] } } try { const response = (await sheets.spreadsheets.batchUpdate(request)).data // TODO: Change code below to process the `response` object: console.log('Exchange Success') } catch (err) { console.log('err:', err) } } 正規表現による置換 regex.ts import { google, sheets_v4, Auth, Common } from 'googleapis' /** * Keyで検索し、valueに置き換える * @param spreadsheetId * @param key * @param value */ async regexExecAPI(spreadsheetId: string, key: string, value: string) { const auth = await this.authentication(SpreadSheetExchangeScope) const sheets = google.sheets({ version: 'v4', auth }); /* batchUpdateのOption */ const request = { spreadsheetId: spreadsheetId, resource: { requests: [{ findReplace: { find: "^\\$\\{.*\\}$", replacement: value, allSheets: true, searchByRegex: true }, }], }, } try { const response = (await sheets.spreadsheets.batchUpdate(request)).data; // TODO: Change code below to process the `response` object: console.log("exchange regex Success") } catch (err) { console.log("err:", err) } } 詳しくはこちらのドキュメントを見てください https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#FindReplaceRequest
- 投稿日:2021-08-15T19:47:37+09:00
nuxt.jsにてfabricでcanvasに四角形図形を追加できるようにする
目的 javascriptのみでnuxt.jsのアプリにfabric.jsを導入したい 前提 fabric.jsをインストールしている。 こちらの記事にインストール方法は書きました。 インストール vue3のcomposition apiを使いたいので、インストールします。 yarn add @nuxtjs/composition-api nuxt.config.jsのモジュールに追加しておきます。 nuxt.config.js buildModules: [ // https://go.nuxtjs.dev/eslint '@nuxtjs/eslint-module', // https://go.nuxtjs.dev/stylelint '@nuxtjs/stylelint-module', // https://go.nuxtjs.dev/vuetify '@nuxtjs/vuetify', '@nuxtjs/composition-api/module', ], vueファイル index.vue <template> <div> <h1>Hello, Fabric!!</h1> <v-btn @click="add()">add</v-btn> <div class="wrapper"> <canvas id="canvas" width="600" height="600" /> </div> </div> </template> <script lang="js"> import { computed, defineComponent } from "@nuxtjs/composition-api" import { fabric } from "fabric" export default defineComponent({ setup() { const canvas = computed(() => new fabric.Canvas("canvas")) const add = () => { canvas.value.add( new fabric.Rect({ top: 100, left: 100, width: 60, height: 60, fill: "red", }) ) } return { add } }, }); </script> <style scoped> .wrapper { width: 600px; height: 600px; border: 1px solid; } </style> どのように見えるか Addするとこのように表示される。四角形は自分の好きなように変形させることが可能。 参考 ありがとうございました。
- 投稿日:2021-08-15T18:43:29+09:00
JavaScriptで時計表示
リアルタイムで動く時計表示 <!DOCTYPE html> <html> <head> <title>サンプル</title> <script> window.onload = function() { setInterval(function() { var dd = new Date(); document.getElementById("T1").innerHTML = dd.toLocaleString(); }, 1000); } </script> </head> <body> <div id="T1"></div> </body> </html>
- 投稿日:2021-08-15T18:11:52+09:00
スクロールアニメーションを実装する
はじめに 以前、Intersection Observerを使った処理について学んだので、 せっかくなら、これを使って既存のサイトのアニメーションを再現してみようと思います。 お手本とするのは、こちらのサイトです。 IN SOMEWHERE NIGHTS - Charlotte is Mine 様々なモーションが施されている本サイトですが、 今回は中でも黒いラベルがスライドしていくアニメーションを模倣したいと思います。 実装するにあたって、参考サイトのソースコードは見ていません。 自分でロジックを考え、実装する力を鍛える事が目的です。 コード See the Pen ラベルアニメーション by sasha (@sasha_pen) on CodePen. *qiita上で見ると画像が表示されていませんが、 Codepenに飛んで頂くと、正常に画像も表示されると思います。 処理の流れ js側でやることはシンプルで、intersection observerを使い、対象要素にクラスを付与しているだけです。 ロジックとしては、表示領域に入ったら、まずlabelにクラスを付与して、スライドするアニメーションを起こし、次にテキストや画像などのコンテンツをsetTimeoutで時間差で発火させています。 初めはsetTimeOutとtransitionだけでアニメーションを作っていたのですが、滑らかに行かなかったので、代わりにanimtationを使用しました。 画像のサイズがMBを超えるような重いものだと、表示にラグがあったので、使用する画像のサイズには注意が必要です。 最後に 世にある既存のサイトのアニメーションを自分で実装してみるというのは、力試しになりますね。 引き続きやっていこうと思います。 参考 IN SOMEWHERE NIGHTS - Charlotte is Mine 特定のHTML要素の子要素、親要素、前後の要素を取得する
- 投稿日:2021-08-15T18:09:37+09:00
【CSS】【JavaScript】フェードイン・フェードアウト
目的 画像や文字が徐々に出現したり消えたりする、フェードイン・フェードアウト機能をCSSとJavaScriptでなるべく簡単に実装します。 実装 index.html <div class="img"> <img id="ghorst" class="ghorst" src="obake.png"> </div> <button id="hide-btn">いないいな〜い</button> <button id="appear-btn">ばぁ!</button> stylesheet.css /* アニメーションの設定 */ .ghorst { transition: opacity 3s; /* opacity要素(不透明度)の変更を3秒間かけて行う */ } script.js // 各要素を取得 const ghorst = document.getElementById('ghorst'); const hideBtn = document.getElementById('hide-btn'); const appearBtn = document.getElementById('appear-btn'); // 画像を透明にする処理 const ghorstHide = () => { ghorst.style.opacity = 0; // 画像要素のopacityプロパティの値を0(透明)にする }; // 画像を不透明にする処理 const ghorstAppear = () => { ghorst.style.opacity = 1; // 画像要素のopacityプロパティの値を1(不透明)にする }; // 各ボタン要素がクリックされた時の処理を指定 hideBtn.addEventListener('click', ghorstHide); appearBtn.addEventListener('click', ghorstAppear); 使用 プロパティ・メソッド CSSプロパティ 意味 参考サイト opacity 要素の不透明度を設定 MDN Web Docs transition 要素のアニメーションを定義する MDN Web Docs JavaScriptプロパティ・メソッド 意味 参考サイト getElementById() HTMLのidから要素を取得する MDN Web Docs style 要素のスタイルを変更する MDN Web Docs addEventListener() 要素にイベントが起こったときに実行される処理を指定 MDN Web Docs 解説 CSSプロパティのopacityで不透明度が指定できるため、transitionでopacityの値が変更されたときの変化アニメーションを定義します。 JavaScriptでopacityの値を変更することで、transitionで定義されたアニメーションが実行されます。 opacity 要素の不透明度を指定します。 0〜1の間で指定し、 0: 透明 1: 不透明 になっています。例えば、opacity: 0.5;と指定すると、その要素は半透明になります。 transition 要素の値が変更された際の、アニメーションの動きを定義します。transitonは、以下のの4つのプロパティを一括で指定できます(半角区切り)。 複数指定する場合はカンマ,で区切ります。 値の変更方法は、JavaScriptのclassListやstyle、CSSの:hoverなど。 プロパティ 意味 参考サイト transition-property 効果を適用するプロパティの指定(propaty: プロパティ) MDN Web Docs transition-duration アニメーションが完了するまでの時間を指定(duration: 持続・継続・間隔) MDN Web Docs transition-timing-function 変化の速度とタイミングを指定(timing function: タイミング機能・時間調整機能) MDN Web Docs transition-delay アニメーションが始まるまでの時間を指定(delay: 遅延・先延ばし) MDN Web Docs .class { transition: 対象プロパティ 完了までの時間 変化の速度 開始までの時間; } /* 複数指定 */ .class { transition: プロパティA 完了時間 変化速度 開始時間, プロパティB 完了時間 変化速度 開始時間, プロパティC 完了時間 変化速度 開始時間; } transition-property 変化させるCSSのプロパティを指定します。 フェードイン・フェードアウトでは、opacityを指定しています。 指定をallとすることで、全てのプロパティを同時に指定することもできます。 transition-duration アニメーションが完了するまでの時間の指定ができます。 s: 秒 ms: ミリ秒 の二つで指定できます。 今回は3sとすることで、3秒でフェードイン・フェードアウトをしています。 transition-timing-function アニメーションの変化の速度を指定できます。 値 意味 ease 滑らか(初期値) linear 一定速度 ease-in ゆっくり始まる ease-out ゆっくり終わる ease-in-out ゆっくり始まり、ゆっくり終わる cubic-bezier 3次ベジェ曲線を数値で指定(x1, x2, y1, y2) ease .car { transition: 4s ease; /* 4秒で滑らかに */ } linear .car { transition: 4s linear; /* 4秒間一定速度で */ } ease-in .car { transition: 4s ease-in; /* 4秒でゆっくり始まる */ } ease-out .car { transition: 4s ease-out; /* 4秒でゆっくり終わる */ } ease-in-out .car { transition: 4s ease-in-out; /* 4秒でゆっくり始まりゆっくり終わる */ } cubic-bezier .car { transition: 4s cubic-bezier(0, 0.76, 1, 0.23); /* 4秒で真ん中ゆっくり */ } 参考サイト: Cubic Bezier(ジェネレータ) transition-delay アニメーションの開始時間を指定できます。 s: 秒 ms: ミリ秒 の二つで指定できます。 .car { transition: 4s 3s; /* スタートを押してから3秒後に4秒間で移動 */ } style JavaScriptでCSSプロパティの値を取得・変更する際に使用します。 今回は、 const ghorst = document.getElementById('ghorst'); で、画像の要素を取得し、 ghorst.style.opacity = 0; とすることで、 opacityの値を変更しています。 この変更にtransitionプロパティが反応し、アニメーションが実行されます。 注意点 簡単にフェードイン・フェードアウトを実装できるtransitionですが、比較的新しい技術のため、古いブラウザ(IE9など)では動作しません。 対応ブラウザはDOM Web Docs(transition)や、Can I use (transition?)などを参照してください。 プラス さらに動きをつけてみました。 <!-- HTMLは変更ありません --> <div class="img"> <img id="ghorst" class="ghorst" src="obake.png"> </div> <button id="hide-btn">いないいな〜い</button> <button id="appear-btn">ばぁ!</button> /* レイアウトが崩れないように画像の場所取り */ .img { width: 300px; height: 320px; } /* 画像に高さ・幅を指定 */ .ghorst { width: 300px; height: 320px; transition: all 3s; /* width, height, opacityをまとめて指定するため、プロパティを all に変更 */ } /* フェードアウトするときの値 */ .hide { width: 0; height: 0; opacity: 0; } /* フェードインするときの値 */ .appear { width: 300px; height: 320px; opacity: 1; } const ghorst = document.getElementById('ghorst'); const hideBtn = document.getElementById('hide-btn'); const appear = document.getElementById('appear-btn'); // クラスを付け替えることで、まとめて値を変更 const ghorstHide = () => { ghorst.classList.remove('appear'); ghorst.classList.add('hide'); }; const ghorstAppear = () => { ghorst.classList.remove('hide'); ghorst.classList.add('appear'); }; hideBtn.addEventListener('click', ghorstHide); appear.addEventListener('click', ghorstAppear); classList 要素のクラスを操作することができるプロパティです。 remove()メソッドを使用することで、クラスを削除し、add()メソッドでクラスを追加しています。 MDN Web Docs ご覧いただき、ありがとうございました。
- 投稿日:2021-08-15T18:07:51+09:00
Vueでお絵かきアプリ作成1(canvas使用)
趣向 vueでお絵かき機能を作成する機会があったため作ってみました! はじめに このページではお絵描きできるCanvasの実装まで行います。 まずはvueの作成。 widthとheight で枠の大きさ、lineWidthでペンの太さ。strokeStyleでペンの色を決めることができます。 JavaScript new Vue({ el: '#app', data() { return { weightNum: 5, canvas: null, color: "black", context: null, } }, mounted () { // カンバス作成 this.canvas = document.querySelector('#myCanvas') this.context = this.canvas.getContext('2d') this.context.lineCap = 'round' this.context.lineCap = 'round' this.canvas.width = 700 this.canvas.height = 600 this.context.lineWidth = this.weightNum this.context.strokeStyle = this.color }, }); 次にhtml。canvasを作成し、マウスを動かした時の処理を書いていきます。 html <div id="app"> <main> <div> <canvas id="myCanvas" ref="myCanvas" style="border:1px solid #aaa" @mousedown="dragStart" @mouseup="dragEnd" @mouseout="dragEnd" @mousemove="draw"></canvas> </div> </main> </div> このままではエラーになってしまうのでマウスを動かした時の処理をvueにも追加 JavaScript methods: { // 描画 draw :function(e) { let x = e.layerX let y = e.layerY if(!this.isDrag) return; this.context.lineTo(x, y); this.context.stroke(); }, // 描画開始(mousedown) dragStart:function(e) { let x = e.layerX let y = e.layerY this.context.beginPath(); this.context.lineTo(x, y); this.context.stroke(); this.isDrag = true; this.context.getImageData(0, 0, this.canvas.width, this.canvas.height) }, // 描画終了(mouseup, mouseout) dragEnd: function() { this.context.closePath(); this.isDrag = false; } } とりあえずこれでかけるようになったと思います。 次回はペン機能と消しゴム機能をわけ。ペンの太さと色を変更できるようにしたいと思います。
- 投稿日:2021-08-15T17:07:46+09:00
Google Nest Hubに、WebRTCで映像配信してみた
Google Nest Hubは、Googleが提供するスマートディスプレイです。 「OK Google」ができる上に、ディスプレイがついているので、いろいろ役に立ちそうです。 ということで、ディスプレイがついているので、映像配信をしてあげようと思います。映像配信と言えば、最近はやりのWebRTCを使います。 Googleのスマートディスプレイで、WebRTCを配信するために、以下を試してみました。 ①Googleスマートホームとしてカメラを実装する。 ②GoogleアシスタントのInteractive Canvasを使う。 ①が、もともとやりたかったことです。 Smart Home CameraStream Trait Schema 上記のページを見ると、スマートホームのトレイトであるCameraStreamがWebRTCが対応しているではないか。。。。 結論からいうと、うまく動きませんでした。 対応デバイスとして、smart displays、Chromecast-enabled devices、smartphonesとあるので、てっきりGoogle Homeアプリからつかえるのかと思いきや動かなかったですし、smart displaysで動かそうにも、cameraStreamSignalingUrl にリクエストを投げてくれず、断念。 以下ご参考まで。 Google Smart HomeデバイスをAWS IoTで実装してみた ESP32をGoogle Smart Homeデバイスにする 今回は②で実現します。 Interactive Canvas スマートディスプレイにHTMLを表示できるんです。 なので、以前以下の投稿で、AWS Kinesis Video Streamsを使ったWebRTC配信をした時のページを使えばできそうです。Interactive Canvasも以下の投稿で経験済み。 AWS Kinesis Video StreamsでMMDをWebRTCで配信する Actions on GoogleのInteractive Canvasを試してみる 以降で流れやつまずきやすいところを示しておきます。詳細は、上記の記事をご参照ください。 もろもろのソースコードはGitHubに上げてあります。 poruruba/WebRTC_InteractiveCanvas Actions on Googleプロジェクトを作成する まずは、Actions Consoleから、プロジェクトを作成します。 このときスマートディスプレイにリンクしたアカウントと同じアカウントでログインしておきます。 Actions Console https://console.actions.google.com/ 「New Project」ボタンを押下し、適当なプロジェクト名を入力します。 例えば、「MyInteractiveCanvas」など。 言語は、Japanese、国はJapanを選択。 What kind of Action do you want to build? では、Gameを、 How do you want to build it? では、Blank projectを選択します。 左側のナビゲーションからSettingsを選択し、Display nameに呼び出しやすい名前を入力しておきます。例えば、「テストキャンバス」など 次に、Custom Intentを作成します。 適当な名前でいいのですが、「ContinueIntent」とします。 training phrasesに、「継続して」を入れておきます。 次に、Sceneを作成します。 とりあえず、名前は「MainScene」とでもしておきます。 User intent handlingには、Intentとして先ほど作成した「ContinueIntent」を割り当て、Call your webhooksにチェックを入れて、「continue」と入力しておきます。 次に、Main invocationを選択し、Call your webhookにチェックを入れて、startと入力、Transitionとして、先ほど作成した「MainScene」を選択しておきます。 次に、左側のナビゲーションから、Interactive canvasを選択し、「Enable Interactive Canvas with server webhook fulfillment」を選択します。 次に、また左側のナビゲーションからWebhookを選択し、HTTPS endpointを選択、URLにこれから立ち上げるサーバのURLを入力します。 こんな感じ、 https://XXX.XXX.XXX/canvas-api ポート番号を付けたい場合はつけて大丈夫です。 サーバを立ち上げる 基本的には、Githubからダウンロードしたものを展開し、npm installすれば、環境は完成します。 受け付けるWebhookの実装は以下にあります。 api/controllers/canvas-api/index.js api/controllers/canvas-api/index.js 'use strict'; const base_url_html = '【WebページのURL】'; const AWS_ACCESSKEY_ID = '【AWSのアクセスキーID】'; const AWS_SECRET_ACCESSKEY = '【AWSのアクセスキーシークレット】'; const { conversation, Canvas, } = require('@assistant/conversation') const app = conversation({ debug: true }); app.handle('start', conv => { console.log(conv); conv.add('これはインタラクティブキャンバスです。'); if (conv.device.capabilities.includes("INTERACTIVE_CANVAS") ){ conv.add(new Canvas({ url: base_url_html + '/interactivecanvas_webrtc/index_viewer.html', enableFullScreen: true, data: { AWS_ACCESSKEY_ID: AWS_ACCESSKEY_ID, AWS_SECRET_ACCESSKEY: AWS_SECRET_ACCESSKEY, } })); }else{ conv.scene.next.name = 'actions.scene.END_CONVERSATION'; conv.add('この端末はディスプレイがないため対応していません。'); } }); app.handle('continue', async conv => { console.log(conv); conv.add(new Canvas({ data: { message: "continue" } })); }); exports.fulfillment = app; それぞれ、startとcontinueをフックしています。 Actions Consoleで入力していたキーワードです。 startは、本アプリを起動した直後に、スマートディスプレイからトリガされます。continueは、KEEP ALIVE的に使っていまして、適当な応答を返しています。(のちほどWebページのところで説明します) api/controllers/smarthome/swagger.yaml で示した通り、/canvas-apiというエンドポイントで公開するようにしています。 api/controllers/smarthome/swagger.yaml swagger: '2.0' info: version: 'first version' title: Lambda Laboratory Server paths: /canvas-api: post: x-handler: fulfillment parameters: - in: body name: body schema: $ref: "#/definitions/CommonRequest" responses: 200: description: Success schema: $ref: "#/definitions/CommonResponse" やっていることは、startのところが重要で、Canvasで指定のURLを表示するようにスマートディスプレイに伝えています。 npmモジュールの「@assistant/conversation」を使わせてもらっていますが、これで実装が楽になってます。 conv.add(new Canvas({ url: base_url_html + '/interactivecanvas_webrtc/index_viewer.html', enableFullScreen: true, data: { AWS_ACCESSKEY_ID: AWS_ACCESSKEY_ID, AWS_SECRET_ACCESSKEY: AWS_SECRET_ACCESSKEY, } enableFullScreenは、表示するページを全画面で表示させる指示です。 dataにあるデータは、この値がそのままスマートディスプレイで表示されるWebページのJavascriptに渡されるので、任意の振る舞いがJavascriptで実装できます。 ここで渡しているのは、AWSのクレデンシャル情報です。 Webページの作成 Webページ側のJavascriptを示しておきます。 Webページは、以下に置いてあります。 public/interactivecanvas_webrtc public/interactivecanvas_webrtc/js/start.js 'use strict'; //const vConsole = new VConsole(); //window.datgui = new dat.GUI(); const AWS_ACCESSKEY_ID = ''; const AWS_SECRET_ACCESSKEY = ''; const SIGNALING_CHANNEL_NAME = 'Room1'; const SIGNALING_CLIENT_ID = ""; const VIEW_WIDHT = 640; const VIEW_HEIGHT = 480; const UPDATE_INTERVAL = 10000; var updated = false; var vue_options = { el: "#top", mixins: [mixins_bootstrap], data: { aws_accesskey_id: AWS_ACCESSKEY_ID, aws_secret_accesskey: AWS_SECRET_ACCESSKEY, signaling_channel_name: SIGNALING_CHANNEL_NAME, signaling_client_id: SIGNALING_CLIENT_ID, width: VIEW_WIDHT, height: VIEW_HEIGHT, webrtc_opened: false, message_logs: '', message_data: '', signalingClient: null, margin: 0, video_style: {}, }, computed: { }, methods: { startViewer: async function() { try { var params = { accessKeyId: this.aws_accesskey_id, secretAccessKey: this.aws_secret_accesskey, channelName: this.signaling_channel_name, clientId: this.signaling_client_id || getRandomClientId(), openDataChannel: true, useTrickleICE: true, }; this.signalingClient = await startViewer(document.querySelector('#remote-view'), params, (type, event) => { console.log(type, event); if (type == 'sdpAnswer' ){ this.webrtc_opened = true; this.panel_close('#webrtc_config_panel'); this.video_style = { position: "absolute", top: 0, width: "100%", height: "100%", background: "#FFF" }; }else if( type == 'close'){ this.webrtc_opened = false; this.panel_open('#webrtc_config_panel'); }else if( type == 'message'){ var now = new Date().toLocaleString('ja-JP', {}); this.message_logs = '[' + now + ' - master] ' + event.event.data + '\n' + this.message_logs; } }); } catch (error) { console.error(error); alert(error); } }, stopViewer: function(){ stopViewer(this.signalingClient); }, send_message: function(){ try{ sendViewerMessage(this.message_data); this.message_logs = '[' + new Date().toLocaleString('ja-JP', {}) + ' - local] ' +this.message_data + '\n' + this.message_logs; }catch(error){ console.error(error); alert(error); } } }, created: function(){ }, mounted: function(){ proc_load(); const callbacks = { onUpdate: (data) => { console.log(data); if( !updated ){ this.aws_accesskey_id = data[0].AWS_ACCESSKEY_ID; this.aws_secret_accesskey = data[0].AWS_SECRET_ACCESSKEY; window.interactiveCanvas.getHeaderHeightPx() .then(height => { console.log("getHeaderHeightPx:" + height); this.margin = height; }); setInterval(() =>{ if ( this.webrtc_opened ) window.interactiveCanvas.sendTextQuery("継続して"); }, UPDATE_INTERVAL); updated = true; } }, }; window.interactiveCanvas.ready(callbacks); } }; vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog vue_add_global_components(components_bootstrap); vue_add_global_components(components_utils); /* add additional components */ window.vue = new Vue( vue_options ); function getRandomClientId() { console.log("call: getRandomClientId"); return Math.random() .toString(36) .substring(2) .toUpperCase(); } 大事なのは以下のところです。 const callbacks = { onUpdate: (data) => { console.log(data); if( !updated ){ this.aws_accesskey_id = data[0].AWS_ACCESSKEY_ID; this.aws_secret_accesskey = data[0].AWS_SECRET_ACCESSKEY; window.interactiveCanvas.getHeaderHeightPx() .then(height => { console.log("getHeaderHeightPx:" + height); this.margin = height; }); setInterval(() =>{ if ( this.webrtc_opened ) window.interactiveCanvas.sendTextQuery("継続して"); }, UPDATE_INTERVAL); updated = true; } }, }; window.interactiveCanvas.ready(callbacks); window.interactiveCanvas.ready(callbacks); を呼び出すと、Actions on Googleのサーバと接続し、接続が完了すると、コールバック関数として実装したonUpdateを呼び出してくれます。 このonUpdateは、最初の接続時だけでなく、以降のIntentによるリクエスト/レスポンスの際にも呼ばれます。 最初の接続時には、dataで指定しておいたAWSクレデンシャルが届くので、内部で覚えておきます。 その後、setIntervalで以下を定期的に呼ぶようにしています。 window.interactiveCanvas.sendTextQuery("継続して") 何もしないと、切断されてしまうので、継続的に発話しているように見せかけています。 継続して、と声で言ったことと同じ動作となり、サーバ側ではContinueIntentが一致し、app.handle(‘continue’でトリガされるという流れです。 あとは、以前の投稿の通り、WebRTC接続することで、映像が配信されてきます。 ついでに、WebRTCの接続が完了すると、HTML Videoタグを全画面表示に切り替えてみやすくなるようにしています。 注意事項 以下の部分で指定するurlのポート番号は無し、すなわち443番にしてください。私はこれではまりました。 api\controllers\canvas-api\index.js conv.add(new Canvas({ url: base_url_html + '/interactivecanvas_webrtc/index_viewer.html', enableFullScreen: true, data: { AWS_ACCESSKEY_ID: AWS_ACCESSKEY_ID, AWS_SECRET_ACCESSKEY: AWS_SECRET_ACCESSKEY, } })); もし443番のポート番号で用意するのが難しそうであれば、とりあえず以下を指定してみてください。 動かす前の準備 まずは、AWS Kinesis Video Streamsを使う想定で、シグナリングチャネル名を作成しておきます。 AWS Kinesis Video Streams 名前を決めるだけで作成できます。 動かしてみる まずは、WebRTCのマスター側で、配信を開始しておきます。 シグナリングチャネル名は、先ほど作成した「Room1」「Room2」「Room3」のいづれかにしておきましょう。 詳細は以下の通り。 AWS Kinesis Video StreamsでMMDをWebRTCで配信する Start Masterボタンを押すと。。。 それではさっそくスマートディスプレイで動かしてみましょう。 Nest Hubに以下を言います。 「OK、Google。テストキャンバスにつないで」 Actions on Googleの設定で、Display nameに指定した名前です。 上記は、エミュレータで表示したときの画像ですが、本物のスマートディスプレイやスマートフォンでもちゃんと表示されます。 ここで、「StartViewer」ボタンを押下すると、めでたく映像配信を受信できるかと思います。 ちなみに、Androidからも、「OK、Google。テストキャンバスにつないで」と言えば、同じように動きます。 以上
- 投稿日:2021-08-15T15:00:44+09:00
更新するべきパッケージ一覧をCSVで出力(ruby, js)
メモ Ruby $ bundle outdated --strict | sed -n "/^Gem /,\$p" | sed -e "s/ \+/,/g" 出力例 i18n-js,3.8.0,3.8.4,~> 3.8.0,default js-routes,1.4.9,2.0.8,>= 0,default json,2.3.1,2.5.1 jwt,2.2.2,2.2.3 JavaScript jqを利用する package-lock.json $ npm outdated --json | jq -r "to_entries | .[] | [.key, .value.current, .value.wanted, .value.latest] | @csv" 出力例 "i18next","19.8.7","19.9.2","20.4.0" "jest","26.6.3","26.6.3","27.0.6" ... yarn.lock $ npx yarn outdated --json | tail -n 1 | jq -r ".data.body[] | [.[0], .[1], .[2], .[3]] | @csv" 出力例 "@babel/plugin-transform-runtime","7.13.10","7.15.0","7.15.0" "@babel/preset-typescript","7.13.0","7.15.0","7.15.0" ... 1つのプロジェクト以下に複数のpackage-lock.jsonやyarn.lockがある時 $ find . -type d -name node_modules -prune -o -type f -name 'package-lock.json' -print -o -type f -name 'yarn.lock' -print | xargs -I{} dirname {} | uniq | xargs -I{} bash -c 'echo {} && cd {} && (if [ -e ./package-lock.json ]; then (npm outdated --json | jq -r "to_entries | .[] | [.key, .value.current, .value.wanted, .value.latest] | @csv"); else (npx yarn outdated --json 2>/dev/null | tail -n 1 | jq -r ".data.body[] | [.[0], .[1], .[2], .[3]] | @csv"); fi)', .value.wanted, .value.latest] | @csv"); else (npx yarn outdated --json 2>/dev/null | tail -n 1 | jq -r ".data.body[] | [.[0], .[1], .[2], .[3]] | @csv"); fi)' 出力例 ./hoge #<= ディレクトリパス "@babel/plugin-transform-runtime","7.13.10","7.15.0","7.15.0" "@babel/preset-typescript","7.13.0","7.15.0","7.15.0" ... ./hoge/foo "i18next","19.8.7","19.9.2","20.4.0" "jest","26.6.3","26.6.3","27.0.6" ... ./fuga "jest","26.6.3","26.6.3","27.0.6" ... 参考 https://bundler.io/man/bundle-outdated.1.html https://docs.npmjs.com/cli/v7/commands/npm-outdated https://classic.yarnpkg.com/en/docs/cli/outdated/
- 投稿日:2021-08-15T14:37:09+09:00
JS単体アプリケーションにNeutralinoを検討してみる
はじめに JSで単体アプリケーションを作成する際Electronでは重すぎて何か代替案はないかと模索していた。 見つけた。 GIt Hub neutralinojs 公式 neutralinojs 動作も軽くビルドもアプリケーションの起動もElectronと比較にならない程速い。 制限は色々とあるものの要件にフィットすればマルチプラットフォームなJSデスクトップアプリとしてElectronの代替になりうると思う。 『業務の効率化を図るためチームのメンバにツールを配布』と言った際には十分はまると思う。 検証環境 以下の環境で開発 OS:windows10 node:14.15.4 npm:7:20.1 neutralinojs/neu : 5.2.1 環境構築 以下のコマンドでNeutralinojsをグローバルにインストール(gじゃなくても可) npm install -g @neutralinojs/neu アプリケーションの作成 neu create myapp アプリケーションの実装 作成されたindex.htmlを編集(実行ファイルの重さがどれくらいになるか知りたかったので色々入れてみた) ムダにSPAを導入(メニューはettricsさんから頂く) ムダにSME(simple markdown editor)を導入しみてた。 ムダにローディングを導入しみてた。 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>NeutralinoJs test</title> <link id='stylesheet' rel='stylesheet' href='./css/bonsai-base.min.css'> <link rel="stylesheet" href="./css/page.css"> <link rel="stylesheet" href="./css/styles.css"> </head> <body> <div id="neutralinoapp"> <!-- menu --> <header class="header"> <div class="burger"> <div class="burger__patty"></div> <div class="burger__patty"></div> <div class="burger__patty"></div> </div> <nav class="menu"> <div class="menu__brand"> <h3>Neutralinojs test</h3> </div> <ul class="menu__list"> <li class="menu__item"><a class="menu__link" name="start"></a></li> <li class="menu__item"><a href="#Page1" class="menu__link" name="part1">Page1</a></li> <li class="menu__item"><a href="#Page2" class="menu__link" name="part2">Page2</a></li> <li class="menu__item"><a href="#Page1" class="menu__link" name="part1">Page3</a></li> <li class="menu__item"><a href="#Page2" class="menu__link" name="part2">Page4</a></li> </ul> </nav> </header> <!-- 各ページメイン(sectionごと) --> <section id="start" class="fade"> <h2>Neutralinojs test</h2> </section> <section id="Page1" class="fade"> <div class="section-content"> <h3>設定</h3> <fieldset> <legend>ヘッダ</legend> <div class="grid" style="--col: 2"> <p><label>タイトル<input type="text" placeholder="タイトル"></label></p> <p><label>サブタイトル<input type="text" placeholder="サブタイトル"></label></p> </div> <p><label>メインイメージファイル<input type="text" placeholder="メインイメージファイル名"></label></p> <label>デスクリプション<textarea placeholder="Hello People..."></textarea></label> </fieldset> </div> </section> <section id="Page2" class="fade"> <div class="section-content"> <textarea id="editor" name="name" rows="8" cols="40"></textarea> </div> </section> <section id="Page3" class="fade"> <div class="section-content"> page3 </div> </section> <section id="Page4" class="fade"> <div class="section-content"> page4 </div> </section> <!-- ローディング --> <div class="loading-wrap" style="display:none"> <div class="loader">Loading...</div> </div> </div> <!-- Neutralino.js client. This file is gitignored, because `neu update` typically downloads it. Avoid copy-pasting it. --> <script src="src/page.js"></script><!-- page control--> <script src="src/neutralino.js"></script> <!-- Your app's source files --> <script src="src/service.js"></script> <script src="src/controller.js"></script> <!-- loading --> <link rel="stylesheet" href="./css/loading.css" /> <!-- Simple MDE(Mark Down Editor) --> <link rel="stylesheet" href="./css/simplemde.min.css"> <script src="./lib/simplemde.min.js"></script> <script>var simplemde = new SimpleMDE({ element: document.getElementById("editor") });</script> </body> </html> 画面 初期画面 メニュ(ettricsさんのソースをダダぱくり) 入力画面(classlessフレームワークとしてbonsai.css導入) マークダウンエディタ(SimpleMDE) アプリケーションの実行 neu run 尚、windowsの場合以下のコマンドの実行が1度だけ必要らしい CheckNetIsolation.exe LoopbackExempt -a -n="Microsoft.Win32WebViewHost_cw5n1h2txyewy ビルド neu build --release ムダに重くしようと試みましたがdist配下の配布用ファイルの合計は4.5MB。Electronとは2桁違い。比較の対象にならない程軽い!(releaseオプションでできるzipファイルは1MBちょっと) デバッグ デバッグ時以下のコマンドで変更をウォッチしてくれます。 またneutralino.config.jsonファイルで"defaultMode": "browser"にすればweb viewでなくブラウザでもデバッグが出来ます(ファイルの読み書きなどしている際は制限有) neu listen 途中で感じたメリット/デメリットをまとめてみた メリット 環境構築が手軽 ビルドが速い jsの資産が利用できる 実行ファイルが異常に軽い typescript可 vueやReact等で画面を作ることも可 デメリット データベースが利用できない(代替案としてNeutralino.storageが用意されている、が貧弱) requireが使用できないので既存のコードにrequireが入っていれば修正の必要がある 短期間で破壊的に変更してくれる 製作者がインド人でチュートリアルの英語がひどい 参考1 ファイルの読み書き /* jsonファイル読込 */ async function readJsonFile(file){ let response = await Neutralino.filesystem.readFile({ fileName: file }); return JSON.parse(response.data); } /* jsonファイル書込 */ async function writeJsonFile(fileName,fileContents,message){ await Neutralino.filesystem.writeFile({ fileName: fileName, data: JSON.stringify(fileContents), }); } 参考2 ストレージ 以下公式そのままだがLocalStorage感覚でNeutralinoが管理するストレージにデータを保管することが出来る /* データの保存 */ await Neutralino.storage.putData({ bucket: 'userDetails', data: JSON.stringify({ username: 'TestValue' }) }); /* データの取得 */ let response = await Neutralino.storage.getData({ bucket: 'userDetails' });
- 投稿日:2021-08-15T12:43:21+09:00
deno deployで簡単なWeb APIを作る
deno deployを利用して簡単なWeb APIを作ってみようと思います。 どんなAPIがいいか迷ったのですが、文章を単語に分割してくれるIntl.Segmenterという関数を発見。 今回はこのIntl.Segmenterを使って、クエリパラメータで渡した文章を単語に分割し、JSONで返すAPIを作っていきましょう。 Web APIのひな型 公式ドキュメントを参考に、クエリパラメータに応じてJSONを返却するコードを書くと、このようになります。 addEventListener("fetch", (event) => { // リクエストされたURLから結果データを作成 const url = new URL(event.request.url) const result = /*urlを元に結果データを作成するコードをここに書く*/ // 結果データをJSON化し、クライアントに返却 event.respondWith( new Response(JSON.stringify(result), { status: 200, headers: { "content-type": "application/json", }, }), ); }); 後は4行目を穴埋めするだけです。 結果データを作成する処理を書く まずURLからクエリパラメータを取り出します。url.searchParams.get('パラメータ名')でOKです。 /* https://word-seg.deno.dev/?text=国境の長いトンネルを抜けると、そこは錦糸町だった。&lang=ja -> targetText: "国境の長いトンネルを抜けると、そこは錦糸町だった。" -> targetLang: "ja" */ const url = new URL(event.request.url) const targetText = url.searchParams.get('text') ?? `吾輩は猫である。名前は${crypto.randomUUID()}` const targetLang = url.searchParams.get('lang') ?? 'ja' ついでにクエリパラメータが与えられなかった時の処理も書いておきましょう。ここでは、適当なデフォルト値にフォールバックさせることにしました。猫の名前はcrypto.randomUUID()に決めてもらいます。 次に、取り出したクエリパラメータから結果データを作成する処理を書きます。 /* 吾輩は猫である。名前はたぬき。 -> result: ["吾輩", "は", "猫", "で", "ある", "。", "名前", "は", "たぬき", "。"] */ const segments = new Intl.Segmenter(targetLang, {granularity: "word"}).segment(targetText); const result = [...segments].map(seg=>seg.segment) 以上で必要な処理は全て書き終わりました!続いて、deno deployからデプロイしていきましょう。 deployする 1. githubへソースコードをpush 先ほど作成したソースコードを、githubへアップロードします。(詳しい手順は割愛) addEventListener("fetch", (event) => { // リクエストされたURLから結果データを作成 const url = new URL(event.request.url) const targetText = url.searchParams.get('text') ?? `吾輩は猫である。名前は${crypto.randomUUID()}` const targetLang = url.searchParams.get('lang') ?? 'ja' const segments = new Intl.Segmenter(targetLang, {granularity: "word"}).segment(targetText); const result = [...segments].map(seg=>seg.segment) // 結果データをJSON化し、クライアントに返却 event.respondWith( new Response(JSON.stringify(result), { status: 200, headers: { "content-type": "application/json", }, }), ); }); 2. プロジェクトの作成 まず、 https://deno.com/deploy にアクセスし、アカウントを登録します。 次に、 https://dash.deno.com/new から新しいプロジェクトを作成します。 ここで設定した名前が公開時にドメイン名の一部に使われます。 例えば myapp という名前を使うと、 myapp.deno.dev というドメイン名になります。(自分で用意したドメインを使うことも可能) 3. コードの登録 次に、作成したコードを登録します。 コードの登録は github連携からデプロイ URLからデプロイ の2種類のやり方があります。 今回はgithub連携を使用します。 「Continue」を選択すると、githubのURLを聞いてくるので、先ほどpushしたリポジトリのURLを入力します。 ちなみに、他人のリポジトリでも大丈夫らしい。 最後に「Link」をクリックするとデプロイ完了です。 簡単に出来ましたね。 作成したAPIはこちら↓で公開中です! コードを変更したい時はどうするか 普通にcommitしてpushすると更新されます。 プルリクエストを開くと、変更後のコードを利用したプレビュー用のデプロイが公開されます。 緑色のチェックマークをクリック→detailsをクリック でプレビュー用のページに飛ぶことが出来ます。 まとめ 記事中のコードにCORS対応を入れるの忘れました。
- 投稿日:2021-08-15T12:30:04+09:00
integromatでGmailとLINEを連携 週間業務リマインド&効率化
忘れてた・・・から始まるイレギュラー 「沖中さん 週間業務報告書と交通費申請まだですか?」集計担当者からの催促メールで 慌ててバタバタ作業開始。メールに気づいてないときは、催促電話をもらう始末。 ファイルを探し、週間を振り返りつつ急いで記入、ん?さっきまで何してたっけ・・・ うっかりから発生するムダを 自動化で少しでも減らしたい 何をどうやって自動化するか決める まず関連するすべての業務の洗い出し。自動化できそうな業務をピックアップ。 次にどのように自動化するのかを決める。 ①リマインドメール送信 integromatでGmailとLINEを連携。報告〆曜日(月)に毎週購読しているメルマガがあり、 毎週メルマガを受信したら、GmailからリマインドメールをOutlookとLINEに自動配信。 ②Excelファイルを探す・送信 Excelの報告書はGoogleDrive保管。Outlookリマインドメール内にファイルURLを記載し、 メール上からクリック1つでジャンプ。集計担当者とURLを共有し、メール送信業務廃止。 ③交通費申請ページへ移動 同様にOutlookメール内に交通費申請ページURLを記載し、即申請ページに移行可。 integromatでGmailとLINEを連携 「メルマガと連携せず、毎週月曜9時にリマインド自動配信」も検討したが、 決まった時間に通知が来ると効果が薄れると考え、毎週月曜日午前中のランダムな時間に 配信されるメルマガとの連携をあえて継続した。 ①GmailからOutlookへリマインドメール リマインドメール本文から ①報告Excelシート②申請画面 に1クリック即ジャンプ! 思い起こさせてくれるだけでなく、すぐに作業に取り掛かれるよう工夫してみました! ②GmailからLINEへのリマインド通知 Outlook&LINEのダブルリマインド! さらに発展 PowerAutomateDesktopで自動化範囲拡大 交通費申請も『途中まで自動化できるのでは?』と思い、PADで自動化してみました! 手作業箇所直前のルーティンワーク4ステップを 1クリックで自動化 さいごに 身近な業務効率化の威力 同じ部内のメンバーに本技術を紹介。「すごい!今すぐ俺のPCにも入れてくれ!」と 太鼓判。今回の数倍時間をかけた「品切AI」を見せた際の「ふーんすごいねー(遠い目)」 という反応とは雲泥の差で驚愕。技術のすごさより、便利かどうかが重要だと身に染みた。 今後も自分の技術は磨きつつ「大勢が共感・利用できる身近で小さな業務改善」と 「将来的な大きな業務改善」の両方にトライできるようにしていきたい!
- 投稿日:2021-08-15T11:31:38+09:00
JavaScriptを基本からまとめてみた【11】【onchangeイベント】
はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。 入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、 卒業後、フロントエンドのエンジニアを目指す事に。 Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。 JavaScriptのonchangeイベントとは 『onchangeイベント』とは、入力欄の内容を変更した時のイベント。JavaScriptでプログラミングを行っていると、ユーザーの入力に合わせて動的に表示内容を変えたい場合に便利なのがonchangeイベントで、HTML要素の値の変更が完了した時に発生するイベント。 対象のHTML要素は と 、及び で、値の変更が完了したタイミングは、 と は入力が完了して他の要素にフォーカスが移動した時、 は選択値が変わった時になる。 onchangeイベントの書き方 onchangeイベントの書き方は2つある。 ① HTMLで指定する方法 <input onchange="関数名()"> ② JavaScriptで指定する方法 document.getElementById(“要素名”)で対象の要素を指定する。関数名を指定せず、以下のように直接function()を記述できる。※ onchangeのようなイベントが発生した時に動作する関数をイベントハンドラーと呼びます。 document.getElementById("要素名").onchange = function() { // onchangeイベントが発生した時の処理を記述する }; 参考サイト アルファシス JavaScriptのonchangeイベントの使い方を現役エンジニアが解説【初心者向け】
- 投稿日:2021-08-15T11:10:03+09:00
【JavaScript】 海外Teck系YouTuberを真似てみた!(part 5) Typing Game
1,はじめに この記事は、海外Teck系YouTuberの動画を参考に、同じプロジェクトを作成してみたものになります! 簡単にですが、動画を通して学べた技術や知識をまとめました! 今回の完成品は、下記に載せています 2,学んだこと async awaitの記述方法 fetchの記述方法 3,参考動画 4,完成
- 投稿日:2021-08-15T09:45:03+09:00
Selenium(JavaScript環境)のtakeScreenshotでエラーが出たときの対応
JavaScript環境でScreenshot撮ろうと思ったらエラーが出たので対応メモ。 場所の準備 cd mkdir ss_test cd ss_test npm install selenium-webdriver chromedriver dayjs dayjsはおまけ。 実装 const { Builder, By } = require("selenium-webdriver"); require("chromedriver"); const fs = require("fs"); const dayjs = require("dayjs"); (async()=>{ const driver = await new Builder().forBrowser("chrome").build(); await driver.get("http://www.yahoo.co.jp"); const img = await driver.takeScreenshot(); const fileName = "evicence_" + dayjs().format("YYYYMMDDHHmmss") + ".png"; await fs.writeFileSync(fileName,img,"base64"); await driver.quit(); })(); エラー ECONNREFUSED connect...的なエラーが出た。 原因と対応 どうやらwebdriver-managerなるもんが起動してないことによるものらしい。 npx webdriver-manager start --standalone
- 投稿日:2021-08-15T06:30:30+09:00
JavaScript: 画像から色を抽出してくれるColor Thief
10 Trending projects on GitHub for web developers - 13th August 2021 で紹介されていた Color Thief というもの。 「例」引用: 画像から色を抽出してくれる Color Thief お試し。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> </head> <body> <h2>https://lokeshdhakar.com/projects/color-thief/<h2> <img id="thomas" src="thomas.png" width=30% height=30% crossorigin="anonymous" /> <label id="label1">Do you like Train?</label> <img id="scene" src="lime.jpg" width=40% height=40% crossorigin="anonymous" /> <label id="label2">So beautiful.</label> <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script> <script> const colorThief = new ColorThief(); const img1 = document.querySelector('img#thomas'); var color; // Make sure image is finished loading if (img1.complete) { color = colorThief.getColor(img1); } else { image.addEventListener('load', function() { color = colorThief.getColor(img1); }); } var rbg = color[0] + "," + color[1] + "," + color[2]; document.getElementById("label1").style.color = "rgb("+ rbg +")" ;//機関車の画像の色 const img2 = document.querySelector('img#scene'); if (img2.complete) { color = colorThief.getColor(img2); } else { image.addEventListener('load', function() { color = colorThief.getColor(img2); }); } rbg = color[0] + "," + color[1] + "," + color[2]; document.getElementById("label2").style.color = "rgb("+ rbg +")" ; //レモンの画像の色 </script> </body> </html> Trouble using Color Thief? 疑問があったらStackoverflowを使おうという潔さ Search Stackoverflow to see if other people have run into the same issue you are having. If your issue is unique, then post a new question on Stackoverflow. Use the color-thief tag to make it easier to find. 1.Stackoverflowを検索して、あなたが抱えている問題と同じ問題に他の人が遭遇していないかを確認しましょう。 2.問題がみつからない場合は、Stackoverflowに新しい質問を投稿しましょう。検索しやすいようにcolor-thiefタグを使いましょう。 以上、触ってみたところのメモ書きです。参考になればさいわいです。
- 投稿日:2021-08-15T01:35:58+09:00
JavaScriptで個人的によく使う操作
URLを操作 URL各要素取得 let url = new URL(location); let href = location.href; let host = location.hostname; let port = location.port; let path = location.pathname; let query = location.search; let protocol = location.protocol; ブラウザー履歴エントリに state, title, url をセット 画面に更新をかけたくはない。 けどURLを更新したいというときに使ってます。 const url = new URL(window.location); url.searchParams.set('foo', 'bar'); window.history.pushState({}, '', url); 参照
- 投稿日:2021-08-15T01:35:41+09:00
nuxtjsにfabricjsを導入するときの手順
nuxtjsにfabricjsを導入するときの情報が少なかったのでそのメモです。 必要な項目のインストール yarn add fabric yarn add canvas package.jsonとyarn.lockに追加される。 plugin直下にfabric.jsファイルを作成。 fabric.js import { fabric } from 'fabric' import Vue from 'vue' Vue.use(fabric) export default fabric nuxt.config.jsに情報を追加 nuxt.config.js // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins plugins: [ { src: '@/plugins/fabric', mode: 'client' } ], 実際のコード これは赤い四角形を表示するためのサンプル fabric/index.vue <template> <canvas ref="can" width="200" height="200"></canvas> </template> <script> import { fabric } from 'fabric'; export default { mounted() { const ref = this.$refs.can; const canvas = new fabric.Canvas(ref); const rect = new fabric.Rect({ fill: 'red', width: 20, height: 20 }); canvas.add(rect); } }; </script> これで localhost:3000/fabricにアクセスすると以下のように赤い四角形が表示されているはず。 参考
- 投稿日:2021-08-15T01:18:51+09:00
「web会議が終わり次第 お礼メール自動作成」で業務をスリムに !!
メール作成の時間を圧縮したい! web会議、本当に多くなりましたよね。顔が映っていない場合は特に、 何だか「ぷつん」と途切れるようで、できれば後程メールでお礼を伝えたい。 web会議が終わり次第 自動でお礼メールが作れたらなぁ 「会議終わり次第 お礼メール自動作成」作ってみた Power Automate Desktopで作成 (今回はTeamsでのweb会議に限定) <フロー> ①Teamsの「web会議ウィンドウが閉じた」を検知 ②Outlook起動 同時に新規メールが作成 ③あらかじめ作成しておいたお礼本文と件名が自動入力 最後に宛先入力や本文の微調整を実施して送信完了! 技術面で工夫したところ 会議中・・・Teamsウィンドウ2個 ➡ 終了・・・ウィンドウ1個 と考え 「ウィンドウ2個の状態が消えたらOutlook起動」のアクションを当初組んだが、 会議中にウィンドウが3個になる場合があり、Outlookが起動してしまう。 ループや条件のアクションでは解決できなかったが、 「ウィンドウが1個になったら起動」を追加し、ウィンドウが途中何個になっても、 会議が終わってウィンドウが1個になった時点でOutlookを起動させることに成功! 1クリック&30秒でお礼メールの完成!! 手入力でメールを作成すると、送付まで約180秒。月間10回web会議があるとして (30秒-180秒)×10=1,500秒 つまり 月間約25分の業務スリム化に成功! 1クリック後はほぼ作業がいらないので、空いた時間でちょっと気の利いた一言を添えよう! 今後はzoomのweb会議にも対応したシステム開発にトライします!
- 投稿日:2021-08-15T01:18:51+09:00
「web会議が終わり次第 お礼メール自動作成」業務をスリムに !
メール作成の時間を圧縮したい! web会議、本当に多くなりましたよね。手軽に実施できる反面、日程調整でメール数往復。 スケジューリングが大変だった会議ほど、終了後、毎回やっぱりメールでお礼を伝えたい。 「web会議が終わり次第お礼メール作成」自動化できないかなぁ 「会議終わり次第 お礼メール自動作成」作ってみた Power Automate Desktopで作成 (今回はTeamsでのweb会議に限定) クリックひとつで、Teams会議終了を察知して、お礼メールを自動生成することに成功! PowerAtuomateDesktopフロー ①Teamsの「web会議ウィンドウが閉じた」を検知 ②Outlook起動 新規メール作成 ③作成済のお礼本文と件名が自動入力 (宛先入力・送信は手作業) 技術面で工夫したところ 会議中・・・Teamsウィンドウ2個 ➡ 終了・・・ウィンドウ1個 と考え 「ウィンドウ2個の状態が消えたらOutlook起動」のアクションを当初組んだが、 まれに会議中にウィンドウが3個になる場合があり、Outlookが起動してしまう。 ループや条件のアクションでは解決できなかったが、 「ウィンドウが1個になったら起動」を追加し、ウィンドウが途中何個になっても、 会議が終わってウィンドウが1個になった時点でOutlookを起動させることに成功! 1クリック&30秒でお礼メールの完成!! 手入力でメールを作成すると、送付まで約180秒。月間10回web会議があるとして (30秒-180秒)×10=1,500秒 つまり 月間約25分の業務スリム化に成功! 1クリック後はほぼ作業がいらないので、空いた時間でちょっと気の利いた一言を添えよう! 今後はzoomのweb会議にも対応したシステム開発にトライします!
- 投稿日:2021-08-15T01:18:51+09:00
web会議が終わったら お礼メール自動作成 PADで業務をスリムに !
メール作成の時間を圧縮したい! web会議、本当に多くなりましたよね。手軽に実施できる反面、日程調整でメール数往復。 スケジューリングが大変だった会議ほど、終了後、毎回やっぱりメールでお礼を伝えたい。 「web会議が終わり次第お礼メール作成」自動化できないかなぁ 「会議終わり次第 お礼メール自動作成」作ってみた Power Automate Desktopで作成 (今回はTeamsでのweb会議に限定) クリックひとつで、Teams会議終了を察知して、お礼メールを自動生成することに成功! PowerAtuomateDesktopフロー ①Teamsの「web会議ウィンドウが閉じた」を検知 ②Outlook起動 新規メール作成 ③作成済のお礼本文と件名が自動入力 (宛先入力・送信は手作業) 技術面で工夫したところ 会議中・・・Teamsウィンドウ2個 ➡ 終了・・・ウィンドウ1個 と考え 「ウィンドウ2個の状態が消えたらOutlook起動」のアクションを当初組んだが、 まれに会議中にウィンドウが3個になる場合があり、Outlookが起動してしまう。 ループや条件のアクションでは解決できなかったが、 「ウィンドウが1個になったら起動」を追加し、ウィンドウが途中何個になっても、 会議が終わってウィンドウが1個になった時点でOutlookを起動させることに成功! 1クリック&30秒でお礼メールの完成!! 手入力でメールを作成すると、送付まで約180秒。月間10回web会議があるとして (30秒-180秒)×10=1,500秒 つまり 月間約25分の業務スリム化に成功! 1クリック後はほぼ作業がいらないので、空いた時間でちょっと気の利いた一言を添えよう! 今後はzoomのweb会議にも対応したシステム開発にトライします!