用粒子画布填充形状

pwl*_*rry 2 html javascript canvas particles particle-system

只是想知道是否有人可以为我指出一个好的方向,让我可以用粒子成行填充不规则形状,然后将其设置为动画。

这是我能找到的最接近的例子 - http://www.wkams.com/#!/work/detail/coca-cola-music-vis

我认为可行的两种方法是计算出我想要的密度,绘制出每行需要多少个粒子,并相应地定位。这种方式看起来很及时,但也不是很稳健。

第二种方法,我似乎不知道如何做到这一点,是在画布中绘制形状,然后用粒子生成填充形状,将它们保持在形状的约束中。

任何关于如何做到这一点的一般概念将不胜感激。

如果没有意义请告诉我。

干杯

Ana*_*Ana 11

我们首先在画布上绘制我们想要的形状。

这个形状可以是任何形状。它可能是文本,也可能是图像中的形状 - 具有透明度的 .png 的非透明部分、.jpg 图像的非黑色或黑色部分 - 这并不重要,所有这些工作。

让我们从形状是文本的非常简单的情况开始。

我们将有一些常量(画布、上下文、RGBA 通道数、一个以我们想要打点的字符串开头的文本框对象、我们定义点半径的点网格和同一行/列上两个连续点之间的距离):

const _C = document.getElementById('c'), 
      CT = _C.getContext('2d'), 
      TEXT_BOX = { str: 'HELLO!' }, 
      DOT_GRID = { gap: 6 }, 
      NUM_CH = 'RGBA'.length;
Run Code Online (Sandbox Code Playgroud)

我们设置画布尺寸,然后计算一些有关文本的信息,以便它很好地适合画布的中间。并不是所有的字母都需要一个方形的盒子,有些字母(比如“I”)要窄得多,但是我们可以从这样的假设开始来获取文本框的高度,我们也将其存储在文本框对象中并设置作为字体大小:

TEXT_BOX.height = Math.min(.7*_C.height, _C.width/TEXT_BOX.str.length);
CT.font = `600 ${TEXT_BOX.height}px arial black, sans serif`;
CT.letterSpacing = '8px';
Run Code Online (Sandbox Code Playgroud)

我们还稍微间隔了字母。

然后,我们使用该字体大小和字母间距测量文本,以获得实际的文本框宽度。我们还计算左上角的坐标(我们将在点化文本时使用这些坐标)。

TEXT_BOX.width = CT.measureText(TEXT_BOX.str).width;
TEXT_BOX.x = .5*(_C.width - TEXT_BOX.width);
TEXT_BOX.y = .5*(_C.height - TEXT_BOX.height);
Run Code Online (Sandbox Code Playgroud)

我们为文本提供自定义填充样式(这是完全可选的,只有当您计划让文本在点网格下方可见时才真正需要),并将其沿其自己的垂直轴和水平轴居中对齐。

CT.fillStyle = 'purple';
CT.textAlign = 'center';
CT.textBaseline = 'middle';
Run Code Online (Sandbox Code Playgroud)

现在我们实际上可以在画布上绘制文本:

CT.fillText(TEXT_BOX.str, .5*_C.width, .5*_C.height);
Run Code Online (Sandbox Code Playgroud)

显示文本框中的文本和对齐轴的插图。

现在到了有趣的部分 - 我们对文本进行点化!

我们首先获取文本框矩形的画布图像数据。

let data = CT.getImageData(TEXT_BOX.x, TEXT_BOX.y, 
                           TEXT_BOX.width, TEXT_BOX.height).data;
Run Code Online (Sandbox Code Playgroud)

这为我们提供了一个非常长的一维数组,其中包含文本框矩形内所有像素的 RGBA 值(逐行、逐列)。

/* 1st row, 1st column: */
R0, G0, B0, A0, 
/* 1st row, 2nd column: */
R1, G1, B1, A1, 
... 
/* last row, last column: */
RN, GN, BN, AN
Run Code Online (Sandbox Code Playgroud)

然后,我们将此数组转换为像素对象数组,每个像素对象包含x,y每个像素的坐标和 RGBA 通道值作为数组。然后,我们过滤掉 alpha 所在的所有像素0(在文本形状之外),并且这些像素不是具有给定间隙的点网格的网格节点。这基本上给了我们想要在文本形状内绘制的点数组。

DOT_GRID.arr = 
  data.reduce((a, c, i, o) => {
    if(i%NUM_CH === 0) 
      a.push({
        x: (i/NUM_CH)%TEXT_BOX.width + TEXT_BOX.x, 
        y: Math.floor(i/NUM_CH/TEXT_BOX.width) + TEXT_BOX.y, 
        rgba: o.slice(i, i + NUM_CH) 
      });
    return a
  }, []).filter(c => c.rgba[NUM_CH - 1] && 
                     !(Math.ceil(c.x - .5*DOT_GRID.gap)%DOT_GRID.gap) && 
                     !(Math.ceil(c.y - .5*DOT_GRID.gap)%DOT_GRID.gap));
Run Code Online (Sandbox Code Playgroud)

如果愿意,我们可以删除点下方的文本。

CT.clearRect(TEXT_BOX.x, TEXT_BOX.y, TEXT_BOX.width, TEXT_BOX.height);
Run Code Online (Sandbox Code Playgroud)

然后我们画点,比如说填充gold

CT.fillStyle = 'gold'

CT.beginPath();

DOT_GRID.arr.forEach(c => {
  CT.moveTo(c.x, c.y);
  CT.arc(c.x, c.y, DOT_GRID.rad, 0, 2*Math.PI)
});

CT.closePath();
CT.fill();
Run Code Online (Sandbox Code Playgroud)

我想在这里指出的一件事是,填充的计算成本很高,因此在这种情况下,所有点都具有相同的填充样式,我们将保留在循环fill()之外forEach,因此它在最后只被调用一次。

一般来说,如果某些东西不需要依赖于循环变量或在每次迭代中随机生成,请将其保留在循环之外!

这是点化结果与下面写的原始文本的样子:

原始文本上方的点状文本

...并且没有:

点状文字,下方没有原文

这就是非常基本的点化文本情况。

这是上面解释的非常基本的案例的工作片段。

const _C = document.getElementById('c'), 
      CT = _C.getContext('2d'), 
      TEXT_BOX = { str: 'HELLO!' }, 
      DOT_GRID = { gap: 6 }, 
      NUM_CH = 'RGBA'.length;
Run Code Online (Sandbox Code Playgroud)
TEXT_BOX.height = Math.min(.7*_C.height, _C.width/TEXT_BOX.str.length);
CT.font = `600 ${TEXT_BOX.height}px arial black, sans serif`;
CT.letterSpacing = '8px';
Run Code Online (Sandbox Code Playgroud)
TEXT_BOX.width = CT.measureText(TEXT_BOX.str).width;
TEXT_BOX.x = .5*(_C.width - TEXT_BOX.width);
TEXT_BOX.y = .5*(_C.height - TEXT_BOX.height);
Run Code Online (Sandbox Code Playgroud)

这是一个得到大量评论的版本,它也可以很好地处理页面大小调整。


当然,我们也可以给文本进行渐变填充,然后将渐变生成的 RGB 像素值用于网格上的点。它们也可以有不同的半径、位置的随机分量以及取决于指针的运动,如本例所示(请注意,在这种情况下,我清除了画布上绘制的原始文本)。

动画 gif。 显示“悬停!”  由粒子组成的字符串,其背景遵循渐变,并且会因鼠标光标的接近而被排斥。


它对于图像的工作方式非常相似。我们在画布上绘制图像,读取图像数据,决定要排除哪些像素(也许是透明的,也许是黑色的,也许是白色的……没关系),然后只保留未排除的网格节点像素前一步。

假设我们有这张猫图像(具有透明度的.png)。我们排除透明像素,然后排除所有不是网格节点的像素。

猫行走的黑暗剪影

我们可以使用 Base64 图像源来避免 CORS 问题。有很多网站可以进行转换(例如这个)。

我们复制它并将其设置为BASE64_SRC常量。

常量几乎相同,只是TEXT_BOX被替换为IMG_RECT

const IMG_RECT = { img: new Image() }
Run Code Online (Sandbox Code Playgroud)

设置画布尺寸后,我们不会在画布上写入文本,而是继续绘制图像。

我们将图像源设置为 Base64 图像源。

IMG_RECT.img.src = BASE64_SRC;
Run Code Online (Sandbox Code Playgroud)

加载完成后,我们将继续根据其尺寸获取其纵横比。然后我们获取绘制图像的框的尺寸和左上角,使其适合画布。然后我们实际上在这个矩形内绘制图像。

IMG_RECT.img.onload = function() {
  IMG_RECT.ratio = IMG_RECT.img.width/IMG_RECT.img.height;
  IMG_RECT.width = 
    Math.min(IMG_RECT.img.width, _C.width, _C.height*IMG_RECT.ratio);
  IMG_RECT.height = 
    Math.min(IMG_RECT.img.height, _C.height, _C.width/IMG_RECT.ratio);
  IMG_RECT.x = .5*(_C.width - IMG_RECT.width);
  IMG_RECT.y = .5*(_C.height - IMG_RECT.height);

  CT.drawImage(IMG_RECT.img, IMG_RECT.x, IMG_RECT.y, 
                             IMG_RECT.width, IMG_RECT.height);
}
Run Code Online (Sandbox Code Playgroud)

点化部分与之前完全相同,我们只是将所有出现的 替换TEXT_BOXIMG_RECT。这样我们就有了点状的猫:

原来的点猫

就像文本的情况一样,我们可以从点下方删除原始形状:

点状猫,原件已从下方移除

这是一个广受好评的演示,展示了这一点的实际效果。

我们实际上不必对我们的图像进行 Base64 处理。我们可以这样做:

const IMG_RECT = {
  img: new Image(), 
  src: 'https://i.stack.imgur.com/KleBk.png'
}
Run Code Online (Sandbox Code Playgroud)

进而...

IMG_RECT.img.crossOrigin = 'anonymous';
IMG_RECT.img.src = IMG_RECT.src;
Run Code Online (Sandbox Code Playgroud)

这是使用此 CORS 设置的现场演示。


我们也不一定需要具有透明度的图像。我们还可以使用像这样的图像,其中芭蕾舞演员的深色形状与背景形成强烈对比。

芭蕾舞演员的剪影

在这种情况下,我们需要更改第一个过滤条件。我们不需要 alpha(第四个)通道非零,而是所有其他通道(RGB,前三个)都为相当低的值(我想在这种特殊情况下对它们求和也能达到目的):

Math.max(...c.rgba.slice(0, 3)) < 36
Run Code Online (Sandbox Code Playgroud)

这几乎可以做到(现场演示):

芭蕾舞演员点状


对于此棕榈树图像的工作方式相同:

黑暗的棕榈树剪影

点化版本(现场演示):

点状棕榈树


我们也可以采取另一种方式,对图像中不暗的部分进行点化:

轮廓

特别是在这种情况下,我们可以只关注蓝色(第三个)通道并使用以下过滤条件:

c.rgba[2] > 200
Run Code Online (Sandbox Code Playgroud)

现场演示

点状轮廓


就像渐变文本的情况一样,我们也可以将原始图像中的 RGB 值用于点化版本。

假设我们从这张图片开始:

路西普尔万岁。 显示一只有角的猫,胸前有一个五角星

点化它以保留 RGB 通道给我们这个(现场演示):

冰雹 Lucipurr 点缀。


这个演示(请注意,它已经有近十年的历史了,JS 变得更好,希望我也如此......无论如何,该代码可以改进)使用类似的技术将图像转换为瓷砖网格,然后折叠。