プログラミング はじめの一歩 JavaScript + p5.js編
18:荷物のシールで仕分ける

この記事の詳しい内容には毎日新聞の「荷物のシールで別のかごに」ページから有料記事に進むことで読めます。

概要

この工場では、ベルトコンベアを流れてきた荷物をロボットが仕分けます。ロボットは荷物にはられたシールで、荷物を入れるかごを決めます。

  • シールが「割れ物注意」のときはかごAに、
  • シールが「危険マーク」のときはかごBに、
  • シールが「ニコちゃんマーク」のときはかごCに入れる

たとえば、「割れ物注意」、「危険マーク」、「ニコちゃんマーク」の順で荷物が流れてきたら、ロボットは荷物をそれぞれ、A,B,Cのかごに入れます。

ここで問題です。「割れ物注意」、「ニコちゃんマーク」、「危険マーク」、「割れ物注意」、「危険マーク」の順で荷物が流れてきたら、ロボットは最初の荷物から最後の荷物まで、どのかごに入れるでしょうか? A,B,Cで答えてください。

論理を考える

答えはいたって簡単です。シールとかごの対応は、

  • 「割れ物注意」=> A
  • 「ニコちゃんマーク」 => C
  • 「危険マーク」=> B
  • 「割れ物注意」=> A
  • 「危険マーク」=> B

となるので、答えはA,C,B,A,Bです。この対応こそがこのお題を解決する根本的な論理です。

荷物をどう表すか?

まず必要なのは荷物です。荷物はどうやって表せばよいでしょうか? 荷物にはシールをはるので、’コップ’や’花瓶’といった文字列でなく、シールを属性として表せるObjectオブジェクトやMapオブジェクトの方が適しています。

また、荷物はシールで仕分けるので、荷物の中身は問われません。実世界では、荷物の中身に対してはるシールが決められますが、今の場合、シールはもうはられており、中身が何かは分かりません。ということは、荷物を表すオブジェクトは、ベルトコンベアを流れてくる荷物にはられたシールで作成できるということです。中身がコップでも花瓶でも、’割れ物注意’というシールがはられた同じ種類のオブジェクトで表せるということです。

流れてくる荷物のシールを文字列の配列で表すと、荷物を表すオブジェクトは次のようなコードで作成できます。荷物はまとめて扱えるように専用の配列に入れます。

// 流れてくる荷物にはられたシールの配列
let seals = ['割れ物注意', '危険マーク', 'ニコちゃんマーク'];
// ベルトコンベアを流れる荷物を入れる配列
let luggagesOnbeltConveyor = seals.map((elm) => {
    const map = new Map();
    map.set('seal', elm); // 荷物のシール
    return map;
});
print(luggagesOnbeltConveyor);

これは、言い方を変えると、文字列を要素に持つ配列のmap()メソッドを使って、その文字列要素をsealキーの値として持つMapオブジェクトを作成し、別の配列に追加した、ということです。

同じものはforループでも作成できます(「方法はいくつも考えられる」)。

// 流れてくる荷物にはられたシールの配列
let seals = ['割れ物注意', '危険マーク', 'ニコちゃんマーク'];
let luggagesOnbeltConveyor = [];
for (let i = 0; i < seals.length; i++) {
    const map = new Map();
    map.set('seal', seals[i]);
    luggagesOnbeltConveyor.push(map);
}
箇条書きにする

コードを書くときには、想定される作業を1度箇条書きにすると、頭の中が整理できます。ロボットの身になって、仕分け作業を1つずつ日本語で書くと次のようになります。

  1. 荷物にはられたシールを調べる
  2. それが「割れ物注意」ならかごAに入れる
  3. そうでなく、それが「危険マーク」ならかごBに入れる
  4. そうでなく、それが「ニコちゃんマーク」のときはかごCに入れる
仕分け作業のコード

ベルトコンベアを流れるのは、Mapオブジェクトを入れたluggagesOnbeltConveyor配列です。ロボットはそこに含まれるMapオブジェクトを調べて仕分けします。上記箇条書きを参考に、3つのかごA,B,Cを配列aBox、bBox、cBoxとして、1回の仕分け作業のコードを書くと、次のようになります。

// 仕分けた荷物を入れる3つのかご
let aBox = [],
    bBox = [],
    cBox = [];

// 1個の荷物の仕分け。luggagesOnbeltConveyor配列の要素を特定
// Mapオブジェクト
const theLuggage = luggagesOnbeltConveyor[0];
// Mapオブジェクトのsealキーの値
const theSeal = theLuggage.get('seal');
// シールが'割れ物注意'なら
if (theSeal === '割れ物注意') {
    // aBoxに追加
    aBox.push(theLuggage);
    print('A');
    // シールが'危険マーク'なら
}
else if (theSeal === '危険マーク') {
    // bBoxに追加
    bBox.push(theLuggage);
    print('B');
    // シールが'ニコちゃんマーク'なら
}
else if (theSeal === 'ニコちゃんマーク') {
    // cBoxに追加
    cBox.push(theLuggage);
    print('C');
}
// 出力して確認
print('かごA:');
print(aBox);
print('かごB:');
print(bBox);
print('かごC:');
print(cBox);

このコードでは、luggagesOnbeltConveyor[0]で、sealキーの値が’割れ物注意’であるMapオブジェクトを仕分けしているので、最初のif条件に該当し、MapオブジェクトはaBoxに追加され、Aが出力されます。

汎用化

1つの荷物(Mapオブジェクト)について仕分けるコードが書けたので、次はこれを汎用化(ほかの場合でも使えるように)します。汎用化には、コードをまとめ、パラメータを取る関数が役立ちます。

パラメータをどう定義するかは重要です。たとえば荷物のMapオブジェクトをパラメータにすると、荷物の数だけ関数を呼び出すことになります。荷物は配列にまとめているので、パラメータを配列にすると、関数内で繰り返し処理ができます。この場合、関数の呼び出し回数は1回で済みます。

// 受け取ったMapオブジェクトの配列を調べてかごに分ける
function sorting(luggageArray) {
    for (let i = 0; i < luggageArray.length; i++) {
        // 荷物(Map)を特定
        const theLuggage = luggageArray[i];
        // 荷物のシールを調べる
        const theSeal = theLuggage.get('seal');
        // シールによって仕分ける
        // シールが'割れ物注意'ならaBoxに入れる
        if (theSeal === '割れ物注意') {
            aBox.push(theLuggage);
            print('A');
            // シールが'危険マーク'ならbBoxに入れる
        }
        else if (theSeal === '危険マーク') {
            bBox.push(theLuggage);
            print('B');
            // シールが'ニコちゃんマーク'ならcBoxに入れる
        }
        else if (theSeal === 'ニコちゃんマーク') {
            cBox.push(theLuggage);
            print('C');
        }
    }
    print('かごA:');
    print(aBox);
    print('かごB:');
    print(bBox);
    print('かごC:');
    print(cBox);
}
実行

sorting()関数はMapオブジェクトを要素として持つ配列を処理して荷物の仕分けを行います。したがって、
let seals = [‘割れ物注意’, ‘危険マーク’, ‘ニコちゃんマーク’];
を使用すれば、お題の例の結果が、
let seals = [‘割れ物注意’, ‘ニコちゃんマーク’, ‘危険マーク’, ‘割れ物注意’, ‘危険マーク’];
を使用すれば、問題の結果が得られます。

下図はそれぞれを実行したときの[コンソール]画面です。どちらの場合も、期待する仕分けができていることが分かります。

可視化:アニメーション化

論理はうまくできました。後は画像ファイルを読み込んでの可視化です。可視化するには、荷物を表すMapオブジェクに、新たにimageキーを設定し、それにシールのイメージを割り当てます。描画するときにはMapオブジェクトのget()メソッドでイメージを参照します。

仕分けをすぐ行いたいのであれば、最初luggagesOnbeltConveyor配列のMapオブジェクトを描画し、ボタンのクリックで仕分けを実行し、かごを表すaBox、bBox、cBoxの中身を描画します。仕分けといっても、本物のロボットが実際の荷物を分けるわけではないので、一瞬で終わります。

しかし、このお題では、荷物がベルトコンベアを流れたり、かごに分けられたりするので、あえてアニメーション表現を取り入れることにします。あえてというのは、本シリーズは「論理を考える」ことに主眼を置くものであり、このお題の論理はもう完成していて、アニメーションは目的でないからです。

アニメーション(ものの移動だけでなく、色やサイズの変化も含みます)は本来的に楽しいもので(「1_1 アニメーションとは?」)、ビジネスのプレゼンなどで使用されるように、表現力や説得力を持つものです。ただしそう簡単ではありません。

*アニメーションの基本については「1_2 アニメーションが動いて見える原理」や「1_4 Webで見られるアニメーションの種類」などでも述べています。

アニメーションの例

下図はアニメーションで仕分けを表現したプログラムのスクリーンショットです。(1)が最初の画面で、仕分け表現は画面のクリックでスタートします。荷物の箱がベルトコンベアを流れてきて((2))止まり、仕分けされて下の3つの箱に移動します((3)と(4))。下図をクリックすると、プログラムの実行画面が開かれます。

重要なことなので最初に述べておきます。このプログラムでは、アニメーション表現にP5.JSのlerp()関数を使っています。これは線形補間と呼ばれる計算を行う関数で、いつまでも2点間の距離を割りつづけます。このプログラムでは計算を終わらせていないので、ウィンドウを開いたまま放置しておくと、ブラウザ全体の挙動が不安定になるおそれがあります。

以下が全コードです。

// ベルトコンベアを流れる荷物を入れる配列
let luggagesOnbeltConveyor;
let sealImage; // シールのイメージを入れる配列

let mode = 'idle'; // このアプリが取るモード
let isStart = false; // 画面のマウスプレスでスタート
const speed = 1; // 右に進むベルトコンベアのスピード

// 仕分けた荷物を入れる3つのかご
let aBox = [],
    bBox = [],
    cBox = [];

// キャンバス上部で往復するハンドル
let handle;
let handleX = 200;
let destX = 350;

function preload() {
    // シールのイメージを読み込み、配列sealImageに追加
    const waremonoImage = loadImage('images/waremono.png');
    const kikenImage = loadImage('images/kiken.png');
    const nikovhanImage = loadImage('images/nikochan.png');
    sealImage = [waremonoImage, kikenImage, nikovhanImage];

    handle = loadImage('images/handle.png');
}

function setup() {
    createCanvas(400, 300);
    // 矩形とイメージの描画をセンターモードにする
    // => (x,y)がセンターとして扱えるので位置が計算しやすい
    rectMode(CENTER);
    imageMode(CENTER);
    textSize(30);

    // 流れてくる荷物にはるシールの配列
    let seals = ['割れ物注意', '危険マーク', 'ニコちゃんマーク'];
    seals = ['割れ物注意', 'ニコちゃんマーク', '危険マーク', '割れ物注意', '危険マーク'];
    //seals = ['危険マーク', '割れ物注意', '割れ物注意', 'ニコちゃんマーク', 'ニコちゃんマーク', '危険マーク', '割れ物注意'];

    // 流れてくる荷物にはるシールの配列の各要素から、Mapオブジェクトを作成し、
    // 配列luggagesOnbeltConveyorに追加する
    luggagesOnbeltConveyor = seals.map((elm) => {
        const map = new Map();
        map.set('x', 0); // 荷物のx位置
        map.set('y', 100); // 荷物のy位置
        map.set('tBox', ''); // 荷物の仕分け先のかご、初期値はなし
        map.set('seal', elm); // 荷物のシール
        // シールに応じて、イメージとカラーを設定
        if (elm === '割れ物注意') {
            map.set('image', sealImage[0]);
            map.set('col', color(255, 122, 103));
        }
        else if (elm === '危険マーク') {
            map.set('image', sealImage[1]);
            map.set('col', color(94, 126, 182));
        }
        else if (elm === 'ニコちゃんマーク') {
            map.set('image', sealImage[2]);
            map.set('col', color(9255, 255, 82));
        }
        return map;
    });
    //print(luggagesOnbeltConveyor);    // Mapオブジェクトがtheme.length個入った配列
    // ベルトコンベアに載せる前に仕分けしておく
    sorting(luggagesOnbeltConveyor)
}

// 画面を描画
function draw() {
    background(220);
    // ハンドルが往復するアニメーション
    backAndForthHandle();
    // ベルトコンベアを描画
    fill(color(139, 134, 119));
    rect(200, 120, width, 50);

    // かごAを描画
    fill(color(255, 122, 103));
    rect(70, 270, 100, 40);
    fill(0);
    text('A', 60, 280 - 50);

    // かごBを描画
    fill(color(94, 126, 182));
    rect(200, 270, 100, 40);
    fill(0);
    text('B', 190, 280 - 50);

    // かごCを描画
    fill(color(255, 255, 82));
    rect(330, 270, 100, 40);
    fill(0);
    text('C', 320, 280 - 50);

    // 荷物が移動するアニメーション
    // 配列に入れたMapオブジェクトの数だけ繰り返す
    for (let i = 0; i < luggagesOnbeltConveyor.length; i++) {
        // 対象とするMapオブジェクを特定
        const map = luggagesOnbeltConveyor[i];
        // そのイメージ
        const img = map.get('image');
        // そのカラー
        const col = map.get('col');
        // 仕分け先のかご
        const target = map.get('tBox');
        // 荷物の箱の色
        fill(col);
        // xとy値
        let x = map.get('x');
        let y = map.get('y');
        // 画面がクリックされたら
        if (isStart) {
            // モードがtoRightなら
            if (mode === 'toRight') {
                // 右に移動
                x += speed;
                // ある点で止まる
                if (x > 350) {
                    mode = 'stop';
                }
                // stopモードになったら
            }
            else if (mode === 'stop') {
                // 1秒待って、かごへ移動開始
                const timerID = window.setTimeout(() => {
                    mode = 'down';
                    window.clearTimeout(timerID);
                }, 1000);
                // かごへ向けて移動
            }
            else if (mode === 'down') {
                if (target === 'A') {
                    x = lerp(x, 70 + 70 * i, 0.01);
                    y = lerp(y, 270, 0.01);
                }
                else if (target === 'B') {
                    x = lerp(x, 200 + 70 * i, 0.01);
                    y = lerp(y, 270, 0.01);
                }
                else if (target === 'C') {
                    x = lerp(x, 330 + 70 * i, 0.01);
                    y = lerp(y, 270, 0.01);
                }
            }
        }
        // 荷物を描画
        rect(x - 70 * i, y, 50, 50);
        image(img, x - 70 * i, y);
        // x,y値を設定
        map.set('x', x);
        map.set('y', y);
    }
}

// 受け取ったMapオブジェクトの配列を調べて仕分ける
function sorting(luggageArray) {
    // 仕分け
    for (let i = 0; i < luggageArray.length; i++) {
        // 荷物(Map)を特定
        const theLuggage = luggageArray[i];
        // 荷物のシールを調べる
        const theSeal = theLuggage.get('seal');
        // シールによって仕分ける
        // シールが'割れ物注意'なら、aBoxに入れ、MapのtBoxキーを'A'に設定する
        if (theSeal === '割れ物注意') {
            aBox.push(theLuggage);
            theLuggage.set('tBox', 'A');
            print('A');
            // シールが'危険マーク'なら、bBoxに入れ、MapのtBoxキーを'B'に設定する
        }
        else if (theSeal === '危険マーク') {
            bBox.push(theLuggage);
            theLuggage.set('tBox', 'B');
            print('B');
            // シールが'ニコちゃんマーク'なら、cBoxに入れ、MapのtBoxキーを'C'に設定する
        }
        else if (theSeal === 'ニコちゃんマーク') {
            cBox.push(theLuggage);
            theLuggage.set('tBox', 'C');
            print('C');
        }
    }
}

// 画面のマウスプレスでスタート
function mousePressed() {
    isStart = true;
    mode = 'toRight';
}

// ハンドルのアニメーション
function backAndForthHandle() {
    if (handleX > 340) {
        destX = 50;
    }
    else if (handleX < 60) {
        destX = 350;
    }
    handleX = lerp(handleX, destX, 0.01);
    image(handle, handleX, 20);
}

コメントを残す

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

CAPTCHA