プログラミング はじめの一歩 JavaScript + p5.js編
27:レシピ通りにパフェを作る

この記事の詳しい内容には毎日新聞の「手順を反復して処理」ページから有料記事に進むことで読めます。

概要

このカフェではロボットがパフェを作ります。ロボットはレシピの手順通りにソースを重ねてパフェを作ります。たとえば下図左のレシピなら、下図右のパフェを作ります。

* 入れ物のグラスには、レシピ手順通りにソースが入っていきます。チョコソースが最初なので、これがグラスの底に入ります。そして生クリーム、コーンフレークの繰り返し(生クリーム、コーンフレーク、生クリーム、コーンフレーク)がその上に重なり、最後のチョコソースが一番上の層になります。

ここで問題です。下図のパフェを作る場合、ロボットにはどんなレシピを渡せばよいでしょう? 下のレシピの空欄にソース名を入れてください。

論理を考える

このお題は前の「16:3段重ねのアイスを注文したら?」や「19:ケーキを作る」を参考にすると解決できます。

解決策が同じでは面白くないので、本稿では、パフェを作る上で人間がやる仕事とロボットがやる仕事を区分けするという観点からプログラムを考えていくことにします。

人間がやる仕事とロボットがやる仕事

たとえばレシピを考えるのは人間の仕事です。最新式のAIロボットならレシピの考案もやるのかもしれませんが、お題のロボットにそこまでの能力はなさそうです。またソースはどのようなものにするかも人間が考えることです。パフェはその色合いや味の組み合わせを楽しむものなので、同じチョコソースを使っても、パティシエによって味も見た目も異なるはずです。

そこで、人間がやる仕事とロボットがやる仕事を下図のように分けることにします。

具体的に言うと、人間の仕事はどのようなソースにするかとソースを使ってどんなレシピにするかを考え、それをロボットに教えることです。ロボットはそのレシピを受け取りレシピ通りにパフェを作ります。

基本的な論理

上図にもとづいて、人間がソースを作るということについて見ていきましょう。ソースを作ると言ってもプログラミングでどうやって? と思われるかもしれませんが、まずは文字列で表すところから始めます。ソースはいくつもあるので配列に入れて扱いやすくします。

ソースを作るというアクションなので、makeSource()という関数を考えます。そしてソースを表す文字列を入れた配列を作成します。作った結果は返したいのでreturnで配列を返します。ソースの文字列の順番は何でもかまいません。どのソースを使うかはレシピを作るときに考えることです。

// ソースを作る(暫定的に文字列で表す)
function makeSource() {
  let sourcesArray = ['チョコソース', '生クリーム', 'コーンフレーク'];
  return sourcesArray;
}

すると、この文字列のソースの配列は、次の呼び出しで得ることができます。

const sources = makeSource();

ソースができたらそれを使ってレシピを作ります。レシピを作るのでmakeRecipeという名前の関数を定義します。この関数はソース(sources配列)を必要とするので、パラメータを設定します。

// ソースからレシピを作る
function makeRecipe(sourcesArray) {
    let recipe = [];
    // ソース配列を使ってソースの層を構成する
    recipe.push(sourcesArray[0]);
    for (let i = 0; i < 2; i++) {
        recipe.push(sourcesArray[1]);
        recipe.push(sourcesArray[2]);
    }
    recipe.push(sourcesArray[0]);
    return recipe;
}

関数の内容は、お題の例のレシピのままです。最初に空の配列を作成しておいて、そこにまずソースの配列から’チョコソース’を追加します。そしてforループを使って2回、’生クリーム’と’コーンフレーク’を追加し、最後に再度’チョコソース’を追加します。

makeRecipe()関数には、前のmakeSource()関数が返したsources配列を渡します。

const sources = makeSource();
const recipe = makeRecipe(sources);

ロボットにはこのrecipe配列を渡します。ロボットの仕事をmakeParfait()関数とすると、この関数にはレシピのパラメータが要ります。次のコードでは受け取った配列を出力するだけです。

// レシピからパフェを作る
function makeParfait(recipeArray) {
  print(recipeArray);
}

makeParfait()関数は、前のmakeRecipe()が返したrecipeを渡して呼び出します。

const sources = makeSource();
const recipe = makeRecipe(sources);
makeParfait(recipe);

するとmakeParfait()関数に記述したprint(recipeArray)が実行されて、[コンソール]に、

 [“チョコソース”, “生クリーム”, “コーンフレーク”, “生クリーム”, “コーンフレーク”, “チョコソース”]

が出力されるはずです。これで、人間がソースとレシピを考え、それをロボットに伝えて、ロボットがパフェを作る、というプログラムの基本的な論理が構築できました。

以下はここまでの全コードです。人間の役割りを果たす関数とロボットの役割りを果たす関数との間に破線を引いて区分けしています。

function setup() {
    noCanvas();
    // 人間がソースを作る
    const sources = makeSource();
    // 人間がレシピを作る
    const recipe = makeRecipe(sources);
    // ロボットがパフェを作る
    makeParfait(recipe);
}

// ソースを作る(暫定的に文字列で表す)
// 人間の仕事
function makeSource() {
    let sourcesArray = ['チョコソース', '生クリーム', 'コーンフレーク'];
    return sourcesArray;
}

// ソースからレシピを作る
// 人間の仕事
function makeRecipe(sourcesArray) {
        let recipe = [];
        // ソース配列を使ってソースの層を構成する
        recipe.push(sourcesArray[0]);
        for (let i = 0; i < 2; i++) {
            recipe.push(sourcesArray[1]);
            recipe.push(sourcesArray[2]);
        }
        recipe.push(sourcesArray[0]);
        return recipe;
    }
    // -------------------------------------------------------------------
    // レシピからパフェを作る
    // ロボットの仕事
function makeParfait(recipeArray) {
    print(recipeArray);
}

* おそらくお気づきでしょうが、お題のレシピでは、グラスのより下層にくるソースから指定しています。パフェを作るには下のものを先に入れる必要があるので、当然の処理です。この基本的な論理でも同様に、より下層にくるソースから先に配列に追加しています。したがって配列からソースを得るときには、配列の先頭から調べることになります。

ソースの試作

ソースは今のところ文字列で表していますが、少なくとも色が必要です。たとえばソースが’チョコソース’の場合には、その部分を濃い茶色で塗る必要があります。

以下ではそのために、ソースをMapオブジェクトで表すことにします。Mapオブジェクトは名前(キー)と値を対応付けするためのオブジェクトで、’チョコソース’と濃い茶色を対応付けできます。

そのためには次のような関数を新たに作成します。このsourceMapping()関数は引数で渡された名前をキーnameの値とし、カラーをキーcolの値として持つMapオブジェクトを返します。

// ソースの素材と色を関係付ける
function sourceMapping(name, col) {
    const map = new Map();
    map.set('name', name);
    map.set('col', col);
    return map;
}

するとmakeSource()関数は次のように書き換えることができます。

// 人間がソースの素材と色を決めてソースを作る
function makeSource() {
    // チョコソースは濃い茶色
    const chocolateSauce = sourceMapping('チョコソース', color(16, 3, 3));
    // コーンフレークは濃い黄色
    const cornFlake = sourceMapping('コーンフレーク', color(223, 188, 43));
    // 生クリームはほぼ白
    const freshCream = sourceMapping('生クリーム', color(239, 238, 240));
    let sourcesArray = [chocolateSauce, cornFlake, freshCream];
    return sourcesArray;
}

makeSource()が返すsourcesArray 配列は、前はただの文字列の配列でしたが、ここでは名前とカラーの情報を持つMapオブジェクトの配列に変わりました。

作るレシピに変わりはないので、makeRecipe()関数に手を加える必要はありません。後はできあがったレシピをロボットに渡して、人間の仕事は終わりです。

ロボットのアクションであるmakeParfait()関数では当然、修正が必要です。受け取ったレシピの配列には、今度は文字列でなくMapオブジェクトが含まれているので、それをforループで走査して調べます。

// ロボットがパフェを作る
function makeParfait(recipeArray) {
    // レシピの配列を走査
    for (let i = 0; i < recipeArray.length; i++) {
        // 配列に含まれるMapオブジェクトを特定
        const targetMap = recipeArray[i];
        // そのMapオブジェクトのcol値を調べる
        const col = targetMap.get('col');
        // それを塗り色にする
        fill(col);
        // y値を変化させて矩形の層を描画する
        rect(150, 200 - i * 20, 100, 20);
    }
}

扱いたいのはソースの色です。これは当該Mapオブジェクトのキーcol値です。この値を取得してfill()関数の塗り色に設定し、矩形を描きます。配列にはより下層に来るソースが先に入っているので、矩形のy値を順に小さくしてより上方に描きます。

setup()関数では、キャンバスを作成し背景色を設定します。後は「基本的な論理」のコードと同じです。

// ソースの試作
function setup() {
    createCanvas(400, 300);
    background(211, 227, 190);
    // 人間がソースを作る
    const sources = makeSource();
    // 人間がレシピを作る
    const recipe = makeRecipe(sources);
    // ロボットがパフェを作る
    makeParfait(recipe);
}

プログラムを実行すると、下図の結果が描画されます。

ロボットの仕事を表すmakeParfait()が受け取るレシピの配列recipeArrayにはソースの色の情報を持ったMapオブジェクトが含まれているので、それを調べてキャンバスの塗り色に設定し矩形を描画します。レシピに含まれるソースと、それをパフェにして順番に重ねたソースの層は、下図のように逆転の関係にあります。

以上がソースの試作です。ソースの種類ごとに色を設定したMapオブジェクトを使うことで、基本的な論理にソースの色を持ち込むことができました。ただしソースにはそれぞれ適切な量(厚さ)があるはずです。多すぎず少なすぎず、パティシエがそのパフェにとって一番適切だとする量です。この情報はMapオブジェクトに含めることができます。

次はグラスのイメージを加え、そこにソースの層を重ねていきましょう。

ソースはグラスの向こうにある

「ソースはグラスの向こうにある」を説明する前に、ソースごとの適切な量を設定しておきましょう。この量は実際にはロボットが描く矩形の高さになります。

まずソースを表すMapオブジェクトを作成するsourceMapping()関数にパラメータhを追加し、’height’キーの値を設定するように変更します。

// ソースの厚さhを加える
function sourceMapping(name, col, h) {
    const map = new Map();
    map.set('name', name);
    map.set('col', col);
    map.set('height', h);
    return map;
}

そしてmakeSource()関数内で呼び出しているsourceMapping()関数の第3引数にソースの厚さの数値を加えます。この値はパティシエが決めるものですが、実際のプログラミングでは、ソースの層の数やグラスのイメージのサイズなどを勘案し、そのパフェらしく見えるように大小を調整して決めることになります。

// sourceMapping()の第3引数にそのソースの厚さを追加
function makeSource() {
    // チョコソースは濃い茶色、厚さは18
    const chocolateSauce = sourceMapping('チョコソース', color(16, 3, 3), 18);
    // コーンフレークは濃い黄色、厚さは5
    const cornFlake = sourceMapping('コーンフレーク', color(223, 188, 43), 5);
    // 生クリームはほぼ白、厚さは10
    const freshCream = sourceMapping('生クリーム', color(239, 238, 240), 10);
    let sourcesArray = [chocolateSauce, cornFlake, freshCream];
    return sourcesArray;
}

ソースの適切な量が設定できたので、「ソースはグラスの向こうにある」を説明していきましょう。

まず使用するイメージを読み込みます。

// 使用するイメージ
let glassImage, robotImage;

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

このglass.pngには次のファイルを使用します。グラスにソースが入る部分を透明にし、グラス全体を囲む矩形の背景(透明部分をのぞく)をキャンバスの背景色と同じにしています。

glass.pngファイルをこのようにしているのは、このファイルのイメージをソースの矩形の後に描き、グラスにソースが入る湾曲した部分でだけソースが見えるようにし、残りはグラスのイメージで覆い隠すためです。下図の左はこの効果をよく示しています。下図のソースの矩形を少し右にずらすと下図右に示す、ソースが入ったグラスに見えます。

実世界では、グラスがあってその中にソースが入っていくのでしょうが、ここではそうせず、ずるくてうまい方法でやり抜けています。下の動画は「【Blender】水をグラスに注ぐような流体シュミレーションのやり方(2/4)【障害物】」で公開されているもので、3DCGアプリの流体シミュレーション機能を使えばこのようなことは可能です。

ソースの矩形を描く

ロボットのmakeParfait()関数では「ソースの試作」と同じように、矩形を描きます。ただしソースの層の厚さ、つまり矩形の高さを変える必要があります。また、矩形のy位置(rect(x,y,w,h)のy)は、「19:ケーキを作る」の「可視化」で述べているように、その矩形の高さ分だけ引く必要があります。

次のコードでは、グラスのイメージを描く位置として、グローバル変数のglassXとglassYを設定しています。ソースの矩形はこの値を基準の位置として描きます。startY変数は矩形が描画を開始するy位置です。この変数にソースの厚さ分だけ引いて、キャンバスのより上方に矩形を描きます。

// グラスの位置 => これを基準にソースの矩形を描く
const glassX = 150;
const glassY = 100;
// 矩形の描画開始位置
let startY = glassY + 66;

function makeParfait(recipeArray) {
    for (let i = 0; i < recipeArray.length; i++) {
        const targetMap = recipeArray[i];
        const col = targetMap.get('col');
        // そのheightキーの値(厚さ)
        const h = targetMap.get('height');
        fill(col);
        // 矩形の描画開始位置をh分だけ上げる
        startY = startY - h;
        rect(glassX + 5, startY, 55, h);
    }
}
チョコパフェを描く全コード

以下はここまで述べてきたことをまとめた、お題の例(チョコパフェ)を描画する全コードです。

// 使用するイメージ
let glassImage, robotImage;
// グラスの位置 => これを基準にソースの矩形を描く
const glassX = 150;
const glassY = 100;
// 矩形の描画開始位置
let startY = glassY + 66;

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

function setup() {
    createCanvas(400, 300);
    noStroke();
    background(211, 227, 190);

    const sources = makeSource();
    const recipe = makeRecipe(sources);

    // ロボットのイメージを描画(ただの飾り)
    image(robotImage, 280, 150);

    // ソースの層を描いてからグラスのイメージを描画する
    makeParfait(recipe);
    image(glassImage, glassX, glassY);

}

// sourceMapping()の第3引数にそのソースの厚さを追加
function makeSource() {
    // チョコソースは濃い茶色、厚さは18
    const chocolateSauce = sourceMapping('チョコソース', color(16, 3, 3), 18);
    // コーンフレークは濃い黄色、厚さは5
    const cornFlake = sourceMapping('コーンフレーク', color(223, 188, 43), 5);
    // 生クリームはほぼ白、厚さは10
    const freshCream = sourceMapping('生クリーム', color(239, 238, 240), 10);
    let sourcesArray = [chocolateSauce, cornFlake, freshCream];
    return sourcesArray;
}

// ソースの厚さhを加える
function sourceMapping(name, col, h) {
    const map = new Map();
    map.set('name', name);
    map.set('col', col);
    map.set('height', h);
    return map;
}

// レシピの作成方法に変更はない
function makeRecipe(sourcesArray) {
    let recipe = [];
    recipe.push(sourcesArray[0]);
    for (let i = 0; i < 2; i++) {
        recipe.push(sourcesArray[1]);
        recipe.push(sourcesArray[2]);
    }
    recipe.push(sourcesArray[0]);
    return recipe;
}

// -------------------------------------------------------------------
function makeParfait(recipeArray) {
    for (let i = 0; i < recipeArray.length; i++) {
        const targetMap = recipeArray[i];
        const col = targetMap.get('col');
        // そのheightキーの値(厚さ)
        const h = targetMap.get('height');
        fill(col);
        // 矩形の描画開始位置をh分だけ上げる
        startY = startY - h;
        rect(glassX + 5, startY, 55, h);
    }
}

下図は実行結果です。

いちごパフェを描くコード

お題の問題のいちごパフェを描くには、makeSource()関数でイチゴソースとイチゴのMapオブジェクトを追加して作成し、makeRecipe()関数でレシピをいちごパフェ用のものに変更するだけです。

// ソースを追加
function makeSource() {
    const chocolateSauce = sourceMapping('チョコソース', color(16, 3, 3), 18);
    const cornFlake = sourceMapping('コーンフレーク', color(223, 188, 43), 5);
    const freshCream = sourceMapping('生クリーム', color(239, 238, 240), 10);

    // イチゴソースは濃い赤、厚さは26
    const strawberrySauce = sourceMapping('イチゴソース', color(201, 66, 5), 26);
    // イチゴは淡い赤、厚さは5
    const strawberry = sourceMapping('イチゴ', color(213, 131, 101), 5);

    // 追加したソースを追加
    let sourcesArray = [chocolateSauce, cornFlake, freshCream, strawberrySauce, strawberry];
    return sourcesArray;
}

// レシピを変更する
function makeRecipe(sourcesArray) {
    let recipe = [];
    // 最初はイチゴソース
    recipe.push(sourcesArray[3]);
    // コーンフレークと生クリームとイチゴのソースを2回
    for (let i = 0; i < 2; i++) {
        recipe.push(sourcesArray[1]);
        recipe.push(sourcesArray[2]);
        recipe.push(sourcesArray[4]);
    }
    return recipe;
}

下図はこの実行結果です。

makeSource()関数には今ソースが5種類あり、みんさんはパティシエさながら、これらをどのようにでもレシピで組み合わせることができます。人間がやる仕事はソースとレシピの作成で、レシピが適切にできてさえいれば、ロボットはレシピ通りにパフェを作ります。

コメントを残す

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

CAPTCHA