本帖最后由 rimelight 于 2023-11-20 21:09 编辑
由于内容比较少就全部塞到一个HTML文件里了,这不是一个好习惯,请勿模仿!
起因:
本人在一个项目中需要实现如下效果:将一个png格式的图片画到一个canvas上,其颜色根据玩家当前状态进行变化。
原本想法是在首次加载时进行预处理,存为多个离屏canvas,但后来添加颜色的过渡动画将此方法排除了(毕竟不可能将过渡时的这么多颜色全都存一份)
目标:
对于给定的任意颜色,能够高效的将该颜色叠加到图片上(毕竟要每帧渲染)
实现:
首先建一个简单的框架
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>test</title>
<style>
.bg {
width: 600px;
height: 600px;
margin: 0;
border: 1px solid #777;
}
label {
margin: 10px;
}
canvas {
background: #777;
}
</style>
</head>
<body style="display: flex; justify-content: center">
<div style="display: flex; flex-direction: column">
<label>
<span>R</span>
<input id="red" type="range" min="0" max="255" value="128">
</label>
<label>
<span>G</span>
<input id="green" type="range" min="0" max="255" value="128">
</label>
<label>
<span>B</span>
<input id="blue" type="range" min="0" max="255" value="128">
</label>
</div>
<div class="bg">
<canvas id="game" width="600" height="600"></canvas>
</div>
</body>
<script>
function setColor() { //something }
</script>
</html>
将图片丢上去
圆内是不透明的(黑白色填充),圆外是透明的
测试用图片
[JavaScript] 纯文本查看 复制代码 const N = 600;
const canvas = document.getElementById("game"),
ctx = canvas.getContext("2d");
let tree = new Image();
let r = 128, g = 128, b = 128;
tree.src = "tree.png";
tree.onload = () => draw();
function setColor(el) {
let single = el.id, value = el.value;
if (single === "red") r = value;
else if (single === "green") g = value;
else b = value;
draw();
}
function draw() {
ctx.drawImage(tree, 0, 0, N, N);
}
看起来只需要对draw函数进行修改就可以了,即设置要在绘制新形状时应用的合成操作的类型并填充颜色(毕竟是对当前图片进行“染色”)
对于globalCompositeOperation,请参考CanvasRenderingContext2D.globalCompositeOperation
经测试,采用multiply(正片叠底)的效果是最符合预期的
[JavaScript] 纯文本查看 复制代码 function draw() {
ctx.save();
ctx.clearRect(0, 0, N, N);
ctx.drawImage(tree, 0, 0, N, N);
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(0, 0, N, N);
ctx.restore();
}
效果图1
看下效果.....哎!不对!原本应该是透明的地方也被填上颜色了!
让我们看一下“正片叠底”:
其原理为对于两张图片在同一位置的像素点 (R1, G1, B1, A1)和(R2, G2, B2, A2) 进行如下运算
R = (R1 * R2) / 255
G、B同理
发现问题所在:按理说A1(Alpha)为0的图片参与运算后A = (0 * A2) / 255 = 0,原本是透明的地方也应该是透明的,但是!A的运算不符合这个公式!
所以,要对处理后图像的透明像素进行处理,source-atop或destination-atop可以完成这个需求,给出一种采用destination-atop的解决方案
[JavaScript] 纯文本查看 复制代码 function draw() {
ctx.save();
ctx.clearRect(0, 0, N, N);
ctx.drawImage(tree, 0, 0, N, N);
ctx.globalCompositeOperation = "multiply";
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(0, 0, N, N);
ctx.globalCompositeOperation = "destination-atop";
ctx.drawImage(tree, 0, 0, N, N);
ctx.restore();
}
效果图2
问题是解决了,但每次都要将同一图像画两次总给人一种冗余的感觉,而且明明“只要RGBA都符合公式”就能完美实现所需效果,所以,应该会有一个更好的解决方法吧?可惜我目前并不知道
此外,尝试采用不同的混合模式可以达到原本进行像素运算会十分麻烦的效果,比如采用lighter来还原一下东方神灵庙的特产——瞎眼光玉
效果图3
附源码
code.zip
(44.41 KB, 下载次数: 11)
|