6.9 グラフィックキャンバスとテンプレートリテラル

(1/1)
ピアソン・スクエア法
HTML5で追加になった canvas要素を利用することで、画面上にベクトル画像を表示できる。
今回は、パティシエが菓子作りをする際に必要な濃度のクリームを得るために使うピアソン・スクエア法とビジネスでお馴染みのマズローの欲求五段階説レンズの絞りイラストを題材にして、canvas要素の使い方とテンプレートリテラルについて学ぶ。
マズローの欲求五段階説
図形を描くのに三角関数逆三角関数を活用する。三角関数は、以前は中学数学で習ったのだが、現在は高校数学で初めて登場する。苦手な方や応用力を付けたい方は、高校数学の参考書や問題集を手元に置いておくことをお勧めする。
レンズの絞り
左図のレンズの絞りイラストは、直角三角形の性質を前提に、タートルグラフィックスの基礎を学ぶとともに、インタラクティブにイラストの形状を変化させる手法を紹介する。

目次

サンプル・プログラム

圧縮ファイルの内容
PearsonSquareMethod.htmlピアソン・スクエア法を描くサンプル・プログラ
Maslow.htmlマズローの欲求5段階説を描くサンプル・プログラム
lensIris.htmlレンズ絞りイラストを描くサンプル・プログラム
pahooClipboard.jsクリップボードを使うためのJavaScriptライブラリ。
MaslowCSS.htmlコラムにあるマズローの欲求5段階説を描くサンプル・プログラム
PearsonSquareMethod.html 更新履歴
バージョン 更新日 内容
1.1.0 2023/08/02 変数名等を見直し
1.0.0 2023/03/25 初版
Maslow.html 更新履歴
バージョン 更新日 内容
1.1.0 2023/08/02 drawMaslow() forEachループに変更
1.0.0 2023/03/26 初版
pahooClipboard.js 更新履歴
バージョン 更新日 内容
1.0 2021/08/31 初版

グラフィックキャンバス

ピアソン・スクエア法
ピアソン・スクエア法については後述するが、この計算法では左図のような図形を用いる。
これをHTMLで実装しようとすると、外枠(四角形)はTABLEタグで実現できそうだが、交差する赤い矢印はグラフィックを使わざるを得ない。矢印をビットマップ画像として作成し、IMGタグを使って配置するという方法が考えられる。
しかし今回は、外枠を任意のサイズに拡大縮小できるようにしたい。そうなると、ビットマップ矢印の形が歪んでしまう。
そこでcanvas要素の出番である。

HTML5で導入されたcanvas要素は、HTMLとJavaScriptを使って画面上のベクター画像を描画することができる仕組みだ。ビットマップ画像と異なり、描画領域を拡大縮小しても画像が歪んだり荒れたりすることがない。HTML5に対応したモダン・ブラウザであれば、プラグインをインストールする必要なく、画面上にグラフィックを描くことができる。
ただし、canvas要素が標準で用意している図形は、直線、折れ線・多角形、ベジエ曲線、矩形(四角形)、円・円弧、楕円・楕円弧と限られている。今回必要となる矢印や、よく使う星型や円筒形などは、これらの標準図形をJavaScriptで組み合わせて作り出さなければならない(ネット上に数多くのフリーのJavaScriptライブラリはある)。
<canvas id="graph" width="600" height="400"></canvas>
canvas要素はこのように指定する。この例では、幅600ピクセル、高さ400ピクセルの領域でグラフィックを描くことができるcanvas要素を、id名 graphとして用意した。id名を変えれば、1つの画面に複数のcanvas要素を配置することもできる。

テンプレートリテラル

今回、ピアソン・スクエア法の図形を任意のサイズにしたい。また、canvas要素の上に文字やテキストボックスを重ね合わせなければならない。それぞれの位置は可変――外枠の大きさに応じて適切な位置に配置する必要がある――このため、CSSのpositionに変数を適用する必要がある。
JavaScriptを使って、これらの要素に変数を代入してやるという方法もあるが、HTML要素が配置された後に代入しなければならず、タイミング調整が厄介である。
そこで、テンプレートリテラルを使って、直接HTML文に変数を埋め込むことにした。

PearsonSquareMethod.html

 133:     document.getElementById('square').innerHTML = `
 134: <canvas id="graph" width="${width}" height="${height}" style="z-index:${zIndex1};"></canvas>
 135: <div style="position:absolute;left:${leftPoint}px;top:${topPoint}px;width:${blockWidth1}px;background-color:${bgcolor1};z-index:${zIndex2};">クリームAの濃度&nbsp;<input id="A" name="A" type="text" style="width:${blockWidth3}px;">%</div>
 136: <div style="position:absolute;left:${leftPoint}px;top:${bottomPoint}px;width:${blockWidth1}px;background-color:${bgcolor1};z-index::${zIndex2};">クリームBの濃度&nbsp;<input id="B" name="B" type="text" style="width:${blockWidth3}px;">%</div>
 137: <div style="position:absolute;left:${middleLeftPoint}px;top:${middleTopPoint}px;width:${blockWidth2}px;background-color:${bgcolor1};z-index:${zIndex2};;" >
 138: 欲しいクリームの濃度&nbsp;<input id="C1" name="C1" type="text" style="width:${blockWidth3}px;">%<br>
 139: 欲しいクリームの重量&nbsp;<input id="C2" name="C2" type="text" style="width:${blockWidth3}px;">g<br>
 140: クリームAの重量&nbsp;<span id="C3" name="C3" style="background-color:${bgcolor2};"></span>g<br>
 141: クリームBの重量&nbsp;<span id="C4" name="C4" style="background-color:${bgcolor2};"></span>g<br>
 142: </div>
 143: <div style="position:absolute;left:${rightPoint}px;top:${topPoint}px;width:${blockWidth1}px;;background-color:#FFFFFF;z-index:${zIndex2};">クリームBの割合&nbsp;<span id="D" name="D" style="background-color:${bgcolor2};"></span></div>
 144: <div style="position:absolute;left:${rightPoint}px;top:${bottomPoint}px;width:${blockWidth1}px;background-color:#FFFFFF;z-index:${zIndex2};">クリームAの割合&nbsp;<span id="E" name="E" style="background-color:${bgcolor2};"></span></div>
 145: 
 146: `;

テンプレートリテラルとは、JavaScript ES6で導入された仕組みで、文字列を拡張したものと考えてほしい。文字列と異なり、記述した改行や制御文字をそのまま格納することができる。テンプレートリテラルはグレイヴ・アクセント ` ではじまり、グレイヴ・アクセントで終わる。
テンプレートリテラルのもう1つの特長は、その中に変数を埋め込めることだ。${変数名} として埋め込むことができる。こうすると、実行時に変数の値が展開される。

上述のテンプレートリテラルは、canvas要素の幅や高さ、DIVタグの配置座標を変数として、id名squereの要素(実際にはDIVタグ)の中にHTMLタグとして追加する。window.onloadのタイミングでこの処理を実行すれば、任意の大きさのcanvas要素と文字列、テキストボックスを画面に配置できる。

矢印を描く

前述の通り、canvas要素には矢印を描くメソッドがない。そこで、直線を3本組み合わせて矢印を描くユーザー関数 drawArrowを用意することにした。絶対座標を指定する方法だと傾いた矢印を描くのが難しいので、矢印の開始・終了座標、矢印の角度(開き具合)、矢印部分の長さを指定することで矢印を描く関数をつくる。
矢印を描く
上図の赤い直線が矢印である(説明の都合で矢印の片方しか描いていない)。
矢印の直線部分 \( P_1 - P_2 \) は直線描画できる。
工夫が要るのは、矢印部分 \( P_2 - P_3 \) である。
矢印の始点 \( P_2(x_2, y_2) \) は決まった値だが、矢印の終点 \( P_3(x_3, y_3) \) は計算で求めなければならない。

直線部分の角度 \( r_1 \) は逆三角関数を使い、\( \displaystyle r_1 = tan^{-1}(\frac{y_2 - y_1}{x_2 - x_1}) \) で求めることができる。
矢印部分の角度 \( r_2 \) は、あらかじめ指定した値であるから、矢印部分の長さを \( l \) とすると \( P_3(x_3, y_3) \) の座標は \( x_3 = x_2 - l \cdot cos(r_1 + r_2) \),\( y_3 = y_2 - l \cdot sin(r_1 + r_2) \) で求めることができる。
もう一方の矢印は、直線部分をはさんで反対側に引けばよいので、角度は \( r_1 - r_2 \) で計算する。

これをJavaScriptで実装したユーザー関数 drawArrowを示す。

PearsonSquareMethod.html

  67: /**
  68:  * 指定したcanvas要素に矢印を描く.
  69:  * @param   object ctx  canvasオブジェクト
  70:  * @param   int x1, y1  矢印の始点座標
  71:  * @param   int x2, y2  矢印の終点座標
  72:  * @param   int ang     矢印の確度(度)
  73:  * @param   int len     矢印線の長さ
  74:  * @return  なし
  75: */
  76: function drawArrow(ctx, x1, y1, x2, y2, ang, len) {
  77:     //直線部分を描く.
  78:     ctx.beginPath();
  79:     ctx.moveTo(x1, y1);
  80:     ctx.lineTo(x2, y2);
  81:     ctx.stroke();
  82: 
  83:     //直線部分の角度を求める.
  84:     let rad1 = Math.atan2(y2 - y1, x2 - x1);
  85:     //矢印線の角度を求める.
  86:     let rad2 = ang * Math.PI / 180;
  87: 
  88:     //矢印線1を描く
  89:     ctx.beginPath();
  90:     ctx.moveTo(x2, y2);
  91:     ctx.lineTo(x2 - len * Math.cos(rad1 + rad2), y2 - len * Math.sin(rad1 + rad2));
  92:     ctx.stroke();
  93: 
  94:     //矢印線2を描く
  95:     ctx.beginPath();
  96:     ctx.moveTo(x2, y2);
  97:     ctx.lineTo(x2 - len * Math.cos(rad1 - rad2), y2 - len * Math.sin(rad1 - rad2));
  98:     ctx.stroke();
  99: }

strokeRect メソッドで外枠を描き、HTMLタグを使って文字列とテキストボックスを配置し、矢印を描く処理はユーザー関数 drawSquare で行っている。

PearsonSquareMethod.html

 101: /**
 102:  * canvas要素を使ってピアソン・スクエア法のフレームを描く.
 103:  * @param   なし
 104:  * @return  なし
 105: */
 106: function drawSquare() {
 107:     //フレーム全体
 108:     let width  = WIDTH;
 109:     let height = HEIGHT;
 110:     //各ブロックの幅
 111:     let blockWidth1 = 200;
 112:     let blockWidth2 = 240;
 113:     let blockWidth3 = 40;
 114:     //配置座標
 115:     let margin = 10;
 116:     let leftPoint   = margin;
 117:     let rightPoint  = WIDTH - blockWidth1 - margin;
 118:     let topPoint    = margin;
 119:     let bottomPoint = HEIGHT - 40 - margin;
 120:     let middleLeftPoint = ((WIDTH - blockWidth2) / 2).toFixed();
 121:     let middleTopPoint  = ((HEIGHT - 100) / 2).toFixed();
 122:     //重なり順序
 123:     let zIndex1 = 10;
 124:     let zIndex2 = zIndex1 + 10;
 125:     //背景色(等価色)
 126:     let bgcolor1 = 'rgba(255, 255, 255, 0.8)';
 127:     let bgcolor2 = 'rgba(255, 255,   0, 0.8)';
 128: 
 129:     document.getElementById('square').style.width  = WIDTH  + 'px';
 130:     document.getElementById('square').style.height = HEIGHT + 'px';
 131:     document.getElementById('square').style.marginBottom = '10px';
 132: 
 133:     document.getElementById('square').innerHTML = `
 134: <canvas id="graph" width="${width}" height="${height}" style="z-index:${zIndex1};"></canvas>
 135: <div style="position:absolute;left:${leftPoint}px;top:${topPoint}px;width:${blockWidth1}px;background-color:${bgcolor1};z-index:${zIndex2};">クリームAの濃度&nbsp;<input id="A" name="A" type="text" style="width:${blockWidth3}px;">%</div>
 136: <div style="position:absolute;left:${leftPoint}px;top:${bottomPoint}px;width:${blockWidth1}px;background-color:${bgcolor1};z-index::${zIndex2};">クリームBの濃度&nbsp;<input id="B" name="B" type="text" style="width:${blockWidth3}px;">%</div>
 137: <div style="position:absolute;left:${middleLeftPoint}px;top:${middleTopPoint}px;width:${blockWidth2}px;background-color:${bgcolor1};z-index:${zIndex2};;" >
 138: 欲しいクリームの濃度&nbsp;<input id="C1" name="C1" type="text" style="width:${blockWidth3}px;">%<br>
 139: 欲しいクリームの重量&nbsp;<input id="C2" name="C2" type="text" style="width:${blockWidth3}px;">g<br>
 140: クリームAの重量&nbsp;<span id="C3" name="C3" style="background-color:${bgcolor2};"></span>g<br>
 141: クリームBの重量&nbsp;<span id="C4" name="C4" style="background-color:${bgcolor2};"></span>g<br>
 142: </div>
 143: <div style="position:absolute;left:${rightPoint}px;top:${topPoint}px;width:${blockWidth1}px;;background-color:#FFFFFF;z-index:${zIndex2};">クリームBの割合&nbsp;<span id="D" name="D" style="background-color:${bgcolor2};"></span></div>
 144: <div style="position:absolute;left:${rightPoint}px;top:${bottomPoint}px;width:${blockWidth1}px;background-color:#FFFFFF;z-index:${zIndex2};">クリームAの割合&nbsp;<span id="E" name="E" style="background-color:${bgcolor2};"></span></div>
 145: 
 146: `;
 147: 
 148:     //canvas要素
 149:     var ctx = document.getElementById('graph').getContext('2d');
 150:     ctx.lineWidth = 5;
 151:     //外枠(四角形)を描く
 152:     ctx.strokeRect(0, 0, WIDTH, HEIGHT);
 153: 
 154:     //矢印を描く
 155:     //配置座標
 156:     let marginArrow = 60;
 157:     let leftArrow   = marginArrow;
 158:     let rightArrow  = WIDTH - marginArrow;
 159:     let topArrow    = marginArrow;
 160:     let bottomArrow = HEIGHT - marginArrow;
 161:     let radArrow    = 30;
 162:     let lenArrow    = 20;
 163:     ctx.lineWidth = 5;
 164:     ctx.strokeStyle = 'red';
 165:     drawArrow(ctx, leftArrow, topArrow, rightArrow, bottomArrow, radArrow, lenArrow);
 166:     drawArrow(ctx, leftArrow, bottomArrow, rightArrow, topArrow, radArrow, lenArrow);
 167: }

まず、矢印の直線部分を描く。
beginPath メソッドを使い、これ以降に描く線分が連続するパスであることを示す。次に moveTo メソッドを使って、描画開始位置を移動する。lineTo メソッドによって、現在位置から指定位置までパスを伸ばす。最後に stroke メソッドを使うと、これまで指定したパスを結ぶ折れ線を描く。

ピアソン・スクエア法

パティシエが菓子作りをする際、乳脂肪分の濃度C1%のクリームをC2グラムが欲しいのだが、手持ちにそれが無い場合、手持ちにあるC1より濃い濃度A%のクリームとC1より薄い濃度B%のクリームを混ぜてC1%のクリームに仕立てることがある。このときにピアソン・スクエア法という計算法を使う。この方法は、クリームに限らず、チョコレートの濃度計算でも利用できる。
本節では、この計算法の証明をしていく。
ピアソン・スクエア法
混ぜ合わせるクリームAの割合を \( D = A - C_1 \)、クリームBの割合を \( E = C_1 - B \) とする。
このとき、クリームAの分量(重さ)は \( \displaystyle A2 = D \frac{A}{A + B} \)、クリームBの分量(重さ)は \( \displaystyle B2 = E \frac{B}{A + B} \) で求めることができる。

次に、混ぜ合わせるクリームAの割合を \( D = A - C_1 \)、クリームBの割合を \( E = C_1 - B \) とする。
このとき、クリームAの分量(重さ)は \( x = D \frac{A}{A + B} \)、クリームBの分量(重さ)は \( \displaystyle y = E \frac{B}{A + B} \) で求めることができる。

これがピアソン・スクエア法である。
連立方程式を立てて、ピアソン・スクエア法が正しいことを証明してみよう。
\[
\begin{align}
\frac{Ax + By}{C_2} &= C_1 \tag{1} \\
x + y &= C_2 \tag{2} \\
\end{align}
\]
(2)より
\[
\begin{align}
y = C_2 - x \tag{3}
\end{align}
\]
となるから、これを(1)に代入し
\[
\begin{align}
\frac{Ax + B(C_2 - x)}{C_2} &= C_1 \\
Ax + B \cdot C_2 - Bx &= C_1 \cdot C_2 \\
(A - B)x &= C_1 \cdot C_2 - B \cdot C_2 \\
x &= \frac{C_2 (C_1 - B)}{A - B}
\end{align}
\]
これを(3)に代入すると
\[ \displaystyle y = C_2 - \frac{C_2(C_1 - B)}{A - B} = \frac{C_2 (A - C_1)}{A - B} \]
\( A > B \) であるときのxとyの比を求めると
\[
\begin{align}
x : y &= \frac{C_2 (C_1 - B)}{A - B} : \frac{C_2 (A - C_1)}{A - B} \\
x : y &= C_1 - B : A - C_1
\end{align}
\]
前述のとおり、ピアソン・スクエア法ではxの割合を \( D = A - C_1 \)、yの割合を \( E = C_1 - B \) と置いたから、この計算法が正しいことが証明できた。

PearsonSquareMethod.html

 169: /**
 170:  * ピアソン・スクエア法を使って分量を求める.
 171:  * @param   なし
 172:  * @return  なし
 173: */
 174: function PearsonSquareMethod() {
 175:     //エラー・クリア
 176:     let errmsg = '';
 177:     document.getElementById('error').innerHTML = errmsg;
 178:     //計算結果クリア
 179:     document.getElementById('text').value  = '';
 180: 
 181:     //Aの濃度
 182:     let densityA = document.getElementById('A').value;
 183:     //Bの濃度
 184:     let densityB = document.getElementById('B').value;
 185:     //欲しい濃度
 186:     let densityC = document.getElementById('C1').value;
 187:     //欲しい分量(重さ)
 188:     let weigthC = document.getElementById('C2').value;
 189: 
 190:     //Bの割合(D)
 191:     let ratioB = Math.abs(densityB - densityC);
 192:     document.getElementById('D').innerHTML = ratioB;
 193:     //Aの割合(E)
 194:     let ratioA = Math.abs(densityA - densityC);
 195:     document.getElementById('E').innerHTML = ratioA;
 196: 
 197:     //Aの分量(重さ)
 198:     let weightA = weigthC * ratioA / (ratioA + ratioB);
 199:     document.getElementById('C3').innerHTML = weightA.toFixed();
 200:     //Bの分量(重さ)
 201:     let weightB = weigthC * ratioB / (ratioA + ratioB);
 202:     document.getElementById('C4').innerHTML = weightB.toFixed();
 203: 
 204:     //結果をコピーできる要素に格納する.
 205:     document.getElementById('text').value = `Aの濃度:${densityA}
 206: Bの濃度:${densityB}
 207: 欲しい濃度:${densityC}
 208: 欲しい分量:${weigthC}
 209: --以下,計算結果--
 210: Aの割合:${ratioA}
 211: Bの割合:${ratioB}
 212: Aの分量:${weightA}
 213: Bの分量:${weightB}
 214: `;
 215: }

PearsonSquareMethod.html

  29: // 初期値 ==================================================================
  30: const TITLE = 'ピアソン・スクエア法';       //プログラム・タイトル
  31: const REFERENCE = 'https://www.pahoo.org/e-soul/webtech/js00/js00-06-09.html';
  32:                                         //参照サイト
  33: const WIDTH  = 600;                     //表示幅(ピクセル)【変更可能】
  34: const HEIGHT = 400;                     //表示高(ピクセル)【変更可能】

前述のとおり、グラフィックキャンバスは自由に拡大縮小できる。このサイズを決めるのが、初期値にある定数である。

マズローの欲求5段階説

マズローの欲求5段階説
次に、ビジネスでお馴染みのマズローの欲求5段階説のピラミッドを描いてみることにする。
ピラミッドは三角形または台形の組み合わせで、塗りつぶし色はグラデーションのような形で計算で求める。文字もグラフィックとして埋め込むことを目標にする。
マズローの欲求5段階説
任意の高さ \( HEIGHT \)、底辺の長さ \( WIDTH \) の二等辺三角形を考える。
次に、頂点 \( P_0 \) から \( h_1 \) だけ離れたところを上辺とし、上辺の長さを \( w_1 \)、さらに \( h_2 \) だけ離れたところを下辺とし、上辺の長さを \( w_2 \) とする台形を考える。
この台形の4つの頂点の座標は
 
\(
\left\{
\begin{array}{l}
P_1 \ (x_1, y_1) \\
P_2 \ (x_1 + w_1, \ y_1) \\
P_3 \ (x_2 + w_2, \ y_2) \\
P_4 \ (x_2, \ y_2)
\end{array}
\right.
\)

で表すことができる。

ここで、二等辺三角形の頂点の角度の半分にあたる \( r \) は逆三角関数を使い、\( \displaystyle r = tan^{-1}(\frac{WIDTH}{2 \cdot HEIGHT}) \) で求めることができる。
次に、台形の上辺の長さは \( w_1 = 2 \cdot y_1 \cdot tan(r) \)、下辺の長さは \( w_2 = 2 \cdot y_2 \cdot tan(r) \) で求めることができる。
さらに、\( \displaystyle x_1 = \frac{WIDTH - w_1}{2} \)、\( \displaystyle x_2 = \frac{WIDTH - w_2}{2} \) で求めることができる。

なお、\( x_1 = x_0 \) のときは \( w_1 = 0 \) となり、三角形を描くことになる。

Maslow.html

  38: /**
  39:  * 指定したcanvas要素に台形を描き,文字を描画する.
  40:  * @param   object ctx  canvasオブジェクト
  41:  * @param   int width, height  最大描画領域(幅, 高さ)
  42:  * @param   int y1        上辺のY座標
  43:  * @param   int y2        下辺のY座標
  44:  * @param   string color  塗り潰し色
  45:  * @param   string text   描画文字
  46:  * @param   int px        文字サイズ
  47:  * @param   string tcolor 文字カラー
  48:  * @return  なし
  49: */
  50: function drawTrapezoid(ctx, width, height, y1, y2, color, text, px, tcolor) {
  51:     let r = Math.atan(width / (2 * height));            //頂点の角度の2分の1
  52: 
  53:     let w1 = Number((2 * y1 * Math.tan(r)).toFixed());  //上辺の長さ
  54:     let w2 = Number((2 * y2 * Math.tan(r)).toFixed());  //下辺の長さ
  55:     let x1 = Number(((width - w1) / 2).toFixed());      //上辺左端のX座標
  56:     let x2 = Number(((width - w2) / 2).toFixed());      //下辺左端のX座標
  57: 
  58:     //台形または三角形を描く.
  59:     ctx.beginPath();
  60:     ctx.moveTo(x1, y1);
  61:     ctx.lineTo(x1 + w1, y1);
  62:     ctx.lineTo(x2 + w2, y2);
  63:     ctx.lineTo(x2, y2);
  64:     ctx.closePath();
  65:     ctx.stroke();
  66:     ctx.fillStyle = color;
  67:     ctx.fill();
  68: 
  69:     //文字を描く
  70:     //フォント・サイズ
  71:     ctx.font = px + 'px sans serif bold';
  72:     ctx.textAlign = 'center';
  73:     ctx.fillStyle = tcolor;
  74:     ctx.fillText(text, (x1 + w1 / 2).toFixed(), (y1 + (y2 - y1 + px) / 2).toFixed())
  75: }

これをJavaScriptに実装したものがユーザー関数 drawTrapezoid である。
台形は lineTo メソッドでパスを繋げることを描くことができる。最後に closePath メソッドを使って閉じた図形(ここでは台形)を描く。そして、fillStyle で塗りつぶし色を、fill メソッドを使って塗りつぶしを実行する。

Maslow.html

  77: /**
  78:  * canvas要素を使ってマズローの欲求五段階説を描く.
  79:  * @param   なし
  80:  * @return  なし
  81: */
  82: function drawMaslow() {
  83:     //マズローの欲求五段階説の各段階
  84:     const steps = ['自己実現の欲求', '承認欲求', '社会的欲求', '安全の欲求', '生理的欲求'];
  85:     let numberOfSteps = Number(steps.length);   //段階の数
  86: 
  87:     //canvas要素を用意する.
  88:     document.getElementById('maslow').width  = WIDTH;
  89:     document.getElementById('maslow').height = HEIGHT;
  90:     var ctx = document.getElementById('maslow').getContext('2d');
  91:     ctx.lineWidth = 2;
  92:     const mm = 20;      //輝度計算用の基数
  93: 
  94:     //台形1つの高さを求める.
  95:     const margin = 10;          //マージン
  96:     let stepHeight = Number(((HEIGHT - margin * (numberOfSteps - 1)) / numberOfSteps).toFixed());
  97: 
  98:     //1つ1つの段階を描く.
  99:     steps.forEach (function(element, i) {
 100:         //輝度を求める(上へ行くほど輝度が高い)
 101:         let lightness = (100 - (((100 - mm) / numberOfSteps* i + mm).toFixed());
 102:         //色相環を使って塗りつぶし色を求める.
 103:         let color = 'hsl(160deg 100% ' + lightness + '%)';
 104: 
 105:         //文字色を求める(輝度が高いときには白、低いときには黒)
 106:         let tcolor = (lightness > 40? 'black' : 'white';
 107: 
 108:         //フォント・サイズを求める.
 109:         let px = Number((stepHeight / 3).toFixed());
 110: 
 111:         //三角形または台形を描き,テキストを描画する.
 112:         drawTrapezoid(ctx, WIDTH, HEIGHT, i * (margin + stepHeight), i * (margin + stepHeight+ stepHeight, color, steps[i], px, tcolor);
 113:     });
 114: }

マズローの欲求五段階説」全体を描くのがユーザー関数 である。
1つ1つの段階を配列 maslow に用意しておき、forEachループ で前述のユーザー関数 drawTrapezoid を繰り返し実行する。
塗りつぶし色は、CSS3で導入された色相環を使って輝度を変化させることで単色グラデーションのように見せている。JavaScriptの hsl 関数を使って指定する。

PearsonSquareMethod.html

  29: // 初期値 ==================================================================
  30: const TITLE = 'ピアソン・スクエア法';       //プログラム・タイトル
  31: const REFERENCE = 'https://www.pahoo.org/e-soul/webtech/js00/js00-06-09.html';
  32:                                         //参照サイト
  33: const WIDTH  = 600;                     //表示幅(ピクセル)【変更可能】
  34: const HEIGHT = 400;                     //表示高(ピクセル)【変更可能】

前述のとおり、グラフィックキャンバスは自由に拡大縮小できる。このサイズを決めるのが、初期値にある定数である。

タートルグラフィックス

タートルグラフィックス
1967年に教育向けプログラミング言語として開発された LOGO (ロゴ)  には、タートルグラフィックスという画像描画機構が備わっていた。文字通り、亀(タートル)のカーソルが画面上を動くことで線を描く仕組みである。
ここまで、canvas要素は画面の左上を原点とする絶対座標系であったが(他のプログラミング言語でも絶対座標系を指定するのが一般的)、タートルグラフィックスでは、手描きするように、ペンの上げ下げ、ペンの向き、ペンを移動する距離といった要素を指定することでグラフィックを描いていく。
タートルグラフィックス
3つの角が 30°、60°、90°から成る直角三角形は、3辺の長さの比が \( 1 : 2 : \sqrt{3} \) になることを、三角関数を学ぶ前の数学で覚えただろう。左図の直角三角形の高さ(辺BC)を1、\( \angle{A} \) を原点 \( (0, 0) \) とすると、\( \angle{B} \) は \( (\sqrt{3}, 0) \) 、\( \angle{C} \) は \( (\sqrt{3}, 1) \) という絶対座標になる。
これら3つの絶対座標を直線で結ぶことで、この直角三角形を描くことができる。
一方、タートルグラフィックでは、点Aでペンを下ろし、右へ \( \sqrt{3} \) だけ移動。ここでペンの向きを反時計回りに90°回して、1 だけ移動。さらにペンの向きを反時計回りに120°回して、2 だけ移動し、点Aに戻ってきたらペンを上げる――という操作を行う。
少しややこしいかもしれないが、私たちは日常、定規を使って製図するのでないかぎり、鉛筆を使ってタートルグラフィックに似た操作で絵を描いている。その意味では、タートルグラフィックの方が自然な手続きと言える。

lensIris.html

  41: // タートルグラフィックス・クラス ===========================================
  42: class pahooTurtle {
  43: 
  44: /**
  45:  * コンストラクタ
  46:  * @param   Object ctx canvasオブジェクト
  47:  * @return  なし
  48: */
  49: constructor(ctx) {
  50:     // canvasオブジェクト
  51:     this.ctx = ctx;
  52: 
  53:     // penオブジェクト
  54:     this.pen = {
  55:         x: 0,               // X座標
  56:         y: 0,               // Y座標
  57:         direction: 0,       // 方向(度)
  58:         status: false,      // 状態 true:pen down, false: pen up
  59:         color: 'black',     // 色
  60:         thick: 1,           // 太さ
  61:     };
  62: }
  63: 
  64: /**
  65:  * 度をラジアンに変換する.
  66:  * @param   Number d 度
  67:  * @return  Number ラジアン
  68: */
  69: deg2rad(d) {
  70:     return d * Math.PI / 180;
  71: }
  72: 
  73: /**
  74:  * ペンを上げる.
  75:  * @param   なし
  76:  * @return  なし
  77: */
  78: penUp() {
  79:     this.pen.status = false;
  80: }
  81: 
  82: /**
  83:  * ペンを下げる(描画可能な状態にする).
  84:  * @param   なし
  85:  * @return  なし
  86: */
  87: penDown() {
  88:     this.pen.status = true;
  89: }
  90: 
  91: /**
  92:  * ペンの色を指定する.
  93:  * @param   String color カラー(Webカラー名または#rrggbb表記)
  94:  * @return  なし
  95: */
  96: penColor(color) {
  97:     this.pen.color = color;
  98: }
  99: 
 100: /**
 101:  * ペンの太さを指定する.
 102:  * @param   Number thick 太さ(自然数)
 103:  * @return  なし
 104: */
 105: penThick(thick) {
 106:     this.pen.thick = thick;
 107: }
 108: 
 109: /**
 110:  * ペンの向きを時計方向に回転する.
 111:  * @param   Number r 回転角度(度)
 112:  * @return  なし
 113: */
 114: rotate(r) {
 115:     this.pen.direction +r;
 116:     // ペンの方向が0度以上360度未満になるように調整する
 117:     while (this.pen.direction >360) {
 118:         this.pen.direction -360;
 119:     }
 120:     while (this.pen.direction < 0) {
 121:         this.pen.direction +360;
 122:     }
 123: }
 124: 
 125: /**
 126:  * ペンをdだけ前に進める.
 127:  * @param   Number d 進める距離(ピクセル)
 128:  * @return  なし
 129: */
 130: forward(d) {
 131:     // 度をラジアンに変換する.
 132:     const rad = this.deg2rad(this.pen.direction);
 133: 
 134:     // 線分の終端の座標を求める.
 135:     const x2 = this.pen.x + d * Math.cos(rad);
 136:     const y2 = this.pen.y + d * Math.sin(rad);
 137: 
 138:     // ペンが下りている場合
 139:     if (this.pen.status == true) {
 140:         this.ctx.beginPath();
 141:         this.ctx.moveTo(this.pen.x, this.pen.y);
 142:         this.ctx.lineTo(x2, y2);
 143:         this.ctx.strokeStyle = this.pen.color;
 144:         this.ctx.lineWidth   = this.pen.thick;
 145:         this.ctx.stroke();
 146:     }
 147: 
 148:     // ペン座標を更新する
 149:     this.pen.x = x2;
 150:     this.pen.y = y2;
 151: }
 152: 
 153: }
 154: // End of Class ============================================================

タートルグラフィックは、「5.5 クラス」で学んだクラスを利用する。ペンの上げ下げ、ペンの色や太さの指定、ペンの向きの回転、ペンの移動をメソッドとして用意した。回転角は、ラジアンではなく、日常で馴染みのある度で指定できるようにした。

絞り羽根イラスト

絞り羽根イラスト
カメラのレンズは、入ってくる光の量を調整する絞りという機構を内蔵している。高級コンデジや一眼デジカメに備わっているF値の設定は、絞りを調整している。F値が大きくなればなるほど、入ってくる光の量が減る――つまり、レンズの開口部分が小さくなる。
絞り機能はレンズによって異なるが、左図のようなイラストで表されることが多い。
このイラストをよく見ると、中央の白い開口部分は正六角形であることがわかる。また、黒い絞り機構(絞り羽根)は直角三角形の一部であるように見える。
絞り羽根イラストのラフ
前述の、3つの角が 30°、60°、90°から成る直角三角形を、左図のように回転するように6つ同じ図形を並べていくと、絞り羽根イラストになることがわかる。このラフスケッチから、絞り羽根の色を白黒反転させ、外側にレンズの縁をイメージする円を描き、その外側を切り捨てれば、絞り羽根イラストの完成である。
その過程を順を追って説明していこう。
絞り羽根
まず、直角三角形を1つ描く。
\( \angle{A} \) から描き始める。ペンの向きを時計の針の3時方向へ向け、\( 2 \) だけ進める。\( \angle{B} \) に到達したら、反時計回りに \( 180 - 30 \) 度回転させ、ペンを左へ \( 2 \) だけ進める。\( \angle{C} \) に到達したら、反時計回りに \( 90 \) 度回転させ、\( 1 \) だけ進め \( \angle{A} \) に戻る。
次の直角三角形を描くため、ペンを上げて、ペンの向きを3時方向へ向け、\( \angle{A^{\prime}} \) へ移動し、ペンの向きを時計回りに \( 60 \) 度回転させておく。
この操作を6回繰り返すと、上図の絞り羽根イラスのラフが出来上がる。
\( A - A^{\prime} \) が開口部を表す正六角形の1辺の長さになるが、これを変数にして増減させると、直角三角形の描画手順を変えずに絞りを開け閉めすることができる。また、イラスト全体の大きさを可変にできるよう、斜辺 \( B - C \) の長さを変数にしておく。

lensIris.html

 168: /**
 169:  * 直角三角形をした絞り羽根を1つ描く.
 170:  * 30度、60度、90度の直角三角形.
 171:  * @param   Object pt pahooTurtleインスタンス
 172:  * @param   Number r  絞り羽根の斜辺の長さ(ピクセル)
 173:  * @param   Number l  正六角形の一辺の長さ(ピクセル)
 174:  * @return  なし
 175: */
 176: function drawIris(pt, r, l) {
 177:     // ペン・カラーを指定する.
 178:     pt.penColor(LINECOLOR);
 179: 
 180:     // ペンの太さを指定する.
 181:     pt.penThick(PENTHICK);
 182: 
 183:     // 斜辺を描く.
 184:     pt.penDown();
 185:     pt.forward(r);
 186:     pt.rotate(30 - 180);
 187: 
 188:     // 長辺は描かない.
 189:     pt.penUp();
 190:     const r2 = (r / 2* Math.sqrt(3);
 191:     pt.forward(r2);
 192:     pt.rotate(-90);
 193: 
 194:     // 短辺を描く.
 195:     pt.penDown();
 196:     const r3 = r / 2;
 197:     pt.forward(r3);
 198:     pt.rotate(60 - 180);
 199:     pt.penUp();
 200: 
 201:     // 次の羽根を描く位置へペンを移動する.
 202:     pt.forward(l);
 203:     pt.rotate(60);
 204: }

ここまでの手順をユーザー関数にしたものが drawIris である。引数として、タートルグラフィック・クラス pahooTurtle のインスタンス、絞り羽根の斜辺 \( B - C \) の長さ、正六角形の一辺 \( A - A^{\prime} \) の長さを渡す。
なお、仕上げの際に斜辺 \( B - C \) は見えない方がいいので、ペンを上げて移動するだけにしておく。
こうして出来たラフ画の仕上げの手順を説明しよう。
  1. canvas全体を白く塗りつぶす。
  2. レンズの縁をイメージする円を描いて内側を黒く塗りつぶす。
  3. ペンの位置を円の中心(canvasの中心)へ移動し、前述のタートル・グラフィック・メソッド drawIris を6回繰り返す。ここで、ペンの色は白色にしておく。
  4. レンズの縁をイメージする円を黒で描き直す。
  5. その内側に縁を強調する円を白で描く。
  6. 開口部分(正六角形)を白で塗りつぶす。
これをプログラムしたものがユーザー関数 drawLens である。

lensIris.html

 206: /**
 207:  * canvas要素を使ってレンズ絞りを描く.
 208:  * @param   Number f f値
 209:  * @return  なし
 210: */
 211: function drawLens(f) {
 212:     const width  = WIDTH;
 213: 
 214:     // 絞り羽根の斜辺の長さ(ピクセル)
 215:     let x1 = width / 1.5;
 216: 
 217:     // 開口部分の1辺の長さ(ピクセル)
 218:     if (f < F_MIN) {
 219:         k = 0.65;
 220:     } else if (f > F_MAX) {
 221:         k = 0.1;
 222:     } else {
 223:         k = 0.65 * Math.sqrt(F_MIN / f);
 224:     }
 225:     let x2 = x1 * k;
 226: 
 227:     // canvas要素
 228:     const canvas = document.getElementById('graph');
 229:     const ctx = canvas.getContext('2d');
 230: 
 231:     // LINECOLORで塗りつぶしておく
 232:     ctx.fillStyle = LINECOLOR;
 233:     ctx.fillRect(0, 0, canvas.width, canvas.height);
 234: 
 235: 
 236:     // レンズの縁をイメージする円を描いて内側をIRISCOLORで塗りつぶす
 237:     ctx.beginPath();
 238:     ctx.arc(width / 2, width / 2, x1 * 0.7, 0, 2 * Math.PI);
 239:     ctx.fillStyle = IRISCOLOR
 240:     ctx.fill();
 241: 
 242:     // タートル・グラフィックスで直角三角形を描く
 243:     let pt = new pahooTurtle(ctx);
 244: 
 245:     // ペンをcanvasの中心へ移動
 246:     pt.forward(width / 2);
 247:     pt.rotate(90);
 248:     pt.forward(width / 2);
 249:     pt.rotate(-30);
 250:     pt.forward(x2);
 251:     pt.rotate(180 - 60);
 252: 
 253:     // 6枚の絞り羽根を描く
 254:     for (let i = 0i < 6i++) {
 255:         drawIris(pt, x1, x2);
 256:     }
 257: 
 258:     // レンズの縁をイメージする円をIRISCOLORで描く.
 259:     ctx.beginPath();
 260:     ctx.arc(width / 2, width / 2, x1 * 0.7, 0, 2 * Math.PI);
 261:     ctx.strokeStyle = IRISCOLOR;
 262:     ctx.lineWidth = 8;
 263:     ctx.stroke();
 264:     ctx.closePath();
 265: 
 266:     // その内側に縁を強調する円をLINECOLORで描く.
 267:     ctx.beginPath();
 268:     ctx.arc(width / 2, width / 2, x1 * 0.7 - 8, 0, 2 * Math.PI);
 269:     ctx.strokeStyle = LINECOLOR;
 270:     ctx.lineWidth = 7;
 271:     ctx.stroke();
 272:     ctx.closePath();
 273: 
 274:     // 開口部分(正六角形)をLINECOLORで塗りつぶす
 275:     const cx = canvas.width / 2;        // 中心 x
 276:     const cy = canvas.height / 2;       // 中心 y
 277:     const angleStep = Math.PI / 3;      // 60度(正六角形)
 278:     ctx.beginPath();
 279:     for (let i = 0i < 6i++) {
 280:         const angle = angleStep * i;
 281:         const x = cx + x2 * Math.cos(angle);
 282:         const y = cy + x2 * Math.sin(angle);
 283:         if (i === 0) {
 284:             ctx.moveTo(x, y);
 285:         } else {
 286:             ctx.lineTo(x, y);
 287:         }
 288:     }
 289:     ctx.closePath();
 290:     ctx.fillStyle = LINECOLOR;
 291:     ctx.fill();
 292: }

最後に、ロード時に呼び出すメイン・プログラムと、HTML部分を見ておこう。

lensIris.html

 294: // メイン・プログラム ======================================================
 295: window.onload = function() {
 296:     //タイトル等をセット
 297:     document.title = TITLE;
 298:     document.getElementById('title').innerHTML = TITLE + '&nbsp;<span style="font-size:small;">' + getLastModified() + '版</span>';
 299:     document.getElementById('reference').innerHTML = '※参考サイト&nbsp;<a href="' + REFERENCE + '">' + REFERENCE + '</a>';
 300: 
 301:     // HTML属性値をセット
 302:     document.getElementById('fSlider').style.width = (WIDTH - 130+ 'px';
 303:     document.getElementById('fSlider').min = F_MIN;
 304:     document.getElementById('fSlider').max = F_MAX;
 305:     document.getElementById('fSlider').value = F_MIN;
 306:     document.getElementById('f_min').innerHTML = F_MIN;
 307:     document.getElementById('f_max').innerHTML = F_MAX;
 308:     document.getElementById('help').style.width  = WIDTH + 'px';
 309:     const viewport = document.querySelector('meta[name="viewport"]');
 310:     if (viewport) {
 311:         viewport.setAttribute('content', 'width=' + WIDTH + ', user-scalable=yes');
 312:     }
 313: 
 314:     // canvasをセット
 315:     let width  = WIDTH;
 316:     document.getElementById('lens').style.width  = WIDTH + 'px';
 317:     document.getElementById('lens').style.height = WIDTH + 'px';
 318:     document.getElementById('lens').style.marginBottom = '10px';
 319:     document.getElementById('lens').innerHTML = `
 320: <canvas id="graph" width="${width}" height="${width}"></canvas>
 321: `;
 322: 
 323:     const slider = document.getElementById("fSlider");
 324:     const valueDisplay = document.getElementById("fValue");
 325: 
 326:     slider.addEventListener("input", () => {
 327:         drawLens(slider.value);
 328:     });
 329: 
 330:     // 絞りを描く
 331:     drawLens(F_MIN);
 332: }
 333: </script>
 334: </head>
 335: 
 336: <!-- HTML部分 ========================================================= -->
 337: <body>
 338: <h2 id="title"></h2>
 339: 
 340: <div id="lens"></div>
 341: F値 <span id="f_min"></span>
 342: <input type="range" id="fSlider" min="" max="" step="0.1" value="">
 343: <span id="f_max"></span>
 344: <br>
 345: <div id="error"></div>
 346: </p>
 347: 
 348: <!-- 使い方 -->
 349: <div id="help" style="border-style:solid; border-width:1px; margin:20px 0px 0px 0px; padding:5px; font-size:small; overflow-wrap:break-word; word-break:break-all;">
 350: <div style="font-size:110%; font-weight:bold;">使い方</div>
 351: <ol>
 352: <li>[<span style="font-weight:bold;">スライダー</span>]を左右に動かすと、レンズ開口部分(中央の白い白い正六角形)が開閉します.</li>
 353: </ol>
 354: <div id="reference" style="font-size:90%;"></div>
 355: </div>
 356: 
 357: </body>
 358: </html>

F値はスライダーを用いて、あらかじめ指定した定数 FMIN から FMAX の範囲で調整できるようにしてある。このスライダーの値が変化したら、ユーザー定義関数 drawLens を呼び出してイラストを描き直す。こうすることで、インタラクティブにイラストを変化させることができる。

練習問題

読みやすいプログラム:DRY原則

DRY原則とは、Don't Repeat Yourselfの略で、David ThomasとAndrew Huntが著書『達人プログラマ』で紹介した概念である。複数の場所に同じ情報が置かれていると変更時に整合性が取れなくなる危険性が高まるため、原則として一箇所で管理し、そこから参照するようにしようというものである。
たとえばユーザー定義関数であれば、汎用性の高い関数を設計しておくといい。1関数1機能を追求していくと、自ずと DRY原則を満たすことになる。

たとえば、ピアソン・スクエア法に追加で矢印を描くとしたら、ユーザー関数 drawArrow をそのまま利用できる。また、他のグラフィックプログラムでも利用できるだろう。このようなユーザー定義関数は DRY原則を満たしている。
逆に、赤い矢印を描く関数 drawRedArrow と青い矢印 drawBlueArrow を描く関数を別々に用意したとすると、DRY原則を反している。ただし、これらの関数が内部で drawArrow を呼び出しているなら、DRY原則を満たしている。

読みやすいプログラム:関数(メソッド)の副作用

ユーザー関数 drawTrapezoid のように、画面に文字を表示したりグラフィックを描くなどの作用のことを「関数(メソッド)の副作用」と呼ぶ。副作用を持たない関数(戻り値だけ得られるような関数)を「純粋関数」と呼ぶ。
副作用がある関数は、関数の冒頭コメントに副作用の内容を記しておくと読みやすいプログラムになる。

コラム:CSSだけで三角形や台形を描く

本文の「マズローの欲求5段階説」だが、じつはスタイルシート(CSS)だけで、同じように描くことができる。

MaslowCSS.html

  12: <!DOCTYPE html>
  13: <html lang="ja">
  14: <head>
  15: <meta charset="UTF-8">
  16: <title>マズローの欲求5段階説</title>
  17: 
  18: <style>
  19: /* 親要素 */
  20: #graph {
  21:     width: 600px;
  22:     height: 400px;
  23:     text-align: center;
  24: }
  25: 
  26: /* 要素を三角形や台形にくり抜く */
  27: #phase1 {
  28:     position: absolute;
  29:     top:    388px;
  30:     left:     0px;
  31:     width:  600px;
  32:     height:  82px;
  33:     background-color: hsl(160deg 100% 16%);
  34:     clip-path: polygon(54px 0px, 546px 0px, 600px 72px, 0px 72px);
  35:     display: flex;
  36:     align-items: center;
  37:     justify-content: center;
  38:     color: white;
  39:     font-size: large;
  40: }
  41: #phase2 {
  42:     position: absolute;
  43:     top:    306px;
  44:     left:     0px;
  45:     width:  600px;
  46:     height:  82px;
  47:     background-color: hsl(160deg 100% 32%);
  48:     clip-path: polygon(116px 0px, 485px 0px, 539px 72px, 62px 72px);
  49:     display: flex;
  50:     align-items: center;
  51:     justify-content: center;
  52:     color: white;
  53:     font-size: large;
  54: }
  55: #phase3 {
  56:     position: absolute;
  57:     top:    224px;
  58:     left:     0px;
  59:     width:  600px;
  60:     height:  82px;
  61:     background-color: hsl(160deg 100% 48%);
  62:     clip-path: polygon(177px 0px, 423px 0px, 477px 72px, 123px 72px);
  63:     display: flex;
  64:     align-items: center;
  65:     justify-content: center;
  66:     color: black;
  67:     font-size: large;
  68: }
  69: #phase4 {
  70:     position: absolute;
  71:     top:   142px;
  72:     left:     0px;
  73:     width:  600px;
  74:     height:  82px;
  75:     background-color: hsl(160deg 100% 64%);
  76:     clip-path: polygon(239px 0px, 362px 0px, 416px 72px, 185px 72px);
  77:     display: flex;
  78:     align-items: center;
  79:     justify-content: center;
  80:     color: black;
  81:     font-size: large;
  82: }
  83: #phase5 {
  84:     position: absolute;
  85:     top:     60px;
  86:     left:     0px;
  87:     width:  600px;
  88:     height:  82px;
  89:     background-color: hsl(160deg 100% 80%);
  90:     clip-path: polygon(300px 0px, 354px 72px, 246px 72px);
  91:     display: flex;
  92:     align-items: center;
  93:     justify-content: center;
  94:     color: black;
  95:     font-size: small;
  96: }
  97: 
  98: /* 使い方 */
  99: #help {
 100:     border: 1px solid black;
 101:     position: absolute;
 102:     top:   450px;
 103:     left:     0px;
 104:     width: 600px;
 105:     margin: 20px 0px 0px 0px;
 106:     padding: 5px;
 107:     font-size: small;
 108:     overflow-wrap: break-word;
 109:     word-break: break-all;
 110: }
 111: </style>
 112: 
 113: </head>
 114: 
 115: <!-- HTML部分 ========================================================= -->
 116: <body>
 117: <div id="graph">
 118: <h2 id="title">マズローの欲求5段階説</h2>
 119: <div id="phase1">生理的欲求</div>
 120: <div id="phase2">安全の欲求</div>
 121: <div id="phase3">社会的欲求</div>
 122: <div id="phase4">承認欲求</div>
 123: <div id="phase5">自己<br>実現の欲求</div>
 124: </div>
 125: 
 126: <!-- 使い方 -->
 127: <div id="help">
 128: <div id="reference" style="font-size:90%;">
 129: 参考サイト&nbsp;<a href="https://www.pahoo.org/e-soul/webtech/js00/js00-06-09.html#Maslow">https://www.pahoo.org/e-soul/webtech/js00/js00-06-09.html#MaslowCSS</a>
 130: </div>
 131: 
 132: </body>
 133: </html>

CSSで定義している #phase1~#phase5 が、5段階のそれぞれの段に相当する三角形または台形をデザインしている。
肝となるのは clip-pathプロパティである。これは、要素のどの部分を表示するかを設定するクリッピング領域を作る――つまり、元々は矩形だった要素の領域を任意の図形としてくり抜くことができる
ただし、くり抜くことが目的であるため、borderプロパティを使って境界線を描くことはできない。
また、CSSでは変数や演算式を用いることができないため、図形のサイズは固定的になる。

コラム:ビットマップ画像とベクター画像

ビットマップ画像とベクター画像
大雑把に言うと、拡大するとドットの粗さが目立つのがビットマップ画像、拡大して荒れないのがベクター画像であるが、パソコン黎明期はベクター画像が中心であった。
ほとんどのホビー用パソコンに搭載されていたBASIC言語がサポートしていたグラフィック命令が、直線や円を描くといったベクター命令だったからだ。また、ビットマップ画像を保存できる容量を備えたメディアが普及していなかったこともある。

ビットマップ画像の容量はどのくらいだろうか――たとえば NEC PC-8801シリーズの場合、グラフィック解像度は640×200ドット、8色である。8色は3ビットで表せるから、必要な容量は3×640×200=384,000bit=48,000バイト≒47Kバイト――1981年に発売されたPC-8801の外部規則装置はカセットテープだったから、このデータをセーブ/ロードするには10分もかかった。
1983年に発売された PC-8801mkIIには5インチフロッピーディスクドライブが搭載されたが、1枚で最大300Kバイトしか記録できなかった。つまり、1枚のフロッピーで6枚分の画面しか保存できなかったのである。すでにJPEG画像圧縮は開発されていたが、当時のホビーパソコンの処理能力では、1画面分の画像を圧縮/展開するのに数十分かかる状況だった。
惑星メフィウス
こうした事情から、グラフィックを多用するアドベンチャーゲームやRPGは、ベクター命令を使って画面にグラフィック描画を行った。だが、いまのように画面に瞬時に表示されるわけではなく、まるで絵筆を使って描いているように時間がかかった。
また、ベクター画像が拡大縮小できるとはいえ、ハードウェアの上限が640×200ドットであるうえ中間色が表示できなかったから、肉眼でドットが見える程度に粗いグラフィックだった。近年、ドット絵と称し、わざと粗いグラフィックを再現することが流行っているようだが、本当に8色しか使っていないドット絵は希少である。

今回紹介した canvas要素は基本図形の描画メソッドしか備わっていないが、当時のBASIC言語も基本図形の命令しかなかった。複雑な図形を描くサブルーチンを用意しなければならないのは、今も昔も同じである――2D図形描画のために組み立てる方程式は当時と変わらず、三角関数逆三角関数が活躍する。当時のパソコンで三角関数を計算するには時間がかかったので、メモリ内に三角関数表を持たせるなど工夫した。数式を素直にプログラミングできるうえ、ネット上で多くのフリーの描画ライブラリが流通している現在はプログラム生産性は高いと言えるだろう。
(この項おわり)
header