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

見積書の日付を変更する

1.はじめに

営業視点だと、見積書の日付をちょこっと修正したいことがある。
週明けに提出する時とか、翌日に原紙を持参して打ち合わせしたい時とか。
でも見積書をシステム化してると、承認日は変更できないし、HTML上で日付のみ変えるにしても無制限だと困る。
だから、条件付きで日付を変更できるようにした。

2.概要

できるようにしたこと

  • カレンダー<input type="date">でHTML見積書の日付を変更。
  • 選択できる日付は、承認日〜承認日の最大3日後。

前提条件

  • 見積書作成をSalesforceでシステム化している。
  • 見積書レコードは、承認後はロックされる。
  • 見積書はHTMLとして出力する。

3.ソースコード

HTML(Visualforce)

<input type="date"
       id="cal"
       onchange="changedate()"
       value="{!Quote__c.appdDateForCal__c}"
       min  ="{!Quote__c.appdDateForCal__c}"
       max  ="{!Quote__c.sbmtDateForCal__c}"
>
</input>

<span id="date">{!Quote__c.appdDateForPrint__c}</span>

<!-- appdDateForCal__c:承認日(YYYY-MM-DD) -->
<!-- sbmtDateForCal__c:提出可能日(YYYY-MM-DD) -->
<!-- appdDateForPrint__c:承認日(YYYY年M月D日) -->

javascript

function changedate(){
    let appdDate = "{!Quote__c.appdDate__c}";
    let DateElmts = $("#cal").val().split("-"); //YYYY-MM-DDを[YYYY, MM, DD]に変換
    if(appdDate != ""){
        $("#date").text(DateElmts[0] + "年" + Number(DateElmts[1]) + "月" + Number(DateElmts[2]) + "日");
    } //この条件分岐がないとmax/minが未設定扱いとなり日付を自在に変更できてしまう
}

Salesforce数式項目

//1.sbmtDate__c(承認日またはその最大3日後)
    IF(
        TODAY() <= appdDate__c + 3,
        appdDate__c,
        appdDate__c + 3
    )

//2.sbmtDateForCal__c(上記をYYYY-MM-DDに変換)
    TEXT(YEAR(appdDate__c)) & "-" &
    RIGHT("0" & TEXT(MONTH(sbmtDate__c)), 2) & "-" &
    RIGHT("0" & TEXT(DAY(sbmtDate__c)), 2)

/*
    ・appdDate__cは承認プロセスで自動入力させる日付型データ
    ・appdForCal__cも同様
    ・コメントアウトは便宜的なもの
*/

4.雑記

  • カレンダー<input type="date"><apex:outputPanel>で囲い、表示条件を!ISBLANK(appdDate__c)にしてもいいかも。
<apex:outputPanel rendered="{!IF(!ISBLANK(Quote__c.appdDate__c), true, false)}">
    ...
</apex:outputPanel>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

業務でチーム開発をしていて思うこと

チームで開発するのって難しい。。。

Webサービス開発を業務でやっていて大変に思うことが色々あります。
特にチーム開発で進めるときには細心の注意を払います。
(上手くいかないとお互いにストレス溜まりますよね)

今後のためになんとなく現段階で気をつけようと思う点をメモしておきます。

言い方って大事
 ⇨ 何かお願いすることの連続
 ⇨ 教えてもらったり教えたり
 ⇨ 自分の知っている部分は他の人は知らなかったり知っていたり
   ↪︎誰が上で誰が下かがあまりないかもしれない
 ⇨ 開発している人は詳しいが、他の人はそのファイル回りをあまり見てないので詳しくない
 ⇨ 指示が不明確だとかなり困る
 ⇨ 横柄な態度はよくない
 ⇨ 指示なのかお願いなのか

何を話したか曖昧になる
 ⇨ 言った言ってないの問題になる
   ↪︎メモとるの大事
 ⇨ 同じイメージをちゃんと共有できているか

人によって開発スピードや順序が違う
 ⇨ 統一するべき?
 ⇨ ペースを合わせるのが大事だし大変

どこまでテキトーにやるか、どこまで厳密にやるか
 ⇨ クオリティかスピードか
 ⇨ 質か量か
 ⇨ 難しい、日頃からすり合わせするべきかもしれない
 ⇨ 性格が出る、亀裂を生む可能性も

今日の名言

お前のためにチームがあるんじゃねぇチームの為にお前がいるんだ!!

-安西先生(湘北高校バスケットボール部顧問)

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

【JavaScript】for-ofとfor-inの違い

for-ofとfor-in

どっちがどっちかわからなくなるときがあるのでメモ

for-of

配列の要素の繰り返しに特化している

namesを一つずつnameに代入する。

main.js
const names = ['田中', '山田', '佐藤', '井上'];

for (const name of names) {
  console.log(name);
}

// 田中
// 山田
// 佐藤
// 井上

また、for-ofでは分割代入と同じ記述が使用でき、複雑な構造を持つ配列から
必要な要素だけを取り出すことができる。

main.js
const personInfo = [
  { name: '田中', age: 22 },
  { name: '山田', age: 21 },
  { name: '佐藤', age: 24 },
  { name: '井上', age: 20 },
];

for (const { age } of personInfo) {
  console.log(age);
}

// 22
// 21
// 24
// 20

for-if

オブジェクトの各プロパティの値に対する繰り返しに特化している。

personInfoプロパティ名を一つずつinfoに代入している

main.js
const personInfo = {
  name: '田中',
  age: 22,
  height: 170,
}

for (const info in personInfo) {
  console.log(`${info} : ${personInfo[info]}`);
}

// name : 田中
// age : 22
// height : 170

配列に対してはindexを代入する。
namesindexを一つずつnameに代入している

main.js
const names = ['田中', '山田', '佐藤', '井上'];

for (const name in names) {
  console.log(name);
}


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

【JavaScript】ネスト構造のオブジェクトの複製方法

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

オブジェクトの複製方法

下記のようなobj1というオブジェクトがあるとする。
このオブジェクトをコピーしてobj2を作る。

const obj = { 
    prop1: "a",
    prop2: "b"
    };

最も簡単な方法は、オブジェクトリテラルを丸ごとコピーして、定義する方法。

const obj2 = { 
    prop1: "a",
    prop2: "b"
    };

こうすることで、objと同じ構造のobj2が生成される。
今回の場合、シンプルな構造なのでコピペで簡単にできるが、複雑なオブジェクトになった時に冗長的になる。
また、objがリテラル構造で表記されていない場合、コピペでは対応できない。

例えば、

obj2 = Object.copyObject(obj);

みたいな一発でコピーできる関数が用意されていたらいいのだが、残念ながらjavascriptには用意されていない。

javascriptは 「オブジェクトを複製する」 というという関数を持ち合わせていない。

ちなみに、

obj2 = obj;

としても、参照情報が複製されるだけで、オブジェクトが複製されることはない。

詳しくは、↓

JavaScriptにおける変数の参照について

ではどうするか。

Object.asignメソッドを利用する。

Object.assignとはObjectオブジェクトが持つassignメソッドのことであり、
第一引数のオブジェクトに対し、第二引数以降のオブジェクトを結合(マージ)するメソッドである。

例えば

const data = { 
    prop1: "a",
    };

const data2 = { 
    prop2: "b"
    };

のオブジェクトをマージするには

mergeObj = Object.assign(data1, data2);

console.log(mergeObj);

//{prop1:"a", prop2:"b"}

とする。

これを応用して、複製する。

第一引数に空のオブジェクトを用意して、そこに複製したいオブジェクトをマージすれば結果複製される。

obj2 = Object.assign( { }, obj1);

ネスト構造のオブジェクトの場合

先ほどと同じように実装してみる。

const obj = { 
    level: 1,
    nest: {
        level: 2
    },
};
const cloneObj = Object.assign({}, obj);

cloneObjを見てみる。

console.log(cloneObj);

//{ level: 1, nest: { level: 2 } }

行けてるぽい。

では複製後にobjのnest.levelプロパティの値を変えてみる。

obj.nest.level = 3;

そして、再度cloneObjを確認すると

console.log(cloneObj);

//{ level: 1, nest: { level: 3 } }

nest.levelの値が連動して変化してしまっている。
つまり、純粋に複製されているわけでなないということです。

これはなぜかと言うと javascriptはプリミティブ型の場合はコピーし、オブジェクトの場合は、参照情報をコピーするという挙動からきます。

詳しくはまたまたこちら。
JavaScriptにおける変数の参照について

Object.assginメソッドも同様に、複製するプロパティーの中身がプリミティブ型(今回の場合levelの値の1)は、そのまま1が複製されて、複製するプロパティーの値が、オブジェクト型(今回の場合nestの値{level:2})については、その参照情報が複製されるということです。
よって、objのnestの値と、cloneObjのnestの値は同じオブジェクトを参照しており、obj.nestの変化が、coneObj.nestに影響を受けることになります。

では、どうすれば良いのか。
再起的にObject.assginをかけます。
どういうことかといいますと、Object.assginメソッドでプロパティを先頭から複製していく時、そのプロパティーの値がオブジェクト型だった場合は、再度その値に対して、Object.assignメソッドをかけてやるのです。こうすることにより、プリミティブ型が出現するまで、潜っていき再起的に複製するという訳です。

こんな感じですね。

//Object.assignメソッドを定義。
const Clone = (obj) => {
    return Object.assign({}, obj);
};

function deepClone(obj) {
    // 一層を複製する。
    const newObj = Clone(obj);
    Object.keys(newObj)//keysメソッドでプロパティーだけの配列を作る。
        .filter(k => typeof newObj[k] === "object")//プロパティの値がオブジェクトだけのものを抜き出す。
        .forEach(k => newObj[k] = deepClone(newObj[k]));//プロパティ:オブジェクトが出現する限り再帰的にdeepCloneメソッドを呼び出す。
    return newObj;
}
const obj = { 
    level: 1,
    nest: {
        level: 2
    }
};
const cloneObj = deepClone(obj);

以上となります。お読みいただきありがとうございました。
間違ってたらすみません。。。

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

wasm(Rust) でJavaScriptファイルの呼び出し

今回は Rust(wasm)からJavaScriptのコード(関数)を呼び出す方法について書きます。
コードは以前書いたものを流用します。
下記のgithabのページを参照してください。
https://github.com/NagaJun1/wasm_sample

以前、Rustでwasmをビルドする方法について書いた記事があります。
下記にURLを貼り付けておきますので、ご参考にどうぞ。
https://qiita.com/NagaJun/items/f43a645ff630aeb858fd

下記にwasm-bindgenの公式ページのURLを貼り付けておきます。
今回説明する内容は、基本的には下記リンクのページで説明されていると同じ内容です。
https://rustwasm.github.io/docs/wasm-bindgen/reference/js-snippets.html

コーディング

今回作成したコードについて説明していきます。
まず、Rustで呼び出すためのjavaScriptのコードは下記の通りです。
処理は単純で、html内のid="id_1"のテキストを関数の引数の値で書き換えるというものです。
注意点としては、exportを関数の頭に付けることぐらいですかね。

/js-file/js-code-file.js
export function write_p_text(a)
{
    document.getElementById("id_1").textContent = a;
}

次に、Rustのコードです。
JavaScriptで設定したwrite_p_text()関数は、Rustでは#[wasm_bindgen(module="ファイルパス")]を頭に付け、extern "C"で囲うことで呼び出すことができます。
module="ファイルパス"の"ファイルパス"は、「Cargo.toml」からしたパスを設定する必要があります。
今回はついでに、RustからJavaScriptのalert()も呼び出します。
JavaScriptのライブラリは頭に#[wasm_bindgen]をつけるだけで呼び出せます。

lib.rs
#[wasm_bindgen]
pub fn read_js_fnction(){
    //jsのアラート
    alert("call JavaScript function".to_string());
    //js関数での処理
    write_p_text("Rust call function".to_string());
}

#[wasm_bindgen(module="/js-file/js-code-file.js")]
extern "C"{
    fn write_p_text(a:String);
}

#[wasm_bindgen]
extern "C"{
    fn alert(text:String);
}

最後に、wasmを呼び出す.htmlは下記の通りです。
wasmを呼び出すための方法は前回同様ですが、今回はJavaScript内のコードで DOM操作を加えたので、<p>タグを追加しておきます。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
  </head>
  <body>
    <h1>wasm_sample</h1>
    <!--下記<p>のテキストを書き換えます-->
    <p id ="id_1">htmlで設定したテキスト</p>
    <script type="module">
      import init, * as wasm from './pkg/sample_wasm_rust.js';
      async function run() {
        await init();
        wasm.read_js_fnction();
      }
      run();
    </script>
  </body>
</html>

ソースコードの配置に関しては、この記事の頭にgitのURLを貼り付けてあるので、そちらからご確認ください。

実行

各条件を揃えたら実行します。(実行方法は前回の記事を見てください)
実行結果としては、まずRustで設定したアラートが出て(このタイミングではまだ<p id ="id_1">はデフォルトのままです)
image.png
アラートを消すと DOMが書き換わって、ページ内のテキストが書き換わります。
image.png
以上で、RustからJavaScriptのコードが呼び出せたことが確認できました。

まとめ

今回はRustからJavaScriptを呼び出す方法について書いていきました。
RustからJavaScriptを呼び出せることで、Rustでは書くことができない処理をJavaScriptで実装するという、柔軟性を持たせたプログラムが作成できる様になるかと思います。
ただ、今回 RustからDOMを操作したのですが、DOM操作自体はJavaScriptのコードを呼び出さずともRustで web-sysをクレートで追加すれば処理できるので、処理速度を速くしたいという場合はこちらを使用するべきかと思います。
https://rustwasm.github.io/docs/wasm-bindgen/examples/dom.html#web-sys-dom-hello-world
また、JavaScriptのオブジェクトはRust内でJsValueを使用すれば処理できるため、多くの場合は.jsファイルを読み込まない形で実装できるかと思います。
どういう形で実装するかは人によるかとは思いますが。
今回は以上になります。
最後まで読んでいただき、ありがとうございました。

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

[メモ][JavaScript] Promise と async await

Promiseとは

ES6から使える、非同期処理の最終的な完了もしくは失敗を表すオブジェクト。
従来の非同期処理より可読性が高くなった

(IEはゴミなので使えないのでトランスパイルが必要。)

構文

new Promise(function(resolve, reject){

}).then(

).catch(

).finaliy(

);

resolveが呼ばれると、thenメソッドに行き、rejectが呼ばれるとcatchにいく
finaliyは最後に呼ばれる。

下記の例だと、OKを押すとresolveを実行するので、thenに処理が移る。
1つ目のthenが終わると、2つ目に行く。
そして、最後にfinallyが呼ばれて終わる。

new Promise((resolve, reject) => {
  const yn = confirm("Yes Or No");
  yn ? resolve() : reject();
})
  .then(() => alert("then なう"))
  .then(() => alert("then2 なう"))
  .catch(() => alert("catch なう"))
  .finally(() => alert("おしまい"));

プロミスチェーン

非同期処理を順に実行していくこと

例えば、 下記はチェーンになっているわけではなく、順々に実行していっているだけ

new Promise(function(resolve, reject) {
    resolve(1);
}).then(val => {
  return val * 2;
}).then(val =>{
  return val + 3;
})

プロミスチェーンとは簡単に言うと、thenの中でPromiseを返すこと
そうすると、非同期処理が順に呼ばれていく

function hoge(val = 0) {
  return new Promise(function (resolve, reject) {
    setTimeout(() => {
      alert(val++);
      resolve(val);
    }, 1000);
  });
}

hoge()
  .then((val) => {
    return hoge(val);
  })
  .then((val) => {
    return hoge(val);
  });

並列処理

Promise.allを使う

すべてのプロミスが解決されるか、拒否されるかするまで待ちます。

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // 全部のプロミスが終わったあと実行される
});

async await

ES8から導入された、Promiseを書きやすくしたもの

書く必要ないくらい良い記事があった...
https://ja.javascript.info/async-await

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

【Javascript】英語の歌を上手に歌いたい

はじめに

これはクソアプリ2 Advent Calendar 2020の7日目の記事となります。
英語の歌を上手に歌いたいって思ったので、そのための準備をする簡易なアプリケーションです。
クソアプリということで、自分的に作り込んでない簡単なアプリケーションという意味合いとしています。

今回はアプリよりもこんな方法もあるよって情報が広まれば、この記事としては成功です。

英語の歌

英語の勉強をしていて、そろそろシャドーイングってのをやらないとなと思って、とりあえずドラえもんの英語音声版を読んでいます。
漫画の「ドラゴン桜」と3巻でも、英語の勉強に「ビートルズを流し運動をしながら歌詞を口ずさむ」なんてやってるわけですよ。
ビートルズで英語を学ぶのは最高の学習法【ドラゴン桜勉強法考察】

試したんですが、英語の歌ってネイティブっぽく歌うって難しいです。
あと、英語で歌えば上手くなるみたい。喉発声法ってのポイント

調査

るびが付いていれば、なんとか歌えるんじゃないかと思って調べると下記サイトを見つけました。
洋楽ふりがなドットコム

このサイトでは歌詞のカタカナ読みではなく、原曲と同じように英語っぽく歌える洋楽ふりがなになっています。

右クリック無効にされているとコピペが出来ないので、右クリック無効を解除するChrome拡張を入れました。
Pumpkin's Right Click Enable

Smile

自分は、ナットキングコール "Smile”というゆったりとした曲が好きだったんで、先ずはこれを検索しました。
http://yogakufurigana.com/271

英語の歌詞
https://www.komazawa-u.ac.jp/~kazov/Nis/smile.html

【ナットキングコール】”Smile”- Nat King Cole 【lyrics 和訳】【名曲】【ジャズ】【洋楽1950年代】
https://www.youtube.com/watch?v=y5B2aX_28Mo

カーペンターズのTop of the Worldもお勧め。
洋楽で英語をマスター|カーペンターズのTop of the World - Youtube

使い方

今回のデモサイト PCのChromeでしか確認してません。
http://yaju.sakura.ne.jp/demo/LyricsRuby.html

入力するのは、曲名とURLと歌詞とるびになります。
るびとして洋楽ふりがな、URLにはYoutubeの動画のURLを指定します。

デモボタンをクリックすると、Smileの歌詞が3行だけセットされます。

出力ボタンをクリックすると、新しいタブに下記が出力されます。

このタブの内容を保存したい場合、Chromeの右クリックメニューの「名前を付けて保存」をクリックします。
※保存名には最初に曲名が指定されています。

名前を付けて保存した場合の変更点

名前を付けて保存した場合、「動画を再生できません」のエラーとなるため下記の変更が必要となります。
曲名 Smileを例とします。

  1. 一緒にできる「Smile_files」フォルダを削除します。
  2. Smile.htmlのiframeのsrc属性の部分を変更します。「y5B2aX_28Mo」の部分は対象動画のものに変更してください。
  3. ローカル等のWebサーバー(IISやApacheなど)に保存します。
  4. Webサーバー経由でSmile.htmlにアクセスします。
<iframe width="336" height="189" src="./Smile_files/y5B2aX_28Mo.html"
                                            ↓
<iframe width="336" height="189" src="https://www.youtube.com/embed/y5B2aX_28Mo" 

ソースコード

LyricsRuby.html
<!DOCTYPE html>
<html lang = "ja">
<head>
<meta charset = "utf-8">
<title>JavaScript</title>
<style>
  input[type="text"] {
    width: 500px;
  }
  textarea {
    width: 500px;
    height:500px;
  }
  .demo {
    margin-left: 50px;
  }
</style>
</head>
<body>
<script>
function output() {
  const title = document.getElementById("title").value;
  const url = document.getElementById("url").value;
  const lyrics = document.getElementById("lyrics").value;
  const ruby = document.getElementById("ruby").value;

  if(!title) {
    alert("曲名を入力してください");
    return;
  };
  if(!lyrics) {
    alert("歌詞を入力してください");
    return;
  };
  if(!ruby) {
    alert("るびを入力してください");
    return;
  };

  var date = new Date();
  var time = ('0' + date.getHours()).slice(-2) + ('0' + date.getMinutes()).slice(-2) + ('0' + date.getSeconds()).slice(-2);
  newtab = window.open("", "NewTab_" + time);
  header(newtab, title);
  movie(newtab, url);
  song(newtab,lyrics, ruby);
  footer(newtab);
};

function demo() {
  document.getElementById("title").value = "Smile";
  document.getElementById("url").value = "https://www.youtube.com/watch?v=y5B2aX_28Mo";
  document.getElementById("lyrics").value = "Smile though your heart is aching\nSmile even though it's breaking\nWhen there are clouds in the sky, you'll get by";
  document.getElementById("ruby").value = "すまーぃぅ ぞうよぁはーでぃずえいきん\nすまーぃぅ いーぶんどーぅ いっつぶれいきん\nうぇんでぁらーくらーぅ(ず) いんだすかーぃ ゆーぅげっばーぃ";
}

function header(newtab, title) {
  newtab.document.open();
  newtab.document.location = "#";
  newtab.document.write("<!DOCTYPE html>");
  newtab.document.write("<html>");
  newtab.document.writeln("<head>");
  newtab.document.writeln("<title>" + title + "</title>");
  newtab.document.writeln("<style>");
  newtab.document.writeln("p { font-size: 120%; }");
  newtab.document.writeln("</style>");
  newtab.document.writeln("</head>");
  newtab.document.writeln("<body>");
  newtab.document.writeln("<h1>" + title + "</h1>");
}

function movie(newtab, url) {
  if(!url) return;

  url = url.replace("watch?v=", "embed/");
  newtab.document.write("<iframe width='336' height='189' ");
  newtab.document.write("src='" + url + "' frameborder='0' ");
  newtab.document.write("allow='autoplay; encrypted-media' allowfullscreen=''>");
  newtab.document.write("</iframe>");
  newtab.document.writeln("");
}

function song(newtab, lyrics, ruby) {
  const lyricsList = lyrics.split('\n');
  const rubyList = ruby.split('\n');

  for(i=0; i<lyricsList.length; i++) {
    var str1 = "<ruby>" + lyricsList[i];
    var str2 = "<rt>" + rubyList[i] + "</rt></ruby>";
    newtab.document.writeln("<P>" + str1 + str2 + "</P>");
  }
}

function footer(newtab) {
  newtab.document.writeln("</body>");
  newtab.document.write("</html>");
  newtab.document.close();
}
</script>
<input type="text" id="title" placeholder="曲名を入力してください"></input>
<br>
<input type="text" id="url" placeholder="YouTubeのURLを入力してください"></input>
<br>
<br>
<textarea id="lyrics" placeholder="歌詞を入力を入力してください"></textarea>
<textarea id="ruby" placeholder="るびを入力を入力してください"></textarea>
<br>
<input type="button" value="出 力" onclick="output()" />
<input type="button" value="デ モ" onclick="demo()" class="demo" />
</body>
</html>

技術的に調べたこと

名前を付けて保存が無効

Chromeで新しいタブを作成した際に、右クリックメニューの「名前を付けて保存」が無効状態で選択できませんでしたが、下記の追記することで解決しました。

newtab.document.location = "#";

youtubeの接続が拒否されました。

埋め込み用にURLを変更する必要がありました。
YouTube動画埋め込み時に「www.youtube.com で接続が拒否されました。」が表示された際に確認すること

url = url.replace("watch?v=", "embed/");

最後に

これを作る上で調べ直したら、下記の2サイトが動画として英語っぽく歌えるるびが付いていました。

カラオケで英語の発音をきたえる英語発音表記システム「Nipponglish(ニッポングリッシュ)」が2017年10月に登場とのことで、カラオケって滅多にいかないし英語の歌を歌うこともなかったのでこんなのあるのは知りませんでした。
カラオケで87%が英語発音アップ!ビッグエコー運営の第一興商が発表

これらのサイトにない英語歌詞があれば、このツールが役立つかも知れません。

英語の勉強

平日の会社帰りに30分〜1時間くらいシェアスペースで英語の勉強をしているのですがTOEICとか如何にも勉強系だと続かないので、自分はゲーム感覚で楽しめるものを選択しています。

iPhoneアプリ

ドラえもんの英語音声版の本

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

お問い合わせフォーム、javascript

// 全角記号・半角記号を除外するために、グローバル変数を定義
// ↓ハイフン不可記号
kigouCheck = /[!-/:-@[-`{-~!-/:-@¥}[-`.{-~、-〜”’・]/; 

window.addEventListener('DOMContentLoaded', () => {
    // 「送信」ボタンの要素を取得
    const submit = document.querySelector('#contact-submit');

    // エラーメッセージと赤枠の削除
    function reset(input_infomation, error_message){
        const input_info = document.querySelector(input_infomation);
        const err_message = document.querySelector(error_message);
        err_message.textContent ='';
        input_info.classList.remove('input-invalid');
    };

    // 「お名前」入力欄の空欄チェック関数
    function invalitName(input_target, error_target, error_message){

        const name = document.querySelector(input_target);
        const errMsgName = document.querySelector(error_target);

        if(!name.value){
            errMsgName.classList.add('form-invalid');
            errMsgName.textContent = error_message;
            name.focus();
            name.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「ふりがな」入力欄の空欄チェック関数
    function invalitHurigana(input_target, error_target, error_message){

        const hurigana = document.querySelector(input_target);
        const errMsgHurigana = document.querySelector(error_target);

        if(!hurigana.value){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = error_message;
            hurigana.focus();
            hurigana.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「郵便番号」入力欄の空欄チェック関数
    function invalitPostal(input_target, error_target, error_message){

        const postal = document.querySelector(input_target);
        const errMsgPostal = document.querySelector(error_target);

        if(!postal.value){
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = error_message;
            postal.focus();
            postal.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;

    };

    // 「住所」入力欄の空欄チェック関数
    function invalitAddress(input_target, error_target, error_message){

        const address = document.querySelector(input_target);
        const errMsgAddress = document.querySelector(error_target);

        if(!address.value){
            errMsgAddress.classList.add('form-invalid');
            errMsgAddress.textContent = error_message;
            address.focus();
            address.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「電話番号」入力欄の空欄チェック関数
    function invalitTel(input_target, error_target, error_message){

        const tel = document.querySelector(input_target);
        const errMsgTel = document.querySelector(error_target);

        if(!tel.value){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = error_message;
            tel.focus();
            tel.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「メールアドレス」入力欄の空欄チェック関数    
    function invalitEmail(input_target, error_target, error_message){

        const email = document.querySelector(input_target);
        const errMsgEmail = document.querySelector(error_target);

        if(!email.value){
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = error_message;
            email.focus();
            email.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「会社名」入力欄の空欄チェック関数
    function invalitCompany(input_target, error_target, error_message){

        const company = document.querySelector(input_target);
        const errMsgCompany = document.querySelector(error_target);

        if(!company.value){
            errMsgCompany.classList.add('form-invalid');
            errMsgCompany.textContent = error_message;
            company.focus();
            company.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「お問い合わせ内容」入力欄の空欄チェック関数
    function invalitContent(input_target, error_target, error_message){

        const content = document.querySelector(input_target);
        const errMsgContent = document.querySelector(error_target);

        if(!content.value){
            errMsgContent.classList.add('form-invalid');
            errMsgContent.textContent = error_message;
            content.focus();
            content.classList.add('input-invalid');          
            // 動作を止める
            return false;
        };
        // 動作を進める
        return true;
    };

    // 「送信」ボタンの要素にクリックイベントを設定する
    submit.addEventListener('click', (e) => {
        // デフォルトアクションをキャンセル
        e.preventDefault();

        reset('#name-js', '#err-msg-name');
        reset('#hurigana-js', '#err-msg-hurigana');
        reset('#postal-js', '#err-msg-postal');
        reset('#address-js', '#err-msg-address');
        reset('#tel-js', '#err-msg-tel');
        reset('#email-js', '#err-msg-email');
        reset('#company-js', '#err-msg-company');
        reset('#department-js', '#err-msg-department');
        reset('#content-js', '#err-msg-content');

        const focus = () => document.querySelector('#name-js').focus();

        // 「お名前」入力欄の空欄チェック
        if(invalitName('#name-js', '#err-msg-name', 'お名前が入力されていません')===false){
            return;
        };
        // 「お名前」入力欄の記号チェック
        const name = document.querySelector("#name-js");
        const errMsgName = document.querySelector("#err-msg-name");
        if(name.value.match(kigouCheck)){
            errMsgName.classList.add('form-invalid');
            errMsgName.textContent = '記号は使用できません';
            name.focus();
            name.classList.add('input-invalid');
            return;
        }else{
            errMsgName.textContent ='';
            name.classList.remove('input-invalid');
            name.blur();
        };
        // 「ふりがな」入力欄の空欄チェック
        if(invalitHurigana('#hurigana-js', '#err-msg-hurigana', '入力必須です')===false){
            return;
        };

        // ひらがなチェック
        // 正規表現huriganaCheckにマッチしするなら、エラー表示する
        const hurigana = document.querySelector("#hurigana-js");
        const errMsgHurigana = document.querySelector("#err-msg-hurigana");
        const huriganaCheck = /[^ぁ-んー  ]/u; 

        if(hurigana.value.match(huriganaCheck)){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = 'ひらがなで入力してください';
            hurigana.focus();
            hurigana.classList.add('input-invalid');
            return;
        }else if(hurigana.value.match(kigouCheck)){
            errMsgHurigana.classList.add('form-invalid');
            errMsgHurigana.textContent = '記号は使用できません';
            hurigana.focus();
            hurigana.classList.add('input-invalid');
            return;
        }else{
            errMsgHurigana.textContent ='';
            hurigana.classList.remove('input-invalid');
            hurigana.blur();
        };

        // 「郵便番号」入力欄の空欄チェック
        if(invalitPostal('#postal-js', '#err-msg-postal', '入力必須です')===false){
            return;
        };

        // 郵便番号形式チェック
        const postal = document.querySelector("#postal-js");
        const errMsgPostal = document.querySelector("#err-msg-postal");
        const postalCheck = /^\d{7}$/; 
        if(postal.value.match(postalCheck)){
            errMsgPostal.textContent ='';
            postal.classList.remove('input-invalid');
            postal.blur();
        }else if(postal.value.match(kigouCheck)){
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = '記号は使用できません';
            postal.focus();
            postal.classList.add('input-invalid');
            return;
        }else{
            errMsgPostal.classList.add('form-invalid');
            errMsgPostal.textContent = '郵便番号は数字7桁で入力してください';
            postal.focus();
            postal.classList.add('input-invalid');
            return;
        };

        // 「住所」入力欄の空欄チェック
        if(invalitAddress('#address-js', '#err-msg-address', '入力必須です')===false){
            return;
        };
        // 「住所」入力欄の記号チェック
        const address = document.querySelector("#address-js");
        const errMsgAddress = document.querySelector("#err-msg-address");
        const kigouAdCheck = /[!-,.-@[-`{-~!-,.-@¥}[-`{-~、-〜”’・]/; 
            if(address.value.match(kigouAdCheck)){
            errMsgAddress.classList.add('form-invalid');
            errMsgAddress.textContent = '記号は使用できません';
            address.focus();
            address.classList.add('input-invalid');
            return;
        }else{
            errMsgAddress.textContent ='';
            address.classList.remove('input-invalid');
            address.blur();
        };        
        // 「電話番号」入力欄の空欄チェック
        if(invalitTel('#tel-js', '#err-msg-tel', '入力必須です')===false){
            return;
        };

        //電話番号形式チェック
        const tel = document.querySelector("#tel-js");
        const errMsgTel = document.querySelector("#err-msg-tel");
        const telCheck = /0\d{1,4}\d{1,4}\d{4}/; 
        if(tel.value.match(kigouCheck)){
            errMsgTel.classList.add('form-invalid');
            errMsgTel.textContent = '記号は使用できません';
            tel.focus();
            tel.classList.add('input-invalid');
            return;
        }else if(tel.value.match(telCheck)){
            errMsgTel.textContent ='';
            tel.classList.remove('input-invalid');
            tel.blur();
        }else{
            errMsgTel.classList.add('form-invaid');
            errMsgTel.textContent = '電話番号の形式で入力してください';
            tel.focus();
            tel.classList.add('input-invalid');
            return;
        };

        // 「メールアドレス」入力欄の空欄チェック
        if(invalitEmail('#email-js', '#err-msg-email', '入力必須です')===false){
            return;
        };

        const email = document.querySelector("#email-js");
        const errMsgEmail = document.querySelector("#err-msg-email");
        // "@"があるかのチェック
        if(email.value.match(/@/)){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        } else {
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください';
            email.focus();
            email.classList.add('input-invalid');
            return;
        }

        // Email形式チェック
        const emailSplit = email.value.split(/(@)/);
        const emailUser = emailSplit[0];
        const emailatto = emailSplit[1];
        const emailDomain = emailSplit[2];
        // console.log(emailSplit);
        const emailUserCheck = /^[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+(\.[-a-z0-9~#&'*/?`\|!$%^&*_=+}{\'?]+)*$/;
        const emailDomainCheck = /([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)|(docomo\ezweb\softbank)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;
        if(emailUser.match(emailUserCheck)){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        }else{
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください';
            email.focus();
            email.classList.add('input-invalid');
            return;
        }
        if(emailDomain.match(emailDomainCheck)){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        }else{
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください';
            email.focus();
            email.classList.add('input-invalid');
            return;
        }

        // Email文字数チェック
        const allLength = email.value.length;
        const userNameLength = emailUser.length;
        const domainNameLength = emailDomain.length;
        console.log("全て:"+allLength);
        console.log("@の前:"+userNameLength);
        console.log("@の後ろ:"+domainNameLength);
        if(allLength>=1 && allLength<=256 && userNameLength>=1 && userNameLength<=64 && domainNameLength>=1 && domainNameLength<=256){
            errMsgEmail.textContent ='';
            email.classList.remove('input-invalid');
            email.blur();
        } else {
            errMsgEmail.classList.add('form-invalid');
            errMsgEmail.textContent = 'Emailの形式で入力してください(文字数が間違っています)';
            email.focus();
            email.classList.add('input-invalid');
            return;
        }

        // 「会社名」入力欄の空欄チェック
        if(invalitCompany('#company-js', '#err-msg-company', '入力必須です')===false){
            return;
        };
        // 「会社名」入力欄の記号チェック
        const company = document.querySelector("#company-js");
        const errMsgCompany = document.querySelector("#err-msg-company");
        if(company.value.match(kigouCheck)){
            errMsgCompany.classList.add('form-invalid');
            errMsgCompany.textContent = '記号は使用できません';
            company.focus();
            company.classList.add('input-invalid');
            return;
        }else{
            errMsgCompany.textContent ='';
            company.classList.remove('input-invalid');
            company.blur();
        }; 

        // 「部署名」入力欄の記号チェック
        const department = document.querySelector("#department-js");
        const errMsgDepartment = document.querySelector("#err-msg-department");
        if(department.value.match(kigouCheck)){
            errMsgDepartment.classList.add('form-invalid');
            errMsgDepartment.textContent = '記号は使用できません';
            department.focus();
            department.classList.add('input-invalid');
            return;
        }else{
            errMsgDepartment.textContent ='';
            department.classList.remove('input-invalid');
            department.blur();
        };        

        // 「お問い合わせ内容」入力欄の空欄チェック
        if(invalitContent('#content-js', '#err-msg-content', '入力必須です')===false){
            return;
        };
        // 「お問い合わせ内容」入力欄の記号チェック
        const content = document.querySelector("#content-js");
        const errMsgContent = document.querySelector("#err-msg-content");
        if(content.value.match(kigouCheck)){
            errMsgContent.classList.add('form-invalid');
            errMsgContent.textContent = '記号は使用できません';
            content.focus();
            content.classList.add('input-invalid');
            return;
        }else{
            errMsgContent.textContent ='';
            content.classList.remove('input-invalid');
            content.blur();
        };        


        document.customerinfo.submit();

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

【Vue.js】基本的なディレクティブまとめ

はじめに

Vue.jsで使用する基本的なディレクティブをまとめました。
Vue.js v3.0の公式ドキュメントを参考としています。

V-onやV-bindは様々な使用法があるため、詳細は今後の記事で書きます。

V-text

<span v-text="message"></span>
<!-- 両者同様 -->
<span>{{ message }}</span> Hello Vue!

以下のように文章の一部をプロパティで表示したい場合は”Mustache” 構文(二重中括弧)を利用することが推奨されています。

<span>Message: {{ message }}</span>
const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!',
  },
});

V-html

DOM 要素の内側を innerHTML として書き換えます。
v-html には XSS の危険性があるため信頼できるコンテンツだけに利用します。

<div v-html="html"></div>
var app = new Vue({
  el: '#app',
  data: {
    html: 'Hello <strong style="color: red">Vue!</strong>',
  },
});

image.png

V-show

参照する値が true として評価され場合は表示し、false として評価される場合は display:none 等のスタイルが付いて非表示になります。

<h1 v-show="hello">Hello!</h1>
const greeting = new Vue({
  el: '#app',
  data: {
    hello: 1,
  },
});
greeting.hello = 1; // Hello!

V-if

バインドした値が true 評価であれば DOM 要素が生成され、false であれば破棄されます。

<h1 v-show="hello">Hello!</h1>
const greeting = new Vue({
  el: '#app',
  data: {
    hello: 1,
  },
});
greeting.hello = 1;

v-ifv-showの違い

v-if は初期表示においてfalseの場合、何もしません。条件付きブロックは、条件が最初にtrueになるまで描画されません。

一方、v-showではは要素は初期条件に関わらず常に描画され、シンプルなCSSベースの切り替えとして常に要素が DOM に保存されます。

一般的に、v-ifはより高い切り替えコストを持っているのに対して、 v-showはより高い初期描画コストを持っています。 そのため、切り替えの頻度が低ければv-if切り替えの頻度が高ければv-showという使い分けをします。

V-else

v-ifの後続分岐処理としてv-elseを使用できます。

<div v-if="Math.random() > 0.5">Hello!</div>
<div v-else>Morning!</div>

V-else-if

<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>

V-for

このように v-for を用いて numbers の各要素を仮変数(エイリアス)num として取り出して、num を li 要素の内部に入れて表示させることができます。

<div id="app">
  <ul>
    <li v-for="num in numbers">{{ num }}</li>
  </ul>
</div>
new Vue({
  el: "#app",
  data: {
    numbers: [
      1, 2, 3, 4, 5
    ]
  });

V-on

DOM要素にイベントリスナーを登録できます。

<div id="app" v-on="click: alert">ClickHere!</div>
new Vue({
  el: "#app",
  methods: {
    alert: function(){
         alert('clicked!');}
  });

V-bind

aタグのhref属性やimgタグのsrc属性などを動的に変更することができます。

<img v-bind:src="imageSrc" />
<!-- 省略記法 -->
<img :src="imageSrc" />

new Vue({
  el: "#app",
  data: {
    imageSrc: "http://example/example"

V-model

HTMLのinput要素やselect要素などのユーザーが入力した値を受け取りたい場合、v-bindディレクティブを用いることで実現しますが、v-modelを使うことでより簡潔に書くことができます。

v-bindとv-onを使った場合
<div id="app">
   名前をここに入力する
    <input  v-bind:value="name"  v-on:change="name = $event.target.value" />
</div>
v-modelを使った場合
<div id="app">
   名前をここに入力する
    <input v-model:value="name"/>
</div>
new Vue({
   el: "#app",
   data: {
       name: ''
   }
})

参考

Vue.js Directives

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

Garmin Connectから自分のデータを取得する~ランサムウェアへの超個人的抵抗~(その3)

本記事は、株式会社ピー・アール・オーアドベントカレンダーの8日目です。

前回の続き

前回では、Garmin Connectから日々の活動を指定年月日以降でダウンロードする部分を作成しました。
今回は、アクティビティの一覧を取得し、そこを足掛かりとして各アクティビティのデータをダウンロードする処理を作りたいと思います。

アクティビティ?

Garmin Connectにおけるアクティビティとは、特定の種類の運動データを指し、ランニングや自転車、水泳などといった基本的な運動から、変わったところだと釣りとかもあったりします。これはGarminの機器の方にプリセットされたアクティビティを明示的に選ぶことで記録されるデータになります。(なので、どのアクティビティを記録可能か?は機種依存になる)

基本的な方針

  1. アクティビティ一覧から過去すべてのアクティビティのIDを取得する。
  2. 取得したアクティビティIDに対しダウンロードURLをたたき、片っ端から取得する。
  3. どこまで取得したか?はローカルストレージに取っておく

こんな感じでやっていきます。

やっていきます

アクティビティ一覧の取得方法

まずは、ブラウザで通信を見てみましょう。
Untitled.png

ほうほう。これですね。

https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?limit=20&start=0&_=1607133727539

上記で、limitやstartはわかりますね。
_ってやつは何でしょうね?値的にはUNIXタイムスタンプのような気がしたので、その前提でやっていきます。

上記URLを(Garmin Connect認証済みで)たたくと以下のようなレスポンスが返ってきました。

[
    {
        "activityId": 5909700814,
        "activityName": "横浜市 ウォーク",
        "description": null,
        "startTimeLocal": "2020-12-04 08:52:33",
        "startTimeGMT": "2020-12-03 23:52:33",
        "activityType": {
            "typeId": 9,
            "typeKey": "walking",
            "parentTypeId": 17,
            "sortOrder": 27,
            "isHidden": false
        },
        "eventType": {
            "typeId": 9,
            "typeKey": "uncategorized",
            "sortOrder": 10
        },
        "comments": null,
        "parentId": null,
        "distance": 1158.3299560546875,
        "duration": 698.3469848632812,
        "elapsedDuration": 698346.9848632812,
        "movingDuration": 698.3469848632812,
        "elevationGain": 10,
        "elevationLoss": 18,
        "averageSpeed": 1.659000039100647,
        "maxSpeed": 1.8849999904632568,
        "startLatitude": 35.396981528028846,
        "startLongitude": 139.5148977264762,
        "hasPolyline": true,
        "ownerId": 6080883,
                ・
                ・
                ・

すごい情報量が返ってくるんですが、いったんここで必要なのはactivityIdだけです。
さて、このAPIから取得するとしてもページングがあるし、前回読み込んだアクティビティ以前を読み込みしたくはないので、ここでは以下の方針で考えてみました。

  1. ローカルストレージから前回取り込んだアクティビティIDを取得
  2. アクティビティ一覧を取得し、1.のアクティビティIDを探す。なければ次のページ読み込む。1.のアクティビティID見つかるまで読み込む
  3. アクティビティIDが見つかったら、その次のアクティビティからアクティビティIDを取得し、ダウンロード開始。ダウロード済みのアクティビティIDをローカルストレージに入れておく
  4. 2.で読み込んだリストの末端まで行ったら処理終了

うーん。やってることが地味ですね。今回の記事もLGTMもらえなさそうです。
(社内でもおそらくこれを必要としている人はいないので、まったく共感を得られない記事になってきました)

fetchで取得

こんな感じで書いて、取得できるか確認してみました。

function getActivityList(start, limit) {
    var url = "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?" +
            "limit=" + limit +
            "&start=" + start +
            "&_=" + moment().valueOf(); // unix timestamp (ms)
    var activityIds = [];

    return new Promise((resolve, reject) => {
      fetch(url, {
        credentials: 'include',
        mode: 'cors',
      })
        .then(response => response.text())
        .then(text => console.log(text))
        .then(function(){
          resolve(activityIds);
        })
        .catch(ex => reject(ex))
    });
}

あれ、失敗ですね。
Untitled (1).png

Cookie飛んでない疑惑です。
Untitled (2).png

もしかして、backgroundからのfetchはセッション共有されてなくてCookie飛ばないの?Cookie自前で取得して一つ一つつけないとダメ?とか一瞬遠い目になりましたが、何のことはないmanifestへのドメイン追加が必要なだけでした。

manifest.json
    "permissions": [
      "activeTab",
      "declarativeContent", 
      "storage", 
      "downloads", 
      "alarms", 
      "*://connect.garmin.com/"],

リスト取得は以下のような感じになりました。ちょっと長い。

background.js
/**
 * Set alarm for regular download in the background.
 */
chrome.alarms.onAlarm.addListener(async function (alarm) {
  let _isStart = await getLocalStorageVal('isStart');

  if (_isStart.isStart && alarm.name == "get_activity_list") {
    var _activityId = await getLocalStorageVal("last_read_activity_id");
    var _unreadIds = await getLocalStorageVal("unread_activity_ids");
    if("unread_activity_ids" in _unreadIds && _unreadIds.unread_activity_ids != '[]') {
      // If there is an undownloaded activity list in the local storage, 
      // do not load the DL target ID additionally
      console.log('There is a list of unloaded activities');
      return;
    }

    var nextPage = true;
    var start = 0;
    var limit = 20;
    while(nextPage) {
      var _ids = await getLocalStorageVal("unread_activity_ids");

      var ids = [];
      if("unread_activity_ids" in _ids) {
        ids = JSON.parse(_ids.unread_activity_ids);
      }

      var olderIds = await getActivityList(start, limit);

      if(olderIds.length == 0) {
        // If can't get the activity list, exit
        nextPage = false;
        break;
      }

      if(!_activityId.last_read_activity_id) {
        // No activity loaded in the past (first time)
        start = start + limit;
        ids = ids.concat(olderIds);
      } else {
        if(olderIds.indexOf(_activityId.last_read_activity_id) >= 0) {
          // The activity ID loaded last time is included in the acquired list
          nextPage = false;
          ids = ids.concat(olderIds.slice(0, olderIds.indexOf(_activityId.last_read_activity_id)));
        } else {
          // No ID previously imported into the acquired list (inspection on the next page)
          start = start + limit;
          ids = ids.concat(olderIds);
        }
      }

      chrome.storage.sync.set({unread_activity_ids: JSON.stringify(ids)}, function() {
        // Save next date to local storage
      });

    }
  }
});

初回のリスト取得では一番過去の一覧まで取得して、その時点での全アクティビティIDをローカルストレージに保存してます。ちょっと乱暴(すごいたくさんのアクティビティを持つ人がこれ動かすと危険かも)なやり方なので上限値は設定した方がいいかなとは思ってますので、githubあたりに上げるときにはそれ入れます。
あと、リスト取得は毎分とかで動かれると大変うざいので、別にalarmをセットしています。最終的には1日に一回動かすのでも十分でしょう。

background.js
/**
 * Set alarm when installing extension
 */
chrome.runtime.onInstalled.addListener(function (details) {
  console.log(details.reason);
  chrome.alarms.create("dl_fire", { "periodInMinutes": 1 });
  chrome.alarms.create("get_activity_list", { "periodInMinutes": 5 });
});

アクティビティのダウンロード処理

2日目に書いたコードの焼き直しです。やや冗長な気はしてるので自分の心の中の要リファクタ一覧の末尾に追加しておきました。

background.js
/**
 * Set alarm for activity download in the background.
 */
chrome.alarms.onAlarm.addListener(async function (alarm) {

  let _isStart = await getLocalStorageVal('isStart');

  if (_isStart.isStart && alarm.name == "dl_fire") {

    var _unreadIds = await getLocalStorageVal("unread_activity_ids");
    if(!"unread_activity_ids" in _unreadIds || 
      ("unread_activity_ids" in _unreadIds && _unreadIds.unread_activity_ids == "[]")) {
      return true;
    }

    ids = JSON.parse(_unreadIds.unread_activity_ids);

    id = ids.pop();

    if(id) {
      var url = "https://connect.garmin.com/modern/proxy/download-service/files/activity/" + id;
      var _dir = await getLocalStorageVal('directory');
      chrome.downloads.download({
        url: url, 
        filename: _dir.directory + id + '.zip'
      });
      chrome.storage.sync.set({unread_activity_ids: JSON.stringify(ids)}, function() {
        // Save updated id_list to local storage
      });
    }

    chrome.storage.sync.set({last_read_activity_id: id}, function() {
      // Save updated id_list to local storage
    });
  }
});

動かしてみる

image.png

動き的にも前回とあまり違いはないです。ただひたすら一定間隔でファイルがダウンロードされるという・・・。
こんな地味な拡張機能ですが、たぶん世の中的には必要としている人はいそうな気がするので公開してみようかなと思ってます。

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

JavaScript jExcel 売上管理サンプル

初めに

JavaScriptでほぼエクセルな画面が作成可能
表形式で入出力させたい場合に良いかも

フォルダ構成

├─ index.html
└─ assets
    ├─ css
    │   ├─ jexcel.css
    │   └─ jsuites.css
    └─ js
        ├─ jexcel.js
        └─ jsuites.js

※ファイルは
 ・jExcel:https://github.com/jspreadsheet/jexcel
 ・jSuites:https://github.com/jsuites/jsuites
 からダウンロード

HTML(使い方)

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>売上管理サンプル</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/css/jexcel.css" type="text/css" />
    <script src="./assets/js/jexcel.js"></script>
    <link rel="stylesheet" href="./assets/css/jsuites.css" type="text/css" />
    <script src="./assets/js/jsuites.js"></script>
    <style>
        /* 表全体 */
        #spreadsheet{
            font-size:12px;
        }

        /* 最終行以外(jsとかでちゃんと定義して処理したほうが良いかも) */
        #spreadsheet tbody.draggable td[data-X="5"],
        #spreadsheet tbody.draggable td[data-X="6"],
        #spreadsheet tbody.draggable td[data-X="8"],
        #spreadsheet tbody.draggable td[data-X="9"],
        #spreadsheet tbody.draggable td[data-X="10"]  {
            color:#000;
            background-color: #ff9;
        }
    </style>

    <script>
        // jExcelオブジェクト変数
        var jExcelsheetObj = null;

        // 取引先データ
        var customerList=[
            {"id": "10000001", "name": "取引先1"},
            {"id": "10000002", "name": "取引先2"},
            {"id": "10000003", "name": "取引先3"},
            {"id": "10000004", "name": "取引先4"},
            {"id": "10000005", "name": "取引先5"},
            {"id": "10000006", "name": "取引先6"}
        ];

        // 商品データ
        var productList=[
            {"id": "00000001", "name": "商品A"},
            {"id": "00000002", "name": "商品B"},
            {"id": "00000003", "name": "商品C"},
            {"id": "00000004", "name": "商品D"},
            {"id": "00000005", "name": "商品E"},
            {"id": "00000006", "name": "商品F"}
        ];

        // 表示データ
        var sheetData = [
          { 
            "selesDate": "2020-12-08"
            , "customer": "10000001"
            , "product": "00000001"
            , "amount": "4"
            , "salesUnitPrice": "2000"
            , "salesSumPrice": "0"
            , "tax": "0"
            , "purchaseUnitPrice": "1600"
            , "purchaseSumPrice": "0"
            , "profit":"0"
            , "profitRate":"0"
            , "remarks": ""
          } //1行目
          ,{ 
            "selesDate": "2020-12-19"
            , "customer": "10000003"
            , "product": "00000003"
            , "amount": "1"
            , "salesUnitPrice": "3000"
            , "salesSumPrice": "0"
            , "tax": "0"
            , "purchaseUnitPrice": "2000"
            , "purchaseSumPrice": "0"
            , "profit":"0"
            , "profitRate":"0"
            , "remarks": ""
          } //2行目
        ];

        // ページ読み込み時
        window.addEventListener("load", function(){

            /** 
            * セルの値が変更された場合
            * @param instance:編集されたタグのインスタンス(使用していない)
            * @param cell:編集されたタグの情報(使用していない)
            * @param x:列のインデックス
            * @param y:行のインデックス 
            * @param value:編集されたタグの内容(使用していない)
            */
            const cellChanged = function(instance, cell, x, y, value) {

                // 合計処理を実行するカラムインデックス
                const executeCalcSumDataColomIdx = ["-1", "3","4","7"];

                // 合計処理を実行するカラム番号の場合
                if(executeCalcSumDataColomIdx.indexOf(x) >= 0){

                    // 計算データ取得
                    let calcData = calcSumData(y, x);

                    if(calcData){
                        // 変更ロジック無効
                        jExcelsheetObj.options.onchange = null;
                        // 計算データ設定
                        jExcelsheetObj.setRowData(y, calcData);
                        // 変更ロジック有効
                        jExcelsheetObj.options.onchange = cellChanged;
                    }

                }        
            }

            /** 
            * 合計計算ロジック(指定行の合計した数値を計算する)
            * @param rowIdx:行のインデックス 
            * @param colomIdx:列のインデックス(デフォルト-1のときは、処理実行)
            */
            const calcSumData = function(rowIdx){

                // 余りの行は、処理しない
                if(rowIdx >= jExcelsheetObj.rows.length - jExcelsheetObj.options.minSpareRows) return null;

                // 行のデータを取得
                rowData = jExcelsheetObj.getRowData(rowIdx);

                try{

                    /** 設定している値で計算(インデックスはリテラルじゃなくて、ちゃんと定義したほうが良い) */
                    // 売上金額 = 数量 × 売上単価
                    rowData[5] = chgStrToInt(rowData[3]) * chgStrToInt(rowData[4]);
                    // 消費税 = 売上金額 × 税率(定義してね)
                    rowData[6] = rowData[5] * 0.1;
                    // 仕入金額 = 数量 × 仕入単価
                    rowData[8] = chgStrToInt(rowData[3]) * chgStrToInt(rowData[7]);
                    // 利益 = 売上金額 ー 仕入金額
                    rowData[9] = rowData[5] - rowData[8];
                    // 利益率 = 利益 ÷ 売上金額
                    rowData[10] = Math.round(rowData[9] / rowData[5] * 100) + "%";

                }catch(e){
                    console.log(e);
                    // エラーが出る前のものまで返す
                    return rowData;
                }
                return rowData;
            }

            // 変換メソッド(コピペ(^^;)
            const chgStrToInt = function(strNum){
                return parseInt(strNum.replace(/,/, ''));
            }

            // シートの領域取得
            var spArea = document.getElementById('spreadsheet');

            // jExcelオブジェクト生成
            jExcelsheetObj = jexcel(spArea, {
                data: sheetData, //設定データ
                minSpareRows: 1, //余り行
                columns: [
                    { type: "calendar", title:"年月日", width:80, options: { format:"YYYY-MM-DD" }},
                    { type: "dropdown", title:"取引先", width:200, source:customerList },
                    { type: "autocomplete", title:"商品名", width:160 , align: "left", source: productList, multiple:false },
                    { type: "numeric", title:"数量", width:80, align: "right" ,mask:"#,##" },
                    { type: "numeric", title:"売上単価", width:80, align: "right" ,mask:"#,##" },
                    { type: "numeric", title:"売上金額", width:80, align: "right" ,mask:"#,##"},
                    { type: "numeric", title:"消費税", width:80, align: "right" ,mask:"#,##"},
                    { type: "numeric", title:"仕入単価", width:80, align: "right" ,mask:"#,##" },
                    { type: "numeric", title:"仕入金額", width:80, align: "right" ,mask:"#,##"},
                    { type: "numeric", title:"利益", width:80 , align: "right" ,mask:"#,##"},
                    { type: "numeric", title:"利益率", width:80, align: "right" },
                    { type: "text", title:"備考", width:120, align: "left" },
                ], //列定義
                tableOverflow:true,    // trueの場合は、領域以上になるとスクロールを表示
                tableHeight:'200px',   // 高さ
                tableWidth:'98vw',     // 幅
                onchange: cellChanged, // 変更時のロジック
            });

            /** 初回データの計算ロジック */
            // 変更時のメソッドを無効にする
            jExcelsheetObj.options.onchange = null;
            // データ(行)数取得
            let allDataRows = jExcelsheetObj.rows.length;
            // データ(行)数ループ
            for(let idx=0; idx<allDataRows;idx++){
                // 計算データ取得
                let calcData = calcSumData(idx);

                if(calcData){
                    // 計算データ設定
                    jExcelsheetObj.setRowData(idx, calcData);
                }

            }
            // 変更メソッドを有効にする
            jExcelsheetObj.options.onchange = cellChanged;

            // ヘッダーの中央寄せ
            var spHeaders = document.querySelectorAll("#spreadsheet thead.resizable td");
            for(let idx = 0; idx<spHeaders.length;idx++){
                spHeaders[idx].style.textAlign="center";
            }

            // 登録ボタン
            document.getElementById("regist").addEventListener("click", function(){
                // 全データ取得
                var allData = jExcelsheetObj.getData();

                // 取得したデータでajaxで指定URLにpostとかする
                // commonAjax(url, allData);
            });

        });
    </script>
</head>
<body>
<h1>売上管理</h1>
<div><input type="button" value="登録" id="regist"></div>
<div id="spreadsheet"></div>
</body>
</html>

あとがき

合計行も入れたかったけど、折れました><;
色々カラム情報を定義すれば、もうちょい良いものができるはず!
にしても、英語のドキュメントのわかりにくいことわかりにくいこと、、、(涙)
onloadにnull突っ込むやり方については、もう少し良いやり方あるかも、、、

参考

https://qiita.com/t-iguchi/items/689b85ff163e0a321f1d
https://www.template-sozai.com/keyword/%E5%A3%B2%E4%B8%8A%E7%AE%A1%E7%90%86 ※参考エクセル

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

Reactで画像を表示させる方法

近頃Reactでいいサイトを作ろうとしているのですが、画像を表示させるのに3日かかってしまったのでたどり着いたコードを書いていきます。

表示のさせ方

test.react
import ReactDOM from 'react-dom';
import Icon from './logo.png';
const IconComponent = () =>{
  return <img src={Icon}  alt="アイコン" />
}
const rootElement = document.getElementById("root");
ReactDOM.render(
  <IconComponent />,rootElement
);

こんな感じになりました。
import Icon from ''でパスを指定するとそのものがIconに
入るみたいです。
const rootElement = document.getElementById("root");
をしてからReactDOM.renderで、
,rootElement
としないとTarget container is not a DOM element
と出ます。ここら辺の理解はまだまだなのでこれから調べます。

終わり

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

textarea内の文字列をテキストファイルでダウンロードしたい

はじめに

PythonのフレームワークのDjangoでWebアプリを制作中、
文字列をtextareaで表示させたのですが、
修正後の文字列を保存するには?
そして、保存したファイルをダウンロードするには?となり、
調査し実装してみようと思いました。

参考文献

この記事は以下の情報を参考にして執筆しました。

当初はFormを用いて、サーバー側で保存等の処理をするのか?と思っていたのですが、
編集後、サーバーには保存させなくても今回は解決策としてOKなので、
JavaScriptでダウンロード可能なクリックリンクを作成することで解決できそうです。
実際に試してみようと思います。

目次

  1. テキストエリアの値(文字列)を取得
  2. 取得した文字列でBlobを作成し、BlobをURLに変換
  3. ダウンロード可能なa要素を作成する
  4. 作成したa要素をクリックし、削除
  5. まとめ

テキストエリアの値(文字列)を取得

今回は要素のidを指定することで、値を取得しました。

index.html
<!-- 読み込みたいtextarea -->
<textarea id="textarea">sample!!</textarea>

<!-- クッリクした際に、テキストエリア内の文字列をダウンロードさせたい -->
<a href='javascript:download()' id="edit-btn">ダウンロード</a>

<script>
    function download() {
    // テキストエリア内の文字列を取得する
    const text = document.getElementById('textarea').value;

    // 下記に続く.....

}
</script>

取得した文字列でBlobを作成し、BlobをURLに変換

Blobはバイナリデータであるため、テキストファイルだけではなく画像やPDFなどいろいろな形式のファイルを扱うことができます。
また、JavaScriptのデータをBlobにすることで、ファイルにすることが可能です。

参照: BlobをJavaScriptで使う方法を現役エンジニアが解説【初心者向け】

追記部分のみ
上記のコードの「下記に続く」に追加していく

index.html
<script>
    // 取得した文字列をBlob形式に変換
    let blobedText = new Blob([text], {type: 'text/plain'});

    // BlobをURLに変換
    let url = URL.createObjectURL(blobedText);
</script>

ダウンロード可能なa要素を作成する

追記部分のみ

index.html
<script>
    // ダウンロード可能なa要素を作成する
    let link = document.createElement('a');
    link.href = url;
    link.download = 'ダウンロード時のファイル名(例えば、sample.txt)';
    // 要素の追加
    document.body.appendChild(link);
</script>

作成したa要素をクリックし、削除

追記部分のみ

index.html
<script>
    // linkをclickすることでダウンロードが完了
    link.click();

    // 「link」は不要な要素になるので、link要素を削除
    link.parentNode.removeChild(link)
</script>

全体のコード

index.html
<!-- 読み込みたいtextarea -->
<textarea id="textarea">sample!!</textarea>

<!-- クッリクした際に、テキストエリア内の文字列をダウンロードさせたい -->
<a href='javascript:download()' id="edit-btn">ダウンロード</a>

<script>
    function download() {
        // テキストエリア内の文字列を取得する
        const text = document.getElementById('textarea').value;

        // 取得した文字列をBlob形式に変換
        let blobedText = new Blob([text], {type: 'text/plain' });

        // BlobをURLに変換
        let url = URL.createObjectURL(blobedText);

        // ダウンロード可能なa要素を作成する
        let link = document.createElement('a');
        link.href = url;
        link.download = 'ダウンロード時のファイル名(例えば、sample.txt)';
        // 要素の追加
        document.body.appendChild(link);

        // linkをclickすることでダウンロードが完了
        link.click();

        // 「link」は不要な要素になるので、link要素を削除
        link.parentNode.removeChild(link)
    }
</script>

まとめ

ポイントは

  • Blob形式に変換することで、JavaScriptのデータをファイルにすることができる
  • ダウンロードリンクを作ること

最後までお読みいただきありがとうございます。

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

初心者なりにコールバック関数をまとめてみた

コールバック関数について学んだので、初心者なりにまとめてみます。

コールバック関数

コールバック関数とは、ある関数を呼び出す際の引数として渡される引数のことである。

main.js
function hello(name){
  console.log('hello ' + name);
}

hello('hoge');

コンソール出力結果

hello hoge


上記例では関数helloを呼び出す際、引数に'hoge'という文字列を渡しているが、この引数に関数を渡すことができ、これをコールバック関数という。
コールバック関数を引数に渡す場合は、

①変数として定義した関数を渡す
②( )内に無名関数をそのまま記述する

の2通りがある。
どうゆうことかというと、

main.js
function hello(callback){
  console.log('hello ' + callback());
}

//関数宣言
const myName = function(){
  return 'hoge';
}
hello(myName);


//無名関数
hello(function(){
  return 'fuga';
});

コンソール出力結果

1. hello hoge
2. hello fuga


ちなみに、関数を呼び出す際は関数名に( )をつけることで関数の実行を意味するため、変数宣言した関数をコールバック関数として渡す場合( )は不要となる。

次にコールバック関数自身に引数を渡す場合を考える。

main.js
function calc(a, b, callback){
  console.log(callback(a, b));
};

// 1.渡した数値を足し合わせる関数
const plus = function(a, b){
  return a + b;
}

// 2.渡した数値を引く関数
const minus = function(a, b){
  return a - b;
}

calc(3, 3, plus);
calc(3, 3, minus);

コンソール出力結果

1. 6
2. 0


上記のようにcalc関数の引数にコールバック関数(それぞれplusとminus)と値を渡し、引数が渡った先のcalc関数内でこの値をコールバック関数の引数に与える。
コールバック関数の引数に与えた値はコールバック関数の宣言時の関数処理に渡り、戻り値として結果が出力される。
少々ややこしいので理解するまで時間がかかりましたが、コールバック関数を渡す際の値と、コールバック関数名を確認すれば何をしようとしているのか想像がつきます。

JavaScriptのメカニズムは理解が難しく、まだ10%も理解できていませんが、コツコツ積み上げて理解を深めていきたいと思います。
不備や修正点などございましたらご指摘していただけると幸いです。

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

moment.jsは使わず、日付の変換にDateオブジェクトを使う

みんなが使ってるmoment.js
便利ですが、ちょっと日付を変換したいだけなのに
いちいちnpm installしたり、require()したりで少々めんどくさくもあります。

今日はもしかしたら皆さんは日常的に使われているかもしれませんが、Javascriptの標準組み込みオブジェクトである、
Dateオブジェクトを使っていきたいと思います。

日付の変換方法

const date = new Date("2020-11-18T13:05:08.357Z") //Dateオブジェクトのインスタンスを作成 詳しくはDateコンストラクタにて
const date_formatted = date.toLocaleDateString("ja-JP") //日本で使われるフォーマットを指定
console.log(date_formatted);//=>2020/11/18
出力結果
2020/11/18

このように簡単に日付の変換を行えます。

Dateコンストラクタ

以下の形でDateインスタンスを作成できるようです。

1.引数なし
2.時刻値またはタイムスタンプ値
3.タイムスタンプ文字列
4.独立した日付と時刻の成分の値

詳細は以下で=>https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/Date

日付の変換

Dateインスタンスのメソッドの一つであるtoLocaleDateStringを使います。

構文
dateObj.toLocaleDateString([locales[, options]])

localesには地域名、言語名などを指定できます。
なので、ここを"ar-EG"なんかにすると、

~
const date_formatted = date.toLocaleDateString("ar-EG") 
console.log(date_formatted);
出力結果
الأربعاء، ١٨ نوفمبر ٢٠٢٠ 

このようにアラビア語にしたりもできます。

次にoptionsには出力時のオプションなどを指定できます。

const format_options = {
        weekday: "long",
        year: "numeric",
        month: "long",
        day: "numeric",
      };
const date = new Date("2020-11-18T13:05:08.357Z",format_options) 
const date_formatted = date.toLocaleDateString("ja-JP") 
console.log(date_formatted);
出力結果
2020年11月18日水曜日

オプション等の詳細はここを参照=>https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString

おわりに

所詮中学生の浅知恵の可能性もあるので、鵜呑みにはしないでください。
今日は個人的な発見を書いてだけの記事を読んでくださりありがとうございました。

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

【剰余演算子】%ってなんだっけ?編

読んでなるほど!と思うのに、しばらくするとなんだっけ?ってなるシリーズその2

本日読んでいて忘れていた部分を抜粋。
剰余演算子は馴染み深い「 +-*/ 」以外に「 % 」というのがいることを忘れていました。

number.js
const num = 60

if (num % 15 == 0) {
  console.log(`${num}は3と5の倍数です`)
} else if (num % 3 == 0) {
  console.log(`${num}は3の倍数です`)
} else if (num % 5 == 0) {
  console.log(`${num}は5の倍数です`)
} else {
  console.log(`${num}は3の倍数でも、5の倍数でもありません`)
}

const num = 60
だから
60 % 15 == 0
ということ

ただの割り算は「 / 」なので考え方が違う
「 % 」は割り算した答えの数字じゃなくて、その答えの余りのこと
60 / 15 = 4
4で割り切れてるから余りは0

つまり60 % 15 == 0は、
0 == 0だよね?ということで→true

 ⬇️出力結果⬇️
60は3と5の倍数です

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

JavaScriptでのUncaught TypeError: Cannot read property 'addEventListener' of null エラー

Javascriptの中で指定ページ以外のページのconsoleを見ると

Uncaught TypeError: Cannot read property 'addEventListener' of null

というエラーが突然出た件。

あれ?指定ページでは出ないのに、、なぜ?となったので備忘録として。

今回のコード

window.addEventListener("load",function(){
const priceGet = document.getElementById("item-price");
priceGet.addEventListener("input", () => {

以下略

原因は他のページではidがitem-priceの要素が存在しないため
priceGetがnullになってしまう。
nullに対してaddEventListenerを使用したことでエラーが発生した。

対応策として

window.addEventListener("load",function(){
const priceGet = document.getElementById("item-price");
if (!priceGet){ return false;}
priceGet.addEventListener("input", () => {

以下略
if (!priceGet){ return false;}

を記載することで
priceGetがnullの場合にそれ以降のコードを読まないように実装できる。

もし何か間違い等あれば教えていただきたいです!

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

早期リターンすればいいってもんじゃねーぞ!

こういうケースでは早期リターンしないほうがいいかも!って話をさせてください。

例えば自分の Webサイト ホームページに、アクセス時の処理として以下のような実装をしたとします。

ページ読み込み後にアクセスカウンターを加算し、キリが良ければalertを出す、というものです。

const BBS_URL = 'https://..'
const accessCounter = ..

const onPageLoad = () => {
  accessCounter.increment()
  if (accessCounter.isKiriban) {
    alert('キリ番おめでとうございます!掲示板で報告お願いします!(踏み逃げ禁止)')
    if (location.href !== BBS_URL) {
      location.href = BBS_URL
      // ..
    }
  }
}

window.addEventListener('load', onPageLoad)

// ※ 例なので単純ですが、実際はもっとネストが深かったり、return後の処理が多かったりして、早期リターンしないと辛い状態だと思ってください。

これを作った数カ月後、早期リターン病を患った僕はこんな風にリファクタリングしてみることにしました。

const onPageLoad = () => {
  accessCounter.increment()
  if (!accessCounter.isKiriban) return // キリ番じゃなければそれ以上何もしないよ
  alert('キリ番おめでとうございます!掲示板で報告お願いします!(踏み逃げ禁止)')
  if (location.href === BBS_URL) return
  location.href = BBS_URL
  // ..
}

大満足。

―――

さらに数カ月後、僕はalert('相互リンク大募集!')っていうアラートも表示したくなりました。

ただしこのアラートは、キリ番の処理よりも後に置きたいです。
キリ番が一番大事ですからね。

ではリファクタリング後のコードの、どの位置に新しいalertを記述しましょう?

キリ番のalertより下に配置しようと思いましたが、そこはキリ番だった人しか到達しません。

………。

…リファクタリング後のコードだと、どこに置いてもそれが実現できないことに気づいてしまいました。

ということは、早期リターンとは悪なのでしょうか? 僕の病気は治るんでしょうか?

なにが良くなかったか

今回注目するべきなのは、onPageLoadという名前とその役割です。

これは「ページをロードしたときにやるべきことをやる」ための関数です。

つまり、アクセスカウンターを処理するためだけの関数ではありません
元々は、偶然アクセスカウンターの処理しかやることがなかっただけです。

にも関わらず、アクセスカウンターの条件次第でこの関数全体を終了させてしまっていたことがよろしくありませんでした。

解決案1

ということは、「アクセスカウンターを処理するためだけの関数」を作ってしまえば解決しそうです。

const countupAndNotifyAccessCounter = accessCounter => {
  accessCounter.increment()
  if (!accessCounter.isKiriban) return
  alert('キリ番おめでとうございます!掲示板で報告お願いします!(踏み逃げ禁止)')
  if (location.href === BBS_URL) return
  location.href = BBS_URL
  // ..
}

const onPageLoad = () => {
  countupAndNotifyAccessCounter(accessCounter)

  alert('相互リンク大募集!')
}

window.addEventListener('load', onPageLoad)

早期リターン云々抜きにしても、タスクの切り分けができて元より良さそうです。

(alertやlocationの描画順等のことは一旦気にしないでください)

もしやることが

const onPageLoad = () => {
  countupAndNotifyAccessCounter()
}

だけだった場合、不要にラッピングした関数のように見えるかもしれませんが、これはこのままで大丈夫です。

仕様上たまたまそのとき1つしかタスクがないだけですし、
文脈としてもやはりそれぞれに適した名前が付けられていたほうが自然な感じがします。

もちろん、新たに追加した2つ目のalertも関数にしてあげてもOKです。

解決案2

あるいは、(カウントと通知を一緒にしたくないとかで)ifが残るような場合でも、やはり関数にまとめるなどで、そもそも早期リターンしなくていいくらいに整理してもよさそうです。

const onPageLoad = () => {
  accessCounter.increment()
  if (accessCounter.isKiriban) notifyKiriban()

  alert('相互リンク大募集!')
}

まとめ

  • 「やるタスクが多岐にわたる(可能性のある)メソッド」において、あるひとつのタスクの都合次第でreturnしてしまうと後から困るケースがあるので、気をつけましょう。
  • 根本的には、そもそもタスクごとに適切に関数を分けられていれば大丈夫だと思います。

という話でした。


Vue.jsのcreated(コンポーネント作成時のフック)などがまさにやるタスクが多岐にわたるですが、

created () {
  if (!canXxx) return
  // doXxx
  // ..
}

みたいなコードを見て「勝手に終わらせないでくれ〜〜」ってことがたまにあったので書いてみました。

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

図形を回転する transformRotate

角度と基点を指定して図形を回転させます。

cap.gif

let polygon1 = {
  "type": "Feature",
  "properties": {},
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [ 137.796, 34.940 ],
        [ 137.748, 34.882 ],
        [ 137.887, 34.888 ],
        [ 137.796, 34.940 ]
      ]
    ]
  }
};

const dataSource = {
  'type': 'geojson',
  'data': {
    "type": "FeatureCollection",
    "features": [
      polygon1,
    ]    
  }
};

const bounds = turf.bbox(dataSource.data);

let anchor;

const map = new mapboxgl.Map({
  container: 'map',
  bounds: bounds,
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [
          "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
        ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
    },
    layers: [{
      id: "BASEMAP",
      type: "raster",
      source: "OSM",
      minzoom: 0,
      maxzoom: 18,
    }],
  },      
});

map.once('load', () => {

  map.fitBounds(bounds, { padding: { bottom: 200, top: 200, right: 200, left: 200 }});

  map.addSource('polygons', dataSource);

  map.addLayer({
    'id': 'polygons',
    'type': 'fill',
    'source': 'polygons',
    'paint': {
      'fill-color': '#FF0000',
      'fill-opacity': 0.7
    }
  });

  const center = turf.center(polygon1);
  anchor = new mapboxgl.Marker()
    .setLngLat(center.geometry.coordinates)
    .setDraggable(true)
    .addTo(map);

});

document.getElementById('rotate').addEventListener('click', () => {
  const angle = Number(document.getElementById('dir').value);

  turf.transformRotate(polygon1, angle, { pivot: anchor.getLngLat().toArray(), mutate: true });

  const dataSource = map.getSource('polygons');
  dataSource.setData({
    "type": "FeatureCollection",
    "features": [
      polygon1,
    ]    
  });
});

使い方は、turf.transformRotate(図形, 角度, { pivot: 基点 }); です。
options に mutate: true を付けると、引数に渡した図形の座標群を入れ替えます。
mutate: false (既定値) の場合は返値に移動した図形を返します。

参考

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

プログラミング用語辞典

インスタンス

インスタンスという言葉と一緒に
・クラス
・オブジェクト
の二つも出てくる

それぞれの意味は
クラス・・・設計図
インスタンス・・・設計図から実際に作ったもの
オブジェクト・・・クラスとかインスタンスをふんわりさせたもの

インスタンス化=new演算子で「newする」とも言う
インスタンス化=実際にものをつくる

メソッド

メソッドとはプロパティに入っている関数の事である

var sumple {
  プロパティ : ,
  プロパティ : 関数  これがメソッド

スコープとは

スコープとは、変数の名前や関数などを参照できる範囲のこと
JavaScriptでは、変数や関数などにアクセスできる範囲が決まっている

function fn() {
    const x = 1;
    // fn関数のスコープ内から`x`は参照できる
    console.log(x); // => 1
}
fn();
// fn関数のスコープ外から`x`は参照できないためエラー
console.log(x); // => ReferenceError: x is not defined

仮引数も同じで関数の中では参照可能だが関数の外からは参照できない。

このスコープという言葉は
・グローバルスコープ
・ローカルスコープ
という形でよく見かける

これらの意味は
グローバルスコープは名前の通り最も外側にあるスコープである
だから「JavaScriptのどこからでもアクセスできて全部の範囲が有効」
グローバルスコープで定義した変数はグローバル変数と呼ばれる

ローカルスコープは更に2種類あり、「関数スコープ」「ブロックスコープ」と言うモノがある。

関数スコープ

関数スコープとは上にあげた物も関数スコープである。
letconstは同じスコープ内に同じ名前の変数を二重に出来ないが、これは各スコープで同じ名前の変数は一つしか宣言できないからである。
js
let toaru;
let toaru;
//SyntaxErrorと表記される

だがスコープが異なれば同じ名前で変数を宣言できる

ブロックスコープ

ブロックスコープとは{~~~}で囲んだ範囲の事である
関数スコープと同じようにブロック内で宣言した変数はスコープ内でしか参照できない。

if文やwhile文でもブロックスコープを作るが、これも同じように外側からは参照することは出来ないのである。

https://jsprimer.net/basic/function-scope/

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

図形を移動する transformTranslate

距離と方向を指定して図形を移動します。

cap.gif

let polygon1 = {
  "type": "Feature",
  "properties": {},
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [ 137.796, 34.940 ],
        [ 137.748, 34.882 ],
        [ 137.887, 34.888 ],
        [ 137.796, 34.940 ]
      ]
    ]
  }
};

const dataSource = {
  'type': 'geojson',
  'data': {
    "type": "FeatureCollection",
    "features": [
      polygon1,
    ]    
  }
};

const bounds = turf.bbox(dataSource.data);

const map = new mapboxgl.Map({
  container: 'map',
  bounds: bounds,
  style: {
    version: 8,
    sources: {
      OSM: {
        type: "raster",
        tiles: [
          "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
        ],
        tileSize: 256,
        attribution:
        "OpenStreetMap",
      },
    },
    layers: [{
      id: "BASEMAP",
      type: "raster",
      source: "OSM",
      minzoom: 0,
      maxzoom: 18,
    }],
  },      
});

map.once('load', () => {

  map.fitBounds(bounds, { padding: { bottom: 200, top: 200, right: 200, left: 200 }});

  map.addSource('polygons', dataSource);

  map.addLayer({
    'id': 'polygons',
    'type': 'fill',
    'source': 'polygons',
    'paint': {
      'fill-color': '#088',
      'fill-opacity': 0.8      
    }
  });

});

document.getElementById('translate').addEventListener('click', () => {
  const distance = Number(document.getElementById('dist').value);
  const direction = Number(document.getElementById('dir').value);

  turf.transformTranslate(polygon1, distance, direction, { units: 'meters', mutate: true });

  const dataSource = map.getSource('polygons');
  dataSource.setData({
    "type": "FeatureCollection",
    "features": [
      polygon1,
    ]    
  });
});

使い方は turf.transformTranslate(図形, 距離, 方向, { 距離の単位、など }) です。
方向は北を0として時計回りに360までの度です。
options に mutate: true を付けると、引数に渡した図形の座標群を入れ替えます。
mutate: false (既定値) の場合は返値に移動した図形を返します。

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

Node.jsを改めて理解する

前提

Node.jsを理解する前に、いくつか確認しておきます。

そもそもJavaScriptとは

HTMLやCSSで作られたWebページをプログラム(JavaScript)で制御することができるのが特徴です。
通常、クライアントサイドで用いられ、次のような場面でJavaScriptは使われています。
・ブラウザ上で画像をクリックすると拡大する
・メールアドレスを入力フォームに入力した時に、適切な形式で入力されているかチェックをする
...etc

クライアントサイドとサーバサイドって?

クライアントサイド

クライアントサイドプログラムは、Webブラウザ上で動作するプログラムのことです。
代表的なものとしてはJavaScriptがあります。
クライアントサイドプログラムの仕組みとして、クライアント側(ブラウザ)がサーバにリクエストすることによって、Webサーバー上のプログラムがクライアント側に送信され、そのプログラムをWebブラウザが実行するという形です。

サーバサイド

サーバサイドプログラムは、Webサーバー上で動作するプログラムのことです。
代表的なものにPHP、Ruby、Pythonなどがあります。
クライアントからのリクエストに対し、サーバ側でプログラムを実行し、実行結果をクライアント側へ送るという仕組みです。

クライアントサイドとサーバサイドの違い

クライアントサイドプログラムとサーバサイドプログラムの違いは、ブラウザ上で動作するのか、それともサーバ上で動作するのかの違いです。

本題

いよいよNode.jsの説明です。

Node.jsとは

Node.jsとは、一言で言うとサーバサイドのJavaScript実行環境(プラットフォーム)のことで、Node.jsを使用する上で使用する言語はJavaScriptです。

Node.jsとは、通常クライアントサイドで使用するJavaScriptをサーバサイドで実行できるようにしてくれるものです。
サーバサイドで動かせると言うと、PHPやRubyなどと同じように思われるかもしれませんが、厳密には少し違います。
Node.jsは単にサーバサイドでアプリケーションを動かすことができるだけでなく、Node.jsではアクセスされたURLを識別して、そのURLごとに別々の要素を表示させるなど、データベースで行う動作も実装できます。

特徴

  • 大量のデータ処理が可能
  • メモリの消費が少なく動作も速い
  • 処理速度が非常に速い
  • サーバサイドで使える

npmとは

npmは、Node.jsのパッケージ管理ツールです。
PHPにおけるcomposer、Rubyにおけるgemのようなものです。

Node.jsを使う場面とは

メモリ消費量が少ないため、小さな規模の開発・運用時に、他の環境と比べるとよりパフォーマンスを出すことが可能です。同時に大量のトラフィックを捌くことができ、動画配信やチャットアプリなどで使われます。

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

ToDoリストにドラック&ドロップ機能を実装してみた

今回は前回作ったtodoリストにドラック&ドロップ機能を実装してみた

ドラック&ドロップ

https://blog.ver001.com/javascript-dragdrop-sort/
https://www.ipentec.com/document/javascript-list-sortable-item
https://ja.javascript.info/mouse-drag-and-drop

更にドラック&ドロップを実装して順番を変える

記事を参考にそのまま書いてみた

document.querySelectorAll('.drag-list li').forEach (elm => {
    elm.ondragstart = function () {
        event.dataTransfer.setData('text/plain', event.target.id);
    };
    elm.ondragover = function () {
        event.preventDefault();
        this.style.borderTop = '2px solid blue';
    };
    elm.ondragleave = function () {
        this.style.borderTop = '';
    };
    elm.ondrop = function () {
        event.preventDefault();
        let id = event.dataTransfer.getData('text/plain');
        let elm_drag = document.getElementById(id);
        this.parentNode.insertBefore(elm_drag, this);
        this.style.borderTop = '';
    };
});

ほぼコピペに近いが一応書いてみた
初めてみるメソッドが多い.....
この時に起きた問題が
・クリックできない
・vscodeにeventが非推奨だと言われる

・クリック出来ない問題
原因を探索
1..list#listに修正

2.追加ボタンをクリックされた時の処理のshowTodoいかに入れてみたらsetDataが未定義だと言われた。
setDataについて調べてみる→
https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/setData
単純なミス

3.draggableが機能してるか確認
デベロッパーツールで確認したらついてたが動かなかった
一応htmlでliにdraggable属性をつけてみて動くか確認した所動かなった
だからdraggableについて調べてみる
そしたらdarggableはjqのライブラリjquery UIにもあるとわかったが今回はhtml属性のdraggableを使いたいと思う。
調べたところ概ね間違っていなさそう
リストには

newItem.setAttribute("draggable",true)

でdraggable属性を追加した。
これがないとドラックしようとしたら複数のリストが一緒に動いてしまい正しく作動しなかった。

4.答えをコピペして弄ってみる
そしたらidを振っていないとドラック&ドロップしても反映されない(ドラック&ドロップは出来る)

そうするとCSSのcursor:pointerによって左右されている事がわかった。
これでdraggableについても完結した

しかしドラックが出来てもドロップが出来ない。

正しい方にはドラックした時に青線が出てきているが、自分が書いたものには出てこないので、そこに問題があると思う。

idを振っていなかった。
今回はリストの番号に使った変数を再利用した

      newItem.setAttribute("id",count2)

まだ出来なかったが、確認した所```querySelectorAllが間違っていたので修正したら青線で出て無事に変更することが出来た。

それからドラック&ドロップしたときに番号も変更できるようにしたい。
ドラック&ドロップしたときにshowTodosで出来るかもしれないのでやってみる。
しかしドラック&ドロップ関数の最後に追加したら青線も出なくなってしまった。

削除ボタンを押してからドラック&ドロップが出来なくなった。

追加ボタンの所に書いてあったのをshowTodosの最後に持ってきたら使えるようになった。

もし追加or削除ボタンで更新したくないなら

前のバージョンで配列に入れないリストを作ればいけると思う。

draggableについて
https://www.pazru.net/html5/DragDrop/010.html

4.text/plainとは

・なぜeventは非推奨なのか
単純に引数にeventを記述すれば問題なくなった

https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault

draggableとは

https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/draggable
論理型ではなく、列挙型属性とは?

ドラック&ドロップしてdoingとdoneを分ける

時間がある時に実装してみたいです

画面いっぱいにドラック&ドロップ(順番など関係なし)
https://q-az.net/elements-drag-and-drop/

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

だらけちゃうので機械学習を使って女の子に叱ってもらった

概要

みなさんこんにちは。今回初Qiita記事を投稿します、フロントエンドエンジニアのブンです。

さて、2020年は多数のエンジニアがリモートワークをしています。

リモートワーク生活が長く続いていますが、周りに人がいないせいか、ついだらけてしまうなんてことはありませんか?
あまりワクワクしないタスクなどを目の前にすると、つい適当な理由をつけて先延ばししようとしてしまいますよね。

「メールに返事しなきゃいけないけど、とりあえず皿でも洗うか」
「このドキュメントすすめなきゃいけないけど、とりあえず歯間ブラシするか」
「このレビューしなきゃリリース遅れるけど、とりあえず洗濯ものが乾いてるか見るか」

などなど、なにかとそれなりの理由をつけて急に立ち上がり仕事から離脱しようとしてしまうのが人間の常なのです(みんなそうだよね?)。

そもそも人間は進化心理学的に、短期的な快楽を追い求め苦痛からは逃げようとするように脳にプログラムされているので、このような行動は至極当たり前です(参考)。

しかし本能に従って生きることが必ずしも正ではないとブッダも言っているのですべき仕事はしたいです。

ただ、仕事をきちんとするためには監視員が必要です。

そこでAI(今回は機械学習)を使えば監視員を人工的に作り出せると考え、
立ち上がろうとすると女の子が叱ってくれるアプリを作りました。

その名もNo-Standing-Appです(upとappがかかっていて非常に上手い)。

できたもの

https://bunhojun.github.io/no-standing-app/

使いかた

Google Chrome 推奨です。
1. ブラウザでウェブカメラ利用の許可をする
2. テキストが「準備OKだよ!」と表示されたら、立ちあがろうとする
3. 女の子に叱られる(特に大きくしているわけではありませんが音量注意です)

以上です。やりたいことはお分かりになられたでしょう。
きわめてコードをシンプルにしたかったので最低限のUIしかありません。

使用ライブラリ・機械学習モデル

ml5.js(※以下ml5と呼びます。)

https://ml5js.org/
機械学習用JavaScriptライブラリです。
すでに学習済みの機械学習モデルを利用できます。ほかには自分が用意した画像で機械学習ができたりします。
フロントエンドしかできない人にも問題なく使えます。とにかく簡単ですごい。
JavaScriptで機械学習を使うといえばTensorFlow.jsが有名ですが、今回は初心者向けのml5を使用しました。
※機械学習とは
※機械学習モデルとは

PoseNet

https://learn.ml5js.org/#/reference/posenet
PoseNetは人間のポーズ推定に特化した機械学習モデルです。TensorFlow.jsでもml5でも使えるようです。
これを使うとウェブカメラに写っている顔や体のパーツがウェブカメラスクリーン上のどこの座標にあるか判定してくれます。

p5.js

https://p5js.org/
ml5を学習するうえでよく併用される描画系ライブラリ。ウェブカメラの描画などを楽々やってくれます。ml5との相性はいいですが、一応ml5はp5.jsなしでも書けるようです(記事下部の参考を参照)。

p5.sound

https://p5js.org/reference/#/libraries/p5.sound
p5.jsの派生ライブラリで、音声ファイルのロードと再生に使用しました。

※音声は以下のページからダウンロードしました。

効果音ラボ - 商用無料、報告不用の効果音素材をダウンロード

Let's code

基本アルゴリズム

  • ウェブカメラ上に写っている顔の、鼻の座標位置によって立ち上がっているかどうか判定
  • 立ち上がっていたら女の子の声で「こら」と叱る

html

ますはhtmlファイルから。
index.htmlを一つ呼ぶだけにして、今回はCDNでライブラリは読み込みます。
pタグを用意して、ml5のモデル群が読みこめているかを表出します(詳細は後述)。
JavaScriptはsketch.jsというファイルで記述していきます。

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Image classification using MobileNet and p5.js</title>
  <!-- CDNでライブラリを読み込む。p5.js/p5.soundとml5をここで呼びます -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.0.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.js"></script>
  <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>
</head>

<body>
  <h1>立ち上がっちゃだめだよ</h1>
  <p id="message">loading</p>
  <script src="sketch.js"></script>
</body>

</html>

JavaScript

準備

まずはp5.jsとml5の準備に必要なコードを書きます。p5.js/p5.soundのためのボイラープレートが多いのですが、今回は詳説しません。詳しくは上記に貼ったp5.jsのドキュメントを見てください。

sketch.js
// 「こら」という声を発生するためのインスタンスを入れるためのグローバル変数。
let kora; 

// p5.js/p5.soundで用意されたプログラム開始前に呼ばれる関数。これで音声の使用ができるようになる。
function preload() {
  soundFormats('mp3');
  kora = loadSound('assets/kora');
}

// p5.jsで用意されたプログラム開始時に実行される関数。
function setup() {
  // createCaptureを使うとp5.jsがvideo要素(ウェブカメラ要素)をdomツリーに作り出してくれる。VIDEOはp5.jsが用意したグローバル変数。
  const video = createCapture(VIDEO);
  // ml5のPoseNetモデルを読み込むための記述。video要素とコールバックを引数にとる。
  const poseNet = ml5.poseNet(video, modelLoaded);
  // PoseNetが人間のポーズを感知した時のリスナー。コールバックのgotPosesについては後述。
  poseNet.on('pose', gotPoses);
}

// ml5の機械学習モデル(今回はPoseNet)が読み込まれた時に呼ばれるコールバック関数。
function modelLoaded() {
  const message = document.querySelector('#message');
  message.innerHTML = '準備OKだよ!';
}

大事なものをピックアップします。

sketch.js
const poseNet = ml5.poseNet(video, modelLoaded); 

これによって、PoseNetモデルを使用できるように宣言してます。ml5.poseNet()はvideo要素とモデルが読み込まれた時のコールバックを引数に入れます。今回は先ほど述べたpタグに、準備完了の旨を表示させるmodelLoaded()という関数を実行するようにしました。

そして次に重要なのは以下です。

sketch.js
poseNet.on('pose', gotPoses);

ここではPoseNetがウェブカメラを通して感知する人間のポーズのリスナーを設定してます。1秒間に何回もコールバックは発火します。
今回はgotPoses()という関数を呼ぶことにしてます。詳細は下に続きます。

ポーズを感知した時

先ほどのposeNet.on('pose', gotPoses)で呼ばれたgotPoses()について見ていきたいのですが、その前に1つポイントがあります。
PoseNetのポーズのリスナーはコールバック関数に引数を渡します。
この引数を見てみると、配列が返ってきています。

[
  {
    pose: {
      keypoints: [{position:{x,y}, score, part}, ...],
      leftAngle:{x, y, confidence},
      leftEar:{x, y, confidence},
      leftElbow:{x, y, confidence},
      // 今回はnoseを使うのでここだけ詳しく書く
      nose: {
        confidence: 0.9991235136985779
        x: 361.67679872030413
        y: 199.94389418961936
      }
      ...
    },
    // 今回は無視
    skeleton: [...]
  }
]

この配列にはオブジェクトがあり、
その中にさらにposeとskeletonというオブジェクトが入ってます。今回はposeを使います。
このposeはウェブカメラに写っている体のパーツのそれぞれの座標位置を教えてくれます。
今回は鼻のy座標が画面上部に移動したら女の子に叱られるというアルゴリズムなので、
poseの中のnoseというプロパティからy座標を取り出します
このy座標が50未満になったら立ち上がろうとしていると判断することにしましょう。※鼻が上に行くほどy座標は小さくなる

そしてgotPoses()はこんな感じになります。

sketch.js
// 人間のポーズを感知した時に発火される関数
function gotPoses(poses) {
  if (poses && poses[0]) {
    const pose = poses[0].pose;
    const noseY = pose.nose.y;
    if (noseY < 50) {
      // 鼻のy座標が50より小さい時=立ち上がっている時
      onStandingUp();
    } else {
      // 座っている時
      onSitting();
    }
  }
}

そして立ち上がっている時とそうでない時のそれぞれの関数は以下になってます。

sketch.js
// 立ち上がっているかどうかの判定のためのグローバル変数。連続で叱られるのを防ぐために使う。
let isStandingUp = false; 

// 座っている時に呼ばれる関数
function onSitting() {
  if (isStandingUp) {
    isStandingUp = false;
  }
}

// 立ち上がろうとしている時に呼ばれる関数
function onStandingUp() {
  if (!isStandingUp) {
    isStandingUp = true;
    // 「こら」と叱る。play()はp5.soundが提供するメソッド。
    kora.play();
  }
}

以上です!
最終的なコードはGitHubにまとめてあります。

課題

なんとタブがバックグランドではml5は動かない模様です...!
アプリを起動しウィンドウをめちゃくちゃ小さくして画面の端っことかに置いておいてやってください…
他のウインドウに覆いかぶさるとうまく作動しません。

また、今回は鼻のy座標だけしか見てないので横に体をスライドさせて抜け出したら叱られません。
でも、無意識にそんな立ち上がり方はしないので今回は別によしとします。

感想

機械学習やAIと聞くと「高度な数学が必要」「学習データを自分で確保しなければいけない」という先入観があるかもしれませんが、もうすでにそのインフラはすべての開発者に対し開けてきており、その敷居は以前に比べだいぶ下がってきたと言えます。
たとえばTensorFlow.jsをつかえば、だいたいのフロントエンドエンジニアになじみの深いJavaScriptでも高度な機械学習モデルの作成や既存モデルの使用も可能です。
しかし今回使用したml5でも簡単なコードで沢山のことができるし、AIや機械学習は畑違いだと思っているフロントエンドの方にも手軽にこの分野に参入できるのはありがたいですね。

参考

The Coding Train
個人的によくプログラミング学習で利用するYouTubeチャンネルです。
講師がめちゃくちゃ気さくで見ているだけで幸せになれます。
基本英語ですが、たまに日本語字幕もついてます。

ジェイソン・ステイサムに「死にたくないなら手を挙げろ!」と言われるのが夢だったので、自分でそんなアプリを作りました
めちゃくちゃ面白いml5に関するQiita記事です。
ml5をp5.jsなしで書こうとする時に役に立ちそう。
この記事ではTeachable Machineを使い自分で撮影した機械学習モデルを利用しているようですね。

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

Flash Advent Calendar 7日目 - 俺のWeb Workersがこんなに遅いわけがない外伝 -

今日は、らくさんのこの記事の外伝となります。

まずは、本家の記事を読んでいただければと思います。

本家「俺のWeb Workersがこんなに遅いわけがない」

目次

  • おさらい
  • worker用のJSファイルを作らない
  • buffer以外のobjectやarrayの転送で遅延するケース

おさらい

遅くなる例

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray });

原因はpostMessageの第2引数にbufferをセットせずに送信した結果
複製されたデータを転送するのに異常な時間がかかる事です。

この回避策として、下記の実装のように第2引数の配列のbufferを設置します。
コピー無しで転送されるため大きなデータでも速度がでます。

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

注意点

この時にメインスレッド側のtypedArrayの中身は空っぽになります。

const typedArray = new Uint8Array(1024 * 1024 * 256);
worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

console.log(typedArray.length); // 0が出力

同じように、workerからメインスレッドに渡すとworker側のbufferは空っぽになります。

this.addEventListener("message", function (event)
{ 
    // 加工処理
    const typedArray = this.edit(event.data);   

    this.postMessage({ "data": typedArray }, [typedArray.buffer]);

    console.log(typedArray.length); // 0が出力
});

加工する前のbufferを保管したい場合は、どこかで複製が必要になるので注意が必要です。

const typedArray = new Uint8Array(1024 * 1024 * 256);
const cloneArray = typedArray.slice(0);

worker.postMessage({ "data": typedArray }, [typedArray.buffer]);

console.log(typedArray.length); // 0が出力
console.log(cloneArray.length); // 268435456が出力

worker用のJSファイルを作らない

今回はswf2js.jsだけで完結したいという目標があったので
worker用のJSファイルを設置しないで作り込みたいので以下のような対応をしました。

const worker = new Worker(URL.createObjectURL(new Blob(
    ['###JavaScriptの処理###'], 
    { "type": "text/javascript" }
));

個別にworkerディレクトリを作ります。

src/
├── worker/
│   ├── UnzipWorker.js
│   ├── UnLZMAWorker.js
│   ├── SwfParserWorker.js
etc...

その中にあるJSファイルをgulpで強制的にMinify
文字列の###JavaScriptの処理###をキーにして置換してしまいます。

gulp起動後

src/
├── worker/
│   ├── UnzipWorker.js
│   ├── UnzipWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
│   ├── UnLZMAWorker.js
│   ├── UnLZMAWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
│   ├── SwfParserWorker.js
│   ├── SwfParserWorker.min.js <= MinifyされたJSファイルが自動で上書きor追加される
etc...

gulp側の処理

const fs = require("fs");

...省略

replace(
    "###JavaScriptの処理###", 
    fs.readFileSync("src/worker/UnzipWorker.min.js", "utf8")
        .replace(/\\/g, "\\\\") // バックスラッシュをエスケープ
        .replace(/'/g, "\\'") // 置換元の文字列をシングルクォーテーションでくくってるので、シングルクォーテーションをエスケープ
        .replace(/\n/g, "") // 改行を削除
);

...省略

これで一個のファイルに複数のworkerを設置できます。

buffer以外のobjectやarrayの転送で遅くなるケース

大容量もしくはネストの深いobjectarrayを転送すると
送る側も受け取る側も負荷が高くなり、そこがボトルネックで速度遅延します。

回避策
1. TypedArrayに変換できないか検討
2. できるだけ小さい単位で転送する

1は上の説明の通り、一番転送速度が安定します。
ただ、文字列など使う場合はこの方法が使えません。
なので、2の「できるだけ小さい単位で転送する」事で回避を試みます。

worker側でメインスレッドにデータを送る

function 関数()
{
    const object = {};

    // objectを作り込むロジック
    ...省略

    // できるだけ小さい単位でメインスレッドに送る
    globalThis.postMessage({
        "key": "分岐しやすい文字列",
        "data": object
    });
}

メインスレッド側の個別受け取り処理

const worker = new Worker(URL.createObjectURL(new Blob(
    ['###JavaScriptの処理###'], 
    { "type": "text/javascript" }
));

worker.onmessage = function (event)
{
    switch (event.key) {

        case "分岐しやすい文字列1":
            // 受け取り処理を実装1
            return ;

        case "分岐しやすい文字列2":
            // 受け取り処理を実装2
            return ;

        case "分岐しやすい文字列3":
            // 受け取り処理を実装3
            return ;

        case "分岐しやすい文字列4":
            // 受け取り処理を実装4
            return ;

        default:
            // workerを終了する
            event.target.terminate();
            break;

    }
}

workerはメインスレッドに負荷がかからない便利な機能ですが
使い方を間違えると、サブスレッドとメインスレッドの両方に負荷がかかってしまいます。

この記事が何かの役に立てば嬉しいです。

明日からはWebGLに移行した時に色々と苦戦した事を書こうと思います。

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

【Javascript】Javascriptで読み上げ【そのままコピペOK】

結論

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
  </head>
  <body>
        <div>
            <input id="text" type="text" max="255" value="こんにちわ" />
            <a onclick="speak()">CLICK ME</a>
        </div>
        <script>
            //text読み上げ
            let isSpeech = false;
            if ('speechSynthesis' in window) {
                isSpeech = true;
            } else {
                alert("このブラウザは音声合成に対応していません。")
            }
            function speak(){
                if (isSpeech){
                    var speak   = new SpeechSynthesisUtterance();
                    speak.text  = document.getElementById('text').value;
                    speak.rate  = 1; // 読み上げ速度 0.1-10 初期値:1 (倍速なら2, 半分の倍速なら0.5, )
                    speak.pitch = 1; // 声の高さ 0-2 初期値:1
                    speak.lang  = 'ja-JP'; //(日本語:ja-JP, アメリカ英語:en-US, イギリス英語:en-GB, 中国語:zh-CN, 韓国語:ko-KR)
                    var voice = speechSynthesis.getVoices().find(function(voice){
                        return voice.name === 'Google 日本語';
                    });
                    // 取得できた場合のみ適用する
                    if(voice){
                        speak.voice = voice;
                    }
                    speechSynthesis.speak(speak);
                }
            }
        </script>
  </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsの基礎をまなぼう!

あいさつ

初めての人は初めまして!知っている人はこんにちは!
中学生バックエンドPGのAtieです!
今回はVue.jsについて学んできたのでアウトプットします!
「え?バックエンドがVue.jsをなんで勉強してるの?」
これはフレームワークに慣れておくためとSPAを開発したかったからです
実際Vue.jsを学んだ感想は少し難しかったです
しかしコードがシンプルだったので読みやすく書きやすかったです
では!

環境構築

まずはVue.jsの環境構築をしていきます
まずは必要なファイルを作っていきます

  • main.js
  • index.html
  • style.css

この3つのファイルを作ってください

次にVue.jsを使えるようにします

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

Vue.jsを使うには以下の1行を追加するだけで使えるようになります
便利で簡単ですね!

index.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

この行をhtmlのbodyタグの最後の行に入れます

双方向データバインディング

Vue.jsには「双方向データバインディング」ができるという特徴があります
双方データバインディングですがデータバインディングとはUIとデータを結びつけるという意味で双方向というのはdataを更新すればUIが更新されて逆にUIが更新されればdataが更新されるという意味です
たとえばTwitterを例に挙げてみましょう
Twitterのハートの部分を押すと色がピンクになりハートの数が表示されるようになれます
これはUI(ハートの部分)が更新されることでデータ(ハートの数)が更新されています
この双方向データバインディングができることでSPAが実現可能です

では実際にしてみましょう
まずはVue.jsで制御する要素を作ります

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

divタグにmainというidを付けました
今回はこのmainという要素の中をVue.jsで制御していきます
次にjsを書いていきます
まずは即時関数でエラーチェックをしてjsからmainを使えるようにします

mian.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main'
    })
})();

UIに結び付くモデルはよくView Modelと言われているので略してvmとしました
そしてnewでVueオブジェクトを作成します
どの領域かを結びつけるかをelementsの略であるelで指定します
cssのように#をつけてidを指定します
これでjsから扱えるようになりました
ではこのモデルにdataを保持してもらいます
dataのなかにnameというキーでAtieという値を保持してもらいます

main.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            comment: "name",
        }
    })
})();

では表示させてみます
表示するには二重波カッコで囲う必要があります

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <p>{{ name }}</p>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

これで値が表示されてます

次はUIからデータに変更できるようにしてみます
inputたぐにv-model="name"とすることでnameと入力フォームが結びつくようになります
即時に変更されていることがわかります

この二重波カッコですがjsの式をそのまま書くことができます
たとえば入力された文字を大文字にするためにname.toUpperCaseと書くことができます

ToDoListアプリを作る

ではVue.jsの基礎を抑えたのでToDoListアプリを作っていきます
まずはToDoListを保存する配列を作ります
todosというキーで配列を保存します

mian.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            todos: [
                'todo 1',
                'todo 2',
                'todo 3'
            ],
        }
    })
})();

次にhtmlに反映させますがただ単にliタグの中身を二重波カッコで囲てしまうと追加や削除するときにリロードを挟んでしまうので挟まないようにliタグを配列のループでその数だけ表示するようにします
そのためにはv-forでv-for="todo in todos"とします
こうすることでtodoにtodosの値が一つずつ入っていきその分ループします
そしてliタグの中身をtodoの値を表示するようにすればループされながら配列の中身が一つずつ表示されるようになります
これで表示する仕組みが整いました

ちなみにですがv-のように始まる特殊な属性をディレクティブと呼びます

次にToDoの追加をできるようにします
そのためにformを追加します

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <li v-for="todo in todos">{{ todo }}</li>
            </ul>
            <form>
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

v-modelを使って結び付けていきます
submitされた時のイベントを設定していきます
イベントを設定するにはv-onとする必要があります
v-onはよく使うので@と略することができます

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <li v-for="todo in todos">{{ todo }}</li>
            </ul>
            <form v-on:submit="addTodo">
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

ではsubmitされた時のイベントを設定していきます
methodsというキーにメソッドを設定していきます
data内のデータにはthisでアクセスすることができます

main.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            newItem: '',
            todos: [
                'todo 1',
                'todo 2',
                'todo 3'
            ],
            methods: {
                addTodo: function() {
                    this.todos.push(this.newItem);
                },
            },
        },
    })
})();

このように書くことができます
しかしこのままではformがsubmitされてページが移行してしまうのでうまくいいきません
これを防ぐためには@submit.preventとすることで防ぐことができます

これでうまくいきます

追加した後にフォームに値が残ってしまうのでnewItemを空にしておきます

main.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            newItem: '',
            todos: [
                'todo 1',
                'todo 2',
                'todo 3'
            ],
            methods: {
                addTodo: function() {
                    this.todos.push(this.newItem);
                    this.newItem = '';
                },
            },
        },
    })
})();

ToDoの削除

次にToDoの削除をできるようにしていきます
原理としてはtodosの配列から削除すればいいので簡単です

spanでxを作っていきます

この場合todosの何番目を削除すればいいのかわからないのでこのようにしてindexに数字が入るようにします

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <li v-for="(todo, index) in todos">{{ todo }} <span @click="deletItem">[X]</span></li>
            </ul>
            <form @submit.prevent="addTodo">
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

では削除するメソッドを作っていきます
といっても簡単ですspliceでindex番目から1番目を消すだけです
簡単簡単♪

main.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            newItem: '',
            todos: [
                'todo 1',
                'todo 2',
                'todo 3'
            ],
            methods: {
                addTodo: function() {
                    this.todos.push(this.newItem);
                    this.newItem = '';
                },
                deletItem: function(index) {
                    this.todos.splice(index, 1);
                    this.newItem = '';
                },
            },
        },
    })
})();

これで削除の仕組みができました

完了状態を管理する

では次に完了状態を管理できるようにします
完了状態を管理するためにtodosをオブジェクトにしてtitleとisDoneで管理します

main.js
(function () {
    'use strict';
    const vm = new Vue({
        el: '#main',
        data: {
            newItem: '',
            todos: [{
                title: 'task 1',
                isDone: false
            }, {
                title: 'task 2',
                isDone: false
            }, {
                title: 'task 3',
                isDone: true
            }],
            methods: {
                addTodo: function() {
                    this.todos.push(Item);
                    this.newItem = '';
                },
                deletItem: function(index) {
                    this.todos.splice(index, 1);
                    this.newItem = '';
                },
            },
        },
    })
})();

ただこのままでは表示がおかしくなるのでhtmlも変えます

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <li v-for="(todo, index) in todos">{{ todo.title }} <span @click="deletItem">[X]</span></li>
            </ul>
            <form @submit.prevent="addTodo">
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

次に完了状態を可視化していきます

まずは以下のようにhtmlを変えます

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <li v-for="(todo, index) in todos">{{ todo.title }} <span @click="deletItem">[X]</span></li>
            </ul>
            <form @submit.prevent="addTodo">
                <input type="checkbox" v-model="todo.isDone">
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

次にisDoneの状態によってチェックの表示状態を変えたいのですが...
便利なことにisDoneがtrueの時にチェックがついてくれます

チェックがついた項目はdoneというclassをつけてあげてあげます
データにおうじてclassを付け替えるにはv-bind:classをつけます
v-bindもv-onと同じくよく使われるので:で略してつけることができます

main.js
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/style.css">
    </head>
    <body>
        <div id="main">
            <ul>
                <input type="checkbox" v-model="todo.isDone">
                <span :class="{done :todo.isDone}"
                <li v-for="(todo, index) in todos">{{ todo.title }}<span @click="deletItem">[X]</span>
                </li>
            </ul>
            <form @submit.prevent="addTodo">
                <input type="text" v-model="newTodo">
                <input type="submit" value="Add Todo">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

次にToDoListがなかったらなにか表示させてみましょう
そのためには条件分岐をする必要があるのですが条件分岐をするにはv-ifというディレクティブがあります
今回はToDoListがなかったら「No todolist」と表示させます
通常ならいつもどおりliのところに入れて条件分岐させるのですがv-ifとv-forではv-forのほうが優先されてしまうのでulのほうに書きます

main.js
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/styles.css">
    </head>
    <body>
        <div id="app" class="container">
            <h1>My Todos</h1>
            <ul v-if="todos.length">
                <li v-for="(todo, index) in todos">
                <input type="checkbox" v-model="todo.isDone">
                <span :class="{done: todo.isDone}">{{ todo.title }}</span>
                <span @click="deleteItem(index)" class="command">[x]</span>
                </li>
            </ul>
            <ul v-else>
                <p>No todolist</p>
            </ul>
            <form @submit.prevent="addItem">
                <input type="text" v-model="newItem">
                <input type="submit" value="Add">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

todos.lengthがtrueだったらv-ifの処理が実行されてv-elseの場合はメッセージを表示するようになっています
v-ifを使わないで書く書き方がありますがそっちのほうがすっきりしているしているのでそちらを使います
v-showというディレクティブです
v-showはv-ifと同じように条件を入れます

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/styles.css">
    </head>
    <body>
        <div id="app" class="container">
            <h1>My Todos</h1>
            <ul>
                <li v-for="(todo, index) in todos">
                <input type="checkbox" v-model="todo.isDone">
                <span :class="{done: todo.isDone}">{{ todo.title }}</span>
                <span @click="deleteItem(index)" class="command">[x]</span>
                </li>
                <li v-show="!todos.length">No todos</li>
            </ul>
            <form @submit.prevent="addItem">
                <input type="text" v-model="newItem">
                <input type="submit" value="Add">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

今回はtodos.lengthがfalseの時に実行してほしいので!を使ってnot論理回路にしました

ToDoListの数を表示する

次にToDoListの数と終わった数を表示させます
Vue.jsは算術プロパティを使うことができるので使っていきます

computedというキーを使ってデータから動的にプロパティを計算してくれる算術プロパティがあるので使っていきます
remainingとしてあげてデータから自動的にremainingを算出してプロパティにしてあげます
今回はisDoneがfalseの項目を調べたいのでjsのfilterという命令を使ってみます
filter関数を引数に取るのでfunction(todo)としつつtodoのisDoneがfalse
つまり残っているタスクをreturnすればそれをフィルターしてitemにまだ終わっていないタスクを入れていけばわかります
今回まだ終わってないタスクの件数を調べたいのでlengthを返してあげればremainingにはisDoneがfalseの件数はいってくるはずです

main.js
(function() {
    'use strict';
    var vm = new Vue({
        el: '#app',
        data: {
            newItem: '',
            todos: [{
                title: 'task 1',
                isDone: false
            }, {
                title: 'task 2',
                isDone: false
            }, {
                title: 'task 3',
                isDone: true
            }]
        },
        methods: {
            addItem: function() {
                var item = {
                    title: this.newItem,
                    isDone: false
                };
                this.todos.push(item);
                this.newItem = '';
            },
            deleteItem: function(index) {
                this.todos.splice(index, 1);
            },
            computed: {
                remaining: function() {
                    const items = this.todos.filter(function() {
                        return !todo.isDone;
                    });
                    return items.length;
                }
            }
        }
    });
})();

少しややこしいですがよく見るとただitemsにtodo.isDoneがfalseのを入れていてその下でitemsの数を返しています

main.js
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/styles.css">
    </head>
    <body>
        <div id="app" class="container">
            <h1>
                My Todos
                <span class="info">({{ remaining }} / {{ todos.length }})</span>
            </h1>
            <ul>
                <li v-for="(todo, index) in todos">
                <input type="checkbox" v-model="todo.isDone">
                <span :class="{done: todo.isDone}">{{ todo.title }}</span>
                <span @click="deleteItem(index)" class="command">[x]</span>
                </li>
                <li v-show="!todos.length">No todos</li>
            </ul>
            <form @submit.prevent="addItem">
                <input type="text" v-model="newItem">
                <input type="submit" value="Add">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

あとはspanタグで表示できるようにしておきました

完了したタスクの一括削除

次に完了したタスクの一括削除をしていきます
purgeというボタンをつけて@clickでpurgeという処理をさせます

purgeというメソッドを作っていきます
まずthis.todosにremainingを割り当てることで終わった数ではなくて終わった配列そのものを返すようにします
remainingを表示するところも直す必要があります

main.js
(function() {
    'use strict';
    var vm = new Vue({
        el: '#app',
        data: {
            newItem: '',
            todos: [{
                title: 'task 1',
                isDone: false
            }, {
                title: 'task 2',
                isDone: false
            }, {
                title: 'task 3',
                isDone: true
            }]
        },
        methods: {
            addItem: function() {
                var item = {
                    title: this.newItem,
                    isDone: false
                };
                this.todos.push(item);
                this.newItem = '';
            },
            deleteItem: function(index) {
                this.todos.splice(index, 1);
            },
            computed: {
                remaining: function() {
                    return this.todos.filter(function(todo) {
                        return !todo.isDone;
                    })
                }
            },
            purge: function() {
                if (!confirm('delet finished?')) {
                    return;
                }
                this.todos = this.remaining;
            }
        }
    });
})();
index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>My Vue App</title>
        <link rel="stylesheet" href="css/styles.css">
    </head>
    <body>
        <div id="app" class="container">
            <h1>
                My Todos
                <span class="info">({{ remaining.length }} / {{ todos.length }})</span>
            </h1>
            <ul>
                <li v-for="(todo, index) in todos">
                <input type="checkbox" v-model="todo.isDone">
                <span :class="{done: todo.isDone}">{{ todo.title }}</span>
                <span @click="deleteItem(index)" class="command">[x]</span>
                </li>
                <li v-show="!todos.length">No todos</li>
            </ul>
            <form @submit.prevent="addItem">
                <input type="text" v-model="newItem">
                <input type="submit" value="Add">
            </form>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue"></script>
        <script src="js/main.js"></script>
    </body>
</html>

LocalStrageでデータの永続化

次にLocalStorageでデータの永続化をしてみます

データの保存はtodosに変更が加えられたときに保存の処理を実行したいのでdeep watcherを使っていきます
watchだけだと配列の中身の変更まで監視してくれないのでdeepオプションをオンにするためにdeep: trueとする必要があります

main.js
(function() {
    'use strict';
    var vm = new Vue({
        el: '#app',
        data: {
            newItem: '',
            todos: [{
                title: 'task 1',
                isDone: false
            }, {
                title: 'task 2',
                isDone: false
            }, {
                title: 'task 3',
                isDone: true
            }]
        },
        watch: {
            todos: {
                handler: function() {
                    localStorage.setItem('todos', JSON.stringify(this.todos));
                }
            },
            deep: true
        },
        methods: {
            addItem: function() {
                var item = {
                    title: this.newItem,
                    isDone: false
                };
                this.todos.push(item);
                this.newItem = '';
            },
            deleteItem: function(index) {
                this.todos.splice(index, 1);
            },
            computed: {
                remaining: function() {
                    return this.todos.filter(function(todo) {
                        return !todo.isDone;
                    })
                }
            },
            purge: function() {
                if (!confirm('delet finished?')) {
                    return;
                }
                this.todos = this.remaining;
            }
        }
    });
})();

データが保存できたので取り出してみます
this.todosに対してjsonデータをparseしつつlocalStrageからtodoのキーでデータをgetItemすればいいです
ついでにthis.todosの配列を空にしておきます

main.js
(function() {
    'use strict';
    var vm = new Vue({
        el: '#app',
        data: {
            newItem: '',
            todos: []
        },
        watch: {
            todos: {
                handler: function() {
                    localStorage.setItem('todos', JSON.stringify(this.todos));
                }
            },
            deep: true
        },
        methods: {
            addItem: function() {
                var item = {
                    title: this.newItem,
                    isDone: false
                };
                this.todos.push(item);
                this.newItem = '';
            },
            deleteItem: function(index) {
                this.todos.splice(index, 1);
            },
            mounted: function() {
                this.todos = JSON.parse(localStorage.getItem('todos')) || [];
            },
            computed: {
                remaining: function() {
                    return this.todos.filter(function(todo) {
                        return !todo.isDone;
                    })
                }
            },
            purge: function() {
                if (!confirm('delet finished?')) {
                    return;
                }
                this.todos = this.remaining;
            }
        }
    });
})();

もし何も保存されていなければエラーになるのでそれを防ぐためになかった場合は空の配列を返すようにしました

これで一通りのVue.jsの基礎をまなべました

最後に

Vue.jsの基礎をこの記事にギュッと詰め込みました!
コンポーネントはまた別の記事で解説します
最後まで読んでいただきありがとうございました!
Twitterしています!→AtieのTwitter
では!また次回の記事で!

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

走る!音が出る!HTMLでミニゲームをつくりました

FIRE RUNNER SAN
会社非公式な感じの、シンプルなランゲームを作りました。

https://hasegawa-campfire.github.io/fire-runner-san/
こちらから遊べます。
PC推奨です。IEは死にました。すこし音も出ます。

実際に開くと、なんだか下のほうがゴチャゴチャしていますね。
ここ数日、色々と後付けをしてこうなりました。
ランキングとか。リプレイとか。SNSシェアとか。効果音とか。
ああ、付けてみたいなと思ってしまったのですから、これは仕方のないことです。

その辺り、思うところや紆余曲折などイロイロありますが。
モロモロは note にしたためて、こちらは技術的な記録を残します。

※note
会社でランゲームをつくりました

技術概要

HTML5 でゲーム作りと言えば、やはり WebGL でしょう。
描画が高速で、表現力も高い。ライブラリやフレームワークも充実しています。

しかし今回は WebGL どころか canvas ですらなく、本当に HTML で構成されています。
ライブラリも特に使っていません。(Firebase を除く)

概略
<div class="app">
  <div class="world" style="transform: translate(32px, 256px);">
    <svg class="player" style="transform: translate(0px, 0px);">...</svg>
    <svg class="item" style="left: 200px; top: 0px;">...</svg>
    ...
  </div>
  <div class="status show">...</div>
  ...
</div>

div を連ねて、 positoin で配置して、 border background などで彩る。
addEventListener を仕掛けて、transform などで動かす。

Webフロントとしては、いつもの見慣れた HTML + CSS + JS です。

つよいところ

  • とにかく書き慣れていること
  • 複雑な仕組みが要らないこと
  • 簡単なアニメーションが、本当に簡単なこと

あと今回に関して言えば、Webフロントエンジニアの発表物として、
HTML と CSS を活用したほうが紹介しやすい部分も多いだろう、
という目論見もありました。
当初は。

つらいところ

  • とにかく遅いこと
  • ゲームとしての表現力は弱いこと
  • 表示の見え方についても動作環境に振り回されること

あと今回に関して言えば、スクリーンショットが取れないこと。
SNSシェアに使いたかったんです。
しかし自前で実現しようと思ったら、ものすごく頑張る必要があります。
html2canvas でもダメだったので、諦めました。

ゲーム表示対応

実は、見慣れた内容になりすぎて、改めて紹介できそうな部分がとても少なくなりました。
とはいえ、
さすがに普段作っているWebページでは使わないようなものも、いくつかあります。

描画速度の改善

HTMLの要素をゴリゴリ動かすので描画が遅くなりがちです。特にスマホ。
完成してから、初めて手元のスマホから見て頭を抱えたりもしましたが、もう遅い。

というわけで描画改善ですが、ただおもむろに will-change をつけるだけです。
簡単ですね。
これは予めどう動くのか宣言しておくことで、
その要素についてGPUを確保するなど最適化をしてくれるそうです。

ただし、乱用するとGPUやメモリを圧迫して逆に遅くなるとも言います。
あと意図しない表示バグを引き起こしたりもします。

.hoge {
  will-change: transform, opacity;
}

とはいえ効果は絶大。ガタガタしていたゲームが、突然ヌルヌルになるほど。

スマホのスペックの目安がわからなくて、どの程度まで動けばいいのか判断に困るのですが、
とりあえず手元の Rakuten Mini で動けばだいたい動くのでは?
という調整で作っています。

※ただしゲーム的にはPC推奨。スマホでのクリアは相当困難です。

GPUで困った時のおまじない

will-changetransform を使っていると、時々、想定外の事態に出くわします。
表示が欠けるとか、ボケるとか。重なりが z-index を無視するとか。

そんな時は backface-visibility: hidden を書くと解決する、こともあります。

.hoge {
  backface-visibility: hidden;
}

まるで魔法のようですが、私には解決する理由がわからないので、だいたい魔法です。
意味合いとしては裏面を表示しないようにする、つまりカリングだと思うんですが。
どうしてそれで直るんでしょうね。

右クリックメニューや文字選択の禁止

Webコンテンツを守る不毛な対策。ではありません。
スマホでの長押し操作は、文字の選択やメニューの表示でキャンセルされてしまうため、
それを回避します。

addEventListener('contextmenu', (e) => {
  e.preventDefault()
})
html {
  user-select: none;
}

スマホのビューポート

だいたい定型文で済まされる viewport の定義。
しかしスマホやゲームなら、ちょっと立ち止まってみる価値があるかもしれません。

width に数値を入れると、画面はその幅で表示してくれます。可能な限り。
もうスマホの画面に合わせようと、スクリプトで頑張らなくてもいいんですね。
画面が合わせます。

ダブルタップや、ピンチアウトでの画面の拡大。
それを禁止するのは良くないことだと言われますが、ゲームでは知ったことではありません。

<meta name="viewport" content="width=640,user-scalable=no">

Pull to Refreshの抑制

画面を下に引っ張って、ページを読み直す。
これもゲームでは操作の間違いになりますから、動かないようにします。

body {
  overscroll-behavior-y: none;
}

キーボードでのスクロールの抑制

当然ですが、スペースキーや矢印キーを押すと、ページがスクロールします。
これもゲームでは嬉しくないのでストップ。
なのですが、テキストの入力も出来なくなってしまいますので、
実際にはそういったものを除外するような記述が必要になります。

addEventListener('keydown', (e) => {
  if (e.key === ' ') {
    e.preventDefault()
  }
})

ところで keydown ってキャンセルしても keyup が動くんですね。
keypress は動かなくなりますけど。

音周り

JavaScriptにおいて、音の再生はとても簡単です。
<audio> を作って play() するだけ。ありがたい。

const audio = new Audio('hoge.mp3')
audio.play()

しかしこれではスマホで遅延が起きます。
読み込みので話ではありません。1度再生をした音でも毎回再生で遅延します。

とはいえ、遅延くらい別にいいのでは。PC推奨ですしね。と思っていたのですが。

Safari対応

実はこのままだとSafariでほとんど音が鳴りません。

Chromeでも何か操作をするまで再生できないというセキュリティ仕様がありますが、
Safariは更に厳格で、毎回、ユーザー操作のタイミングでしか再生できないようです。

当然のように回避策はあって、初回のユーザー操作時に再生して、すぐに一時停止して、
あとは好きなタイミングで再開すれば良いとかなんとか。なるほど。

しかし1度再生し終えたら、また再生するには同じことが必要になりそうなので、
効果音として使うのは難しそうでした。
(動きを見ないで次に進んだため、試していません。違っていたらすみません)

Web Audio API

そこで Web Audio API
こちらにも似たセキュリティ仕様はありますが、大本の AudioContext をなんとかすれば、
あとはそれぞれの音を好きなタイミングで鳴らせます。スマホでの遅延もなくなります。
素敵ですね!

今回はこんな感じのものを書いて AudioContext を取りました。

const audioContextPromise = new Promise(resolve => {
  for (const type of ['touchend', 'mouseup', 'keyup']) {
    addEventListener(type, resolve)
  }
}).then(async () => {
  const ctx = new AudioContext()
  await ctx.resume()
  return ctx
})

※参考
Web Audio APIの闇

Firebase

今回、気まぐれにランキングやアカウントの紐付けを実装したくなったので、
初めて Firebase を利用してみました。

これは、なんと言いますか、
とても簡単に使えすぎて知見としてはあまり残せそうにないのが難ですね。
ありがたいことです。

しいて言うと、
Firebase Authentication がユーザー名を持ってくれるので便利だとか、
ランキングは Firestore から1度読んだら、あとは放置しても自動更新されて楽だとか、
そんな感じです。

※参考
0から始める Firestore + Firebase Authentication
Firestore Security Rules の書き方と守るべき原則

おわりに

Webフロントはたのしいですね!
Webフロントはたのしいですよ!

※ FIRE RUNNER SAN リポジトリ
https://github.com/hasegawa-campfire/fire-runner-san

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

Conditional Types にて関数の第一引数により第二引数以降の候補を切り替える

Conditional Types にて関数の第一引数により第二引数以降の候補を切り替える

以下のようなオブジェクトがあった場合に、
第一引数には a, b, c を、
第二引数には第一引数により取得されるオブジェクトの配下のキー (a1, a2, b1, ・・・) を、
第三引数には第二引数により取得されるオブジェクトの配下のキーを指定したいとします。

multi-dimensional-object
const multiDimensional = {
  a: {
    a1: {
      AAA: 1,
      BBB: 2,
      CCC: 3,
    },
    a2: {
      DDD: 4,
      EEE: 5,
    },
  },
  b: {
    b1: 6,
    b2: 7,
  },
  c: 8,
}

実装

実装は以下のようになります。
JSDoc にて TypeScript を利用してますが JavaScript になります。

multi-dimensional-access
const multiDimensional = {
  a: {
    a1: {
      AAA: 1,
      BBB: 2,
      CCC: 3,
    },
    a2: {
      DDD: 4,
      EEE: 5,
    },
  },
  b: {
    b1: 6,
    b2: 7,
  },
  c: 8,
}

/**
 * @typedef {typeof multiDimensional} MultiDimensional
 */
/**
 * 多次元オブジェクトで引数を切替
 * @template {keyof MultiDimensional} T1
 * @template {MultiDimensional[T1] extends object ? keyof MultiDimensional[T1] : undefined} T2
 * @template {T2 extends keyof MultiDimensional[T1] ? MultiDimensional[T1][T2] extends object ? keyof MultiDimensional[T1][T2] : undefined : undefined} T3
 * @param {T1} level1
 * @param {T2} [level2]
 * @param {T3} [level3]
 */
const multiDimensionalAccess = (level1, level2, level3) => {
  console.log(multiDimensional[level1])
  if (level2) {
    console.log(multiDimensional[level1][level2])
    if (level3) {
      console.log(multiDimensional[level1][level2][level3])
    }
  }
}

a 配下の場合 level3 まで候補が存在します。
image.png

b 配下の場合候補があるのは level2 までです。
image.png

c 配下は level2 以降は undefined となります。
image.png

a 配下でも level2undefined を指定した場合には level3 の候補はなしです。
image.png

JSDoc の内容

型定義

multiDimensional を対象としてキーを候補としたいので typeof を利用して型定義を実施しています。

MultiDimensional
type MultiDimensional = typeof multiDimensional

ジェネリクスの指定

引数の候補を切り替えるために各引数にはジェネリクスの指定をしています。
TypeScript でのつじつまを合わせるため記述が増えています。

  • level1 に指定する T1 には MultiDimensional のキーを指定
  • level2level1 により切り替わるため、 MultiDimensional[T1] がオブジェクトの場合にそのキーを指定
  • level3level2 と同様
MultiDimensionalAccess
type MultiDimensionalAccess<
  // `level1` に指定する `T1` には `MultiDimensional` のキーを指定
  T1 = keyof MultiDimensional,
  T2 = T1 extends keyof MultiDimensional
    // `level2` は `level1` により切り替わるため、 `MultiDimensional[T1]` がオブジェクトの場合にそのキーを指定
    ? MultiDimensional[T1] extends object
      ? keyof MultiDimensional[T1]
      : undefined
    : undefined,
  T3 = T1 extends keyof MultiDimensional
    ? T2 extends keyof MultiDimensional[T1]
      // `level3` は `level2` と同様
      ? MultiDimensional[T1][T2] extends object
        ? keyof MultiDimensional[T1][T2]
        : undefined
      : undefined
    : undefined
> = (level1: T1, level2?: T2, level3?: T3) => void
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(React)npm start起動時のCORS回避

前提

フロントエンド - React.js
バックエンド - Go

問題

npm startでサーバを起動した際、React App内で外部APIをfetchすると、CORSエラーが発生する。

すでに行った対処(参考にした記事)

  • fetchの第二変数に{mode: 'cors'}を追加する。
  • GoのResponse Headerにw.Header().Set("Access-Control-Allow-Origin", "*")を追加。

この二つの対処では、CORSエラーは解消されなかった。

解決法

(ここに書いてあった。プロキシを挟むと良い。)

ブラウザからのAPIの送り先がlocalhost:4000の場合は、React Appのpackage.jsonに、
"proxy": "http://localhost:4000",
と追加で記述すると良い。

fetchでlocalhost:4000/apiにリクエストを投げたい時は、
fetch("/api", {mode: 'cors'}).then....
と書く。

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