本稿は、p5.jsに追加して組み込むことで、キャンバス以外のHTML要素へのアクセスや操作を可能にするp5.dom.jsについて述べた「Beyond the canvas」の訳文です。
createCanvas()関数は、グラフィックが描画できる特別な要素のHTML5キャンバスを作成します。しかし、p5.domアドオンライブラリを使用すると、p5.jsはまた、グラフィックを描くキャンバスの外にあるHTML要素の作成やインタラクティブな操作に使用することができます。
目次
p5.domライブラリを組み込む
p5.domライブラリを使用するにはまず。HTMLにp5.dom.jsファイルを読み込む必要があります。ファイルをダウンロードしてHTMLヘッダに次の行を追加するか、
<script type='text/javascript' src='relative/path/to/your/p5.dom.js'>
HTMLヘッダに次のリンク先を追加します。
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.dom.min.js"></script>
ポインタの保持とメソッドの呼び出し
createCanvas(w, h)を呼び出すとき、キャンバスを指定した幅と高さで作成できますが、そのキャンバスを変数に保持することもできます。これはポインタや参照と呼ばれます。このポインタを使用すると、要素自身が持つ、たとえば位置やid、クラスなどを設定するメソッドを呼び出すことができます。メソッドの全リストはここに書かれています。ただしすべてのメソッドがすべての要素で動作し有意な結果をもたらすわけではないので、少し頭を使う必要があります。たとえばスライダのvalue()メソッドはスライダの値を返すか設定しますが、キャンバスから呼び出しても効果はありません。
function setup() {
// createCanvas()を呼び出しているが、その結果を変数として保持している。
// これによって、位置の設定など、要素のメソッドが呼び出せるようになる。
let canvas = createCanvas(240, 120);
// ここでは位置とクラスを設定するメソッドを呼び出している。
// このコードが生成したHTMLを確認するには、インスペクタを使う。
canvas.position(100, 50);
canvas.class("lemon"); // 枠を黄色にするクラス
}
function draw() {
// これらの命令は通常通り、キャンバスに適用される。
background(220, 180, 200); // ピンク色
ellipse(width / 2, height / 2, 100, 100);
ellipse(width / 4, height / 2, 50, 50);
}
要素レベルの操作(要素自身が持つメソッドの呼び出し)と、rect()やfill()といったキャンバスに直接描画する関数の呼び出しには、1つ重要な違いがあります。ループの実行中キャンバスに描画しているときには、シーン全部を毎フレーム再描画する必要があります。たとえば、矩形を画面に表示しつづけたい場合には、draw()内にrect()を記述し、矩形を1秒間に何度も再描画します。
しかし、要素レベルで操作するときには、要素は、要素自身のメソッドを呼び出すことで、いつでも変更できる静的な状態を保持しています。上記の例で言うと、キャンバスはウィンドウの左上隅を基準に(300, 50)に配置されています(canvas.position(100, 50))。このメソッドはsetup()内で1度だけ呼び出されただけですが、その後はずっとその位置のまま変わりません。毎フレーム設定し直す必要はありません。
parent()の使用
create関数(createCanvas()やcreateDiv()、createImg()など)のどれかを使って新しい要素を追加するとき、その要素は左上隅(0, 0)に現れずに、ページの最後(=既存の最後の要素の後)に付加されることに気づかれるかもしれません。要素はまた、ページに設定されている既存のCSSスタイルの影響を受けます。このときの指針になる考えは、p5では、ページをできるだけ台無しにしないように、要素をページの流れにしたがわせるようにしている、ということです。変更を加えたい場合には、p5のメソッドやCSSスタイルを使用します。
<div class="blue">既存のDIV</div>
function setup() {
noCanvas();
const newDiv = createDiv();
newDiv.class('red');
}
ページの最後に直接付加するのではなく、位置を指定したい場合には、parent()メソッドを使用します。HTMLファイルの<body>内の、キャンバスを挿入したい場所に、IDを付けたコンテナを作成します。
<div id='myContainer'></div>
コードでは、作成した要素のポインタを保持する変数を作成し、その変数からparent()を呼び出します。
function setup() {
let myCanvas = createCanvas(600, 400);
myCanvas.parent('myContainer');
}
position()の使用
要素がどのコンテナに含まれるかは問題ではなく、ページ上の位置が設定したい場合には、position(x, y)メソッドを使用します。このメソッドの呼び出しは、要素のデフォルトの位置決めを上書きし(CSSスタイルのposition:absoluteを適用することで)、ウィンドウの左上隅(0, 0)を基準とする位置を指定することができます。次の例は、要素を作成し、(100, 100)に配置します。
function setup() {
const myCanvas = createCanvas(600, 400);
myCanvas.position(100, 100);
}
上記の例ではcreateCanvas()関数を使っていますが、ほかのcreate関数でも同様に動作します。
そのほかのHTML要素の作成
create関数には、createCanvas(w, h)のほか、createDiv()やcreateP()、createA()など多くの関数があります(全リストは「p5.dom library」を参照)。次のサンプルでは、テキスト付のdivを作成し、キャンバスとdivの位置をそれぞれ設定しています。
function setup() {
let canvas = createCanvas(240, 120);
canvas.position(200, 20);
let txt = createDiv('これはHTMLの文字列');
txt.position(20, 20);
}
function draw() {
background(220, 180, 200);
ellipse(width / 2, height / 2, 100, 100);
ellipse(width / 4, height / 2, 50, 50);
}
ここでは要素内にただテキスト文字列を置いているだけですが、要素は実はどのHTMLでも中に含むことができます。次のコードを試してみてください。リンク部分のテキストはクリックできます。
let txt = createDiv("ここはテキストで<a href='https://i.imgur.com/WXaUlrK.gif'>ここはHTMLリンク</a>!");
これらのメソッドはp5.Elementオブジェクトを作成します。p5.Elementは、HTML要素の多くのプロパティへのアクセスを簡単にする、要素を包むラッパーですが、p5.Elementオブジェクトの基底にあるHTML要素にアクセスしたいときには、eltプロパティが使用できます。要素の全プロパティはここで読むことができます。
HTMLイメージの作成
イメージそのものが欲しい場合には、createImg(src)を使用します(注:作成されるのはp5.Elementオブジェクトですが、ここではこれをHTMLイメージと呼んでいます)。HTMLイメージは、image()でキャンバスに描画したイメージとは違います。loadImage()を使う必要も、毎フレーム描画する必要もなく、作成したら、それを削除するまで、ページに存在しつづけます。またこのイメージはそれ自体が1つの要素なので、その上をマウスでドラッグすると、ハイライト表示できます(下図参照)。これは、その要素に直接マウスイベントのハンドラを割り当てることができる、ということを意味していますが、これについては後で見ていきます。次の例では、イメージとキャンバスを作成し、それぞれの位置とサイズを設定しています。
function setup() {
const img = createImg("http://th07.deviantart.net/fs70/PRE/i/2011/260/3/5/dash_hooray_by_rainbowcrab-d49xk0d.png");
const canvas = createCanvas(400, 400);
img.position(190, 50);
img.size(200, 200);
canvas.position(300, 50);
}
function draw() {
noStroke();
background(220, 180, 200);
fill(180, 200, 40);
strokeWeight(6);
stroke(180, 100, 240);
for (let i = 0; i < width; i += 15) {
line(i, 0, i, height);
}
}
HTMLメディア要素の作成
create関数にはまた、ビデオやオーディオのメディアを扱う要素を追加する関数があります(createVideo()、createAudio()、createCapture())。これらの関数は、通常のp5.Elementは持っていない、追加的なメソッドを持つp5.MediaElementオブジェクトを作成します。HTML5メディアの機能の多くにはp5.dom APIを通してアクセスできますが、それでもネイティブな機能性は多くあります。基底を成す要素にはeltプロパティからアクセスでき、ネイティブ機能全部が使用できることを覚えておいてください。
次のサンプルは、ボタンのクリックでビデオの再生と一時停止を切り替える方法を示しています。実際の動作はここで確認できます。
let playing = false;
let fingers;
let button;
function setup() {
fingers = createVideo('assets/fingers.mp4');
button = createButton('play');
button.html('再生');
// ボタンにリスナーを追加
button.mousePressed(toggleVid);
}
// 現在の状態に応じて、ビデオを再生または一時停止する
function toggleVid() {
if (playing) {
fingers.pause();
button.html('再生');
}
else {
fingers.loop();
button.html('一時停止');
}
playing = !playing;
}
とは言え、すべてのブラウザがこういったメディア機能をサポートしているとは限りません。ブラウザごとのサポート状況はcaniuse.comで調べることができます。
createElement()の使用
p5.dom APIがサポートするのは全HTML要素の一部です。固有のcreate関数のない要素を追加したい場合には、汎用的なcreateElement()関数を使用します。最初のパラメータはタグ名で、2つめはオプションの、要素内に置く内容です。次の例では、<h1>要素を作成しています。
function setup() {
let h1 = createElement('h1', 'これは見出しテキスト');
let canvas = createCanvas(240, 120);
noStroke();
background(220, 180, 200);
fill(180, 200, 40);
strokeWeight(6);
stroke(180, 100, 240);
for (let i = 0; i < width; i += 15) {
line(i, 0, i, height);
}
}
要素固有のリスナー
要素はどれでも、マウスが重ねられたりそこからはずれたりしたときに呼び出されるmouseOver()やmouseOut()メソッドを自分自身が持っています。そういった出来事(イベント)の1つが発生したときに固有のアクションを実行するには、これらのメソッドに引数として関数か関数の名前を渡します。
次のサンプルでは、マウスがキャンバスに重なったときユニコーンのイメージを隠す振る舞いと、キャンバスから出たときに表示する振る舞いを割り当てています。
let img;
function setup() {
let canvas = createCanvas(240, 120);
img = createImg("http://th07.deviantart.net/fs70/PRE/i/2011/260/3/5/dash_hooray_by_rainbowcrab-d49xk0d.png");
img.position(50, 50);
img.size(200, 200);
canvas.position(300, 50);
// マウスイベントリスナーをキャンバスに関して割り当てる
canvas.mouseOver(uniHide);
canvas.mouseOut(uniShow);
}
function draw() {
// キャンバスで以下を描画
noStroke();
background(220, 180, 200);
fill(180, 200, 40);
strokeWeight(6);
stroke(180, 100, 240);
for (let i = 0; i < width; i += 15) {
line(i, 0, i, height);
}
}
// ユニコーンイメージを隠す関数と表示する関数。
// これらはキャンバスに関するマウスイベントをきっかけに呼び出される
function uniHide() {
img.hide();
}
function uniShow() {
img.show();
}
上記サンプルでは、関数名のuniHideとuniShowを渡していますが、同じ結果は、名前なしで関数を丸ごと渡すことでも行えます。この名前のない関数は”無名関数”と呼ばれます。
canvas.mouseOver(function() {
img.hide();
})
要素固有のリスナーとグローバルなリスナー
要素はまた、関数とmousePressedイベントを要素ごとのレベルで結びつけることのできるmousePressed()メソッドを持っています。これは、どこでもいつでもマウスがクリックされたときに引き起こされるグローバルなmousePressed()関数とは違います。要素は固有のハンドラを持ち、要素のメソッドはその要素が直接クリックされたときだけ呼び出されます。
let img;
function setup() {
let canvas = createCanvas(240, 120);
img = createImg("http://th07.deviantart.net/fs70/PRE/i/2011/260/3/5/dash_hooray_by_rainbowcrab-d49xk0d.png");
img.position(190, 50);
img.size(200, 200);
// マウスイベントリスナーをimgに関して割り当てる
img.mousePressed(uniHide);
canvas.position(300, 50);
}
function draw() {
noStroke();
background(220, 180, 200);
fill(180, 200, 40);
strokeWeight(6);
stroke(180, 100, 240);
for (let i = 0; i < width; i += 15) {
line(i, 0, i, height);
}
}
// ユニコーンイメージを隠す関数と表示する関数。
// igmに関するマウスイベントをきっかけに呼び出される
function uniHide() {
img.hide();
}
function uniShow() {
img.show();
}
// グローバルなkeyPressed時の振る舞いを定義。
// これはグローバルなリスナーで、キーの押し下げ時に自動的に発火する。
function keyPressed() {
uniShow();
}
次も、要素固有のリスナーとグローバルなリスナーの違いを示すサンプルです。キャンバスも含むページのどこかをクリックすると背景が明るくなります。キャンバスを直接クリックすると円のサイズが変わります。
let gray = 0;
let diameter = 5;
function setup() {
let canvas = createCanvas(200, 200);
// キャンバスをマウスプレスでincDiameter()を呼び出す
canvas.mousePressed(incDiameter);
}
function draw() {
background(gray);
ellipse(width / 2, height / 2, diameter, diameter);
}
// グローバルなマウスプレスのリスナー
function mousePressed() {
gray = gray + 10;
}
// 円を大きくする
function incDiameter() {
diameter = diameter + 5;
}
要素を探す
要素にクラスやidを割り当てておくとCSSスタイルシートを使った要素のスタイル処理に役立ちますが、ページの要素を探すときにも便利です。
class()メソッドは要素に名前付きのクラスを割り当てるときに使用します。どんなクラス名をつけるかはつける人次第です。ドキュメント内の複数の要素は同じクラス値を持つことができます。id()メソッドは要素にidを付けるときに使用します。どんなidをつけるかはつける人次第です。ただしid名はドキュメントで一意でなくてはならず、同じidを持つ要素がほかにあってはいけません。
ページに存在している要素を探すときに使用できる関数は2つあります。select(#id)は、指定されたidを持つ要素を返し、見つからないときはnullを返します。selectAll(.className)は指定されたクラス名を持つ全要素の配列を返し、見つからないときは空の配列を返します。
function setup() {
let myDiv0 = createDiv('これはdiv 0');
let myDiv1 = createDiv('これはdiv 1');
let myDiv2 = createDiv('これはdiv 2');
// 各要素のメソッドを呼び出し、位置とクラスを設定する。
// myDiv0とmyDiv1のクラスはdonkeyで、myDiv2のクラスはyogurtにする
myDiv0.position(50, 50);
myDiv0.class('donkey');
myDiv1.position(300, 50);
myDiv1.class('donkey');
myDiv2.position(550, 50);
myDiv2.class('yogurt');
}
// キーの押し下げで、donkeyクラスの要素を全部隠す
function keyPressed() {
// selectAll()はクラスにdonkeyが設定された全部の要素の配列を返す
// 見つからない場合は、空の配列[]を返す
let donkeys = selectAll('.donkey');
// 配列を走査して、全要素を隠す
for (let i = 0; i < donkeys.length; i++) {
donkeys[i].hide();
}
}
”要素を返す”と言っているときの”要素”は、実際にはp5.Elementオブジェクトです。基底を成すHTML要素にアクセスしたい場合には、eltプロパティを使用します。
スタイルの設定
キャンバスは描画することでその外見を変えますが、ほかのHTML要素はCSS(カスケーディングスタイルシート)と呼ばれるものを使ってスタイルを設定します。CSSは、画面にレンダリングされるHTML要素表現の記述に使用される言語で、背景色やフォントサイズ、フォントカラー、パディングといった事柄が設定できます。
p5.jsでは、要素のstyle()メソッドを使ってCSSプロパティが設定できます。設定できるプロパティの全リストについては「MDN CSSリファレンス」を参照してください。
function setup() {
let text = createP("これはCSSを適用したHTML文字列");
let canvas = createCanvas(240, 120);
text.position(50, 50);
text.style("font-family", "monospace");
text.style("background-color", "#FF0000");
text.style("color", "#FFFFFF");
text.style("font-size", "18pt");
text.style("padding", "10px");
canvas.position(150, 150);
}
function draw() {
background(220, 180, 200);
ellipse(width / 2, height / 2, 100, 100);
ellipse(width / 4, height / 2, 50, 50);
}
また別の方法として、設定するCSSプロパティをまとめ、セミコロンで区切った1つの文字列として入力することもできます。
text.style("font-family:monospace; background-color:#FF0000; color:#FFFFFF; font-size:18pt; adding:10px;");
CSSスタイルシートの使用
スケッチに組み込むもう1つの方法に、スタイルシートの自作があります。そのためには、style.cssといった名前のファイルを作成し、HTMLファイルのヘッダでこのファイルへのリンクを追加します。
<link rel="stylesheet" type="text/css" href="style.css">
CSSファイル(style.css)には、”ルール”、つまり要素の外見をどのように提示するかを決めるコードを記述します。ルールは、HTMLタグ(pやdiv、spanなど)や要素クラス(頭に.が付く)、要素id(頭に#が付く)にもとづいて定義します。次の例は前の例と同じですが、style()メソッドの代わりにCSSスタイルシートを使っています。この場合、プロパティ名や値を引用符で囲んではいけません。
HTML
<link rel="stylesheet" type="text/css" href="style.css">
CSS(style.css)
.lemon {
font-family: monospace;
background-color: #FF0000;
color: #FFFFFF;
font-size: 18pt;
padding: 10px;
}
sketch.js
function setup() {
let text = createP("これはCSSを適用したHTML文字列");
let canvas = createCanvas(240, 120);
text.position(50, 50);
// CSSスタイルシートに記述しているクラスを適用
text.class("lemon");
canvas.position(150, 150);
}
function draw() {
background(220, 180, 200);
ellipse(width / 2, height / 2, 100, 100);
ellipse(width / 4, height / 2, 50, 50);
}
要素の削除
要素は、その要素のremove()メソッドを呼び出すことで削除できます。これにより、その要素に結び付けられているすべてのイベントハンドラも削除され、要素もページから削除されます。
let myDiv = createDiv('これはテキスト');
myDiv.remove();
removeElements()関数は、createCanvas()やcreateGraphics()関数で作成されたキャンバスやグラフィックをのぞく、p5によって作成された全要素を削除します。これらの要素からはすべてのイベントハンドラも削除され、要素もページから削除されます。
function setup() {
createCanvas(100, 100);
background(204);
createDiv('これはテキスト');
createP('これは段落');
}
// ページのマウスプレスで、
function mousePressed() {
// divとpは削除され、キャンバスは削除されない
removeElements();
}
また、remove()関数の呼び出しによってp5スケッチ全体が削除できます。remove()関数は、p5.jsやp5.dom.jsによって作成されたキャンバスや全要素を削除します。そしてdraw()関数のループを停止し、イベントリスナーを削除して、windowグローバルスコープからプロパティやメソッドをアンバインド(結び付き解除)します。1つの変数p5は、新しいスケッチの作成に使用できるように、残ります。p5も削除したい場合には、p5 = nullを設定します。
function setup() {
createCanvas(200, 200);
}
function draw() {
ellipse(width / 2, height / 2, 0, 0);
}
function mousePressed() {
remove(); // マウスプレスでスケッチ全体を削除
}
HTML5 ビデオ
p5.jsでは、createVideo()関数によって、オーディオやビデオの簡単な再生に使用できるHTML5要素がDOM内に作成できます。
let vid;
function setup() {
vid = createVideo(['assets/fingers.mp4', 'assets/fingers.ogv', 'assets/fingers.webm'], vidLoad);
}
// ビデオがロードされたら呼び出される
function vidLoad() {
vid.play();
}
ビデオ要素は、デフォルトでは表示されますが、hide()メソッドで隠すことができます。ビデオ要素は、指定されていれば(parent()などで)コンテナノードに付加され、指定されていない場合は<body>に付加されます。1つめのパラメータには、ビデオファイルへのパスの文字列か、同じビデオの異なる形式へのパスの文字列の配列が指定できます。配列で渡す方法は、ブラウザによってサポートするビデオ形式が異なるので、できるだけ多くの環境で再生できるようにしたい場合に役立ちます。
ビデオの上に画像を描画したいときには、まずビデオを作成し、その後キャンバスを作成して画像を描画します。
let vid;
let img;
function preload() {
img = loadImage('death.png');
}
function setup() {
// ビデオを作成してから、キャンバスを作成する
// ビデオのアスペクト比は16:9
vid = createVideo('movie.mp4', vidLoad);
const canvas = createCanvas(160, 90);
// キャンバスの位置を調整
canvas.position(10, 10);
// キャンバスに画像を描画
image(img, 0, 0, 30, 30);
}
// ビデオのロードが終わったら
function vidLoad() {
// ビデオのサイズと位置を調整
vid.size(160, 90);
vid.position(10, 10)
vid.pause();
}
function mousePressed() {
print(vid.elt.readyState)
// 4 = HAVE_ENOUGH_DATA - 再生開始に十分なデータがある
if (vid.elt.readyState === 4) {
vid.play();
}
}
また別の方法として、ビデオは表示せず、ビデオのピクセルをキャンバスにコピーする方法もあります。
let vid;
let img;
function preload() {
img = loadImage('death.png');
}
function setup() {
createCanvas(160, 90);
background(0);
vid = createVideo('movie.mp4', vidLoad);
}
function vidLoad() {
vid.size(160, 90);
vid.hide();
vid.pause();
}
function draw() {
if (vid) {
image(vid, 0, 0, 160, 90); // ビデオをキャンバスに描画
image(img, 0, 0, 30, 30);
}
}
function mousePressed() {
print(vid.elt.readyState)
// 4 = HAVE_ENOUGH_DATA
if (vid.elt.readyState === 4) {
vid.play();
}
}