まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:api

実を言うと前回このブログエントリを書いたのは、今日のエントリへの前フリでした:もともと以下で紹介する Swagger Editor を使う様子を紹介したかったのですが、そのためには扱う題材となる API が必要でした。というわけで簡単な仕様で、かつ試験的にクロスオリジン問題を解決することのできる(←ここ大事!)REST API を Node-RED で作ってみる、というのが上記の内容でした。同様に自分で管理可能な API があればそれを使って読み替えていただいてもいいのですが、そうでない場合まずは上記エントリを参照して、この後の紹介で管理する REST API を作っておいてください。


さて本題は「REST API のドキュメントをどのように用意するか」です。REST API の仕様書として最低限必要な情報は HTTP メソッドと URL のパス、実行時に与えるパラメータ、結果、そしてその API が何をするものかをざっと紹介したものでしょうか?気が利いているものだと各パラメータ毎の必須/オプションの情報、エラー時の情報などがあったりしてより便利になります。上記の Node-RED で作った API の場合だと対象の API は1つでこんな感じでしょうか:
メソッドパスパラメータの指定方法パラメータとその意味実行例結果の Content-Type結果
GET/getDateURLパラメータtimestamp : タイムスタンプ値(オプション、未指定時は現在時刻)curl -XGET 'http://xxx.com/getDate?timestamp=1000'text/plain指定した日付時刻の文字列


このドキュメントでもある程度は理解できると思いますが、もう少し便利に、というか、Swagger という標準フォーマットを使ってドキュメントを作ってみます。その際に便利なのが Swagger Editor です。Swagger Editor はここにアクセスしてオンライン版をそのまま使うこともできますし、Docker イメージやソフトウェアをダウンロードして専用環境下で利用することもできます。今回はオンライン版を使って紹介します。

Swagger Editor を開いて、API ドキュメントを yaml と呼ばれるテキストフォーマットで書いていきます。今回の例だとこんな感じでしょうか:
swagger: "2.0"
info:
  description: "俺の API"
  version: "0.0.1"
  title: "俺の API"
host: "**********.mybluemix.net"
basePath: "/"
tags:
- name: "myapi"
  description: "俺のAPI"
schemes:
- "http"
- "https"
paths:
  /getDate:
    get:
      tags:
      - "myapi"
      summary: "時刻を文字列で取得する"
      description: "現在時刻または指定したタイムスタンプ値の日付文字列を返す"
      produces:
      - "text/plain"
      parameters:
      - in: "query"
        name: "timestamp"
type: "number"
format: "integer" description: "タイムスタンプ値" responses: 200: description: "成功"

この内容を Swagger Editor の画面左に入力します。するとこの Swagger で定義した Open API ドキュメントが画面右に表示されます:
2017091001


今回定義したのは GET /getDate という1つの API です。ここをクリックすると展開され、パラメータなどより詳しい情報が表示されます。API ドキュメントとしても便利ですが、この Swagger によるドキュメント最大の特徴は「実行できる」ことです。このページから AJAX によるリクエストを発行して API を実行します。一般的には AJAX でリモートサイトにアクセスするにはクロスドメイン問題を考慮する必要がありますが、今回用意した API は(そのための目的もあって)クロスドメインからも実行できるような HTTP レスポンスヘッダを設定しています。というわけで実行してみましょう。"Try it out" と書かれたボタンをクリックします:
2017090902


するとパラメータ部分が入力可能な状態になります。今回は timestamp という必須ではないパラメータを指定できるようにしていることがわかります。とりあえずはここには何も設定せずにそのまま実行してみます:
2017090903


実行するには "Execute" と書かれたボタンをクリックします。すると実行した時の curl コマンドなどと一緒に実行結果も表示されます。この API はパラメータなしで実行すると現在時刻のテキストを返すようになっており、実際に実行したタイミングの日付時刻が表示されます:
2017090904


次にパラメータを指定した上で実行してみます。timestamp に 1000 と入力してみました。これはタイムスタンプの値が 1000 になる日時(1970年1月1日午前零時から1000ミリ秒後)を指定したことになります:
2017090905


この状態で "Execute" ボタンをクリックすると、実行結果は 1970/01/01 00:00:01 になるはずです。API が正しく動いていることがドキュメントの中から実行して確認することができました(クロスドメイン問題を抱えたままだとここでの実行は失敗します):
2017090906


と、これが Swagger で作った API ドキュメントの魅力です。 最後にここで定義した Swagger ドキュメントをエクスポートして(Swagger Editor 上ではない)別のサーバー上でも動くようにしてみます。今回は Node.js 上で実行できるような形式でエクスポートしてみましょう(サーバーサイド JavaScript である Node.js 上で実行する場合はクロスドメインは意識する必要がありません)。画面メニューの "Generate Server" から "nodejs-server" を選択します。すると自動的にダウンロードが始まり、nodejs-server-server-generate.zip というファイルがダウンロードされます:
2017090907


この zip ファイルを Node.js がインストールされたシステム上に展開し、"npm install" 後に "npm run start" すると起動します:
$ cd nodejs-server-server
$ npm install
$ npm run start

> api@0.0.1 prestart /home/pi/src/nodejs-server-server
> npm install

up to date in 5.819s

> api@0.0.1 start /home/pi/src/nodejs-server-server
> node index.js

Your server is listening on port 8080 (http://localhost:8080)
Swagger-ui is available on http://localhost:8080/docs

起動したドキュメントページ(上記例だと http://localhost:8080/docs)にアクセスすると Swagger Editor の右ペインでプレビューとして見ていた部分が表示されます:
2017091002


実際にパラメータを指定するなどして実行することも可能です:
2017091003


以上、自分で作った API に対する実際に動かせる Swagger ドキュメントを作り、そのドキュメントを自分のサーバー上で動かす、という一連の手順を紹介しました。開発者としては API の仕様書を読み解くだけでなく、実際に動かして返ってくる値やそのフォーマットを確認しながら利用できるので、自分が API を使って作ったアプリがうまく動かなかった時の問題切り分けにも使えるツールになるので非常に便利です。

以前のブログエントリの中で Hyperledger Composer を使うことでブロックチェーンのビジネスネットワークを比較的簡単な定義で動かすことができることを紹介しました:
Hyperledger Composer フレームワークを使ってみる

↑ここで紹介したように Hyperledger Fabric v1.0 と Hyperledger Composer を使うことで、"asset" と呼ばれる取扱データ、"participant" と呼ばれるユーザー、 "transaction" と呼ばれる実行処理、そして "ACL" と呼ばれるアクセス権管理を定義してブロックチェーン環境に簡単にデプロイすることができるようになります。

この方法でデプロイしたブロックチェーンネットワークを外部のプログラムから使う方法も何通りかあるのですが、今回はその中から「Composer の内容を Web API 化」して、外部からは HTTP(S) を使った REST API として使えるように公開する方法を紹介します。以下の手順を実際に試す場合はローカル環境内に Hyperledger Fabric v1.0 および Hyperledger Composer がインストールされている必要があります。その手順は以下を参照して、ローカル環境で Hyperledger Composer が使える状態を作っておいてください:
Hyperledger Composer フレームワークをインストールする


また、実際に REST API として公開する内容のビジネスネットワークを定義した .bna ファイルが必要です。今回は以下のページで紹介されている方法に従って my-network.bna ファイルを作り、それを使うことにします(以下に日本語で解説します):
Developer Tutorial for creating a Hyperledger Composer solution


ではビジネスネットワークの定義ファイルを用意するまでの手順を紹介します。今回は github.com に公開されているサンプルをベースにクローンして用意します。

まず、上記リンク先の手順に従って Hyperledger Fabric v1.0 および Hyperledger Composer が導入された環境を用意して、両方のサービスを有効にします。

次に同環境にログインし、作業ディレクトリ(以下の例では ~/work)を作って、サンプルプロジェクトを git clone します:
$ mkdir ~/work (作業ディレクトリ)
$ cd ~/work
$ git clone https://github.com/hyperledger/composer-sample-networks.git

このサンプルプロジェクト内の basic-sample-network の内容を my-network という名前でコピーします:
$ cp -r ./composer-sample-networks/packages/basic-sample-network/  ./my-network

my-network プロジェクトの package.json ファイルを変更します。変更内容は name と prepublish 内のプロジェクト名を "my-network" にすることと、description の内容を変えることです:
    :
  "name": "my-network",
  "version": "0.1.6",
  "description": "My Commodity Trading network",
  "networkImage": "https://hyperledger.github.io/composer-sample-networks/packages/basic-sample-network/networkimage.svg",
  "networkImageanimated": "https://hyperledger.github.io/composer-sample-networks/packages/basic-sample-network/networkimageanimated.svg",
  "scripts": {
    "prepublish": "mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/my-network.bna",
    "pretest": "npm run lint",
    :

そして実際のビジネスネットワークの内容を書き換えます。定義する内容は上記ブログエントリの中で紹介したものと同じ定義をこのプロジェクト内にも作成することにします。というわけでまずは models/sample.cto ファイルを開き、以下の内容に書き換えて保存します:
/**
 * My commodity trading network
 */
namespace org.acme.mynetwork
asset Commodity identified by tradingSymbol {
    o String tradingSymbol
    o String description
    o String mainExchange
    o Double quantity
    --> Trader owner
}
participant Trader identified by tradeId {
    o String tradeId
    o String firstName
    o String lastName
}
transaction Trade {
    --> Commodity commodity
    --> Trader newOwner
}

↑ Commodity という asset と、Trader という participant と、Trade という transaction を定義しています。

同様にして lib/sample.js ファイルを開き、以下の内容に書き換えます:
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Track the trade of a commodity from one trader to another
 * @param {org.acme.mynetwork.Trade} trade - the trade to be processed
 * @transaction
 */
function tradeCommodity(trade) {
    trade.commodity.owner = trade.newOwner;
    return getAssetRegistry('org.acme.mynetwork.Commodity')
        .then(function (assetRegistry) {
            return assetRegistry.update(trade.commodity);
        });
}

↑tradeCommodity という処理を定義しています。

そして同様にして permissions.acl ファイルも以下の内容に書き換えます:
/**
 * Access control rules for mynetwork
 */
rule Default {
    description: "Allow all participants access to all resources"
    participant: "ANY"
    operation: ALL
    resource: "org.acme.mynetwork.*"
    action: ALLOW
}

rule SystemACL {
  description:  "System ACL to permit all access"
  participant: "org.hyperledger.composer.system.Participant"
  operation: ALL
  resource: "org.hyperledger.composer.system.**"
  action: ALLOW
}

↑デフォルトで全参加者にリソースへのアクセス権を与えるという内容です。


ではここまでに定義した models/sample.cto, lib/sample.js, permissions.acl の内容でビジネスネットワークをビルドします:
$ cd ~/work/my-network
$ npm install

このコマンドが成功すると、 my-network/dist フォルダ内に my-network.bna ファイルが生成されます。このファイルと Hyperledger Composer を使って Hyperledger Fabric v1.0 に同ビジネスネットワークをデプロイします:
$ cd dist
$ composer network deploy -a my-network.bna -p hlfv1 -i PeerAdmin -s randomString

デプロイが成功しているかどうかは以下の ping コマンドで確認できます:
$ composer network ping -n my-network -p hlfv1 -i admin -s adminpw

ここまでの作業でビジネスネットワークを定義し、Hyperledger Fabric v1.0 上にデプロイすることができました。最後にこのビジネスネットワークを Web API 化して HTTP クライアントから REST でアクセスできるようにします。 そのためには composer-rest-server というツールを使います。composer-rest-server は npm を使ってインストールします:
$ sudo npm install -g composer-rest-server

composer-rest-server の使い方は REST API 化したい Hyperledger Composer プロジェクト上でコマンド実行するだけです:
$ cd ~/work/my-network
$ composer-rest-server

すると Hyperledger-Composer ロゴが現れ、知る人ぞ知る Strongloop loopback のようなインターフェースでのプロパティ指定画面になります:
2017081401


質問内容に以下のように答えます:
 Fabric Connection Profile Name: hlfv1
 Business Network Identifier: my-network
 Fabric username: admin
 secret: adminpw
 namespaces: never use namespaces
 (ここから下はデフォルトのまま)
 REST API to be secured: No
 event publication over WebSockets: Yes
 TLS security: No
2017081402


すると上記画面のように http://(IPアドレス):3000/explorer と表示されます。ウェブブラウザでこの URL にアクセスすると、(これも知る人ぞ知る StrongLoop Loopback のような)定義したビジネスネットワークに REST API でアクセスするための Open API ドキュメントが表示されます:
2017081403


この画面からは定義した Commodity や Trader, Trade といった asset, perticipant, transaction を読み書き実行するための各 API を展開したり、
2017081404


実際にパラメータを指定して実行したりすることもできます:
2017081405


この方法であればブロックチェーンにあまり詳しくないデベロッパーでも REST API で利用できるので非常に便利といえます。

 

このブログでも何度か Node.js のネタを扱ってますが、非同期処理に悩まされることが多いので、自分の理解の意味でもまとめておくことにしました。

まず理解の大前提として、Node.js はシングルスレッドで動作するため、いわゆる並列処理はできない仕様になっています。そして非同期に処理を実行することができます。これによって並列処理ができなくても、何かの時間のかかる処理があった場合にその終了を待たずに次の処理に進むことができることを意味しています。 ただ、この辺りがややこしく難解になっていることも事実です。

例えば REST API などの HTTP リクエストを行って、その結果が得られたら、その得られた結果の JSON オブジェクトの値を使って処理をする、などというよくあるケースでもこの問題に直面します。

まずは何が難解なのかを紹介します。REST API だとローカル環境で気軽に試せないので、わざと実行に時間のかかる関数を用意して説明します。

例えばこのような処理を考えてみます:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行
func1( n0 );

この中では func1 という関数を定義しています。setTimeout を使ってわざと1秒(1000ミリ秒)待ってから、パラメータの値の2倍を画面に出力する、という関数です。 この関数を n0 (=10 に設定)という変数をパラメータにして実行する、というものです。なので "20" という結果が出力されることを期待しています。

この内容を test.js というファイルに保存して、実行してみます(青字部分が出力結果):
$ node test
n0 = 10
20

期待通りに "20" と出力されました。とりあえずここまでは成功です。


さて問題はここからです。上記例では関数 func1 の中で console.log が実行されて出力までを行いました。これを func1 からは与えた数値を2倍した結果を受け取るようにして、func1 の外側で出力するように変更してみます。深く考えずにやるとこんな感じでしょか?
// test.js(注 正しく動きません)

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  setTimeout( function(){
    return ( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
var n1 = func2( n0 );
console.log( 'n1 = ' + n1 );

func1 をほぼコピペして func2 という関数を作りました。console.log の代わりに return にして、値を返すようにしています。呼び出し元からはこの func2 を実行して、得られた結果を出力するようにしています。

これを先程と同様に実行するとこうなります:
$ node test
n0 = 10
n1 = undefined

n0(10)の値の2倍の "20" という結果を期待していたのですが、"undefined" と表示されてしまいます(正確に書くと、上記の2行はすぐに表示されますが、更に1秒くらい経過してから終了します)。この理由は最初に書いたように func2 は非同期に実行されているので、return の行が実行されるまでには1秒かかります。しかしその前に(return の行が実行される前に)関数そのものの処理は終了してしまいます。つまり値が戻る前に(戻っていない値を受け取ることになっている)n1 という変数を出力しているので "undefined" になっているのでした。

このように、期待通りに動かなかった理由は明白なのですが、ではどうすればこの関数が期待通りに動く(1秒後に与えられた結果を戻り値として戻し、受け取った側がその値を出力する)ようにできるでしょうか?これが今日紹介する大きなテーマです。


結論を先に紹介すると、ここで Promise オブジェクトを使って関数を修正し、受け取った側もその変更に合わせて一部書き直す必要があります。具体的には以下のように修正します:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  return new Promise( function( resolve ){
    setTimeout( function(){
      resolve( 2 * x );
    }, 1000 );
  });
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
func2( n0 ).then( function( n1 ){
  console.log( 'n1 = ' + n1 );
});

まず関数 func2 側は、Promise オブジェクトを新規に作成します。Promise オブジェクトは処理が成功した場合の関数をパラメータに指定します。上図だと
function( resolve ){
  setTimeout( function(){
    resolve( 2 * x );
  }, 1000 );
}

という関数がパラメータに指定されているので、成功するとこの関数が実行されます(今回は使っていませんが、第二パラメータを指定した場合は失敗時に実行する処理を指定したことになります)。この処理の中で1秒待って、指定したパラメータを2倍して resolve とする、ということになります。

そしてこの関数 func2 を呼び出す側も少し変更が必要になります。 func2() 関数の実行結果をそのまま変数として受け取るのではなく、成功した場合(今回の例だと1秒待って2倍になった値が返された場合)の処理を .then() 内に渡して処理することになります。この then 内の処理で計算結果(resolve で処理された内容)を n1 という変数で受け取って console.log で表示する、という内容にしています。

こうして修正した test.js を実行すると、以下のような結果になります(実際には n0 = 10 がすぐに表示され、1秒くらい待ってから n1 = 20 の行が表示されて終了します):
$ node test
n0 = 10
n1 = 20

Node.js の関数内で非同期処理を実行して、その非同期処理の終了を待って値を受け取るような関数を作る場合は、Promise オブジェクトを使って上記のように記述します、という紹介でした。非同期実行に慣れていないと、この辺りで戸惑うことが多いと感じたので、まとめておきました。


Watson API の1つで、複数の選択肢の中から自分の与えた条件に合うアイテムをトレードオフ判断で絞り込んでくれる、という便利な Tradeoff Analytics API があります(ありました)が、残念ながら5月11日をもって新規インスタンスの生成ができなくなってしまいました(作成済みインスタンスは2018年4月まで使えるようです。詳しくはこちら)。

Bluemix に Watson API が追加された当初から存在していた API の1つであるだけでなく、個人的にもデモなどで使っていて、非常にわかりやすくて便利な API だっただけに残念でした。

このアナウンスがされたタイミングから準備はしていたのですが、実はこの Tradeoff Analytics と同様の(近い互換性を持つ)API を自分でオープンソース化前提で作っています。まだ途中といえば途中ですが、制約付きで一応動く状態にはなっています。新インスタンスが生成できなくなったこのタイミングで一旦公開してみようと思いました:
https://github.com/dotnsf/yatradeoff


なお、この API を実際にアプリケーションサーバー上で稼働させているものもこちらに用意しました。ただ API だけ使いたい人はこちらをどうぞ:
https://yatradeoff.au-syd.mybluemix.net/dilemmas


この URL にブラウザで GET アクセスしても何も起こりません(エラーメッセージが表示されるだけ)が、POST アクセスすると API として動きます。その際にはユーザー名: username &パスワード: password を指定してください(オリジナル同様 Basic 認証です)。


元の Tradeoff Analytics API 同様、/dilemma という POST API エンドポイント1つだけが定義されています。ポストする JSON 型インプットデータのフォーマットには互換性があります。

API の実行結果としての JSON データにも互換性をもたせました。ただ現状はオリジナルのサブセットになっています。現状、以下の3点の制約事項があります:
(1) preferable_solutions データは常にブランクになります
(2) 解析結果は solutions に含まれますが、'FRONT' ステータスを持つデータだけが含まれます
(3) map information を出力するようパラメータで指定しても map information は返されません



ただ1点、オリジナルにはなかった優先順位を考慮する機能も加えています。エンドポイント URL のパラメータに prioritised=1 を追加して実行(例: /dilemma?prioritised=1)すると、インプットデータの JSON データは優先順位順に解釈されます(先にある方が高い優先順位であると解釈されて実行されます。例えば「項目AとBを両方ともトレードオフの材料にする」のではなく、「項目AとBを両方ともトレードオフの材料にするが、Aの方をBよりも優先する」というトレードオフを可能にしています)。


そして、このパラメータを付けて実行した時の API 実行結果も優先順位順に返されます。先にある方がよりオススメな結果、ということになります。まあ preferable_solutions の代わりという位置付けです。


※実はこの機能を付けたくて自分で API を改良した、という経緯があります。


結果の JSON テキストの solutions 内がトレードオフの結果です。オリジナル API では全ての選択肢に対して 'FRONT' か 'EXCLUDE' かの結果を含めていましたが、現状この API では 'FRONT' のもの(トレードオフ判断の結果、選択肢の候補として残ったもの)だけが返される点に注意してください。


IBM Watson Summit 2017 開催記念作品!


自分だけではないと思いますが、可愛らしいフリー素材を数多く公開していただいている「いらすとや」http://www.irasutoya.com/)さんには、大変お世話になっております。



僕の場合はプレゼンテーション内のイラストに多く使わせていただいています。中にはいらすとやさんの素材だけでサイトや資料を作ってしまう職人さんもいらっしゃるようです。

ある程度「いらすとや」を使っていて感じたことは「目的の素材をうまく見つけるのが難しい」ということです。「昔こんな感じのイラストを見た記憶があるんだけど、どのカテゴリーだったっけな?」とか、「この人が使ってるこのイラストと同じものを使いたい」とか、自分の記憶が曖昧だったり、これというキーワードが思いつかない時に目的のイラストをうまく検索できないことがたまにあるのでした。


で、その解決策になるかどうかわかりませんが、興味半分でこんなサイトを作ってみました:
「いらすとや検索」



↑見た通りのサイトです。「なんとなく」覚えているイラストをなんとなく描いて、search して、そのイラストに似た「いらすとや」画像を探す、というものです。PCであればマウスで、スマホの場合はタップで描きます。検索結果は最大100件表示され、その中に含まれていれば目的のページに(クリックで)移動できる、というものです:
2017042501

    ↓

2017042502


例えばこの↑例、「鳩」で検索すればすぐに見つかりますが、「鳥」で検索するとなかなか候補が出てきません。「鳩のイラスト」とまで認識できていればテキスト検索でも探せるのですが、そこまでハッキリを覚えていないようなケースでも「たしかこんな感じの・・・」というイラストが描ければ検索できるようになっています(たぶんw)。


今のところ描くイラストは黒線一本のみで描く必要があります。描き直しに消しゴムなどはなく、reset する必要があります。編集機能にはまだ制約が多いですが、シンプルさを重要視しました(ということにします)!
2017042503



なお、このサイトはコグニティブエンジンである IBM WatsonVisual Recognition(画像認識) API を使って、あらかじめ学習させたイラストからの類似画像を人工知能のテクノロジーを使って検索する、という仕組みで実装しています。いらすとや内の全ページをクロールする方法が分からなかったので、現在は「リクエスト」ラベルから辿れる画像を対象にしています。仕組みはシンプルですが、実はそこそこなテクノロジーが裏に潜んでいたりします。


・・・まあ、ネタにどうぞ(笑)。
 

また、このアプリを作る上で、以下の2つの情報を参考にしました。HTML5 の Canvas にマウス移動(とスマホのタッチ)で線を描画するワザと、特にスマホのタッチで描画をする際に画面のスクロールを強制的に止めるワザです:

JavaScript でマウス座標を取得し、Canvas上に線を描画


このページのトップヘ