【前端】Canvas画布实现在线的唇膏换色功能
推荐超级课程:
@TOC
背景概述
===================
在我们的网站上销售美容产品,必须要帮助客户从种类中选择正确产品。 对于美容产品来说,用户通过屏幕上的图片做出正确的选择至关重要。例如,用户可能想知道某款唇色涂抹在特定肤色上会是什么样子。
解决这个问题有多种方法。
- 我们可以为每种唇色准备一张图片,并在用户浏览色板时更换图片。这种方法可行,但会影响网站性能。更快的页面意味着更好的用户体验。每次为数百种颜色变体加载图片会在低带宽网络上造成不必要的延迟,从而影响用户体验。
- 我们也可以使用SVG,然后在上面应用颜色,但这种方法的一个挑战是,对于唇色这样的用例,细线和阴影看起来会“动画化”,结果会显得有些不自然。
- 为了确保用户看到的颜色和唇形尽可能真实,我们使用了PNG图片,将这些PNG像素转换成所需的颜色,并使用画布来应用颜色,这样做速度很快!以下是我们取得的成果!
以下是我们的实现方法!
=====================
让我们逐步探索Canvas API,在本教程结束时,你可以使用这种简单技术在你的网站上解决类似的问题。
第一步 — 找到灰度唇部图像
=========================================
首先,我们需要一张唇部图像,我们将在上面尝试不同的颜色。
这里我们开始!由于图像是灰度的,我们在应用任何颜色之前不需要计算任何东西。如果图像不是灰度的,我们需要先将图像转换为灰度,然后继续。
我们确保在给唇部上色时不会扭曲细线和阴影,否则它会看起来不自然。
第二步 — 创建一个画布
==========================
让我们编写一些代码来创建一个画布。
这是HTML标记:
<div>
<canvas id=”finalCanvas” />
</div>
<div style="display: none">
<canvas id=”hiddenCanvas” />
</div>
在这个HTML标记中,你可以看到两个画布元素,一个名为‘finalCanvas’,另一个名为‘hiddenCanvas’。我们将使用‘hiddenCanvas’来绘制唇部图像,然后在上面添加颜色,最后将其映射回‘finalCanvas’。
我们需要两个画布,因为我们在这里使用了一个两遍算法。在第一遍中,我们用所需颜色绘制整个画布,在第二遍中,我们将画布裁剪以适应最终的‘finalCanvas’。
第三步 — 加载唇部图像
==============================
const image = new Image();
image.crossOrigin = ‘Anonymous’;
image.src = ‘http://xxx.com/test/lips.png';
image.onload = function () {
// 在图像加载后执行操作
}
在这一步中,我们
- 创建一个新的图像元素。
- 设置 image.crossOrigin = ‘Anonymous’.
- 添加一个处理函数,将图像绘制到画布上,之后我们开始应用颜色。
‘Anonymous’属性很重要,因为只有在跨域资源上,你才能在第一次绘制后更改图像的像素。
如果我们不将资源标记为跨域,画布会抛出错误:
Canvas has been tainted by cross-origin data
这个错误是因为用户不能更改非跨域启用资源的像素数据。画布不允许用户在假设资源为只读的情况下在像素上方绘制。
第四步 — 在我们的画布上绘制图像
========================================
在画布上执行操作之前,我们需要获取其上下文,如第二步 — 创建画布中所述
const finalCanvas = document.getElementById('finalCanvas');
const finalCtx = finalCanvas.getContext('2d');
const hiddenCanvas = document.getElementById('hiddenCanvas');
const hiddenCtx = hiddenCanvas.getContext('2d');
然后,我们使用其上下文tempCtx在‘hiddenCanvas’上绘制图像。
image.onload = function () {
finalCanvas.width = hiddenCanvas.width = image.width;
finalCanvas.height = hiddenCanvas.height = image.height;
hiddenCtx.drawImage(image, 0, 0);
// TODO: 在这里执行更多操作
}
我们将两个画布的高度设置为与图像相同,然后在‘hiddenCanvas’画布上绘制图像。在hiddenCtx.drawImage(image, 0, 0)中,值0,0表示图像需要绘制的顶部和左侧位置。
第五步 — 在‘hiddenCanvas’上完成图像着色操作
==============================================================
这是最有趣的步骤,我们尝试不同的画布操作来实现用各种颜色绘制唇部图像的期望行为。让我们一个一个尝试,看看哪个最适合我们的用例。
Canvas API提供了一个非常棒的方法叫做‘globalCompositeOperation’,它允许用户在画布上绘制的图像上执行复合或层叠操作。这非常类似于CSS混合模式,但我们需要这个功能在多种浏览器上都能使用,而混合模式的支持是有限的。
我们利用这个特性来找到最适合我们的选项。
Canvas 2D API的CanvasRenderingContext2D.globalCompositeOperation属性设置了复合操作的类型。
在我们得到最终操作之前,让我们尝试几个。
颜色
tempctx.globalCompositeOperation='color';
结果:
不符合预期,颜色更多地在外边界而不是内边界。
差异
tempctx.globalCompositeOperation='difference';
它会减去两个图层中的颜色并应用结果。
结果:
上层红色和下层灰色的减法得到了不同的颜色。
叠加
tempctx.globalCompositeOperation='overlay';
它会创建一个叠加层并保留暗部和亮部。
结果:
比前两个选项好一些,但唇部的颜色仍然看起来不够暗。
乘法
tempctx.globalCompositeOperation='multiply';
它会将顶层像素与底层像素相乘以实现期望的结果。
结果:
我们得到了!完美的红色唇妆
**‘乘法’操作给了我们期望的结果,**但我们仍然看到整个画布都是红色,而不仅仅是图像部分。
image.onload = function () {
finalCanvas.width = hiddenCanvas.width = image.width;
finalCanvas.height = hiddenCanvas.height = image.height;
hiddenCtx.drawImage(image, 0, 0);
// 对于光泽颜色使用color-burn
// 对于哑光使用multiply
hiddenCtx.globalCompositeOperation='multiply';
hiddenCtx.filter = 'brightness(150%) contrast(200%) saturate(150%)';
hiddenCtx.fillStyle='red';
hiddenCtx.fillRect(0, 0, hiddenCanvas.width, hiddenCanvas.height);
}
第六步 — 在真实画布上绘制唇部
========================================
现在我们需要将这个‘hiddenCanvas’放在我们的‘finalCanvas’上方,并且只绘制唇部部分。画布为我们提供了更多用于此操作的操作。
让我们现在尝试’source-in’操作。
‘source-in’操作只在新形状和目标画布重叠的地方绘制新形状。它使其他所有东西都变成透明的。
这看起来就像我们想要的事情的工作方式。所以让我们放入一个’source-in’操作,看看会发生什么:
图片加载完成后,执行以下函数:
image.onload = function () {
finalCanvas.width = hiddenCanvas.width = image.width;
finalCanvas.height = hiddenCanvas.height = image.height;
hiddenCtx.drawImage(image, 0, 0);
// 对于光泽使用color-burn
// 对于哑光使用multiply
hiddenCtx.globalCompositeOperation = 'multiply';
hiddenCtx.filter = 'brightness(150%) contrast(200%) saturate(150%)';
hiddenCtx.fillStyle = 'red';
hiddenCtx.fillRect(0, 0, hiddenCanvas.width, hiddenCanvas.height);
finalCtx.drawImage(image, 0, 0);
finalCtx.globalCompositeOperation = 'source-in';
finalCtx.drawImage(hiddenCanvas, 0, 0);
finalCtx.restore();
}
在这里,我们将唇部图像绘制到实际画布上方,然后使用globalCompositeOperation
的*source-in*
属性,我们将着色的唇部画布重叠到原始唇部画布上方。
结果:
瞧! 阴影完美地覆盖在唇部;唇线和阴影都保持得非常好!
第七步 — 将着色的唇部放置在脸上
============================================
根据设备宽度和高度,我们改变脸部的宽度和高度。接下来,我们将唇部图像相应地放置在脸部上方。我们还在画布上绘制脸部图像。
将globalContextOperation
设置为*source-over*
,我们将唇部图像放置在脸部上方。source-over
是默认设置,它在现有画布内容上方绘制新形状。
// 执行与上述类似的步骤,将脸部图像绘制到画布上
// 让图像称为‘faceImg’,画布称为‘faceCanvas’
const lipWidth = Math.ceil(FACE_IMAGE_WIDTH * lipToFaceRatio);
const lipHeight = Math.ceil(lipWidth / LIP_ASPECT_RATIO);
const faceContext = faceCanvas.getContext('2d');
faceCanvas.width = faceImg.width;
faceCanvas.height = faceImg.height;
faceContext.drawImage(this.faceImg, 0, 0);
faceContext.globalCompositeOperation = 'source-over';
faceContext.drawImage(finalCanvas, lipWidth * 0.91, lipHeight * 2.08);
这段代码首先绘制脸部图像,然后使用globalContextOperation
将唇部放置在脸部上方,并将其全部混合为一个画布!
结果:
我们所有的画布操作都非常快速,并且也可以离线工作!
这就是你在点击色块时,如何在说明性脸部图像上即时改变唇部阴影的方法!颜色变化的惊人速度是由Canvas API提供的。这确保了没有延迟时间,可以离线工作,并且不会阻塞用户网络。
如前所述,我们可以选择通过SVG来实现这一点,但这看起来会显得不自然,并且不会像我们通过画布实现的那样达到理想的结果。我们也可以使用CSS混合模式,但浏览器对此的支持有限,我们希望尽可能广泛地覆盖用户。
结语
==============
使用大约十行画布代码,我们为我们的网站构建了完整的功能支持。我们将为其他类别和具有类似需求的使用案例提供同样的支持。