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

意識の高いサイトに対して意識の低いスクレイピングをする

意識低いけどスクレイピングがしたい!

久しぶりにやる気がでてきて、ちょっと情報サイトでも作って遊ぼうと思ってるんですね

ほんでデータ集めるためにスクレイピングしよっかなって

意識高いサイトはスクレイピング対策してて困る!

意識が低いので基本 file_get_contents とか curl とかで雑にやっちゃうマンなわけですが
なんかヘッダいじったりしてもうまいこといかないんですよ

意識高いことはしたくない!

しょうがないので別の方法でやるかーってなったんですけど
seleniumみたいな大がかりなことしたくないわけ

意識低民の味方 tampermonkey

そこで chrome版 greasemonkey こと tampermonkey の出番だぜ!(意識が低いので今日知った)

くわしくはこのへんみてね
https://www.lisz-works.com/entry/chrome-tampermonkey

あとダウンロードの自動化については以下を参考にしました ありがとうございます
https://tks-kan.com/2016/08/25/154/

// ==UserScript==
// @name         download
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://動かしたいサイト.com/*
// @grant        none
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==

(function() {
    'use strict';

    // どん詰まりになったらとめる
    if ($('.hosiikontentu').length == 0) {
        return false;
    }

    var sspage = new XMLSerializer().serializeToString(document);
    var blob = new Blob([sspage], { "type" : "text/html" });
    var url = window.URL;
    var bloburl = url.createObjectURL(blob);

    // わにゃわにゃしてダウンロードファイル名をつくる
    var filename = hogefuga();

    // 新たにaタグを作成してダウンロード
    var a = document.createElement('a');
    a.download = filename;
    a.href = bloburl;
    a.click();

    // サーバにやさしく
    setTimeout(next, 5000);

})();

function next () {
    // なんだか次のURLを作る
    var newUrl = higehage();
    window.location = newUrl;
}

ちなみに chrome は最近はダウンロード時にダウンロード場所を選ぶダイアログが出るのがデフォなんで設定でオフにできるよ!

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

NodeCGで配信を華やかに

Splatoon2の大会で運営としてOBSを使って運営放送(配信)をする際に放送をかっこよくするために何か良いものはないかと探しているうちにNodeCGというものを見つけました。

NodeCGで何ができる?

NodeCGを使うとOBS組み込みブラウザ(ブラウザソース)に用意したHTMLを表示させて、それを外部から操作することができます。
文字じゃよくわからないと思いますが使ってみるとおおすげぇってなりますたぶん
自分の場合は以前ブラウザに表示させてそれをキャプチャして表示させていたんですが、書き換えている間は違うシーンに変えなきゃいけないし、誤ってウィンドウ閉じたりすると余計なものが映り込んだりしてしまっていたので、その方法もまた別に良いやり方があるのかもしれないが書き換えが必要なものをブラウザソースで表示させることができるのは凄い便利でした。

導入方法

についてはcma2819氏Hoishin氏が詳しく説明してくださっているのでそちらを参考にしてみてください。

実際につかってみる

NodeCGのbundlesフォルダの中にフォルダを作ります。以後ここで作ったフォルダ名をexampleとして進めていきます。
その中に

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

  "nodecg": {
    "compatibleRange": "^1.0.0"
  }
}

を置きます(NodeCGを動かす際に必要です)。
加えて、exampleフォルダの中にdashboardとgraphicsというフォルダを作っておいてください。

コメント表示

まずはdashboardフォルダの中に

test.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <input id="comment" type="text">
    <button id="submitButton">反映</button>
</body>
<script>
    const com = document.getElementById("comment");
    const testRep = nodecg.Replicant("test");
    submitButton.onclick = () => {
        testRep.value = com.value;
    }
</script>
</html>

次にgraphicsフォルダの中に

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
  <div id="simple" style="font-size: 100px; color: white"></div>
</body>

<script>
  const simple = document.getElementById("simple");
  const testRep = nodecg.Replicant("test");
  testRep.on("change", newValue => {
    simple.innerText = newValue;
  });
</script>
</html>

を作成します。

できたら先程exampleフォルダに置いたpackage.jsonに書き加えます。

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

  "nodecg": {
    "compatibleRange": "^1.0.0",
    "dashboardPanels": [
      {
        "width": "1",
        "name": "test",
        "title": "コメント",
        "file": "test.html"
      }
    ],
    "graphics": [
      {
        "file": "index.html",
        "width": 800,
        "height": 600
      }
    ]
  }
}

これで完成です。動かしてみましょう。

コマンドプロンプトを開いてNodeCGのディレクトリ(bundlesフォルダのが入っているディレクトリ)に移動して実行します。

cd ここにNodeCGのディレクトリのアドレス
node .

しばらくすると

NodeCG running on http://localhost:9090

みたいに表示されるのでそのURL(この場合はhttp://localhost:9090)をブラウザで開きます。
すると
dashboard
何かかっこいいのが表示されました。これがダッシュボードで、ここで操作します。
このままだと表示するやつがないので右上のGRAPHICSと書いてある目のマークをクリックしてください。
すると
image.png
こんな感じの画面が出てくるのでCOPY URLをクリックし、OBSを起動してブラウザソースを追加してください。
obs
追加した状態だと値が入っていないので何も表示されていません。
ひとまず先程開いたダッシュボードのWORKSPACEをクリックして操作画面に戻ります。
コメントと書いてある所に適当な文字を入力して反映ボタンをクリックしてみてください。
result
変わった!!!!!
と、こんな感じで動かせます。

もっといろいろやってみましょう。

試合経過表示

HTMLやCSS部分はあんまりわかってないので参考にせずに、どんな感じに動かすかを見ていただければ…
まずはdashboardフォルダの中に

panel.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
  <style>
    * {
    font-family: "コーポレート・ロゴ(ラウンド)";
        font-size: 20px;
  }
    .team, .member {
        width: 370px;
    }
    .content {
        margin-bottom: 10px
    }
  </style>
</head>
<body>
    <div id="folder">
    <div class="content">
            <label>チームA</label>
            <input id="team1" class="team" type="text" placeholder="チーム名" onchange="teamSet()">
            <textarea id="member1" class="member" placeholder="メンバー"></textarea>
    </div>
    <div class="content">
            <label>チームB</label>
            <input id="team2" class="team" type="text" placeholder="チーム名" onchange="teamSet()">
          <textarea id="member2" class="member" placeholder="メンバー"></textarea>
    </div>
  </div>
    <label>ルール&ステージ</label>
    <div id="folder">
    <div class="content">
            <select id="result1">
                <option selected>-</option>
          </select><br>
            <select id="rule1">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage1">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result2">
                <option selected>-</option>
          </select><br>
            <select id="rule2">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage2">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result3">
                <option selected>-</option>
          </select><br>
            <select id="rule3">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage3">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result4">
                <option selected>-</option>
          </select><br>
            <select id="rule4">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage4">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
        <div class="content">
            <select id="result5">
                <option selected>-</option>
          </select><br>
            <select id="rule5">
            <option selected>? ? ?</option>
            <option>ナワバリ</option>
            <option>ガチエリア</option>
            <option>ガチヤグラ</option>
            <option>ガチホコ</option>
            <option>ガチアサリ</option>
          </select>
            <select id="stage5">
                <option value="none" selected>? ? ?</option>
                <option value="0">バッテラストリート</option>
                <option value="1">フジツボスポーツクラブ</option>
                <option value="2">ガンガゼ野外音楽堂</option>
                <option value="3">チョウザメ造船</option>
                <option value="4">海女美術大学</option>
                <option value="5">コンブトラック</option>
                <option value="6">マンタマリア号</option>
                <option value="7">ホッケふ頭</option>
                <option value="8">タチウオパーキング</option>
                <option value="9">エンガワ河川敷</option>
                <option value="10">モズク農園</option>
                <option value="11">Bバスパーク</option>
                <option value="12">デボン海洋博物館</option>
                <option value="13">ザトウマーケット</option>
                <option value="14">ハコフグ倉庫</option>
                <option value="15">アロワナモール</option>
                <option value="16">モンガラキャンプ場</option>
                <option value="17">ショッツル鉱山</option>
                <option value="18">アジフライスタジアム</option>
                <option value="19">ホテルニューオートロ</option>
                <option value="20">スメーシーワールド</option>
                <option value="21">アンチョビットゲームズ</option>
                <option value="22">ムツゴ楼</option>
                <option value="x">DEAR SENPAI</option>
            </select>
    </div>
  </div>
    <button id="submitButton">反映</button>
</body>
<script>
    const team1 = document.getElementById("team1");
    const team2 = document.getElementById("team2");
    const member1 = document.getElementById("member1");
    const member2 = document.getElementById("member2");
    const result1 = document.getElementById("result1");
    const result2 = document.getElementById("result2");
    const result3 = document.getElementById("result3");
    const result4 = document.getElementById("result4");
    const result5 = document.getElementById("result5");
    const rule1 = document.getElementById("rule1");
    const rule2 = document.getElementById("rule2");
    const rule3 = document.getElementById("rule3");
    const rule4 = document.getElementById("rule4");
    const rule5 = document.getElementById("rule5");
    const stage1 = document.getElementById("stage1");
    const stage2 = document.getElementById("stage2");
    const stage3 = document.getElementById("stage3");
    const stage4 = document.getElementById("stage4");
    const stage5 = document.getElementById("stage5");
    const submitButton = document.getElementById("submitButton");

    function teamSet() {
        let teams = [document.getElementById("team1").value, document.getElementById("team2").value];
        teams.forEach((team, index) => {
            for (var i = 1; i <= 5; i++) {
                let team_select = document.getElementById("result" + i)
                team_select.options[index+1] = new Option(team, team);
            }
        });
    }

    const dataRep = nodecg.Replicant("data", {persistent: false});

    submitButton.onclick = () => {
        let winArray = [result1.value,result2.value,result3.value,result4.value,result5.value];
        let winA = 0;
        let winB = 0;
        for (var i = 0; i < winArray.length; i++) {
            if (winArray[i] === team1.value) winA++;
            if (winArray[i] === team2.value) winB++;
        }
        dataRep.value = {
            "counter1": winA,
            "counter2": winB,
            "team1": team1.value,
            "team2": team2.value,
            "member1": member1.value,
            "member2": member2.value,
            "result1": result1.value,
            "result2": result2.value,
            "result3": result3.value,
            "result4": result4.value,
            "result5": result5.value,
            "rule1": rule1.value,
            "rule2": rule2.value,
            "rule3": rule3.value,
            "rule4": rule4.value,
            "rule5": rule5.value,
            "stage1": stage1.value,
            "stage2": stage2.value,
            "stage3": stage3.value,
            "stage4": stage4.value,
            "stage5": stage5.value
        };
    }
</script>
</html>

こんな感じで複数の値を送ることもできちゃいます。

次にgraphicsフォルダの中に

3games.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
  * {
    font-family: "コーポレート・ロゴ(ラウンド)";
  }
  p {
    text-align: center;
    font-family: "Splatoon2";
  }
  .counter {
    text-align: center;
    font-family: "Splatoon2";
    font-size: 120px;
  }
  .team {
    text-align: center;
    font-size: 60px;
  }
  .member {
    overflow: hidden;
    height: 3.0em;
    line-height: 1.5;
    text-align: center;
    font-size: 40px;
  }
  .result, .rule, .stage {
    font-size: 40px;
  }
  #teaminfo {
    width: 100%;
    margin: 3% 0 0;
  }
  #teamprof {
    width: 50%;
  }
  #folder {
    width: 80%;
    margin: 30px auto;
    display: -webkit-box;
    -webkit-box-pack: justify;
  }
  .content {
    width: 31%;
    height: auto;
    margin: 0 0 0 0;
  }
  .stage {
    width: 100%;
    height: auto;
  }
  .image {
    position: relative;
  }
  .image p {
    position: absolute;
    top: 0;
    left: 0;
    margin: 0;
    color: white;
    background: rgba(0, 0, 0, 0.6);;
    font-size: 30px;
    line-height: 1;
    padding: 5px 20px;
  }
  </style>
</head>
<body bgcolor="2C2F33" text="#FFFFFF" link="#00AEEF" vlink="#7289DA" alink="#FC0000">
  <center>
    <div id="teaminfo" style="display:inline-flex">
      <div id="teamprof">
        <div id="counter1" class="counter">0</div>
        <div id="team1" class="team">チーム名</div>
        <div id="member1" class="member">メンバー</div>
      </div>
      <p style="font-size: 100px">VS</p>
      <div id="teamprof">
        <div id="counter2" class="counter">0</div>
        <div id="team2" class="team">チーム名</div>
        <div id="member2" class="member">メンバー</div>
      </div>
    </div>
  </center>
  <div id="folder">
    <div class="content">
      <div id="result1" class="result">-</div>
      <div class="image">
        <img id="image1" src="./image/stage/none.png" class="stage">
        <p>1</p>
      </div>
      <div id="rule1" class="result">? ? ?</div>
      <div id="stage1" class="result">? ? ?</div>
    </div>
    <div class="content">
      <div id="result2" class="result">-</div>
      <div class="image">
        <img id="image2" src="./image/stage/none.png" class="stage">
        <p>2</p>
      </div>
      <div id="rule2" class="result">? ? ?</div>
      <div id="stage2" class="result">? ? ?</div>
    </div>
    <div class="content">
      <div id="result3" class="result">-</div>
      <div class="image">
        <img id="image3" src="./image/stage/none.png" class="stage">
        <p>3</p>
      </div>
      <div id="rule3" class="result">? ? ?</div>
      <div id="stage3" class="result">? ? ?</div>
    </div>
  </div>
</body>

<script>
  const counter1 = document.getElementById("counter1");
  const counter2 = document.getElementById("counter2");
  const team1 = document.getElementById("team1");
  const team2 = document.getElementById("team2");
  const member1 = document.getElementById("member1");
  const member2 = document.getElementById("member2");
  const result1 = document.getElementById("result1");
  const result2 = document.getElementById("result2");
  const result3 = document.getElementById("result3");
  const image1 = document.getElementById("image1");
  const image2 = document.getElementById("image2");
  const image3 = document.getElementById("image3");
  const rule1 = document.getElementById("rule1");
  const rule2 = document.getElementById("rule2");
  const rule3 = document.getElementById("rule3");
  const stage1 = document.getElementById("stage1");
  const stage2 = document.getElementById("stage2");
  const stage3 = document.getElementById("stage3");

  const dataRep = nodecg.Replicant("data", {persistent: false});

  const stageList = {"none":"? ? ?","0":"バッテラストリート","1":"フジツボスポーツクラブ","2":"ガンガゼ野外音楽堂","3":"チョウザメ造船","4":"海女美術大学","5":"コンブトラック","6":"マンタマリア号","7":"ホッケふ頭","8":"タチウオパーキング","9":"エンガワ河川敷","10":"モズク農園","11":"Bバスパーク","12":"デボン海洋博物館","13":"ザトウマーケット","14":"ハコフグ倉庫","15":"アロワナモール","16":"モンガラキャンプ場","17":"ショッツル鉱山","18":"アジフライスタジアム","19":"ホテルニューオートロ","20":"スメーシーワールド","21":"アンチョビットゲームズ","22":"ムツゴ楼","x":"DEAR SENPAI"};

  dataRep.on("change", newValue => {
    counter1.innerText = newValue.counter1;
    counter2.innerText = newValue.counter2;
    team1.innerText = newValue.team1;
    team2.innerText = newValue.team2;
    member1.innerText = newValue.member1;
    member2.innerText = newValue.member2;
    result1.innerText = newValue.result1;
    result2.innerText = newValue.result2;
    result3.innerText = newValue.result3;
    image1.src = "./image/stage/" + newValue.stage1 + ".png";
    image2.src = "./image/stage/" + newValue.stage2 + ".png";
    image3.src = "./image/stage/" + newValue.stage3 + ".png";
    rule1.innerText = newValue.rule1;
    rule2.innerText = newValue.rule2;
    rule3.innerText = newValue.rule3;
    stage1.innerText = stageList[newValue.stage1];
    stage2.innerText = stageList[newValue.stage2];
    stage3.innerText = stageList[newValue.stage3];
  });
</script>
</html>

みたいに作っていきます。
初期状態が映っても良いように適当な値を既に入れています。
また、persistent: falseにすると前回分のデータが残らなくなるみたいです。
画像とかはまあ適当に用意してください。
上でも述べましたが、綺麗なコードではないので動かし方だけ参考にしていただければ…

先程と同様にpackage.jsonに書き加えていきます。
ここには書きませんが3games.htmlと同じように5games.htmlも作りました。

package.json
{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",

  "nodecg": {
    "compatibleRange": "^1.0.0",
    "dashboardPanels": [
      {
        "width": "3",
        "name": "panel",
        "title": "試合経過",
        "file": "panel.html"
      },
      {
        "width": "1",
        "name": "test",
        "title": "コメント",
        "file": "test.html"
      }
    ],
    "graphics": [
      {
        "file": "3games.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "5games.html",
        "width": 1920,
        "height": 1080
      },
      {
        "file": "index.html",
        "width": 800,
        "height": 600
      }
    ]
  }
}

これでもう一度実行します。
すると
image.png
image.png
項目が増えました。

またURLをコピーしてOBSにブラウザソースとして追加するんですが、今回は背景も含めたいのでカスタムCSSの部分を空白にします。サイズも数値を変更しておいてください。
image.png

入力欄に入力して反映ボタンをクリックすれば画面内でも変わります。
image.png
同じように作った5games.htmlも同じ操作パネルで同時に変更できます。

感想とか

自分は作っていませんが、チーム名と勝数カウンターの部分を使って試合中に何対何なのかを表示できるようにしても面白そうです。あとは表示する部分にアニメーションとかも付けてみたいですね。上手くやれば書き換え自体も演出にできそう(?)。
間違っている部分とかあれば教えて下さい。

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

IE11 と reCAPTCHA と core-js を組み合わせると何かが壊れることがある

何が起きたか

IE11 で core-js を使った JavaScript を動かしてるとこに reCAPTCHA のwww.google.com/recaptcha/api.js を組み合わせたらこんなエラーが発生した。

SCRIPT5005: 文字列を指定してください
recaptcha__ja.js (2,220)

色々調べてると core-js のto-primitive.jsからもエラーが出てる。IE11は catch されない throw は構文エラーになるやつ。

SCRIPT5022: catchステートメントでは適用されますが、throwステートメントでは適用されません

Symbol のポリフィルが reCAPTCHA との兼ね合いでエラー起こすようになってる?

解決策

Cannot convert object to primitive value with core-js 3.1.3 + webpack #566 の Cobertos さんのコメント通り、recaptcha/api.js を core-js より後ろで呼ぶようにしたら怒られなくなった。

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

ReactNativeでQRコード生成

QRコードを生成するライブラリはいくつかありますが、Canvasに依存していたり、SVGとして出力するしかなかったりするため、ReactNativeで使おうとすると問題があるケースが多いです。

簡単すぎる記事で申し訳ないですが、ReactNativeとの相性が良さそうなqrcode-generatorを使ったコンポーネント例を載せておきます。

QRCode.js
import React from 'react';
import PropTypes from 'prop-types';
import { Image } from 'react-native';
import qrcode from 'qrcode-generator';

export default class QRCode extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      size: null
    };
    this.onLayout = this.onLayout.bind(this);
  }

  componentWillReceiveProps(nextProps, nextContext) {
    if (nextProps.size !== this.props.size) {
      this.setState({
        size: null
      });
    }
  }

  onLayout(e) {
    const { width } = e.nativeEvent.layout;
    if (this.state.size !== width) {
      this.setState({ size: width });
    }
  }

  render() {
    const { data, cellSize, margin, typeNumber, errorCorrectionLevel } = this.props;
    const size = this.state.size || this.props.size;
    const QRCode = qrcode(typeNumber, errorCorrectionLevel);
    QRCode.addData(data);
    QRCode.make();
    let calculatedCellSize = cellSize;
    if (typeof calculatedCellSize !== 'number' && typeof size === 'number') {
      calculatedCellSize = typeof margin === 'number' ? Math.round((size - margin * 2) / QRCode.getModuleCount())
        : Math.round(size / (QRCode.getModuleCount() + 8));
    }
    const uri = QRCode.createDataURL(calculatedCellSize, margin);
    return (
      <Image
        key={uri}
        onLayout={this.onLayout}
        style={{
          width: size,
          aspectRatio: 1
        }}
        source={{ uri }}
      />
    );
  }
}

QRCode.defaultProps = {
  typeNumber: 0 // 自動
};

QRCode.propTypes = {
  data: PropTypes.string.isRequired, // データ文字列
  size: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, // 画像の大きさ
  cellSize: PropTypes.number, // セルの大きさ
  margin: PropTypes.number, // 余白の大きさ
  typeNumber: PropTypes.number, // 型番
  errorCorrectionLevel: PropTypes.string.isRequired // 誤り訂正レベル
};

propsの型番、誤り訂正レベルについてはこちらを参照してください。

QRコード - Wikipedia

セルの大きさ、余白、型番は何も入れなければ自動的に決定されるようになりますので、基本的に非推奨です。
sizeには文字列(100%など)を入れた場合にも自動で画像の解像度が調整されるようにしてみました。

シンプルなQRコードの生成はこれで最低限かつ安定してできますが、色を変えたりロゴを配置したりするのには対応していないので、その点ほかのライブラリ(Expoだとそれぞれ不具合ありですがreact-native-qrcode-svgreact-native-qrcode)に劣ります。

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

ifやswitchが式じゃなくて困る時の対処

javascriptのifやswitchは式ではない

javascriptのifやswitchは式ではなく文です。
要するに、それ自体が値を返すことが出来ません。
関数型言語はifが式のものも多いですが(kotlinとか)
javascriptは後方互換性が重視されるタイプの言語なのでクラシカルな文形式です。

と言うわけで、コードを固い方向に寄せるためになるべくconstを使おうとしたりすると
すぐ問題にぶち当たります。

例えば、以下のようなコードは通りません。

const hoge;
if(fuga){
 hoge = "fuga";
}else{
 hoge = "piyo";
}

constの初期化時に条件に応じて異なる値を入れるというようなありふれたケースにすらjavascriptのifは対応できないわけです。
当然、switch文も同様です。妥協してletやvarを使うのも手ですが、なるべくそれは避けたいですし、
React+JSXにおける{}の中など単純にそれだけで逃げられないケースもあります。

三項演算子

一番手軽な解決策は三項演算子です。
演算子は値を返すので問題はありません。

const hoge = fuga ? "fuga" : "piyo";

条件部分や返すべき結果が短い場合は上記のようにいい感じになります。

しかし、常にそううまく行くわけではありません。
例えばcaseがいくつも連なるswitch文やifの条件部分が長いものまで三項演算子で書くのはどうでしょうか。

const hoge = fuga ? "fuga" : piyo ?  "piyo" : hogehoge ? "hogehoge" :"hogePiyo"

短めのものを3つつなげただけでこれなので、相当気持ち悪いコードになることは間違いありません。
縦はともかく右方向に長いコードは可読性がかなり低くなります。
言語を問わず三項演算子を蛇蝎のごとく嫌う人がたまにいますが、おそらくこういうコードで苦しんだことがあるんでしょう。

関数に切り出す

function decideFugaOrPiyo(fuga) {
  if(fuga){
    return "fuga";
  }
  return "piyo";
}
const hoge = decideFugaOrPiyo(fuga);

そういう場合に有効な手段が、関数への切り出しです。
関数に切り出せば複雑な条件やswitch文があろうと問題ありません。
命名が適切ならば可読性もむしろ上がるケースが多いでしょう。

しかし、大した長さでは無かったり、再利用する予定のない場合はいちいち切り出した方が読みにくい場合もあります。

即時関数を使う

const hoge = (() => {
  if(fuga){
    return "fuga";
  }
  return "piyo";
})();

わざわざ切り出すほど複雑ではなくかといって三項演算子を使うにはちょっと…と言うようなケースには即時関数が便利です。
少々括弧が多くなりますが、思いの外インデントも深くならない(せいぜい1段下がるだけ)なので可読性の点でもそこまで悪くないと思います。

まとめ

  • 短い・シンプル=>三項演算子
  • 長い・複雑・再利用する=>関数切り出し
  • 中間くらい=>即時関数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超音波センサーで近点距離を計測し、老眼か判定するLINE Bot×Iotの作成

概要

以前、老眼とモスキート音による加齢性難聴をチェックするLINE Bot×Iotを作成しました。
老眼と加齢性難聴のチェックができるLINE Bot×Iotの作成

前回は近点距離(人差し指の指紋が一番鮮明に見える距離)は実際にメジャーで計測してLINEに数値を入力していましたが、今回は、超音波センサーで近点距離を自動計測しました。機能も老眼判定のみに絞ったBotをobnizと組み合わせて作成しました。

実装

LINE BOtとobnizの連携。近点(人差し指の指紋が一番鮮明に見える)でLINEに入力すると自分が老眼かどうかが分かるBOT。

概念図

node.js expressでLINE bot APIとobnizを連携しました。
rougann.jpg

作成方法

1.obnizの準備
obnizをPCと接続します。
obniz公式サイト
Node.jsのダウンロード

obnizを使えるようにWifiに接続し、画面上にQRコードと8桁の数字が表れるようにします。

超音波センサーを準備し、obnizのio0・io1・i02にさします。
image.png
今回は、超音波距離センサー HC-SR04 [101-60-142 / 20-019-100-A]を使用しました。

超音波センサーのドキュメント

2. Botアカウントを作成する

3. Node.jsでBot開発

4. ngrokでトンネリング

上記の2~4を以下の参考記事の通りに行います。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017

5.obnizとの連携

コードを以下のように書き換えます。

'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: '自分のchannelSecretを入力',
    channelAccessToken: '自分のchannelAccessTokenを入力'
};

// Obnizの準備
var Obniz = require("obniz");
var obniz = new Obniz("自分のObniz ID を入力");  // Obniz ID を入力
//超音波センサー
var hcsr04;//全体に使えるようにするスコープ
obniz.onconnect = async function () {
  hcsr04 = obniz.wired("HC-SR04", { gnd: 0, echo: 1, trigger: 2, vcc: 3 });


}

console.log("hcsr04 sample!");


const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);

  //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
  if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
    res.send('Hello LINE BOT!(POST)');
    console.log('疎通確認用');
    return; 
  }

  Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});



const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  // LINE botのプログラム
// 聞いてくる間はHTTP APIで取得するように取得待ちが生じる
getDistance(event.source.userId);

return client.replyMessage(event.replyToken, {
  type: 'text',
  text: 'ちょっとまってね。いま距離調べてる。'
});
}

const getDistance = async (userId) => {

  // 処理が待てるmeasureWaitのほうを使う
  let res;
  const dis_mm = await hcsr04.measureWait();
  const dis_cm = dis_mm / 10;
  const distance = dis_cm.toFixed(); 

  if (distance>=80) {
    res =" 近点距離は" + distance + "cm です。ピント調整力は61歳以上相当です。老眼の可能性が高いです。老眼度数は3以上と思われます。";
  }else if (distance >= 60){
    res = "近点距離は" + distance + "cm です。ピント調整力は56~60歳相当です。老眼の可能性が高いです。老眼度数は2.5程度と思われます。。";
  }else if (distance >= 50){
    res = "近点距離は" + distance + "cm です。ピント調整力は51~55歳相当です。老眼の可能性が高いです。老眼度数は2程度と思われます。";
  }else if(distance >= 40){
    res = "近点距離は" + distance + "cm です。ピント調整力は46~50歳相当です。老眼の可能性が高いです。老眼度数は1.5程度と思われます。";
     }else if(distance >= 30){
    res = "近点距離は" + distance + "cm です。ピント調整力は41~45歳相当です。老眼の可能性が高いです。老眼度数は1程度と思われます。";
  } else if(distance<30) {
    res = "近点距離は" + distance + "cm です。ピント調整力は40歳未満相当です。老眼の可能性は低いです。";
  }else {
    res = "指先にピントが合い、人差し指の指紋がはっきりと見えたところでLINEに何かメッセージを送ってください。";
  }


  await client.pushMessage(userId, {
      type: 'text',
      text: res,
  });
}


app.listen(PORT);
console.log(`Server running at ${PORT}`);

動作確認

ちゃんと動いています。
image.png

考察

超音波センサーは対象が柔らかいと正確に距離を測定できないということだったので心配でしたが、メジャーで計測して比較すると誤差は数㎝ほどでした。スマートフォンのカメラで距離が取得できるとUIがずっと良くなるので何か方法はないか調べてみたいと思います。

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

ローグライクゲームを作ってみるその3 プレイヤーの移動

過去記事一覧

現在のコードについては前回の記事の最後の項を参照してください。

プレイヤーの移動

このゲームは基本的にキーボードを使って操作することにします。

プレイヤーの移動は矢印キーを使って行うことにします。

まず、プレイヤーは全てのマスに移動できる訳ではないのでした。

床のマスには移動できますが、壁のマスには移動できません。

つまり、マスの種類によって移動できるかどうかが決まります。

これをB_CAN_STANDという配列で下のように定義することにします。trueは移動できるマスであることを表し、falseは移動できないマスであることを表します。

var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;

そして、キー入力の処理を下のように変更します。

なお、矢印キーのkeyCodeの値はそれぞれ下のようになっています。

  • ・・・37
  • ・・・38
  • ・・・39
  • ・・・40
    c.on('keydown', function (e) {
        if (!startf) {
            if (e.keyCode === 90) {
                startf = true;

                init();

                draw(con);
            }

            return;
        }

        if (e.keyCode >= 37 && e.keyCode <= 40) {
            var x = player.x;
            var y = player.y;
            if (e.keyCode === 37) {
                if (x === 0) {
                    return;
                }
                x--;
            }
            else if (e.keyCode === 38) {
                if (y === 0) {
                    return;
                }
                y--;
            }
            else if (e.keyCode === 39) {
                if (x === LX - 1) {
                    return;
                }
                x++;
            }
            else if (e.keyCode === 40) {
                if (y === LY - 1) {
                    return;
                }
                y++;
            }

            if (x !== player.x || y !== player.y) {
                var block = field.blocks[x][y];
                if (B_CAN_STAND[block.base]) {
                    player.x = x;
                    player.y = y;

                    draw(con);
                }
            }
        }
    });

矢印キーが押された時の処理が追加されました。

矢印キーの種類によってプレイヤーの新しい位置が決まり、その位置のマスの種類が移動できるものである場合には移動し(プレイヤーの位置を更新し)、再描画を行います。

See the Pen roguelike-3-1 by pizyumi (@pizyumi) on CodePen.

斜め移動

ローグライクゲームはやっぱり斜め移動もできないとですよね。

斜め移動を実装しましょう。

斜め移動はShiftキーを押しながら矢印キーを押すことで行うことにします。

実装においては少なくとも以下の2点を満たす必要があるかと思います。

  • Shiftキーを押している時のみしか斜め移動できない(Shiftキーを押していないのに斜め移動できてはならない)。
  • Shiftキーを押している時には斜め移動しか出来ない(Shiftキーを押している時には上下左右の移動はできてはならない)。

このようにすることにより、誤操作が起きにくくなります。

このような斜め移動の実装方法は幾つかあると思いますが、私は下のような実装方法が好きです。

keylkeyukeyrkeydという変数を用意し、矢印キーが押されているかどうかを常に監視するようにします。

そして、Shiftキーが押されている場合にはこれらの変数の値を基に移動を行うべきかを判断します。

    var keyl = false;
    var keyu = false;
    var keyr = false;
    var keyd = false;

    var c = $('body');
    c.on('keydown', function (e) {
        if (e.keyCode === 37) {
            keyl = true;
        }
        else if (e.keyCode === 38) {
            keyu = true;
        }
        else if (e.keyCode === 39) {
            keyr = true;
        }
        else if (e.keyCode === 40) {
            keyd = true;
        }
        else {
            keyl = false;
            keyu = false;
            keyr = false;
            keyd = false;
        }
    });
    c.on('keyup', function (e) {
        if (e.keyCode === 37) {
            keyl = false;
        }
        else if (e.keyCode === 38) {
            keyu = false;
        }
        else if (e.keyCode === 39) {
            keyr = false;
        }
        else if (e.keyCode === 40) {
            keyd = false;
        }
    });
    c.on('keydown', function (e) {
        if (!startf) {
(省略)
        }

        if (e.keyCode >= 37 && e.keyCode <= 40) {
            var x = player.x;
            var y = player.y;
            if (e.shiftKey) {
                if (keyl && keyu) {
                    if (x === 0 || y === 0) {
                        return;
                    }
                    x--;
                    y--;
                }
                else if (keyr && keyu) {
                    if (x === LX - 1 || y === 0) {
                        return;
                    }
                    x++;
                    y--;
                }
                else if (keyl && keyd) {
                    if (x === 0 || y === LY - 1) {
                        return;
                    }
                    x--;
                    y++;
                }
                else if (keyr && keyd) {
                    if (x === LX - 1 || y === LY - 1) {
                        return;
                    }
                    x++;
                    y++;
                }
                else {
                    return;
                }
            }
            else {
                if (e.keyCode === 37) {
                    if (x === 0) {
                        return;
                    }
                    x--;
                }
                else if (e.keyCode === 38) {
                    if (y === 0) {
                        return;
                    }
                    y--;
                }
                else if (e.keyCode === 39) {
                    if (x === LX - 1) {
                        return;
                    }
                    x++;
                }
                else if (e.keyCode === 40) {
                    if (y === LY - 1) {
                        return;
                    }
                    y++;
                }
            }

            if (x !== player.x || y !== player.y) {
                var block = field.blocks[x][y];
                if (B_CAN_STAND[block.base]) {
                    player.x = x;
                    player.y = y;

                    draw(con);
                }
            }
        }
    });

斜め矢印の表示

Shiftキーを押した時に斜め移動が可能なことをユーザーに分かりやすく見せるために、Shiftキーを押した時にプレイヤーの周りに斜め矢印が表示されるようにしましょう。

この斜め矢印は、たとえば、ふし幻(不思議の幻想郷)でも採用されていますね。

01.jpg

これを行うには、Shiftキーが押されているか(というか斜め移動可能な状態か)を保持しておく必要があります。

この状態はenv.diagonalで保持することにします。env.diagonalの値がtrueである場合には斜め移動可能で、falseである場合には斜め移動不可能です。

    var env = {
        diagonal: false
    };

そして、この状態(env.diagonalの値)によってゲームの描画内容(プレイヤーの周りに斜め矢印を表示するかどうか)が変わりますので、この状態はdraw関数に渡さなければならないということになります。

そこで、全てのdraw関数の呼び出しにおいてはenvを第2引数として指定することにします(変更したコードは示しませんが、全てのdraw関数の呼び出しで第2引数としてenvを渡すように変更します)。

そうしたら、2つ目のkeydownイベントハンドラを下のように変更します。

つまり、Shiftキーが押された場合の処理を追加します。

Shiftキーが押された場合にはenv.diagonalの値をtrueにし、再描画を行います。

    c.on('keydown', function (e) {
        if (!startf) {
(省略)
        }

        if (e.keyCode === 16) {
            if (!env.diagonal) {
                env.diagonal = true;

                draw(con, env);
            }

            return;
        }

        if (e.keyCode >= 37 && e.keyCode <= 40) {
(省略)
        }
    });

更に、新たにkeyupイベントハンドラとwindowblurイベントハンドラを追加します。

keyupイベントハンドラにはShiftキーが離された場合の処理を記述します。

Shiftキーが離された場合にはenv.diagonalの値をfalseにし、再描画を行います。

blurイベントハンドラにはウィンドウからフォーカスが外れた場合の処理を記述します。

ウィンドウからフォーカスが外れた場合にもenv.diagonalの値をfalseにし、再描画を行います。

これはウィンドウからフォーカスが外れた場合にも斜め移動が有効なままであるように表示されるのを防ぐためです。

    c.on('keyup', function (e) {
        if (e.keyCode === 16) {
            if (env.diagonal) {
                env.diagonal = false;

                draw(con, env);
            }
        }
    });
    $(window).on('blur', function (e) {
        if (env.diagonal) {
            env.diagonal = false;

            draw(con, env);
        }
    });

最後にdraw関数を変更します。

env.diagonalの値がtrueである場合の描画処理を追加します。

function draw (con, env) {
(省略)

    if (env.diagonal) {
        con.save();

        con.strokeStyle = 'white';
        con.translate(player.x * PX + (PX / 2), player.y * PY + (PY / 2));
        con.rotate(Math.PI / 4);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();

        con.restore();
    }
}

これでShiftキーを押すとプレイヤーの周りに4つの斜め矢印が表示され、斜め移動できるようになりました。

See the Pen roguelike-3-2 by pizyumi (@pizyumi) on CodePen.

今回はここまで

今回はここまでです。

game.jsは下のようになりました。

var TITLE = 'シンプルローグライク';

var SCREEN_X = 1600;
var SCREEN_Y = 800;

var LX = 25;
var LY = 25;
var PX = 32;
var PY = 32;

var B_FLOOR = 0;
var B_WALL = 1;

var B_CAN_STAND = [];
B_CAN_STAND[B_FLOOR] = true;
B_CAN_STAND[B_WALL] = false;

var startf = false;

var field = null;
var player = null;

$(function(){
    var canvas = document.getElementById('game');
    var con = canvas.getContext('2d');

    var keyl = false;
    var keyu = false;
    var keyr = false;
    var keyd = false;

    var env = {
        diagonal: false
    };

    var c = $('body');
    c.on('keydown', function (e) {
        if (e.keyCode === 37) {
            keyl = true;
        }
        else if (e.keyCode === 38) {
            keyu = true;
        }
        else if (e.keyCode === 39) {
            keyr = true;
        }
        else if (e.keyCode === 40) {
            keyd = true;
        }
        else {
            keyl = false;
            keyu = false;
            keyr = false;
            keyd = false;
        }
    });
    c.on('keyup', function (e) {
        if (e.keyCode === 37) {
            keyl = false;
        }
        else if (e.keyCode === 38) {
            keyu = false;
        }
        else if (e.keyCode === 39) {
            keyr = false;
        }
        else if (e.keyCode === 40) {
            keyd = false;
        }
    });
    c.on('keydown', function (e) {
        if (!startf) {
            if (e.keyCode === 90) {
                startf = true;

                init();

                draw(con, env);
            }

            return;
        }

        if (e.keyCode === 16) {
            if (!env.diagonal) {
                env.diagonal = true;

                draw(con, env);
            }

            return;
        }

        if (e.keyCode >= 37 && e.keyCode <= 40) {
            var x = player.x;
            var y = player.y;
            if (e.shiftKey) {
                if (keyl && keyu) {
                    if (x === 0 || y === 0) {
                        return;
                    }
                    x--;
                    y--;
                }
                else if (keyr && keyu) {
                    if (x === LX - 1 || y === 0) {
                        return;
                    }
                    x++;
                    y--;
                }
                else if (keyl && keyd) {
                    if (x === 0 || y === LY - 1) {
                        return;
                    }
                    x--;
                    y++;
                }
                else if (keyr && keyd) {
                    if (x === LX - 1 || y === LY - 1) {
                        return;
                    }
                    x++;
                    y++;
                }
                else {
                    return;
                }
            }
            else {
                if (e.keyCode === 37) {
                    if (x === 0) {
                        return;
                    }
                    x--;
                }
                else if (e.keyCode === 38) {
                    if (y === 0) {
                        return;
                    }
                    y--;
                }
                else if (e.keyCode === 39) {
                    if (x === LX - 1) {
                        return;
                    }
                    x++;
                }
                else if (e.keyCode === 40) {
                    if (y === LY - 1) {
                        return;
                    }
                    y++;
                }
            }

            if (x !== player.x || y !== player.y) {
                var block = field.blocks[x][y];
                if (B_CAN_STAND[block.base]) {
                    player.x = x;
                    player.y = y;

                    draw(con, env);
                }
            }
        }
    });
    c.on('keyup', function (e) {
        if (e.keyCode === 16) {
            if (env.diagonal) {
                env.diagonal = false;

                draw(con, env);
            }
        }
    });
    $(window).on('blur', function (e) {
        if (env.diagonal) {
            env.diagonal = false;

            draw(con, env);
        }
    });

    draw(con, env);
});

function init () {
    field = create_field();
    player = {
        x: 12,
        y: 17
    };
}

function create_field () {
    var blocks = [];
    for (var i = 0; i < LX; i++) {
        blocks[i] = [];
        for (var j = 0; j < LY; j++) {
            if ((i === 0 || j === 0) || (i === LX -1 || j === LY - 1)) {
                blocks[i][j] = {
                    base: B_WALL
                };
            }
            else {
                blocks[i][j] = {
                    base: B_FLOOR
                };
            }
        }
    }

    return {
        blocks: blocks
    };
}

function draw (con, env) {
    con.fillStyle = 'black';
    con.fillRect(0, 0, SCREEN_X, SCREEN_Y);

    if (!startf) {
        con.textBaseline = 'alphabetic';
        con.textAlign = 'center';
        con.fillStyle = 'white';

        con.font = "48px consolas";
        con.fillText(TITLE, SCREEN_X / 2, SCREEN_Y / 4);

        con.font = "32px consolas";
        con.fillText('> はじめる', SCREEN_X / 2, SCREEN_Y / 4 * 3);

        return;
    }

    for (var i = 0; i < LX; i++) {
        for (var j = 0; j < LY; j++) {
            var block = field.blocks[i][j];
            if (block.base === B_FLOOR) {
                con.fillStyle = 'white';
                con.beginPath();
                con.arc((i + 0.5) * PX, (j + 0.5) * PY, 1, 0, Math.PI * 2);
                con.closePath();
                con.fill();
            }
            else if (block.base === B_WALL) {
                con.strokeStyle = 'white';
                con.strokeRect(i * PX, j * PY, PX, PY);
                con.beginPath();
                con.moveTo(i * PX, j * PY);
                con.lineTo((i + 1) * PX, (j + 1) * PY);
                con.moveTo((i + 1) * PX, j * PY);
                con.lineTo(i * PX, (j + 1) * PY);
                con.closePath();
                con.stroke();
            }
        }
    }

    con.textBaseline = 'middle';
    con.textAlign = 'center';
    con.fillStyle = 'red';
    con.fillText('?\uFE0E', player.x * PX + (PX / 2), player.y * PY + (PY / 2));

    if (env.diagonal) {
        con.save();

        con.strokeStyle = 'white';
        con.translate(player.x * PX + (PX / 2), player.y * PY + (PY / 2));
        con.rotate(Math.PI / 4);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();
        con.rotate(Math.PI / 4 * 2);
        con.beginPath();
        con.moveTo((PX / 2) + 4, -4);
        con.lineTo((PX / 2) + 4 + 8, -4);
        con.lineTo((PX / 2) + 4 + 8, -4 - 4);
        con.lineTo((PX / 2) + 4 + 8 + 8, 0);
        con.lineTo((PX / 2) + 4 + 8, 4 + 4);
        con.lineTo((PX / 2) + 4 + 8, 4);
        con.lineTo((PX / 2) + 4, 4);
        con.closePath();
        con.stroke();

        con.restore();
    }
}

次回はダンジョンの生成を少しだけ考えてみたいと思います。

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

http モジュール(node.js)で、ローカル画像ファイルを使用する。

この記事は、httpモジュールで、ローカル画像ファイルを使う方法を書き込んだ
個人的なメモです。:point_up:

httpモジュール内で、画像を表示させる一番簡単な方法として、

画像アドレスを使用する

//html
<img src="http://○○○○○○">

//pug
img(src="http://○○○○○○")

が、あります。が、この方法は任意の画像をどこぞにアップロードして画像アドレスを
取得しなければなりません。
例えば私がN予備校の授業で作ったアンケートページでは、
グーグルドライブに画像をアップロードして、そこからアドレスを取得して使っています。:cloud:

尚、グーグルドライブの画像を使うにはちょっとクセがあります。

:writing_hand: グーグルドライブの画像を使う場合
グーグルドライブにアップロードした任意の画像→
右クリック→「共有可能なリンクを取得」で取得するURL ・・ではエラーになってしまいます。

//「共有可能なリンクを取得」でコピーできるURL
https://drive.google.com/open?id=○○○○○○○○○○

利用するには、このアドレスの「id=」の前を入れ替えます。

//表示用のアドレス
http://drive.google.com/uc?export=view&id=○○○○○○○○○○

↑表示用のアドレスでidを入れ替えればOK。

これでも十分なんですけどね。

httpモジュール内のWebページでローカル画像を読み込ませる。

本題です。やり方としては、上に書いた通り、画像URL(画像が表示されるアドレス)があれば
画像が使えるので、画像を表示させるアドレス(パス)を作ります。
使用してる画像ファイル(参考
buta.png buta.png
ファイル構成
index.js  (http.createServerしてるファイル)
views   (ページをレイアウトするpugファイルなどなどを入れたディレクトリ)
 ┣ pugファイル
 ┗ images  (画像ファイルを入れているディレクトリ)
   ┣ buta.png

const fs=require("fs"); // ← 超重要(忘れやすい)

  switch (req.url) {
    case '/index':
      ${indexにリクエストが来た時に動く関数}(req, res);
      break;
    case '/images/buta.pug':  // ← アドレスは任意。本当はuuidとか使うのがいいのかもしれませんが。
      res.writeHead(200, {
      'Content-Type': `image/png; charset=utf-8`  // ← ここがキモ!
      });
      var image = fs.readFileSync("./views/images/buta.png", "binary"); // ← ファイルpathはその環境に合わせてください
      res.end(image, "binary");
      break;
    default:
      break;
  }

switch文で画像アドレスを作り、そこにリクエストがきたらfsモジュールで画像を読み込んで表示させるというただそれだけです。
readFileはSyncにしないとエラーがでます。(私の環境だと)
※ jpg画像の場合は'Content-Type': `image/jpeg;です。(jpgではない)
これでローカル画像が使えます:v_tone1:
例)
<img src="/images/buta.pug">

まとめ
Expressフレームワークは偉大

:star2:参考にさせてもらったContent-Typeまとめページ:star2:
:point_right: https://qiita.com/AkihiroTakamura/items/b93fbe511465f52bffaa

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

Node.jsでWebSocketサーバをUnixドメインソケットで立てる

Node.jsでwsパッケージのWebSocketサーバをUnixドメインソケットでlistenする方法です。サンプルコードはTypeScriptです。

Unixドメインソケットを使ったWebSocketサーバを立てるコード

Serverクラスのコンストラクタのserverオプションにhttp.Serverのインスタンスを渡すのがミソです。

server.ts
import websocket from 'ws'
import http from 'http'
import fs from 'fs'

// UnixドメインソケットでWebSocketを待ち受けるHTTPサーバー
const hs: http.Server = http.createServer()

// WebSocketサーバー (いわゆるechoサーバーです)
const wss = new websocket.Server({server: hs}) // 最重要
wss.on('connection', ws => {
  ws.send('hello, this is websocket echo server.')
  ws.on('message', message => {
    ws.send(message) // オオム返し
  })
})

// Unixドメインソケットのファイル名
const socketPath = '/tmp/server.sock'

// UnixドメインソケットのファイルがあるとHTTPサーバが起動しないので必ず削除する
fs.existsSync(socketPath) && fs.unlinkSync(socketPath)

// HTTPサーバのlistenを開始する
hs.listen(socketPath, () => {
  console.log(`websocket server listening on ${socketPath}`)
})

WebSocketサーバーと話してみる

WebSocketサーバと話すには、wscatというWebSocket CLIクライアントが便利です。

Unixドメインソケットではない、普通のTCPのWebSocketと疎通するには、npx wscat -c ws://echo.websocket.orgのように、URLをws://で始めますが、Unixドメインソケットでは、URLスキーマの部分がws+unix://になります:

$ npx wscat -c ws+unix:///tmp/server.sock
Connected (press CTRL+C to quit)
< hello, this is websocket echo server.
> てすと
< てすと
>

参考文献

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

【未完】LINE Botにobnizを組み合わせて感情をもたせることに挑戦した

はじめに 

LINE Botでオウム返しやAPIと連携させていたりしたが、今回はLINE botとobnizを連携させることでbotの機械的な表現に感情があるように表現できないかを検証したく作成しました。
結論、タイトルにある通り完成はできていないので、今後も作成は継続していきます。

概要

システムと違い人間は同じ言葉を話していても、その時の感情は発言と反していることがあります。
例えば「好き」言われて本当はうれしいのに「ふーん」とそっけない返事をしてしまうようなこととかがあります。
このツンデレな感じをBotで表現できないか、というのが今回の作成コンセプトです。

仕様はLINE Botに「好き」以外の言葉だと「ふーん」とそっけない返事のみだけですが
「好き」という言葉を送ると、返事は「ふーん」なのですが
obnizのLEDが赤く発行します。

似ているものとして、嘘発見機をイメージしていただけるとわかりやすいかもしれません。

環境

Node.js v10.16.3
Windows 10 pro
Visual Studio Code v1.39.1

作り方・参照サイト

まず必要な素材ですが下記のURLを参照ください。

obniz LEDパーツ

また、実際のLINE Botとobnizの連携についてですが

老眼と加齢性難聴のチェックができるLINE Bot×Iotの作成

こちらの記事がわかりやすく参考になりました。
node.jsを使用してAPI連携をしております。

使用したコード

'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelSecret: '作成したBOTのチャンネルシークレット',
    channelAccessToken: '作成したBOTのチャンネルアクセストークン'
};

// Obnizの準備
var Obniz = require("obniz");
var obniz = new Obniz("");  // Obniz ID を入力
var leds; // 全体で使えるようにするスコープ
obniz.onconnect = async function () {
  var leds = obniz.wired("WS2811", {gnd:0, vcc: 1, din: 2});
}

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let ans = "";
  let question = event.message.text;
  let color;//RGB値

  if (question == "好き") {
    ans = "ふーん";
    color = [255, 0, 0];
  } else {
    ans = "ふーん";

  }

  led.rgbs(color);

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: ans //実際に返信の言葉を入れる箇所
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

結果

IMG_4820 (1).jpg
S__58744842 (2).jpg

...不発

全く振り向いてくれません(笑)
どうやらLINE自体が応答してくれないようです。webhookの接続は問題ないのでコードに問題がありそうです。
あとはobnizが常時発光している状態なのでこれもなんとかしないといけませんね。。

考察 次回に向けて

いろんな記事を参考にしていますがやはりコピペだけだと限界がきますね。node.jsなのでjavascriptのコードの理解が早急に必要だと感じました。
それでも、今ある材料の中から作品をどう表現していくのかというコンセプト、企画の点でとても楽しく作成できました。
プログラミングの知識と合わせて企画力も意識して制作していこうと意欲的になりました。

今回は未完の状態で終了しておりますが、引き続き継続して作品を完成させたいと思います。

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

コピペで使うFirebase【Authentication編】

概要

Authentication(認証)を使う際にコピペで動かしたいのでそのまとめです。

公式ドキュメント
認証

Firebase SDK Auth
 ・初期設定 FirebaseSDK
 ・ユーザー登録
 ・ログイン
 ・ユーザーデータ取得

Admin SDK
 ・初期設定 AdminSDK
 ・ユーザー取得
 ・ユーザー作成
 ・ユーザー更新
 ・ユーザー削除

Firebase SDK Auth

client側

初期設定 FirebaseSDK

インストール

$ npm install firebase --save

firebase初期化

import firebase from "firebase/app"

const firebaseConfig = {
    /* firebase config */
}

// 初期化は一度だけ
if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig);
}

ユーザー登録

ユーザーをEmailとpasswordを用いて新規に作成する
Firebaseコンソール > [Authentication] > [ログイン方法] で事前に設定する必要がある

firebase.auth().createUserWithEmailAndPassword(email, password)

ログイン

多くの方法でのログインが提供されている
Firebaseコンソール > [Authentication] > [ログイン方法] で事前に設定する必要がある
※よく使われるものだけ抜粋

メールアドレスとパスワードを使用してログイン

firebase.auth().signInWithEmailAndPassword(email, password)

Google ログイン

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(result => /* ... */);

Facebook ログイン

const provider = new firebase.auth.FacebookAuthProvider();
firebase.auth().signInWithPopup(provider).then(result => /* ... */);

Twitter ログイン

const provider = new firebase.auth.TwitterAuthProvider();
firebase.auth().signInWithPopup(provider).then(result => /* ... */);

匿名認証

firebase.auth().signInAnonymously();

匿名認証後に各プロバイダーで匿名認証のUIDを引き継ぎたい場合
・例: 匿名認証済みで、googleログインしたアカウントへUIDを引き継ぎたい場合

const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().currentUser.linkWithPopup(provider).then(result => /* ... */);

ユーザーデータ取得

ユーザーのログイン状態が変わるたびにユーザーデータを取得する

firebase.auth().onAuthStateChanged(user => /* ... */)

Admin SDK

backend側

初期設定 AdminSDK

インストール

$ npm install firebase-admin --save
var admin = require('firebase-admin');

// 初期化
admin.initializeApp({
    credential: XXXX,
    databaseURL: XXXX
});

ユーザー取得

・uidでユーザーを取得

admin.auth().getUser(uid).then(function(userRecord) { /* ... */ })

・Emailでユーザーを取得

admin.auth().getUserByEmail(email).then(function(userRecord) { /* ... */ })

・電話番号でユーザーを取得

admin.auth().getUserByPhoneNumber(phoneNumber).then(function(userRecord) { /* ... */ })

ユーザー作成

プロパティーを指定しない場合は、その値は空となる

var user = {
    uid: /** 指定しなければ自動で生成される **/,
    email: /** Emailアドレス **/,
    emailVerified: false /** boolean Emailアドレスの確認の有無 **/,
    phoneNumber: '+11234567890' /** 電話番号 **/,
    password: /** パスワード **/,
    displayName: /** ユーザーの表示名 **/,
    photoURL: /** ユーザーの写真 URL **/,
    disabled: false /** ユーザーが無効かどうか **/
};

admin.auth().createUser(user).then(function(userRecord) { /* ... */ })

ユーザー更新

uidでユーザーを指定しデータを更新する
プロパティーを指定しない場合は、その値は更新されない

var user = {
  email: /** Emailアドレス **/,
  phoneNumber: '+11234567890' /** 電話番号 **/,
  emailVerified: false /** boolean Emailアドレスの確認の有無 **/,
  password: /** パスワード **/,
  displayName: /** ユーザーの表示名 **/,
  photoURL: /** ユーザーの写真 URL **/,
  disabled: false /** ユーザーが無効かどうか **/
}

admin.auth().updateUser(uid, user).then(function(userRecord) { /* ... */ })

ユーザー削除

uidでユーザーを指定しユーザーを削除する

admin.auth().deleteUser(uid).then(function() { /* ... */ })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web Share API Level2を使ってアプリ連携無しでTwitterに画像を投稿する

ブラウザから直接ネイティブにインストールされているアプリの共有機能(下の画像参照)を呼び出す、Web Share APIという機能があります。

メッセージやURLなどのテキストの共有は、Web Share API Level1としてすでにiOS SafariとAndroid Chromeで利用することができます。

更に、Android Chrome75以降(2019/10/24現在最新版は78)ではファイルなどの共有も可能なWeb Share API Level 2が使えるので今回はこれを使ってTwitterとのアプリ連携なしにTwitterに画像を投稿できる仕組みを作ってみました。

それがこちらです
No Login Image Share

好きな文字をテキストボックスに入れてShareボタンを押すと、ネイティブの共有モーダルが表示され、アプリを選択するとそこに作成した吹き出しの画像が共有されるはずです。

実際にツイートしたものが↓です。


この記事ではソースコードの中から重要な部分をかいつまんで説明します。
実際のソースコードはtohutohu/no-login-image-shareから確認してください。

DOMからFileを作成する

吹き出しを含めたシェアされる部分全体のDOMは以下のようになっています。
この内、div#message内に入力したメッセージが挿入されます。

<div id="image-container">
  <div id="message-container">
    <div id="message"></div>
  </div>
</div>

tsayen/dom-to-imageというライブラリを使って、DOMからpng形式のBlobを取得しFileクラスに変換します。

const blob = await domtoimage.toBlob(document.getElementById('image-container'))
const image = new File([blob], 'tmp.png', {type: 'image/png'})

Fileを共有する

navigator.shareにシェアしたいデータを渡すことで、シェアモーダルを表示させることができます。
また、navigator.canShareにシェアしたいデータを渡すことで、そのデータをシェアすることができるかどうかを知ることができます。

詳細はWeb Share API - Level 2を確認してください。

navigator.share({
  text: 'アプリ連携無しで画像がシェアできました!',
  url: 'https://tohutohu.github.io/no-login-image-share/',
  files: [image]
}).then(() => {
  console.log('Share was successful.')
}).catch((error) => {
  console.log('Sharing failed', error)
})

たったこれだけで、画像のシェア機能を実装することが可能です。

その他の画像投稿方法との比較

1. アプリ連携を行い、API経由で投稿する

Twitterとのアプリ連携を行い、アクセストークンを取得後サーバー側から画像を投稿する方法です。

  • メリット
    • プラットフォームを選ばない
  • デメリット
    • サーバーサイドの実装が必要
    • トークンなどの取り扱いに気をつけなければならない
    • Twitterと連携したらTwitterにしか投稿できない

2. 別のアカウントから画像を投稿した画像のURLを添付する

予め画像投稿用アカウントでトークンを取得しそこに画像つきツイートを行ったあと、そのツイートから画像URLを取得してそのURLをTwitter Intentなどでシェアする方法です。
極一部のアプリなどで実装されているのを見たことがあります。

  • メリット
    • プラットフォームを選ばない
  • デメリット
    • サーバーサイドの実装が必要
    • 実装が非常に煩雑
    • 規約的にOKなのか不明瞭

3. OGPで画像っぽく表示する

結果ごとに一意なURLを生成し、それに対応するOGPイメージを生成することで画像っぽく表示させる方法です。

  • メリット
    • プラットフォームを選ばない
    • リンクをクリックすることで結果ごとのより詳しい内容を確認するのような導線を敷くことができる
  • デメリット
    • サーバーサイドの実装が必要
    • シェア先がOGPの表示に対応している必要がある

まとめ

Web Share APIが様々なプラットフォームで利用できるようになればWebアプリの画像のシェアという観点ではアプリ連携が必要なくなるのではないかと思っています。
〇〇メーカーのようなUGCをシェアしてもらうタイプのWebアプリではかなりシェアまでのハードルが下がってシェアされやすくなりそう。

CanvasをStreamとして取得しそれを動画ファイルとして取得できれば、ゲームのリプレイなども直接SNSにシェアできたりするんでしょうか……?

参考ページ

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

【Django】テキストファイルを基にメーラー起動リンク作成

mailtoを使えば、a href~で簡単にリンク作れるんですけども、
リンクにカーソルを合わせたとき左下に~の部分が全部出てしまうんですよね。
メールアドレスだけならまだしも、本文がずらーっと出るのはダサいので嫌です!

よって、考えた手段がこちら
①txtから文章を読み込んで(python)
②読み込んだ文章とmailto使ってメーラー起動リンクを作成(javascript)

Djangoでこれを実現するには画面側からpython側にイベントを送らないといけないと思ったので、
javascriptやAjaxを調べつつチャレンジ。

javascript

こちらを参考に、クリックした箇所に対してメーラー起動リンクを作成するようなjsを書きます。
javascriptとはコンソールログで遊んだ程度の付き合いしかありません。

Django側からデータ受け取るためにAjax使ってますが、
こちらからは特に何も送ってないので変な使い方してる気はします。

function sendmail() {
    var is = this;

    $.ajax({
        url: "read_mail_text/",//Djangoのurlsに書いてあるパス
        type : 'POST',
        dataType: 'json',
        data : {app: null},//空っぽのjsonを送信
        cache: false,
        // 成功
        success: function(data) {
            //中身確認
            console.log(data["ac"]);
            //受け取った値からmailtoでメーラー起動用のリンク作成
            is.location.href = "mailto:" + data["ac"] + "?subject=" + data["sub"] + "&body=" + data["text"];

        },
        // エラー処理
        error: function(XMLHttpRequest, textStatus, errorThrown) {
            alert("Error : " + errorThrown);
       }
   });

}

Django

やることは
①Ajaxから呼び出されて
②txtから文章を読み込んで
③jsonを返す
……かと

①Ajaxにお呼ばれするために(Django)

path("read_mail_text/",[アプリ名].read_mail_text,name="read_mail_text")

こんな感じでパスを書いておきます。
敢えて書く必要もない気もしますが、一応メモ。

②txtの中身

最終的にmailtoに組み込むのでそれを準拠に、一行ずつ変数指定しました。
txtの中身は以下のような感じです

ac = "V3 <damidami@v3.mask>"; //送り先のアドレス
sub = "年賀状";//件名
text = "新年おめでとう。%0D%0A%0D%0A今年こそ、裏切り者は殺す。%0D%0AADKデストロンを代表して、ヨロイ元帥。";//本文

エスケープシーケンスのタグでも改行が出来なかった気がするので、改行コードがギエピーしてます。

③javascriptにjsonを返す(Django)

Ajaxのリクエストを受け取ってるので(たぶん)、引数にrequestを入れます。
現在地が分からなかったので、相対パス使ってます。

      def read_mail_text(self,request):
          #txt読込→変数に入れる
          path =os.path.dirname(__file__)+"\\mail_text.txt"

          myfile =File(open(path,  'rb'))
          txt=[]
          for t in myfile:
              txt.append(t.decode('utf-8'))

          #json形式で返す
          return JsonResponse({'ac':txt[0],'sub':txt[1],'text':txt[2]})

完成

自分の環境で動きましたのでひとまず、題名の内容はクリア。
汎用性が低いのがネックですね。

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

年末まで毎日webサイトを作り続ける大学生 〜6日目 ajax & Youtube APIを触ってみる〜

はじめに

初めまして。
年末まで毎日webサイトを作っている者です。
今日はajaxとYoutube APIを触ってみました。
結論をいうとほんとうに触りだけです。
扱う技術レベルは低いですが、同じように悩んでる初心者の方を勇気付けられれば幸いです。
今日は6日目。(2019/10/24)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day6.html

やったこと

ボタンを押せば・・・
スクリーンショット 2019-10-24 13.41.09.png

youtubeの日本人気動画の情報を取得できます。
が、この配列のようなもの↓を展開できず意味のない文字の羅列に・・・
(連想配列・多次元配列・hashやら理解できていない)
スクリーンショット 2019-10-24 13.40.42.png

こだわったところ

ajaxとapiという巨頭に果敢に挑みました。
が、彼らはとても強大でこだわるにも前提知識が不足しすぎておりこだわりきれませんでした。

 <script>
        test7 = document.querySelector("#test5");
        test8 = document.querySelector("#test6");
        test7.onclick = function () {
            var x = new XMLHttpRequest();
            x.onreadystatechange = function () {
                if (x.readyState == 4) {
                    var test = x.responseText;
                    test2 = JSON.parse(test);
                    Object.keys(test2).forEach(key => test8.innerHTML = ('key:' + key + ' value:' + test2[key]));
                }
            }
            x.open('GET', 'https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&regionCode=jp&key=[キー]');
            x.send();
        }
    </script>

onreadystatechangeメソッドはリソースの入手状況が変化したときに自動で動いてくれる優れものです。
いつ動くかは

if (x.readyState == 4)

で指定しています。4はリソースの取得が完了したという意味です。

感想

今日はてんで歯が立ちませんでした。
githubでソースを公開しているので、apiキーにhttpリファラーの制限をかけたらエラーになったり、せっかくデータを取得してきても表示させたい部分の表示方法がわからなかったり。
ただ、ajaxとAPIを扱えるようになればできることの幅がぐんと広がることは分かりました。
年末になる頃には必ずこの2大巨頭を倒してみせます!
今日はアレ、物語の序盤で強敵にコテンパンにされるみたいな日。最終回では見事にリベンジを果たすというフラグを立てただけ。というわけでへこたれずに頑張ります。

参考

上記2つのサイトはとても参考になりました。ありがとうございます。

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

[Node] 標準入力を扱うコードをテストする

ユーザからの入力を受け付けるCUIのようなコードのテストサンプル。

テスト対象コード

prompt.js
const readline = require('readline');

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

module.exports = function prompt(question) {
  return new Promise((resolve, reject) => {
    rl.question(question, answer => {
      rl.close();
      resolve(answer);
    });
  })
};

使う側はこんな感じ。

main.js
const prompt = require('./prompt');

prompt('Enter something: ').then(answer => {
  console.log(answer);
});

実行結果

% node ./main.js
Enter something: abc
abc

テストコード

spec/prompt-spec.js
const prompt = require('../prompt');

describe('prompt function', () => {
  it('returns answer', done => {
    prompt('question: ').then(answer => {
      expect(answer).toEqual('abc');
      done();
    });
    process.stdin.emit('data', 'abc\n');
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ts-node-devで起動されたかを調べる方法

プロセスがts-node-devで起動されたかを調べる方法です。

const isTsNodeDev = Object.keys(require.cache).some(path => /\/ts-node-dev\//.test(path))

ts-node-devで起動すると、nodeの-rオプションにts-node-devが渡されるため、ロード済みのモジュールに'ts-node-dev'の文字列があるかを調べれば良いわけです。

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

[React][Memo] Eventの処理 JavaScript から書き換えるとき 

 setSelected = (ev) => {
    const { value } = ev.target;
    this.setState({ selected: value });
  };
    setSelected = (ev:  React.FormEvent<HTMLInputElement>) => {
    ev.preventDefault();//イベントの伝播を制御
    const value = ev.currentTarget.value;
    this.setState({ selected: Number(value) });
  };

Reactでなにげなくつかっている TextFromとかの
e.target.value

React.FormEvent

preventDefault()は、内部的に発生しているイベント、ブラウザが持つデフォルトの動作を止める役割があるらしい。
勝手に発火してくれるの防止するため、念のためいれておく。

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

[React][Memo] Eventの処理 JavaScript からTypeScript書き換えるとき 

 setSelected = (ev) => {
    const { value } = ev.target;
    this.setState({ selected: value });
  };
    setSelected = (ev:  React.FormEvent<HTMLInputElement>) => {
    ev.preventDefault();//イベントの伝播を制御
    const value = ev.currentTarget.value;
    this.setState({ selected: Number(value) });
  };

Reactでなにげなくつかっている TextFromとかの
e.target.value

React.FormEvent型

preventDefault()は、内部的に発生しているイベント、ブラウザが持つデフォルトの動作を止める役割があるらしい。
勝手に発火してくれるの防止するため、念のためいれておく。

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

JavaScriptのデバッグメモ

概要

JSConfの動画を見て、便利そうだと思ったので個人的メモします。
動画はこちら

Chrome DevTools

console.log

変数をログで吐き出す時に、オブジェクトにするとキー・バリューで見れる。

const name = 'Hiroshi';
const age = 40;

console.log({ name, age });

Screen Shot 2019-10-23 at 22.20.56.png

色付け

console.log(`%c${name}`, 'background-color: red; color: white;');

Screen Shot 2019-10-24 at 10.17.17.png

※残念ながらオブジェクトは[object Object]になります。
Screen Shot 2019-10-24 at 10.19.54.png

console.table

const people = [
    { id: 1, name: 'Hiroshi', age: 40 },
    { id: 2, name: 'Tomozo', age: 76 },
    { id: 3, name: 'Momoko', age: 9 }
];

console.table(people);

テーブル形式で出力
Screen Shot 2019-10-24 at 10.21.24.png

console.group

function personData(person) {
    console.group('person');
    console.log(`name: ${person.name}`);
    console.log(`age: ${person.age}`);
    console.groupEnd();
}

const person = {name: 'Hiroshi', age: 40};
personData(person);

ログ出力を見やすく出来る。
Screen Shot 2019-10-24 at 10.22.52.png

Consoleタブ

Elementsタブで選択しているDOMエレメントを出力
現在選択しているものは\$0で表示出来る。
\$1〜$4まで履歴が見れる。
Screen Shot 2019-10-24 at 10.27.11.png

Consoleタブでのエレメントの取得

\$('selector') = document.querySelector('selector')
\$$('selector') = document.querySelectorAll('selector')
Screen Shot 2019-10-24 at 10.28.58.png

console.trace

関数がどこから呼ばれているかをトレース出来る。

function first() {
    console.log('first');
    console.trace();
}

function second() {
    console.log('second');
    first();
}

function third() {
    console.log('third');
    second();
}

third();

Screen Shot 2019-10-24 at 10.32.23.png

console.dir

const el = document.querySelector('div');
console.log(el)
console.dir(el);

エレメントのプロパティが全部表示される。
プロパティの書き換え可能
Screen Shot 2019-10-24 at 10.35.32.png

Node.js環境だとネストしたオブジェクトが内部まで表示可能。

const obj = {
    a: {
        b: {
            c: {
                d: {
                    e: {
                        f: { 
                            value: 1,
                            msg: 'Hello, world!'
                        }
                    }
                }
            }
        }
    }
}

console.log(obj);
console.dir(obj, { depth: null });

Screen Shot 2019-10-24 at 10.37.57.png

参考

Essential JavaScript debugging tools for the modern detective by Rebecca Hill | JSConf Budapest 2019

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

【jQuery】カレンダーの作り方

【jQueryでカレンダーを作ってみた】

jQueryの勉強がてらに、カレンダーを作成したので、要点メモ

今回作成する主なカレンダーの機能

・年、月、日、曜日の出力
・前月、次月の切り替え
・表示、非表示の切り替え

以上のようなカレンダーを作っていきます。

用意したHTMLはコチラ

index.html
<!doctype html>
<html lang="ja">
<head>
<meta name="robots" content="noindex"><!-- クローラー回避 -->
<meta charset="utf-8">
<title>カレンダー</title>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="js/schedule.js" type="text/javascript"></script><!-- ここで読み込まなくても可 -->
<script src="js/calendar.js" type="text/javascript"></script>
<!-- css -->
<link href="css/normalize.css" rel="stylesheet" type="text/css">
<link href="css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
<form method="post" action="#">
<input type="text" value="日付を入力してください" id="calendar_form">
<button type="submit">送信</button>
</form>
<div id="calendar"></div>
</body>
</html>

jQueryのバージョンは3.4.1

■ schedule.js

まず、休日設定用のデータを作成

schedule.js
// 休日指定したい日付を連想配列で指定しておく
var holidays = {
    // 休日設定していない場合のために、デフォルト値を設定
    'default':[
        {'1':[]},
        {'2':[]},
        {'3':[]},
        {'4':[]},
        {'5':[]},
        {'6':[]},
        {'7':[]},
        {'8':[]},
        {'9':[]},
        {'10':[]},
        {'11':[]},
        {'12':[]}
    ],// 設定例:2019年1月1,2,3日が休日の場合 '2019':[{1:[1,2,3]}]
    '2019':[
        {'1':[]},
        // 省略
        {'12':[]}
    ]
}

もっとスマートに描く方法はないものか、、、
連想配列の“キー”に“年”、“月”を設定し、該当の“日”を割り出せるように作成

■ calendar.js

カレンダー本体の作成

・カレンダー本体を出力するボタン作成
・操作ボタンを出力
・日付クリックで日付情報取得

calendar.js
/*global $*/
/* jQuery Contents */
$(function(){
    // カレンダー出力ボタンの設置
    distBtn.init();
    // カレンダーの表示非表示の制御
    var clickCount = 0;
    dBtn.on('click', function(){
        if(closeBtn != 'off'){
            clickCount++;
        }
        if(closeBtn == 'off'){
            clickCount = clickCount + 0;
        }
        if(clickCount%2 == 1){
            calendar.init();
            calendarPanel.init();
            clickToActions.init();
            dataDist.init();
            closeCalBtn.init();
            $('#calendar').css('display', 'none').fadeIn();
        }else{
            $("#btns").fadeOut(function(){
                $(this).remove();
            });
            calInner.fadeOut(function(){
                $(this).remove()
            });
        }
    });
});

// calendar close function
var closeCalBtn = (function(){
    function init(){
        closeBtn.on('click', function(){
            $("#btns").fadeOut(function(){
                $(this).remove();
            });
            calInner.fadeOut(function(){
                $(this).remove()
            });
            closeBtn = 'off';
        });
    }
    return {init:init}
})();

var calInner = '';
var closeBtn = '';
//////////////////////////// calendar close


// calendar dist btn function
var distBtn = (function(){
    function init(){
        var distBtn = '<div id="calBtn"><span>calendar</span></div>'
        $("#calendar").before(distBtn);

        dBtn = $("#calBtn");
    }
    return {init:init}
})();

var dBtn = '';
//////////////////////////// calendar dist

// 祝日設定 html上でなく、ここでも取得可
//$.get('js/schedule.js');

/*-----------------------
    variables
------------------------*/
var now, now_m, now_y;
now = new Date();
now_m = now.getMonth()+1;
now_y = now.getFullYear();
var weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
var prevText = 'prev';
var nextText = 'next';

var yearData = '';
var monthData = '';
var weekData = '';
var daysArea = '';
var prevdaysArea = '';
var nextdaysArea = '';

/*-----------------------
    clickToActions
------------------------*/
var clickToActions = (function(){
    function init() {
        var _cal = '';
        var dt = now;
        $("#btns a").on("click", function(){
            if($(this).is(":contains("+prevText+")")){
                _cal = $("#calInner");
                _cal.remove();
                dt.setDate(1);
                dt.setMonth(dt.getMonth()-1);
                now_m = dt.getMonth()+1;
                now_y = dt.getFullYear();
                calendar.init();
                dataDist.init();
                closeCalBtn.init();
            }
            if($(this).is(":contains("+nextText+")")){
                _cal = $("#calInner");
                _cal.remove();
                dt.setDate(1);
                dt.setMonth(dt.getMonth()+1);
                now_m = dt.getMonth()+1;
                now_y = dt.getFullYear();
                calendar.init();
                dataDist.init();
                closeCalBtn.init();
            }
        });
    }
    return {init:init};
})();

/*-----------------------
    calendar panel
------------------------*/
var calendarPanel = (function(){
    function init() {
        // 操作パネル追加
        var panelHtml = '<div id="btns"><a>'+prevText+'</a><a>'+nextText+'</a></div>';
        $("#calendar").append(panelHtml);
    }
    return {init:init};
})();

/*-----------------------
    calendar
------------------------*/
var calendar = (function(){
    function init() {
        // 月の末日出力
        function getMonthDays(year, month) {
            return new Date(year, month, 0).getDate();
        }
        // 引数が当月なら本日の日を返す
        var isToday = (function istoday(year, month){
            var _ = new Date();
            var toYear = _.getFullYear();
            var toMonth = _.getMonth()+1;
            if(toMonth == month && toYear == year){
                return new Date().getDate();
            }else{
                return false;
            }
        });
        // 当月日数分表示(祝日にはclass="holiday")
        var gmd = getMonthDays(now_y, now_m);
        var check = '';
        if(holidays[now_y]){
            check = holidays[now_y][now_m-1];
        }else{
            check = holidays['default'][now_m-1];
        }
        var daysHTML = '';
        for(var i=1; i<=gmd; ++i){
            if(check[now_m].indexOf(i) >= 0){
                daysHTML += '<li class="holiday"><span>' + i +'</span></li>';
            }else if(isToday(now_y, now_m) == i){
                daysHTML += '<li class="today"><span>' + i +'</span></li>';
            }else{
                daysHTML += '<li><span>' + i +'</span></li>';
            }
        }
        // 前月端数表示
        var monthFirstWeekNum = new Date(now_y, now_m - 1, 1).getDay();
        var beforeDt = new Date(now_y,now_m - 1, 0);

        var prevDays = beforeDt.getDate()+1;
        var prevHTML = '';
        for(var num=0; num<monthFirstWeekNum; ++num){
            prevDays = prevDays -1;
            prevHTML = '<li class="prev"><span>'+prevDays+'</span></li>'+prevHTML;
        }
        // 翌月端数表示
        var monthLastWeekNum = new Date(now_y, now_m, 0).getDay();
        var nextDays = 0;
        var nextHTML = '';
        for(var numb=monthLastWeekNum+1; numb<=6; ++numb){
            nextDays++;
            nextHTML += '<li class="next"><span>'+nextDays+'</span></li>';
        }

        var html = '';

        html = '<div id="calInner">';
        html += '<div id="closeBtn"><a>close</a></div>';
        html += '<div id="year"><p>'+now_y+'</p></div>';
        html += '<div id="month"><p>'+now_m+'</p></div>';

        var week = '';
        $.each(weekdays, function(index, value){
            week += '<li><span>'+value+'</span></li>';
        });

        html += '<div id="week"><ul>'+week+'</ul></div>';

        html += '<ul>' + prevHTML + daysHTML + nextHTML + '</ul></div>';

        // id="calendar"取得
        var cal = $('#calendar');

        // html出力
        cal.prepend(html);

        calInner = $("#calInner");
        closeBtn = $("#closeBtn");
        yearData = $("#year");
        monthData = $("#month");
        daysArea = $("#calInner>ul li:not(.prev, .next)");
        prevdaysArea = $("#calInner>ul li.prev");
        nextdaysArea = $("#calInner>ul li.next");
        weekData = $("#week>ul li");
    }
    return {init:init};
})();

// クリックで文字列取得
var dataDist = (function(){
    function init(){
        daysArea.on('click', function(){
            daysArea.removeClass("on");
            prevdaysArea.removeClass("on");
            nextdaysArea.removeClass("on");
            $(this).addClass("on");
            var otherDt = new Date(yearData.text(), monthData.text() - 1, $(this).text()).getDay();
            var otherWeek = weekdays[otherDt];
            var dateData = yearData.text() + "" + monthData.text() + "" + $(this).text() + "" + "(" + otherWeek + ")";
            $('#calendar_form').attr('value', dateData);
        })
        prevdaysArea.on('click', function(){
            daysArea.removeClass("on");
            prevdaysArea.removeClass("on");
            nextdaysArea.removeClass("on");
            $(this).addClass("on");
            var opDt = new Date(yearData.text(), monthData.text() - 2, $(this).text()).getDay();
            var opWeek = weekdays[opDt];
            var pMonth = monthData.text() - 1;
            var pdateData = yearData.text() + "" + pMonth + "" + $(this).text() + "" + "(" + opWeek + ")";
            $('#calendar_form').attr('value', pdateData);
        })
        nextdaysArea.on('click', function(){
            daysArea.removeClass("on");
            prevdaysArea.removeClass("on");
            nextdaysArea.removeClass("on");
            $(this).addClass("on");
            var onDt = new Date(yearData.text(), monthData.text(), $(this).text()).getDay();
            var onWeek = weekdays[onDt];
            var nMonth = parseInt(monthData.text()) + parseInt(1);
            var ndateData = yearData.text() + "" + nMonth + "" + $(this).text() + "" + "(" + onWeek + ")";
            $('#calendar_form').attr('value', ndateData);
        })
        yearData.on('click', function(){
            yearData.removeClass("on");
            $(this).addClass("on");
        })
        monthData.on('click', function(){
            monthData.removeClass("on");
            $(this).addClass("on");
        })
        weekData.on('click', function(){
            weekData.removeClass("on");
            $(this).addClass("on");
        })
    }
    return {init:init}
})();

これでhtmlの出力制御を行い、見た目をcssで整形

用途不明だが、カレンダーでフォームの日付入力するのに役立つかも…

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

JavaScript初歩知識2

JavaScriptを書く

    //main.js
    <script src="js/main.js"></script>
        window.onload=function(){
            //</html>までが終わった段階で自動的に動く
        }
        function fc(){
            //イベントが起きた時に動作する
            //<body>〜</body>の中
            <input type="button" onClick="fc()" />
        }

DOMツリー

DOMツリーって?

DOM(Document Object Model)

  • HTMLドキュメントの構造を記述するもので、これを使ってブラウザとのやり取りを行い、表示をコントロールする。

DOMのgetメソッド

tx=document.body.from.input.value;
  • DOMの一群には「getメソッド」が用意されており、これによりHTML要素を素早く指定することができる。(よく使われる一例。)
  • ただし、idなので一つ一つ指定しなければならない。 ## idを使った時
tx=document.getElementById("tx1");

classを使った時

        <form>
            <input type="text" id="tx1" class="c1" />
            <input type="text" id="tx2" class="c1" />
    </body>
</html>

classを使った場合は一気に取得できる。(c1を指定)

tx=document.getElementsByClassName("c1");
                        tx[0]id="tx1"
                        tx[1]id="tx2"

inputを指定

tx=document.getElementsByTagName("input");

これらを使ってgetmethod

<input type="text" id="tx1" class="c1" name="c1" />
<input type="text" id="tx2" class="c1" name="c1" />
tx=document.getElementsByName("c1");

ツリー状に展開

  • ドット(.)で繋ぐことで使用できる。(inputを入れなくて良い)
input   type
        value
        id
        name
        size
        style //<input type="text" style="height:15px;" />

  • textboxをターゲットとする時,プログラムが動くとボタンになる。
(ex)
document.getElementById("tx1").type="button";

                    .id="tx3";
                    .size="50";
                    .style.スタイルシートのプロパティを書く=+"単位";
  • ボタンを押すとtextboxの左余白が150pxになる
document.getElementById("tx1").style.left="150px";
  • ボタンを押すと文字色が緑になる
document.getElementById("tx1").style.color="green";
  • background-imageのような時マイナスと認識されないために
document.getElementById("tx1").style.backgroundImage="images/p1.png";
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javascriptの同期/非同期って何?関数Promise、Async、Await

はじめに

皆さん、こんにちは!Webシステム開発エンジニアの蘭です!
今日はJavascriptの同期/非同期関数:【Promise、Async、Await】について語りたいと思います。

・そもそも同期と非同期処理って何?

結論から言いますと、

・同期処理:あるタスクが実行している間、他のタスクの処理は中断される方式です。

・非同期処理:あるタスクが実行をしている際に、他のタスクが別の処理を実行できる方式になります。

想像してみましょう

 仮にあなたは今SNSのユーザーだとします、普段は忙しいパパでも、休みを取り、家族との記念で海外旅行に行きました。
 写真撮影はいいカメラを使い、高画質な画像やビデオばかり。
 そしてそれをSNSに投稿した後、ネットのせいか30分ぐらい処理待ちの状態、画面も真っ白になり、不安しか積もるばかりで、投稿処理が終わらないと何もできない...

何故ユーザーがずっと処理待つことになるのか

上記の例で見てみよう

■今までのフロントエンド処理

  1. 投稿画面表示:ブラウザーからサーバー画面要求(GET)やデータ送信(POST)し、サーバーからHTMLが返却され、画面を表示。
  2. SNSに投稿:ブラウザーからサーバー画面要求(GET)やデータ送信(POST)します。
  3. 写真や投稿内容の保存:サーバーでデータの新規追加処理が行われます。
  4. 投稿内容をSNSに表示:サーバーで処理が完了後、HTMLをブラウザーに返却し、ブラウザーにHTMLを描画して投稿内容を画面に表示。

alt
マルチページアプリケーションの通信パターンはこのワンセットの繰り返しです。
これではサーバーの実行が完了しないと、画面の更新はされない状態になり、ユーザーは画面が更新されるまで、ずっと待つ状態となります。

■近年はSPAで非同期処理

Webサイトで近年SPA(シングルページアプリケーション)の導入が多くなりました。何故なら非同期に複数の処理ができるからです。

  1. 投稿画面表示:ブラウザは最初に画面を要求し、サーバは HTML を返却し、画面表示。
  2. バックで投稿処理をし、同時に画面更新:ページ遷移せずに、Ajax等でサーバーにデーター要求や送信をし、同時にJavascriptにより画面の更新部分だけを書き換えて、ユーザーは更新後の画面が見られます。
  3. 後はAjaxでサーバーのやり取り結果を待つ事になりますが、画面が真っ白等、画面更新処理が中断されることは有りません。

以下の利点があります。
  ・更新部分だけ書き換えるので、画面更新処理向上
  ・複数処理を同時に行うため、処理速度が速くなる
  ・ユーザーの使用感も改善

alt

もちろん、同期と非同期処理にはメリットとデメリットがあります。
こちらは開発内容に応じて調整しましょう。
  同期処理と非同期処理

Javascriptの同期/非同期

以下の内容をこちらを引用。

簡単な同期処理の例を見てみましょう。

同期.js
console.log(1);
console.log(2);
console.log(3);

ログを3つ表示するだけのシンプルな処理です。
3つの関数上からはlog(1)、log(2)、log(3)の順番に処理されます。

1
2
3

上から順に処理していくのは同期処理です。

非同期処理

非同期.js
console.log(1);
setTimeout(function(){console.log(2)}, 1000); 
//console.log(2)の処理を1秒後実行する
console.log(3);
1
3
2

 ・先ず、こちらsetTimeout()はコールバック関数と呼びます。
Javascriptではコールバック関数は非同期処理になります。(※コールバック関数は後ほど説明します。)
 ・setTimeout()が呼び出された後、内部の関数(console.log(2))は別世界、別次元で動いてると思ってください。ですのでsetTimeout()と別に関数内部は非同期に処理されます。setTimeout()は呼び出すだけで、表は実行完了となります。
 ・setTimeout()は内部の処理は別として実行完了となり、次のconsole.log(3)の処理を開始します。
 ・最後にsetTimeout()内部で、一秒後にconsole.log(2)が実行された為、実行完了時間がconsole.log(3)より遅く完了しました。
 ・console.log(2)実行完了後に2という結果が表示されました。

⇛console.log(3)を処理していると同時にconsole.log(2)を処理する、これが非同期処理です。

コールバック関数って何?

・別の関数に呼び出してもらうための関数
・簡単に言うと関数の引数に関数が指定されてる。

コールバック.js
function Callback(B){
  ...
}
//関数Bが関数Aの引数に指定されてる
function B(){
  ...
}

ここで、何?関数を引数として指定ってどういう意味?

実はJavascriptでは数字、ブーリン、文字、配列、関数全て値です。

以下の内容はこちらを引用。

値.js
const numValue = 100;
const strValue = "私は値です!";
const boolValue = true;
const arrayValue = [1, 2, 3];
const objValue = { key: 'value' };
関数も値.js
const addFunc = function(a, b){
   return a + b;
}
console.log(addFunc(2, 3));
//5が表示される。
//addFuncも値で、引数として指定できます。

ここで注意、関数を値として扱う時は括弧()は付けません

関数を値として指定する時、括弧付けない.
// 自分で定義する関数
function add(a, b) {
  return a + b;
}

// 定義した関数を変数に入れる
const addFunc = add; // カッコはつけない!

// JavaScriptに標準でついてる関数でもできる
const myMax = Math.max; // max関数をmyMaxという変数に入れる

// 呼び出してみる
console.log(add(1, 2), addFunc(1, 2)); // どっちも3になる
console.log(Math.max(1, 2), myMax(1, 2)); // どっちも2

⇛関数に括弧があれば処理の呼び出しになり、括弧がなければ値として扱われます。

では、本題に戻ります。
コールバック関数とは高階関数に渡すための関数です。

コールバック関数
// 関数を2回実行する関数!!
function doTwice(func) {
  func(); // 1回目!
  func(); // 2回目!
}

// あいさつを2回実行する
doTwice(sayHello);

//あいさつをする
function sayHello() {
  console.log('Hello!');
}

上記を見ますとdoTwiceに関数sayHello()が引数として指定されてますね、このdoTwiceがコールバック関数です。
これで何故setTimeout()がコールバック関数なのか理解できましたね。

非同期でコールバック関数地獄

以下の内容はこちらを引用。

先ず非同期のコールバック関数を作ります

今て手元に100ポイントがあります。
りんご一つ買うと40ポイント減ります。
40ポイント以下になるとりんごが買えなくなり、エラーが表示されます。

コールバック関数.js
var asyncBuyApple = function(restPoint, callback){
  setTimeout(function(){
    if(restPoint >= 40){
      callback_pointCalculate(restPoint-40, null);
    }else{
      callback_pointCalculate(null, '金額が足りません。');
    }
  }, 1000);
}

コールバック関数を複数非同期処理

コールバック関数地獄.js
asyncBuyApple(100, function(restPoint, error){
  if(restPoint !== null){
    console.log('1回目の残りのポイントは' + restPoint + '円です。');
    asyncBuyApple(change, function(restPoint, error){
      if(restPoint !== null){
        console.log('2回目の残りのポイントは' + restPoint + '円です。');

        asyncBuyApple(change, function(restPoint, error){
          if(restPoint !== null){
            console.log('3回目の残りのポイントは' + restPoint + '円です。');
          }
          if(error !== null){
            console.log('3回目でエラーが発生しました:' + error);
          }
        });
      }
      if(error !== null){
        console.log('2回目でエラーが発生しました:' + error);
      }
    });
  }
  if(error !== null){
    console.log('1回目でエラーが発生しました:' + error);
  }
});

上記の例を見ますと、ネストが深く、これが仮に100回の処理がある場合、まさに大変なことになり、バグも見つかりにくなります。:cold_sweat:

コールバック関数地獄から抜け出そう:Promise

コールバック関数の問題を解決するために、Javascriptでは【Promise】という仕様が登場しました。:clap_tone1::clap_tone1:

※PromiseはES6対応ですので、ES5の場合はbluebird等のライブラリーが必要になります。

Promiseの関数を見てみましょう!

Promise.js
var promiseBuyApple = function(restPoint){
  return new Promise(function(resolve, reject){
    if(restPoint >= 40){
      resolve(restPoint-40);
   //実行成功、resolveをPromiseコンストラクタに渡す
   //return restPoint-40 と同じ意味です。
    }else{
      reject('ポイントが足りません。');
   //実行失敗の場合、rejectをPromiseコンストラクタに渡す
   //return 'ポイントが足りません。' と同じ意味です。
    }
  });
}

・Promise関数はPromiseオブジェクトを返します。
・成功の場合は「resolve」、失敗の場合は「reject」関数をPromiseコンストラクタに渡します。

Promise関数複数処理

Promiseではresolveやrejectをthenメソッドに渡します。

Promise_then.js
promiseBuyApple(100).then(function(restPoint){
  console.log('残りのポイントは' + restPoint + 'です');
  return promiseBuyApple(restPoint);
}).then(function(restPoint){
  console.log('残りのポイントは' + restPoint + 'です');
  return promiseBuyApple(restPoint);
}).then(function(restPoint){
  console.log('残りのポイントは' + restPoint + 'です');
}).catch(function(error){
  console.log('エラーが発生しました:' + error);
});

 ・Promiseを使うことで、コードが綺麗に整いました。
 ・最初の関数が実行した後、thenメソッドのコールバック中でreturnされた結果が次のthenメソッドのコールバックに引数として渡されます。
 ・then(function(restPoint)restPointに成功すればresolveが渡されて、失敗の場合はrejectが渡されます。
 ・エラーの場合はcatchメソッドが実行されます。

簡単に言うといかの感じになります。

以下の内容はこちらを引用。

コールバック関数地獄
A(function(){
  B(function(){
    C(function(){
      console.log('Done!');
    });
  });
});
Promiseを使用後
A().then(B).then(C).then(function(){
  console.log('Done!');
});

更に、async、awaitはPromiseを簡単に扱える仕組み

・内容は以下を引用しました。
Promiseが分かれば簡単!async, await
【JavaScript入門】5分で理解!async / awaitの使い方と非同期処理の書き方

Promiseだけでも同じ処理ができますが、async、awaitを使うとよりスッキリしたコードになります。

基本的な構文について

まずは、基本の構文を見てみましょう!
「async」は「function」の前に記述するだけで非同期処理を実行できる関数を定義できます。

async function() { }

このようにasyncを記述しておくと、この関数はPromiseを返すようになります。また、「await」はPromise処理の結果が返ってくるまで一時停止してくれる演算子となります。

await Promise処理
・ここで注意ですが、「await」は「async」で定義された関数の中でしか使えません。今では「await」と「async」は一緒に使われてることが多いですね!

実際の例を見てみましょう。
以下がPromiseの処理(非同期)です。

基本なPromise処理
function myPromise(num) {
  return new Promise(function(resolve) {

    setTimeout(function() { resolve(num * num) }, 3000)

  })
}

非同期処理で3秒間かかる処理を記述し、引数numで受け取った値を2乗した結果を返す単純な処理です。

以下は「then」を使わずに、「await/async」を利用します。

Promise処理でasyncを使用
async function myAsync() {

    const result = await myPromise(10);

    console.log(result);

}

myAsync();

この例では、asyncを付与することで非同期処理の関数を作成していますね。その関数内でPromise処理を記述している「myPromise()」の前に「await」を付与しているのが分かります。

これにより、3秒後に結果が返ってくるPromise処理を一時的に待つことになり、結果を取得した瞬間に関数内の処理が続行されるのです。実行結果には引数に与えた「10」が2乗された値「100」が取得できていますね。

「then」を使わずに非同期処理を複数実行する方法

Promise処理でthenを使用
myPromise(10).then(function(data) {

    console.log(data);
    return myPromise(100)

}).then(function(data) {

    console.log(data);
    return myPromise(1000)

}).then(function(data) {

    console.log(data);

})

同じ処理をasync、awaitで書くと

Promise処理でasyncを使用
async function myAsyncAll() {

    console.log(await myPromise(10));
    console.log(await myPromise(100));
    console.log(await myPromise(1000));

}

myAsyncAll();

「Promise.all」と「async/await」の並列処理

「async/await」を使い、Promise処理が終わり、resolve(reject)が返された後にまた次のPromise処理を実行するのもいいですが、例えばPromise処理がAPIでデータを取得する処理としよう、仮に取得するAPIが合計個あるとします、API処理で1分かかり、全てのAPIを取得するだけで20分かかります。:point_up:
これは非同期で一括取得したほうが速いですよね!

ここで「Promise.all」と「async/await」の並列処理を紹介します。

まずは「Promise.all」を使った一括処理

Promise.all([
    myPromise(10),
    myPromise(100),
    myPromise(1000)
]).then(function(data) {

    console.log(data);

})

上記を見てみますと、一括処理はできたが、個別の処理結果を見たい時は少し不便ですよね。

ここで「async/await」を使った一括処理

nc function myAsyncAll() {

    const r1 = myPromise(10);
    const r2 = myPromise(100);
    const r3 = myPromise(1000);

    console.log(await r1, await r2, await r3);

}

myAsyncAll();

上記を見ますと、Promise処理を全て起動させて変数に格納し、結果を取得できます。

おまけに、以下はSPAで一括API取得処理の例

一括処理
async function xxx() {
  let res1;
  let res2;
  let res3;
  try {
    [res1, res2, res3] = await Promise.all([
      axios.get('http://.../get1').catch(e => { throw 'get1 error '+e.message}),
      axios.get('http://.../get2').catch(e => { throw 'get2 error '+e.message}),
      axios.get('http://.../get3').catch(e => { throw 'get3 error '+e.message}),
    ]);
  } catch(err) {
    console.log(err);
    return; // 1つでもエラーになったら、関数を抜ける
  }

  // 3つ全てが正常データを取得できたとき、以下を実行
  console.log(res1);
  console.log(res2);
  console.log(res3);
}

まとめ

いかがでしょうか。
今回はJavascriptの同期/非同期、コールバック関数、Promise関数、Async/Await、非同期一括処理についての簡単な紹介をしました。
また今後も現場で活用していただければ嬉しいです!:relaxed:

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

iPad上のSafariでアップルペンシルの座標検知精度が低い

これはWebブラウザ上で動くお絵かきソフトの作成過程でぶつかった問題です。
回避策が無いかと試行錯誤を繰り返して頭がこんがらがってきたので、整理と共有の為に書きました。
私はブラウザアプリ開発についてはど素人なので、根本的な勘違いをしているかもしれません。
識者の方がいましたらご指摘お願いします。

一般的なペンタブレットの座標精度

デジタルお絵描きには欠かせない、ペンタブレットや液晶タブレットというものがあります。
タブレットの座標検知はとても精密で、一般的には画面上の1ピクセルの10分の1以下という細かい精度で座標を知ることができます。
コンピュータで絵を描く上では、この座標精度がとても重要です。

アップルペンシルの座標精度

ネイティブアプリに関しては、高精度な座標検知APIがあるようです。
https://qiita.com/usagimaru/items/69b3acfa251f7d191334

Safari上でのアップルペンシルの座標精度

ここが本題。
少なくとも標準的なWebサイトの作り方では、アップルペンシルの座標精度が十分には発揮されませんでした。
画面解像度が1536x2048pixelのiPadで試したところ、座標精度は768x1024程度のようです。

Webブラウザ上でのアップルペンシルの座標検知精度は、CSSピクセル単位に丸められているのではないかというのが私の推測です。
(2019/10/24: iPadOS 13.1.3で実験しました)

精度低下を回避するために試みた事

Viewportの設定を書き換える

<meta name="viewport" content="width=device-width, initial-scale=0.5">
htmlに上記の設定を書いてみると、画面の物理ピクセル単位の座標検知が行えるようになりました。
しかしこれだと全ての画面上の表示要素が2分の1サイズになってしまいます。
スライダー等コントロール要素を含めて元の大きさで表示するために、
.style.transform = 'scale(2,2)';
このような記述を追加し、2倍に拡大することで対処しました。
(拡大することで、UIの表示は荒くなってしまいます)

Appleさんお願いです!Webブラウザ上でも小細工いらずで高精度な座標検知ができるようにしてください!

実験中のブラウザお絵描きアプリのURL

アップルペンシルの精度が出ないバージョン:https://minordaimyo.net/8bitpaintweb/
色々弄っている最中の、精度が出るバージョン:https://minordaimyo.net/8bitpaintweb_test/

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

LINE BOTとlotで、いつでもレベルアップできるようになった!

はじめに

アニメ・ゲーム・漫画を愛するタレント、喜屋武ちあきです。

2019年7月から16年間いた事務所から独立し、フリーランスとして活動しはじめました。
色々新しいことに自由に挑戦できる環境の中、もともとオタクとして興味があったプログラミングにもチャレンジしています。

今月からnodeでjavasqriptを始めたばかりの全くの素人で、まだ学校で言われたことをそのままトレースしている段階ですが、少しずつコードを読めるようになりたいです。

obnizをゲット

「obniz」という、IOTの基盤があります。この基盤をインターネットとつなげると、それだけでnodeから色々な指示を送ることができて、すごく面白いんです!!!

LEDライトやスピーカー、超音波センサーなど色々差し込んで遊ぶ中で、私が一番気になったのは音の出る「スピーカー」でした。

正直、まだ、プログラミングは「辛い」

これ、動機の話なんですが。
本当にまだ何も分からない中で色々なことにチャレンジしていますが、意味不明のエラーは出るし、新品のLEDなのに光らないし(これはモノの問題かもですが)、なかなか心をへし折られることばかり。

でも、ちょっとずつ成長していることを、誰かに認めてもらいたいし、成長しているということをもっと感じたい!!!

そう思ったので、LINEbotとobnizをつなげて、頑張りを認めてレベルアップさせてくれるbotを作ることにしました。

私の目的とするbotは

「今日は、朝5時起きでお仕事に行って頑張ったよ!」
など、”頑張った”という文字に反応して
「よく頑張ったね!勇者きゃんちはレベルが1上がりました。」
と褒めて、さらに某国民的RPGのレベルアップ音を鳴らしてくれるbotです。

でっきるっかな、でっきるっかな♪

開発環境と用意したもの

環境
node.js v11.10.0
mac os 10.15
Visual Studio Code v1.39.1

用意
Obniz ボード
圧電スピーカー(圧電サウンダ)(13mm)PKM13EPYH4000-A0
USB2.0ケーブル(A-microBタイプ)50cm

じゃ、やってみようか

参考にした記事

「1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest」

「老眼と加齢性難聴のチェックができるLINE Bot×Iotの作成」

「超初心者がLineBot×Obnizでスピーカから音を出してみた。」

まず、一番上の記事を参考に、LINE BOTの設定を行います。
何回か同じことを繰り返しているうちに、LINE BOT自体作るのは慣れが出てスムーズに行くようになりました。個人的にはこれがすでに奇跡

続いて、2番目の記事を参考に、Aコードを埋め込み、スピーカーとLINEbotを繋ぎます。

シンプルに、「頑張った」というワードに文字と音を同時に送るということがしたかったので、3番目の記事からコードをお借りして、文字を変更。

さらに、一番時間をかけた、レベルアップ音の指定を入力します。

以下、functionからのコードです。
頑張ったレベルアップ音の指定を!!!見て!!!

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  var hz = Number(event.message.text);
  speaker.play(1396); // ファ
  obniz.wait(130); 
  speaker.play(0); // 休符
  obniz.wait(10);
  speaker.play(1396);// ファ
  obniz.wait(130);
  speaker.play(0); // 休符
  obniz.wait(10);
  speaker.play(1396);// ファ
  obniz.wait(130);
  speaker.play(0); // 休符
  obniz.wait(10);
  speaker.play(1396); // ファ
  obniz.wait(130);
  speaker.play(0); // 休符
  obniz.wait(70);
  speaker.play(1244);// ミ
  obniz.wait(210);
  speaker.play(0); // 休符
  obniz.wait(50);
  speaker.play(1567);// ソ
  obniz.wait(200);
  speaker.play(50); // 休符
  obniz.wait(30);
  speaker.play(1396); // ファ
  obniz.wait(1000);
  speaker.stop();
  let replyText = '';
  if(event.message.text === '頑張った'){
    replyText = 'よく頑張ったね!勇者きゃんちはレベルが1上がりました。';
  }else{
    replyText = hz;
  }


  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: replyText
  });
}

音階はともかく、スタッカート機能なんてついているのかわからないので、音の長さと休符(音のならない0ヘルツ)の設定だけでレベルアップ音を作りました。

そして、早速Webhookを使ってトンネリング。
LINEbotに話しかけたところ・・・

S__64856132.jpg

不思議なことに、どんな言葉にも、レベルアップ音で返すようになりました。

そして、シンプルに"頑張った"と打つと、

「よく頑張ったね!勇者きゃんちはレベルが1上がりました。」
と褒めてくれるようになりました。

IMG_0498.jpg
画像、おっきいな・・・;

ま、何を打ってもレベルが上がる音がするなら、それはそれでいっか!!!と思います。
この後、「頑張った」というワードが言葉のどこかに入っていれば自動で返信できるようにと頑張ったのですがうまくいかず、何を送っても返信もなく、レベルアップ音が虚しく鳴り響くという形になってしまったので、元に戻しました。

レベルアップ音の動画をUPしました!こちらからどうぞ。

まとめ

電脳の世界のみで完結するというのがまだわかりづらく、こうしてIOTとして繋げてみると、工作感が出てきてわかりやすく楽しいと感じました。

スピーカーから発せられるレベルアップ音もゲームっぽくて可愛いーーと思いましたが、元祖1ってすでに単音じゃないので、30年も後なのに・・・!?!?と思うとエモいです。

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

Javascript JQuery

jquery
<html>
<head>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
</body>
  <input type="text" id="text1">
  <button id="register">追加</button>
  <button id="search">検索</button>
  <table id='tbl'></table>

<script>
  let items = [];

  function createTable(items) {
    let tbl = '';
    for (const i in items) {
      tbl += `<tr><td>${items[i]}</td><td><button class="item" id="update-${i}">編集</button></td><td><button class="item" id="delete-${i}">削除</button></td></tr>`;
    }
    $('#tbl').removeAttr('border');
    if (tbl) { 
      $('#tbl').attr('border','1');
    }
    $('#tbl').html(tbl);
  }

  $('#register').click(() => {
    items.push($('#text1').val());
    createTable(items);
  });

  $('#search').click(() => {
    const val = $('#text1').val();
    const items2 = items.filter(i => i.indexOf(val) >= 0);
    items = items2;
    createTable(items);
  });

  $(document).on('click', '.item', function() {
    const tmp = $(this).attr('id').split('-');
    const type = tmp[0];
    const id = tmp[1];
    if (type === 'update') {
      items[id] = $('#text1').val();
    } else if (type === 'delete') {
      items.splice(id, 1);
    }
    createTable(items);
  })
</script>
</html>


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