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

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

目次

サンプル・プログラム

圧縮ファイルの内容
PearsonSquareMethod.htmlサンプル・プログラム本体。
Maslow.htmlサンプル・プログラム本体。
pahooClipboard.jsクリップボードを使うためのJavaScriptライブラリ。
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文に変数を埋め込むことにした。

 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を示す。

  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 で行っている。

 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 \) と置いたから、この計算法が正しいことが証明できた。

 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: }

  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 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) \\ \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 \) となり、三角形を描くことになる。

  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 メソッドを使って塗りつぶしを実行する。

  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 関数を使って指定する。

  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;                     //表示高(ピクセル)【変更可能】

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

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

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

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

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

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

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

ビットマップ画像とベクター画像
大雑把に言うと、拡大するとドットの粗さが目立つのがビットマップ画像、拡大して荒れないのがベクター画像であるが、パソコン黎明期はベクター画像が中心であった。
ほとんどのホビー用パソコンに搭載されていた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