
Usar imagens SVG como espaços reservados é uma idéia muito boa, especialmente em nosso mundo, quando quase todos os sites consistem em várias fotos que estamos tentando carregar de forma assíncrona. Quanto mais imagens e mais volumosas elas são, maior a probabilidade de obter vários problemas, começando pelo fato de o usuário não entender bem o que é carregado lá e terminando com o conhecido salto de toda a interface após o carregamento das imagens. Especialmente na Internet de má qualidade do seu telefone - ele pode voar em várias telas. É nesses momentos que os stubs vêm em socorro. Outra opção para seu uso é a censura. Há momentos em que você precisa ocultar uma imagem do usuário, mas eu gostaria de manter o estilo geral da página, as cores e o local que a imagem ocupa.
Mas na maioria dos artigos, todo mundo fala sobre teoria, que seria bom inserir todas essas imagens em páginas em linha, e hoje veremos na prática como você pode gerá-las ao seu gosto e cor usando o Node.js. Vamos criar modelos de guidão a partir de imagens SVG e preenchê-los de diferentes maneiras, do preenchimento simples com cor ou gradiente à triangulação, mosaico Voronoi e uso de filtros. Todas as ações serão classificadas em etapas. Acredito que este artigo será interessante para iniciantes que estão interessados em como isso é feito e precisam de uma análise detalhada das ações, mas desenvolvedores experientes também podem gostar de algumas idéias.
Preparação
Para começar, iremos a um repositório sem fundo de todos os tipos de coisas chamado NPM. Como a tarefa de gerar nossas imagens stub envolve uma geração única delas no lado do servidor (ou mesmo na máquina do desenvolvedor, se estamos falando de um site mais ou menos estático), não trataremos da otimização prematura. Vamos conectar tudo o que gostamos. Então, começamos com o npm init
spell e prosseguimos com a seleção de dependências.
Para iniciantes, este é o ColorThief . Você provavelmente já ouviu falar dele. Uma biblioteca maravilhosa que pode isolar a paleta de cores das cores mais usadas na imagem. Só precisamos de algo assim para começar.
npm i --save color-thief
Ao instalar este pacote no Linux, ocorreu um problema - alguns pacotes cairo ausentes, que não estão no diretório NPM. Este erro estranho foi resolvido instalando versões de desenvolvimento de algumas bibliotecas:
sudo apt install libcairo2-dev libjpeg-dev libgif-dev
O funcionamento desta ferramenta será observado no processo. Mas não será supérfluo conectar imediatamente o pacote rgb-hex para converter o formato de cores de RGB para Hex, o que é óbvio em seu nome. Não nos envolveremos no ciclismo com funções tão simples.
npm i --save rgb-hex
Do ponto de vista do treinamento, é útil escrever essas coisas você mesmo, mas quando há uma tarefa de montar rapidamente um protótipo que funcione minimamente, é uma boa idéia conectar tudo o que estiver no catálogo do NPM. Economiza uma tonelada de tempo.
Um dos parâmetros mais importantes para plugues são as proporções. Eles devem corresponder às proporções da imagem original. Por conseguinte, precisamos saber seu tamanho. Usaremos o pacote de tamanho da imagem para resolver esse problema.
npm i --save image-size
Como tentaremos criar versões diferentes das imagens e todas elas estarão no formato SVG, de uma forma ou de outra, a questão dos modelos para elas surgirá. É claro que você pode se esquivar das seqüências de caracteres padrão em JS, mas por que tudo isso? É melhor usar um mecanismo de modelo "normal". Por exemplo, guidão . Simples e de bom gosto, pois nossa tarefa será perfeita.
npm i --save handlebars
Não organizaremos imediatamente algum tipo de arquitetura complexa para este experimento. Criamos o arquivo main.js e importamos todas as nossas dependências para lá, além de um módulo para trabalhar com o sistema de arquivos.
const ColorThief = require('color-thief'); const Handlebars = require('handlebars'); const rgbHex = require('rgb-hex'); const sizeOf = require('image-size'); const fs = require('fs');
ColorThief requer inicialização adicional
const thief = new ColorThief();
Usando as dependências que conectamos, resolver os problemas de "enviar uma foto para um script" e "obter o tamanho" não é difícil. Digamos que temos uma imagem 1.jpg:
const image = fs.readFileSync('1.jpg'); const size = sizeOf('1.jpg'); const height = size.height; const width = size.width;
Para pessoas não familiarizadas com o Node.js., vale dizer que quase tudo relacionado ao sistema de arquivos pode acontecer de forma síncrona ou assíncrona. Para métodos síncronos, "Sync" é adicionado no final do nome. Vamos usá-los para não encontrar complicações desnecessárias e não quebrar nossos cérebros do nada.
Vamos para o primeiro exemplo.
Preenchimento de cores

Para começar, resolveremos o problema de simples preenchimento de um retângulo. Nossa imagem terá três parâmetros - largura, altura e cor de preenchimento. Criamos uma imagem SVG com um retângulo, mas em vez desses valores substituímos pares de colchetes e os nomes dos campos que conterão os dados transmitidos a partir do script. Você provavelmente já viu essa sintaxe com o HTML tradicional (por exemplo, o Vue usa algo semelhante), mas ninguém se incomoda em usá-lo com uma imagem SVG - o mecanismo de modelo não se importa com o que será a longo prazo. O texto é ele e o texto na África.
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none' height='{{ height }}' width='{{ width }}'> <rect x='0' y='0' height='100' width='100' fill='{{ color }}' /> </svg>
Além disso, o ColorThief fornece uma das cores mais comuns, no exemplo, é cinza. Para usar o modelo, lemos o arquivo com ele, digamos guidão para que esta biblioteca o compile e depois geramos uma linha com o stub SVG finalizado. O próprio mecanismo de modelo substitui nossos dados (cor e tamanho) nos lugares certos.
function generateOneColor() { const rgb = thief.getColor(image); const color = '#' + rgbHex(...rgb); const template = Handlebars.compile(fs.readFileSync('template-one-color.svg', 'utf-8')); const svg = template({ height, width, color }); fs.writeFileSync('1-one-color.svg', svg, 'utf-8'); }
Resta apenas gravar o resultado em um arquivo. Como você pode ver, trabalhar com o SVG é muito bom - todos os arquivos são de texto, você pode facilmente ler e escrever. O resultado é uma imagem retangular. Nada interessante, mas pelo menos garantimos que a abordagem estava funcionando (um link para as fontes completas estará no final do artigo).
Preenchimento de gradiente
Usar gradientes é uma abordagem mais interessante. Aqui, podemos usar algumas cores comuns da imagem e fazer uma transição suave de uma para outra. Às vezes, isso pode ser encontrado em sites que carregam longas fitas de fotos.

Nosso modelo SVG agora foi expandido com esse mesmo gradiente. Por exemplo, usaremos o gradiente linear usual. Estamos interessados em apenas dois parâmetros - a cor no início e a cor no final:
<defs> <linearGradient id='my-gradient' x1='0%' y1='0%' x2='100%' y2='0%' gradientTransform='rotate(45)'> <stop offset='0%' style='stop-color:{{ startColor }};stop-opacity:1' /> <stop offset='100%' style='stop-color:{{ endColor }};stop-opacity:1' /> </linearGradient> </defs> <rect x='0' y='0' height='100' width='100' fill='url(#my-gradient)' />
As próprias cores são obtidas usando o mesmo ColorThief. Ele tem dois modos de operação - ou fornece uma cor primária ou uma paleta com o número de cores que especificamos. Confortável o suficiente. Para o gradiente, precisamos de duas cores.
Caso contrário, este exemplo é semelhante ao anterior:
function generateGradient() { const palette = thief.getPalette(image, 2); const startColor = '#' + rgbHex(...palette[0]); const endColor = '#' + rgbHex(...palette[1]); const template = Handlebars.compile(fs.readFileSync('template-gradient.svg', 'utf-8')); const svg = template({ height, width, startColor, endColor });
Dessa forma, você pode fazer todos os tipos de gradientes - não necessariamente lineares. Mas ainda assim este é um resultado bastante chato. Seria ótimo criar algum tipo de mosaico que se parecesse remotamente com a imagem original.
Mosaico retangular
Para começar, vamos fazer muitos retângulos e preenchê-los com cores da paleta que a mesma biblioteca nos fornecerá.

O guidão pode fazer muitas coisas diferentes, em particular, possui ciclos. Passaremos a ele uma série de coordenadas e cores, e então ele descobrirá. Nós apenas envolvemos nosso retângulo no modelo em cada um:
{{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='11' width='11' fill='{{ color }}' /> {{/each }}
Assim, no próprio script, agora temos uma paleta de cores completa, percorremos as coordenadas X / Y e fazemos um retângulo com uma cor aleatória da paleta. Tudo é bem simples:
function generateMosaic() { const palette = thief.getPalette(image, 16); palette.forEach(function(color, index) { palette[index] = '#' + rgbHex(...color); }); const rects = []; for (let x = 0; x < 100; x += 10) { for (let y = 0; y < 100; y += 10) { const color = palette[Math.floor(Math.random() * 15)]; rects.push({ x, y, color }); } } const template = Handlebars.compile(fs.readFileSync('template-mosaic.svg', 'utf-8')); const svg = template({ height, width, rects });
Obviamente, o mosaico, embora parecido com a imagem, mas com o arranjo das cores, nem tudo é como gostaríamos. Os recursos do ColorThief nessa área são limitados. Gostaria de obter um mosaico no qual a imagem original seria adivinhada, e não apenas um conjunto de tijolos com mais ou menos as mesmas cores.
Melhorando o mosaico
Aqui temos que ir um pouco mais fundo e obter as cores dos pixels da imagem ...

Como obviamente não temos uma tela no console a partir da qual geralmente obtemos esses dados, usaremos a ajuda na forma de um pacote get-pixels. Ele pode extrair as informações necessárias do buffer com uma imagem que já temos.
npm i --save get-pixels
Será algo parecido com isto:
getPixels(image, 'image/jpg', (err, pixels) => {
Obtemos um objeto que contém o campo de dados - uma matriz de pixels, o mesmo que obtemos da tela. Deixe-me lembrá-lo de que, para obter a cor de um pixel por coordenadas (X, Y), você precisa fazer cálculos simples:
const pixelPosition = 4 * (y * width + x); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ];
Assim, para cada retângulo, podemos pegar a cor não da paleta, mas diretamente da imagem e usá-la. Você obterá algo assim (o principal aqui é não esquecer que as coordenadas na imagem diferem das nossas "normalizadas" de 0 a 100):
function generateImprovedMosaic() { getPixels(image, 'image/jpg', (err, pixels) => { if (err) { console.log(err); return; } const rects = []; for (let x = 0; x < 100; x += 5) { const realX = Math.floor(x * width / 100); for (let y = 0; y < 100; y += 5) { const realY = Math.floor(y * height / 100); const pixelPosition = 4 * (realY * width + realX); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; const color = '#' + rgbHex(...rgb); rects.push({ x, y, color }); } }
Para maior beleza, podemos aumentar um pouco o número de "tijolos", reduzindo seu tamanho. Como não passamos esse tamanho para o modelo (é claro, valeria a pena torná-lo o mesmo parâmetro que a largura ou a altura da imagem), alteraremos os valores de tamanho no próprio modelo:
{{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='6' width='6' fill='{{ color }}' /> {{/each }}
Agora temos um mosaico que realmente se parece com a imagem original, mas ao mesmo tempo ocupa uma ordem de magnitude menos espaço.
Não se esqueça que o GZIP compacta bem essas seqüências repetidas nos arquivos de texto, para que, ao transferir para o navegador, o tamanho dessa visualização se torne ainda menor.
Mas vamos seguir em frente.
Triangulação

Os retângulos são bons, mas os triângulos geralmente dão resultados muito mais interessantes. Então, vamos tentar fazer um mosaico a partir de uma pilha de triângulos. Existem diferentes abordagens para esse problema, usaremos a triangulação de Delaunay :
npm i --save delaunay-triangulate
A principal vantagem do algoritmo que usaremos é que ele evita triângulos com ângulos muito nítidos e obtusos sempre que possível. Para uma imagem bonita, não precisamos de triângulos estreitos e longos.
Esse é um daqueles momentos em que é útil saber quais algoritmos matemáticos em nosso campo existem e qual a diferença entre eles. Não é necessário lembrar de todas as implementações, mas pelo menos é útil saber o que pesquisar no Google.
Divida nossa tarefa em outras menores. Primeiro você precisa gerar pontos para os vértices dos triângulos. E seria bom adicionar alguma aleatoriedade às suas coordenadas:
function generateTriangulation() {
Depois de revisar a estrutura da matriz com triângulos (console.log para nos ajudar), nos encontramos nos pontos em que tomaremos a cor do pixel. Você pode simplesmente calcular a média aritmética das coordenadas dos vértices dos triângulos. Em seguida, movemos os pontos extras da borda extrema para que eles não se arrastem para fora e, tendo recebido coordenadas reais e não normalizadas, obtemos a cor do pixel, que se tornará a cor do triângulo.
const polygons = []; triangles.forEach((triangle) => { let x = Math.floor((basePoints[triangle[0]][0] + basePoints[triangle[1]][0] + basePoints[triangle[2]][0]) / 3); let y = Math.floor((basePoints[triangle[0]][1] + basePoints[triangle[1]][1] + basePoints[triangle[2]][1]) / 3); if (x === 100) { x = 99; } if (y === 100) { y = 99; } const realX = Math.floor(x * width / 100); const realY = Math.floor(y * height / 100); const pixelPosition = 4 * (realY * width + realX); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; const color = '#' + rgbHex(...rgb); const points = ' ' + basePoints[triangle[0]][0] + ',' + basePoints[triangle[0]][1] + ' ' + basePoints[triangle[1]][0] + ',' + basePoints[triangle[1]][1] + ' ' + basePoints[triangle[2]][0] + ',' + basePoints[triangle[2]][1]; polygons.push({ points, color }); });
Resta apenas coletar as coordenadas dos pontos desejados em uma sequência e enviá-la juntamente com a cor ao Guiador para processamento, como fizemos anteriormente.
No próprio modelo, agora não teremos retângulos, mas polígonos:
{{# each polygons }} <polygon points='{{ points }}' style='stroke-width:0.1;stroke:{{ color }};fill:{{ color }}' /> {{/each }}
Triangulação é uma coisa muito interessante. Ao aumentar o número de triângulos, você pode obter apenas imagens bonitas, porque ninguém diz que devemos usá-las apenas como esboços.
Mosaico de Voronoi
Há um problema, o espelho do anterior - uma partição ou um mosaico de Voronoi . Já o usamos ao trabalhar com shaders , mas aqui também pode ser útil.

Como em outros algoritmos conhecidos, temos uma implementação pronta:
npm i --save voronoi
Ações adicionais serão muito semelhantes ao que fizemos no exemplo anterior. A única diferença é que agora temos uma estrutura diferente - em vez de uma matriz de triângulos, temos um objeto complexo. E as opções são um pouco diferentes. Caso contrário, tudo é quase o mesmo. Uma matriz de pontos base é gerada da mesma maneira, pule-a para não tornar a listagem muito longa:
function generateVoronoi() {
Como resultado, obtemos um mosaico de polígonos convexos. Também é um resultado muito interessante.
É útil arredondar todos os números para números inteiros ou pelo menos algumas casas decimais. A precisão excessiva no SVG é completamente desnecessária aqui, apenas aumentará o tamanho das imagens.
Mosaico turva
O último exemplo que veremos é um mosaico embaçado. Temos todo o poder do SVG em nossas mãos, então por que não usar filtros?

Pegue o primeiro mosaico de retângulos e adicione o filtro "desfoque" padrão:
<defs> <filter id='my-filter' x='0' y='0'> <feGaussianBlur in='SourceGraphic' stdDeviation='2' /> </filter> </defs> <g filter='url(#my-filter)'> {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='6' width='6' fill='{{ color }}' /> {{/each }} </g>
O resultado é uma visualização borrada e "censurada" da nossa imagem, que ocupa quase 10 vezes menos espaço (sem compressão), vetor e se estende a qualquer tamanho de tela. Da mesma forma, você pode desfocar o restante de nossos mosaicos.
Ao aplicar esse filtro a um mosaico regular de retângulos, o "efeito jipe" pode resultar; portanto, se você usar algo assim na produção, especialmente em fotos de tamanho grande, pode ser mais bonito aplicar o desfoque não a ele, mas à divisão de Voronoi.
Em vez de uma conclusão
Neste artigo, vimos como você pode gerar todos os tipos de imagens stub SVG no Node.js e garantimos que essa não seja uma tarefa tão difícil se você não escrever tudo manualmente e, se possível, montar módulos prontos. Fontes completas estão disponíveis no github .