Hello.

I am Paul Kinlan.

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

Puppeteer Go🔗

Paul Kinlan

J'adore Puppeteer - cela me permet de jouer avec les idées de The Headless Web - c'est-à-dire faire tourner le Web dans un navigateur sans navigateur visible et même construire des outils comme DOM-curl (Curl DOM-curl JavaScript). En particulier, j'adore écrire des scripts dans le navigateur pour gratter, manipuler et interagir avec des pages.

Une démo que je voulais faire était inspirée par le message Capturing 422 live images d'Ire, Capturing 422 live images lequel elle Capturing 422 live images un script de marionnettiste qui permettait de parcourir plusieurs pages et de prendre une capture d'écran. Au lieu d'aller sur plusieurs pages, je voulais prendre de nombreuses captures d'écran des éléments de la page.

Le problème que j'ai avec Puppeteer est la strophe d'ouverture sur laquelle vous devez faire quelque chose. Lancement, onglet Ouvrir, navigation - ce n’est pas complexe, c’est plus que ce que je veux créer pour de simples scripts. C'est pourquoi j'ai créé Puppeteer Go . C'est juste un petit script qui m'aide à créer facilement des utilitaires CLI qui ouvrent le navigateur, accèdent à une page, effectuent l'action votre et se nettoient ensuite.

Vérifiez-le.

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

Le code ci-dessus va trouver l'élément h1 dans mon blog et prendre une capture d'écran. C’est loin d’être aussi bon que le travail d’Ire, mais j’ai pensé qu’il était intéressant de voir si nous pouvons rapidement extraire des captures d’écran de canisuse.com directement à partir de la page.

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

Prendre plaisir!

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

J'aime beaucoup EditorJS . Cela me permet de créer une interface très simple hébergée sur le Web pour mon blog statique Hugo.

EditorJS a presque tout ce dont j'ai besoin dans un éditeur simple basé sur des blocs. Il possède un plugin pour les en-têtes, le code et même un moyen simple d'ajouter des images à l'éditeur sans nécessiter une infrastructure d'hébergement. Jusqu'à présent, il ne disposait pas d'un moyen simple d'ajouter des vidéos à l'éditeur.

J'ai pris le simple-image plug-in simple-image l' simple-image modifié (juste un peu) pour créer un simple-video in npm module ( npm module ). Maintenant, je peux facilement inclure des vidéos dans ce blog.

Si vous êtes familier avec EditorJS, il est assez simple d'inclure dans vos projets. Il suffit de l'installer comme suit

npm i simple-video-editorjs

Et ensuite, incluez-le dans votre projet comme bon vous semble.

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

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

L'éditeur propose des options simples vous permettant de configurer le mode d'hébergement de la vidéo dans la page:

  1. Lecture automatique - la vidéo sera-t-elle lue automatiquement lorsque la page sera chargée?
  2. muet - le son de la vidéo n’est-il pas activé par défaut (nécessaire pour la lecture automatique)
  3. contrôles - la vidéo aura-t-elle les contrôles HTML par défaut.

Vous trouverez ci-dessous un exemple rapide de vidéo intégrée (et montrant certaines des options).

Quoi qu’il en soit, je me suis amusé à créer ce petit plugin. Ce n’était pas si difficile à créer et la seule chose que j’ai fait est de différer la conversion en base64 qui utilise des images simples et d’utiliser plutôt les URLs Blob.

Test post Video upload

Friendly Project Name Generator with Zeit🔗

Paul Kinlan

J'ai quelques idées de projets qui facilitent la création de sites sur le Web. L'une des idées est de créer un projet netlify-like drag and drop interface pour zeit (j'aime zeit mais cela nécessite un peu de cli magie à déployer).

Cet article ne couvre qu'un petit morceau du casse-tête: créer des noms de projets.

Glitch est un bon exemple. Lorsque vous créez un projet, il lui donne un nom fantaisiste généré aléatoirement. L’équipe a également créé un good dictionary of fairly safe words qui combine bien (et si vous le souhaitez, ils ont un serveur simple à héberger).

Le projet parallèle de ce dimanche consistait donc à créer un micro-service simple permettant de générer des noms de projet aléatoires à l’aide de serverless-functions de Zeit et du dictionnaire de Glitch.

And here it is ( code ), c'est assez court et pas trop complexe.

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 vous ne souhaitez pas l'inclure directement dans votre projet, vous pouvez utiliser le point de terminaison HTTP pour générer des noms de projet aléatoires (sous la forme "XY") en envoyant une demande Web à l'adresse https: // nom-projet-convivial. kinlan.now.sh/api/names, qui retournera quelque chose comme ce qui suit.

["momentous-professor"]

Vous pouvez également contrôler le nombre de noms à générer avec le paramètre de chaîne de requête count = x , par exemple 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"]

Vous pouvez contrôler separator avec le paramètre query-string de separator. c'est-à-dire, separator = @, par exemple https://friendly-project-name.kinlan.now.sh/api/names?separator=@

["handsomely@asterisk"]

Un aspect très utile de ce projet est que, si une combinaison de mots a tendance à être offensante, il est facile de mettre à jour le rapport Glitch pour s’assurer que cela ne se reproduise plus.

En supposant que l'hébergement du projet ne soit pas trop coûteux, je maintiendrai le service, mais n'hésitez pas à le cloner vous-même si vous souhaitez un jour créer un micro-service similaire.

Exemple live

Ce qui suit est un exemple très rapide de l’API en action.

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

réponse unique


Beaucoup de réponses


Séparateurs personnalisés


{{<raw-html>}}

{{</ raw-html>}}

Frankie and Bennys: Pay for your meal via the web

Paul Kinlan

Chaque fois que je vois un restaurant dire que vous pouvez payer sur mobile, je le vérifie toujours, surtout pour pouvoir déplorer le fait que vous deviez utiliser une application. Imaginez ma surprise lorsque le code QR mène à un flux de paiements basé sur le Web ..... et que cela a fonctionné. Super travail Frankie et Benny! À ce stade, j'ai sélectionné Google Pay, mais cela n'a pas fonctionné (courrier électronique envoyé en interne!

Read More

Podroll

Adding "dark mode" to my blog

Paul Kinlan

J'ai vu les post about adding dark mode to his blog Jeremy Keith et cela semblait simple, alors j'ai décidé de le faire tourner. Voici le diff of the work la diff of the work de tous. C'était étonnamment facile (en dehors des erreurs stupides de ma part). Il y avait un petit refactor pour prendre en charge les variables CSS et assurer ma solution de repli s'il y avait un navigateur qui ne prend pas en charge les propriétés personnalisées CSS, mais c'est à peu près tout.

Read More

Using Web Mentions in a static site (Hugo)

Paul Kinlan

Mon blog est un site entièrement statique, construit avec Hugo et hébergé avec Zeit. C'est une excellente solution pour moi, un simple blog a un processus de déploiement assez simple et son chargement est extrêmement rapide. Les sites générés statiquement présentent certains inconvénients, le plus important étant lorsque vous avez besoin d'intégrer quelque chose de dynamique dans votre page (commentaires, par exemple). Ne pas être en mesure d'héberger facilement du contenu dynamique signifie que vous finirez par vous fier à du code JavaScript tiers qui obtiendra alors un accès complet à votre page.

Read More

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

Paul Kinlan

Mise à jour: 8 octobre - Problèmes importants avec ce doc. J'ai rattrapé Jake Archibald propos de ce post parce que je pensais avoir quelque chose de nouveau. Au cours de la conversation, nous avons découvert beaucoup de choses qui rendent certains de ces posts invalides, et j'ai également beaucoup appris au cours du processus que la plupart des développeurs ne pensent pas connaître. L'appel de .append() et .appendChild() adopte le nœud.

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

Je suis récemment allé en vacances avec les garçons et nous avons passé au- Carlisle Castle (le berceau du rebond mondial Jake Archibald). C'est l'un des meilleurs châteaux que nous avons visités au Royaume-Uni et je recommanderais vivement de passer un peu de temps à le visiter si vous vous trouvez dans la région. Le rôle joué par le château de Carlisle dans l’histoire de l’Angleterre et de l’Écosse n’était pas significatif et c’était génial de pouvoir en savoir plus pendant notre séjour.

Read More

Idle observation: Indexing text in images

Paul Kinlan

L’autre jour, j’étais avec les garçons à Llangollen (c’est une belle ville) et je prenais des photos des panneaux d’information contenant une partie de l’histoire de la région pour pouvoir la lire plus tard, et j’ai pensé que j’aimerais regarder sur le Web pour voir si l’information était disponible pour plus que les personnes qui passaient devant le panneau - et ce n’est pas le cas. J'ai ensuite pensé aux brochures, leur contenu est presque impossible à trouver sur le Web.

Read More

Liverpool World Museum

Paul Kinlan

J'ai emmené les enfants au musée d'histoire mondiale de Liverpool la semaine dernière, c'était plutôt chouette. La section Espace et Temps n’a pas changé depuis environ 30 ans, une grande partie de l’enceinte Bug a été fermée et l’Aquarium semblait un peu plus petit que ce dont je me souviens. La section égyptienne était ouverte (ce n'était pas la dernière fois que je suis allée) et c'était plutôt génial.

Read More

Bookstore - Llangollen

Paul Kinlan

J'adore cet endroit, il est au sommet d'un café à Llangollen. Je suis venu ici avec mes grands-parents il y a près de 30 ans et c'est à peu près la même chose maintenant. Mon seul souhait est qu'il y ait encore plus de livres Commix - je jure qu'il y en avait beaucoup plus quand j'étais enfant. Check it out

Read More

Webmention.app🔗

Paul Kinlan

J'aime l'idée de Webmentions , mais je n'ai pas encore eu le temps de la mettre en œuvre sur mon site. Sur un site Web de haut niveau, les mentions vous permettent de commenter, de répondre à d’autres contenus sur le Web et de les rendre visibles sans les centraliser avec des outils tels que Disqus (que je souhaite supprimer de mon site).

Les mentions Web sont divisées en deux composants, l'expéditeur et le destinataire. Le destinataire est le site sur lequel j'écris un article et il se peut qu'il y ait quelque chose sur leur site qui montre des liens entrants ou des réactions à leur blog; et l'expéditeur est bien moi. Je dois laisser le site distant que j'ai écrit ou réagi au contenu qu'ils ont créé.

Remy Sharp plutôt génial, Remy Sharp créé webmention.app pour résoudre une partie du problème: l'envoi de pings. L'outil de Remy facilite l'envoi de "pings" aux destinataires potentiels que j'ai liés, en appelant simplement un script CLI.

J'organise mon blog en utilisant Zeit en utilisant Hugo et l'outil static-builder, donc relatively trivial for me to add in support for webmention app . Je viens de npm i webmention , puis j'appelle la version CLI de l'outil à partir de mon fichier build.sh . C'est aussi simple que cela.

Maintenant, lorsque je crée un article, il devrait envoyer un ping rapide à toutes les nouvelles URL pour lesquelles j'ai créé du contenu sur leur site.

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

Paul Kinlan

Mon site est entirely static . Il est construit avec Hugo et hébergé avec Zeit . Je suis assez content de la configuration, je reçois des versions quasi-instantanées et une livraison de contenu CDN très rapide, et je peux faire tout ce dont j'ai besoin car je ne dois gérer aucun état. J'ai créé un simple UI pour ce site ainsi que mon podcast creator qui me permet de publier rapidement un nouveau contenu sur mon site hébergé de manière statique.

Read More

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

Paul Kinlan

Mon objectif est de créer le logiciel de capture d'écran le plus simple au monde et je m'occupe du projet lentement au cours des deux derniers mois (je veux dire très lentement).

Dans les articles précédents, j'avais eu le screen recording and a voice overlay en utilisant les flux de toutes les sources d'entrée. Un point de frustration cependant était que je ne pouvais pas savoir comment obtenir le son du bureau * et * superposer le son du haut-parleur. J'ai finalement trouvé comment le faire.

Tout d’abord, getDisplayMedia dans Chrome permet désormais la capture audio. Il semble qu’il getDisplayMedia un oubli dans la spécification en ce sens qu’il ne vous permettait pas de spécifier audio: true dans l’appel de la fonction, c’est désormais possible.

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

Deuxièmement, j'avais d'abord pensé qu'en créant deux pistes dans le flux audio, je pouvais obtenir ce que je voulais. Cependant, j'ai appris que l'API MediaRecorder de Chrome ne peut générer qu'une seule piste et, 2e, cela n'aurait pas fonctionné ressemblent aux DVD multiples pistes audio dans la mesure où un seul peut jouer à la fois.

La solution est probablement simple pour beaucoup de gens, mais elle était nouvelle pour moi: Utiliser Web Audio.

Il s'avère que les API WebAudio ont createMediaStreamSource et createMediaStreamDestination , qui sont toutes deux des API nécessaires à la résolution du problème. Le createMediaStreamSource peut prendre des flux de l'audio et du microphone de mon bureau. En reliant les deux ensemble à l'objet créé par createMediaStreamDestination il me permet de createMediaStreamDestination ce flux vers l'API 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.

Le code complet peut être trouvé sur my glitch , et la démo peut être trouvée ici: https://screen-record-voice.glitch.me/

{{<fast-youtube oGIdqcMFKlA>}}

Extracting text from an image: Experiments with Shape Detection🔗

Paul Kinlan

J'ai eu un peu de temps après Google IO et je voulais me débarrasser de mes démangeaisons à long terme. Je veux juste pouvoir copier du texte contenu dans des images du navigateur. C'est tout. Je pense que ce serait une fonctionnalité intéressante pour tout le monde.

Il n'est pas facile d'ajouter des fonctionnalités directement dans Chrome, mais je sais que je peux tirer parti du système d'intention sur Android et que je peux maintenant le faire avec le Web (ou au moins Chrome sur Android).

Deux nouveaux ajouts à la plate-forme Web - Partage de niveau cible 2 (ou comme je l’appelle partage de fichiers) et TextDetector dans l’API de détection de forme - have allowed me to build a utility that I can Share images to and get the text held inside them .

L'implémentation de base est relativement simple, vous créez une cible de partage et un gestionnaire dans le Service Worker, puis une fois que vous avez l'image partagée par l'utilisateur, vous exécutez le TextDetector dessus.

Share Target API permet à votre application Web de faire partie du sous-système de partage natif. Dans ce cas, vous pouvez maintenant vous enregistrer pour gérer tous les types image/* en le déclarant comme suit dans votre Web App Manifest .

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

Lorsque votre PWA est installé, vous le verrez dans tous les endroits où vous partagez des images, comme suit:

L'API Share Target traite le partage de fichiers comme une publication de formulaire. Lorsque le fichier est partagé avec Web App, l'agent de service est activé, le gestionnaire fetch est fetch avec les données du fichier. Les données se trouvent maintenant dans Service Worker, mais j'en ai besoin dans la fenêtre actuelle pour pouvoir les traiter. Le service sait quelle fenêtre a appelé la demande. Vous pouvez ainsi facilement cibler le client et lui envoyer les données.

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

Une fois l'image dans l'interface utilisateur, je la traite ensuite avec l'API de détection de texte.

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

Le plus gros problème est que le navigateur ne fait pas naturellement pivoter l'image (comme vous pouvez le voir ci-dessous), et l'API de détection de la forme a besoin que le texte soit dans le sens de la lecture.

J'ai utilisé EXIF-Js library assez facile à utiliser, pour détecter la rotation, puis pour effectuer quelques manipulations de base sur la toile afin de réorienter l'image.

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

Et voila, si vous partagez une image avec l'application, celle-ci la fera pivoter puis analysera en renvoyant la sortie du texte qu'elle aura trouvé.

C'était très amusant de créer cette petite expérience et cela m'a été immédiatement utile. Il met toutefois en évidence les inconsistency of the web platform . Ces API ne sont pas disponibles dans tous les navigateurs. Elles ne sont même pas disponibles dans toutes les versions de Chrome. Cela signifie qu'en écrivant cet article Chrome OS, je ne peux pas utiliser l'application, mais en même temps, quand je peux l'utiliser. ... OMG, tellement cool.

Wood Carving found in Engakuji Shrine near Kamakura

Sakura