使用canvas绘制海报并在微信里实现保存图片的功能 vue3+ts+canvas

512 阅读2分钟

今天接到一个需求需要生成一个海报并实现保存成图片的功能,也是爬了很多的微信的坑,以及ts的写法,绘制的画板在电脑上看着好好地,可是在手机上打开图片就看着模糊了,使用 window.devicePixelRatio解决,微信之坑,我最开始的版本是通过blob转成图片通过a标签下载,微信有安全策略不让h5页面在微信中下载,我就一直点点点就是下载不了 后面这个就弃用了 第二个是通过blob转base64这个听着没问题对吧 把image转成base64可是在安卓机还不行安卓手机直接卡的死机,我就一个三百多kb的照片给我手机干死机,也是真棒,最后就使用麻烦一点的方式实现把我们生成好的图片上传到oss服务器上面,后端再返回一下全路径,让用户长按下载

1.首先创建一个画布

 <canvas ref="canvasRef" id="canvas"></canvas>

2.引入你自己所需的图片

import bgImage1 from "@/assets/image/buxing.png";

3.开始绘制我们背景图

 const canvasRef = ref<HTMLCanvasElement | null>(null); //拿到我们的canvas容器
 // window.devicePixelRatio获取到的是机型缩放大小 为了解决图片模糊的问题 图片的大小*比例获取到原比例的图片
 canvas.width = window.innerWidth * window.devicePixelRatio; //窗口可视宽度 
 canvas.height = window.innerHeight * window.devicePixelRatio; //窗口可视高度
 const ctx: any = canvas.getContext("2d"); //创建画板
 
 const backgroundImg = new Image(); //拿到引入的图片
 backgroundImg.src = imageMap[combination as keyof typeof imageMap]; //我这里是一个map 动态的获取图片
 backgroundImg.onload = () => {
       ctx?.drawImage(backgroundImg, 0, 0, canvas.width, canvas.height); //绘制背景图 参数为:资源 X轴位置 y轴位置 图片的宽度 图片的高度
 }

4.有一个需求就是有一个容器宽度就那个150,里面的文字计算出来可能是160导致溢出容器,特别的不美观,我们使用动态计算fontSize的大小来完成此任务

   let fontSize = 42; // 将初始字体大小乘以 devicePixelRatio
          while (true) {
            ctx.font = `${fontSize * window.devicePixelRatio}px DingTalk`; // 使用了缩放功能也要把文字给*上去这样页面的比例才合适
            const measuredWidth = ctx
              ? ctx.measureText(getCarbonFt() + "kg").width //获取文字的宽度 如果大于175就--
              : undefined;
            const measuredHeight = fontSize;
            if (
              measuredWidth <= 175 * window.devicePixelRatio &&
              measuredHeight <= 175 * window.devicePixelRatio
            ) {
              break; // 符合目标尺寸范围,退出循环
            }
            fontSize--;
      }

5.保存照片

   const downLoadPic = () => {
      downloadCanvasImage(canvasRef.value as HTMLCanvasElement); //获取到canvas的内容
   };
 const downloadCanvasImage = async (canvas: HTMLCanvasElement) => {
      // 将 Canvas 转换为图片 URL
      showLoadingToast({
        message: "图片生成中...",
        forbidClick: true,
        loadingType: "spinner",
        duration: 0,
      });
      const dataURL = canvas.toDataURL("image/jpeg"); //转换为图片
      const blob = await (await fetch(dataURL)).blob(); // 图片转换为bob类型
  
      const formData = new FormData();
      formData.append("file", blob, "EC-CarbonFootprint.png"); // 把图片的信息塞到我们的formData中
      let {
        data: { data, success },
      } = await uploadImage(formData); // 调用后端的接口
      if (success) {
        closeToast();
        const imageUrl = data.fileAddress; // 把返回的线上地址传入到下一个页面
        router.push({
          path: "/downloadImg",
          query: { imageUrl: imageUrl },
        });
      
      }
    };

完整代码

<template>
  <div class="canvas-container">
    <canvas ref="canvasRef" id="canvas"></canvas>
  </div>

  <div class="downLoad" @click="downLoadPic">
    <iconpark-icon name="xiazai"></iconpark-icon><span>保存至相册</span>
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import bgImage1 from "@/assets/image/buxing.png";
import bgImage2 from "@/assets/image/qixing.png";
import bgImage3 from "@/assets/image/gongjiao.png";
import bgImage4 from "@/assets/image/ditie.png";
import bgImage5 from "@/assets/image/dianche.png";
import bgImage6 from "@/assets/image/ranyouche.png";
import bgImage7 from "@/assets/image/gaotie.png";
import bgImage8 from "@/assets/image/feiji.png";
import logo from "@/assets/image/logo.png";
import { useMyStore } from "@/store/index";
import { uploadImage } from "@/api/upload";
import { showLoadingToast, closeToast } from "vant";
export default defineComponent({
  setup() {
    const canvasRef = ref<HTMLCanvasElement | null>(null);
    const route = useRoute();
    const router = useRouter();
    const distanceSum = computed(() => {
      return route.query.distance;
    });
    const storeData = useMyStore();
    // console.log(storeData.cIndex);
    onMounted(() => {
      if (!storeData.cIndex) {
        router.back();
      }
      // console.log(getCarbonFt() || 0);
      const canvas = canvasRef.value;
      if (canvas) {
        // 设置 canvas 宽高为全屏尺寸
        canvas.width = window.innerWidth * window.devicePixelRatio;
        canvas.height = window.innerHeight * window.devicePixelRatio;

        // 获取绘图上下文
        const ctx: any = canvas.getContext("2d");

        // 绘制背景

        const backgroundImg = new Image();
        const combination = "bgImage" + storeData.cIndex;
        const imageMap = {
          bgImage1,
          bgImage2,
          bgImage3,
          bgImage4,
          bgImage5,
          bgImage6,
          bgImage7,
          bgImage8,
        };
        backgroundImg.src = imageMap[combination as keyof typeof imageMap];
        // 放着背景图
        backgroundImg.onload = () => {
          ctx?.drawImage(backgroundImg, 0, 0, canvas.width, canvas.height);

          // 创建logo
          const logoImage = new Image();
          logoImage.src = logo;
          logoImage.onload = () => {
            ctx?.drawImage(
              logoImage,
              13 * window.devicePixelRatio,
              12 * window.devicePixelRatio,
              89 * window.devicePixelRatio,
              30 * window.devicePixelRatio
            );
          };
          ctx.fillStyle = " rgba(255,255,255,0.8)";
          ctx?.beginPath();

          ctx?.arc(
            canvas.width - 27 - 92.5 * window.devicePixelRatio,
            150 * window.devicePixelRatio,
            92.5 * window.devicePixelRatio,
            0,
            2 * Math.PI
          );

          ctx?.fill();

          // 绘制文字

          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          const textX = canvas.width - 27 - 92.5 * window.devicePixelRatio; // 文字的 x 坐标
          const textY = 150 * window.devicePixelRatio; // 文字的 y 坐标
          const lineHeight = 35 * window.devicePixelRatio; // 文字行高
          const text1 = "您的碳足迹为:";
          const text2 = getCarbonFt() + "g";
          const text3 = " CO₂eq";
          ctx.font = `${24 * window.devicePixelRatio}px DingTalk`;
          ctx.fillStyle = "#1D2129";
          ctx.fillText(
            text1,
            textX + 10 * window.devicePixelRatio,
            textY - lineHeight + 5 * window.devicePixelRatio
          );

          let fontSize = 42; // 将初始字体大小乘以 devicePixelRatio
          while (true) {
            ctx.font = `${fontSize * window.devicePixelRatio}px DingTalk`;
            const measuredWidth = ctx
              ? ctx.measureText(getCarbonFt() + "kg").width
              : undefined;
            const measuredHeight = fontSize;
            if (
              measuredWidth <= 175 * window.devicePixelRatio &&
              measuredHeight <= 175 * window.devicePixelRatio
            ) {
              break; // 符合目标尺寸范围,退出循环
            }
            fontSize--;
          }
          // ctx.font = "30px Arial";
          ctx.fillStyle = "#04AF85";
          ctx.fillText(text2, textX, textY + 10 * window.devicePixelRatio);

          ctx.font = `${28 * window.devicePixelRatio}px DingTalk`;
          ctx.fillStyle = "#04AF85";
          ctx.fillText(
            text3,
            textX + 15 * window.devicePixelRatio,
            textY + 10 * window.devicePixelRatio + lineHeight
          );

          window.addEventListener("resize", () => {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;

            // ctx?.drawImage(backgroundImg, 0, 0, canvas.width, canvas.height);
            // ctx?.drawImage(logoImage, 13, 12, 89, 30);
          });
        };
      }
    });
    const metersToKilometers = (meters: number) => {
      const kilometers = meters / 1000; // 将米数除以1000,得到千米数
      return kilometers;
    };
    const Formula = (value: number) => {
      const FormuleMap: { [key: number]: number } = {
        1: 0,
        2: 0,
        3: 24,
        4: 22,
        5: 50,
        6: 185,
        7: 30,
        8: 214,
      };
      return FormuleMap[value as keyof typeof FormuleMap];
    };
    const getCarbonFt = () => {
      return (
        metersToKilometers(parseInt(distanceSum.value as string)) *
        Formula(storeData.cIndex)
      ).toFixed(1);
    };
    const downLoadPic = () => {
      downloadCanvasImage(canvasRef.value as HTMLCanvasElement);
    };
    const downloadCanvasImage = async (canvas: HTMLCanvasElement) => {
      // 将 Canvas 转换为图片 URL
      showLoadingToast({
        message: "图片生成中...",
        forbidClick: true,
        loadingType: "spinner",
        duration: 0,
      });
      const dataURL = canvas.toDataURL("image/jpeg");
      const blob = await (await fetch(dataURL)).blob();
    
      const formData = new FormData();
      formData.append("file", blob, "EC-CarbonFootprint.png");
      let {
        data: { data, success },
      } = await uploadImage(formData);
      if (success) {
        closeToast();
        const imageUrl = data.fileAddress;
        router.push({
          path: "/downloadImg",
          query: { imageUrl: imageUrl },
        });
     
      }
    };

    return {
      canvasRef,
      downLoadPic,
      distanceSum,
    };
  },
});
</script>
<style lang="less" scoped>
.canvas-container {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  position: relative;
}
#canvas {
  width: 100%;
  height: 100%;
  transform-origin: top left;
}
.downLoad {
  width: 180px;
  height: 44px;
  background: #04af85;
  box-shadow: 0px 2px 6px 0px rgba(141, 141, 161, 0.5);
  border-radius: 22px;
  line-height: 44px;
  text-align: center;
  font-size: 16px;
  font-weight: 500;
  color: #ffffff;
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);

  iconpark-icon {
    vertical-align: middle;
  }
  span {
    vertical-align: middle;
    margin-left: 5px;
  }
}
</style>

end:最终效果 展示

微信图片_20230608183821.jpg