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

最強のしりとりAIを作ってみた

しりとりができるAIを作ってみた。

前回はGASを使ってLINE BOTを作りましたが、
今回はWebでJavascriptを使って作ってみます。

使ったAPI

gooラボ ひらがな化API
ウィキペディア MediaWikiAPI

あとjQueryとFont Awesomeのアイコンフォントを使います。
使用したアイコンはこれこれです

コード

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>しりとり AI</title>
    <link rel="stylesheet" href="chat_UI.css">
    <script src="https://kit.fontawesome.com/xxxxx.js" crossorigin="anonymous"></script>
    <link rel="icon" href="./icons/favicon.ico">
</head>
<body>
    <script src="./jquery-3.4.1.min.js"></script>
    <div id="chat-box">
        <div class="kaiwa">
            <!--左からの吹き出し-->
                <figure class="kaiwa-img-left">
                    <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
                    <figcaption class="kaiwa-img-description">
                        しりとり AI
                    </figcaption>
                </figure>
                <div class="kaiwa-text-right">
                    <p class="kaiwa-text">
                        こんにちは。僕はしりとりAIだよ。<br>
                        Wikipediaから集めたデータを使ってしりとりができるよ。<br>
                        じゃあ、始めるよ~
                    </p>
                </div>
        </div>
        <!--左からの吹き出し 終了-->

            <div class="kaiwa">
                <figure class="kaiwa-img-left">
                    <img src="./icons/Wikipedia-logo-v2-ja.png" alt="no-img2">
                    <figcaption class="kaiwa-img-description">
                        しりとり AI
                    </figcaption>
                </figure>
                <div class="kaiwa-text-right">
                    <p class="kaiwa-text">
                        「しりとり」
                    </p>
                </div>
            </div>
    </div>
    <div id="form">
        <textarea id="text" cols="30" rows="3" placeholder="「り」から始まる言葉"></textarea>
        <div id="buttons">
            <button id="submit"><i class="far fa-paper-plane"></i> <span id="submit_text">送信</span></button>
            <button id="btn"><i class="fas fa-microphone"></i> <span id="btn_text">マイク</span></button>

        </div>
    </div>

    <script src="./siritori.js"></script>
</body>
</html>

次に見た目を作るCSSです。

chat_UI.css
/*——————–
 吹き出しを作る
——————–*/

/* 全体のスタイル */

.kaiwa {
  margin-bottom: 35px;
}

/* 左画像 */

.kaiwa-img-left {
  margin: 0;
  float: left;
  width: 60px;
  height: 60px;
  margin-right: -70px;
}

/* 右画像 */

.kaiwa-img-right {
  margin: 0;
  margin-right: 10px;
  float: right;
  width: 60px;
  height: 60px;
  margin-left: -70px;
}

.kaiwa figure img {
  width: 100%;
  height: 100%;
  border: 1px solid #aaa;
  border-radius: 50%;
  margin: 0;
}

/* 画像の下のテキスト */

.kaiwa-img-description {
  padding: 20px 0 0;
  font-size: 12px;
  text-align: center;
  position: relative;
  bottom: 15px;
}

/* 左からの吹き出しテキスト */

.kaiwa-text-right {
  position: relative;
  margin-left: 80px;
  padding: 10px;
  border-radius: 10px;
  background: #eee;
  margin-right: 20%;
  float: left;
}

/* 右からの吹き出しテキスト */

.kaiwa-text-left {
  position: relative;
  margin-right: 80px;
  padding: 10px;
  border-radius: 10px;
  background-color: #9cd6e7;
  margin-left: 20%;
  float: right;
}

p.kaiwa-text {
  margin: 0 0 20px;
}

p.kaiwa-text:last-child {
  margin-bottom: 0;
}

/* 左の三角形を作る */

.kaiwa-text-right:before {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  top: 15px;
  left: -20px;
}

.kaiwa-text-right:after {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  border-right: 10px solid #eee;
  top: 15px;
  left: -19px;
}

/* 右の三角形を作る */

.kaiwa-text-left:before {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  top: 15px;
  right: -20px;
}

.kaiwa-text-left:after {
  position: absolute;
  content: '';
  border: 10px solid transparent;
  border-left: 10px solid #9cd6e7;
  top: 15px;
  right: -19px;
}

/* 回り込み解除 */

.kaiwa:after, .kaiwa:before {
  clear: both;
  content: "";
  display: block;
}

#text {
  resize: none;
  float: left;
  margin-right: 10px;
}

#chat-box {
  height: 500px;
  overflow-y: scroll;
}

#btn, #submit {
  margin-left: 10px;
  margin-bottom: 50px;
}

#btn, #submit {
  border-radius: 25px;
  position: relative;
  display: inline-block;
  font-weight: bold;
  padding: 0.25em 0.5em;
  text-decoration: none;
  color: #FFF;
  background: #00bcd4;
  transition: .4s;
  border-bottom: solid 4px #627295;
}

#submit:hover {
  background: #00ff00;
}

#btn:hover {
  background: #ff0000;
}

#btn:active {
  /*ボタンを押したとき*/
  -webkit-transform: translateY(4px);
  transform: translateY(4px);
  /*下に動く*/
  border-bottom: none;
  /*線を消す*/
}

#submit:active {
  /*ボタンを押したとき*/
  -webkit-transform: translateY(4px);
  transform: translateY(4px);
  /*下に動く*/
  border-bottom: none;
  /*線を消す*/
}

::placeholder {
  color: #ff0000;
  opacity: 0.5;
  font-size: 18px;
}

index.htmlの8行目の

<script src="https://kit.fontawesome.com/xxxxx.js" crossorigin="anonymous"</script>

の「xxxxx.js」の部分は自分で変更してください。

ここまで書くと、このような見た目になると思います。プレビュー

次はしりとりの処理のJavascriptのコードです。

siritori.js
var msg = new SpeechSynthesisUtterance();
//msg.lang = 'ja-JP'; //言語
var words = [];
var Word_history = [];
var cpu_word = "";
var next_word = "";
var Iswork = false;
//音声認識の準備
const obj = document.getElementById("chat-box");
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const speech = new SpeechRecognition();
if ('SpeechRecognition' in window) {
    // ユーザのブラウザは音声合成に対応しています。
} else {
    $("#btn").hide();
}
speech.lang = "ja-JP";
speech.continuous = true;
//使用する変数を用意
$("#submit").click(function () {
    $("#submit").css('background-color', '#999999');
    $("#btn").css('background-color', '#999999');
    var text = $("#text").val();
    if (text == "") {
        $("#btn").prop("disabled", false);
        $("#submit").prop("disabled", false);
        $("#btn_text").text("マイク");
        $("#submit_text").text("送信");
        $("#btn").css('background-color', '#00bcd4');
        $("#submit").css('background-color', '#00bcd4');
        return; //何もないなら関数を終了させる
    }
    $("#btn").prop("disabled", true);
    $("#btn_text").text("処理中");
    $("#submit").prop("disabled", true);
    $("#submit_text").text("処理中");
    console.log("リザルト")
    console.log(text);//textが結果
    //ここから返答処理
    $("#chat-box").html($("#chat-box").html() + "<div class=\"kaiwa\"><!–右からの吹き出し–><figure class=\"kaiwa-img-right\"><img src=\"./icons/human_icon.png\" alt=\"no-img2\"><figcaption class=\"kaiwa-img-description\">あなた</figcaption></figure><div class=\"kaiwa-text-left\"><p class=\"kaiwa-text\">「" + text + "」</p></div></div><!–右からの吹き出し 終了–>");
    obj.scrollTop = obj.scrollHeight;
    //処理が終わったら考え中の文字を削除し、結果を入れる
    if (next_word != str_chenge(text, 1)[0]) {
        say("" + next_word + "」から言葉を始めてね!", $("#chat-box"));
        obj.scrollTop = obj.scrollHeight;
        $("#text").val("");
        $("#btn").prop("disabled", false);
        $("#submit").prop("disabled", false);
        $("#btn_text").text("マイク");
        $("#submit_text").text("送信");
        $("#btn").css('background-color', '#00bcd4');
        $("#submit").css('background-color', '#00bcd4');
        return;
    } else if (Word_history.indexOf(text) != -1) {
        say("" + text + "」は、もう使われた言葉だよ!", $("#chat-box"));
        obj.scrollTop = obj.scrollHeight;
        $("#text").val("");
        $("#btn").prop("disabled", false);
        $("#submit").prop("disabled", false);
        $("#btn_text").text("マイク");
        $("#submit_text").text("送信");
        $("#btn").css('background-color', '#00bcd4');
        $("#submit").css('background-color', '#00bcd4');
        return;
    } else {
        Word_history.push(text);
        siritori(text).then(function (value) {
            // 非同期処理成功
            console.log(value);
            $("#text").attr("placeholder", "" + str_chenge(value, -1)[0] + "」から始まる言葉");
            next_word = str_chenge(value, -1)[0]
            say("" + value + "", $("#chat-box"))
            Word_history.push(value);
            obj.scrollTop = obj.scrollHeight;
            msg.text = value;
            speechSynthesis.speak(msg);
            console.log("処理終了");
            $("#text").val("");
            $("#submit").prop("disabled", false);
            $("#btn").prop("disabled", false);
            $("#btn_text").text("マイク");
            $("#submit_text").text("送信");
            $("#btn").css('background-color', '#00bcd4');
            $("#submit").css('background-color', '#00bcd4');
        }).catch(function (error) {
            // 非同期処理失敗。呼ばれない
            console.log(error);
            say("エラーが起きました", $("#chat-box"))
            $("#text").val("");
            $("#btn").prop("disabled", false);
            $("#submit").prop("disabled", false);
            $("#btn_text").text("マイク");
            $("#submit_text").text("送信");
            $("#btn").css('background-color', '#00bcd4');
            $("#submit").css('background-color', '#00bcd4');
        });
    }
})
$("#btn").click(function () {
    // 音声認識をスタート
    if (!Iswork) {
        Iswork = true;
        $("#btn").prop("disabled", true);
        $("#btn_text").text("マイクで録音中");
        $("#btn").css('background-color', '#ff0000');
        $("#submit").prop("disabled", true);
        speech.start();
    } else { return; }
});
speech.onnomatch = function () {
    console.log("認識できませんでした");
    say("認識できませんでした", $("#chat-box"))
    $("#btn").prop("disabled", false);
    $("#btn_text").text("マイク");
    $("#submit").prop("disabled", false);
    $("#submit_text").text("送信");
    $("#btn").css('background-color', '#00bcd4');
    $("#submit").css('background-color', '#00bcd4');
    Iswork = false;
};
speech.onerror = function () {
    console.log("認識できませんでした");
    say("認識できませんでした", $("#chat-box"))
    $("#btn").prop("disabled", false);
    $("#btn_text").text("マイク");
    $("#submit").prop("disabled", false);
    $("#submit_text").text("送信");
    $("#btn").css('background-color', '#00bcd4');
    $("#submit").css('background-color', '#00bcd4');
    Iswork = false;
};
//音声自動文字起こし機能
speech.onresult = function (e) {
    $("#btn_text").text("処理中");
    $("#submit").prop("disabled", true);
    $("#submit_text").text("処理中");
    $("#submit").css('background-color', '#999999');
    $("#btn").css('background-color', '#999999');
    console.log("リザルト")
    speech.stop();
    if (e.results[0].isFinal) {
        console.log("聞き取り成功!")
        var autotext = e.results[0][0].transcript
        console.log(e);
        console.log(autotext);//autotextが結果
        //ここから返答処理
        $("#chat-box").html($("#chat-box").html() + "<div class=\"kaiwa\"><!–右からの吹き出し–><figure class=\"kaiwa-img-right\"><img src=\"./icons/human_icon.png\" alt=\"no-img2\"><figcaption class=\"kaiwa-img-description\">あなた</figcaption></figure><div class=\"kaiwa-text-left\"><p class=\"kaiwa-text\">「" + autotext + "」</p></div></div><!–右からの吹き出し 終了–>");
        obj.scrollTop = obj.scrollHeight;
        //処理が終わったら考え中の文字を削除し、結果を入れる
        if (next_word != str_chenge(autotext, 1)[0]) {
            say("" + next_word + "」から言葉を始めてね!", $("#chat-box"));
            obj.scrollTop = obj.scrollHeight;
            $("#text").val("");
            $("#btn").prop("disabled", false);
            $("#submit").prop("disabled", false);
            $("#btn_text").text("マイク");
            $("#submit_text").text("送信");
            $("#btn").css('background-color', '#00bcd4');
            $("#submit").css('background-color', '#00bcd4');
            return;
        } else if (Word_history.indexOf(autotext) != -1) {
            say("" + autotext + "」は、もう使われた言葉だよ!", $("#chat-box"));
            obj.scrollTop = obj.scrollHeight;
            $("#text").val("");
            $("#btn").prop("disabled", false);
            $("#submit").prop("disabled", false);
            $("#btn_text").text("マイク");
            $("#submit_text").text("送信");
            $("#btn").css('background-color', '#00bcd4');
            $("#submit").css('background-color', '#00bcd4');
            return;
        } else {
            Word_history.push(autotext);
            siritori(autotext).then(function (value) {
                // 非同期処理成功
                console.log(value);
                $("#text").attr("placeholder", "" + str_chenge(value, -1)[0] + "」から始まる言葉");
                next_word = str_chenge(value, -1)[0]
                say("" + value + "", $("#chat-box"));
                Word_history.push(value);
                obj.scrollTop = obj.scrollHeight;
                msg.text = value; speechSynthesis.speak(msg);
                console.log("処理終了")
                $("#btn").prop("disabled", false);
                $("#btn").css('background-color', '#00bcd4');
                $("#submit").css('background-color', '#00bcd4');
                $("#btn_text").text("マイク");
                $("#submit").prop("disabled", false);
                $("#submi_text").text("送信");
                Iswork = false;
            }).catch(function (error) {
                // 非同期処理失敗。呼ばれない
                console.log(error);
                $("#btn").prop("disabled", false);
                $("#btn").css('background-color', '#00bcd4');
                $("#submit").css('background-color', '#00bcd4');
                $("#btn_text").text("マイク");
                $("#submit").prop("disabled", false);
                $("#submit_text").text("送信");
                Iswork = false;
            });
        }
    }
}
function siritori(user_msg) {
    return new Promise(function (resolve, reject) {
        words = [];
        var chenges = str_chenge(user_msg, -1)
        var taskA = new Promise(function (resolve, reject) {
            WikipediaAPI(chenges[0], resolve);
        });
        var taskB = new Promise(function (resolve, reject) {
            WikipediaAPI(chenges[1], resolve);
        });
        Promise.all([taskA, taskB]).then(function () {
            console.log(words);
            cpu_word = words[Math.floor(Math.random() * words.length)]
            resolve(cpu_word);
        })
    });
}
function WikipediaAPI(query, end) {
    var NG_word = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
        , "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
        , "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
        , "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "綿", "", "", ""
        , "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""];
    //API呼び出し
    console.log(query)
    $.ajax({
        type: "GET",
        timeout: 10000,
        dataType: "jsonp",
        url: "https://ja.wikipedia.org/w/api.php?format=json&action=query&list=prefixsearch&pssearch=" + query + "&pslimit=200&psnamespace=0",
        async: false,
        success: function (json) {
            console.log(json)
            json.query.prefixsearch.forEach(function (value) {
                if (value.title != query) {
                    var word = value.title;
                    word = word.replace(/ *\([^)]*\) */g, "");
                    if (NG_word.indexOf(word.slice(-1)) == -1 && Word_history.indexOf(word) == -1) {
                        words.push(word);
                    }
                }
            });
            end();
        }
    });

}
function str_chenge(str, ran) {
    var range = ran
    if (range == 1) {
        range = [0, 1]
    } else {
        range = [-1, undefined]
    }
    const hiragana = ["", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "",
        "", "", "",
        "", "", "", "", "",
        "",
        "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
    ]
    const katakana = ["", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "",
        "", "", "",
        "", "", "", "", "",
        "",
        "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",
        "", "", "", "", "",]
    var r = [];
    var func_str = str;
    if (func_str.slice(range[0], range[1]) == "" || func_str.slice(range[0], range[1]) == "-" || func_str.slice(range[0], range[1]) == "!" || func_str.slice(range[0], range[1]) == "?" || func_str.slice(range[0], range[1]) == "" || func_str.slice(range[0], range[1]) == "") {
        func_str = func_str.slice(-2);
        func_str = func_str.slice(0, 1);
    }
    if (hiragana.indexOf(func_str.slice(range[0], range[1])) != -1) {//ひらがな
        r.push(func_str.slice(range[0], range[1]));
        r.push(katakana[hiragana.indexOf(func_str.slice(range[0], range[1]))]);
        console.log(r)
    } else if (katakana.indexOf(str.slice(range[0], range[1])) != -1) {//カタカナ
        r.push(hiragana[katakana.indexOf(func_str.slice(range[0], range[1]))]);
        r.push(func_str.slice(range[0], range[1]));
        console.log(r)
    } else {//漢字
        $.ajax({
            type: 'POST',
            timeout: 10000,
            url: "https://labs.goo.ne.jp/api/hiragana",
            async: false,
            'headers': {
                'Content-Type': "application/json",
            },
            data: JSON.stringify({
                'app_id': '自分のapp id',
                'sentence': func_str,
                'output_type': 'hiragana'
            }),
        }).done(function (data) {
            func_str = data.converted;
            if (func_str.slice(range[0], range[1]) == "") {
                func_str = func_str.slice(-2);
                func_str = func_str.slice(0, 1);
            } else {
                func_str = func_str.slice(range[0], range[1]);
            }
            r.push(func_str.slice(range[0], range[1]));
            r.push(katakana[hiragana.indexOf(func_str.slice(range[0], range[1]))]);
            console.log(r)
        });
    }
    switch (r[0]) {//小文字変換 ひらがな
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        case "":
            r[0] = "";
            break;
        default:
            break;
    }
    switch (r[1]) {//小文字変換 カタカナ
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        case "":
            r[1] = "";
            break;
        default:
            break;
    }
    console.log(r);
    return r;
}
function say(text, element) {
    element.html(element.html() + "<div class=\"kaiwa\"><!-左からの吹き出し-><figure class=\"kaiwa-img-left\"><img src=\"./icons/Wikipedia-logo-v2-ja.png\" alt=\"no-img2\"><figcaption class=\"kaiwa-img-description\">しりとり AI</figcaption></figure><div class=\"kaiwa-text-right\"><p class=\"kaiwa-text\">" + text + "</p></div></div><!-左からの吹き出し 終了->")
}

とても長くなりました!

解説

このしりとりAIはなんと音声認識ができます!
しかし、SpeechRecognitionはまだブラウザが完全に対応してないのでバグが多いです。
(Windows版Chrome 80.0.3987.149 では結構しっかり動いた)
なのでtextareaから入力することをお勧めします。

siritori.jsではtextareaまたは音声認識で入力が来たら、まず前の言葉の最後の文字と頭文字が同じかチェックし、そのあとその言葉が今までに出たかをチェックします。

if (next_word != str_chenge(text, 1)[0]) {
//      ・・・略・・・
} else if (Word_history.indexOf(text) != -1) {
//      ・・・略・・・
}

その二つの条件をクリアしたら
関数siritori()を呼び、その中でWikipediaのAPIからしりとりの条件にある言葉を取得して返答しています。
関数WikipediaAPI()の中の配列NG_wordには「ん」で読み仮名が終わる漢字など返答の候補に入れる際、除外してほしい文字が入っています。
関数str_chengeは言葉をひらがなや、カタカナに変換し、最後や最初の一文字を返します。
「ー」や「!」などが最後の一文字の場合はその一つ前の文字を、
「ぁ」や「ゃ」などの小さい文字は「あ」や「や」に変換してくれます。

ひらがな化APIの部分にはgooラボAPI利用登録からgithubで登録してappidを取得して、str_chenge()のapp_idを書き換えてください。

function str_chenge(str, ran) {
    //  省略
     else {//漢字
        $.ajax({
            type: 'POST',
            timeout: 10000,
            url: "https://labs.goo.ne.jp/api/hiragana",
            async: false,
            'headers': {
                'Content-Type': "application/json",
            },
            data: JSON.stringify({
                'app_id': 'xxxxxxxxxxxxxx',// <=ここを書き換える
                'sentence': func_str,
                'output_type': 'hiragana'
            }),
        })
//   省略

こんなコードを書くとWikipediaの頭脳を持ったしりとりAIが完成します。

なんと!このAIは最後の文字を「ん」にしても言葉を返してきます!

遊んでみてください!
しりとり AI
GitHub

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

PlayCanvasでスプライトアニメーション作成、そしてアニメーションからイベント発火

最初に

昔作ったゲームをブラウザゲームとして作り直そうと思っていて、

kuchiinu_a_attack03.png

こんなアニメーションをする画像をPlayCanvas上で使えるようにしたいなと考えています。

ちなみに以前作った環境ではメインループの中に入力も当たり判定も描画もぶっ込んでいるような、パワータイプなプログラムを書いていました。

今回はGUI環境に合わせてプログラムしていきたいと思います。

アセットへスプライトの原料をアップロード

画像は透過PNGで行きます。
スクリーンショット 2020-03-26 1.20.23.png

透過PNGってゲーム作るとき便利よね・・・
256色ビットマップしかゲームプログラムで使える選択肢がなくて、256色の中から透過色を一つ選ばねばならなかった頃が懐かしい・・・

テクスチャーアトラスを作成

アップした画像で右クリックして「Create Texture Atlas」を選択
スクリーンショット 2020-03-26 1.20.59.png

もとの画像はtypeが「texture」ですが、
スクリーンショット 2020-03-26 1.21.20.png

新しく生成した方はtypeが「textureatlas」となっていて、アイコンが左上にあります。
スクリーンショット 2020-03-26 1.21.30.png

もとの画像の方はもう不要なので削除しても構いません。

テクスチャアトラスをフレーム分けする

作成したテクスチャアトラスをダブルクリックするとスプライトエディタが開きます。
スクリーンショット 2020-03-26 1.23.11.png

この画像をスプライトアニメーションのコマとなるフレームに分けていきます。

スクリーンショット 2020-03-26 1.23.33.png

本来はマウスドラッグで1個1個作っていくのですが、とっっっても面倒くさいのでここで手品を使います。
(同じ大きさの矩形がお行儀よく並んでいること前提なのですが)

以下のJSONファイルを「UPLOAD TEXTURE PACKER JSON」というボタンからアップロードしてください。

spritesheet.json
{
    "frames": {
        "rei_00": {
            "frame": {"x": 0,"y": 0,"w": 256,"h": 256}
        },
        "rei_01": {
            "frame": {"x": 256,"y": 0,"w": 256,"h": 256}
        },
        "rei_02": {
            "frame": {"x": 512,"y": 0,"w": 256,"h": 256}
        },
        "down_00": {
            "frame": {"x": 0,"y": 256,"w": 256,"h": 256}
        },
        "down_01": {
            "frame": {"x": 256,"y": 256,"w": 256,"h": 256}
        },
        "down_02": {
            "frame": {"x": 512,"y": 256,"w": 256,"h": 256}
        },
        "down_03": {
            "frame": {"x": 768,"y": 256,"w": 256,"h": 256}
        },
        "a_stand_00": {
            "frame": {"x": 0,"y": 512,"w": 256,"h": 256}
        },
        "a_front_00": {
            "frame": {"x": 256,"y": 512,"w": 256,"h": 256}
        },
        "a_back_00": {
            "frame": {"x": 512,"y": 512,"w": 256,"h": 256}
        },
        "a_parry_00": {
            "frame": {"x": 0,"y": 768,"w": 256,"h": 256}
        },
        "a_parry_01": {
            "frame": {"x": 256,"y": 768,"w": 256,"h": 256}
        },
        "a_receive_00": {
            "frame": {"x": 512,"y": 768,"w": 256,"h": 256}
        },
        "a_receive_01": {
            "frame": {"x": 768,"y": 768,"w": 256,"h": 256}
        },
        "a_attack_x00": {
            "frame": {"x": 0,"y": 1024,"w": 256,"h": 256}
        },
        "a_attack_x01": {
            "frame": {"x": 256,"y": 1024,"w": 256,"h": 256}
        },
        "a_attack_x02": {
            "frame": {"x": 512,"y": 1024,"w": 256,"h": 256}
        },
        "a_attack_y00": {
            "frame": {"x": 0,"y": 1280,"w": 256,"h": 256}
        },
        "a_attack_y01": {
            "frame": {"x": 256,"y": 1280,"w": 256,"h": 256}
        },
        "a_attack_y02": {
            "frame": {"x": 512,"y": 1280,"w": 256,"h": 256}
        },
        "a_attack_z00": {
            "frame": {"x": 0,"y": 1536,"w": 256,"h": 256}
        },
        "a_attack_z01": {
            "frame": {"x": 256,"y": 1536,"w": 256,"h": 256}
        },
        "a_attack_z02": {
            "frame": {"x": 512,"y": 1536,"w": 256,"h": 256}
        },
        "b_stand_00": {
            "frame": {"x": 0,"y": 1792,"w": 256,"h": 256}
        },
        "b_front_00": {
            "frame": {"x": 256,"y": 1792,"w": 256,"h": 256}
        },
        "b_back_00": {
            "frame": {"x": 512,"y": 1792,"w": 256,"h": 256}
        },
        "b_parry_00": {
            "frame": {"x": 0,"y": 2048,"w": 256,"h": 256}
        },
        "b_parry_01": {
            "frame": {"x": 256,"y": 2048,"w": 256,"h": 256}
        },
        "b_receive_00": {
            "frame": {"x": 512,"y": 2048,"w": 256,"h": 256}
        },
        "b_receive_01": {
            "frame": {"x": 768,"y": 2048,"w": 256,"h": 256}
        },
        "b_attack_x00": {
            "frame": {"x": 0,"y": 2304,"w": 256,"h": 256}
        },
        "b_attack_x01": {
            "frame": {"x": 256,"y": 2304,"w": 256,"h": 256}
        },
        "b_attack_x02": {
            "frame": {"x": 512,"y": 2304,"w": 256,"h": 256}
        },
        "b_attack_y00": {
            "frame": {"x": 0,"y": 2560,"w": 256,"h": 256}
        },
        "b_attack_y01": {
            "frame": {"x": 256,"y": 2560,"w": 256,"h": 256}
        },
        "b_attack_y02": {
            "frame": {"x": 512,"y": 2560,"w": 256,"h": 256}
        },
        "b_attack_z00": {
            "frame": {"x": 0,"y": 2816,"w": 256,"h": 256}
        },
        "b_attack_z01": {
            "frame": {"x": 256,"y": 2816,"w": 256,"h": 256}
        },
        "b_attack_z02": {
            "frame": {"x": 512,"y": 2816,"w": 256,"h": 256}
        }
    },
    "meta": {
        "app": "",
        "version": "1.0",
        "image": "nuemaru.png",
        "format": "RGBA8888",
        "size": {"w": 1024,"h": 4096},
        "scale": "1"
    }
}

スクリーンショット 2020-03-26 2.03.59.png

JSONをアップロードすると自動で256×256のフレームに切り分けられます。
スクリーンショット 2020-03-26 15.09.08.png

使ったJSONファイルは1024×4096サイズの画像を256×256のフレームを切り分けるように記述してあります。
使う際は書き換えてください。

スプライトを作る

前項のフレーム分けでできたrei_00,rei_01,rei_02をつなげてスプライトをつくります。

一番最初にくるフレームを選択して「NEW SPRITE FROM SELECTION」をクリック。
スクリーンショット 2020-03-26 15.16.32.png

「SPRITE ASSETS」に「rei_00」というスプライトが作成されます。
続けてフレームを追加する場合は「ADD FRAMES TO SPRITE ASSET」をクリック。
スクリーンショット 2020-03-26 15.19.44.png

追加したいフレームを選択して「ADD SELECTED FRAMES」をクリック。
スクリーンショット 2020-03-26 15.24.49.png

追加完了。
スクリーンショット 2020-03-26 15.25.02.png

スプライトアニメーションの土台を作る

Root直下で「Animated Sprite」のエンティティを追加します。
スクリーンショット 2020-03-26 1.19.30.png

スプライトをエンティティにセットする。

スクリーンショット 2020-03-26 15.34.29.png

これでLaunchするとアニメーションしているのが確認できます。

スプライトからイベントを発火してみる。

ここまでくると攻撃などのアニメーション開始時に移動や攻撃判定などの処理を追加したくなるはずです。

ちょっとだけ試してみましょう。

AnimatedSpriteにスクリプトを追加し、以下のコードを追加します。

CtrlPlayer.prototype.initialize = function() {

/* 中略 */

this.entity.sprite.clip("Clip 1").on("loop",this.looploop);

}

CtrlPlayer.prototype.looploop = function(){
    console.log("looploop");
    return;
};

これで"Clip 1"のスプライトがループするときにイベントが起きるようになるので、イベントハンドラ内に移動なり当たり判定を発生させるなり煮るなり焼くなり好きにできるようになります。

他のスプライトアニメーションにイベントハンドラをセットしたい場合は、clipメソッドの引数を別のスプライトアニメーションの名前に変えてみましょう。

pc.SpriteAnimationClipの説明によるとイベントは繰り返し発生時のloop以外にも

  • end
  • pause
  • play
  • resume
  • stop

があります。

わがままを言えば、各アニメーションのフレームが変わる時・・・例えば4フレーム目だけで起こすイベントなんかを設定できたらいいんだけどなあーと思っています。(こういうのは自分で作るべき?)

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

「うごけ!道案内」の地図スクリプトを YOLP JavaScript マップ API で再生してみる

うごけ!道案内とは

地図コマンドの羅列による地図スクリプトで地図を操作できる Web サービス。

ugomichi.png

「うごけ!道案内」で地図プログラミング - Yahoo! JAPAN Tech Blog

「うごけ!道案内」は、MS-DOSのバッチファイル風にコマンドを並べるだけで地図を思い通りに動かせるプログラミングツールなのです。

うごけ!道案内の使い方 - LatLongLab

「うごけ!道案内」は、地図を使って動きのある道案内を簡単に作成し共有できるサービスです。
お店やサービスの所在地を紹介したり、行動記録を確認したり、アドベンチャーゲームやクイズなど、様々なシーンで活用できます。

うごけ!道案内の使い方 - LatLongLab

アドバンスモードではさらにインタラクティブな地図を作る手段として簡単なスクリプトで地図操作のプログラミングが可能です。
アドバンスモードを使えば条件分岐や写真の表示などの機能を使って本格的なゲームなどのコンテンツも作成いただけます。

「うごけ!道案内」は2020年3月末でクローズされてしまう。

LatLongLab

約10年間にわたりサービスを運用してまいりましたLatLongLabですが、
誠に勝手ながら、2020年3月末をもってサービスを終了させていただくこととなりました。

地図スクリプト

地図スクリプトは以下のような地図コマンドとパラメータによる地図コマンドを並べたものになっている。

moveto 36/12/50.407,137/22/37.774
layerto 16
message 日本で一番短い鉄道って知ってます?
sleep 3000
smoveto 35/46/10.385,140/23/14.688
message それは千葉県にある
sleep 3000
layerto 6
message 芝山鉄道です。

地図コマンド

以下のように地図を操作するコマンドがある。

うごけ!道案内の使い方 - LatLongLab

ugomichi-command.png

地図スクリプトを再生するサンプルコード

YOLP Yahoo! JavaScript マップ API を使用して、いくつかの地図コマンドを実現してみた (YOLP Yahoo! JavaScript マップ API は2020年10月31日にクローズ予定)。

macOS Catalina + Google Chrome 80.0.3987.149 で動作確認済み。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>「LatLongLab うごけ!道案内」風</title>

<script type="text/javascript" charset="utf-8" src="https://map.yahooapis.jp/js/V1/jsapi?appid=YOUR_APPLICATION_ID"></script>

<script>

// 「うごけ!道案内」っぽい再生をするクラス
class UgoMichi {

  // map: 地図操作オブジェクト
  // script: 地図スクリプトテキスト
  constructor(map, script) {
    this.script = script.split(/\r\n|\r|\n/);
    this.map = map;
  }

  // 地図スクリプトを再生する
  play() {
    this._doCommand(this._doCommand, this.map, this.script, 0);
  }

  // コマンドを実行する
  _doCommand(doCommand, map, script, index) {

    if(index >= script.length) {
      return; // 地図スクリプトを全部処理した
    }

    // 地図スクリプトから1行抜き出す
    const item  = script[index];

    // コマンド名とパラメータに分ける
    const sepIndex = item.indexOf(" ");
    const command  = item.substring(0, sepIndex);
    const param    = item.substring(sepIndex + 1);

    let sleepTime = 200;
    if(command === "sleep") {
      // sleep コマンド
      sleepTime = parseInt(param);
    } else {
      // sleep コマンド以外
      map[command](param);
    }

    // 再帰呼び出し
    setTimeout(doCommand, sleepTime, doCommand, map, script, ++index);
  }
}

// YOLP Yahoo! JavaScriptマップAPI を使用した地図操作クラス
// 各種コマンドに対応したメソッドを持つ
class YolpMap {
  constructor(ymap) {
    this.ymap = ymap;
  }
  // moveto 緯度,経度: 地図を指定した位置に瞬間的に移動
  moveto(param) {
    const dms = param.split(",");
    const ll = new Y.LatLng(this._dmsToDeg(dms[0]), this._dmsToDeg(dms[1]));
    this.ymap.panTo(ll, false);
  }
  // smoveto 緯度,経度: 地図を指定した位置まで連続的に移動
  smoveto(param) {
    const dms = param.split(",");
    const ll = new Y.LatLng(this._dmsToDeg(dms[0]), this._dmsToDeg(dms[1]));
    this.ymap.panTo(ll, true);
  }
  // layerto レイヤーID: レイヤー切り替え
  layerto(param) {
    const layerId = parseInt(param);
    // うごけ!道案内のレイヤーIDをYOLPのズームレベルに変換
    const zoomLevel = 20 - layerId;
    this.ymap.setZoom(zoomLevel, true);
  }
  // message 文字列: メッセージを表示
  message(param) {
    const text = param;
    this.ymap.openInfoWindow(this.ymap.getCenter(), text);
  }
  // 度分秒表記(35/59/59.999)を度(35.999999)に変換
  _dmsToDeg(dmsString) {
    const sign = dmsString.charAt(0) == "-" ? -1 : 1; // 負の値か
    const items = dmsString.split("/"); // 度/分/秒を分解
    const d = Math.abs(parseInt(items[0]));
    const m = parseInt(items[1]);
    const s = parseFloat(items[2]);
    return sign * (d + m / 60 + s / 60 / 60);
  }
}

window.onload = function() {

  // YOLP Yahoo! JavaScriptマップAPI の準備
  const ymap = new Y.Map("map");
  ymap.addControl(new Y.CenterMarkControl()); // 地図中心点を表示
  ymap.drawMap(new Y.LatLng(35, 135), 17, Y.LayerSetId.NORMAL); // 地図を表示

  // 「うごけ!道案内」っぽい処理をするクラスを使用
  const map = new YolpMap(ymap);
  const myScript = document.getElementById("my-script").innerHTML; // 地図スクリプトデータ
  const ugo = new UgoMichi(map, myScript);
  ugo.play();
}

</script>
</head>
<body>

<!-- 地図表示領域 -->
<div id="map" style="width:600px; height:430px"></div>

<!--
日本一短い鉄道 - うごけ!道案内 - LatLongLab
https://latlonglab.yahoo.co.jp/macro/watch?id=b834fc87f0ba874c24174b3de436f306
-->
<textarea id="my-script" style="display:none">moveto 36/12/50.407,137/22/37.774
layerto 16
message 日本で一番短い鉄道って知ってます?
sleep 3000
smoveto 35/46/10.385,140/23/14.688
message それは千葉県にある
sleep 3000
layerto 6
message 芝山鉄道です。
sleep 3000
message では乗ってみましょう
sleep 3000
moveto 35/45/14.756,140/23/58.567
layerto 2
message 出発進行ー!
sleep 3000
smoveto 35/45/18.881,140/23/48.28
smoveto 35/45/20.018,140/23/46.8
smoveto 35/45/26.19,140/23/41.917
smoveto 35/45/29.892,140/23/38.996
message ここからトンネルです
sleep 3000
smoveto 35/45/35.901,140/23/34.153
smoveto 35/45/42.429,140/23/29.55
smoveto 35/45/48.08,140/23/25.788
smoveto 35/45/53.666,140/23/23.587
smoveto 35/45/58.18,140/23/22.307
smoveto 35/46/2.077,140/23/20.105
smoveto 35/46/7.306,140/23/16.343
smoveto 35/46/12.502,140/23/13.021
message 東成田~。東成田~
sleep 4000
message はい。おしまいです。
sleep 3000
message 長さは2.2km。時間にして4分ほどの旅です。
sleep 3000</textarea>

</body>
</html>

本家「うごけ!道案内」と「うごけ!道案内」風の比較動画

本家「うごけ!道案内」と「うごけ!道案内」風の比較 - YouTube

参考資料

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

【JavaScript】numberのビットを見る【IEEE754】

はじめに

JavaScriptの数値がどのようにビットが立っているか知りたくなりました。
そこでFloat64Arrayをを使って表示してみました。
こちらの浮動小数点シミュレーターで比較したり、toString(2)で比較して一致しているのを確認しているので、numberでもメモリ上はFloat64Arrayと同じなのかな?って思っています。

IEEE754について簡単に説明

IEEE754の64bitは以下のようになっております。
符号部(1bit) | 指数部(11bit) | 仮数部(52bit)
仮数部は先頭の1を省略するので、実質53bitです。
詳しくはググってください。
ここが分かりやすいと思います。

プログラム

const float64 = new Float64Array(1);
float64[0] = 0.1; // ここで値を変更してください。
console.log(float64[0]);
const uint8 = new Uint8Array(float64.buffer);
const str = uint8.reduce((p, c) => ('00000000' + c.toString(2)).slice(-8) + p, '');
console.log(str);

プログラム実行して比較

0.1と0.2でプログラムを実行して、
こちらの浮動小数点シミュレーターで比較したり、toString(2)で比較してみます。

0.1をプログラム実行

01pg.png

0.1を浮動小数点シミュレーターで実行

一致しています。
01pg.png

0.1をtoString(2)

2番目の1から比較すると、仮数部が一致しています。
01_02.png

0.2をプログラム実行

02pg.png

0.2を浮動小数点シミュレーターで実行

一致しています。
02simu.png

0.2をtoString(2)

2番目の1から比較すると、仮数部が一致しています。
02_02.png

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

ブロックチェーンでリモート承認してみる(3)

今回は社会実装を念頭により実践的なリモート承認を想定をしてみます。

前回までの記事を先に読んでおいてください。

ブロックチェーンでリモート承認してみる(1)
ブロックチェーンでリモート承認してみる(2)

制約条件

  • 申請者が手数料分のお金を持っていない
  • 申請に使用するアカウントと申請者を区別したい

解決策

  • 承認者が申請に必要な手数料を肩代わりする
  • 申請アカウントを申請者が署名者とするマルチシグアカウントとして、申請者が申請書の所有を表現する

何がどう実践的なの?

申請をする立場の人がネットワーク利用のための手数料を持ち合わせていないことは、今後よくあり得ることかもしれません。取引所を使用したことが無い人のことを考えておくのが重要です。
また、ブロックチェーンが扱う秘密鍵は本来自分が生成するものであって会社から貸与されるべきものではありません。一方で申請書などに使用するアカウントは会社が会社が管理したアカウント上で行うことが望ましいです。そのため、今回はマルチシグを用いて会社の資産を従業員(申請者)が所有するというモデルを表現してみました。

事前準備

インポート

alice,bob
(script = document.createElement('script')).src = 'https://s3-ap-northeast-1.amazonaws.com/xembook.net/nem2-sdk/symbol-sdk-0.17.3.js';
document.getElementsByTagName('head')[0].appendChild(script);

定義

モジュールのインポート、定数宣言、ノードへアクセスするための準備を行います。

alice,bob
nem = require("/node_modules/symbol-sdk");
op = require("/node_modules/rxjs/operators");

NODE = "https://sym-test.opening-line.jp:3001";
GENERATION_HASH = "44D2225B8932C9A96DCB13508CBCDFFA9A9663BFBA2354FEEC8FCFCB7E19846C";

txHttp = new nem.TransactionHttp(NODE);
accountHttp = new nem.AccountHttp(NODE);
wsEndpoint = NODE.replace('http', 'ws');
listener = new nem.Listener(wsEndpoint, WebSocket);
listener.open().then(() => {
  listener.newBlock();
});

初期設定

承認者アカウント

bob
bob = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET);
console.log(bob.address.plain());
console.log(bob.publicAccount);
"http://faucet-01.symboldev.network/?recipient=" + bob.address.plain() +"&amount=10"

申請者は手持ちの手数料を持たない条件なので蛇口から入金するのはこちらだけになります。

申請者と申請アカウント

aliceを申請アカウント、carolを申請者アカウントとします。

alice
alice = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET); //会社が割り振ったアカウント
carol = nem.Account.generateNewAccount(nem.NetworkType.TEST_NET); //社員が自分で生成したアカウント
console.log(alice.address.plain());
console.log(alice.publicAccount);
console.log(carol.publicAccount);
bob
alicePublicAccount = nem.PublicAccount.createFromPublicKey("{aliceのpublicKey}",nem.NetworkType.TEST_NET);
carolPublicAccount = nem.PublicAccount.createFromPublicKey("{carolのpublicKey}",nem.NetworkType.TEST_NET);

承認者側でトランザクションを組み立てるので公開アカウント情報をbob側で生成しておきます。

トランザクション生成

feeTx = nem.TransferTransaction.create(
    nem.Deadline.create(),
    bob.address,
    [
        new nem.Mosaic(
            new nem.MosaicId('747B276C30626442'),
            nem.UInt64.fromUint(10000000)
        )
    ],
    nem.PlainMessage.create(''),
    nem.NetworkType.TEST_NET,
);

multisigTx = nem.MultisigAccountModificationTransaction.create(
    nem.Deadline.create(), 1,1,
    [carolPublicAccount],
    [],
    nem.NetworkType.TEST_NET
);

aplicationTx = nem.TransferTransaction.create(
    nem.Deadline.create(),
    bob.address,[],
    nem.PlainMessage.create('b7d3e3191d2d2e77ed6e455eeaec147c13e19f0c079f0ca0dcff853f3df46911'),
    nem.NetworkType.TEST_NET
);

approvalTx = nem.TransferTransaction.create(
    nem.Deadline.create(),
    alicePublicAccount.address,[],
    nem.PlainMessage.create('approved:b7d3e3191d2d2e77ed6e455eeaec147c13e19f0c079f0ca0dcff853f3df46911'),
    nem.NetworkType.TEST_NET
);

  • feeTx
    • 申請者が送信するときに必要な手数料を承認者から送金しておきます。
  • multisigTx
    • 申請者アカウントを申請アカウントの署名者に設定します。
  • aplicationTx
    • 申請アカウントから承認アカウントへ申請ファイルのハッシュ値を刻んだメッセージを送信します。
  • approvalTx
    • 承認者アカウントから申請アカウントへ承認メッセージを送信します

トランザクションの集約と署名

aggregateTx = nem.AggregateTransaction.createBonded(
    nem.Deadline.create(),
    [
        feeTx.toAggregate(bob.publicAccount),
        multisigTx.toAggregate(alicePublicAccount),
        aplicationTx.toAggregate(alicePublicAccount),
        approvalTx.toAggregate(bob.publicAccount)
    ],
    nem.NetworkType.TEST_NET,[],
    nem.UInt64.fromUint(1000000)
);

signedTx =  bob.sign(aggregateTx, GENERATION_HASH);

署名するアカウントが誰かを間違えないようにして配列に並べます。

ロックトランザクションの作成と署名

集約したトランザクションは署名が集まるまで、ブロックチェーンで仮置きしてもらうためにそれに関する手数料支払いのトランザクションを作成します。

bob
lockTx = nem.HashLockTransaction.create(
    nem.Deadline.create(),
    new nem.Mosaic(
        new nem.MosaicId('747B276C30626442'),
        nem.UInt64.fromUint(10000000)
    ),
    nem.UInt64.fromUint(5000),
    signedTx,
    nem.NetworkType.TEST_NET,
    nem.UInt64.fromUint(100000)
);
lockSignedTx = bob.sign(lockTx, GENERATION_HASH);

ロック承認後のトランザクション発行処理

リスナーを利用して自動化しておきます。

bob
listener.confirmed(bob.address).pipe(
    op.filter(tx => {
        console.log(tx);
        return tx.transactionInfo !== undefined && tx.transactionInfo.hash === lockSignedTx.hash;
    }),
    op.mergeMap(_ => {
        console.log(_);
        return txHttp.announceAggregateBonded(signedTx);
    })
).subscribe(x => console.log(x), err => console.error(err));

申請者側の署名処理

alice
bondedListener = listener.aggregateBondedAdded(alice.address)

bondedListener.pipe(
    op.filter(_ => !_.signedByAccount(alice.publicAccount)),
    op.map(_ => alice.signCosignatureTransaction(nem.CosignatureTransaction.create(_))),
    op.mergeMap(_ => txHttp.announceAggregateBondedCosignature(_))
).subscribe(x => console.log(x), err => console.error(err));

bondedListener.pipe(
    op.filter(_ => !_.signedByAccount(carol.publicAccount)),
    op.filter(_ => { //申請の確認
        return _.innerTransactions[2].message.payload === "b7d3e3191d2d2e77ed6e455eeaec147c13e19f0c079f0ca0dcff853f3df46911";
    }),
    op.map(_ => carol.signCosignatureTransaction(nem.CosignatureTransaction.create(_))),
    op.mergeMap(_ => txHttp.announceAggregateBondedCosignature(_))
).subscribe(x => console.log(x), err => console.error(err));

申請者側で必要な署名処理をリスナーで記述しておきます。
承認者が作成したトランザクションが間違いなく自分が作成したハッシュを記しているかの確認が必要です。
aliceでの署名はマルチシグ時の署名です。申請の署名は申請者であるcarolの署名を用いて行います。

実行

それでは実行してみましょう。

bob
txHttp.announce(lockSignedTx).subscribe(x => console.log(x), err => console.error(err));

//ロックトランザクションのconfirmed確認用
"https://sym-test.opening-line.jp:3001/transaction/" + lockSignedTx.hash +  "/status"

//全トランザクションのconfirmed確認用
"https://sym-test.opening-line.jp:3001/transaction/" + signedTx.hash +  "/status"

手数料を持ち合わせているのが承認者であるためにBobがネットワークにアナウンスする必要があります。
ひとたび実行すれば後の処理は自動的に実行されます。

最後に

全3回にわたるリモート承認についての解説を行いました。
3回目は少し難しかったかもしれませんが、マルチシグ、アグリゲートトランザクション、WebsocketなどNEMブロックチェーンの開発しやすい特徴をフル活用した内容となっています。いつかこの記事が悩めるブロックチェーン技術者を導く光となりますように。

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

技術書典応援際に参加しています。

技術書典(WEB版)にて

・WebRTCでビデオ会議システムを作る方法
・ngrokの使用方法
・HEROKUへのホスティング

を解説した電子書籍を販売しております。

表紙_FIT.png

初めての出版なので、至らぬ点も多々あるかと思いますがどうかよろしくお願い致します。

(追記)
どなたでも動かせるWebビデオ通話アプリのデモを作ってみました!
複数のタブ間で同じ部屋名を入れてJoinするとビデオ会話する様子をお試しいただけます。
http://bit.ly/2v61L8m

この技術書を一通り行うと、同様のWEBアプリが誰でも作れるようになります。
http://bit.ly/2TP6mEm

1000円・PayPal

技術書展(応援祭)

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

技術書展応援際に参加しています。

技術書典(WEB版)にて

・WebRTCでビデオ会議システムを作る方法
・ngrokの使用方法
・HEROKUへのホスティング

を解説した電子書籍を販売しております。

表紙_FIT.png

初めての出版なので、至らぬ点も多々あるかと思いますがどうかよろしくお願い致します。

この技術書を一通り行うと、同様のWEBアプリが誰でも作れるようになります。
http://bit.ly/2TP6mEm

1000円・PayPal

(追記)
どなたでも動かせるWebビデオ通話アプリのデモを作ってみました!
複数のタブ間で同じ部屋名を入れてJoinするとビデオ会話する様子をお試しいただけます。
http://bit.ly/2v61L8m

技術書展(応援祭)

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

scrollMagicを利用してアニメーション

scrollMagicを利用してアニメーションさせたけど、
CDNとかじゃなくてyarnでインストールしてimportした故にちょっとめんどくさかった話です。

TweenMaxとscrollMagicを組み合わせて、指定した要素をアニメーションさせたい。

jsはwebpackでバンドルしているので、importする。
yarnを利用してscrollmagicを追加。

yarn add scrollmagic
yarn add gsap //これはTweenMax

scrollMagicを使いたい所に以下のように記述。
ついでにTweenMaxも入れておく。

import scrollMagic from 'scrollmagic'
import {TweenMax} from 'gsap'

const controller = new ScrollMagic.Controller()
new ScrollMagic.Scene({
      triggerElement: tigger,
      triggerHook: hook
    })
     .setTween(TweenMax.from(
    // ~ここにアニメーション~
     ))
     .addTo(controller)

という風にしたがエラーが出た。

scrollMagicのissueなどを見て、その中にあるanimation.gsap.jsを使ってみたり(失敗)imports-loader入れてみたり(失敗)…
解決には至らずだった。

なんとか解決(?)

他色々と記事を読んでいくうちに、最終的には↓で動くようになった。

yarn add scrollmagic-plugin-gsap
import * as ScrollMagic from 'scrollmagic'
import { ScrollMagicPluginGsap } from 'scrollmagic-plugin-gsap'
import {TweenMax} from 'gsap'

ScrollMagicPluginGsap(ScrollMagic, TweenMax)

const controller = new ScrollMagic.Controller()
new ScrollMagic.Scene({
      triggerElement: tigger,
      triggerHook: hook
    })
     .setTween(TweenMax.from(
    // ~ここにアニメーション~
     ))
     .addTo(controller)

scrollMagicとTweenMaxを使ってアニメーションをしたいとき、
もしかするとまた引っ掛かるかもしれないので覚えメモ


★補足

scrollMagicの中にあるanimation.gsap.jsプラグインはES6モジュールとの互換性がないために、問題が結構発生しているらしい。
(参考元)
・scrollmagic-plugin-gsap
https://www.npmjs.com/package/scrollmagic-plugin-gsap

・scrollMagicのissue内
https://github.com/janpaepke/ScrollMagic/issues/842#issuecomment-573303518

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

tinderで自動スワイプ

ブラウザでtinderを開きましょう

https://tinder.com

開発者ツールでconsoleタブを開きましょう

(command+option+iで開けるよ)

consoleに以下をコピペ

setInterval(() => {
document.querySelector(".recsCardboard__cardsContainer > div:nth-of-type(2)").querySelectorAll("button")[3].click();
},1000);

以上

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

WebSocket: Server=Java, Client=Java,JavaScript

Runtime

Server : Tomcat 8.5

Server : Java

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/hellowebsocket")
public class ChatWebSocket {
    // 全てのクライアント
    private static Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    // メッセージを送信してきたクライアント
    private Session currentSession = null;
    /*
     * 接続がオープンしたとき
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig ec) {
        sessions.add(session);
        currentSession = session;
    }
    @OnMessage
    public String onMessage(String message) throws IOException {
        Iterator<Session> it = sessions.iterator();
        while (it.hasNext()) {
            Session s = it.next();
            if (s != currentSession) {
                s.getBasicRemote().sendText(message);
            }
        }
        return message;
    }
    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }
}

Client 1 : Java

import java.net.URI;
import java.util.Scanner;
import javax.websocket.ContainerProvider;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
public class Client {
    public static void main(String[] args) throws Exception {
        String msg;
        Scanner scanIn = new Scanner(System.in);
        // 初期化のため WebSocket コンテナのオブジェクトを取得する
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        // サーバー・エンドポイントの URI
        URI uri = URI.create("ws://localhost:8080/hellowebsocket");
        // サーバー・エンドポイントとのセッションを確立する
        Session session = container.connectToServer(new WebSocketClientMain(), uri);

        while (true) {
            msg = scanIn.nextLine();
            session.getBasicRemote().sendText(msg);
            if (msg.equals("")) {
                break;
            }
        }
        scanIn.close();
        session.close();
    }
}

Client 1 : maven

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.glassfish.tyrus.bundles/tyrus-standalone-client -->
    <dependency>
        <groupId>org.glassfish.tyrus.bundles</groupId>
        <artifactId>tyrus-standalone-client</artifactId>
        <version>1.16</version>
    </dependency>
</dependencies>

Client 2 : JavaScript

<html>
<head>
<title>Chat</title>
<script type="text/javascript" src="./jquery.js"></script>
<script>
    var fullpath = document.location.pathname;
    var path = fullpath.substring(0, fullpath.lastIndexOf("/"));
    var url = "ws://" + document.location.host + path + "/hellowebsocket";
    var ws = new WebSocket(url); // 重要
    $(function() {
        $("#button-search").click(postChat);
        ws.onmessage = function(receive) {
            $("#message").text(receive.data);
        };
    });
    function postChat() {
        var text = $("#q").val();
        var state = ws.readyState;
        if (state == ws.OPEN) {
            ws.send(text);
        } else {
            alert("connection state is not OPEN: " + state);
        }
    }
</script>
</head>
<body>
    <div>
        <div id="inputform">
            <textarea id="q" cols="80" rows="2"></textarea>
            <input type="button" id="button-search" value="POST" />
        </div>
    </div>
    <div id="message"></div>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これだけでOK WebSocket: Server=Java, Client=Java,JavaScript

Java製のWebSocket Serverに対し、
複数のJava WebSocket Client と JavaScript WebSocket Client が接続し、
クライアントからのメッセージは各クライアントに対してブロードキャストされるものと想定します。

動作確認済 Runtime

  1. Local Server : Tomcat 8.5
  2. Azure WebApp Service (Tomcat 9.0)

Server : Java

import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/hellowebsocket")
public class ChatWebSocket {
    // 全てのクライアント
    private static Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    // メッセージを送信してきたクライアント
    private Session currentSession = null;
    /*
     * 接続がオープンしたとき
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig ec) {
        sessions.add(session);
        currentSession = session;
    }
    @OnMessage
    public String onMessage(String message) throws IOException {
        Iterator<Session> it = sessions.iterator();
        while (it.hasNext()) {
            Session s = it.next();
            if (s != currentSession) {
                s.getBasicRemote().sendText(message);
            }
        }
        return message;
    }
    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }
}

Client 1 : Java

import java.net.URI;
import java.util.Scanner;
import javax.websocket.ContainerProvider;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
public class Client {
    public static void main(String[] args) throws Exception {
        String msg;
        Scanner scanIn = new Scanner(System.in);
        // 初期化のため WebSocket コンテナのオブジェクトを取得する
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        // サーバー・エンドポイントの URI
        URI uri = URI.create("ws://localhost:8080/hellowebsocket");
        // サーバー・エンドポイントとのセッションを確立する
        Session session = container.connectToServer(new WebSocketClientMain(), uri);

        while (true) {
            msg = scanIn.nextLine();
            session.getBasicRemote().sendText(msg);
            if (msg.equals("")) {
                break;
            }
        }
        scanIn.close();
        session.close();
    }
}

Client 1 : maven

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.glassfish.tyrus.bundles/tyrus-standalone-client -->
    <dependency>
        <groupId>org.glassfish.tyrus.bundles</groupId>
        <artifactId>tyrus-standalone-client</artifactId>
        <version>1.16</version>
    </dependency>
</dependencies>

Client 2 : JavaScript

<html>
<head>
<title>Chat</title>
<script type="text/javascript" src="./jquery.js"></script>
<script>
    var fullpath = document.location.pathname;
    var path = fullpath.substring(0, fullpath.lastIndexOf("/"));
    var url = "ws://" + document.location.host + path + "/hellowebsocket";
    var ws = new WebSocket(url); // 重要
    $(function() {
        $("#button-search").click(postChat);
        ws.onmessage = function(receive) {
            $("#message").text(receive.data);
        };
    });
    function postChat() {
        var text = $("#q").val();
        var state = ws.readyState;
        if (state == ws.OPEN) {
            ws.send(text);
        } else {
            alert("connection state is not OPEN: " + state);
        }
    }
</script>
</head>
<body>
    <div>
        <div id="inputform">
            <textarea id="q" cols="80" rows="2"></textarea>
            <input type="button" id="button-search" value="POST" />
        </div>
    </div>
    <div id="message"></div>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

fetch APIによる画像の取得とData URIの発行

内容

POST通信しながら画像をイメージタグに入れる方法がないかと探してたけど、上手くまとまっているものが見当たらんかったので、備忘録。通信後にレスポンス結果をバイナリデータとし、それに対してData URI(webブラウザ上に領域を確保した際の仮のURI?)を発行し、これをタグのsrcに設定する。

仮に以下のようなhtmlがあったとする。

<img class="image"></img>

そしたら以下のような感じ。

fetch(url, {
  method: "POST",# GET, POST
  mode: "cors", // no-cors, cors, *same-origin
  cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
  credentials: "same-origin", // include, same-origin, *omit
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "hoge":"hoge" // post時の引数
  })
  }).then(respons => {
     return respons.blob(); // バイナリデータとする?
  }).then(blob =>{
     return URL.createObjectURL(blob); // Data URI発行
  }).then(dataUri =>{
     $('.image').attr('src', dataUri);
  })
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuexを初めから丁寧に(1)~Vuexを理解するために必須の前提知識~

はじめに

この記事を読むと

  • Vuexを理解するために必要な知識を習得できます
  • Vuexを学ぶためのマイルストーンが明確となります

想定読者

  • Vue.js や Nuxt.js の初級〜中級者
  • Vuex を何となく雰囲気で使っている

前提知識

JavaScript 及び Vue についての基本知識があることは前提とします。
(Vue の基本知識がない方はこちらが入門書として最も最適です。)
『Vue.js 超入門』(掌田津耶乃/秀和システム)

またJavaScriptにおいては特に、オブジェクトの使い方にも慣れておくとスムーズでしょう。
(こちらの第9章が最も良い説明だと思います。)
『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン)

Vuex の理解が難しい原因

なぜ Vuex が難しいと感じるのでしょうか?
私の場合は専門用語の意味が省略されていることに起因していました。
さらに問題なのは、 「Vuexを理解するためのキーとなる用語」が、全く違う意味で使われているにも関わらず見た目は一般的な日本語と一緒なのでなんとなくわかった気になり、「何が分からないのか分からない」状況に陥ることです。
例えば Vuex における「状態」
「アプリケーションが保持するデータ」
のことを指します。
なので、「Vuex は状態管理ライブラリである」「Vuex は状態を管理するために単方向データフローを採用している」といった説明や図解※を見ても、肝心の「状態」が分からないので、文章の意味が消化できないまま頭を素通りしていくだけでした。

しかし逆に言うと、用語の意味さえ押さえておけば Vuex はスラスラ理解できます。

※Vuexデータフローの図解
Vuex図解(Vuex公式ドキュメントより)

Vuex を理解するためのツボ

さて、前置きが長くなりましたが本題です。
たった 4 つだけです。

  • 用語を正確に理解する

    • 「状態」
    • 「データフロー」
  • 「データフローの設計」と「状態管理」の意義を理解する

    • 信頼できる唯一の情報源(Single Source of Truth)
    • 単方向フロー(one-way data flow)
    • 情報と取得のカプセル化(Encapsulation of sorce and receiving)
  • Vuex の構成要素の役割と使い方を理解する

    • State
    • Getters
    • Mutations
    • Actions
  • ※「ストアのモジュール分割」は一旦省略します

Vuex に入る前に

いきなり Vuex に入るより、まず状態管理やデータフローの基本知識を押さえておくと、スムーズに理解が進みます。

「状態」とは

状態とは
「アプリケーションが保持するデータ」
のことです。
ユーザーの操作やイベントの発生などによってその値が更新されていきます。

例えば、EC サイトのショッピングカートです。カートは何も入っていない空の状態から始まり、ユーザーが商品をカートに入れる操作を行うことでカートは空の状態に戻り、購入処理が完了します。

規模が大きいアプリケーションは保持する状態の数、それぞれの組み合わせの数も多くなり、そのままでは扱いきれなくなります。

繰り返しになりますが、Vuex において「状態」は普段の日本語とは異なる特別な意味がある言葉なので注意してください。

データフローとは

「データフロー」とは
「状態を含む、アプリケーションが持つデータの流れ」
のことを指します。
具体的には、どこにデータを保持し、データを読み込む時や更新するときはどこからどのように行うのかという点を表すことが多いです。

データフローの設計において、以下の三つのプラクティスが重要です。

信頼できる唯一の情報源

「信頼できる唯一の情報源」(single source)とは、「管理する対象のデータを一箇所に集約することで管理を容易にすることを目的とする設計のパターン」です。

  • どのコンポーネントも同一のデータを参照するため、データや表示の不整合が発生しづらい
  • 複数のデータを組み合わせた処理を比較できる容易に実装できる
  • データの変更のログ出力、現在のデータの確認などの開発に便利なツールを作りやすい

「状態の取得・更新」のカプセル化

「状態の取得・更新」のカプセル化を行うことで、状態管理のコストを下げることができます。
例えばカウンターアプリの例では更新処理を store 内に記述することでカプセル化しており、コンポーネント側からは具体的にどのような実装がされているかは隠されています。

  • 状態の取得・更新のロジックを様々な場所から利用できる
  • 詳細な実装をビューから隠すことで、データ構造や取得、更新処理の変更の影響範囲を小さくする
  • デバッグ時に確認する場所が限られるため、デバッグが容易になる

単方向データフロー

単方向データフローにすることで、状態の取得、更新のコードが簡潔になります。
データが単方向でないと、データの取得と更新の両方を同時にできてしまい、より複雑な処理になり理解が難しくなってしまいます。

  • データを取得しつつ更新するといったようなことができなくなり、実装やデバッグが単純になる
  • データを取得、更新するために何をするかの選択肢が絞られて、理解が容易なコードをかきやすい

まとめ

ここまでデータフローの三つのプラクティスを見てきましたが、実はVuex は先ほど紹介したデータフローのプラクティスを全て満たします。

まず、Vuex はアプリケーションの状態やそれに付随するロジックが一つの場所(ストア)にまとまるように設計されているため、「信頼できる唯一の情報源」を満たします。

また、Vuex において状態の更新はミューテーションでのみ行うことができ、取得に関してもゲッターという機能で詳細な実装は隠蔽できるため「状態の取得と更新」のカプセル化も満たします。

さらに、状態の取得と更新の窓口が異なるため(冒頭の図解をもう一度参照ください)、強制的に実装が単方向データフローになります。

おわりに

いかがだったでしょうか。VueやNuxtで開発を行う方が、Vuexを理解するための助けになれば幸いです。
「状態管理」「データフロー」についてはバッチリですか?
次の記事ではいよいよ Vuex による状態管理について見ていきます。

参考文献

『Vue.js入門 基礎から実践アプリケーション開発まで』(川口和也, 喜多啓介, 野田陽平, 手島拓也, 片山真也/技術評論社)
Vue.jsについての書籍は増えてきていますが、問題なのはその殆どがVuexについての説明を省略していることです。Vue.jsやNuxt.jsを用いた実際の開発においてVuexによる状態管理は必須ですが、学習の障壁になるとして避けてしまっているのでしょう。私が読んだ中で唯一、Vuexについて丁寧に説明していたのが本書です。Vuex以外の内容も素晴らしいの一言。本書はVue.js・Nuxt.jsの開発に関わるエンジニアや組織にとって必携です。保存用・実用用・観賞用に3冊購入しましょう。あるいは、あなたが経営者の場合はぜひエンジニアに対して一人一冊ずつ買い与えてください。
ただし、全くVueについて未経験という方への第一歩としては内容が本格的すぎるかもしれません。その場合は『Vue.js 超入門』がおすすめです。

『Vue.js 超入門』(掌田津耶乃/秀和システム)
とにかく分かりやすく、まず概要を把握するために最適の一冊です。「なんとなくで良いので概要を把握する」⇨「より詳細で厳密な理解する」という流れで学ぶとスムーズです。

『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン)
JavaScriptの根本的な理解ができる、革命的な良書です。分厚いので手強そうに見えますが、実際はとても親切で分かりやすい作りです。本書も一人一冊は欲しいところです。

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

URLからパラメータを取得して活用する方法

目的

URLに設定されてたパラメータを利用できるように配列として取得する。
JQueryは不要。

コード

parameter.js
    let param;
    let param_split;
    if(window.location.search){
        param = window.location.search.substring(1,window.location.search.length)
        param_split = param.split("&");
    }

解説

parameter.js
    //変数宣言
    let param
    let param_split;

    //URLにパラメータが存在する場合
    if(window.location.search){
        //URL内のパラメータを取得(substring(1,~~~)の1はパラメータの最初の文字から1文字目
        param = window.location.search.substring(1,window.location.length);
        //取得したパラメータを&区切りで配列に代入
        param_split = param.split("&");
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スプレッド構文

スプレッド構文

「...」の形で記述され、配列やオブジェクトの要素を展開する構文です。

const test1 = { a: 1 };
const test2 = { b: 2 };

// test1とtest2が平らな状態でオブジェクトに入れることができます!
const test3 = { ...test1, ...test2};

// スプレッド構文を使わない場合
const test4 = {test1, test2};

// ネストしてい場合
const test_Oj1 = {a: {test: 1}, b: {test: 2}};
const test_Oj2 = {c: {test: 3}, d: {test: 4}};
const test_Oj = {...test_Oj1, ...test_Oj2};

// スプレッド構文を使わない場合
const test_Oj3 = {test_Oj1, test_Oj2};

結果
スクリーンショット 2020-03-21 3.04.37.png

ネストしてい場合はこんな感じになります↓
スクリーンショット 2020-03-21 3.53.40.png

スプレッド構文は配列でも使えます!

const test1 = [1,2];
const test2 = [3,4];

// こちらもtest1とtest2が平らな状態で配列に入れることができます!
const test3 = [...test1, ...test2];

結果
スクリーンショット 2020-03-21 3.26.19.png

Object.assign

同じようにコピーすることができるObject.assignというメソットもあります.
コピー元オブジェクトから列挙可能enumerableかつ直接所有ownのプロパティだけをコピー先オブジェクトにコピーします。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

const test1 = { a: 1 };
const test2 = { b: 2 };

// { ...test1, ...test2} と同じ結果になります
const test3 = Object.assign({}, test1, test2);

結果
スクリーンショット 2020-03-21 3.13.44.png

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

JavaScript系:Qiitaのタグ統計を浄化するためのタグ詰め合わせ記事

タグだけ

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

超雑な IndexedDB のメモ

雑な理解メモ

@startsalt
{{T
+ IndexedDB
++ DB_NAME
+++ OBJECT_STORE
++++ RECORDS
+++++ ROW
}}
@endsalt

普通の RDB に置き換えるなら

  • DB_NAME → DB
  • OBJECT_STORE → Table
  • RECORDS → Rowset
  • ROW → Row
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユーザーページにLGTMトップの記事を表示するユーザースクリプト

Screen Shot 2020-03-26 at 08.32.09-fullpage.png

// ==UserScript==
// @name         Qiita top items
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  なんか名前が変わったやつをたくさんあつめたのを表示する
// @author       khsk
// @match        https://qiita.com/*
// @exclude      https://qiita.com/
// @exclude      https://qiita.com/timeline*
// @exclude      https://qiita.com/tag-feed*
// @exclude      https://qiita.com/notifications*
// @grant        none
// ==/UserScript==

(async function() {
  'use strict';
  // APIで取得しようと思ったけどソートしないといけないし100投稿ごとにタイムラグが大きくなるのでもう検索結果を雑に流し込むことにした

  //ユーザーページ判定とid取得を雑に新ページ対応
  const userMain = document.querySelector('[class^="UserMain"]')
  if (!userMain) {
    return
  }

  // ページが変わったからまたID取得が怪しいかも @削除
  const id = document.querySelector('[class^="AccountBaseInfo__UrlName"]').textContent.substr(1)

  const response = await fetch('https://qiita.com/search?&sort=like&q=user%3A' + id)
  if (response.ok == false || response.status != 200) {
    console.error('fetch error', id , response)
    return
  }

  const dummyDOM = document.createElement('div')
  dummyDOM.innerHTML = await response.text()
  // ちょっとnth-of-typeの挙動を意図通りにできない。クラスでなくtag名(div)にかかるのでresult以外のdivひとつ分増やしている
  const topItems = dummyDOM.querySelectorAll('.searchResultContainer_main > div.searchResult:nth-of-type(-n+6)')
  const sideBar = document.querySelector('[class^="UserSidebar"]')

  topItems.forEach ((topItem)=> {
    // アイコン画像がでっかく表示されちゃうから雑に消す
    let tmp = topItem.querySelector('.searchResult_left')
    tmp.parentElement.removeChild(tmp)
    // 概要も不要
    tmp = topItem.querySelector('.searchResult_snippet')
    tmp.parentElement.removeChild(tmp)
    sideBar.innerHTML += topItem.outerHTML
    // TODO(しない)整形もっとがんばる
  })
})();

デザイン思い出せない

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

使いこなそう正規表現(入門)

この記事は株式会社クロノスの「~2020年春~勝手にやりますアドベントカレンダー」の18日目の記事です!

はじめに

正規表現の基本知識をまとめました。

正規表現は、ファイル検索や文字列置換を行うときに使用することができます。

エディタの検索で正規表現を活用することで、検索効率や置換効率があがるので活用していきましょう。
VSCodeでは以下の「.*」を選択すると正規表現が使用できるようなります。
image.png

サンプルコードはJavaScriptの match メソッドを使用しています。

'対象文字列'.match(/正規表現/);
// 取得結果

メタ文字(使用頻度が高いもののみ)

.

任意の1文字 にマッチします。

【例】

'test'.match(/./)[0];
// "t"

+

直前の文字が 1回以上 繰り返す場合にマッチします。

【例】

'tttee'.match(/t+/)[0];
// "ttt"
'tttee'.match(/a+/)[0];
// エラー( Cannot read property '0' of null )

*

直前の文字が 0回以上 繰り返す場合にマッチします。

【例】

'tttee'.match(/t*/)[0];
// "ttt"
'tttee'.match(/a*/)[0];
// ""

[]

角括弧に含まれるいずれかの1文字にマッチします。

角括弧内で「-」を用いることで範囲指定をすることもできます。

【例】

'test'.match(/[abcde]/)[0];
// "e"
'test'.match(/[a-e]/)[0];
// "e"
'13579'.match(/[2-4]/)[0];
// "3"
'090-0000-1111'.match(/0[789]0/)[0];
// "090"

?

「?」には大きく、以下の2つの役割があります。
1. 直前の文字は省略可能
1. 最短マッチにする。

【例】

  1. 直前の文字は省略可能。
'https'.match(/https?/)[0];
// "https"
'http'.match(/https?/)[0];
// "http"
  1. 最短マッチにする。
'test'.match(/.*t/)[0];
// "test" (*は最長マッチ)
'test'.match(/.*?t/)[0];
// "t" (*は最長マッチを最短マッチに変更する)

|

いずれかの条件 (OR条件)として使用可能。

【例】

'test'.match(/s|e/)[0];
// "e"
'sample'.match(/a|p|e/)[0];
// "a"

()

文字を1つのグループとして、まとめることができる。

【例】

'test'.match(/(et)|(te)/)[0];
// "te"
'oioioioiooi'.match(/(oi)+/)[0];
// "oioioioi"
'test_dayo'.match(/test_(dayo)?/)[0];
// "test_dayo"

\

直後の正規表現記号をエスケープする。

【例】

'test.com'.match(/\.com/)[0];
// ".com"
'test(.com)'.match(/\(\.com\)/)[0];
// "(.com)"

^

  1. 行の始まりにマッチする。
  2. [] 内で使用した場合、否定の意味になる。

【例】

  1. 行の始まりにマッチする。
'http://www.test.com'.match(/^http/)[0];
// "http"
'http://www.test.com'.match(/^www/)[0];
// エラー ( Cannot read property '0' of null )
  1. [] 内で使用した場合、否定の意味になる。
'123456789'.match(/[^0-8]/)[0];
// "9"
'http://www.test.com'.match(/[^w]+/)[0];
// "http://"

$

行の終わりにマッチする。

【例】

'http://www.test.com'.match(/com$/)[0];
// "com"
'http://www.test.com'.match(/com|jp)$/)[0];
// "com"

{}

  1. 直前の文字の桁数を指定可能。
  2. {n,} で桁数n以上の指定が可能
  3. {n,m} で桁数n以上m以下の指定が可能

【例】

'http://www.test.com'.match(/w{3}/)[0];
// "www"
'http://www.test.com'.match(/w{2,}/)[0];
// "www"
'http://www.test.com'.match(/w{2,4}/)[0];
// "www"

(?<=)

直前に特定の文字がある場合にマッチする。(肯定先読み)

【例】

'http://www.test.com'.match(/(?<=www).+/)[0];
// ".test.com"

(?=)

直後に特定の文字がある場合にマッチする。(肯定後読み)

【例】

'http://www.test.com'.match(/.+(?=com)/)[0];
// "http://www.test."
'http://www.test.com'.match(/(?<=www\.).+(?=\.com)/)[0];
// "test"

エスケープシーケンスを使ったメタ文字(使用頻度が高いもののみ)

\d

すべての数字。[0-9] と同等の意味。

\D

すべての数字以外。[^0-9] と同等の意味。

\t

タブ。

\n

改行。LF(Line Feed:0x0A)

\s

垂直タブ以外のすべての空白文字。[\t\f\r\n]と同等の意味。

\S

垂直タブ以外のすべての空白文字以外。[^\t\f\r\n]と同等の意味。

[\s\S] で改行を考慮した任意の文字マッチが可能。

\w

アルファベット、アンダーバー、数字。[a-zA-Z_0-9]と同等の意味。

\W

アルファベット、アンダーバー、数字以外。[^a-zA-Z_0-9]と同等の意味。

オプション(使用頻度が高いもののみ)

g

最初の1個だけでなく、文字列の最後まで検索を繰り返す。

グローバルマッチとも呼ばれる。

【例】

'test'.match(/t/g);
// ["t", "t"]

i

大文字・小文字を区別しない。

【例】

'test'.match(/T/i);
// "t"

u

文字コードをUTF-8

m

改行を考慮可能。

【例】

`改行を
考慮できる
mオプション`.match(/オプション/m)[0];
// "オプション"

おまけ

$1,$2,$3,...

() で囲われた文字を参照用変数として、使用することができる。

置換でよく使用する。

【例】

'7月10日'.replace(/(\d{1,2})(\d{1,2})日/, '$1/$2');
// "7/10"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsby.jsのsourceNodesライフサイクルでresourceを参照する方法

Gatsby.jsでノードを作るライフサイクルのsourceNodesでresourceのデータを参照したい時に詰まったのでメモ。

経緯

あるresourceのデータに外部画像のURLがあり、それを gatsby-image で使うために、gatsby-source-filesystem の createRemoteFileNode で外部画像用のNodeを生やす必要がありました。

新たにNodeを生やすのでsourceNodesのライフサイクルで

  1. 内部のスキーマに対してGraphQLで対象のURLを取得
  2. そのURLに対してcreateRemoteFileNodeでノードを作成する

で良いのかなと思ったのですが、sourceNodesではまだschemaがないのでGraphQLのAPIを呼べませんでした。

ということで、sourceNodesライフサイクルでresourceを参照する方法です。

方法

getNode API で直接resouceのノードの情報を参照する。以上です。 schemaはないのですが、ノードはあるのでアクセスできるのですね。

const node = getNode("参照したいノードのID")

実際のコードはこちらです。
sourceNodesでfacebookのfeedのresourceから画像情報を取得してcreateRemoteFileNodeから外部画像用のノードを作成する例です

gatsby-node.js
"use strict";
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);

exports.sourceNodes = async ({
  actions: { createNode, createNodeField },
  createNodeId,
  cache,
  store,
  getNode
}) => {
  // クエリは投げられないでNodeから直接取得。
  // 画像がないfeedもあるのでfilterで除外
  const feedData = getNode("ノードID").feed.data.filter(
    u => u.full_picture != null
  );

  await Promise.all(
    feedData.map(async data => {
      // 外部画像のダウンロードとノードの構築
      const fileNode = await createRemoteFileNode({
        url: data.full_picture,
        cache,
        store,
        createNode,
        createNodeId
      });

      await createNodeField({
        node: fileNode,
        name: "feedImage",
        value: "true"
      });

      await createNodeField({
        node: fileNode,
        name: "feedId",
        value: data.id
      });

      return fileNode;
    })
  );
};

参考

以下参考にさせて頂きました。良記事ありがとうございます。

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

Web開発の基本知識(メモ用)

背景

フロントサイドのソースを見たら、なんじゃこれ?のこれをメモする目的

jQuery

  • $から始まる
  • ready(function(){})関数

内側の関数は:HTMLの読み込みが終わった後に、実行される。

$(document).ready(function() {
  ここに処理記述
});

readyが省略可能

上記の例の省略形
$(function() {
  ここに処理記述
});
  • 各種セレクター

1.要素セレクター:$("li").css("color", "blue");

要素セレクター
<ul>
  <li>要素</li>
</ul>

2.IDセレクター:$("#myID").css("color", "blue");

IDセレクター
<ul>
  <li id="myID">IDセレクターの場合、"#"を使う</li>
</ul>

3.クラスセレクター:$(".myClass").css("color", "blue");

クラスセレクター
<ul>
  <li class="myClass">クラスセレクターの場合、"."を使う</li>
</ul>

4.子孫セレクター:$(".myClass strong").css("color", "blue");

クラスセレクター
<ul>
  <li class="myClass"><strong>子孫セレクター</strong>の場合、"space"を使う</li>
</ul>

5.ユニバーサルセレクター:$(".li *").css("color", "blue");

ユニバーサルセレクター
<ul>
  <li><strong>ユニバーサルセレクター</strong>の場合、"*"を使う</li>
  <li><span>ユニバーサルセレクター</span>の場合、"*"を使う</li>
</ul>

6.グループセレクター:$("#myId1", #myId3).css("color", "blue");

グループセレクター
<ul>
  <li id="myId1">IDセレクターの場合、","を使う</li>
  <li id="myId2">IDセレクターの場合、","を使う</li>
  <li id="myId3">IDセレクターの場合、","を使う</li>
</ul>

まとめ

セレクター サンプル 備考
要素 $("li").css("color", "blue");
ID $("#myID").css("color", "blue"); "#"を使う
クラス $(".myClass").css("color", "blue"); "."を使う
子孫 $(".myClass strong").css("color", "blue"); spaceを使う
ユニバーサル $(".li *").css("color", "blue"); "*"を使う
グループ $("#myId1", #myId3).css("color", "blue"); ","を使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ジェネリクスをもう少し使いこなす。

4歳娘「パパ、具体的な名前をつけないで?」

↑こちらの記事の続きです。

前回のあらすじ

前回の記事は、

  • 引数が数値であれば、数値の配列を返すべし
  • 引数が文字列であれば、文字列の配列を返すべし
  • 引数がオブジェクトであれば、オブジェクトの配列を返すべし
  • 引数が配列であれば、配列の配列(二次元配列)を返すべし

上記のような制限を持った関数を、
ジェネリクスを使って表現しよう、という内容でした。

コードも軽く見返してみましょう。

function toArray<T>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

できあがった関数は上記のようなものでした。

  • 引数がTだったら、戻り値は[T, T, T]であるべし

つまり、

  • 引数がnumberだったら、戻り値は[number, number, number]であるべし

このような制限を、ジェネリクスで表現していましたね。

いただいたコメント

前回の記事を読んだあるユーザーさんから、
こんなコメントをいただきました。

娘の友達「文字列と数値とオブジェクトと数値の配列を受け取れる関数
ってお願いしたのに、真偽値も浮動小数点数もnullも受け取れるじゃない。
それどころか何でも受け取れるわ。これでは使えない。困ったわ」

私は即答しました。

ガキが・・・
舐めてると潰すぞ

もちろん、潰すというのはは冗談です。
少しも潰しません。

もっと制限したい

  • 引数と同じ型の値からなる配列を返すべし

上の条件だけでは、確かに───

  • 引数として、文字列と数値とオブジェクトと数値の配列のみを受け取れる関数

───という条件を満たしていません。

  • 引数と同じ型の値の配列を返すべし
  • 受け取ることができる引数は、
    文字列・数値・オブジェクト・数値の配列のみとすべし

この2つの制限を両方とも型で保証するために、extendsを使ってみましょう。

まず、文字列と数値とオブジェクトと数値の配列を許容する型エイリアスを定義します。

type Hage = string | number | object | number[]

これは、

Hage型は、
文字列または数値またはオブジェクトまたは数値の配列ですよ

という意味です。

そして次は、

Tという何らかの型は、Hage型を継承したものであるべし

ということを表現していきましょう。

function toArray<T extends Hage>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

このように書きます。
<T extends Hage>の部分が、

  • THage型を継承すべし

ということを表しています。

これで、関数toArrayは───

  • 引数と同じ型の値の配列を返すべし

上記の条件に加え、

  • 受け取る引数は、
    文字列・数値・オブジェクト・数値の配列のどれかにすべし

───という制限も持つことができました。

試しに真偽値を渡してみます。

const trueArray = toArray(true);

すると、VSCodeに───

trueの引数を、型Hageのパラメーターに割り当てることはできません。

───こんなエラーメッセージが表示されました。
上手くいっているようです。

ちなみにHageという型エイリアスを定義しなくとも、
<T extends string | number | object | number[]>と書くこともできます。

もっと細かい制限もできる

例えばこんな関数があるとします。

function getFullName(user: User): string {
  return user.firstName + ' ' + user.familyName;
}

ユーザ情報を引数にとって、フルネームを返す関数です。

このgetFullName関数は───

  • Userを受け取ってstringを返すべし

───といった型付けになっています。

Userは以下のようなinterfaceです。

interface User {
  firstName: string
  familyName: string
  tel: number
}

これは───

  • ユーザは、姓と名と電話番号を持つべし

───といった意味になります。

getFullName関数は、引数としてUserしか受け取れないため、
間違って他の型の値を渡してしまって、
firstNamefamilyNameundefinedになってしまう、といった事故を防げます。

しかし、ユーザだけでなくライターさんも登場

ここでUser意外に、Writerという
姓と名を持つ概念が登場したとします。

Writerは以下のようなinterfaceです。

interface Writer {
  firstName: string
  familyName: string
  articles: article[]
}
  • ライターは、姓と名と記事(配列)を持っている

上記のようなイメージです。
持っているプロパティが微妙に違います。

  • でも、WriterにもgetFullName関数を使いたい
  • 姓と名があるのは同じだからね

という状況だとします。

このような場合に、

interface HasFullName {
  firstName: string
  familyName: string
}

上記のようなHasFullNameというinterfaceを定義して、
ジェネリクスと共に使用することができます。

HasFullNameは、
getFullName関数に必要なプロパティ、
つまり、フルネームを作るのに必要なプロパティだけを定義したinterfaceですね。

  • 姓と名を持つべし

といったイメージです。

getFullName関数の型付けは以下のようにします。

function getFullName<T extends HasFullName>(person: T): string {
  return person.firstName + ' ' + person.familyName;
}
  • 引数TはHasFullNameを継承していないといけないよ

つまり、

  • 引数には、firstNamefamilyNameを持ってるやつを
    渡さないといけないよ

という制限を表現できます。

ジェネリクスを使わない書き方だと、

type Person = User | Writer

上記のように、UserWriterを許容する
Personという型エイリアスを定義して、以下のように書くこともできます。

function getFullName(person: Person): string {
  return person.firstName + ' ' + person.familyName;
}
  • 引数はPerson型じゃないといけないよ

つまり、

  • 引数には、UserWriterを渡さないといけないよ

というイメージですね。

ですが、getFullName関数の型付けに使用するならば、
HasFullNameを使って<T extends HasFullName>と書く方が

  • 姓と名が要るんやで!

という感じを表現できていて、ワイのようなザコーダーにとっては
脳死で読めて楽かもしれません。

getFullNameの引数としては、
人間というより姓と名を持ったやーつが必要ですもんね。

ワイ君の思考プロセス例
ワイ「あー、フルネームの文字列を作る関数やから」
ワイ「firstNamefamilyNameを持ったやつを引数に渡さなアカンのね」

まとめ

このように、ジェネリクスを使用すると

  • 引数と返り値は同じ型であるべし

だけでなく、

  • 姓と名を持った何らかの型を受け取るべし

といったことも表現できるようになります。

大きめの案件で、様々なオブジェクトを扱うような場面が増えると
本当に訳が分からなくなってくることがあります。

そんな時に、型やジェネリクスがあると、例えば

  • 姓と名を持った何らかの型を受け取るべし

とか、
色々と取り締まってくれるので、undefinedエラー等が発生しにくく、
開発が捗ります。

ブラウザ上で実行する前に、エディタ上で

「君、違うことしてるで!」

って注意してもらえる訳ですもんね。

面倒くさいようで、ないと逆に困るものだと思います。

〜おしまい〜

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

ジェネリクスをもう少しだけ使いこなす。

4歳娘「パパ、具体的な名前をつけないで?」

↑こちらの記事の続きです。

前回のあらすじ

前回の記事は、

  • 引数が数値であれば、数値の配列を返すべし
  • 引数が文字列であれば、文字列の配列を返すべし
  • 引数がオブジェクトであれば、オブジェクトの配列を返すべし
  • 引数が配列であれば、配列の配列(二次元配列)を返すべし

上記のような制限を持った関数を、
ジェネリクスを使って表現しよう、という内容でした。

コードも軽く見返してみましょう。

function toArray<T>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

できあがった関数は上記のようなものでした。

  • 引数がTだったら、戻り値は[T, T, T]であるべし

つまり、

  • 引数がnumberだったら、戻り値は[number, number, number]であるべし

このような制限を、ジェネリクスで表現していましたね。

いただいたコメント

前回の記事を読んだあるユーザーさんから、
こんなコメントをいただきました。

娘の友達「文字列と数値とオブジェクトと数値の配列を受け取れる関数
ってお願いしたのに、真偽値も浮動小数点数もnullも受け取れるじゃない。
それどころか何でも受け取れるわ。これでは使えない。困ったわ」

私は以下のように回答しました。

ガキが・・・
舐めてると潰すぞ

もちろん、潰すというのは冗談です。
少しも潰しません。

もっと制限したい

  • 引数と同じ型の値からなる配列を返すべし

上の条件だけでは、確かに───

  • 受け取ることができる引数は、
    文字列または数値またはオブジェクトまたは数値の配列のみとすべし

───という条件を満たしていません。

  • 引数と同じ型の値の配列を返すべし
  • 受け取ることができる引数は、
    文字列または数値またはオブジェクトまたは数値の配列のみとすべし

この2つの制限を両方とも型で保証するために、extendsを使ってみましょう。

まず、文字列と数値とオブジェクトと数値の配列を許容する型エイリアスを定義します。

type Hage = string | number | object | number[]

これは、

Hage型は、
文字列または数値またはオブジェクトまたは数値の配列ですよ

という意味です。

そして次は、

Tという何らかの型は、Hage型を継承したものであるべし

ということを表現していきましょう。

function toArray<T extends Hage>(arg: T): [T, T, T] {
  return [arg, arg, arg];
}

このように書きます。
<T extends Hage>の部分が、

  • THage型を継承すべし

ということを表しています。

これで、関数toArrayは───

  • 引数と同じ型の値の配列を返すべし

上記の条件に加え、

  • 受け取る引数は、
    文字列・数値・オブジェクト・数値の配列のどれかにすべし

───という制限も持つことができました。

試しに真偽値を渡してみます。

const trueArray = toArray(true);

すると、VSCodeに───

trueの引数を、型Hageのパラメーターに割り当てることはできません。

───こんなエラーメッセージが表示されました。
上手くいっているようです。

ちなみにHageという型エイリアスを定義しなくとも、
<T extends string | number | object | number[]>と書くこともできます。

もっと細かい制限もできる

例えばこんな関数があるとします。

function getFullNamePersonList(persons: User[]): User[] {
  const fullNamePersonList =
    persons.filter(person => person.firstName || person.familyName);
  return fullNamePersonList;
}

ユーザ情報が入った配列の中から、
姓と名を両方持つユーザだけを抽出する関数です。
姓か名を空文字で登録しているユーザは取り除かれます。

このgetFullNamePersonList関数は───

  • Userの配列を受け取って、Userの配列を返すべし

───といった型付けになっています。

Userは以下のようなinterfaceです。

interface User {
  firstName: string
  familyName: string
  tel: number
}

これは───

  • ユーザは、姓と名と電話番号を持つべし

───といった意味になります。

getFullNamePersonList関数は、引数としてUser[]しか受け取れないため、
間違って他の型の値を渡してしまって、
関数が正しく実行できずにエラーになってしまう、といった事故を防げます。

しかし、ユーザだけでなくライターさんも登場

ここでUser以外に、Writerという
姓と名を持つ概念が登場したとします。

Writerは以下のようなinterfaceです。

interface Writer {
  firstName: string
  familyName: string
  articles: article[]
}
  • ライターは、姓と名と記事(配列)を持っている

上記のようなイメージです。
持っているプロパティがUserと微妙に違います。

  • でも、Writerの配列にもgetFullNamePersonList関数を使いたい
  • 姓と名があるのは同じだからね

という状況だとします。

このような場合に、

interface HasFullName {
  firstName: string
  familyName: string
}

上記のようなHasFullNameというinterfaceを定義して、
ジェネリクスを書く際に使用することができます。

HasFullNameは、
getFullNamePersonList関数に必要なプロパティ、
つまり、フルネームを作るのに必要なプロパティだけを定義したinterfaceですね。

  • 姓と名を持つべし

といったイメージです。

getFullNamePersonList関数の型付けは以下のようにします。

function getFullNamePersonList<T extends HasFullName>(persons: T[]): T[] {
  const fullNamePersonList =
    persons.filter(person => person.firstName || person.familyName);
  return fullNamePersonList;
}
  • 引数は、HasFullNameを継承したT型の値からなる配列でなくてはならないよ

つまり、

  • 引数には、firstNamefamilyNameを持ってるやつ(の配列)を渡さないといけないよ
  • 戻り値も、firstNamefamilyNameを持ってるやつ(の配列)を返さないといけないよ

という制限を表現できます。

関数を呼び出す時は、以下のようなコードになります。

const fullNameUserList = getFullNamePersonList<User>(users);
const fullNameWriterList = getFullNamePersonList<Writer>(writers);

なお、関数呼び出し時に<User><Writer>を省略したとしても、
コンパイラによる型推論が働くため、正しく実行可能です。

HasFullNameインターフェイスを定義しなくてもいい

HasFullNameインターフェイスを使わない書き方だと、

type Person = User | Writer

上記のように、UserWriterを許容する
Personという型エイリアスを定義して、以下のように書くこともできます。

function getFullNamePersonList<T extends Person>(persons: T[]): T[] {
  const fullNamePersonList = persons.filter(person => person.firstName || person.familyName);
  return fullNamePersonList;
}
  • 引数も戻り値もPerson[]型じゃないといけないよ

つまり、

  • 引数には、User[]Writer[]を渡さないといけないよ
  • 引数と同じ型の戻り値を返さないといけないよ

というイメージですね。

HasFullNameを定義したほうが、コードの説明書代わりとして良いかも

型エイリアスPersonを使用しても良いのですが、
getFullNamePersonList関数の型付けに使用するジェネリクスとしては、
HasFullNameを定義して<T extends HasFullName>と書く方が

  • 姓と名が要るんやで!

という感じを表現できていて、ワイのようなザコーダーにとっては
脳死で読めて楽かもしれません。

getFullNamePersonListの引数としては、
人間というより姓と名を持ったやーつが必要ですもんね。

interface HasFullName {
  firstName: string
  familyName: string
}

コード内に上記のHasFullNameインターフェースが書いてあると、

ワイ「あー、フルネームを持ってる人を選別する関数やから」
ワイ「firstNamefamilyNameを持ってることが要点なんやね」

上記のようなドキュメント的な働きをしてくれるかもしれません。

ジェネリクスで色々なルールを表現できた

このように、ジェネリクスを使用すると

  • 引数と返り値は同じ型であるべし

だけでなく、さらに

  • 姓と名を持った何らかの型を受け取るべし

といったことも表現できるようになります。

何らかの型、という自由な感じを持ちつつも、

  • firstNamefamilyNameは持っていること!

といった具体的な制限を加えることができます。

まとめ

大きめの案件で、様々なオブジェクトを扱う場面が増えると
本当に訳が分からなくなってくることがあります。

そんな時に、型やジェネリクスがあると、例えば

  • 姓と名を持った何らかの型を受け取って、返すべし

とか、
色々と取り締まってくれるので、undefinedエラー等が発生しにくく、
開発が捗ります。

ブラウザ上で実行する前に、エディタ上で

「君、違うことしてるで!」

って注意してもらえる訳ですもんね。

面倒くさいようで、ないと逆に困るものだと思います。

〜おしまい〜

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

Reactのチュートリアルの三目並べを素のJavaScriptに

前回のjQueryで作ったReactチュートリアルを素のJavaScriptにしてみます。すべてのブラウザで動くかは試してません。Edge(chromium)で確認しました。

マス目に数値を表示する

index.html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="css/index.css" />
    <script src="js/index.js"></script>
  </head>
  <body>
    <div id="root">
      <div class="game">
        <div class="gmae-board">
          <div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
            <div class="board-row">
              <button class="square"></button><button class="square"></button
              ><button class="square"></button>
            </div>
          </div>
        </div>
        <div class="game-info">
          <div>次の手番: X</div>
          <div>
            <li><button>Go to game start</button></li>
            <!-- <li><button>Go to move #1</button></li> -->
          </div>
        </div>
      </div>
    </div>
  </body>
</html>
index.css
body {
  font: 14px 'Century Gothic', Futura, sans-serif;
  margin: 20px;
}

ol,
ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}
index.js
(function() {
  // DOM読み込み後
  window.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll(".square").forEach((element, index) => {
      element.textContent = index;
    });
  });
})();

image.png

querySelectorAllがなかった時代は、getElementsByClassNameで取得してforとかでぐるぐる回していたんでしょうね。

XとOを入力できるようにする

index.js
(function() {
  // DOM読み込み後
  window.addEventListener("DOMContentLoaded", () => {
    let xIsNext = true;
    const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい
    document.querySelectorAll(".square").forEach((element, index) => {
      // クリックイベント
      element.addEventListener("click", squareClick);
    });

    function squareClick(event) {
      event.target.textContent = xIsNext ? "X" : "O";
      status.textContent = "次の手番: " + (xIsNext ? "O" : "X");
      xIsNext = !xIsNext;
    }
  });
})();

image.png

循環参照が起きると思ってクリック処理を分けてみました!
MDNのメモリ管理には、「もはや問題ではありません」と書かれているけど、どうなんだろ・・・。

履歴なしの完成までもっていく

index.js
(function() {
  // DOM読み込み後
  window.addEventListener("DOMContentLoaded", () => {
    const squares = new Array(9).fill(null);
    let xIsNext = true;
    const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい
    document.querySelectorAll(".square").forEach((element, index) => {
      // クリックイベント
      element.addEventListener("click", squareClick.bind(null, index));
    });

    function squareClick(index, event) {
      if (calculateWinner(squares) || squares[index]) {
        return;
      }

      squares[index] = xIsNext ? "X" : "O";
      event.target.textContent = squares[index];

      // 勝利判定
      if (calculateWinner(squares)) {
        status.textContent = "勝者: " + squares[index];
      } else {
        status.textContent = "次の手番: " + (xIsNext ? "O" : "X");
      }

      xIsNext = !xIsNext;
    }

    // 勝敗判定関数(公式チュートリアルから拝借)
    function calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
      ];
      for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    }
  });
})();

image.png

もはや循環参照とかわからんとです。
前回は「勝者:〇〇」って実装してなかったので、今回しました。

履歴機能を持たせる

cssとhtmlは最初と一緒です。

index.js
(function() {
  // DOM読み込み後
  window.addEventListener("DOMContentLoaded", () => {
    const history = [
      { squares: new Array(9).fill(null), nextStatus: "次の手番: X" } // 履歴
    ];
    let stepNumber = 0; // 現在表示している履歴のインデックス
    const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい

    // Go to game start ボタン
    document
      .querySelector(".game-info li > button")
      .addEventListener("click", historyButtonClick.bind(null, 0));

    // 全square
    document.querySelectorAll(".square").forEach((element, index) => {
      // クリックイベント
      element.addEventListener("click", squareClick.bind(null, index));
    });

    function squareClick(index, event) {
      // 現在のhistory
      const current = history[stepNumber];
      const squares = current.squares.concat(); // コピー

      if (calculateWinner(squares) || squares[index]) {
        return;
      }

      squares[index] = stepNumber % 2 === 0 ? "X" : "O";
      event.target.textContent = squares[index];

      // 勝利判定
      if (calculateWinner(squares)) {
        status.textContent = "勝者: " + squares[index];
      } else {
        status.textContent = "次の手番: " + (stepNumber % 2 === 0 ? "O" : "X");
      }

      // 現在のstemNumberより後ろの履歴と履歴ボタン削除
      const lis = document.querySelectorAll(".game-info li");
      const removeCount = history.length - (stepNumber + 1);
      for (let i = 1; i <= removeCount; i++) {
        lis[stepNumber + i].parentNode.removeChild(lis[stepNumber + i]);
        history.pop();
      }

      // 新しい要素追加
      history.push({ squares: squares, nextStatus: status.textContent });
      stepNumber++;

      createHistoryButton(stepNumber);
    }

    function createHistoryButton(index) {
      // 履歴ボタン
      const button = document.createElement("button");
      button.textContent = "Go to move #" + index;
      button.addEventListener("click", historyButtonClick.bind(null, index)); // クリックイベント

      // 履歴ボタンの親
      const li = document.createElement("li");
      li.appendChild(button); // 履歴ボタンを追加

      // liの親に追加
      document.querySelector(".game-info").appendChild(li);
    }

    function historyButtonClick(index) {
      stepNumber = index;

      // 手番
      status.textContent = history[stepNumber].nextStatus;

      // マス目を全て上書く
      const domSquares = document.querySelectorAll(".square");
      history[stepNumber].squares.forEach((value, index) => {
        domSquares[index].textContent = value;
      });
    }

    // 勝敗判定関数(公式チュートリアルから拝借)
    function calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
      ];
      for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (
          squares[a] &&
          squares[a] === squares[b] &&
          squares[a] === squares[c]
        ) {
          return squares[a];
        }
      }
      return null;
    }
  });
})();

image.png
手番はマス目の配列と一緒に保持するようにしました。
こっちの方が楽でした。

感想

jQueryの時のソースと、バグりまくった経験があったからか、割と早く作れた。むしろこっちの方が内容に無駄がないかも・・・?

でも相変わらず見にくい。

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