p5.js + TypeScriptで描画と動画変換を行う
はじめに
前回までp5.jsを使用する際はそのままJavaScriptを使用していましたが、実装中に型付け出来ないのが不便だったので TypeScriptへの切り替えを行いました。その際p5.createloopのtypesが見つからなかったので、動画変換用の実装が必要になりました。
変換にはMediaStream Recording APIを使用します。 当初はmp4形式をメインに変換しようと考えていましたが、 MediaRecorderによるmp4の変換ではたまに画質が悪くなるケースが発生したためwebm形式変換メインに変更しました。 今回はwebm変換のみを行いますが、紹介する方法と同じ方法でmp4にも変換可能ですし、ffmpegを使用してwebmからmp4形式に変換する選択肢もあります。 また注意としてはMediaRecorderによる変換では動画時間などのメタデータは記録されません。
バンドラーはwebpackとParcelで迷いましたが、tsconfigの手間が省ける分Parcelを選択しました。
package.jsonと必要なものインストール
package.jsonは以下のように記載しておきます。scripts以外は好きに書き換えて構いません。 parcelを実行したときに開くhtmlファイルはsrcフォルダに入れておくようにします。
{ "name": "p5-typescript", "version": "0.1.0", "description": "p5-typescript Project", "scripts": { "start": "parcel src/index.html --open --no-cache --no-source-maps", "build": "parcel build src/index.html --no-cache --no-source-maps" }, "author": "tk87" }
また以下をインストールします。
npm install --save @types/p5 npm install --save p5 npm install --save parcel
するとpackage.jsonにdependenciesが追加されます。
"dependencies": { "@types/p5": "^1.4.2", "p5": "^1.4.1", "parcel": "^2.5.0" }
htmlファイル
また描画を行うためのTypeScriptファイル名をsketch.tsとしてindex.htmlと同じくsrcフォルダに入れておきます。
<!DOCTYPE html> <html lang=""> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>p5 typescript main</title> <style> body { padding: 0; margin: 0; } </style> <script type="module" src="./sketch.ts"></script> </head> <body> <div id = "status">stop</div> <main> </main> </body> </html>
後で動画変換中の状態を見るために id = status のdiv要素を挿入しておきます。
webm変換用TypeScriptコード作成
p5で実行した時のアニメーションをwebmに変換するコードを作成しますが、 これはMediaRecorderを使用するのでstreamにcanvas要素やvideo要素を指定すれば他のケースでも使用可能です。
https://developer.mozilla.org/ja/docs/Web/API/MediaRecorder
描画用コードと変換用コードファイルを分けるため、 sketch.tsとは別にsrcフォルダにrecord.tsファイルを作成します。
//record.ts export default (framerate: number) => { let recorder: MediaRecorder; let chunks = []; let stream = document.querySelector('canvas').captureStream(framerate); const options = { mimeType : 'video/webm; codecs=h264' }; recorder = new MediaRecorder(stream, options); recorder.ondataavailable = e => { if (e.data.size) { chunks.push(e.data); } document.getElementById('status').textContent = "Recording... " + (chunks.length).toString() + " sec"; }; recorder.onstart = e => { document.getElementById('status').textContent = "Recording... " + "0" + " sec"; }; const exportVideo = (e: Event) => { let blob = new Blob(chunks, {'type': 'video/webm'}); let url = URL.createObjectURL(blob); let a = document.createElement('a'); document.body.appendChild(a); a.style.display = "none"; a.href = url; a.download = 'video.webm'; a.click(); window.URL.revokeObjectURL(url); chunks = []; document.getElementById('status').textContent = "stop"; }; recorder.onstop = exportVideo; return recorder; };
フレームレートとoptionsからコーデックを設定して、MediaRecorderオブジェクトを作成します。 p5.jsはcanvas要素に描画されますので、キャプチャstreamにはcanvas要素を指定します。
recorderインスタンスを作成する際にtimesliceを指定しますが、timesliceミリ秒ごとに recorder.ondataavailable が呼び出されます。 timesliceを1000に設定することで1秒ごとに呼び出されるようにして、document.getElementById('status').textContent から #starus 要素に秒単位で録画経過時間が表示されるようにします。
recorder.onstopで録画が終了したことを検知してexportVideoを呼び出します。 chunksに保持しておいたデータをblobオブジェクトに変換し、 blobオブジェクトURLをURL.createObjectURLで作成して、 a要素href属性にURLを設定してから自動クリックでダウンロード処理を行い、処理を終えるとオブジェクトを開放するようにしています。
描画用TypeScriptコード作成例(perlin noise)
次はTypeScriptを使用してp5で描画を行います。 ここではperlin noiseとファイルからロードした画像をブレンドして描画を行います。 record.tsは二つ目の例で使用します。
//sketch.ts import p5 from "p5"; import image from "../data/bird_mask.png"; const sketch = (p: p5) => { const W = 280; const H = 280; const colors = ["#2e8b57","#191970","#1e90ff","#d2691e","#006400","#191970"]; let img: p5.Image; p.setup = () => { p.createCanvas(W, H); p.fill(255); img = p.loadImage(image); }; p.draw = () => { p.blendMode(p.SCREEN); let noisescale = 0.02; for(let x = 0; x < W; ++x) { for(let y = 0; y < H; ++y){ let val = p.noise(x*noisescale, y*noisescale); p.stroke(colors[p.int(val*36)%6]); p.point(x, y); } } p.image(img, 0, 0, W, H); p.noLoop(); }; }; new p5(sketch);
perlin noiseはnoiseメソッドから取得できますが、 perlin noiseの性質上全ての画素位置に対してnoise値を取得することになります。 よって、一つ一つ画素を走査・編集を行う必要があるため描画のスピードがかなり遅くなります。
追記: 点を打つ度にstroke,pointを呼んでいるので効率が悪いです。setとupdatePixelsを使用してください。
p5.jsで対応しているブレンドモードは https://p5js.org/reference/#/p5/blendMode を参照してください。 今回はSCREENを使用しました。SCREENは色を乗算して反転させますが、色を乗算すると暗くなるので明るい色合いにするために反転させました。
画像のロードはparcelから使用する場合、importしてからloadImageに適用します。 以下は出力画像になります。元画像はハチドリの画像に二値化加工したものを使用しています。
描画用TypeScriptコード作成例2(Truchet Tiles)
実際にrecord.tsを使用して、描画アニメーションを作成してwebm変換してみようと思います。 ここではトルシェタイルを描画してタイルを動かします。
まずrecord.tsをimportしておきます。
import p5 from "p5"; import record from "./record";
setup関数で recorder = record(framerate); を挿入すればrecorderオブジェクトを作成できます。
p.setup = () => { p.createCanvas(W, H); for(let x = 0; x < cols; ++x) { tiles[x] = new Array(rows); for(let y = 0; y < rows; ++y) { tiles[x][y] = new Tile(tilesz, x*tilesz, y*tilesz, p.int(p.random(0,4)) * p.TWO_PI / 4, colors[0], colors[1]); tiles[x][y].display(); } } p.frameRate(framerate); recorder = record(framerate); };
録画のスタートのさせ方ですが、自動的に録画を行う方法と、 キーを押して任意のタイミングで録画スタート/ストップできるようにする方法の二つを考えます。
自動録画
まず自動的に録画をスタートさせる方法ですが、以下のようにauto_record関数を作成します。 1秒あたりのフレームカウントと秒数から保存すべきフレーム数を計算することができます。 これをフレームごとに呼び出されるdraw関数部分に挿入すれば、自動的に録画がスタートし指定秒数を録画後保存処理を行います。 以下では30フレームレートで6秒間録画しようとしています。
p.draw = () => { auto_record(30, 6); }; function auto_record(rate: number, duration: number) { if(p.frameCount-1 == 0) { recorder.start(1000); } if(p.frameCount-1 == rate * duration) { recorder.stop(); } }
任意のタイミングで録画
次にキーを押して任意のタイミングで録画を行う方法ですが、keyPressedイベントを利用します。 以下ではsキーを押してスタートし、dキーを押して録画ストップして保存するようにしています。
p.keyPressed = () => { if (p.key == 's') { recorder.start(1000); } else if (p.key == 'd') { recorder.stop(); } };
トルシェタイルは以下のようなClassを作成し描画を行います。 以下の実装は一般的な三角形のトルシェタイルです。これをp5オブジェクト内のdraw関数でアニメーションを加えます(draw関数の記載は省略)。
class Tile { sz: number; x: number; y: number; rot: number; color1: p5.Color; color2: p5.Color; constructor(sz: number, x: number, y: number, rot: number, color1: p5.Color, color2: p5.Color) { this.sz = sz; this.x = x; this.y = y; this.rot = rot; this.color1 = color1; this.color2 = color2; } display() { p.push(); p.translate(this.x + this.sz/2, this.y + this.sz/2); p.rotate(this.rot); p.translate(-this.sz/2, -this.sz/2); this.draw(); p.pop(); } draw() { p.fill(this.color1); p.triangle(this.sz, 0, this.sz, this.sz, 0, this.sz); p.fill(this.color2); p.triangle(this.sz, 0, 0, 0, 0, this.sz); } }
まとめ
TypeScript経由でp5.jpを使用し、描画の実装や動画への変換を行いました。 webmに変換できればffmpegを使用して他の動画形式に変換することも可能になります。