想弄一个节日头像,结果全是广告!带你用 Canvas 自己制作节日头像
一、为什么要自己制作节日头像?
很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。
为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!
二、源码 & 在线体验
👀 在线体验 | 📖 源码地址 | 欢迎start、欢迎共同交流
注意事项
demo_admin
为体验用户,项目一人一号 ,如果体验人数过多,请自行选中项目中的登录方式进行登录- 本文源码在
yf/ yf-vue-admin / src / views / demo / festival-avatar
三、 实现的功能与后续发展
在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:
- 头像裁剪功能
- 头像与框架的拼接
- 头像框透明度调节
- 头像框颜色过滤(可自定义头像框)
- 后续发展:Fabric.js 自定义贴图功能
- 后续发展:更新更多节日的头像 & 贴图
四、当前素材及投稿征集
展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)
1. 头像框
2. 贴图
五、代码实现
整体逻辑非常简单 : 头像 + 头像框 = 所需头像
1. 头像裁剪功能
页面部分
- 使用
:width
来根据设备类型设置宽度(device === DeviceEnum.MOBILE ? '95%' : '42%'
)。 用于图像裁剪功能。
- 底部有文件上传和旋转按钮。
<template>
<el-dialog
v-model="dialog.visible"
:width="device === DeviceEnum.MOBILE ? '95%' : '42%'"
class="festival-avatar-upload-dialog"
destroy-on-close
draggable
overflow
title="上传头像"
>
<div style="height: 45vh">
<vue-cropper
ref="cropper"
:autoCrop="true"
:centerBox="true"
:fixed="true"
:fixedNumber="[1,1]"
:img="imgTemp"
:outputType="'png'"
/>
div>
<template #footer>
<div class="festival-avatar-dialog-options">
<el-button @click="uploadAvatar">
<el-icon style="margin-right: 5px;">
<UploadFilled/>
el-icon>
上传头像
<input ref="avatarUploaderRef" accept="image/*" class="avatar-uploader" name="file" type="file"
@change="handleFileChange">
el-button>
<el-button @click="rotateLeft">
<el-icon><RefreshLeft/>el-icon>
el-button>
<el-button @click="rotateRight">
<el-icon><RefreshRight/>el-icon>
el-button>
<el-button type="primary" @click="submitForm">提 交el-button>
div>
template>
el-dialog>
template>
代码逻辑部分(核心部分)
imgTemp
用来存储上传的临时图片数据。handleFileChange
处理文件上传事件,校验文件类型并使用FileReader
读取图片数据,并本地存储。rotateLeft
和rotateRight
分别用于左旋和右旋图片。
// 省略部分属性定义
const imgTemp = ref<string>("") // 临时图片数据
const cropper = ref(); // 裁剪实例
const avatarUploaderRef = ref<HTMLInputElement | null>(null); // 上传头像 input 引用
// 上传头像功能
function uploadAvatar() {
avatarUploaderRef.value?.click(); // 点击 input 触发上传
}
// 上传文件前校验 : 略
// 处理文件上传
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
if (!beforeAvatarUpload(file)) return;
const reader = new FileReader();
reader.onload = (e: ProgressEvent) => {
imgTemp.value = e.target?.result as string; // 读取的图片数据赋给 imgTemp
};
reader.readAsDataURL(file);
}
}
// 旋转功能
function rotateLeft() {
cropper.value?.rotateLeft();
}
const rotateRight = () => {
cropper.value?.rotateRight();
};
实现效果图
2. 头像与头像框合并
页面部分 (核心部分)
compositeAvatar
为组合头像 ,avatarData
为头像数据 ,compositeCanvas
头像 Canvas ,avatarFrameCanvas
头像框 Canvas- 在没有
compositeAvatar
的时候展示avatarData
, 没有avatarData
提示用户点击PLUS
的图片
<div class="festival-avatar-preview">
<div class="festival-avatar-preview__plus" @click="openAvatarDialog">
<img v-if="compositeAvatar" :src="compositeAvatar" alt="合成头像"/>
<img v-else-if="avatarData" :src="avatarData" alt="头像"/>
<el-icon v-else color="#8c939d" size="28">
<Plus>Plus>
el-icon>
div>
div>
<canvas ref="compositeCanvas" style="display: none;">canvas>
<canvas ref="avatarFrameCanvas" style="display: none;">canvas>
逻辑部分 (核心部分)
- 通过
toDataURL
转换后合成为组合头像 , 通过drawImage
合并avatarFrameCanvas
和上文中avatarData
进行合并
const context = getCanvasContext(compositeCanvas); // 获取主 Canvas 上下文
const frameContext = getCanvasContext(avatarFrameCanvas); // 获取头像框 Canvas 上下文
// 省略非相关逻辑 , context 中写入 avatarData 内容
// 将处理后的头像框绘制到主 Canvas 上
context.drawImage(avatarFrameCanvas.value, 0, 0, avatarImg.width, avatarImg.height);
// 将合成后的图片转换为数据 URL
compositeAvatar.value = compositeCanvas.value!.toDataURL('image/png');
实现效果
当我们点击头像框的时候,合并头像
3. 头像框透明度调整
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
逻辑部分 (核心部分)
通过 context
中 globalAlpha
属性设置全局透明度。
setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
/**
* 设置 Canvas 的透明度
* @param context Canvas 的 2D 上下文
* @param opacity 透明度值
*/
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}
实现效果
4. 头像框颜色过滤
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底
的问题,所以更新此功能。
逻辑部分 (核心部分)
filterColorToTransparent
函数
- 作用:将与指定颜色相近的像素变为透明。
colorDistance
函数
- 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
- 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(
tolerance
),则认为两种颜色足够接近。
rgbStringToArray
函数
- 作用:将 RGB 字符串(例如
'rgb(255,255,255)'
)转换为包含r, g, b
值的对象。
/**
* 将指定颜色过滤为透明
* @param context Canvas 的 2D 上下文
* @param width Canvas 宽度
* @param height Canvas 高度
*/
function filterColorToTransparent(context: CanvasRenderingContext2D, width: number, height: number) {
const frameImageData = context.getImageData(0, 0, width, height);
const data = frameImageData.data;
const targetColor = rgbStringToArray(colorFilter.value.color); // 将目标颜色转换为 RGB 数组
// 遍历所有像素点
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const distance = colorDistance({r, g, b}, targetColor); // 计算当前颜色与目标颜色的差距
// 如果颜色差距在容差范围内,则将其透明度设为 0
if (distance <= colorFilter.value.tolerance) {
data[i + 3] = 0; // 设置 alpha 通道为 0(透明)
}
}
// 将处理后的图像数据放回 Canvas
context.putImageData(frameImageData, 0, 0);
}
/**
* 计算两种颜色之间的距离(欧几里得距离)
* @param color1 颜色 1,包含 r、g、b 属性
* @param color2 颜色 2,包含 r、g、b 属性
* @returns number 返回颜色之间的距离
*/
function colorDistance(color1: { r: number; g: number; b: number }, color2: { r: number; g: number; b: number }) {
return Math.sqrt(
(color1.r - color2.r) ** 2 +
(color1.g - color2.g) ** 2 +
(color1.b - color2.b) ** 2
);
}
/**
* 将 RGB 字符串转换为 RGB 数组
* @param rgbString RGB 字符串(例如 'rgb(255,255,255)')
* @returns 返回一个包含 r、g、b 值的对象
*/
function rgbStringToArray(rgbString: string) {
const result = rgbString.match(/\d+/g)?.map(Number) || [0, 0, 0]; // 匹配并转换 RGB 值
return {r: result[0], g: result[1], b: result[2]}; // 返回 r、g、b 对象
}
实现效果
- 在 Canva 自己制作一个头像
- 上传头像框,制作头像 ( 过滤白色 )
六、结束语
开发很容易,祝大家各个节日快乐 !!!
作者:翼飞
来源:juejin.cn/post/7419223935005605914
来源:juejin.cn/post/7419223935005605914