今天接到一个需求需要生成一个海报并实现保存成图片的功能,也是爬了很多的微信的坑,以及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:最终效果 展示