entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

筆跡再生機能の Flash を Canvas に移植しました

福岡オフィスで課題解決チームに所属しているエンジニアの @petamoriken です。弊社では drawr というサービスを11年間提供していましたが、2019年12月2日13:00をもってサービスを終了することとなりました。詳しくは drawr 特設サイトを御覧ください。

drawr はサービスの中核が Flash の技術で成り立っています。特にイラストを描くドロー画面と、投稿された作品の筆跡再生機能がブラウザにプラグインとして搭載されている Flash Player を使って提供されています。

drawrのサービス終了にともない、以下の機能を新規開発・提供しました。

  • 投稿作品一括ダウンロード機能
  • お絵かきコミュニケーションサービス「pixiv Sketch」への引っ越しツール
  • drawrの描き味を再現したドロー機能「シンプルドローモード」(pixiv Sketch で利用可能)

その中でも特に「投稿作品一括ダウンロード機能」において、Flash による筆跡再生機能を Canvas に移植する部分を担当しました。このあたりは特にドキュメントも残っていないようでしたので、ほとんど ActionScript を書いたことがありませんでしたが Flash のプロジェクトファイルのコードを解析することになりました。

Flash について

drawr 内で使われている Flash は ActionScript 3.0 で実装されています。言語仕様としては廃止された ECMAScript 4 をベースにしており、API リファレンスが公開されています。 Adobe® Flash® Platform 用 ActionScript® 3.0 リファレンスガイド

ActionScript をタイムライン上のフレームに割り当てて、そこのフレームに到達すると処理が実行されるようになっています。任意のタイミングでコードを実行するには次の2つの方法を使うことになるかと思います。

フレームの再生を制御する

グローバルオブジェクトに割り当てられている MovieClipplay()stop() を呼び出すことによって今の再生を制御することが出来ます。また gotoAndPlay(frame: Object)gotoAndStop(frame: Object) 1を呼び出して、再生位置を移動する事ができます。各フレーム間で変数を共有したい場合は所謂グローバル汚染をすることになります。

つまりフレームの移動がステートの変更と対応付けられると言うことが出来るのかなと思います。

イベントをハンドリングする

ActionScript 3.0 API の StageSpriteEventDispatcher を継承しているので、DOM API の EventTarget や Node.js の EventEmitter のように addEventListener(type: String, listener: Function) を使うことが出来ます。例えば画面のクリックに合わせて処理をさせるには以下のように記述します。

筆跡再生機能の Flash の解析

drawr で使われている筆跡再生機能の Flash を解析すると、複数のフレームを移動して成り立っており、大きく分けて2つの処理で成り立っていることがわかりました。

  1. サーバーから筆跡再生用のデータを取得する
  2. 取得したデータを元に Sprite を作り、画面に描画する

それぞれ詳しく説明しますと以下のようになっています。

サーバーから筆跡再生用のデータを取得する

投稿された作品の ID から該当する XML を取得しパースする。パースした結果から筆跡再生用のデータの URL がわかるので URLLoader を使ってバイナリデータとして取得する。フェッチが完了したら ByteArray としてバイナリデータが得られるので ByteArray#uncompress() を呼んで Zlib 形式のバイナリを解凍し、ByteArray#readObject() を呼び出して Action Message Format 3 形式2(以下 AMF3 と表記します)のバイナリを Object に変換して次のフレームに渡す。

取得したデータを元に Sprite を作り、画面に描画する

再生の状態や位置、スピードを制御する UI の Sprite にクリックイベントなどを設定して制御できるようにする。前のフレームから得た Object を元に筆跡の線一つ一つの Sprite を作り、再生位置に合わせて描画する。

筆跡データの取得とデコード

前述した「1. サーバーから筆跡再生用のデータを取得する」については一括ダウンロードの ZIP に同封するためにサーバー側で処理するようにしました。またローカルで file:// として HTML をブラウザで実行した場合、Fetch API を使うことが出来ない3ため、こちらについてもサーバー側であらかじめデコードする必要があります。クライアント側では基本的にデータの非同期読み込みは HTMLScriptElement を使うしかない4ため、昔懐かしい JSONP のような方法5を使うことにしました。

今回 drawr クローズのために用意したサーバーは Beego で構成しているため、Go を使って AMF3 をデコードしました。今回は goamf を使わせていただきました。

AMF3 から JSON へのコンバートは一筋縄ではいかず、色々と考慮しなければならないことがありました。

  • ES4 では 32bit integer に収まる Number については Int にキャストされることがあるらしく、実際に AMF3 のファイルは NumberInt が入れ交じって格納されていたため、Go の json.Marshal を呼び出す前に float64 に統一化した struct を構成した。
  • 歴史的な経緯からか AMF3 が Array ではなく length プロパティを持たない ArrayLike のような Object の形式になっていたため、クライアント側で使いやすくするために Array に変換する必要があった。
  • 特定の期間に投稿された作品について AMF3 として不正なデータが見つかったため、可能なところまでデコードするようにした。

色々と試行錯誤した結果、最終的に以下のような形式の JSON を作成できるようになりました。

Canvas に描画する

クライアント側で前述した「2. 取得したデータを元に Sprite を作り、画面に描画する」の部分を再現しました。ActionScript 3.0 のコードが残っているため、API に互換性のある CreateJS の EaselJS を使うことにしました。これによって UI の部分は特に元の ActionScript 3.0 とほとんど変わらないコードで書くことが出来ます。

筆跡の再生部分については Flash 内ではステートがグローバル変数で制御されており、React のコンポーネントでラップする制約上そのままでは厳しかったため、JavaScript の class を使って書き直しました。描画を1フレーム進める next メソッドと1フレーム戻す prev メソッドを作り、経過時間や UI のイベントハンドリングによってこれを操作するような感じで実装して上手くいったかなと思っています。

Canvas で筆跡再生機能を再現したのが以下の GIF です。

感想

新機能を実装するというよりかは既存の機能を調査して正確に再現するタスクでした。個人的に ECMAScript の仕様を追っているので ActionScript 3.0 の仕様を見て挙動を確認するのはとても楽しい業務でした。グローバル変数を追うのが大変でしたが……。

また、サービスクローズにともない開発した機能は、いずれも想定以上にご利用いただいています。ありがとうございます。今後とも創作活動を楽しめるようにサービスの開発に注力し、より良いサービスの提供ができるように努めていきたいと思います。


  1. ES4 における型アノテーションの Object は TypeScript における any と対応付けられます。 

  2. AMF 3 Specification 

  3. URL のスキームが file のとき Fetch API の仕様でちゃんと定義されておらず、ブラウザで動作しません。 

  4. 実際は筆跡データが膨大な JSON ファイルになってしまい読み込み時にブラウザを固めてしまうため、可能ならば Web Worker 内で importScripts() を使って読み込むようにしています。それが出来なければ HTMLScriptElement を使う方法にフォールバックするようにしています。 

  5. 実際は JSONP とは違い JSON ファイルの先頭に self["foobarbaz"]= を付与して JavaScript のファイルとして扱い、グローバル汚染をする形で受け取るようにしています。 

20191219014119
petamoriken
2017年7月から福岡オフィスでアルバイトをしている。元々フロントエンドエンジニアだったがiOSアプリやサーバーサイドの開発も携わっている。 午後のおやつと同じくらいECMAScriptやDOM APIの仕様を追うのが好き。百合男子。腐男子。