Excalidraw et Fugu: améliorer le parcours utilisateur principal

Toute technologie suffisamment avancée est indiscernable de la magie. À moins que vous ne les compreniez. Je m'appelle Thomas Steiner et je travaille dans l'équipe chargée des relations avec les développeurs chez Google. Dans cet article de ma présentation Google I/O, je vais vous présenter certaines des nouvelles API Fugu et la façon dont elles améliorent les principaux 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é à Excalidraw

Je veux commencer par une histoire. Le 1er janvier 2020, Christopher Chedeau, ingénieur logiciel chez Facebook, a tweeté au sujet d'une petite application de dessin sur laquelle il a commencé à travailler. Avec cet outil, vous pouvez dessiner des cases et des flèches qui ressemblent à des dessins animés et dessinés à la main. Le lendemain, vous pouvez également dessiner des ellipses et du texte, mais aussi sélectionner des objets et les déplacer. Le 3 janvier, l'application s'appelait Excalidraw et, comme pour tout bon projet parallèle, l'achat du nom de domaine était l'un des premiers actes de Christophe. À présent, vous pouvez utiliser des couleurs et exporter l'intégralité du dessin au format PNG.

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

Le 15 janvier, Christopher a publié un article de blog qui a beaucoup attiré l'attention sur Twitter, y compris le mien. Ce post commençait par 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 seulement deux semaines, ce n'est pas mal du tout. Mais la chose qui a vraiment suscité mon intérêt était plus en dessous dans l'article. Christopher a écrit qu'il a essayé quelque chose de nouveau cette fois: accorder à tous les utilisateurs qui ont reçu une demande d'extraction un accès inconditionnel à un engagement. Le jour même de la lecture de l'article de blog, j'ai reçu une demande d'extraction qui ajoutait la prise en charge de l'API File System Access à Excalidraw, résolvant ainsi une demande de fonctionnalité envoyée par un autre utilisateur.

Capture d'écran du tweet dans lequel j'annonce mes relations publiques

Ma demande d'extraction a été fusionnée un jour plus tard et, à partir de là, j'avais un accès complet à la validation. Inutile de dire que je n'ai pas abusé de mon pouvoir. Aucun autre utilisateur parmi les 149 contributeurs à ce jour.

Aujourd'hui, Excalidraw est une progressive web app installable à part entière qui fonctionne hors connexion et offre un mode sombre époustouflant. Elle permet d'ouvrir et d'enregistrer des fichiers grâce à l'API File System Access.

Capture d'écran de la PWA Excalidraw dans l'état actuel.

Lipis explique pourquoi il consacre tant de temps à Excalidraw

C'est ainsi la fin de mon histoire sur "comment je suis arrivé à Excalidraw", mais avant de découvrir quelques fonctionnalités incroyables d'Excalidraw, j'ai le plaisir de vous présenter Panayiotis. Panayiotis Lipiridis, sur Internet simplement connu sous le nom de lipis, est le contributeur le plus prolifique d'Excalidraw. Je lui ai demandé ce qui le motive à consacrer tant de temps à Excalidraw:

Comme tout le monde, j'ai découvert ce projet grâce au tweet de Christopher. Ma première contribution a été d'ajouter la bibliothèque Open Color, les couleurs qui font encore partie d'Excalidraw à l'heure actuelle. Au fur et à mesure que le projet se développe et que nous recevons de nombreuses demandes, ma prochaine contribution majeure a été de créer un backend pour le stockage des dessins afin que les utilisateurs puissent les partager. Mais ce qui me motive vraiment, c'est que celui qui a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.

Je suis entièrement d'accord avec Lipis. Qui a essayé Excalidraw ? cherche des excuses pour l'utiliser à nouveau.

Excalidraw en action

Je veux vous montrer maintenant comment utiliser Excalidraw dans la pratique. Je ne suis pas un grand artiste, mais le logo Google I/O est assez simple, alors laissez-moi essayer. Une boîte est le « i », une ligne peut être la barre oblique et le « o » est un cercle. Je maintiens la touche Maj enfoncée pour obtenir un cercle parfait. Laissez-moi déplacer un peu la barre oblique pour qu'elle ait l'air mieux. Maintenant, un peu de couleur pour le « i » et le « o ». Le bleu, c'est bien. Peut-être un style de remplissage différent ? Tous pleins ou à hachures croisées ? Non, Hachure a l'air super. Ce n'est pas parfait, mais c'est le principe d'Excalidraw, alors je vais 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. Dans Chrome, un navigateur compatible avec 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, et où, si j'apporte des modifications, je peux simplement les enregistrer dans le même fichier.

Laissez-moi changer le logo et rendre le « i » rouge. Si je clique à nouveau sur "Save" (Enregistrer), ma modification est enregistrée dans le même fichier qu'auparavant. Pour preuve, laissez-moi vider la toile et rouvrir le fichier. Comme vous pouvez le voir, le logo rouge-bleu modifié est à nouveau là.

Utiliser des fichiers

Dans les navigateurs qui ne sont actuellement pas compatibles avec l'API File System Access, chaque opération d'enregistrement est un téléchargement. Par conséquent, lorsque j'effectue des modifications, plusieurs fichiers s'affichent avec un numéro incrémentiel dans le nom de fichier qui remplit mon dossier "Téléchargements". Mais malgré cet inconvénient, je peux toujours enregistrer le fichier.

Ouvrir des fichiers

Alors, quel est le secret ? Comment l'ouverture et l'enregistrement peuvent-ils fonctionner dans différents navigateurs compatibles ou non avec l'API File System Access ? L'ouverture d'un fichier dans Excalidraw s'effectue dans une fonction appelée loadFromJSON)(, qui appelle à son tour 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 appelée browser-fs-access, que nous utilisons dans Excalidraw. Cette bibliothèque fournit un accès au système de fichiers via l'API File System Access avec une ancienne solution de remplacement, ce qui permet de l'utiliser dans n'importe quel navigateur.

Commençons par vous montrer comment implémenter quand l'API sera prise en charge. Après avoir négocié les types MIME et les extensions de fichier acceptés, l'étape centrale consiste à appeler la fonction showOpenFilePicker() de l'API File System Access. Cette fonction renvoie un tableau de fichiers ou un fichier unique, selon que plusieurs fichiers sont sélectionnés ou non. Il ne vous reste plus qu'à placer le handle de fichier sur l'objet fichier, afin qu'il puisse être récupéré à 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 la création de remplacement repose sur un élément input de type "file". Après la négociation des types et des extensions MIME à accepter, l'étape suivante consiste à cliquer par programmation sur l'élément d'entrée pour afficher la boîte de dialogue d'ouverture de fichier. Lors d'une modification, c'est-à-dire lorsque l'utilisateur a sélectionné un 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(). Elle commence par sérialiser le tableau d'éléments Excalidraw au format JSON, le convertit en blob, puis appelle une fonction appelée fileSave(). Cette fonction est également fournie par la bibliothèque 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 };
};

Voyons d'abord comment implémenter les navigateurs compatibles avec l'API File System Access. Les deux premières lignes semblent un peu complexes, mais elles permettent simplement de négocier les types MIME et les extensions de fichier. Si j'ai enregistré et que j'ai déjà un handle de fichier, aucune boîte de dialogue d'enregistrement n'a besoin d'être affichée. Toutefois, s'il s'agit du premier enregistrement, une boîte de dialogue de fichier s'affiche, et l'application récupère un handle de fichier pour une utilisation ultérieure. Le reste est simplement écrit dans le fichier, qui s'effectue via un 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;
};

La fonctionnalité "Enregistrer sous"

Si je décide d'ignorer un gestionnaire de fichier existant, je peux implémenter une fonctionnalité "Enregistrer sous" pour créer un fichier à partir d'un fichier existant. Pour illustrer cela, permettez-moi d'ouvrir un fichier existant, d'y apporter des modifications, puis de ne pas écraser le fichier existant, mais de créer un nouveau fichier à l'aide de la fonctionnalité "Enregistrer sous". Le fichier d'origine reste ainsi intact.

L'implémentation pour les navigateurs qui ne sont pas compatibles avec l'API File System Access est courte, car elle crée simplement un élément d'ancrage avec un attribut download dont la valeur est le nom de fichier souhaité et une URL d'objet blob comme 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 est alors sélectionné de manière programmatique. Pour éviter les fuites de mémoire, vous devez révoquer l'URL de l'objet blob 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, et tous les fichiers sont placés dans le dossier Downloads par défaut.

Glisser-déposer

L'une de mes intégrations système préférées sur le bureau est le glisser-déposer. Dans Excalidraw, lorsque je dépose un fichier .excalidraw dans l'application, il s'ouvre immédiatement et je peux commencer à le modifier. Dans les navigateurs compatibles avec l'API File System Access, je peux même enregistrer immédiatement mes modifications. Il n'est pas nécessaire d'ouvrir une boîte de dialogue d'enregistrement de fichier, car la poignée de fichier requise a été obtenue à partir de l'opération de glisser-déposer.

Pour cela, appelez getAsFileSystemHandle() sur l'élément de transfert de données lorsque l'API File System Access est compatible. Je transmets ensuite ce handle de fichier à loadFromBlob(), dont vous vous souvenez peut-être dans les paragraphes ci-dessus. Vous pouvez faire de nombreuses choses avec les fichiers: ouvrir, enregistrer, enregistrer plus, faire glisser, déposer. Mon collègue Pete et moi avons documenté toutes ces astuces et plus encore dans notre article afin que vous puissiez vous rattraper au cas où tout cela se passerait 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 sous Android, ChromeOS et Windows consiste à utiliser l'API Web Share Target. Me voici dans l'application Files, dans mon dossier Downloads. Je peux voir deux fichiers, l'un d'eux avec le nom non descriptif untitled et un horodatage. Pour vérifier ce qu'il contient, je clique sur les trois points, puis sur "Share" (Partager). L'une des options qui s'affiche est Excalidraw. Lorsque j'appuie sur l'icône, je peux alors voir que le fichier contient à nouveau le logo I/O.

Lipis sur la version obsolète d'Electron

Vous pouvez utiliser un double clic sur des fichiers dont je n'ai pas encore parlé. En général, lorsque vous effectuez un double-clic sur un fichier, c'est que l'application associée au type MIME du fichier s'ouvre. Par exemple, pour .docx, il s'agit de Microsoft Word.

Excalidraw utilisait pour disposer d'une version Electron de l'application compatible avec ce type d'associations de types de fichiers. Par conséquent, lorsque vous double-cliquez sur un fichier .excalidraw, l'application Excalidraw Electron s'ouvre. Lipis, que vous avez déjà rencontré, était à la fois le créateur et l'abandon d'Excalidraw Electron. Je lui ai demandé pourquoi il pensait qu'il était possible d'abandonner la version Electron:

Les utilisateurs demandent une application Electron depuis le début, principalement parce qu'ils voulaient 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, quelqu'un a suggéré de créer une PWA. Nous avons donc fait les deux. Heureusement, nous avons découvert les API Project Fugu, telles que l'accès au système de fichiers, l'accès au presse-papiers, la gestion de fichiers, etc. En un seul clic, vous pouvez installer l'application sur votre ordinateur ou votre mobile, sans avoir à supporter le poids supplémentaire d'Electron. Il a été facile d'abandonner la version Electron, de se concentrer uniquement sur l'application Web et d'en faire la meilleure PWA possible. En outre, nous sommes désormais en mesure de publier des PWA sur le Play Store et le Microsoft Store. C'est énorme !

On peut dire qu'Excalidraw pour Electron n'est pas obsolète, car Electron n'est pas du tout dangereux, mais parce que le Web est devenu suffisamment bon. J'aime ça !

Gestion des fichiers

Quand je dis « le Web est devenu assez bon », c'est grâce à des fonctionnalités telles que la future gestion des fichiers.

Il s'agit d'une installation macOS Big Sur standard. Regardez maintenant ce qui se passe lorsque je fais un clic droit sur un fichier Excalidraw. Je peux l'ouvrir avec Excalidraw, la PWA installée. Bien sûr, un double-clic fonctionne également. Il est tout simplement moins spectaculaire d'effectuer une démonstration dans un enregistrement d'écran.

Comment cela fonctionne-t-il ? La première étape consiste à indiquer au système d'exploitation les types de fichiers que mon application peut gérer. Pour ce faire, j'utilise un nouveau champ appelé file_handlers dans le fichier manifeste de l'application Web. Sa valeur est un tableau d'objets avec une action et une propriété accept. L'action détermine le chemin d'URL via lequel le système d'exploitation lance votre application. L'objet accepté est constitué de paires clé/valeur de types MIME et des 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 l'interface launchQueue, où je dois définir un consommateur en appelant setConsumer(). Le paramètre de cette fonction est une fonction asynchrone qui reçoit le launchParams. Cet objet launchParams comporte un champ appelé "files" qui me fournit un tableau de poignées de fichiers avec lesquelles travailler. Je ne m'occupe que du premier et, à partir de ce handle de fichier, j'obtiens un blob que je passe 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 });
      });
    });
}

Là encore, si le processus est trop rapide, vous pouvez en savoir plus sur l'API File Handling dans mon article. Vous pouvez activer la gestion des fichiers en définissant l'indicateur des fonctionnalités de la plate-forme Web expérimentale. Il devrait arriver dans Chrome plus tard dans l'année.

Intégration du presse-papiers

L'intégration du presse-papiers est une autre fonctionnalité intéressante d'Excalidraw. Je peux copier tout mon dessin ou seulement certaines parties dans le presse-papiers, en ajoutant un filigrane si j'en ai envie, puis le coller dans une autre application. Il s'agit d'une version Web de l'application Paint pour Windows 95.

Le fonctionnement est étonnamment simple. Tout ce dont j'ai besoin, c'est le canevas en tant que blob, que j'écris ensuite dans le presse-papiers en transmettant un tableau à un élément avec une ClipboardItem avec l'objet blob à la fonction navigator.clipboard.write(). Pour en savoir plus sur ce que vous pouvez faire avec l'API Presse-papiers, consultez l'article de Jason et my 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 propose également un mode collaboratif ? Différentes personnes peuvent collaborer sur le même document. Pour démarrer une nouvelle session, je clique sur le bouton de collaboration en direct, puis je lance une session. Je peux partager facilement l'URL de la session avec mes collaborateurs grâce à 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 constater que les modifications que j'apporte sur un appareil sont répercutées sur tous les autres.

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

Afficher l'état des collaborateurs

Pour améliorer l'expérience de collaboration en temps réel, un système de détection d'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 de navigateur ou à une autre application. Et lorsque je suis dans l'application Excalidraw, mais que je ne fais rien, le curseur me montre comme inactif, symbolisé par les trois zZZ.

Les lecteurs assidus de nos publications auront peut-être tendance à penser que la détection d'inactivité est réalisée par le biais de l'API Idle Detection, une proposition préliminaire qui a fait l'objet de travaux dans le cadre du projet Fugu. Alerte spoiler: ce n’est pas le cas. Bien que nous ayons eu une implémentation basée sur cette API dans Excalidraw, nous avons finalement décidé d'opter pour une approche plus traditionnelle, basée sur la mesure des mouvements du pointeur et de la visibilité des pages.

Capture d&#39;écran du commentaire de détection d&#39;inactivité enregistré dans le dépôt WICG de détection d&#39;inactivité.

Nous avons envoyé des commentaires sur les raisons pour lesquelles l'API Idle Detection ne résolvait pas le cas d'utilisation que nous avions. Toutes les API de Project Fugu sont développées de façon ouverte, afin que chacun puisse intervenir et se faire entendre.

Lipides sur ce qui retient Excalidraw

À ce propos, j'ai posé une dernière question à Lipis concernant ce qui, selon lui, manque sur la plate-forme Web qui retient Excalidraw:

L'API File System Access est géniale, mais vous savez quoi ? La plupart des fichiers qui m'intéressent aujourd'hui se trouvent dans mon Dropbox ou Google Drive, et non sur mon disque dur. J'aimerais que l'API File System Access inclue une couche d'abstraction avec laquelle les fournisseurs de systèmes de fichiers distants tels que Dropbox ou Google puissent s'intégrer et avec laquelle les développeurs pourraient coder. Les utilisateurs peuvent alors se détendre et s'assurer que leurs fichiers sont protégés auprès du fournisseur de services cloud de confiance.

Je suis entièrement d'accord avec Lipis, je vis aussi dans le cloud. Nous espérons que cela sera mis en œuvre rapidement.

Mode d'application par onglets

Ouah ! Nous avons constaté de nombreuses intégrations d'API très intéressantes dans Excalidraw. Système de fichiers, gestion de fichiers, presse-papiers, partage Web et cible de partage Web Mais voici une dernière chose. Jusqu'à présent, je ne pouvais modifier qu'un seul document à la fois. Plus vraiment. Profitez pour la première fois d'une version préliminaire du mode d'application à onglets d'Excalidraw. Voici à quoi cela ressemble.

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

Le mode d'application par onglets n'en est qu'à ses débuts. Tout n'est pas encore figé. Pour en savoir plus sur l'état actuel de cette fonctionnalité, consultez cet article.

Conclusion

Pour vous tenir informé de cette fonctionnalité et d'autres, consultez notre outil de suivi des API Fugu. Nous sommes ravis de faire évoluer le Web et de vous permettre d'en faire plus sur la plate-forme. Excalidraw : amélioration constante et ajout à toutes les applications incroyables que vous allez développer Commencez à créer des contenus sur excalidraw.com.

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