20200925のJavaScriptに関する記事は26件です。

クロージャ―を簡単にまとめてみた(1話)

クロージャ―
→「関数と、その関数が宣言されたレキシカル環境の組み合わせ」らしい。

わからん。。。。

★かみ砕いていく★
「関数と、その関数を産んだ(定義した)親が持っている変数を合わせたもの」

★さらにかみ砕いていく★
例を使って説明する。

「一郎function」と、自分を産んだ「太郎function」が持っている変数(age,「次郎function」)を合わせたもの

キャプチャ.PNG

クロージャ―の利点
①オブジェクトのカプセル化
 →上の例でいうと、外部からは「age変数」、「一郎function」にはアクセスできない。
②イベントハンドラ(イベントが発生したら起動)として登録しておくことで、コールバックとして使える。

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

addEventListenerをクラス名で使用する方法

はじめに

クラス名でaddEventListenerを作る際にエラーが出たので、解決策を投稿します。

エラーが出たソースコード

HTML
<body>
  <button class="btn">ボタン</button>
</body>
JavaScript
const btn = document.getElementsByClassName('btn');

btn.addEventListener('click', () => {
  alert('上手に押せました!')
})
エラー文
TypeError: btn.addEventListener is not a function

ボタンタグがidであればエラーは出ませんが、classなのでエラーが出てしまいます。

解決方法

JavaScript
const btn = document.getElementsByClassName('btn');

for (i = 0; i < btn.length; i++){
  btn[i].addEventListener('click', () => {
    alert('上手に押せました!')
  })
}

このようにfor文で囲んであげると、エラーが出ずに動作してくれます。

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

[PlayCanvas]入力イベントの取得

概要

キーボードやマウス、スマホのタッチ操作などのイベントを処理する機能。

実装例

マウス

mouse.js
var Mouse = pc.createScript('mouse');

Mouse.prototype.initialize = function() {
    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.mouseDown, this);
};

Mouse.prototype.mouseDown = function(event) {
    console.log("X: " + event.x.toString() + "Y: " + event.y.toString());
};

Mouse.prototype.update = function(dt) {
    // ホールドはupdateで取れる
    if( this.app.mouse.isPressed(pc.MOUSEBUTTON_LEFT) ){
        console.log("Pressing Left Mouse Button.")
    }
};

initializethis.app.mouseに実行したい処理とイベントのキーを渡す事で実装する。
もしくは、wasPressedupdateで呼び出して押されたかをチェックする事で実装もできる。(離した瞬間はwasReleased)

マウスボタンを押し続けている(ホールド)間に処理を実行したい場合はisPressedで状態を取れる。

Event 内容
pc.EVENT_MOUSEDOWN 押された時
pc.EVENT_MOUSEUP 離した時
pc.EVENT_MOUSEMOVE 移動した時
pc.EVENT_MOUSEWHEEL ホイールを動かした時

キーボード

keyboard.js
var Keyboard = pc.createScript('keyboard');

Keyboard.prototype.initialize = function() {
    this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
};

Keyboard.prototype.onKeyDown = function(event) {
    // event.keyにキーコードが入っている
    if( event.key === pc.KEY_A ){
        console.log( "Press A" );
    }
};

initializethis.app.keyboardに実行したい処理とイベントのキーを渡す事で実装する。
マウス同様、isPressedwasPressedがあるので、ホールドの検知やupdateでの記述が可能。

Event 内容
pc.EVENT_KEYDOWN 押された時
pc.EVENT_KEYUP 離した時

タッチ(スマホなど)

touch.js
var Touch = pc.createScript('touch');

Touch.prototype.initialize = function() {
    this.app.touch.on(pc.EVENT_TOUCHSTART, this.touchStart, this);
};

Touch.prototype.touchStart = function(event) {
    // event.touchesに画面に触れた場所の配列が入ってくる
    if( event.touches.length > 0 ){
        console.log("X: " + event.touches[0].x.toString() + "Y: " + event.touches[0].y.toString());
    }
};

initializethis.app.touchに実行したい処理とイベントのキーを渡す事で実装する。
タッチイベントはマウスとキーボードと違い、update内で検知を行う関数はない。

Event 内容
pc.EVENT_TOUCHSTART 触れた時
pc.EVENT_TOUCHEND 離した時
pc.EVENT_TOUCHMOVE スワイプされた時
pc.EVENT_TOUCHCANCEL 何かしらの理由で、タッチをキャンセルされた時
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワンタイムパスワードをサーバー側とクライアント側で作るサンプル

ワンタイムパスワードのロジックを作ってみた

2020/09/25 現在では何かと銀行がクラックされまくっているが、いずれもワンタイムパスワードのような認証が入ってない。もちろんワンタイムパスワードも「そのワンタイムの間に」盗まれればアウトなんだけど、常時通信内容を奪うのは盗む方も大変だ。

ただ、ワンタイムパスワードの仕組みが、Google Authenticator などの外部の仕組みに依存するのは、別の意味でリスク(急に仕様が変わるなど)。

なので、自前でどこまでできるかを検証。

どのようにして動く?

以下の要素を組み合わせて、ダイジェスト(sha256)を生成し、ダイジェストから数値6桁を取り出してます。

  • ユーザ毎に違う秘密鍵(クライアントとサーバーで同じ秘密鍵を持ちます)
  • タイムスタンプ(今回のサンプルでは60秒で割った整数部を使います)
  • ソルト(サーバー側で保持する長い文字列)

サンプルソース

端末側

onetime.js
async function digestMessage(message) {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const hash = crypto.subtle.digest('SHA-256', data);
  return hash;
}

function buf2hex(buffer) { // buffer is an ArrayBuffer
  return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}

async function onetimePass(secret_key){
  const ts_min = parseInt( Date.now() / 60000 ).toString();
  const buffer = await digestMessage(secret_key + '_and_sault' + ts_min);
  const hexString = '0x' + await buf2hex(buffer) ;
  const digitString = BigInt(hexString).toString(10);
  return digitString.slice(-6);
}
onetime.html
<html>
<script type="text/javascript" src="./onetime.js"></script>
<body>
<script>
(async () => {
document.write(await onetimePass('secret_key'));
})();
</script>
</body>
</html>

サーバー側

onetime.rb
require 'digest/sha1'

def onetime_pass(secret_key, next_min = false)
  # タイムスタンプを秒から分に丸める
  ts_min = Time.now.to_i / 60
  ts_min += 1 if next_min # 次の分までカバーしたい場合(チェックを受ける側)
  text = "#{secret_key}_and_sault"
  Digest::SHA256.hexdigest("#{text}#{ts_min}").to_i(16).to_s[-6, 6]
end

# ちょうど一分をまたがることを想定して、一分後のパスワードも取得できるようにしてる
puts onetime_pass('secret_key')
puts onetime_pass('secret_key', true)

実行結果

フロント側とサーバー側で同じ6桁ができてることがわかる。
(サーバー側のニ行目は、1分後のパスワード)

image.png

心残り部分

  • 久しぶりにJS書いたら何実行してもPromiseばかり帰ってくる。試行錯誤で, async, await を書きまくったけど、あんまり自身がない。
  • RFCにもちゃんとワンタイムパスワードのロジックはあるらしいが読まずに「きっとこういうことだろう」で済ませてる
  • 端末側はJSで書いちゃったけど、肝心の「秘密鍵を渡す方法」がこの記事では触れられてない。QRコードで渡すのが良さそうだけど、QRコードを読んで端末側のローカストレージに秘密鍵を保存するというプログラムの方がよほど上記のソースよりも長くなりそう。そういうことまで考えると、 Google Authenticator を使い、 Google Authenticator のサーバー側のライブラリを使っちゃうので解決か。
  • sha256でダイジェストを作ったけど、10進数にして最後の6桁を取り出してる時点で、衝突確率は増える。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.js(nuxt.js)のdataはvuexのgettersを使ってデータ管理しよう

はじめに

vueinstanceでは親子component同士でdataを通信をする場合はProps/$emitcoやcomponent同士でデータ通信をするためにはEvent Busを使うのが一般ですがこのように複雑なdata通信を簡単に通信することができる考え方がVuxです。
image.png

Vuexとは

ざっくりですが下記のイメージ図のように中央管理所からデータを必要な時呼び出しして使えるイメージです。

image.png

自分のイメージだと親と子component同士関係なく中央管理所からdataを管理し必要な時使うのがVuexでつまり中央管理所はVuexとのことです。

公式からの説明は下記です。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。
https://vuex.vuejs.org/ja/

vuex使い方

nuxtインストールは公式サイト参考 https://ja.nuxtjs.org/guide/installation
nuxtをインストールしたらvuexをインストールします。

$ npm i vuex

nuxtのプロジェクトを作成すると直下にstoreフォルダが作成されるのでstore直下にindex.jsを新しく作って下記のサンプルコードのようにvuexをimportして必要なdataの登録をします。

store/index.js
import Vuex from 'vuex'

export const appStore = () => {
  return new Vuex.Store({
    state: {//data
      myData:{
        firstName: 'thunder',
        lastName: 'fury',
      }
    },
    mutations: {
    },
    actions: {
    }
  })
};
export default appStore

vuexのstateはvueinstanceで言うと下記のdataの役割です。
storeにあるdataを使うためには$storeで呼び出しができます。

<template lang="pug">
  div
    p {{ $store.state.myData.firstName }}
    p {{ $store.state.myData.lastName }}
</template>

image.png
これでも良いですがdataのオブジェクトの下層が深い場合など繰り返しコードが多くなってしまうのでその時使うのがGettersです。

Getters実行イメージ

image.png

storeのindex.jsにstateのdataをgettersに関数として登録してreturnして使える流れです。

store/index.js.js
getters: {
   myData: state => {
      return state.myData
   },
//上下同じ
getters: {
   myData: function(state) {
      return state.myData
   },

どちらでも書き方は問題ないです。
少しでもにコード量を減らすためにはarrow functionが良いか思います。

登録が終わったら使いたいvueのcomponentmapGettersをimportをしてcomputedstore/index.js宣言した関数を配列の中に入れます。
そうすることに寄って$store.stateから呼び出したのをcomponent内でで宣言したようにで呼び出しすることができます。

<template lang="pug">
  div
    p {{ myData.firstName }} // p {{ $store.state.myData.firstName }}が省略できる
    p {{ myData.lastName }} // p {{ $store.state.myData.firstName }}が省略できる
</template>
<script>
import { mapGetters } from 'vuex' //ここにmapGettersをimport
export default {
  computed: {
    ...mapGetters(['myData']) //ここでmapGettersを使う
  }
}
</script>

image.png
結果$store.state.myData.firstNameを呼び出した時と同じくなります。
gettersで宣言した関数名を別の名前で受け取りする方法も有って配列で受けたのをオブジェクトで受け取ってのkeyとvalueで割当して使うこともできます。:sunny:

computed: {
    ...mapGetters({
       testData:'myData'
    })
  }

image.png

同じ結果です。:sunny:
このように別のvueComponentを作ってもわざわざ処理をしなくてもimportだけでdataを呼び出しして使うことができます。グロバル関数系のmutationsactionsもありますが自分はMixinでグロバル化して使っても十分使えるかと思いました。

終わり

ページを遷移した場合データの渡すしや受けるなどサイトが膨大になると複雑になるので中大サイトをもし、vueで作成する場合vuexのgettersを使って簡単に管理するのがどうですか

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

dataはvuexのgettersでデータ管理しよう

はじめに

vueinstanceでは親子component同士でdataを通信をする場合はProps/$emitcoやcomponent同士でデータ通信をするためにはEvent Busを使うのが一般ですがこのように複雑なdata通信を簡単に通信することができる考え方がVuxです。
image.png

Vuexとは

ざっくりですが下記のイメージ図のように中央管理所からデータを必要な時呼び出しして使えるイメージです。

image.png

自分のイメージだと親と子component同士関係なく中央管理所からdataを管理し必要な時使うのがVuexでつまり中央管理所はVuexとのことです。

公式からの説明は下記です。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。
https://vuex.vuejs.org/ja/

vuex使い方

nuxtインストールは公式サイト参考 https://ja.nuxtjs.org/guide/installation
nuxtをインストールしたらvuexをインストールします。

$ npm i vuex

nuxtのプロジェクトを作成すると直下にstoreフォルダが作成されるのでstore直下にindex.jsを新しく作って下記のサンプルコードのようにvuexをimportして必要なdataの登録をします。

store/index.js
import Vuex from 'vuex'

export const appStore = () => {
  return new Vuex.Store({
    state: {//data
      myData:{
        firstName: 'thunder',
        lastName: 'fury',
      }
    },
    mutations: {
    },
    actions: {
    }
  })
};
export default appStore

vuexのstateはvueinstanceで言うと下記のdataの役割です。
storeにあるdataを使うためには$storeで呼び出しができます。

<template lang="pug">
  div
    p {{ $store.state.myData.firstName }}
    p {{ $store.state.myData.lastName }}
</template>

image.png
これでも良いですがdataのオブジェクトの下層が深い場合など繰り返しコードが多くなってしまうのでその時使うのがGettersです。

Getters実行イメージ

image.png

storeのindex.jsにstateのdataをgettersに関数として登録してreturnして使える流れです。

store/index.js.js
getters: {
   myData: state => {
      return state.myData
   },
//上下同じ
getters: {
   myData: function(state) {
      return state.myData
   },

どちらでも書き方は問題ないです。
少しでもにコード量を減らすためにはarrow functionが良いか思います。

登録が終わったら使いたいvueのcomponentmapGettersをimportをしてcomputedstore/index.js宣言した関数を配列の中に入れます。
そうすることに寄って$store.stateから呼び出したのをcomponent内でで宣言したようにで呼び出しすることができます。

<template lang="pug">
  div
    p {{ myData.firstName }} // p {{ $store.state.myData.firstName }}が省略できる
    p {{ myData.lastName }} // p {{ $store.state.myData.firstName }}が省略できる
</template>
<script>
import { mapGetters } from 'vuex' //ここにmapGettersをimport
export default {
  computed: {
    ...mapGetters(['myData']) //ここでmapGettersを使う
  }
}
</script>

image.png
結果$store.state.myData.firstNameを呼び出した時と同じくなります。
gettersで宣言した関数名を別の名前で受け取りする方法も有って配列で受けたのをオブジェクトで受け取ってのkeyとvalueで割当して使うこともできます。:sunny:

computed: {
    ...mapGetters({
       testData:'myData'
    })
  }

image.png

同じ結果です。:sunny:
このように別のvueComponentを作ってもわざわざ処理をしなくてもimportだけでdataを呼び出しして使うことができます。グロバル関数系のmutationsactionsもありますが自分はMixinでグロバル化して使っても十分使えるかと思いました。

終わり

ページを遷移した場合データの渡すしや受けるなどサイトが膨大になると複雑になるので中大サイトをもし、vueで作成する場合vuexのgettersを使って簡単に管理するのがどうですか

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

Nuxr.jsのdataはvuexのstate,gettersでデータ管理しようstate、

はじめに

vueinstanceでは親子component同士でdataを通信をする場合はProps/$emitcoやcomponent同士でデータ通信をするためにはEvent Busを使うのが一般ですがこのように複雑なdata通信を簡単に通信することができる考え方がVuxです。
image.png

Vuexとは

ざっくりですが下記のイメージ図のように中央管理所からデータを必要な時呼び出しして使えるイメージです。

image.png

自分のイメージだと親と子component同士関係なく中央管理所からdataを管理し必要な時使うのがVuexでつまり中央管理所はVuexとのことです。

公式からの説明は下記です。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。
https://vuex.vuejs.org/ja/

vuex使い方

nuxtインストールは公式サイト参考 https://ja.nuxtjs.org/guide/installation
nuxtをインストールしたらvuexをインストールします。

$ npm i vuex

nuxtのプロジェクトを作成すると直下にstoreフォルダが作成されるのでstore直下にindex.jsを新しく作って下記のサンプルコードのようにvuexをimportして必要なdataの登録をします。

store/index.js
import Vuex from 'vuex'

export const appStore = () => {
  return new Vuex.Store({
    state: {//data
      myData:{
        firstName: 'thunder',
        lastName: 'fury',
      }
    },
    mutations: {
    },
    actions: {
    }
  })
};
export default appStore

vuexのstateはvueinstanceで言うと下記のdataの役割です。
storeにあるdataを使うためには$storeで呼び出しができます。

<template lang="pug">
  div
    p {{ $store.state.myData.firstName }}
    p {{ $store.state.myData.lastName }}
</template>

image.png
これでも良いですがdataのオブジェクトの下層が深い場合など繰り返しコードが多くなってしまうのでその時使うのがGettersです。

Getters実行イメージ

image.png

storeのindex.jsにstateのdataをgettersに関数として登録してreturnして使える流れです。

store/index.js.js
getters: {
   myData: state => {
      return state.myData
   },
//上下同じ
getters: {
   myData: function(state) {
      return state.myData
   },

どちらでも書き方は問題ないです。
少しでもにコード量を減らすためにはarrow functionが良いか思います。

登録が終わったら使いたいvueのcomponentmapGettersをimportをしてcomputedstore/index.js宣言した関数を配列の中に入れます。
そうすることに寄って$store.stateから呼び出したのをcomponent内でで宣言したようにで呼び出しすることができます。

<template lang="pug">
  div
    p {{ myData.firstName }} // p {{ $store.state.myData.firstName }}が省略できる
    p {{ myData.lastName }} // p {{ $store.state.myData.firstName }}が省略できる
</template>
<script>
import { mapGetters } from 'vuex' //ここにmapGettersをimport
export default {
  computed: {
    ...mapGetters(['myData']) //ここでmapGettersを使う
  }
}
</script>

image.png
結果$store.state.myData.firstNameを呼び出した時と同じくなります。
gettersで宣言した関数名を別の名前で受け取りする方法も有って配列で受けたのをオブジェクトで受け取ってのkeyとvalueで割当して使うこともできます。:sunny:

computed: {
    ...mapGetters({
       testData:'myData'
    })
  }

image.png

同じ結果です。:sunny:
このように別のvueComponentを作ってもわざわざ処理をしなくてもimportだけでdataを呼び出しして使うことができます。グロバル関数系のmutationsactionsもありますが自分はMixinでグロバル化して使っても十分使えるかと思いました。

終わり

ページを遷移した場合データの渡すしや受けるなどサイトが膨大になると複雑になるので中大サイトをもし、vueで作成する場合vuexのgettersを使って簡単に管理するのがどうですか

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

Nuxt.jsのdataはvuexのstate,gettersでデータ管理しよう

はじめに

vueinstanceでは親子component同士でdataを通信をする場合はProps/$emitcoやcomponent同士でデータ通信をするためにはEvent Busを使うのが一般ですがこのように複雑なdata通信を簡単に通信することができる考え方がVuxです。
image.png

Vuexとは

ざっくりですが下記のイメージ図のように中央管理所からデータを必要な時呼び出しして使えるイメージです。

image.png

自分のイメージだと親と子component同士関係なく中央管理所からdataを管理し必要な時使うのがVuexでつまり中央管理所はVuexとのことです。

公式からの説明は下記です。

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。 また Vue 公式の開発ツール拡張と連携し、設定なしでタイムトラベルデバッグやステートのスナップショットのエクスポートやインポートのような高度な機能を提供します。
https://vuex.vuejs.org/ja/

vuex使い方

nuxtインストールは公式サイト参考 https://ja.nuxtjs.org/guide/installation
nuxtをインストールしたらvuexをインストールします。

$ npm i vuex

nuxtのプロジェクトを作成すると直下にstoreフォルダが作成されるのでstore直下にindex.jsを新しく作って下記のサンプルコードのようにvuexをimportして必要なdataの登録をします。

store/index.js
import Vuex from 'vuex'

export const appStore = () => {
  return new Vuex.Store({
    state: {//data
      myData:{
        firstName: 'thunder',
        lastName: 'fury',
      }
    },
    mutations: {
    },
    actions: {
    }
  })
};
export default appStore

vuexのstateはvueinstanceで言うと下記のdataの役割です。
storeにあるdataを使うためには$storeで呼び出しができます。

<template lang="pug">
  div
    p {{ $store.state.myData.firstName }}
    p {{ $store.state.myData.lastName }}
</template>

image.png
これでも良いですがdataのオブジェクトの下層が深い場合など繰り返しコードが多くなってしまうのでその時使うのがGettersです。

Getters実行イメージ

image.png

storeのindex.jsにstateのdataをgettersに関数として登録してreturnして使える流れです。

store/index.js.js
getters: {
   myData: state => {
      return state.myData
   },
//上下同じ
getters: {
   myData: function(state) {
      return state.myData
   },

どちらでも書き方は問題ないです。
少しでもにコード量を減らすためにはarrow functionが良いか思います。

登録が終わったら使いたいvueのcomponentmapGettersをimportをしてcomputedstore/index.js宣言した関数を配列の中に入れます。
そうすることに寄って$store.stateから呼び出したのをcomponent内でで宣言したようにで呼び出しすることができます。

<template lang="pug">
  div
    p {{ myData.firstName }} // p {{ $store.state.myData.firstName }}が省略できる
    p {{ myData.lastName }} // p {{ $store.state.myData.firstName }}が省略できる
</template>
<script>
import { mapGetters } from 'vuex' //ここにmapGettersをimport
export default {
  computed: {
    ...mapGetters(['myData']) //ここでmapGettersを使う
  }
}
</script>

image.png
結果$store.state.myData.firstNameを呼び出した時と同じくなります。
gettersで宣言した関数名を別の名前で受け取りする方法も有って配列で受けたのをオブジェクトで受け取ってのkeyとvalueで割当して使うこともできます。:sunny:

computed: {
    ...mapGetters({
       testData:'myData'
    })
  }

image.png

同じ結果です。:sunny:
このように別のvueComponentを作ってもわざわざ処理をしなくてもimportだけでdataを呼び出しして使うことができます。グロバル関数系のmutationsactionsもありますが自分はMixinでグロバル化して使っても十分使えるかと思いました。

終わり

ページを遷移した場合データの渡すしや受けるなどサイトが膨大になると複雑になるので中大サイトをもし、vueで作成する場合vuexのgettersを使って簡単に管理するのがどうですか

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

チェックボックス全選択した時のみ適用する確認ボタン

<script type="text/javascript">
  function change() {
    var element;

    if ((document.getElementById("check1").checked)
    && (document.getElementById("check2").checked) 
    && (document.getElementById("check3").checked)) {
    element = document.getElementById("check");
    element.disabled = false;

    } else {
    element = document.getElementById("check");
    element.disabled = true;
    }
  }
</script>

チェック出来ているかの確認

<div>
<button class="input_btn_dou f_btn dark" 
        type="button" 
        value="" 
        onclick="history.go(-1); return false;"
>
戻る
  <span class="icon-arow-left01 single_left" />
</button>

<button class="input_btn_dou_red f_btn red c-terms__submit changed" type="submit" id="check" value="" disabled>
                解約する
  <span class="icon-arow-right01 single_right"></span>
</button>
</form>
div id="wrapper check">
          <p class="box" form="check">
            <input type="checkbox" name="check" id="check1" onchange="change()">
            <label class="checkbox_font" for="check1">
            本当に良いですか?1
            </label>
          </p>

          <p class="box" form="check">
            <input type="checkbox" name="check" id="check2" onchange="change()">
            <label class="checkbox_font" for="check2">
            本当に良いですか?2
            </label>
          </p>

          <p class="box" form="check">
            <input type="checkbox" name="check" id="check3" onchange="change()"> 
            <label class="checkbox_font" for="check3">
           本当に良いですか?3
            </label>
          </p>
          </div>
              <button class="input_btn_dou f_btn dark" type="button" value="" onclick="history.go(-1); return false;">
                                戻る
                                <span class="icon-arow-left01 single_left"></span>
              </button>


              <button class="input_btn_dou_red not-btn red c-terms__submit" type="submit" id="check-btn1" value="" disabled>
                解約する
                <span class="icon-arow-right01 single_right"></span>
              </button>

              <button class="input_btn_dou_red f_btn red c-terms__submit display_btn " type="submit" id="check-btn2" value="" disabled>
                解約する
                <span class="icon-arow-right01 single_right"></span>
              </button>
            </form>

            <script type="text/javascript">

              function change() {
                if ((document.getElementById("check1").checked)
                 && (document.getElementById("check2").checked) 
                 && (document.getElementById("check3").checked)) {
                  document.getElementById("check-btn2").disabled = false;
                  document.getElementById("check-btn1").style.display = "none";
                  document.getElementById("check-btn2").style.display = "inline";


                } else {
                  document.getElementById("check-btn1").disabled = true;
                  document.getElementById("check-btn1").style.display = "inline";
                  document.getElementById("check-btn2").style.display = "none";
                }
              }
            </script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

チェックボックス全選択した時のみ適用出来るボタン

<!DOCTYPE html>
<head>
</head>
<body>
<p>確認ボタン</p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check1">
    <label class="checkbox_font" for="check1">
    確認チェックボックス1
    </label>
  </p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check2">
    <label class="checkbox_font" for="check2">
    確認チェックボックス2
    </label>
  </p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check3">
    <label class="checkbox_font" for="check3">
    確認チェックボックス3
    </label>
  </p>

  <button type="submit" id="check-btn" disabled>
  承認 
  </button>


  <script type="text/javascript">
    function change() {
      if  ((document.getElementById("check1").checked)
        && (document.getElementById("check2").checked) 
        && (document.getElementById("check3").checked)) 
      {
        document.getElementById("check-btn").disabled = false;

      } else {
        document.getElementById("check-btn").disabled = true;
      }
    }
  </script>
</body>
</html>

全てのチェックボックスをチェックする事によってボタンを押すことが出来る

スクリーンショット 2020-09-25 18.00.36.png
スクリーンショット 2020-09-25 18.01.07.png
スクリーンショット 2020-09-25 18.00.58.png
スクリーンショット 2020-09-25 18.01.17.png

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

複数チェックボックスを全選択した時のみ押せるボタン

<!DOCTYPE html>
<head>
</head>
<body>
<p>確認ボタン</p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check1">
    <label for="check1">
    確認チェックボックス1
    </label>
  </p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check2">
    <label for="check2">
    確認チェックボックス2
    </label>
  </p>

  <p form="check">
    <input type="checkbox" onchange="change()" id="check3">
    <label for="check3">
    確認チェックボックス3
    </label>
  </p>

  <button type="submit" id="check-btn" disabled>
  承認 
  </button>


  <script type="text/javascript">
    function change() {
      if  ((document.getElementById("check1").checked)
        && (document.getElementById("check2").checked) 
        && (document.getElementById("check3").checked)) 
      {
        document.getElementById("check-btn").disabled = false;

      } else {
        document.getElementById("check-btn").disabled = true;
      }
    }
  </script>
</body>
</html>

全てのチェックボックスをチェックする事によってボタンを押すことが出来る

スクリーンショット 2020-09-25 18.00.36.png
スクリーンショット 2020-09-25 18.01.07.png
スクリーンショット 2020-09-25 18.00.58.png
スクリーンショット 2020-09-25 18.01.17.png

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

Vue.jsでビジュアルコンテンツエディタ作成

概要

記事コンテンツの管理ツールの需要があって、Vue.jsを用いて作成てみました。
諸事情によってサーバーサイドを利用できず、すべてフロントで完結する必要があります。
イメージとしては簡易版のワードプレス管理画面のようなものを想定して設計しました。

フロントエンド(エンドユーザー向け)
ReactやVueなどで記事コンテンツのjsonを用いて描画する
バックエンド
エディタを使って、このような記事コンテンツのjsonファイルを作成、管理、更新

第一弾としてはバックエンドのパートをご紹介します。
デモはこちら

ページ構成

トップ
既存記事コンテンツのjsonをアップロードして、リスト形式で表示、完成したデータをjson形式でダウンロード
記事作成画面
Vue-Quill-Editorを使ってビジュアルエディタを導入、各項目の入力
記事編集画面
基本的には作成と同じく、各項目の入力、既存記事コンテンツの更新、削除

必要なライブラリー

vue:ページ遷移時に、propsの受け渡しが必要ですので、CLIの導入が必要です。
vue-router:ページ遷移のために、必要となります。
axios:画像ファイルのアップロード時に必要です。
vue-quill-editor:ビジュアルエディタのプラグインです。

ファイルの構成

corefiles.png
メインのファイルを紹介します。

main.js

router.jsの導入

main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

router/index.js

各ページへのURLルールとテンプレートを設定
データの受け渡しが必要ですので、propsはtrueにします。

router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    props: true
  },
  {
    path: '/new',
    name: 'New',
    component: () => import('../views/New.vue'),
    props: true
  },
  {
    path: '/detail/:aid',
    name: 'Detail',
    component: () => import('../views/Detail.vue'),
    props: true
  }
]
const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})
export default router

App.vue

特筆すべき点もなく、基本的に各Viewsに振り分けます。

App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

Home.vue

既存記事コンテンツのjsonをアップロードして、リスト形式で表示、完成したデータをjson形式でダウンロード、ほかのページへ遷移するときに、記事コンテンツの最新情報を渡す

views/Home.vue
<template>
  <div>
    <header>
      <p><img src="logo.png" alt="" width="48"></p>
      <h1>Quill Editor</h1>
    </header>
    <main>
      <div class="homeArea">
        <div class="homeDisplay"> 
          <p>Please upload a json file.</p>
          <input class="jsonUL" type="file" @change="upload" />
          <h2>Article List</h2>
          <ul class="articleList">
            <li v-for="item in items" :key="item.aid">
              <router-link v-bind:to="{ name: 'Detail', params: { aid:item.aid, getItems: items }}">
                <div>
                  <h3>{{ item.title }}</h3>
                  <p>{{ item.description }}</p>
                </div>
                <div>
                  <img :src="item.thumbnail">
                </div>
              </router-link>
            </li>
          </ul>
        </div>
        <div class="homeDescription">
          <h3>How to use</h3>
          <ul>
            <li>Upload a json format data file (<span class="sample"><a v-on:click="sample()">Sample</a></span>) to start.</li>
            <li>Click "Create a new article" to create a new one, input the necessary information and click "Add" to finish.</li>
            <li>Click each article to edit contents, and click "save" when finished.</li>
            <li>Click "Download" to export the newest article data.</li>
            <li>Please do not use browser "back" or "refresh", this may remove all the data.</li>
          </ul>
          <h3>Element in article</h3>
          <dl>
            <dt>aid (Require/Unique)</dt>
            <dd>Article id, can not change after created, please do not use same aid for different article, we recommond "yyyymmddID" (eg. "2020101401")</dd>
            <dt>Title (Require)</dt>
            <dd>Article title, can be seen in list</dd>
            <dt>Description (Require)</dt>
            <dd>Article description, can be seen in list</dd>
            <dt>Thumbnail (Require)</dt>
            <dd>Article thumbnail image, can be seen in list</dd>
            <dt>Content (Require)</dt>
            <dd>Article contents</dd>
            <dt>Category</dt>
            <dd>Article category, planning to be used for filter</dd>
            <dt>Tag</dt>
            <dd>Article category, planning to be used for filter</dd>
          </dl>
        </div> 
      </div>
    </main>
    <footer>
      <router-link class="greenBtn button" v-bind:to="{ name: 'New', params: { getItems: items }}">Create a new article</router-link>
      <a class="blackBtn button" v-on:click="download()">Download</a>
    </footer>
  </div>
</template>
<script>
  import Vue from 'vue'
  import VueQuillEditor from 'vue-quill-editor'
  import 'quill/dist/quill.core.css'
  import 'quill/dist/quill.snow.css'
  import 'quill/dist/quill.bubble.css'
  Vue.use(VueQuillEditor)

  const today = new Date();
  const update = today.getFullYear() + "/" + (today.getMonth() + 1) + "/" + today.getDate() + " " + today.getHours() + ":" + today.getMinutes()

  export default {
    props: {
      getItems: Array,
    },
    data(){
      return {
        results: [],
        items: this.getItems,
        dlData: {
          update: update,
          data: this.getItems
        },
        sampleData: {
          update: update,
          data: [
            {
              aid: '2020010101',
              title: 'Sample Title1',
              description: 'Sample Description1',
              thumbnail: '',
              category: 'Sample Category1',
              tag: 'Sample Tag1',
              content: 'Sample Contents1'
            },
            {
              aid: '2020010203',
              title: 'Sample Title2',
              description: 'Sample Description2',
              thumbnail: '',
              category: 'Sample Category2',
              tag: 'Sample Tag2',
              content: 'Sample Contents2'
            },
          ]
        }
      }
    },
    methods: {
      upload: function(e) {
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.onload = (e) => {
          this.results = JSON.parse(e.target.result)
          this.items = this.results.data
        }
        reader.readAsText(file);
      },
      download: function() {
        const output = this.dlData
        const data = JSON.stringify(output)
        const blob = new Blob([data], {
          type: 'text/plain'
        })
        const e = document.createEvent('MouseEvents'),
          a = document.createElement('a');
        a.download = "data.json";
        a.href = window.URL.createObjectURL(blob);
        a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
        e.initEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
        a.dispatchEvent(e);
      },
      sample: function() {
        const output = this.sampleData
        const data = JSON.stringify(output)
        const blob = new Blob([data], {
          type: 'text/plain'
        })
        const e = document.createEvent('MouseEvents'),
          a = document.createElement('a');
        a.download = "data.json";
        a.href = window.URL.createObjectURL(blob);
        a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
        e.initEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
        a.dispatchEvent(e);
      }
    }
  }
</script>

Detail.vue

Vue-Quill-Editorを使ってビジュアルエディタを導入、各項目の入力、保存、トップへ戻すときにデータを渡す

views/Detail.vue
<template>
  <div>
    <header>
      <p><img src="logo.png" alt="" width="48"></p>
      <h1>Article Detail</h1>
    </header>
    <main> 
      <div class="detail">
        <div id="editorArea">
          <h2>Editor</h2>
          <div class="inner">
            <h3 class="required">Contents</h3>
            <quill-editor v-model="content" ref="quillEditor" :options="editorOption"></quill-editor>
          </div>
          <h3>Information</h3>
          <dl class="detailList">
            <dt>aid</dt>
            <dd>{{aid}}</dd>
            <dt class="required">Title</dt>
            <dd><input v-model="title" size="40"></dd>
            <dt class="required description">Description</dt>
            <dd class="description"><textarea v-model="description" rows="3" cols="40"></textarea></dd>
            <dt>Category</dt>
            <dd><input v-model="category" size="15"></dd>
            <dt>Tag</dt>
            <dd><input v-model="tag" size="15"></dd>
            <dt class="required">Thumbnail</dt>
            <dd><input type="file" @change="upload" accept="image/*" /></dd>
          </dl>
          <p><img class="thumbnail" :src="thumbnail"></p>
        </div>
        <div id="previewArea">
          <h2>Preview</h2>
          <div class="inner">
            <div id="preview" class="content ql-editor" v-html="content"></div>
          </div>
        </div>
      </div>
    </main>
    <footer>
      <a class="greenBtn button" v-on:click="save()">Save</a>
      <router-link class="redBtn button" @click.native="remove()" v-bind:to="{ name: 'Home', params: { getItems: postItems }}">Remove</router-link>
      <router-link class="blackBtn button" v-bind:to="{ name: 'Home', params: { getItems: postItems }}">Back to home</router-link>
    </footer>
  </div>
</template>
<script>
  import Vue from 'vue'
  import axios from 'axios';
  import VueQuillEditor from 'vue-quill-editor'
  import 'quill/dist/quill.core.css'
  import 'quill/dist/quill.snow.css'
  import 'quill/dist/quill.bubble.css'

  Vue.use(VueQuillEditor)

  export default {
    props: {
      aid: String,
      getItems: Array,
    },
    data() {
      return {
        item : this.getItems.filter(item => item.aid === this.aid)[0],
        index: this.getItems.findIndex(item => item.aid === this.aid),
        content: this.getItems.filter(item => item.aid === this.aid)[0].content,
        title: this.getItems.filter(item => item.aid === this.aid)[0].title,
        description: this.getItems.filter(item => item.aid === this.aid)[0].description,
        thumbnail: this.getItems.filter(item => item.aid === this.aid)[0].thumbnail,
        category: this.getItems.filter(item => item.aid === this.aid)[0].category,
        tag: this.getItems.filter(item => item.aid === this.aid)[0].tag,
        postItems: this.getItems,
        editorOption: {
          theme: 'snow'
        }
      }
    },
    methods: {
      upload(e) {
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.onload = (e) => {
          this.thumbnail = e.target.result
          this.postData()
        }
        reader.readAsDataURL(file)
      },
      postData() {
        const params = new FormData()
        params.append('image', this.image)
        axios.post('http://0.0.0.0:9999/', params).then(res => {
          this.thumbnail = res.data.url
        })
      },
      save: function() {
        this.postItems[this.index].content = this.content
        this.postItems[this.index].title = this.title
        this.postItems[this.index].description = this.description
        this.postItems[this.index].thumbnail = this.thumbnail
        this.postItems[this.index].category = this.category
        this.postItems[this.index].tag = this.tag
      },
      remove: function() {
        this.postItems.splice(this.index, 1);
      }
    }
  }
</script>

New.vue

Vue-Quill-Editorを使ってビジュアルエディタを導入、各項目の入力、保存、トップへ戻すときにデータを渡す

views/New.vue
<template>
  <div>
    <header>
      <p><img src="logo.png" alt="" width="48"></p>
      <h1>New Article</h1>
    </header>
    <main> 
      <div class="detail">
        <div id="editorArea">
          <h2>Editor</h2>
          <div class="inner">
            <h3 class="required">Contents</h3>
            <quill-editor v-model="content" ref="quillEditor" :options="editorOption"></quill-editor>
          </div>
          <h3>Information</h3>
          <dl class="detailList">
            <dt class="required">aid</dt>
            <dd><input v-model="aid" size="15"></dd>
            <dt class="required">Title</dt>
            <dd><input v-model="title" size="40"></dd>
            <dt class="required description">Description</dt>
            <dd class="description"><textarea v-model="description" rows="3" cols="40"></textarea></dd>
            <dt>Category</dt>
            <dd><input v-model="category" size="15"></dd>
            <dt>Tag</dt>
            <dd><input v-model="tag" size="15"></dd>
            <dt class="required">Thumbnail</dt>
            <dd><input type="file" @change="upload" accept="image/*" /></dd>
          </dl>
          <p><img class="thumbnail" :src="thumbnail"></p>
        </div>
        <div id="previewArea">
          <h2>Preview</h2>
          <div class="inner">
            <div id="preview" class="content ql-editor" v-html="content"></div>
          </div>
        </div>
      </div>
    </main>
    <footer>
      <router-link class="greenBtn button" @click.native="add()" v-bind:to="{ name: 'Home', params: { getItems: postItems }}">Add</router-link>
      <router-link class="blackBtn button" v-bind:to="{ name: 'Home', params: { getItems: postItems }}">Back to home</router-link>
    </footer>
  </div>
</template>
<script>
  import Vue from 'vue'
  import axios from 'axios';
  import VueQuillEditor from 'vue-quill-editor'
  import 'quill/dist/quill.core.css'
  import 'quill/dist/quill.snow.css'
  import 'quill/dist/quill.bubble.css'

  Vue.use(VueQuillEditor)

  export default {
    props: {
      getItems: Array,
    },
    data() {
      return {
        aid: '',
        content: '',
        title: '',
        description: '',
        thumbnail: '',
        category: '',
        tag: '',
        postItems: this.getItems,
        editorOption: {
          theme: 'snow'
        },
      }
    },
    methods: {
      upload(e) {
        const file = e.target.files[0]
        const reader = new FileReader()
        reader.onload = (e) => {
          this.thumbnail = e.target.result
          this.postData()
        }
        reader.readAsDataURL(file)
      },
      postData() {
        const params = new FormData()
        params.append('image', this.image)
        axios.post('http://0.0.0.0:9999/', params).then(res => {
          this.thumbnail = res.data.url
        })
      },
      add: function() {
        const newItem = {
          "aid": this.aid,
          "title": this.title,
          "description": this.description,
          "thumbnail": this.thumbnail,
          "category": this.category,
          "tag": this.tag,
          "content": this.content
        }
        this.postItems.push(newItem)
      }
    }
  }
</script>

完成版はこちらです。

引き続き、第二弾ではフロント側の描画をご紹介致します。
完成次第、またこちらにて投稿します。

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

【JS学習その⑦】JavaScriptにおけるthis

JS学習シリーズの目的

このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)

thisとは

呼び出し元のオブジェクトへの参照を保持するキーワード

main.js
const person = {
    name: 'Tom',
    hello: function() {
        console.log('Hello ' + this.name);
    }
}

person.hello(); /*Hello Tom*/

上記のコードでは、

1.JavaScriptではまず変数personを呼び出す
2.person内のhelloメソッドに参照が向く
3.helloメソッドが参照しているfunctionを実行する

↑の流れになっています。
thisはこの時、呼び出し元のオブジェクトであるpersonを参照するので、this.nameはpersonオブジェクトのnameプロパティを参照するというわけです。

参照のコピーとthis

thisの基礎が分かったところで、次のようなコードを見てみましょう

main.js
window.name = 'John';

const person = {
    name: 'Tom',
    hello: function() {
        console.log('Hello ' + this.name);
    }
}

const ref = person.hello;
ref(); /*Hello John*/

person.hello(); /*Hello Tom*/

変数refにperson.helloを代入した場合、変数person内にあるhelloメソッドへの参照をコピーします。
この状態でref()を実行すると、変数personを呼び出していないので、thisは呼び出し元のオブジェクトがpersonではなくなります。
この時、thisがどのオブジェクトを参照するのかというと、グローバルオブジェクト(windowオブジェクト)を参照します。

ここで非常に重要なことを言いますが、

オブジェクトのメソッドとして実行される場合
'this' => 呼び出し元のオブジェクト

関数として実行される場合
'this' => グローバルオブジェクト

↑のようにthisの参照先は変わります。

したがって

main.js
window.name = John;

function a() {
    console.log('Hello ' + this.name);
}

a(); /*Hello John*/

上記のようなコードでは、関数a内のthisはグローバルオブジェクトを参照するので、a()を実行した結果は、'Hello John'となります。

コールバック関数とthis

オブジェクトのメソッドをコールバック関数として実行した場合を見てみます

main.js
window.name = 'John';

const person = {
    name: 'Tom',
    hello: function() {
        console.log('Hello ' + this.name);
    }
}
person.hello(); /*Hello Tom*/

function fn(ref) {
    ref();
}

fn(person.hello); /*Hello John*/

上記のコードのように、
person.hello()を実行した場合は、今までどおりthisはpersonを呼び出し元のオブジェクトとして参照します。
しかし、
関数fnにコールバック関数としてperson.helloを渡した場合、personオブジェクト内のhelloの参照先であるfunctionを変数(引数)に代入しているのと同じことなので、
コールバック関数ref()はperson.helloを参照先とする関数として実行され、thisはグローバルオブジェクトを参照します。

bindとthis

関数として実行するけど、thisの参照先を呼び出し元のオブジェクトにしたい。という時は、'bind'というメソッドを使います。

main.js
window.name = 'John';

const person = {
    name: 'Tom',
    hello: function() {
        console.log('Hello ' + this.name);
    }
}
person.hello(); /*Hello Tom*/

const helloTom = person.hello.bind(person);

function fn(ref) {
    ref();
}

fn(helloTom); /*Hello Tom*/

上記のコードのように、
変数helloTomにperson.hello.bind(person)と書くと、'bind'メソッドによってperson.helloのthisの参照先がpersonオブジェクトに固定されます。
したがって、コールバック関数としてfn(helloTom)を実行すると、'Hello Tom'と出力されます。

このように、'bind'メソッドによるthisの参照先の固定をbindによるthisの束縛と表現します。

また、'bind'は'this'の参照先だけではなく、そのメソッドまたは関数の'引数'も固定できます。

main.js
window.name = 'John';

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

const b = a.bind(null, 'Tim');

b(); /*hello Tim*/

上記のように書いた場合、'bind'の第2引数以降に指定したキーワードで関数の引数を固定できます。(※引数の固定は、複数指定できます)
※'bind'の第1引数で'this'の参照先を固定しない場合は、'null'を指定します。

ここで、重要なメカニズムとして'bind'メソッドでは、(今回の例で説明すると)

1.person.helloの参照先であるfunctionが存在する
2.person.hello.bind(person)によって'this'の参照先をpersonに固定したfunction'が別のメモリ空間にコピーされる

この、'this'の参照先を固定した別のfunction'がメモリ空間に作成されるというところは大事なのでしっかり理解しておきましょう!

call,applyと'this'

前述した'bind'のもう一つの特徴として'bind'の使用時点で実行はしないという点があります。

ここで紹介する'call'メソッドと'apply'メソッドは、使用時点で実行します

main.js
function a(name, name1) {
    console.log('hello ' + this.name + '' + name + ' ' + name1);
}

const tim = {name: 'Tim'};

const b = a.bind(tim, 'Bob', 'John');

b(); /*hello Tim Bob John*/

a.apply(tim, ['Bob', 'John']); /*hello Tim Bob John*/
a.call(tim, 'Bob', 'John'); /*hello Tim Bob John*/

上記のコードのように、
'bind'では使用時点では実行されず、b()のように関数を実行した時点で実行されますが、
'call','apply'では、使用時点で実行されます。
'call'と'apply'の違いは、固定する引数の指定をする際に、'call'は'bind'と同じように「,」(カンマ)区切りで指定しますが、'apply'は配列で指定します。

アロー関数と'this'

今まで、'this'について解説してきました。ここでアロー関数と'this'について解説します。
アロー関数とは、「無名関数を記述しやすくした省略記法」です。
そして結論から言いますが、
アロー関数内では、'this'という値を保持しません
では、アロー関数内で'this'を使ったらどうなるか?次のコードを見てみましょう。

main.js
window.name = 'John';

const person = {
    name: 'Tom';
    hello: () => {
        console.log('Hello ' + this.name);
    }
}
person.hello(); /*Hello John*/

上記のコードでは、
person.helloメソッドではアロー関数を使用しています。そして、そのメソッド内で'this'という値を使用しています。
前述した通り、アロー関数では'this'という値を保持しません。そこで、'this'はスコープチェーンをたどってレキシカルスコープ(外部スコープ)から値を探します
今回の場合、直近の値はグローバルオブジェクト(windowオブジェクト)内のwindow.nameになります。
よってperson.hello()の値は'Hello John'となります。

まとめ

いかがでしたでしょうか。
JavaScriptにおいて'this'は初学者が躓きやすいところだと思いますが、しっかり理解しておきましょう!

おまけ

今回の解説では、
オブジェクト内のメソッドを下記のように書きましたが、

main.js
const person = {
    name: 'Tom',
    hello: function() {
        console.log('Hello ' + this.name);
    }
}

person.hello(); /*Hello Tom*/

下記のように省略した記法もあり、主にこちらの書き方で書かれます。この書き方にも慣れておきましょう。

main.js
const person = {
    name: 'Tom',
    hello() {
        console.log('Hello ' + this.name);
    }
}

person.hello(); /*Hello Tom*/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Angular+Golang(gin)でSPAを作る

目標

Angularで作成したSPA(シングルページアプリケーション)に対して、GolangのWebフレームワークであるginを使ってHTTPアクセスできるようにします。

なぜginを使うか。

  • ログがわかりやすいから。
  • 記事が見当たらなかったから

前提

  • 「Angularのチュートリアル」を通していること。

  • チュートリアル程度のAngular CLIが使えること。

  • 既に、ご自身で作成されたAngularプロジェクトがあることを想定しています。

ご自身で作成されたAngularプロジェクトがない場合:
後述する「プロジェクトをコピー」の手順を無視して進めてください。
デフォルトで生成されるSPAを表示することができます。

手順

プロジェクト作成

始めのプロジェクト構成は以下のようにしています。

go-angular
└── main.go


go-angularディレクトリに移動して、Angularプロジェクトを作成します。

UHNaKZ:go-angular $ ng new view
? Would you like to add Angular routing? Yes      // Yesを入力
? Which stylesheet format would you like to use? CSS // CSSを選択


プロジェクトの構成が以下のようになります。

go-angular
├── main.go
└── view
    ├── README.md
    ├── angular.json
    ├── e2e
    ├── karma.conf.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json

プロジェクトをコピー

注意! 上記の「プロジェクト作成」で生成したプロジェクト以外に、ご自身で作成されたAngularプロジェクトがない場合は無視してください

ここで、生成されたviewディレクトリ配下のsrcディレクトリを、既に作成済みのプロジェクトのsrcディレクトリに置き換えます。

Angularプロジェクトをビルド

viewディレクトリへ移動してビルドします。

UHNaKZ:go-angular $ cd ./view
UHNaKZ:go-angular/view $ ng build

すると、

go-angular
├── main.go
└── view
    ├── README.md
    ├── angular.json
    ├── dist       // NEW!!
    ├── e2e
    ├── karma.conf.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json

のようになったかと思います。

Goビルド

go-angular/main.goを以下のように修正します。

go-angular/main.go
package main
import(
    "github.com/gin-gonic/gin"
)
func main(){
    router := gin.Default()
    router.Static("/", "./view/dist/view")
    router.Run()
}



修正できたらビルドします。

UHNaKZ:go-angular $ go build

最後に、生成されたバイナリファイルを実行します。

UHNaKZ:go-angular $ ./go-angular

localhost:8080にアクセスすれば、Angularで作ったSPAが動いているのが分かると思います。

screencapture-localhost-8080-2020-09-25-17_29_07.png

ginを使わずに標準パッケージだけで書いたコード

main.go
package main
import (
    "net/http"
)
func main(){
    ang := http.FileServer(http.Dir("./view/dist/view"))
    http.Handle("/",ang)
    http.ListenAndServe(":8080",nil)
}

ログが出ないです。
ログが出るようにすると少し長くなる。

main.go
import (
    "net/http"
    "fmt"
    "time"
)
func main(){
    http.HandleFunc("/",angSF)
    fmt.Println(" Your Angular application is runnning.")
    http.ListenAndServe(":8080",nil)
}
func angSF(w http.ResponseWriter, r *http.Request){
    t := time.Now().Format("Mon Jan 2 15:04:05 -0700 MST 2006")
    fmt.Println(fmt.Sprintf("%s  Status:%v Method:%s URL:%s",t,http.StatusOK,r.Method,r.URL))
    http.ServeFile(w,r,"./view/dist/view")
}

ログこんな感じで作れば良いんかな。(ステータスは面倒なので200しか返さないです。)

まとめ

ginが楽。

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

Angular+ginでSPAを作る

目標

Angularで作ったSPA(シングルページアプリケーション)をginを使ってサーブします。

なぜginを使うか。

  • ログがわかりやすいから。
  • 記事が見当たらなかったから

前提

  • Angularのチュートリアルを通していること。

  • チュートリアル程度のAngular CLIが使えること。

  • 既に、ご自身で作成されたAngularのプロジェクトがあるとします。

ご自身で作成されたAngularプロジェクトがない場合:
Angularプロジェクトをコピーを無視して進めてください。
デフォルトで生成されるSPAを表示することができます。

手順

プロジェクト作成

始めのプロジェクト構成は以下のようにしています。

go-angular
└── main.go


go-angularディレクトリに移動して、Angularプロジェクトを作成します。

UHNaKZ:go-angular $ ng new view
? Would you like to add Angular routing? Yes      // Yesを入力
? Which stylesheet format would you like to use? CSS // CSSを選択


プロジェクトの構成が以下のようになります。

go-angular
├── main.go
└── view
    ├── README.md
    ├── angular.json
    ├── e2e
    ├── karma.conf.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json

Angularプロジェクトをコピー

注意! ご自身で作成されたAngularプロジェクトがない場合は無視してください

ここで、生成されたviewディレクトリ配下のsrcディレクトリを、既に作成済みのプロジェクトのsrcディレクトリに置き換えます。

Angularプロジェクトをビルド

viewディレクトリへ移動してビルドします。

UHNaKZ:go-angular $ cd ./view
UHNaKZ:go-angular/view $ ng build

すると、

go-angular
├── main.go
└── view
    ├── README.md
    ├── angular.json
    ├── dist       // NEW!!
    ├── e2e
    ├── karma.conf.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── src
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.spec.json
    └── tslint.json

のようになったかと思います。

Goビルド

go-angular/main.goを以下のように修正します。

go-angular/main.go
package main
import(
    "github.com/gin-gonic/gin"
)
func main(){
    router := gin.Default()
    router.Static("/", "./view/dist/view")
    router.Run()
}



修正できたらビルドします。

UHNaKZ:go-angular $ go build

最後に、生成されたバイナリファイルを実行します。

UHNaKZ:go-angular $ ./go-angular

localhost:8080にアクセスすれば、Angularで作ったSPAが動いているのが分かると思います。

screencapture-localhost-8080-2020-09-25-17_29_07.png

ginを使わずに標準パッケージだけで書いたコード

main.go
package main
import (
    "net/http"
)
func main(){
    ang := http.FileServer(http.Dir("./view/dist/view"))
    http.Handle("/",ang)
    http.ListenAndServe(":8080",nil)
}

ログが出ないです。
ログが出るようにすると少し長くなる。

main.go
import (
    "net/http"
    "fmt"
    "time"
)
func main(){
    http.HandleFunc("/",angSF)
    fmt.Println(" Your Angular application is runnning.")
    http.ListenAndServe(":8080",nil)
}
func angSF(w http.ResponseWriter, r *http.Request){
    t := time.Now().Format("Mon Jan 2 15:04:05 -0700 MST 2006")
    fmt.Println(fmt.Sprintf("%s  Status:%v Method:%s URL:%s",t,http.StatusOK,r.Method,r.URL))
    http.ServeFile(w,r,"./view/dist/view")
}

ログこんな感じで作れば良いんかな。(ステータスは面倒なので200しか返さないです。)

まとめ

ginが楽。

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

【廃止】昔のデザインの YouTube に戻すユーザースクリプト作った(誰得)

追記

ということで、昔のデザインは完全に廃止されました。
R.I.P.
ついでにこの記事も死にました。
R.I.P.

って、眠れないですよまったく…。
w3mから見れないじゃないですか。

とりあえずこの記事は歴史的資料として遺します。

はじめに

昔、Google Chrome で YouTube を開こうとしたら Polymer(Google 製 JS ライブラリ)のエラーで見れませんでした。
そのとき調べていると、disable_polymer=1というクエリパラメータを渡せば Polymer を用いない昔のデザインに戻せるということを知りました。

ユーザースクリプト

そして、暇だったのでユーザースクリプトを書きました。

// ==UserScript==
// @name         Classic YouTube
// @namespace    https://qiita.com/okayurisotto/items/1ca3caffd55d787c39a8
// @version      1.0
// @description  昔のデザインのYouTubeに戻します
// @author       okayurisotto
// @match        *://www.youtube.com/*
// @exclude      *://www.youtube.com/embed/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(() => {
  'use strict';

  const url = new URL(location.href);
  const key = 'disable_polymer';
  if (!url.searchParams.has(key) || url.searchParams.get(key) !== '1') {
    url.searchParams.set(key, '1');
    location.replace(url.href);
  }
})();

URLSearchParams 便利

おわりに

新しいデザインのほうが、ダークモードがあるので私は好きです。
ただ、古いデザインのほうがユーザースタイルシートでカスタマイズしやすいため、独自のダークモードを導入できますけどね。

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

簡単!JavaScript での URL 操作・検索パラメータ操作

はじめに

常識かもしれませんが、とても感動したので。

new URL()

URL インターフェイスは、URL オブジェクトを生成するための静的なメソッドを提供するオブジェクトを表します。
MDNより

これを使えば、文字列として取得した URL をパースし欲しい値だけを取得することができる他、相対パスを絶対パスに変換することなどもできます。
文字列をhrefsrcのような感じに扱うことができるようになります。
それどころか URL インターフェイスのほうが扱いやすい場合もあるのでnew URL(location.href)みたいなことをすることもしばしば。

new URLSearchParams()

こちらは、名前からだいたい想像がつくように、URL の検索パラメータの操作に特化したインターフェイス。

URLSearchParams インターフェイスは URL のクエリー文字列の操作に役立つメソッドを定義します。
MDNより

.append()とか.delete()とか.sort()とか、いろいろと楽しそうなものがあるなか、最も興奮したのが、

new URLSearchParams({ foo: 1, bar: 2 });

これをtoString()してしまえばもう?foo=1&bar=2になるっていうんだからすごい楽。

また、これにはnew URL(str).searchParamsからもアクセスできます。

おわりに

幸せ。

これらを知らなかったら、いちいちdocument.createElement('a')していたでしょう…。
知ることができて本当に良かったです。

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

JavaScript 実務必須知識

DOMについて

※わかりやすいサイト
https://note.com/skipla/n/nb65a0bb4c24c

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

【Gatsby.js】jsonファイルのデータをGraphQL経由で取得する

この記事ではjsonファイルからGraphQL経由でデータを取得し、ページ内で利用する方法を紹介します。

jsonファイルを元に、複数ページを生成する方法を知りたい場合は昨日書いた以下の記事を参照。
【Gatsby.js】単一のjsonファイルを元に複数のページを生成する - Qiita

jsonファイルを配置

$ mkdir src/data
$ touch src/data/events.json
src/data/events.json
[
  {
    "title": "Gatsby勉強会",
    "date": "2020/10/01"
  },
  {
    "title": "React勉強会",
    "date": "2020/11/01"
  },
  {
    "title": "JavaScript勉強会",
    "date": "2020/12/01"
  }
]

プラグインをインストール

$ yarn add gatsby-transformer-json gatsby-source-filesystem
gatsby-config.js
module.exports = {
  plugins: [
    `other-plugin-hoge`,
    //追記↓
    `gatsby-transformer-json`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `data`,
        path: `${__dirname}/src/data/`,
      },
    },
  ],
}

クエリを作成

gatsby developで開発サーバーを起動し、http://localhost:8000/___graphql?query にアクセスしGraphiQLを開く。
まずはroot?にallEventsJsonという項目が追加されていることを確認。

allEventJson > edges > nodeを開き、titledateにチェックを入れてクエリを発行。

データを使いたいページ内で、データを取得

ここではsrc/pages/events.jsというページでデータを利用します。

$ touch src/pages/events.js
src/pages/events.js
import React from "react"
import { graphql, useStaticQuery } from "gatsby"

export default () => {
  const data = useStaticQuery(graphql`
    query {
      allEventsJson {
        edges {
          node {
            date
            title
          }
        }
      }
    }
  `)
  const events = data.allEventsJson.edges
  return (
    <div>
      {events.map(e => (
        <div>
          <h2>{e.node.title}</h2>
          <p>{e.node.date}</p>
        </div>
      ))}
    </div>
  )
}

結果

 2020-09-25 12.10.14.png

参考

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

未経験からフロントエンドエンジニアとして自社開発企業へ転職するまでにやったこと

はじめに

こんにちは☺
このたび、働きながら独学5ヶ月でフロントエンドエンジニアとして自社開発企業へ転職することができました。
今回は、学習開始から転職活動〜内定までを振り返ってみたいと思います。
フロントエンドエンジニアの転職活動についてまとめたものをあまり見なかったので、誰かの参考になれば嬉しいです。

わたしの経歴について

地方国立大学文系学部卒業後、法律事務所勤務を経て地方公務員として働いています。
学生の頃、趣味でホームページを作成した経験はあるものの、HTML/CSS以外のプログラミング言語についてはまったく知らない状態でした。

準備(2020年4月頃)

まずはじめに以下の環境を整えました。

・PCを購入(MacBook Air)
・VScodeインストール
・iTerm2インストール
・学習用Twitterアカウント開設

また、短期目標として下記を設定しました。
・5月でインプット
・6〜7月でポートフォリオ作成
・8月〜転職活動開始、年内に転職先決定

1ヶ月目(2020年5月):Webデザイン&コーディング

前述のとおりホームページ作成した経験もあって最初はWebデザイナーに興味があり、Webデザインとコーディングの学習から始めました。
書籍やUdemyでインプットし、まずはサイト模写やバナートレースなどのアウトプットを行いました。

また、学習用に開設したTwitterアカウントで日々の積み上げやトレースしたバナーをつぶやいたり、自分と同じように勉強している方を見つけたりしてモチベーションを上げていました。

2ヶ月目(2020年6月):アプリ開発、ポートフォリオ検討

ネット上のチュートリアルを参考に簡単なアプリ開発を始めました。
この頃から、Webデザインよりアプリ開発のほうが楽しいと感じ、目標をWebデザイナーからフロントエンドエンジニアに変更し、学習内容のプランとポートフォリオの内容についてプランを練り直しました。

3ヶ月目(2020年7月):ポートフォリオ作成

未経験から転職活動を行うにあたり、学習の成果や技術レベルを客観的に証明できるポートフォリオは必須です。
(実際に選考でもポートフォリオ必須の企業はいくつかありましたし、しっかりポートフォリオを作成したことで高評価をいただくこともありました。)

フロントエンドエンジニアのポートフォリオについてはあまり情報がありませんでしたが、
①ログイン認証のない、誰でも気軽に利用できるアプリ
②ログイン認証があり、データベースへのCRUD処理ができるアプリ
③ポートフォリオサイト
の3つを作成することに決め、アプリのアイデアから考えました。

アプリのアイデアについて

「なにか困っていることを解決したい」というような自分の”想い”が大切だと思います。
わたしは、”誰もが簡単に発言できる時代だからこそ、ひと手間かけて相手に気持ちを伝えることの大切さ”や”嬉しいできごとを共有して皆がHappyになってほしい”という想いから
①のアプリについては嬉しい出来事をカードにしてTwitterでシェアできるアプリ
②のアプリは嬉しい出来事をユーザーで投稿して共有できるアプリ
を開発しました。
(①については嬉しい出来事だけでなく誕生日などのお祝いや感謝の気持ちも伝えられるカードアプリを目標としていました。現在、追加実装中です・・)

アプリの使用言語について

JavaScriptのフレームワークやライブラリであるVue.jsReactなどを使用するのがよいと思います。
わたしは
・日本語の公式ドキュメントが充実している
・学習コストが低い
などの理由からVue.jsを選びました。
バックエンドについてはBaaS(Backend as a Service)であるFirebaseを利用しました。

コードはすべてGitHubへ

ポートフォリオで作成したアプリのコードはすべてGitHubにpushしました。
選考の段階でGitHubのURLを送ってほしいと依頼されることもあったので、すべてpushしておいたほうがよいと思います。

4ヶ月目(2020年8月)

ポートフォリオが完成したため、さっそく転職活動開始。
わたしの場合は地域を限定していたので、勤務希望地とフロントエンドエンジニアで検索して、実務経験必須の求人にも応募しました。

転職活動中の学習について

転職活動中も、アプリの機能修正や書籍を読んだりしました。
また、面接で聞かれた技術的な質問でわからなかったことは必ずすぐ調べ、まとめるようにしました。

5ヶ月目(2020年9月)

転職活動開始から1ヶ月、面談や面接は5社ほど受け、第一希望の企業より内定をいただきました。

学習に使用した教材やサービスについて

基本的に書籍とUdemyを利用しインプットして、チュートリアル等でコードを書いていくようにしました。
仕事の休み時間などのスキマ時間はQiitaの記事を読んだり、そのときわからないことをググってまとめるようにしています。

Udemy

幅広い講座の中から、目的や好みに合った講座を購入できます。
セール時の購入がオススメです。

書籍

◯1冊ですべて身につくHTML&CSSとWebデザイン入門講座(Mana著)

第一歩はここから。

◯JavaScript本格入門

THE入門書。基礎が大事なので、理解できないところは時間をかけてでも。
ポートフォリオ作成と並行してもOKだと思います。

◯開眼!JavaScript

こちらも上記書籍と同様に。

◯Vue.js入門

Udemy講座終了後、ポートフォリオアプリ開発中こちらで復習しました。

◯Vue.jsのコツとツボがゼッタイにわかる本

こちらも復習用に読みました。

◯Webを支える技術

Webサービス設計の基礎について学ぶことができます。

◯リーダブルコード

独学でコードを書くことに不安があったため読みました。
読みやすいコードはどうあるべきか?さまざまなパターンが紹介されています。

◯安全なWebアプリケーションの作り方

選考中の企業のエンジニアの方からお勧めされた本。
転職活動中に読みました。

公式ドキュメント

コードを書いていてわからなければまず公式ドキュメントを確認していました。

ドットインストール

JavaScript〜ざっとスキマ時間を利用して復習で使用しました。
2分程度の動画の講座なので、ここから入るのも理解しやすいと思います。

MENTA!

自分に合ったメンターさんと契約できるサービスです。
コードレビューや転職サポート(職務経歴書の添削など)をしてもらったり、メンターさんによってはチーム開発にも参加できるので、独学の方は利用をオススメします!

Youtube

しまぶーのIT大学さんのYouTube講座は全体像がわかりやすく、オススメです!

転職活動について

利用した転職エージェント

Wantedly

「まずは話を聞きたい」から企業に連絡をとることができるので、カジュアル面談から入るところが多く気軽に話を聞きやすい。

Green

初回応募時に選考理由等フォーム入力するため敷居が高い印象だが、そのぶん初回応募で通ると面接に進みやすい。

履歴書、職務経歴書について

まず面接の前に提出を求められることが多いため、履歴書と職務経歴書も用意しておきます。
面接に向け、志望動機もより掘り下げて考えるようにしました。
できれば第三者に見てもらって意見をもらい、校正したほうがよいと思います。

面接事前準備について

企業理念や社風に共感できるか、魅力を感じるかを重要視していたため、その点を重点的に調べました(ここは人によると思います)。
企業や業界研究をする中で、自己分析が進み、志望動機がより明確になりました。

まとめ

未経験からエンジニア転職は難しいと言われていますが、わたしのような文系未経験30代でも自社開発企業に転職することができました!

転職活動にあたって、
・フロントエンドエンジニア志望でもしっかりポートフォリオを作成すること
・自己分析を行い、過去の職歴に基づく強みをアピールすること
が重要だと思いました。 
また、完全独学だったので、早めにメンターサービスを利用してもよかったなと思います。

いつ転職を目指すか、どのような学習方法が合っているかは個人によって異なりますので、自分に合ったやり方が一番です。
わたしの場合は転職先決定までの期間を短く設定していますが、長期目標として基礎をしっかり身につけた上でポートフォリオ作成してもよいと思いますし、いきなり正社員でなくともインターンをしながら学習するのもよいと思います。
ただ、やると決めたら徹底的にやること、それだけです!

少しでも参考になれば嬉しいです☺

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

three.jsで表示するJSONファイルを手書きするときの最低限の要素

はじめに

Spectaclesという、Webブラウザ上で簡単に3Dデータを表示できるthree.jsをもとにしたJavaScriptのライブラリがあります。「BIM」とか「AEC」とか書いてあるので建築業界向けのやつですね。
これを使うにあたっては基本的には上記URL内で誘導されているRevit、GrasshopperのExporterを使えば良いのですが、中身そもそもどうなってんの?ということでJSONファイルを自分で書いたときのメモです。(手書きといっても出力にはコードを書きました)
3D全然分からない者が最低限何かの物体が表示できれば…というものなのでガラスの透明感の表現などの領域には及んでいません、すみません。

JSONファイルの構成

GitHubのthree.jsのwikiでJSONのフォーマットが公開されています。(「Work in progress.」と書いてあるので途中っぽいですが)
3D初心者のためこれだけだと何が何やら分からなかったので中身の把握をやっていきます。
大まかに「geometries」「materials」「object」の三つで構成されています。

geometries

物体一つ一つの座標と形状の定義です。
「geometries」項目の中に配列の形で個々の図形が記述されます。
{"geometries":[{図形の情報},{図形の情報},{図形の情報}]}
図形の情報は以下の形で書いていきます。

"geometries": [
  {
    "uuid":"一意のID",
    "type":"Geometry",
    "data": {
      "vertices":[X0,Y0,Z0,X1,Y1,Z1,...,Xn,Yn,Zn],
      "normals": [],
      "uvs": [],
      "faces": [0,0,1,2,0,1,2,3,...,0,0,4,5],
      "scale":1.0,
      "visible":true,
      "castShadow":true,
      "receiveShadow":false,
      "doubleSided":true
    }
  },
  {図形の情報}, ・・・ ,{図形の情報}
]

重要な情報は「uuid」「vertices」「faces」の三つです。
これ以外の項目は上の通りで構いません。
・uuid
 一意のUUIDです。ここで宣言したUUIDを使って「object」内でこの図形を呼び出し、図形を表示させます。
・vertices
 [0点目のX,0点目のY,0点目のZ,1点目のX,1点目のY,1点目のZ,...,n点目のX,n点目のY,n点目のZ]
 のように3つごとの塊で認識される実数値の配列です。
 負の値も大丈夫です。
 Y座標が高さ、Z座標が奥行きとなっています。
・faces
 [0,三角形の頂点1,三角形の頂点2,三角形の頂点3,0,三角形の頂点1,三角形の頂点2,三角形の頂点3,...,0,三角形の頂点1,三角形の頂点2,三角形の頂点3]
 のように4つごとの塊で認識される三角形の頂点の配列です。(最初これが分かりませんでした)
 それぞれの三角形の頂点の番号は「vertices」で定義した頂点のインデックスになっています。
 直方体を描いた場合は実際こういう感じで三角形ごとに切れているということです。3D表現を知っている人には常識だそうです。
直方体

materials

各物体に割り当てる色や模様、質感の定義です。今回は表示だけできればいいので色についてだけ扱います。
「materials」項目の中に配列の形で個々のマテリアル情報が記述されます。
{"materials":[{マテリアル},{マテリアル},{マテリアル}]}
マテリアル情報は以下の形で書いていきます。

"materials": [
  {
    "uuid":"一意のUUID",
    "type":"MeshLambertMaterial",
    "color":9895680,
    "ambient":9895680,
    "emissive":0,
    "opacity":1.0,
    "transparent":false,
    "wireframe":false,
    "shading":1
  },
  {マテリアル},・・・,{マテリアル}
]

ここでは「uuid」「color」「ambient」の三つを変更します。他の値は上の通りで構いません。
・ uuid
 一意のUUIDです。ここで宣言したUUIDを使って「object」内で図形とこのマテリアルを関連付けます。
・color
 OLE値で表現された色数値です。OLE値はRGB値を変換したもので、変換式はこちらで提示されており、以下の形になっています。
OLE = red + (green * 256) + (blue * 256 * 256)
RGB = "red % 255" + "(green / 256) % 256" + "(blue / 256 / 256) % 256"
 上の例の「9895680」だとRGBでは「00ff96」で、明るめの緑ということになります。
・ambient
 環境光の色です。
 colorと同じ値にしましたが、別の色でもいいとは思います。個人的には同じ色にしておいた方が若干見やすいです。

object

物体一つ一つをマテリアルと関連付けて表示するための定義です。
ここは他の二つと少し違い、{"object":{色々設定, "children":[{描画したい図形}, ・・・,{描画したい図形}]}}という形になっています。

設定部分

設定部分は以下の値を設定します。ここでは「uuid」「name」「layers」を変更します。
「uuid」はこれまでの通り一意のUUIDです。今回は他から参照してませんが念のため書いてます。消しても動作はします。
「name」はただの名前なので特に気を使う必要はありません。消しても動作はします。
「layers」ではレイヤとして使いたい名前のリストを「,」でそのまま繋げて記述します。「"A","B","C"」ではなく「"A,B,C"」となることに注意です。

"object":{
  "uuid":"一意のUUID",
  "name":"sample name",
  "type":"Scene",
  "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
  "userData":{"layers":"A,B,C"},
  "children":[・・・]
}

children

ここで表示したい図形とマテリアルを関連付け、一つの物体にしていきます。

"children": [
  {
    "uuid":"一意のUUID",
    "name":" ",
    "type":"RevitElement",
    "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
    "children":[
      {
      "uuid":"描画したい図形のUUID",
      "name":" ",
      "type":"Mesh",
      "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
      "geometry":"描画したい図形のUUID",
      "material":"設定したいマテリアルのIIUD"
      }
    ],
    "userData":{
      "sample_1":"40.0",
      "sample_2":"50.0",
      "sample_3":"0.9",
      "layer":"A"
    }
  },{},・・・,{}
]

「children」の中にも「children」があったりして複雑です。ここでは自分自身の「uuid」を設定するほか、これまでに定義してきた「uuid」を使って物体の図形とマテリアルを関連付けています。
「type」で「RevitElement」と書いてるのは嘘ですね、手書きなので…。「type」を書かなくても動作はしましたが念のため残しています。
「userData」についてはSpectaclesの画面で各部品についての情報を確認するための設定です。各部品をクリック(黄色のハイライト)するとこんな感じで情報が表示されて便利です。最後の「layer」は上で設定した「layers」のうちの一つです。
userDataの表示

まとめる

これまで書いたものをまとめると以下の形になります(長いので折り畳み)。文字コードはUTF-8です。
UUIDのつけ方はRevit Exporterに倣いました。


JSON全文
{
    "geometries": [
    {
        "uuid":"abef3476-2342-46f1-8931-81d22ab55256-MaterialNode_9895680_0",
        "type": "Geometry",
        "data":{
            "vertices":[
                2220.0,0.0,20.0,2180.0,0.0,20.0,2180.0,0.0,-20.0,2220.0,0.0,-20.0,2220.0,430.0,20.0,2180.0,430.0,20.0,2180.0,430.0,-20.0,2220.0,430.0,-20.0
            ],
            "normals": [],
            "uvs": [],
            "faces": [
                0,0,1,2,0,0,2,3,0,4,5,6,0,4,6,7,0,0,3,4,0,3,4,7,0,3,2,7,0,2,7,6,0,6,1,2,0,6,5,1,0,0,1,5,0,0,4,5
            ],
            "scale":1.0,
            "visible":true,
            "castShadow":true,
            "receiveShadow":false,
            "doubleSided":true
        }
    },
    {
        "uuid":"a0c57d79-188e-4315-9233-f2105a2f5262-MaterialNode_16774400_0",
        "type": "Geometry",
        "data":{
            "vertices":[
                2220.0,0.0,630.0,2180.0,0.0,630.0,2180.0,0.0,590.0,2220.0,0.0,590.0,2220.0,430.0,630.0,2180.0,430.0,630.0,2180.0,430.0,590.0,2220.0,430.0,590.0
            ],
            "normals": [],
            "uvs": [],
            "faces": [
                0,0,1,2,0,0,2,3,0,4,5,6,0,4,6,7,0,0,3,4,0,3,4,7,0,3,2,7,0,2,7,6,0,6,1,2,0,6,5,1,0,0,1,5,0,0,4,5
            ],
            "scale":1.0,
            "visible":true,
            "castShadow":true,
            "receiveShadow":false,
            "doubleSided":true
        }
    },
    {
        "uuid":"cfa8d5e4-d255-4b4b-924a-a92807a7ed49-MaterialNode_16743680_0",
        "type": "Geometry",
        "data":{
            "vertices":[
                2200.55,428.2,0.0,2215.0,428.2,0.0,2215.0,430.0,0.0,2185.0,430.0,0.0,2185.0,428.2,0.0,2199.45,428.2,0.0,2199.45,383.0,0.0,2185.0,383.0,0.0,2185.0,381.2,0.0,2215.0,381.2,0.0,2215.0,383.0,0.0,2200.55,383.0,0.0,2200.55,428.2,610.0,2215.0,428.2,610.0,2215.0,430.0,610.0,2185.0,430.0,610.0,2185.0,428.2,610.0,2199.45,428.2,610.0,2199.45,383.0,610.0,2185.0,383.0,610.0,2185.0,381.2,610.0,2215.0,381.2,610.0,2215.0,383.0,610.0,2200.55,383.0,610.0
            ],
            "normals": [],
            "uvs": [],
            "faces": [
                0,0,11,12,0,11,12,23,0,0,1,12,0,1,12,13,0,1,2,13,0,2,13,14,0,2,3,14,0,3,14,15,0,3,4,15,0,4,15,16,0,4,5,16,0,5,16,17,0,5,6,17,0,6,17,18,0,6,7,18,0,7,18,19,0,7,8,19,0,8,19,20,0,8,9,20,0,9,20,21,0,9,10,21,0,10,21,22,0,10,11,22,0,11,22,23,0,1,2,3,0,1,3,4,0,0,5,6,0,6,11,0,0,7,8,9,0,9,10,7,0,13,14,15,0,15,16,13,0,12,17,18,0,12,18,23,0,19,20,21,0,19,21,22
            ],
            "scale":1.0,
            "visible":true,
            "castShadow":true,
            "receiveShadow":false,
            "doubleSided":true
        }
    }
],
"materials": [
    {
        "uuid":"MaterialNode_9895680_0",
        "type":"MeshLambertMaterial",
        "color":9895680,
        "ambient":9895680,
        "emissive":0,
        "opacity":1.0,
        "transparent":false,
        "wireframe":false,
        "shading":1
    },
    {
        "uuid":"MaterialNode_16774400_0",
        "type":"MeshLambertMaterial",
        "color":16774400,
        "ambient":16774400,
        "emissive":0,
        "opacity":1.0,
        "transparent":false,
        "wireframe":false,
        "shading":1
    },
    {
        "uuid":"MaterialNode_16743680_0",
        "type":"MeshLambertMaterial",
        "color":16743680,
        "ambient":16743680,
        "emissive":0,
        "opacity":1.0,
        "transparent":false,
        "wireframe":false,
        "shading":1
    }
],
"object": {
    "uuid":"4bc6463b-9f4f-4705-afeb-89d8f71dfecd",
    "name": "sample name",
    "type":"Scene",
    "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
    "userData":{"layers":"柱,梁"},
    "children": [
        {
            "uuid":"abef3476-2342-46f1-8931-81d22ab55256",
            "name":" ",
            "type":"RevitElement",
            "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
            "children":[
                {
                "uuid":"abef3476-2342-46f1-8931-81d22ab55256-MaterialNode_9895680_0",
                "name":" ",
                "type":"Mesh",
                "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
                "geometry":"abef3476-2342-46f1-8931-81d22ab55256-MaterialNode_9895680_0",
                "material":"MaterialNode_9895680_0"
                }
            ],
            "userData":{
                "H(cm)":"40.0",
                "B(cm)":"40.0",
                "layer":"柱"
            }
        },
        {
            "uuid":"a0c57d79-188e-4315-9233-f2105a2f5262",
            "name":" ",
            "type":"RevitElement",
            "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
            "children":[
                {
                "uuid":"a0c57d79-188e-4315-9233-f2105a2f5262-MaterialNode_16774400_0",
                "name":" ",
                "type":"Mesh",
                "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
                "geometry":"a0c57d79-188e-4315-9233-f2105a2f5262-MaterialNode_16774400_0",
                "material":"MaterialNode_16774400_0"
                }
            ],
            "userData":{
                "H(cm)":"40.0",
                "B(cm)":"40.0",
                "layer":"柱"
            }
        },
        {
            "uuid":"cfa8d5e4-d255-4b4b-924a-a92807a7ed49",
            "name":" ",
            "type":"RevitElement",
            "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
            "children":[
                {
                "uuid":"cfa8d5e4-d255-4b4b-924a-a92807a7ed49-MaterialNode_16743680_0",
                "name":" ",
                "type":"Mesh",
                "matrix":[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],
                "geometry":"cfa8d5e4-d255-4b4b-924a-a92807a7ed49-MaterialNode_16743680_0",
                "material":"MaterialNode_16743680_0"
                }
            ],
            "userData":{
                "H(cm)":"48.8",
                "B(cm)":"30.0",
                "t1(cm)":"1.1",
                "t2(cm)":"1.8",
                "layer":"梁"
            }
        }
    ]
}
}


上のコードで表示できたもの

単純な柱と梁(H型)です。
外観

おまけ(Revit Exporter)

2020年9月現在、Revit Exporterは最近のRevit(少なくともRevit 2019)に対応していないようです。
usingを一か所修正してif文を一行追加したら使えるようになったのでこれも記録しておきます。

SpectaclesExportContext.cs
//using Autodesk.Revit.Utility ←この行を下の行に修正
using AUtodesk.Revit.DB.Visual
Command.cs
foreach (var fi in collector){
    if(fi.Category == null) continue; //←この行を追加
    string category = fi.Category.Name;

ちなみにRevitの標準サンプルファイルrac_advanced_sample_projectにExporterを使って変換したものをSpectaclesで表示するとこんな感じです。
外観 外から見た図 中から見た図
ガラスもちゃんと透けてます。すごいですね。

最後に

これをさらに頑張って色々していけばあんな感じになっていくんだな…と思うと果てしないですね。大人しくExporter使います。

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

JavaScript で複数の配列で重複したデータのみを取得するだけ

はじめに

数学的に言うと共通部分、Intersection のことです。
AND 検索とかで使えるかもしれません。

JavaScript と TypeScript

const getIntersection = (baseArray, ...arrays) => {
  return baseArray.filter((value) => {
    return arrays.every((array) => {
      return array.includes(value);
    });
  });
};
const getIntersection = <T>(
  baseArray: Array<T>,
  ...arrays: Array<Array<T>>
) => {
  return baseArray.filter((value) => {
    return arrays.every((array) => {
      return array.includes(value);
    });
  });
};

getIntersection関数に引数として複数の配列を渡して使います。

console.log(getIntersection([0, 1, 2, 3, 4, 5], [5, 6, 7, 8, 9, 0]));
// [0, 5]

引数の 1 つ目をbaseArrayとして、それをfilterしていくという形です。
残りの配列すべてにbaseArrayの値(value)が含まれていた場合trueとなり、そうでない場合はfalseとなりbaseArrayから取り除かれます。

おわりに

配列操作大好き!

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

Iterableなオブジェクトを配列風に扱うためのライブラリを作った

Iterableなオブジェクトを配列風に扱うためのライブラリを作った

JS標準オブジェクトのArray風にIterableなオブジェクトを操作するライブラリ(Iteration-JS)を作ってnpmで公開しました。
このライブラリの持っている機能は先行で作ってる人が千人くらいはいると思います。1 2
型定義ファイルを同梱しているのでTypeScript環境でも利用可能です。
大元のソースはTypeScriptで書いてあるのでDeno環境からなら直接インポートすることもできます。

リポジトリ(GitHub,npm)

ソースコードはGitHubのリポジトリで管理しています。
ルート階層にソースが大量に置いてありますが、これはDeno環境などから直リンクで取得しやすい様にする工夫です。3

https://github.com/felis392/iteration-js/
https://www.npmjs.com/package/@felis392/iteration-js

Issueを上げてもらえれば対応するかもしれません。

機能の一例

リポジトリのREADMEを読んでもらえば全部書いてあるのでここでは一部だけ抜粋します。
中間操作でArrayは作られないので入力が100万件あってもメモリ消費は小さく抑えられます。
IterationクラスはItrableプロトコルを実装しているので必要があればArray.from()で配列を得ることもできます。

import { Iteration, rangeClosed, iterate } from '@felis392/iteration-js';

const lcm = Iteration.on(rangeClosed(1, 1000000))
.filter(i => i % 17 === 0)
.filter(i => i % 19 === 0)
.filter(i => i % 23 === 0)
.findFirst(i => i % 29 === 0);

console.log(lcm);
// 215441

const total = Iteration.on(rangeClosed(1, 10000))
.filter(i => i % 17 === 0)
.filter(i => i % 19 === 0)
.reduce((r, e) => r + e, 0);

console.log(total);
// 150195

let i = 0;
for (const n of iterate(0, n => n < 10000000000000, n => n + 1)) {
    i += n;
    if (i >= 9000) {
        console.log(`n = ${n} i = ${i}`);
        break;
    }
}
// n = 134 i = 9045

実装の解説

この記事のここから先は興味がある人向けです。ライブラリの利用がしたいだけならば読まなくても問題ありません。
ソースの全体はGitHubの方を見てもらえば分かるのでここでは一部のソースを例に解説を加えます。

ジェネレータ関数を活用する

下記の関数では1行目でいきなりreturn文を書いていますが、この関数はIterableの実体としてGeneratorを返しています。
GeneratorFunctionが欲しい訳ではないのでジェネレータ関数式はその場で実行しています。4 5
始端操作と中間操作はこの様にジェネレータ関数を活用して実装しています。

iterate.ts
export function iterate<T>(
  seed: T,
  hasNext: (v: T) => boolean,
  next: (v: T) => T
): Iterable<T> {
  return function* (seed, hasNext, next) {
    for (let v = seed; hasNext(v); v = next(v))
      yield v;
  }(seed, hasNext, next);
}

委譲を多用する

ライブラリ内で定義済みの関数は別の関数の実装に利用できる場面であれば積極的に利用し、処理を委譲しています。6
特にIterationクラスは薄いラッパーに徹するためファクトリメソッド以外はすべて単品の関数を呼び出す形になっています。
単品の関数はどれも単独で使えるものです。

利用したい場面が多そうなrange()関数は以下の様に実装しています。
先程の説明で登場したiterate()関数を呼び出す事で繰り返し処理の部分の実装を繰り返し行うのを回避しています。

range.ts
// @deno-types="./iterate.d.ts"
import { iterate } from './iterate.js';

export function range(
  start: number,
  end: number
): Iterable<number> {
  start = Math.floor(start);
  end = Math.floor(end);
  return start < end
    ? iterate(start, i => i < end, i => i + 1)
    : iterate(start, i => i > end, i => i - 1);
}

単体テストを行う

TypeScriptをテストするツールはこれと言ったものが見つけられなかったため、ESモジュールに変換してからテストしています。7
こちらの記事で紹介されているtiny-esm-test-runnerを利用してテストを実施しています。8

test-range.js
import { range } from '../range.js';
import { assert } from 'tiny-esm-test-runner';
const { is } = assert;

export function test_ToPositive() {
  const v = function*() {
    for (const i of range(8, 13))
      yield i;
  }();

  is(8, v.next().value);
  is(9, v.next().value);
  is(10, v.next().value);
  is(11, v.next().value);
  is(12, v.next().value);
  is(undefined, v.next().value);
}

export function test_ToNegative() {
  const v = function*() {
    for (const i of range(4, -3))
      yield i;
  }();

  is(4, v.next().value);
  is(3, v.next().value);
  is(2, v.next().value);
  is(1, v.next().value);
  is(0, v.next().value);
  is(-1, v.next().value);
  is(-2, v.next().value);
  is(undefined, v.next().value);
}

以上


  1. JS標準のArrayの他にJava標準ライブラリのStream APIなども参考にして実装している。お手本があるのだから似たものは山程あるはず。 

  2. npmリポジトリにパッケージを公開してみたいという動機が先にあって、どうせなら実用的なものを作ろうという事でこの題材になった。 

  3. ディレクトリ構成はLoDashやUnderScoreJSを参考にした。それらがどういう理由で今の構成になっているのかまでは調べていない。 

  4. exportしないジェネレータ関数を外で定義しておけばいいので別に関数式にこだわる必要はない。イディオムとしてマイブームだったので多用しているだけだったりする。 

  5. 見て分かる様にJavaのStream.iterate()を真似して作った関数。冒頭の例の様にfor...of文に投入するなら普通にfor文書いた方が見易いのは言うまでもない。 

  6. 完全な最適化はしていないが、テストケースを書いてあるのでリファクタリングは気軽に行える。 

  7. JSにトランスパイルしてから行うならjestが使えそうだがESモジュールと相性が悪いのか設定の書き方が悪いのか上手く行かなかった。テストツールなんぞで悩みたくないので今回は気軽さ重視にした。 

  8. テストランナーがグローバルに注入してくるオブジェクトではなく明示的にアサーション用関数をimportするので非常に分かりやすい。 

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

Blazor + canvas APIでアニメを描けるようにしてみた

はじめに

Blazorなどの学習を兼ねてアニメを描けるシンプルなお絵描きWebアプリを作りました。
Blazorを使っているとは口ばかりで、C#よりもTypeScriptを書いてることの方が多いです。

Demo(Firebase)
Source(GitHub)

blazorcanvas2.gif
某謎アニ団さんぐらいアニメを描けたら良かったのですが、私では瞬きが限界です。

現在は8fps(いわゆる三コマ打ち)のみですが、いずれはフレームレートを変更できるようにします。レートの変更だけなら楽ですが、アニメーション1秒の再生に24枚(24fps:日本のアニメの一般的なフレームレート)は普通描かないので、その空フレームに対する対応をする必要があります。

環境

Blazor WebAssembly 3.2.1 + .NET Standard 2.1
Microsoft.TypeScript.MSBuild 4.0.3
Firebase

他、Blazorのテンプレートに内包されてたBootstrapなど。

実装について

Blazorと言いながらBlazorで行っているのはUIの制御だけです。

線の描き方

Canvas上で線を描く実装方法を探していたところ、lineTo()のメソッドを使用した例が多かったのですが、ペンを素早く動かした時に変な描き方になることがあり、ペンの軌道に合わせて円を置いていく形になりました。
ただし、そのままでは素早く描こうとしたときに線が切れてしまうので、以下のように微小な変化量を使って間を埋めるようにしています。しかし、現在では直線的な補完の仕方なので、もう少し曲線的な形を考えたい。数学を復習する必要がありそう。

    private prevX :number;
    private prevY :number;
    public drawLineWithPen(x:number, y:number, isDrawing:boolean){
        let scaledX = x/this.scaleRate;
        let scaledY = y/this.scaleRate;
        if(!isDrawing) {
            this.context.beginPath();
            this.context.moveTo(scaledX,scaledY);
        } else {
            // 分割数
            let div = 200;
            let dx = (scaledX - this.prevX) / div;
            let dy = (scaledY - this.prevY) / div;
            let r = this.context.lineWidth/2;
            for(let i = 0; i<=div; i++){
                let x = this.prevX + dx*i;
                let y = this.prevY + dy*i;
                this.context.beginPath();
                this.context.moveTo(x,y);
                this.context.arc(x,y, r, 0, 2 * Math.PI, false);
                this.context.stroke();
                this.context.fill();
                this.context.closePath();
            } 
        }
        this.prevX = scaledX;
        this.prevY = scaledY;
    }

ペンの入り抜きに関しては時間の差分をパラメータとして円の大きさや透明度を変えていくような形を考えています。

オニオンスキンの表示方法

オニオンスキンとは編集しているフレームの前後にあるフレームを特定の色で表示する機能です。今回は後方のフレームをピンク、前方のフレームを水色で表示しています。

あらかじめ保持しておいたImageDataの配列に対して、コピーを作り色の置換と透明度の設定を行っています。その後、Bitmapのデータを作成したのち、さらにdrawImage()のメソッドを利用してオニオンスキン表示用のキャンバスへ書き込んでいます。ImageDataで直接canvasに書き込めれば良かったのですが、putImageData()ではcanvasのすべてを置き換えてしまうのでこのような形になっています。

private setOnionSkinsInternal(start:number, end:number, color:Color, frames: ImageData[], isPrev:boolean){
    let startNum = Math.max(start, 0);
    let endNum = Math.min(end,frames.length);
    for(let i= startNum; i< endNum; i++) {
        let imageData = new ImageData(frames[i].data.slice(),frames[i].width,frames[i].height);
        for(let j=0;j<imageData.data.length; j+=4) {
            imageData.data[j] = color.r;
            imageData.data[j+1] = color.g;
            imageData.data[j+2] = color.b;
            // 現在のフレームから遠いフレームは透過度を強くして表示を薄くする
            if(isPrev)
                imageData.data[j+3] = imageData.data[j+3] * (i+1) / (endNum+1);
            else
                imageData.data[j+3] = imageData.data[j+3] * (startNum+1) / (i+1);          
        }
        window.createImageBitmap(imageData).then(
            (img) => {
                // scale()で設定した倍率分、imageがさらに縮小されるので、倍率で割る?
                this.context.drawImage(img,0,0,this.width/this.scaleRate,this.height/this.scaleRate);
            }
        ).catch(() => {
            console.log(`${i} Error`)});
    }
}

1枚あたり921,600かそれ以上のループが回るので、サイズを上げて10枚ほど表示しようとするとだいぶ重くなります。もう少し良い方法を考えたい。オニオンスキンの枚数分canvasを追加する……という方法も考えましたが試していません。

消しゴムとレイヤーの合成

https://hai3.net/blog/html5-canvas-eraser/
こちらの方法を参考にさせていただきました。ありがとうございます。

CanvasRenderingContext2D.globalCompositeOperation = "destination-out";
消しゴムでの描画時はこれを設定してあげればOKでした。

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
MDNを読む限り、レイヤーの合成やペン先の水彩っぽさなどはこれを用いて実現できそうな感じがします。

現在、消しゴムからペンへの切り替え方が、色を切り替えるかペン先サイズを変えるかしかないので、ちゃんとラジオボタン的な切替方法を作っておきたいと思います。

undo/redo

履歴保存用のImageData[]を持っておいて、undo/redoメソッドが呼び出されたその都度PopやPushするだけです。履歴の保存に関してはTypeScript側で全て行うことも考えましたが、UI側で制御したいと思い、呼ぶタイミングはBlazor側に任せてあります。現状、実装が楽なので各フレーム毎にそれぞれ履歴を持つようになっていますが、フレームの削除を実装するタイミングで履歴の持ち方を変えそうです。

public undo(){
    if(this.currentFrame.prevHistory.length > 0) {
        let prev = this.currentFrame.prevHistory.pop() ?? new ImageData(this.width,this.height) ;
        this.currentFrame.nextHistory.push(this.getImageData());
        this.putImageData(prev);      
    }
}

public redo(){
    if(this.currentFrame.nextHistory.length > 0){
        let next = this.currentFrame.nextHistory.pop() ?? new ImageData(this.width,this.height);
        this.currentFrame.prevHistory.push(this.getImageData());
        this.putImageData(next);
    }
}

public saveHistory(){
    this.currentFrame.prevHistory.push(this.getImageData());
    // 新しく履歴が追加された時、前方方向の履歴は削除する。
    this.currentFrame.nextHistory = [];
}

canvas要素の優先順位

<canvas id="1"></canvas>
<canvas id="2"></canvas>
<canvas id="3"></canvas>

イベントを拾うのがid="1"の場合、一番下に持っていかないとイベントが拾えません。

TypeScript + dotnet CLI

dotnet add package Microsoft.TypeScript.MSBuildのコマンドとjsonファイルの設定でdotnet runした時などに.tsファイルもC#のコードと共にコンパイルされるので非常に楽でした。
Fableを使ってF#で書くのも考えたのですが、メソッド名の生成方法からメソッド名を指定しなければいけないJavaScript相互運用の仕様上厳しそうなので諦めました。

今後追加する機能

GitHubのREADMEにやたらめったらと書きましたが、描き心地の問題は大きいので、線がカクついたり、入り抜きが一辺倒である問題は早めに解決したい。あとWeb Storage APIを使えば色やペンのサイズの設定値を保存できそうなので、そのあたりも対応していきたい。

タブレットやスマートフォン対応はやるとしてもだいぶ後の話になるでしょう。

私について

かつて、Windows Formアプリケーションを使ったシステム制作に関わっていたらしいニートです。
C#とCがほんの少しわかるだけで、TypeScriptなどの経験は特にありません。

作っている意図

某お絵かきソフトは気軽にアニメーションを描いて動かせないし、アニメ制作するにしても使いにくいし、画面効果もないし、意外とこちら方面に目を向けたお絵描き(Web)アプリは無さそうだなという考えから。また、データベースの操作やログイン機能などがありませんが、機会・質などの問題は別として、どこかで誰かに見せたい時に見せられる用です。

元々はCanvas APIのお試しでアニメ機能などは考えていませんでしたが、作っているうちに機能を載せたくなってきて今に至ります。

おわりに

まだまだ実装したい機能や改善したい点など色々あるため、私のモチベーションが続く限りはやっていきたいですね。
TypeScriptはMicrosoftが開発に関わっているのもあり、C#に似ている部分があるのとdotnet CLIとの組み合わせもあって、コンパイル時にTypeScript側のエラーがある程度わかるのでだいぶ楽でした。Blazorも自身だけでブラウザのAPIに触れればいいのですが、IJSRuntimeを介してしかできないのでBlazor(C#)のみというのは厳しそうです。

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

Vuex + TypeScript + vuex-module-decoratorsでのmutationとgetterの単体テスト

やりたいこと

vuex-module-decoratorsを使うことで、TyepScriptでVuexを記述する際により型付けの恩恵を受けられるようになります。この記事ではvuex-module-decoratorsを使った際のVuexのmutationとgetterのJestでの単体テストの記述方法を紹介したいと思います。

以下の環境で動作検証しています。

  • Vue.js: 2.6.19
  • Vuex: 3.0.1
  • vuex-module-decorators: 0.9.9
  • 単体テストユニット: Jest

テスト対象のVuex

テスト対象として以下のVuexを作成します。

counter.ts
import { Action, Module, Mutation, VuexModule } from "vuex-module-decorators";

@Module({ name: "counter", namespaced: true })
export class Counter extends VuexModule {

  // state
  private count: number = 0;

  // mutaion
  // カウンターをインクリメントする
  @Mutation
  public increment(): void {
    this.count++;
  }

  // getter
  // カウンターの値を取得
  get getCount() {
    return this.count;
  }

  // action
  // カウンターの値を2インクリメントするアクション
  @Action({})
  public add2(): void {
    this.increment();
    this.increment();
  }
}

Jestでの単体テスト

以下のように単体テストを記述できます。ポイントとしては、Counter.mutations!.increment(mockState, {})のようにモック化したstateに対してmutation, getterを実行することでテストケースごとに別のステートを使用している点です。

テストケースごとにステートの値を変更できる点と、mutaionとgetterを独立してテストできる点がメリットです。

counter.spec.ts
import Vuex from "vuex";
import { Counter } from "@/store/modules/counter";
import { createLocalVue } from "@vue/test-utils";
import { cloneDeep } from "lodash";
import { Action, getModule } from "vuex-module-decorators";

// ローカルのVueインスタンを使用する
const localVue = createLocalVue();
localVue.use(Vuex);

describe("Counter test", () => {

  // mutationnのテスト
  it("mutation test increment1", () => {

    // ステートをモック化
    const mockState = {
      count: 0
    };

    Counter.mutations!.increment(mockState, {});
    expect(mockState.count).toBe(1);
  });

  // mutationnのテスト
  it("mutation test increment2", () => {

    // ステートをモック化
    const mockState = {
      count: 0
    };

    // テストケースごとに別のモック化したステートを使用できる
    expect(mockState.count).toBe(0);
    Counter.mutations!.increment(mockState, {});
    Counter.mutations!.increment(mockState, {});
    expect(mockState.count).toBe(2);
  });

  // getterのテスト
  it("getter test", () => {

    // ステートをモック化、カウンターの初期値を変更
    const mockState = {
      count: 3
    };

    const actions = Counter.actions!;
    expect(Counter.getters!.getCount(mockState, null, null, null)).toBe(3);
  });
});

おわりに

actionも同様にモック化したstatenを使用してテストしたかったのですが、記述方法がわかりませんでした。ご存知の方がいらっしましたら教えていただけると嬉しいです。

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

JavaScriptで指定した期間の、月単位、日単位の連番を作成する

やりたいこと

datepicker等で選択した日付の範囲を、月単位、日単位で区切り配列にぶち込む。

'2020-08-17' から '2020-09-03' で期間指定したとき、
日単位の場合は素直に
['2020-08-17','2020-08-18','2020-08-19',...,'2020-09-03']
月単位の場合は、'yyyy-mm' 部分をみて
['2020-08','2020-09']

が欲しい。

本題

今回は、期間は決め打ちで。
配列を用意して

var fromDate = '2020-08-17'
var toDate = '2020-09-03'

var monthlyRange = [];
var daylyRange = [];

for文で日、月を加算していくので、date型に変換

new Date('yyyy-mm-dd')
の形でOK。
参考:http://cly7796.net/wp/javascript/how-to-use-new-date/

new Date('yyyy-mm')
だとddの部分は '01' になる。月の加算、配列格納はこちらを利用。

// 指定した日付から年と月を取り出す。'yyyy-mm-dd' => 'yyyy-mm'
regexp = new RegExp(/\d\d\d\d-\d\d/);
var fromYM = fromDate.match(regexp)[0];
var toYM = toDate.match(regexp)[0];

// 日付の大小比較のため、date型に変換
var start = new Date(fromDate);
var end = new Date(toDate);
// ddを省略することで、その年月の一日のdateが手に入る
var startYM = new Date(fromYM); 
var endYM = new Date(toYM);

後は以下。
型変換は下で定義した関数で行う。

// date型を文字列に変換し配列に格納
for(var ym = startYM; ym <= endYM; ym.setMonth(ym.getMonth()+1)) {
    var strYearMonth = convertToStr(ym, 0);
    monthlyRange.push(strYearMonth);
}

for(var d = start; d <= end; d.setDate(d.getDate()+1)) {
    var strDate = convertToStr(d, 1);
    daylyRange.push(strDate);
}

// date型を文字列に変換する
function convertToStr(date, day){
    // dayはフラグ。真のとき'yy-mm-dd'、偽のとき'yy-mm'。
    var strDate;

    if(day){
        strDate = [
            date.getFullYear(),
            ('0' + (date.getMonth() + 1)).slice(-2),
            ('0' + date.getDate()).slice(-2)
        ].join('-');
    }else{
        strDate = [
            date.getFullYear(),
            ('0' + (date.getMonth() + 1)).slice(-2),
        ].join('-');
    }
    return strDate;
}

結果

["2020-08", "2020-09"]
["2020-08-17", "2020-08-18", "2020-08-19", "2020-08-20", "2020-08-21", "2020-08-22", "2020-08-23", "2020-08-24", "2020-08-25", "2020-08-26", "2020-08-27", "2020-08-28", "2020-08-29", "2020-08-30", "2020-08-31", "2020-09-01", "2020-09-02", "2020-09-03"]

最適解ではないと思いますが、とりあえず思い通りです。

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