提要
在 PS中色相、饱和度、明度调节的一个功能模块。这两天业务需要一个一样的功能,用于批量自动渲染。 那在 前端如何实现呢,特此记录下。
功能效果如下:
思路
- 色相的调节,可以经过 CSS或 Canvas 中的 filter 属性 hue滤镜来实现.
- 饱和度的调节,可以经过 CSS或 Canvas 中 filter 属性的 saturate 滤镜来实现。
- 明度的调节,哈哈哈,可能大家想当然的觉得是brightness滤镜,其实不是的!!这里是个坑, 在 PS中的明度和亮度是有区别的,PS 中明度向上调满是变成了纯白,向下调满是纯黑。两者的算法和效果是有区别的。那应当如何??应该自己写一个 filter 去应用画布数据。
实现
- 可以看到我以下代码主要是通过filter 来应用效果的:
ctx.filter =
hue-rotate(${hueValue}deg) saturate(${saturateValue}%) brightness(${brightnessValue}%) contrast(${contrastValue}%) opacity(${opacityValue}%)
;
其中:
- hue-rotate :对应的是色相。
- saturate : 对应的是饱和度,取值 0%~100%(或以上),PS 是从-100 到+100,所以我们需要做数据转换。
- brightness : 是亮度,不是 PS 的明度。
- contrast : 是对比度
- opacity : 是透明度
ts
/**
* 绘制图片
*/
function drawImage() {
const canvas = refCanvas.current;
const ctx = canvas.getContext("2d");
const hueValue = hue;
const saturateValue = saturation + 100;
const brightnessValue = brightness + 100;
const contrastValue = contrast + 100;
const opacityValue = opacity;
ctx.filter = `hue-rotate(${hueValue}deg) saturate(${saturateValue}%) brightness(${brightnessValue}%) contrast(${contrastValue}%) opacity(${opacityValue}%)`;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (refImage.current != null) {
ctx.drawImage(refImage.current, 0, 0, canvas.width, canvas.height);
//应用明度滤镜
applyLightFilter(ctx, lightness / 100);
} else {
refImage.current = new Image();
refImage.current.style.objectFit = "contain";
refImage.current.style.background = "transparent";
refImage.current.setAttribute("crossOrigin", "anonymous");
refImage.current.crossOrigin = "anonymous";
refImage.current.style.width = refCanvas.current.style.width;
refImage.current.style.height = refCanvas.current.style.height;
refImage.current.src = `${src}?${Date.now()}`;
refImage.current.onload = () => {
ctx.drawImage(refImage.current, 0, 0, canvas.width, canvas.height);
//明度滤镜
applyLightFilter(ctx, lightness / 100);
};
}
}
自定义明度的滤镜实现
实现原理: 明度为正数的情况下,相当于混合了一张纯白的图片,负数的情况混合了一张纯黑的图片。然后设置这个纯色图片的透明度,就可以达到 PS 中的明度滤镜效果了。
- 这里有个小知识点,Canvas 的 ImageData是一个数组,其中每四位是一个像素点的颜色,分别对应[a,r,g,b],也就是透明度,红色,绿色,蓝色。
ts
/**
* 明亮效果的滤镜
* @param ctx
* @param light 0 ~ 1 ,百分比
*/
function applyLightFilter(ctx: CanvasRenderingContext2D, light: number) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var nPixels = imageData.data.length;
//判断亮度是正数还是负数,来设置混合的图片是白色还是黑色。其中亮度值是对应的透明度
const lightValue = light < 0 ? 0 : 255;
const r = lightValue;
const g = lightValue;
const b = lightValue;
const a = Math.abs(light) * 255;
for (var i = 3; i < nPixels; i += 4) {
if (imageData.data[i] == 0) {
//透明部分跳过
} else {
const a1 = imageData.data[i];
const r1 = imageData.data[i + 1];
const g1 = imageData.data[i + 2];
const b1 = imageData.data[i + 3];
// 颜色混合
imageData.data[i] = a1 + a - (a1 * a) / 255;
imageData.data[i + 1] =
(r1 * a1 + r * a - (r1 * a1 * a) / 255) / (a1 + a - (a1 * a) / 255);
imageData.data[i + 2] =
(g1 * a1 + g * a - (g1 * a1 * a) / 255) / (a1 + a - (a1 * a) / 255);
imageData.data[i + 3] =
(b1 * a1 + b * a - (b1 * a1 * a) / 255) / (a1 + a - (a1 * a) / 255);
}
}
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.putImageData(imageData, 0, 0);
}