目次
サンプル・プログラム
PearsonSquareMethod.html | サンプル・プログラム本体。 |
Maslow.html | サンプル・プログラム本体。 |
pahooClipboard.js | クリップボードを使うためのJavaScriptライブラリ。 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.0 | 2023/08/02 | 変数名等を見直し |
1.0.0 | 2023/03/25 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
1.1.0 | 2023/08/02 | drawMaslow() forEachループに変更 |
1.0.0 | 2023/03/26 | 初版 |
バージョン | 更新日 | 内容 |
---|---|---|
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要素を配置することもできる。
テンプレートリテラル
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の濃度 <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の濃度 <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: 欲しいクリームの濃度 <input id="C1" name="C1" type="text" style="width:${blockWidth3}px;">%<br>
139: 欲しいクリームの重量 <input id="C2" name="C2" type="text" style="width:${blockWidth3}px;">g<br>
140: クリームAの重量 <span id="C3" name="C3" style="background-color:${bgcolor2};"></span>g<br>
141: クリームBの重量 <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の割合 <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の割合 <span id="E" name="E" style="background-color:${bgcolor2};"></span></div>
145:
146: `;
テンプレートリテラルのもう1つの特長は、その中に変数を埋め込めることだ。${変数名} として埋め込むことができる。こうすると、実行時に変数の値が展開される。
上述のテンプレートリテラルは、canvas要素の幅や高さ、DIVタグの配置座標を変数として、id名squereの要素(実際にはDIVタグ)の中にHTMLタグとして追加する。window.onloadのタイミングでこの処理を実行すれば、任意の大きさのcanvas要素と文字列、テキストボックスを画面に配置できる。
矢印を描く
矢印の直線部分 \( 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: }
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の濃度 <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の濃度 <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: 欲しいクリームの濃度 <input id="C1" name="C1" type="text" style="width:${blockWidth3}px;">%<br>
139: 欲しいクリームの重量 <input id="C2" name="C2" type="text" style="width:${blockWidth3}px;">g<br>
140: クリームAの重量 <span id="C3" name="C3" style="background-color:${bgcolor2};"></span>g<br>
141: クリームBの重量 <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の割合 <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の割合 <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: }
ピアソン・スクエア法
本節では、この計算法の証明をしていく。
このとき、クリーム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}g
209: --以下,計算結果--
210: Aの割合:${ratioA}
211: Bの割合:${ratioB}
212: Aの分量:${weightA}g
213: Bの分量:${weightB}g
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段階説
ピラミッドは三角形または台形の組み合わせで、塗りつぶし色はグラデーションのような形で計算で求める。文字もグラフィックとして埋め込むことを目標にする。
次に、頂点 \( 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: }
台形は 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原則
たとえばユーザー定義関数であれば、汎用性の高い関数を設計しておくといい。1関数1機能を追求していくと、自ずと DRY原則を満たすことになる。
たとえば、ピアソン・スクエア法に追加で矢印を描くとしたら、ユーザー関数 drawArrow をそのまま利用できる。また、他のグラフィックプログラムでも利用できるだろう。このようなユーザー定義関数は DRY原則を満たしている。
逆に、赤い矢印を描く関数 drawRedArrow と青い矢印 drawBlueArrow を描く関数を別々に用意したとすると、DRY原則を反している。ただし、これらの関数が内部で drawArrow を呼び出しているなら、DRY原則を満たしている。
読みやすいプログラム:関数(メソッド)の副作用
副作用がある関数は、関数の冒頭コメントに副作用の内容を記しておくと読みやすいプログラムになる。
コラム:ビットマップ画像とベクター画像
ビットマップ画像の容量はどのくらいだろうか――たとえば 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画面分の画像を圧縮/展開するのに数十分かかる状況だった。
今回紹介した canvas要素は基本図形の描画メソッドしか備わっていないが、当時のBASIC言語も基本図形の命令しかなかった。複雑な図形を描くサブルーチンを用意しなければならないのは、今も昔も同じである――2D図形描画のために組み立てる方程式は当時と変わらず、三角関数と逆三角関数が活躍する。当時のパソコンで三角関数を計算するには時間がかかったので、メモリ内に三角関数表を持たせるなど工夫した。数式を素直にプログラミングできるうえ、ネット上で多くのフリーの描画ライブラリが流通している現在はプログラム生産性は高いと言えるだろう。
今回は、パティシエが菓子作りをする際に必要な濃度のクリームを得るために使うピアソン・スクエア法とビジネスでお馴染みのマズローの欲求五段階説を題材にして、canvas要素の使い方とテンプレートリテラルについて学ぶ。