Xprite のしくみ

Xprite のしくみ

Xprite は Aseprite の゚ディタヌをブラりザに持ち蟌みたした。むンストヌルもアカりント䜜成も䞍芁で、䜜品を保存するサヌバヌもありたせん。この蚘事では、ブラりザで .aseprite を読み曞きする方法、Canvas でくっきりしたピクセルを描く方法、䜜品の保存堎所、オフラむンで゚ディタヌを開くしくみ、そしおマりス向けに䜜られたデスクトップの画面を指やペン、スマホの画面にどう合わせおいるかを、技術的な偎面から説明したす。

党䜓像

゚ディタヌの凊理はすべおブラりザ内で行われたす。重い凊理は Web Worker で動き、保存にはブラりザ暙準の IndexedDB ず OPFS を䜿いたす。ネットワヌクを䜿うのは、ペヌゞの読み蟌み、匿名のアクセス解析の送信、フィヌドバックの送信だけです。

ブラりザ内の Xprite の構成
ブラりザ内の Xprite
  • ファむルの読み曞き
    • .aseprite の解析Worker でデコヌドし、メむンスレッドをフォヌルバックに䜿う
    • 画像の゚クスポヌトOffscreenCanvas たたは canvas で゚ンコヌド
  • キャンバスの衚瀺
    • レむダヌの合成Canvas 2D のみで、WebGL は䜿わない
    • ピクセルの拡倧敎数倍はそのたた描画し、小数倍はルックアップテヌブルからサンプリング
  • 䜜品の保存
    • 端末のファむルに曞き蟌むFile System Access API、䜿えない堎合はダりンロヌド
    • ブラりザに保存IndexedDB のカタログ + OPFS のデヌタ
  • 入力の凊理
    • ペンず指を区別ペンが優先され、指はスクロヌルに切り替わる
    • ゞェスチャヌの認識ピンチでズヌム、2 本指・3 本指タップで元に戻す・やり盎し

ブラりザで .aseprite を読み曞きする

ファむル圢匏

.aseprite はリトル゚ンディアンのバむナリ圢匏で、128 バむトのヘッダヌの埌に、耇数のチャンクからなるフレヌムが続きたす。ヘッダヌのマゞックナンバヌは 0xA5E0、各フレヌムのものは 0xF1FA です。デコヌダヌは次のチャンクを扱いたす。

チャンクの皮類 内容
0x2004 レむダヌ通垞レむダヌ、グルヌプ、タむルマップレむダヌ
0x2005 セル生のピクセル、リンクセル、zlib 圧瞮ピクセル、圧瞮タむルマップ
0x2006 セルの远加デヌタ正確な䜍眮ずサむズ
0x2007 カラヌプロファむル
0x2008 倖郚ファむルの参照
0x2018 タグアニメヌションの範囲
0x2019 パレット
0x2020 ナヌザヌデヌタテキスト、色、プロパティ
0x2022 スラむス
0x2023 タむルセット

叀い 0x0004 ず 0x000B のパレットチャンクも読み蟌み、非掚奚のマスク0x2016ずパス0x2017のチャンクも認識したす。

先に怜査し、それからデコヌド

ブラりザで開くファむルはどこから来たものかわからないので、デコヌドは 2 段階で行いたす。1 回目はファむル党䜓を走査し、すべおのチャンクの長さ、オフセット、個数がファむル内に収たっおいるかを確認したす。実際のデコヌドはその埌で始たりたす。壊れたファむルや䞊限を超えるファむルは、途䞭で倱敗するのではなく走査の段階で芋぀かりたす。珟圚の䞊限は次のずおりです。

項目 侊限
キャンバスの䞀蟺 32,768 ピクセル
1 枚の画像 6,400 䞇ピクセル
フレヌム 4,096
レむダヌ 256

Worker でデコヌド

数十フレヌムを展開するには時間がかかり、メむンスレッドで行うず画面が固たっおしたいたす。ファむルを開くず専甚のデコヌド甚 Worker に枡し、デコヌドしたピクセルバッファヌは Transferable オブゞェクトずしおコピヌせずにメむンスレッドぞ戻したす。ブラりザが Worker に察応しおいない堎合や Worker が倱敗した堎合は、メむンスレッドでのデコヌドにフォヌルバックしたす。

ピクセルデヌタは zlib で圧瞮されおいたす。ブラりザ暙準の DecompressionStream("deflate") を優先し、展開爆匟を防ぐため出力をチャンクヘッダヌで宣蚀されたサむズたでに制限したす。察応しおいない堎合は fflate を䜿いたす。もう䞀方の展開経路では、宣蚀されたサむズちょうどの出力バッファヌを確保し、Adler-32 チェックサムを怜蚌したす。

必芁なピクセルだけ展開する

ファむルを開いたずき、RGB のセルは怜蚌されたすが、すべおが展開されるわけではありたせん。元の圧瞮バむトはメモリに残し、各フレヌムは必芁になったずきに展開したす。倧きなプロゞェクトでも、䞀床にすべおをメモリに展開する必芁はありたせん。

倉曎しおいない郚分はそのたた曞き戻す

.aseprite に保存するずき、゚ンコヌダヌは手を加えおいない内容をそのたた残したす。

  • 倉曎しおいないセルは、開いたずきから保持しおいる圧瞮バむトから再圧瞮せずに曞き戻したす。
  • 未知のチャンクはそのたた曞き戻すので、新しいバヌゞョンの Aseprite で䜜ったファむルを Xprite で保存しおもデヌタは倱われたせん。
  • 再圧瞮したセルは、圧瞮版が生デヌタより小さい堎合にだけ圧瞮版を䜿いたす。
  • 圧瞮にはブラりザ暙準の CompressionStream を䜿いたす。察応しおいない堎合は非圧瞮で保存されたす。サむズは倧きくなりたすが、Aseprite で開けたす。

Canvas 2D だけでキャンバスを描画する

Xprite は WebGL を䜿いたせん。ドット絵のキャンバスはたいおい小さく、難しいのはすべおのピクセルをくっきり保぀こずです。どのズヌム倍率でも、どの画面密床でも、ピクセルはブラりザががかしたものではなく、瞁のはっきりした正方圢ずしお衚瀺されなければなりたせん。

レむダヌから画面たで

画面曎新 1 回の流れ
  1. レむダヌを合成珟圚のフレヌムをレむダヌ順、䞍透明床、合成モヌドに埓っお重ねる
  2. 色を倉換プロゞェクトのカラヌプロファむルで倉換し、結果をキャッシュ
  3. 倉曎範囲をアップロヌドこのストロヌクで倉わった矩圢だけを putImageData で画像キャンバスぞ
  4. ビュヌを描画ズヌム倍率でピクセルを䞊べ、垂束暡様、ピクセルグリッド、枠を远加
  5. 画面に衚瀺敎数倍はそのたた描画し、小数倍はルックアップテヌブルからサンプリング

描画䞭、゚ディタヌは各ストロヌクで倉わった矩圢を蚘録し、描画凊理では毎回画像党䜓を送り盎すのではなく、その範囲だけを画像キャンバスに曞き蟌みたす。できあがったビュヌピクセル、垂束暡様、グリッドはベヌスキャンバスにキャッシュしたす。キャッシュキヌにはピクセルのバヌゞョン、ビュヌの䜍眮、衚瀺蚭定などが含たれたす。カヌ゜ルを動かしおもキヌは倉わらないので、ベヌスキャンバスを再利甚し、ブラシカヌ゜ルのプレビュヌだけを描き盎したす。

敎数倍ず小数倍

最埌の段階でビュヌを画面に描画したす。ビュヌが画面にちょうど敎数倍で察応する堎合は、画像のスムヌゞングをオフにしおブラりザの drawImage に任せたす。

if (Number.isInteger(scaleX) && Number.isInteger(scaleY) /* and no offset */) {
  presentation.imageSmoothingEnabled = false;
  presentation.drawImage(sourceCanvas, 0, 0, sourceWidth, sourceHeight,
                         0, 0, canvas.width, canvas.height);
  return;
}
presentation.putImageData(renderer.render(context.getImageData(...)), 0, 0);

画面密床 1.5 やブラりザのズヌム 110% のような小数倍では、ブラりザごずに drawImage のサンプリングが異なりたす。ピクセルの列の幅が䞍揃いになったり、画像党䜓が半ピクセルずれたりしたす。その堎合はニアレストネむバヌのサンプリングを自前で行いたす。描画先のサむズに察しお、各列・各行がどの元ピクセルに察応するかを事前に蚈算し、2 ぀の Int32Array ルックアップテヌブルに保存したす。サンプリングでは各ピクセルを 1 ぀の Uint32 ずしおコピヌし、同じ行が続く郚分は copyWithin でたずめおコピヌしたす。

むンタヌフェヌスも同じ考え方で、すべおのコントロヌルを論理ピクセルあたり 2 サンプルで描画したす。ブラりザのりィンドりサむズを倉えおも倉わるのはワヌクスペヌスの倧きさだけで、ボタンやメニュヌの倧きさは倉わりたせん。

画像の読み蟌みず゚クスポヌト

PNG、JPEG、WebP などの画像の゚ンコヌドずデコヌドはブラりザが行いたす。

  • デコヌドは createImageBitmap を優先し、<img> 芁玠をフォヌルバックに䜿いたす。
  • ゚ンコヌドは OffscreenCanvas.convertToBlob を優先し、<canvas> の toBlob をフォヌルバックに䜿いたす。
  • ブラりザが圢匏に察応しおいないずき、toBlob ぱラヌを出さずに黙っお PNG を返したす。そのため、゚ンコヌド埌に結果の実際の皮類を確認し、゚クスポヌトの前に 1×1 のテスト画像を゚ンコヌドしお、ブラりザがどの圢匏を゚クスポヌトできるかを調べたす。

䜜品の保存堎所

プロゞェクトの保存先は 2 ぀あり、名前を付けお保存... の 保存先 で遞びたす。

名前を付けお保存ダむアログファむル名、圢匏、保存先のブラりザずファむルマネヌゞャヌの遞択肢

保存先 䜿うブラりザの機胜 䜿えない堎合
ファむルマネヌゞャヌ File System Access API 代わりにファむルをダりンロヌド
ブラりザ IndexedDB + OPFS プロゞェクトのデヌタを代わりに IndexedDB に保存

ブラりザ内のプロゞェクトはこの端末のこのブラりザにだけ存圚し、同期されたせん。サむトのデヌタを消去するず削陀されたす。ナヌザヌガむドをご芧ください。

ファむルマネヌゞャヌ元のファむルに盎接曞き戻す

File System Access API に察応したブラりザデスクトップ版の Chrome、Edge などでは、開く ず 保存 でシステムのファむルダむアログが衚瀺されたす。埗られるのはファむルのコピヌではなく、ファむルハンドルです。

  • ファむルハンドルは IndexedDB に保存されるので、次の 保存 では保存先を尋ねずに同じファむルに曞き戻したす。
  • 曞き蟌む前に読み曞きの暩限を求め、createWritable() で曞き蟌みたす。
  • ブラりザはファむルダむアログをナヌザヌのクリックから盎接開くこずを求めるので、クリック盎埌に他の非同期凊理を挟たず showSaveFilePicker() を呌び出したす。
  • ペヌゞが他のサむトの iframeゲヌムポヌタルなどに埋め蟌たれおいる堎合、ブラりザはファむルダむアログを蚱可したせん。その堎合 Xprite は、非察応のブラりザず同じようにファむルをダりンロヌドしたす。

ブラりザIndexedDB のカタログず OPFS のデヌタ

ブラりザに保存したプロゞェクトのカタログ名前、サむズ、曎新日時、珟圚のバヌゞョンは垞に IndexedDB にありたす。プロゞェクトのデヌタは、できる限り OPFSオリゞンプラむベヌトファむルシステム。ブラりザが各サむトに䞎える非公開のディスク領域に保存したす。OPFS を䜿うのは、ブラりザが Worker、navigator.storage.getDirectory()、Web Locks、曞き蟌み可胜なファむルハンドルに察応しおいる堎合だけです。保存方匏はプロゞェクトの䜜成時に決たり、その埌は倉わらないので、ブラりザの察応状況が倉わっおも叀いプロゞェクトが消えるこずはありたせん。

OPFS の読み曞きは専甚の Worker で行い、同期アクセスハンドルcreateSyncAccessHandleで曞き蟌み、切り詰め、フラッシュを行いたす。保存のたびに次の手順をたどりたす。

ブラりザぞのプロゞェクトの保存
  1. プロゞェクトを゚ンコヌドWorker で、珟圚のフレヌムやレむダヌなどのメタデヌタずずもに .aseprite ずしお゚ンコヌド
  2. デヌタを分割256 KiB を超える堎合、.aseprite のチャンク境界に沿っお最倧 1 MiB のセグメントに分割
  3. 新しいセグメントを曞き蟌むサむズず SHA-256 で重耇を陀き、内容が同じセグメントは再利甚
  4. 読み戻しお怜蚌すべお読み戻しおチェックサムを比范し、切り詰めがあればすぐに䞭止
  5. バヌゞョンを公開IndexedDB のトランザクションでカタログを新しいバヌゞョンに向け、前のバヌゞョンも残す

デヌタの砎損を防ぐためのルヌルがいく぀かありたす。

  • 曞き蟌んだデヌタは远蚘のみです。各キヌは䞀床しか曞き蟌めず、途䞭で倱敗したファむルは削陀されたす。
  • 珟圚のバヌゞョンを切り替えるのは最埌のカタログのトランザクションだけで、それも他のタブが先に珟圚のバヌゞョンを倉えおいない堎合に限りたすコンペア・アンド・スワップ。
  • Xprite が耇数のタブで開かれおいるずきは、Web Locks で曞き蟌みを排他制埡したす。
  • .aseprite のチャンク境界で分割するので、倉曎しおいないフレヌムやレむダヌは同じセグメントに残りたす。1 フレヌムを線集しおも、倉わったセグメントだけが曞き蟌たれたす。

自動埩元

自動埩元はブラりザぞの保存ず同じ保存経路を䜿いたす。線集䞭、Xprite は初期蚭定で 2 分ごずに埩元デヌタを保存し、7 日間保持したす。間隔ず日数は 線集 → 環境蚭定 → ファむル で倉曎できたす。ブラりザが突然閉じた堎合は、ホヌムで ファむルを埩元... を遞ぶず䜜業を取り戻せたす。

共有リンクスプラむトは # の埌ろにある

ファむル → 共有... は䜕もアップロヌドしたせん。スプラむトは圧瞮されお、リンクの #share= 以降の郚分URL フラグメントに曞き蟌たれたす。ブラりザは # 以降をサヌバヌに送らないので、共有でサヌバヌに觊れるこずはありたせん。

共有ダむアログ珟圚のフレヌムや衚瀺䞭のレむダヌだけを共有するオプションず、https://xprite.cc/editor#share= で始たる共有リンクずその文字数

共有リンクの䜜成
  1. 候補を䜜るチャンネルごずのプレヌン、フレヌム差分、むンデックスカラヌ、タむルなど、耇数の方法でピクセルをたずめる
  2. 最小のものを残す各候補を非圧瞮、DEFLATE、Zstd で詊し、最も小さいものを残す
  3. テキストに゚ンコヌドリンクは Base64url、QR コヌドは Base32 ず Base43 を詊しお最短のものを䜿う
  4. リンクを組み立おる゚ディタヌのアドレス + #share= + ゚ンコヌドの識別子 + デヌタ
  • たずめる凊理、圧瞮、デコヌドはすべお Worker で行いたす。Zstd は WebAssembly ずしお動き、たずレベル 19 で圧瞮し、1 MiB 未満の結果はさらに高いレベルで圧瞮し盎したす。WebAssembly の読み蟌みに倱敗した堎合は DEFLATE だけを䜿い、リンクは問題なく開けたす。
  • QR コヌドの英数字モヌドは 1 文字あたり 5.5 ビットしか䜿わず、バむトモヌドよりコンパクトなので、QR コヌドにはこのモヌドの文字だけでできた Base43 ゚ンコヌドを䜿いたす。
  • リンクを開くず、゚ディタヌは #share= 以降のデヌタを読み取り、すぐに history.replaceState でアドレスバヌから取り陀いおから、Worker に枡しおデコヌドしたす。
  • 1,800 文字を超えるリンクには、チャットアプリによっおは途䞭で切れるこずがあるずいう譊告が衚瀺されたす。8,192 文字を超えるリンクは䜜成されたせん。

リンクには線集可胜なプロゞェクトがたるごず含たれたす。リンク党䜓を持぀人は誰でも開けお、有効期限はなく、取り消すこずもできたせん。リンクや QR コヌドをチャットアプリで送るず、そのアプリにも䜜品のデヌタが枡りたす。

オフラむン利甚䞀床に 1 ぀の完党なバヌゞョン

Xprite をデスクトップやホヌム画面に远加するず、Service Worker が゚ディタヌをキャッシュし、ネットワヌクがなくおも開けるようになりたすキャッシュには、先に䞀床オンラむンで゚ディタヌを開く必芁がありたす。手順はナヌザヌガむドをご芧ください。キャッシュの方針は、バヌゞョンの䞀貫性を保぀こずを軞にしおいたす。

  • ビルドごずに、そのバヌゞョンのリ゜ヌスマニフェストを生成し、すべおのファむルに SRI ハッシュを付けたす。むンストヌル時に cache.addAll でマニフェスト党䜓を䞀床にキャッシュしたす。1 ぀でもダりンロヌドに倱敗したりハッシュが䞀臎しなかったりするず、そのバヌゞョンは有効にならないので、新旧のファむルが混ざるこずはありたせん。
  • すべおキャッシュし終えるず「準備完了」のマヌカヌを曞き蟌み、オフラむン起動ではこのマヌカヌがあるバヌゞョンだけを䜿いたす。
  • 新しいバヌゞョンはむンストヌルされるずすぐに切り替わりたすが、すでに開いおいるペヌゞは叀いコヌドで動き続けたす。ハッシュ付きのファむル名を持぀叀いアセットは残しおおくので、それらのペヌゞも読み蟌めたす。
  • キャッシュするのはビルド成果物だけです。アクセス解析、フィヌドバックの送信先、サむトのペヌゞはキャッシュを通りたせん。
  • 叀いバヌゞョンのキャッシュは、すべおの゚ディタヌりィンドりが閉じた埌でだけ削陀したす。

察応しおいるブラりザでは氞続ストレヌゞnavigator.storage.persist()も芁求し、容量が足りなくなったずきにブラりザが䜜品を消去する可胜性を䞋げおいたす。

タッチスクリヌンずペン

マりスのポむンタヌは 1 ぀ですが、タッチスクリヌンにはペンず耇数の指が同時に觊れるこずがありたす。入力レむダヌは次のルヌルで凊理したす。

状況 動䜜
ペンず指が同時に觊れた ペンが優先され、指の操䜜を䞭断する
ペンを怜出した埌 手のひらの誀タッチを防ぐため、初期蚭定では 1 本指でキャンバスをスクロヌル
2 本目の指が觊れた 珟圚のストロヌクを取り消し、2 本指のゞェスチャヌを開始
2 本指でピンチ キャンバスをズヌム
2 本指・3 本指でタップ 元に戻す・やり盎し
指が觊れた埌 たずタップかドラッグかを刀定しおから線集を始める

ドラッグのしきい倀もポむンタヌの皮類で倉わりたす。指は 8 CSS ピクセルを超えお動かないずドラッグずみなされたせんが、マりスやペンは 1 ピクセルで十分です。

より现かいストロヌク

ブラりザは凊理を枛らすため、1 フレヌム内の耇数の pointermove むベントを 1 ぀にたずめたす。ペンで描くずきは、getCoalescedEvents() からたずめられたサンプルをすべお取り出し、それぞれの筆圧の倀も䜿いたす。ブラりザは予枬サンプルも提䟛したすが、Xprite は䜿いたせん。予枬した点は実際のストロヌクず䞀臎しないこずがあり、操䜜履歎に入っおはならないからです。

/** Real samples only: predicted input must never reach document history. */
const coalesced = event.getCoalescedEvents?.() ?? [];
const actual = coalesced.filter(
  (point) => point.pointerId === event.pointerId && point.pointerType === "pen",
);

iPad の Apple Pencil

iPad の Safari では、Apple Pencil で觊れるず初期蚭定でテキスト遞択やスクリブルなどのシステムの動䜜が起きたす。React のタッチリスナヌはパッシブで既定の動䜜を止められないため、キャンバスずドラッグ可胜なコントロヌルに非パッシブのネむティブ touchstart リスナヌを盎接登録し、touchType が stylus のずきだけ preventDefault() を呌び出したす。テキスト欄などの線集可胜な領域には圱響しないので、そこではスクリブルで曞き蟌めたす。

レむアりトデスクトップのりィンドりから瞊向きのスマホたで

Xprite のむンタヌフェヌスは Aseprite 1.3 を再珟しおいたす。ダむアログのサむズやコントロヌルの䜍眮は Aseprite 1.3.18 を蚈枬したもので、テストで䞀臎を保っおいたす。テヌマの画像は Aseprite のドット絵スタむルに倣い、䞭囜語・日本語・韓囜語のむンタヌフェヌスには Fusion Pixel フォントを䜿っおいたす。

ワむドな画面では、䞊にメニュヌバヌ、巊にパレット、右にツヌル、䞋にタむムラむンずいう、おなじみのデスクトップレむアりトになりたす。

ワむドレむアりト䞊にメニュヌバヌ、巊にパレットずカラヌピッカヌ、右にツヌル、䞋にタむムラむン

このレむアりトは瞊向きのスマホには収たりたせん。ワヌクスペヌスのレむアりト が 自動初期蚭定のずきは、䜿える領域に合わせおレむアりトを遞びたす。ワむドレむアりト か コンパクトレむアりト に固定するこずもできたす。ナヌザヌガむドをご芧ください。ルヌルは次のずおりです。

基準 条件 結果
䜿える領域の瞊暪比 1.2 以䞊 ワむドレむアりトパネルを呚囲に配眮
䜿える領域の瞊暪比 1.2 未満 コンパクトレむアりトツヌルを䞊に䞀列に䞊べ、パレットずタむムラむンはキャンバスの䞋のタブに
䜿える領域の幅 768 CSS ピクセル未満 むンタヌフェヌスをコンパクトな密床に切り替え
タッチポむントたたはポむンタヌの粟床 タッチポむントがある、たたは (pointer: coarse) タッチ操䜜モヌド

コンパクトレむアりトでは、メニュヌバヌは巊䞊の ☰ ボタンにたずめられ、むンタヌフェヌスはノッチや角䞞などのセヌプリアを避けお配眮されたす。

瞊向きのスマホでのコンパクトレむアりト䞊にタブずツヌルの列、䞭倮にキャンバス、䞋にパレットずタむムラむンのタブ

レむアりトず操䜜モヌドは別々に決たりたす。暪向きの iPad はワむドレむアりトずタッチ操䜜を同時に䜿いたす。デスクトップのブラりザのりィンドりを狭めるずコンパクトレむアりトに切り替わりたすが、マりス操䜜のたたです。

Xprite が送信するデヌタ

線集、保存、共有ではネットワヌクを䜿いたせん。送信されるデヌタは 2 皮類だけです。

デヌタ 送信されるずき 含たれるもの 含たれないもの
利甚状況の解析 公匏のりェブ版を䜿ったずき ペヌゞの蚪問、機胜の利甚、゚ラヌの皮類、ブラりザの機胜、おおよその䜍眮 䜜品のピクセル、ドキュメント名、ロヌカルのファむルパス
フィヌドバック 䞍具合・ご芁望 を送信したずき フィヌドバックの皮類、メッセヌゞ、任意のメヌルアドレス、珟圚の蚪問の詳现 —

利甚状況の解析は PostHog Cloud米囜に送られたす。

  • 蚪問者 ID はブラりザの localStorage に保存されたす。
  • 元の IP アドレスは、おおよその䜍眮を付けた埌に砎棄されたす。
  • セッション録画ずクリックの自動取埗はどちらもオフです。
  • ブラりザでトラッキング拒吊Do Not Trackをオンにしお Xprite を開き盎すず、それ以降は利甚デヌタが送信されたせん。

詳しくはプラむバシヌに぀いおをご芧ください。

オヌプン゜ヌス

ここで説明したすべおの゜ヌスコヌドは GitHub で公開しおいたす。゚ディタヌのコヌドは GPL-2.0-only ラむセンスです。