Excalidraw と Fugu: コア ユーザー ジャーニーの改善

十分に発達したテクノロジーは、魔法と見分けがつきません。理解できなければ。Google のデベロッパー リレーションズを担当している Thomas Steiner です。この Google I/O での講演を執筆するこのブログ投稿では、新しい Fugu API のいくつかと、それらが Excalidraw PWA の主要なユーザー ジャーニーをどのように改善するかについて説明します。これらのアイデアからヒントを得て、ご自身のアプリに適用してください。

Excalidraw に来た経緯

お話から始めましょう。2020 年 1 月 1 日に Facebook のソフトウェア エンジニアである Christopher Chedeau 氏は、 彼が持っていた小さな描画アプリについてツイート 取り組みを開始しました。このツールでは、漫画のような印象を与える箱や矢印を 作成します。翌日は、楕円やテキストを描画したり、オブジェクトを選択して できます。このアプリは 1 月 3 日に「Excalidraw」という名前に変わり、 プロジェクトでは、ドメイン名の購入はクリストファーの初期の行為の 1 つです。方法 色を使い、図形描画全体を PNG としてエクスポートできます。

長方形、矢印、省略記号、テキストがサポートされていることを示す Excalidraw プロトタイプ アプリケーションのスクリーンショット。

Christopher は 1 月 15 日、 ブログ投稿に Twitter でも 多くの注目を集めていますこの投稿では、

  • ユニーク アクティブ ユーザー数 1.2 万人
  • GitHub で星 1,500 個
  • 投稿者 26 人

わずか 2 週間前に開始したプロジェクトにとって、これはまったく悪くないでしょう。しかし、本当に 関心が高まっています。Christopher は、新しい方法を試したと書いています。 pull リクエストに到達したすべてのユーザーに無条件の commit アクセス権が付与されます。同じ日に このブログ投稿を読んで、私の pull リクエストを Excalidraw に File System Access API のサポートが追加され、 提出された機能リクエスト

PR を発表したツイートのスクリーンショット。

1 日後に pull リクエストがマージされ、その後は完全な commit アクセス権が付与されました。言うまでもなく、 私は自分の力を悪用しなかった。今のところ 149 名の投稿者は他にいません。

現在 Excalidraw は、インストール可能な本格的なプログレッシブ ウェブアプリとして オフライン サポート、見栄えの良いダークモードのほか、 File System Access API。

現在の Excalidraw PWA のスクリーンショット。

多くの時間をエクスカリドローに費やした理由についてリピスが語る

これで「Excalidraw に至った経緯」はこれで終わりです。詳しく見ていく前に、 Excalidraw の優れた機能に加え、Panayiotis をご紹介します。Panayiotis Lipiridis 氏、 インターネットは、単に lipis と名付けられており、 Excalidraw です。私は lipis に、エクスカリドローに多くの時間を費やす動機になった理由を尋ねました。

他の皆さんと同様に、私は Christopher のツイートからこのプロジェクトについて知りました。初めての投稿 Open Color Library を Excalidraw の一部となっています。プロジェクトが成長して多くのリクエストが寄せられるにつれ、 ユーザーが図形描画を共有できるように、図形描画を保存するためのバックエンドを構築しました。では、 Excalidraw をお試しになった人は、言い訳を探そうとしています。 もう一度クリックします。

私はリピに全面的に同意します。Excalidraw を試した人は誰でも、もう一度使う言い訳を模索しています。

Excalidraw の実例

Excalidraw の実際の使い方をご紹介します。私は素晴らしいアーティストではありませんが、 Google I/O のロゴはとてもシンプルなので、使ってみましょう。ボックスは「i」で、行は 「o」はです。Shift キーを押したままにすると、完璧な円が描画されます。体を動かしましょう 見栄えが良くなります。「i」の文字の色をあります。青色は良いです。どちらともいえない 別の塗りつぶしスタイルを使用できますか?すべてソリッドか、クロスハッチか?うわ、ハチュレはすごくいいね。完璧ではありませんが、 Excalidraw のアイデアなので保存しておきます。

保存アイコンをクリックし、ファイル保存ダイアログでファイル名を入力します。Chrome では、 File System Access API をサポートしています。これはダウンロードではなく、真の保存オペレーションです。 ファイルの場所と名前を選択します。編集が完了したら、 表示されます。

ロゴを変更し、「i」文字を赤色で表示されます。ここでもう一度 [Save] をクリックすると、 作成します。それでは、キャンバスをクリアしてファイルを再度開きます。ご覧のとおり 赤と青のロゴが変わっています。

ファイルを操作する

File System Access API を現在サポートしていないブラウザでは、各保存操作は ダウンロードするので、変更を加えると複数のファイルが ダウンロード フォルダいっぱいのファイル名。しかし、このようなデメリットにもかかわらず、ファイルを保存することはできます。

ファイルを開く

その秘訣は何でしょうか。さまざまなブラウザで作業を開いて保存する方法(作業の有無は問わない) File System Access API をサポートするにはどうすればよいでしょうか。Excalidraw でファイルを開くと、 loadFromJSON)()によって呼び出され、fileOpen() という関数が呼び出されます。

export const loadFromJSON = async (localAppState: AppState) => {
  const blob = await fileOpen({
    description: 'Excalidraw files',
    extensions: ['.json', '.excalidraw', '.png', '.svg'],
    mimeTypes: ['application/json', 'image/png', 'image/svg+xml'],
  });
  return loadFromBlob(blob, localAppState);
};

私が作成した小さなライブラリの fileOpen() 関数 browser-fs-access。 Excalidraw です。このライブラリは、サーバー モジュールを介して File System Access API と従来のフォールバックがあるため、あらゆる環境で利用可能 できます。

まず、API がサポートされる場合の実装について説明します。交渉が終わったら、 MIME タイプとファイル拡張子の 中心的な部分は File System Access API の呼び出しです 関数 showOpenFilePicker()。この関数は、ファイルの配列または単一のファイルを返します。 選択されるかどうかが決まりますあとは、ファイルにファイル ハンドルを設定するだけです。 再度取得できるようにします。

export default async (options = {}) => {
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  const handleOrHandles = await window.showOpenFilePicker({
    types: [
      {
        description: options.description || '',
        accept: accept,
      },
    ],
    multiple: options.multiple || false,
  });
  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
  if (options.multiple) return files;
  return files[0];
  const getFileWithHandle = async (handle) => {
    const file = await handle.getFile();
    file.handle = handle;
    return file;
  };
};

フォールバックの実装は、"file" 型の input 要素に依存します。交渉の結果 設定したら、次はプログラムで入力ボタンを [ファイルを開く] ダイアログが表示されます。変更時、つまりユーザーが 1 つまたは複数のコンテナを Promise は解決されます。

export default async (options = {}) => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    const accept = [
      ...(options.mimeTypes ? options.mimeTypes : []),
      options.extensions ? options.extensions : [],
    ].join();
    input.multiple = options.multiple || false;
    input.accept = accept || '*/*';
    input.addEventListener('change', () => {
      resolve(input.multiple ? Array.from(input.files) : input.files[0]);
    });
    input.click();
  });
};

ファイルを保存中

さあ、保存しましょう。Excalidraw では、saveAsJSON() という関数で保存を行います。最初に Excalidraw の要素配列を JSON にシリアル化し、JSON を blob に変換して fileSave() という関数を使用します。関数も同様に、UDM イベントで browser-fs-access ライブラリを使用します。

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: 'application/vnd.excalidraw+json',
  });
  const fileHandle = await fileSave(
    blob,
    {
      fileName: appState.name,
      description: 'Excalidraw file',
      extensions: ['.excalidraw'],
    },
    appState.fileHandle,
  );
  return { fileHandle };
};

前述したように、まず File System Access API をサポートするブラウザの実装を見てみましょう。「 最初の数行は少し複雑に見えますが、行うのは MIME タイプとファイルをネゴシエートすることだけです。 できます。以前に保存したファイル ハンドルがすでにある場合は、保存ダイアログは不要 表示されます。ただし、初めて保存する場合は、ファイル ダイアログが表示され、アプリはファイル ハンドルを取得します。 再使用できます。後はファイルへの書き込みだけです。 書き込み可能ストリーム

export default async (blob, options = {}, handle = null) => {
  options.fileName = options.fileName || 'Untitled';
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  handle =
    handle ||
    (await window.showSaveFilePicker({
      suggestedName: options.fileName,
      types: [
        {
          description: options.description || '',
          accept: accept,
        },
      ],
    }));
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
  return handle;
};

[名前を付けて保存]機能

既存のファイル ハンドルを無視する場合は、「名前を付けて保存」機能を実装できます。作成する特徴量のことです 作成することもできます。これを表示するには、既存のファイルを開いて、 既存のファイルを上書きせずに、save-ass ファイルを使用して 機能。これにより、元のファイルはそのまま残ります。

File System Access API をサポートしていないブラウザでは、すべての API 呼び出しが ここでは、目的のファイル名を値とする download 属性を持つアンカー要素を作成します。 href 属性値として blob URL を指定します。

export default async (blob, options = {}) => {
  const a = document.createElement('a');
  a.download = options.fileName || 'Untitled';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', () => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

その後、アンカー要素がプログラムによってクリックされます。メモリリークを防ぐために、blob URL には 使用後に取り消すことができます。これは単なるダウンロードであるため、ファイル保存ダイアログは表示されません。 デフォルトの Downloads フォルダに配置されます。

ドラッグ&ドロップ

パソコンでのシステム統合の中で気に入っているのがドラッグ&ドロップです。Excalidraw でテキストを .excalidraw ファイルをアプリケーションに追加すると、すぐに開き、編集を開始できます。ブラウザ ファイル システム アクセス API をサポートしている場合は、変更をすぐに保存することもできます。移動不要 必要なファイル ハンドルはドラッグ&ドロップで取得されているため、ファイル保存ダイアログの あります。

これを実現する秘訣は、getAsFileSystemHandle()を File System Access API がサポートされている場合のデータ転送項目。次に、これを渡します。 loadFromBlob() へのファイル ハンドル。これは前のいくつかの段落で説明したとおりです。多数 ファイルに対して行える操作(開く、保存する、保存しすぎる、ドラッグする、ドロップする)。同僚のピート これらすべてのコツなどについては、こちらの記事をご覧ください。 進捗があまりにもなかったかもしれません

const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
  this.setState({ isLoading: true });
  // Provided by browser-fs-access.
  if (supported) {
    try {
      const item = event.dataTransfer.items[0];
      file as any.handle = await item as any
        .getAsFileSystemHandle();
    } catch (error) {
      console.warn(error.name, error.message);
    }
  }
  loadFromBlob(file, this.state).then(({ elements, appState }) =>
    // Load from blob
  ).catch((error) => {
    this.setState({ isLoading: false, errorMessage: error.message });
  });
}

ファイルの共有

現在 Android、ChromeOS、Windows で利用されているもう一つのシステム統合は、 Web Share Target API。こちらは、Downloads フォルダの Files アプリです。私は 2 つのファイルを確認できます。そのうちの 1 つは、記述なしの名前が untitled で、タイムスタンプが 1 つのファイルです。Google Cloud の その他アイコンをクリックして [Share] を選択すると、 Excalidraw です。このアイコンをタップすると、ファイルに再び I/O ロゴのみが含まれていることが確認できます。

サポートが終了した Electron バージョンの Lipis

まだ説明していないファイルについては、そのファイルをダブルクリックしてもかまいません。通常、 ファイルが MIME タイプに関連付けられていることが確認された場合、 開きます。たとえば、.docx の場合は Microsoft Word です。

Excalidraw には、これまで サポートされているため、.excalidraw ファイルをダブルクリックすると、 Excalidraw Electron アプリが開きます。リピスは、これまでに会ったことのあるクリエイターで、 Excalidraw Electron のサポートを終了しました。なぜ移行を廃止できると考えるのか、 Electron のバージョン:

Electron アプリは当初から求められていました。 ダブルクリックしてファイルを開くことができます。また、アプリストアでの公開も予定していました。同時に、 代わりに PWA の作成を提案したため幸いなことに Project Fugu を紹介しました ファイル システム アクセス、クリップボード アクセス、ファイル操作などの API。ワンクリックで Electron を追加することなく、デスクトップまたはモバイルにアプリをインストールできます。簡単だった Electron バージョンのサポートを終了することを決定し 構築することを目指していますさらに、Google Play ストアと Microsoft 保管!すごい!

Excalidraw for Electron のサポートは終了していないと言えます。なぜなら Electron が悪いからであって、まったくないからです。 というのも、ウェブは今でも十分に進歩しているからです。気に入った!

ファイル処理

私が「ウェブは十分に進化した」と言ったのは、リリース予定のファイル 取り扱い。

これは通常の macOS Big Sur のインストールです。では、右クリックするとどうなるか見てみましょう。 Excalidraw ファイル。インストール済みの PWA である Excalidraw で開くこともできます。もちろん ダブルクリックでも操作できますが、スクリーンキャストでデモを行う方がドラマチックではありません。

では、その仕組みについて説明します。最初のステップは、アプリケーションが処理できるファイル形式を 自動的に作成されます。これは、ウェブアプリ マニフェストの file_handlers という新しいフィールドで行います。その value は、アクションと accept プロパティを持つオブジェクトの配列です。アクションによって URL が オペレーティング システムがアプリを起動するパスと accept オブジェクトは、MIME の Key-Value ペアです。 種類と関連するファイル拡張子を指定します。

{
  "name": "Excalidraw",
  "description": "Excalidraw is a whiteboard tool...",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "file_handlers": [
    {
      "action": "/",
      "accept": {
        "application/vnd.excalidraw+json": [".excalidraw"]
      }
    }
  ]
}

次のステップでは、アプリケーションの起動時にファイルを処理します。これは launchQueue で発生します。 このインターフェースでは、setConsumer() を呼び出してコンシューマを設定する必要があります。このパラメータに、 関数は、launchParams を受け取る非同期関数です。この launchParams オブジェクト files というフィールドで、操作するファイル ハンドルの配列を取得します。私が気にしているのは 1 つ目のファイル ハンドルから blob を取得し、 loadFromBlob()

if ('launchQueue' in window && 'LaunchParams' in window) {
  window as any.launchQueue
    .setConsumer(async (launchParams: { files: any[] }) => {
      if (!launchParams.files.length) return;
      const fileHandle = launchParams.files[0];
      const blob: Blob = await fileHandle.getFile();
      blob.handle = fileHandle;
      loadFromBlob(blob, this.state).then(({ elements, appState }) =>
        // Initialize app state.
      ).catch((error) => {
        this.setState({ isLoading: false, errorMessage: error.message });
      });
    });
}

処理が早すぎる場合は、File Handling API の詳細を こちらをご覧ください。ファイル処理を有効にするには、試験運用版のウェブ プラットフォームを設定します。 使用します。この機能は今年後半に Chrome で利用可能になる予定です。

クリップボードの統合

Excalidraw のもう一つの優れた機能は、クリップボードの統合です。図形描画全体をコピーするか、 必要に応じて透かしを入れて クリックします。ちなみに、これは Windows 95 Paint アプリの Web バージョンです。

仕組みは驚くほど簡単です。必要なのは blob としてのキャンバスだけです。これを ClipboardItem を含む 1 つの要素の配列と blob を navigator.clipboard.write() 関数を使用します。クリップボードの用途について詳しくは、 Jason と私の記事をご覧ください。

export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
  const blob = await canvasToBlob(canvas);
  await navigator.clipboard.write([
    new window.ClipboardItem({
      'image/png': blob,
    }),
  ]);
};

export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => {
        if (!blob) {
          return reject(new CanvasError(t('canvasError.canvasTooBig'), 'CANVAS_POSSIBLY_TOO_BIG'));
        }
        resolve(blob);
      });
    } catch (error) {
      reject(error);
    }
  });
};

他のユーザーとのコラボレーション

セッション URL の共有

Excalidraw にも共同編集モードがあることをご存じですか?異なる複数のユーザーが 作成する必要があります。新しいセッションを開始するには、[リアルタイムのコラボレーション] ボタンをクリックして あります。セッションの URL を共同編集者と簡単に共有できるのは、 Excalidraw が統合している Web Share API

ライブ コラボレーション

Google Pixelbook の Google I/O ロゴを操作して、ローカルでコラボレーション セッションをシミュレートしました。 Google Pixel 3a と iPad Pro です1 つのデバイスで行った変更は 接続します

すべてのカーソルが動いているのがわかります。Google Pixelbook のカーソルは制御されており、ゆっくりと動く Google Pixel 3a のカーソルと iPad Pro のタブレットのカーソルがあちこちに移動するのは、 指でタップしてこれらのデバイスを操作

共同編集者のステータスを表示する

リアルタイムのコラボレーション エクスペリエンスを向上させるために、アイドル状態の検出システムも稼働させています。 iPad Pro を使用していると、カーソルに緑色のドットが表示されます。Google Chat の設定に切り替えると、 クリックします。Excalidraw アプリを開いて何もしていないと、 カーソルがアイドル状態であることを示し、3 つの zZZ で表されています。

当社の出版物を熱心に読んでいる人は、アイドル状態の検出は Idle Detection API は、 コンテキストです。事実ではありません。この API に基づく実装もありましたが 最終的には ポインタの移動やページの表示など

WICG アイドル検出リポジトリに提出されたアイドル検出のフィードバックのスクリーンショット。

Idle Detection API が必要な理由に関するフィードバックを提出しました 解決できませんでした。Project Fugu API はすべてオープンソースで開発されているため、 参加すれば、皆の意見を聞くことができます。

エクスカリドローの妨げとなっている理由についてリピス

それについて、私は lipis に、ウェブに欠けていると思われるものについて最後にもう一つ質問しました。 Excalidraw の例をご紹介します。

File System Access API は便利ですが、最近注目しているほとんどのファイル Dropbox や Google ドライブに保管されていますFile System Access API で Dropbox や Google などのリモートファイルシステムプロバイダが 開発者がコーディングに使える さまざまな機能も備えていますユーザーはリラックスしてファイルが安全であることを確認できる 信頼の置けるクラウド プロバイダと契約することです。

私は lipis に全面的に同意します。私もクラウドに住んでいます。これが実装されることを期待しています 提供する予定です。

タブ付きアプリケーション モード

結果を確認しましょう。Excalidraw では、数多くの優れた API 統合が確認されています。 ファイル システムファイル処理クリップボードウェブ共有ウェブ共有ターゲット。しかし、もう 1 つあります。これまでは 一度に 1 つのドキュメントを編集できます。今はそうではありません。以前のバージョンの Excalidraw のタブ付きアプリケーション モード。このように表示されます

スタンドアロン モードで実行しているインストール済みの Excalidraw PWA で既存のファイルを開いています。現在 スタンドアロン ウィンドウで新しいタブを開きます。これは通常のブラウザタブではなく、PWA のタブです。この 新しいタブでセカンダリ ファイルを開き、同じアプリ ウィンドウから独立して作業できます。

タブ形式アプリケーション モードは初期段階にあり、すべての機能が変更されるわけではありません。もし ご興味がある場合は、 こちらをご覧ください。

結びの言葉

この機能やその他の機能に関する最新情報を入手するには、 Fugu API トラッカー。私たちはウェブを進化させて プラットフォームでできることの幅が広がります。進化を続ける Excalidraw を お届けします 優れたアプリケーションを開発できます。作成を開始: excalidraw.com.

本日ご紹介した API が、皆様のアプリにポップアップ表示されるのを楽しみにしています。Tom と申します。 私は Twitter やインターネット全般で @tomayac として見つかります。 ご視聴ありがとうございました。引き続き Google I/O もお楽しみください。