IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    国庆微信头像DIY:轻松打造个性化头像

    南玖发表于 2023-09-24 21:28:26
    love 0

    前言

    国庆节马上要到了,今天就教你如何从0到1使用canvas生成国庆风微信头像。

    本文包含以下内容:

    • vue3项目搭建,需求分析
    • canvas合成图片原理
    • github自动化部署
    • 开发过程遇到的问题及解决方案

    文末附源码及在线体验地址~

    gq1.png

    搭建项目,分析需求

    项目的话就直接使用脚手架生成一个 Vue3 + TS项目

    npm create vue@latest

    为了方便,使用了Element PlusUI库

    npm install element-plus --save

    配置的话,可以查看文档,全局导入、按需导入都可以看自己的需求

    项目搭建完后,就可以来分析一下本次需求大概会涉及哪些功能了

    • 上传头像

    这是一个合成微信头像的工具,那就必须得让用户上传自己的微信头像了

    • 合成模版

    为了方便,我们当然还需要提供多种模版供用户自己选择

    • 用户自定义内容

    为了让生成的头像更具独一无二性,我们还需要提供用户自定义内容的功能,比如:用户输入文字、选择文字颜色等

    • 合成头像

    本次需求的重点当然是合成头像了

    • 下载合成后的头像

    用户合成完当然还得支持让他下载

    功能开发

    上传头像

    <script setup lang="ts">
    // 用户头像
    const user_img = ref("");
    const change = (file: any, fileList: any) => {
      console.log(file, fileList);
      const fileReader = new FileReader();
      fileReader.readAsDataURL(file.raw);
      fileReader.onload = (e: any) => {
        user_img.value = e.target.result;
      };
    };
    
    // 删除用户头像
    const remove = () => {
      user_img.value = "";
    };
    </script>

    这部分比较简单,主要是用户上传自己的微信头像后再进行展示,UI部分就不贴了,后面有源码。

    合成模版

    合成模版部分,这里主要是需要考虑各个模版所需要的合成功能有哪些

    <script>
    const gqList = ref([
      {
        id: 1,
        name: "模版1",
        img: getImg("gq0", "jpg"),
        template: getImg("tem1"),
        has: ["text"],
        textLabel: "请输入你的姓",
        desc: "最多输入1个字",
        text: "宋",
        textLength: 1,
      },
      {
        id: 2,
        name: "模版2",
        img: getImg("gq1", "jpg"),
        template: getImg("tem2"),
      },
      {
        id: 3,
        name: "模版3",
        img: getImg("gq2", "jpg"),
        template: getImg("tem3"),
      },
      {
        id: 4,
        name: "模版4",
        img: getImg("gq3", "jpg"),
        template: getImg("tem4"),
        has: ["text"],
        textLabel: "请输入祝福语",
        textColor: "#FED800",
        text: "生在国旗下,长在春风里",
        desc: "最多输入12个字, 请用中文逗号隔开",
        textLength: 12,
      },
      { id: 5, name: "模版5", img: getImg("gq4"), template: getImg("tem5") },
      {
        id: 6,
        name: "模版6",
        img: getImg("gq5", "jpg"),
        template: getImg("tem6"),
        has: ["text"],
        textLabel: "请输入祝福语",
        textColor: "#FED800",
        desc: "最多输入12个字, 请用中文逗号隔开",
        text: "不负韶华,只争朝夕",
        textLength: 12,
      },
      { id: 7, name: "模版7", img: getImg("gq6"), template: getImg("tem7") },
    ]);
    const template_id = ref(1);
    // 选择模版
    const gqChange = (val: any) => {
      console.log(val);
      template_id.value = val;
      generateImgRef.value.clear();
      generateImgRef.value.init();
    };
    </script>

    合成图片

    这里其实也不难,主要是使用canvas来绘制图片以及文字,由于各个模版的合成逻辑不一样,这里就不全部展示了,但整体上的合成流程是一样

    // 模版4
    const drawImg4 = (ctx: any) => {
      const img = new Image();
      img.src = user_img.value;
      const gqImg = new Image();
      gqImg.src = gqList.value[template_id.value - 1].img;
      img.onload = () => {
        ctx.drawImage(img, 0, 0, 300, 300); // 绘制头像
        gqImg.onload = () => {
          ctx.drawImage(gqImg, 0, 0, 300, 300); // 绘制国庆图
          ctx.fillStyle = textColor.value; // 设置文字颜色
          ctx.font = "20px kaiti"; // 设置文字大小及字体
          const textList = text.value?.split(",") ?? []; // 以中文逗号分割文字
          textList.forEach((item: string, i: number) => {
            drawVerticalText(ctx, item ?? "", 20 + i * 20, 186 + i * 20, {
              size: 20,
            }); // 绘制文字
          });
        };
        canDownload.value = true; // 合成完成
      };
    };

    这里主要的难点在于canvas默认不支持文字竖排绘制,所以这里需要特殊处理,原理其实就是遍历文字,计算文字高度,然后再一个一个去绘制

    // 文字竖排
    const drawVerticalText = (
      context: any,
      text: string,
      x: number,
      y: number,
      font: any
    ) => {
      context.save();
      context.font = font;
      for (var i = 0; i < text.length; i++) {
        context.fillText(text[i], x, y + i * font.size);
      }
      context.restore();
    };

    下载图片

    这里主要是借助a标签的download属性,这里在手机上有点坑,后面会提到...

    const downloadImg = () => {
      if (!canDownload.value) {
        ElMessage({
          message: "请先合成头像~",
          type: "warning",
        });
        return;
      }
    
      const url = canvas.value.toDataURL("image/png");
      const a = document.createElement("a");
      a.href = url;
      a.download = "国庆头像.png";
      a.click();
    };

    自动化部署

    这里其实之前有写过文章,主要是使用github action来完成

    搭建完就是这样的,我们写完代码只需要将代码提交上去就能够自动打包部署了

    gq2.png

    对这个不了解的可以去看我之前的文章:使用GitHub Actions实现自动化部署

    体验

    开发部署完就可以来体验一下了:体验地址

    gq3.gif

    PC上体验下来,效果还可以。

    问题及解决方案

    开发过程中也遇到一些问题,来看看是如何解决的吧

    保存图片不清晰

    canvas绘制图片不清晰的原因主要是:

    • 图片被放大或缩小
    • 图片没处于完整像素的位置

    因为canvas是点阵图,由一个个像素组成,当图像被放大时,一个像素会被强形拉伸至一个以上,多出来的像素均匀的分部在图像中,计算机为了使拉伸后的图像看起来平滑,会给这些多出来的像素计算出一个过渡色,缩小图像时,多个像素合成一个像素,计算机会用这多个像素的色彩值计算出一个过渡色来填充这个像素,不管是放大还是缩小,都会造成图像原有像素信息丢失。

    所以只需要加上以下代码就能解决

    const dpr = window.devicePixelRatio || 1; // 获取设备的devicePixelRatio
    canvas.value.width = 300 * dpr; // 画布宽高放大dpr倍,绘制后再缩小dpr倍,解决模糊问题
    canvas.value.height = 300 * dpr; // 画布宽高放大dpr倍,绘制后再缩小dpr倍,解决模糊问题
    canvas.value.style.width = "300px"; // 显示高
    canvas.value.style.height = "300px"; // 显示高
    ctx.value.scale(dpr, dpr); // 按比例缩放画布,解决模糊问题

    gq4.png

    优化完,清晰度提升还是非常明显的

    移动端体验问题

    手机上下载图片会失败,这主要是因为blob格式在手机上不能下载,base64格式有点大,那就只能上传CDN再进行下载了?

    不需要,我们可以利用手机上的长按图片保存来实现

    const downloadImg = () => {
      if (!canDownload.value) {
        ElMessage({
          message: "请先合成头像~",
          type: "warning",
        });
        return;
      }
    
      const url = canvas.value.toDataURL("image/png");
      if (devices.some((item) => ua.includes(item))) {
        ElMessageBox.alert(
          `
        请长按图片保存
        <img src="${url}" style="width: 100%;height: 100%;object-fit: contain;" />
        `,
          "保存图片",
          {
            dangerouslyUseHTMLString: true,
          }
        );
        return;
      }
      const a = document.createElement("a");
      a.href = url;
      a.download = "国庆头像.png";
      a.click();
    };

    gq5.png

    打包部署问题

    打包生成的_plugin-vue_export-helper.cdc0426e.js文件访问404,刚开始我还以为是打包路径配置的有问题,但如果是打包路径的问题的话也不会只有这一个文件有问题。

    gq6.png

    最终,我在vite的issues中找到了答案

    gq7.png

    简单点讲就是Github Pages 阻止了以下划线字符开头的文件,所以会导致这个文件访问返回404.

    解决方法就是修改打包逻辑:

    const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
    const DRIVE_LETTER_REGEX = /^[a-z]:/i;
    
    build: {
        outDir: "dist",
        assetsDir: "assets",
        chunkSizeWarningLimit: 2000, // 解决包大小超过500kb的警告
        rollupOptions: {
          output: {
            manualChunks: {
              // elementPlus: ["element-plus"],
              // highlightjs: ["highlight.js"],
            },
            chunkFileNames: "assets/[name]-[hash].js",
            entryFileNames: "assets/[name]-[hash].js",
            assetFileNames: "assets/[name]-[hash].[ext]",
            sanitizeFileName: (name) => {
              const match = DRIVE_LETTER_REGEX.exec(name);
              const driveLetter = match ? match[0] : "";
              return (
                driveLetter +
                name.slice(driveLetter.length).replace(INVALID_CHAR_REGEX, "") // 处理文件名中的非法字符
              );
            },
          },
        },
      },

    vite.config.ts中加上以上代码,重新提交部署就可以了。

    最后

    整个内容到这里就结束了

    • 体验地址:https://bettersong.github.io/nanjiu-tools/#/generate_image
    • 源码:公众号回复国庆快乐即可获取


沪ICP备19023445号-2号
友情链接