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

フロントエンド開発者のための刺激的なプロジェクト10選 1選目考察【前編】

下記の記事をみてフロントエンドエンジニアになりたい気持ちが強くなったので、1選ずつコード見ていき素人ながらに分析しました。かいつまんで、こんな書き方してるんだーみたいな発見を書いていくだけなので、体系的な説明にはなっていないと思うので悪しからず。お願いします。

Object.keys

オブジェクトがもっているキー名を配列として返す。
このプロジェクトではサーバーにおいているSVGへのパスをオブジェクトにまとめてあり、それをObject.keysを用いて、img要素を作るために利用している。

const CONSTANTS = {
  assetPath: "https://s3-us-west-2.amazonaws.com/s.cdpn.io/184729",
}

const ASSETS = {
  head: `${CONSTANTS.assetPath}/head.svg`,
  waiting: `${CONSTANTS.assetPath}/hand.svg`,
  stalking: `${CONSTANTS.assetPath}/hand-waiting.svg`,
  grabbing: `${CONSTANTS.assetPath}/hand.svg`,
  grabbed: `${CONSTANTS.assetPath}/hand-with-cursor.svg`,
  shaka: `${CONSTANTS.assetPath}/hand-surfs-up.svg`
}

// Preload images
Object.keys(ASSETS).forEach(key => {
  const img = new Image();
  img.src = ASSETS[key];
});

けど、Object.keysの部分を消しても正常に動作しているっぽいので、何のためにこれをやっているのかは理解できていない。

カスタムフック Ref addEventListener

戻り値としてrefとstate(真偽値を示すhoveredという変数)の2つの値配列を返すuseHoverというカスタムフックを作成している。
GrabZoneという関数コンポーネントでuseHoverを呼び出し、refをouterRef変数,innerRef変数に、stateをouterHovered変数,innerHovered変数にそれぞれ分割代入している。
outerRef変数,innerRef変数がref属性に指定されたReact要素に、useHover関数内で定義されたaddEventListenerが設定され、カーソルがそこにホバーするたびにuseHover関数内のenter関数が呼び出され、カーソルが範囲外に行くことでleave関数が実行される。
それによってsetHoverdが実行されhoverd変数の真偽値が切り替わり、if文の条件分岐により、UIに変化をもたらしている。

const useHover = () => {
  const ref = useRef();
  const [hovered, setHovered] = useState(false);

  const enter = () => setHovered(true);
  const leave = () => setHovered(false);

  useEffect(
    () => {
      ref.current.addEventListener("mouseenter", enter);
      ref.current.addEventListener("mouseleave", leave);
      return () => {
        ref.current.removeEventListener("mouseenter", enter);
        ref.current.removeEventListener("mouseleave", leave);
      };
    },
    [ref]
  );

  return [ref, hovered];
};

const GrabZone = ({ cursorGrabbed, gameOver, onCursorGrabbed }) => {
  const [outerRef, outerHovered] = useHover();
  const [innerRef, innerHovered] = useHover();
  const [isExtended, setExtendedArm] = useState(false);

  let state = "waiting";
  if (outerHovered) {
    state = "stalking";
  }
  if (innerHovered) {
    state = "grabbing";
  }
  if (cursorGrabbed) {
    state = "grabbed";
  }
  if (gameOver) {
    state = "shaka"
  }

  // If state is grabbing for a long time, they're being clever!
  useEffect(() => {
      let timer;
      if (state === "grabbing") {
        timer = setTimeout(() => {
          // Not so clever now, are they?
          setExtendedArm(true);
          timer = null;
        }, 2000);
      }
      return () => {
        setExtendedArm(false);
        if (timer) {
          clearTimeout(timer);
        }
      };
    },
    [state]
  );

  return (
    <div className="grab-zone" ref={outerRef}>
      <div className="grab-zone__debug">
        <strong>Debug info:</strong>
        <p>Current state: {state}</p>
        <p>Extended arm: {isExtended ? "Yes" : "No"}</p>
      </div>
      <div className="grab-zone__danger" ref={innerRef}>
        <Grabber
          state={state}
          gameOver={gameOver}
          extended={isExtended}
          onCursorGrabbed={onCursorGrabbed}
        />
      </div>
    </div>
  );
};

イベントハンドラー

このプロジェクトで扱われているイベントは下記の4つ。

mousemove

マウスなどのポインティングデバイスで、カーソルのホットスポットが要素内にある間に動いた時に発行されるイベントです。

ここではwindow内でカーソルが動くというイベントに対して、マウスの位置をプログラムが把握するために使用されている。

mouseenter

ポインティングデバイス (通常はマウス) のホットスポットが最初にイベントが発生した要素の中に移動したときに Element に発生します。

mouseleave

mouseleave イベントは、ポインティングデバイス (ふつうはマウス) のカーソルが Element 外に移動したときに発行されます。

mouseentermouseleaveはある要素内へのカーソルの出入りを検知し、stateの真偽値を更新するために使用されていた。

const useHover = () => {
  const ref = useRef();
  const [hovered, setHovered] = useState(false);

  const enter = () => setHovered(true);
  const leave = () => setHovered(false);

  useEffect(
    () => {
      ref.current.addEventListener("mouseenter", enter);
      ref.current.addEventListener("mouseleave", leave);
      return () => {
        ref.current.removeEventListener("mouseenter", enter);
        ref.current.removeEventListener("mouseleave", leave);
      };
    },
    [ref]
  );

  return [ref, hovered];
};

resize

windowサイズの変更を検知し、要素の寸法と位置を返すgetBoundingClientRectを呼び出している。

つづく

コードみたら何をしているのかは推測がつくのですが、このコードを作り上げる発想がすごいと思いました。
引き続きコード読み解いていきます。

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

【JavaScript】postMessageでデータの送受信

はじめに

外部システム(異なるドメイン)とのデータの受け渡しを行う必要があり、
postMessageでデータの受け渡しが行えたので、忘れないように記載

実装

iframeを使用した例などがよく出てくるが、
諸事情により使用出来なかったため、別の方法で対応

データ送信側のHTML

<script type="text/javascript">
    // 初期処理
    $(function() {
        // 初期処理で受信側のサイトを開く
        window.open('値受信側のHTML.com', 'open');
    });

    window.addEventListener('message', function(event) {
        // postMessageを受け取った時の処理
        alert(event.data); // 出力結果 => 準備出来たよー

        if ('値受信側のHTML.comのorigin' == event.origin) {
            // 値受信側からのpostMessageだった場合データの送信を行う
            const jsonData = JSON.stringify({'dataA':'データA!!','dataB':'データB!!'});
            event.source.window.postMessage(jsonData, event.origin);
        }
    }, false);
</script>

データ受信側のHTML

<script type="text/javascript">
    // 初期処理
    $(function() {
        window.opener.postMessage('準備出来たよー', '*');
    });

    window.addEventListener('message', function(event) {
        // postMessageを受け取った時の処理
        const data = JSON.parse(event.data);
        alert(data.dataA) // 出力結果 => "データA!!"
        alert(data.dataB) // 出力結果 => "データB!!"
    }, false);
</script>

流れ

 ・データ送信側の画面を表示
 ・初期処理でデータ受信側の画面を別タブで表示
 ・データ受信側の初期処理(postMessage)でデータ送信側の画面にメッセージを送信
 ・データ送信側の画面でpostMessageを受信し、originチェック実施
 ・複数データを送信したいので、Json形式のメッセージをpostMessageで送信
 ・データ受信側の画面でpostMessageを受信し、やりたい放題実施

おわりに

 説明が上手くなりたい。。

参考

window.postMessage
[JavaScript] postMessageでクロスドメインメッセージ通信

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

JavaScriptでおみくじゲームを作る流れ(メモ)

「大吉です」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  大吉です。
</body>
</html>

JavaScriptで「大吉です!」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    document.write('大吉です!')
  </script>
</body>
</html>

関数を使って「大吉です!!」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    function omikuji() {
      document.write('大吉です!!')
    }
    omikuji()
  </script>
</body>
</html>

変数を定義して1/3の確率で「大吉です♪」とブラウザに表示

その他は、吉、小吉が出てくる。(仮)

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    function omikuji() {
      const fortune = Math.floor(Math.random()*3) // 0~2の間でランダムで数字が変化する
      if (fortune == 2) {
        document.write('大吉です♪')
      } else if (fortune == 1) {
        document.write('吉です')
      } else {
        document.write('小吉です')
      }
    }
    omikuji()
  </script>
</body>
</html>

Math.random は0以上1未満の数字(小数点以下含む)が取得できます。これに3をかけると、0以上3未満の数字になります。それをさらに Math.floor で処理すると、小数点以下が削られて、0, 1, 2 のいずれかの整数になります。

文字の大きさとか色とかを変えたりしてみる

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
  <style>
    h1 { color: red; font-size: 200px;}
    h2 { color: green; font-size: 100px;}
    h3 { color: blue; }
  </style>
</head>
<body>
  <script>
    function omikuji() {
      const fortune = Math.floor(Math.random()*3)
      if (fortune == 2) {
        document.write('<h1>大吉です♪</h1>')
      } else if (fortune == 1) {
        document.write('<h2>吉です</h2>')
      } else if (fortune == 0){
        document.write('<h3>小吉です</h3>')
      }
    }
    omikuji()
  </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでおみくじゲームを作る流れ(動画内のコードコピペ用)

「大吉です」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  大吉です。
</body>
</html>

JavaScriptで「大吉です!」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    document.write('大吉です!')
  </script>
</body>
</html>

関数を使って「大吉です!!」とブラウザに表示

index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    function omikuji() {
      document.write('大吉です!!')
    }
    omikuji()
  </script>
</body>
</html>

変数を定義して1/3の確率で「大吉です♪」とブラウザに表示

その他は、吉、小吉が出てくる。(仮)

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
</head>
<body>
  <script>
    function omikuji() {
      const fortune = Math.floor(Math.random()*3) // 0~2の間でランダムで数字が変化する
      if (fortune == 2) {
        document.write('大吉です♪')
      } else if (fortune == 1) {
        document.write('吉です')
      } else {
        document.write('小吉です')
      }
    }
    omikuji()
  </script>
</body>
</html>

Math.random は0以上1未満の数字(小数点以下含む)が取得できます。これに3をかけると、0以上3未満の数字になります。それをさらに Math.floor で処理すると、小数点以下が削られて、0, 1, 2 のいずれかの整数になります。

文字の大きさとか色とかを変えたりしてみる

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>おみくじゲーム</title>
  <style>
    h1 { color: red; font-size: 200px;}
    h2 { color: green; font-size: 100px;}
    h3 { color: blue; }
  </style>
</head>
<body>
  <script>
    function omikuji() {
      const fortune = Math.floor(Math.random()*3)
      if (fortune == 2) {
        document.write('<h1>大吉です♪</h1>')
      } else if (fortune == 1) {
        document.write('<h2>吉です</h2>')
      } else if (fortune == 0){
        document.write('<h3>小吉です</h3>')
      }
    }
    omikuji()
  </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの関数

JavaScriptの関数

メソッド定義
def index
  # 処理
end

Rubyでいうところのメソッドを、

関数定義
function 関数名(引数) {
  // 処理
}

JavaScriptでは関数と呼ぶ


今回はこの記述で何がどうなっているのかに関しては触れません。

デベロッパーツールで読み込んだら出てくるんだぜ?やべーだろ?

までで終わります。


早速出力してみる

エディターにあるindex.htmlファイルを
Chromeにドラック&ドロップでポイッと…

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // ここに書いていくよー
  </script>
</body>
</html>

コードを追記したらデベロッパーツールで出力結果を確認します

文字列の結合

Qiita投稿日は?
function hello(what) {
  return '火曜と木曜は' + what + '投稿日';
}
console.log( hello('Qiita') );

指定した「what」と文字列を組み合わせて作成し、その文字列をreturnで返す

実行結果
火曜と木曜はQiita投稿日

その結果
文字列が組み合わされ出力される

ちなみに「return」を使わなかったら…

Qiita投稿日
function hello(what) {
  '火曜と木曜は' + what + '投稿日';
}
console.log( hello('Qiita') );
実行結果
undefined

値は出力されない。

計算結果の出力

掛け算
const num1 = 3
const num2 = 4

function calc(num1,num2){
  return num1*num2
}
console.log(calc(num1,num2))

リロードしてデベロッパーツールを確認すると…

出力結果
12

抑えておきたい知識

  • JavaScriptってのはこんな感じでブラウザ側で動作する言語
  • HTMLCSSはプログラミング言語ではなくてマークアップ言語
  • javaJavaScriptは別物
  • 文字列として扱う場合は…
    • ''シングルコーテーションで囲う
    • ""ダブルコーテーションも可
  • 数値同士の計算ではRuby同様こちら代数演算子が使用される
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Geolocation APIで現在地を取得している間にローディングアニメーションが起動されるようにしてみた。

実装のきっかけ

現在個人開発を行なっており、その際にGeolocation APIで現在地の緯度経度を取得する処理を実装していました。
現在地の取得まで最大で5秒ほどかかることがあり、従来だと以下のように何のアニメーションも表示されないので、ユーザーに不安感を抱かせてしまうと思いました。
そこで、「現在地の取得」をクリックした際にローディングアニメーションが実行されるようにすればユーザーの不安感を軽減できるのではないかと思い、実装することにしました。
現在地の取得.gif

この記事の対象となる方

  • Geolocation APIで現在地を取得する機能を実装している方
  • その上でローディングアニメーションを実装したい方

開発環境

  • Ruby 2.6.6
  • Rails 6.1.0

コード

app/views/layout/application.html.erb
  <body class="font-mono text-base sm:text-xl">
    <div class="loading_zone">
      <div class="spinner"></div>
    </div>
    <%= render  "layouts/header" %>
    <div>
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
      <%= yield %>
    </div>
    <%= render  "layouts/footer" %> 
  </body>
app/assets/stylesheets/application.scss
  // ローディング画面のcss

.loading_zone {
  position: fixed;
  top: 0;
  left: 0;
  opacity: 0;
  color: #f5f5f5;
}
.spinner {
  margin: 300px auto;
  width: 200px;
  height: 200px;
  background-color: #fff;
  border-radius: 100%;
  animation: sk-scaleout 1.0s infinite ease-in-out;
  @media(max-width: 500px){
    margin: 200px auto;
  }
}
/* ローディングアニメーション */
@keyframes sk-scaleout {
  0% {
    transform: scale(0);
  } 100% {
    transform: scale(1.0);
    opacity: 0;
  }
}

.loading {
  width: 100vw;
  height: 100vh;
  transition: all 1s;
  background-color:gray;
  opacity: 0.9;
  z-index: 9999;
  position: fixed;
}
app/javascript/packs/getcurrentlocation.js
  function geoFindMe() {
  const spinner = document.getElementsByClassName('loading_zone')[0];
  spinner.classList.add('loading')
  function success(position) {
    const latitude  = position.coords.latitude;
    const longitude = position.coords.longitude;
    document.getElementById('location').value = `${latitude},${longitude}`;
    spinner.classList.remove('loading'); 
  }

  function error() {
    alert('エラーが発生しました。')
  }

  if(!navigator.geolocation) {
     alert('Geolocation is not supported by your browser');
  } else {
    navigator.geolocation.getCurrentPosition(success, error);
  }
}

document.querySelector('#get_current_spot').addEventListener('click', geoFindMe);

何をやっているのか?

では一個づつ解説していきます。

app/views/layout/application.html.erb
  <body class="font-mono text-base sm:text-xl">
    <div class="loading_zone">
      <div class="spinner"></div>
    </div>
    <%= render  "layouts/header" %>
    <div>
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
      <%= yield %>
    </div>
    <%= render  "layouts/footer" %> 
  </body>

application.html.erbの中にローディングアニメーション用のdivを用意しています。

app/assets/stylesheets/application.scss
  // ローディング画面のcss

.loading_zone {
  position: fixed;
  top: 0;
  left: 0;
  opacity: 0;
  color: #f5f5f5;
}
.spinner {
  margin: 300px auto;
  width: 200px;
  height: 200px;
  background-color: #fff;
  border-radius: 100%;
  animation: sk-scaleout 1.0s infinite ease-in-out;
  @media(max-width: 500px){
    margin: 200px auto;
  }
}
/* ローディングアニメーション */
@keyframes sk-scaleout {
  0% {
    transform: scale(0);
  } 100% {
    transform: scale(1.0);
    opacity: 0;
  }
}

.loading {
  width: 100vw;
  height: 100vh;
  transition: all 1s;
  background-color: gray;
  opacity: 0.9;
  z-index: 9999;
  position: fixed;
}

ここで見慣れない@keyframesというコードが出てきたと思います。
@keyframesはアニメーション開始から終了するまでどのようなアニメーションをするのか指定できるCSSの文法です。
0%を開始直後、100%を終了時としています。

@keyframesについて詳しく知りたい場合はこちらを参照ください!

  .spinner {
  margin: 300px auto;
  width: 200px;
  height: 200px;
  background-color: #fff;
  border-radius: 100%;
  animation: sk-scaleout 1.0s infinite ease-in-out;
  @media(max-width: 500px){
    margin: 200px auto;
  }
}
/* ローディングアニメーション */
@keyframes sk-scaleout {
  0% {
    transform: scale(0);
  } 100% {
    transform: scale(1.0);
    opacity: 0;
  }
}

つまり、上のコードは

1.アニメーション開始時
→何も表示されない
2.アニメーション開始〜終了直前まで
→円を表示する(最大サイズは直径200px)
3.アニメーション終了時
→opacityがゼロなので、消える。

以降1~3を繰り返す。
というアニメーションになっています。

最後にJavaScriptで、クリックしたときにアニメーションが表示されるように処理を書きます。

app/javascript/packs/getcurrentlocation.js
  function geoFindMe() {
  const spinner = document.getElementsByClassName('loading_zone')[0];
  spinner.classList.add('loading')
  function success(position) {
    const latitude  = position.coords.latitude;
    const longitude = position.coords.longitude;
    document.getElementById('location').value = `${latitude},${longitude}`;
    spinner.classList.remove('loading'); 
  }

  function error() {
    alert('エラーが発生しました。')
    spinner.classList.remove('loading'); 
  }

  if(!navigator.geolocation) {
     alert('Geolocation is not supported by your browser');
  } else {
    navigator.geolocation.getCurrentPosition(success, error);
  }
}

document.querySelector('#get_current_spot').addEventListener('click', geoFindMe);

clickした後に走るgeoFindMeという関数の中に詳しい設定を書きます。
loading_zoneクラスを持つdivに対して、処理が走っている間はloadingクラスを付与、処理が終わった後はloadingクラスを削除するようにコードを書きました。

最後に

以上で、ローディングアイコンの実装が完了しました!
最後まで読んでいただき、誠にありがとうございます。

少しでも参考になったと思ったらLGTMを頂けますと幸いです!

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

ランダムな文字列のファイル名を作成

概要

blobにランダムなファイル名をつけてアップロードしたい場合の処理をメモします。

実装

const imageName = Math.random().toString(32).substring(2)
const formData = new FormData()
formData.append('file', blob, imageName + '.jpg')
const config = {
    headers: {
      'content-type': 'multipart/form-data'
    }
}
await axios.post('/api/post_upload', formData, config).then(()=>{
      alert('画像アップロード完了')
})
  1. Math.randomで0以上1未満の少数をランダムで生成
  2. toString()で32進数に変換
  3. substring(2)で3文字目以降を取得

まとめ

本格的にするならもっとセキュアな名前つけた方がいいかもしれません。

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

[JS] コールバック関数

callback関数

関数を渡すことによって、その関数を受け取った側の関数内で実行することができる

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

function getName() {
  return 'Shun';
}

hello(getName);

Image from Gyazo

これはどうなっているかというと

function hello(getName) {
  console.log('hello' + 'Shun');
}

function getName() {
  return 'Shun';
}

hello(getName);

まず最初のhelloの引数callbackにgetNameが入ります
次に2行目の+ callbackが+ 'Shun'に置換されます
そして結果は一緒です


関数として取り扱う

function hello(callback) {
  console.log(callback);
}

function getName() {
  return 'Shun';
}

function getFirstName() {
  return 'Sato';
}

hello(getFirstName);

Image from Gyazo


変数として取り扱う

function hello(callback) {
  console.log(callback);
}

function getName() {
  return 'Shun';
}

const getFirstName = function () {
  return 'Sato';
}

hello(getFirstName);

Image from Gyazo


無名関数をそのままcallback関数として渡す

function hello(callback) {
  console.log(callback);
}

function getName() {
  return 'Shun';
}

hello(function () {
  return 'Sato';
});

Image from Gyazo


アロー関数をcallback関数として渡す

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

function getName() {
  return 'Shun';
}

hello(() => 'Sato');

Image from Gyazo


引数2つ

function hello(callback, lastname) {
  console.log(callback);
  console.log('hello' + callback(lastname));
}

function getName() {
  return 'Shun';
}

hello(function(name) {
  return 'Sato' + name;
}, 'Shun');

無名関数をcallback関数として渡しています
引数nameはどこから来てるか? lastnameの値が入ります

Image from Gyazo


計算

function doSomething(a, b, callback) {
  const result = callback(a, b);
  console.log(result);
}

function multiply(a, b) {
  return a * b;
}

function plus(a, b) {
  return a + b;
}

doSomething(3, 4, multiply); 
doSomething(11, 4, plus); 

Image from Gyazo

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

JavaScriptの変数定義

Rubyでは定義したい変数名を記述するだけで変数定義が可能だが、JavaScriptでは変数定義の様式は、var、const、letと3つ存在する。

var

再定義、再代入可能

var 変数名 = ""

変数名 = "再代入する値"

const

再代入、再定義ともに不可
再代入、再定義を行うとエラーが起こる

const 変数名 = ""

let

再代入は可、再定義は不可
再定義を行うとエラーが起こる

let 変数名 = ""

変数名 = "再代入する値"

letとconstの使い分け

再代入する予定のある変数を定義する際はletを使用する
再定義する予定のない変数を定義する際はconstを使用

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

javascriptの復習

変数と定数の違い

  • const 定数。変更不可。
  • let 変数。再代入○
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsファイル デバックする方法(binding.pry)

はじめに

railsではよくbinding.pryを使用して値が入っているか確認する際によく使いますが、
jsはどうやって調べられるのか?
可能であれば動きをみた方が理解度も上がりますよね!
とても簡単です^^それでは行きましょう!

手順

1.検証ツールを開き。
Sourcesを選択します。スクリーンショット 2021-03-25 18.54.41.png

2.jsファイルを選択して開きます。
スクリーンショット 2021-03-25 18.57.58.png

3.処理を止めたい行を選択します。
スクリーンショット 2021-03-25 19.00.40.png

あとはbinding.pryの時と同じようにブラウザで操作します。

選択した場所で処理が止まり、

その状態で、検証ツールのコンソールで変数の中身を確認することもできます!

誰かの参考に慣れば幸いです^^

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

「SVGでアニメーションさせたいんじゃ」の詳細報告

※ニコニコのユーザーブロマガサービス終了に合わせて、移動&編集した記事です。
元記事(2019-03-06)
https://ch.nicovideo.jp/FlyingEchidna/blomaga/ar1708679

放送でも取り上げてたけども…

【過去放送】2019/03/02 雑談「SVGでアニメーションさせたいんじゃ」
https://www.nicovideo.jp/watch/sm34717013

放送内では語り切れないことについてクッソだらだら書いていくぅ~↑

安売りしてたとかでおとんが買ってくれた
Moho Pro12 と Poser Pro 11
のうち、今回使ったのはMohoだけ。

Poserの方は自作で3Dモデルを作る技術もツールもないので、いまいち使い道が思いつかないnow。
デッサン人形代わりにはなるかもね?ぐらい。
大量にプリセットがあるけども、リアルテイストすぎて上手く使いこなせる気がしない(白目)

Mohoに対するマニアックな感想

Mohoの方は2Dだから無茶ができる感。
ラスタ形式でもベクタ形式でも読み込めるし、ボーンを入れられるし、アニメーションを付けられるので、大体でOKができてありがたい。
フォトショ(psd)もイラレ(ai)もそのまま読み込める。
が、イラレはバージョン8までの形式しか対応してないらしく、CC2019を使っていようが、保存時にわざわざバージョンを落とさないといけない。
まあaiを読み込めるだけマシ?

Moho側で完結して作る場合はベクタ形式だから、パスの扱いに慣れてない人はキツいだろうけど、逆に言えばベクタ形式系の編集機能は付いてるわけで。
読み込んだデータも含め、パスの線の太さ・角度・塗りあたりもその場で編集&アニメーションできるし、フレーム間で勝手にモーフィングしてくれる。
ただ、アンカーポイントの数の変更には弱く、途中で変形させるために無駄にアンカーポイントを増やす羽目になったり…
その辺も含め、AEのシェイプ機能みたいなもんなわけで。
シェイプ乱用に定評があるオレ歓喜。

Moho上でのアニメーションの概念についてだけども。
描画ツール系でよくあるレイヤーと、AEのコンポジション・キーフレームの概念にプラスして、ボーンを使って制御する感じ。
理解できるまでクソややこしかったけども、分かってしまえばクソ便利だなぁという印象。

表示時の重なり順はレイヤーパネル上の順になる…まあ描画ツールではよくある話で。
途中でレイヤーの表示順序を変更するようなアニメーションは、上位のグループ(フォルダ)のオプションで設定すればできるようになる。
AEだったら同じコンポのレイヤーを重ねて、切り替えたいタイミングでインポイントとアウトポイントを…
ってやるところを、アニメーションの再生途中でマジでレイヤー入れ替えちゃうあたり、かなり強引。
意図せず編集途中でレイヤーを増やしたり移動させたりすると、既存のレイヤー順序アニメーション設定がご乱心なさるのでマジ注意。

で、このレイヤーはAEで言うところのフッテージ…かと思いきや、コンポ的な扱いらしい。
つまり、レイヤーを増やせば増やすほどコンポが増えるようなもので、グループレイヤーは中に入れてるレイヤーのプリコンポみたいな感じ。
レイヤーの各々にタイムラインが存在し、各々にアニメーションの設定を入れる。
グループに移動・回転アニメーションを設定すると、中のレイヤーのアニメーションも再生しつつ、一括で移動・回転する。

単なるグループもあれば、機能を持ったグループっぽいものがあって…
特にMoho的に一番重要であろう『ボーンレイヤー』は、中のレイヤーに対してボーンを入れてグリグリ動かせる。
単純にボーンに追従させて動かすこともできるし、メッシュワープみたいな感じでグニグニ変形させながら動かすこともできる。
今回は使ってないけど『フレームバイフレームレイヤー』なら中のレイヤーをパラパラ漫画形式で再生、『スイッチレイヤー』なら口パクや目パチのような差分レイヤーとして管理&切り替えられるようになる。
ラスタ形式だったらクソお世話になりそう。
ボーンの中にフレームバイフレームやスイッチを入れ子にすることも可能。
マスクの設定を入れることも可能。今回は目を顔でマスクしてるけど…作業画面では分からないね。動画出力時には反映される。
あとは使ってないけど、乗算やスクリーンのようなブレンドモードもある。
大体のことできるやん…

あと、めっちゃAE用語で喋っちゃうけど…
全コンポ(レイヤー)のタイムラインのインポイントは一律同時。つまり全部同時再生状態。
だからこのままだと再生開始タイミングの調整ができない。
特定アニメーションを含んだコンポを、インポイントをずらして設置して、再生速度はタイムリマップ入れて微調整!
…ってことは、できないofできない。

その代わりに使うのが『アクション』&『スマートボーン』。
個人的にはこの機能が一番ヤベェと思っている。
アクションごとのタイムラインは孤立していて、アクション各々にコンポのアニメーション設定ができる感じ。
作ったアクションを別のアクションにそのまま読み込ませて再生させることも可能。
さらに、このアクションにスマートボーンを紐づける、ということも可能。
スマートボーンは、回転角度=紐づけたアクションのタイムラインの再生位置、で…
別のアクションの中のスマートボーンを回転させると、紐づけたアクションを再生してくれる。
タイムリマップよりも断然直感的。つおい。

じゃあ、同じレイヤーに対してアニメーションを入れたアクション2つを、スマートボーンを使って再生したらどうなるのか。
なんとまあ、イイ感じにアニメーションをマージしてくれる。
今回だと、歩きのアクション中に、瞳の動き、瞬き、首振りのアクションのスマートボーンを操作してみてる。
歩きには体の揺れとかも入ってるけど、その辺も含めて全部マージされる。
立体っぽい動きもこれで表現できる。
なんやこれ…動作差分作り放題やないか…

あとはキーフレームだけども、ここについてはAEほどの自由度はない印象。
補間方法として、スムーズ、リニア、イーズイン、イーズアウトとかは設定できるけど、AEみたいにグラフエディターで速度を調整、みたいなことはできなさげ。
数値直打ちでの微調整もできないし、エクスプレッションも当然ない。
とはいえ、ループの設定は一応キーフレームに対してできて、次のキーフレームがくるまで指定した範囲のキーフレームをループ再生してくれる。
ただ、ループした瞬間の補間が効かない(そもそも補間とループの設定が同列扱い)ので、カクつくのがすっげー気になる。
今回は、足を下した瞬間の体の揺れでごまかしたけど、こんな苦労をするぐらいならループ設定を使わず、キーフレームコピペ乱打してやろうかと思ってしまうレベル。

その他、一応スクリプトも対応はしてるみたい。言語はLua。
Luaのコードはお遊びで買ったCodeaで見たことあるけども、組んだことはないなぁ…
まあJavaScriptと比べれば…いや、どっこいどっこいかな…ww

Mohoから吐き出したsvgを魔改造したった

mp4とかaviとかgifで出力できるのは、わかる。
しっかしsvg(サイトで表示できるベクタ形式画像)で出力できるとか…マジかよ…
やろうと思ったらサイト上で好きに配置&配色&変形できるし、JavaScriptで再生フレームいじれるんちゃうん…?
ってことで、実際にやってみた。

何パターンか試したけど…
つい昨日モンストにハブられかけた(強制終了バグ出ましたが何か?今日は無事)
Android4.4のブラウザでもそれなりに動作するのはこれっぽい。
20MBぐらいあるから気をつけろ!
http://dmrb.nobody.jp/walk/walk.html

で。
今回は260フレームってことで260個のsvgが出力された。
全部合わせて24MBでーす!ってゲロ重いわやめてくれww
いや、初回はもっと重かったんだけど、レイヤー名がそのまま出力されるから日本語から英語にしたり、今回は塗り設定だけで十分だから線を非表示にしたりしてケチった。

svgの中身を見てみる。
律儀に全ファイルにxml宣言とDOCTYPE宣言、svgのバージョンと名前空間宣言が入ってるのは、まあ分かる。
が。
いくらクラス名の衝突のリスクがあるとはいえ、各パスにデザインの属性を片っ端から付与するのはさすがにやめてくれ…
fill="none" stroke="#a60d3f" stroke-width="1" stroke-linecap="butt" stroke-linejoin="round"
ってだけで文字数どんだけあると思っとんねん…
ということで、さすがにクラス&CSSでの指定に移行させたい。
あと、パスそのものの情報も一部空白を取っ払えそう。

いや、それより一番の問題は…
実際にsvgを表示したときに、設定していた一部のマスクが効いてない。
詳しく差分データを用意してタグを解読していくと…
どうやら、マスク設定を有効にしたグループ内にグループを入れ子で置いた場合、マスク設定が出力されない模様。
mp4で出力したときは問題なかったし、Moho側のバグっぽい。

オマケにタグをよく見ると、表示しているパスをそのままマスクとしても流用する設定を入れていた場合、内容が全く同じパスを表示用とマスク用として出力していることが発覚。
どうにかならんかとsvgの仕様を調べたところ、useタグを使うことでパス情報の使いまわしができることが発覚。
どうにかしてuseタグに移行させたい。

とにかく。
出力したsvgの内容がめっちゃ不服。どうにかしたい。
AEだったら速攻でスクリプト書いてただろうけど、今回はMoho。
今回のケースに留まらず、ローカルファイルをいじるという意味では、もうちょっと汎用的で楽な手段が欲しいところでもある。
Ruby on Railsも勉強したことだし…
ということで、Rubyでsvgタグをいじるプログラムを組むことにした。

マスクの設定が出力できてないレイヤーの名前に識別用の名前を付けてsvgを出力しなおす。
で、svgをRubyで読み込んで、その名前を見つけたら、マスクの設定を自動で付与。
マスクのパスが使いまわせそうだったらuseタグで参照するようにした。
あと、無くても動くからxml宣言とDOCTYPE宣言をごっそり削除。
デザインの属性も一旦ハッシュに登録して、同内容の設定だったら同じクラス名を付与。
全部パス走査し終わったらハッシュの中身を確認して、styleタグにクラス名とCSSを出力した。

他に、1個のファイルにする実験もやってみたんだけど、それだと今度はファイル1個が重すぎる問題が発生。
自分が使ってるレンタルサーバーはアップロードできるファイル1個の最大サイズは3MB。1個で20MBなんて論外of論外。
試しに3MB未満になるように適当に10個ほどに分割してみたけど…
オレのAndroidじゃキャッシュがうまく動かないのか、全く表示できず。
結局、最終版は260個のままなのであった。

あとはアニメーション。
svgファイルは各々1個で完結した静止画なので、html上ではこれをJavaScriptでパラパラ漫画の如く再生(切り替え)させる。
これ自体はよくあるimgタグの切り替え処理みたいなもんで、そんなに難しいことではないんだけども…
問題はロード時間と処理速度。ということで、読み込み方法あれこれ試した。

愚直なのは、htmlに最初から全フレームのimgタグを非表示設定付きで置いとくパターン。
JavaScriptで後から必要数分のimgタグをぶち込む汎用的な方法もあるけど、速度で言えば前者の方が早い。
先に結論をいっちゃうと、試した中でこれが最強だったよ(爆)
それでもオレのAndroidだと表示までに30秒ぐらい待たされるし、なぜか未だに1フレーム目だけ表示時のリサイズ処理が走らないんだけどね。なんだろね?

次にやったのは、読み込むファイルが多いからいけないんだ!的な発想で、最早別ファイルを読み込まない、svgをhtmlに埋め込むパターン。
「svgファイルを1個にしたら20MBになった」っていってんのに、それをhtmlに埋め込むってことはだな…まあアップロードできないわな。
一応GitHubの方で無理矢理試したけど、オレのAndroidはロードが終わらないどころか、ブラウザが死んだよ(爆)

さらに、ajaxを使って後でがっつりリクエスト&ロードするパターン。
動かなくもないけど…オレのAndroidは1分ぐらい待たされた。うん。

あと別件で、imgタグじゃなくuseタグを使った表示もやってみた。
埋め込み式(ゲロ重htmlかajax)じゃないとダメなんだけど、一応表示&アニメーションはできた。
ファイルパスの指定を省略できるし、ファイルアクセス的には早くなるかな?と思ったけど、速度の体感差は皆無だったね…
まあそういう方法もあると知れただけでもOKとしましょう。

報告は以上。

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

テストは型の代わりにならないし、型はテストの代わりにならない

(書きはしたのですが、文章がとっちらかってしまいました…。まあ駄文の存在も許されるのがインターネットのいいところだよね?ということで)

時々ネットで「テストコードを書けばいいのだから(静的)型はいらない」という意見を見る。さすがに型はテストの代わりにならない、というのは自明に近いものがあるので、「型があるのでテストは要らない」はほとんど見ないが、ちょいちょいTypeScript不要論の文脈で前者の意見は見られる。なので、これに関して書く。えっ?タイトルの「型はテストの代わりにならない」はって?まあ自明だけど一応書いただけだ。

ちなみにここで言うテストはテストのパターンを用意して、それがpassするか検証するテストのことを指している。
また、「型」は静的型付け言語での型を指す。実行時型のことを指し示すときは、「実行時型」と書く。(単に書くのがめんどくさい。)

ここで一つの疑問を投げかけよう。

型がない言語でテストが書けますか?

題材とするテスト

題材はJavaScript系言語でメジャーなJESTのドキュメントの一番最初のコードとする。

まずはテスト対象とするコードを示す。

sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

次にこれをテストするコードを示す。

sum.test.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

見ての通りだが、足し算を行う関数に、実際に1と2を入れて、3となることを確認している。単純で典型的でわかりやすいコードだ。

使ってみる

さて、別のコードから文字列を入れてみよう。

concat01.js
const sum = require('./sum');
let pyon = sum('Hoge', 'Pyon');

至極当然のように pyon の中身は"HogePyon"となる。

次に以下のコードを試す。

concat02.js
const sum = require('./sum');
let sanzyu = sum('10', '20');

sanzyu の中身は"1020"となる。

次に以下のコードを試す。

concat03.js
const sum = require('./sum');
let sanzyu = sum([10], [20]);

sanzyu の中身は"1020"となる。

次に以下のコードを試す。

concat04.js
const sum = require('./sum');
let sanzyu = sum({val: 10}, {val: 20});

sanzyu の中身は"[object Object][object Object]"となる。

次に以下のコードを試す。

concat05.js
const sum = require('./sum');
let nyan = sum(null, null);

nyan の中身は0となる。

次に以下のコードを試す。

concat06.js
const sum = require('./sum');
let nyan = sum(null, true);

nyan の中身は1となる。

次に以下のコードを試す。

concat07.js
const sum = require('./sum');
let nyan = sum(true, true);

nyan の中身は2となる。

次に以下のコードを試す。

concat08.js
const sum = require('./sum');
let nyan = sum(null, undefined);

nyan の中身はNaNとなる。

次に以下のコードを試す。

concat09.js
const sum = require('./sum');
let nyan = sum('', undefined);

nyan の中身は"undefined"となる。

次に以下のコードを試す。

concat10.js
const sum = require('./sum');
let num = {
  val: "100",
  [Symbol.toPrimitive](hint) {
    return parseInt(this.val); // 普通は hint で分岐する
  }
};
let nyan = sum(num, num);

nyan の中身は200となる。

テストはパスしている

ここで使われているsumの実装はsum.test.jsのテストをパスしている。テストコードというのは、それをすべてパスしていればテスト対象のコードが(ある程度)正しいと検証するためのコードである。実装とテスト、それぞれが正しいかどうかで4パターン考えられる。

  1. sum.jssum.test.jsが間違っている。
  2. sum.jsは間違っているが、sum.test.jsは正しい。
  3. sum.jsは正しく、上の動作はすべて仕様どおりであるが、テストコードが足りない。
  4. sum.jsは正しく、上の動作はすべて仕様どおりであり、sum.test.jsで十分に検証できている。

sum.jsが間違っているのだろうか?この場合はそれをpassしてしまったテストコードも間違っているといえる。それは本当だろうか?

それとも、上の動作はすべて仕様どおりであるが、テストコードが足りないのだろうか?

それとも、上の動作はすべて仕様どおりであり、かつ、その仕様はsum.test.jsで十分に検証できているのだろうか?

あらゆる製品には動かすための前提条件がある

ソフトウェアも含め、自動車や家電といったあらゆる製品にはその部品も含め動作させる前提条件がある。前提条件がない製品というのは私の知っている範囲ではない。最低でも未だ人類は太陽に突っ込ませることを許容した製品というのを作ったことがないはずである。だからマニュアルやデータシートには温度や湿度などの動作要件が書かれており、その範囲内でのみの動作を保証している。実際の製品試験も動作要件の範囲内での挙動を試験するので、例えば溶鉱炉の中に車を突っ込んで試験をするなどという意味不明なことはしない。溶鉱炉に突っ込んで車が壊れたなら、明らかに溶鉱炉に突っ込んだ人が、動作要件を守らなかったのが悪い。

ソフトウェアにおけるテストコードというのはこの動作要件の範囲内での挙動を検証するための試験に相当する。では、ソフトウェアにおける動作要件とはなんなのだろうか?今回のような関数記述であるなら、それは言語処理系と関数への引数であろう。(closureも考える必要があるが、ここでは無視する。)今回、の sum.js は数値同士の加算を想定したプログラムであり、そこに数値以外を代入されることは想定していない。だけど、先の sum.jssum.test.js のどちらともに、その想定に関して一切記述していない。このままの状態では暗黙的に動作要件を設けているだけで、このプログラムを利用するものにそれが伝わらない。

どのように動作要件を伝え、守らせるか

では、どのように動作要件を利用者に伝えるべきだろうか?愚直な方法はコメントを記述する方法だろう。

sum.jdoc.js
/**
 * 2つの引数を受け取り、それらを加算します。
 * @param {number} a 1つ目の引数
 * @param {number} b 2つ目の引数
 */
function sum(a, b) {
  return a + b;
}
module.exports = sum;

せっかくなのでJDocで書いてみた。確かにこれで伝わるだろう。電子機器などの製品の部品であれば、データシートに動作要件の記載を記載しているので、ほぼ同程度に達しているように見える。開発者が片っ端からコメントを読んで、適切に守れているか検証すれば良い。(なお実際の回路設計ではCADの類にやらせることになるのだが。)

だが、膨大な数のソフトウェアコンポーネントに対してそんな作業をしていられるだろうか?不可能ではないが、とんでもなく高いコストが掛かることは目に見えている。これを可能な限り削減できることに越したことはないのは明らかである。

もう一つ実行時型を検証するという方法もありえるだろう。

sum.rtti.js
function sum(a, b) {
  if (typeof(a) != "number" || typeof(b) != "number") {
    throw new TypeError("Invalid type");
  }
  return a + b;
}
module.exports = sum;

この挙動そのものを仕様として、利用者側に渡すということである。だが、これを渡された利用者はどのようにして、自分たちのコードが sum を呼び出すときに必ず数値を与えているか検証するのだろうか?製品を外に出したあとにこの例外が発生してしまうのは大変困る以上、気合で検証をする必要がある。利用者側でもテストコードを書いて、この例外を踏み抜いていないことを確認することになるが、そのためのテストパターンを増強する必要が出てくる。その利用者側コードでは当然のように他の関数も使っているだろうから、このテストパターンは倍々ゲームで複雑化していくことになる。

型検査

そこで「型」の登場である。

sum.ts
function sum(a:number, b:number): number {
  return a + b;
}
module.exports = sum;

こう書けば、人間の目にも number 型を2つ与えればいいことが明らかな上に、言語処理系がそれを検査することができる。実行時型が number 型であることを、実行するより前、開発者の手元にある段階で保証することができる。(もちろん型検査に関する仕組みが適切に機能している前提はあるが。)こうすることで、開発者はテストパターンを用意するときにも数値型が入力に来ることを想定できるため、現実的な思考規模でテストパターンを用意することができる。あらゆる入力が来るかもしれない、なんてことを考えなくて済むのだ。

ソフトウェアに限らず大半の製品というのは開発者の手元を離れたところで活躍することで価値を生み出すことができる。そのため開発者は実行時環境というのを完全に制御することが難しい。車を太陽に突っ込むのはあまりに非現実的だが、スマートフォンを水中に入れたり、ソフトウェアに対してトンチンカンなデータを入れることはごく簡単にできてしまう。だからこそ、マニュアルを書いたり、データシートを用意したり、型を書いたりすることで、それを可能な限り制御して、その上で初めて製品仕様どおり動いているかが検証可能になる。そして、ソフトウェアの世界ではとても強力な「型」という助っ人がいる。床一面に並べられた設計図を片っ端から検算するなどということをしなくても、特定のコンポーネントに入るデータをある程度絞り込むことができる。

もちろん、型の能力の問題から完全に設計を記述、検証することはできない。(それこそ定理証明が必要である。)だが、検証コストを劇的に下げることは可能なのだ。これはテストコードでは困難である。だからテストは型の代わりにならない。

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

個人的な Nuxt.js のコンポーネントを Github Pages で公開した

先日、以下のような記事を書いた。

実際、このようなコンポーネントを開発して、動かせる場所があるといいと思ったので、Github Pages でページを作成した。 なお、公開には gh-pages を利用した。

以下、記事執筆時点で記載している2コンポーネントについて簡単に説明

OneClickButton.vue

以下の記事の内容から名前を変えただけ。

EventMessageBoard.vue

メッセージを追加してから、一定時間後にメッセージが消えてくれるようなコンポーネント。
イベント(遅延処理やエラーなど)の完了後、これを一時的にユーザーに通達したいようなケースで利用することを想定している。

以下のような仕組みで実装した。

  • 利用時に ref を使って、コンポーネント利用者が特定のオブジェクトを参照可能にする (あまり ref は使いたくなかったが...もっと良い方法があれば...)
  • イベント発火時に this.$refs.__参照名__.add(message); のようにして通達すると、コンポーネント側で値を自動追加し、同時にTimeoutで一定時間後にメッセージを削除するイベントを追加する
  • transition によってある程度アニメーション付きで消えるようにする

苦労した点としては単体テストで setTimeout をどう扱うかだったが、これは jestsetTimeout をテストするための仕組みを持っていたので、これをうまく使うことで解決できた。

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

AfterEffectsでニコ生コメント合成プログラムを書いたときのログ


※ニコニコのユーザーブロマガサービス終了に合わせて、移動&編集した記事です。
元記事(2017-10-02)
https://ch.nicovideo.jp/FlyingEchidna/blomaga/ar1343181

さて。
生放送動画(配信映像+コメントの動画)を投稿するにあたり、自分がやったことを、ログ残しがてら、ぶっちゃかす。
各々の細かいツールの仕様や操作等々は省くのであしからず。

なんでこんなややこしいことを…?

「生放送動画なんてタイムシフトの画面をキャプチャするんが手っ取り早いやん?」
「投コメでコメント再現した方が文字列データとしても残るで?」
「探したら確かそういう動画を出力するツールあったと思うんやけど…」
ごもっとも。
理由を挙げるとしたら…
「コメントの流し方(アニメーション)を制御したかった」になる。
「興味半分で作ってみたかった」とも言う。

具体的には、表示時間や配置位置をコントロールできるようにしたかった。
ニコニコのコメントは3秒で、画面右から左に、画面上から順に流れる仕様、とのこと。
これを少なからず、もっとゆっくり、動画へのコメントと被らないよう、画面の下から順に流れるようにしたかった。

あとは、今回はシステムコメント排除ぐらいしかしてないけども…
既存の配置や配色設定、あるいはこっちで定義した特定文字列やユーザーを検出して、表示方法を切り替えるってのも、できる状態にしておきたかった。
「自分のコメントは動画化するときには含めて欲しくない」とか要望があっても、一応対応できる。
他、お遊び定義を入れて遊んでもいいかな?とも思う。

ついでに自分でテロップ用の文字列情報作って流し込めば、別に生放送動画じゃなくても、テロップ機能として使える。
アニメーション方法の定義を自作でするわけだから、好きなようにできるし、まあ後々自作動画で何かしたいときに使えるでしょう。

ま。そんなかんじ。

動画投下までの手順&解説

 1. NiconicoLiveEncoderで放送映像をローカル保存
 2. NiconamaCommentViewerでコメントをファイル保存
 3. AfterEffectsで動画とコメントを読み込んでエクスプレッションでコメントを合成&必要あれば編集
 4. MediaEncoderでレンダリング
 5. ニコ動に投下

1. NiconicoLiveEncoderで放送映像をローカル保存

「配信時に録画を行う」設定を入れておけば、勝手に保存される。
ここで保存される映像は、実際に配信したタイムシフトのような映像…ではなく、ツール側が配信するために取り込んだ大元の映像なので、
例え放送準備中の段階であっても、ツールの「配信開始」ボタンを押した瞬間から、ぜーんぶ保存される。
ツール側の不良がない限りは、ニコ生側の配信関連サーバーエラーによる画面真っ暗期間も関係ない。
細かいところを気にしだすといろいろ融通は利かないのかもしれないが、便利。

2. NiconamaCommentViewerでコメントをファイル保存

接続して表示した生放送コメントを「名前を付けて保存」すれば、xml形式で保存化される。使うコメントのデータはこれ。
一応「テキスト形式で保存」もしておく。こっちの方が情報量は少なくて見やすいので、こっちでコメントされた再生時間を確認する。
最初はテキスト形式の方を元データとして読み込んで解析&使ってたけども、1コメントの情報の区切り文字がタブだったり改行だったりで、コメントそのものにソレを使われたら1コメント分の解析ができなくなってアウトだなと思ったから、やめた。
細かい解析処理は3で。

3. AfterEffectsで動画とコメントを読み込んでエクスプレッションでコメントを合成&必要あれば編集

本題はここ。
先に軽く説明すると『エクスプレッション』はAfterEffectsで使えるスクリプト(プログラム)のこと。
実際はJavaScriptだったりするけど…もうね。JavaScriptって環境によって言語使用変えたい放題だから、わけわかんないよね…
まあ、それはさておき。
処理の概要は…

  1. 保存しておいたコメントのxmlデータをresouceという『テキストレイヤーに』貼り付ける
  2. 処理本体となるエクスプレッションのソースコードをfunctionという『テキストレイヤーに』書く
  3. intermediateというテキストレイヤーのソーステキストのエクスプレッションで、中間処理結果が出るように『functionレイヤーに書いた処理をeval()で実行』
  4. 各種コメントを表示するためのテキストレイヤーのソーステキスト&位置のエクスプレッションで『functionレイヤーに書いた処理をeval()で実行』してコメントとして表示

『テキストレイヤーにプログラムのソースコードを書いて実行する』という
「普段AfterEffectsを使ってる人でも、こんな使い方しないだろう!?」みたいな、実はとんでもないことをしている。
「テキストレイヤーをデータ格納場所として使う方法、かなり便利だよね」と言いつつ乱用した結果がこれだよ。
非表示にしてもデータは参照できるし、適当な位置に適当に書ける感じ。適当すぎる。

AfterEffectsらしからぬ画面のスクショも張っておこう。
AE_SS.png

resourceレイヤーには、以下のエクスプレッション制御エフェクトのスライダーもかけてある。
TEXT_HEIGHT :コメント1行の高さ ※行間を開けたかったらここで設定
TEXT_WIDTH :コメント1文字の幅 ※下の方に余談あり
VIEW_DURATION:1コメントの表示時間
OFFSET_MINUTE:解析&表示するコメントの時間のオフセット ※コンポジションMAX尺が3時間で、6時間分のコメントを表示切替するために用意

各種クソコードもぶっちゃかすんだぜ。ヒューきったねぇー。
なんでもいいけど『データ』ってレイヤー名…ってかコンポジション名、すげぇブサイクだなwwwいいんだけどさ。
見返すと気になるところいろいろあるなぁ…まあいいんだけど(適当すぎ

functionレイヤーのソーステキスト(処理本体)
var TextData = function( lineIndex, viewTime, viewText ) {
  this.lineIndex = lineIndex;
  this.viewTime  = viewTime;
  this.viewText  = viewText;
};

var FUNCTION = {
  makeData: function()
  {
    resourceLayer = thisComp.layer("resource");
    originalStr = resourceLayer.text.sourceText.valueAtTime(0);
    liveStartTime = parseInt(originalStr.match(/<StartTime>(\d+)<\/StartTime>/)[1]);
    textStr = originalStr.match(/<chat.+<\/chat>/)[0];
    viewDuration = resourceLayer.effect("VIEW_DURATION")(1).valueAtTime(0);
    offsetTime = -resourceLayer.effect("OFFSET_MINUTE")(1).valueAtTime(0) * 60 - liveStartTime;
    result = Array();
    reg = RegExp("date=\"(\\d+?)\".*?>(.+?)<\/chat>", "g");
    while( (targetList = reg.exec(textStr)) !== null ) {
      if( targetList[2][0] == '\/' ) continue;
      viewStartTime = parseInt(targetList[1]) + offsetTime;
      if( viewStartTime < 0 ) continue;
      lineIndex = 0;
      if( result.length > 0 ) {
        dataIndex = result.length;
        isRewrite = false;
        for( i = result.length - 1; i >= 0; i-- ) {
          if( viewStartTime > result[i].viewTime + viewDuration ) break;
          dataIndex = i;
          lineIndex = Math.max(lineIndex, result[dataIndex].lineIndex + 1);
        }
        for( ; dataIndex < result.length; dataIndex++ ) {
          diffTimeRatio = (viewStartTime - result[dataIndex].viewTime) / viewDuration;
          textRatio = (result[dataIndex].viewText.length) / 40;
          if( diffTimeRatio - textRatio > 0.5 ) {
            lineIndex = Math.min(lineIndex, result[dataIndex].lineIndex);
            isRewrite = true;
            break;
          }
        }
        if( isRewrite && lineIndex > 0 ) lineIndex = 0;
      }
      result.push(new TextData(lineIndex, viewStartTime, targetList[2]));
    }
    resultStr = "";
    for( i = 0; i < result.length; i++ ) {
      if( result[i].lineIndex < 0 ) continue;
      resultStr += result[i].lineIndex.toString() + "," + result[i].viewTime.toString() + "," + result[i].viewText.toString() + "\r";
    }
    return resultStr;
  },

  getViewText: function()
  {
    dataComp = thisComp.layer("データ").source;
    textList = dataComp.layer("intermediate").text.sourceText.valueAtTime(0).split("\r");
    baseIndex = thisComp.layer("base").index;
    dataIndex = index - baseIndex - 1;

    if(textList == null || dataIndex >= textList.length) return "";

    viewDuration = dataComp.layer("resource").effect("VIEW_DURATION")(1).valueAtTime(0);
    maxLineNum = thisComp.numLayers - baseIndex;
    for( ; dataIndex < textList.length; dataIndex += maxLineNum ) {
      dataList = textList[dataIndex].match(/(\d+?),(\d+?),(.+)/);
      if( dataList == null ) return "";
      viewTime = parseInt(dataList[2]);
      if( time < viewTime ) break;
      if( time >= viewTime + viewDuration ) continue;
      return dataList[3];
    }
    return "";
  },

  getViewPos: function()
  {
    dataComp = thisComp.layer("データ").source;
    textList = dataComp.layer("intermediate").text.sourceText.valueAtTime(0).split("\r");
    baseIndex = thisComp.layer("base").index;
    dataIndex = index - baseIndex - 1;

    if(textList == null || dataIndex >= textList.length) return [0, 0];

    resourceLayer = dataComp.layer("resource");
    viewDuration = resourceLayer.effect("VIEW_DURATION")(1).valueAtTime(0);
    maxLineNum = thisComp.numLayers - baseIndex;
    for( ; dataIndex < textList.length; dataIndex += maxLineNum ) {
      dataList = textList[dataIndex].match(/(\d+?),(\d+?),(.+)/);
      if( dataList == null) return [0, 0];
      viewTime = parseInt(dataList[2]);
      if( time < viewTime ) break;
      if( time >= viewTime + viewDuration ) continue;
      targetIndex = parseInt(dataList[1]);
      viewRatio = 1 - (time - viewTime) / viewDuration;
      textWidth = dataList[3].length * resourceLayer.effect("TEXT_WIDTH")(1).valueAtTime(0);
      textHeight = resourceLayer.effect("TEXT_HEIGHT")(1).valueAtTime(0);

      return [viewRatio * (thisComp.width + textWidth) - textWidth, textHeight * (thisComp.numLayers - targetIndex - baseIndex)];
    }
    return [0, 0];
  }
};
intermediateレイヤーのソーステキストのエクスプレッション
eval(thisComp.layer("function").text.sourceText.value);
FUNCTION.makeData();
各種コメントを表示するためのテキストレイヤーのソーステキストのエクスプレッション
eval(thisComp.layer("データ").source.layer("function").text.sourceText.valueAtTime(0));
FUNCTION.getViewText();
各種コメントを表示するためのテキストレイヤーの位置のエクスプレッション
eval(thisComp.layer("データ").source.layer("function").text.sourceText.valueAtTime(0));
FUNCTION.getViewPos();

【余談】

テキストレイヤーの現在の高さ&幅ってエクスプレッション側から取れないみたいね。
「sampleImage使ってピクセル走査して幅を調べたわ」って人がいて…おぉう…ってなった。
そこまですれば確かに厳密な幅が取れるだろうけど、今回レンダリング時間もそんなにかけたくないし、処理の軽量化かねて自分で幅を定義&算出することにした。
とはいえ等幅フォントじゃないし、大体なんだけどね…

処理速度がレンダリング時間にダイレクトアタックなのも悩みどころだったね。
軽く検証してみたら、正規表現での字句解析がゲロ重だったから、極力軽くなるように解析対象文字列数を減らしたり実行回数を減らしたりした。
その関係で、実は途中で結構デカめのリファクタリングもした。
具体的には…
元々、中間データとして『現在表示するコメントを表示順で出力』していたんだけども、これだと毎時間元データを参照&解析しなきゃいけない状態だった。
これを、1フレーム目の時点で全コメントを走査して『表示する時間と表示インデックスを出力』するようにした。
もひとつおまけに、プリコンポジット化してデュレーションを1にしたものを、レイヤーとして配置&タイムリマップで1フレーム目で停止させる、まで徹底してみた。
一応これで毎フレーム中間データを出力する処理が走ることはなくなったし、軽くはなったっぽい。

他にあったことと言えば…
6時間の動画にもなってくると、読み込み時にAfterEffectsに
『After Effects エラー: オーバーフロー比分母変換( 17 、 18 )』
つって、怒られてね。
Media Encoderで、
音だけの6時間分aacファイル、
映像だけの1時間分mp4ファイル×複数、
を出力しなおして、それをAfterEffectsで編集した。
音ズレもしてたから映像側をタイムリマップで無理矢理調整したりもした。
結果的に時々映像にノイズが入っちゃってたので、それはそれで別途反省。

4. MediaEncoderでレンダリング

前までAfterEffectsからそのままmp4を出力できてたんだけどね。
CCになってからか、めんどくさいことに、できなくなってるんだよね。
ということで、あまり使ったことのないMediaEncoderのお世話になった。
AtferEffectsで作業しててもレンダリングを進めてくれるから、作業の手を止めなくて済んだね。
まあでも多分、ちゃんとした方法でレンダリングをマルチタスク化するとか、無理矢理別タスクのAfterEffectsを立ち上げて動かすとか、贅沢なので言えばレンダリングマシンを用意するとか、レンダリングの効率化自体はいろいろ方法があると思うから、自分がやった方法は微妙なんだろうなとも思う。
3Dバリバリでもないし、今のところそこまで苦労したことはないから、まあいいんだけど…

5. ニコ動に投下

そのまんま。出力結果をニコ動にダンクシュート。

ざっと、以上。
かなり概要すぎて、伝えるための情報になってないけど…まあ、自分用のメモって意味合いの方が強いから、いっか(酷

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

Canvasでノイズを実装する方法

サンプル

See the Pen noise by pd_kosaka (@pd_kosaka) on CodePen.

解説

①canvas要素を作成

canvas#canvas

②ノイズを作成(size、interval、alphaの値を変更して見え方を調整する)

window.onload = function() {

    var size = 100,
        interval = 5, //再描画する間隔を設定
        alpha = 25; // 0〜255の間で設定

    var canvas = document.getElementById('canvas'),
        ctx = canvas.getContext('2d'),
        cw = ctx.canvas.width,
        ch = ctx.canvas.height,
        length = size * size * 4,
        noise,
        noiseCtx,
        noiseData,
        num = 0,
        value;


    noise = document.createElement('canvas');
    noise.width = size;
    noise.height = size;
    noiseCtx = noise.getContext('2d');
    noiseData = noiseCtx.createImageData(size, size);
    requestAnimationFrame(loop);


    function loop() {
        if (++num % interval === 0) {
            for (var i = 0; i < length; i += 4) {
                value = (Math.random() * 255) | 0;
                noiseData.data[i    ] = value;
                noiseData.data[i + 1] = value;
                noiseData.data[i + 2] = value;
                noiseData.data[i + 3] = alpha;
            }
            noiseCtx.putImageData(noiseData, 0, 0);
            ctx.clearRect(0, 0, cw, ch);
            ctx.fillStyle = ctx.createPattern(noise, 'repeat');
            ctx.fillRect(0, 0, cw, ch);
        }
        requestAnimationFrame(loop);
    }

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

Yellowfinのコードモードで適用されたフィルターの値を取得する方法

どのような時に必要か

Yellowfinのダッシュボード構築中に、フィルターがたくさんある場合、どのフィルターの何の値で絞られた結果なのかを表示したい場合などに使えると便利だと思います。予めフィルターの状態を表示するテキストの箇所を配置してinnerHTMLで突っ込むイメージです。
YellowfinWikiに説明がありますが少し分かりづらいです。また、デフォルトの値も取得することができます。(同じページ内の記述参照)

各フィルターの種類によって取得の仕方が変わる

これが一番わかりにくいのですが、大きく分けて「一覧に含む」、「〜の間」、「単一入力」の3種類のフィルターの絞り込む種類によって適用されている値の取得法が変わります。

フィルターの種類が「一覧に含む」の場合(リスト・チェックボックスなど)

フィルターオブジェクトのappliedValues.valueListに値が格納されるのでこれをforやforeachで取得する。

inList.js
let comments = this.apis.canvas.select('current_filters');
let text_detail = '';
let filters = this.apis.dashboard.filters;
let filter1 = filters.getFilter('一覧に含むフィルター');

if (filter1.appliedValues.valueList.length > 0) {
    text_detail = '<b>一覧に含むフィルター:</b>';
    for (var i = 0; i < filter1.appliedValues.valueList.length; i++) {
        text_detail = text_detail + ' ' + filter1.appliedValues.valueList[i];
    }
    text_detail = text_detail + ' ';
}

フィルターの種類が「〜の間」の場合(日付や年齢など)

フィルターオブジェクトのappliedValueOneとappliedValueTwoにそれぞれの値が格納されるのでそれぞれのプロパティから取得する。

between.js
let comments = this.apis.canvas.select('current_filters');
let text_detail = '';
let filters = this.apis.dashboard.filters;
let filter2 = filters.getFilter('〜の間フィルター');

if (filter2.appliedValues) {
    text_detail = text_detail + '<b>〜の間フィルター:</b>';
    text_detail = text_detail + ' ' + filter2.appliedValueOne + '~' + filter2.appliedValueTwo + ' ';
}

フィルターの種類が「単一入力」の場合(名前など)

フィルターオブジェクトのappliedValueOneに値が格納されるのでそこから値を取得する。

textbox.js
let comments = this.apis.canvas.select('current_filters');
let text_detail = '';
let filters = this.apis.dashboard.filters;
let filter3 = filters.getFilter('単一入力フィルター');

if (filter3.appliedValues) {
    text_detail = text_detail + '<b>単一入力フィルター:</b>';
    text_detail = text_detail + ' ' + filter3.appliedValueOne + ' ';
}

値の表示について

current_filtersの名前がついたオブジェクトの文字列に取得したtext_detail変数を突っ込みます。

innnerHTML.js
comments.innerHTML = text_detail;

まとめ

複雑なフィルターを駆使している場合、ぱっと見でわかるような仕組みはすごく便利ですよね。
コードモードを使用することによってフィルターの表示の仕方も自由度が上がりましたね。

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

JavaScriptで正規表現を扱ってみた!【備忘録】

こんにちは!
今回は、開発で正規表現を少し利用したので、JavaScriptで正規表現を扱う方法について、簡単にまとめてみます!

正規表現って...?

「正規表現」って、なんかお堅いお名前ついてますが...

正規表現

文字と記号を使って、パターンを指定し、そのパターンに合った文字を検索をする。
文字列だけでなく、数字も検索対象に含まれる。
書き方には、「正規表現リテラル」と「RegExpのコンストラクターを経由する」の2種類がある。


正規表現を利用することで、検索する文字列が曖昧な状態でもパターンを使って、検索することができます!

パターン指定でよく使われる記号(正規表現パターン)

ABC 「ABC」と言う文字列
[ABC] A、B、Cのいずれか1文字
[^ABC] A、B、C以外のいずれか1文字
[A-Z] A〜Zの間の1文字
A|B|C A、B、Cのいずれか
[0-9] 0〜9のいずれか
X* 0文字以上のX
X? 0または1文字のX
X+ 1文字以上のX
X{n} Xとn回一致
X{n,} Xとn回以上一致
X{m,n} Xとm〜n回一致
^ 行の先頭に一致
$ 行の末尾に一致
. 任意の1文字に一致
\w 大文字/小文字の英字、数字、アンダースコアに一致。([A-Za-z0-9_]と同意。)
\W 文字以外に一致([^\w]と同意。)
\d 数字に一致([0-9]と同意。)
\D 数字以外に一致([^0-9]と同意。)
\n 改行に一致
\t タブ文字に一致
\s 空白文字に一致([\n\r\t\v\f]と同意。)
\S 空白以外の文字に一致([^\s]と同意。)
\〜 「〜」で表される文字

正規表現の作り方

①正規表現リテラル

let 変数 = /パターン/フラグ

②RegExpのコンストラクターを経由する

let 変数 = new RegExp('パターン', 'フラグ');

コンストラクターとは、クラスをnew演算子で実行したときに呼び出され、オブジェクトのプロパティを初期化する関数です。

ここで、「フラグ」というものが出てきました。「フラグ」は、正規表現の挙動を決めるためのものです。検索範囲や、大文字・小文字の区別などを指定することができます。以下が、主な「フラグ」です。

g 文字列全体に対して処理を行う(無指定であれば、1度一致したらおわり)
i 大文字・小文字を区別しない
m 複数行に対応
u Unicode対応

文字列検索をやってみよう!

それでは、ちょっと使ってみましょう!
正規表現リテラルを使ったver.と、RegExpのコンストラクター経由したver.の2種類の使って、実践してみます!

execメソッド

テキストの中に含まれている「数字」を検索してみます。

// 正規表現リテラルver.
let sample1 = /[0-9]/;
let txt = 'テレフォンショッピング!午後3時までにお電話頂いた方には、割引!お電話は、0120-123-456まで!'
let result = sample1.exec(txt);
console.log(result);
/* 実行結果
3
*/

// RegExpのコンストラクター経由ver.
let sample2 = new RegExp('[0-9]', 'gi');
let txt = 'テレフォンショッピング!午後3時までにお電話頂いた方には、割引!お電話は、0120-123-456まで!'
let result2 = sample2.exec(txt);
console.log(result2);
/* 実行結果
3
*/

execメソッドで、1度に得られる結果は、最初の1つだけです。

matchメソッド

こちらも、テキストの中に含まれている「数字」を検索してみます。

// 正規表現リテラルver.
let sample3 = /[0-9]/gi;
let txt = 'テレフォンショッピング!午後3時までにお電話頂いた方には、割引!お電話は、0120-123-456まで!'
let result3 = txt.match(sample3);
console.log(result3);
/* 実行結果
[3, 0, 1, 2, 0, 1, 2, 3, 4, 5, 6]
*/

// RegExpのコンストラクター経由ver.
let sample4 = new RegExp('[0-9]', 'gi');
let txt = 'テレフォンショッピング!午後3時までにお電話頂いた方には、割引!お電話は、0120-123-456まで!'
let result4 = txt.match(sample4);
console.log(result4);
/* 実行結果
[3, 0, 1, 2, 0, 1, 2, 3, 4, 5, 6]
*/

matchメソッドもexecメソッドと同じように、検索をしていますが、matchメソッドの場合は、検索した全ての結果を返します。

置き換えをやってみよう!

正規表現を使って文字列検索をし、一致した文字列・数字・記号などを、任意の文字列などに置き換えることができます。

// 正規表現リテラルver.
let sample5 = /。|$/;
let txt = 'こんにちは。';
let result5 = txt.replace(sample5, '、JavaScriptよ!');
console.log(result5);
/* 実行結果
こんにちは、JavaScriptよ!
*/

// RegExpのコンストラクター経由ver.
let sample6 = new RegExp('。', 'gi');
txt = 'こんにちは。';
let result6 = txt.replace(sample6, '、JavaScriptよ!');
console.log(result6);
/* 実行結果
こんにちは、JavaScriptよ!
*/

「こんにちは。」と言う文字列の中で、句点(。)を検索してきて、句点(。)を「、JavaScriptよ!」に置き換えています!

replaceメソッドについてちょこっと解説

上のコードで、「matchメソッドも使ってないし、どこで検索してるの?」と思ったかと思います。
replaceメソッドの解説をしてみます!

let 変数 = 検索する文字列.replace(正規表現(検索したいパターン), 置き換えたい文字列など);

第一引数に検索するパターンを渡し、そのパターンに一致した部分を第二引数に渡した文字列などに置き換えるのです!
もし、パターンに一致する部分がなければ、何も変化していない「検索する文字列」がそのまま返ってくるのみです!

まとめ

JavaScriptのテキストの正規表現の項目を初めて見たときは、「記号だらけでわけわからん!」と思い、少し勉強する気が失せそうだったのですが、実際に使ってみると、便利で、面白かったです!
開発の幅が広がった気がします!

お読みくださり、ありがとうございました!
もし、間違いや補足などございましたら、コメント頂けますと助かります!

参考文献

柳井政和著 『JavaScript[完全]入門』(2021)p.238〜p.248
山田祥寛著 『改訂新版 JavaScript本格入門~モダンスタイルによる基礎から現場での応用まで』(2018)p.151〜161

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

Introduction to Cookies & Storage (Local and Session Storage)

If you have been working on frontend development for quite some time, undoubtedly you will encounter cookies and storages at some point.

So, what are they?

Cookies

Cookies in terms of software development are information stored in a file or several files on your computer by websites that you visit.

A cookie...

  • Is a pretty old technology dating back to 1994.
  • Is used mainly for state management (it remembers your logins, shopping carts), personalization (user preferences), and tracking (might be used for recording and analyzing user behavior).
  • Is small-sized, up to 4096 bytes (~4kb).
  • Can be accessed server-side or client-side.
  • Has a lifetime depending on its definition. Session cookies are deleted when the user's browsing session ends, while permanent cookies last until its Expires date attribute or Max-age attribute.
  • Uses key-value pairings.

How to make secure cookies

Use Secure attribute

This ensures cookies to be sent only to the server on encrypted requests over HTTPS, never over HTTP. It's better to use this attribute in tandem with HttpOnly.

Use HttpOnly attribute

Despite its potentially confusing name (as it contradicts the above attribute!), this attribute prevents access of cookies using Document.cookie API by ensuring they are sent only to the server. This helps mitigating XSS (cross-site scripting) attacks.

Shorten the lifetime of sensitive cookies

If cookies are used to store user-related information such as authentication, it's better to set a short lifetime.

Combined all three together, we can set up a cookie like:

Set-Cookie: id=[value]; Expires=[date]; Max-Age=[age]; Secure; HttpOnly
Set-Cookie: id=cookie_id; Expires=Thu, 16 June 2020 06:30:00 GMT; Max-Age=2592000; Secure; HttpOnly

If both Expires and Max-Age exist, Max-Age has precedence.

Set SameSite attribute

Cookies misuse can lead to CSRF (cross-site request forgery). Let say you visited an e-commerce website (A). The website links to another website (B) that hosts some of the images for the product you are browsing. While sending request for images to B, cookies belong to that website might be sent along, thus potentially enabling B to know what are you doing in A.

We can, for instance, block all third-party websites` cookies to prevent CSRF, but that might lead to a poor browsing experience.

Instead, we should be setting SameSite attribute. By doing so, we can control whether to allow cookies to be sent along for requests initiated by third-party websites. For example:

Set-Cookie: SameSite=Lax; 
Set-Cookie: SameSite=Strict;

You can read a much complete explanation here.

Local storages

Local storages in terms of software development, as the name implies, are storages stored locally in your computer by websites that you visit. It might sound similar to cookies because, in a sense, they have a similar purpose! But here's the difference.

Local storage...

  • Came into prominence around the early 2010s.
  • Is usually used for personalization (user preferences, preferred theme) that is not sensitive data.
  • Has max size up to 10MB depending on the browser that you use (much bigger than cookie).
  • Has no expiration time.
  • Can only be accessed on the client-side via window.localStorage. This means on each HTTP request, data in local storage is not sent to the server.
  • Uses key-value pairings, always in UTF-16 format.

Note: Don't confuse local storage with cache storage. That's an entirely different storing method.

On security

Local storages store information as is, and it has no data protection. Period. So obviously we should never store in local storage sensitive data such as:

  • User & sessions IDs
  • JWTs (JSON Web Tokens)
  • Personal information
  • Any credit card related information
  • API keys

And since local storages are pure JavaScript, there's no telling that a malicious script may access your storages and pull out all the information there since it's quite easy to loop over storage without knowing the keys.

Something like:

for (let i = 0; i < localStorage.length; i++) {
  let key = localStorage.key(i);
  console.log(`${key}: ${localStorage.getItem(key)}`);
}

What about session storage?

It's basically just local storage with a limited lifetime. Session storages exist as long as the webpages/browser tabs that initiated them are alive.

  • If you open a site in several browser tabs, each browser tab will have different session storage.
  • If you do a page refresh, the data stays. But closing the tab will remove the data.

I personally have never used session storage. Feel free to let me know what would be the optimum usage.

Closing

This short article (more like a memo) is nothing spectacular. Other people might have written something more comprehensive regarding this topic.

So why did I do this?

Because I'm a firm believer that if you want to remember something, write it down in your own words and you will remember it better. And it's been working great for me so far! ^^

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

【JavaScript初心者】ババ抜きを作った。

JavaScriptの勉強のため、ババ抜きを作りました。
遊んでみるのもよし、自作して私のものと比較してみるのもよしだと思います。

本記事の環境

※PCに環境構築を行う必要はありません。
WEBブラウザ(Google Chome)
テキストエディタ

ババ抜きとは

ババ抜き(Old Maid、Lose with the Joker、Joker Game)とは、複数人で行うトランプの遊び方のひとつ。始めに同数のカードを人数分配り、一枚ずつ他者から抜き取り同じ札があれば捨て、最後にジョーカーを持っている人が負け。
https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%90%E6%8A%9C%E3%81%8D

要件・仕様

  • 言語の勉強が主目的なので、見た目はこだわらない。
  • 言語の勉強が主目的なので、処理は小分けにするし、コメントも残す。
  • フレームワークは使わない。
  • 基本的なババ抜きのルールに準ずる。
  • 4人対戦とする。
  • 勝敗くらいは表示する。
  • プレイヤー1人、CPU3人とする。
  • プレイヤー01がプレイヤー02の手札を引くところから始める。(本来のルールは、一番手札が多い人からスタート。)
  • CPUが引く手札の位置はランダムとする。
  • 各プレイヤーが持っている手札は、ターンごとに手札内でシャッフルする。
  • CPU同士の動きは処理だけ行って画面上ではスキップする。
  • ジョーカー(以下、画像のJKR)を持っているプレイヤーは、一度だけ、プレイヤー同士の手札のシャッフルを行うことができる(シャッフルタイム)。どのようにシャッフルされるかはランダムとする。

成果物(画面)

oldMaid_05.png

所感

  • ボタン、非同期での画面更新等、いくつか要素があるわりに、デザイン要素が少ないため言語学習に丁度よかった。
  • CPUの実装は難しくなかった。
  • シャッフルはオマケの機能だったが、データの入れ替え等、いい勉強になった。

成果物(ソースコード)

  • 参考程度にしてください。軽くは動作確認済みです。
oldMaid.html
<!DOCTYPE html>
<html>
    <head>
        <title>ババ抜き</title>

        <style>
            body {
                background-color : #eee;
            }
            #start, #next, #shuffle {
                font-size : 15px;
                background-color : #fff;
                text-align : center;
            }
            #result {
                font-size : 15px;
                background-color : #fff;
            }
            #player00Info, #player01Info, #player02Info, #player03Info {
                font-size : 20px;
            }
            #card {
                font-family:'MS Gothic';
                font-size : 20px;
                text-align : center;
            }
        </style>
    </head>
    <body>
        <div>
            <div>
                <span id="button"></span>
                <p id="result"></p>
                <p id="turn"></p>
                <p id="player00Info"></p>
                <p id="player01Info"></p>
                <p id="player02Info"></p>
                <p id="player03Info"></p>
            </div>
        </div>
        <script language="JavaScript">

            // カード基本情報
            const MAX_NUM = 13;
            const MIN_NUM = 1;
            const CARD_JOKER = "JKR";
            const CARD_SPADE = "&#9824;";
            const CARD_CLUB  = "&#9827;";
            const CARD_HEART = "&#9829;";
            const CARD_DIA   = "&#9830;";
            const COLOR_01   = "black";
            const COLOR_02   = "red";
            const COLOR_03   = "white";

            // ババ抜き情報
            const MAX_PLAYER = 4;

            // その他
            const MAX_SHUFFLE = 6;
            //const FLAG_DEBUG = true;
            const FLAG_DEBUG = false;

            // カード
            function Card(mark, num) {
                // プロパティ
                this.mark = mark;
                this.num = num;
            }

            // デッキ
            function Deck() {
                // プロパティ
                this.deck = [];

                // 初期化処理
                this.init = function() {
                    this.deck = [];
                    return;
                };

                // デッキ作成処理
                this.make = function() {
                    let card;
                    //for (let num = MIN_NUM; num <= 5; num++) {
                    for (let num = MIN_NUM; num <= MAX_NUM; num++) {
                        card = new Card(CARD_SPADE, num);
                        this.deck.push(card);
                        card = new Card(CARD_CLUB, num);
                        this.deck.push(card);
                        card = new Card(CARD_HEART, num);
                        this.deck.push(card);
                        card = new Card(CARD_DIA, num);
                        this.deck.push(card);
                    }
                    card = new Card(CARD_JOKER, 0);
                    this.deck.push(card);
                    return;
                };

                // デッキシャッフル処理
                this.shuffle = function() {
                    for (let loop = 0; loop < this.deck.length; loop++) {
                        let random = Math.floor(Math.random() * this.deck.length);
                        let work = this.deck[0];
                        this.deck[0] = this.deck[random];
                        this.deck[random] = work;
                    }
                    return;
                };

                // ドロー処理
                this.draw = function() {
                    return this.deck.shift();
                };
            }

            // 手札
            function Hand() {
                // プロパティ
                this.hand = [];

                // 初期化処理
                this.init = function() {
                    this.hand = [];
                    return;
                };

                // 手札セット処理
                this.set = function(card) {
                    this.hand.push(card);
                    return;
                };

                // 手札全取得処理
                this.getAll = function() {
                    return this;
                };

                // 手札全整理処理
                this.checkPairAll = function() {
                    // ペアを探し、見つけたら捨てる。
                    let handInfo = this.getAll();
                    for (let baseIdx = 0; baseIdx < handInfo.hand.length; baseIdx++) {
                        for (let targetIdx = 0; targetIdx < handInfo.hand.length; targetIdx++) {
                            if ((targetIdx != baseIdx) &&
                                (handInfo.hand[targetIdx].num == handInfo.hand[baseIdx].num)) {
                                if (targetIdx < baseIdx) {
                                    handInfo.hand.splice(baseIdx, 1);
                                    handInfo.hand.splice(targetIdx, 1);
                                } else {
                                    handInfo.hand.splice(targetIdx, 1);
                                    handInfo.hand.splice(baseIdx, 1);
                                }
                                baseIdx--;
                                break;
                            }
                        }
                    }
                    return;
                };

                // 指定手札取得処理
                this.getTarget = function(index) {
                    // 引数で指定した手札を返す。
                    let handInfo = this.getAll();
                    let card;
                    //alert("len:" + handInfo.hand.length);
                    for (let loop = 0; loop < handInfo.hand.length; loop++) {
                        if (loop == index) {
                            card = handInfo.hand[loop];
                            handInfo.hand.splice(loop, 1);
                            break;
                        }
                    }
                    //alert("mark:" + card.mark + "  num:" + card.num);
                    return card;
                };

                // 手札シャッフル処理
                this.shuffle = function() {
                    let handInfo = this.getAll();
                    for (let loop = 0; loop < handInfo.hand.length; loop++) {
                        let random = Math.floor(Math.random() * handInfo.hand.length);
                        let work = handInfo.hand[0];
                        handInfo.hand[0] = handInfo.hand[random];
                        handInfo.hand[random] = work;
                    }
                    return;
                };
            }

            // 桁数整形処理
            function spacePadding(num){
                return (Array(2).join(" ") + num).slice(-2);
            }

            // 桁数整形処理
            function zeroPadding(num){
                return (Array(2).join("0") + num).slice(-2);
            }

            // プレイヤー
            function Player() {
                // プロパティ
                this.strWin = "";
                this.results = [];
                this.results["win1"] = 0;
                this.results["win2"] = 0;
                this.results["win3"] = 0;
                this.results["lose"] = 0;
                this.cpu = true;
                this.shuffle = false;

                // 初期化処理
                this.init = function() {
                    // 抜けた番目を初期化する。
                    this.strWin = "";

                    // シャッフルタイム使用フラグを初期化する。
                    this.shuffle = false;
                    return;
                };

                // CPUフラグ設定処理
                this.setCpu = function(flag) {
                    // CPUフラグを設定する。trueがCPU。
                    this.cpu = flag;
                    return;
                };
            }

            // ババ抜き
            function OldMaid () {
                // プロパティ
                let cntTurn = 0;
                let cntWin = 1;
                let players = [];
                players[0] = new Player();
                players[1] = new Player();
                players[2] = new Player();
                players[3] = new Player();
                let deck = new Deck();
                let hands = [];
                hands[0] = new Hand();
                hands[1] = new Hand();
                hands[2] = new Hand();
                hands[3] = new Hand();

                // 初期化処理
                this.init = function() {
                    // ターン数を初期化する。
                    cntTurn = 0;

                    // 抜けた番目を初期化する。
                    cntWin = 1;

                    // デッキの初期化処理を呼び出す。
                    deck.init();

                    // プレイヤーの初期化処理を呼び出す。
                    players[0].init();
                    players[1].init();
                    players[2].init();
                    players[3].init();

                    // 手札の初期化処理を呼び出す。
                    hands[0].init();
                    hands[1].init();
                    hands[2].init();
                    hands[3].init();

                    // CPUフラグ設定処理を呼び出す。
                    players[0].setCpu(false);
                    return;
                };

                // カードイメージ作成処理
                this.makeCardImg = function(card, playerId, index) {
                    // 黒色
                    let color = COLOR_01;
                    if ((card.mark == CARD_HEART) || (card.mark == CARD_DIA)) {
                        // 赤色
                        color = COLOR_02;
                    }

                    // カードイメージを作成する。
                    let cardImg = "";
                    if (card.mark != CARD_JOKER) {
                        // ジョーカー以外である場合
                        cardImg += "<span id='card' ";
                        cardImg += "name='" + String(playerId) + "_" + String(index) + "' ";
                        cardImg += "style='color:" + color + "; ";
                        cardImg += "background-color:" + COLOR_03 + ";'>";
                        cardImg += card.mark + String(spacePadding(card.num));
                        cardImg += "</span>";
                    } else {
                        // ジョーカーである場合
                        cardImg += "<span id='card' ";
                        cardImg += "name='" + String(playerId) + "_" + String(index) + "' ";
                        cardImg += "style='color:" + color + "; ";
                        cardImg += "background-color:" + COLOR_03 + ";'>";
                        cardImg += card.mark;
                        cardImg += "</span>";
                    }
                    return cardImg;
                };

                // 手札表示共通処理
                this.outputHandPCommon = function(playerId, handInfo) {
                    let detail = "";
                    if (handInfo.hand.length == 0) {
                        // 抜けたプレイヤーである場合
                        if (players[playerId].strWin == "") {
                            // 抜けた初回である場合
                            players[playerId].strWin = "<span id='card' ";
                            players[playerId].strWin += "style='color:" + COLOR_02 + "; ";
                            players[playerId].strWin += "background-color:" + COLOR_03 + ";'>";
                            players[playerId].strWin += "  " + String(cntWin) + "抜け" + "  ";
                            players[playerId].strWin += "</span>";
                            players[playerId].results["win" + cntWin]++;
                            cntWin++;
                        }
                        detail = players[playerId].strWin;
                    } else {
                        // 抜けていないプレイヤーである場合
                        for (let loop = 0; loop < handInfo.hand.length; loop++) {
                            let card = handInfo.hand[loop];
                            if (FLAG_DEBUG == true) {
                                // カードイメージ作成処理を呼び出す。
                                //alert("outputHandPCommon card:" + card.mark + card.num + " playerId:" + playerId + " loop:" + loop);
                                detail += this.makeCardImg(card, playerId, loop);
                            } else {
                                //if ((playerId == cntTurn) && (players[playerId].cpu == false)) {
                                if (players[playerId].cpu == false) {
                                    // カードイメージ作成処理を呼び出す。
                                    detail += this.makeCardImg(card, playerId, loop);
                                } else {
                                    detail += "<span id='card' ";
                                    detail += "name='" + String(playerId) + "_" + String(loop) + "' ";
                                    detail += "style='color:" + COLOR_01 + "; ";
                                    detail += "background-color:" + COLOR_01 + ";'>";
                                    //detail += "&#92171;";
                                    detail += CARD_SPADE + "XX";
                                    detail += "</span>";
                                }
                            }
                            detail += " ";
                        }
                    }
                    return detail;
                };

                // 手札表示処理
                this.outputHandP = function() {
                    for (let loop = 0; loop < MAX_PLAYER; loop++) {
                        // 各プレーヤーごとに手札表示共通処理を呼び出す。
                        let handInfo = hands[loop].getAll();
                        let work = String(zeroPadding(loop + 1)) + "" + this.outputHandPCommon(loop, handInfo);
                        if (loop != 3) {
                            work += "<br>↓";
                        }

                        // 処理結果を設定する。
                        let id = "player" + String(zeroPadding(loop)) + "Info";
                        document.getElementById(id).innerHTML = work;
                    }
                    return;
                };

                // 手札配布処理
                this.distribute = function() {
                    let loop = 0;
                    while(1) {
                        // デッキからカードをドローする。
                        let card = deck.draw();
                        if (card == undefined) {
                            //alert("手札配布終了!");
                            break;
                        }

                        // 手札セット処理を呼び出す。
                        hands[loop % MAX_PLAYER].set(card);
                        loop++;
                    }
                    return;
                };

                // ジョーカー探索処理
                this.searchJoker = function() {
                    let info = [];
                    for (let playerId = 0; playerId < MAX_PLAYER; playerId++) {
                        //alert("searchJoker playerId:" + playerId);
                        let handInfo = hands[playerId].getAll();
                        //alert("searchJoker len:" + handInfo.hand.length);
                        let index;
                        for (index = 0; index < handInfo.hand.length; index++) {
                            if (handInfo.hand[index].mark != CARD_JOKER) {
                                continue;
                            }
                            // プレイヤーと手札の情報を設定する。
                            info["id"] = playerId;
                            info["index"] = index;
                            break;
                        }
                        if (index < handInfo.hand.length) {
                            break;
                        }
                    }
                    //alert("searchJoker playerId:" + info["id"] + "  index:" + info["index"]);
                    return info;
                };

                // ゲーム終了判定処理
                this.isFin = function() {
                    let result;
                    if (cntWin >= MAX_PLAYER) {
                        // ゲーム終了
                        result = true;
                    } else {
                        // ゲーム続行
                        result = false;
                    }
                    return result;
                };

                // ゲーム終了処理
                this.fin = function() {
                    // ジョーカー探索処理を呼び出す。
                    let info = this.searchJoker();
                    //alert("fin playerId:" + info["id"] + "  index:" + info["index"]);

                    // 処理結果を設定する。
                    let playerName = "プレイヤー" + String(zeroPadding(info["id"] + 1));
                    let work = "<p id='turn' style='color:red'>勝敗:" + playerName + "の負け</p>";
                    document.getElementById("turn").innerHTML = work;

                    // 開始ボタンを活性に変更する。
                    document.getElementById("start").disabled = false;

                    // 次へボタンを活性に変更する。
                    document.getElementById("next").disabled = true;

                    // シャッフルボタンを活性に変更する。
                    document.getElementById("shuffle").disabled = true;

                    // 勝敗カウンタをインクリメントする。
                    for (let loop = 0; loop < MAX_PLAYER; loop++) {
                        if (loop == info["id"]) {
                            players[loop].results["lose"]++;
                        } else {
                            //let winindex = "win" + info["id"];
                            //alert("winindex"+winindex);
                            //players[loop].results[winindex]++;
                        }
                    }

                    // 結果表示処理を呼び出す。
                    this.outputResult();
                    return;
                };

                // シャッフルタイム実処理
                this.shuffleTimeCore = function(kind) {
                    let winPlayerId = -1;
                    if (kind <= 3) {
                        // 抜けているプレイヤーがいるか確認する。
                        // 2人以上抜けている場合は、本メソッドは呼ばれない想定。
                        for (let playerId = 0; playerId < MAX_PLAYER; playerId++) {
                            if (players[playerId].strWin != "") {
                                winPlayerId = playerId;
                                break;
                            }
                        }
                    }

                    // 手札をシャッフルする。
                    let work;
                    switch (kind) {
                        case 0  : alert("出目 : 右1(手札を右隣りに1つ移動。)");
                                  hands.unshift(hands.pop());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 1) % MAX_PLAYER];
                                      hands[(winPlayerId + 1) % MAX_PLAYER] = work;
                                  }
                                  break;
                        case 1  : alert("出目 : 右2(手札を右隣りに2つ移動。)");
                                  hands.unshift(hands.pop());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 1) % MAX_PLAYER];
                                      hands[(winPlayerId + 1) % MAX_PLAYER] = work;
                                  }
                                  hands.unshift(hands.pop());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 1) % MAX_PLAYER];
                                      hands[(winPlayerId + 1) % MAX_PLAYER] = work;
                                  }
                                  break;
                        case 2  : alert("出目 : 左1(手札を左隣りに1つ移動。)");
                                  hands.push(hands.shift());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 3) % MAX_PLAYER];
                                      hands[(winPlayerId + 3) % MAX_PLAYER] = work;
                                  }
                                  break;
                        case 3  : alert("出目 : 左2(手札を左隣りに2つ移動。)");
                                  hands.push(hands.shift());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 3) % MAX_PLAYER];
                                      hands[(winPlayerId + 3) % MAX_PLAYER] = work;
                                  }
                                  hands.push(hands.shift());
                                  if (winPlayerId >= 0) {
                                      work = hands[winPlayerId];
                                      hands[winPlayerId] = hands[(winPlayerId + 3) % MAX_PLAYER];
                                      hands[(winPlayerId + 3) % MAX_PLAYER] = work;
                                  }
                                  break;
                        case 4  : alert("出目 : ×(シャッフル発動失敗)");
                                  break;
                        case 5  : alert("出目 : ×(シャッフル発動失敗)");
                                  break;
                        default : alert("shuffleTime kind=" + kind);
                                  break;
                    }

                    // 手札シャッフル処理を呼び出す。
                    for (let playerId = 0; playerId < MAX_PLAYER; playerId++) {
                        if (players[playerId].strWin == "") {
                            hands[playerId].shuffle();
                        }
                    }

                    // 手札表示処理を呼び出す。
                    game.outputHandP();

                    // カード選択イベント設定処理を呼び出す。
                    game.setSelectCardEvt();
                    return;
                };

                // シャッフルタイム処理
                this.shuffleTime = function() {
                    let work = "シャッフル発動!!";
                    alert(work);

                    // サイコロを振ってシャッフル方法を決める。
                    let random = Math.floor(Math.random() * MAX_SHUFFLE);
                    //alert("出目 : " + random);

                    // シャッフルタイム実処理を呼び出す。
                    this.shuffleTimeCore(random);
                    //this.shuffleTimeCore(1);

                    // シャッフルタイム使用フラグをONにする。
                    players[cntTurn].shuffle = true;

                    // シャッフルボタン表示切替処理を呼び出す。
                    this.changesShuffleButton();
                    return;
                };

                // 対象プレイヤー情報取得処理
                this.getTargetPlayer = function() {
                    // 引くプレイヤーと引かれるプレイヤーを調べる。
                    let toPlayer = cntTurn;
                    let fromPlayer = (cntTurn + 1) % MAX_PLAYER;
                    while(1) {
                        if ((toPlayer != fromPlayer) && (players[fromPlayer].strWin == "")) {
                            break;
                        }
                        fromPlayer = (fromPlayer + 1) % MAX_PLAYER;
                    }
                    let result = [];
                    result["from"] = fromPlayer;
                    result["to"] = toPlayer;
                    return result;
                };

                // カード選択共通処理
                this.selectCardCommon = function(fromPlayer, toPlayer, intPlayer, intIdx) {
                    if (fromPlayer != intPlayer) {
                        // カードを引く先が間違っている場合
                        let work = "プレイヤー" + zeroPadding(toPlayer + 1) + "の手番です。";
                        work += "プレイヤー" + zeroPadding(fromPlayer + 1) + "のカードを引いてください。";
                        alert(work);
                    } else {
                        // カードを引く先が正しい場合

                        // 指定手札取得処理を呼び出す。
                        let card = hands[fromPlayer].getTarget(intIdx);
                        //alert("fromPlayer:" + fromPlayer + "  intIdx:" + intIdx + "  mark:" + card.mark + "  num:" + card.num);

                        // 手札セット処理を呼び出す。
                        hands[toPlayer].set(card);

                        // 手札整理処理を呼び出す。
                        hands[toPlayer].checkPairAll();

                        // 手札シャッフル処理を呼び出す。
                        hands[toPlayer].shuffle();

                        // 手札表示処理を呼び出す。
                        game.outputHandP();

                        // カード選択イベント設定処理を呼び出す。
                        game.setSelectCardEvt();

                        // ターン変更処理を呼び出す。
                        game.changeTurn();
                    }
                    return;
                };

                // カード選択イベント処理
                this.selectCardUser = function(event) {
                    // クリックしたカードが妥当かどうかによって処理を分岐する。
                    if (players[cntTurn].cpu == true) {
                        // 手番がCPUである場合
                        let work = "プレイヤー" + zeroPadding(cntTurn + 1) + "(CPU)の手番です。";
                        work += "カード選択はできません。";
                        alert(work);
                    } else {
                        // 対象プレイヤー情報取得処理を呼び出す。
                        let result = game.getTargetPlayer();

                        // イベント情報からクリックしたカードを特定する。
                        let name = event.target.getAttribute("name").split("_");
                        let intPlayer = parseInt(name[0], 10);
                        let intIdx = parseInt(name[1], 10);

                        // カード選択共通処理を呼び出す。
                        game.selectCardCommon(result["from"], result["to"], intPlayer, intIdx);
                    }
                    return;
                };

                // カード自動選択処理
                this.selectCardCpu = function() {
                    // 対象プレイヤー情報取得処理を呼び出す。
                    let result = this.getTargetPlayer();

                    // 引くカードを決める。
                    let handInfo = hands[result["from"]].getAll();
                    let random = Math.floor(Math.random() * handInfo.hand.length);
                    let intPlayer = result["from"];
                    let intIdx = random;
                    //alert("intPlayer + "、" + intIdx);
                    //let work = "プレイヤー" + zeroPadding(result["to"] + 1) + "が、";
                    //work += "プレイヤー" + zeroPadding(result["from"] + 1) + "のカードを引きます。";
                    //alert(work);

                    // カード選択共通処理を呼び出す。
                    this.selectCardCommon(result["from"], result["to"], intPlayer, intIdx);
                    return;
                };

                // カード選択イベント設定共通処理
                this.setSelectCardEvtCommon = function(playerId, handInfo) {
                    // 各カードにイベントを設定する。
                    for (let loop = 0; loop < handInfo.hand.length; loop++) {
                        let work = document.getElementsByName(String(playerId) + "_" + String(loop));
                        work[0].addEventListener("click", this.selectCardUser);
                    }
                    return;
                };

                // カード選択イベント設定処理
                this.setSelectCardEvt = function() {
                    for (let loop = 0; loop < MAX_PLAYER; loop++) {
                        // 各プレーヤーごとにカード選択イベント設定共通処理を呼び出す。
                        let handInfo = hands[loop].getAll();
                        this.setSelectCardEvtCommon(loop, handInfo);
                    }
                    return;
                };

                // シャッフルボタン表示切替処理
                this.changesShuffleButton = function() {
                    // シャッフルボタンを活性に変更する。
                    document.getElementById("shuffle").disabled = false;

                    if (cntWin >= 3) {
                        // 残りのプレイヤーが2人以下の場合

                        // シャッフルボタンを非活性に変更する。
                        document.getElementById("shuffle").disabled = true;
                    }
                    if (players[0].shuffle != false) {
                        // プレイヤー01がシャッフルタイム使用済の場合

                        // シャッフルボタンを非活性に変更する。
                        document.getElementById("shuffle").disabled = true;
                    }

                    // ジョーカー探索処理を呼び出す。
                    let info = this.searchJoker();
                    //alert("fin playerId:" + info["id"] + "  index:" + info["index"]);
                    if (info["id"] != 0) {
                        // プレイヤー01がジョーカーを保持していない場合

                        // シャッフルボタンを非活性に変更する。
                        document.getElementById("shuffle").disabled = true;
                    }
                    return;
                };

                // ゲーム開始処理
                this.start = function() {
                    // 初期化処理を呼び出す。
                    this.init();

                    // 開始ボタンを非活性に変更する。
                    document.getElementById("start").disabled = true;

                    // デッキ作成処理を呼び出す。
                    deck.make();

                    // 作成したデッキをシャッフルする。
                    deck.shuffle();

                    // 手札配布処理を呼び出す。
                    this.distribute();

                    // 各プレーヤーごとに手札全整理処理を呼び出す。
                    for (let loop = 0; loop < MAX_PLAYER; loop++) {
                        hands[loop].checkPairAll();
                    }

                    // 手札表示処理を呼び出す。
                    this.outputHandP();

                    // カード選択イベント設定処理
                    this.setSelectCardEvt();

                    // 手番表示処理を呼び出す。
                    this.outputTurn();

                    // シャッフルボタン表示切替処理を呼び出す。
                    this.changesShuffleButton();
                    return;
                };

                // ターン変更処理
                this.changeTurn = function() {
                    // ゲーム終了判定処理を呼び出す。
                    result = game.isFin();
                    if (result == true) {
                        // ゲーム終了処理を呼び出す。
                        game.fin();
                    } else {
                        // ターンを切り替える。
                        cntTurn = (cntTurn + 1) % MAX_PLAYER;
                        while(1) {
                            if (players[cntTurn].strWin == "") {
                                break;
                            }
                            cntTurn = (cntTurn + 1) % MAX_PLAYER;
                        }

                        // 手札表示処理を呼び出す。
                        this.outputHandP();

                        // カード選択イベント設定処理
                        this.setSelectCardEvt();

                        // 手番表示処理を呼び出す。
                        this.outputTurn();

                        // 手番によって処理を分岐する。
                        if (players[cntTurn].cpu == true) {
                            // 手番がCPUである場合

                            // 次へボタンを活性に変更する。
                            document.getElementById("next").disabled = false;

                            let info = this.searchJoker();
                            if (info["id"] != 0) {
                                // プレイヤー01がジョーカーを保持していない場合
                                // 自動でターンを進める。
                                game.next();
                            } else {
                                // プレイヤー01がジョーカーを保持している場合
                                // ユーザの次へボタン操作イベント待ち。
                            }
                        } else {
                            // 手番がユーザである場合

                            // 次へボタンを非活性に変更する。
                            document.getElementById("next").disabled = true;

                            // ユーザのカード選択イベント待ち。
                        }

                        // シャッフルボタン表示切替処理を呼び出す。
                        this.changesShuffleButton();
                    }
                    return;
                };

                // ターン進行処理
                this.next = function() {
                    // ジョーカー探索処理を呼び出す。
                    let info = this.searchJoker();
                    //alert("fin playerId:" + info["id"] + "  index:" + info["index"]);
                    if (info["id"] != 0) {
                        // プレイヤー01がジョーカーを保持していない場合

                        if (players[info["id"]].shuffle != true) {
                            // シャッフルタイム未使用である場合

                            if (cntWin == 2) {
                                // 残りのプレイヤーが3人の場合

                                // シャッフルタイム処理を呼び出す。
                                this.shuffleTime();
                            }
                        }
                    }

                    // カード自動選択処理を呼び出す。
                    this.selectCardCpu();
                    return;
                };

                // 結果表示処理
                this.outputResult = function() {
                    let work = "";
                    for (let loop = 0; loop < MAX_PLAYER; loop++) {
                        if (loop == 0) {
                            work += "<p id='result' style='color:red'>";
                        } else {
                            work += "<p id='result' style='color:black'>";
                        }
                        work += "プレイヤー" + String(zeroPadding(loop + 1)) + "";
                        work += "1位 " + String(spacePadding(players[loop].results["win1"])) + "";
                        work += " 2位 " + String(spacePadding(players[loop].results["win2"])) + "";
                        work += " 3位 " + String(spacePadding(players[loop].results["win3"])) + "";
                        work += " 4位 " + String(spacePadding(players[loop].results["lose"])) + "";
                        let total = 0;
                        total += parseInt(String(spacePadding(players[loop].results["win1"])));
                        total += parseInt(String(spacePadding(players[loop].results["win2"])));
                        total += parseInt(String(spacePadding(players[loop].results["win3"])));
                        let win = total;
                        total += parseInt(String(spacePadding(players[loop].results["lose"])));
                        let par = "---";
                        if (total != 0) {
                            par = win / total * 100.00;
                            par = par.toFixed(3);
                        }
                        work += " 勝率 " + par + "%" + "<br>";
                    }
                    work += "</p>";
                    document.getElementById("result").innerHTML = work;
                    return;
                };

                // 手番表示処理
                this.outputTurn = function() {
                    let work =  "<span>";
                    work += "プレイヤー" + zeroPadding(cntTurn + 1) + "の手番  ";
                    work += "</span>";
                    document.getElementById("turn").innerHTML = work;
                    return;
                };

                // ボタン表示処理
                this.outputButton = function() {
                    let work =  "<span>";
                    work += "<input type='button' id='start' value='開始' onclick='game.start()'>";
                    work += "<input type='button' id='next' value='次へ' disabled=true onclick='game.next()'>";
                    work += "<input type='button' id='shuffle' value='シャッフル' disabled=true onclick='game.shuffleTime()'>";
                    work += "</span>";
                    document.getElementById("button").innerHTML = work;
                    return;
                };
            }

            // 初期化処理を呼び出す。
            let game = new OldMaid();
            game.init();

            // ボタン表示処理を呼び出す。
            game.outputButton();

            // 結果表示処理を呼び出す。
            game.outputResult();
        </script>
    </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[js]文字列操作、配列操作

?[js]文字列操作、配列操作

≪いろいろ≫ 見つける系、etc…、2次元配列を1次元配列、配列かどうか判定、etc…
≪エンコード/デコード≫ URL形式、Unicode形式、base64形式
≪文字列切り出し≫ substr、substring、charAt、indexOf、search、mutch
≪配列操作≫ slice(n1,n2), splice(n1,n2,n3...), toString, split, join, pop

なんだかんだしょっちゅう見返してる...
詳しい解説とか説明とかないです、見返し用なので(自分が)見たらわかる程度の説明文...
なのである程度jsが分かってる人向けかも...

※前書き

・jsのコードを、色付きとか変数を図形類(例:○や△や□など)でメモることが多く、それが自分にとって一番わかりやすいので、個人的メモをQiitaに移行する意味合いが大きいこの記事ではmarkdown記法の「コード」をコード部分で使ってないことが多いです。
・これは、初めの頃、一言一句(大文字と小文字さえ)違ったらいけないカ所と、(引数,変数など)自分で命名できるヶ所の違いが、例文や構文として出てる記述方法だとよく分からなかったからです。よく分からないというのは具体的には、単に斜体、引数(の長ったらしい説明文)、barとかfooとか(自分にとって全く)馴染みのない単語が変数例として当然のように出てくること、などです...

※色付き:
 変数とか引数を色分け≒個人的にこっちの方が一目瞭然(視認性)
 「好き勝手付けていいヶ所」の意味もある
※変数が○△□類:
 「好き勝手付けていいヶ所」
 あとbarとかfooとかよく出るけど『何なん?それ…』と常々思ってるから
   ↑※慣例みたいだが由来を知らないという意味で

≪いろいろ≫

*見つける系
indexOf()、findIndex()、find()

・indexOf()
文字列配列から指定文字列がどの位置か取得. 最初の分だけ.
一致する指定文字が見つからなかい場合は -1
〇〇.indexOf('指定文字列') //結果は数値

・findIndex()
配列から指定文字列を条件を表す関数として指定してどの位置かindex番号を取得.
〇〇.findIndex(v => v==='指定文字列') //結果は数値=配列〇〇のindex番号
※つまりアロー関数みたいになる

・find()
配列から指定文字列の要素そのものを探して取得.
〇〇.find()
→指定文字を含む要素の場合は?=調べきれてない=
〇〇.find(〇〇.indexOf('指定文字列')) // ?指定文字を含む要素の場合?

  
*重複を取り除いた配列を得る
  const ○○ = [1, 2, 4, 2, 3 , 3, 2, 4, 5, 7, 5, 6, 1, 2]; //重複あり配列

例1) filterとindexOf
※この方法は元の配列○○を加工し重複のない配列○△とする
○△.filter( (val, idx ) => ○○.indexOf(val) === idx );

例2) filterとindexOf
※この方法は元の配列○○から新たに重複のない配列△△を作る
const △△=○○.filter( (val, idx, arr) => arr.indexOf(val) === idx);

例3) Set とArray.from
※この方法は元の配列○○から新たに重複のない配列△△を作る
const △△ = Array.from(new Set(○○)); //Setを生成し配列にする

例4) Setとスプレッド演算子
※この方法は元の配列○○から新たに重複のない配列△△を作る
const △△ = […new Set(○○)]; //Setを生成し配列にする

  
*指定範囲の整数が要素の配列を作る
・[…Array(n).keys()]
スプレッドなんちゃらで作る. 0からn-1まで
▼例1) 1始まり60の長さ(要素数)の配列
const ○○ = [...Array(60).keys()].map(i => ++i) ];
// [1, 2, 3, 4,....,60]

▼例2) 20始まり60の長さ(要素数)の配列 ↓20始
const ○○ = [...Array(60).keys()].map(i => ++i+19) ;
// [20, 2, 3, 4,….,80]

・forで作る. forの方が早いんだって
▼例1) 0始まり60の長さ(要素数)の配列
const ◯ = new Array(60);
for (let i = 0; i < n; i++) {◯[i] = i; }
// i=0のヶ所を i=5とかしにしたら5始まり
▼例2) 20始まり60の長さ(要素数)の配列
const ◯ = new Array(60);
for (let i = 20; i < n; i++) {◯[i] = i; }

  
*2次元配列を1次元配列にする
  var ary = [[1,2,3,4,5,6,7],[8,9,10],[11,12]]; //←共通:元の2次元配列

・… (←コレ。てんてんてん、スプレッド演算子で展開)
const 〇〇=[…ary];

・Array.prototype.concat.apply
⚠︎上記 …(スプレッド演算子)で展開か下記flat使った方がラク
  var ary2 = Array.prototype.concat.apply([],ary);
  // [1,2,3,4,5,6,7,8,9,10,11,12];;

・□.flat(n)
nは何次元までの配列をフラット化するかを指定する数字. ES2019(ES10)で追加
  const 〇〇=ary.flat(2) // [1,2,3,4,5,6,7,8,9,10,11,12];;

・□.flatMap()
配列に対して map() を行い、結果として得られた多次元配列を flat() でフラット化
  この例では半角スペースで区切ってる
  var arr = ["Blue Green", "Red Yellow"];
  const 〇〇=arr.flatMap(x => x.split(" ")) // => ["Blue", "Green", "Red", "Yellow"];

  
*配列かどうか判定
・〇〇 instanceof Array
対象〇〇が配列かどうかtrue/falseで判定. 連想配列は配列と判定されない.
  
  console.log( 〇〇 instanceof Array );

・Array.isArray(〇〇)
対象〇〇が配列かどうかtrue/falseで判定. 連想配列は配列と判定されない.
  
  console.log( Array.isArray(〇〇) );

  
*配列のようなオブジェクトから配列を作る
・Array.of(〇〇,△△,□□)
引数に与えられた値(〇〇とか△△とか□□とか)を持った配列を作成.
(newなんちゃらで配列を作成するときとほぼ同じ)

・Array.from(□)
配列のようなオブジェクト□や反復可能オブジェクト□から新たに配列を作成.
例 ↓文字列も配列として出力する
  console.log(Array.from("mojiretsu")); //配列として出力
  //["m", "o", "j", "i", "r", "e","t","s","u"]

⚠︎querySelectorAll()  の戻り値は Nodelist なので、forEach() が使える
⚠︎getElementsByClassName()  の戻り値は HTMLCollection  で forEach() 使えない

js
//例 ↓querySelectorAll() で取得したやつを<font color=#0071B0>配列</font>に変換してforEachで処理
//※あくまでコード例
//※id名には「#」、class名には「.」が必要=cssの仕様と同じ
const eles=document.querySelectorAll(.hidden);
Array.from(eles).forEach( val => {
    //処理例:class名 hiddenがあるタグは非表示
    val.style=none;
});

//例 ↓getElementsByTagName() を使って取得したやつ(配列っぽいナニか)から<font color=#0071B0>配列</font>を生成
var anchors_array = Array.from(document.getElementsByTagName('a')); 

//例 ↓getElementsByClassName() を使って取得したやつ(以下同文)
var arr_hidden = Array.from(document.getElementsByClassName(hidden)); 

 
・call() と slice() を使う 【⚠️】上記のArray.fromを使った方が簡潔
//❶配列のようなオブジェクトを取得
var anchors = document.getElementsByTagName('a');

//❷配列のようなオブジェクトを配列に変換

var anchors_array = Array.prototype.slice.call(anchors);

js
//↑上記は以下のように ↓まとめて記述可
var anchors_array = Array.prototype.slice.call( 
  document.getElementsByTagName('a')
); 

  
*数値にする(整数にする)
・parseInt(○○,10)
第一引数:数値にしたいやつ ※〇〇部分は計算式でも可
第二引数:10で10進数 ※10進数以外もあるがここでは略
文字列の数字を数値にしたいときとか、明示的に数値であるとしたいとき *今後の推奨 ES2015以降
→Numberの静的メソッドになった
→Number.parseInt(○○,10) のように使う

  
*Number関連
・toExponential()
指数形式に変換. 〜よくわからん詳細略〜

・toFixed(n)
数値を指定小数点桁数で文字列に変換. 値は四捨五入される
  console.log(Math.PI); //Math.PI は円周率
  //3.141592653589793(数値)
  console.log(Math.PI.toFixed()); //3 (文字列)
  console.log(Math.PI.toFixed(2)); //3.14(文字列)

・toPrecision(n)
指定桁数に変換
  console.log(Math.PI); //Math.PI は円周率
  //3.141592653589793(数値)
  console.log(Math.PI.toPrecision()); //3.141592653589793(文字列)
  console.log(Math.PI.toPrecision(2)); //3.1(2桁)

・toString(n)
数値から文字列(n 進数の値)への変換. nを指定しないと10進数
整数を変換する際に使われるのが一般的

  
*数値の正数、ゼロ、負数を判定
・Math.sign(○)
※○が正数(ex. 1,2,3,40, …)かゼロ(ex. 0)か負数(-2, -3,-40 …)を判定
※正数の場合は1、負数の場合は-1、ゼロの場合は0

  
*数値の桁数が足りない時に先頭に0をつける
・padStart
〇〇.padStart(4,'0'); //例) 4桁数字,足りない場合先頭に0を足す
* 文字列の長さが足りないときに指定した文字で埋めるメソッド
padStart は文字列の先頭に、padEnd は文字列の最後に文字列を付け足す
例) ↓数値の桁数が足りない時に先頭に0を付け足したい場合
console.log('6'.padStart(2, ‘0’)); // "06”

文字列の長さが指定された数値(≒この場合2桁)より短い場合
足りない分だけ第2引数に指定された文字を付け足す
※第2引数を省略すると" "(半角スペース)で補われる

  
*数値が整数かどうか判定
・Number.isInteger
Integerというのは整数
console.log(Number.isInteger(3)); /// true
console.log(Number.isInteger(2.5)); /// false
console.log(Number.isInteger(2 ** 128)); /// true

  
*大文字 or 小文字にする
〇〇.toLowerCase() //例) A→a
〇〇.toUpperCase() //例) a→A

  
*文字列の前後にある空白や改行を消す
・trim()
"___aaa bbb ccc___".trim() // "aaa bbb ccc"
※上記例は空白部分をわかりやすく「___ 」で表してるだけ

≪エンコード/デコード≫

*urlエンコード/デコード
・encodeURIComponent(〇〇)
文字列 → url形式(にエンコード). 引数〇〇は普通の文字列.
const str = encodeURIComponent('エン');
console.logstr); // %E3%82%A8%E3%83%B3

・decodeURIcomponent(△△)
url形式 → 文字列(にデコード). 引数△△)はurlエンコードされた文字列(下記みたいなの).
const uristr = encodeURIComponent('%E3%83%87%E3%82%B3');
console.log(uristr); //デコ

  
*Unicodeエンコード/デコード
・escape(〇〇)
文字列 → Unicode形式(にエンコード). 引数〇〇は普通の文字列.
const str = escape('ユニ');
console.log(str); //%u30E6%u30CB

・unescape(△△)
Unicode形式 → 文字列(にデコード). 引数△△はUnicode形式文字列(下記みたいなの).
const unistr = unescape( ‘%u30E6%u30CB’ );
console.log(unistr); //ユニ

  
*base64エンコード/デコード
*64進数化. すべてのデータをアルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表す
・btoa(〇〇)
文字列 -> base64(にエンコード)
const str = btoa(ベース);
console.log(str); //44G544O844K5

・atob(〇△)
base64 → 文字列(にデコード)
const base64str = btoa(44G544O844K5);
console.log(base64str); //ベース

≪文字列切り出し≫

・substr(n1,n2)
文字列から位置指定&文字数指定で切り出す.
引数の数字は2コ n1=切り出し位置、n2=切り出す文字数
*n1= -1 にすると末尾からの位置
*substrの切り出し位置にindexOfで出した値を使ったり

・charAt(n)
文字列から1文字だけ切り出す. 引数は切り出す位置.

・substring(n1,n2)
文字列の指定範囲の文字を切り出す. n1=開始位置、n2=終了位置

・indexOf
文字列から指定文字がどの位置か取得. 最初の分だけ.
一致する指定文字が見つからなかい場合は -1

複雑なhtmlタグを用いる場合、タグを何かの文字に置き換えて
表示するときにタグに戻すとスッキリ
http://www.pori2.net/js/number/10.htmly

・search
指定文字列の位置を取得. 正規表現も可能

・match
検索文字の指定. 結果は文字列が出る. 正規表現のみ

≪配列操作≫

・slice(n1,n2)
配列内の指定範囲n1番目からn2番目のひとつ前までの要素を新しい配列として取り出す

*配列の最後の要素をどーのこーの
〇〇.slice(-1)[0] //配列の最後の要素
  〇〇.length-1 の代わりに 〇〇.slice(-1)[0] 

*配列の最初の要素
・△=〇〇[0] //配列の最初の要素
・[△]=〇〇 //配列の最初の要素、この書き方でも(分割代入)

・splice(n1,n2,n3, …)
配列の途中に追加する場合と削除する場合がある:
▼要素を追加. 元の配列が変更される(「破壊的メソッド」)
n1に挿入する位置の左側の要素index数
第2引数n2は0でOk.(途中に追加するなら)
n3以降には挿入する要素を指定
第4引数、第5引数mと指定すれば複数の要素を挿入可.
  第1引数:先頭からn個を無視
  第2引数:第1引数の後のn個を削除
  第3引数〜:第1引数の後に追加する要素

▼要素を削除. index番号で指定
要素を削除する配列の削除開始位置n1を 0 から始まる番号で指定
何個削除するかをn2で指定

・Array.toString()
指定された配列とその要素を表す文字列を返す. 引数はなし.

○○.length
文字列に「length」を使うと全体の文字数を取得し、結果として「何文字」の文字列であることが分かる. ※数値データにlengthを使うとエラー
 関数に「length」を使うと引数の数を取得することが可能
  ↓
 引数の数で条件分岐なども出来るようになる

 
*配列からundefinedの値を持つ要素を削除
重複がある場合( undefinedが2つ以上あってそれも全部削除)
削除を行ったら変数「i」をデクリメントすれば削除漏れを防げる

js
//配列をループして値を照合して要素を削除
for(i=0; i<〇〇.length; i++){
    if(〇〇[i] == undefined){
        //spliceメソッドで要素を削除
        〇〇.splice(i--, 1);
    }
}

  
*配列から空要素を削除
1) filterを使う
var 〇〇 = ['1', '', '2', '', '3', ''];
//var △△=〇〇.filter(function(e) {return e !=='';} );
//▼アロー関数で短く書いてこう!
var △△=〇〇.filter(e=> e !=='' );

2) grepを使う (空要素,数字の0,undefined,falseも削除)
var 〇〇 = [ 〜省略〜 ];
var △△= 〇〇.grep(a, function(e) {return e; });

・split('' ,0);
対象文字列を引数で指定したで区切って取得しそれぞれを配列の要素として格納.
→例) 改行ごとに区切って配列化して処理を行ったり
2番目の引数は分割する回数. 負の値(-1)を指定した場合は制限無し、
0を指定した場合、分割された後で最後の項目が空白の場合にはそれを配列に格納しない
例)split(',',0) ※「,(カンマ)」の位置で区切って
  split('|',0) ※「|」の位置で区切って

・join('')
配列のすべての要素をつないで文字列に. 引数で指定したでつなぐ.
→例) splitで(文字列から)配列化してたものを、また文字列として出力とか
例)join() ※何も指定しなかったら「,(カンマ)」で繋ぐ
  join('') ※空文字を使ったら隙間なく繋ぐ
  join('と')join('、') ※任意の文字でつなぐ
  ↑任意の文字「と」、↑任意の文字「、(句点)」

*追加
・unshift()
配列の先頭に要素を追加.

・push()
配列の最後に要素を追加.

*先頭を削除
・shift()
配列の先頭を削除. 元の配列が変更される

*削除
・push()
配列の先頭を削除して詰める.

・pop()
配列の最後の要素を削除して詰める.
→例) 配列の最後の空要素やカンマを削除したり.
 split('\n')split(',') ※改行やカンマ区切りで区切ってデータとした場合に空要素が入る場合のその削除

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

定期的にネットワーク速度を計測してスプレッドシートにまとめる part2

できたもの

こんな感じで1時間ごとに自宅のネットワーク速度を計測してGoogleスプレッドシートにまとめてくれるようになりました。

スクリーンショット 2021-03-25 0.03.19.png

ネットワーク計測とGASにPOSTをリクエストするスクリプトの作成とGASでウェブアプリの作成が主な作業内容です。

下記を参考に作成しました。

part1でネットワーク速度の計測までできたので、
スプレッドシートにまとめられるようにウェブアプリケーションを作成します。

GASでウェブアプリケーション作成

スプレッドシートをひらいて 拡張機能 -> App Script を選択します。
開いた先で、下記のコード記述します。

var prop = PropertiesService.getScriptProperties();
var verifyToken = prop.getProperty('VERIFY_TOKEN');
var sheetId = prop.getProperty('SHEET_ID');
var sheetName = prop.getProperty('SHEET_NAME');

function doPost(e) {
  // トークンの検証
  var params = JSON.parse(e.postData.getDataAsString());
  var token = params.token;
  if (verifyToken !== token) {
    throw new Error(`invalid token. token=${token}`);
  }

  // スプレッドシート設定
  var ss = SpreadsheetApp.openById(sheetId);
  var sh = ss.getSheetByName(sheetName);

  // 最終行を取得
  var lastrow = sh.getLastRow();
  var date = params.data.date;
  if (lastrow === 1) {
    sh.insertRowAfter(lastrow);
    lastrow++;
    sh.getRange(lastrow, 1).setValue(date);
  } else {
    // yyyy-MM-dd形式で最終行の日付を取得
    var lastdate = Utilities.formatDate(sh.getRange(lastrow, 1).getValue(), 'Asia/Tokyo', 'yyyy-MM-dd');
    // 取得した日付とPOSTの日付が異なる場合
    if (lastdate !== date) {
      // 最終行の1行下に新しく用意
      sh.insertRowAfter(lastrow);
      lastrow++;
      sh.getRange(lastrow, 1).setValue(date);
    }
  }

  // 時刻
  var hour = params.data.hour;
  // ネット速度
  var speed = params.data.speed;

  // 最終行と時刻に対応した列を指定して、速度の値を設定
  sh.getRange(lastrow, hour + 2).setNumberFormat('0.00').setValue(speed);

  return ContentService.createTextOutput('OK');
}

下記のページを参考にプロパティを設定します。

VERIFY_TOKEN:任意の文字列(あとでスクリプトに設定する)
SHEET_ID:スプレッドシートのID
スプレッドシートのURLから取得できます。
下記のようなURLの場合は
https://docs.google.com/spreadsheets/d/abc1234567/edit#gid=0
abc1234567がIDになります。
SHEET_NAME:シート名(スプレッドシートの名前ではなく、シート自体の名前)

ウェブアプリのデプロイ

AppScriptのページの右上の デプロイ ボタンからデプロイします。
新しいデプロイ から ウェブアプリ を選択して実行します。
表示されたURLはコピーしておきます。

スプレッドシートの準備

スプレッドシートに1列めの時間の行だけ入力が必要です。
できたものを参考に入力しておいてください。

スクリプトの修正

スクリプトを下記のように修正しました。
ログ出力とGASへのPOSTができるようになっています。

test.sh
#!/bin/bash
########################################
# スピードテスト用スクリプト
#
#
########################################

# ログ出力先ディレクトリ
OUTPUT_DIR=~/logs/
# ログ出力先ファイル
OUTPUT_NAME=speedtest.log
# GASで公開したスクリプトのURL
URL=**コピーしたURL**
# 認証用の文字列(GASのVERIFY_TOKENと合わせる)
TOKEN=**GASで設定したVERIFY_TOKEN**
# スピードテストコマンド
command="/opt/anaconda3/bin/speedtest --simple"

########################################

echo "Start speed test!"

NEXT_WAIT_TIME=0
until RET=`$command` || [ $NEXT_WAIT_TIME -eq 3 ]; do
  # リトライ回数×60秒後にリトライ
  sleep $(( (NEXT_WAIT_TIME++) * 60 ))
done

echo $RET

# ログ出力ディレクトリが存在しなければ作成
if [ ! -d $OUTPUT_DIR ]; then
  echo "create log directory."
  mkdir $OUTPUT_DIR
fi

# ログ出力ファイルが存在しなければ作成
if [ ! -f $OUTPUT_DIR$OUTPUT_NAME ] ;then
  echo "create log file."
  touch "$OUTPUT_DIR$OUTPUT_NAME"
fi

# 下記形式でログ出力
# 2021/03/22 00:39:35 Ping: 13.054 ms Download: 88.29 Mbit/s Upload: 63.94 Mbit/s
echo `date "+%Y/%m/%d %H:%M:%S"` $RET >> "$OUTPUT_DIR$OUTPUT_NAME"

# 日付取得
DATE=`date "+%Y-%m-%d"`
# 時刻取得
HOUR=`date "+%-H"`

# ダウンロード速度
DOWNLOAD_SPEED=`echo $RET | awk '{print $5}'`
# アップロード速度
UPLOAD_SPEED=`echo $RET | awk '{print $8}'`

# スプレッドシートに出力
curl "$URL" -v -d'{
  "token":"'$TOKEN'",
  "data" :{
    "date":"'$DATE'",
    "hour":'$HOUR',
    "speed":"'$DOWNLOAD_SPEED'"
  }
}' -H "Content-Type:application/json" -X POST

echo "End speed test!"

実行結果

スクリプトを実行してみると ~/logs/speedtest.log にログが出力され、
スプレッドシートには日付とネットワーク速度が出力されていると思います。

定期実行

crontabで1時間ごとに実行されるように設定します。
まずはcronファイルを作成します。
ここで設定しているスクリプトの場所は実際にスクリプトを保存している場所に変更してください。

speedtest_cron.conf
0 * * * * 「スクリプトのあるディレクトリ」/test.sh

下記を実行して0分になる度にスクリプトが実行されるようにします。

$ crontab speedtest_cron.conf 

下記のように設定されていれば完了です。

$ crontab -l
0 * * * * 「スクリプトのあるディレクトリ」/test.sh

最後に

1時間ごとに速度計測をしてスプレッドシートに出力できるようになりました。:relieved:

参考

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

定期的にネットワーク速度を計測してスプレッドシートにまとめる part1

できたもの

こんな感じで1時間ごとに自宅のネットワーク速度を計測してGoogleスプレッドシートにまとめてくれるようになりました。

スクリーンショット 2021-03-25 0.03.19.png

ネットワーク計測とGASにPOSTをリクエストするスクリプトの作成とGASでウェブアプリの作成が主な作業内容です。

下記を参考に作成しました。

ネットワーク速度の計測

pythonのツールであるspeedtestを使用して計測します。

まずはインストールします。

$ pip install speedtest-cli

これで計測できるようになりました。
下記が例です。

$ speedtest --simple
Ping: 10.21 ms
Download: 83.54 Mbit/s
Upload: 90.17 Mbit/s

速度計測用スクリプトの作成

後々、GASにPOSTすることも考慮して、スクリプトを作成します。
下記が作成したスクリプトです。
速度の計測に失敗した場合は3回までリトライするようにしています。

test.sh
#!/bin/bash
########################################
# スピードテスト用スクリプト
#
#
########################################

# スピードテストコマンド
command="/opt/anaconda3/bin/speedtest --simple"

########################################

echo "Start speed test!"

NEXT_WAIT_TIME=0
until RET=`$command` || [ $NEXT_WAIT_TIME -eq 3 ]; do
  # リトライ回数×60秒後にリトライ
  sleep $(( (NEXT_WAIT_TIME++) * 60 ))
done

echo $RET

echo "End speed test!"

スクリプトを実行できるようになりました。

$ sh test.sh 
Start speed test!
Ping: 13.226 ms Download: 88.75 Mbit/s Upload: 5.55 Mbit/s
End speed test!

最後に

コマンドでネットワーク速度を計測できるようになったところで part2 に続きます。

参考

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

Reactのstate hookに値を変更した配列を渡してもre-renderされなかった

タイトル通りです。
JSのArrayは参照渡しということがわかっていなかったのと、ReactのuseStateの理解が浅かったことが原因で若干時間をとってしまいました。

起こったこと

(コンポーネントの設計が色々とアレなのは許してください...)

一覧表示画面を実装していました。

//色々略

const App = () => {
    //略

    const [blogs, setBlogs] = useState([])

    //略

    return (
        <div>
            {/*略*/}

            {user && <BlogList blogs={blogs} setBlogs={setBlogs} />}
        </div>
    )
}

export default App;

↑のBlogListというのが一覧です。中身はこうなっています

const BlogList = ({ blogs, setBlogs}) => {
    return (
        <ul className="blog_ul">
            {blogs.map(blog =>
                <Blog key={blog.id} blog={blog} blogList={blogs} setBlogs={setBlogs} />
            )}
        </ul>
    )
}

Appコンポーネントからblogsステートとblogsを更新するためのsetBlogsをpropsとして受け取り、さらにリスト要素のBlogコンポーネントに渡しています。こういうバケツリレーをやっていいのかやるべきでないのか正直自信ないですが、とりあえずこうなってます。
Blogコンポーネントの中身は以下の通りです。

const Blog = ({blog, blogList, setBlogs}) => {
    const [showDetail, setShowDetail] = useState(false)

    const toggleDetail = () => {
        setShowDetail(!showDetail)
    }

    const incrementLike = async blog => {
        const currentBlogList = blogList
        const targetBlogId = blog.id
        const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)

        blog.likes = blog.likes + 1
        const updatedBlog = await blogService.like(blog)

        currentBlogList[targetBlogIndex] = updatedBlog
        setBlogs(currentBlogList)
    }

    const blogBrief = () => (
        <>
            {blog.title} {blog.author}
            <button onClick={toggleDetail}>view</button>
        </>
    )

    const blogDetail = () => (
        <>
            {blog.title} <button onClick={toggleDetail}>hide</button><br />
            {blog.url}<br />
            likes {blog.likes} <button onClick={() => incrementLike(blog)}>like</button><br />
            {blog.author}<br />
        </>
    )

    return (
        <li className="blog_style">
            {showDetail ? blogDetail() : blogBrief()}
        </li>
    )
}

色々書いてありますが、今回問題が起こったのは incrementLike の関数でした。
この関数はblogのlikeの値をインクリメントする関数で、画面上だと "like" のボタンをクリックすることで発火します。
サーバーから更新されたblogオブジェクトが返ってきたらblogList内の該当する要素と置き換えて、setBlogs関数にその配列を渡します。そうすることでblogsが更新されて画面がre-renderされ、画面上のlikeの数が変わるというわけです。

likeボタンを押すと確かにlikeの値はしっかり変更されるのですが、画面上ではlikeの数はそのままでした。
つまり、re-renderされていませんでした。

一旦デバッグしてみる

とりあえずconsole.logしまくります。

const incrementLike = async blog => {
    const currentBlogList = blogList
    console.log('current blogs:',currentBlogList)
    const targetBlogId = blog.id
    console.log('target blog id:',targetBlogId)
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
    console.log('targ blog index:', targetBlogIndex)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)
    console.log('updated blog:',updatedBlog)

    currentBlogList[targetBlogIndex] = updatedBlog
    setBlogs(currentBlogList)
}

で、この要素を更新してみます
スクリーンショット 2021-03-24 23.31.57.png
現在likesの値は6なので、一度likeをクリックすれば7になるはずです。

スクリーンショット 2021-03-24 23.54.44.png

ちゃんと7になってます。
ただ、なんか元の配列内の要素も同じく更新されてしまっています。参照渡しになってるっぽい?
意図していた挙動ではなかったので一旦Arrayでググってみる。

Arrays are a special type of objects. The typeof operator in JavaScript returns "object" for arrays.

ArrayはObjectらしいです。

Objectということは参照渡しです。つまり、BlogコンポーネントのincrementLikeでcurrentBlogListを更新するということは、もとを辿ってゆくとAppコンポーネントのblogsを更新しているのと同じということです。
ただ、同じ値であったとしてもblogsを更新するsetBlogsに値を渡しているのだからre-renderされるのでは? という考えが払拭できなかったので、とりあえず公式ドキュメントを読み直してみました。

同じ値で更新を行った場合re-renderされない

現在値と同じ値で更新を行った場合、React は子のレンダーや副作用の実行を回避して処理を終了します(React は Object.is による比較アルゴリズムを使用します)。

らしいです。
つまり

const incrementLike = async blog => {
    const currentBlogList = blogList
    console.log('current blogs:',currentBlogList)
    const targetBlogId = blog.id
    console.log('target blog id:',targetBlogId)
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)
    console.log('targ blog index:', targetBlogIndex)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)
    console.log('updated blog:',updatedBlog)

    currentBlogList[targetBlogIndex] = updatedBlog
    console.log('are blogList and currentBloglist the same object?:', Object.is(blogList, currentBlogList))
    setBlogs(currentBlogList)
}


スクリーンショット 2021-03-25 0.29.00.png

こういうことなので画面はそのままだったということのようです。

修正

const incrementLike = async blog => {
    const currentBlogList = [...blogList]
    const targetBlogId = blog.id
    const targetBlogIndex = currentBlogList.findIndex(blog => blog.id === targetBlogId)

    blog.likes = blog.likes + 1
    const updatedBlog = await blogService.like(blog)

    currentBlogList[targetBlogIndex] = updatedBlog
    setBlogs(currentBlogList)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(JS)  Strorageオブジェクトでストレージデータを保存する

はじめに

Webアプリでは、クッキーを利用することで、クライアントに対して、小さなデータ(テキスト)を保存することができます。
しかし、JavaScriptでは、クッキーを操作しにいので、代わりにWebStorageを利用し、データを保存することがおすすめです。
ストレージは、データを特定するキーと値の組み合わせで保存されます。

ストレージにデータを保存する

storageData.js
  // localStorageプロパティの戻り値を変数に格納
  let storage = localStorage;
  // 保存方法①
  storage.setItem('lang1', 'JavaScript');
  // 保存方法②
  storage.lang2 = 'PHP';
  // 保存方法③
  storage['lang3'] = 'Ruby';

  // 取得方法①
  console.log(storage.getItem('lang1'));  //JavaSciript
  // 取得方法②
  console.log(storage.lang2); //PHP
  // 取得方法③ 
  console.log(storage['lang3']); //Ruby

上記のようにデータの保存や取得は、複数あります。
ストレージの内容は、ブラウザの開発者ツールの[Application]タグの[Local Storage]から確認することができます。
set/get data

データを削除する

storageData.js
  //削除方法①
  storage.removeItem('lang1');
  //削除方法②
  delete storage.lang2;
  //削除方法③
  delete storage['lang3'];

delete data
また、clearメソッドを使うことで、データを全て削除することができます。

storageData.js
 //全てのデータを削除
 storage.clear();

ストレージにオブジェクトを保存/取得する

ストレージに保存できるのは、基本的には文字列です。文字列の方法でオブジェクトを保存してしまうと、内部的に文字列化されてしまうので、のちにオブジェクトとして復元することができなくなってしまいます。
オブジェクトを復元できる形でストレージに保存したい場合は以下のように行います。

storageData.js
  // localStorageプロパティの戻り値を変数に格納
  let storage = localStorage;

  //profileオブジェクト生成
  let profile = {name: 'Tom', age: 23, height: 180};

  //オブジェクトを復元可能な文字列に変換する。
  storage.setItem('profile',JSON.stringify(profile));

  // 文字列をJSON.parseメソッドに渡す
  let data = JSON.parse(storage.getItem('profile'));

  console.log(data.age) //23

set ObjectData

JSON.stringifyメソッドを使い、保存することで、内部的に文字列にされたとしても、オブジェクトのような記法で書かれた文字列になるので、復元が可能になります。
データを取得する時は、JSON.parseメソッドに文字列を渡すことで、文字列からオブジェクトに復元します。

上記の仕組みをクラスとして準備すれば、オブジェクトの変換をあまり意識せずにオブジェクトを保存/取得することができるのではないでしょうか。

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