Excalidraw e Fugu: como melhorar as principais jornadas do usuário

Qualquer tecnologia suficientemente avançada é indistinguível da magia. A menos que você entenda. Meu nome é Thomas Steiner, e trabalho em relações com desenvolvedores no Google. Neste artigo da minha palestra do Google I/O, vou analisar algumas das novas APIs do Fugu e como elas melhoram as principais jornadas do usuário no PWA do Excalidraw, para que você possa se inspirar com essas ideias e aplicá-las aos seus próprios apps.

Como cheguei ao Excalidraw

Quero começar com uma história. Em 1o de janeiro de 2020, Christopher Chedeau, engenheiro de software do Facebook, twittou sobre um pequeno app de desenho que ele tinha começar a trabalhar. Com essa ferramenta, você pode desenhar caixas e setas que parecem desenhos desenhadas à mão. No dia seguinte, você também pode desenhar elipses e texto, além de selecionar objetos e mover por aí. No dia 3 de janeiro, o app ganhou seu nome, Excalidraw, e, como todos os lados positivos, projeto, comprar o nome de domínio foi um dos primeiros atos de Cristóvão. De agora, você pode usar cores e exportar todo o desenho como PNG.

Captura de tela do aplicativo do protótipo do Excalidraw mostrando a compatibilidade com retângulos, setas, elipses e texto.

Em 15 de janeiro, Christopher publicou um postagem de blog que atraiu uma muita atenção no Twitter, inclusive a minha. A postagem começou com algumas estatísticas impressionantes:

  • 12 mil usuários ativos únicos
  • 1.500 estrelas no GitHub
  • 26 colaboradores

Para um projeto que começou há apenas duas semanas, isso não é nada ruim. Mas o que realmente aumentou meu interesse ainda mais na postagem. Christopher escreveu que tentou algo novo tempo: oferecendo a todos que receberam uma solicitação de envio acesso de confirmação incondicional. O mesmo dia Ao ler a postagem do blog, recebi um pedido de pull que adicionou o suporte da API File System Access ao Excalidraw, corrigindo uma solicitação de recurso enviada por alguém.

Captura de tela do tweet em que anuncio minha RP.

Minha solicitação de envio foi mesclada um dia depois e, a partir daí, eu tinha acesso total de confirmação. Nem preciso dizer que Eu não abusei do meu poder. E ninguém dos 149 colaboradores fizeram isso até agora.

Hoje, o Excalidraw é um Progressive Web App instalável completo com suporte off-line, um incrível modo escuro e, sim, a capacidade de abrir e salvar arquivos graças ao a API File System Access.

Captura de tela do PWA do Excalidraw no estado de hoje.

Lipis e explica por que ele dedica tanto tempo ao Excalidraw

Chegamos ao fim da minha série de vídeos história, mas antes de eu mergulhar em alguns dos Os recursos incríveis do Excalidraw, tenho o prazer de apresentar o Panayiotis. Panayiotis Lipiridis, em a Internet simplesmente conhecida como lipis, é o contribuidor mais prolífico para Escalonar. Perguntei ao lipis o que o motiva a dedicar tanto tempo dele a Excalidraw:

Como todos os outros, eu soube do projeto pelo tweet de Christopher. Minha primeira contribuição foi adicionar a biblioteca Open Color, as cores que ainda estão do Excalidraw hoje. Conforme o projeto cresceu e tivemos muitas solicitações, minha próxima grande contribuição foi criar um back-end para armazenar desenhos para que os usuários pudessem compartilhá-los. Mas o que realmente me motiva a contribuir é que quem já testou o Excalidraw quer encontrar desculpas para usar de novo.

Eu concordo plenamente com lipis. Quem usou o Excalidraw quer encontrar desculpas para usá-lo novamente.

Excalidraw em ação

Quero mostrar agora como você pode usar o Excalidraw na prática. Não sou um grande artista, mas O logotipo do Google I/O é bem simples, então vou testar. A caixa é o "i", a linha pode ser o barra e o "o" é um círculo. Eu seguro shift, então tenho um círculo perfeito. Quero mudar a barra um pouco, para ficar melhor. Agora um pouco de cor para o "i" e "o". Azul é bom. Talvez um estilo de preenchimento diferente? Tudo sólido ou hachado? Ah, Hachure está ótimo. Não é perfeito, mas essa é a ideia do Excalidraw, então vou salvá-la.

Clico no ícone de salvamento e insiro um nome de arquivo na caixa de diálogo de salvamento de arquivo. No Chrome, um navegador que oferece suporte à API File System Access, não é um download, mas uma operação de salvamento. É possível escolher o local e o nome do arquivo e onde, se eu fizer edições, posso apenas salvá-las no mesmo arquivo.

Vou mudar o logotipo e colocar o "i" vermelho. Se eu clicar novamente em "Salvar", minha modificação será salva em o mesmo arquivo de antes. Como prova, vou limpar a tela e abrir o arquivo de novo. Como você pode ver, o logotipo vermelho-azul modificado está lá de novo.

Como trabalhar com arquivos

Em navegadores que atualmente não oferecem suporte à API File System Access, cada operação de salvamento é uma de modo que, quando faço alterações, tenho vários arquivos com um número que preenchem minha pasta Downloads. Mas, apesar dessa desvantagem, ainda é possível salvar o arquivo.

Como abrir arquivos

Qual é o segredo? Como abrir e salvar funcionam em diferentes navegadores, que podem ou não oferecem suporte à API File System Access? A abertura de um arquivo no Excalidraw acontece com uma função chamada loadFromJSON)(), que, por sua vez, chama uma função com o nome 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);
};

A função fileOpen(), que vem de uma pequena biblioteca que escrevi, chamada browser-fs-access que usamos no Escalonar. Ela fornece acesso ao sistema de arquivos pela API File System Access com um substituto legado, para que possa ser usada em qualquer navegador.

Primeiro, vou mostrar a implementação de quando a API é compatível. Depois de negociar o tipos MIME e extensões de arquivo aceitos, o elemento central é chamar a API File System Access função showOpenFilePicker(). Essa função retorna uma matriz de arquivos ou um único arquivo, dependente se vários arquivos foram selecionados. Agora só falta colocar o identificador no arquivo para que ele possa ser recuperado novamente.

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

A implementação substituta depende de um elemento input do tipo "file". Após a negociação do os tipos MIME e as extensões a serem aceitos, a próxima etapa será clicar programaticamente na entrada para que a caixa de diálogo de abertura do arquivo seja exibida. Na alteração, ou seja, quando o usuário seleciona um ou vários arquivos, a promessa é resolvida.

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

Como salvar arquivos

Agora vamos salvar. No Excalidraw, o salvamento acontece em uma função chamada saveAsJSON(). Primeiro serializa a matriz de elementos do Excalidraw para JSON, converte o JSON em um blob e chama um chamada fileSave(). Essa função também é fornecida pelo 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 };
};

Vou analisar primeiro a implementação para navegadores com suporte à API File System Access. A as primeiras linhas parecem um pouco envolvidas, mas tudo o que elas fazem é negociar os tipos MIME e o arquivo extensões. Se eu já tiver um identificador de arquivo e eu já tiver salvado esse conteúdo, a caixa de diálogo não precisará ser mostrados. No entanto, se este for o primeiro salvamento, uma caixa de diálogo de arquivo será exibida e o app receberá um identificador de arquivo para uso futuro. O restante é apenas gravar no arquivo, o que acontece por uma stream gravável.

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

A opção "Salvar como" recurso

Se eu decidir ignorar um identificador de arquivo já existente, posso implementar uma opção "salvar como" um recurso para criar um novo arquivo com base em um arquivo existente. Para mostrar isso, vou abrir um arquivo existente. modificação e não substitua o arquivo existente, mas crie um novo arquivo usando o botão . Isso deixa o arquivo original intacto.

A implementação para navegadores que não oferecem suporte à API File System Access é curta, já que todos faz é criar um elemento âncora com um atributo download, cujo valor é o nome de arquivo desejado e um URL de blob como o valor do atributo 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();
};

O elemento âncora é clicado programaticamente. Para evitar vazamentos de memória, é necessário sejam revogados após o uso. Como é apenas um download, nenhuma caixa de diálogo para salvar arquivos é exibida, e todos são direcionados para a pasta Downloads padrão.

Arrastar e soltar

Uma das minhas integrações de sistema favoritas na área de trabalho é a de arrastar e soltar. No Excalidraw, quando solto uma .excalidraw no aplicativo, ele é aberto imediatamente e posso começar a editar. Em navegadores que oferecem suporte à API File System Access, poderei salvar as alterações imediatamente. Não preciso ir por uma caixa de diálogo para salvar o arquivo, já que o identificador do arquivo necessário foi obtido do recurso de arrastar e soltar operação

O segredo para fazer isso acontecer é chamar getAsFileSystemHandle() no data transfer quando a API File System Access é compatível. Eu passo isso Identificador de arquivo para loadFromBlob(), que você deve se lembrar de alguns parágrafos acima. Tantas ações que podem ser realizadas com os arquivos: abrir, salvar, salvar em excesso, arrastar e soltar. Meu colega Pete documentei todos esses truques e muito mais no nosso artigo para que você possa se acostumar, caso tudo tenha acontecido rápido demais.

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

Compartilhamento de arquivos

Atualmente, outra integração de sistema no Android, ChromeOS e Windows é pela API Web Share Target. Aqui estou no app Arquivos na minha pasta Downloads. eu pode ver dois arquivos, um deles com o nome não descritivo untitled e um carimbo de data/hora. Para saber o que ela contém, clico nos três pontos e, em seguida, compartilho, e uma das opções que aparece é Escalonar. Ao tocar no ícone, vejo novamente que o arquivo contém apenas o logotipo do I/O.

Lipis na versão descontinuada do Electron

Uma coisa que você pode fazer com arquivos sobre os quais ainda não falei é usar o DoubleClick. O que normalmente acontece quando você clica duas vezes em um arquivo é que o app associado ao tipo MIME desse arquivo abre. Por exemplo, para .docx, seria Microsoft Word.

O Excalidraw que costumava ter uma versão Electron do app que compatível com essas associações de tipo de arquivo, portanto, quando você clicasse duas vezes em um arquivo .excalidraw, a O app Excalidraw Electron seria aberto. Lipis, que você já conhece, foi a criadora e o depreciador do Excalidraw Electron. Perguntei por que ele achava que era possível descontinuar Versão eletrônica:

As pessoas têm solicitado o aplicativo Electron desde o início, principalmente porque queriam abrir arquivos clicando duas vezes. Nossa intenção também era colocar o aplicativo em app stores. Paralelamente, alguém sugeriu a criação de um PWA, então acabamos de fazer as duas coisas. Por sorte, fomos apresentados ao Projeto Fugu APIs como acesso ao sistema de arquivos, acesso à área de transferência, processamento de arquivos e muito mais. Com um único clique, você pode instale o aplicativo em seu computador ou celular, sem o peso extra do Electron. Foi um processo de descontinuar a versão Electron, focar apenas no aplicativo da Web e torná-la a o melhor PWA possível. Além disso, agora podemos publicar PWAs na Play Store e no Loja! Isso é incrível!

O uso do Excalidraw para Electron não foi descontinuado porque o Electron é ruim, mas não é. porque a Web já se tornou boa o suficiente. Gostei!

Processamento de arquivos

Quando digo "a web se tornou boa o suficiente", é por causa de recursos como o próximo Manuseio.

Esta é uma instalação comum do macOS Big Sur. Veja o que acontece quando clico com o botão direito Excalidraw. Posso optar por abri-lo com o Excalidraw, o PWA instalado. Claro clicar duas vezes também funcionaria, mas é menos dramático demonstrar em um screencast.

Então, como isso funciona? A primeira etapa é tornar conhecidos os tipos de arquivo que meu aplicativo pode lidar no sistema operacional. Faço isso em um novo campo chamado file_handlers no manifesto do app da Web. Seu O valor é uma matriz de objetos com uma ação e uma propriedade accept. A ação determina o URL caminho em que o sistema operacional inicia seu aplicativo e o objeto de aceitação são pares de chave-valor de e as extensões de arquivo associadas.

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

A próxima etapa é processar o arquivo quando o aplicativo for iniciado. Isso acontece na launchQueue em que preciso definir um consumidor chamando setConsumer(). O parâmetro para esta é uma função assíncrona que recebe o launchParams. Este objeto launchParams tem um campo chamado "files" que me dá uma matriz de identificadores de arquivos para trabalhar. Só me importo com primeiro. A partir desse identificador de arquivo, recebo um blob que, depois, passo para nosso amigo de infância 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 });
      });
    });
}

Novamente, se o processo tiver sido muito rápido, leia mais sobre a API File Handling em meu artigo. Você pode ativar o processamento de arquivos definindo a plataforma da Web experimental. flag de recursos. Ela está programada para ser lançada no Chrome ainda este ano.

Integração com a área de transferência

Outro recurso interessante do Excalidraw é a integração com a área de transferência. posso copiar meu desenho inteiro ou apenas partes dele para a área de transferência, talvez adicionando uma marca d'água, se quiser, e colá-lo outro app. A propósito, esta é uma versão Web do app Windows 95 Paint.

A forma como isso funciona é surpreendentemente simples. Tudo que preciso é da tela como um blob, que depois escrevo para a área de transferência transmitindo uma matriz de um elemento com um ClipboardItem com o blob ao função navigator.clipboard.write(). Para mais informações sobre o que é possível fazer com a área de transferência API, consulte o do Jason e o meu artigo.

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

Como colaborar com outras pessoas

Como compartilhar um URL de sessão

Você sabia que o Excalidraw também tem um modo colaborativo? Pessoas diferentes podem trabalhar juntas o mesmo documento. Para iniciar uma nova sessão, clico no botão de colaboração ao vivo e inicio uma sessão. Posso compartilhar o URL da sessão com meus colaboradores facilmente, graças à API Web Share integrada pelo Excalidraw.

Colaboração em tempo real

Simulei uma sessão de colaboração localmente trabalhando no logotipo do Google I/O no meu Pixelbook. meu smartphone Pixel 3a e meu iPad Pro. As mudanças que eu faço em um dispositivo são aplicadas todos os outros dispositivos.

Consigo até ver todos os cursores se movendo. O cursor do Pixelbook se move constantemente, já que é controlado por um trackpad, mas o cursor do smartphone Pixel 3a e o cursor do tablet do iPad Pro pulam, já que controlar esses dispositivos tocando com o dedo.

Conferir os status dos colaboradores

Para melhorar a experiência de colaboração em tempo real, há até um sistema de detecção de inatividade em execução. O cursor do iPad Pro mostra um ponto verde quando eu o uso. O ponto fica preto quando eu mudo para um em outra guia ou app do navegador. Quando estou no aplicativo Excalidraw, mas sem fazer nada, o cursor me mostra como inativo, simbolizado pelos três zZZs.

Leitores ávidos das nossas publicações podem estar inclinados a pensar que a detecção ociosa é realizada por meio de a API Idle Detection, uma proposta em estágio inicial que foi trabalhada no contexto do Projeto Fugu. Spoiler: não é. Apesar de termos uma implementação baseada nessa API no Excalidraw. No final, decidimos adotar uma abordagem mais tradicional, baseada na medição movimento do ponteiro e visibilidade da página.

Captura de tela do feedback de detecção de inatividade registrado no repositório de detecção de inatividade do WICG.

Enviamos feedback sobre o motivo da API Idle Detection não resolveu o caso de uso que tínhamos. Todas as APIs do Project Fugu estão sendo desenvolvidas de forma aberta, por isso todos podem entrar na conversa e ouvir suas opiniões.

Lipis sobre o que está impedindo o Excalidraw

Por falar nisso, perguntei ao lipis uma última pergunta sobre o que ele acha que está faltando na Web. que contém o Excalidraw:

A API File System Access é ótima, mas você sabe de uma coisa? A maioria dos arquivos importantes hoje em dia no meu Dropbox ou Google Drive, não no disco rígido. Eu queria que a API File System Access incluir uma camada de abstração para integrar provedores de sistemas de arquivos remotos, como Dropbox ou Google, e que os desenvolvedores possam usar para código. Assim, os usuários podem relaxar e saber que seus arquivos estão seguros com o provedor de nuvem de confiança.

Eu concordo plenamente com lipis, eu também moro na nuvem. Espero que isso seja implementado logo.

Modo de aplicativo com guias

Uau! Vimos muitas integrações de API excelentes no Excalidraw. Sistema de arquivos, gerenciamento de arquivos área de transferência, compartilhamento na Web e destino de compartilhamento na Web. Mas aqui tem mais uma coisa. Até agora, eu só podia editar um documento em um determinado momento. Isso já não é mais necessário. Aproveite pela primeira vez uma versão antecipada do com guias no Excalidraw. É assim que fica.

Tenho um arquivo aberto no PWA do Excalidraw instalado que está sendo executado no modo independente. Agora Abro uma nova guia na janela independente. Essa não é uma guia comum do navegador, mas uma guia do PWA. Neste nova guia, posso abrir um arquivo secundário e trabalhar nele de forma independente na mesma janela do app.

O modo de aplicação com guias está nos estágios iniciais e nem tudo é definitivo. Se você estiver interesse, leia sobre o status atual desse recurso em meu artigo.

Encerramento

Para ficar por dentro desse e de outros recursos, assista nosso Rastreador de APIs do Fugu (link em inglês). Estamos muito animados para fazer a Web avançar e permitem que você faça mais na plataforma. Este é o Excalidraw, que está sempre melhorando, e a todos os aplicativos incríveis que você criará. Comece a criar em excalidraw.com.

Mal posso esperar para ver algumas das APIs que mostrei hoje aparecer nos seus apps. Meu nome é Tom pode me encontrar como @tomayac no Twitter e na Internet em geral. Agradecemos por assistir e aproveite o Google I/O.