6:メディア イメージとサウンド Creative Coding p5.js


アリソンパリッシュによる

このチュートリアルでは、既存のメディアをスケッチに読み込む方法について見ていきます。

イメージ

p5.jsで画像を読み込みイメージとして表示するのは簡単です。以下のサンプルでは、PNGかJPEG形式の画像を見つけておく必要があります(後で誰かと共有することを考えている場合には、その画像を使用する権利があるか確認してください。Creative Commons Searchは、パーミッシブライセンス付きの画像を探すときの良い場所です)。

p5.jsで画像を使用するには、それをスケッチフォルダにコピーする必要があります。次のサンプルでは、kitty.jpgという名前の画像を使用しています。これはここからダウンロードできます(Image source, used under the CC BY 2.0 license)。

let kitty;

function preload() {
    kitty = loadImage("kitty.jpg");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    image(kitty, mouseX, mouseY);
}

このサンプルには、詳しく見ておく価値のある新しい関数と概念が含まれています。その1つめはpreload()関数です。これは、setup()とdraw()と同様、スケッチ内で定義すると、p5.jsが自動的に呼び出す関数です。preload()内のコードはsetup()やdraw()が呼び出されるより先に実行されるので、通常は、スケッチで使用する画像(やそのほかのメディア)の読み込みに使用されます。その目的は、使用する画像の読み込みが確実に成功してから、スケッチの以降の部分を先に進むようにすることです。

2つめはloadImage()関数です。これは、画像からデータを取得しスケッチ内で使用できるようにする関数で、描画自体は行いません。オブジェクトと呼ばれる特殊な種類のJavaScript値で画像のデータを返します。

kitty = loadImage("kitty.jpg");

ここでは変数kittyに、loadImage()が返すオブジェクト値を代入しています(画像のファイル名と変数名が同じですが、これはたまたまで、必ずしも一致させる必要はありません。変数には好きな名前を付けることができます)。

変数kittyは、draw()関数からアクセスできるように、preload()やsetup()より先に宣言しています。この変数は、宣言時に値を代入していないので、少し変に見えます。これは、JavaScriptに対して、”kittyという名前の変数を後で使うので、そのつもりでいてね”と言うようなものです(詳細は後述します)。

そして最後、イメージを画面に実際に描画するために、draw()内でimage()関数を呼び出しています。image()関数は少なくともパラメータを3つ取ります。1つめは描画したいイメージオブジェクトで、2つめと3つめはイメージを描画するXとY座標です。

しかし実際のimage()関数の機能はもっと多才です。image()関数にはさらに、4つめと5つめのパラメータとして、描画するイメージの幅と高さ(ピクセル単位)を渡すことができます。また、Processingには、rectMode()関数によく似たimageMode()関数があり、imageMode()関数を使用することで、イメージの左上隅ではなく、イメージのセンターを、image()関数の呼び出し時に指定したX/Y座標に描画することができます。以下はその両方を示すスケッチです。

let kitty;

function preload() {
    kitty = loadImage("kitty.jpg");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    imageMode(CENTER);
    image(kitty, mouseX, mouseY,
        200 + sin(frameCount * 0.1) * 100,
        200 + cos(frameCount * 0.1) * 100);
}

preload()とsetup()とdraw()

何で次のコードをプログラムの最初に書かないのか?と不思議に思われているかもしれません。

let kitty = loadImage("kitty.jpg");

変数を宣言するときに同時になぜloadImage()関数を呼び出さないのでしょうか。上記コードは整っていますが残念ながら機能しません。関数の中には(このチュートリアルで取り上げるload…()関数も含まれる)、preload()内でしか呼び出せないものがあるからです。その理由はp5.jsの実装方法に関係していて、必ずしも深く知る必要はないのですが、覚えておいても損はないでしょう。

技術的には、loadImage()をpreload()以外でも呼び出すことはできますが、この先みなさんは、人々が便法のようにloadImage()をsetup()内で使っているのをたびたび目にするでしょう。しかしやってほしくないのはloadImage()をdraw()内で呼び出すことです。次のスケッチはその理由を示す例です。

// このスケッチは、画像を読みこまない方法を示すためのもの
let kitty;

function setup() {
    createCanvas(400, 400);
}

function draw() {
    kitty = loadImage("kitty.jpg");
    background(255);
    image(kitty, mouseX, mouseY);
}

このスケッチを実行すると、コンピュータによって結果が異なる可能性がありますが、わたしの場合には、何も起きていないように見えます。それは、p5.jsが毎秒、何十回もdraw()関数を実行しているからです。これは、draw()の1回の実行に1/100秒ほどしかかけられないということを意味しています。一方、画像の読み込みには、たとえ小さなものでも、はるかに長い時間がかかります。したがって、上記スケッチで起こるのは、次のようなことです。わたしたちはp5.jsに画像を読み込むように言いますが、それが読み込まれないうちに、また画像を読み込むように伝えているのです。結果として、そのイメージが表示されることはありません。

覚えておくべきことは、loadImage()をpreload()以外の場所で呼び出すと、p5.jsは画像が読み込まれる前でもスケッチの実行をつづけるということです。これは、イメージの属性(たとえば幅や高さ、ピクセルデータなど)が、draw()関数が何度か呼び出されないと、プログラムで使用できない、ということです。この理由によりpreload()を使用するのがつねに安全なのです。

複数のイメージ

イメージは1つに限られるわけではありません。実際、イメージを保持する変数はいくつでも作成できます。ただ忘れてはいけないのは、画像の読み込みはpreload()内で行うということです。次のサンプルでは、マウスが押されたときネコを犬に変えています。画像はこれを使用しています。元はここのものです(under a CC BY 2.0 license)。

let kitty;
let doggy;

function preload() {
    kitty = loadImage("kitty.jpg");
    doggy = loadImage("doggy.jpg");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    imageMode(CENTER);
    if (mouseIsPressed) {
        image(doggy, mouseX, mouseY,
            200 + sin(frameCount * 0.1) * 100,
            200 + cos(frameCount * 0.1) * 100);
    }
    else {
        image(kitty, mouseX, mouseY,
            200 + sin(frameCount * 0.1) * 100,
            200 + cos(frameCount * 0.1) * 100);
    }
}

アルファチャンネル

ここまでサンプルで使ってきたのはJPEG形式の画像でした。JPEG形式の画像は圧縮され、小さなファイルサイズでも一般的に高品質ですが、透明度が保持できないという制約があります。次のスケッチはこれを、同じネコの画像を2つ使って示しています。

let kitty;

function preload() {
    kitty = loadImage("kitty.jpg");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    imageMode(CENTER);
    image(kitty, 200 + sin(frameCount * 0.05) * 100,
        200 + cos(frameCount * 0.05) * 100);
    image(kitty, 200 + cos(frameCount * 0.06) * 100,
        200 + sin(frameCount * 0.06) * 100);
}

一方のネコがもう一方の上に描画されるとき、イメージの矩形全体が描画されることが分かります。これが問題ない場合もあるでしょうが、ここで期待している結果ではありません。

背景が透明のイメージの描画方法を理解しやすくするためには、アルファチャンネルの概念を取り入れる必要があります。アルファチャンネルは、ピクセルに保持される、赤/緑/青につづく4つめの情報で、ピクセルをどれだけ透明に見せるかを決める値です。255のアルファ値は完全な不透明で、0のアルファ値は完全な透明です。ここまでカラー制御に使用してきた命令はどれも、実際にはアルファ値を制御する4番めのパラメータをサポートしています。

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    rectMode(CENTER);
    noStroke();
    // 不透明の赤い矩形
    fill(240, 50, 50);
    rect(width / 2, height / 2, 300, 300);
    // 変化するアルファ値の青い矩形
    fill(50, 50, 240, 128 + sin(frameCount * 0.05) * 128);
    rect(mouseX, mouseY, 100, 100);
}

上記サンプルでは、fill()の4番めのパラメータ(sin(frameCount * 0.05) * 128)が塗り色の透明度を制御しています(sin()にframeCountを与えて、このパラメータ値をゆっくり単振動する値に設定しています)。

透明度を持つPNG

画像形式にはまた、透明度の情報を保持するPNG形式があります。PNGでは、そのデータの全ピクセルが赤、青、緑を表す量に加え、透明度の量を表す値(アルファチャンネル)を持っています。インターネット上で目にする多くのPNGは透明度の情報を持っています。次のスケッチでは、Wikimedia Commonsで見つけたフィレオフィッシュのPNGファイルを使っています。

let img;

function preload() {
    img = loadImage("filetofish.png");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    imageMode(CENTER);
    image(img, random(width), random(height),
        random(25, 150), random(25, 150));
}

これはこれで申し分ないのですが、ネコの画像についてはどうでしょう? 残念ながら、背景付きのJPEGを背景が透明なPNGに変換するのは、画像編集プログラムを開いてPNGとして保存すれば済む簡単な作業ではなく、背景を手作業で削除する必要があります。画像の背景の削除は高度な技術であり、Web上にもその作業に関する多くのチュートリアルが公開されています。わたしは本チュートリアルのこのセクションがうまく閉じられるように、勝手ながらネコの画像から背景を削除しました(下手くそですが)。その結果の画像はこれです。

次の例は、透明な背景を持つネコのPNGを使ったスケッチです(イメージの背景が透明であることが分かるように、キャンバスの背景に色を着けています)。

let kitty;

function preload() {
    kitty = loadImage("kitty_transparent.png");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(50, 50, 240);
    imageMode(CENTER);
    image(kitty, 200 + sin(frameCount * 0.05) * 100,
        200 + cos(frameCount * 0.05) * 100);
    image(kitty, 200 + cos(frameCount * 0.06) * 100,
        200 + sin(frameCount * 0.06) * 100);
}

イメージデータを扱う

イメージオブジェクトは、ただ画面に表示するだけでなく、それを扱うことができます。イメージオブジェクトにはそれに関係するデータや関数が付属していて、イメージ内部のデータを読み取ったり操作することができます。

イメージの幅と高さは、イメージのwidthとheight属性にアクセスすることで取得できます。オブジェクトの属性はそのオブジェクトに属する特別な値で、オブジェクトの変数名の後にドット(.)をつづけ、その後に属性の名前をつづけることでアクセスします。loadImage()を使ってイメージオブジェクトをimgという名前の変数に読み取った場合、そのイメージの幅と高さには次のようにしてアクセスします。

img.width
img.height

次のスケッチでは、ネコのイメージのコピーを10個描画しています。そのときには、イメージの幅と高さを使った式を記述して、イメージのサイズを一定の割合で大きくしています。

let kitty;

function preload() {
    kitty = loadImage("kitty_transparent.png");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(50, 50, 240);
    for (var i = 0; i < 10; i++) {
        image(kitty, (i * 25), (i * 25),
            kitty.width * (i + 1) / 10,
            kitty.height * (i + 1) / 10);
    }
}

すべてのイメージオブジェクトはまた、.get()という名前のメソッドを持っています。.get()メソッドはXとY座標を指定する2つのパラメータを取り、その座標の色を返します。呼び出すには次のようにします。

img.get(x, y)
ノート

メソッドは、値のタイプに関連付けられた特殊な種類の関数です。たとえば、loadImage()が返すイメージオブジェクトは、自分自身のコードのスニペットの”ライブラリ”を持っています。これはメソッドと呼ばれ、個々のオブジェクトが持っています。そのコードスニペットは関数とよく似ていますが、関連付けられているオブジェクトが内部に持っているデータを参照して、特別なシンタックスを使って呼び出す必要がある、という点が異なります。メソッドと属性については、オブジェクト指向プログラミングを学ぶ際に、詳しく見ていきます。

.get()メソッドを使用するにはその前に、イメージの.loadPixels()メソッドを呼び出す必要があります。呼び出す最適な場所はsetup()関数の中です。

.loadPixels()メソッドはp5.Imageオブジェクトのメソッドで、イメージのピクセルデータをイメージのpixels属性に読み込みます。

次のスケッチは、.get()メソッドを使って現在のマウス位置のピクセルカラーにアクセスし、その塗り色を使ってrect()命令でイメージを”描く”サンプルです(使用した画像は、ニューホライズンズ探査機が撮影した冥王星の衛星Charonの美しい写真です)。

let charon;

function preload() {
    charon = loadImage("charon.jpg");
}

function setup() {
    createCanvas(400, 400);
    // print(charon.width);	// 400
    // print(charon.height);	// 400
    charon.loadPixels();
    background(0);
    noStroke();
    rectMode(CENTER);
}

function draw() {
    // charon.get(mouseX, mouseY)からは[207, 203, 200, 255]
    // といった配列が返される
    let rgba = charon.get(mouseX, mouseY);
    // 配列の最後の要素255を削除
    rgba.pop();
    // 配列の最後の要素として128を追加 => 半透明のRGBA値になる
    rgba.push(128);
    // 塗り色を半透明のカラーに設定
    fill(rgba);
    // その塗り色で長方形を黒いキャンバスに描画
    rect(mouseX, mouseY, 40, 10);
}

黒い画面上をマウスでなぞると、Charonの写真から取ったカラーで画面が塗られます。

このサンプルの動作は下図のように表すことができます。

左の黒い矩形はcreateCanvas()で作成した400 x 400のキャンバスです。これはbackground(0)によって黒く塗られます。右はloadImage(“charon.jpg”)で作成したイメージオブジェクトのcharonです。charon.pngの幅と高さはともに400なので、このイメージオブジェクトの幅と高さも400です。これはキャンバスと同じサイズです。

draw()関数ではまず、charon.get(mouseX, mouseY)で、charonイメージ上でのマウスのX位置とy位置を変数rgbaに代入しています。もちろんmouseXとmouseYは、p5.jsのキャンバス上での位置ですが、キャンバスとcharonイメージは同じサイズなので、キャンバス上でのmouseXとmouseYをそのままcharonイメージに対応させることができます。

.get()メソッドからは、指定されたXとy位置のピクセルが[R,G,B,A]の配列で返されます(たとえば[207, 203, 200, 255])。次いで配列のpop()を使って、[R,G,B,A]配列からA要素を削除し、その後push(128)で、[R,G,B,128]という配列に変えています(255 -> 128に変えただけ)。これをfill()関数に与えると、塗り色を、.get()メソッドで取得したcharonイメージのカラーを半透明にした色に設定できます。

もう1つの典型的なProcessingサンプルは、イメージのピクセルデータを使ったモザイク模様の作成です。次のスケッチでも同じCharonのイメージを用い、froループをネストしてモザイクを作成しています。

let charon;
const gridx = 25;
const gridy = 25;

function preload() {
    charon = loadImage("charon.jpg");
}

function setup() {
    createCanvas(400, 400);
    noLoop();
    charon.loadPixels();
    background(0);
    rectMode(CENTER);
    noStroke();
}

function draw() {
    for (var i = 0; i < gridx; i++) {
        for (var j = 0; j < gridy; j++) {
            fill(
                charon.get(
                    i * (width / gridx),
                    j * (height / gridy)
                )
            );
            push();
            translate(i * (width / gridx), j * (height / gridy));
            if (((i + j) % 2) == 0) {
                ellipse(0, 0, (width / gridx), (height / gridy));
            }
            else {
                rect(0, 0, (width / gridx), (height / gridy));
            }
            pop();
        }
    }
}
ノート

ここには複雑な算術演算が含まれています。よく分からない場合は、draw()関数内の式が、いくつかの異なるiとjに対して何に評価されるか手で計算し、必要なら実際にそれを描画してみてください。

サウンド

p5.jsスケッチではサウンドの再生も簡単に行えます。このセクションでは、これを実現する基本的な方法を見ていきます。

サウンドの形式

分かりやすくするため、このセクションで使用するオーディオファイルは全部MP3形式にします。しかし、必ずしもすべてのWebブラウザがMP3をサポートしているわけでないことは知っておくべきです。特に、FirefoxのMP3サポートはOSに依存します。p5.jsで公開されているこのサンプルスケッチは、クロスプラットフォームの互換性を最大にするための、オーディオファイルを異なる形式で提供する方法を示しています。

参考:「audio要素を記述して音楽ファイルを読み込ませる方法


注意

このチュートリアルの原文はp5.js Webエディタの使用を前提としていると思われます。原文で紹介されているコードは、ユーザー登録を行いログインしたp5.js Webエディタに、必要な画像やサウンドファイルをアップロードした状態で動作します。そうでなく、通常のWebサーバーで動作させるには、サウンド用のライブラリのp5.sound.jsを読み込む必要があります。またChromeブラウザでは、サウンドの自動再生に関する制約「Chrome autoplay policy」あるので、それを回避する手段が必要になります。以降のサンプルでは、p5.js Webエディタ上ではなく、通常のWenサーバー上で実行できる方法を示しています。


サウンドの読み込みと再生

p5.jsスケッチでサウンドファイルを使用する基本的なワークフローはイメージの使用とよく似ています。使用したいサウンドを見つけ、それをスケッチフォルダにコピーします。そしてスケッチ内にそのサウンドファイルのデータを読み込むという流れです。

通常のWebサーバー上に配置したp5.jsスケッチでサウンドを再生するには、p5.jsのサウンド用のライブラリ、p5.soundを読み込む必要があります。

次のsketch.jsでは、ブラウザ画面のマウスプレスでサウンドファイルを読み込み、再生するようにしています。

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.sound.min.js"></script>
let kitty;
let meow;
function preload() {
  kitty = loadImage("kitty.jpg");
}
function setup() {
  createCanvas(400, 400);
}
function draw() {
  background(255);
  imageMode(CENTER);
  image(kitty, width / 2, height / 2);
}

// マウスが押されたら => ユーザーの積極的なジェスチャー
function mousePressed() {
  // サウンドファイルを読み込む。読み込みが完了したらコールバック関数を呼び出す。
  meow = loadSound("meow.mp3", successCallback);
}

// サウンドを再生する
function successCallback() {
  meow.play();
}

参考までに

Chromeブラウザでサウンドを再生しようとすると、コンソールに「The AudioContext was not allowed to start. It must be resumed (or created) after a user gesture on the page」という警告が表示されます。これは、「AudioContextは開始できなかった。ユーザーがページ上で何らかの積極的なジェスチャ(再生ボタンのクリックなど)を行った後で、再開(または作成)しなければいけない」という意味です。

Chrome autoplay policyには、「ユーザーに何かが起きることを知らせるように、、ユーザーのインタラクションを待ってからオーディオの再生を開始するようにします。[再生]ボタンや[オン/オフ]スイッチを検討してください。またアプリケーションの流れに応じて、[消音解除]ボタンを追加することもできます」といった意味の事柄が英語で書かれています。

つまりサウンドを再生するときには、自動再生ではなく、ボタンのクリックなど、ユーザーが自分からアクションを起こして再生を開始する仕組みが必要になります。上記サンプルではこれを、画面のクリックで再生を開始するようにしています。
以降は再び、原文の日本語訳に戻ります。


イベント関数

別の方法は、ここまで取り上げて来なかったp5.jsの機能、イベント関数の使用です。イベント関数は、プログラムで定義しておくと、外部からの何らかのイベントが発生したとき、p5.jsによって自動的に呼び出される関数です。その全リストはここで見ることができます。最も理解しやすいのはmousePressed()です。スケッチでmousePressedという名前の関数を定義すると、p5.jsは、スケッチを使用する誰かがマウスボタンを押すとつねに、この関数を呼び出します。mousePressed()はマウスの1回の押し下げで1回だけ呼び出されるので、今の目的に適っています。

便利なもう1つのイベント関数はkeyTyped()です。この関数はキーが押し下げられたとき呼び出されます。次のサンプルはマウスの押し下げではなく、キーの押し下げによって、サウンドが再生されます(キープレスを発効させるには、スケッチ内を一度クリックしてフォーカスをスケッチに移す必要があるかもしれません)。

let kitty;
let meow;

function preload() {
    kitty = loadImage("kitty.jpg");
}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    imageMode(CENTER);
    image(kitty, width / 2, height / 2);
}

// キーが押されたら => ユーザーの積極的なジェスチャー
function keyTyped() {
    // サウンドファイルを読み込む。読み込みが完了したらコールバック関数を呼び出す。
    meow = loadSound("meow.mp3", successCallback);
}

// サウンドを再生する
function successCallback() {
    meow.play();
}

簡単なドラムキット

keyTyped()関数内部では、keyという名前の特別な変数が、現在押し下げられているキーを含んでいます。特定のキーが押し下げられているかどうかを調べるには、次の式を使用します。

key == "X"

Xは調べたいキーです(引用符を忘れないように)。

次のスケッチでは、この機能を利用してキーボード制御による簡単なドラムキットを作成しています。”A”キーを押すとキックドラム(バスドラ)、”L”キーを押すとクローズドハイハット、”S”キーを押すとスネアドラム、そのほかのキーを押すとネコの声が鳴ります。

let kitty;
let meow;
let kick;
let snare;
let hihat;
let extraScale = 0;

// 画像やサウンドファイルを読み込んでからsetup()に移る
function preload() {
    kitty = loadImage("kitty.jpg");
    meow = loadSound("meow.mp3");
    kick = loadSound("kick.mp3");
    snare = loadSound("snare.mp3");
    hihat = loadSound("hihat.mp3");

}

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(255);
    imageMode(CENTER);
    translate(width / 2, height / 2);
    scale(1 + extraScale);
    image(kitty, 0, 0);
    if (extraScale > 0) {
        extraScale -= 0.05;
    }
}

function keyTyped() {
    extraScale = 1;
    if (key == "a") {
        kick.play();
    }
    else if (key == "l") {
        hihat.play();
    }
    else if (key == "s") {
        snare.play();
    }
    else {
        meow.play();
    }
}

コメントを残す

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

CAPTCHA