Hello.

I am Paul Kinlan.

A Developer Advocate for Chrome and the Open Web at Google.

Puppeteer Go🔗

Paul Kinlan

Me encanta Puppeteer, me permite jugar con las ideas de The Headless Web , que es ejecutar la web en un navegador sin un navegador visible e incluso crear herramientas como DOM-curl (Curl que ejecuta JavaScript). Específicamente, me encanta crear scripts en el navegador para raspar, manipular e interactuar con las páginas.

Una demostración que quería hacer se inspiró en la publicación Capturing 422 live images de Ire, donde ejecutó un guión titiritero que Capturing 422 live images por muchas páginas y tomaría una captura de pantalla. En lugar de ir a muchas páginas, quería tomar muchas capturas de pantalla de elementos en la página.

El problema que tengo con Puppeteer es la estrofa de apertura que necesitas para hacer cualquier cosa. Iniciar, abrir pestaña, navegar: no es complejo, es más repetitivo de lo que quiero crear para scripts simples. Por eso creé Puppeteer Go . Es solo un pequeño script que me ayuda a crear utilidades CLI fácilmente que abre el navegador, navega a una página, realiza su acción y luego se limpia después de sí mismo.

Echale un vistazo.

const { go } = require('puppeteer-go');

go('https://paul.kinlan.me', async (page) => {
    const elements = await page.$$("h1");
    let count = 0;
    for(let element of elements) {
      try {
        await element.screenshot({ path: `${count++}.png`});
      } catch (err) {
        console.log(count, err);
      }
    }
});

El código anterior encontrará el elemento h1 en mi blog y tomará una captura de pantalla. Esto no es tan bueno como el trabajo de Ire, pero pensé que era bueno ver si podemos obtener rápidamente capturas de pantalla de canisuse.com directamente desde la página.

const { go } = require('puppeteer-go');

go('https://caniuse.com/#search=css', async (page) => {
    const elements = await page.$$("article.feature-block.feature-block--feature");
    let count = 0;
    for(let element of elements) {
      try {
        await element.screenshot({ path: `${count++}.png`});
      } catch (err) {
        console.log(count, err);
      }
    }
});
4.png
3.png
2.png
1.png
0.png

¡Disfrutar!

About Me: Paul Kinlan

I lead the Chrome Developer Relations team at Google.

We want people to have the best experience possible on the web without having to install a native app or produce content in a walled garden.

Our team tries to make it easier for developers to build on the web by supporting every Chrome release, creating great content to support developers on web.dev, contributing to MDN, helping to improve browser compatibility, and some of the best developer tools like Lighthouse, Workbox, Squoosh to name just a few.

A simple video insertion tool for EditorJS🔗

Paul Kinlan

Realmente me gusta EditorJS . Me permite crear una interfaz muy simple alojada en la web para mi blog estático de Hugo.

EditorJS tiene la mayor parte de lo que necesito en un simple editor basado en bloques. Tiene un complemento para encabezados, código e incluso una forma simple de agregar imágenes al editor sin requerir infraestructura de alojamiento. No tiene una manera simple de agregar videos al editor, hasta ahora.

Tomé el simple-image repositorio de plugins y cambió hacia arriba (sólo un poco) para crear un simple-video plug-in ( npm module ). Ahora puedo incluir videos fácilmente en este blog.

Si está familiarizado con EditorJS, es bastante simple incluirlo en sus proyectos. Simplemente instálelo de la siguiente manera

npm i simple-video-editorjs

Y luego simplemente inclúyalo en su proyecto como mejor le parezca.

const SimpleVideo = require('simple-video-editorjs');

var editor = EditorJS({
  ...
  
  tools: {
    ...
    video: SimpleVideo,
  }
  
  ...
});

El editor tiene algunas opciones simples que le permiten configurar cómo se debe alojar el video en la página:

  1. Reproducción automática: el video se reproducirá automáticamente cuando se cargue la página
  2. silenciado: el video no tendrá sonido activado de forma predeterminada (necesario para la reproducción automática)
  3. controles: el video tendrá los controles HTML predeterminados.

A continuación se muestra un ejemplo rápido de un video que está incrustado (y que muestra algunas de las opciones).

De todos modos, me divertí creando este pequeño complemento: no fue demasiado difícil de crear y lo único que hice fue diferir la conversión a base64 que usan imágenes simples y en su lugar solo usar las URL de Blob.

Test post Video upload

Friendly Project Name Generator with Zeit🔗

Paul Kinlan

Tengo algunas ideas para proyectos que facilitan la creación de sitios en la web, una de las ideas es hacer un netlify-like drag and drop interface para proyectos basados en zeit (me gusta zeit pero requiere un poco de cli magic para implementar).

Esta publicación cubre solo una pequeña pieza del rompecabezas: crear nombres de proyectos.

Glitch es un buen ejemplo de esto, cuando crea un proyecto le da un nombre caprichoso generado aleatoriamente. El equipo también creó un good dictionary of fairly safe words que combina bien (y si lo desea, tienen un servidor simple para alojar).

Por lo tanto, el proyecto paralelo de este domingo fue crear un serverless-functions simple para generar nombres de proyectos aleatorios usando las serverless-functions de Zeit y el diccionario de Glitch.

And here it is ( code ), es bastante corto y no demasiado complejo.

const words = require("friendly-words");

function generate(count = 1, separator = "-") {
  const { predicates, objects } = words;
  const pCount = predicates.length;
  const oCount = objects.length;
  const output = [];

  for (let i = 0; i < count; i++) {
    const pair = [predicates[Math.floor(Math.random() * pCount)], objects[Math.floor(Math.random() * oCount)]];
    output.push(pair.join(separator));
  }

  return output;
}

module.exports = { generate }

Si no desea incluirlo directamente en su proyecto, puede usar el punto final HTTP para generar nombres de proyecto aleatorios (en forma de "XY") haciendo una solicitud web a https: // nombre-proyecto-amigable. kinlan.now.sh/api/names, que devolverá algo como lo siguiente.

["momentous-professor"]

También puede controlar cuántos nombres generar con un parámetro de cadena de consulta de count = x , por ejemplo, https://friendly-project-name.kinlan.now.sh/api/names?count=100

["melon-tangerine","broad-jury","rebel-hardcover","far-friend","notch-hornet","principled-wildcat","level-pilot","steadfast-bovid","holistic-plant","expensive-ulna","sixth-gear","political-wrench","marred-spatula","aware-weaver","awake-pair","nosy-hub","absorbing-petunia","rhetorical-birth","paint-sprint","stripe-reward","fine-guardian","coconut-jumbo","spangle-eye","sudden-euphonium","familiar-fossa","third-seaplane","workable-cough","hot-light","diligent-ceratonykus","literate-cobalt","tranquil-sandalwood","alabaster-pest","sage-detail","mousy-diascia","burly-food","fern-pie","confusion-capybara","harsh-asterisk","simple-triangle","brindle-collard","destiny-poppy","power-globeflower","ruby-crush","absorbed-trollius","meadow-blackberry","fierce-zipper","coal-mailbox","sponge-language","snow-lawyer","adjoining-bramble","deserted-flower","able-tortoise","equatorial-bugle","neat-evergreen","pointy-quart","occipital-tax","balsam-fork","dear-fairy","polished-produce","darkened-gondola","sugar-pantry","broad-slouch","safe-cormorant","foregoing-ostrich","quasar-mailman","glittery-marble","abalone-titanosaurus","descriptive-arch","nickel-ostrich","historical-candy","mire-mistake","painted-eater","pineapple-sassafras","pastoral-thief","holy-waterlily","mewing-humor","bubbly-cave","pepper-situation","nosy-colony","sprout-aries","cyan-bestseller","humorous-plywood","heavy-beauty","spiral-riverbed","gifted-income","lead-kiwi","pointed-catshark","ninth-ocean","purple-toucan","tundra-cut","coal-geography","icy-lunaria","agate-wildcat","respected-garlic","polar-almandine","periodic-narcissus","carbonated-waiter","lavish-breadfruit","confirmed-brand","repeated-period"]

Puede controlar el separador con el parámetro de cadena de consulta del separador. es decir, separador = @, por ejemplo, https://friendly-project-name.kinlan.now.sh/api/names?separator=@

["handsomely@asterisk"]

Un aspecto muy útil de este proyecto es que si una combinación de palabras tiende a ser ofensiva, es fácil actualizar el repositorio de Glitch para garantizar que no vuelva a suceder.

Suponiendo que el alojamiento del proyecto no sea demasiado caro, mantendré el servicio en funcionamiento, pero siéntase libre de clonarlo si alguna vez desea crear un microservicio similar.

Ejemplo en vivo

Lo que sigue es un ejemplo súper rápido de la API en acción.

const render = (promise, elementId) => {
  promise.then(async(response) => {
    const el = document.getElementById(elementId);
    el.innerText = await response.text();
  })
};


onload = () => {
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names"), "basic");
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names?count=100"), "many");
  render(fetch("https://friendly-project-name.kinlan.now.sh/api/names?separator=@"), "separator");
}

Respuesta única


Muchas respuestas


Separadores personalizados


{{<raw-html>}}

{{</ raw-html>}}

Frankie and Bennys: Pay for your meal via the web

Paul Kinlan

Cada vez que veo un restaurante que dice que puedes pagar en el móvil, siempre lo reviso, principalmente para poder lamentar el hecho de que necesites usar una aplicación. Imagine mi sorpresa cuando el código QR condujo a un flujo de pagos basado en la web ... y funcionó. ¡Impresionante trabajo de Frankie y Benny! En este punto, seleccioné Google Pay, pero no funcionó (¡correo electrónico enviado internamente!

Read More

Podroll

Adding "dark mode" to my blog

Paul Kinlan

Vi las post about adding dark mode to his blog Jeremy Keith y parecía simple, así que decidí darle una vuelta. Aquí está el diff of the work para que todos lo vean. Fue sorprendentemente fácil (aparte de errores tontos de mi parte). Hubo un pequeño refactor para admitir variables CSS y garantizar que tenga respaldo si hay un navegador que no admite propiedades personalizadas CSS, pero eso es todo. Hice casi lo mismo que Jeremy hizo.

Read More

Using Web Mentions in a static site (Hugo)

Paul Kinlan

Mi blog es un sitio completamente estático, construido con Hugo y alojado con Zeit. Esta es una gran solución para mí, un blog simple tiene un proceso de implementación bastante simple y se carga increíblemente rápido. Los sitios generados estáticamente tienen algunos inconvenientes, el más grande es cuando necesita algo dinámico para integrarse en su página (comentarios, por ejemplo). No poder alojar fácilmente contenido dinámico significará que terminará confiando en JavaScript de terceros que luego tendrá acceso completo a su página y no sabrá lo que está haciendo: podría estar rastreando a sus usuarios o ralentizando su página carga.

Read More

Creating a pop-out iframe with adoptNode and "magic iframes"

Paul Kinlan

Actualización de ### : 8 de octubre: problemas importantes con este documento. Me puse al día con Jake Archibald sobre esta publicación porque pensé que tenía algo nuevo, durante la conversación descubrimos muchas cosas que invalidan algunas de estas publicaciones, y también aprendí mucho en el proceso que no creo que la mayoría de los desarrolladores saber. Las llamadas a .append() y .appendChild() adoptan el nodo. Esto hace que el uso de adoptNode en esta instancia sea inútil porque el Algoritmo adoptNode garantiza que se adopte el nodo.

Read More

Meatspace Augmented Reality: From Chester to Nagoya

Paul Kinlan

Some thoughts on AR after finding some during my travels. TL;DR - cheaper content creation and better discovery tools are needed.

Read More

Photos from Carlisle Castle

Paul Kinlan

Recientemente fui de vacaciones con los niños y pasamos por delante de Carlisle Castle (lugar de nacimiento del rebote mundial Jake Archibald). Es uno de los mejores castillos en los que hemos estado en el Reino Unido y recomiendo encarecidamente pasar un tiempo visitándolo si estás en la zona. No supe cuán importante fue el papel que desempeñó el Castillo de Carlisle en la historia de Inglaterra y Escocia, y fue genial saber más mientras estuvimos allí.

Read More

Idle observation: Indexing text in images

Paul Kinlan

Estuve con los chicos en Llangollen el otro día (es una ciudad hermosa) y estaba tomando fotos de los carteles informativos que contienen parte de la historia de la zona para poder leerlo más tarde, y pensé que podría mirarlo. la web para ver si la información estaba disponible para algo más que las personas que pasaban el cartel, y no lo está. Luego me puse a pensar en los folletos, el contenido en ellos es casi imposible de encontrar en la web.

Read More

Liverpool World Museum

Paul Kinlan

Llevé a los niños al Museo de Historia Mundial de Liverpool la otra semana, fue bastante bueno. La sección de espacio y tiempo no ha cambiado en aproximadamente 30 años, una gran parte del recinto de Bug se cerró y el acuario parecía un poco más pequeño de lo que recuerdo. La sección egipcia estaba abierta (no fue la última vez que fui) y fue bastante impresionante.

Read More

Bookstore - Llangollen

Paul Kinlan

Me encanta este lugar, está en la cima de una cafetería en Llangollen. Vine aquí con mis abuelos hace casi 30 años y ahora es casi lo mismo. Mi único deseo es que hubiera aún más libros de Commix: juro que había un montón más cuando era niño. Check it out

Read More

Webmention.app🔗

Paul Kinlan

Me encanta la idea de Webmentions , pero no he tenido tiempo de implementarla en mi sitio. En una web de alto nivel, las menciones le permiten comentar, dar me gusta y responder a otros contenidos en la web y hacer que sea visible para ese contenido sin estar centralizado con herramientas como Disqus (que estoy dispuesto a eliminar de mi sitio).

Las menciones web se dividen en dos componentes, el emisor y el receptor. El receptor es el sitio sobre el que escribo una publicación y pueden tener algo en su sitio que muestre enlaces entrantes o reacciones a su blog; y el remitente es, bueno, yo. Necesito dejar que el sitio remoto que he escrito o que haya reaccionado a algún contenido que hayan creado.

El increíble Remy Sharp creó webmention.app para resolver una parte del problema: enviar pings. La herramienta de Remy facilita el envío de 'pings' a receptores potenciales a los que me he vinculado, simplemente llamando a un script CLI.

Alojo mi blog usando Zeit usando Hugo y la herramienta de construcción estática, por lo que fue relatively trivial for me to add in support for webmention app . Simplemente npm i webmention y luego llamo a la versión CLI de la herramienta desde mi archivo build.sh ; realmente es así de simple.

Ahora, cuando creo una publicación, debería enviar un ping rápido a todas las URL nuevas en las que he creado algún contenido sobre su sitio.

Creating a commit with multiple files to Github with JS on the web

Paul Kinlan

Mi sitio es entirely static . Está construido con Hugo y alojado con Zeit . Estoy bastante contento con la configuración, obtengo compilaciones instantáneas y entrega de contenido CDN'd súper rápida y puedo hacer todo lo que necesito porque no tengo que administrar ningún estado. He creado un simple UI para este sitio y también mi podcast creator que me permite publicar rápidamente contenido nuevo en mi sitio alojado estáticamente.

Read More

Screen Recorder: recording microphone and the desktop audio at the same time🔗

Paul Kinlan

Tengo el objetivo de construir el software de grabación de pantalla más simple del mundo y he estado dando vueltas lentamente sobre el proyecto durante los últimos meses (quiero decir, muy lentamente).

En publicaciones anteriores, obtuve el screen recording and a voice overlay al analizar las transmisiones de todas las fuentes de entrada. Sin embargo, un área de frustración fue que no pude resolver cómo obtener el audio del escritorio * y * superponer el audio del altavoz. Finalmente resolví cómo hacerlo.

En primer lugar, getDisplayMedia en Chrome ahora permite la captura de audio, parece que hay un descuido extraño en la Especificación, ya que no le permitió especificar audio: true en la llamada de función, ahora puede audio: true .

const audio = audioToggle.checked || false;
desktopStream = await navigator.mediaDevices.getDisplayMedia({ video:true, audio: audio });

En segundo lugar, originalmente pensé que al crear dos pistas en la secuencia de audio podría obtener lo que quería, sin embargo, aprendí que la API MediaRecorder de Chrome solo puede generar una pista y, en segundo lugar, no habría funcionado de todos modos porque las pistas son como las múltiples pistas de audio de DVD en las que solo una puede reproducirse a la vez.

La solución es probablemente simple para muchas personas, pero era nueva para mí: usar Web Audio.

Resulta que WebAudio API tiene createMediaStreamSource y createMediaStreamDestination , los cuales son API necesarios para resolver el problema. createMediaStreamSource puede tomar secuencias desde el audio y el micrófono de mi escritorio, y al conectar las dos juntas al objeto creado por createMediaStreamDestination me da la capacidad de canalizar esta secuencia a la API de MediaRecorder .

const mergeAudioStreams = (desktopStream, voiceStream) => {
  const context = new AudioContext();
    
  // Create a couple of sources
  const source1 = context.createMediaStreamSource(desktopStream);
  const source2 = context.createMediaStreamSource(voiceStream);
  const destination = context.createMediaStreamDestination();
  
  const desktopGain = context.createGain();
  const voiceGain = context.createGain();
    
  desktopGain.gain.value = 0.7;
  voiceGain.gain.value = 0.7;
   
  source1.connect(desktopGain).connect(destination);
  // Connect source2
  source2.connect(voiceGain).connect(destination);
    
  return destination.stream.getAudioTracks();
};

Simples

El código completo se puede encontrar en my glitch , y la demostración se puede encontrar aquí: https://screen-record-voice.glitch.me/

{{<fast-youtube oGIdqcMFKlA>}}

Extracting text from an image: Experiments with Shape Detection🔗

Paul Kinlan

Tuve un poco de tiempo de inactividad después de Google IO y quería rascarme una picazón a largo plazo que tuve. Solo quiero poder copiar el texto que se encuentra dentro de las imágenes en el navegador. Eso es todo. Creo que sería una característica interesante para todos.

No es fácil agregar funcionalidad directamente a Chrome, pero sé que puedo aprovechar el sistema de intención en Android y ahora puedo hacerlo con la Web (o al menos Chrome en Android).

Dos nuevas incorporaciones a la plataforma web: Share Target Level 2 (o como me gusta llamarlo File Share) y TextDetector en la API de detección de formas - have allowed me to build a utility that I can Share images to and get the text held inside them .

La implementación básica es relativamente sencilla: crea un destino de compartir y un controlador en el Service Worker, y luego, una vez que tiene la imagen que el usuario ha compartido, ejecuta el TextDetector en él.

Share Target API permite que su aplicación web forme parte del subsistema de uso compartido nativo, y en este caso ahora puede registrarse para manejar todos los tipos de image/* declarándolo dentro de su Web App Manifest siguiente manera.

"share_target": {
  "action": "/index.html",
  "method": "POST",
  "enctype": "multipart/form-data",
  "params": {
    "files": [
      {
        "name": "file",
        "accept": ["image/*"]
      }
    ]
  }
}

Cuando su PWA esté instalada, la verá en todos los lugares donde comparte imágenes de la siguiente manera:

La API Share Target trata el intercambio de archivos como una publicación de formulario. Cuando el archivo se comparte con la aplicación web, el trabajador de servicio se activa fetch se invoca el controlador fetch con los datos del archivo. Los datos ahora están dentro del Service Worker, pero los necesito en la ventana actual para poder procesarlos, el servicio sabe qué ventana invocó la solicitud, por lo que puede apuntar fácilmente al cliente y enviarle los datos.

self.addEventListener('fetch', event => {
  if (event.request.method === 'POST') {
    event.respondWith(Response.redirect('/index.html'));
    event.waitUntil(async function () {
      const data = await event.request.formData();
      const client = await self.clients.get(event.resultingClientId || event.clientId);
      const file = data.get('file');
      client.postMessage({ file, action: 'load-image' });
    }());
    
    return;
  }
  ...
  ...
}

Una vez que la imagen está en la interfaz de usuario, la proceso con la API de detección de texto.

navigator.serviceWorker.onmessage = (event) => {  
  const file = event.data.file;
  const imgEl = document.getElementById('img');
  const outputEl = document.getElementById('output');
  const objUrl = URL.createObjectURL(file);
  imgEl.src = objUrl;
  imgEl.onload = () => {
    const texts = await textDetector.detect(imgEl);
    texts.forEach(text => {
      const textEl = document.createElement('p');
      textEl.textContent = text.rawValue;
      outputEl.appendChild(textEl);
    });
  };
  ...
};

El mayor problema es que el navegador no rota naturalmente la imagen (como puede ver a continuación), y la API de detección de forma necesita que el texto tenga la orientación de lectura correcta.

Utilicé el EXIF-Js library bastante fácil de usar, para detectar la rotación y luego hacer una manipulación básica del lienzo para reorientar la imagen.

EXIF.getData(imgEl, async function() {
  // http://sylvana.net/jpegcrop/exif_orientation.html
  const orientation = EXIF.getTag(this, 'Orientation');
  const [width, height] = (orientation > 4) 
                  ? [ imgEl.naturalWidth, imgEl.naturalHeight ]
                  : [ imgEl.naturalHeight, imgEl.naturalWidth ];

  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext('2d');
  // We have to get the correct orientation for the image
  // See also https://stackoverflow.com/questions/20600800/js-client-side-exif-orientation-rotate-and-mirror-jpeg-images
  switch(orientation) {
    case 2: context.transform(-1, 0, 0, 1, width, 0); break;
    case 3: context.transform(-1, 0, 0, -1, width, height); break;
    case 4: context.transform(1, 0, 0, -1, 0, height); break;
    case 5: context.transform(0, 1, 1, 0, 0, 0); break;
    case 6: context.transform(0, 1, -1, 0, height, 0); break;
    case 7: context.transform(0, -1, -1, 0, height, width); break;
    case 8: context.transform(0, -1, 1, 0, 0, width); break;
  }
  context.drawImage(imgEl, 0, 0);
}

Y Voila, si comparte una imagen en la aplicación, rotará la imagen y luego la analizará devolviendo el resultado del texto que ha encontrado.

Fue increíblemente divertido crear este pequeño experimento, y me ha sido inmediatamente útil. Sin embargo, resalta las inconsistency of the web platform . Estas API no están disponibles en todos los navegadores, ni siquiera están disponibles en todas las versiones de Chrome; esto significa que al escribir este artículo Chrome OS, no puedo usar la aplicación, pero al mismo tiempo, cuando puedo usarla ... Dios mío, tan genial.

Wood Carving found in Engakuji Shrine near Kamakura

Sakura

Paul Kinlan

Me han dicho que, más concretamente, esto es 'Yaezakura'.

Read More