Excalidraw et Fugu: améliorer le parcours utilisateur principal

Toute technologie suffisamment avancée est indissociable de la magie. À moins que vous ne compreniez. Je m'appelle Thomas Steiner et je travaille au service Relations avec les développeurs chez Google. Dans ce compte-rendu de ma conférence Google I/O, j'aborderai certaines des nouvelles API Fugu et comment elles améliorent les parcours utilisateur dans la PWA Excalidraw. Vous pourrez ainsi vous inspirer de ces idées et les appliquer à vos propres applications.

Comment je suis arrivé chez Excalidraw

Je veux commencer par une histoire. Le 1er janvier 2020, Christopher Chedeau, ingénieur logiciel chez Facebook, a publié un tweet à propos d'une petite application de dessin qu'il avait a commencé à travailler. Avec cet outil, vous pourriez dessiner des cases et des flèches qui semblent caricaturales et dessinées à la main. Le lendemain, vous pouvez également dessiner des points de suspension et du texte, et sélectionner des objets et vous déplacer. autour d'eux. Le 3 janvier, l'application s'appelait Excalidraw. l'achat du nom de domaine a été l'un des premiers projets de Christopher. Par vous pouvez utiliser des couleurs et exporter l'ensemble du dessin au format PNG.

Capture d'écran de l'application de prototype Excalidraw montrant qu'elle prend en charge les rectangles, les flèches, les ellipses et le texte.

Le 15 janvier, Christopher a publié un article de blog qui a dessiné un beaucoup d'attention sur Twitter, y compris le mien. Au départ, le post contenait des statistiques impressionnantes:

  • 12 000 utilisateurs actifs uniques
  • 1,5 k étoiles sur GitHub
  • 26 contributeurs

Pour un projet qui a commencé il y a à peine deux semaines, ce n'est pas mal du tout. Mais la chose qui vraiment a considérablement augmenté, mon intérêt était plus bas dans le post. Christopher a écrit qu'il a essayé quelque chose de nouveau : L'octroi d'un accès commit inconditionnel à tous ceux qui ont envoyé une demande d'extraction Le même jour, en lisant l'article de blog, j'avais une demande d'extraction qui a ajouté la compatibilité de l'API File System Access avec Excalidraw, ce qui corrige un demande de fonctionnalité déposée par un utilisateur.

Capture d'écran du tweet dans lequel j'annonce ma communication de presse.

Ma demande d'extraction a été fusionnée un jour plus tard et, à partir de là, j'avais un accès complet au commit. Bien entendu, Je n'ai pas utilisé mon pouvoir. pas plus que personne d'autre parmi les 149 contributeurs jusqu'à présent.

Aujourd'hui, Excalidraw est une progressive web app installable complète qui inclut mode hors connexion, mode sombre époustouflant, et possibilité d'ouvrir et d'enregistrer des fichiers API File System Access.

Capture d'écran de la PWA Excalidraw dans sa version actuelle.

Lipis explique pourquoi il consacre autant de son temps à Excalidraw

C'est la fin de mon parcours sur Excalidraw. histoire, mais avant de me plonger dans certains Grâce aux incroyables fonctionnalités d'Excalidraw, j'ai le plaisir de vous présenter Panayiotis. Panayiotis Lipiridis, sur Internet, tout simplement appelé lipis, est le contributeur le plus prolifique de Excalidraw. J'ai demandé à Lipis ce qui le motive à consacrer autant de son temps à Excalidraw:

Comme tout le monde, j'ai appris ce projet grâce au tweet de Christopher. Ma première contribution était d'ajouter la bibliothèque de couleurs ouvertes, les couleurs qui sont toujours fait partie d'Excalidraw aujourd'hui. Au fur et à mesure que le projet se développait et que nous avions reçu de nombreuses demandes, a été de créer un backend pour stocker les dessins afin que les utilisateurs puissent les partager. Mais que c'est que quelqu'un qui a essayé Excalidraw cherche des excuses à nouveau.

Je suis tout à fait d'accord avec lipis. Quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.

Excalidraw en action

Maintenant, je veux vous montrer comment vous pouvez utiliser Excalidraw en pratique. Je ne suis pas une grande artiste, Le logo Google I/O est assez simple, alors laissez-moi essayer. Un rectangle est le "i", une ligne peut être le et le "o" est un cercle. Je maintiens la touche Maj enfoncée pour obtenir un cercle parfait. Je veux bouger un peu la barre oblique pour que le résultat soit plus agréable. Maintenant, de la couleur pour le « i » et le "o". Le bleu, c'est bien. Peut-être un style de remplissage différent ? Tous pleins ou hachurés ? Non, hachure a l'air super. Ce n'est pas parfait, mais C'est le concept d'Excalidraw. Laissez-moi l'enregistrer.

Je clique sur l'icône d'enregistrement et je saisis un nom de fichier dans la boîte de dialogue d'enregistrement de fichier. Dans Chrome, un navigateur qui prend en charge l'API File System Access, il ne s'agit pas d'un téléchargement, mais d'une véritable opération d'enregistrement, où je peux choisir l'emplacement et le nom du fichier. Si j'effectue des modifications, il me suffit de les enregistrer même fichier.

Permettez-moi de changer le logo et de faire en sorte que le « i » rouge. Si je clique à nouveau sur "Enregistrer", ma modification est enregistrée dans le même fichier qu'avant. À titre de preuve, laissez-moi effacer la toile et rouvrir le fichier. Comme vous pouvez le voir, le logo rouge-bleu modifié est là à nouveau.

Utiliser des fichiers

Dans les navigateurs qui ne prennent actuellement pas en charge l'API File System Access, chaque opération d'enregistrement est une télécharger. Ainsi, lorsque j'apporte des modifications, je me retrouve avec plusieurs fichiers dont le numéro est incrémenté dans le de fichiers qui remplissent mon dossier Téléchargements. Mais malgré cet inconvénient, je peux toujours enregistrer le fichier.

Ouverture des fichiers

Alors, quel est le secret ? Comment l'ouverture et l'enregistrement peuvent-ils fonctionner dans différents navigateurs ? prend-il en charge l'API File System Access ? L'ouverture d'un fichier dans Excalidraw s'effectue dans une fonction appelée loadFromJSON)(), qui à son tour appelle une fonction appelée 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);
};

La fonction fileOpen() qui provient d'une petite bibliothèque que j'ai écrite et qui s'appelle browser-fs-access que nous utilisons Excalidraw. Cette bibliothèque fournit un accès au système de fichiers via le l'API File System Access avec une ancienne solution de secours, qui peut être utilisée dans n'importe quel type navigateur.

Commençons par vous montrer comment implémenter l'API lorsque celle-ci est prise en charge. Après la négociation des les types et extensions de fichiers MIME acceptés, la partie centrale est celle qui appelle l'API File System Access fonction showOpenFilePicker(). Cette fonction renvoie un tableau de fichiers ou un seul fichier, en fonction de la sélection de plusieurs fichiers. Il ne vous reste plus qu'à mettre la poignée de fichier sur le fichier afin de pouvoir le récupérer à nouveau.

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;
  };
};

L'implémentation de remplacement repose sur un élément input de type "file". Après la négociation de les types et extensions MIME à accepter, l'étape suivante consiste à programmer un clic sur l'entrée pour que la boîte de dialogue d'ouverture de fichier s'affiche. Lors d'une modification, c'est-à-dire lorsque l'utilisateur en a sélectionné une ou plusieurs fichiers, la promesse est résolue.

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();
  });
};

Enregistrement de fichiers

Passons à l'enregistrement. Dans Excalidraw, l'enregistrement s'effectue dans une fonction appelée saveAsJSON(). Tout d'abord sérialise le tableau d'éléments Excalidraw au format JSON, le convertit en blob, puis appelle un appelée fileSave(). Cette fonction est également fournie par 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 };
};

Commençons par examiner l'implémentation des navigateurs compatibles avec l'API File System Access. La les premières lignes semblent un peu compliquées, mais il suffit de négocier les types MIME et les fichiers . Lorsque j'ai déjà effectué un enregistrement et que j'ai déjà un handle de fichier, aucune boîte de dialogue d'enregistrement n'est nécessaire affichés. S'il s'agit du premier enregistrement, une boîte de dialogue de fichier s'affiche et l'application obtient un handle de fichier. pour une utilisation ultérieure. Le reste se contente d'écrire dans le fichier, ce qui s'effectue via une flux accessible en écriture.

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;
};

L'option "Enregistrer sous" fonctionnalité

Si je décide d'ignorer un handle de fichier existant, je peux implémenter une option "Enregistrer sous" pour créer à partir d'un fichier existant. Pour vous montrer cela, laissez-moi ouvrir un fichier existant, faire quelques modification, mais ne pas écraser le fichier existant, mais créer un nouveau fichier à l'aide de la fonction . Le fichier d'origine reste ainsi intact.

L'implémentation pour les navigateurs qui ne prennent pas en charge l'API File System Access est courte, car elle crée un élément d'ancrage avec un attribut download dont la valeur correspond au nom de fichier souhaité et une URL de blob en tant que valeur d'attribut href.

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();
};

L'élément d'ancrage reçoit alors un clic programmatique. Pour éviter les fuites de mémoire, l'URL du blob doit être être révoqués après utilisation. Comme il ne s'agit que d'un téléchargement, aucune boîte de dialogue d'enregistrement de fichier ne s'affiche. se trouvent dans le dossier Downloads par défaut.

Glisser-déposer

L'une de mes intégrations système préférées sur ordinateur est le glisser-déposer. Dans Excalidraw, lorsque je place .excalidraw dans l'application, il s'ouvre immédiatement et je peux commencer à apporter des modifications. Sur les navigateurs qui prennent en charge l'API File System Access, je peux même enregistrer immédiatement mes modifications. Pas besoin d'y aller via une boîte de dialogue d'enregistrement de fichier, car la poignée de fichier requise a été obtenue par glisser-déposer opération.

Le secret pour y parvenir est d'appeler getAsFileSystemHandle() sur la élément de transfert de données lorsque l'API File System Access est compatible. Je transmets ensuite ceci vers loadFromBlob(), dont vous vous souvenez peut-être grâce aux paragraphes ci-dessus. Beaucoup ce que vous pouvez faire avec les fichiers: ouvrir, enregistrer, trop économiser, glisser-déposer. Mon collègue Pete Toutes ces astuces et bien d'autres sont expliquées dans cet article. Vous pourrez ainsi rattraper au cas où tout cela allait un peu trop vite.

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 });
  });
}

Partager des fichiers

Une autre intégration système actuellement sur Android, ChromeOS et Windows est la suivante : API Web Share Target : Me voici dans l'application Fichiers de mon dossier Downloads. Je peut afficher deux fichiers, l'un avec le nom non descriptif untitled et un code temporel. Pour savoir je clique sur les trois points, puis je partage, et l'une des options qui s'affiche est Excalidraw. Lorsque j'appuie sur l'icône, je peux voir que le fichier contient à nouveau le logo E/S.

Lipis sur la version obsolète de Electron

Une chose que vous pouvez faire avec les fichiers dont je n'ai pas encore parlé, c'est doubleclick. Quel est qui se produit lorsque vous doublez un fichier, c'est que l'application associée au type MIME du fichier s'ouvre. Par exemple, il s'agit de Microsoft Word pour .docx.

Excalidraw avait auparavant une version Electron de l'application qui prend en charge ce type d'association. Ainsi, lorsque vous double-cliquez sur un fichier .excalidraw, le L'application Excalidraw Electron s'ouvre alors. Lipis, que vous avez déjà rencontrée, était à la fois le créateur et le déprécateur d'Excalidraw Electron. Je lui ai demandé pourquoi il pensait qu'il était possible d'abandonner Version Electron:

Depuis le début, les utilisateurs nous ont demandé une application Electron, principalement parce qu'ils souhaitaient ouvrir des fichiers en double-cliquant. Nous avions également l'intention de proposer l'application sur des plates-formes de téléchargement d'applications. En parallèle, une personne nous avons suggéré de créer une PWA. Nous avons donc fait les deux. Heureusement, nous avons découvert le projet Fugu API comme l'accès au système de fichiers, l'accès au presse-papiers, la gestion de fichiers, etc. D'un simple clic, vous pouvez installer l'application sur votre ordinateur de bureau ou votre mobile, sans le poids supplémentaire de la technologie Electron. C'était un jeu d'enfant la décision d'abandonner la version Electron, de se concentrer uniquement sur l'application Web et d'en faire la la meilleure PWA possible. De plus, nous pouvons désormais publier des PWA sur le Play Store et sur le site Magasin ! C'est énorme !

On pourrait dire qu'Excalidraw pour Electron n'était pas obsolète, car Electron est mauvais, pas du tout, mais parce que le Web devient suffisamment bon. J'aime ça !

Gestion de fichiers

Quand je dis que le Web est devenu assez bon, c'est grâce à des fonctionnalités comme la prochaine Manipulation.

Il s'agit d'une installation standard de macOS Big Sur. Voyons maintenant ce qui se passe lorsque je fais un clic droit Fichier Excalidraw. Je peux choisir de l'ouvrir avec Excalidraw, la PWA installée. Bien entendu, double-cliquer fonctionnerait également, il est tout simplement moins spectaculaire de le démontrer dans un enregistrement d'écran.

Comment cela fonctionne-t-il ? La première étape consiste à indiquer les types de fichiers que mon application peut gérer le système d'exploitation. Je le fais dans un nouveau champ appelé file_handlers dans le fichier manifeste de l'application Web. Son "value" est un tableau d'objets avec une action et une propriété accept. L'action détermine l'URL chemin sur lequel le système d'exploitation lance votre application et l'objet "accepte" sont des paires clé/valeur de et les extensions de fichier associées.

{
  "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"]
      }
    }
  ]
}

L'étape suivante consiste à gérer le fichier au lancement de l'application. Cela se produit dans les launchQueue où je dois définir un client en appelant setConsumer(). Le paramètre est une fonction asynchrone qui reçoit l'launchParams. Cet objet launchParams a un champ appelé fichiers qui me donne un tableau de descripteurs de fichier avec lesquels travailler. Je ne me soucie que des d'abord et à partir de ce gestionnaire de fichiers, j'obtiens un blob que je transmets ensuite à notre vieil ami. 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 });
      });
    });
}

Encore une fois, si cela est allé trop vite, vous pouvez en savoir plus sur l'API File Handling dans mon article. Vous pouvez activer la gestion des fichiers en définissant la plate-forme Web expérimentale de fonctionnalités. Elle devrait être disponible dans Chrome dans le courant de l'année.

Intégration du presse-papiers

Une autre fonctionnalité intéressante d'Excalidraw est l'intégration du presse-papiers. Je peux copier tout mon dessin ou dans le presse-papiers, en ajoutant par exemple un filigrane et en le collant dans une autre application. Il s'agit d'une version Web de l'application Windows 95 Paint.

Le fonctionnement est étonnamment simple. Tout ce dont j'ai besoin, c'est le canevas comme blob, que j'écris ensuite dans le presse-papiers en transmettant un tableau à un élément avec un élément ClipboardItem avec le blob au fonction navigator.clipboard.write(). Pour en savoir plus sur ce que vous pouvez faire avec le presse-papiers consultez Jason et mon article.

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);
    }
  });
};

Collaboration avec d'autres personnes

Partager une URL de session

Saviez-vous qu'Excalidraw dispose également d'un mode collaboratif ? Différentes personnes peuvent collaborer sur sur le même document. Pour démarrer une nouvelle session, je clique sur le bouton de collaboration en direct, session. Je peux partager facilement l'URL de la session avec mes collaborateurs grâce au l'API Web Share intégrée par Excalidraw.

Collaboration en direct

J'ai simulé une session de collaboration localement en travaillant sur le logo Google I/O sur mon Pixelbook. mon téléphone Pixel 3a et mon iPad Pro. Vous pouvez voir que les modifications que j'apporte sur un appareil sont répercutées sur sur tous les autres appareils.

Je peux même voir tous les curseurs se déplacer. Le curseur du Pixelbook se déplace de façon régulière, car il est contrôlé par un pavé tactile, mais le curseur du téléphone Pixel 3a et celui de la tablette de l'iPad Pro sautent, car je contrôler ces appareils en tapotant avec mon doigt.

Affichage de l'état des collaborateurs

Pour améliorer l'expérience de collaboration en temps réel, un système de détection de l'inactivité est même en cours d'exécution. Le curseur de l'iPad Pro affiche un point vert lorsque je l'utilise. Le point devient noir lorsque je passe un autre onglet du navigateur ou une autre application. Dans l'application Excalidraw, mais sans rien faire, le curseur m'indique que je suis inactif, symbolisé par les trois « zZZ ».

Les lecteurs assidus de nos publications seront peut-être enclins à penser que la détection des inactivités se concrétise par le biais l'API Idle Detection, une proposition en phase de démarrage sur laquelle nous avons travaillé dans le du projet Fugu. Alerte spoiler: non. Notre implémentation était basée sur cette API, dans Excalidraw, mais nous avons finalement décidé d'adopter une approche plus traditionnelle, basée sur la mesure le déplacement du pointeur et la visibilité de la page.

Capture d&#39;écran des commentaires sur la détection d&#39;inactivité enregistrés dans le dépôt WICG Idle Detection Repo.

Nous avons envoyé des commentaires sur les raisons pour lesquelles l'API Idle Detection ne résolvait pas notre cas d'utilisation. Toutes les API du projet Fugu sont développées à l'air libre, donc tout le monde peut intervenir et se faire entendre !

Lipis parle de ce qui freine Excalidraw

À ce propos, j'ai posé une dernière question à Lipis concernant ce qu'il pense qu'il manque sur le Web. plate-forme qui freine Excalidraw:

L'API File System Access est géniale, mais vous savez quoi ? La plupart des fichiers qui m'intéressent ces jours-ci en ligne dans mon compte Dropbox ou Google Drive, et non sur mon disque dur. J'aimerais que l'API File System Access inclure une couche d'abstraction pour permettre aux fournisseurs de systèmes de fichiers distants comme Dropbox ou Google d'intégrer que les développeurs peuvent utiliser pour coder. Les utilisateurs peuvent alors se détendre et savoir que leurs fichiers sont en sécurité. avec le fournisseur de services cloud de confiance.

Je suis tout à fait d'accord avec lipis, je vis aussi dans le cloud. J'espère que cela sera implémenté très bientôt.

Mode d'application à onglets

Impressionnant ! Nous avons vu un grand nombre d'intégrations d'API très intéressantes dans Excalidraw. Système de fichiers, gestion de fichiers presse-papiers, partage sur le Web et cible de partage Web. Mais encore une chose. Jusqu'à présent, je pouvais seulement modifier un document à la fois. Plus vraiment. Pour la première fois, nous vous invitons à découvrir avec onglets dans Excalidraw. Voici à quoi cela ressemble.

J'ai ouvert un fichier existant dans la PWA Excalidraw installée, qui s'exécute en mode autonome. Maintenant J'ouvre un nouvel onglet dans la fenêtre autonome. Il ne s'agit pas d'un onglet de navigateur classique, mais d'un onglet PWA. Dans ce nouvel onglet. Je peux ensuite ouvrir un fichier secondaire et travailler dessus indépendamment de la même fenêtre de l'application.

Le mode application par onglets n'en est qu'à ses débuts, et tout n'est pas figé. Si vous utilisez vous pouvez vous renseigner sur l'état actuel de cette fonctionnalité mon article.

Conclusion

Pour vous tenir informé de l'évolution de cette fonctionnalité et d'autres, visionnez notre Outil de suivi de l'API Fugu : Nous sommes très enthousiastes à l'idée de faire avancer le Web et vous permettent d'en faire plus sur la plate-forme. Un Excalidraw qui ne cesse de s'améliorer, et tous des applications incroyables que vous allez créer. Commencez à créer à excalidraw.com.

J'ai hâte de voir apparaître certaines des API que j'ai présentées aujourd'hui dans vos applications. Je m’appelle Tom, vous pouvez me retrouver sous le nom @tomayac sur Twitter et Internet en général. Merci de votre attention, et bonne continuation de Google I/O.