Qualquer tecnologia suficientemente avançada é indistinguível da magia. A menos que você entenda. Meu nome é Thomas Steiner, trabalho em relações com desenvolvedores no Google e, neste artigo sobre minha palestra no Google I/O, vou analisar algumas das novas APIs Fugu e como elas melhoram as principais jornadas do usuário no PWA Excalidraw. Assim, você pode se inspirar nessas ideias e aplicá-las aos seus próprios apps.
Como cheguei ao Excalidraw
Quero começar com uma história. Em 1º de janeiro de 2020, Christopher Chedeau, um engenheiro de software do Facebook, postou no Twitter sobre um pequeno app de desenho em que ele tinha começado a trabalhar. Com essa ferramenta, você pode desenhar caixas e setas que parecem de desenho animado e feitas à mão. No dia seguinte, você também pode desenhar elipses e texto, além de selecionar objetos e movê-los. Em 3 de janeiro, o app recebeu o nome Excalidraw e, como em todo bom projeto paralelo, comprar o nome de domínio foi uma das primeiras ações de Christopher. Até agora, você podia usar cores e exportar todo o desenho como um PNG.
Em 15 de janeiro, Christopher publicou uma postagem no blog que chamou muita atenção no Twitter, inclusive a minha. A postagem começou com algumas estatísticas impressionantes:
- 12 mil usuários ativos únicos
- 1,5 mil estrelas no GitHub
- 26 colaboradores
Para um projeto que começou há apenas duas semanas, isso não é nada mal. Mas o que realmente despertou meu interesse estava mais abaixo na postagem. Christopher escreveu que tentou algo novo desta vez: dar a todos que enviaram uma solicitação de pull acesso incondicional de commit. No mesmo dia em que li a postagem do blog, eu tinha uma solicitação de pull que adicionou suporte à API File System Access ao Excalidraw, corrigindo um pedido de recurso que alguém havia enviado.
Minha solicitação de envio foi mesclada um dia depois e, a partir daí, tive acesso total ao commit. Não preciso dizer que não abusei do meu poder. E ninguém mais entre os 149 colaboradores até agora.
Hoje, o Excalidraw é um app da Web progressivo instalável completo com suporte off-line, um modo escuro incrível e, sim, a capacidade de abrir e salvar arquivos graças à API File System Access.
Lipis explica por que dedica tanto tempo ao Excalidraw
Então, este é o fim da minha história de como conheci o Excalidraw, mas antes de falar sobre alguns dos recursos incríveis do Excalidraw, tenho o prazer de apresentar Panayiotis. Panayiotis Lipiridis, conhecido na Internet como lipis, é o colaborador mais prolífico do Excalidraw. Perguntei a lipis o que o motiva a dedicar tanto tempo ao Excalidraw:
Assim como todo mundo, eu soube desse projeto pelo tweet do Christopher. Minha primeira contribuição foi adicionar a biblioteca Open Color, que ainda faz parte do Excalidraw. À medida que o projeto cresceu e recebemos 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á usou o Excalidraw sempre procura desculpas para usar de novo.
Concordo totalmente com lipis. Quem já usou o Excalidraw sempre encontra um motivo para usar de novo.
Excalidraw em ação
Agora quero mostrar como usar o Excalidraw na prática. Não sou um grande artista, mas o logotipo do Google I/O é simples o suficiente. Vamos lá. Uma caixa é o "i", uma linha pode ser a barra, e o "o" é um círculo. Eu pressiono Shift para fazer um círculo perfeito. Vou mover a barra um pouco para que fique melhor. Agora, vamos colocar um pouco de cor no "i" e no "o". Azul é bom. Talvez um estilo de preenchimento diferente? Totalmente preenchido ou com hachuras? Não, o hachure está ótimo. Não é perfeito, mas essa é a ideia do Excalidraw. Então, vou salvar.
Eu clico no ícone de salvar e insiro um nome de arquivo na caixa de diálogo de salvamento. No Chrome, um navegador que oferece suporte à API File System Access, isso não é um download, mas uma operação de salvamento real, em que posso escolher o local e o nome do arquivo e, se fizer edições, posso salvá-las no mesmo arquivo.
Deixe-me mudar o logotipo e deixar o "i" vermelho. Se eu clicar em salvar novamente, minha modificação será salva no mesmo arquivo de antes. Como prova, vou limpar a tela e reabrir o arquivo. Como você pode ver, o logotipo vermelho e azul modificado está lá novamente.
Como trabalhar com arquivos
Em navegadores que não são compatíveis com a API File System Access, cada operação de salvamento é um download. Portanto, quando faço mudanças, acabo com vários arquivos com um número crescente no nome do arquivo que preenchem minha pasta "Downloads". Mas, apesar dessa desvantagem, ainda posso salvar o arquivo.
Abrir arquivos
Qual é o segredo? Como abrir e salvar arquivos em diferentes navegadores que podem ou não
ser compatíveis com a API File System Access? A abertura de um arquivo no Excalidraw acontece em uma função chamada
loadFromJSON)(
, que por sua vez chama uma função chamada 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()
vem de uma pequena biblioteca que escrevi chamada
browser-fs-access, que usamos no
Excalidraw. Essa biblioteca fornece acesso ao sistema de arquivos pela
API File System Access com um fallback legado, para que possa ser usada em qualquer
navegador.
Primeiro, vou mostrar a implementação quando a API é compatível. Depois de negociar os
tipos MIME e extensões de arquivo aceitos, a parte central é chamar a função
showOpenFilePicker()
da API File System Access. Essa função retorna uma matriz de arquivos ou um único arquivo, dependendo se vários arquivos estão selecionados. Tudo o que resta é colocar o identificador de arquivo no objeto
de 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"
. Depois da negociação dos tipos MIME e extensões a serem aceitos, a próxima etapa é clicar programaticamente no elemento de entrada para que a caixa de diálogo de abertura de arquivo seja mostrada. Na mudança, 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 falar sobre como salvar. No Excalidraw, o salvamento acontece em uma função chamada saveAsJSON()
. Primeiro, ele
serializa a matriz de elementos do Excalidraw em JSON, converte o JSON em um blob e chama uma
função chamada fileSave()
. Essa função também é fornecida pela biblioteca
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 };
};
Primeiro, vamos analisar a implementação para navegadores com suporte à API File System Access. As primeiras linhas parecem um pouco complicadas, mas tudo o que elas fazem é negociar os tipos MIME e as extensões de arquivo. Quando eu já salvei antes e já tenho um identificador de arquivo, não é necessário mostrar uma caixa de diálogo de salvamento. Mas, se 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 um 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;
};
O recurso "Salvar como"
Se eu decidir ignorar um identificador de arquivo já existente, posso implementar um recurso "Salvar como" para criar um novo arquivo com base em um arquivo existente. Para mostrar isso, vou abrir um arquivo, fazer algumas modificações e não substituir o arquivo atual, mas criar um novo usando o recurso "Salvar como". Isso deixa o arquivo original intacto.
A implementação para navegadores que não oferecem suporte à API File System Access é curta, já que tudo o que ela
faz é criar um elemento de âncora com um atributo download
cujo valor é o nome de arquivo desejado e
um URL de blob como 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();
};
Em seguida, o elemento de âncora é clicado de forma programática. Para evitar vazamentos de memória, o URL do blob precisa
ser revogado após o uso. Como é apenas um download, nenhuma caixa de diálogo de salvamento de arquivo é mostrada, e todos os
arquivos são colocados na pasta padrão Downloads
.
Arrastar e soltar
Uma das minhas integrações de sistema favoritas no computador é arrastar e soltar. No Excalidraw, quando eu solto um arquivo .excalidraw
no aplicativo, ele abre imediatamente e posso começar a editar. Em navegadores
que oferecem suporte à API File System Access, posso até salvar minhas mudanças imediatamente. Não é necessário passar por uma caixa de diálogo de salvamento de arquivo, já que o identificador de arquivo necessário foi obtido na operação de arrastar e soltar.
O segredo para isso é chamar getAsFileSystemHandle()
no item transferência de dados quando a API File System Access é compatível. Em seguida, transmito esse
descritor de arquivo para loadFromBlob()
, que você talvez se lembre de alguns parágrafos acima. Você pode fazer muitas coisas com arquivos: abrir, salvar, sobrescrever, arrastar, soltar. Meu colega Pete e eu documentamos todos esses truques e muito mais neste artigo para que você possa acompanhar tudo caso tenha sido 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
Outra integração de sistema disponível no Android, ChromeOS e Windows é a API Web Share Target. Estou aqui no app Arquivos na minha pasta Downloads
. Vejo dois arquivos, um deles com o nome não descritivo untitled
e um carimbo de data/hora. Para verificar o que ele contém, clico nos três pontos, depois em "Compartilhar", e uma das opções que aparece é "Excalidraw". Ao tocar no ícone, vejo que o arquivo contém apenas o logotipo do I/O novamente.
Lipis na versão descontinuada do Electron
Uma coisa que você pode fazer com os arquivos e que ainda não mencionei é clicar duas vezes neles. Normalmente, quando você clica duas vezes em um arquivo, o app associado ao tipo MIME dele é aberto. Por exemplo, para .docx
, seria o Microsoft Word.
O Excalidraw tinha uma versão do app no Electron que
aceitava essas associações de tipos de arquivo. Assim, quando você clicava duas vezes em um arquivo .excalidraw
, o
app Excalidraw Electron era aberto. Lipis, que você já conheceu, foi o criador e o responsável pela descontinuação do Excalidraw Electron. Perguntei por que ele achava possível descontinuar a versão do Electron:
As pessoas pedem um app Electron desde o início, principalmente porque querem abrir arquivos com um clique duplo. Também pretendíamos colocar o app nas lojas de aplicativos. Paralelamente, alguém sugeriu criar um PWA. Então, fizemos os dois. Felizmente, conhecemos as APIs do Project Fugu, como acesso ao sistema de arquivos, acesso à área de transferência, processamento de arquivos e muito mais. Com um único clique, você pode instalar o app no computador ou dispositivo móvel, sem o peso extra do Electron. Foi fácil decidir descontinuar a versão do Electron, concentrar-se apenas no app da Web e transformá-lo no melhor PWA possível. Além disso, agora é possível publicar PWAs na Play Store e na Microsoft Store. Isso é muito!
Pode-se dizer que o Excalidraw para Electron não foi descontinuado porque o Electron é ruim, de jeito nenhum, mas porque a Web se tornou boa o suficiente. Gostei!
Gerenciamento de arquivos
Quando digo que "a Web ficou boa o suficiente", é por causa de recursos como o próximo File Handling.
Esta é uma instalação normal do macOS Big Sur. Agora confira o que acontece quando clico com o botão direito do mouse em um arquivo do Excalidraw. Posso escolher abrir com o Excalidraw, o PWA instalado. É claro que clicar duas vezes também funciona, mas é menos dramático para demonstrar em uma screencast.
Como isso funciona? A primeira etapa é informar ao sistema operacional os tipos de arquivo que meu aplicativo pode processar. Faço isso em um novo campo chamado file_handlers
no manifesto do app da Web. O valor é uma matriz de objetos com uma ação e uma propriedade accept
. A ação determina o caminho do URL
em que o sistema operacional inicia o app, e o objeto de aceitação são pares de chave-valor de tipos
MIME 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 interface launchQueue
, em que preciso definir um consumidor chamando setConsumer()
. O parâmetro dessa função é uma função assíncrona que recebe o launchParams
. Esse objeto launchParams
tem um campo chamado "files" que me dá uma matriz de identificadores de arquivo para trabalhar. Só me importo com o primeiro, e desse identificador de arquivo recebo um blob que passo para nosso velho amigo 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 });
});
});
}
Se você não entendeu, leia mais sobre a API File Handling neste artigo. Para ativar o processamento de arquivos, defina a flag de recursos experimentais da plataforma da Web. Ela está programada para chegar ao 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 todo o desenho ou apenas partes dele para a área de transferência, talvez adicionando uma marca-d'água se quiser, e depois colar em outro app. Esta é uma versão da Web do app Paint do Windows 95.
O funcionamento é surpreendentemente simples. Tudo o que preciso é da tela como um blob, que depois gravo
na área de transferência transmitindo uma matriz de um elemento com um ClipboardItem
com o blob para a
função navigator.clipboard.write()
. Para mais informações sobre o que você pode fazer com a API
da área de transferência, consulte o artigo de Jason e o meu.
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
Compartilhar o URL de uma sessão
Você sabia que o Excalidraw também tem um modo colaborativo? Várias pessoas podem trabalhar juntas no mesmo documento. Para iniciar uma nova sessão, clico no botão de colaboração ao vivo e depois em "Iniciar sessão". Posso compartilhar o URL da sessão com meus colaboradores com facilidade graças à API Web Share integrada ao Excalidraw.
Colaboração em tempo real
Simulei uma sessão de colaboração localmente trabalhando no logotipo do Google I/O no meu Pixelbook, no smartphone Pixel 3a e no iPad Pro. Você pode ver que as mudanças feitas em um dispositivo são refletidas em todos os outros.
Consigo até ver todos os cursores se moverem. O cursor do Pixelbook se move de forma constante, já que é controlado por um trackpad, mas o cursor do smartphone Pixel 3a e do tablet iPad Pro pulam, já que controlo esses dispositivos tocando com o dedo.
Como ver 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 mudo para outra guia do navegador ou app. E quando estou no app Excalidraw, mas não estou fazendo nada, o cursor me mostra como inativo, simbolizado pelos três zZZs.
Os leitores assíduos das nossas publicações podem achar que a detecção de inatividade é realizada pela API Idle Detection, uma proposta em estágio inicial que está sendo trabalhada no contexto do Projeto Fugu. Alerta de spoiler: não é. Embora tivéssemos uma implementação baseada nessa API no Excalidraw, decidimos usar uma abordagem mais tradicional baseada na medição do movimento do ponteiro e da visibilidade da página.
Enviamos feedback sobre por que a API Idle Detection não estava resolvendo o caso de uso que tínhamos. Todas as APIs do Projeto Fugu estão sendo desenvolvidas em código aberto, para que todos possam participar e ter suas opiniões ouvidas.
Lipis sobre o que está impedindo o Excalidraw
Falando nisso, fiz a lipis uma última pergunta sobre o que ele acha que está faltando na plataforma da Web que impede o Excalidraw:
A API File System Access é ótima, mas sabe o que mais é? A maioria dos arquivos que me interessam hoje em dia fica no Dropbox ou no Google Drive, não no meu disco rígido. Queria que a API File System Access incluísse uma camada de abstração para provedores de sistemas de arquivos remotos, como Dropbox ou Google, para integração e que os desenvolvedores pudessem programar. Assim, os usuários podem ficar tranquilos sabendo que os arquivos estão seguros com o provedor de nuvem em que confiam.
Concordo totalmente com o lipis. Eu também vivo na nuvem. Espero que isso seja implementado em breve.
Modo de aplicativo com guias
Uau! Vimos muitas integrações de API excelentes no Excalidraw. Sistema de arquivos, processamento de arquivos, área de transferência, compartilhamento na Web e destino de compartilhamento na Web. Mas há mais uma coisa. Até agora, só era possível editar um documento por vez. Nada disso. Aproveite pela primeira vez uma versão antecipada do modo de aplicativo com guias no Excalidraw. Confira como ele aparece.
Tenho um arquivo aberto no PWA do Excalidraw instalado que está sendo executado no modo independente. Agora vou abrir uma nova guia na janela independente. Essa não é uma guia normal do navegador, mas uma guia de PWA. Nessa nova guia, posso abrir um arquivo secundário e trabalhar neles de forma independente na mesma janela do app.
O modo de aplicativo com guias está nas fases iniciais e nem tudo está definido. Se quiser saber mais, leia o status atual desse recurso no meu artigo.
Encerramento
Para ficar por dentro desse e de outros recursos, assista nosso rastreador de APIs Fugu. Estamos muito felizes em impulsionar a Web e permitir que você faça mais na plataforma. Que o Excalidraw continue melhorando e que você crie aplicativos incríveis. Comece a criar em excalidraw.com.
Mal posso esperar para ver algumas das APIs que mostrei hoje aparecerem nos seus apps. Meu nome é Tom, você pode me encontrar como @tomayac no Twitter e na Internet em geral. Muito obrigado por assistir e aproveite o restante do Google I/O.