Excalidraw y Fugu: cómo mejorar los recorridos principales de los usuarios

Ninguna tecnología lo suficientemente avanzada puede distinguirse de la magia. A menos que lo entiendas. Mi nombre es Thomas Steiner, trabajo en Relaciones con Desarrolladores en Google. En este informe de mi charla de Google I/O, analizaré algunas de las nuevas APIs de Fugu y cómo mejoran los recorridos principales del usuario en la AWP Excalidraw, de modo que puedas inspirarte en estas ideas y aplicarlas a tus propias apps.

Cómo llegué a Excalidraw

Quiero empezar con una historia. El 1 de enero de 2020, Christopher Chedeau, ingeniero de software de Facebook, tuiteó sobre una pequeña app de dibujo que tenía. empezaste a trabajar. Con esta herramienta, puedes dibujar cajas y flechas que parecen caricaturas y dibujado a mano. Al día siguiente, también podrás dibujar elipses y texto, además de seleccionar objetos y moverlos a su alrededor. El 3 de enero, la app recibió su nombre, Excalidraw, y, como con todos los aspectos positivos, comprar el nombre de dominio fue uno de los primeros actos de Christopher. De ahora, se puede usar colores y exportar todo el dibujo como un archivo PNG.

Captura de pantalla de la aplicación del prototipo Excalidraw en la que se muestra que admitía rectángulos, flechas, elipses y texto.

El 15 de enero, Christopher publicó un entrada de blog que dibujó mucha atención en Twitter, incluso el mío. La publicación comenzó con estadísticas impresionantes:

  • 12,000 usuarios activos únicos
  • 1,500 estrellas en GitHub
  • 26 colaboradores

Para un proyecto que comenzó hace apenas dos semanas, eso no está nada malo. Pero lo que realmente aumentó mi interés se mantuvo más bajo en la publicación. Christopher escribió que probó algo nuevo esto Tiempo: Dar acceso de confirmación incondicional a todos los que hayan recibido una solicitud de extracción El mismo día del al leer la entrada de blog, tuve una solicitud de extracción que agregó compatibilidad con la API de File System Access a Excalidraw, lo que corrigió un solicitud de función que alguien presentó.

Captura de pantalla del tweet en el que anuncio mi RR.PP.

Mi solicitud de extracción se combinó un día después y, a partir de ese momento, tuve acceso de confirmación total. No hace falta decir que No abusé de mi poder. Tampoco lo hicieron nadie más de los 149 colaboradores hasta ahora.

Hoy en día, Excalidraw es una app web progresiva instalable y completa que incluye soporte sin conexión, un impresionante modo oscuro y la capacidad de abrir y guardar archivos gracias a la API de File System Access.

Captura de pantalla de la AWP de Excalidraw en el estado actual.

Lipis explica por qué dedica mucho de su tiempo a la Excalidraw

Aquí termina nuestro “Cómo llegué a Excalidraw” pero antes de sumergirme en algunas de Tengo el placer de presentarle las increíbles funciones de Excalidraw. Panayiotis Lipiridis, en Internet, simplemente conocida como lipis, es el colaborador más prolífico de Excalidraw. Le pregunté a lipis qué lo motiva a dedicar tanto de su tiempo a la Excalidición:

Como todos los demás, aprendí sobre este proyecto por el tweet de Christopher. Mi primera contribución era agregar Open Color library, los colores que todavía parte de Excalidraw hoy. A medida que el proyecto crecía y teníamos muchas solicitudes, mi próxima fue crear un backend para almacenar dibujos para que los usuarios pudieran compartirlos. Pero lo que realmente me motiva a contribuir es que quien haya probado Excalidraw busca excusas para usar de nuevo.

Estoy totalmente de acuerdo con lipis. Quien haya probado Excalidraw busca excusas para volver a usarlo.

Excalidraw en acción

Quiero mostrarte ahora cómo puedes usar Excalidraw en la práctica. No soy un gran artista, pero El logotipo de Google I/O es muy simple, así que déjame probarlo. Un cuadro es la "i", una línea puede ser la y la “o” es un círculo. Mantengo presionada la tecla Mayúsculas para obtener un círculo perfecto. Permitirme moverme un poco la barra, por lo que se ve mejor. Ahora, un poco de color para la "i" y la "o". El azul es bueno. Tal vez un estilo de relleno diferente? ¿Todo sólida o con trama cruzada? No, el hachure se ve genial. No es perfecto, pero esa es la idea de Excalidraw, así que la guardaré.

Hago clic en el ícono de guardar e ingreso un nombre de archivo en el cuadro de diálogo de guardado. En Chrome, un navegador que es compatible con la API de File System Access, esta no es una descarga, sino una operación de guardado real, en la que puedo elegir la ubicación y el nombre del archivo, y dónde, si hago cambios, puedo guardarlos en mismo archivo.

Permítanme cambiar el logotipo y hacer la "i" rojo. Si vuelvo a hacer clic en Guardar, mi modificación se guardará en el mismo archivo que antes. Como prueba, voy a borrar el lienzo y volver a abrir el archivo. Como puedes ver, el logotipo rojo-azul modificado está allí otra vez.

Trabaja con archivos

En los navegadores que actualmente no admiten la API de File System Access, cada operación de guardado es de modo que, cuando hago cambios, recibo varios archivos con un número creciente en la nombre de archivo que ocupan mi carpeta Descargas. A pesar de este inconveniente, aún puedo guardar el archivo.

Cómo abrir archivos

Entonces, ¿cuál es el secreto? ¿Cómo se puede abrir y guardar el trabajo en diferentes navegadores que pueden o no es compatible con la API de File System Access? La apertura de un archivo en Excalidraw ocurre en una función llamada loadFromJSON)(), que, a su vez, llama a una función llamada 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 función fileOpen(), que proviene de una pequeña biblioteca que escribí, se llamó a browser-fs-access que usamos en Excalidraw. Esta biblioteca proporciona acceso al sistema de archivos a través del API de File System Access con un resguardo heredado, de modo que pueda usarse navegador.

Primero, te mostraré la implementación para cuando la API sea compatible. Después de negociar el los tipos de MIME y extensiones de archivo aceptados, la pieza central es llamar a las APIs de File System Access la función showOpenFilePicker(). Esta función muestra un array de archivos o un solo archivo, dependiente de seleccionar varios archivos. Todo lo que falta entonces es colocar el controlador de archivos en el para que pueda recuperarse de nuevo.

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

La implementación de resguardo se basa en un elemento input de tipo "file". Después de la negociación de los tipos y extensiones de MIME que se aceptarán, el siguiente paso es hacer clic de forma programática en la entrada para que se muestre el diálogo para abrir el archivo. Si se produce un cambio, es decir, cuando el usuario selecciona varios archivos, la promesa se resuelve.

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

Cómo guardar archivos

Ahora a guardar. En Excalidraw, el guardado se realiza en una función llamada saveAsJSON(). Primero serializa el array de elementos de Excalidraw en JSON, convierte el JSON en un BLOB y, luego, llama a un función llamada fileSave(). Esta función también la proporciona el 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 };
};

Una vez más, veamos primero la implementación para navegadores compatibles con la API de File System Access. El las primeras líneas se ven un poco complicadas, pero lo único que hacen es negociar los tipos de MIME y el archivo extensiones. Cuando ya guardé y ya tengo un identificador de archivo, no es necesario que se muestra. Pero si es la primera vez que se guarda, se muestra un diálogo de archivo y la app obtiene un controlador de archivos. para volver a usarla en el futuro. El resto solo escribe en el archivo, lo que ocurre a través de un transmisión con capacidad de escritura.

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 opción "Guardar como" atributo

Si decido ignorar un identificador de archivo existente, puedo implementar la opción “Guardar como” para crear un archivo nuevo basado en uno existente. Para mostrar esto, voy a abrir un archivo existente, hacer algo modificación y no reemplazar el archivo existente, sino crear uno nuevo con el comando . El archivo original quedará intacto.

La implementación para los navegadores que no admiten la API de File System Access es corta, ya que es crear un elemento de anclaje con un atributo download cuyo valor sea el nombre de archivo deseado y una URL de BLOB como su valor del 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();
};

Luego, se hace clic en el elemento de anclaje de forma programática. Para evitar fugas de memoria, la URL del BLOB debe que se revocará después del uso. Como se trata de una descarga, no se muestra ningún diálogo para guardar archivos, y todos se encuentran en la carpeta Downloads predeterminada.

Arrastrar y soltar

Una de mis integraciones de sistemas favoritas para computadoras de escritorio es arrastrar y soltar. En Excalidraw, cuando suelto un elemento .excalidraw en la aplicación, se abre de inmediato y puedo comenzar a editarlo. En navegadores compatibles con la API de File System Access, puedo guardar de inmediato mis cambios. No necesitas ir a través de un diálogo de guardado de archivos, ya que el controlador de archivos requerido se obtuvo al arrastrar y soltar una sola operación.

El secreto para hacer que esto suceda es llamar a getAsFileSystemHandle() en el elemento de transferencia de datos cuando se admite la API de File System Access. Luego, paso esto el controlador de archivos en loadFromBlob(), que quizás recuerdes de un par de párrafos anteriores. Muchos cosas que puedes hacer con los archivos: abrir, guardar, guardar en exceso, arrastrar y soltar. Mi colega Pete y documenté todos estos trucos y más en nuestro artículo para que puedas ponte al día en caso de que esto fuera demasiado rápido.

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

Cómo compartir archivos

Otra integración de sistema que actualmente se encuentra en Android, ChromeOS y Windows es a través del API de Web Share Target. Estoy en la app de Archivos en la carpeta Downloads. me puede ver dos archivos, uno de ellos con el nombre no descriptivo untitled y una marca de tiempo. Para comprobar qué que contiene, hago clic en los tres puntos, comparto y una de las opciones que aparece es Excalidraw. Cuando presiono el ícono, puedo ver que el archivo solo contiene el logotipo de I/O otra vez.

Lipis en la versión obsoleta de Electron

Algo que se puede hacer con los archivos de los que aún no hablé es hacer doble clic en ellos. Lo que suele cuando haces doble clic en un archivo es que la app está asociada con el tipo de MIME del archivo se abre. Por ejemplo, para .docx sería Microsoft Word.

Excalidraw solía tener una versión electrónica de la app que admite este tipo de asociaciones de archivos, de modo que al hacer doble clic en un archivo .excalidraw, la Se abrirá la app de Excalidraw Electron. Lipis, a quien ya conociste, fue el creador y la obsolescencia de Excalidraw Electron. Le pregunté por qué sentía que era posible dar de baja la Versión electrónica:

Las personas han estado pidiendo una aplicación Electron desde el comienzo, abre los archivos haciendo doble clic. También teníamos la intención de ubicar la aplicación en las tiendas de aplicaciones. Al mismo tiempo, alguien sugirió crear una AWP, así que hicimos ambas cosas. Por suerte, nos presentaron el Proyecto Fugu APIs como acceso al sistema de archivos, acceso al portapapeles, manejo de archivos y mucho más. Con un solo clic, puedes instala la app en tu computadora de escritorio o dispositivo móvil, sin el peso adicional de Electron. Fue un proceso sencillo de dar de baja la versión Electron, concentrarse solo en la aplicación web y hacerla la mejor AWP posible. Además, ahora podemos publicar AWP en Play Store y Microsoft ¡Tienda! Eso es fundamental.

Se podría decir que Excalidraw para Electrón no dejó de estar disponible porque Electrón es malo, para nada, pero porque la Web se volvió suficientemente buena. ¡Me gusta!

Manejo de archivos

Cuando digo "la Web se ha vuelto lo suficientemente buena", es por funciones como la próxima Manipulación.

Esta es una instalación normal de macOS Big Sur. Ahora veamos lo que sucede cuando hago clic con el botón derecho Archivo Excalidraw. Puedo elegir abrirlo con Excalidraw, la AWP instalada. Por supuesto, hacer doble clic también funcionaría, solo que es menos dramático de demostrarlo en una presentación en pantalla.

¿Cómo funciona? El primer paso es hacer que los tipos de archivos que mi aplicación puede manejar sean conocidos por el sistema operativo. Lo hago en un campo nuevo llamado file_handlers, en el manifiesto de la app web. Es es un array de objetos con una acción y una propiedad accept. La acción determina la URL la ruta en la que el sistema operativo inicia tu app y el objeto de aceptación son pares clave-valor de MIME y las extensiones de archivo asociadas.

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

El siguiente paso es controlar el archivo cuando se inicia la aplicación. Esto sucede en launchQueue en la que debo configurar un consumidor llamando a setConsumer(). El parámetro de este es una función asíncrona que recibe el launchParams. Este objeto launchParams tiene un campo llamado files que me da un array de controladores de archivos con los que trabajar. Solo me importa el primero y desde este controlador de archivo obtengo un BLOB que luego le paso a nuestro viejo 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 });
      });
    });
}

Nuevamente, si esto fue muy rápido, puedes leer más sobre la API de File Handling en mi artículo. Puedes habilitar el manejo de archivos configurando la plataforma web experimental de atributos. Se programó para llegar a Chrome más adelante este año.

Integración en el portapapeles

Otra gran función de Excalidraw es la integración del portapapeles. puedo copiar todo mi dibujo o solo algunas partes en el portapapeles, tal vez agregar una marca de agua, si quiero, y luego otra app. Por cierto, esta es una versión web de la aplicación Paint de Windows 95.

El funcionamiento es sorprendentemente simple. Todo lo que necesito es el lienzo como un BLOB, que luego escribo en el portapapeles pasando un array de un elemento con un ClipboardItem con el BLOB al Función navigator.clipboard.write(). Para obtener más información sobre lo que puedes hacer con el portapapeles API, Consulta el artículo de Jason y mi artículo.

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

Colaboración con otras personas

Cómo compartir la URL de una sesión

¿Sabías que Excalidraw también tiene un modo colaborativo? Diferentes personas pueden trabajar juntas en el mismo documento. Para iniciar una sesión nueva, hago clic en el botón de colaboración en vivo y, luego, inicio un sesión. Puedo compartir fácilmente la URL de la sesión con mis colaboradores gracias al API de Web Share que integró Excalidraw.

Colaboración en vivo

Simulé una sesión de colaboración a nivel local con el logotipo de Google I/O en mi Pixelbook. mi teléfono Pixel 3a y mi iPad Pro. Puedes ver que los cambios que realizo en un dispositivo se reflejan en todos los demás dispositivos.

Incluso puedo ver todos los cursores en movimiento. El cursor de la Pixelbook se mueve de manera constante, ya que está controlada junto al panel táctil, pero el cursor del teléfono Pixel 3a y el de la tablet del iPad Pro saltan, ya que controlar estos dispositivos presionando con el dedo.

Cómo ver los estados de los colaboradores

Para mejorar la experiencia de colaboración en tiempo real, se está ejecutando incluso un sistema de detección de inactividad. El cursor del iPad Pro muestra un punto verde cuando lo uso. El punto se vuelve negro cuando cambio a en otra pestaña o app del navegador. Y cuando estoy en la app Excalidraw, pero sin hacer nada, el el cursor me muestra como inactivo, simbolizado por las tres "zZZ".

Los ávidos lectores de nuestras publicaciones podrían estar inclinados a pensar que la detección de inactividad se logra mediante la API de Idle Detection, una propuesta de etapa inicial en la que se ha trabajado contexto del Proyecto Fugu. Alerta de spoiler: No lo es. Aunque teníamos una implementación basada en esta API en Excalidraw, al final, decidimos optar por un enfoque más tradicional basado en la medición el movimiento del puntero y la visibilidad de la página.

Captura de pantalla de los comentarios de la detección de inactividad archivados en el repositorio de detección de inactividad de WICG.

Enviamos comentarios sobre los motivos por los que la API de Idle Detection no estaba resolviendo el caso de uso que teníamos. Todas las APIs de Project Fugu se desarrollan de forma abierta, por lo que todos pueden intervenir y escuchar su voz.

Lípis sobre lo que está reteniendo a Excalidibujo

Hablando de eso, le hice una última pregunta a lipis sobre lo que él cree que le faltaba en la Web. que retiene Excalidraw:

La API de File System Access es genial, pero ¿sabes qué? La mayoría de los archivos que me interesan en estos días viviendo en mi Dropbox o Google Drive, no en mi disco duro. Me gustaría que la API de File System Access Incluyan una capa de abstracción para que los proveedores de sistemas de archivos remotos, como Dropbox o Google, se integren. y en los que los desarrolladores podrían codificar. Los usuarios pueden relajarse y saber que sus archivos están a salvo. con el proveedor de servicios en la nube en el que confían.

Estoy totalmente de acuerdo con lipis, también vivo en la nube. Esperamos que esto se implemente pronto.

Modo de aplicación con pestañas

¡Vaya! Hemos visto muchas integraciones de API realmente excelentes en Excalidraw. Sistema de archivos, manejo de archivos, portapapeles, uso compartido web y objetivo de uso compartido web. Pero aquí hay algo más. Hasta ahora, solo podía editar un documento a la vez. Ya no. Disfruta por primera vez de una versión anticipada de modo de aplicación con pestañas en Excalidraw. Así se ve.

Tengo un archivo existente abierto en la AWP Excalidraw instalada que se ejecuta en modo independiente. Ahora Abro una nueva pestaña en la ventana independiente. Esta no es una pestaña normal del navegador, sino una pestaña AWP. En este En una pestaña nueva, puedo abrir un archivo secundario y trabajar en ellos independientemente desde la misma ventana de la app.

El modo de aplicación con pestañas está en sus primeras etapas y no todo es grabado a fuego. Si estás asegúrate de leer sobre el estado actual de esta función en mi artículo.

Closing

Para estar al tanto de esta y otras funciones, mira nuestra Seguimiento de la API de Fugu. Nos entusiasma que la Web siga avanzando y te permitirá hacer más en la plataforma. Por una Excalidraw en constante mejora, y por todas aplicaciones increíbles que compilarás. Comienza a crear en excalidraw.com.

No puedo esperar a ver ventanas emergentes en tus apps de algunas de las APIs que mostré hoy. Me llamo Tom, puedes encontrarme como @tomayac en Twitter y en Internet en general. Muchas gracias por mirar este video. Que disfrutes el resto de Google I/O.