プログラミング はじめの一歩 JavaScript + p5.js編
17:かべにかざりを付けるには?

この記事の詳しい内容には毎日新聞の「かべにかざりを付けるには?」ページから有料記事に進むことで読めます。

概要

ロボットにメモを渡して、かべかざりを手伝ってもらいます。かざりは下図の円と星と四角です。

ロボットには「2回繰り返す 円 星 四角」といったメモを渡します。するとロボットは円、星、四角を2回繰り返したかざり付けをします。

ここで問題です。次のようにかざるには、ロボットにどんなメモを渡せばよいでしょうか?

論理を考える

問題の答えは簡単です。左から星、四角、円、星の並びが3回繰り返されているので、渡せばよいのは「3回繰り返す 星 四角 円 星」というメモです。

では、どのような論理を組み立てたらこれが実現できるのか、ロボットの立場で考えていきましょう。

ロボットが受け取るものと返すもの

ロボットが受け取るのは、「円 星 四角」といったかざりの並びと、2や3の繰り返し回数です。繰り返し回数は数値で表せます。かざりの並びは、円や星、四角を表すものを、円、星、四角の順で入れた配列で表せます。円や星、四角を表すものは、描画に必要なイメージを持つ必要がありますが、プログラミングのとっかかり時には、最も簡単で分かりやすい文字列で表せます。

let decos = ['円', '星', '四角']; // かざりの並びを文字列で表す配列
let num = 2; // 繰り返し回数は数値

かざりの並びと繰り返し回数を受け取ったロボットは、結果を返します。結果は「円 星 四角 円 星 四角」といった文字列の並びなので、配列で表せます。かざりの並びはまだ受け取っていないので配列は空です。

// かざりの並びの繰り返しを入れる配列
let wall = [];

上記コードの場合、decos 配列[‘円’, ‘星’, ‘四角’]を受け取り、これを2回繰り返すことで、[‘円’, ‘星’, ‘四角’, ‘円’, ‘星’, ‘四角’]というwall配列を作成したいわけです。このための方法としては、むろん配列のメソッドを使う方法もありますが、それは後に回すとして、理屈の分かりやすい、もっと地道な方法があります。

それは、decos 配列を走査してかざりの文字列を1つずつ取得し、それをwall配列に追加していく方法です。元になる配列の要素を1つずつ、空の配列に追加していくのですから、同じ要素を持つ配列になるのは当然です。2回繰り返すと、目的とする配列が得られます。

// 繰り返す回数だけ
for (let i = 0; i < num; i++) {
    // かざりの配列の文字列数分繰り返し
    for (let j = 0; j < decos.length; j++) {
        // かざりの各文字列を特定して
        const decoStr = decos[j];
        // かざりの並びを入れる配列に追加
        wall.push(decoStr);
    }
}

ここまでをsetup()関数で実行し、最後にwall配列を出力すると、[“円”, “星”, “四角”, “円”, “星”, “四角”] という結果が得られます。

ロボットを関数化する

ロボットの働きをgetDecorateArrayという名前の関数で表してみましょう。この関数はかざりの配列と繰り返し回数をパラメータとして取ります。関数本体({から}までのコード)は前述したforループで、最後にreturnで配列を関数の呼び出し元に返します。

// 配列と繰り返す回数を受け取り、配列の文字列の並びを繰り返して結果を新しい配列で返す
function getDecorateArray(array, repeatedNum) {
    let result = [];
    for (let i = 0; i < repeatedNum; i++) {
        for (let j = 0; j < array.length; j++) {
            const decoStr = array[j];
            result.push(decoStr);
        }
    }
    return result;
}

getDecorateArray()関数は次のように使用できす。

num = 2;
let result = getDecorateArray(['円', '星', '四角'], num);
print(result);
num = 3;
result = getDecorateArray(['星', '四角', '円', '星'], num);
print(result);

関数の実行結果から、[‘円’, ‘星’, ‘四角’]と2を渡すことで下図に示す上の配列が、[‘星’, ‘四角’, ‘円’, ‘星’]と3を渡すことで下の配列が返されていることが分かります。

可視化

論理ができたので、かざりのイメージを描画しましょう。

Mapオブジェクト

論理で言うと、’円’と’星’、’四角’で表してきた円と星、四角を、イメージでも表せるようにすることです。当シリーズでは「16:3段重ねのアイスを注文したら?」の「文字列からイメージにたどり着く方法2」で述べているObjectオブジェクトや「文字列からイメージにたどり着く方法3」で述べているMapオブジェクトによる方法などを取り上げています。本稿ではMapオブジェクトを使うことにします。

MapオブジェクトはObjectオブジェクトと同じ目的で使用でき、get()やset()など独自のメソッドを持っています。

preload()関数では、次のようにして、Mapオブジェクトを作成し、decos配列に収めます。

// かざりを表す文字列とイメージを持つMapオブジェクトを入れる配列
let decos = [];

function preload() {
    const circle = loadImage('images/circle.png');
    const rect = loadImage('images/rect.png');
    const star = loadImage('images/star.png');
    let decosName = ['円', '四角', '星']; // かざりの文字列
    let decosImages = [circle, rect, star]; // かざりのイメージ

    // かざりの数だけ繰り返し
    for (let i = 0; i < decosName.length; i++) {
        // 各かざりを表すMapオブジェクトを作成して
        const map = new Map();
        map.set('name', decosName[i]);
        map.set('image', decosImages[i]);
        // decos配列に追加する
        decos.push(map);
    }
    print(decos); // [Map, Map, Map]という配列
}

setup()関数では、getDecorateArray()関数を呼び出します。

function setup() {
    createCanvas(800, 120);
    background(220);
    // 円 星 四角 を2回繰り返す
    const resultArray = getDecorateArray(['円', '星', '四角'], 2);
    ...
文字列をイメージへ

getDecorateArray()関数は配列を返します。[‘円’, ‘星’, ‘四角’], 2 を渡した場合には、[‘円’, ‘星’, ‘四角’, ‘円’, ‘星’, ‘四角’]を返します。これを、Mapオブジェクトを入れたdecos配列を使って、イメージに置き換えるわけです。

forループを使った方法

それを行う関数には、forループをがんばって使うと、次のようなものが記述できます。

// 渡された配列を使って、decos配列内のMapオブジェクトが持つイメージに置き換え、キャンバスに描画する
function drawDeco(arr) {
    // 描画するイメージを入れる配列
    let wall = [];
    // 受け取った配列の文字列を調べ、
    for (let i = 0; i < arr.length; i++) {
        const name = arr[i];
        // その文字列とMapオブジェクトを比較して
        for (let j = 0; j < decos.length; j++) {
            const map = decos[j];
            // 文字列と同等のMapオブジェクトなら
            if (name === map.get('name')) {
                // そのMapオブジェクトが持つイメージをwall配列に追加する
                wall.push(map.get('image'));
            }
        }
    }
    // wall配列にarrの文字列と同等のイメージを描画する
    for (let i = 0; i < wall.length; i++) {
        image(wall[i], 50 + 40 * i, 50);
    }
}

後はsetup()関数のgetDecorateArray()の呼び出しの後で、このdrawDeco()関数に文字列の配列(resultArray)を渡します。

const resultArray = getDecorateArray(['円', '星', '四角'], 2);
drawDeco(resultArray);

すると、下図の上に示すかざりのイメージの並びが描画されます。getDecorateArray([‘星’, ‘四角’, ‘円’, ‘星’], 3)を実行すると、下図の下に示すイメージの並びが描画されます。

方法はいくつも考えられる

プログラミングの正解は、思ったことが実現できたときのコードであると言えます。数学のテストのように正解は1つではなく、正解というよりも解決策で、通常はいくつもあります。

配列のコピー

前述のgetDecorateArray()関数では、forループを駆使して空のwall配列にかざりの文字列を1つずつ追加していきましたが、ほかにも方法はあります。

次のgetDecorateArray()関数では、配列のconcat()メソッドを使って配列を複製し、それを空の配列に追加しています。配列は [[],[],…] という構造の2次元配列になるので、それをflat()メソッドで1次元配列にならしています。

// 配列のconcat()を使って配列を複製する
function getDecorateArray(array, repeatedNum) {
    // 配列を入れる配列
    let outerArray = [];
    // 指定された回数だけ繰り返し、
    for (let i = 0; i < repeatedNum; i++) {
        // 受け取った配列のコピーを作成して
        let copy = array.concat();
        // 配列に追加する => 2次元配列
        outerArray.push(copy);
    }
    // 2次元配列を1次元配列に変換
    outerArray = outerArray.flat();
    return outerArray; // 配列を返す
}
・・から配列を作る

また次のgetDecorateArray()関数では、Arrayクラスの静的メソッドfrom()を使って、反復可能なオブジェクトから配列を作成し、そこに引数の配列を入れています。これも2次元配列になるので、flat()で平滑化します。

function getDecorateArray(array, repeatedNum) {
    let outer = Array.from({
        length: repeatedNum
    }, (element, i) => {
        //print(i)  // 0,1,2
        return array;
    });
    outer = outer.flat();
    return outer;
}

Array.from({ length: repeatedNum }で作成されるのは、lengthプロパティがrepeatedNumの配列で、repeatedNumが2の場合には、[ undefined,undefined ] という配列が作成されます。from()メソッドの第2引数には各要素に対して実行する関数が指定できるので、ここにreturn array; を記述すると、繰り返しのたびにarrayが返され、結果、[array, array,array ]という2次元配列になります。

配列の各要素に対して

もちろん、drawDeco()関数の機能も書き方は1つではありません。次のコードでは、wall配列に追加するMapオブジェクトのイメージの特定に、配列のforEach()メソッドを使っています。

function drawDeco(arr) {
    let wall = [];
    arr.forEach((elm) => {
        //print(elm); // '星', '四角', '円', '星'
        for (let i = 0; i < decos.length; i++) {
            if (elm === decos[i].get('name')) {
                wall.push(decos[i].get('image'));
            }
        }
    });
    for (let i = 0; i < wall.length; i++) {
        image(wall[i], 50 + 40 * i, 50);
    }
}

方法はいくつもあります。ではどこで優劣をつけるのか?というと、実行スピードや後々の修正のしやすさなどで決められます。たとえばforEach()は便利なメソッドですが、forループの方が高速に処理できます。

かべかざりアプリ

最後に、下図の「かべかざりアプリ」のコード例を示しておきます。キャンバス下部左の3つのイメージは<img>要素のボタンで、かざりが選べます。その右のラジオボタンで繰り返し回数を指定し、その右の[ACTION!]ボタンで飾りを描画します。

let decos = [];
// キャンバス左下に<img>要素を表示するので、イメージのロードと共用できるように、パスをグローバル変数にする
let imagePath = ['images/circle.png', 'images/rect.png', 'images/star.png'];
// 共用できるようにグローバル変数にする
let decosName = ['円', '四角', '星'];

let actionArray = []; // <img>要素のクリックで追加するかざりの文字列を入れる配列
let wall = []; // 描画するかざりのイメージを入れる配列

function preload() {
    const circle = loadImage(imagePath[0]);
    const rect = loadImage(imagePath[1]);
    const star = loadImage(imagePath[2]);
    let decosImages = [circle, rect, star];

    for (let i = 0; i < decosName.length; i++) {
        const map = new Map();
        map.set('name', decosName[i]);
        map.set('image', decosImages[i]);
        decos.push(map);
    }
}

function setup() {
    createCanvas(800, 120);
    // かざりの<img>要素を作成し、クリックでかざりの文字列をactionArray配列に追加
    for (let i = 0; i < imagePath.length; i++) {
        const img = createImg(imagePath[i], decosName[i], () => {
            img.position(20 + img.width * i, 130);
            img.mousePressed(() => {
                const alt = img.elt.alt;
                actionArray.push(alt);
                print(actionArray); //  ["星", "四角", "円"...]
            })
        });
    }
    // 繰り返し回数を指定するラジオボタン
    const radioButton = createRadio();
    radioButton.option('1');
    radioButton.option('2');
    radioButton.option('3');
    radioButton.position(150, 140);
    radioButton.style('width', '150px');
    radioButton.selected('1');

    // [Action]ボタン
    const actionButton = setButton('ACTION!', {
        x: 250,
        y: 135
    });
    // ボタンのクリックでかざりの並びを描画
    actionButton.mousePressed(() => {
        wall = [];
        const resultArray = getDecorateArray(actionArray, radioButton.value());
        drawDeco(resultArray);
        actionArray = [];
    });
}

function draw() {
    background(220);
    // かざりのイメージを描画
    for (let i = 0; i < wall.length; i++) {
        image(wall[i], 50 + 40 * i, 50);
    }
}

function getDecorateArray(array, repeatedNum) {
    let result = [];
    for (let i = 0; i < repeatedNum; i++) {
        for (let j = 0; j < array.length; j++) {
            const decoStr = array[j];
            result.push(decoStr);
        }
    }
    return result;
}

function drawDeco(arr) {
    for (let i = 0; i < arr.length; i++) {
        const name = arr[i];
        for (let j = 0; j < decos.length; j++) {
            const map = decos[j];
            if (name === map.get('name')) {
                wall.push(map.get('image'));
            }
        }
    }
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(90, 30);
    button.position(pos.x, pos.y);
    return button;
}

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA