プログラミング はじめの一歩 JavaScript + p5.js編
19:ケーキを作る

この記事の詳しい内容には毎日新聞の「くり返しは台形で表す」ページから有料記事に進むことで読めます。

概要

あるロボットがいます。このロボットは、受け取ったレシピに書かれた順に材料を重ね、ケーキを作ります。たとえば、
「スポンジおく クリームぬる これを2回繰り返す」というレシピの場合には、
スポンジ クリーム スポンジ クリーム という4層のケーキを作ります。

では、「スポンジおく クリームぬる いちごおく これを3回繰り返す」というレシピでは、ロボットはどんなケーキを作るでしょう?

論理を考える

このお題は「16:3段重ねのアイスを注文したら?」によく似ています。「3段重ねのアイスを注文したら?」では、チョコ、バニラ、ストロベリーなどの文字列でアイスを表すことか始め、注文のリストに含まれるアイスのイメージを描画しました。

このお題でも同様の方法が取れますが、「スポンジおく クリームぬる これを2回繰り返す」という、動詞を使ったレシピに着目した別の方法で解決策を探ることにします。具体的に言うと、’スポンジ’、’クリーム’といった文字列でなく、スポンジおく、クリームぬる といった動作、アクションをベースにします。

フローチャート

例であげられている「スポンジおく クリームぬる これを2回繰り返す」という命令は下図のフローチャートで表されます。

コードで表す

このループは次のコードで表すことができます。putSponge()はスポンジをおく動作をする関数で、applyCream()はクリームをぬる動作を行う関数です。

// スポンジをおく関数
function putSponge() {
    print('スポンジをおいた');
}

// クリームをぬる関数
function applyCream() {
    print('クリームをぬった');
}

// 繰り返し回数
const repeatedNum = 2;
// repeatedNum分だけ繰り返す
for (let i = 0; i < repeatedNum; i++) {
    putSponge();
    applyCream();
}

putSponge()とapplyCream()関数はforループで2回繰り返して呼び出されるので、[コンソール]には、

スポンジをおいた
クリームをぬった
スポンジをおいた
クリームをぬった

と表示されます。このとき、レシピの「スポンジをおく」はputSponge()への呼び出しで、「クリームぬる」はapplyCream()への呼び出しで、「これを2回繰り返す」は、2が代入されたrepeatedNumを使ったforループで表されていると考えられます。

このお題を解決する基本的な論理は以上です。「いちごおく」は、ほかの関数と同様、putStrawberriesといった名前の関数を定義し、ループ内で呼び出します。

可視化

可視化にあたり、前の「3段重ねのアイスを注文したら?」では画像ファイルを使っていましたが、本稿ではp5.jsのrect()関数を使うことにします。

rect()関数はデフォルトで、描画する矩形の左上隅の座標を引数に取ります。ケーキの各層は画面下から上に向けて、rect()が描く矩形を積んでいくことで表現します。

そのとき考慮する必要があるのが、rect(x, y, w, h)のy値です。下図に示すように、y値は矩形を描くたびに変化します。前のy値から新しいy値への変化量は、新しいy値で描こうとしている層の厚さです。

また図のx値やw値は変化しないので、あらかじめ定数(変化しない変数)として宣言、初期化しておきます。h1やh2もその層固有の値なので定数で設定しておきます。

変数を次のように作成し、

// 変更しない値を定数として初期化しておく
const CAKE_W = 100; // ケーキの幅
const SPONGE_H = 40; // スポンジの厚み(高さ)
const CREAM_H = 20; // クリームの高さ
const STRABERRY_H = 10; // クリームの高さ

// この5つはp5.jsの定数を使用するので、setup()内で初期化する
let cakeX; // ケーキの左端
let cakeY; // ケーキの高さ
// 各層のカラー
let spongeColor, creamColor, straberryColor;

setup()関数内で次のように設定した場合、

function setup() {
    ...
    // 描画するケーキの位置取り
    cakeX = width / 2 - CAKE_W / 2;
    cakeY = height - 30;
    // カラーの設定
    spongeColor = color(223, 185, 101);
    creamColor = color(255, 252, 254);
    straberryColor = color(119, 50, 52);
    ...
}

スポンジを描画するputSponge()関数は次のように定義できます。

// スポンジの層を描く
function putSponge() {
  print('スポンジをおいた');
  fill(spongeColor);
  // スポンジの厚さ分だけ描画のy位置を上に移す
  cakeY = cakeY - SPONGE_H;
  rect(cakeX, cakeY, CAKE_W, SPONGE_H);
}

変数spongeColorはスポンジのカラーで、setup()内で色を割り当てています。これをfill()に渡すことで矩形の塗り色がspongeColorになります。その後は、前述した「y2 = y1 – h2」に当たるコードです。

後のapplyCream()やputStrawberries()関数も同様です。塗り色をその層のカラーに設定し、cakeYからその層の厚さを引いてcakeYに代入します。

// クリームの層を描く
function applyCream() {
    print('クリームをぬった');
    fill(creamColor);
    // クリームの厚さ分だけ描画のy位置を上に移す
    cakeY = cakeY - CREAM_H;
    rect(cakeX, cakeY, CAKE_W, CREAM_H);
}

// いちごの層を描く
function putStrawberries() {
    print('いちごをおいた');
    fill(straberryColor);
    // いちごの厚さ分だけ描画のy位置を上に移す
    cakeY = cakeY - STRABERRY_H;
    rect(cakeX, cakeY, CAKE_W, STRABERRY_H);
}

後はsetup()内で、前述したコードを実行します。repeatedNumには繰り返し回数を指定し、ループ内には、実行する順番で関数を記述します。

// 繰り返し回数
const repeatedNum = 2;
// 繰り返し回数分、ケーキの層を描く
for (let i = 0; i < repeatedNum; i++) {
    putSponge();
    applyCream();
    // putStrawberries();
}

下図はお題の例と問題の実行結果です。

動的変化に対応する

動的変化とは大げさな言い方ですが、要するにユーザー側でお好みのレシピが作れ、それにプログラムが対応する、ということです。Webアプリの場合、ユーザーはHTMLの入力要素を使って値をプログラムに送り、プログラムはそれに応じて結果を変えます。

静的とは動的の反対で、変化しないということです。今の場合、変化しないのは、forループでの関数の呼び出し順です、

putSponge();
applyCream();
putStrawberries();

この順番をつづける限り層の重なり順は変わりません。これを動的に、つまりユーザー操作によって変化させたいわけです。「16:3段重ねのアイスを注文したら?」のときには、アイスの配列への追加順で、アイスの重なりを変化させました。しかし今の場合は関数です。関数は配列に追加できるのでしょうか??

実はJavaScriptでは関数もオブジェクトなので、アイスのオブジェクトを追加したのと同じようにして配列に追加できます。しかも配列に[]をつづけ要素(関数)を特定し、その後に()をつづけることで、関数を呼び出すこともできます。普段はあまり使わないテクニックですが、覚えておくといつか役に立つ日がくるかもしれません。

// 関数の配列
let cakes = [putSponge, applyCream];
// 0番めの関数を呼び出す
cakes[0]();

以下に示すコードでは、<select>要素でケーキの構成要素を選び、ラジオボタンで繰り返し回数が指定できます。[ケーキを作る!]ボタンのクリックで、希望したケーキが描画されます。

// 変更しない値を定数として初期化しておく
const CAKE_W = 100; // ケーキの幅
const SPONGE_H = 40; // スポンジの厚み(高さ)
const CREAM_H = 20; // クリームの高さ
const STRABERRY_H = 10; // いちごの高さ

// この5つはp5.jsの変数と関数を使用するので、setup()内で初期化する
let cakeX; // ケーキの左端
let cakeY; // ケーキの高さ
// 各層のカラー
let spongeColor, creamColor, straberryColor;

let repeatedNum = 0; // 繰り返し回数
let cakes = []; // ケーキの構成要素の文字列を入れる

function preload() {
    robotImage = loadImage('images/robot.png');
}

function setup() {
    createCanvas(400, 300);
    // 描画するケーキの位置取り
    cakeX = width / 2 - CAKE_W / 2;
    cakeY = height - 30;
    // カラーの設定
    spongeColor = color(223, 185, 101);
    creamColor = color(255, 252, 254);
    straberryColor = color(119, 50, 52);
    // ケーキを構成する要素を選択
    let select = createSelect();
    select.option('選択');
    select.option('スポンジ');
    select.option('クリーム');
    select.option('いちご');
    select.position(20, 240);
    // 選択に変化があったら
    select.changed(() => {
        // '選択'が選ばれた場合は無視
        if (select.value() === '選択') return;
        // 選択値をcakes配列に追加
        cakes.push(select.value());
        // ここでキャンバス要素を再描画し次のケーキの描画に備える
        drawBack();
    });

    // 繰り返し回数を指定するラジオボタン
    const repeatedNumber = createRadio();
    repeatedNumber.option(2);
    repeatedNumber.option(3);
    repeatedNumber.position(20, 280);
    repeatedNumber.style('width', '250px');
    // 選択値をrepeatedNum変数に代入
    repeatedNumber.changed(() => {
        repeatedNum = int(repeatedNumber.value());
    });

    // [ケーキを作る]ボタン
    const cakeButton = setButton('ケーキを作る!', {
        x: 280,
        y: 250
    });
    // ボタンのクリックで
    cakeButton.mousePressed(() => {
        // makeRecipe()関数にcakes配列を渡し、ケーキの手順を含んだ配列を受け取る
        // この配列には関数への参照が入っている
        const functions = makeRecipe(cakes);
        // ケーキを作る
        makeCake(functions);
        cakes = [];
        cakeY = height - 30;
    });
    // キャンバス要素を描画
    drawBack();
}

// レシピを作る
// 引数の配列の順番に合わせ、その動作を実行する関数への参照を配列に追加しその配列を返す
// ['スポンジ','クリーム'] => [putSponge, applyCream]
function makeRecipe(arr) {
    let funcs = [];
    for (let i = 0; i < arr.length; i++) {
        const cakeStr = arr[i];
        if (cakeStr === 'スポンジ') {
            funcs.push(putSponge);
        }
        else if (cakeStr === 'クリーム') {
            funcs.push(applyCream);
        }
        else if (cakeStr === 'いちご') {
            funcs.push(putStrawberries);
        }
    }
    return funcs;
}

// ケーキを作る 引数の配列には関数への参照が入っている
function makeCake(functions) {
    // ラジオボタンで指定された繰り返し回数だけ繰り返し
    for (let i = 0; i < repeatedNum; i++) {
        // 引数の配列の長さ分(関数への参照数)だけ繰り返し
        for (let j = 0; j < functions.length; j++) {
            // 配列に含まれる関数を順番に実行する
            functions[j]();
        }
    }
}

// スポンジを置く
function putSponge() {
    print('スポンジをおいた');
    fill(spongeColor);
    // 描画前に自分の高さ分だけ、描画位置を上に移す
    cakeY = cakeY - SPONGE_H;
    rect(cakeX, cakeY, CAKE_W, SPONGE_H);
}

// クリームをぬる
function applyCream() {
    print('クリームをぬった');
    fill(creamColor);
    cakeY = cakeY - CREAM_H;
    rect(cakeX, cakeY, CAKE_W, CREAM_H);
}

// いちごをおく
function putStrawberries() {
    print('いちごをおいた');
    fill(straberryColor);
    cakeY = cakeY - STRABERRY_H;
    rect(cakeX, cakeY, CAKE_W, STRABERRY_H);
}

// 背景とロボットを描画する
function drawBack() {
    background(230, 237, 186);
    image(robotImage, 300, 120);
}

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

コメントを残す

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

CAPTCHA