アリソンパリッシュによる
このチュートリアルでは、p5.jsを使ってスケッチにテキストを表示する方法と、テキストのさまざまなサイズやフォントの形状にもとづいて、スケッチに異なる振る舞いを与える方法を見ていきます。また、テキストを保持し操作するJavaScriptのデータ型である文字列値も詳しく見ていきます。そして最後に、外部ファイルからテキストを読み込む方法を紹介します。
目次
テキスト:基本
スケッチの画面へのテキストの描画は、text()関数で行います。text()関数はパラメータを少なくとも3つ、つまり、表示する文字の文字列、テキストを表示したいXとY座標を取ります。
テキストのカラーは、fill()関数で制御します。またstroke()関数を使用すると、テキストのアウトライン(枠線)のカラーが設定できます。
フォントのデフォルトサイズは12ピクセルです。フォントのサイズはtextSize()関数で制御できます。次の例ではここまで述べた機能をすべて使っています(マウスボタンを押しつづけると、テキストの枠線が見えるようになります)。
スケッチに表示されるテキストを変更するには、引用符(“)内のテキストを変更します。
テキストのアライメント
デフォルトでは、text()関数の2つめと3つめのパラメータとして指定する座標が、テキストのベースラインを開始する位置、つまり文字列の最初の文字の左下隅を決めます。この振る舞いはtextAlign()関数で変えることができます。
textAlign()関数は、パラメータを1つ指定して呼び出されると、テキストの水平方向のアライメントを制御します。これは、text()関数で指定された座標を基準に、テキストを水平方向のどこに描画するか、ということです。次のスケッチでは、水平方向のアライメントについて指定できる3つの値を例示しています(緑の円はtext()関数の呼び出し時に指定した座標です)。
textAlign()関数は、2つめのパラメータを渡されると、テキストの垂直方向のアライメントを制御します。これは、指定された座標を基準に、テキストを垂直方向のどこに描画するか、ということです。すでに述べたように、デフォルトではテキストのベースラインが使用されます(定数BASELINE)。そのほかの指定できる値には、TOP(テキストの左上隅)、BOTTOM(ディセンダを含むテキストの左下隅)、CENTER(テキストのセンター)があります。
参考:「4_6:応答:キー p5.js JavaScript」
BASELINEとBOTTOMの違いは少しですが、重要です。
ボックス内のテキスト
デフォルトでは、text()関数の呼び出しで指定したテキストは、長い1行で描画されます。テキストを折り返したい(ワードラップ)場合には、text()関数にさらに2つのパラメータを追加します。これによりp5.jsは、4つの座標で指定された矩形の中にテキストを描画するようになります。たとえば次の例では、テキストを、現在のマウス位置で 200 x 200 のボックス内に描画します(マウスボタンを押しつづけるとボックスの視覚表現が表示されます)。
自動的に折り返すテキスト(や改行文字を含んでいるテキスト)を表示するときには、textLeading()関数で、テキストの行間を変えることができます。textLeading()関数は、行間を表示するためのピクセル数を設定します。
textLeading()関数に0を指定すると、行間がなくなります(つまり、行同士が重なって描画されます)。
カスタムフォント
p5.jsのデフォルトフォントははなはだ退屈です。loadFont()関数を使うと、別のフォントが指定できます。
textFont()をこのように呼び出すときには、指定した名前のフォントが、スケッチを使用している人のコンピュータに存在している必要があります。いくつかのよく使用されるフォントならおそらく問題になりませんが、意図した通りに確実にユーザーに見せたい場合には、使用するフォントをスケッチに含める必要があります。
そのためには、そのフォントの.ttfか.otfファイルをアップロードし、preload()内でloadFont()関数を使ってフォントファイルを読み込みます。次のスケッチでは、League of Moveable TypeからダウンロードしたKnewaveフォントを使用しています。
let myFont;
function preload() {
myFont = loadFont("knewave.otf");
}
function setup() {
createCanvas(400, 400);
}
function draw() {
background(50);
strokeWeight(2);
fill(255);
textFont(myFont);
textSize(12 + (mouseX / width) * 72);
text("Attention, please.", 50, 200);
}
ご覧のように、textFont()関数はパラメータとして、文字列かフォント値(loadFont()関数が返した値)を取ります。
フォントのライセンスは複雑で、自分のコンピュータに入っているからといって、それをあらゆる目的に使用できる法的な権利があるわけではありません。自由に使用できるフォントについては、上記のLeague of Moveable TypeやOpen Font Libraryで探してみてください
文字列(ストリング)
問題はないのですが、引用符は本当のところ何なのでしょう? 何を意味し、どこから来るのでしょう? 注意すべきなのでしょうか? わたしはこうした質問に答えることができます。
JavaScriptには、文字列(ストリング)と呼ばれる特別なデータ型があります。文字列は配列のようなものですが、一連の値を保持するのではなく、一連の文字(連なった文字、つまり”文字列”)を保持します。文字列型の値は、文字列の小さな部分(部分文字列、サブストリング)を取得したり、複数の小さな文字列から大きな文字列を作成したり、また文字列をトークン(単語の1つ1つ)の配列に分解したり、与えられたサブストリングのインデックスを探したりできる特殊なメソッドを持っています(ほかにもたくさんあります)。
文字列値を作成するには、一重引用符(‘)か二重引用符(“)で一連の文字を囲みます。その値は変数に代入することができます(引用符内に何も入れない場合には、空の文字列(”)を作ったことになります。空の文字列は、プログラムの実行中に文字列を少しずつ長くしていくような場合に役立ちます)。
const someText = "this is a test";
const someMoreText = 'This is a test'; // 一重引用符もOK
const oneChar = "a"; // 一文字の文字列
const nothingYet = ""; // 空の文字列
文字列の長さ(つまり文字列に含まれる文字の数)は、配列と同じで、.lengthプロパティで取得できます。
const someText = "this is a test";
console.log(someText.length); // 出力 14
部分文字列(サブストリング)
前述したように、文字列値は役に立つさまざまなメソッドを持っています。まずは.substring()メソッドから見ていきましょう。このメソッドにパラメータを2つ渡して呼び出すと、最初のパラメータで与えられたインデックスから始まり、2つめのパラメータで与えられたインデックスで終わる(しかし含まない)文字列の部分を返します(配列のインデックスと同様に、文字列のインデックスもゼロベースです)。パラメータが1つしか与えられない場合には、与えられたインデックスで始まり、文字列の最後までの文字列の部分を返します。空のp5.jsスケッチに次のコードを入力して試してみてください。
const someText = "it was the best of times";
// インデックス0からインデックス1の前まで
console.log(someText.substring(0, 1)); // 出力 "i"
// インデックス7からインデックス15の前まで
console.log(someText.substring(7, 15)); // 出力 "the best"
// インデックス19から最後まで
console.log(someText.substring(19)); // 出力 "times"
// インデックス18から最後まで
console.log(someText.substring(18)); // 出力 " times"
メモ
“私はbekkyです”で同様のテストをしてみます。
const someText = "私は bekkyです";
console.log(someText.length); // 出力 10
// インデックス0からインデックス1の前まで => 0
console.log(someText.substring(0, 1)); // 出力 "私"
// インデックス0からインデックス2の前まで => 0 -> 1
console.log(someText.substring(0, 2)); // 出力 "私は"
// インデックス3からインデックス8の前まで => 3 -> 7
console.log(someText.substring(3, 8)); // 出力 "bekky"
// インデックス3から最後まで => 3 -> 9
console.log(someText.substring(3)); // 出力 "bekkyです"
では、.substring()メソッドを使って、文字列を調べ、一度に一文字ずつ画面に表示するスケッチを記述しましょう。
次のスケッチは、マウス位置に応じて、表示する文字列の部分を変更します。
演習
文字列からのすべての文字を画面上のランダムな位置に描画するスケッチを作成します(forループを使って)。
文字列の追加と結合
異なるソースからの文字列が2つあり、それらを1つの文字列に結合したいとします。JavaScriptでこれを行う最も簡単な方法は、+ 演算子を使った方法です。右辺と左辺がともに文字列であるとき、結果はそれらを結合したものになります。
const beginning = "it was the raddest of times, ";
const ending = "it was the baddest of times";
// 出力 "it was the raddest of times, it was the baddest of times"
console.log(beginning + ending);
文字列のこの面白い事実を例で示すために、Processingのビルトイン変数であるkeyを使うことにします。key変数については、keyTyped()関数を使ってユーザー入力に応答する方法を簡単に見たときに、すでに述べています。key変数は、ユーザーが直近に入力した文字を持っていて、これはほかの文字列と同じように使用できる、普通の文字列値です。次の例では、空の文字列のスケッチから開始します。ユーザーがキーを入力すると、入力されたキーが空の文字列の末尾に追加されます。文字列はdraw()で表示します。これは簡単なテキスト入力ボックスです。
文字列の配列への解析
ここまで扱ってきたサンプルは、文字列の個々の文字に対して作用するものでした。英語では時々テキストを、あまり知られていない”単語”と呼ばれる単位を使って扱いたい場合があります。任意のテキストを単語に解析するのは易しい作業ではありませんが、それらしいことなら簡単に実現できます。次の例では、文字列値の.split()メソッドを使ってこれを行っています。
.split()メソッドは、元の文字列の部分で構成される配列を返します。パラメータにはセパレータ(区切り文字)と呼ばれる、区切る部分の範囲を決めるものを1つ指定します。
const commaSeparated = "parrish,allison,college professor,surly";
const parts = commaSeparated.split(",");
console.log(parts[2]); // 表示 "college professor"
セパレータとして” “(1つの空白文字)を使うと、結果は(大体)、元の文字列に含まれる単語を1要素として持つ配列になります。
const someText = "it was the best of times";
const words = someText.split(" "); // 文字列の配列に評価される
for (var i = 0; i < words.length; i++) {
console.log(words[i]);
}
次のスケッチは、.split()メソッドをこの方法で使った例です。ここでは、文字列を指定し、それを単語に分割して、スケッチ中のランダムな位置に表示します。
演習
上記スケッチを、.split()メソッドを使って、1フレームごとに1単語を表示するように作り変えます。
エスケープ記法
普段はあまり使わない文字を文字列に含めなければならない場合が時々あります。たとえば、一重引用符(')を使っている場合には、文字列に一重引用符(アポストロフィー)を含めることはできません。JavaScriptはそれを、文字列を閉じようとしているのだと解釈し、シンタックスエラーを引き起こします。
const someText = 'the emperor's new clothes'; // シンタックスエラー
その場合には、文字列を囲む一重引用符を二重引用符にかえると解決できます。
const someText = "the emperor's new clothes"; // シンタックスエラー起きない
しかしそれでも、次のように一重引用符と二重引用符両方を含む文字列はあり得ます。
// シンタックスエラー
const someText = '"Which clothes?" he asked. "The emperor's new, natch!"';
引用文字('と")はJavaScriptコードの一部ではなく、文字列のリテラル部を意味することをJavaScriptに伝えるには、エスケープ文字を使用します。エスケープ文字はバックスラッシュ(\)が前に付いた文字で、特殊な意味を持ちます。たとえば、一重引用符のエスケープ文字は\'です。
// シンタックスエラー起きない
const someText = '"Which clothes?" he asked. "The emperor\'s new, natch!"';
同様に、二重引用符で囲まれた文字列内には、\"エスケープ文字を使って、二重引用符を含めることができます。
const someText = "She frowned. \"I don't know what you mean.\"";
よく使用されるもう1つのエスケープ文字は\nです。これはJavaScriptに、文字列内に改行(line break)を入れるよう伝えます。
Mozilla Developer Networkにはエスケープ文字の一覧が掲載されています。
外部テキストの読み込み
p5.jsスケッチのコードに大量のテキストを含める簡単な方法はありません。しかしそれに代わる、外部ファイルに保存したテキストを実行時にスケッチに読み込むという選択肢があります。Processinにはこれを容易にするloadStrings()関数があります。loadStrings()関数はテキストファイルを読み込み、ソースファイルの1行を1項目とする、文字列の配列を返します。
loadStrings()関数は、プレーンなテキストファイルに対してのみ機能します(つまり、たとえばWordドキュメントやHTMLファイルはロードできません)。プレーンテキストに関する詳しい解説はここで読むことができます。
以降のサンプルでは、ヒルダドゥリトルのSea Roseのテキストを使用します。スケッチでこのテキストファイルを使用するには、これをダウンロードしスケッチフォルダにコピーします(画像やサウンドファイルで行ったことと同じです)。loadImage()やloadFont()と同様、loadStrings()関数もpreload()内で呼び出す必要があります。
次のスケッチは、ファイル全体を読み込み、それを1行ごとに表示する簡単な例です。
let seaRoseLines;
function preload() {
seaRoseLines = loadStrings('sea_rose.txt');
}
function setup() {
createCanvas(400, 400);
noLoop();
}
function draw() {
background(50);
textSize(16);
for (var i = 0; i < seaRoseLines.length; i++) {
fill(128 + (i * 10));
text(seaRoseLines[i], 50, 50 + i * 20);
}
}
次のスケッチでは、p5.jsのビルトイン関数のshuffle()を使って、行を含む配列の順番をマウスプレス時にランダムに変えています。
let seaRoseLines;
function preload() {
seaRoseLines = loadStrings('sea_rose.txt');
}
function setup() {
createCanvas(400, 400);
}
function draw() {
background(50);
textSize(16);
for (var i = 0; i < seaRoseLines.length; i++) {
fill(128 + (i * 10));
text(seaRoseLines[i], 50, 50 + i * 20);
}
}
function mousePressed() {
shuffle(seaRoseLines, true);
}
テキストのメトリクス(計測)
ほとんどのフォントでは、ある文字列が何ピクセル幅で表示されるかを言い当てるのは困難です。Processingはこの作業の手助けをするtextWidth()関数を提供しています。textWidth()関数はパラメータとして文字列を取り、現在のフォント設定での、その文字列の幅をピクセル単位で返します。
次のスケッチでは、Sea Roseのテキストの表示にtextWidth()を使用します。しかし表示するのは、実際の単語ではなく、その単語のサイズに等しい幅を持つ矩形です(単語はマウスボタンを押しつづけると表示されます)。
let seaRoseLines;
function preload() {
seaRoseLines = loadStrings('sea_rose.txt');
}
function setup() {
createCanvas(400, 400);
}
function draw() {
background(50);
textSize(16);
noStroke();
textAlign(LEFT, TOP);
for (var i = 0; i < seaRoseLines.length; i++) {
var words = seaRoseLines[i].split(" ");
var currentOffset = 0;
for (var j = 0; j < words.length; j++) {
var wordWidth = textWidth(words[j]);
fill(128 + (i * 10));
rect(25 + currentOffset, 25 + i * 20,
wordWidth, 16);
if (mouseIsPressed) {
fill(0);
text(words[j], 25 + currentOffset, 25 + i * 20);
}
currentOffset += wordWidth + 4;
}
}
}