想弄一个节日头像,结果全是广告!带你用 Canvas 自己制作节日头像

1,482 阅读6分钟

一、为什么要自己制作节日头像?

很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。

为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!

二、源码 & 在线体验

👀 在线体验 | 📖 源码地址 | 欢迎start、欢迎共同交流

注意事项

  • demo_admin 为体验用户,项目一人一号 ,如果体验人数过多,请自行选中项目中的登录方式进行登录
  • 本文源码在 yf/ yf-vue-admin / src / views / demo / festival-avatar

三、 实现的功能与后续发展

在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:

  1. 头像裁剪功能
  2. 头像与框架的拼接
  3. 头像框透明度调节
  4. 头像框颜色过滤(可自定义头像框)
  5. 后续发展:Fabric.js 自定义贴图功能
  6. 后续发展:更新更多节日的头像 & 贴图

四、当前素材及投稿征集

展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)

1. 头像框

image.png

2. 贴图

五、代码实现

整体逻辑非常简单 : 头像 + 头像框 = 所需头像

1. 头像裁剪功能

页面部分

  • 使用 :width 来根据设备类型设置宽度(device === DeviceEnum.MOBILE ? '95%' : '42%')。
  • <vue-cropper> 用于图像裁剪功能。
  • 底部有文件上传和旋转按钮。
<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<FileReader>) => {
      imgTemp.value = e.target?.result as string; // 读取的图片数据赋给 imgTemp
    };
    reader.readAsDataURL(file);
  }
}

// 旋转功能
function rotateLeft() {
  cropper.value?.rotateLeft();
}

const rotateRight = () => {
  cropper.value?.rotateRight();
};

实现效果图

image.png

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 -->
<canvas ref="compositeCanvas" style="display: none;"></canvas>
<!-- 头像框绘制 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');

实现效果

当我们点击头像框的时候,合并头像

QQ录屏20240928110300.gif

3. 头像框透明度调整

页面部分 与上文一样 , 通过调整 avatarFrameCanvas 的内容而调整头像框

逻辑部分 (核心部分)

通过 contextglobalAlpha 属性设置全局透明度。

setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
 
/**
 * 设置 Canvas 的透明度
 * @param context Canvas 的 2D 上下文
 * @param opacity 透明度值
 */
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
  context.globalAlpha = opacity; // 设置全局透明度
}

实现效果

QQ录屏20240928110300.gif

4. 头像框颜色过滤

页面部分 与上文一样 , 通过调整 avatarFrameCanvas 的内容而调整头像框

服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底的问题,所以更新此功能。

逻辑部分 (核心部分)

filterColorToTransparent 函数

  • 作用:将与指定颜色相近的像素变为透明。

colorDistance 函数

  • 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
  • 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(tolerance),则认为两种颜色足够接近。 image.png

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 对象
}

实现效果

  1. Canva 自己制作一个头像

image.png

  1. 上传头像框,制作头像 ( 过滤白色 )

QQ图片20240707160518.gif

六、结束语

开发很容易,祝大家各个节日快乐 !!!