20201204のJavaScriptに関する記事は28件です。

RxJSをSubscribeなしで使う / AsyncPipe

前置き

AsyncPipe使った方が良いよ!っていうことを最近教えてもらったので、色々調べてまとめてみました。
AngularとかAsyncPipeとかRxJSについて一応知ってはいるけど、まだあまり使ったことないっていう方向けなので、
RxJSって何?Observableって何?って方は、先に RxJS公式ドキュメントなどをご参照ください?

なんでSubscribe使わないの?

"NO SUBSCRIBING MEANS... NO UNSUBSCRIBING!" (参考動画)

(SUBSCRIBEしないってことは... UNSUBSCRIBEしないでいいってことじゃん!)
AsyncPipeを使うと自分でUnsubscribeする必要がなくなるので便利+Unsubscribeし忘れもなくなります??

もうちょっと説明します

まずはsubscribeを使って書いてみます。

subscribe.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { UserService } from '../user.service';

@Component({
  selector: 'app-subscribe',
  template: `
    ng-container(*ngFor="let user of users")
      h1 {{ user.name }}
      h2 {{ user.email_address }} 
  `,
})
export class SubscribeComponent implements OnInit {
  users: User[];
  private subscription: Subscription;

  constructor(private userService: UserService) {}

  // AngularのComponentインスタンスが作成されるタイミングと、
  // 実際にComponentがビューに配置されるタイミングは一致しないため、
  // subscribeはconstructor内ではなく、ngOnInit以降で呼び出した方が良いそうです?
  ngOnInit() {
    this.subscription = this.userService.users$.subscribe((userData) => this.userData = userData);

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

次に、AsyncPipeありバージョン

async-pype.component.ts
import { Component } from '@angular/core';
import { UserService } from '../user.service';

@Component({
  selector: 'app-async-pype',
  template: `
    ng-container(*ngIf="users$ | async as users") 
      div(*ngFor="let user of users")
        h1 {{ user.name }}
        h2 {{ user.email_address }}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SubscribeComponent {
  users$: Observable<User[]> = this.userStoreService.users;

  constructor(private userService: UserService) {}
}

すっきりしましたね!
AsyncPipeを使った場合、ngOnInitライフサイクルフックは必要なく、必要なObservableデータをプロパティに置くだけでOKです!(データの変更検知、反映も同じようにしてくれます!)

Subscribeをすると、Unsubscribeをしないとメモリリークが起きる可能性があるみたいなんですが、AsyncPipeを使うとコンポーネントが破棄されるタイミングで自動的にUnsubscribeしてくれるので、手動でUnsubscribeする必要がなくなります??

また、ChangeDetectionStrategy.OnPushというものが使えるようになり、下記項目発生時のみ変更を検知するようになるため、Change Detectionサイクルを最小限に抑えることができ、パフォーマンスが上がるそうです。
- @Inputプロパティに変更があった時
- イベント発生時
- 指定したObservableが流れた時(今回の場合は"users$")

参考記事

combineLatestドキュメント
AsyncPipeドキュメント

最後に一応自己紹介

2ヶ月前に初めてAngular, TypeScript, RxJSに触れ、現在ウェブアプリの開発をしている新米エンジニアです。
初Qiita記事なので、間違えている部分があれば、温かい目で、優しい気持ちで教えてくださると嬉しいです?

????メリークリスマス????

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

【jQuery】スムーススクロールの実装

はじめに

スムーススクロールは、同じページ内を滑らかに移動させる機能のことです。
何か項目をクリックすると、画面がぬるっと動くというWebサイトをよく見かけると思いますが、まさにあの動きを実現するためのものです。
自分のWebサイトに実装する際、実際に触ってみて学んだことを整理しました。

想定読者

・JavaScript、jQueryを学習中の方
・自作Webサイトに何か動きをつけたい方

実現すること

スムーススクロールを実装し、ページが指定の場所に滑らかに移動するようにします。

前提

純粋なJavaScriptやCSSだけでも実装する方法はありますが、今回はjQueryを使います。
jQueryの導入方法については、下記URLのサイトが分かりやすいかと思います。
本記事では「Google CDN」を活用しています。
「3.x snippet:」のscriptタグの記述を、そのままhtmlファイルにコピペすればOKです。

(参考)
https://creive.me/archives/19581/

ソースコード・挙動

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Scroll</title>
  <link rel="stylesheet" type="text/css" media="all" href="sample.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script type="text/javascript" src="scroll.js"></script>
</head>

<body>
  <h1 id="top">Top</h1>
  <ul>
    <li><a class="item" href="#first">First</a></li>
    <li><a class="item" href="#second">Second</a></li>
    <li><a class="item" href="#third">Third</a></li>
  </ul>
  <div id="first">
    <h2>First</h2>
  </div>
  <div id="second">
    <h2>Second</h2>
  </div>
  <div id="third">
    <h2>Third</h2>
  </div>
  <a class="backToTop" href="#top">Topに戻る</a>
</body>
</html>
sample.css
/* スクロールの動きを確認するため、div要素に幅を持たせています */
div{
  height: 700px;
}

/* Topに戻るためのリンクを画面右下に固定しています */
.backToTop{
  position: fixed;
  right: 50px;
  bottom: 50px;
}
scroll.js
$(function(){
  $('a[href^="#"]').click(function() { /* ①クリックアクションを設定 */
    var speed = 500; /* ②スクロールの速さを指定 */
    var href= $(this).attr('href'); /* ③クリックするリンクの位置の値を取得 */
    var target = $(href == "#" || href == "" ? 'html' : href); /* ④スクロール先を取得 */
    var position = target.offset().top; /* ⑤ページのトップからスクロール先までの位置を数値として取得 */
    $('body,html').animate({scrollTop: position}, speed, 'swing'); /* ⑥スクロールのアニメーション設定 */
    return false; /* ⑦falseを返し、URLに影響を与えないようにする */
  });
});

See the Pen SmoothScroll by jnd_acgm (@jnd_acgm) on CodePen.

解説

①クリックアクションを設定

$('a[href^="#"]')
この記述は、「aタグのhref属性の値が#で始まるとき」ということを意味しています。
「^=」はjQueryのセレクタ指定方法の一つであり、前方一致を表します。
これにより、href属性で値が#から始まるすべての要素を取得することができます。

②スクロールの速さを指定

var speed = 500;
変数speedにミリ秒単位でスクロールの速さを代入します。
この数値を変えることで、スクロールの速さを調整することが可能です。

③クリックするリンクの位置の値を取得

var href= $(this).attr('href');
attrメソッドを使い、クリックするリンクの位置であるhref属性の値を取得し、変数hrefに代入します。

④スクロール先を取得

var target = $(href == "#" || href == "" ? 'html' : href);
スクロール先を取得し、変数targetに代入します。
記述の意味としては、「変数hrefの値が#または空白であればhtml(リンク先指定箇所)、そうでなければhrefの値を変数targetに代入する」という内容になります。
論理演算子「||」や三項演算子「?」・「:」を使用し、簡潔に一行で表現しています。

⑤ページのトップからスクロール先までの位置を数値として取得

var position = target.offset().top;
offsetメソッドを利用し、ページのトップからtargetまでの位置を取得し、変数positionに代入します。

⑥スクロールのアニメーション設定

$('body,html').animate({scrollTop: position}, speed, 'swing');
animateメソッドを使用し、クリックするリンクからスクロール先までの動きを設定します。
{scrollTop: position}でpositionまでスクロールするという動き、speedで速さ、'swing'でスクロールの動き方を定めています。

⑦falseを返し、URLに影響を与えないようにする

リンクをクリックすると、URLにIDタグが付与されてしまい正しく動作しないので、falseを返すことで防ぎます。

まとめ

スムーススクロールはほとんどのWebサイトに実装されていると思いますが、ユーザーの使いやすさを考えると採用して損はないはずなので、ぜひ試してみて下さい!
少しでも参考になりましたら幸いです!

参考情報

・jQuery導入方法
https://creive.me/archives/19581/

・スムーススクロール実装手順
https://changeup.tech/article/jquery-smooth-scroll/
https://techacademy.jp/magazine/9532

・JavaScriptリファレンス
(論理演算子)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_Operators
(三項演算子)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Conditional_Operator

・jQuery日本語リファレンス
(attrメソッド)
http://semooh.jp/jquery/api/attributes/attr/name/
(offsetメソッド)
http://semooh.jp/jquery/api/css/offset/_/

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

条件分岐 if文

Rubyと同じように、条件を満たしているかで実行内容を分岐する処理。

Sample
if (条件式1) {
  // 条件式1がtrueのときの処理
} else if (条件式2) {
  // 条件式1がfalseで条件式2がtrueのときの処理
} else {
  // 条件式1も条件式2もfalseのときの処理
}

条件分岐の特徴
・条件式は()でくくる
・条件式の後に続く波括弧{}内の処理が実行されること
・複数条件を指定する場合は、elseのあとに続けてif文を記述すること
※ } else if (条件式2) { ← ここの記述

実際に条件式と処理を書いて、確認してみます。

Test
const num = 60

if (num % 15 == 0) {
  console.log(`${num}35の倍数です`)
} else if (num % 3 == 0) {
  console.log(`${num}3の倍数です`)
} else if (num % 5 == 0) {
  console.log(`${num}5の倍数です`)
} else {
  console.log(`${num}3の倍数でも、5の倍数でもありません`)
}

条件分岐の流れ
1. constで定義しているnumという変数に60を代入している
2. ifの条件式でnum=60で、(num % 15 == 0)がtrueかfalseで実行する処理を分岐
3. (num % 15 == 0)がtrueなら、console.log(${num}は3と5の倍数です)がブラウザ上で実行される
4. (num % 15 == 0)がfalseな、else ifの条件分岐がされる
5. elseはelse以外の条件式がfalseの場合、処理が動く

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

Flash Advent Calendar 4日目 - JavaScriptでClass設計 -

バイナリを分解・解読できた後は、クラスへ情報を適用していきます。

Flashのクラス設計に準拠してクラスを作っていきます。

目次

  • Flashのクラス構成をJSへ置き換える
  • Flashで定義されてるプロパティと関数を設置

Flashのクラス構成をJSへ置き換える

沢山クラスがあるのですが、一番小さいクラスShapeを作っていこうと思います。
(当時はes6はそこまでサポートされていなかったので、es5形式で書いていきます。)

Shapeの機能一覧

継承は以下のようになります

Shape > DisplayObject > EventDispatcher > Object

小さいクラスではあるのですが、機能は盛り沢山ではあります。

まずは必要なクラスを作成

const EventDispatcher = function () {};
const DisplayObject = function () {};
const Shape = function () {};

子のprototypeを親のprototypeで上書きます。

src/flash/display/Shape.js
Shape.prototype = Object.create(DisplayObject.prototype);

このままだとconstructorは親のconstructorになってしまうので
自分のクラスでさらに上書きする。

src/flash/display/Shape.js
Shape.prototype.constructor = Shape;

親のクラスでも同じことを行う。

src/flash/display/DisplayObject.js
DisplayObject.prototype = Object.create(EventDispatcher.prototype);
DisplayObject.prototype.constructor = DisplayObject;

Flashで定義されてるプロパティと関数を設置

Flashで定義されてるものは全て設置していきます。

Shape

src/flash/display/Shape.js
/**
 * @extends DisplayObject
 * @constructor
 * @public
 */
const Shape = function () 
{
    // 親のconstructorを起動
    DisplayObject.call(this);
};

/**
 * @return {string}
 * @static
 */
Shape.toString = function ()
{
    return "[class Shape]";
};

/**
 * extends {DisplayObject}
 */
Shape.prototype = Object.create(DisplayObject.prototype);
Shape.prototype.constructor = Shape;

/**
 * properties
 */
Object.defineProperties(Shape.prototype, {
    /**
     * @description このShapeオブジェクトに属するGraphicsオブジェクトを指定します。
     *              The Shape class includes a graphics property,
     *              which lets you access methods from the Graphics class.
     *
     * @memberof Shape#
     * @property {Graphics} graphics
     * @readonly
     * @public
     */
    graphics: {
        /**
         * @returns {Graphics}
         */
        get: function () {
            return this._$graphics;
        }
    }
});

DisplayObject

src/flash/display/DisplayObject.js
/**
 * @extends {EventDispatcher}
 * @constructor
 * @public
 */
const DisplayObject = function ()
{
    // 親のconstructorを起動 
    EventDispatcher.call(this, this);
};

/**
 * @return {string}
 * @static
 */
DisplayObject.toString = function ()
{
    return "[class DisplayObject]";
};

/**
 * extends {EventDispatcher}
 */
DisplayObject.prototype = Object.create(EventDispatcher.prototype);
DisplayObject.prototype.constructor = DisplayObject;

/**
 * properties
 */
Object.defineProperties(DisplayObject.prototype, {
    /**
     * @description この表示オブジェクトの現在のアクセシビリティオプションです。
     *              The current accessibility options for this display object.
     *
     * @memberof DisplayObject#
     * @property {AccessibilityProperties} accessibilityProperties
     * @public
     */
    accessibilityProperties: {
        /**
         * @returns {AccessibilityProperties}
         */
        get: function () {
        },
        /**
         * @param  {AccessibilityProperties} accessibility_properties
         * @return void
         */
        set: function (accessibility_properties) {
        }
    },
    /**
     * @description 指定されたオブジェクトのアルファ透明度値を示します。
     *              有効な値は 0(完全な透明)~ 1(完全な不透明)です。
     *              デフォルト値は 1 です。alpha が 0 に設定されている表示オブジェクトは、
     *              表示されない場合でも、アクティブです。
     *              Indicates the alpha transparency value of the object specified.
     *              Valid values are 0 (fully transparent) to 1 (fully opaque).
     *              The default value is 1. Display objects with alpha set to 0 are active,
     *              even though they are invisible.
     *
     * @memberof DisplayObject#
     * @property {number} alpha
     * @public
     */
    alpha: {
        /**
         * @returns {number}
         */
        get: function () {
        },
        /**
         * @param   {number} alpha
         * @returns void
         */
        set: function (alpha) {
        }
    },
    /**
     * @description 使用するブレンドモードを指定する BlendMode クラスの値です。
     *              A value from the BlendMode class that specifies which blend mode to use.
     *
     * @memberof DisplayObject#
     * @property {string} blendMode
     */
    blendMode: {
        /**
         * @return {string}
         */
        get: function () {
        },
        /**
         * @param  {string} blend_mode
         * @return void
         */
        set: function (blend_mode) {
        }
    },
    /**
     * @description 前景と背景のブレンドに使用するシェーダーを設定します。
     *              Sets a shader that is used for blending the foreground and background.
     *
     * @memberof DisplayObject#
     * @property {Shader} blendShader
     * @write-only
     */
    blendShader: {
        /**
         * @param {Shader} blend_shader
         * @return void
         */
        set: function (blend_shader) {
        }
    },
    /**
     * @description true に設定されている場合、表示オブジェクトの内部ビットマップ表現が
     *              Flash ランタイムにキャッシュされます。
     *              If set to true, Flash runtimes cache
     *              an internal bitmap representation of the display object.
     *
     * @memberof DisplayObject#
     * @property {boolean} cacheAsBitmap
     */
    cacheAsBitmap: {
        /**
         * @return {boolean}
         */
        get: function () {
        },
        /**
         * @param  {boolean} cache_as_bitmap
         * @return void
         */
        set: function (cache_as_bitmap) {
        }
    },
    /**
     * @description true に設定されている場合、表示オブジェクトの内部ビットマップ表現が
     *              Flash ランタイムにキャッシュされます。
     *              If set to true, Flash runtimes cache
     *              an internal bitmap representation of the display object.
     *
     * @memberof DisplayObject#
     * @property {Matrix} cacheAsBitmapMatrix
     */
    cacheAsBitmapMatrix: {
        /**
         * @return {Matrix}
         */
        get: function () {
        },
        /**
         * @param  {Matrix} cache_as_bitmap_matrix
         * @return void
         */
        set: function (cache_as_bitmap_matrix) {
        }
    },
    /**
     * @description 表示オブジェクトに現在関連付けられている各フィルターオブジェクトが
     *              格納されているインデックス付きの配列です。
     *              An indexed array that contains each filter object
     *              currently associated with the display object.
     *
     * @memberof DisplayObject#
     * @property {array} filters
     */
    filters: {
        /**
         * @return {array}
         */
        get: function () {
        },
        /**
         * @param  {array} filters
         * @return void
         */
        set: function (filters = null) {
        }
    },
    /**
     * @description 表示オブジェクトの高さを示します(ピクセル単位)。
     *              Indicates the height of the display object, in pixels.
     *
     * @memberof DisplayObject#
     * @property {number} height
     */
    height: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} height
         * @return void
         */
        set: function (height) {
        }
    },
    /**
     * @description この表示オブジェクトが属するファイルの読み込み情報を含む LoaderInfo オブジェクトを返します。
     *              Returns a LoaderInfo object containing information
     *              about loading the file to which this display object belongs.
     *
     * @memberof DisplayObject#
     * @property {LoaderInfo} loaderInfo
     */
    loaderInfo: {
        /**
         * @return {LoaderInfo}
         */
        get: function () {
        }
    },
    /**
     * @description 呼び出し元の表示オブジェクトは、指定された mask オブジェクトによってマスクされます。
     *              The calling display object is masked by the specified mask object.
     *
     * @memberof DisplayObject#
     * @property {DisplayObject} mask
     */
    mask: {
        /**
         * @return {DisplayObject|null}
         */
        get: function () {
        },
        /**
         * @param  {DisplayObject|null} mask
         * @return void
         */
        set: function (mask) {
        }
    },
    /**
     * @description メタデータが PlaceObject4 タグによってこの DisplayObject のインスタンスと一緒に
     *              SWF ファイル内に保存されている場合に、DisplayObject インスタンスのメタデータオブジェクトを取得します。
     *              Obtains the meta data object of the DisplayObject instance
     *              if meta data was stored alongside the the instance
     *              of this DisplayObject in the SWF file through a PlaceObject4 tag.
     *
     * @memberof DisplayObject#
     * @property {object} metaData
     */
    metaData: {
        /**
         * @return {object}
         */
        get: function () {
        },
        /**
         * @param  {object} meta_data
         * @return void
         */
        set: function (meta_data) {
        }
    },
    /**
     * @description マウスまたはユーザー入力デバイスの x 軸の位置をピクセルで示します。
     *              Indicates the x coordinate of the mouse or user input device position, in pixels.
     *
     * @memberof DisplayObject#
     * @property {number} mouseX
     */
    mouseX: {
        /**
         * @return {number}
         */
        get: function () {
        }
    },
    /**
     * @description マウスまたはユーザー入力デバイスの y 軸の位置をピクセルで示します。
     *              Indicates the y coordinate of the mouse or user input device position, in pixels.
     *
     * @memberof DisplayObject#
     * @property {number} mouseY
     */
    mouseY: {
        /**
         * @return {number}
         */
        get: function () {
        }
    },
    /**
     * @description DisplayObject のインスタンス名を示します。
     *              Indicates the instance name of the DisplayObject.
     *
     * @memberof DisplayObject#
     * @property {string} name
     */
    name: {
        /**
         * @returns {string}
         */
        get: function () {
        },
        /**
         * @param  {string} name
         * @return void
         */
        set: function (name) {
        }
    },
    /**
     * @description 表示オブジェクトが特定の背景色で不透明であるかどうかを指定します。
     *              Specifies whether the display object is opaque with a certain background color.
     *
     * @memberof DisplayObject#
     * @property {object} [opaqueBackground=null]
     */
    opaqueBackground: {
        /**
         * @returns {number}
         */
        get: function () {
        },
        /**
         * @param  {number} [opaque_background=null]
         * @return void
         */
        set: function (opaque_background = null) {
        }
    },
    /**
     * @description この表示オブジェクトを含む DisplayObjectContainer オブジェクトを示します。
     *              Indicates the DisplayObjectContainer object that contains this display object.
     *
     * @memberof DisplayObject#
     * @property {DisplayObjectContainer} parent
     * @readonly
     * @public
     */
    parent: {
        /**
         * @returns {DisplayObjectContainer}
         */
        get: function () {
        }
    },
    /**
     * @description 読み込まれた SWF ファイル内の表示オブジェクトの場合、
     *              root プロパティはその SWF ファイルが表す表示リストのツリー構造部分の一番上にある表示オブジェクトとなります。
     *              For a display object in a loaded SWF file,
     *              the root property is the top-most display object
     *              in the portion of the display list's tree structure represented by that SWF file.
     *
     * @memberof DisplayObject#
     * @property {DisplayObject} root
     * @readonly
     * @public
     */
    root: {
        /**
         * @returns {DisplayObject}
         */
        get: function () {
        }
    },
    /**
     * @description DisplayObject インスタンスの元の位置からの回転角を度単位で示します。
     *              Indicates the rotation of the DisplayObject instance,
     *              in degrees, from its original orientation.
     *
     * @memberof DisplayObject#
     * @property {number} rotation
     */
    rotation: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} rotation
         * @return void
         */
        set: function (rotation) {
        }
    },
    /**
     * @description DisplayObject インスタンスの 3D 親コンテナを基準にした元の位置からの x 軸の回転角を度単位で示します。
     *              Indicates the x-axis rotation of the DisplayObject instance,
     *              in degrees, from its original orientation relative to the 3D parent container.
     *
     * @memberof DisplayObject#
     * @property {number} rotationX
     */
    rotationX: {
        get: function () {
        },
        set: function (rotation_x) {
        }
    },
    /**
     * @description DisplayObject インスタンスの 3D 親コンテナを基準にした元の位置からの x 軸の回転角を度単位で示します。
     *              Indicates the x-axis rotation of the DisplayObject instance,
     *              in degrees, from its original orientation relative to the 3D parent container.
     *
     * @memberof DisplayObject#
     * @property {number} rotationY
     */
    rotationY: {
        get: function () {
        },
        set: function (rotation_y) {
        }
    },
    /**
     * @description DisplayObject インスタンスの 3D 親コンテナを基準にした元の位置からの x 軸の回転角を度単位で示します。
     *              Indicates the x-axis rotation of the DisplayObject instance,
     *              in degrees, from its original orientation relative to the 3D parent container.
     *
     * @memberof DisplayObject#
     * @property {number} rotationZ
     */
    rotationZ: {
        get: function () {
        },
        set: function (rotation_z) {
        }
    },
    /**
     * @description 現在有効な拡大 / 縮小グリッドです。
     *              The current scaling grid that is in effect.
     *
     * @memberof DisplayObject#
     * @property {Rectangle} [scale9Grid=null]
     * @public
     */
    scale9Grid: {
        /**
         * @return {Rectangle|null}
         */
        get: function () {
        },
        /**
         * @param  {Rectangle} scale_9_grid
         * @return void
         */
        set: function (scale_9_grid) {
        }
    },
    /**
     * @description 基準点から適用されるオブジェクトの水平スケール(パーセンテージ)を示します。
     *              Indicates the horizontal scale (percentage)
     *              of the object as applied from the registration point.
     *
     * @memberof DisplayObject#
     * @property {number} scaleX
     */
    scaleX: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} scale_x
         * @return void
         */
        set: function (scale_x) {
        }
    },
    /**
     * @description 基準点から適用されるオブジェクトの垂直スケール(パーセンテージ)を示します。
     *              IIndicates the vertical scale (percentage)
     *              of an object as applied from the registration point.
     *
     * @memberof DisplayObject#
     * @property {number} scaleY
     */
    scaleY: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} scale_y
         * @return void
         */
        set: function (scale_y) {
        }
    },
    /**
     * @description 基準点から適用されるオブジェクトの奥行きスケール(パーセンテージ)を示します。
     *              Indicates the depth scale (percentage)
     *              of an object as applied from the registration point
     *
     * @memberof DisplayObject#
     * @property {number} scaleZ
     */
    scaleZ: {
        /**
         * @return {number}
         */
        get: function () {
            return 0;
        }, 
        /**
         * @param {number} scale_z
         */
        set: function (scale_z) {
        }
    },
    /**
     * @description 表示オブジェクトのスクロール矩形の境界です。
     *              The scroll rectangle bounds of the display object.
     *
     * @memberof DisplayObject#
     * @property {Rectangle} [scrollRect=null]
     */
    scrollRect: {
        /**
         * @return {Rectangle}
         */
        get: function () {
        },
        /**
         * @param  {Rectangle} [scroll_rect=null]
         * @return void
         */
        set: function (scroll_rect) {
        }
    },
    /**
     * @description 表示オブジェクトのステージです。
     *              The Stage of the display object.
     *
     * @memberof DisplayObject#
     * @property {Stage} stage
     */
    stage: {
        /**
         * @returns {Stage}
         */
        get: function () {
        }
    },
    /**
     * @description 表示オブジェクトのマトリックス、カラー変換、
     *              ピクセル境界に関係するプロパティを持つオブジェクトです。
     *              An object with properties pertaining
     *              to a display object's matrix, color transform, and pixel bounds.
     *
     * @memberof DisplayObject#
     * @property {Transform} transform
     */
    transform: {
        /**
         * @returns {Transform}
         */
        get: function () {
        },
        /**
         * @param   {Transform} transform
         * @returns void
         */
        set: function (transform) {
        }
    },
    /**
     * @description 表示オブジェクトが可視かどうかを示します。
     *              Whether or not the display object is visible.
     *
     * @memberof DisplayObject#
     * @property {boolean} visible
     */
    visible: {
        /**
         * @return {boolean}
         */
        get: function () {
        },
        /**
         * @param  {boolean} visible
         * @return void
         */
        set: function (visible) {
        }
    },
    /**
     * @description 表示オブジェクトの幅を示します(ピクセル単位)。
     *              Indicates the width of the display object, in pixels.
     *
     * @memberof DisplayObject#
     * @property {number} width
     */
    width: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} width
         * @return void
         */
        set: function (width) {
        }
    },
    /**
     * @description 親 DisplayObjectContainer のローカル座標を基準にした
     *              DisplayObject インスタンスの x 座標を示します。
     *              Indicates the x coordinate
     *              of the DisplayObject instance relative to the local coordinates
     *              of the parent DisplayObjectContainer.
     *
     * @memberof DisplayObject#
     * @property {number} x
     */
    x: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} x
         * @return void
         */
        set: function (x) {
        }
    },
    /**
     * @description 親 DisplayObjectContainer のローカル座標を基準にした
     *              DisplayObject インスタンスの y 座標を示します。
     *              Indicates the y coordinate
     *              of the DisplayObject instance relative to the local coordinates
     *              of the parent DisplayObjectContainer.
     *
     * @memberof DisplayObject#
     * @property {number} y
     */
    y: {
        /**
         * @return {number}
         */
        get: function () {
        },
        /**
         * @param  {number} y
         * @return void
         */
        set: function (y) {
        }
    },
    /**
     * @description 3D 親コンテナを基準にした、DisplayObject インスタンスの z 軸に沿った z 座標位置を示します。
     *              Indicates the z coordinate position along the z-axis
     *              of the DisplayObject instance relative to the 3D parent container.
     *
     * @memberof DisplayObject#
     * @property {number} z
     */
    z: {
        /**
         * @return {number}
         */
        get: function () {
            return 0;
        }, 
        /**
         * @param {number} z
         */
        set: function (z) {
        }
    }
});

/**
 * @description targetCoordinateSpace オブジェクトの座標系を基準にして、
 *              表示オブジェクトの領域を定義する矩形を返します。
 *              Returns a rectangle that defines the area
 *              of the display object relative to the coordinate system
 *              of the targetCoordinateSpace object.
 *
 * @param  {DisplayObject} target_coordinate_space
 * @return {Rectangle}
 */
DisplayObject.prototype.getBounds = function (target_coordinate_space)
{
};

/**
 * @description シェイプ上の線を除き、
 *              targetCoordinateSpace パラメーターによって定義された座標系に基づいて、
 *              表示オブジェクトの境界を定義する矩形を返します。
 *              Returns a rectangle that defines the boundary
 *              of the display object, based on the coordinate system defined
 *              by the targetCoordinateSpace parameter,
 *              excluding any strokes on shapes.
 *
 *
 * @param  {DisplayObject} target_coordinate_space
 * @return {Rectangle}
 */
DisplayObject.prototype.getRect = function (target_coordinate_space)
{
};

/**
 * @description point オブジェクトをステージ(グローバル)座標から
 *              表示オブジェクトの(ローカル)座標に変換します。
 *              Converts the point object from the Stage (global) coordinates
 *              to the display object's (local) coordinates.
 *
 * @param   {Point} point
 * @returns {Point}
 * @public
 */
DisplayObject.prototype.globalToLocal = function (point)
{
};

/**
 * @description ステージ(グローバル)座標の 2 次元のポイントを
 *              3 次元の表示オブジェクトの(ローカル)座標に変換します。
 *              Converts a two-dimensional point from the Stage (global) coordinates
 *              to a three-dimensional display object's (local) coordinates.
 *
 * @param   {Point} point
 * @returns {Vector3D}
 * @public
 */
DisplayObject.prototype.globalToLocal3D = function (point)
{
};

/**
 * @description 表示オブジェクトの境界ボックスを評価して、
 *              obj 表示オブジェクトの境界ボックスと重複または交差するかどうかを調べます。
 *              Evaluates the bounding box of the display object to see
 *              if it overlaps or intersects with the bounding box of the obj display object.
 *
 * @param   {DisplayObject} object
 * @returns {boolean}
 * @public
 */
DisplayObject.prototype.hitTestObject = function (object)
{
};

/**
 * @description 表示オブジェクトを評価して、x および y パラメーターで指定された
 *              ポイントと重複または交差するかどうかを調べます。
 *              Evaluates the display object to see if it overlaps
 *              or intersects with the point specified by the x and y parameters.
 *
 * @param   {number}  x
 * @param   {number}  y
 * @param   {boolean} [shape_flag=false]
 * @returns {boolean}
 * @public
 */
DisplayObject.prototype.hitTestPoint = function (x, y, shape_flag = false)
{
};

/**
 * @description 3 次元の表示オブジェクトの(ローカル)座標の 3 次元のポイントを
 *              ステージ(グローバル)座標の 2 次元のポイントに変換します。
 *              Converts a three-dimensional point of the three-dimensional
 *              display object's (local) coordinates to a two-dimensional point in the Stage (global) coordinates.
 *
 * @param   {Vector3D} point3d
 * @returns {Point}
 * @public
 */
DisplayObject.prototype.local3DToGlobal = function (point3d)
{
};

/**
 * @description point オブジェクトを表示オブジェクトの(ローカル)座標から
 *              ステージ(グローバル)座標に変換します。
 *              Converts the point object from the display object's (local) coordinates
 *              to the Stage (global) coordinates.
 *
 *
 * @param   {Point} point
 * @returns {Point}
 * @public
 */
DisplayObject.prototype.localToGlobal = function (point)
{
};

EventDispatcher

src/flash/events/EventDispatcher.js
/**
 * @param   {EventDispatcher} [target=null]
 * @extends OriginalObject
 * @constructor
 * @public
 */
const EventDispatcher = function (target = null)
{
};

/**
 * @return {string}
 * @static
 */
EventDispatcher.toString = function ()
{
    return "[class EventDispatcher]";
};

/**
 * @description イベントリスナーオブジェクトを EventDispatcher オブジェクトに登録し、
 *              リスナーがイベントの通知を受け取るようにします。
 *              Registers an event listener object with an EventDispatcher object
 *              so that the listener receives notification of an event.
 *
 * @param  {string}   type
 * @param  {function} listener
 * @param  {boolean}  [use_capture=false]
 * @param  {number}   [priority=0]
 * @param  {boolean}  [use_weak_reference=false]
 * @return void
 * @public
 */
EventDispatcher.prototype.addEventListener = function (
    type, listener, use_capture = false, priority = 0, use_weak_reference = false
) {
};

/**
 * @description イベントをイベントフローに送出します。
 *              Dispatches an event into the event flow.
 *
 * @param  {Event}   event
 * @return {boolean}
 * @public
 */
EventDispatcher.prototype.dispatchEvent = function (event)
{
};

/**
 * @description EventDispatcher オブジェクトに、特定のイベントタイプに対して登録されたリスナーがあるかどうかを確認します。
 *              Checks whether the EventDispatcher object has any listeners registered for a specific type of event.
 *
 * @param   {string}  type
 * @returns {boolean}
 * @public
 */
EventDispatcher.prototype.hasEventListener = function (type)
{
};

/**
 * @description EventDispatcher オブジェクトからリスナーを削除します。
 *              Removes a listener from
 * the EventDispatcher object.
 *
 * @param   {string}   type
 * @param   {function} listener
 * @param   {boolean}  [use_capture=false]
 * @returns void
 * @public
 */
EventDispatcher.prototype.removeEventListener = function (type, listener, use_capture = false)
{
};

/**
 * @description 指定されたイベントタイプについて、
 *              この EventDispatcher オブジェクトまたはその祖先にイベントリスナーが
 *              登録されているかどうかを確認します。
 *              Checks whether an event listener is registered
 *              with this EventDispatcher object or
 *              any of its ancestors for the specified event type.
 *
 * @param  {string}  type
 * @return {boolean}
 * @public
 */
EventDispatcher.prototype.willTrigger = function (type)
{
};

これでShapeクラスが完成です。
早速、動かしてみます。

var shape = new Shape();

// 親クラスの関数をコール
console.log(shape.getBounds(shape));

// さらに上層の親クラスの関数をコール
console.log(shape.hasEventListener("enterFrame"));

後は、Flashで関数をコールして、挙動を真似て実装していきます。
挙動が一致したらひたすらテスト書いて仕様を固めていきます。

っという事で、クラスを作れるようになったので今日はこの辺で終わります。
明日は描画部分、「Canvas2D」に関して書こうと思います。

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

npmについてまとめ

npmとは

Node Package Managerの略

Node.jsのパッケージ管理システムである。

2010年にIsaac Z. Schlueter氏によって開発された。

パッケージ管理システムとは

パッケージ管理システム(パッケージかんりシステム)は、オペレーティングシステム (OS) というひとつの環境で、各種のソフトウェアの導入と削除、そしてソフトウェア同士やライブラリとの依存関係を管理するシステムである。

要は世界の凄い人たちが作って公開しているモジュールをパッケージとして管理し、検索、閲覧、及びダウンロードして使えるよ〜というシステムです。

また、使用したいパッケージの依存パッケージ、そのバージョンまで自動で管理してくれます。

npmを使わないとどうなる?

例えばexpressというパッケージを使用したいとします。
expressは30ものパッケージと依存関係にあります。
スクリーンショット 2020-12-04 21.00.01.png

この場合expressの他にこの30ものパッケージを別途手動でダウンロードしなければexpressは動きません。
更にはこれらのパッケージもまたそれぞれ依存先を持っており、更にそのまた依存先のそのまた依存先の・・・・

・・とにかく全てのパッケージをダウンロードする必要があり、しかもバージョンの整合性もとらなければなりません。

そんな面倒なことも、npmが全て自動でやってくれる訳ですね(感謝)

package.json

package.jsonというJSONファイルにはそのパッケージ(プロジェクト)の情報が記述されています。
依存パッケージやそのバージョンもここで管理されています。

まとめ

npmについてざっと調べたことを書きました。
何気なく使っていたnpmのありがたみを知ることができました。

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

fitbitとAlexaスキルを連携させてみる

はじめに

最近fitbit versa3を買いました。心拍数や睡眠状態など色々見れて面白いです。

このモデルにはAlexaが搭載されており、音声でスキル呼び出しが可能です。
fitbitの公式Alexaスキルはすでにありますが、自分でもスマートウォッチで集めた情報を利用したスキルを作れるのか、試してみようと思います。

スキル開発

概要

スマートウォッチから直接情報を得るわけではなく、fitbitのサイトからAPI経由で取得します。だいたい以下のようなイメージですね。
全体図7.png
Alexaのアカウントリンク機能を使って、Alexaとfitbit間でID連携する形になります。

fitbit側

アプリケーション登録

fitbitのdeveloperサイトから、Regiter An Appでアプリケーションを新規登録します。
特に開発者アカウント登録などしなくても、通常のfitbitアカウントで利用できるようです。
fitbit1.png
以下のようにアプリケーション設定を登録します。

項目名 登録内容
Application Name 任意のアプリケーション名
Description アプリケーションの説明
Application Website アプリケーションのWebサイト。Alexaスキルが公開されたらamazon.co.jpの該当スキルの画面を入れるのがよさそうです
Organization 自身の組織名
Organization Website 組織のWebサイト
Terms Of Service Url 利用規約のURL
Privacy Policy Url プライバシーポリシーのURL
OAuth 2.0 Application Type Client
Callback URL ※後述
Default Access Type Read-Only

試験的に動かすだけであれば、OAuth 2.0 Application Type、Callback URL、Default Access Type以外はダミー値で大丈夫です。ただし、利用者に提示される内容なので、実際に公開する場合はきちんとした値を入れる必要があります。

Callback URLだけは要注意です。
fitbit側で整合性チェックをしているようで、Alexa側から渡ってくるコールバックURLと一致した値を入れておかないと、後で利用者向けの同意画面を表示するときにエラーになります。
この時点ではとりあえず何かURLを入れておき、後続の作業でAlexa側のコールバックURLが判明したら書き換えます。

登録できると、以下のように各種IDや連携に必要なURLが表示されます。
ここの値は次の工程で使います。
fitbit3.png

Alexaスキル側

アカウントリンク設定

Alexa Developer Consoleでアカウントリンク設定をしていきます。
alexa3.png

設定内容は以下です。

項目名 登録内容
Authorization Grant種別 Auth Code Grant
Web認証画面のURI fitbit側の「OAuth 2.0: Authorization URI」
アクセストークンのURI fitbit側の「OAuth 2.0: Access/Refresh Token Request URI」
ユーザーのクライアントID fitbit側の「OAuth 2.0 Client ID」
ユーザーのシークレット fitbit側の「Client Secret」
ユーザーの認可スキーム HTTP Basic認証
スコープ ※後述
ドメインリスト 空でOK
デフォルトのアクセストークンの有効期限 空でOK

スコープには、こちらを参照してアクセス許可を与える対象を指定します。
今回は心拍数の情報を使いたいので、「heartrate」を指定します。

また、このタイミングでAlexaのリダイレクト先のURLがわかりますが、
このURLと同じ値を全て、fitbit側設定の「Callback URL」に反映させておきます。
alexa2.png

実装

ここまでの設定で、利用者側でアカウントリンク設定が済んでいれば、fitbitのAPIを呼び出すためのアクセストークンが自動的にLambdaまで渡ってくるようになります。

{
    "requestEnvelope": {
        "version": "1.0",
        "session": {
            "new": true,
            "sessionId": "amzn1.echo-api.session.xxx...",
            "application": {
                "applicationId": "amzn1.ask.skill.xxx..."
            },
            "user": {
                "userId": "amzn1.ask.account.xxx...",
                "accessToken": "xxx..."    // ←☆これ
            }
        },

ソース内ではアクセストークンを取得し、API仕様を確認しながら必要なAPIを呼んであげればOKです。

index.js
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    async handle(handlerInput) {

        // アクセストークンを取得
        const token = Alexa.getAccountLinkingAccessToken(handlerInput.requestEnvelope);

        // fitbitのAPIを呼び出す
        const url = `https://api.fitbit.com/1/user/-/activities/heart/date/today/1d.json`;
        const headers = {
            Authorization: `Bearer ${token}`
        };
        let response;
        try {
            // リクエスト実行
            response = await Axios.get(url, { headers: headers });
        } catch (error) {
            throw new Error(`get fitbit data error , url:${url} , error:${error}`);
        }

        // API呼び出し結果を利用してAlexaの応答を組み立て
        const restingHeartRate = response.data['activities-heart'][0].value.restingHeartRate;
        const speakOutput = `今日の安静時の心拍数は${restingHeartRate}です。`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard('測定結果', speakOutput)
            .getResponse();
    }
};

今回は、心拍数を取得するheart-rate APIを呼び出し、そこから安静時の心拍数(restingHeartRate)を取り出しています。
また、このソースでは省略していますが、実際にはトークンをとれなかったときに連携設定を促す処理などが別途必要になります。こちらなどを参考に実装するのがよいでしょう。

利用者から見た動き

アカウントリンク

スキルを有効にした後、Alexaアプリからアカウントリンクを行います。
連携設定.png
fitbitにログインしていなければログインを求められ、その後心拍数データ取得の同意を確認する画面フローになります。

スキル呼び出し

fitbitに向かってスキル起動をを呼びかけてみます。
result.png
出た!やった!
画面表示だけでなく、きちんと読み上げてくれます。

おわりに

アカウントリンク機能を使って、fitbitとAlexaスキルを連携させることができました。
利用者ごとのトークン管理やリフレッシュなどの面倒なところをAlexaが全部やってくれるので、思っていたより遥かに簡単でした。

心拍数を表示するだけであれば標準機能でも普通にできますし、fitbitの上で動くカスタムアプリを作る方法もありそうなので、あえてAlexaスキルを使う強みがあるとしたら、音声が使える点や、他の据え置きのAmazon Echoなどからも同じように呼べる点になるかと思います。
どんなスキルを作れるかはアイデア次第ですね。

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

【javascript】空の配列に対するevery/someメソッド

こんなの知らなかった。。。
忘れないようにメモ。

everyメソッド

[].every(x => false);   //true

注意: このメソッドを空の配列に対して呼び出すと、無条件に true を返します。

someメソッド

[].some(x => true); //false

注: このメソッドは空の配列ではあらゆる条件式に対して false を返します。

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

TypeScriptで文字列を配列に変換する方法。(+エラーが出た時の対処方法)

文字列を配列に変換する

スプレット演算子を用いると文字列を配列に変換できます。
これはJavaScriptでもTypeScriptでも同じです。

//[...文字列]という形がスプレット演算子です。
const str = "konnitiwa";
const array = [...str];
console.log(array);

// split()でも変換できます。
const array2 = str.split("");
console.log(array2);

結果はどちらも同じになります。

[
  'k', 'o', 'n', 'n', 'i', 't', 'i', 'w', 'a'
]

スプレット演算子を使ったほうがスマートですね。

スプレット演算子とは?

スプレッド構文 (...) を使うと、配列式や文字列などの反復可能オブジェクトを、0 個以上の引数 (関数呼び出しの場合) や要素 (配列リテラルの場合) を期待された場所で展開したり、オブジェクト式を、0 個以上のキーと値のペア (オブジェクトリテラルの場合) を期待された場所で展開したりすることができます。
(MDNより)

スプレッド構文 - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

エラーが出た時。

でもTypeScriptで動かそうとすると何故かエラーが出るときがあります。
これはファイルの拡張子「.ts」を「.js」に変えるとうまく動きます。
何故でしょうか???

function stringToArry(str: string): string[] {
  const array = [...str];
  return array;
}

console.log(stringToArry("konnitiwa"));

エラー:

error TS2569: Type 'string' is not an array type or a string type. Use compiler option '--downlevelIteration' to
allow iterating of iterators.

2   const array = [...str];

その対処方法は・・・

プロジェクトのルートで
tsc --init
を実行して tsconfig.jsonファイルを作成します。

compilerOptionsでES6以降を指定します。
これでうまく動くようになります。

{
  "compilerOptions": {
        // "target": "ES3", //(default値)
        // "target": "ES5", // 動かない
        "target": "ES6",    // 動く
        // "target": "ES2015", 
        // "target": "ES2016",
        // "target": "ES2017",
        // "target": "ES2018",
        // "target": "ES2019",
        // "target": "ES2020",
        // "target": "ESNEXT",
  }
}

原因は

tsconfig.jsonを作ってないとES3がデフォルト値となっているので、
tsconfig.jsonを作ってcompilerOptionsのES6以降を指定すれば動くようになります。

VSCodeの拡張機能「Code Runner」などで直接「.ts」ファイルを実行していた時に起きたエラーでした。

ほんの少し前までは
tsconfig.jsonを新規作成した時にES5と設定した人も多かったのではないでしょうか?この場合もエラーが出るのでコンパイラオプションを指定するかES6に設定を書き換えることでエラーは出なくなります。

ES5でも動かしたいときは
エラーメッセージにあるように
tsconfig.jsonの設定で
"downlevelIteration": true,
とすることで動くようになります。

古いコンパイラでも、新しい構文を使えるようにするオプションということですね。

CodeWars

Codewars
https://www.codewars.com/

英語のサイトですがTypeScriptで挑戦中です。
VSCodeの拡張機能「Code Runner」を使えばプロジェクトを作ってインストールしなくてもTypeScriptのファイル「*.ts」が動きます。
ブラウザ上で編集するのは大変なので、一旦VSCode上でプログラムを書いてからCodewarsのサイト上に貼り付けて提出しています。

今回のエラーはこの状況でおきたものでした。

code runner

VSCodeでRubyを気軽に実行する環境を作る。 - Qiita
https://qiita.com/masakinihirota/items/ec90086bab86f369fa15

Ruby以外でも、 C, C++, Java, JavaScript, PHP, Python, Perl, Ruby, Go, Lua, Groovy, PowerShell, BAT/CMD, BASH/SH, F# Script, F# (.NET Core), C# Script, C# (.NET Core), VBScript, TypeScript, CoffeeScript, Scala, Swift, Julia, Crystal, OCaml Script, R, AppleScript, Elixir, Visual Basic .NET, Clojure, Haxe, Objective-C, Rust, Racket, AutoHotkey, AutoIt, Kotlin, Dart, Free Pascal, Haskell, Nim, D, が特に設定せずに使用可能です、他の言語もコマンドを設定すれば可能です。

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

Garmin Connectから自分のデータを取得する~ランサムウェアへの超個人的抵抗~(その2)

本記事は、株式会社ピー・アール・オーアドベントカレンダーの5日目です。

実際に作っていきましょう

前回は、やりたいことと方針のみグダグダと書いてしまいましたが。今回からはいよいよ実際の機能を作っていきたいと思います。

今回のゴール

Garmin Connectから取得可能なデータのうち、日々の活動記録の方をChrome拡張からダウンロードできるようにしてみます。なぜ日々の活動記録を選んだかというと、こっちの方がURLに日付が含まれるため、自動でURLを作るのが楽だからです。もう一方のアクティビティはアクティビティID取得したりしないといけないんで、そっちは追々追加します。
そしてダウンロードする際の要件はなんとなく以下で考えてみました。

  • 拡張機能独自のメニューを持つ
  • メニューでは以下を設定可能にする
    • ダウンロードON/OFF
    • 日時(いつからのデータをダウンロードするか?)
    • ダウンロード場所を指定させる
  • ダウンロード中もブラウザで他の作業が可能なようにする(バックグラウンドでDLは行う)
  • サーバ負荷を避けるために、一定間隔でのDLを行う
  • いつまでDLしたかを記録しておき、途中で中断してもそこから再開できる

何はなくともChrome拡張のひな型を作る

Chrome拡張、何気に作るのは初めてです。
例によって例のごとく、Qiitaで参考になりそうな記事を探しながら進めていきます。
タグ:chrome-extensionで690記事(2020/12/4時点)もあるので選り取り見取りといったところです。

manifest.json

とりあえず、一番最初は以下のようなシンプルなものにしてみました。

manifest.json
{
    "name": "Garmin Connect Downloader",
    "version": "1.0",
    "description": "Download all activities and healthcare original data",
    "permissions": ["declarativeContent", "storage"],
    "background": {
      "scripts": ["background.js"],
      "persistent": false
    },
    "page_action": {
      "default_popup": "popup.html",
      "default_icon": {
        "32": "images/get_started32.png"
      }
    },
    "manifest_version": 2
}

permissionsは、とりあえずgarmin connectのページでのみ本拡張機能を有効にしようかと思いdeclarativeContentを、設定値の保存用にstorageをチョイスしてみました。
あと、background.jsはバックグラウンドでのDL処理を行う場として、
page_actionは拡張機能のメニューとして定義しています。

popup.html

popup.htmlに、あらかじめ設定として必要なものをUIとして入れておきます。
コメントアウトしている個所は今回は実装しない予定のものです。

popup.html
<!DOCTYPE html>
  <html>
    <head>
      <script src="lib/jquery-3.5.1.min.js"></script>
      <link  href="styles/datepicker.css" rel="stylesheet">
      <link  href="styles/popup.css" rel="stylesheet">
      <script src="lib/datepicker.js"></script>
    </head>
    <body>
      <div class="whole-content">
      <!--<button id="changeColor"></button>-->
        <h2>Garmin Connect Downloader</h2>
        <div>
          <div class="item">
            <span>Download after this date</span>
            <input type="text" class="startDate" data-toggle="datepicker">
          </div>
          <!--
          <div class="item">
            <div>Download data</div>
            <label>Activity</label><input type="checkbox" class="checkActivity">
            <label>Healthcare</label><input type="checkbox" class="checkHealthcare">
          </div>
        </div>
        -->
        <!--
        <div class="item">
            <span>Download interval (seconds)</span>
            <input type="text" class="intervalTime" value="1">
        </div>
        -->
        <div class="item">
          <span>Download Directory</span>
          <input type="text" class="dlDirectory">
        </div>

        <div class="item">
          <span>Open Garmin Connect</span>
          <div>
            <a href="https://connect.garmin.com/modern/">Garmin Connect</a>
          </div>
        </div>
        <div>
          <p class="toggle-title">download</p>
          <div class="toggle-switch">
            <input id="toggle" class="toggle-input" type='checkbox' />
            <label for="toggle" class="toggle-label">
              <span></span>
          </div> 
        </div>
      </div>
      <script src="popup.js"></script>
    </body>
</html>

なお、画面中DL状態を表すためにトグルスイッチを付けたくて、以下の記事を丸パクリ参考にさせていただいて実現しました。ありがとうございます。
CSSで作るToggle Switchを学んだのでさっそく作ってみた

一つ一つ作っていきます。

設定画面の実装

値の出し入れのところから作っていきましょう。
Chrome Extension APIの、chrome.storage.sync.setおよびchrome.storage.sync.getを使って実現できそうです。

popup.js
// Save startDate to local storage
$('[data-toggle="datepicker"]').change(function(){
  var date = $(this).val();
  chrome.storage.sync.set({start_date: date}, function() {
    console.log(date);
  });
});

// Save directory to local storage
$('.dlDirectory').change(function(){
  var directory = $(this).val();
  chrome.storage.sync.set({directory: directory}, function() {
    console.log(directory);
  });
});

// Restore 
chrome.storage.sync.get('start_date', function(data) {
  $('[data-toggle="datepicker"]').val(data.start_date);
});
chrome.storage.sync.get('directory', function(data) {
  $('.dlDirectory').val(data.directory);
});

こんな感じですね。

トグルスイッチ変更時のイベントハンドラ

イベントハンドラで処理のとっかかりを作ります。

popup.js
// Toggle changed
$(".toggle-input").click(function toggleHandler(e) {
  console.log('handle on');
  var toggled = $(this).prop('checked');
  // Set params to storage
  chrome.storage.sync.set({isStart: toggled}, function() {
    console.log('isStart ' + toggled);
  });
});

// Restore
chrome.storage.sync.get('isStart', function(data) {
  $('.toggle-input').prop('checked', data.isStart);
});

これで、設定画面から登録した値と、トグルスイッチのイベントを受け取れるようになりました。

カレンダコントロールの導入

日付入力があるので入れましょう(入れました)。

popup.html(抜粋)
<!DOCTYPE html>
  <html>
    <head>
      <script src="lib/jquery-3.5.1.min.js"></script>
      <link  href="styles/datepicker.css" rel="stylesheet">
      <link  href="styles/popup.css" rel="stylesheet">
      <script src="lib/datepicker.js"></script>
    </head>
popup.js(抜粋)
// DatePicker
$('[data-toggle="datepicker"]').datepicker({
  format: 'yyyy-mm-dd'
});

backgroundの実装

page側はほぼできたので、実際のダウンロード処理周りを作っていきます。
まず一番重要なのは、どうやってDLするか?です。単に指定のURLをwindow.openするだけでもDLはできましたが、一瞬ウインドウが開かれてしまうのがイケてない感じなので却下です。
調べてみたところ、やはりChrome Extension APIのchrome.downloads.downloadが使えそうでしたので調べてみましたところ、一つだけ残念な点が。。。
image.png

はい。完全パスはダメよって言われちゃってますね。'..'も封じられているのでディレクトリトラバース的なやり方も駄目のようです。任意ディレクトリを本拡張機能の中だけで指定することはできなそうです(APIマニュアル見る前から薄々気づいてはいました。そりゃそうだ)。
まあ仕方ないので、保存場所は相対パスでのみ選べるようにはしておきましょう。

次に、定期実行処理ですが、はじめはsetTimeOutでの実装を考えていたんですが、こちらも調べてみたところ便利そうなAPIがあったのでそっちを使うことにしました(chrome.alarms.onAlarm.addListener)。

定期実行とダウンロード処理を組み合わせたのが以下コードです。

background.js
/**
 * Set alarm when installing extension
 */
chrome.runtime.onInstalled.addListener(function (details) {
  console.log(details.reason);
  chrome.alarms.create("dl_fire", { "periodInMinutes": 1 });
});

/**
 * Set alarm for regular download in the background.
 */
chrome.alarms.onAlarm.addListener(async function (alarm) {

  let _isStart = await getLocalStorageVal('isStart');

  if (_isStart.isStart && alarm.name == "dl_fire") {

    let _startDateString = await getLocalStorageVal('start_date');
    var start = moment(_startDateString.start_date);
    //Download until yesterday
    var yesterday = moment().subtract(1, 'd');

    if(yesterday.isAfter(start)) {
      var urlDate = start.format('YYYY-MM-DD');
      var url = 'https://connect.garmin.com/modern/proxy/download-service/files/wellness/' + urlDate;
      var _dir = await getLocalStorageVal('directory');

      chrome.downloads.download({
        url: url, 
        filename: _dir.directory + urlDate + '.zip'
      });
      start = start.add(1, 'd');
      chrome.storage.sync.set({start_date: start.format('YYYY-MM-DD')}, function() {
        // Save next date to local storage
      });
    }
  }
});

アラームのセットをchrome.runtime.onInstalledでやってますが、これはこのタイミングで実行しないとアラームがうまく発火してくれなかったためです。本当にそうなのか?はまだ調べてないのでわかりません。
本当はDL間隔も設定画面で任意に変更したいので、こちらの問題についてはその時が来たら取り雲くことになるでしょう(なので今は塩漬け)。
ダウンロードしたら、次の取得日時をchrome.storage.sync.setでローカルストレージを更新するようにしました。お手軽対応ですが、一応一旦停止した後後日再取得もできるようにしています。
また、上記コードの中で呼び出しているgetLocalStorageValというかっちょ悪い名前のメソッドは、ローカルストレージからの値読み出しでawaitさせるためだけのメソッドです。
awaitさせないで書こうとすると当然コールバック地獄が待ってますので、いわゆる必要悪というやつです(違う)。

background.js
/**
 * Get from local storage
 * @param {String} key 
 */
async function getLocalStorageVal(key) {
  return new Promise((resolve, reject) => {
    try {
      chrome.storage.sync.get(key, function(value) {
        resolve(value);
      })
    } catch (ex) {
      reject(ex);
    }
  });
}

そうそう。chrome.alarms.onAlarmとchrome.downloads.downloadの利用にあたってはmanifestの"permission"に追加が必要でした。あとついでに、javascriptでの日付演算なんてかったるくてやりたくなかったのでmoment.jsも組み込みました。なのでmanifest.jsonは以下のようになりました。

background.js
{
    "name": "Garmin Connect Downloader",
    "version": "1.0",
    "description": "Download all activities and healthcare original data",
    "permissions": ["activeTab","declarativeContent", "storage", "downloads", "alarms"],
    "background": {
      "scripts": ["lib/moment.min.js", "background.js"],
      "persistent": true
    },
    "page_action": {
      "default_popup": "popup.html",
      "default_icon": {
        "32": "images/get_started32.png"
      }
    },
   "icons": {
      "48": "images/get_started48.png"
    },
    "options_page":"option.html",
    "manifest_version": 2
}

さあこれで最低限の実装ができました。
実際にChromeに組み込んで動かしてみます。
chrome://extensionsを開き、「パッケージ化されていない拡張機能を読み込み」からソースディレクトリを指定します。
image.png

ちなみに、ここで「バッググランドページ」を開くとbackground.jsで出したconsole.logなどが見れるようになります(私は途中で気づきました)。たぶん・・・常識ですね。

拡張機能の設定画面はこんな感じ。まあ気の抜けた感じです。トグルスイッチだけ完成度高くて逆に変という・・・
image.png

早速ONにしてみましょう!
image.png

アラームで設定した間隔(今回は1分)で、Garmin Connectからダウンロード開始されます。
バックグラウンドなのでQiitaを書きながらでも大丈夫w
image.png

まとめ

とりあえずChrome拡張初心者が1パス通すところまではできました。
これを励みに残りの機能も作っていきたいと思います。
(あ、この一連のシリーズは弊社のAdvent Calendarで誰も記事をエントリしてない日用のピンチヒッターとして用意したものですので、他に書く人がいた場合は永遠に続きがないかもしれません。あと筆者が飽きた時なども同様)

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

[VSCode] javascriptのSnippet作成

Snippet作成

1.[歯車マーク]-[User Snippets]-[javascript]を選択

スクリーンショット 2020-12-04 19.39.26.png

スクリーンショット 2020-12-04 19.44.04.png

2.Snippet登録

{
    "Print to console": {
        "prefix": "test",
        "body": [
            "console.log('${1|おはよう,こんにちは,こんばんは|}');",
            "$0"
        ],
        "description": "Log output to console"
    }
}

Snippet利用

1.prefixに設定した文字を入力

スクリーンショット 2020-12-04 19.48.30.png

スクリーンショット 2020-12-04 19.48.43.png

スクリーンショット 2020-12-04 19.48.58.png

Snippet削除

Macの場合

cd ~/Library/Application\ Support/Code/User/snippets/
rm javascript.json
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

enebular x 導電布で空席を検知する

この記事は2020年12月1日に開催された 【オンライン】enebular developer meetup で発表した内容をベースにしています。

はじめに

UXデザイナーの どたてつや です。

普段はUI設計やUX開発などの仕事の傍ら、趣味でプロトタイプなどを作ったりしています。
先日、Eテキスタイルを使ったインプットモジュール「nüno」をnanbwrks さんと作りました。
nünoの最新バージョンはver.2ですが、
今回は余ってるnüno ver.1を使用しての空席通知システムを作ってみました。

nünoについては2018年12月の記事「enebularで布センサーからLINEに通知できるようにしたよ」も参考にしてください!!

できるもの

最終的にできるものの動画はこちらです

わかりづらいですが、一番右下の座布団が黄色になっていますね。(なっています!)
こちらについてご説明します。

全体の構成

構成として空席データ送信はnünoからM5Stack経由でWifiを使用してenebularに接続、
enebularからFirebaseへ検知情報を送っています。

そして空席情報を表示するためのデータ取得はPC(スマホ)からenebularにリクエストを送信、
enbularがFirebaseからデータ尾を取得し、PCへ空席情報を表示する、という流れです。

ScreenShot 2020-12-04 19.22.04.png

ハードウェア

プロトタイプ感まるだしてですが、ハードウェアはこちら。
IMG_0679.JPG

無印良品の座布団にnünoを接続し、M5Stack Core2を使用しています。
M5Stack Core2とnünoの通信はI2Cを使用しています。

Arduino

まずはArduinoから
また、 nünoではMTCH6102という静電タッチセンサをつかっているので、
プログラムと同じ階層にこちらから借りてきた
- MTCH6102.h
- MTCH6102.cpp
を利用させてもらっています。

#include <M5Core2.h>
#include <Arduino.h>
#include <Wire.h>
#include "MTCH6102.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>

#define ADDR 0x25
#define ScreenWidth 320
#define ScreenHeight 240

MTCH6102 mtch = MTCH6102();

const int len = 8;//感知ポイント数
int nuno_mode = 2;
int cnt; //ループ用変数
uint32_t chipId = 0;


const bool ONLINE = true;//オンラインモード
const char* WIFI_SSID ="SSID";
const char* WIFI_PASSWORD = "PASSWORD";
const char* POST_URL = "URL";

WiFiClient client;

void setup() {
  delay(1000);

  // Initialize the M5Stack object
  M5.begin();
  //M5.Power.begin();
  M5.Lcd.fillScreen(TFT_BLACK);
  Serial.begin(115200);
  //mtch6102
  mtch.begin(ADDR);
  delay(100);
  mtch.writeRegister(MTCH6102_NUMBEROFXCHANNELS, 0x08);
  mtch.writeRegister(MTCH6102_NUMBEROFYCHANNELS, 0x03);//最低3点必要なため
  mtch.writeRegister(MTCH6102_MODE, MTCH6102_MODE_FULL);
  mtch.writeRegister(MTCH6102_HORIZONTALSWIPEDISTANCE, 0x04);
  mtch.writeRegister(MTCH6102_MINSWIPEVELOCITY, 0x02);
  mtch.writeRegister(MTCH6102_TAPDISTANCE, 0x02);
  mtch.writeRegister(MTCH6102_SWIPEHOLDBOUNDARY, 0x04);

  mtch.writeRegister(MTCH6102_BASEPOSFILTER, 0x00);
  mtch.writeRegister(MTCH6102_BASENEGAFILTER, 0x00);

  mtch.writeRegister(MTCH6102_CMD, 0x20);
  delay(500);

  //chipID
  for(int i=0; i<17; i=i+8) {
    chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
  }
  Serial.print("chip Id:");
  Serial.println(chipId);

  //WIFI
  if (ONLINE) {
    WiFi.mode(WIFI_STA);
    WiFi.disconnect(true);
    delay(1000);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    Serial.println("connecting");
    M5.Lcd.print("========== WIFI connecting ==========\n\n");

    while (WiFi.status() != WL_CONNECTED) {
      Serial.print(".");
      delay(500);
      Serial.print(WiFi.status());
      Serial.print(",");
    }
    Serial.println();
    Serial.print("connected: ");
    Serial.println(WiFi.localIP());
    M5.Lcd.print("========== WIFI connected ==========\n\n");
  }
}


const int log_max = 10;
JSONVar move_log;
int move_current = 0;

//動作ログ送信
void SendLog() {
  if (!ONLINE) return;
  if (WiFi.status() != WL_CONNECTED) return;

  HTTPClient http;
  Serial.print("[HTTP] begin...\n");
  http.begin(POST_URL); //HTTP
  http.addHeader("Content-Type", "application/json");
  String jsonString = JSON.stringify(move_log);
  int httpCode = http.POST(jsonString);
  http.end();
}


void loop() {

    M5.update();
    M5.lcd.clear();
    M5.Lcd.setCursor(0, 70);

    byte data;
    int sensVals[len];

    for (int i = 0; i < len; i++) {
      data = mtch.readRegister(MTCH6102_SENSORVALUE_RX0 + i);
      sensVals[i] = data;
      M5.Lcd.fillRect(30 + (i * 35), ScreenHeight - 20, 30, 10, TFT_BLACK);
      M5.Lcd.setCursor(30 + (i * 35), ScreenHeight - 20);
      M5.Lcd.print(data);
    }

    Serial.println(String(chipId));
    for (int j = 0; j < len; j++) {
      move_log["chipId"] = String(chipId);
      move_log["value"][j] = sensVals[j];
      Serial.print(sensVals[j]);
      Serial.print(",");
    }
    Serial.println();
    //ログ投げる


    M5.Lcd.setCursor(0, 70);
    //背景ライン
    for (int i = 0; i < len; i++) {
      M5.Lcd.drawLine((i + 1) * 35, ScreenHeight - 40, (i + 1) * 35, 0, 0x0000cc);
    }
    for (int i = 1; i < 11; i++) {
      M5.Lcd.drawLine(0, i * 20, ScreenWidth, i * 20, 0x0000cc);
    }
    //グラフ線の描画
    for (int i = 0; i < len + 1; i++) {
      float prev = 0;
      float current = 0;
      if (i == 0) {
        prev = 0;
      }else{
        prev = sensVals[i - 1];
      }
      if (i == len) {
        current = 0;
      } else {
        current = sensVals[i];
      }
      M5.Lcd.drawLine(i * 35, 200 - (prev / 255) * 200, ((i + 1) * 35), 200 - (current / 255) * 200, TFT_WHITE);
    }

  SendLog();
  delay(5000);//10秒に1回投げる
}

将来的に複数のデバイスが稼働することを想定しているので

chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;

ここでチップIDを取得して、どのデバイスから送信された値かを判別できるようにしています。

Firebase

Firebase側ではRealtimeデータベースを使用しています。
Realtimeデータベースとしてはこちらの記事を参考にさせてもらい、設定しました。

enebular

enebularnのフローはこちらです。
ScreenShot 2020-12-01 17.12.25.png

上が送信用、下が取得ようのフローです。
非常にシンプルですがFirebaseノードが便利ですぐにFirebaseとの連携が実現できました。

1点つまづきポイントとして、データ取得時にCORSのエラーが表示されたので、
http responseノードにCORSのワイルドカードを設定して事なきを得ました。

ScreenShot 2020-12-01 17.16.38.png

javascript

今回はお手軽実装なのでjQueryを使用しています。
nünoのデータは0~255の静電容量値が8点返ってくるので、
その8点のなかで1つでも150以上の値がある場合は着席状態として seated クラスを付与して状態を変更しています。

$(document).ready(function(){

    setInterval(function(){     
        $.ajax({
            url: 'https://nuno-seat2.herokuapp.com/get-data',
            success: function(result) {
                refreshChair(result);
            }
        })
    }, 1000);

    function refreshChair(data){
        $.each(data,function(index, value){
            if(index == "value"){
                console.log(value);
                var m = Math.max.apply(null, value);
                if(m > 150){
                     $('#seat_8481756').addClass('seated');  
                 }else{
                     $('#seat_8481756').removeClass('seated');   
                 }
             }

        });
    }

});

完成!

これで空席検知ができるようになりました。
かんたんなシステムであれば1日くらいで作れるので非常にいいですね!

皆様、よいenebularライフを〜!

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

素数一覧を求めるワンライナー各言語まとめ

お遊び記事です.【追記】お遊びのつもりが,とても秀逸な別解がコメント欄に集まりましたので,そちらもぜひ御参照下さい.とりあえず,J言語すげえ.

アルゴリズムと実装

主に関数型リスト処理を用いて,$x$を$2〜x-1$の各整数で割った余りのリストに0が含まれていなければ$x$を素数と判定しています.このため,任意範囲の整数リスト生成(range等),リスト各要素への関数適用(map等),リストに特定の値が含まれているかの判定(include等),条件を満たした要素のみをリストから取り出す処理(filter等)を行う関数があると,より短いワンライナーとなります.

各言語での記述例

$1<n<100$の$n$について素数判定を行った結果を表示しています.どうしても長くなりがちなので,字句解析が可能な程度に空白等を詰めています.

Ruby
>> ->n{(2...n).reject{|x|(2...x).map{|z|x%z}.include?(0)}}[100]
=> [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
Haskell
> (\n->filter(\x->not$elem 0$(\x->map(\z->rem x z)[2..x-1])x)[2..n])100
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97]
Python
>>> (lambda n:[x for x in range(2,n)if not 0 in map(lambda z:x%z,range(2,x))])(100)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
JavaScript
> (n=>(r=>r(n).filter(x=>!(i=>r(i).map(z=>i%z).includes(0))(x)))(n=>[...Array(n-2)].map((v,k)=>k+2)))(100)
[ 2,
  3,
//(途中結果は省略)
  97 ]
Scheme
> ((lambda(n)(filter(lambda(x)(not(member 0(map(lambda(z)(modulo x z))(cddr(iota x))))))(cddr(iota n))))100)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)

備考

記事に関する補足

  • JavaScriptに範囲生成関数が標準で用意されていないのがちょっと意外.最近のJavaScriptでは[...Array(n).keys()].slice(s)あたりが定番の模様(コメントより).

更新履歴

  • 2020-12-04:冒頭にコメント欄参照の旨追記
  • 2020-12-04:初版公開(Ruby,Haskell,Python,JavaScript,Scheme)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

?を押したら●●が見える実装方法公開

パスワードマスキングとは...

Image from Gyazo

コレですね! 言葉では説明しません。

導入することで解決することができる課題

  1. 入力ミスによる認証失敗が防げる

  2. 誤った情報を登録した場合には、ログインができなくなってしまう場合がある

  3. パスワードを覗かれる可能性がある→セキュリティの観点で良くない

個人的見解

・ユーザーにストレスを与えない、離脱率を減らす観点で、導入するに越した事はないのではないのかという見解です。導入の為への工数も少ない為、コスパの良いユーザビリティ向上手段だと感じました。

有名企業のWEBサイトのパスワードマスキング導入有無の調査

・有名な企業のwebサイトで導入されているかを確認してみました。ザッと思いついたWEBサイトを4件調べてみたので、参考程度に実際にサイトを確認してもいいかもしれませんね。

?導入調査 (アカウント登録画面について, 2020/12/4時点)
企業名称 導入の有無
Amazon ❌ (無)
Instagram ? (有)
楽天 ❌ (無)
メルカリ ? (有)

→4件だけの調査で一概にいえないですが、導入していない2社はどちらともECサイトなので、絶対にパスワードをチラ見させないようにしているように感じました。

導入方法 (Ruby on Rails)

※UIについては考慮しない実装になります

STEP1 ▶ type属性のinput要素を設置する

= f.password_field :password, class: "password-input"

まずはtype属性のinput要素を作成してください。

classを指定して、JSで取得できるようにします。

STEP2 ▶ 目などを配置する

<div class="fas fa-eye"></div>

今回はfont-awesomeを使用しました。

STEP3 ▶ Javascript実装

function PassMask() {
  const PasswordInput = document.querySelector('.password-input');
  const PasswordEye = document.querySelector('.password-eye');

  PasswordEye.addEventListener("click", () => {
    if(PasswordInput.type === "password"){
      PasswordInput.type = "text";
      PasswordEye.classList.remove("fa-eye");
      PasswordEye.classList.add("fa-eye-slash");
    }else{
      PasswordInput.type = "password";
      PasswordEye.classList.remove("fa-eye-slash"); 
      PasswordEye.classList.add("fa-eye");
    }
  });
}
window.addEventListener('load',PassMask);

ぜひ、実装してみてください。

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

【kintone】JavaScriptでレコード一覧をCSVファイルで出力してみよう

CSVをJavaScriptでちょっぴり柔軟に出力したい!!!

と思うことがありますよね。

今日はJavaScriptを出力する機能を作ってみましょう。
ちょっと長め&重めです?

アプリの準備

kintone アプリストアから、顧客リストを「サンプルデータを含める」にチェックを付けて作成します。
image.png

JavaScript

JavaScript全体のコードはこちら
kintoneのレコード一覧CSVファイルダウンロードするスクリプト

ボタンをつける

こちらの記事を参考にボタンを設置します。
【kintone】アプリの「スペース」フィールドにボタンを設置する

ボタンの機能の中身は↓のようにしておいてください。

//ボタンをクリックしたときの動作
btn.onclick=()=>{
    //CSVダウンロード
    dlCsv();
}

encoding.js

Cybozu CDN
↓こちらを「JavaScript / CSSでカスタマイズ」の設定に追加しておきます。
https://js.cybozu.com/encodingjs/1.0.30/encoding.min.js

CSVを出力する機能を書く

↓こちらを参考にkintoneのレコードをCSVファイルに書き込むようなコードにしてみました。
解説は後述します。

参考:
ファイルをダウンロード保存する方法
ブラウザからjsの配列をcsvとしてダウンロードする。
javascriptで文字コード変換

// 1.CSVファイル生成用
const HEADER =['会社名','部署名','担当者名','郵便番号','TEL'];//CSVに出力したいフィールドのフィールドコード

// CSVをダウンロード
const dlCsv = async ()=>{
    //フィールド名からCSVの文字列データを作る
    const setHeaderData = () => {
        let headerData = '';
        HEADER.forEach(h => {
            headerData += h + ',';
        });
        headerData += '\r\n';
        return headerData;
    };  

    //レコードからCSVの文字列データを作る
    const recordToCsvData = records => {
        let rowData = '';
        records.forEach(r => {
            HEADER.forEach(h => {
                rowData += r[h].value + ',';
            });
            rowData += '\r\n';
        });
        return rowData;
    }; 

    //URLエンコード
    const createDataUriFromString = str => {
        // 文字列を配列に変換
        const array = str.split('').map(s => s.charCodeAt());
        // エンコード
        const sjis_array = Encoding.convert(array, 'SJIS', 'UNICODE');
        const uInt8List = new Uint8Array(sjis_array);
        return uInt8List;
    };  

    //2. CSVにしたいレコードを取得する
    const param_get = {
        app:kintone.app.getId(),
        //query:'出力フラグ != 1', // 絞り込みたいときはクエリを書く"出力フラグ = 0" など
    };
    const obj_get = await kintone.api('/k/v1/records','GET',param_get);
    const targetRecords =obj_get.records;

    //3. レコード1件以上だったらCSV出力
    if (targetRecords.length > 0) {

        //4. CSVにするテキストデータを作成
        const str = createDataUriFromString( setHeaderData() + recordToCsvData(targetRecords));

        //5. CSVファイル作成
        const blob = new Blob([str], {type:"text\/csv"});
        const url = URL.createObjectURL(blob);

        //6. ダウンロード処理
        const a = document.createElement("a");
        document.body.appendChild(a);
        a.download = "test.csv";//ここすきなファイル名に
        a.href = url;
        a.click();
        a.remove();
        URL.revokeObjectURL(url);

        // // 出力フラグをONにするようなコードはこのへんに
        // let param_records = [~,~, ~,~];
        // const param_upd = {
        //     app:kintone.app.getId(),
        //     records:param_records,
        // };
        // const obj_put = await kintone.api('/k/v1/records','PUT',param_upd);
    }
}

CSVダウンロードの流れ

ざっくり解説ですが、流れとしてはこの順で実行します。

  1. 設定の定数
  2. CSVにしたいレコードを取得する
  3. レコード1件以上だったらCSV出力するIF文(レコード無ければ終了)
  4. CSVにするテキストデータを作成
  5. CSVファイル作成
  6. ダウンロード処理

一つずつ見ていきます。

1.設定の定数

HEADER に、出力したいフィールドのフィールドコードを配列で書いておきます。

// CSVファイル生成用
const HEADER =['会社名','部署名','担当者名','郵便番号','TEL'];//CSVに出力したいフィールドのフィールドコード

2.CSVにしたいレコードを取得する

出力したいCSVの元になるレコードを取りに行きます。
ここで、アプリに「出力フラグ」などを準備しておくと、queryに未出力のレコードを指定できたりします。
たとえばquery:'出力フラグ != 1',など・・・。

targetRecords = obj_get.records;の部分でレコードの配列が代入されます。

参考:レコードの一括取得(クエリで条件を指定)

    //CSVにしたいレコードを取得する(1)
    const param_get = {
        app:kintone.app.getId(),
        //query:'出力フラグ != 1', // 絞り込みたいときはクエリを書く
    };
    const obj_get = await kintone.api('/k/v1/records','GET',param_get);
    const targetRecords = obj_get.records;

3.レコード1件以上だったらCSV出力

2.で取得したレコードが1件以上だったら出力しましょう

    //レコード1件以上だったらCSV出力(2)
    if (targetRecords.length > 0) { ~~

4.CSVにするテキストデータを作成

上の方でたくさん定義してあるCSV作成のための関数を使っています。
関数たちはこちらを参考に書いています。
参考:
ブラウザからjsの配列をcsvとしてダウンロードする。
javascriptで文字コード変換

    //CSVにするテキストデータを作成
    const str = createDataUriFromString( setHeaderData() + recordToCsvData(targetRecords));

5.CSVファイル作成

4.で作ったstrをCSV「ファイル」にします。
5.6.は↓のサイトを参考にしています。
参考:ファイルをダウンロード保存する方法

    //CSVファイル作成(4)
    const blob = new Blob([str], {type:"text\/csv"});
    const url = URL.createObjectURL(blob);

6.ファイルのダウンロード処理

ダウンロードするときは、aタグを隠してクリックして削除するという流れなんですね。勉強になりました。

    //ダウンロード処理(5)
    const a = document.createElement("a");
    document.body.appendChild(a);
    a.download = "test.csv";//ここすきなファイル名に
    a.href = url;
    a.click();
    a.remove();
    URL.revokeObjectURL(url);

まとめ

今回はとにかくダウンロードしてみる。という仕組みを紹介しました。
工夫を加えると、「CSVファイルをダウンロードしたらフラグを立てる」というような仕組みも作れます。
コメントアウト部分も参考にされると良いかもしれません!?

是非挑戦してみてくださいね^0^

以上です。

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

react-konvaでお絵描き

canvasを組み込んだwebサービスを作る機会があり、その際に利用したreact-konvaが使いやすかったので紹介です。

今回はreact-konvaとhookを利用して簡単なお絵描き機能を作っていきます。
以下のような感じです。
https://codesandbox.io/s/elastic-agnesi-pn76b
test.gif

react-konvaとは

canvasのjsフレームワークkonva.jsを名前の通りreactで利用できるものです。
konva.jsのオブジェクトクラスがコンポーネントとして提供されています。

konva.js

https://konvajs.org/docs/overview.html

react-konvaはStageというコンポーネントを土台に、その上にいろいろな要素を載せて実装を進めていきます。
例えば、LayerコンポーネントをStage上に複数載せることで、絵を描くときにあると嬉しいレイヤー機能を簡単に実装することができます。
イメージとして以下のようなコンポーネント階層を作っていきます(公式より抜粋)
スクリーンショット 2020-12-04 13.14.32.png

hookを使った実装ではstateにオブジェクト要素を詰め込んでいき、その内容をcanvas上に描画していきます。

canvas上に絵を描くという目的を達成するために必要な処理は大まかに以下です。
①mouseDownなどのeventに反応してstateに線の描画位置や色設定などを詰め込んでいく。
②Layerコンポーネント内でstateの値を走査し、オブジェクトを描画していく。

canvasの描画内容をstateで管理できるため、すごく楽に実装を進められます。

実装イメージ(いろいろ省いて抜粋)

sample.js
const App = () => {
  const [lines, setLines] = React.useState([]);
  const handleMouseDown = (e) => {
    const pos = e.target.getStage().getPointerPosition();
      // mouseDownなどのeventに反応してstateに値(線の描画位置や色設定など)を詰め込んでいきます。
      setLines([...lines,{points: [pos.x, pos.y], color, size}
    ]);
  };

  return (
    <>
       <Stage
         onMouseDown={handleMouseDown}
       >
          <Layer>
            {/* stateを走査して詰め込んだ値を描画していきます。 */}
            {lines.map((line, i) => (
              <Line
                key={i}
                points={line.points}
                stroke={line.color}
                strokeWidth={line.size}
                tension={0.5}
                lineCap="round"
              />
            ))}
          </Layer>
        </Stage>
      </>
  );
};
export default App;

最後に

業務はサーバーAPI構築がメインのためReactの勉強がてらと思いreact-konvaを選んだのですが、webは技術進歩が目覚ましく、すぐに置いていかれてしまいますね、、

(記事の内容と直接関係ないのですが)特に今では当たり前のように使われているオンラインエディター(今回はCodeSandbox)が使いやすく驚きました。

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

C4DとThree.jsで製品の3D表示ページを開発する

完成品の表示ページ:

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

C4D

1. C4Dモデリング

C4Dを使って製品のモデルを作成する。

c4d.png

ご注意:

Discの使用を避けること、ブラウザで解析できないため。

各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。

2. *.objファイルをエクスポートする

file -> Export -> Wavefront OBJ(*.obj)

export.png

ソース実装

開発環境:Node.js/Webpack4/React16/Three.js

ソース:https://github.com/capricorncd/blog/tree/master/demos/three

1. Install

# "three": "^0.120.1"
npm i -S three
# or
yarn add three

2. ソース

src/components/ClockObj/core.js

import {
  AmbientLight,
  DirectionalLight, PerspectiveCamera,
  Scene, WebGLRenderer
} from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

let scene, renderer

/**
 * load resource 
 * @returns {Promise<unknown>}
 */
 function loadResource() {
   return new Promise((resolve, reject) => {
     const objLoader = new OBJLoader()
     const mtlLoader = new MTLLoader()
     // テクスチャファイルをロードする
     mtlLoader.load('static/clock.mtl', mtl => {
       // オブジェクトをロードする前に、まずテクスチャデータを設定します
       objLoader.setMaterials(mtl)
       // オブジェクトをロードする
       objLoader.load('static/clock.obj', res => {
         resolve(res)
       }, undefined, reject)
     }, undefined, reject)
  })
}

/**
 * 初期化
 */
function _init(el, obj) {
  // コンテナサイズを取得する
  // windowの場合、window.innerWidthとwindow.innerHeightで取得する
  const width = el.offsetWidth
  const height = el.offsetHeight

  // シーンを作成する
  scene = new Scene()
  // オブジェクトをシーンに追加する
  scene.add(obj)

  // 周囲光を作成する
  const ambientLight = new AmbientLight(0x666666)
  ambientLight.position.set(100, -100, -200)
  scene.add(ambientLight)

  // 指向性ライトを作成する
  const light = new DirectionalLight(0xcccccc, 1)
  light.position.set(2000, 1000, 1000)
  scene.add(light)

  // カメラを作成する
  const camera = new PerspectiveCamera(45, width / height, 1, 80000)
  camera.position.set(-150, -50, 300)

  // レンダラーを作成する
  renderer = new WebGLRenderer({
    antialias: true
  })
  // レンダリング領域のサイズを設定する
  renderer.setSize(width, height)
  // 背景色を設定する
  renderer.setClearColor(0x000000, 1)
  el.appendChild(renderer.domElement)

  const orbitControls = new OrbitControls(camera, el)
  orbitControls.addEventListener('change', render)

  function render() {
    renderer.render(scene, camera)
  }
  render()
}

/**
 * init
 */
export function init(el) {
  loadResource().then(res => {
    _init(el, res)
  }).catch(console.error)
}

/**
 * destroy
 */
export function destroy() {
  if (!scene || !renderer) return
  scene.remove()
  renderer.dispose()
  scene = null
  renderer = null
}

src/components/ClockObj/index.jsx

import React, { useEffect, useRef } from 'react'
import { destroy, init } from './core'

function ClockObjDemo() {
  const elRef = useRef()
  useEffect(() => {
    init(elRef.current)
    return () => {
      destroy()
    }
  }, [])
  return <main className="font-size-zero" ref={elRef} />
}

export default ClockObjDemo

完成品URL

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

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

C4DとThree.jsで製品の3D表示ページの開発

完成品の表示ページ:

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

C4D

1. C4Dモデリング

C4Dを使って製品のモデルを作成する。

c4d.png

ご注意:

Discの使用を避けること、ブラウザで解析できないため。

各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。

2. *.objファイルをエクスポートする

file -> Export -> Wavefront OBJ(*.obj)

export.png

ソース実装

開発環境:Node.js/Webpack4/React16/Three.js

ソース:https://github.com/capricorncd/blog/tree/master/demos/three

1. Install

# "three": "^0.120.1"
npm i -S three
# or
yarn add three

2. ソース

src/components/ClockObj/core.js

import {
  AmbientLight,
  DirectionalLight, PerspectiveCamera,
  Scene, WebGLRenderer
} from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

let scene, renderer

/**
 * load resource 
 * @returns {Promise<unknown>}
 */
 function loadResource() {
   return new Promise((resolve, reject) => {
     const objLoader = new OBJLoader()
     const mtlLoader = new MTLLoader()
     // テクスチャファイルをロードする
     mtlLoader.load('static/clock.mtl', mtl => {
       // オブジェクトをロードする前に、まずテクスチャデータを設定します
       objLoader.setMaterials(mtl)
       // オブジェクトをロードする
       objLoader.load('static/clock.obj', res => {
         resolve(res)
       }, undefined, reject)
     }, undefined, reject)
  })
}

/**
 * 初期化
 */
function _init(el, obj) {
  // コンテナサイズを取得する
  // windowの場合、window.innerWidthとwindow.innerHeightで取得する
  const width = el.offsetWidth
  const height = el.offsetHeight

  // シーンを作成する
  scene = new Scene()
  // オブジェクトをシーンに追加する
  scene.add(obj)

  // 周囲光を作成する
  const ambientLight = new AmbientLight(0x666666)
  ambientLight.position.set(100, -100, -200)
  scene.add(ambientLight)

  // 指向性ライトを作成する
  const light = new DirectionalLight(0xcccccc, 1)
  light.position.set(2000, 1000, 1000)
  scene.add(light)

  // カメラを作成する
  const camera = new PerspectiveCamera(45, width / height, 1, 80000)
  camera.position.set(-150, -50, 300)

  // レンダラーを作成する
  renderer = new WebGLRenderer({
    antialias: true
  })
  // レンダリング領域のサイズを設定する
  renderer.setSize(width, height)
  // 背景色を設定する
  renderer.setClearColor(0x000000, 1)
  el.appendChild(renderer.domElement)

  const orbitControls = new OrbitControls(camera, el)
  orbitControls.addEventListener('change', render)

  function render() {
    renderer.render(scene, camera)
  }
  render()
}

/**
 * init
 */
export function init(el) {
  loadResource().then(res => {
    _init(el, res)
  }).catch(console.error)
}

/**
 * destroy
 */
export function destroy() {
  if (!scene || !renderer) return
  scene.remove()
  renderer.dispose()
  scene = null
  renderer = null
}

src/components/ClockObj/index.jsx

import React, { useEffect, useRef } from 'react'
import { destroy, init } from './core'

function ClockObjDemo() {
  const elRef = useRef()
  useEffect(() => {
    init(elRef.current)
    return () => {
      destroy()
    }
  }, [])
  return <main className="font-size-zero" ref={elRef} />
}

export default ClockObjDemo

完成品URL

https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj

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

【HTML5】ガラスが割れる感じで画像を切り替えたい【JavaScript】

はじめに

ふと、画像の切り替えを「ガラスが割れる風」にしたいと思いました。
AGDRec.gif

本ライブラリの使い方

imgタグのサイズは指定してください。あんまり大きくないほうが良いと思います。
borderとかは対応していません。
ES6で書いているので,IEはたぶん動かない。
Chromeで動作確認をしました。
物理現象は割と無視しています。

/**
 * 画像をガラスが割れるように差し替える
 * @module destructImage
 * @param {object} argOption - 設定内容
 * @param {HTMLImageElement} argOption.element - 画像を変更する<img>要素(省略不可)
 * @param {string} argOption.src - 変更する画像のパス(省略不可)
 * @param {number} argOption.vectorsCount -  画像の中央から伸びる線の数(整数)。8以上にしてください。8~20ぐらいが推奨。(省略時の値は14)
 * @param {number} argOption.velocityRate - 速度の比。大きくすると、初期速度が上がる。0.5~5.0ぐらいが推奨。(省略時の値は1)
 * @param {number} argOption.accelerationRate - 加速度の比。大きくすると重力加速が大きくなる。0.5~5.0ぐらいが推奨。(省略時の値は1)
 * @param {number} argOption.zIndex - アニメーション中に表示される<div>のzIndex。(省略時の値は0)
 * @param {function} argOption.finished - 処理が完了したときに呼ばれるコールバック関数。仮引数は無い。 (省略可能)
 * @return {number} - 0: 成功時, -1: 失敗時(処理中に呼ばれた場合)
 */

elementに<img>を、srcに変更したい画像のパスを指定して、destructImageを呼んでください。
他のパラメータで動きを指定できます。(詳細はコメント参照)

const myImage = document.getElementById('my-image')
destructImage({
    element: myImage,
    src: myImage.src.indexOf('img/src.jpg') >= 0 ? 'img/dst.jpg' : 'img/src.jpg',
    vectorsCount: 12,
    velocityRate: 1.2,          
    accelerationRate: 0.7,
    zIndex: 0,
    finished: () => { console.log('finished'); }
});

ライブラリのソース

/**
 * 画像をガラスが割れるように差し替える
 * @module destructImage
 * @param {object} argOption - 設定内容
 * @param {HTMLImageElement} argOption.element - 画像を変更する<img>要素(省略不可)
 * @param {string} argOption.src - 変更する画像のパス(省略不可)
 * @param {number} argOption.vectorsCount -  画像の中央から伸びる線の数(整数)。8以上にしてください。8~20ぐらいが推奨。(省略時の値は14)
 * @param {number} argOption.velocityRate - 速度の比。大きくすると、初期速度が上がる。0.5~5.0ぐらいが推奨。(省略時の値は1)
 * @param {number} argOption.accelerationRate - 加速度の比。大きくすると重力加速が大きくなる。0.5~5.0ぐらいが推奨。(省略時の値は1)
 * @param {number} argOption.zIndex - アニメーション中に表示される<div>のzIndex。(省略時の値は0)
 * @param {function} argOption.finished - 処理が完了したときに呼ばれるコールバック関数。仮引数は無い。 (省略可能)
 * @return {number} - 0: 成功時, -1: 失敗時(処理中に呼ばれた場合)
 */
function destructImage(argOption) {
    if(!argOption || !argOption.element) {
        throw 'argument is invalid.'
    }
    // 対象の<img>
    const img = argOption.element;
    if(typeof img.destructImageFlag !== 'undefined' && img.destructImageFlag) {
        // 処理中は何もしない
        return -1;
    }

    // デフォルト値を反映
    const option = Object.assign({
        vectorsCount: 14,
        velocityRate: 1,
        accelerationRate: 1,
        zIndex: 0,
        finished: null
    }, argOption);

    // 画像の矩形を計算する
    const rect = img.getBoundingClientRect();

    // 処理中のフラグを立てる
    img.destructImageFlag = true;
    const imgSrc = img.src; // 変更前のsrcを退避
    //img.src = option.src;

    // 画像のサイズを求める
    const size = {
        width: rect.width,
        height: rect.height
    };

    // 枠を作成する
    const frameElm = document.createElement('div');
    frameElm.style.position = `absolute`;
    frameElm.style.zIndex = `${option.zIndex}`;
    frameElm.style.left = `${rect.left}px`;
    frameElm.style.top = `${rect.top}px`;
    frameElm.style.width = `${rect.width}px`;
    frameElm.style.height = `${rect.height}px`;
    frameElm.style.clipPath = `polygon(0px 0px, ${rect.width}px 0px, ${rect.width}px ${rect.height}px, 0px ${rect.height}px)`;
    frameElm.style.overflow = `hidden`;

    // 枠をbodyに追加
    document.body.appendChild(frameElm);

    // ポリゴングループを作成する
    const groups = getPolygonGroups(option, size);

    // ガラスの破片を作成する
    groups.forEach((group, i) => {
        group.forEach((polygon, j) => {
            // <img>を作成
            const imgElm = document.createElement('img');
            imgElm.src = imgSrc;
            imgElm.style.position = `absolute`;
            imgElm.style.left = `0px`;
            imgElm.style.width =`${rect.width}px`;
            imgElm.style.height = `${rect.height}px`;
            imgElm.style.clipPath = getClipPath(groups[i][j]);
            imgElm.style.transformOrigin = `${polygon.quantity.g.x}px ${polygon.quantity.g.y}px`;
            // 枠に<img>を追加
            frameElm.appendChild(imgElm);

            polygon.imgElm = imgElm;
        });
    });

    // option.srcを先読みする
    const tmpImg = new Image();
    const tmpTimeoutId = setTimeout(() => {
        throw 'image load timeout error';
    }, 10000);
    tmpImg.onload = () => {
        clearTimeout(tmpTimeoutId);
        img.src = option.src;
        requestAnimationFrame(anim); 
    };
    tmpImg.onerror = () => {
        clearTimeout(tmpTimeoutId);
        throw 'image load error';
    };
    tmpImg.src = option.src;    

    let frame = 0;  

    return 0;

    // 毎フレーム呼ばれる関数
    function anim() {
        frame++;
        groups.forEach((group, i) => {
            group.forEach((polygon, j) => {
                const q = polygon.quantity;

                if(frame >= q.late) {
                    // 位置と角度を更新
                    const time = frame - q.late;
                    q.x += q.vx;
                    q.y = q.vy * time + 0.5 * option.accelerationRate * (size.height / 480) * time * time;
                    q.r += q.vr;
                }                   

                // <img>の表示を変更
                polygon.imgElm.style.left = `${q.x}px`;
                polygon.imgElm.style.top = `${q.y}px`;
                polygon.imgElm.style.transform = `rotate3d(${q.xaxis}, ${q.yaxis}, ${q.zaxis}, ${q.r}rad)`;
            });
        });

        if(checkFinished(groups, size)) {
            groups.forEach((group, i) => {
                group.forEach((polygon, j) => {
                    polygon.imgElm.remove();
                });
            });
            frameElm.remove();
            if(option.finished) {
                delete img.destructImageFlag;
                option.finished();
            }

        } else {
            requestAnimationFrame(anim);
        }
    }  

    // 終了判定
    // Y+方向のみで判断する(回転していない状態で判断する)
    function checkFinished(groups, size) {
        return groups.every(group => 
            group.every(polygon => {
                return polygon.every(pos => polygon.quantity.y - pos.y > size.height);
            })
        );
    }

    // 重心、初速度、初期位置からの相対位置、回転軸(保留)、角速度(保留)を求める
    // 割と雑に決める(基本的に中心に近いものが大きく動くようにする)
    function addGroupQuantities(option, size, groups) {
        const c = {
            x: size.width / 2,
            y: size.height / 2,
        };

        const xRate = size.width / 720,
            yRate = size.height / 480;

        groups.forEach((group, i) => {

            group.forEach((polygon, j) => {

                // 重心を求める
                const g = getGravity(polygon);

                // 中心からの重心までの方向ベクトルを求める
                const v = {
                    x: g.x - c.x,
                    y: g.y - c.x
                };

                // ベクトルの長さを求める
                const len = Math.sqrt(v.x * v.x + v.y * v.y);

                // 単位ベクトル化
                v.x /= len;
                v.y /= len;

                // ずれ量を求める(中心からポリゴンの中心に向かってぶれる)
                // これが初速度となる
                let deltaX, deltaY;
                if(i === 0) {// 中心に一番近い => 大きくぶれる
                    deltaX = option.velocityRate * xRate * rand(5, 10) * v.x;
                    deltaY = option.velocityRate * yRate * rand(5, 10) * v.y;
                } else if(i === 1) {// 中心に一番近い => 大きくぶれる
                    deltaX = option.velocityRate * xRate * rand(3, 5) * v.x;
                    deltaY = option.velocityRate * yRate * rand(3, 5) * v.y;
                } else {// 中心に一番近くない => 小さくぶれる
                    deltaX = option.velocityRate * xRate * rand(1, 2) * v.x;
                    deltaY = option.velocityRate * yRate * rand(1, 2) * v.y;
                }

                // 遅延処理(何フレーム後に動き出すか)
                let late;
                if(i === 0) {// 中心に一番近い => 遅延あまりなし
                    late = randInt(1, 5);
                } else if(i === 1) {// 中心に2番目に近い => 遅延ややあり
                    late = randInt(8, 10);
                } else {// 中心に一番近くない => 遅延あり
                    late = randInt(5, 15);
                }

                // 角速度
                let vr;
                if(i === 0) {// 中心に一番近い => 速い
                    vr = rand(5, 15) / 360 * Math.PI * 2;
                } else if(i === 1) {// 中心に2番目に近い => やや遅い
                    vr = rand(2, 3) / 360 * Math.PI * 2;
                } else {// 中心に一番近くない => 遅い
                    vr = rand(1, 2) / 360 * Math.PI * 2;
                }

                // 回転軸を決定する
                let xaxis, yaxis, zaxis;
                xaxis = v.y;
                yaxis = -v.x;
                if(i === 0) {// 中心に一番近い => 遅延あまりなし
                    zaxis = rand(0.1, 0.2);
                } else if(i === 1) {// 中心に2番目に近い => 遅延ややあり
                    zaxis = rand(0.05, 0.1);
                } else {// 中心に一番近くない => 遅延あり
                    zaxis = rand(0.025, 0.05);
                }

                polygon.quantity = {
                    g: g,
                    x: deltaX,
                    y: deltaY,
                    r: 0,
                    xaxis: xaxis,
                    yaxis: yaxis,
                    zaxis: zaxis,
                    vx: deltaX,
                    vy: deltaY,
                    vr: vr,
                    late: late,
                };
            });
        });
    }
    // ポリゴンの重心を求める
    function getGravity(polygon) {
        const g = polygon.reduce((p, c) => { return { x: p.x + c.x, y: p.y + c.y }; }, { x: 0, y: 0 });
        g.x /= polygon.length;
        g.y /= polygon.length;
        return g; 
    }
    // クリップ用のパスを取得
    function getClipPath(polygon) {
        let path = 'polygon('
        polygon.forEach((pos, i) => {
            if(i !== 0) {
                path += ', ';
            }
            path += `${pos.x}px ${pos.y}px`
        });
        path += ')';
        return path;
    }
    // ポリゴングループ作成
    function getPolygonGroups(option, size) {
        // canvasの中心から伸びるベクトル群を求める
        const vectors = getBaseVectors(option.vectorsCount, 2, 7);

        // canvasの中心から伸びる線分の端点群(canvasの境界上の点)を求める
        const ends = getLineEnds(size, vectors);

        // 辺を分割する(中央点とcanvasの端点も含む)
        const edges = getDividesEdges(size, ends, 0.95, [3, 4]);

        // 多角形を取得する(多角形は三角形か凸四角形となる)
        let groups = getPolygonGroupsNoCorners(edges);

        // 4隅の三角形のポリゴン群を取得する
        // (放射状の線がほぼ隅の点と重なる場合もあるのでその時は三角形のポリゴンが作成されないが無視してもいいかもしれない)
        const cornerPolygons = get4CornersPolygons(size, ends);

        // 4隅の三角形のポリゴン群を最後尾のグループに追加する
        groups[groups.length - 1] = groups[groups.length - 1].concat(cornerPolygons);       

        addGroupQuantities(option, size, groups);

        return groups;
    }

    // 4隅の多角形(三角形)を取得する
    function get4CornersPolygons(size, ends) {

        // 各端の座標を求める

        // 上端のX
        const topXPositions = ends.filter(end => end.y === 0).map(end => end.x);
        const topMinX = Math.min(...topXPositions);
        const topMaxX = Math.max(...topXPositions);

        // 下端のX
        const bottomXPositions = ends.filter(end => end.y === size.height).map(end => end.x);
        const bottomMinX = Math.min(...bottomXPositions);
        const bottomMaxX = Math.max(...bottomXPositions);

        // 左端のY
        const leftYPositions = ends.filter(end => end.x === 0).map(end => end.y);
        const leftMinY = Math.min(...leftYPositions);
        const leftMaxY = Math.max(...leftYPositions);

        // 右端のY
        const rightYPositions = ends.filter(end => end.x === size.width).map(end => end.y);
        const rightMinY = Math.min(...rightYPositions);
        const rightMaxY = Math.max(...rightYPositions);

        // 隅毎に多角形を作成する
        const polygons = [];
        let polygon;

        // 左上隅
        if(leftMinY !== Infinity && topMinX !== Infinity) {
            polygon = [];
            polygon.push({ x: 0, y: 0 });
            polygon.push({ x: 0, y: leftMinY });
            polygon.push({ x: topMinX, y: 0 });
            polygons.push(polygon);
        }       

        // 右上隅
        if(topMaxX !== -Infinity && rightMinY !== Infinity) {
            polygon = [];
            polygon.push({ x: size.width, y: 0 });
            polygon.push({ x: topMaxX, y: 0 });
            polygon.push({ x: size.width, y: rightMinY });
            polygons.push(polygon);
        }       

        // 左下隅
        if(bottomMinX !== Infinity && leftMaxY !== -Infinity) {
            polygon = [];
            polygon.push({ x: 0, y: size.height });
            polygon.push({ x: bottomMinX, y: size.height });
            polygon.push({ x: 0, y: leftMaxY });
            polygons.push(polygon);
        }

        // 右下隅
        if(rightMaxY !== -Infinity && bottomMaxX !== -Infinity) {
            polygon = [];
            polygon.push({ x: size.width, y: size.height });
            polygon.push({ x: size.width, y: rightMaxY });
            polygon.push({ x: bottomMaxX, y: size.height });
            polygons.push(polygon);
        }

        return polygons;
    }

    // 線分からポリゴンを作成する
    function getPolygonGroupsNoCorners(edges) {

        const groups = [];

        // edgeの配列の大きさはすべて同じになっている

        for(let j = 0; j < edges[0].length - 1; j += 1) {
            const polygons = [];

            for(let i = 0; i < edges.length; i += 1) {
                const inext = (i + 1) % edges.length,
                    edge = edges[i],
                    nextEdge = edges[inext];

                const polygon = [];
                if(j !== 0) {
                    polygon.push(edge[j]);
                }
                polygon.push(nextEdge[j]);
                polygon.push(nextEdge[j + 1]);
                polygon.push(edge[j + 1]);

                polygons.push(polygon);
            }
            groups.push(polygons);
        }

        return groups;
    }

    // 線分を分割する
    function getDividesEdges(size, ends, lenRate, divides) {
        const center = {
            x: size.width / 2,
            y: size.height / 2
        };

        // 辺を分割する
        const dividedEdges = ends.map(end => {
            const ret = [];
            // 単位ベクトルを求める
            const v = {
                x: end.x - center.x,
                y: end.y - center.y
            };
            const len = Math.sqrt(v.x * v.x + v.y * v.y);
            v.x /= len;
            v.y /= len;
            let dividesSum = divides.reduce((p, c) => p + c, 0);

            for(let i = 0; i <= divides.length; i += 1) {
                if(i === 0) {
                    ret.push({
                        x: center.x,
                        y: center.y
                    });
                } else {
                    const baseRate = divides[i - 1] / dividesSum;
                    const rate = baseRate * rand(0.5, 1.0);
                    const curLen = len * lenRate * rate;
                    const prev = ret[ret.length - 1];
                    ret.push({
                        x: prev.x + v.x * curLen,
                        y: prev.y + v.y * curLen
                    });
                }
            }
            ret.push({
                x: end.x,
                y: end.y
            });
            return ret;
        });

        return dividedEdges;
    }
    // 中央からの放射状のベクトルから線分の端点を求める
    function getLineEnds(size, vectors) {

        const center = {
            x: size.width / 2,
            y: size.height / 2
        };

        const cSlope = size.height / size.width; 

        const ret = vectors.map(v => {

            // ベクトルの傾きを求める
            let slope;
            if(Math.abs(v.x) > 0.0001) {// 0徐算を防ぐ
                slope = v.y / v.x;
            } else {
                if(v.y > 0) {
                    slope = 100000;
                } else {
                    slope = 100000;
                }
            }

            let ret = { x: 0, y: 0 };

            if(v.x >= 0 && Math.abs(slope) < cSlope) {
                ret = {
                    x: size.width / 2,
                    y: slope * size.width / 2
                };
            } else if(v.x < 0 && Math.abs(slope) < cSlope) {
                ret = {
                    x: -size.width / 2,
                    y: slope * (-size.width / 2)
                };
            } else if(v.y >= 0) {
                ret = {
                    x: (1 / slope) * size.height / 2,
                    y: size.height / 2
                };
            } else {
                ret = {
                    x: (1 / slope) * (-size.height / 2),
                    y: -size.height / 2
                };
            }

            // はみ出している場合は修正する
            if(ret.x <= -size.width / 2) {
                ret.x = -size.width / 2;
            } else if(ret.x >= size.width / 2) {
                ret.x = size.width / 2;
            }

            if(ret.y <= -size.height / 2) {
                ret.y = -size.height / 2;
            } else if(ret.y >= size.height / 2) {
                ret.y = size.height / 2;
            }

            ret.x += center.x;
            ret.y += center.y;

            return ret;
        });

        return ret;
    }
    // 中央からの放射状のベクトルを求める
    function getBaseVectors(divides, base, delta) {
        // 中心から放射状に分割する
        let randVals = [];

        for(let i = 0; i < divides; i += 1) {
            randVals.push(rand(base, base + delta));
        }

        const sum = randVals.reduce((p, c) => c + p, 0);

        // radに変換
        randVals = randVals.map(v => Math.PI * 2 * v / sum);

        const ret = [];

        for(let i = 0, angle = rand(0, Math.PI * 2); i < divides; i += 1) {
            if(i !== 0) {
                angle += randVals[i - 1];
            } 
            ret.push({
                x: Math.cos(angle),
                y: Math.sin(angle)
            });
        }

        return ret;
    }

    // 乱数作成
    function rand(min, max) {
        if(!min && !max) {
            return Math.random();
        } else {
            return min + Math.random() * (max - min);
        }       
    }

    // 整数の乱数作成
    function randInt(min, max) {
        return Math.floor( Math.random() * (max + 1 - min) ) + min ;
    }
}

最後に

多分webGLで3dでやったほうが良いと思う。(three.jsとかつかって)
ご自由にお使いください。
バグがあったら直して使ってください。

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

ApolloServerでGraphQLを体験する

はじめに

この記事は2020年のRevCommアドベントカレンダー5日目の記事です。4日目はmiyさんの「AGI(Asterisk Gateway Interface)を使って内線電話からPythonスクリプトを実行する」でした。

こんにちは。RevCommでエンジニアをしている酒井です。

業務ではWebアプリケーションのフロントエンドバックエンド両方担当しており、よく触るのはnodejs、たまにpythonといった感じです。
業務でGraphQLを扱っており「便利だなーすごいなー他の人にも触って欲しいなー」と思ったのですが、とりあえず内容読んでコピペして動くようなチュートリアルが少ないなと感じたので書いてみます。

とりあえずやってみましょう

「とりあえず動くぜすごい!」を目標にしているので詳しい説明はすっ飛ばして進みます。「ここどう動いてるの?」「こういうことできないの?」と疑問が多々出てくると思いますが、そういった物は公式ページを参照してください。

サーバー構築〜query実行まで

この項はほぼ公式のチュートリアルと同じ流れですのでそちらも参照ください。

適当なディレクトリを作成してから

npm init --yes
npm install apollo-server graphql
touch index.js

作成されたindex.jsに以下を貼り付け

index.js
const { ApolloServer, gql } = require('apollo-server');

// GraphQLスキーマの定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

// queryで取得するデータ
const books = [
    {
      title: 'The Awakening',
      author: 'Kate Chopin',
    },
    {
      title: 'City of Glass',
      author: 'Paul Auster',
    },
  ];

// フィールドのデータを返す関数(リゾルバ)
const resolvers = {
    Query: {
      books: () => books,
    },
  };

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`?  Server ready at ${url}`);
});

これでもう動くようにはなっています。

node index.js

http://localhost:4000/にアクセスすると以下の画面が表示されるはずです。
スクリーンショット 2020-12-03 20.28.19.png

これはApolloServerにホストされたプレイグラウンドです。ここからクエリを発行して結果を受け取ることができます。入力補完が効いたり、自動でqueryを書き起こしておいてくれたりとなかなか便利です。(もちろん無効にしてプレイグラウンドにアクセスできないようにすることもできます)

左側にqueryを書いて、真ん中の再生マークをクリックすれば右側に結果が表示されます。簡単です。
試しに以下を左側に入力してみましょう

query {
  books {
    title
    author
  }
}

二冊の本の情報が返ってきたはずです。
queryはRESTAPIでいうとGETにあたります。ここでは全ての本の情報を取得する処理になっていますが、queryに引数を入れることもできます。

mutation

データの取得をできたので次は更新です。GraphQLではデータ更新系の物はmutationに記述します。createもupdateもdeleteも全部mutationです。
この項では本の追加と削除の処理を追加していきます。
(ちなみにここからは公式のチュートリアルにはない内容になってきます)

まずGraphQLスキーマを次のように書き換えます

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(title: String!, author: String): Book
    deleteBook(title: String!): Boolean
  }
`;

addBookdeleteBookが追加されました。
次にリゾルバを書き換えます。

const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    addBook: (parent, args, context, info) => {
      books.push(args);
      return args;
    },
    deleteBook: (parent, {title}, context, info) => {
      const bookIndex = books.findIndex(item => item.title == title);
      if (bookIndex == -1) {
        throw new ApolloError('book not found', 'NOT_FOUND');
      }
      books.splice(bookIndex, 1);
      return true;
    }
  },
};

最後に最初の行を変更します。

const { ApolloServer, ApolloError, gql } = require('apollo-server');

これで本の追加と削除ができるようになっているはずです。
以下をプレイグラウンドに入力してみましょう。

mutation {
  addBook(title: "hoge", author: "hogehoge") {
    title
    author
  }
}

追加した本の情報が返ってきたかと思います。
次に前の項で使った本を取得するqueryを実行してみてください。返ってくる本の情報が三冊に増えているはずです。

追加ができたので削除をしてみましょう。

mutation {
  deleteBook(title: "City of Glass")
}

実行後、再度本を取得すると、"City of Glass"が削除されていることが確認できると思います。
(存在しない本を削除しようとするとエラーが返ってくるはずです)

subscription

subscriptionは簡単にいうと変更検知の機能です。今まで使っていた例でいうと、mutationで本が追加されたり削除されたりあるいは更新されたのをリアルタイムに知ることができます。とても便利。いちいちqueryせずにすみますやったね。
とっても便利なのですが、queryやmutationと少し書き方違うのでハマりどころだったりします。ここでは出来上がったコードをお見せしますが、もし皆さんがご自分で勉強するときはしっかりとドキュメント読み込むことをお勧めします。

今回は書き換わっている箇所が多いので一気にドンといっちゃいます。

index.js
const { ApolloServer, ApolloError, PubSub, gql } = require('apollo-server');

const pubsub = new PubSub();

const BOOK_ADDED = 'BOOK_ADDED';

// GraphQLスキーマの定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(title: String!, author: String): Book
    deleteBook(title: String!): Boolean
  }

  type Subscription {
    bookAdded: Book
  }
`;

// queryで取得するデータ
const books = [
    {
      title: 'The Awakening',
      author: 'Kate Chopin',
    },
    {
      title: 'City of Glass',
      author: 'Paul Auster',
    },
  ];

// フィールドのデータを返す関数(リゾルバ)
const resolvers = {
  Query: {
    books: () => books,
  },
  Mutation: {
    addBook: (parent, args, context, info) => {
      books.push(args);
      pubsub.publish(BOOK_ADDED, { bookAdded: args });
      return args;
    },
    deleteBook: (parent, {title}, context, info) => {
      const bookIndex = books.findIndex(item => item.title == title);
      if (bookIndex == -1) {
        throw new ApolloError('book not found', 'NOT_FOUND');
      }
      books.splice(bookIndex, 1);
      return true;
    }
  },
  Subscription: {
    bookAdded: {
      subscribe: () => pubsub.asyncIterator([BOOK_ADDED]),
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`?  Server ready at ${url}`);
});

(mutationのaddBookにも変更が加わっているのでご注意を)

新しく以下のqueryを実行してください

subscription {
  bookAdded{
    title
    author
  }
}

右下の方にListening...と表示されたかと思います。
この状態でaddBookで本を追加し、subscriptionに戻ってみてください。追加した本の情報が表示されているはずです。別タブで同時に見るとわかりますが、addBookを実行すると即時にsubscription側に反映されています。

最後に

GraphQLを初めて触った時の「これすごい便利じゃん」を体験してもらうためにこの記事書いてみました。
需要(とやる気と時間)があれば続きで詳しい解説やクライアント側のことを書くかもしれません。
読んでくださった方ありがとうございました。

明日はsohichiroさんの記事です。

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

? Microsoft Teams 開発の初心者向けガイド その3: メッセージ・エクステンション

みなさんこんにちは〜。この記事は、Microsoft Teams 開発の初心者向けガイド第3弾になります。前回の2つのチュートリアル ( タブの開発Bot 開発)も楽しんでもらえていたらうれしいです。

今回は、Teams UI からのユーザからのアクションで検索結果やメッセージを書き出す方法について説明します。
3-ext1-cover-1000x420.png
Teams アプリの作成にはいくつもの方法やツールがあるのですが、このチュートリアルでは、コンセプトを学んでもらうことを重視したいので、最低限のコードと最小限のツールセットを使用しています。また、マイクロソフト Azure から 環境をセットアップすることもできますが、今回はそのプロセスを使わず、気軽に学んでもらえるようにブラウザ上でコードを実行していきます。


このチュートリアルでは、メッセージング・エクステンション (Messaging Extensions / メッセージング拡張機能)という機能をつかった開発について紹介します。

メッセージング拡張機能には、アクションと検索という 2 種類があるのですが、今回は「アクション・コマンド」(操作コマンド)について紹介します。

チームの機能: 「アクション・コマンド」

アクション・コマンドを使用すると、情報を収集または表示するためのモーダル ポップアップをユーザーに表示できます。フォームを送信すると、アプリ、つまり Web サービスは、メッセージを会話に直接挿入したり、メッセージ作成ボックスに挿入してユーザーの選択でグループにメッセージを送信できるようにしたりできます。

Teams action command

メッセージ・アクションをつかって、メッセージ内容をモールス コードに変換するサービスを作る!

いまから作成していくアプリは、ユーザーが選んだ任意のメッセージに含まれるテキストを抽出してモールスコードに変換する、というものです。

アプリの動作は次のとおりです。

  1. ユーザーがメッセージをマウスオーバーしてメッセージメニューを開き、「そのほかの操作」(More actions) メニューからモールス bot を選択
  2. ユーザーからの操作がトリガーされると、ペイロードがメッセージングエンドポイントに送信。 (/api/messages)
  3. fetchTask の呼び出し。ここでメッセージ テキスト データが抽出されます。
  4. ポップアップ ダイアログが表示されます。ユーザーは必要に応じてテキストコンテンツを編集し、送信
  5. アプリは、テキストをモールスコードに変換し、コンテンツを返信として表示
  6. ユーザーは結果をクライアントに新規メッセージとして送信

結果は次のようになります。
morsebot-final.gif

? 必須科目

Teams にアプリをインストールできるようにするには、組織の管理者がアクセス許可を付与する必要があります。
それ以外の場合は、無料のMicrosoft 365 開発者プログラム に登録しましょう。このプログラムでは、開発者テナントのサンドボックス、サンプル データ パックに付属しているモック ユーザーデータなどが使え、サブスクリプションはなどでもリニューアルできます。

  • 開発の許可がある Teams テナント、または開発専用テナント (M365 開発者プログラムにサインアップしよう)
  • App Studio - チームクライアントのアプリメニューからアプリを探し、テナントのワークスペースにインストールしてね
  • JavaScript の基礎知識

? このチュートリアルで使用する技術

アクションの構築

? コード サンプルの取得

このチュートリアル シリーズでは、プロジェクトのコードのホスティングと実行を簡略化するために、サードパーティのツール、Glitchを使用しています。

Glitch は、node.jsコードを記述して実行できるWebベースのIDEなので、少なくとも今のところ、localhostのトンネル、デプロイを気にすることなく、Teamsアプリ開発の概念と基本を学ぶことに集中できます。

まず、この Glitch プロジェクトの remix ができるリンク、または下のボタンをクリックしてください。リミックスとは GitHub で言うところのリポジトのをフォークのようなようなもので、自分用のプロジェクトのコピーが生成されるため、元のコードに影響なく自分用に好きなように変更できます。
Remix on Glitch
さて、自分専用プロジェクトのリポを取得すると、自動的に独自のWebサーバーURLを取得します。たとえば、生成されたプロジェクトは、3単語ほどでできたランダムな語で構成されています。たとえばそのプロジェクト名が achieved-diligent-bell だったら、ウェブサーバーのURLは https://achieved-diligent-bell.glitch.me になります。必要に応じて名前をカスタマイズすることもできます。

⚙️ App Configuration: App Studioを使ってアプリ マニフェストを作成

Teams アプリパッケージの基本については前のチュートリアルを参照してください。

? App Studio を使う

App Studio 上部の Manifest Editor(マニフェスト エディター) タブをクリックし、Create a new app(新しいアプリの作成])を選択します。必要事項を入力してください。

そして App ID を生成してください。
exts00-app-studio-ext.png

? メッセージ・エクステンションの設定

左側のメニューから、 Capabilities > Massaging Extensions を選択
exts01-app-studio-ext.png
先に進み、設定するボタンをクリック。
exts02-app-studio-ext1.png
名前を付けます。

? App credentials

ボット名の横にある ID ('2cd53e8a-e698-4exx-...' のように見える) をコピーし、隠しファイルである.envファイルに環境変数として貼り付けます ('.env-sample' の名前を '.env' に変更します)。

App Passwordsの下で新規パスワードを生成し、それをコピーし、これも .envファイルに貼り付けます。

これらの情報は、bot アダプターを初期化するために使用されます。(index.js を参照してください。
exts03-app-studio-ext2.png
(上記の画面画像のステップ3は、このすぐ後に説明します。)

? アクションの設定

Messaging Endpoint で、サーバーを入力します。今回は、Glitch サーバ上でコードを動かしているので、 https://[your project].glitch.me/api/messages のように自分のプロジェクト名が入った URL がサーバとなります。

 Command までスクロールし [Add] をクリック

ダイアログ ボックスで
1. Allow users to trigger actions in external service... を選択
2. Fetch a dynamic set of parameters from your bot を選択
3. command IDtitle text を入力。 Massage チェックボックスを選択 (他のチェックボックスが事前に選択されている場合は、そのチェックボックスをオフにします。) 残りは空白のままにして保存します。
exts04a-app-studio-ext3.png
exts04b-app-studio-ext4.png
exts04c-app-studio-ext5.png

? App manifest パッケージをインストール

Finish > Test and distribute へ行きます。

エラーが発生した場合は、それを修正してください、そうでなければ、Install をクリックしてください。
exts05-app-studio-install.png
また、'manifest.json' を含む zip ファイルと、後でインストールまたは配布する 2 つのアイコン イメージをダウンロードすることもできます。

コード サンプルをそのままリミックスしている限り、この bot は既に動作するはずでが、試してみる前に、これがどのようにコーディングされているかを簡単に説明してみます。

? Microsoft Bot Framework

マイクロソフトボットフレームワーク は、インテリジェントなエンタープライズグレードのボットを構築できるオープンソースSDKです。

この SDK は、Teams だけでなく、Web やモバイル チャット、Skype、Facebook、Amazon Alexa、Slack、Twilio など、幅広い種類のチャット ボットで動作するように設計された強力なプラットフォームです。

? Initiating the bot service

まず、Glitch コード サンプル Repo に 2 つの JS ファイルがあります。 index.js and bots.js です。

ここでは、Express を使用して HTTP サーバーを設定し、HTTP リクエストをルーティングしています。サービスを開始する方法は、前の Bots チュートリアルと同じですが、これは初期化とボット アダプタの作成について、もう一度要約したコードが下のようになります。

// Import bot services
const { BotFrameworkAdapter } = require('botbuilder');

// Bot's main dialog
const { ReverseBot } = require('./bot');

// App credentials from .env
const adapter = new BotFrameworkAdapter({
  appId: process.env.MicrosoftAppId,
  appPassword: process.env.MicrosoftAppPassword
});

// Create the main dialog
const myBot = new MorseBot();

注:この例では、私はボットビルダーのバージョン4.10.0を使用しています。コードが期待どおりに動作しない場合は、使用しているバージョンを確認してください!

? Bot ロジックへの要求の転送

Express を使用して、着信リクエストをリッスンするルーティングを処理します。

app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async context => {
    await myBot.run(context);
  });
});

前の手順で、 App Studio で URL を設定しました。/api/messages は、クライアント要求に応答するアプリケーションのエンドポイント URL です。

?‍♀️ リクエストの処理

エンドポイントで要求を受信し、ボット ロジックに転送すると、アプリは要求のコンテクストを受け取り、bots.js でカスタム応答を作成します。

要求に対する適切なハンドラーを作成するために拡張された TeamsActivityHandler を参照してください。

class ReverseBot extends TeamsActivityHandler {

  // ユーザからのアクションによってトリガー
  handleTeamsMessagingExtensionFetchTask(context, action) {
    /*
         ここで表示するダイアログのコンテンツを adaptive card UI (modal dialog)を作成。
         ユーザがダイアログを確認して送信。
    */
  }

// FetchTask からユーザによって送信されたらトリガー
  handleTeamsMessagingExtensionSubmitAction(context, action) {
    // display the result 
  }

TeamsActivityHandler は、メッセージ イベント (*例えば onMembersAdded メソッドが会話にメンバーが追加されるたびに呼び出される) などのメッセージを処理し、返信を送信するチーム固有のクラスです。

ここでは、ユーザがメッセージに対しアクションを起こすと、handleTeamsMessagingExtensionFetchTask がトリガーされ、ボットが元のメッセージに関する情報を受け取ります。

これらについてはドキュメント「タスクモジュールを作成して送信する 」でもっと詳しく学ぶことができます。

? アダプティブ カードを使用したモーダル ダイアログの表示

ダイアログ UI は、 Microsoft のオープン ソースである アダプティブ カードを使って JSON で UI を構築することができます。アダプティブ カードは、Teams の他、Outlook のアクション可能なメッセージ、Cortana スキルなどさまざまな Microsoft ファミリーのサービスの開発で使うことができます。

handleTeamsMessagingExtensionFetchTask が呼び出されると、メッセージ コンテンツ テキストを取得し、応答としてダイアログとしてアダプティブ カードに表示します。
adaptive-card-message-action.png

アダプティブ カードとコンテンツを定義するには:

const card = {
  type: 'AdaptiveCard',
  version: '1.0'
};

card.body = [
        { type: 'TextBlock', text: 'The message to be encoded to Morse code:', weight: 'bolder'},
        { id: 'editedMessage', type: 'Input.Text', value: content },
      ];
      card.actions = [{
        data: { submitLocation: 'messagingExtensionFetchTask'},
        title: 'Encode to Morse!',
        type: 'Action.Submit'
      }];

const adaptiveCard = CardFactory.adaptiveCard(card);

return {
      task: {
        type: 'continue',
        value: {
          card: adaptiveCard
        }
      }
    };

抽出されたメッセージテキストを type: 'Input.Text' で表示し、ユーザーがモールスコード化するテキストを編集できるようにします。

完全なコードを表示するには、グリッチのコード サンプルの bot.js ファイルを参照してください。

? ユーザーの送信を処理する

ユーザーがタスク モジュールを送信すると、handleTeamsMessagingExtensionSubmitAction がトリガーされ、Web サービスはコマンド ID とパラメーター値が設定されたオブジェクトを受け取ります。

このサンプル コードでは、カスタム データ editedMessage が存在するかどうかだけを確認します。その場合は、値(ここでは文字列) を取得し、それをモールスに変換して、新しいメッセージとして作成するコンテンツを表示します。

async handleTeamsMessagingExtensionSubmitAction(context, action) {

    if(action.data.editedMessage) {
      const text = action.data.editedMessage;
      const morseText = await encodeToMorse(text);

  return {
    composeExtension: {
    type: 'result',
      attachmentLayout: 'list',
      attachments: [
        // the message UI component here
      ]
    }
  }

}

bots.js に示されているコードサンプルでは、結果メッセージを作成するために Bot Framework に付属のサムネイルカードと呼ばれるシンプルなUI カードを使用していますが、もちろん前述のアダプティブカードも使用できます。

?? メッセージアクションを試してみる

それでは、アクションを試してみましょう!
Teams クライアントに移動し、リッチ フォーマットや画像ではない、シンプルテキストで構成されたメッセージのいずれかをクリックしてみてください。

すべてが期待どおりに動作する場合は、任意のテキスト メッセージをモールス コードに変換できるはずです。

more-actions-ja.png

?

チュートリアルを楽しんでいただけましたか?モールス・コードへの変換は正直、あまり普段役に立つものではないでしょうけれど、みなさんはもっと良いユースケースを見つけて素晴らしいアプリを作成することを願っています!

次のチュートリアルでは、検索コマンドである別の種類のメッセージング拡張機能を構築する方法について説明します。次回お会いしましょう ?

?? この記事を英語で読みたいという方は dev.to のリンクからどうぞ!

? Learn more

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

javascript ~map使い方~

mapの使い方

配列内の数字を文字列から数値にまとめて変えたいとき

  let yoso = [ '0', '0', '1', '2' ]
      yoso = yoso.map(Number);
      console.log(yoso) // [ 0, 0, 1, 2 ]

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。
参考:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map

const array1 = [1, 4, 9, 16];

// pass a function to map
const map1 = array1.map(x => x * 2);

console.log(map1);
// expected output: Array [2, 8, 18, 32]

便利だなmap関数

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

1分で学ぶJSON入門

JSONってなんですか?という質問に口頭で答えられるようになればいいなーと思い改めてJSONについて超初歩的な解説の記事を作成しました。

JSONとは?

wikiによると下記の通り。

JavaScript Object Notation(JSON、ジェイソン)はデータ記述言語の1つである。
軽量なテキストベースのデータ交換用フォーマットでありプログラミング言語を問わず利用できる。

この2行がJSONの説明になるのですが難しいワードが出てきたので噛み砕いて説明します。

データ記述言語とは?

ざっくり説明するとデータの記録に使うフォーマット。
ここでいうフォーマットは書き方のルールである。
JSONの場合は下記のようなフォーマットとなっている

[
  {"name" : "太郎", "age" : 26},
  {"name" : "花子", "age" : 23}
]

データ交換用フォーマットとは?

各プログラミング言語間のデータの受け渡しのフォーマット(書き方のルール)
RubyやPHPといったさまざまなプログラム言語からデータのやり取りを行うイメージ。

なぜJSONが生まれたのか?

15年ぐらい前まではXMLというデータ記述言語でjavasciptとデータのやりとりを行なってました。
XMLはHTMLと同様に文章中の文字列をdivなどのタグで挟むように書く必要があります。
先ほどのコードをXMLで記述すると下記の通りです

<?xml version="1.0" encoding="utf-8"?>
<data>
  <item>
    <name>太郎</name>
    <age>26</age>
  </item>
  <item>
    <name>花子</name>
    <age>23</age>
  </item>
</data>

データ量はタグ分だけ多くなり処理のパフォーマンスも下がるという問題点がありました。
そこで登場したのがデータをタグではなくテキスト(オブジェクト)で記載したJSON形式。
タグを記載する必要がない分データ量は XML よりも少なくて済みます。
つまり処理のパフォーマンスも下がる可能性が下がるということです。

データのやりとりイメージ

スクリーンショット 2020-12-02 10.33.27(2).png

続きまして、アルサーガパートナーズ Advent Calendar 2020 5日目の記事は @ryuji-oda さんのオブジェクト指向についてです:tada:

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

JS 知識をつなげる (JSアウトプット)

あいさつ

初めての人は初めまして!知っている人はこんにちは!
中学生バックエンドPGのAtieです!
今回は今まで学んできたJSで一つの作品を作っていきます!
ここまで来たらとりあえず基礎は自分なりにできたと思います!
では!

作品の設計

今回はボタンを押すとユーザーのリストが増えていくものを作っていきます!

ユーザーは前回の記事のこれを使っていきます!

必要な機能
- webApiを叩いてくる
- リストを追加
- リストを表示
大まかな処理がこのようになると思います

準備

まずは作っていくための準備をします

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf8">
    </head>
    <body>
        <script src="index.js"></script>
    </body>
</html>

まずはhtmlです
構造は簡単でスクリプトファイルを読み込んでいるだけです

DOM操作

まずはDOM操作を実装していきます
今回の作品はボタンとリストが必要なのでhtmlに記述していきます

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf8">
    </head>
    <body>
        <ol id="lists">
        </ol>
        <button id="btn">もっと</button>
        <script src="index.js"></script>
    </body>
</html>

ボタンのidはbtnリストのidはlistsにしました

ではidをjsで使えるようにしていきます

main.js
const btn = document.getElementById("btn");
const lists = document.getElementById("lists");

これでjsからbtnとlistsが使えるようになりました

webApiを叩く

DOM操作の準備が整ったのでwebApiを叩いていきます!
webApiを叩くにはasyncで非同期関数にしてawaitを使います
ボタンが押されたらwebApiを叩きたいのでこのようにします

index.js
btn.addEventListener("click", async function () {
    const res = await fetch("http://jsonplaceholder.typicode.com/users");
    const users = await res.json();
});

これでwebApiが叩けるようになりました

listに追加

ではlistに追加していきます
コードが見やすいように関数で分けて記述しました

index.js
// DOM
const btn = document.getElementById("btn");
const lists = document.getElementById("lists");

// 関数
function addList(user) {
    const list = document.createElement("li");
    list.innerText = user.name;
    lists.appendChild(list);
}

async function getUsers() {
    const res = await fetch("http://jsonplaceholder.typicode.com/users");
    const users = await res.json();

    return users;
}

async function listUsers () {
    const users = await getUsers();

    users.forEach(addList);
};

//イベント
window.addEventListener("load", listUsers);
btn.addEventListener("click", listUsers);

コードがすっきりなりました
最後のwindowsのところではリロードされたら表示されるようになっています

さいごに

いやぁ長かったjsの基礎のアウトプットが終わりました!!
今度は一つの分野を掘り下げた記事を書きたいと思っています!
Twitterしています→AtieのTwitter
では!また次回の記事で!

参考、学習資料

【JavaScript入門 #9】これまで講座で学習した知識をフル活用しよう【ヤフー出身エンジニアの入門プログラミング講座】

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

Flash Advent Calendar 3日目 - JavaScriptバイナリ入門 -

開発環境は整ったので、いざSWFファイルのバイナリデータを分解していこうと思います。
とはいえ、バイナリに関して全く知識がないので、必要そうな知識を頭に入れておきます。

目次

  • TypedArray
  • Bit演算

TypedArray

バイナリデータを扱う上でTypedArrayの知識は必須です。
型と扱える範囲を覚えておきます。

参考サイト
TypedArray - MDN - Mozilla

範囲
Int8Array -128 ~ 127
Uint8Array 0 ~ 255
Int16Array -32768 ~ 32767
Uint16Array 0 ~ 65535
Int32Array -2147483648 ~ 2147483647
Uint32Array 0 ~ 4294967295
Float32Array 1.2×10-38 ~ 3.4×1038
Float64Array 5.0×10^-324 ~ 1.8×10^308

また、TypedArrayは可変できないので注意が必要です。
必要な分だけメモリーを確保します。

const typedArray = new Uint8Array(100);

// 100以上の数値を代入する
for (let idx = 0; idx < 110; idx++) {
    typedArray[idx] = idx;
}

// 結果
console.log(typedArray.length); // 100
console.log(typedArray) // 0から99までの値が代入されます。

Bit演算

ここまでは、なんとか理解できたのですが
ここからが難題です・・・
事前にBit演算の式と結果をふんわりみておきます。

ビットシフト演算子

演算子
<< 左シフト演算子 typedArray[offset] << 8
>> 右シフト演算子 typedArray[offset] >> 7
>>> 符号なし右シフト演算子 typedArray[offset] >> 5

バイナリービット演算子

演算子
& 論理積 (AND) typedArray[offset] & 0x1
論理和 (OR) typedArray[offset]|0
^ 排他的論理和 (XOR) typedArray[offset] ^ 2

では、Adobeが提供しているSWFのバイナリの仕様書からSWFの実装を読み解いていきます。
SWF File Format

ええ。全然、理解できません。。。
そこで、バイナリを操作できるライブラリを探しました。

最初に辿り着いたのが@yoya さんの作ったIO_Bitでした。
ありがたく、実装内容を参考にSWFの仕様書を読み解いていきます。

UI8 [Unsigned 8-bit integer value]

getUI8 () 
{
    // offsetをインクリメントして次のポインターに移動
    return typedArray[offset++];
}

UI16 [Unsigned 16-bit integer value]

getUI16 () 
{
    return typedArray[offset++] | (typedArray[offset++] << 8);
}

UI24 [Unsigned 24-bit integer value]

getUI24 () 
{
    return typedArray[offset++] | (typedArray[offset++] 
        | (typedArray[offset++] << 8) << 8);
}

UI32 [Unsigned 32-bit integer value]

getUI32 () 
{
    return typedArray[offset++] | (typedArray[offset++]
            | (typedArray[offset++] | (typedArray[offset++] << 8) << 8) << 8);
}

IO_Bitを参考にしながら
一つ一つ仕様書の実装を入れ込んでいきます。

後は仕様があっていれば、そのパターンをテスト実装していきます。
JavaScriptでのテスト駆動開発

とても地味な作業ですが、ここをしっかり作れば2度3度と改修する必要はないので気長にがんばります。。。

仕様書は複数あるので、ひたすら解読と実装とテストを繰り返します。
ActionScript3.0の仕様書
Action Message Format AMF3
Video File Format

少し気が遠くなりそうですが、ここさえできれば
っという気持ちでがんばります。

とはいえ、あまり実用性もないと思いますので
今日はこの辺で終わろうと思いますw

明日はclass設計(プロトタイプチェーン)を書こうと思います。

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

642日前に日本中を震撼させた闇の魔術に対する防衛術

闇の魔術に対する防衛術 Advent Calendar 2020 を完走するため、深夜のテンションで書き殴ったポエムです。(4日目)

明日(5日目)は、@taptappun さんによる Github Hacking ~Githubを容量無制限のクラウドストレージとして使用する試み~ です。


先生「本日は日本中を震撼させた闇の魔術に対する防衛術を教えます。皆さんは、642日前に、日本中を震撼させた、あの呪いを知っていますか?」

先生「あの呪いの恐ろしいところは、ほんとうに何気もない、日常生活で気にもとめないほど、ありふれた魔法が、あの日を境に呪いとして発現したことです。」

先生「当時、その呪いを解くために多くの人が戦いました。自ら呪いにかかるリスクを負ってでもも、その呪いを解こうと戦ったものたち。偉大な魔法使いも戦いに加わりました。」

先生「それでも、呪いを完全に解くことはできませんでした。呪いの効力は弱まったといわれていますが、また、いつ呪いが発現するかは分からないのです。」

先生「しかし、呪いが発言するリスクは、事前の祈りの魔法で軽減することもできます。本日は、あの恐ろしい呪いに有効な<script>を考えます。」

先生「あの呪いの特徴は、無限とalertです。この特徴を事前に知っていれば、祈りの魔法も作成できます。どのような<script>が書けますか?」

生徒「無限を有限(5回)に変える以下のような<script>はどうでしょうか。」

var _alert = window.alert;
var _alert_cnt = 0;
window.alert = (arg) => {
  if (_alert_cnt < 5) {
    _alert(arg);
  }
  _alert_cnt += 1;
};

先生「発想は非常によいですね。グローバル変数名などの細かい点は無視して、alertの無限呼出は回避できでいます。ただし、これだと、5回alertが呼ばれてしまうと、以降alertが使えなくなってしまいますね。」

先生「これを回避するためには、どうしますか?」

生徒「alertの前後でタイマーを挟んだ以下のような<script>はどうでしょうか。」

var _alert = window.alert;
var _alert_cnt = 0;
var _alert_last = Date.now();
window.alert = (arg) => {
  let now = Date.now();
  if (now - _alert_last < 1000) {
    if (_alert_cnt < 5) {
      _alert(arg);
      _alert_cnt += 1;
    }
    _alert_last = Date.now();
  } else {
    _alert(arg);
    _alert_cnt = 0;
  }
};

先生「いいでしょう。これなら連続したalertでなければ、またalertの再利用ができますね。」

先生「しかし、まだ大きな問題があります。」

生徒「なんでしょうか?」

先生「それは、実際に体験してみましょう。では、今から、この事前の祈りの魔法を行った後、あの呪いが発現するかもしれない魔法をかけます。準備はいいですか。では、」

先生「while(true) { alert(1); } ! 」

生徒「ポチ、ポチ、ポチ、ポチ、ポチ」

生徒「!!ぐるぐるが止まらない!!」

先生「そうです。これは、今は目が回る程度ですが、ひとたび呪いに姿を変えると、あなたは2度と魔法が使えなくなるかもしれません。」

先生「金縛りのような状態は、あの恐ろしい呪いが発現した時と同じです。それに加えて、CPUに負荷をかけたことで発現した別の呪いもあるのです。」

生徒「知っています!あの呪いより少し前に発現した呪いですね。」

先生「良く知っていますね。CPUは呪いに関係ないと思われていた常識をひっくり返したことで有名な呪いです。呪いは身近なところにも潜んでいるのです。」

先生「それでは、ぐるぐるしないようにするにはどうすればいいですか?」

生徒「うーん。。分かりません。」

先生「以下のように、助けを呼ぶように魔法を変えてみましょう。」

var _alert = window.alert;
var _alert_cnt = 0;
var _alert_last = Date.now();
window.alert = (arg) => {
  let now = Date.now();
  if (now - _alert_last < 1000) {
    if (_alert_cnt < 5) {
      _alert(arg);
      _alert_cnt += 1;
    } else {
      throw 'Help Me!!';
    }
    _alert_last = Date.now();
  } else {
    _alert(arg);
    _alert_cnt = 0;
  }
};

生徒「ぐるぐるがなくなりました。けど、そのぶん殴られたような痛みを感じたのですが。。。」

先生「それは、魔法を無理やり中断した痛みですね。中断したことにより怪我をすることもあります。」

先生「しかし、覚えておいてください。魔法は、失敗すれば自分に返ってきて怪我をすることがあります。大事故になれば、それこそ元には戻らないような酷いけがを負うこともあります。」

先生「それでも、魔法を使えなくなることはありません。呪いの最も恐ろしい事は、それが魔法自体を使えなくするかもしれないことです。」

生徒「!!」

先生「今では、あの呪いの効力は弱まったため、このような事前の祈りの魔法を行うことはないでしょう。また、事前にあの呪いを回避するために、このような事前の魔法を行うこともできなかったでしょう。」

先生「呪いを確実に回避する方法はありません。呪いは魔法の副作用のような部分があるのです。しかし、呪いにかかっても決して諦めないでください。あの恐ろしい呪いの時も、多くのものが声を上げ、戦ったのです。」

生徒「はい!」

先生「それでは、本日の授業を終わりにします。」


深夜のテンションなのでお許しを。

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

JavaScriptによるオーディオプログラミング例

JavaScript

オーディオプログラミング言語 Advent Calendar 2020

概要

https://www.w3.org/TR/webaudio/

Web Audio APIはWebブラウザのJavaScriptで使用できるオーディオAPI。W3Cが仕様を策定して各ブラウザのデベロッパーが実装している。2011年にGoogle Chromeの機能として公開されたものが最初の実装事例。現在はスマートフォン用も含め主要なWebブラウザが対応している。

実装例

JavaScriptでオーディオプログラミングをするにはWeb Audio APIを直接使用することもありますが、最近はTone.jsを使うことが多いようです。また、Processing言語のJavaScript版であるp5.jsのオーディオ機能も利用しやすいのでこちらの実装例も紹介します。Tone.jsもp5.jsもWeb Audio APIをラップして使っています。

Web Audio API

特に追加のライブラリはありませんが、現実的なプロジェクトを想定してnpmとwebpackを使うようにしてみました。

package.jsonの内容は以下のようになります。

{
  "name": "example",
  "version": "1.0.0",
  "description": "web audio example",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "serve": "webpack serve"
  },
  "author": "aike",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  }
}

index.htmlは以下の内容です。

<!DOCTYPE html>
<html>
<head></head>
<body>
  <script src="dist/main.js"></script>
  <button>click</button>
</body>
</html>

サイン波生成

Web Audio APIでは、単機能のノードを生成して、connect()でつなげていくことでオーディオ処理を記述します。最終的にAudioContextオブジェクトのdestinationにconnect()すると音が出力されます。

重要な注意点としてAutoplay Policyがあります。これは、Webではユーザのアクションなしに音声の自動再生を禁止するというものです( https://developers.google.com/web/updates/2018/11/web-audio-autoplay )。そのため、onclickなどのユーザーアクションのコールバックとして音声処理を実装する必要があります。コールバックの先頭ではオーディオコンテキストをチェックして休止していたらresumeする処理を入れておきます。

また、オシレータなどの発音ノードは一度start()してstop()したら再利用はできない点も注意が必要です。再び音を鳴らす場合はまた新しくOscillatorNodeを作る必要があります。これはかなり直感に反するので最初は戸惑うかもしれません。

以下の例ではオシレータだけでなくゲインノードもクリックするたびに毎回生成しています。

window.addEventListener("load", ()=>{
  const ctx = new AudioContext();

  const button = document.querySelector('button');
  button.onclick = ()=>{
    if (ctx.state=="suspended")
      ctx.resume();

    // setup nodes
    const osc = new OscillatorNode(ctx);
    osc.type = 'sine';
    osc.frequency.value = 440;

    const amp = new GainNode(ctx);
    amp.gain.value = 0.5;

    // setup audio graph
    osc.connect(amp).connect(ctx.destination);

    // play
    osc.start(0);
    // after 1 second, stop the sound
    osc.stop(1);
  };

});

Delayエフェクト

Webブラウザは原則としてローカルファイルを読めないので、wavファイルは非同期でサーバから取得する必要があります。以下の例では非同期処理をシンプルに書くためにasync/awaitやPromiseを使っています。

wavファイルを再生するためのAudioBufferSourceNodeも一回鳴らしたら再利用はできません。毎回サーバへファイルを要求しないように一度取得したデータはbufferに格納しておいて、生成したAudioBufferSourceNodeに渡すようにしています。

以前はcontext.createDelay()のようなAPIで各ノードを生成していましたが、最近はnew DelayNode(context)のようにしても生成できるようになりました。newを使う方がより自然に見えるので以下の例ではnewで生成しています。

window.addEventListener("load", async ()=>{
  const ctx = new AudioContext();

  const buffer = await LoadSample(ctx, "voice.wav");

  const button = document.querySelector('button');
  button.onclick = ()=>{
    if (ctx.state=="suspended")
      ctx.resume();

    // setup nodes
    const voice = new AudioBufferSourceNode(ctx);
    voice.buffer = buffer;

    const delay = new DelayNode(ctx);
    delay.delayTime.value = 0.4; // second
    const wetLevel = new GainNode(ctx);
    wetLevel.gain.value = 0.5;
    const feedback = new GainNode(ctx);
    feedback.gain.value = 0.5;

    // setup audio graph
    voice.connect(ctx.destination); // dry
    voice.connect(delay).connect(wetLevel).connect(ctx.destination);
    delay.connect(feedback).connect(delay);

    // play
    voice.start(0);
  }
});

function LoadSample(ctx, url) {
  return new Promise((resolv)=>{
    fetch(url).then((response)=>{
      return response.arrayBuffer();
    }).then((ary)=>{
      return ctx.decodeAudioData(ary);
    }).then((buf)=>{
      resolv(buf);
    })
  });
}

Tone.js

https://tonejs.github.io/

Tone.jsは、前項で書いたようなWeb Audio APIの直感に反する点や頻出する定型的な記述を軽減します。

Tone.jsは、npm install tone --save としてインストールします。これによりpackage.jsonのdependenciesにtoneが追加されます。

{
  "name": "example",
  "version": "1.0.0",
  "description": "Tone.js example",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "serve": "webpack serve"
  },
  "author": "aike",
  "license": "MIT",
  "dependencies": {
    "tone": "^14.7.58"
  },
  "devDependencies": {
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  }
}

index.htmlはWeb Audio APIのものと同じです。

<!DOCTYPE html>
<html>
<head></head>
<body>
  <script src="dist/main.js"></script>
  <button>click</button>
</body>
</html>

サイン波生成

Web Audio APIの例と比べるとかなりシンプルになっているのがわかるかと思います。ノード生成とパラメータ設定と接続先設定を一行で書いています。また、発音ノードを毎回生成するように書く必要はありません。

import * as Tone from 'tone';

window.addEventListener("load", ()=>{

  // setup nodes and audio graph
  const amp = new Tone.Gain(0.5).toDestination();
  const osc = new Tone.Oscillator(440, "sine").connect(amp);

  const button = document.querySelector('button');
  button.onclick = async ()=>{
    await Tone.start();

    // play
    osc.start(0);
    // after 1 second, stop the sound
    osc.stop("+1.0");
  };

});

Delayエフェクト

Delayエフェクトもシンプルです。Tone.jsではフィードバックディレイが最初から用意されているため複雑なルーティングは必要ありません。

import * as Tone from 'tone';

window.addEventListener("load", ()=>{

  // setup nodes and audio graph
  const voice = new Tone.Player("voice.wav");
  const delay = new Tone.FeedbackDelay().toDestination();
  delay.delayTime.value = 0.4;
  delay.feedback.value = 0.5;
  delay.wet.value = 0.33; // dry:wet = 0.66:0.33 = 2:1
  voice.connect(delay);

  const button = document.querySelector('button');
  button.onclick = async ()=>{
    await Tone.start(0);
    // play
    voice.start(0);
  };

});

p5.js

https://p5js.org/

p5.jsはProcessing言語同等のものをJavaScriptで実装したライブラリです。構文はあくまでもJavaScriptなのでProcessingとソースコードレベルの互換性はありませんが、わずかな違いなのでProcessing経験者であればすぐに使えるようになります。JavaScript側からの観点でも、Processingの直感的なわかりやすさは大きなメリットです。インストール不要のWebプレイグラウンドp5.js Editor( https://editor.p5js.org/ )が用意されていて手軽に試すことができます。

p5.jsのオーディオ機能は、以前はMinim風のライブラリもありましたが、現状はProcessingのオーディオAPIとの互換性をあまり意識していないp5.soundが標準的に使われています( https://p5js.org/reference/#/libraries/p5.sound )。比較的変わりやすいWeb Audio APIの最近の仕様にも追従していて使いやすいライブラリになっています。

p5.jsの場合、npmやwebpackを使わない方がシンプルでProcessingらしい書き方になるので、それらを使わずに以下のindex.htmlのようにheadの中でp5.jsとp5.sound.jsを取得して利用するようにしました。

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/p5@1.1.9/lib/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.js"></script>
    <script src="sketch.js"></script>
  </head>
  <body>
    <main>
    </main>
  </body>
</html>

サイン波生成

p5.jsもシンプルに直感的に書くことができます。発音ノードのライフサイクルを気にする必要はありません。p5.jsではキャンバスのクリックに反応するプログラムも慣習的に多いのでAutoplay Policyもあまり気にならないように思います。

let osc;

function setup() {
  let canvas = createCanvas(100, 100);
  background('#ed225d');

  osc = new p5.Oscillator('sine');
  osc.amp(0.5);
  osc.freq(440);
  canvas.mousePressed(play);
}

function draw() {
}

function play() {
  osc.start();
  osc.stop(1);
}

このプログラムはp5.js Editorでも実行することができます。

Delayエフェクト

p5.soundにはフィードバックディレイが最初から用意されているのでこちらもシンプルです。ただしディレイ成分にはかならずローパスフィルターがかかるようになっているため、他の実装例と近い音になるようフィルターのカットオフ周波数を全開(=ナイキスト周波数)にしてフィルター効果を無効にしています。

let voice;

function preload() {
  voice = loadSound('voice.wav');

  let delay = new p5.Delay();
  delay.process(voice);
  delay.delayTime(0.4);
  delay.feedback(0.5);
  delay.amp(0.5);
  delay.filter(sampleRate() / 2); // set nyquist frequency to disable LPF
}

function setup() {
  let canvas = createCanvas(100, 100);
  background('#ed225d');
  canvas.mousePressed(play);
}

function draw() {
}

function play() {
  voice.play();
}

このプログラムはp5.js Editorでは実行できません。サウンドファイルがサーバに存在しないためLoading...の状態で止まります。実行確認はローカルサーバなどでおこなってください。

感想

Web Audio APIが最初に登場したときは、本格的なオーディオプログラミングが可能で、それをそのまますぐにWebで公開できるということがとても画期的と感じました。その後、Web Audio APIも、JavaScriptをとりまく環境も進化していった結果、使いやすくなった点も多いもののシンプルさが失われていった部分もあります。また発音ノードのライフサイクルなどあまり直感的ではない仕様もやはり気になります。

Tone.jsはそういった理不尽さをほとんど感じることがないように、利用しやすさを重視したライブラリになっているので、現状では最初からTone.js前提で考えても良いように思います。

p5.jsのオーディオ機能は、以前使用したときはまだ実装が十分追いついていなかったイメージがありましたが、現在は機能も豊富で実行も安定しているのでProcessingのような使い方をする場合はこちらも良い選択肢です。

p5.jsをはじめ、Faust、Gibberなど、プログラミング言語をローカルにインストールせずにWeb上のプレイグラウンドでオーディオ処理を試すことができる環境が増えてきたのもWeb Audio APIの重要な功績です。

一方で、実装がブラウザのデベロッパー依存であるため、細かい部分ではブラウザによって挙動が異なることもある点は注意が必要です。今回のプログラムはすべてWindows PCのGoogle Chromeで確認しました。

なお、Web Audio APIはあくまでブラウザのAPIという位置づけのようで、Node.jsにオーディオ機能が搭載されることはなさそうです。Node.js上にWeb Audio API相当のものを実現するライブラリはいくつか開発されましたが、いずれもNode.js公式のものではなく、今回調べたところ数年前から開発が止まっていて最新のWeb Audio APIに追従したものはないようでした。

オーディオプログラミング言語 Advent Calendar 2020

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

最近やったことのメモ(MVVM, asyncとdefer)

MVVM

Webサービスの設計思想(ソフトウェアアーキテクチャ)
MはModel、VはView、VMはViewModelのこと

View -> View Model -> Model の順

役割でいうと、
表示 → 表示するために値の取得と加工 → 値  こんな感じ?

View Modelが値と表示の間を取り持ってくれます。超優秀ですね☆

asyncとdefer

JavaScriptファイルの非同期での読み込み
<script async src="/abc.js"></script>
<script defer src="/xyz.js"></script>

asyncとdeferどちらもドキュメントのパース中にスクリプトをダウンロード実行。

async・・・パースが完了前に実行。asyncスクリプトの実行は必ずしも順番通りではない。
defer・・・パース完了後に実行。deferスクリプトの実行は順番に行われる。

スクリプトが独立 → async
スクリプトが依存 → defer

今日の名言

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
(付け足すものがなくなったときでなく、取り除くものがなくなったとき、それが完璧になるということだ)
-アントワーヌ・ド・サン=テグジュペリ(作家)

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

Vue.jsでオセロ盤を作ってみる

はじめに

本日は12月4日❗️
チノちゃん誕生日おめでとう???

本題

オセロ盤はビットボードというデータ構造で実装するのが良いらしい⚪️⚫️✨
Vue.jsで石を並べるところまでやるぞ❗️?

ビットボード(Wikipedia)

※この記事では、ビットボードの難しい説明を省略します?
ビットボードで石を返す処理はサーバー側で行い、その結果の石情報が渡ってくるところからやります✊?❗️

あらかじめ仕込んでおく3分クッキングスタイルですね??✨

早速実装

残念ながらJavaScriptの数値は32bitなので、1つの変数で64マス表すことができません❗️?
ゆるせねぇ...

というわけで、サーバー側で1次元配列に変換してフロント側に渡ってくるってことにしました✊?❗️

最初に思い付いたやつ

Board.vue
<template>
  <div class="board">
    <div v-for="i in 8" class="row">
      <div v-for="j in 8" class="cell">
        <div v-if="blackStones[i * 8 + j]" class="stone black"></div>
        <div v-else-if="whiteStones[i * 8 + j]" class="stone white"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      blackStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ],
      whiteStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ]
    }
  }
}
</script>

二重ループで回して 1 が立ってたらそれぞれの石の要素を追加する感じ

しかしここで問題が…
v-for="i in 8" のindexが1始まりなのです?
[i * 8 + j][(i -1) * 8 + j - 1] と書かなければならないのでわかりづらい❗️?

改良

Board.vue
<template>
  <div class="board">
    <div v-for="i in Array(boardSize).keys()" class="row">
      <div v-for="j in Array(boardSize).keys()" class="cell">
        <div v-if="blackStones[i * boardSize + j]" class="stone black"></div>
        <div v-else-if="whiteStones[i * boardSize + j]" class="stone white"></div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      boardSize: 8,
      blackStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ],
      whiteStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ]
    }
  }
}
</script>

いろんな方法がありそうだけど、一番短そうな Array(8).keys() で動いたのでこれでいいや❗️?

完成版

Board.vue
<template>
  <div class="board">
    <div class="logo rot">Othello</div>
    <div class="board-main">
      <div v-for="i in Array(boardSize).keys()" class="row">
        <div v-for="j in Array(boardSize).keys()" class="cell">
          <div v-if="blackStones[i * boardSize + j]" class="stone black"></div>
          <div v-else-if="whiteStones[i * boardSize + j]" class="stone white"></div>
        </div>
      </div>
    </div>
    <div class="logo">Othello</div>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      boardSize: 8,
      blackStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ],
      whiteStones: [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 0, 0, 0, 0,
        0, 0, 0, 0, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
      ]
    }
  }
}
</script>

<style lang="scss" scoped>
.board {
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  margin: auto;
  width: 430px;
  height: 470px;
  background: #333;
  border-top: 3px solid rgba(255, 255, 255, 0.2);
  border-right: 5px solid rgba(0, 0, 0, 0.2);
  border-bottom: 5px solid rgba(0, 0, 0, 0.2);
  border-left: 3px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;

  .logo {
    color: gold;
    text-align: center;
  }

  .rot {
    transform: scale(-1, -1);
  }

  .board-main {
    display: flex;
    margin: auto;
    width: 408px;
    height: 408px;
    background: darkgreen;
    border-left: 1px solid #000;
    border-top: 1px solid #000;

    .cell {
      display: flex;
      justify-content: center;
      align-items: center;
      border-bottom: 1px solid #000;
      border-right: 1px solid #000;
      width: 50px;
      height: 50px;

      .stone {
        border-radius: 50%;
        width: 85%;
        height: 85%;
      }

      .black {
        background: #333;
      }

      .white {
        background: #eee;
      }
    }
  }
}
</style>

CSSでオセロ盤っぽくした?✨
コメント 2020-11-23 143746.png

あとは、なんらかのイベントと紐付けて、 blackStones whiteStones の値が変わったら盤面を差分レンダリングする感じですね❗️?

おしまい?

※気が向いたらPHP版のビットボード実装の記事書きます

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