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

Medium Editorでカーソルを指定の位置に設定する方法

環境

Rails6 (6.0.1)
ruby 2.7.0

参考

Change Cursor Position to particular para Medium Editor

MediumEditorとは?

有名ブログサービスのMediumで使われているWYSIWYGエディターです。

※RailsにMediumEditorを導入する際はこちらの記事を参考にしました。

なぜカーソル位置を指定の場所に移動させるのか?

インデントが適用されたpタグ内でテキストを入力し文字確定でEnterを押すとなぜかカーソル位置がpタグ内の先頭に移動してしまうバグが発生。このバグを解消させるために、元の位置にカーソルを戻す方法がないか調べた結果、カーソル位置を指定の場所に移動させる方法があったので、この記事にまとめました。

カーソル位置を指定の場所に移動させる方法

MediumEditor.selectionが持つmoveCursor()メソッドを使うことで実現できた。

MediumEditor.selection.moveCursor(document, node, offset);

node: カーソルの移動先となるノード
offset: カーソルの位置(ノード内の文字列の先頭から〜番目)

今回の場合は文字確定時のカーソルがあるノードを取得する必要があったので
getSelection()メソッドのfocusNodeオプションを利用しました。

target_node = document.getSelection().focusNode;

あとはoffsetを指定することで任意の場所にカーソルを移動させることができます。

テキストノードの先頭にカーソルを移動させる

target_node = document.getSelection().focusNode;
MediumEditor.selection.moveCursor(document, target_node, 0);

テキストノードの先頭からn番目にカーソルを移動させる
5番目に移動させる場合

target_node = document.getSelection().focusNode;
MediumEditor.selection.moveCursor(document, target_node, 5);

結果

今回のバグ対応は、文字確定時のカーソル位置に再びカーソルを戻すという特殊な対応だったので、getSelection()メソッドのfocusOffsetオプションでノードの先頭を基準に現在のカーソル位置を取得しました。

offset = document.getSelection().focusOffset;

上記の方法でnodeとoffsetを取得できたので、カーソル位置が変わってしまった後に下記の処理を走らせることで無事バグの解消はできました。

MediumEditor.selection.moveCursor(document, target_node, offset);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのformをBootstrapからMaterial UIに移行する際の注意点

Reactを勉強する際に、YouTubeやUdemyなど様々なところで超有益な動画がたくさんあります。特に英語での解説がされている動画をみていると、短い時間で綺麗なデザインで、かつ機能もある程度しっかり実装されていることが多いです。

このような短時間で綺麗なデザインのwebアプリを作っている動画では、React Bootstrapが使われていることが多いです。

ただ、「最近はMaterialデザイン流行ってそうだし、MaterialUIでも導入してみるか」と言うことで、後から作り替えるなんて方もいるかもしれません。

この記事ではそんな「BootstrapからMaterial UIに移行したい」と考えている方で、その中でも特に移行したい機能の一部にFormがあると言う方向けになっています。

TextFieldでハマった?

この記事で一番伝えたいのは、BootstrapからMaterial UIに移す時に、valueはしっかり設定しようと言うところなのですが、そこについて深掘ります。

以下のコードをご覧ください

PostForm.jsx
//一部省略してます
export default function PostForm() {
    const classes = useStyles();
    const [error, setError] = useState("")
    const [loading, setLoading] = useState(false)
    const history = useHistory()
    const titleRef = useRef()
    const contentRef = useRef()
    const { createPost, currentUser } = useAuth()

    const handleSubmit = async(e) => {
      e.preventDefault();

      setError("")
      setLoading(true)
      const authorName = currentUser.username
      createPost(titleRef.current.valur, contentRef.current.value, authorName)
      history.push("/")
      setError("投稿に失敗しました")
      setLoading(false)
    }

    return (
        <>
          {error && <Alert variant="danger">{error}</Alert>}
              <h2 className={classes.header}>新規投稿を作成</h2>
              <form className={classes.root} noValidate autoComplete="off" onSubmit={handleSubmit}>
                  <div className={classes.postFormBox}>
                    <TextField 
                      type="text" 
                      label="タイトル" 
                      ref={titleRef} 
                      className={classes.postFormTextField} 
                      multiline={true}
                      required
                    />
                    <br/>
                    <TextField 
                      type="text" 
                      label="内容" 
                      ref={contentRef} 
                      className={classes.postFormTextField} 
                      multiline={true}
                      rows={4} 
                      required 
                    />
                    <br/>
                    <Button 
                      variant="contained" 
                      type="submit" 
                      color="primary" 
                      disabled={loading}
                    >
                      投稿
                    </Button>
                  </div>
              </form>      
        </>
    )
}

タイトル投稿内容を入力するためのTextFieldにsubmit用のボタン、さらにはボタンが押された時に関数handleSubmitが呼ばれ、handleSubmit内の処置が走ると言うシンプルなformのコンポーネントです。

これをみると一見、動きそうに見えますが、handleSubmitのcreatePostの処理でエラーがおきます。

どのようなエラーかと言うと、定義されていない値を参照してしまうエラーで、僕の場合はfirestoreを使っていたので、×
Unhandled Rejection (FirebaseError): Function DocumentReference.set() called with invalid data. Unsupported field value: undefined
と言うエラーが出ましたが、普通のJavaScriptならUncaught TypeError: Cannot read property ‘プロパティ名’ of undefinedと言うエラーになります。

これはundifinedの値を参照しようとしている時に起こります。

そうです、上記のコードではTextField、つまりinputのvalueがundifinedとして返されてしまいます。

なので、inputでvalueをせってしてあげる必要があります。

解決策

今回はReactを利用しているので、useStateでそれぞれのvalueのstateを作る必要があります。
上記の例で言うなら、タイトルにはtitle、内容にはcontentと言う名前でstateを持たせます。

さらにそれらのstateの変更の度にコンポーネントを再レンダリングするためのuseCallbackを使ったinputContentとinputTitleを加えたコードが以下になります。

PostForm.jsx
export default function PostForm() {
    const classes = useStyles();
    const [error, setError] = useState("")
    const [title, setTitle] = useState("")
    const [content, setContent] = useState("")
    const [loading, setLoading] = useState(false)
    const history = useHistory()
    const titleRef = useRef()
    const contentRef = useRef()
    const { createPost, currentUser } = useAuth()

    const inputTitle = useCallback((event) => {
      setTitle(event.target.value)
    }, [setTitle]);

    const inputContent = useCallback((event) => {
      setContent(event.target.value)
    }, [setContent]);

    const handleSubmit = async(e) => {
      e.preventDefault();

      setError("")
      setLoading(true)
      setTitle("")
      setContent("")
      const authorName = currentUser.username
      createPost(title, content, authorName)
      history.push("/")
      setError("投稿に失敗しました")
      setLoading(false)
    }

    return (
        <>
          {error && <Alert variant="danger">{error}</Alert>}
          <Card className={classes.card}>
            <CardContent>
              <h2 className={classes.header}>新規投稿を作成</h2>
              <form className={classes.root} noValidate autoComplete="off" onSubmit={handleSubmit}>
                  <div className={classes.postFormBox}>
                    <TextField 
                      type="text" 
                      label="タイトル" 
                      ref={titleRef} 
                      className={classes.postFormTextField} 
                      multiline={true}
                      required
                      //大切なのはここ!!valueをちゃんと設定する!!
                      value={title}
                      onChange={inputTitle}
                    />
                    <br/>
                    <TextField 
                      type="text" 
                      label="内容" 
                      ref={contentRef} 
                      className={classes.postFormTextField} 
                      multiline={true}
                      rows={4} 
                      required 
                      //大切なのはここ!!valueをちゃんと設定する!!
                      value={content}
                      onChange={inputContent}
                    />
                    <br/>
                    <Button 
                      variant="contained" 
                      type="submit" 
                      color="primary" 
                      style={{ width: "100%", marginTop: "15px" }}
                      disabled={loading}
                    >
                      投稿
                    </Button>
                  </div>
              </form>  
            </CardContent>
          </Card> 
          <Link to="/" className={classes.cancel}>
            キャンセル
          </Link>          
        </>
    )
}

まとめ

ちゃんとvalueを設定しよう

ついでですが、buttonのtypeは忘れずに設定しましょう。僕はsubmitを忘れて、エラーも出ずほんと数時間無駄にしました...w

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

スライドパズルの制作

スライドパズルの制作

1〜24までの数字と数字の振られていないパネルで、数字の振られていないパネルにパネルを移動させて数字を並べていくゲームです。

<body onload="init()">
  <p>パネル</p>
  <table id="table"></table>
</body>

まず、body要素内にtable要素をidを指定して記述します。

  <script>
    let tiles = [];
    function init() {
      let table = document.getElementById("table");
      for(let i = 0; i < 5; i++) {
        let tr = document.createElement("tr");
        for(let j = 0; j < 5; j++) {
          let td = document.createElement("td");
          index = i * 5 + j;
          td.index = index;
          td.value = index;
          td.textContent = index == 0?"":index;
          td.className = "tile";
          td.onclick = clicked;
          tr.appendChild(td);
          tiles.push(td);
        }
        table.appendChild(tr);
      }
      for(let i = 0; i < 10000; i++) {
        clicked({target: {index: Math.floor(Math.random() * 25)}});
      }
    }

次にscript要素内に配列tilesをグローバル変数で指定して、init関数を作成します。
init関数内では5×5になるようにtable要素内に二次元配列を展開します。(下記の部分)「tiles.push(td)」の部分で作成したtd要素を配列tilesに追加していきます。

let table = document.getElementById("table");
    for(let i = 0; i < 5; i++) {
      let tr = document.createElement("tr");
      for(let j = 0; j < 5; j++) {
        let td = document.createElement("td");
        tr.appendChild(td);
        tiles.push(td);
      }
      table.appendChild(tr);
    }

td要素にはそれぞれプロパティを作成していきます。
indexには0〜24までの数字を代入していき、tdのindex要素とvalue要素に代入します。textContentには三項演算子でindexの値が0のときは空をそれ以外の値のときはindexを代入します。
クリックしたときに「clicked関数」が発火するようにします。

  index = i * 5 + j;
  td.index = index;
  td.value = index;
  td.textContent = index == 0?"":index;
  td.className = "tile";
  td.onclick = clicked;

※三項演算子
「index == 0?"":index」であれば、indexの値が0のときは?の後にある""を、それ以外のときは:の後にあるindexを代入する。

    function clicked(e) {
      let i = e.target.index;
      if(i % 5 !== 0 && tiles[i - 1].value === 0) {
        swap(i, i - 1);
      }
      if(i > 4 && tiles[i - 5].value === 0) {
        swap(i, i - 5);
      }
      if(i % 5 !== 4 && tiles[i + 1].value === 0) {
        swap(i, i + 1);
      }
      if(i < 20 && tiles[i + 5].value === 0) {
        swap(i, i + 5);
      }
    }

clicked関数は引数に「e」を指定します。「let i = e.target.index」でクリックしたtd要素(パネル)のindexの値を取得します。
空のパネルがクリックしたパネルの左、上、右、下、それぞれに配置されているときで処理を変えます。swap関数をそれぞれ行うようにします。(iがクリックしたパネルで「i - 1」がiの左、「i - 5」がiの上、「i + 1」がiの右、「i + 5」がiの下)

    function swap(x, y) {
      let i = tiles[x].value;
      tiles[x].value = tiles[y].value;
      tiles[x].textContent = tiles[y].textContent;
      tiles[y].value = i;
      tiles[y].textContent = i;
    }

swap関数はクリックしたパネルと空のパネルの配置を変更する関数です。valueプロバティとtextContentの値を交換します。

下記が全体のコードです。ファイルにコピーして実行すれば試せます。

<!DOCTYPE html>

<html>

<head>
  <meta charset="UTF-8">
  <title>SlidePuzzle</title>
  <style>
    table {
      border-collapse: collapse;
    }
    .tile {
      width: 100px;
      height: 100px;
      font-size: 70px;
      text-align: center;
      border: 1px solid black;
    }
  </style>
  <script>
    let tiles = [];
    function init() {
      let table = document.getElementById("table");
      for(let i = 0; i < 5; i++) {
        let tr = document.createElement("tr");
        for(let j = 0; j < 5; j++) {
          let td = document.createElement("td");
          let index = i * 5 + j;
          td.index = index;
          td.value = index;
          td.textContent = index == 0?"":index;
          td.className = "tile";
          td.onclick = clicked;
          tr.appendChild(td);
          tiles.push(td);
        }
        table.appendChild(tr);
      }
      for(let i = 0; i < 10000; i++) {
        clicked({target: {index: Math.floor(Math.random() * 25)}});
      }
    }

    function clicked(e) {
      let i = e.target.index;
      if(i % 5 !== 0 && tiles[i - 1].value === 0) {
        swap(i, i - 1);
      }
      if(i > 4 && tiles[i - 5].value === 0) {
        swap(i, i - 5);
      }
      if(i % 5 !== 4 && tiles[i + 1].value === 0) {
        swap(i, i + 1);
      }
      if(i < 20 && tiles[i + 5].value === 0) {
        swap(i, i + 5);
      }
    }

    function swap(x, y) {
      let i = tiles[x].value;
      tiles[x].value = tiles[y].value;
      tiles[x].textContent = tiles[y].textContent;
      tiles[y].value = i;
      tiles[y].textContent = i;
    }
  </script>
</head>

<body onload="init()">
  <p>パネル</p>
  <table id="table"></table>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】タブ等で内容を出し分けるメニューの作り方

どうも7noteです。タブ等をクリックで内容を出し分ける作り方。

とくにこれ以上説明もないのでソースをどうぞ。

sample.gif

ソース

※jQueryを使用しています。jQueryってなんだ???って方はこちら

index.html
<ul class="tab">
  <li class="carrent" rel="item1">メニュー1</li>  <!-- 初期で選択状態のものにクラス「carrent」 -->
  <li rel="item2">メニュー2</li>    <!--rel属性を作り、任意の名前を設定 -->
</ul>
<div class="items">
  <div class="detail item1"><!-- 上のタブのrelと連動するようにクラス名を付ける(例:item1) -->
    <p>ここに1つ目のアイテムの内容を入れる</p>
    <p>ここに1つ目のアイテムの内容を入れる</p>
    <p>ここに1つ目のアイテムの内容を入れる</p>
  </div>
  <div class="detail item2"><!-- 同様にクラス名を付ける -->
    <p>ここに2つ目のアイテムの内容を入れる</p>
    <p>ここに2つ目のアイテムの内容を入れる</p>
    <p>ここに2つ目のアイテムの内容を入れる</p>
    <p>ここに2つ目のアイテムの内容を入れる</p>
  </div>
</div>
style.css
.tab {
  width: 300px;  /* 横幅を300pxに指定 */
  display: flex; /* タブを横並びの配置にする */
}

.tab li {
  color: #aaa;             /* 文字色をグレーに(選択されていない方の色) */
  font-size: 16px;         /* 適当な文字サイズを入力 */
  text-align: center;      /* 文字を中央揃えにする */
  width: 50%;              /* タブが2つなので半分ずつするため50%を指定 */
  border: 1px solid #000;  /* 1pxの枠線を作成 */
  padding: 5px 10px;       /* 余白を指定 */
  cursor: pointer;         /* マウスカーソルが乗った時?の形にする */
}

.tab li.carrent {
  color: #fff;             /* 選択されているときは文字色を白 */
  font-weight: bold;       /* 選択されているときは太文字 */
  background: #777;        /* 選択されているときは背景色をグレー */
}

.items .detail {
  font-size: 14px;         /* 文字サイズを14pxに指定 */
  width: 300px;            /* 横幅を300pxに指定 */
  color: #fff;             /* 文字色を白に指定 */
  background: #000;        /* 背景色を黒に指定 */
  box-sizing: border-box;  /* 余白の計算を簡単にする */
  padding: 10px 20px;      /* 余白を指定 */
  display: none;           /* ページ読み込み時は非表示 */
}
script.js
$('.item1').show();                      // 初期で見せる箇所を表示
$('.tab li').on('click',function () {    // どれかのタブがクリックされた時

  /* タブの処理 */
  $('.tab li').removeClass('carrent'); // タブについているクラス「carrent」を削除
  $(this).addClass('carrent');         // クリックしたタブを選択状態にするためクラス「carrent」を追加

  /* itemの処理 */
  $('.detail').hide();                 // 一度詳細を全ての「.detail」を非表示にする
  var targ = $(this).attr('rel');      // クリックしたタブのrel属性の値を取得
  $('.' + targ).fadeIn();              // 取得したrel情報と同じクラスをもつitemを表示

});

解説

今回の仕様はタブと表示内容を切り離して作成したので、離れた場所やサイドバーにメニューを配置したい場合でも利用が可能です。

「タブのほうにrel属性」、「要素の方にクラス名」をそれぞれ同じ名前のものを設定して紐づけておくことで、表示することができます。

クラスや表示の切り換えはシンプルな構造で、一度全部非表示(クラス削除)してから、任意のものだけ表示(クラスを付与)することで切り換え処理を行なっています。

CSSはほぼ体裁を整えるだけのものなので今回はあまり解説するほどのポイントはありません。

まとめ

見た目がタブでなくても使える方法なので、様々なところに応用できるかと思います。

余分なdiv要素を減らしたかったので、2箇所にwidth:300px;を指定していますが、このデザインであれば2つをくくる親要素を作成して、その親要素にwidthを指定する方が最善かも。
デザインに合わせて修正してお使いください。

おそまつ!

~ Qiitaで毎日投稿中!! ~
【初心者向け】HTML・CSSのちょいテク詰め合わせ

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

無名関数をアロー関数に書換える

アロー関数

無名関数を記述しやすくした記法。

無名関数の例
const b = function(name) {
  return 'hello ' + name;
}

これをアロー関数で書き換えると

アロー関数
const b = (name) => {
  return 'hello ' + name;
}

となる。

引数が1つの場合

さらにアロー関数は、引数が1つの場合、丸カッコ()が省略できる。

const b = name => {
  return 'hello ' + name;
}

また、実行する行が1行のみの場合は、波カッコ{}とreturnも省略できる。

const b = name => 'hello ' + name;

引数を2つ以上取る場合

const b = (name, name1) => 'hello ' + name + ' ' + name1;

このように記述する。

引数を取らない場合

const b = () => 'hello ';

丸カッコは省略不可な点に注意すること。

アロー関数で書き換え可能な部分は記述量が少なくなるので、アロー関数を使おう!!

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

【JavaScriptの超基本】繰り返し処理と配列・オブジェクトについて簡単に解説

概要

この記事では、JavaScriptの繰り返し処理と配列・オブジェクトについて、超基本的な知識をメモ的にまとめています。
自分用の備忘録なのであしからず。

目次

繰り返し処理

繰り返し処理とは、ある処理を条件を満たしている場合に繰り返し実行するという処理です。

この記事では、JavaScriptの繰り返し処理で代表的なwhile文for文についてまとめていきます。

while文

「while文」は繰り返し処理を行う方法の一つです。

while文の構文は次のようになっています。

index.js
while (条件式) {
  処理
}

条件式がtrueの間だけ{ }内の処理が繰り返し実行されます。{ }の後ろに;は必要ありません。

気を付ける点としては、処理内で必ず値を更新することです。値の更新を忘れてしまうと、条件式がfalseになることなく、永遠に処理が繰り返されてしまいます。

この無限ループは、パソコンに大きな負荷がかかってしまうので、必ず値の更新を忘れないようにしましょう。

while文を用いて、10から0までカウントダウンするプログラムを書いてみます。

index.js
let number = 10;
while (number >= 0) {
    console.log(number);
    number--;
}
//出力結果 : 10から0までが表示される

具体例をみていきます。

まず、let number = 10;で変数を定義します。

次にwhileのブロックになります。

条件式であるnumber >= 0number0より大きいか判定しています。最初は、10が代入されているので、trueとなりwhileブロックの処理が実行されます。

whileブロックの処理には、コンソールにnumberの値を表示する処理numberから1を引いて値を更新する処理が書かれています。

これらの処理を終えると、またwhileブロックの先頭に戻り、条件式でnumber0を比較します。numberには値が更新されて9が代入されているので、条件式はtrueとなり、また処理が行われます。

これを条件式がfalseになるまで繰り返します。

最終的には、number-1になった時、条件式がfalseとなるので、処理は実行されず繰り返しが終わります。

for文

「for文」も繰り返し処理を行うための方法の一つです。

できることはwhile文と変わりませんが、コードをシンプルに書くことができます。

構文は以下のようになっています。

index.js
while (条件式) {
  処理
}

for文では( )の中に「変数の定義」「条件式」「変数の更新」を;で区切って書きます。

先ほどwhile文で書いたカウントダウンをfor文で書いてみます。

index.js
for (let number = 10; number >= 0; number--) {
    console.log(number);
}
//出力結果 : 10から0までが表示される

while文と比べるとだいぶスッキリしていると思います。コードを見ていきましょう。

for()の中を見ていきましょう。まず、let number = 10で変数を定義しています。
次に、number >= 0という条件式が書かれています。
最後に、number--で変数の値を更新しています。

コードの流れとしては、変数が定義されると、条件式を満たしているか判定されます。trueであれば、forブロックの処理が実行されます。例の場合だと、コンソールにnumberを出力します。
その後、numberは更新され、再度条件を満たしているか判定されます。

これを条件を満たさなくなるまで繰り返します。

最終的には、number-1になった時、条件式がfalseとなるので、処理は実行されず繰り返しが終わります。

配列

複数の値をまとめて扱う際には、「配列」を用います。
[値1, 値2, 値3]のように[]の中に値を,で区切って書きます。配列に入っている値を「要素」と呼びます。

配列は一つの値なので、定数に代入することができます。配列を代入する定数名は一般的に複数形にします。

配列が代入された定数を出力すると、配列がコンソールに出力されます。

コードを見てみましょう。

index.js
const players = ['Messi', 'Pique', 'deJong'];
console.log(players);
//出力結果 : ['Messi', 'Pique', 'deJong']

const numbers = [2, 3, 5, 7];
console.log(numbers);
//出力結果 : [2, 3, 5, 7]

このように、配列が入った定数をコンソールに出力すると、配列がそのまま出力されます。

配列の要素の取得

配列の要素にはそれぞれ「インデックス番号」が割り振られています。
インデックス番号は0から始まるので、一つ目の要素が0ということになります。

このインデックス番号を用いることで配列の要素を取得することができます。
配列[インデックス番号]と書きます。

具体的なコードをみてみましょう。

index.js
const players = ['Messi', 'Pique', 'deJong'];

console.log(players[0]);
//出力結果 : Messi

console.log(players[2]);
//出力結果 : deJong

このようにインデックス番号で指定した要素のみが、コンソールに出力されます。

配列の要素の更新

インデックス番号を用いて、要素に値を代入することでその要素を更新することができます。

index.js
const players = ['Messi', 'Pique', 'deJong'];

console.log(players[2]);
//出力結果 : deJong

//インデックス番号2の要素の値を更新する
players[2] = 'Sergio';
console.log(players[2]);
//出力結果 : Sergio

コードを見ていきましょう。

配列に3つの要素が入っています。

まず、console.log(players[2]);で3つ目の要素をコンソールに出力します。
次に、players[2] = 'Sergio';で要素の値を更新します。
もう一度、console.log(players[2]);で出力すると、出力結果が変わっていますね。

このように、値を更新することができます。

ここで、一つ疑問が生じます。
constで定義した値って更新できなかったんじゃないの?
そうです。constで定義した値を更新することはできなかったはずです。

実は、constで定義した値というのは、不変ということではありません。
constで定義した値は変数識別子が再代入できないというのが正しい解釈です。
つまり、配列を定義した定数は、配列自体が変わらなければ中身を変えてもいいということです。

そのため、先程の例でplayers = [Coutinho, Dembele, Fati]というように配列自体を再代入しようとした場合には、エラーが生じます。
大事なところなので、しっかり理解しておきたいところです。

繰り返し処理で配列を出力する

配列の要素が多くなると、全ての配列の要素を出力したいときに一つ一つ出力していくのは大変です。

そこで、for文を用いて配列の要素を取り出してみましょう。

index.js
const players = ['Messi', 'Pique', 'deJong'];

for (let i = 0; i < 3; i++) {
    console.log(players[i]);
}
//Messi
//Pique
//deJong

コードを見ていくと、まず定数を宣言し、配列を代入しています。
その次に、for文を書いています。

注目したいのは、console.log(players[i]);というように、インデックス番号にfor文で定義した変数を使っている点です。
このように、インデックス番号に変数用いてfor文で繰り返し処理をすることで各要素を出力することができます。

length

lengthは配列の要素数を取得する際に使われます。

配列.lengthとすることで、配列の要素数を取得することができます。

具体例を見てみましょう。

index.js
const players = ['Messi', 'Pique', 'deJong'];

console.log(players.length);
//出力結果 : 3

このように、配列.lengthの出力結果は、配列の要素数になります。

lengthを用いて、先ほどの繰り返し処理の条件式を書き直してみます。

index.js
const players = ['Messi', 'Pique', 'deJong'];
for (let i = 0; i < players.length; i++) {
    console.log(players[i]);
}
//Messi
//Pique
//deJong

コードで変わった部分は、for文の条件式のところです。
i < players.lengthとすることで、要素数だけ繰り返し処理を行うことができます。

オブジェクト

オブジェクト」は、配列と同じく複数のデータをまとめて扱う際に用います。
オブジェクトは、それぞれの値にプロパティと呼ばれる名前をつけて管理します。

オブジェクトは以下のように書きます。

index.js
//オブジェクト
{プロパティ1: 値1, プロパティ2: 値2}

オブジェクトは{ }で囲みます。プロパティと値の間は:でつなぎます。プロパティと値はセットで一つの要素とみなされるので、それぞれのセットは配列と同じように,で区切ります。

オブジェクトも配列と同様に定数に代入することができ、出力するとオブジェクトがコンソールに表示されます。

具体例を見てみましょう。

index.js
const playerData = {name: 'Messi', team: 'FC Barcelona'};

console.log(playerData);
//出力結果 : {name: 'Messi', team: 'FC Barcelona'}

このように、オブジェクトを代入した定数を出力すると、オブジェクトがそのまま出力されます。

オブジェクトの値の取得

オブジェクトの値は、オブジェクト.プロパティ名とすることで取り出すことができます。
配列のインデックス番号がプロパティ名になった感じです。

オブジェクトの値を取り出すコードをみてみましょう。

index.js
const playerData = {name: 'Messi', team: 'FC Barcelona'};

console.log(playerData.name);
//出力結果 : Messi

このように、オブジェクトの中からプロパティがnameである値を取り出すことができました。

オブジェクトの値の更新

オブジェクトも配列と同じように、プロパティ名を使って更新することができます。
オブジェクト.プロパティ名 = 新しい値と書きます。

先程の例のplayerDatanameの値を更新してみましょう。

index.js
const playerData = {name: 'Messi', team: 'FC Barcelona'};

console.log(playerData.name);
//出力結果 : Messi

playerData.name = 'Leo Messi';
console.log(playerData.name);
//出力結果 : Leo Messi

コードをみていきます。

まず、定数playerDataを宣言し、オブジェクトを代入します。
console.log(playerData.name);でプレイヤーの名前を出力します。
プレイヤーの名前をやっぱりフルネームに変更しようということで、playerData.name = 'Leo Messi';として値を更新します。
再度console.log(playerData.name);を出力すると、出力されるプレイヤーの名前が変わっています。

オブジェクトの場合も、配列と同じようにconstで定義していてもオブジェクトの中身は更新が可能です。

オブジェクトを要素にもつ配列

配列の中にオブジェクトをコンマで区切って書くことで、オブジェクトを要素とした配列を作ることができます。
各要素が長くなるので、要素ごとに改行します。

オブジェクトは配列の一要素なのでインデックス番号が割り振られています。インデックス番号を用いることで、特定の要素を取り出すこともできます。

また、配列の中のオブジェクトの値を取り出すには、プロパティ名を用います。
配列[インデックス番号].プロパティ名のように書くことで特定の値を取り出すことができます。

具体例を見てみましょう。

index.js
//オブジェクトが要素に入った配列を定義する
const playerData = [
    {name: 'Messi', uniformNumber: 10},
    {name: 'Pique', uniformNumber: 3},
    {name: 'deJong', uniformNumber: 21}
];

//配列を出力する
console.log(playerData);
//[
//  { name: 'Messi', uniformNumber: 10 },
//  { name: 'Pique', uniformNumber: 3 },
//  { name: 'deJong', uniformNumber: 21 }
//]

//配列の中のオブジェクトを出力する
console.log(playerData[0]);
//{ name: 'Messi', uniformNumber: 10 }

//配列の中のオブジェクトの中の値を出力する
console.log(playerData[0].name);
//Messi

今回の記事でまとめていることの、組み合わせですね。特に難しいとこをはないと思います。

undefined

配列の存在しないインデックス番号の要素や、オブジェクトの存在しないプロパティの要素を取得しようとすると、undefinedと出力されます。
undefinedは特別な値で、値が定義されていないことを意味しています。

index.js
const playerData = [
    {name: 'Messi', uniformNumber: 10},
    {name: 'Pique', uniformNumber: 3},
    {name: 'deJong', uniformNumber: 21}
];

//存在しないインデックス番号の要素を取り出そうとしてみる
console.log(playerData[5]);
//undefined

//存在しないプロパティの値を取り出そうとしてみる
console.log(playerData[1].team);
//undefined

JavaScriptでは、値が存在しなくてもプログラムが動いてしまうので、注意が必要です。

配列やオブジェクトを組み合わせる

実際には、配列やオブジェクトを組み合わせて使います。
配列の要素をオブジェクトにしたり、オブジェクトの値を配列にしたりすることができます。

index.js
const playerData = {
    barcelona: [
        {name: 'Messi', uniformNumber: 10},
        {name: 'Pique', uniformNumber: 3},
        {name: 'deJong', uniformNumber: 21},
        {name: 'Sergio', uniformNumber: 5}
    ],
    realMadrid: [
        {name:'Ramos', uniformNumber: 4},
        {name:'Hazard', uniformNumber: 10}
    ]
};

このように、配列やオブジェクトを組み合わせることで、大量のデータも管理できるようになります。

まとめ

今日まとめたことを組み合わせて、「背番号△△ : 選手名」と繰り返し処理で出力するプログラムを書いてみました。

index.js
onst playerData = {
    barcelona: [
        {name: 'Messi', uniformNumber: 10},
        {name: 'Pique', uniformNumber: 3},
        {name: 'deJong', uniformNumber: 21},
        {name: 'Sergio', uniformNumber: 5}
    ],
    realMadrid: [
        {name:'Ramos', uniformNumber: 4},
        {name:'Hazard', uniformNumber: 10}
    ]
};

console.log('FC Barcelonaの選手紹介')
for (i = 0; i < playerData.barcelona.length; i++) {
    console.log(`背番号 ${playerData.barcelona[i].uniformNumber} : ${playerData.barcelona[i].name}`);
}

// FC Barcelonaの選手紹介
// 背番号 10 : Messi
// 背番号 3 : Pique
// 背番号 21 : deJong
// 背番号 5 : Sergio

こんな感じですね。
自分で手を動かしてみると理解が深まるので、オブジェクトの中身を自分の好きなものに置き換えてやってみると楽しいですよ。

最後まで読んでいただいてありがとうございます。ではまた。

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

【React】チェックボックスにチェックされたときだけ送信ボタンを有効化(関数コンポーネント)

プライバシーポリシーに同意するにチェックがあったときだけ、問い合わせを送信できるようにした時に。
required属性つけるだけでも実装できるけど、HTMLのフォームエラーのデザインが気に入らない時などに。

概要

  • isCheckedというstate(boolean)を作成
  • 送信ボタンのdisabled属性にisCheckedをセット
  • isCheckedの値を変化させる関数(toggleCheckbox)を作成
  • toggleCheckboxをチェックボックスの状態が変化した時に発火

コード

import React, { useState } from "react"

export default () => {
  const [isChecked, setIsChecked] = useState(false)
  const toggleCheckbox = () => {
    setIsChecked(!isChecked)
  }
  return (
    <>
      <h2>お問い合わせ</h2>
      <form method="post" action="#">
        <label htmlFor="email">メールアドレス</label>
        <input type="text" name="email" id="contact_email" />
        <label htmlFor="content">お問い合わせ内容</label>
        <textarea name="content" id="contact_content" cols="30" rows="10"
        ></textarea>
        <p>利用規約に同意します。</p>
        <input type="checkbox" name="agree" id="agreeCheck" onChange={() => toggleCheckbox()}
        />
        <label htmlFor="agreeCheck">同意する</label>
        <div>
          <button type="submit" disabled={!isChecked}>送信</button>
        </div>
      </form>
    </>
  )
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebRTCを使って複数人がビデオチャットできるデモを作った。

切っ掛け

WebRTCとは、ウェブ・ブラウザーを使ってP2P通信をする規格ですが、WebRTCを使ってデモを作ろうと思ったのは、ブラウザーで楽しめるマルチプレイヤー・オンラインゲームを作成する為、P2P接続について学習したのが切っ掛けでした。しかし、実際に複数人が参加できるビデオチャットのサンプルを探した所、マンツーマンのサンプルはある物も、複数人でのサンプルが、あまり無かったので何かそれっぽい物を作りました。

本記事で自分が学んだ事とWebRTCの実装方法などについて触れて行きたいと思います。

サンプル・コード

https://github.com/h-nasu/webrtc-multi-example

クローン後、、、

npm install
node index.js

localhost:8080にアクセスすればサンプルが見れます。
ページ内に、
Room URL: http://localhost:8080/#f50fb77d54324
と言う形で、勝手にハッシュタグにランダムな文字列があるURL出ます。
これを別のブラウザにコピペすれば同じ部屋での接続が可能になります。
準備がてきたら「start」の後、「call」ボタンを押す形です。

ブラウザでP2P接続の流れ

接続方法がHTTPリクエストやソケット接続と違い、接続用のセッション情報を作成して、それを送信したりなど、ブラウザ同士でP2Pを行うまで一連の流れを理解するのに少々苦労しました。
これを解りやすい図で表すと、、、
Screen Shot 2020-10-28 at 14.25.38.png
↑このような形になりますが、ここにあります「STUN」や「TURN」サーバーについては、下記のサイトにあるリストから選び、それをペアーオブジェクト作成時に設定します。
https://gist.github.com/zziuni/3741933

let peerConn = new RTCPeerConnection({
  'iceServers': [{
    'urls': 'stun:stun.l.google.com:19302'
  }]
})

ペアーオブジェクトを作成した後は、ICE Candidate(P2P接続情報)を「STUN」から受信した後の処理を用意しておきます。

peerConn.onicecandidate = function(event) {
  console.log('icecandidate event:', event)
  if (event.candidate) {
    sendCandidate({
      type: 'candidate',
      label: event.candidate.sdpMLineIndex,
      id: event.candidate.sdpMid,
      candidate: event.candidate.candidate
    })
  } else {
    console.log('End of candidates.')
  }
}

※sendCandidateは自作の関数です。

ICE情報を受信したらそれを、sendCandidateで接続したいリモート宛に送信し、リモート側はICEをペアーオブジェクトに登録します。

// messageはリモートに送られて来た情報
peerConn.addIceCandidate(new RTCIceCandidate({
  sdpMLineIndex: message.label,
  candidate: message.candidate
}))

※あるチュートリアルでは、sdpMLineIndexは入れなくても大丈夫のような事を言われていますが、実際無いと起動しなかったりしたので一応入れておいた方が無難です。

これで接続に必要な初期設定は出来ました。後は、接続する時のOfferとAnswer情報の作成と送受信になります。

まずは、接続を依頼する側(Aさん)からOfferを作成して、生成されたDescription(詳細情報)をsendMessageOfferで受信側(Bさん)に送ります。

// ローカルから送信
peerConn.createOffer(function (desc) {
  console.log('local session created:', desc)
  peerConn.setLocalDescription(desc, function() {
    console.log('sending local desc:', peerConn.localDescription)
    sendMessageOffer(peerConn.localDescription)
  }, logError)
}, logError)

※後の図でも入れ忘れたのですが、createOfferでDescriptionを作成した後、それを必ずsetLocalDescriptionに登録してください。createOfferの段階で勝手に登録されれば良いと思うのですが、構造上、後で受信側(Bさん)のDescriptionをsetRemoteDescriptionで登録しますので、送信側(Aさん)も自分の情報を似たような形で登録するようにしていると思われます。

※sendMessageOfferは自作の関数です。

受信側は、送られて来たDescription(コード内ではmessage)をsetRemoteDescriptionでペアーオブジェクトに登録します。
今度は、createAnswerでAnswerを作成して、同じく生成されたDescriptionを元の送信者にsendAnswerで返信します。

// リモートが受信
console.log('Got offer. Sending answer to peer.')
peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {}, logError)

peerConn.createAnswer(function (desc) {
  console.log('local session created:', desc)
  peerConn.setLocalDescription(desc, function() {
    console.log('sending local desc:', peerConn.localDescription)
    sendAnswer(peerConn.localDescription)
  }, logError)
}, logError)

※sendAnswerは自作の関数です。

後は、送信者がAnswerのDescriptionを登録して完了になります。

// ローカルが受信
console.log('Got answer.')
peerConn.setRemoteDescription(new RTCSessionDescription(message), function() {}, logError)

ビデオ・ストリームの追加

ネット上を検索すればビデオ・ストリームのやり方は、沢山あるとは思いますが、ここでは本当に簡単な説明だけにします。

まずは、ローカル・ビデオの取得はnavigator.mediaDevices.getUserMediaでビデオを取得しそれをペアーオブジェクトに登録します。

// ローカル・ビデオ
let localStream
navigator.mediaDevices.getUserMedia({
  video: true,
}).then(mediaStream => {
  localStream = mediaStream
})

// ペアーオブジェクトにローカル・ビデオを追加
peerConn.addStream(localStream)

そして、リモート・ビデオはリスナーから登録します。

// リモート・ビデオ
let remoteStream

// ペアーオブジェクトにリモート・ビデオを追加
peerConn.addEventListener('addstream', event => {
  remoteStream = event.stream
})

はい、これでビデオ・ストリームの登録は完了です。
P2P接続時に送信側(Aさん)と受信側(Bさん)共に同じ形でビデオ・ストリームをペアーオブジェクトに登録します。

実際の使用例としては、リモート・ビデオの表示などありますが、詳細については、gitのコード見て貰えればです。
下記の関数をコールバックとして登録しております。

// 受信したビデオの処理
const remoteVideos = document.getElementById('remoteVideos')

// Handles remote MediaStream success by adding it as the remoteVideo src.
function gotRemoteMediaStream(event) {
  const video = document.createElement("video")
  const autoplay = document.createAttribute("autoplay")
  video.setAttributeNode(autoplay)
  const playsinline = document.createAttribute("playsinline")
  video.setAttributeNode(playsinline)

  const mediaStream = event.stream
  video.srcObject = mediaStream
  remoteVideos.appendChild(video)
  trace('Remote peer connection received remote stream.')
}

データ・チャンネルからメッセージを送受信

チャットメッセージなどデータを送信するのには、データ・チャンネルを作成する必要があります。
ペアー接続で送信側(Offer)がチャンネルを作成し、受信側(Answer)は作成されたチャンネルを登録します。
送信側はペアーオブジェクトからcreateDataChannelでデータ・チャンネルオブジェクトを作成します。

// ローカルでデータ・チャンネルを作成
let dataChannel = peerConn.createDataChannel('message')

そして、受信側はペアー接続が完了した後、作成されたデータ・チャンネルを受信しondatachannelから登録します。

// リモートでデータ・チャンネルを受信
let dataChannel
peerConn.ondatachannel = (event) => {
  dataChannel = event.channel
}

両側でメッセージを受信した時の処理をonmessageに登録しておきます。

let msg
dataChannel.onmessage = (event) => {
  msg = JSON.parse(event.data)
}

※JSONデータの送受信になりますが、受信メッセージは文字列になる為、JSON形式に変換します。

データ・チャンネルの作成は、ペアーオブジェクト作成後でOffer作成前に作成する必要があります。

// ローカルでデータ・チャンネルを作成
let peerConn = new RTCPeerConnection(config)
let dataChannel = peerConn.createDataChannel('message')
let msg
dataChannel.onmessage = (event) => {
  msg = JSON.parse(event.data)
}
peerConn.createOffer(function (desc) {
...

受信側も同じ流れでデータ・チャンネルの受信を登録しておきます。

// リモートでデータ・チャンネルを受信
let peerConn = new RTCPeerConnection(config)
let dataChannel
peerConn.ondatachannel = (event) => {
  dataChannel = event.channel
}
let msg
dataChannel.onmessage = (event) => {
  msg = JSON.parse(event.data)
}
...
// Offer受信後、Answerを作成
peerConn.createAnswer(function (desc) {
...

これでペアー接続後はデータの送受信が可能になります。
データ送信はデータ・チャンネルオブジェクトのsend関数で送信します。

const msg = {
  name: 'Michael',
  message: 'FOOOOOW!'
}
dataChannel.send(JSON.stringify(msg))

以上がWebRTCで必要な接続とデータのやり取りになります。今までの説明からローカル側とリモート側を一つのサンプルコードに実装して、一つのプラウザで確認する事も可能です。上記例ではペアーオブジェクトをpeerConnとしておりますが、一つのプラウザで試す場合は、peerConnLocalとpeerConnRemoteなど作成しローカル側とリモート側を一つのブラウザで再現する形になります。

次は実際に接続する人が別サーバーや違うブラウザの場合に必要なシグナル・チャンネル(中継サーバー)について説明します。

P2P接続に必要なシグナル・チャンネル(中継サーバー)

2人の間でWebRTCを使ってP2P接続をするには、Offer Answer Candidateの情報を送受信するシグナル・チャンネル(中継サーバー)が必要になります。WebRTCの接続方法上、相手からのOfferを待つ必要がある為、これに適した通信方法としては随時接続しサーバーからの応答を待つウェブ・ソケットが挙げられます。サンプル・コードではNodejsのSocket.ioを使っています。
シグナル・チャンネルを使用した接続の流れとしては、、、

  1. ローカル側が部屋を作成する。
  2. リモート側が部屋へ参加する。
  3. シグナル・チャンネルが既に接続されている全てのメンバーに対して新しいリモートが参加した事を通知する。
  4. 全てのメンバーがOfferを新規参加者に送信する。
  5. 新規参加者は全てのメンバーにAnswerを送信する。
  6. 全てのメンバーがICEを新規参加者に送信する。
  7. 新規参加者は全てのメンバーにICEを送信する。

Nodejs側で部屋の登録ができるシグナル・チャンネルの用意をしておきます。
サーバー側でindex.jsにSocket.ioから部屋を作成します。

index.js
// 中継サーバーで部屋を登録
socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
    log('Room ' + room + ' now has ' + numClients + ' client(s)');

   // 部屋に誰もいない場合は、新規に作成 
   if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);
    // 既に部屋に誰かいる場合は、参加させる 
    } else {
      log('Client ID ' + socket.id + ' joined room ' + room);
      socket.join(room);

      // 新規参加者に既存メンバーの情報を送信
      socket.emit('joined', room, socket.id, Object.keys(clientsInRoom.sockets));

      // 既存メンバーに新規参加者の情報を送信
      socket.to(room).emit('ready', socket.id);

    }
  });

ローカルでmain.jsを用意します。部屋IDを作成しサーバー側へ登録します。

main.js
// ローカルで部屋を作成
room = window.location.hash = randomToken()
socket.emit('create or join', room)

新規参加者が部屋に登録した後は、、、

main.js
// 新規参加者が部屋に参加した後の処理
socket.on('joined', function(room, clientId, socketIds) {
  console.log('This peer has joined room with client ID', clientId)
  socketIds.forEach((socketId) => {
    if (socket.id == socketId) return
    // ここで各メンバーに対してペアーオブジェクトを作成する。
  });
});

また、既存メンバーでは、、、

main.js
// 既存メンバーが新規参加者の情報を受信した後の処理
socket.on('ready', function(socketId) {
  console.log('Socket is ready')
  // ここで新規参加者のペアーオブジェクトを作成する。
});

これで、シグナル・チャンネルでのデータ送受信の準備ができました。
新規参加者は次の既存メンバーになる流れになります。
こちらはSocket.ioでの例になりますが、部屋の作成などはサーバー側で使うモジュールにもよります。

複数のペアー接続をさせるには、

今まで説明しましたWebRTCの接続、ビデオ・ストリーム、データ・チャンネル、とシグナル・チャンネルの流れを図にしますと下記になります。

Screen Shot 2020-11-05 at 12.28.21.png

さてこれでWebRTCでのP2P接続の基本ができた所で最後に、どのようにして複数人のデータを送受信するかなのですが、今までに使ったペアーオブジェクトを接続した人数分、作れば良い事です。
今までですと、、、

let peerConn = new RTCPeerConnection(config)
let dataChannel = peerConn.createDataChannel('message')

このように、ペアーオブジェクトは一つでしたが、ソケットから受信した相手側のソケットIDをキーにしてペアーオブジェクトを複数格納する形です。

// 全てのペアーを格納
let peerConns = {}
// 全てのデータ・チャンネルを格納
let dataChannels = {}

...
// 既存メンバーが新規参加者の情報を受信した後の処理
socket.on('ready', function(socketId) {
  console.log('Socket is ready')
  // ここで新規参加者のペアーオブジェクトを作成する。
  peerConns[socketId] = new RTCPeerConnection(config)
  dataChannels[socketId] = peerConn.createDataChannel('message')
});

後は、使いたいペアーオブジェクトをソケットIDで引き出します。

まとめ

以上でWebRTCの基本的な接続方法などをできるだけ簡単に説明したつもりですが、全体を理解するには、今まで説明した知識を元にサンプルコードを追って頂ければと思います。

WebRTCのチュートリアルはウェブを検索すればありますが、シグナル・チャンネルからの接続方法や複数ペアーの持ち方などは、具体例があまり見当たらなかったので、本記事とサンプルコードが参考になれば幸いです。

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

JSで数字 → 文字に変換

toStringを使う

to-string.ts
const numToString = (num: number): string => num.toString();

numToString(1234)
// "1234"

String関数を使用する

to-string.ts
const numToString = (num: number): string => String(num);
numToString(1234)
// "1234"

undefinedなどにも使用できるため、toStringより安全

テンプレートリテラルを使う

to-string.ts
const numToString = (num: number): string => `${num}`;

numToString(1234)
// "1234"

文字列連結を使用する

to-string.ts
const numToString = (num: number): string => "" + num;
numToString(1234)
// "1234"

参考:
MDN: toString
MDN: テンプレートリテラル
MDN: Stringコンストラクター

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

【JS】変数とメソッドの使い分けについて

変数とメソッドの使い分けについて。同様の処理を返すのに、なぜ、変数ではなくメソッドを使うのかわからない場面があったので理解用のメモ。

どういう場合か?

変数
obj = {a:1, b:2}
メソッド
obj = () => {return {a:1, b:2}}

上記はどちらも{a:1, b:2}を返すが、それぞれで明確な違いがある。

メソッドを使う理由

変数ではなくメソッドを使う理由は、

・作成したオブジェクトが各々、固有になる
・変数だとどれも同じになってしまい、一つの要素を変更するとイコールで結ばれる要素が全て変更されてしまうため。

なので、依存関係にしたくない場合はメソッドを使う


実例で見るとわかりやすい

▼変数の場合

変数の場合
obj = {a:1, b:2}

x = obj  //{a: 1, b: 2}
y = obj  //{a: 1, b: 2}

x == y
//true

変数で宣言した値を、新たな変数x, yに代入すると、xとyは完全に同じになる。

つまり、依存関係となる。

この状態でxを変更すると、yも連動して変更される。


▼メソッドの場合

メソッドの場合
obj = () => {return {a:1, b:2}}

x = obj()   //{a: 1, b: 2}
y = obj()   //{a: 1, b: 2}

x == y
//false

メソッドで作成した値を、x, yに代入すると、xとyはイコールにならない。

つまり、依存関係にならない。

この状態でxを変更しても、yは変更されない。



見た目が類似するので混乱したが、とても重要な概念。

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

【JS】変数とメソッドの使い分け。(依存関係かどうか)

変数とメソッドの使い分けについて。同様の処理を返すのに、なぜ、変数ではなくメソッドを使うのかわからない場面があったので理解用のメモ。

どういう場合か?

変数
obj = {a:1, b:2}
メソッド
obj = () => {return {a:1, b:2}}

上記はどちらも{a:1, b:2}を返すが、それぞれで明確な違いがある。

メソッドを使う理由

変数ではなくメソッドを使う理由は、

・作成したオブジェクトが各々、固有になる
・変数だとどれも同じになってしまい、一つの要素を変更するとイコールで結ばれる要素が全て変更されてしまうため。

なので、依存関係にしたくない場合はメソッドを使う


実例で見るとわかりやすい

▼変数の場合

変数の場合
obj = {a:1, b:2}

x = obj  //{a: 1, b: 2}
y = obj  //{a: 1, b: 2}

x == y
//true

変数で宣言した値を、新たな変数x, yに代入すると、xとyは完全に同じになる。

つまり、依存関係となる。

この状態でxを変更すると、yも連動して変更される。


▼メソッドの場合

メソッドの場合
obj = () => {return {a:1, b:2}}

x = obj()   //{a: 1, b: 2}
y = obj()   //{a: 1, b: 2}

x == y
//false

メソッドで作成した値を、x, yに代入すると、xとyはイコールにならない。

つまり、依存関係にならない。

この状態でxを変更しても、yは変更されない。



見た目が類似するので混乱したが、とても重要な概念。

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

【JS】変数と関数の使い分け。(依存関係かどうか)

変数と関数の使い分けについて。同様の処理を返すのに、なぜ、変数ではなく関数を使うのかわからない場面があったので理解用のメモ。

どういう場合か?

変数
obj = {a:1, b:2}
関数
obj = () => {return {a:1, b:2}}

上記はどちらも{a:1, b:2}を返すが、それぞれで明確な違いがある。

関数を使う理由

変数ではなく関数を使う理由は、

・作成したオブジェクトが各々、固有になる
・変数だとどれも同じになってしまい、一つの要素を変更するとイコールで結ばれる要素が全て変更されてしまうため。

なので、依存関係にしたくない場合は関数を使う


実例で見るとわかりやすい

▼変数の場合

変数の場合
obj = {a:1, b:2}

x = obj  //{a: 1, b: 2}
y = obj  //{a: 1, b: 2}

x == y
//true

変数で宣言した値を、新たな変数x, yに代入すると、xとyは完全に同じになる。

つまり、依存関係となる。

この状態でxを変更すると、yも連動して変更される。


▼関数の場合

関数の場合
obj = () => {return {a:1, b:2}}

x = obj()   //{a: 1, b: 2}
y = obj()   //{a: 1, b: 2}

x == y
//false

関数で作成した値を、x, yに代入すると、xとyはイコールにならない。

つまり、依存関係にならない。

この状態でxを変更しても、yは変更されない。



見た目が類似するので混乱したが、とても重要な概念。

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

【画像処理】膨張処理で文字列を変化させる【JavaScript】

はじめに

※現在修正中。。。お待ちください
ふと、「数値が0から9まで変化するときに、いい感じで変化させたい:wink:」と思いました。
今回はこれを膨張処理を使ってやっていきたいと思います。
数字だけでなく普通の文字にも対応しています。一応複数の文字(長さが2以上の文字列)にも対応しています。

こんな感じになりました。
「0123456789」
0123456789 (8).gif

「こんにちは」
こんにちは (2).gif

膨張処理とは

周辺の8pxに現在の色がなければ現在の色で塗りつぶすという、比較的単純なアルゴリズムです。
こちらを参照してください。
https://imagingsolution.blog.fc2.com/blog-entry-101.html

アルゴリズム

1: 変化前、変化後の画像を2値化します。
2: 変化前、変化後の画像画像を走査して、塗りつぶしている領域(複数)を取得します。
3: 作業用画像に変化前の画像をコピーします。
4: 作業用の画像を戻り値の配列に追加します。
5: 変化後の塗りつぶし領域内の画素が変化前に塗りつぶされていなければ、変化後の塗りつぶし領域の重心に一番近い点を
作業用画像に塗りつぶして、作業用の画像を戻り値の配列に追加します。
6: 指定回数分だけ作業用の画像の背景画素を膨張する。
7: 指定回数分だけ作業用の画像の塗りつぶされている画素を膨張する。(出力後の画像で塗りつぶされていない画素のみ)
8: 作業用の画像を戻り値の配列に追加します。(出力後の画像で塗りつぶされている画素のみ)
9: 作業用の画像が変化後の画像と一致すれば、処理終了。一致しなければ6に戻る。

本アルゴリズムの良い点と悪い点

[良い点]
・アルゴリズムが単純
・フォント、サイズが指定できる
・任意の文字列に対応している(はず)
[悪い点]
・文字の変化があまりかっこよくない
・2値化する必要がなかったかもしれない
・マルチスレッド処理した方がよいと思う
・そもそも膨張処理を採用しているのがいけてないかも

使い方

'あ'から'い'へ変化するImageData配列を取得する例を説明します。(コメントで)
width, heightは100~300ぐらいがおすすめです。
backgroundDilationCount,fillDilationCountは1以上の整数を指定してください。
フレーム数を増やしたいときは、fillDilationCountを小さくしてください。

const anim = new StringAnimation({
            srcText: '',         // 変化前の文字列
            dstText: '',         // 変化後の文字列
            width: 250,  // canvasの幅
            height: 250,   // canvasの高さ
            x: 250/ 2,     // 文字の描画位置 x
            y: 250/ 2,    // 文字の描画位置 y
            font: `250px 'MS ゴシック'`,              // フォント
            textAlign: 'center',   // textAlign
            textBaseline: 'middle', // textBaseline
            fillColor: 'rgb(0, 255, 0)',       // 塗りつぶし色
            backgroundColor: 'rgb(0, 0, 0)',  // 背景色
            logging: true, // ログ出力
            maxFrames: 1000, // 最大フレーム(これを超える場合エラーが発生します)
            backgroundDilationCount: 1,  // 1フレームにつき背景画素の膨張処理を何回行うか(背景画素の膨張処理は速いので、1にしておいたほうがいいです)
            fillDilationCount: 3, // 1フレームにつき塗りつぶし画素の膨張処理を何回行うか(塗りつぶし画素の膨張処理は遅いので、2~4ぐらいを推奨します)
        });

// 全フレームのImageDataを取得する
const imgDatas = anim.getImageDatas();

// あとは、このImageDataをお好きに使ってください...

全ソース(サンプルプログラムも含む)

image.png

3つのボタン押下時にサンプルプログラムを動かしています。
text repeatボタンを押下すると、文字列をアニメーションさせます。
前処理にやや時間がかかるので、ボタン押下後ちょっと待ってください。
one characterボタンを押下すると、1文字をアニメーションさせます。
前処理にやや時間がかかるので、ボタン押下後ちょっと待ってください。

StringAnimationがライブラリのメインのクラスで
BucketFillはStringAnimationから呼び出されています。

ですので、もしこのライブラリを使う場合は
StringAnimationクラスとBucketFillクラスを貼り付けてください。

最後に

このライブラリは複数文字でも使えるはずです。
ImageDataの配列を返すので、まあまあメモリを使います。ご注意ください。
あと注意すべきは、設定内容によって返すImageDataの配列の大きさが異なりますので、受け取った側(ライブラリを使う側)でうまいことやってください。

文字変化については他の方法も考えてみたいと思っています。
プログラムについては商用、非商用関係なく自由に使ってください。

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

【JS】shorthand propertyとは?エラーの対処法

オブジェクトのプロパティ操作で、shorthand propertyに関連するエラーが発生したので、その原因と対処法についてのメモ。

エラー事例

obj = {a=1}

//Uncaught SyntaxError: Invalid shorthand property initializer

他にも、以下のようなエラーが発生する場合がある。
今回発生したのはこちらのエラー。vueとjsファイル間のデータやりとりで発生。

エラー
return {rows= _rows}

//Module parse failed: Shorthand property assignments are valid only in destructuring patterns

destructuring patternsは分割代入。


発生要因と対処法

どちらもオブジェクトのプロパティに対して「=」で値を指定したことが要因。

修正は、シンプルに「=」を「:」に変換すればOK。

修正例
//obj = {a=1}
obj = {a:1}

//return {rows= _rows}
return {rows: _rows}


shorthand propertyとは?

そもそもshorthand propertyとは、プロパティ名と値の変数名が同じ場合に変数名を省略する書き方のこと。

プロパティ名に対して「:」を記述しなかったことで、shorthand propertyとしてみなされたが、間にイコールがあるためエラーになったと考えられる。

shorthand_propertyの例
number = 111
obj = {number} //プロパティ名のみ指定

console.log(obj)
//{number: 111}

上記では、objectを宣言する時に、プロパティ名のみ指定して値を指定していないがエラーになっていない。

これは省略前の表記が、obj = {number: number}となっているため。



▼注意
プロパティ名と同じ名前の変数がない場合はエラーになる。

obj = {test}

//Uncaught ReferenceError: test is not defined
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tone.jsで1ステップで使用できるチューナーを作った

楽器を練習するときに使う電子機器がある。
・チューナー
・メトロノーム
WebAPIを使って簡単にスマホから使えるものを作れないか考えた。

・ブラウザから音を出す方法
ブラウザから音を出す場合、セキュリティの都合上、ユーザにクリックさせる必要がある。
今どきは、WebAPIを使うと簡単に実装できる。中でもJavascriptのライブラリであるTone.jsを使うと、より簡単に実装できる。
・メトロノームとチューナー、どっちが簡単に実装できて使えるか
メトロノームはテンポ、リズムを指定するが、チューナーはピッチさえ合わせればいい。
・アクションが少ない方法
できるだけシンプルにしたい。
通常、チューナーを使うまでに3ステップある
・電源入れる→ピッチ合わせる→鳴らす→止める(4ステップ)
これを1ステップで使うには何もかも捨ててこうすればいい
・鳴らす(1ステップ)
というわけで、442Hzにあらかじめセットしておき、10秒で減衰して鳴りやむチューナーを作った。

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

文字列に変数を混ぜる方法を言語ごとにまとめてみた

ゆるっと Advent Calender 2020 の初日に参加させていただきましたー。
よろしくお願いします。

初日にふさわしいかわかりませんが、ゆるゆる仕事してたら恥をかいた件を書きますね。

先日先輩にレビューしてもらったとき、文字列の中に変数を足しこむやり方を、+演算子を使ってやっていたら先輩に『ダサい』と一蹴されました。(*'▽')ガフッ

string.js
const word = "hogehoge";
const text = "彼はおもむろにこう言った。\n" + word + " 』、と。";
console.log(text);
彼はおもむろにこう言った。
『 hogehoge 』、と。

勉強不足でごめんなさい。( ;∀;)
あと、ユニク〇ばかり着ててごめんなさい。

反省したので自分の学んだことのある言語での書き方や名称をまとめたい思います。

Javascript, Node.js

Javascript および Node.js ではテンプレートリテラルという名称になっています。

文字列をグレイヴ・アクセント(`)で囲み、変数を入れるときはドル記号と波括弧でくくります。

templateLiterals.js
const word = "hogehoge";
const text = `彼はおもむろにこう言った。\n『 ${word} 』、と。`;
console.log(text);
彼はおもむろにこう言った。
『 hogehoge 』、と。

Python

Python ではフォーマット済み文字列リテラルという名称になっています。

f または F を先頭に書いてから文字列を書いていきます。
変数を入れるときは波括弧で区切ります。

formattedStringLiteral.py
word = "hogehoge";
text = f"彼はおもむろにこう言った。\n{word} 』、と。";
print(text);
彼はおもむろにこう言った。
『 hogehoge 』、と。

Java

ない。

強いて言うなら String.format() を使うとできる。

stringFormat.java
String word = "hogehoge";
String text = "彼はおもむろにこう言った。\n『 %s 』、と。";
String output = String.format(text, word);
System.out.println(output);
彼はおもむろにこう言った。
『 hogehoge 』、と。

C♯

C# では文字列補間という名称になっています。

$ を先頭に書いてから文字列を書いていきます。
変数を入れるときは波括弧で区切ります。

stringInterpolation.cs
string word = "hogehoge";
string text = $"彼はおもむろにこう言った。\n『 {word} 』、と。";
System.Console.WriteLine(text);
彼はおもむろにこう言った。
『 hogehoge 』、と。

おわりに

Java にはなかったもんなー( ゚Д゚)

知らなかったの私だけかもしれませんが、実践に勝る経験はありませんね。

この記事が誰かの助けになれば幸いです。|д゚)

自身のメモ用でもあるので、新しい言語を使うときには更新していきますー。

ではまた!(^^)/

参考にさせていただきましたm(_ _)m

MDN - テンプレートリテラル
Python documentation - フォーマット済み文字列リテラル
.NET documentation - 文字列補間

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

【入門者向け】Canvas入門講座#6 五角形を塗りつぶそう【JavaScript】

問題6

点(100, 100)、(300, 200)、(250, 250)、(170, 280)、(50,150)を結ぶ五角形を塗りつぶしなさい。
塗りつぶす色は緑色(#00ff00)であること。
なお、以下のHTMLを使うこと。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題6</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // ここにプログラムを書く
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

答え

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題6</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // コンテキストを取得
    const ctx = $('#my-canvas')[0].getContext('2d');

    // 塗りつぶしの色を緑色にする
    ctx.fillStyle = '#00ff00';

    ctx.beginPath();        // 現在のパスをリセットする
    ctx.moveTo(100, 100);   // パスの開始座標を指定する
    ctx.lineTo(300, 200);   // 座標を指定してラインを引く
    ctx.lineTo(250, 250);   // 座標を指定してラインを引く
    ctx.lineTo(170, 280);   // 座標を指定してラインを引く
    ctx.lineTo(50, 150);    // 座標を指定してラインを引く
    ctx.closePath();        // パスを閉じる
    ctx.fill();           // 現在のパスを塗りつぶす
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

五角形が緑色で塗りつぶせました!
ダウンロード (7).png

解説

コンテキストを取得します。これは問題1と同じです。

// コンテキストを取得
const ctx = $('#my-canvas')[0].getContext('2d');

次に線の色と太さを指定します。
塗りつぶしの色はfillStyleで指定します。

// 塗りつぶしの色を緑色にする
ctx.strokeStyle = '#00ff00';

後はパスを作成し、最後にfillを呼びます。

ctx.beginPath();        // 現在のパスをリセットする
ctx.moveTo(100, 100);   // パスの開始座標を指定する
ctx.lineTo(300, 200);   // 座標を指定してラインを引く
ctx.lineTo(250, 250);   // 座標を指定してラインを引く
ctx.lineTo(170, 280);   // 座標を指定してラインを引く
ctx.lineTo(50, 150);    // 座標を指定してラインを引く
ctx.closePath();        // パスを閉じる
ctx.fill();           // 現在のパスを塗りつぶす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門者向け】Canvas入門講座#5 五角形を描こう【JavaScript】

問題5

点(100, 100)、(300, 200)、(250, 250)、(170, 280)、(50,150)を結ぶ五角形を線分で描画しなさい。
線分は赤色(#ff0000)で太さが5であること。
なお、以下のHTMLを使うこと。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題5</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // ここにプログラムを書く
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

答え

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題5</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // コンテキストを取得
    const ctx = $('#my-canvas')[0].getContext('2d');

    // 線の色を赤色にする
    ctx.strokeStyle = '#ff0000';
    // 線の太さを5にする
    ctx.lineWidth = 5;

    ctx.beginPath();        // 現在のパスをリセットする
    ctx.moveTo(100, 100);   // パスの開始座標を指定する
    ctx.lineTo(300, 200);   // 座標を指定してラインを引く
    ctx.lineTo(250, 250);   // 座標を指定してラインを引く
    ctx.lineTo(170, 280);   // 座標を指定してラインを引く
    ctx.lineTo(50, 150);    // 座標を指定してラインを引く
    ctx.closePath();        // パスを閉じる
    ctx.stroke();           // 現在のパスを描画する
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

五角形が描画できました。
ダウンロード (6).png

解説

コンテキストを取得します。これは問題1と同じです。

// コンテキストを取得
const ctx = $('#my-canvas')[0].getContext('2d');

次に線の色と太さを指定します。
線の色はstrokeStyleで指定します。
線の太さはlineWidthで指定します。

// 線の色を赤色にする
ctx.strokeStyle = '#ff0000';
// 線の太さを5にする
ctx.lineWidth = 5;

後はパスを作成し、最後にstrokeを呼びます。

ctx.beginPath();        // 現在のパスをリセットする
ctx.moveTo(100, 100);   // パスの開始座標を指定する
ctx.lineTo(300, 200);   // 座標を指定してラインを引く
ctx.lineTo(250, 250);   // 座標を指定してラインを引く
ctx.lineTo(170, 280);   // 座標を指定してラインを引く
ctx.lineTo(50, 150);    // 座標を指定してラインを引く
ctx.closePath();        // パスを閉じる
ctx.stroke();           // 現在のパスを描画する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【入門者向け】Canvas入門講座#4 三角形を塗りつぶそう【JavaScript】

問題4

点(100,100)と点(300,200)を点(50,150)結ぶ三角形を塗りつぶしなさい。
なお、以下のHTMLを使うこと。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題4</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // ここにプログラムを書く
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

答え

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>問題4</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
$(() => {
    // コンテキストを取得
    const ctx = $('#my-canvas')[0].getContext('2d');

    ctx.beginPath();        // 現在のパスをリセットする
    ctx.moveTo(100, 100);   // パスの開始座標を指定する
    ctx.lineTo(300, 200);   // 座標を指定してラインを引く
    ctx.lineTo(50, 150);    // 座標を指定してラインを引く
    ctx.closePath();        // パスを閉じる
    ctx.fill();           // 現在のパスを塗りつぶす
});
</script>
</head>
<body>
<canvas id="my-canvas" width="500" height="300"></canvas>
</body>
</html>

三角形が塗りつぶせました!
ダウンロード (5).png

解説

コンテキストを取得します。これは問題1と同じです。

const ctx = $('#my-canvas')[0].getContext('2d');

問題3と異なり、最後にfillを呼んで塗りつぶしています。

ctx.beginPath();        // 現在のパスをリセットする
ctx.moveTo(100, 100);   // パスの開始座標を指定する
ctx.lineTo(300, 200);   // 座標を指定してラインを引く
ctx.lineTo(50, 150);    // 座標を指定してラインを引く
ctx.closePath();        // パスを閉じる
ctx.fill();           // 現在のパスを塗りつぶす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

背景にもってこいのインタラクティブjs(丸々コピペ

背景にもってこいのインタラクティブjs

ラビリンス
labyrinth

マウスカーソルで触れたところから幾何学模様が展開する。

参考:https://codepen.io/tmrDevelops/pen/avrZjr/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

<!--Google Analytics-->
<script src="/static/js/ga.js"></script>
<!--Google Analytics-->

<style>
@import url(https://fonts.googleapis.com/css?family=Philosopher);
body {
  width: 100%;
  margin: 0;
  overflow: hidden;
  background: hsla(0, 0%, 95%, 1);
  cursor: move;
}
</style>

</head>
<body>
    <canvas id='canv'></canvas>
    <!--
    Mousemove/Touchswipe 
    Motion Driven Nodes - 
    they'll fade away if movement stops.
    !-->

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script>

var c = document.getElementById("canv");
var $ = c.getContext("2d");
c.width = window.innerWidth;
c.height = window.innerHeight;

var max = 100;
var num = 1;
var darr = [];
var dst;
var gsz = 50;
var msX = 0;
var msY = 0;
var grav = 150;
var _psz = 1;
dst = Dist(gsz);

for (var i = 0; i < num; i++) {
  dst.add(Node(c));
}

function nPart() {
  var p;
  if (dst.parr.length < max) {
    if (darr.length > 0) {
      p = darr.pop();
      p.res_(msX, msY);
      dst.add(p);
    } else {
      p = Node(c, msX, msY)
      dst.add(p);
    }
  }
  return p;
}

var pull = .03;

function txt(){
  var t = "labyrinth".split("").join(String.fromCharCode(0x2004));
  $.font = "3.5em Philosopher";
  $.fillStyle = 'hsla(0,0%,30%,1)';
  $.fillText(t, (c.width - $.measureText(t).width) * 0.5, c.height * 0.5);
}

function draw() {
 $.fillStyle = 'hsla(0,0%,95%,.45)';
 $.fillRect(0, 0, c.width, c.height);
  txt();
  dst.ref();
  var pos = dst.pos;
  var i = dst.parr.length;
  while (i--) {
    var p = dst.parr[i];
    var n = dst.next(p);
    if (n) {
      var l = n.length;
      while (l--) {
        var pnxt = n[l];
        if (pnxt === p) {
          continue;
        }
        conn(p, pnxt);
        _px = (p.x - pnxt.x) / _dist(pnxt, p);
        _py = (p.y - pnxt.y) / _dist(pnxt, p);
        p.velX -= _px * pull;
        p.velY -= _py * pull;
      }
    }
  }
  upd();
}

function addP(px, py) {
  var p = Node(c, px, py);
  dst.add(p);
}

function conn(p1, p2) {
  $.strokeStyle = 'hsla(0,0%,15%,1)';
  var dist = _dist(p1, p2);
  $.globalAlpha = 1 - dist / 100;
  $.beginPath();
  $.moveTo(p1.x, p1.y);
  $.lineTo(p2.x, p2.y);
  $.stroke();
}

function _dist(p1, p2) {
  var _px = 0;
  var _py = 0;
  _px = p2.x - p1.x;
  _px = _px * _px;
  _py = p2.y - p1.y;
  _py = _py * _py;
  return Math.sqrt(_px + _py);
}

function upd() {
  for (var i = 0; i < dst.parr.length; i++) {
    dst.parr[i].upos();
  }
}

function pRem(p) {
  var i = dst.rem(p)
  darr.push(i[0]);
}

var frict = .9;

function Node(c, px, py) {
  var _p = {};
     _p.res_ = function(px, py) {
     _p.mass = rnd(1, 10);
     _p.gx = rnd(-5, 5);
     _p.gy = rnd(-5, 5);
     _p.x = px || rnd(10, c.width - 10);
     _p.y = py || rnd(10, c.height - 10);
     _p.gx2 = rnd(-2, 2) * .5;
     _p.gy2 = rnd(-2, 2) * .5;

 var vel = 25;
     _p.velX = rnd(-vel, vel);
     _p.velY = rnd(-vel, vel);
}
  _p.upos = function() {
    if (Math.abs(_p.velX) < 1 && Math.abs(_p.velY) < 1) pRem(_p);
    if (rnd(0, 100) > 98) {
      var np = nPart();
      if (np) {
        np.res_(_p.x, _p.y);
        np.velX += rnd(-5, 5);
        np.velY += rnd(-5, 5);
      }
    }
    _p.velX *= frict;
    _p.velY *= frict;

    if (_p.x + _p.velX > c.width) _p.velX *= -1;
    else if (_p.x + _p.velX < 0)  _p.velX *= -1;
    if (_p.y + _p.velY > c.height) _p.velY *= -1;
    else if (_p.y + _p.velY < 0) _p.velY *= -1;

    conn(_p, {
      x: _p.x + _p.velX,
      y: _p.y + _p.velY
    })
    _p.x += _p.velX;
    _p.y += _p.velY;
  }
  _p.res_(px, py);
  return _p;
}

function Dist(gsz) {
  var ret = {};
      ret.gsz = gsz;
      ret.parr = [];
      ret.pos = [];

  ret.next = function(a) {
    var x = Math.ceil(a.x / gsz);
    var y = Math.ceil(a.y / gsz);
    var p = ret.pos;
    var r = p[x][y];

    try {
      if (p[x - 1][y - 1]) {
        r = r.concat(p[x - 1][y - 1]);
      }
    } catch (e) {}
    try {
      if (p[x][y - 1]) {
        r = r.concat(p[x][y - 1]);
      }
    } catch (e) {}
    try {
      if (p[x + 1][y - 1]) {
        r = r.concat(p[x + 1][y - 1]);
      }
    } catch (e) {}
    try {
      if (p[x - 1][y]) {
        r = r.concat(p[x - 1][y]);
      }
    } catch (e) {}
    try {
      if (p[x + 1][y]) {
        r = r.concat(p[x + 1][y]);
      }
    } catch (e) {}
    try {
      if (p[x - 1][y + 1]) {
        r = r.concat(p[x - 1][y + 1]);
      }
    } catch (e) {}
    try {
      if (p[x][y + 1]) {
        r = r.concat(p[x][y + 1]);
      }
    } catch (e) {}
    try {
      if (p[x + 1][y + 1]) {
        r = r.concat(p[x + 1][y + 1]);
      }
    } catch (e) {}
    return r;
  }

  ret.ref = function() {
    ret.pos = [];
    var i = ret.parr.length;
    while (i--) {
      var a = ret.parr[i];
      var x = Math.ceil(a.x / gsz);
      var y = Math.ceil(a.y / gsz);
      if (!ret.pos[x]) ret.pos[x] = [];
      if (!ret.pos[x][y]) ret.pos[x][y] = [a];
      continue;
      ret.pos[x][y].push(a);
    }
  }
  ret.add = function(a) {
    ret.parr.push(a);
  }

  ret.rem = function(a) {
    var i = ret.parr.length;
    while (i--) {
      if (ret.parr[i] === a) return ret.parr.splice(i, 1);
    }
  }
  return ret;
}

function rnd(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

window.addEventListener('mousemove', function(e) {
  var np = nPart();
  if (np) np.res_(e.clientX, e.clientY);
}, false);

window.addEventListener('touchmove', function(e) {
  e.preventDefault();
  var np = nPart();
  if (np)  np.res_(e.touches[0].clientX, e.touches[0].clientY);
}, false);

function run() {
  window.requestAnimationFrame(run);
  draw();
}
run();

window.addEventListener('resize',function(){
  c.width = window.innerWidth;
  c.height = window.innerHeight;
}, false);

</script>

</body>
</html>


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

JavaScriptで昼の12時にリロードする

今まで深夜の0時にリロードが必要だったものが、
要件の変更で昼の12時にリロードが必要になりました。

もともと0時にリロードのときは以下のように書いていました。

index.js
//0時にリロード
window.onload = function() {
  let time = new Date();
  let h = time.getHours();
  let m = time.getMinutes();
  let s = time.getSeconds();
  let rest = (((24 - h) * 3600) - ((60 - m)* 60) - s) * 1000

  setTimeout(function() {
    location.reload(true)
  }, rest);
};

昼の12時リロードに変更になり、以下のように書きかえました。

index.js
// 昼の12時にリロード
window.onload = function() {
  let time = new Date();
  let h = time.getHours();
  let m = time.getMinutes();
  let s = time.getSeconds();

  let rest = 0;
  if (h >= 12) {
    rest = (((36 - h) * 3600) - ((60 - m)* 60) - s) * 1000;
  } else {
    rest = (((12 - h) * 3600) - ((60 - m)* 60) - s) * 1000;
  }

  setTimeout(function() {
    location.reload(true)
  }, rest);
};

※ 初学者です。もっと効率的な方法、綺麗な方法があれば教えてください。

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

GASでChatworkのBotを作ろう!(ローカル開発編)

初めまして
今回は、GoogleAppScriptを使いつつ、スプレッドシートを編集するだけで、定期的にChatWorkにタスクを投げるといったものを作っていきたいと思います。

技術的仕様

  • Node 15.1.0
  • Clasp 2.3.0
  • Webpack 4.44.2

 スプレッドシートのスクリプトエディタは使いにくい

ブラウザでコードを書いていっても良いのですが、やはりシンタックスも効かないし、普段使っているエディタでコードを書きたいですよね。
GASをローカル開発環境に落とし込むライブラリ「Clasp」があったのでそれを使っていきます。

Claspのインストール方法や使い方は下記記事が参考になりました

GAS のGoogle謹製CLIツール clasp

また、jsの最新記法も気兼ねなく使いたかったので、Webpack+Babelを使用することにしました。
本当は、有志が作成したdate処理のライブラリやchatworkのライブラリを使用したかったのですが、gasで実行すると、エラー多発だったので、ライブラリを使用することは諦めて、gasに準拠したライブラリのみ使用することにしました。

Webpackは4系を使いました。gasが5系にまだ対応していないみたいなので、4系にダウングレードしたらうまく実行してくれました。

webpackの構成ファイルを載せておきます。

Webpack.config.js
const path = require('path')
const GasPlugin = require('gas-webpack-plugin')

module.exports = {
    mode: "development",
    devtool: false,
    entry: {
        app: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },

    module: {
      rules: [
        {
          // 拡張子 .js の場合
          test: /\.js$/,
          exclude: /node_modules/,
          use: [
            {
              // Babel を利用する
              loader: "babel-loader",
              // Babel のオプションを指定する
              options: {
                babelrc: false,
                presets: [[
                  // プリセットを指定することで、ES2020 を ES5 に変換
                  "@babel/preset-env", {
                    modules: false,
                    useBuiltIns: 'entry',
                    corejs: 3
                  }
                ]],
                plugins: [
                  ['@babel/plugin-transform-runtime', { 'corejs': 3 }]
                ],
              }
            }
          ]
        }
      ]
    },
    resolve: {
        extensions: ['.js']
    },
    plugins: [new GasPlugin({
      comment: false
    })]
  }

おそらく、余計な設定が多く入っていますが、これで動きました。

実際の製作物について

下図のようなスプレッドシートを作成します。

スイッチ ルームID タスク実行人 タスク内容 期限 タスクを投げる日
ボタン Text Text Text text text

スイッチがtrueの行のみ、処理を実行します。

ChatWorkの下記エンドポイントにHTTPリクエストを投げます。

"https://api.chatwork.com/v2/rooms/{ルームID}/tasks?body={タスク内容}&limit={期限}&limit_type=date&to_ids={タスク実行人}"

gasでは、

UrlFetchApp.fetch(endpoint, data)

でAPIにリクエストを投げられます。

endpointを設定します。

let endpoint = https://api.chatwork.com/v2/rooms/{ルームID}/tasks

ルームIDの部分は、タスクを投げたいルームのIDです。
Chatwork公式ヘルプのルームIDの確認方法を参考にしました。

ルームIDを確認する

そして、下記オブジェクトを作成します。

let data = {
 method: 'post',
 header: { 'X-ChatWorkToken': ENV.CHATWORK_API_TOKEN },
 payload: {
  'body': {タスク内容},
  'limit': {期限},
  'limit_type': 'date',
  'to_ids': {タスク実行人}
 }
}

タスク内容
期限
タスク実行人
はそれぞれ、スプレッドシートから取得します。

期限は、UNIX時間を秒表記に直します。
タスク実行人は、実行者のChatworkアカウントのユーザーidを入力します。

これでURLFetchすれば、chatworkにタスクが出現!
無事完成です。
あとは、トリガーの設定やらがありますが、続きは次回!

※こちらの記事は、追ってより詳しく再編集いたします。

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

Google Mapsでマーカーラベルを大量に付けると ごちゃごちゃになるのを解決する

今日もまた「霊園ガイドサイト開発日記」のスタイルで書いてみます。

今回は前回のGoogle Maps APIの記事のシリーズになります。

霊園ガイドは一目でわかる通り、Google Mapsなどの枯れたマッシュアップを中心に据えています。そんなわけで、Google Maps APIのネタには事欠きません(^^;
霊園ガイドサイト 

さて、今日取り上げるのは「Google Mapsでマーカーラベルを大量に付けると
ごちゃごちゃになる問題」です。

つまりこれですね。下の2枚の画像の左側は、zoomが16の時のものですが、そのズームを12にすると文字がごちゃごちゃになって読めなくなります。

image.png

動作を確認してみる

では実際に目で見て確認してみましょう。
デモページ: https://reien.top/blog/test/gmap/marker/2020-11-12-1.html

デモページにいてみると下の画像のようにズームボタンのついた地図があります。これをクリックしてズーム値を低い値(たとえば12)にすると字が読めなくなるのを確認できます。
image.png

つまり地図を俯瞰するとこうなってしまうわけです。
image.png

因みに今回使ったデータは、国土交通省の下記を使わせていただきました。ありがとうございます(_ )

位置参照情報 ダウンロードサービス
<a href="https://nlftp.mlit.go.jp/cgi-bin/isj/dls/
download_files.cgi">https://nlftp.mlit.go.jp/cgi-bin/isj/dls/_download_files.cgi 

対策の方針を考えてみる

この対策もいくつか考えられると思いますが、今日は「ズーム値が下がったら(俯瞰したら)マーカーラベルを消す」という方向で対策してみようと思います。

Google Maps API では

1.ズーム値は map.getZoom() メソッドで取得できます。
2.ズームの変更は map.addListener("zoom_changed") リスナで取得できます

一応これをおさえてマーカーの書き換え操作などを加えるとおおむねこの対策ができるようになります。今回はアニメーションを最初だけに限定するというフラグも立てて操作してみました。

今回の対策

これも実際に目で見て確認してみましょう。
デモページ: https://reien.top/blog/test/gmap/marker/2020-11-12-2.html

このページも上と沿うようにズームボタンが並んでいるので押してみると、zoom 13以下でラベル文字が消えるのが判ります。

そしてデモページでズーム値を操作してみるとわかりますが、たとえば zoom 13だとこうなり、文字が消えています。

image.png

対策前のコードと対策後のコード

一応いま急いで書いたざっくりしたサンプルですが、参考までに置いておきます。google Maps のapiキーはご自身で取得したものをお使いください。サイトにおいてコンソール上でそのサイトを許可する必要がありますがそのあたりは下に書いた参考ページが判りやすいです。

対策前

<div id="map" style="width:90%;height:60%"></div>

<script>
    let lists=`"13","東京都","13101","千代田区","131010001002","内幸町二丁目","35.670812","139.754182","0","3"
"13","東京都","13101","千代田区","131010001001","内幸町一丁目","35.670839","139.758119","0","3"
"13","東京都","13101","千代田区","131010002003","霞が関三丁目","35.671825","139.746988","0","3"
"13","東京都","13101","千代田区","131010003000","北の丸公園","35.691555","139.751639","0","1"
   //(省略)
`

let map //ここではmap だけあとでzoomを操作するためにグローバルにおいておきます

//地図を描画する
function initMap(){

    //中心の経緯度
    let lat=35.688069 
    let lng=139.763929

    //経緯度のインスタンスを作る
    let centerlocation = new google.maps.LatLng(
        lat,
        lng
    );

    //DOM要素id map に地図を描画する
    map = new google.maps.Map(document.getElementById("map"), {
        center: centerlocation,
        zoom: 16, //ズーム値
    });
    //経緯度のインスタンスを作る
    let mylocation = new google.maps.LatLng(
        lat,//経度
        lng//経度
    );

    //マーカーを描く
    drawMarkers(map, lists)
}

//データcsvから町名と経緯度を取り出しマーカーを描く
function drawMarkers(map, lists){

    //データcsvから町名と経緯度を取り出す
    let lines=lists.split('\n')//行で分解し配列にする
    //行を回してデータを取り出す
    for(let i=0;i<lines.length;i++){
        let line=lines[i]
        let cols=line.split(',')//列で分解し配列にする

        //マーカーを描く "霞が関一丁目" "35.673944" "139.752558" 
        mkMarker(
            map
            //町名は5番目の列にある
            , cols[5] 
            //latは6番目の列にある
            //単に+やparseFloatでは数値化できなかったので+JSON.parseした
            , +JSON.parse(cols[6]) 
            //lngは7番目の列にある
            , +JSON.parse(cols[7])  
        )
    }
}

//マーカーを描く
function mkMarker(map, name, lat, lng){

    //経緯度のインスタンスを作る
    let mylocation = new google.maps.LatLng(
        lat,//経度
        lng//経度
    );
    //マーカーを描く
    let cityMarker=new google.maps.Marker({
        position: mylocation,//経緯度
        icon: {
            path: google.maps.SymbolPath.CIRCLE,//シンボル円
            scale: 6,          //アイコンサイズ
            fillColor: '#fff',  //塗り潰し色
            fillOpacity: 0.8,   //塗り潰し透過率
            strokeColor: "red", //枠線の色
            strokeWeight: 8,    //枠線の幅
        },
        label: {
            text:  name,           //ラベル文字
            color: 'oraneg',       //文字の色
            fontSize: '12px',      //文字のサイズ
            fontWeight: '900'      //文字の太さ
        },
        draggable: false, //ドラッグしたいならtrue
        map: map, //地図オブジェクト
        animation:  google.maps.Animation.DROP //マーカーアニメーション
    });
}

</script>
<script 
    src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC0BiR5x2hwF3Tj2IQZadRHOLOSznUzXQI&callback=initMap"
    async defer>
</script>

対策後

<div id="map" style="width:90%;height:60%"></div>

<script>
    let lists=`"13","東京都","13101","千代田区","131010001002","内幸町二丁目","35.670812","139.754182","0","3"
"13","東京都","13101","千代田区","131010001001","内幸町一丁目","35.670839","139.758119","0","3"
"13","東京都","13101","千代田区","131010002003","霞が関三丁目","35.671825","139.746988","0","3"
"13","東京都","13101","千代田区","131010003000","北の丸公園","35.691555","139.751639","0","1"
   //(省略)
`

let map //ここではmap だけあとでzoomを操作するためにグローバルにおいておきます
let cityMarkers=[]

//地図を描画する
function initMap(){

    //中心の経緯度
    let lat=35.688069 
    let lng=139.763929

    //経緯度のインスタンスを作る
    let centerlocation = new google.maps.LatLng(
        lat,
        lng
    )

    //DOM要素id map に地図を描画する
    map = new google.maps.Map(document.getElementById("map"), {
        center: centerlocation,
        zoom: 16, //ズーム値
    })

    //ズーム後にzoom値がマーカーを再描画する
    map.addListener("zoom_changed", () => {
        reSetMarkers()
        if(map.getZoom()<14){
            //14未満はラベルなし
            drawMarkers(map, lists, false, false)
        } else {
            //14以上はラベルあり
            drawMarkers(map, lists, true, false)
        }

    })

    //マーカーを描く
    drawMarkers(map, lists, true, true)
}

//マーカーをリセットする
function reSetMarkers(){
    for(let i=0;i<cityMarkers.length;i++){
        cityMarkers[i].setMap(null)
    }
}

//データcsvから町名と経緯度を取り出しマーカーを描く
function drawMarkers(map, lists, labelFlg, animeFlg){

    //データcsvから町名と経緯度を取り出す
    let lines=lists.split('\n')//行で分解し配列にする
    //行を回してデータを取り出す
    for(let i=0;i<lines.length;i++){
        let line=lines[i]
        let cols=line.split(',')//列で分解し配列にする

        //マーカーを描く "霞が関一丁目" "35.673944" "139.752558" 
        mkMarker(
            map
            //町名は5番目の列にある
            , cols[5] 
            //latは6番目の列にある
            //単に+やparseFloatでは数値化できなかったので+JSON.parseした
            , +JSON.parse(cols[6]) 
            //lngは7番目の列にある
            , +JSON.parse(cols[7])
            ,labelFlg
            ,animeFlg
        )
    }
}

//マーカーを描く
function mkMarker(map, name, lat, lng, labelFlg, animeFlg){

    //経緯度のインスタンスを作る
    let mylocation = new google.maps.LatLng(
        lat,//経度
        lng//経度
    );

    // icon設定
    let icon= {
        path: google.maps.SymbolPath.CIRCLE,//シンボル円
        scale: 6,          //アイコンサイズ
        fillColor: '#fff',  //塗り潰し色
        fillOpacity: 0.8,   //塗り潰し透過率
        strokeColor: "red", //枠線の色
        strokeWeight: 8,    //枠線の幅
    }

    //labelフラグによるoption設定分岐
    if(!labelFlg){
        option={
            position: mylocation,//経緯度
            icon: icon,
            draggable: false, //ドラッグしたいならtrue
            map: map, //地図オブジェクト
        }
    } else {
        option={
            position: mylocation,//経緯度
            icon: icon,
            label: {
                text:  name,           //ラベル文字
                color: 'oraneg',       //文字の色
                fontSize: '12px',      //文字のサイズ
                fontWeight: '900'      //文字の太さ
            },
            draggable: false, //ドラッグしたいならtrue
            map: map, //地図オブジェクト
        }
    }

    //マーカーを描く
    let cityMarker=new google.maps.Marker(option);
    //マーカーアニメーションは最初だけにしておく
    if(!!animeFlg){
        cityMarker.setAnimation(google.maps.Animation.DROP)
    } else {
        cityMarker.setAnimation(null);
    }
    //マーカー配列に入れておく
    cityMarkers.push(cityMarker)

}

</script>
<script 
    src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC0BiR5x2hwF3Tj2IQZadRHOLOSznUzXQI&callback=initMap"
    async defer>
</script>

参考

霊園ガイドサイト(ここのCTOやってます)
https://reien.top/

Google maps APIで東京駅を表示する方法(事前準備(APIキー取得)方法など)
https://qiita.com/Jenny1025/items/74935088ced3f70893da

Maps JavaScript API
https://developers.google.com/maps/documentation/javascript/overview
APIキーを取得する
https://developers.google.com/maps/documentation/javascript/get-api-key

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

IE11にJavaScriptを対応させるためにしたことまとめ

急にIE11対応を頼まれた

JavaScript初心者の私が、IE11対応を頼まれて苦戦した箇所をまとめます。
修正前と修正後のコードを載せたので、参考になれば嬉しいです。

アロー関数

該当のコード

$button.click((event) => {
    alert('ボタンを押しました');
});

// => SCRIPT1002: 構文エラーです。

修正後のコード

$button.click(function(event) {
    alert('ボタンを押しました');
});

テンプレートリテラル

バッククォートで囲んだ文字列の中で変数展開させるやつ

該当のコード

const age = 20
console.log(`私の年齢は${age}です。`);

// => SCRIPT1014: 文字が正しくありません。

修正後のコード

const age = 20;
console.log('私の年齢は' + age + 'です。');

// => 私の年齢は20です。

isNaN

引数が非数かどうか評価するメソッドらしい

該当のコード

const int = 1000;
console.log(Number.isNaN(int));

// => SCRIPT438: オブジェクトは 'isNaN' プロパティまたはメソッドをサポートしていません。

修正後のコード

const int = 1000;
console.log(isNaN(int));

// => false

.find()

該当のコード

const fruits = ['Apple', 'Orange', 'Banana']
if(fruits.find(function (fruit) {return fruit === 'Grape'})) {
  console.log('りんごだよ!');
}else {
  console.log('りんごじゃないよ!');
}

// => SCRIPT438: オブジェクトは 'find' プロパティまたはメソッドをサポートしていません。

修正後のコード

const fruits = ['Apple', 'Orange', 'Banana']
if(fruits.filter(function (fruit) {return fruit === 'Grape'})[0]) {
  console.log('りんごだよ!');
}else {
  console.log('りんごじゃないよ!');
}

// => りんごじゃないよ!

バージョン情報

IE: 11.418.18362.0

「もっとこうした方がいいよ!」とか「こういう書き方もあるよ!」とか、アドバイスがあればコメントしてくださいm(_ _)m

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

JavaScriptをIE11対応させるためにしたことまとめ

急にIE11対応を頼まれた

JavaScript初心者の私が、IE11対応を頼まれて苦戦した箇所をまとめます。
修正前と修正後のコードを載せたので、参考になれば嬉しいです。

アロー関数

該当のコード

$button.click((event) => {
    alert('ボタンを押しました');
});

// => SCRIPT1002: 構文エラーです。

修正後のコード

$button.click(function(event) {
    console.log('ボタンを押しました');

// => ボタンを押しました
});

テンプレートリテラル

バッククォートで囲んだ文字列の中で変数展開させるやつ

該当のコード

const age = 20
console.log(`私の年齢は${age}です。`);

// => SCRIPT1014: 文字が正しくありません。

修正後のコード

const age = 20;
console.log('私の年齢は' + age + 'です。');

// => 私の年齢は20です。

isNaN

引数が非数かどうか評価するメソッドらしい

該当のコード

const int = 1000;
console.log(Number.isNaN(int));

// => SCRIPT438: オブジェクトは 'isNaN' プロパティまたはメソッドをサポートしていません。

修正後のコード

const int = 1000;
console.log(isNaN(int));

// => false

.find()

該当のコード

const fruits = ['Apple', 'Orange', 'Banana']
if(fruits.find(function (fruit) {return fruit === 'Grape'})) {
  console.log('りんごだよ!');
}else {
  console.log('りんごじゃないよ!');
}

// => SCRIPT438: オブジェクトは 'find' プロパティまたはメソッドをサポートしていません。

修正後のコード

function sample(arg) {
    const fruits = ['Apple', 'Orange', 'Banana']
    if(fruits.filter(function (fruit) {return fruit === arg})[0]) {
      console.log('くだものだよ!');
    }else {
      console.log('くだものじゃないよ!');
    }
}

sample('Banana')
sample('Tomato')

// => くだものだよ!
// => くだものじゃないよ!

2020/11/12追記

indexOfの方がわかりやすく書ける

function sample(arg) {
    const fruits = ['Apple', 'Orange', 'Banana']
    if(fruits.indexOf(arg) !== -1) {
      console.log('くだものだよ!');
    }else {
      console.log('くだものじゃないよ!');
    }
}

sample('Banana')
sample('Tomato')

// => くだものだよ!
// => くだものじゃないよ!

@il9437さんご指摘ありがとうございますm(_ _)m

バージョン情報

IE: 11.418.18362.0

「もっとこうした方がいいよ!」とか「こういう書き方もあるよ!」とか、アドバイスがあればコメントしてくださいm(_ _)m

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