前言
國慶節馬上要到了,今天就教你如何從0到1使用canvas生成國慶風微信頭像。
本文包含以下內容:
- vue3項目搭建,需求分析
- canvas合成圖片原理
- github自動化部署
- 開發過程遇到的問題及解決方案
文末附源碼及在線體驗地址~
搭建項目,分析需求
項目的話就直接使用腳手架生成一個 Vue3 + TS
項目
npm create vue@latest
爲了方便,使用了Element Plus
UI庫
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
來完成
搭建完就是這樣的,我們寫完代碼只需要將代碼提交上去就能夠自動打包部署了
對這個不瞭解的可以去看我之前的文章:使用GitHub Actions實現自動化部署
體驗
開發部署完就可以來體驗一下了:體驗地址
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); // 按比例縮放畫布,解決模糊問題
優化完,清晰度提升還是非常明顯的
移動端體驗問題
手機上下載圖片會失敗,這主要是因爲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();
};
打包部署問題
打包生成的_plugin-vue_export-helper.cdc0426e.js
文件訪問404,剛開始我還以爲是打包路徑配置的有問題,但如果是打包路徑的問題的話也不會只有這一個文件有問題。
最終,我在vite
的issues
中找到了答案
簡單點講就是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
- 源碼:公衆號回覆國慶快樂即可獲取