HTML5 Canvasで矢印を描いてみた(三角関数を利用)
この投稿ではHTML5 Canvasの2次元コンテキストのAPIを利用して矢印を描くJavaScriptのコードを書いてみた。
ここで描く図は次のようなもの。
たったこれだけ。にもかかわらずコードのほうはオブジェクト指向の体裁になるように汎用性を多少持たせてやや大げさに設計してみた。これを更に発展させれば低次元のベクトル空間を描けるものになるかもしれない。
HTMLのcanvas要素
矢印を描くためにHTML5 Canvasの2次元コンテキストのAPIを利用するので、HTML文書のbody要素内にcanvas要素を記述しておく必要があった。それは例えば次のようになる。
<canvas id="arrow" width="250" height="250"> このブラウザはHTML5 CanvasかJavaScritが有効ではありません。 </canvas>
canvasタグのid属性にはこのcanvas要素を一意に特定するための名前を指定する。ここではarrowとした。
widthはキャンバスの幅。heightはキャンバスの高さ。
HTML5 Canvasが有効でないときにはcanvasタグに挟まれた部分の文字列が表示される。
JavaScriptのコード
JavaScriptのコードはscriptタグで挟んだscript要素内に記述する。script要素はHTML文章のbody要素の後に置くことが望ましい。少なくともcanvas要素の後である必要がある。
</body> <script> // JavaScriptのコード </script> </html>
そうでなければ、例えばcanvas.jsと名付けた別のファイルに保存し、HTML文書内から次のように呼び出すこともできる。
<script type="text/javascript" src="arrow.js"></script>
矢印オブジェクトを返すファクトリ函数
まず矢印オブジェクトを設計した。オブジェクト・リテラルを返すファクトリ
ファクトリ函数ではreturn文にオブジェクト・リテラルないしはオブジェクト・リテラルを指し示す変数を記述する。
//Arrowオブジェクトを返すファクトリ函数 function Arrow(){ //プライベートなプロパティ return //オブジェクト・リテラル }
オブジェクト・リテラルとは1つまたは複数のプロパティをその値と共に波括弧によって挟む記法。JavaScriptにはクラスの概念がなく、オブジェクトをリテラルとして直接表記することができる。その記法はPythonの辞書に近い。
{プロパティ名:値, プロパティ名:値, メソッド名:匿名函数, メソッド名:匿名函数}
プロパティはその値に匿名の函数を持つこともできる。値に函数を持つプロパティはコード・プロパティやメソッドと呼ばれている。メソッドも函数を値として持つプロパティの一種。
プライベートなプロパティ
JavaScriptにはprivateやpublicやprotectedといったアクセス修飾子がこれまでのところない。アクセス修飾子はオブジェクトや函数の外からのアクセスを許可するかどうかを決めるキーワード。
その代わりにファクトリ函数ではreturn文によって返されるオブジェクト・リテラルの外に通常のローカル変数としてプロパティを記すことにより、それらのプロパティに対してオブジェクトの外から直接アクセスできないようにすることができる。
次のコードは矢印オブジェクトを返すファクトリ函数のローカル変数として指定したプロパティ。これらのローカル変数はその函数(ファクトリ函数では結果的にオブジェクト)の外から直接アクセスできないため、プライベートなプロパティとして働く。
function Arrow(){ //プライベートなプロパティとその値 const shaftTailX = 0; //幹末端の水平座標 const shaftTailY = 0; //幹末端の垂直座標 let shaftHeadX = 100; //幹先端の水平座標 const shaftHeadY = 0; //幹先端の垂直座標 let headLength = 7; //鏃の長さ let headAngle = 160; //鏃の角度 let arrowWidth = 1; //矢印の太さ let color = "black" //矢の色 return //オブジェクト・リテラル }
shaftTailXプロパティとshaftTailYプロパティは矢印の幹の末端部分のそれぞれ水平座標Xと垂直座標Yを表している。
shaftHeadXプロパティとshaftHeadYプロパティは矢印の幹の先端部分のそれぞれ水平座標Xと垂直座標Yを表している。
ここで言うところの先端とは矢印の方向を指し示す
それぞれの初期値はshaftTailXとshaftTailYを(0,0)とし、shaftHeadXとshaftHeadYを(100,0)とした。
shaftHeadXプロパティの値が矢印の幹の長さを事実上決める。それ以外の座標を表すプロパティ(ローカル変数)は変更されては困る設計にしたので、constをそれらの前に置いて定数化した。
ちなみにJavaScriptで変数の宣言を表すletとvarは函数の内部では同じ働きをするのでどちらを用いても構わない。
これらのプロパティの初期値によって矢印の幹の部分だけを描くと次のように水平な直線になる。
headLengthプロパティは矢印の
headAngleプロパティは矢印の鏃を構成する折れ曲がった2つの線分の角度を表すもの。
ただし鏃のこの角度の概念は常識的な直感にやや反しているかもしれないので、分かりやすくするために次の図によって角度ごとの鏃の状態の違いをならべて示してみた。
余談だけれど、Y字に似た二股の鏃を持った矢は実際に存在する。例えば音を出す
arrowWidthプロパティは矢印の線の太さを表している。ただしこの値を大きくしすぎると、特に最後の例ではっきり分かるように鏃部分の形が崩れてしまう。矢印オブジェクト(を返すファクトリ函数)を設計する段階での改善の余地があるが、今回はこのまま。
colorプロパティは文字通り矢印の色を表すもの。
オブジェクト・リテラルを返すreturn文
ファクトリ函数のローカル変数として記したプライベートなプロパティの後にはreturn文を置く。
//Arrowオブジェクトを返すファクトリ函数 function Arrow(){ //プライベートなプロパティ //return文 return{ //プロパティ名:値, //プロパティ名:値, //メソッド名:匿名函数 }; }
return文に置かれたプロパティとメソッドは、ファクトリ函数を介して作成されたオブジェクトの外部から直接アクセスすることができるので、オブジェクトの外に公開されたパブリックなものになる。
オブジェクト・リテラルにはまず、オブジェクト指向プログラミングらしくアクセサー・メソッドを記した。アクセサー・メソッドとはプライベートなプロパティにアクセスするために専用に設けるメソッドのこと。ここでは設定用のアクセサー、すなわちセッターだけを記述した。
function Arrow(){ //プライベートなプロパティ //オブジェクト・リテラルを返す return { //パブリックなアクセサーメソッド(セッター) setArrowLength : function(arg){shaftHeadX = arg}, setHeadLength : function(arg){headLength = arg}, setHeadAngle : function(arg){headAngle = arg}, setArrowWidth : function(arg){arrowWidth = arg}, setColor : function(arg){color = arg}, }; }
shaftHeadXプロパティの値が矢印の幹の長さを決めるため、そのアクセサー名はsetArrowLengthにした。
アクセサー・メソッドの名前(識別子)はもちろん任意だけれども、セッターの場合は「setプロパティ名」と名付けられていることが多い。ちなみに取得用のゲッターの場合は「getプロパティ名」と名付けられていることが多い。
各々の匿名函数の仮引数に指定したargはargumentの略で任意。parameterのparaとしても良かったのかもしれない。
オブジェクト・リテラルのコーディングで間違え易いのはイコールではなくコロンでプロパティと値との間を区切ることと、文の終わりがセミコロンではなくカンマで区切ること。これらを間違えていたり忘れていたりすることがよくある。その際、文法エラーとなる。
これらのアクセサー・メソッド(セッター)には見ての通り、値を外から受け取ってプライベートなプロパティに渡す以上の機能を持たせていない。
描画メソッド
アクセサー・メソッドの次に記したのは描画メソッド。メソッドの各々の仮引数にもコメントを付けるためにここではインデントと改行を使ったブロック記法で記述してみた。
return{ //アクセサー・メソッド draw : function( canvasId, //canvas要素のID属性 x, //座標系の原点の水平座標 y, //座標系の原点の垂直座標 angle //座標系の回転角度 ){ //... }
このメソッドは引数としてHTMLのcanvas要素に設定したID属性と座標系の原点となる水平座標xと垂直座標y、そしてその座標系を原点を軸に回転させたときの角度angleを受け取ることにした。
スクリーン座標系
矢印を描く上で方眼紙として働く座標系の原点と角度については少し説明を要すると思う。
HTML5 Canvasの2次元コンテキストで用いられる座標系はいわゆるスクリーン座標系と呼ばれているもの。スクリーン座標系はデフォルトでは左上端が(0,0)になり、下に向かうほど、または右に向かうほどそれらの値が大きくなる。
スクリーン座標系は次のようにデカルト座標系が逆立ちしたような構造になっている。
HTML5 Canvasのtranslate()というメソッドを使うとスクリーン座標系の原点(0,0)を移動できる。
HTML5 Canvasのrotate()というメソッドを使えば引数に角度を指定することによってそのスクリーン座標系をその原点を軸にして回転移動させることができる。ただし角度は弧度法(ラジアン)で指定する必要がある。
スクリーン座標系の原点の位置が動くとその上に描かれた矢印も動くので、これによって矢印を描く位置を変えることができる。スクリーン座標系を回転移動させるとその上に描かれた矢印も回転移動するので、これによって矢印の向きを変えることができる。
描画メソッドdraw()の仮引数にあるxとyは座標系の原点の位置を決め、translate()メソッドの引数として使う。
描画メソッドdraw()の仮引数にあるangleは座標系の角度を決め、rotate()メソッドの引数として使う。
次に描画メソッドdraw()の中身について記す。
まずはHTML5 Canvasの2次元コンテキストのAPIを利用するためのお決まりの文。描画メソッドの仮引数の1つcanvasIdはHTMLの特定のcanvas要素を取得するためにここで使う。
//HTML5 Canvas 2DのAPIを利用する準備 const canvas = document.getElementById(canvasId); const ctx = canvas.getContext("2d");
スクリーン座標系の状態の保存と復元
translate()を使って原点を移動させる前にスクリーン座標系のデフォルト状態を保存しておく必要がある。それにはHTML5 Canvasのsave()というメソッドを利用する。そして描画メソッドの終わりでrestore()というメソッドを呼び出し、save()メソッドによって保存しておいた状態を復元する。
この保存と復元の手続きを省くと描画メソッドを繰り返し呼び出したときに想定外の副作用が生じてしまう。
draw : function(canvasId,x,y,angle){ //HTML5 Canvas 2DのAPIを利用する準備 const canvas = document.getElementById(canvasId); const ctx = canvas.getContext("2d"); //この時点の状態を保存 ctx.save(); //... //save()で保存した状態を復元 ctx.restore(); }
save()メソッドでスクリーン座標系のデフォルト状態を保存した後、translate()メソッドによって原点を移動させる処理をする。その仮引数となるx(水平座標)とy(垂直座標)にはこの描画メソッドであるdraw()を呼び出すときに受け取る引数の水平座標xと垂直座標yが収まる。
そしてrotate()メソッドによって原点を軸にしてスクリーン座標系を回転移動させる。draw()を呼び出すときに受け取る引数のangleがrotate()メソッドの引数に収まる。
//この時点の状態を保存 ctx.save(); //スクリーン座標系の原点を移動 ctx.translate(x,y); //スクリーン座標系を回転移動 ctx.rotate(-1 * toRadians(angle));
rotate()メソッドの引数に与える角度はラジアンという単位である必要があるのでtoRadians()函数を呼び出して度数法を弧度法に変換させるようにした。このtoRadians()函数は自作したもの。
更にそこで-1を掛けているのは、角度の値の正負の符号を反転させ、角度が反時計回りに計られるようにしたため。これをしないと反時計回りに計られる通常の角度とは違い、なぜだか時計回りに角度が計られてしまうので、angleに値を渡すときに混乱すると思う。
次にHTML5 Canvasの2次元コンテキストのAPIを利用して矢印の幹に当たる直線を描くコードを書く。
//矢印の幹の直線を描く ctx.beginPath(); //パスの初期化 ctx.lineWidth = arrowWidth; //線の太さ ctx.strokeStyle = color; //線の色 ctx.moveTo(shaftTailX, shaftTailY); //線の始点座標 ctx.lineTo(shaftHeadX, shaftHeadY); //線の終点座標 ctx.stroke(); //パスに従って直線を描く
線を描く通り道(パス)をbeginPath()メソッドによってまずは初期化しておく必要がある。
HTML5 CanvasのlineWidthプロパティに線の太さを表すために用意しておいたarrowWidthプロパティの値を代入。
HTML5 CanvasのstrokeStyleプロパティに線の色を表すために用意しておいたcolorプロパティの値を代入。
線の始点となる水平座標と垂直座標をHTML5 CanvasのmoveTo()メソッドの引数に代入。
線の終点となる水平座標と垂直座標をHTML5 CanvasのlineTo()メソッドの引数に代入。
線の通り道(パス)が決まったところでHTML5 Canvasのstroke()メソッドを呼び出して直線を描く。
次に描くのは矢印の鏃を形作るために矢印の先端から少し角度を変えて伸びる2つの線分。
この鏃の部分を描くのに今回は数学的な技術をちょっとだけ使ってみた。こうする必要はなかったのだけれども数学的技術を導入することで鏃の長さや角度を自由に変えられるように汎用性を多少与えてみた。
数学的な変換を行う函数
数学的手続き(アルゴリズム)に汎用性を持たせるためにそれらを独立した3つの函数として宣言した。
その1つが角度を度数法の単位から弧度法の単位へと変換する函数であるtoRadians()。度数法の値dを弧度法の値rへ変換するための公式は例えば次のとおり。
度数法の値に円周率πを掛けてその解を180で割るという簡単な計算。
ラジアンは弧度法という角度の測定法に基づいた角度の単位のこと。弧度法では円の半径の長さを1ラジアンとして円弧の距離を測り、反時計回りに半回転するとちょうど円周率πと同じ値になる。反時計回りに一回転するとちょうど円周率πの2倍になる。つまり度数法での180度がπになり、360度が2πになる。
この函数は入力値として度数法の値degreeを受け取り、上の式による計算をした後、その解である弧度法の値radianを出力する。
この函数をJavaScriptで表すと例えば次のようになる。
//度数法を弧度法に変換する函数 function toRadians(degree){ return degree * (Math.PI/180.0); }
残りの2つの函数は2次元平面の極座標系である円座標系を直交座標系に変換するもの。円座標系は半径の長さと角度で定義された座標系であるので、この座標系を直交座標系に変換すれば線分の長さと角度から直交座標系での座標(x,y)を得ることができる。
線分の長さをr、水平座標をx、垂直座標をy、角度を
要するに、直交座標系での水平座標xは原点までの直線距離を斜辺rとする直角三角形の余弦函数
ここで三角函数(三角比)についてのちょっとしたおさらい。
三角函数と呼ばれているものは実は三角法函数のことであり、その言葉通りに三角法に基づいている。語源を辿ると三角形測量函数と呼ぶこともできる。
三角法を用いると直接には測量できない物の長さや区間の距離を推定することができる。
三角法を理解するためには次のように半径を1とする単位円に基づいて描いた座標系を理解するのが良い方法であると思う。次の図は2次元平面の直交座標系と2次元平面の極座標系である円座標系とを重ね合わせるように描いたもの。
上の図では点線の
三角法で重要なのは時計の針が時刻3時の始点から反時計回りにどれだけ回転移動した位置にあるかということ。その角度θが常に直角三角形の内角になるとは限らない。角度θが度数法で90度を超えると、すなわち
三角法は直角三角形によって特徴付けられているよりは直交座標系と極座標系(円座標系)との互換性によって特徴付けられている。
ちなみに極座標系とは半径rと角度座標θから位置を定める次のような座標系のこと。極座標系では原点は極と呼ばれており、半径rは
三角法函数とは、角度θの値を決めてやると、点Pのx座標とy座標と半径(動径)rの長さとのいずれかの2対を取り出したときのその比率が一意に定まる関係を意味している。
各々の入力の値の集合に応じて出力の値が常に一定になるので函数と見なすことができる。同じ入力値を与えているのにその度に一定しない気まぐれな出力値が返ってくるようだとそれを函数と見なすことはできない。函数とは入力値と出力値との間の因果関係がはっきりしている関係であると考えることができる。
頻繁に使われる最も代表的でかつ汎用性を持った三角法函数は3種類あり、ここではその内の2種類を利用する。その2種類とは
数学的な表記法では、正弦函数は
正弦函数は角度θが与えられたときに斜辺に対して対辺がどのくらいの比率になるのかを示している。余弦函数は角度θが与えられたときに斜辺に対して隣辺がどのくらいの比率になるのかを示している。
- 正弦函数
- 角度θを引数として入力したとき、正弦函数は直角三角形の斜辺の長さに対する対辺の長さの割合(比率)を出力する。
- もしくは円の半径rに対する点Pのy座標の割合(比率)を出力する。
- したがってこのことは、正弦函数が出力した比率に斜辺の長さ(半径r)を掛ければ対辺の長さ(y座標)が得られることを意味している。
- 余弦函数
- 角度θを引数として入力したとき、余弦函数は直角三角形の斜辺の長さに対する隣辺の長さの割合(比率)を出力する。
- もしくは円の半径rに対する点Pのx座標の割合(比率)を出力する。
- したがってこのことは、余弦函数が出力した比率に斜辺の長さ(半径r)を掛ければ隣辺の長さ(x座標)が得られることを意味している。
- 正接函数
- 角度θを引数として入力したとき、正接函数は直角三角形の隣辺の長さに対する対辺の長さの割合(比率)を出力する。
- もしくは点Pのx座標に対するy座標の割合(比率)を出力する。
- したがってこのことは、正接函数が出力した比率に隣辺の長さ(x座標)を掛ければ対辺の長さ(y座標)が得られることを意味している。
要するに、斜辺の長さ(半径rまたは動径r)と角度θが分かっているとき、正弦函数によって対辺の長さ(y座標)を計算でき、余弦函数によって隣辺の長さ(x座標)を計算することができる。
そういう訳で正弦函数と余弦函数を利用すると極座標系(円座標系)を直交座標系に変換することができる。
極座標から直交座標への変換を式にして表すと例えば次のようになる。
これをJavaScriptの函数にしてみると次のように書けると思う。
/* 極座標(円座標)を直交座標へ変換する函数 引数は極座標r,θ(θの単位はラジアン) */ function polarToCartesianX(r,radian){ return r * Math.cos(radian); } function polarToCartesianY(r,radian){ return r * Math.sin(radian); }
x座標(水平座標)を返す函数とy座標(垂直座標)を返す函数の2つを宣言した。それぞれpolarToCartesianX()とpolarToCartesianY()と名付けた。
これらの函数内では余弦函数と正弦函数を記すためにJavaScriptの組み込みメソッドであるMath.cos()とMath.sin()を利用した。
以上の3つの函数はどこからでも自由に呼び出せるように特定の函数や特定のオブジェクトの内部に閉じ込めず、外に置いてグローバル・スコープにした。
矢印の鏃の部分を描く方法
矢印オブジェクトを返すファクトリ函数に話を戻す。そのreturn文によって返すオブジェクト・リテラルの中に記したdrawという描画メソッドの続き。
HTML5 Canvasの2次元コンテキストのAPIを利用して矢印の幹の部分の次に描くのは
矢の幹の先端から上下対称(左右対称)に両側に伸びる2つの線分を描く。始点は矢の幹の先端だとして、終点になる座標はそれらの線分の長さと角度から相対的に計算するようにした。
それらのコードは次のようになった。
draw : function(canvasId,x,y,angle){ //... //鏃の相対的座標を計算して各変数に代入 const headLeftWingX = polarToCartesianX(headLength, toRadians(headAngle)) + shaftHeadX; const headLeftWingY = polarToCartesianY(headLength, toRadians(headAngle)) + shaftHeadY; const headRightWingX = polarToCartesianX(headLength, toRadians(-1 * headAngle)) + shaftHeadX; const headRightWingY = polarToCartesianY(headLength, toRadians(-1 * headAngle)) + shaftHeadY; //... }
鏃を形作る2本の線分のそれぞれ終点を表す座標を入れるために、headLeftWingXとheadLeftWingY、そしてheadRightWingXとheadRightWingYという4つの変数を用意した。Xが水平座標、Yが垂直座標。
鏃を形作る2本の線分の終点を表す座標をその角度とその長さから相対的に決めるために極座標系(円座標系)を直交座標系に変換する2つの函数を呼び出して利用した。線分の長さと角度を引数として渡すと水平座標xを出力するpolarToCartesianX()と、同様に引数を与えると垂直座標yを出力するpolarToCartesianY()がそれら。
極座標系(円座標系)を直交座標系に変換するこれら2つの函数には鏃を形作る線分の長さを表すheadLengthと角度を表すheadAngleという引数を変数で与えている。ただし線分のもう片方に与える角度には-1を掛けて負数し、時計回りに角度を指定する必要があった。
これらの2つの函数に与える角度は弧度法で指定する必要があるので、同じく自作したtoRadians()函数を呼び出して利用した。
しかしこれだけだとまだ思ったところに鏃が描画されず、鏃が異常に伸びてしまったので、極座標を直交座標に変換する函数が返した値に矢印の幹の先端の水平座標を表すshaftHeadXの値を更に加算する必要があった。
これで鏃を描く準備が整った。
次のコードは鏃の部分をHTML5 Canvasの2次元コテンテキストのAIPを利用して描画するもの。
draw : function(canvasId,x,y,angle){ //... //鏃を描く ctx.beginPath(); //パスの初期化 //パスの指定 ctx.moveTo(shaftHeadX, shaftHeadY); ctx.lineTo(headLeftWingX, headLeftWingY); ctx.moveTo(shaftHeadX, shaftHeadY); ctx.lineTo(headRightWingX, headRightWingY); ctx.stroke(); //描画 //... }
まずHTML5 CanvasのbeginPath()メソッドによってパスを初期化。
次にHTML5 CanvasのmoveTo()メソッドに鏃を形作る線分の始点となる座標を与えた。つまり鏃の幹の先端の座標が入ることになるshaftHeadXとshaftHeadY。
HTML5 CanvasのlineTo()メソッドの引数には鏃を形作る線分の終点となる座標を与えた。極座標を直交座標に変換する2つの函数であるpolarToCartesianX()またはpolarToCartesianY()を呼び出して計算する相対的座標を入れる4つの変数はここで使った。
こうしておいてHTML5 Canvasのstroke()メソッドを呼び出して鏃を描画させるようにした。
最後にrestore()メソッドを呼び出してsave()メソッドで保存しておいたスクリーン座標系の元の状態を復元すれば波括弧でブロックを閉じてdrawメソッドは完成。
//save()で保存した状態を復元 ctx.restore();
これで矢印オブジェクトを返すファクトリ函数も波括弧でブロックを閉じれば完成する。
矢印オブジェクト・リテラルを返すファクトリ函数Arrowのコード全体は次のようになる。
//Arrowオブジェクトを返すファクトリ函数 function Arrow(){ //プライベートなプロパティ const shaftTailX = 0; //幹末端の水平座標 const shaftTailY = 0; //幹末端の垂直座標 let shaftHeadX = 100; //幹先端の水平座標 const shaftHeadY = 0; //幹先端の垂直座標 let headLength = 7; //鏃の長さ let headAngle = 160; //鏃の角度 let arrowWidth = 1; //矢印の太さ let color = "black" //矢の色 //オブジェクト・リテラルを返す return{ //パブリックなアクセサーメソッド(セッター) setArrowLength : function(arg){shaftHeadX = arg}, setHeadLength : function(arg){headLength = arg}, setHeadAngle : function(arg){headAngle = arg}, setArrowWidth : function(arg){arrowWidth = arg}, setColor : function(arg){color = arg}, /*描画メソッド: 引数はcanvas要素のIDと描画座標と描画角度*/ draw : function(canvasId,x,y,angle){ //HTML5Canvas2DのAPIを利用する準備 const canvas = document.getElementById(canvasId); const ctx = canvas.getContext("2d"); //この時点の状態を保存 ctx.save(); //座標系の原点を移動 ctx.translate(x,y); /* 座標系を原点を軸に回転移動 かつ正負を反転させて反時計回りに */ ctx.rotate(toRadians(-1 * angle)); //矢印の幹の直線を描く ctx.beginPath(); ctx.lineWidth = arrowWidth; ctx.strokeStyle = color; ctx.moveTo(shaftTailX,shaftTailY); ctx.lineTo(shaftHeadX,shaftHeadY); ctx.stroke(); //鏃の部分を描く //鏃の相対的座標を計算して各変数に代入 const headLeftWingX = polarToCartesianX(headLength, toRadians(headAngle)) + shaftHeadX; const headLeftWingY = polarToCartesianY(headLength, toRadians(headAngle)) + shaftHeadY; const headRightWingX = polarToCartesianX(headLength, toRadians(-1 * headAngle)) + shaftHeadX; const headRightWingY = polarToCartesianY(headLength, toRadians(-1 * headAngle)) + shaftHeadY; //鏃を描く ctx.beginPath(); ctx.moveTo(shaftHeadX,shaftHeadY); ctx.lineTo(headLeftWingX,headLeftWingY); ctx.moveTo(shaftHeadX,shaftHeadY); ctx.lineTo(headRightWingX,headRightWingY); ctx.stroke(); //save()で保存した状態を復元 ctx.restore(); } }; }
ラベル・オブジェクトを返すファクトリ函数
矢印になんらかのラベル付けを行いたかったのでラベル・オブジェクトを返すファクトリ函数も作ってみた。
//ラベル・オブジェクトを返すファクトリ函数 function Label(){ //プライベートなプロパティ let font = "15px serif"; //フォント属性 let color = "black"; //文字の色 let align = "center"; //文字の水平位置 let baseline = "middle"; //文字の垂直位置 //オブジェクト・リテラルを返す return { //アクセサー(セッター) setFont : function(arg){font = arg;}, setColor : function(arg){color = arg;}, setAlign : function(arg){align = arg;}, setBaseline : function(arg){baseline = arg;}, //描画メソッド(ID,文字,x座標,y座標,最大文字幅) draw : function(canvasId,text,x,y,largestWidth){ //HTML5 Canvas 2DのAPIを利用するための準備 const canvas = document.getElementById(canvasId); const ctx = canvas.getContext("2d"); //文字列を描画する ctx.font = font; //フォント ctx.strokeStyle = color; //色 ctx.textAlign = align; //文字の整列 ctx.textBaseline = baseline; // //文字を描画 ctx.fillText(text,x,y,largestWidth); } }; }
fontプロパティの15pxが文字の大きさを意味している。
それらのプロパティはプライベート領域に置いてローカル変数にし、アクセサー・メソッドのセッターを介して値を変更できるようにした。
そしてdrawメソッドではHTML5 Canvasの2次元コンテキストのAPIを呼び出して指定した座標にラベル付けする文字列を描画できるようにした。
オブジェクトの生産と利用
ファクトリ函数はオブジェクト量産機なので、これを呼び出して任意の変数に代入すれば、オブジェクト(インスタンス変数)を次々に作り出すことができる。
描きたいのは色違いの3つの矢印なので、3つのインスタンス変数を宣言して3つの矢印オブジェクトを生産することにした。
//矢印オブジェクトを生産 const arrow1 = Arrow(); const arrow2 = Arrow(); const arrow3 = Arrow();
ファクトリ函数が返すのはオブジェクト・リテラルであるので、コンストラクター函数やクラス函数と違い、new演算子を代入演算子と一緒にそこで用いないことに注意。
で、ここで生産した矢印オブジェクトのインスタンス変数のdrawメソッドを呼び出して矢印を描いてみた。
drawメソッドの引数には、第1にcanvas要素のID属性、第2に描画位置を表すx座標、第3に描画一を表すy座標、第3にその角度を渡す必要がある。
ただし描画位置の座標は矢印の末端(尾部)の位置を表す。
角度の値を正数にすると反時計回りに角度を計り、負数にすると時計回りに角度を計る。
//矢印オブジェクトの生産 const arrow5 = Arrow(); const arrow6 = Arrow(); const arrow7 = Arrow(); //セッターを呼び出して色を設定 arrow5.setColor("green"); arrow6.setColor("#ff00ff"); arrow7.setColor("rgb(0,128,128)"); //描画メソッドを呼び出して描画 //引数はcanvas要素ID,x座標,y座標,角度 arrow5.draw("arrow",100,100,0); arrow6.draw("arrow",100,100,150); arrow7.draw("arrow",100,100,-90);
矢印の長さを変更するには矢印オブジェクトのセッターsetArrowLength()メソッドを呼び出す。
//矢印の長さを変更 arrow5.setArrowLength(200); arrow6.setArrowLength(200); //描画メソッドdrawを呼び出して矢印を描画 arrow5.draw("arrow",0,100,0); arrow6.draw("arrow",100,200,90); arrow7.draw("arrow",100,100,45);
鏃の長さを変更するには矢印オブジェクトのセッターsetHeadlength()を呼び出す。
鏃の角度を変更するには矢印オブジェクトのセッターsetHeadAngle()を呼び出す。
//矢印オブジェクトを生産 const arrow8 = Arrow(); const arrow9 = Arrow(); const arrow10 = Arrow(); //鏃の長さを変更 arrow8.setHeadLength(20); arrow9.setHeadLength(30); arrow10.setHeadLength(50); //鏃の角度を変更 arrow8.setHeadAngle(45); arrow9.setHeadAngle(90); arrow10.setHeadAngle(150); //矢印を描画 arrow8.draw("arrow",0,30,0); arrow9.draw("arrow",0,100,0); arrow10.draw("arrow",0,180,0);
各々の矢印にx,y,zのラベル付けを行いたい場合、ラベル・オブジェクトを返すファクトリ函数Levelを呼び出して利用する。
ラベル・オブジェクトの描画メソッドdrawの引数には、第1にcanvas要素のID、第2にラベルの文字列、第3にラベルを描く位置のx座標、第4にラベルを描く位置のy座標、第5にラベルの最大幅を渡す。
//矢印オブジェクトを生産 const arrow11 = Arrow(); const arrow12 = Arrow(); const arrow13 = Arrow(); /* 色設定メソッドを呼び出して矢の色を指定し、 描画メソッドを呼び出して矢印を描く */ arrow11.setColor("red"); arrow11.draw("arrow",75,115,90); arrow12.setColor("blue"); arrow12.draw("arrow",75,115,0); arrow13.setColor("green"); arrow13.draw("arrow",75,115,230); //ラベル・オブジェクトを作成 const label1 = Label(); //drawメソッドを呼び出して描く label1.draw("arrow","x",5,200,10); label1.draw("arrow","y",185,115,10); label1.draw("arrow","z",75,5,10);
コメント
コメントを投稿