thumbnails every 5 seconds

とある動画があったとして、例えば5秒毎にその再生位置の画像を生成したい。

$ ffmpeg -i 01.mp4 -vf fps=12/60 "%03d.jpg"

ffmpeg でこんなふうにすると良いらしい。秒数の指定が逆数になるのが何とも言えない。ところでファイル名は上記のような連番よりも、分-秒.jpg という形のほうがありがたいのだが。どうすれば良いのだろうか。

ググってみると、-strftime 1 というスイッチを付加するとそんな感じのプレースホルダが有効になるのだそうだ。なるほど。きっと基準時間はその時々の再生位置なのだろう素敵だ。

とか思いつつ試してみたところ、基準時間は普通に現在時刻だった。いや、そうじゃないだろ! いや、そうじゃないだろ! 保存時のタイムスタンプが欲しい時に使うためのものなのかな。

これをどうすれば良いのかは調べてもよく分からなかったので、とりあえず連番で出力するようにし、適当なスクリプトでそれをリネームするようにした。

memento of what you did

矩形云々をやると言いつつ、今度は undo/redo 周りに手を付けるライブ感覚。

レイヤーが3枚固定だった頃は、undo の仕組みはかなりシンプルで、リニアなリストに各レイヤー(を png でエンコードした data URL)を保持していた。それを undo マネージャが適宜使用するという memento パターンである。

さてレイヤーの枚数が自由になったので、リストの要素としてレイヤーそのものを直接持つのは容量的に良くなくなった。

そこで、undo リストとレイヤーのデータは管理を別にし、特に後者は版数と参照カウンタを伴ったクラスタとして保持するようにした。そのため undo リスト側では対応する版のレイヤーに対するポインタのみを保持することになる。

ところでひとつ気になるのは、前述の通りレイヤーをエンコードして保持している。これは容量削減のためなのだがエンコードに時間がかかるようだとよくない。ペンでクロスハッチを描く場合なんかは高速かつ大量に undo 情報の追加が行われる。ここで追加がもたつくのは避けたい。

なのでエンコードではなくcanvas要素そのものを保持することも考えられる。実はそのほうが undo 時に非同期的にデコードする処理を挟まなくていい分楽ではある。あるいはハイブリッドに、とりあえずクラスタ内のレイヤーはcanvasで持っといて、利用されないままある程度の時間が経過したら自動的にエンコードするとかにすればいいのかな。それがいいかもしれない。

* * *

data URL ではなく toBlob() で非同期的に blob に変換するようにした。これに伴い undo 情報の追加は数ミリ秒のオーダーになった。これならいいかな。

merge #2

統合と結合の違いについて考えてみる。

前の記事の通り、結合という訳はおかしいのであるが、では統合はどうかと言うとこちらは別に合ってるわけでも合ってないわけでもないどうでもいい感じ。重要なのは、結合と統合は実際に動作が異なるということだ。

それには、まず背景レイヤーと通常のレイヤーの違いを認識しておく必要がある。桃では、背景レイヤーは

  • 常にレイヤーの最奥にある
  • 削除できない
  • 移動できない
  • 不透明度が1ではないピクセルを置けない(これは変えるかもしれない)
  • そのサイズがキャンバス自体のサイズを表している

という扱いになっている。逆に通常レイヤーはサイズと位置が自由で、特に位置とは背景レイヤーに対する相対位置を意味している。

それを踏まえて、まず統合とは全てのレイヤーを背景レイヤー上に合成することだ。この際、各通常レイヤーの、キャンバスサイズからはみ出した部分はすべて切り落とされる。一方で結合は通常レイヤーの大きさと位置が保持される。

ところでPhotoshopやgimpの場合、可視レイヤーの結合が背景レイヤーも含める仕様になっているのだがこれはいいのだろうか。つまり背景レイヤーを含めて全てのレイヤーが可視である状態だと、可視レイヤーの結合と画像の統合に基本的に違いがないのである。

これはちょっと気になったので、桃における可視レイヤーの結合は背景を除外するようにした。

merge

と言いつつ矩形選択よりも先にレイヤーの結合・統合を片付けたい。

この結合・統合というのは、Photoshop 日本語版の用語なのだが、まずこれがおかしい。訳がおかしい。

英語版における “Merge Down” が「レイヤーを結合」、”Merge Visible” が「可視レイヤーを結合」と訳されている。つまり、Merge → 結合ということなのだが。結合という熟語は文字通り複数の何かが結びつきあうことそのものを意味していて、そしてそれ以上の含みはない。結合されるもの同士がどうなるのか、元のままなのか、とか、一度結合したものは元に戻れるのか、という副次的な意味はない。

しかしレイヤーのMergeというのは、それを実行した結果、複数のレイヤーがひとつになって、しかもそれは可逆ではない…というのが最も重要な点だ。味噌とマヨネーズを混ぜ合わせたら、Undo という時間操作魔法以外にもうそれを分離することはできないのと同じだ。こういった視点は、上記の通り、結合という熟語があまり関心を払っていない部分である。すなわち Merge を結合と訳すことにおける決定的なズレがここにある。それでもなお結合という言葉を使うなら、「レイヤーを結合(してひとつにする)」のようにしなければならず、そして括弧内がこのメニュー項目の最も重要なポイントなのだが、あろうことか最も重要な箇所が省略されている。

一方で、結合という言葉を捨てるとすると、例えば融合とか、溶融とか、一体化とか合体とかそんな感じのほうが合っている。いや最も merge の意味を正しく表しているのは実は「まぜまぜ」だ。語感も merge と似ている。「下のレイヤーとまぜまぜ」。

などとつらつらかんがえた結果、結局「結合」のままで行くことにした。

tweak layers #8

レイヤーパレットを作ったことでヘッダからレイヤー関連を一旦削除したが、キャンバス上部にも最低限のレイヤー操作インターフェースがあったほうがいいかなということで追加しなおした。

レイヤーパレット上のサムネイルは、レイヤー全体を対象とするのではなく、透明色を除いた実際の内容部分を対象とするようにした。

次は範囲選択時の演算を作る。

Habit modulation #2

というわけでテストを書く。

テストは mocha に実行させる。一方、桃は esm の文法に基づいてモジュールを分けてある。ここで問題が出てくる。ググればたくさんその手の話が出てくるが、要するに mocha、ひいては node.js が今現在 cjs と esm の過渡期にあって、まだあんまり esm の対応が行き届いていない。

mocha においても、import/export を使ったソースを渡してもエラーになる。その辺をどうするかはまさに議論中のようだ。

で、いくつかのワークアラウンドが考えられて:

  • ブラウザ上でテストを実行する: cjs と esm の混乱はあくまで node.js の中の話である。ブラウザを起動してその上でテストを実行すれば普通に esm のコードがテスト対象になる、はず
  • esm のコードを babel でトランスパイルした上でそれをテストする
  • モジュールをダイナミックロードする

前2者はワークアラウンドにしては大掛かりになるので、最後の奴で行ってみよう。

const assert = require('assert');
let testfunc1, testfunc2;

before(() => import('./path/to/module').then(module => {
  ({testfunc1, testfunc2} = module);
});

describe('test', () => {
  it('works', () => {
    // test code with testfunc[12]
  });
});

どうでもいいが最近のこの wordpress のエディタが使いにくい。

Habit modulation

「レイヤーの複製」という機能を実装した。これも、処理の本体よりいろいろな周辺の事柄の方が面倒なパターンである。

Photoshopにも同じようなメニュー項目がある。こちらは「レイヤーの複製…」であり、後ろに点々があるというのはつまりダイアログを経由するということだ。といってもこのダイアログではそんなにできることは多くない。レイヤー名と、出力先を指定できるくらいだ。

この出力先としてPhotoshopで開いている別のドキュメントを選択できるのだが、これはなかなか重要なようなそうでないような微妙な位置づけだ。これがないとレイヤーがもともと属するドキュメント上で複製し、それをカットして、別のドキュメント上でペーストする手順を踏まなければならないのだが、ダイアログで出力先を指定できることでこの手順を飛ばせる。

一方で、PhotoshopをいわゆるMDIアプリケーションとして運用するならレイヤーパレットからドラッグを開始して目的の文書上でドロップすればだいたい同じことはできる。こちらのほうが分かりやすいし手っ取り早い。微妙というのはそういうことだ。

さて桃の場合、いまのところ同一ページで複数の画像を取り扱うことはないのですなわち「…」もこの微妙なダイアログもない。メニュー項目を選択したら即レイヤーが複製される。いいことだ。この時、複製されたレイヤー名は自動的に付けられるのだが、それがこの記事の主題だ。

この複製後のレイヤー名の自動生成がなかなかめんどくさい。複製元のレイヤーの名前が「のコピー」で終わっている、「のコピー #\d+」で終わっている、そうではない場合のそれぞれについてふさわしい名前を生成しないといけない。めんどくさい。

めんどくさいが、しかし入力と出力は決定的でユーザのインタラクティブな操作に左右されることもないので、むしろユニットテストに回しやすいネタだ。そうだテストを書こう。

tweak layers #7

ドラッグ&ドロップのついでに、画像ファイルを桃にドロップした場合レイヤーとして読み込むようにした。また、Alt+[、Alt+] でアクティブなレイヤーを前後に切り替えられるようにした(Photoshopと同じ)。

その他、Ctrl+Shift+[、Ctrl+Shift+] などアクティブなレイヤー自体を前後に動かす系のショートカットも実装したのだがこれがとても難しい。

処理自体が難しいのではなく、ショートカットの扱いが難しい。キー入力は keydown イベントで受け、KeyboardEvent#key で判断することになる。一方、Shift を押しているか否かでマッピングが変わる系のキーというものがある。こういったキーを keydown イベントで取得すると例えば Shift+[ というストロークは Shift+{ という扱いになる。これが正しいのか間違っているのかよく分からない。入力された文字と押されたキーの混同が起こっている。はて。

tweak layers #6

大量にレイヤーを追加した場合、当然ながらレイヤーパレットの最大表示幅を超えるレイヤーサムネイルが生成されることになり、隠れているサムネイルの表示の制御はユーザーのスクロール操作によって行われる。

さて、レイヤーを並べ替えるためにドラッグを開始して、スクロール領域から隠れている場所にドロップしたい場合、スクロール領域の端っこにポインタを持っていくと自動的にスクロールするような機能が必要になるのでそれを作らないといけない。

ところが驚くことに。そういう機能作らないとだなーめんどくさいなーなどと思いながらドロップを試してみた所、すでにそういう動作をするではありませんか。んん〜? ブラウザの標準の機能なの?

Chromium ではだいたい想定したとおりの動作をし、Firefox ではスクロール領域の端っこでポインタをちょこちょこ動かした時に限り自動スクロールするようだ。ふーんじゃあ別にコードを書かなくてもいいのかな。

だいたい、というのはスクロール領域内部の端っこだけではなく領域の外側も自動スクロールのトリガーになっててほしいのだけど、まあ、いいかな。

tweak layers #5

さてレイヤーパレットができたことでヘッダ部分のレイヤー周りは不要になったので、色々と整理した。また、描画ツールのオプションはポップアップ形式をやめ、ヘッダの下部に表示するようにした。

レイヤーパレットの実装がもうちょっと続く。ドラッグ&ドロップによるレイヤーの並べ替えが必要。