これは9月19日の信濃毎日新聞に掲載されていた記事です。詳しい内容には毎日新聞の「くり返しの「入れ子構造」」ページから有料記事に進むことで読めます。
目次
概要
メモに書かれた通りに花を植えるロボットがいます。メモには次のように書かれています。
これは、「青、黄、オレンジの順で横に花を並べる作業」を「2回繰り返す」ことを、下に移動しながら「3回繰り返す」ということです。を植えさらにもう1回を植えることを3回繰り返すわけです。結果は下のようになります。これをプログラミングしていきます。
論理を考える
元の記事に「くり返しの入れ子構造」と書かれているように、このお題は繰り返しを繰り返します。JavaScriptではforループが使用できます。
1重のforループ
次のforループでは、print()関数は2回繰り返し実行されます。出力される”0回め”は本来は”1回め”ですが、forループでは、配列要素の走査など、0からスタートする場合が多いので、iを0から始めたときには、0回めは1回めを意味します。変数名に使用しているiは、繰り返しを意味するiterateの頭文字で、forループでよく使用されます。
// 2回の繰り返し
for (let i = 0; i < 2; i++) {
print(i + '回めの繰り返し');
}
赤い矩形を横に2つ並べるforループは次のように記述できます。
function setup() {
createCanvas(100, 100);
background(200);
// 線はなし
noStroke();
// 塗りを赤に
fill(255, 0, 0);
// 1行2列
for (let i = 0; i < 2; i++) {
rect(i * 20, 0, 10, 10);
}
}
1回めの繰り返しでiは0なので、rect(0,0,10,10)となり、位置(0,0)を左上隅とする、幅10の正方形が描かれ、2回めの繰り返しでiは1なので、rect(20,0,10,10)となり、1つめとは異なり位置(20,0)を左上隅とする、幅10の正方形が描かれます。この並びは横に1行、縦に2列なので1行2列と見なせます。rect()関数については「2_3:p5.js 線、四角形、三角形、長方形を描く」を参照してください。
2重のforループ
2重のforループは、forループの中にもう1つforループを入れたもので、その構造は入れ子、ネストと呼ばれます。「繰り返しの繰り返し」にはこの構造を使います。
// 2重のforループ
for (let i = 0; i < 2; i++) {
print('外側の繰り返し:' + i + '回め');
for (let j = 0; j < 3; j++) {
print('内側の繰り返し:' + j + '回め');
}
print('-----------------');
}
このコードからは下図の左が出力されます。外側のループのiが0のとき、内側のjは0,1,2と変化し、外側のループのiが1のとき、内側のjは0,1,2と変化していることが分かります。
外側のループの変数と内側のループの変数を、内側のループの中で使用すると、矩形の並びが一気に描画できます。
// 2行4列
fill(0, 0, 255);
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 2; y++) {
print(x, y);
rect(x * 20, y * 20, 10, 10);
}
}
下図は実行結果です。
xが0のとき、yは0,1と変化するので、青い矩形の左上隅は(0,0)と(0,20)になります。これは縦の並び(1列)です。xが1のとき、yは0,1と変化するので、青い矩形の左上隅は(20,0)と(20,20)になります。これは前の縦の並びから横に進んだ列です。以降も同様で、結果2行4列並びになります。
rect()関数のxとyを入れ替えると、行数と列数が入れ替わります。
// xとyを入れ替えた
rect(y * 20, x * 20, 10, 10);
列の色分け
縦に並ぶ矩形を色分けしたいとしましょう。
2列の場合、列数の変数が0かそれ以外かで分けることで、塗り分けできます。
// 4行2列
for (let x = 0; x < 2; x++) {
if (x === 0) {
fill(0, 0, 255);
} else {
fill(243, 152, 0);
}
for (let y = 0; y < 4; y++) {
rect(x * 20, y * 20, 10, 10);
}
}
列数がもっと増えても、変数が0か1か2か….の場合で分けて、各列の塗り色を指定することで、色分けできます。ただしこの場合は、使用する色数に応じて長くなります。
また、たとえば、青黄橙青黄橙のように、一定のパターンの繰り返しがある場合には、%演算子がうまく利用できます。%は剰余演算子と呼ばれ、割った余りを返します。たとえば0 % 3 なら0(0は3で割り切れる)を、1 % 3なら1(1 / 3は余りが1)を返します。
割られる変数xが0,1,2,3,4,5と変化するとき、x % 3は0,1,2,0,1,2になります。これを利用します。
// 3行6列
for (let x = 0; x < 6; x++) {
print(x % 3); // 0,1,2,0,1,2
// 0 / 3 = 0、3 / 3 = 0
if (x % 3 === 0) {
fill(0, 0, 255);
// 1 / 3と4 / 3の余りは1
}
else if (x % 3 === 1) {
fill(255, 255, 0);
// 2 /3と5 / 3の余りは2
}
else if (x % 3 === 2) {
fill(243, 152, 0);
}
for (let y = 0; y < 3; y++) {
rect(x * 20, y * 20, 10, 10);
}
}
剰余演算子の左辺に増加しつづける値があり、右辺に一定の値があると、剰余演算子の計算から、0から演算子の右辺にある値まで(その値は含まない)を繰り返す一連の値が作成できます。(「4_1:経時的変化 Creative Coding p5.js」)
格子ブロックの作成
ブロック崩しゲームに見られるような、矩形を格子状に並べたいときに役立つひな型コードを紹介しておきます。
格子ブロックのひな型コード
const rows = 2; // 横向きの行数
const columns = 3; // 縦向きの列数
const gutter = 5; // 矩形間の空き
const w = 20; // 矩形の幅
const h = 10; // 矩形の高さ
// 矩形の開始位置をずらす量 => (offsetX,offsetY)から始まる
const offsetX = 10;
const offsetY = 20;
// 矩形を格子状に並べて描く
for (let c = 0; c < columns; c++) {
for (let r = 0; r < rows; r++)
// 行数(rows) x 列数(columns)に矩形を描く
rect(offsetX + c * (gutter + w), offsetY + r * (gutter + h), w, h);
// 列数(columns) x 行数(rows)に矩形を描く
// rect(offsetX + r * (gutter + w), offsetY + c * (gutter + h), w, h);
}
行と列数に加え、矩形の幅と高さ、矩形間の空き、矩形ブロックをずらす位置などの変数が指定できます。
rowとcolumn、どっちが行でどっちが列か?
2重のforループで格子模様を作る時、どっちが行でどっちが列か分からなくなるときがあります。前述のコードのように、rowとcolumnに置き換えた場合にはなおさらです。
数学の行列では、横の並びを行、縦の並びを列と言います(ウィキペディア「行列」)。
エクセルの表でも、同様に、横が行、縦が列です(社会人の教科書「横・縦どっち?「行」と「列」の覚え方」)。
データベースにも行と列があり、行がロウ(row)、列がカラム(column)に当たります(GMOクラウドアカデミー「データベースの用語を理解しよう 「テーブル」「レコード」「カラム」「フィールド」とは?」)。
rowとcolumnの覚え方には「ひと目でわかる行列(Row ・ Column)の方向の覚え方」ページが役立ちます。
プログラムその1
では、お題の花を植えるプログラムを見ていきましょう。実際には、列の色分けで見たコードに、花の画像のイメージを加えるだけです。
// 花のイメージ用変数
let blueFlowerImage, yellowFlowerImage, orangeFlowerImage;
let flowerImage;
// 3つの花の画像ファイルを読み込む
function preload() {
blueFlowerImage = loadImage('images/blue.png');
yellowFlowerImage = loadImage('images/yellow.png');
orangeFlowerImage = loadImage('images/orange.png');
}
function setup() {
createCanvas(400, 300);
background(230);
// 6行3列
for (let x = 0; x < 6; x++) {
// %演算子と3を使って、0,1,2,0,1,2という循環を得る
// 0と3列めは青い花
if (x % 3 === 0) {
flowerImage = blueFlowerImage;
// 1と4列めは黄色い花
}
else if (x % 3 === 1) {
flowerImage = yellowFlowerImage;
// 2と5列めはオレンジの花
}
else if (x % 3 === 2) {
flowerImage = orangeFlowerImage;
}
// xとyを使ってイメージを描く位置を決める
// 30はイメージの幅とイメージ間の空きを考慮した決め打ちの数値
for (let y = 0; y < 3; y++) {
image(flowerImage, x * 30, y * 30);
}
}
}
このコードは、多くのプログラミングの場面で役に立つと思われますが、3つの個別の花のイメージを3 x 6に並べて描画しているので、厳密に言うと、冒頭のメモの通りではありません。
プログラムその2
冒頭で述べた、渡されたメモの通りに花を植えるロボットのプログラムを考えてみました。ロボットはJavaScriptのクラスと呼ばれる方法を使ってその設計図を書いています。クラスからはnew演算子を使ってそのクラスのインスタンスと呼ばれるオブジェクトが作成できます。
ロボットのオブジェクトにメモ、つまり描画する画像ファイルのパスと行数、列数を渡してplant()メソッドを呼び出すと、ロボットはその行数と列数で画像ファイルのイメージを描画します。
function setup() {
createCanvas(400, 300);
background(230);
// GardeingRobotクラスのオブジェクトを作成する
const robot = new GardeingRobot();
// GardeingRobotオブジェクトのplant()メソッドを呼び出す
robot.plant('images/flowers3.png', 3, 2);
}
// 渡された画像を指定された行数列数で描画する
class GardeingRobot {
// コンストラクタには、描画開始位置と描画するイメージの空きが指定できる
// デフォルトは0
constructor(offX = 0, offY = 0, gutter = 0) {
this.offsetX = offX;
this.offsetY = offY;
this.gutter = gutter;
}
// 渡された画像ファイルのイメージを格子状に描く
plant(imageFilePath, rows, columns) {
this.imageFilePath = imageFilePath; // 画像ファイルのパス
this.rows = rows; // 行数
this.columns = columns; // 列数
// 画像ファイルのパスを使って画像を読み込む
loadImage(this.imageFilePath, (result) => {
// 読み込みが終わったら、格子状に描画する
this.image = result;
for (let x = 0; x < this.columns; x++) {
for (let y = 0; y < this.rows; y++) {
image(this.image, this.offsetX + x * (this.gutter + this.image.width), this.offsetY + y * (this.gutter + this.image.height));
}
}
});
}
}
下図はこの実行結果です。
クラスは設計図なので、ロボットはいくつでも作成できます。異なる開始位置や空き、画像ファイル、行数を与えることができます。
const robot1 = new GardeingRobot(20, 50, 5);
robot1.plant('images/blue.png', 8, 3);
const robot2 = new GardeingRobot(250, 20, 10);
robot2.plant('images/orange.png', 2, 3);
const robot3 = new GardeingRobot(150, 150);
robot3.plant('images/flowers3.png', 2, 2);
花壇を作っているような感覚です。