


目次
サンプル・プログラム
PearsonSquareMethod.html | ピアソン・スクエア法を描くサンプル・プログラ |
Maslow.html | マズローの欲求5段階説を描くサンプル・プログラム |
lensIris.html | レンズ絞りイラストを描くサンプル・プログラム |
pahooClipboard.js | クリップボードを使うためのJavaScriptライブラリ。 |
MaslowCSS.html | コラムにあるマズローの欲求5段階説を描くサンプル・プログラム |
バージョン | 更新日 | 内容 |
---|---|---|
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文に変数を埋め込むことにした。
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の濃度 <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を示す。
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: }
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の濃度 <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 \) と置いたから、この計算法が正しいことが証明できた。
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}g
209: --以下,計算結果--
210: Aの割合:${ratioA}
211: Bの割合:${ratioB}
212: Aの分量:${weightA}g
213: Bの分量:${weightB}g
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段階説

ピラミッドは三角形または台形の組み合わせで、塗りつぶし色はグラデーションのような形で計算で求める。文字もグラフィックとして埋め込むことを目標にする。

次に、頂点 \( 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: }
台形は 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; //表示高(ピクセル)【変更可能】
タートルグラフィックス

ここまで、canvas要素は画面の左上を原点とする絶対座標系であったが(他のプログラミング言語でも絶対座標系を指定するのが一般的)、タートルグラフィックスでは、手描きするように、ペンの上げ下げ、ペンの向き、ペンを移動する距離といった要素を指定することでグラフィックを描いていく。

一方、タートルグラフィックでは、点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 ============================================================
絞り羽根イラスト

絞り機能はレンズによって異なるが、左図のようなイラストで表されることが多い。
このイラストをよく見ると、中央の白い開口部分は正六角形であることがわかる。また、黒い絞り機構(絞り羽根)は直角三角形の一部であるように見える。

その過程を順を追って説明していこう。

\( \angle{A} \) から描き始める。ペンの向きを時計の針の3時方向へ向け、\( 2 \) だけ進める。\( \angle{B} \) に到達したら、反時計回りに \( 180 - 30 \) 度回転させ、ペンを左へ \( 2 \) だけ進める。\( \angle{C} \) に到達したら、反時計回りに \( 90 \) 度回転させ、\( 1 \) だけ進め \( \angle{A} \) に戻る。
この操作を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: }
なお、仕上げの際に斜辺 \( B - C \) は見えない方がいいので、ペンを上げて移動するだけにしておく。
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 = 0; i < 6; i++) {
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 = 0; i < 6; i++) {
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: }
lensIris.html
294: // メイン・プログラム ======================================================
295: window.onload = function() {
296: //タイトル等をセット
297: document.title = TITLE;
298: document.getElementById('title').innerHTML = TITLE + ' <span style="font-size:small;">' + getLastModified() + '版</span>';
299: document.getElementById('reference').innerHTML = '※参考サイト <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>
練習問題
読みやすいプログラム:DRY原則
たとえばユーザー定義関数であれば、汎用性の高い関数を設計しておくといい。1関数1機能を追求していくと、自ずと DRY原則を満たすことになる。

たとえば、ピアソン・スクエア法に追加で矢印を描くとしたら、ユーザー関数 drawArrow をそのまま利用できる。また、他のグラフィックプログラムでも利用できるだろう。このようなユーザー定義関数は DRY原則を満たしている。
逆に、赤い矢印を描く関数 drawRedArrow と青い矢印 drawBlueArrow を描く関数を別々に用意したとすると、DRY原則を反している。ただし、これらの関数が内部で drawArrow を呼び出しているなら、DRY原則を満たしている。
読みやすいプログラム:関数(メソッド)の副作用
副作用がある関数は、関数の冒頭コメントに副作用の内容を記しておくと読みやすいプログラムになる。
コラム: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: 参考サイト <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>
コラム:ビットマップ画像とベクター画像


ビットマップ画像の容量はどのくらいだろうか――たとえば 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要素の使い方とテンプレートリテラルについて学ぶ。