Excalidraw e Fugu: migliorare i percorsi dell'utente principali

Qualsiasi tecnologia sufficientemente avanzata è indistinguibile dalla magia. A meno che tu non la capisca. Mi chiamo Thomas Steiner e lavoro nel team Developer Relations di Google. In questo riepilogo della mia conferenza Google I/O, analizzerò alcune delle nuove API Fugu e mi occuperò di come migliorano i percorsi degli utenti nella PWA Excalidraw, in modo che tu possa trarre ispirazione da queste idee e applicarle alle tue app.

Come sono arrivato a Excalidraw

Voglio iniziare con una storia. Il 1° gennaio 2020, Christopher Chedeau, ingegnere informatico di Facebook, ha twittato a proposito di una piccola app di disegno su cui aveva iniziato a lavorare. Con questo strumento puoi disegnare caselle e frecce dall'aspetto strano e disegnato a mano. Il giorno successivo puoi disegnare puntini di sospensione e testo, nonché selezionare oggetti e spostarli. Il 3 gennaio l'app aveva preso il suo nome Excalidraw e, come per ogni buon progetto secondario, l'acquisto del nome di dominio è stata una delle prime azioni di Christopher. A questo punto, puoi utilizzare i colori ed esportare l'intero disegno come PNG.

Screenshot dell'applicazione prototipo Excalidraw che mostra che supporta rettangoli, frecce, ellissi e testo.

Il 15 gennaio, Christopher ha pubblicato un post del blog che ha attirato molta attenzione su Twitter, incluso il mio. Il post è iniziato con alcune statistiche impressionanti:

  • 12.000 utenti attivi unici
  • 1500 stelle su GitHub
  • 26 collaboratori

Per un progetto iniziato solo due settimane fa, non è affatto male. Ma la cosa che mi ha davvero interessato è stata più in basso nel post. Christopher ha scritto di aver provato qualcosa di nuovo questa volta: offrire a tutti coloro che hanno ricevuto una richiesta di pull l'accesso incondizionato al commit. Lo stesso giorno in cui ho letto il post del blog, ho ricevuto una richiesta di pull che aggiungeva il supporto dell'API File System Access a Excalidraw, correggendo una richiesta di funzionalità presentata da qualcuno.

Screenshot del tweet in cui annuncio il mio PR.

La mia richiesta di pull è stata unita un giorno dopo e, da lì, ho ottenuto l'accesso completo al commit. Inutile dire che non ho abusato del mio potere. E nemmeno altri tra i 149 collaboratori finora.

Oggi Excalidraw è un'app web progressiva installabile a tutti gli effetti con supporto offline, una fantastica modalità Buio e, sì, la possibilità di aprire e salvare file grazie all'API File System Access.

Screenshot della PWA Excalidraw nello stato odierno.

Lipi spiega perché dedica così tanto tempo a Excalidraw

E con questo si conclude il mio racconto "Come sono venuto a Excalidraw", ma prima di approfondire alcune delle fantastiche caratteristiche di Excalidraw ho il piacere di presentare Panayiotis. Panayiotis Lipiridis, su internet, semplicemente lipis, è il collaboratore più prolifico di Excalidraw. Ho chiesto a Lipis cosa lo spinge a dedicare così tanto tempo a Excalidraw:

Come tutti gli altri che ho scoperto su questo progetto, grazie al tweet di Christopher. Il mio primo contributo è stato aggiungere la libreria colori aperta, i colori che oggi fanno ancora parte di Excalidraw. Man mano che il progetto cresceva e ricevevamo un bel po' di richieste, il mio altro grande contributo è stato creare un backend per l'archiviazione dei disegni in modo che gli utenti potessero condividerli. Ma quello che mi spinge davvero a contribuire è che chiunque abbia provato Excalidraw sta cercando delle scuse per usarlo di nuovo.

Sono pienamente d'accordo con le lipis. Chiunque abbia provato Excalidraw sta cercando delle scuse per utilizzarlo di nuovo.

Excalidraw in azione

Voglio mostrarti ora come puoi usare Excalidraw nella pratica. Non sono un grande artista, ma il logo di Google I/O è abbastanza semplice, quindi fammi provare. Una casella è la "i", una linea può essere la barra e la "o" è un cerchio. Tengo premuto Maiusc per ottenere un cerchio perfetto. Fammi spostare un po' la barra, in modo che venga migliorata. Ora un po' di colore per la "i" e la "o". Il blu va bene. Forse uno stile di riempimento diverso? Tutti solidi o con tratteggi incrociati? No, l'hachure è bellissimo. Non è perfetta, ma è l'idea di Excalidraw, quindi fammi salvare.

Faccio clic sull'icona Salva e inserisco un nome per il file nella finestra di dialogo Salva file. In Chrome, un browser che supporta l'API File System Access, non si tratta di un download, ma di una vera operazione di salvataggio, in cui posso scegliere il percorso e il nome del file e dove, se apporto modifiche, posso semplicemente salvarli nello stesso file.

Fammi cambiare il logo e rendere rossa la "i". Se ora faccio di nuovo clic su Salva, la modifica viene salvata nello stesso file di prima. Come prova, svuoto la tela e riapri il file. Come puoi vedere, il logo rosso-blu modificato è ancora lì.

Lavorare con i file

Sui browser che attualmente non supportano l'API File System Access, ogni operazione di salvataggio avviene tramite download, quindi, quando apporto modifiche, mi trovo più file con un numero incrementale nel nome file che riempie la mia cartella Download. Ma nonostante questo svantaggio, posso comunque salvare il file.

Apertura di file in corso...

Quindi qual è il segreto? Come funziona l'apertura e il salvataggio su diversi browser che potrebbero supportare o meno l'API File System Access? L'apertura di un file in Excalidraw avviene in una funzione denominata loadFromJSON)(), che a sua volta chiama una funzione denominata 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 funzione fileOpen() che proviene da una piccola libreria che ho scritto, chiamata browser-fs-access, che utilizziamo in Excalidraw. Questa libreria consente l'accesso al file system tramite l'API File System Access con un fallback legacy, quindi può essere utilizzata in qualsiasi browser.

Vediamo innanzitutto l'implementazione per quando l'API sarà supportata. Dopo aver negoziato i tipi MIME e le estensioni dei file accettati, la parte centrale chiama la funzione showOpenFilePicker() dell'API File System Access. Questa funzione restituisce un array di file o un singolo file, a seconda che siano selezionati più file. Non ti resta che posizionare l'handle file nell'oggetto file, in modo che possa essere recuperato.

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'implementazione di riserva si basa su un elemento input di tipo "file". Dopo la negoziazione dei tipi MIME e delle estensioni accettati, il passaggio successivo consiste nel fare clic in modo programmatico sull'elemento di input in modo che venga visualizzata la finestra di dialogo di apertura del file. Al momento del cambiamento, ovvero quando l'utente ha selezionato uno o più file, la promessa si risolve.

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

Salvataggio dei file in corso...

Ora passiamo al risparmio. In Excalidraw, il salvataggio avviene in una funzione denominata saveAsJSON(). Innanzitutto serializza l'array di elementi Excalidraw in formato JSON, converte il codice JSON in un blob e quindi chiama una funzione chiamata fileSave(). Questa funzione è fornita anche dalla libreria 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 };
};

Vediamo di nuovo l'implementazione per i browser con supporto per l'API File System Access. Le prime due righe sembrano un po' complesse, ma si limitano a negoziare i tipi MIME e le estensioni dei file. Una volta che ho eseguito il salvataggio e ho già un handle, non è necessario visualizzare alcuna finestra di dialogo per il salvataggio. Tuttavia, se si tratta del primo salvataggio, viene visualizzata una finestra di dialogo del file e l'app ottiene un handle per il file da utilizzare in futuro. Il resto si limita a scrivere sul file, che avviene tramite un stream scrivibile.

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

Funzione "Salva con nome"

Se decido di ignorare un handle di file già esistente, posso implementare una funzionalità "Salva con nome" per creare un nuovo file sulla base di un file esistente. Per mostrarlo, devo aprire un file esistente, apportare alcune modifiche e quindi non sovrascrivere il file esistente, ma creare un nuovo file utilizzando la funzionalità di salvataggio con nome. Il file originale rimane così intatto.

L'implementazione per i browser che non supportano l'API File System Access è breve, poiché tutto ciò che fa è creare un elemento anchor con un attributo download il cui valore è il nome file desiderato e un URL blob come relativo valore dell'attributo 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'elemento anchor viene quindi selezionato in modo programmatico. Per evitare fughe di memoria, l'URL del BLOB deve essere revocato dopo l'uso. Poiché si tratta solo di un download, non vengono visualizzate finestre di dialogo per il salvataggio e tutti i file vengono indirizzati alla cartella Downloads predefinita.

Trascina

Una delle mie integrazioni di sistema preferite su desktop è la funzionalità di trascinamento. In Excalidraw, quando trascino un file .excalidraw nell'applicazione, questo si apre subito e posso iniziare ad apportare modifiche. Sui browser che supportano l'API File System Access, posso persino salvare immediatamente le modifiche. Non è necessario aprire una finestra di dialogo per il salvataggio dei file poiché l'handle file richiesto è stato ottenuto dall'operazione di trascinamento.

Il secret per eseguire questa operazione consiste nel chiamare getAsFileSystemHandle() nell'elemento Data Transfer quando è supportata l'API File System Access. Poi passo l'handle di questo file a loadFromBlob(), come ricorderai da un paio di paragrafi qui sopra. Puoi fare così tante cose con i file: aprirli, salvarli, salvarli troppo, trascinarli o rilasciarli. Io e il mio collega Pete abbiamo documentato tutti questi trucchi e altro ancora nel nostro articolo, così potrete venirvi a conoscenza nel caso in cui tutto sia andato un po' troppo velocemente.

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

Condivisione di file

Un'altra integrazione di sistema attualmente su Android, ChromeOS e Windows avviene tramite l'API Web Share Target. Eccomi nell'app Files, nella mia cartella Downloads. Vedo due file, uno con il nome senza descrizione untitled e un timestamp. Per verificare cosa contiene, faccio clic sui tre puntini, quindi condivido. Una delle opzioni visualizzate è Excalidraw. Quando tocco l'icona, vedo che il file contiene di nuovo solo il logo di I/O.

Lipis sulla versione deprecata di Electron

Una cosa che puoi fare con file di cui non ho ancora parlato è fare doppio clic. In genere, quando esegui un doppio clic su un file si apre l'app associata al tipo MIME del file. Ad esempio, per .docx si tratta di Microsoft Word.

Excalidraw aveva una versione Electron dell'app che supportava queste associazioni di tipi di file, quindi quando facevi doppio clic su un file .excalidraw, si apriva l'app Excalidraw Electron. Lipis, che hai già incontrato in precedenza, è stata l'autore e il deprecatore di Excalidraw Electron. Gli ho chiesto perché ritiene che sia possibile ritirare la versione di Electron:

Le persone hanno chiesto fin dall'inizio un'app Electron, principalmente perché volevano aprire i file facendo doppio clic. Intendevamo inoltre mettere l'app negli store. Parallelamente, qualcuno ha suggerito di creare una PWA, quindi abbiamo fatto entrambe le cose. Per fortuna abbiamo scoperto le API di Project Fugu, come l'accesso al file system, l'accesso agli appunti, la gestione dei file e altro ancora. Con un solo clic puoi installare l'app sul tuo computer desktop o dispositivo mobile senza il peso aggiuntivo di Electron. È stata facile decidere di ritirare la versione di Electron, concentrarti solo sull'app web e renderla la migliore PWA possibile. Inoltre, ora siamo in grado di pubblicare le PWA sul Play Store e nel Microsoft Store. Grande!

Si potrebbe dire che Excalidraw for Electron non è stato deprecato perché Electron non è affatto male, ma perché il web è diventato abbastanza buono. Mi piace!

Gestione dei file

Quando dico "il web è diventato abbastanza bravo", è per funzionalità come la funzionalità Gestione file che verrà implementata prossimamente.

Questa è una normale installazione di macOS Big Sur. Ora controlla cosa succede quando faccio clic con il tasto destro del mouse su un file Excalidraw. Posso scegliere di aprirla con Excalidraw, la PWA installata. Naturalmente andrebbe bene anche il doppio clic, dato che è meno drammatico dare una dimostrazione in uno screencast.

Come funziona? Il primo passaggio consiste nel rendere noti al sistema operativo i tipi di file che l'applicazione è in grado di gestire. Devo eseguire questa operazione in un nuovo campo denominato file_handlers nel file manifest dell'app web. Il suo valore è un array di oggetti con un'azione e una proprietà accept. L'azione determina il percorso dell'URL in cui il sistema operativo avvia l'app e l'oggetto accetta sono coppie chiave-valore di tipi MIME e estensioni di file associate.

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

Il passaggio successivo consiste nel gestire il file all'avvio dell'applicazione. Questo si verifica nell'interfaccia launchQueue in cui devo impostare un consumatore chiamando, beh, setConsumer(). Il parametro di questa funzione è una funzione asincrona che riceve l'oggetto launchParams. Questo oggetto launchParams ha un campo chiamato file che mi fornisce un array di handle di file con cui lavorare. Mi interessa solo il primo e da questo handle di file ricevo un blob che poi passo al nostro vecchio amico 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 });
      });
    });
}

Anche in questo caso, se la procedura è troppo veloce, puoi scoprire di più sull'API File Managing nel mio articolo. Puoi attivare la gestione dei file impostando il flag sperimentale della piattaforma web. L'arrivo su Chrome è previsto entro la fine dell'anno.

Integrazione degli appunti

Un'altra interessante funzionalità di Excalidraw è l'integrazione con gli appunti. Posso copiare negli appunti tutto il disegno o solo una sua parte, ad esempio aggiungendo una filigrana, se voglio, e incollarlo in un'altra app. Questa è tra l'altro una versione web dell'app Paint per Windows 95.

Il funzionamento è sorprendentemente semplice. Tutto ciò di cui ho bisogno è il canvas sotto forma di blob, che poi scrivo negli appunti passando un array di un elemento con ClipboardItem con il BLOB alla funzione navigator.clipboard.write(). Per ulteriori informazioni su cosa puoi fare con l'API degli appunti, consulta Giuseppe e il mio articolo.

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

Collaborazione con altri utenti

Condivisione dell'URL di una sessione

Sapevi che Excalidraw ha anche una modalità di collaborazione? Più persone possono lavorare insieme sullo stesso documento. Per avviare una nuova sessione, faccio clic sul pulsante Collaborazione in tempo reale e avvio una sessione. Posso condividere facilmente l'URL della sessione con i miei collaboratori grazie all'API Web Share integrata da Excalidraw.

Collaborazione dal vivo

Ho simulato una sessione di collaborazione locale lavorando sul logo Google I/O su Pixelbook, telefono Pixel 3a e iPad Pro. Le modifiche che apporto su un dispositivo si riflettono su tutti gli altri dispositivi.

Vedo persino che tutti i cursori si muovono. Il cursore del Pixelbook si sposta costantemente perché è controllato da un trackpad, ma il cursore del telefono Pixel 3a e quello del tablet dell'iPad Pro saltano a rotazione, dato che io controllo questi dispositivi toccando il dito.

Visualizzazione degli stati dei collaboratori

Per migliorare l'esperienza di collaborazione in tempo reale, è anche in esecuzione un sistema di rilevamento di inattività. Il cursore dell'iPad Pro mostra un pallino verde quando lo uso. Il punto diventa nero quando passo a un'altra scheda o a un'app del browser. E quando sono nell'app Excalidraw, senza fare nulla, il cursore mi mostra come inattivo, simbolizzato dai tre simboli zZZ.

I lettori accaniti delle nostre pubblicazioni potrebbero essere inclini a pensare che il rilevamento di inattività sia realizzato attraverso l'API Idle Detection, una proposta in fase iniziale su cui si è lavorato nel contesto di Project Fugu. Spoiler: non lo è. Mentre avevamo un'implementazione basata su questa API in Excalidraw, alla fine abbiamo deciso di adottare un approccio più tradizionale basato sulla misurazione del movimento del puntatore e della visibilità delle pagine.

Screenshot del feedback di Idle Detection registrato nel repository WICG Idle Detection.

Abbiamo inviato un feedback sul motivo per cui l'API Idle Detection non risolve il nostro caso d'uso. Tutte le API Project Fugu sono sviluppate all'aperto, così tutti possono intervenire e far sentire la propria voce.

Lipis su quello che trattiene Excalidraw

A tal proposito, ho fatto a lipis un'ultima domanda su cosa secondo lui manca dalla piattaforma web che ostacola Excalidraw:

L'API File System Access è ottima, ma sai una cosa? Quasi tutti i file che mi interessano in questo periodo si trovano in Dropbox o Google Drive, non sul disco rigido. Vorrei che l'API File System Access includesse un livello di astrazione per i fornitori di file system remoti come Dropbox o Google da integrare con cui gli sviluppatori potevano scrivere codice. Gli utenti possono quindi rilassarsi e avere la certezza che i propri file siano al sicuro con il provider cloud di cui si fida.

Sono pienamente d'accordo con Lipis, vivo anch'io nel cloud. Speriamo che la modifica venga implementata a breve.

Modalità applicazione a schede

Complimenti. Abbiamo visto molte integrazioni di API davvero eccezionali in Excalidraw. File system, gestione file, appunti, condivisione web e destinazione condivisione web. Ma c'è un'ultima cosa: Finora, potevo modificare un solo documento alla volta. Ora non più. Goditi per la prima volta una versione in anteprima della modalità applicazione a schede in Excalidraw. Ecco come si presenta.

Ho un file aperto nella PWA Excalidraw installata in esecuzione in modalità autonoma. Ora apro una nuova scheda nella finestra autonoma. Questa non è una normale scheda del browser, ma una scheda PWA. In questa nuova scheda posso quindi aprire un file secondario e lavorarci in modo indipendente dalla stessa finestra dell'app.

La modalità di applicazione a schede è ancora nelle fasi iniziali e non tutto è ben definito. Se ti interessa, assicurati di leggere lo stato attuale di questa funzionalità nel mio articolo.

Chiusura

Per essere sempre al corrente su questa e altre funzionalità, assicurati di guardare il nostro tracker dell'API Fuugu. Siamo davvero entusiasti di far progredire il web e consentirti di fare di più sulla piattaforma. Passiamo a un Excalidraw in costante miglioramento, alle fantastiche applicazioni che creerai. Inizia a creare su excalidraw.com.

Non vedo l'ora di vedere alcune delle API che ho mostrato oggi nelle vostre app. Mi chiamo Tom, puoi trovarmi come @tomayac su Twitter e su internet in generale. Grazie per l'attenzione e buon divertimento con il resto della conferenza Google I/O.