我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛
效果
前言
年关将至,相信大伙都翘首期盼着放假过年了,在这先祝大家新年快乐!
为了增加点年味打算换个壁纸,便找了一张兔年的壁纸
专业杠精上线,这个红不是我想要的红,我要换成中国传统的妃红,就下面这个,还得用代码实现
如何实现
当然是利用canvas实现了,下面介绍下getImageData方法
getImageData
作用: 用来获取canvas画布上指定矩形区域的像素数据; 参数:
| 参数 | 含义 |
|---|---|
| x | 矩形的左顶点横坐标 |
| y | 矩形的左顶点纵坐标 |
| width | 矩形的宽度 |
| height | 矩形的高度 |
返回值: 返回的是一个ImageData对象,该对象包含了三个只读属性:
| 属性 | 含义 |
|---|---|
| ImageData.width | ImageData的宽度,用像素表示 |
| ImageData.height | ImageData的高度,用像素表示 |
| ImageData.data | 类型为Uint8ClampedArray的一维数组,每四个数组元素代表了一个像素点的RGBA信息,每个元素数值介于0~255 |
有了这个方法那还不是简简单单就能实现,马上开始梳理需求
- 用户选择图片,利用canvas绘制并获取像素点信息
- 遍历像素点修改想要修改的rgba
- 利用canvas重新绘制修改后的图片
- 利用toDataURL生成图片
实现
先实现图片选择,并将图片绘制到canvas
// html
<div class="upload">
<img id="imgSrc" :src="imgSrc" alt="" />
<input type="file" @change="fileChange" />
</div>
//..
// javascript
/**
*
* @param {*} e
*/
const fileChange = (e) => {
loading.value = true;
let file = e.target.files[0];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
imgSrc.value = this.result;
img = document.querySelector("#imgSrc");
canvas = document.querySelector("#canvas");
img.onload = function () {
const naturalImgSize = [img.naturalWidth, img.naturalHeight];
setCanvas(naturalImgSize);
};
};
};
/**
* 初始化canvas
* @param {*} naturalImgSize
*/
const setCanvas = (naturalImgSize) => {
ctx = canvas.getContext("2d");
canvas.width = naturalImgSize[0];
canvas.height = naturalImgSize[1];
ctx.drawImage(img, 0, 0);
loading.value = false;
};
接下来获取图片像素信息
//获取imageData
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
//打印该数据
console.log(imgData);
再组织一下,拼成rgba,并计算每个rgba的数量
/**
* 获取主色
* @param {*} list
* @param {*} num
*/
const getMainColors = (list) => {
let rgbaObj = {};
let rgbaList = [];
list.forEach((colorVal, i) => {
if (i % 4 === 0) {
let rgba = [list[i], list[i + 1], list[i + 2], list[i + 3]];
if (rgbaObj[rgba.join(",")]) {
rgbaObj[rgba.join(",")] += 1;
} else {
rgbaObj[rgba.join(",")] = 1;
}
}
});
Object.keys(rgbaObj).forEach((key) => {
rgbaList.push({
rgba: key.split(",").map((item) => (item = Number(item))),
num: rgbaObj[key],
});
});
rgbaList.sort((a, b) => {
return b.num - a.num;
});
console.log(rgbaList)
};
这时候问题就出现了,明明图片上看着就三个颜色,这里统计出来却有很多,仔细看一下发现很多色值都很接近,应该是色块边缘的过渡色了吧。
对于这种简单的图案那就先简单处理了,保留我们想要的颜色,把其他相近的颜色都修改为我们保留的主色。对于我这张图来说主色就三个,那对于别的图呢?看来还是得用户输入想要保留的颜色。
但是这时思考一下,这种方式好像只能处理比较简单的图片,图片复杂的话必然没法简单地处理就得到想要的效果,那就先实现个简单的看看效果~
既然简单处理,那么我们让用户输入想要保留的主色数量n,我们将色值按数量排序,取该数组的前n个色值,针对我这张图保留前三个,发现其中两个色值相近,而我们需要的白色不在其中,那我们还得支持用户输入色值,强制保留该色值
那就暂时设计成这样
获取完主色我们还需要展示出来,然后让用户可以修改颜色,这里可以使用<input type="color"/>选取颜色,但是这个选择出来的颜色是十六进制的,我们还需要转换成rgba色值后去替换原先的色值
/**
* 转换成16进制
* @param {*} n
*/
const toHex = (n) => `${n > 15 ? "" : 0}${n.toString(16)}`;
/**
* 转换成16进制颜色字符串
* @param {*} colorArr
*/
const toHexString = (colorArr) => {
const {
r,
g,
b,
a = 1,
} = { r: colorArr[0], g: colorArr[1], b: colorArr[2] };
return `#${toHex(r)}${toHex(g)}${toHex(b)}${
a === 1 ? "" : toHex(Math.floor(a * 255))
}`;
};
/**
* 16进制颜色字符串解析为颜色对象
* @param color 颜色字符串
* @returns IColorObj
*/
const parseHexColor = (color) => {
let hex = color.slice(1);
let a = 1;
if (hex.length === 3) {
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
}
if (hex.length === 8) {
a = parseInt(hex.slice(6), 16) / 255;
hex = hex.slice(0, 6);
}
const bigint = parseInt(hex, 16);
return {
r: (bigint >> 16) & 255,
g: (bigint >> 8) & 255,
b: bigint & 255,
a,
};
};
这样前期基础工作就差不多了,这张图我想要保留三个主色,但是色值查下来前三个不包含白色,那么就要设置保留两个主色,再手动加上白色,一共就是三个主色。
点击色块左上角就可以选择想要的颜色,选好之后点击生成图片,就调用方法替换色值,然后利用canvas生成图片
这样更换图片颜色就实现啦
完整的代码可以查看 码上掘金
最后
这里只是简单地进行了图片颜色的更改,复杂的图片比较容易出问题且效果不好,建议还是拿简单的插画进行尝试哦。
祝大家新年快乐,钱兔无量!