我们在Node.js上生成漂亮的SVG占位符


使用SVG图像作为占位符是一个很好的主意,尤其是在我们的世界中,当几乎所有站点都包含一堆我们试图异步加载的图片时,尤其如此。 图片越多,数量越多,出现各种问题的可能性就越高,这始于用户不太了解那里加载了什么,然后以加载图片后整个界面的著名跳跃结束。 尤其是在手机上网效果不佳的情况下,它会在多个屏幕上飞走。 正是在这样的时刻,存根才得以解救。 使用它们的另一种选择是检查。 有时您需要向用户隐藏图片,但我想保留页面的整体样式,颜色以及图片所占的位置。


但是在大多数文章中,每个人都在谈论理论,将所有这些存根图像插入到页面中将是很好的选择,今天我们将在实践中看到如何使用Node.js生成符合您的口味和颜色的图像。 我们将根据SVG图像创建车把模板,并以不同的方式填充它们,从简单的颜色或渐变填充到三角剖分,Voronoi马赛克以及使用滤镜。 所有操作将分步进行。 我相信对于感兴趣的初学者来说,这篇文章会很有趣,他们需要如何做,并且需要对操作进行详细的分析,但是有经验的开发人员也可能喜欢一些想法。


准备工作


首先,我们将进入一个称为NPM的所有内容的无底存储库。 由于生成存根映像的任务涉及在服务器端(甚至在开发人员的机器上,如果我们谈论的是或多或少的静态站点)一次生成它们的存根映像,因此我们不会处理过早的优化。 我们将连接我们喜欢的一切。 因此,我们从npm init拼写开始,然后进行依赖项的选择。


对于初学者来说,这是ColorThief 。 您可能已经听说过他。 一个很棒的库,可以隔离图片中最常用颜色的调色板。 首先,我们只需要类似的东西。


 npm i --save color-thief 

在Linux下安装此软件包时,出现了一个问题-一些缺少的cairo软件包,该软件包不在NPM目录中。 通过安装某些库的开发版本,解决了此奇怪的错误:


 sudo apt install libcairo2-dev libjpeg-dev libgif-dev 

在此过程中将观察该工具的工作方式。 但是立即连接rgb-hex包以将颜色格式从RGB转换为Hex并不是多余的,这从其名称可以明显看出。 我们不会使用如此简单的功能进行骑行。


 npm i --save rgb-hex 

从培训的角度来看,自己编写这样的东西很有用,但是当任务是快速组装一个工作量最小的原型时,然后将NPM目录中的所有内容连接起来是一个好主意。 节省大量时间。

塞子最重要的参数之一是比例。 它们必须匹配原始图像的比例。 因此,我们需要知道它的大小。 我们将使用图像大小包来解决此问题。


 npm i --save image-size 

由于我们将尝试制作不同版本的图片,并且它们都将采用SVG格式,因此会出现一种或多种方式的图片模板问题。 您当然可以在JS中使用模式字符串进行闪避,但是为什么要所有这些呢? 最好使用“常规”模板引擎。 例如, 把手 。 简单而有品位,对于我们的任务将是正确的。


 npm i --save handlebars 

我们不会立即为该实验安排某种复杂的体系结构。 我们创建main.js文件,并将所有依赖项导入其中,并导入一个用于处理文件系统的模块。


 const ColorThief = require('color-thief'); const Handlebars = require('handlebars'); const rgbHex = require('rgb-hex'); const sizeOf = require('image-size'); const fs = require('fs'); 

ColorThief需要额外的初始化


 const thief = new ColorThief(); 

使用我们连接的依赖关系,解决“将图片上传到脚本”和“获取图片的大小”的问题并不困难。 假设我们有一张图片1.jpg:


 const image = fs.readFileSync('1.jpg'); const size = sizeOf('1.jpg'); const height = size.height; const width = size.width; 

对于不熟悉Node.js的人来说,值得一提的是,几乎所有与文件系统相关的事物都可以同步或异步发生。 对于同步方法,在名称末尾添加“同步”。 我们将使用它们,以免遇到不必要的并发症,也不会使我们的大脑发疯。


让我们继续第一个示例。


颜色填充



首先,我们将解决简单填充矩形的问题。 我们的图片将具有三个参数-宽度,高度和填充颜色。 我们用一个矩形制作一个SVG图像,但是代替这些值,我们替换成对的括号和将包含从脚本传输的数据的字段名称。 您可能已经在传统的HTML中看到了这种语法(例如,Vue使用了类似的东西),但是没有人愿意将它与SVG图像一起使用-模板引擎并不关心它的长远发展。 文字是他和非洲的文字。


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

此外,ColorThief为我们提供了最常见的颜色之一,在示例中为灰色。 为了使用模板,我们用它读取文件,例如句柄,以便该库对其进行编译,然后使用完成的SVG存根生成一行。 模板引擎本身会在正确的位置替换我们的数据(颜色和大小)。


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

仅保留将结果写入文件。 如您所见,使用SVG非常好-所有文件都是文本,您可以轻松地读写它们。 结果是一个矩形图片。 没什么有趣的,但至少我们确保该方法有效(在本文结尾处有到完整源的链接)。


渐变填充


使用渐变是一种更有趣的方法。 在这里,我们可以使用图片中的几种常见颜色,并实现从一个到另一个的平滑过渡。 有时可以在加载长色带的站点上找到它。



现在,我们的SVG模板已使用此渐变进行了扩展。 例如,我们将使用通常的线性渐变。 我们只对两个参数感兴趣-开头的颜色和结尾的颜色:


 <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)' /> 

颜色本身是使用相同的ColorThief获得的。 它有两种操作模式-要么给我们提供一种原色,要么为调色板提供我们指定的颜色数量。 足够舒适。 对于渐变,我们需要两种颜色。


否则,此示例与上一个示例相似:


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

这样,您可以制作各种渐变-不一定是线性的。 但这仍然是一个相当无聊的结果。 制作某种与原始图像遥遥相似的马赛克会很棒。


矩形马赛克


首先,让我们制作很多矩形,并用同一个库将为我们提供的调色板中的颜色填充它们。



车把可以做很多不同的事情,特别是有循环。 我们将向他传递一系列坐标和颜色,然后他会弄清楚。 我们只将矩形包装在每个模板中:


 {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='11' width='11' fill='{{ color }}' /> {{/each }} 

因此,在脚本本身中,我们现在有了一个完整的调色板,在X / Y坐标中循环,并从调色板中创建一个具有随机颜色的矩形。 一切都非常简单:


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

显然,马赛克虽然在颜色上与图片相似,但是在颜色的排列上,一切都不尽如人意。 ColorThief在此领域的功能有限。 我想得到一个可以猜测原始图片的马赛克,而不仅仅是一组颜色大致相同的砖块。


改善马赛克


在这里,我们必须更深入一点,从图片中的像素获取颜色...



由于显然在控制台中通常没有从中获取此数据的画布,因此我们将以get-pixels包的形式使用帮助。 他可以使用我们已经拥有的图片从缓冲区中提取必要的信息。


 npm i --save get-pixels 

它看起来像这样:


 getPixels(image, 'image/jpg', (err, pixels) => { // . . . }); 

我们得到一个包含数据字段的对象-一个像素数组,与从画布上获得的对象相同。 让我提醒您,为了通过坐标(X,Y)获得像素的颜色,您需要进行简单的计算:


 const pixelPosition = 4 * (y * width + x); const rgb = [ pixels.data[pixelPosition], pixels.data[pixelPosition + 1], pixels.data[pixelPosition + 2] ]; 

因此,对于每个矩形,我们可以不使用调色板中的颜色,而直接使用图片中的颜色,然后使用它。 您将获得类似的信息(这里的主要目的是不要忘记图片中的坐标与我们的“归一化”坐标从0到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 }); } } // . . . 

为了获得更大的美观,我们可以稍微增加“砖”的数量,减小其尺寸。 由于我们没有将此尺寸传递给模板(当然,值得将其与图像的宽度或高度设为相同的参数),因此我们将更改模板本身的尺寸值:


 {{# each rects }} <rect x='{{ x }}' y='{{ y }}' height='6' width='6' fill='{{ color }}' /> {{/each }} 

现在,我们有了一个实际上看起来像原始图像的马赛克,但同时却减少了一个数量级的空间。


不要忘记GZIP可以很好地压缩文本文件中的此类重复序列,以便在传输到浏览器时,此类预览的大小将变得更小。

但是,让我们继续前进。


三角剖分



矩形很好,但是三角形通常会产生更多有趣的结果。 因此,让我们尝试从一堆三角形制作马赛克。 有多种方法可以解决此问题,我们将使用Delaunay三角剖分


 npm i --save delaunay-triangulate 

我们将使用的算法的主要优点是,只要有可能,就可以避免三角形的锐角和钝角。 为了获得美丽的图像,我们不需要狭窄和长三角形。


这是了解我们领域中存在哪些数学算法以及它们之间有何区别的有用时刻之一。 不必记住它们的所有实现,但是至少知道谷歌搜索有用。

将我们的任务划分为较小的任务。 首先,您需要为三角形的顶点生成点。 最好在其坐标上添加一些随机性:


 function generateTriangulation() { // . . . const basePoints = []; for (let x = 0; x <= 100; x += 5) { for (let y = 0; y <= 100; y += 5) { const point = [x, y]; if ((x >= 5) && (x <= 95)) { point[0] += Math.floor(10 * Math.random() - 5); } if ((y >= 5) && (y <= 95)) { point[1] += Math.floor(10 * Math.random() - 5); } basePoints.push(point); } } const triangles = triangulate(basePoints); // . . . 

在用三角形检查数组的结构(console.log以帮助我们)之后,我们发现了要获取像素颜色的点。 您可以简单地计算三角形顶点坐标的算术平均值。 然后,我们将额外的点从极端边界移开,以使它们不会在任何地方出现,并且在接收到真实的,未归一化的坐标后,得到像素的颜色,该颜色将变为三角形的颜色。


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

像以前一样,仅保留字符串中所需点的坐标并将其连同颜色一起发送到车把进行处理。


在模板本身中,现在我们将没有矩形,而是多边形:


 {{# each polygons }} <polygon points='{{ points }}' style='stroke-width:0.1;stroke:{{ color }};fill:{{ color }}' /> {{/each }} 

三角剖分是一件非常有趣的事情。 通过增加三角形的数量,您可以得到漂亮的图片,因为没有人说我们必须仅将它们用作存根。


沃罗诺伊马赛克


有一个问题,前一个的镜像-Voronoi的分区或马赛克在使用着色器时 ,我们已经使用过它,但是在这里它也很有用。



与其他已知算法一样,我们有一个现成的实现:


 npm i --save voronoi 

进一步的操作将与我们在前面的示例中所做的非常相似。 唯一的区别是,现在我们有了不同的结构-我们有了一个复杂的对象,而不是三角形数组。 选项略有不同。 否则,一切都差不多。 以相同的方式生成基点数组,请跳过该基点,以免列表过长:


 function generateVoronoi() { // . . . const box = { xl: 0, xr: 100, yt: 0, yb: 100 }; const diagram = voronoi.compute(basePoints, box); const polygons = []; diagram.cells.forEach((cell) => { let x = cell.site.x; let y = cell.site.y; 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); let points = ''; cell.halfedges.forEach((halfedge) => { const endPoint = halfedge.getEndpoint(); points += endPoint.x.toFixed(2) + ',' + endPoint.y.toFixed(2) + ' '; }); polygons.push({ points, color }); }); // . . . 

结果,我们得到了凸多边形的镶嵌图。 也是一个非常有趣的结果。


将所有数字四舍五入为整数或至少两位小数很有用。 SVG中过高的精度在这里是完全没有必要的,它只会增加图片的大小。

马赛克模糊


我们将看到的最后一个示例是模糊的马赛克。 我们掌握了SVG的全部功能,那么为什么不使用过滤器呢?



取得矩形的第一个马赛克,并向其中添加标准的“模糊”滤镜:


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

结果是模糊,“删减”了我们的图片预览,它几乎减少了10倍的空间(不压缩),向量,并且可以拉伸到任何屏幕尺寸。 同样,您可以模糊其余马赛克。


将此类滤镜应用于规则的矩形马赛克时,可能会出现“吉普效果”,因此,如果在生产中使用此类滤镜,尤其是对大型图片,则对模糊而不是对Voronoi分割应用模糊效果可能会更漂亮。

而不是结论


在本文中,我们研究了如何在Node.js上生成各种SVG存根映像,并确保如果您不手工编写所有内容并组装可能的现成模块,那么这并不是一项艰巨的任务。 完整资源可在github上找到

Source: https://habr.com/ru/post/zh-CN431232/


All Articles