(关注微信公众号“NetX行者”浏览更多精彩文章)
问题
使用 HTML Canvas 构建网站或应用程序时,通常需要支持填充。也就是说,当用户选择一种颜色并单击一个像素时,用用户选择的颜色填充与所单击像素的颜色相匹配的所有周围像素。
为此,您可以编写一个相当简单的算法,一次一个地遍历像素,将它们与单击的像素进行比较,然后改变或不改变它们的颜色。如果您在执行此操作时重绘画布,以便为用户提供视觉反馈,它可能看起来像这样。
这可行,但速度慢且丑陋。可以大大加快速度,所以它基本上是即时的,看起来像这样
为实现这一点,我们预处理源图像并使用输出立即将彩色蒙版应用到 HTML Canvas。
相关网站
演示:https://shaneosullivan.github.io/example-canvas-fill
代码:https://github.com/shaneosullivan/example-canvas-fill
https://excalidraw.com/: Excalidraw
案例
Kidz Fun Art:Kidz Fun Art 基于网络的应用程序,该应用程序针对在平板电脑上的使用进行了优化(https://www.kidzfun.art/)
解决方案
从具有多个封闭区域的图像开始,每个封闭区域在这些区域内具有统一的颜色。在此示例中,我们将使用具有四个封闭区域的图像,编号为 1 到 4。
现在创建一个 web worker,它是在浏览器线程的单独线程上运行的 JavaScript,因此在处理大量数据时不会锁定用户界面。
let worker = new Worker("./src/worker.js");
该worker.js
文件包含执行填充算法的代码。在浏览器 UI 代码中,通过将图像绘制到 Canvas 元素并调用该getImageData
函数,将图像像素发送给工作人员。请注意,您将 ImageBuffer 对象发送给工作人员,而不是 ImageData 本身
const canvas = document.getElementById('mycanvas');
const context = canvas.getContext('2d');
const dimensions = { height: canvas.height, width: canvas.width };
const img = new Image();
img.onload = () => {
context.drawImage(img, 0, 0);
const imageData =
canvas.getImageData(0, 0, dimensions.width, dimensions.height);
worker.postMessage({
action: "process",
dimensions,
buffer: imageData.data.buffer,
},
[imageData.data.buffer]
);
};
工作脚本然后异步检查图像中的每个像素。它首先将每个像素的 alpha(透明度)值设置为零,这将像素标记为未处理。当它找到一个具有零 alpha 值的像素时,它会从该像素执行 FILL 操作,其中每个周围的像素都被赋予一个增量 alpha 值。也就是说,第一次执行填充时,所有周围像素的 alpha 版本都指定为 1,第二次指定的 alpha 值为 2,依此类推。
每次 FILL 完成时,工作人员都会存储 FILL 使用的区域的独立图像(存储为数字数组)。当它检查了源图像中的所有像素后,它会将它计算出的所有单个图像“掩码”以及所有 alpha 值设置在 1 到 255 之间的数字的单个图像发送回 UI 线程。这意味着使用这种方法,我们最多可以支持 255 个不同的区域进行即时填充,这应该没问题,因为如果给定像素没有经过预处理,我们可以回退到慢速填充。
您在上面完全处理的图像中看到源图像中的所有像素都分配了一个 alpha 值。数值对应于掩码之一,如下所示。
对于这张图片,它会生成四个蒙版,如上图所示。红色区域是具有非零 alpha 值的像素,白色区域是具有 alpha 值零的像素。
当用户单击 HTML Canvas 节点的像素时,UI 代码会检查从 worker 返回的图像中的 alpha 值。如果值为 2,它会选择它收到的掩码数组中的第二项。
现在是时候通过属性使用一些 HTML Canvas 魔法了globalCompositeOperation
。此属性允许使用 Canvas 执行各种有趣和有趣的操作,但出于我们的目的,我们对其值感兴趣source-in
。这使得调用fillRect()
Canvas 上下文只会填充非透明像素,而其他像素保持不变。
const pixelMaskContext = pixelMaskCanvasNode.getContext('2d');
const pixelMaskImageData = new ImageData(
pixelMaskInfo.width,
pixelMaskInfo.height
);
pixelMaskImageData.data.set(
new Uint8ClampedArray(pixelMaskInfo.pixels)
);
pixelMaskContext.putImageData(pixelMaskImageData, 0, 0);
// Here's the canvas magic that makes it just draw the non
// transparent pixels onto our main canvas
pixelMaskContext.globalCompositeOperation = "source-in";
pixelMaskContext.fillStyle = colour;
pixelMaskContext.fillRect(
0, 0, pixelMaskInfo.width, pixelMaskInfo.height
);
现在您已经用一种颜色填充了蒙版,在本例中为紫色,然后您只需将其绘制到画布上用户可见的蒙版左上角位置,即可完成!
context.drawImage(
pixelMaskCanvasNode,
pixelMaskInfo.x,
pixelMaskInfo.y
);
完成后应该如下图所示
需要注意的是,如果您通过打开文件在本地计算机上尝试此代码index.html
,它将不起作用,因为浏览器安全性不会让 Worker 注册。您需要运行本地主机服务器并从那里运行它。