Hello.

I am Paul Kinlan.

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

Puppeteer Go

Paul Kinlan

Adoro o Puppeteer - ele me permite brincar com as idéias do The Headless Web - que estão rodando a web em um navegador sem um navegador visível e até mesmo construir ferramentas como o DOM-curl (Curl que executa JavaScript). Adoro especificamente o script do navegador para raspar, manipular e interagir com as páginas.

Uma demonstração que eu queria fazer foi inspirada no post Capturing 422 live images de Ire, onde ela executava um script de marionetista que navegava por muitas páginas e fazia uma captura de tela. Em vez de ir para muitas páginas, eu queria tirar muitas capturas de tela de elementos na página.

O problema que tenho com o Puppeteer é a estrofe de abertura de que você precisa fazer qualquer coisa. Iniciar, abrir guia, navegar - não é complexo, é apenas mais um padrão do que eu quero criar para scripts simples. Por isso criei o Puppeteer Go . É apenas um pequeno script que me ajuda a criar utilitários CLI facilmente, que abre o navegador, navega para uma página, executa sua ação e depois limpa-se.

Confira.

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

O código acima encontrará o elemento h1 no meu blog e fará uma captura de tela. Isso não é nem de longe tão bom quanto o trabalho de Ire, mas achei que era legal ver se conseguimos obter rapidamente capturas de tela do canisuse.com diretamente da 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

Apreciar!

Paul Kinlan

Trying to make the web and developers better.

RSS Github Medium

A simple video insertion tool for EditorJS

Paul Kinlan

Eu realmente gosto do EditorJS . Permiti-me criar uma interface hospedada na web muito simples para o meu blog estático do Hugo.

O EditorJS tem a maior parte do que eu preciso em um editor simples baseado em bloco. Possui um plug-in para cabeçalhos, código e até uma maneira simples de adicionar imagens ao editor sem exigir infraestrutura de hospedagem. Até agora, não há uma maneira simples de adicionar vídeos ao editor.

Tomei a simple-image repositório de plug-in e mudou-se (apenas um pouco) para criar um simple-video plug-in ( npm module ). Agora eu posso incluir vídeos facilmente neste blog.

Se você conhece o EditorJS, é bastante simples incluir em seus projetos. Basta instalá-lo da seguinte maneira

npm i simple-video-editorjs

Depois, basta incluí-lo no seu projeto como achar melhor.

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

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

O editor possui algumas opções simples que permitem configurar como o vídeo deve ser hospedado na página:

  1. Reprodução automática - o vídeo será reproduzido automaticamente quando a página for carregada
  2. mudo - o vídeo não terá som por padrão (necessário para reprodução automática)
  3. Controles - o vídeo terá os controles HTML padrão.

Abaixo está um exemplo rápido de um vídeo incorporado (e mostrando algumas das opções).

Enfim, me diverti criando esse pequeno plug-in - não foi tão difícil de criar e a única coisa que fiz foi adiar a conversão para base64 que as imagens simples usam e, em vez disso, apenas usar os URLs do Blob.

Test post Video upload

Friendly Project Name Generator with Zeit

Paul Kinlan

Eu tenho algumas idéias para projetos que facilitam a criação de sites na Web - uma das idéias é criar um netlify-like drag and drop interface para projetos baseados em zeit (eu gosto do zeit, mas requer um pouco de magia do cli para implantar).

Este post aborda apenas uma pequena parte do quebra-cabeça: a criação de nomes de projetos.

Glitch é um bom exemplo disso, quando você cria um projeto, ele fornece um nome caprichoso gerado aleatoriamente. A equipe também criou um good dictionary of fairly safe words que combina bem (e se você quiser que eles tenham um servidor simples para hospedar).

Portanto, o projeto paralelo neste domingo foi criar um micro-serviço simples para gerar nomes de projetos aleatórios usando o serverless-functions do Zeit e o dicionário da Glitch.

And here it is ( code ), é bem curto e não é muito complexo.

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 }

Se você não quiser incluí-lo diretamente no seu projeto, poderá usar o terminal HTTP para gerar nomes de projetos aleatórios (na forma de "XY") fazendo uma solicitação da Web para https: // friendly-project-name. kinlan.now.sh/api/names, que retornará algo como o seguinte.

["momentous-professor"]

Você também pode controlar quantos nomes serão gerados com o parâmetro de string de consulta de count = x , por exemplo, 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"]

Você pode controlar o separador com o parâmetro de uma string de consulta do separador. por exemplo, separator = @, por exemplo, https://friendly-project-name.kinlan.now.sh/api/names?separator=@

["handsomely@asterisk"]

Um aspecto muito útil deste projeto é que, se uma combinação de palavras tende a ser ofensiva, é fácil atualizar o repositório Glitch para garantir que isso não ocorra novamente.

Supondo que a hospedagem do projeto não fique muito cara, eu continuarei com o serviço, mas sinta-se à vontade para cloná-lo se desejar criar um microsserviço semelhante.

Exemplo ao vivo

A seguir, é apresentado um exemplo super rápido da API em ação.

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

Resposta única



Muitas respostas



personalizados



{{<raw-html>}}

{{</ raw-html>}}

Frankie and Bennys: Pay for your meal via the web

Paul Kinlan

Sempre que vejo um restaurante dizer que você pode pagar no celular, eu sempre o verifico, principalmente para que eu possa lamentar o fato de que você precisa usar um aplicativo. Imagine minha surpresa quando o código QR levar a um fluxo de pagamentos baseado na Web ….. e funcionou. Trabalho impressionante Frankie e Benny! Nesse momento, selecionei o Google Pay, mas não funcionou (e-mail enviado internamente!

Read More

Podroll

Paul Kinlan

Adoro podcasts e ouço muitos, mas ainda acho que descobrir novos podcasts é um processo bastante difícil e confio em amigos para compartilhar o que eles ouvem. Então, com isso em mente, aqui estão minhas podroll . Eu uso o Player.fm criado pelo meu bom amigo Mike Mahemoff, e que pode compartilhar e exportar sua lista de assinaturas. Esta página será refreshed frequently using my little script .

Read More

Adding "dark mode" to my blog

Paul Kinlan

Eu vi post about adding dark mode to his blog Jeremy Keith e parecia simples, então decidi dar uma volta. Aqui está o diff of the work para todos verem. Foi surpreendentemente fácil (fora de erros bobos da minha parte). Havia um pequeno refator para suportar variáveis CSS e garantir que eu tivesse fallback se houver um navegador que não suporte propriedades personalizadas de CSS, mas é isso. Eu fiz praticamente a mesma coisa que Jeremy.

Read More

Using Web Mentions in a static site (Hugo)

Paul Kinlan

Meu blog é um site totalmente estático, construído com Hugo e hospedado com Zeit. Esta é uma ótima solução para mim, um blog simples tem um processo de implantação bastante simples e carrega incrivelmente rápido. Sites gerados estaticamente têm algumas desvantagens, a maior delas é quando você precisa de algo dinâmico para ser integrado à sua página (comentários, por exemplo). Não poder hospedar conteúdo dinâmico com facilidade significa que você acaba confiando no JavaScript de terceiros que obterá acesso total à sua página e você não saberá o que está fazendo - isso pode estar rastreando seus usuários ou tornando sua página mais lenta carga.

Read More

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

Paul Kinlan

Atualização do ### : 8 de outubro - Problemas significativos com este documento. Eu Jake Archibald com o Jake Archibald sobre este post porque achava que tinha algo novo. Durante a conversa, descobrimos muitas coisas que invalidam parte deste post e também aprendi muito no processo que não acho que a maioria dos desenvolvedores conhecer. Chamar .append() e .appendChild() adota o nó. Isso torna inútil o uso do adoptNode nesta instância, porque o algoritmo anexado garante que o nó seja adotado.

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

Recentemente, saí de férias com os meninos e passamos por Carlisle Castle (local de nascimento do rebote mundial, Jake Archibald). É um dos melhores castelos que já visitamos no Reino Unido e recomendo vivamente passar algum tempo visitando-o, se você estiver na área. Eu não sabia o quão significativo era o papel que Carlisle Castle desempenhou na história da Inglaterra e da Escócia, e foi ótimo descobrir mais enquanto estávamos lá.

Read More

Idle observation: Indexing text in images

Paul Kinlan

Eu estava com os meninos em Llangollen outro dia (é uma cidade bonita) e estava tirando fotos dos sinais informativos que continham um pouco da história da área para que eu pudesse ler mais tarde, e pensei em olhar na web para ver se as informações estavam disponíveis para mais do que apenas as pessoas que passavam pela placa - e não estão. Então eu pensei sobre os folhetos, o conteúdo neles é quase impossível de encontrar na web.

Read More

Liverpool World Museum

Paul Kinlan

Levei as crianças ao Museu de História Mundial de Liverpool na outra semana, foi bem legal. A seção Espaço e Tempo não mudou em cerca de 30 anos, uma grande parte do gabinete de insetos foi fechada e o Aquarium parecia um pouco menor do que eu me lembro. A seção egípcia estava aberta (não foi quando eu fui pela última vez) e foi bastante incrível.

Read More

Bookstore - Llangollen

Paul Kinlan

Eu amo este lugar, é no topo de um café em Llangollen. Eu vim aqui com meus avós há quase 30 anos e é praticamente o mesmo agora. Meu único desejo é que houvesse ainda mais livros da Commix - eu juro que havia muito mais quando criança. Check it out

Read More

Webmention.app

Paul Kinlan

Adoro a ideia do Webmentions , mas ainda não tive tempo de implementá-la no meu site. Em um alto nível, as menções na web permitem que você comente, curta e responda a outros conteúdos na web e faça com que fique visível para esse conteúdo sem ser centralizado com ferramentas como o Disqus (que tenho o desejo de remover do meu site).

As Menções na Web são divididas em dois componentes, o remetente e o destinatário. O destinatário é o site sobre o qual estou escrevendo um post e eles podem ter algo no site que mostra links de entrada ou reações ao blog; e o remetente sou, bem, eu. Eu preciso deixar o site remoto que eu escrevi ou reagir a algum conteúdo que eles criaram.

O impressionante Remy Sharp criou o webmention.app para resolver uma parte do problema: enviar pings. A ferramenta de Remy facilita o envio de 'pings' para potenciais receptores aos quais eu me vinculei, simplesmente chamando um script CLI.

Eu hospedo meu blog usando Zeit usando Hugo e a ferramenta static-builder, então foi relatively trivial for me to add in support for webmention app . Eu apenas npm i webmention e depois chamo a versão CLI da ferramenta do meu arquivo build.sh - é realmente simples assim.

Agora, quando eu criar uma postagem, ela deverá enviar um ping rápido a todos os novos URLs que eu criei algum conteúdo sobre o site deles.

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

Paul Kinlan

Meu site é entirely static . Ele foi construído com o Hugo e hospedado com o Zeit . Estou muito feliz com a configuração, chego perto de compilações instantâneas e entrega de conteúdo super rápida com CDN e posso fazer tudo o que preciso, porque não preciso gerenciar nenhum estado. Eu criei um simple UI para este site e também o meu podcast creator que me permite publicar rapidamente novo conteúdo no meu site hospedado estaticamente.

Read More

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

Paul Kinlan

Eu tenho o objetivo de criar o software de gravação de tela mais simples do mundo e tenho andado lentamente pesquisando o projeto nos últimos meses (quero dizer, bem devagar).

Nas postagens anteriores, eu obtive o screen recording and a voice overlay futzing com os fluxos de todas as fontes de entrada. Uma área de frustração, porém, foi que eu não conseguia descobrir como obter o áudio da área de trabalho * e * sobrepor o áudio do alto-falante. Eu finalmente descobri como fazê-lo.

Em primeiro lugar, o getDisplayMedia no Chrome agora permite a captura de áudio, parece uma supervisão estranha na Especificação, pois não permite especificar o audio: true na chamada de função, agora você pode.

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

Em segundo lugar, originalmente eu pensava que, ao criar duas faixas no fluxo de áudio, conseguiria o que queria, mas aprendi que a API MediaRecorder do Chrome só pode MediaRecorder uma faixa e a segunda não funcionaria, porque as faixas são como as várias faixas de áudio do DVD, em que apenas uma pode ser reproduzida por vez.

A solução é provavelmente simples para muitas pessoas, mas era nova para mim: use o áudio da Web.

Acontece que a API do WebAudio possui createMediaStreamSource e createMediaStreamDestination , as quais são necessárias para resolver o problema. O createMediaStreamSource pode receber fluxos do meu áudio e microfone da área de trabalho e, ao conectar os dois no objeto criado pelo createMediaStreamDestination , é possível canalizar esse fluxo na API do 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.

O código completo pode ser encontrado em my glitch , e a demonstração pode ser encontrada aqui: https://screen-record-voice.glitch.me/

{{<oGIdqcMFKlA do YouTube>}}

Extracting text from an image: Experiments with Shape Detection

Paul Kinlan

Eu tive um tempo de inatividade após o Google IO e queria coçar uma coceira a longo prazo que tive. Eu só quero copiar o texto contido nas imagens do navegador. Isso é tudo. Eu acho que seria um recurso interessante para todos.

Não é fácil adicionar funcionalidade diretamente ao Chrome, mas sei que posso tirar proveito do sistema de intenções no Android e agora posso fazer isso com a Web (ou pelo menos o Chrome no Android).

Duas novas adições à plataforma da Web - o Nível de Destino de Compartilhamento 2 (ou como eu gostaria de chamá-lo de Compartilhamento de Arquivos) e o TextDetector na API de Detecção de Forma - have allowed me to build a utility that I can Share images to and get the text held inside them .

A implementação básica é relativamente direta, você cria um destino de compartilhamento e um manipulador no Service Worker e, depois de ter a imagem que o usuário compartilhou, você executa o TextDetector nele.

O Share Target API permite que seu aplicativo da Web faça parte do subsistema de compartilhamento nativo e, nesse caso, agora você pode se registrar para lidar com todos os tipos de image/* , declarando-o dentro do Web App Manifest seguinte maneira.

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

Quando o seu PWA estiver instalado, você o verá em todos os lugares em que compartilha imagens, da seguinte maneira:

A API Share Target trata o compartilhamento de arquivos como uma postagem de formulário. Quando o arquivo é compartilhado com o Web App, o trabalhador do serviço é ativado, o manipulador fetch é chamado com os dados do arquivo. Os dados agora estão dentro do Service Worker, mas eu preciso deles na janela atual para poder processá-los, o serviço sabe qual janela invocou a solicitação, para que você possa direcionar facilmente o cliente e enviar os dados.

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

Depois que a imagem está na interface do usuário, eu a processo com a API de detecção 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);
    });
  };
  ...
};

O maior problema é que o navegador não gira naturalmente a imagem (como você pode ver abaixo), e a API de detecção de forma precisa que o texto esteja na orientação correta de leitura.

Usei o EXIF-Js library bastante fácil de usar para detectar a rotação e, em seguida, fiz alguma manipulação básica da tela para reorientar a imagem.

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

E, se você compartilhar uma imagem com o aplicativo, ele girará a imagem e analisará retornando a saída do texto encontrado.

Foi incrivelmente divertido criar esse pequeno experimento e foi imediatamente útil para mim. No entanto, destaca as inconsistency of the web platform . Essas APIs não estão disponíveis em todos os navegadores, nem em todas as versões do Chrome. Isso significa que, enquanto escrevo este artigo, o Chrome OS não posso usar o aplicativo, mas ao mesmo tempo quando posso usá-lo … OMG, tão legal.

Wood Carving found in Engakuji Shrine near Kamakura

Sakura

Paul Kinlan

Me disseram que mais especificamente que isso é 'Yaezakura'

Read More