前言
近期需要開發(fā)一個微信小程序生成海報分享的功能。在h5一般都會直接采用 html2canvas
或者 dom2image
之類的庫直接處理。但是由于小程序不具備傳統(tǒng)意義的dom元素,所以也沒有辦法采用此類工具。
所以就只能一筆一筆的用 canvas
畫出來了,下面對實現(xiàn)這個功能中遇到的問題做一個簡單的復(fù)盤。
制作要求:
- 主題切換。
- 圖片彈框展示,適應(yīng)不同的手機(jī)尺寸。
- 圖片上層有彈出框展示保存圖片按鈕。
- 海報內(nèi)容,
- 標(biāo)題部分根據(jù)實際內(nèi)容展示,可能為一行也可能為兩行
- 描述部分,最多展示四行,超出的顯示成…
- 圓角圖片展示
- 圓角虛線框
基本方案流程
- 預(yù)先加載好所有需要的圖片。
- 在偏離視窗顯示區(qū)域使用
canvas
繪制海報,并生成臨時文件。 - 彈窗的圖片使用 生成的臨時圖片。
- 設(shè)置圖片的寬度為適應(yīng)屏幕的,可通過定位或者
flex
來實現(xiàn),圖片高度根據(jù)寬度自動縮放。超出的內(nèi)容滾動顯示。
效果圖如下:
微信canvas組件的相關(guān)問題
canvas
屬于微信客戶端創(chuàng)建的原生組件,所以需要注意一些原生組件的限制
- 原生組件的層級是最高的,所以頁面中的其他組件無論設(shè)置
z-index
為多少,都無法蓋在原生組件上。- 后插入的原生組件可以覆蓋之前的原生組件。
- 原生組件還無法在
picker-view
中使用 - 部分 CSS 樣式無法應(yīng)用于原生組件
- 無法對原生組件設(shè)置 CSS 動畫
- 無法定義原生組件為
position: fixed
- 不能在父級節(jié)點使用
overflow: hidden
來裁剪原生組件的顯示區(qū)域
所以無法使用 canvas
繪制的圖片直接用于顯示。會遇到層級以及尺寸的問題。
預(yù)加載圖片資源
在繪制之前我們需要先加載好圖片資源并保存。
function create(){
const img1 = preLoadImg("https:xxxx.img1", 'img1')
const img2 = preLoadImg("https:xxxx.img2", 'img2')
const img3 = preLoadImg("https:xxxx.img3", 'img3')
Promise.all([img1, img2, img3]).then(res=>{
// 開始繪制canvas
})
}
function preLoadImg(url, taskId) {
if(this.imageTempPath[taskId]) return Promise.resolve();
if (!url) return Promise.resolve();
url = /^https/.test(url) ? url : `https:${url}`;
return wx.getImageInfo({src: url}).then((res)=>{
this.imageTempPath[taskId] = res.path;
})
}
文本處理
計算不同長度的文本繪制高度
對于不同的文本長度,可能存在占一行或者多行的情況,這個時候?qū)τ谖谋疽韵碌膬?nèi)容繪制的 y
軸坐標(biāo)會造成影響。
解決方案:先定義好每一個元素在標(biāo)準(zhǔn)情況下的坐標(biāo)位置,然后對于存在可能有占據(jù)空間改變的文本,通過測量其文本寬度,計算出實際占據(jù)行數(shù),然后出多出的 y
軸位置(diff)
,并在后續(xù)的元素繪制上加上這個差值。
基本思路:
- 測量出文本的實際繪制需要的總長度
- 計算出實際繪制多少行
- 計算實際繪制行數(shù)與默認(rèn)行數(shù)的高度差
計算方法如下:
function getWordDiffDistance(
ctx, // canvas 上下文
text, // 要計算的文本
baseline, // 默認(rèn)顯示行數(shù)
lineHeight, // 行高
fontSize, // 字號
textIndent, // 首行縮進(jìn)字符
maxWidth, // 每一行繪制的最大寬度
maxLine // 最大允許顯示行數(shù)
) {
// 設(shè)置上下文的字號
ctx.setFontSize(fontSize);
// 首行縮進(jìn)的寬度
const textIndentWidth = fontSize * textIndent;
//實際總共能分多少行
let allRow = Math.ceil((ctx.measureText(text).width + textIndentWidth) / maxWidth);
allRow = Math.min(allRow, maxLine);
return (allRow - baseline) * lineHeight;
}
ctx.measureText() 要先設(shè)置好文本屬性。
文本超出指定行數(shù)后顯示 …
基本思路:
- 設(shè)置好 canvas 上下文的文字樣式
- 通過 measureText 計算出當(dāng)前文本需要繪制多少行
- 如果是首行且設(shè)置了首行縮進(jìn),繪制的 x 要加上縮進(jìn)的寬度
- 然后計算出每一行要繪制的文字并進(jìn)行繪制,并記錄最后的截取位置
- 如果最后一行的實際繪制寬度大于設(shè)置的最大寬度,添加… 否則正常繪制
dealWords(options) {
const {
ctx,
fontSize,
word,
maxWidth,
x,
y,
maxLine,
lineHeight,
style,
textIndent = 0,
} = options;
ctx.font = style || "normal 12px PingFangSC-Regular";
//設(shè)置字體大小
ctx.setFontSize(fontSize);
// 首行縮進(jìn)的寬度
const textIndentWidth = fontSize * textIndent;
//實際總共能分多少行
let allRow = Math.ceil((ctx.measureText(word).width + textIndentWidth) / maxWidth);
//實際能分多少行與設(shè)置的最大顯示行數(shù)比,誰小就用誰做循環(huán)次數(shù)
let count = allRow >= maxLine ? maxLine : allRow;
//當(dāng)前字符串的截斷點
let endPos = 0;
for (let j = 0; j < count; j++) {
let startWidth = 0;
if (j == 0 && textIndent) startWidth = textIndentWidth;
let rowRealMaxWidth = maxWidth - startWidth;
//當(dāng)前剩余的字符串
let nowStr = word.slice(endPos);
//每一行當(dāng)前寬度
let rowWid = 0;
if (ctx.measureText(nowStr).width > rowRealMaxWidth) {
//如果當(dāng)前的字符串寬度大于最大寬度,然后開始截取
for (let m = 0; m < nowStr.length; m++) {
//當(dāng)前字符串總寬度
rowWid += ctx.measureText(nowStr[m]).width;
if (rowWid > rowRealMaxWidth) {
if (j === maxLine - 1) {
//如果是最后一行
ctx.fillText(
nowStr.slice(0, m - 1) + "...",
x + startWidth,
y + (j + 1) * lineHeight
); //(j+1)*18這是每一行的高度
} else {
ctx.fillText(
nowStr.slice(0, m),
x + startWidth,
y + (j + 1) * lineHeight
);
}
endPos += m; //下次截斷點
break;
}
}
} else {
//如果當(dāng)前的字符串寬度小于最大寬度就直接輸出
ctx.fillText(nowStr.slice(0), x, y + (j + 1) * lineHeight);
}
}
}
繪制多行文本計算行寬的時候,空白字符可能會對最終的計算結(jié)果造成一定影響,所以可以先對其空白字符進(jìn)行過濾。
圖文對齊
微信小程序中通過 setTextBaseline
設(shè)置文本豎直對齊方式??蛇x值有 top
,bottom
,middle
,normal
;
圖片的坐標(biāo)基點為左上角坐標(biāo),所以在繪制的時候要注意 y
的起始坐標(biāo)。如果有修改 文本的對齊方式,在結(jié)束的時候最好將文本豎直對齊方式設(shè)置為 normal
,避免影響后續(xù)的繪制。
形狀處理
繪制圓角矩形路徑
使用arc()
方式繪制弧線
// 按照canvas的弧度從 0 - 2PI 開始順時針繪制
function drawRoundRectPathWithArc(ctx, x, y, width, height, radius) {
ctx.beginPath();
// 從右下角順時針繪制,弧度從0到1/2PI
ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2);
// 矩形下邊線
ctx.lineTo(x + radius, y + height);
// 左下角圓弧,弧度從1/2PI到PI
ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI);
// 矩形左邊線
ctx.lineTo(x, y + radius);
// 左上角圓弧,弧度從PI到3/2PI
ctx.arc(x + radius, y + radius, radius, Math.PI, (Math.PI * 3) / 2);
// 上邊線
ctx.lineTo(x + width - radius, y);
//右上角圓弧
ctx.arc(x + width - radius,y + radius, radius, (Math.PI * 3) / 2, Math.PI * 2);
//右邊線
ctx.lineTo(x + width, y + height - radius);
ctx.closePath();
}
使用arcTo()
方式繪制弧線
function drawRoundRectPathWithArcTo(ctx, x, y, width, height, radius) {
ctx.beginPath();
// 上邊線
ctx.lineTo(x + width - radius, y);
// 右上弧線
ctx.arcTo(x + width, y, x + width, y + radius, radius)
//右邊線
ctx.lineTo(x + width, y + height - radius);
// 從右下角順時針繪制,弧度從0到1/2PI
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
// 矩形下邊線
ctx.lineTo(x + radius, y + height);
// 左下角圓弧,弧度從1/2PI到PI
ctx.arcTo(x, y + height, x, y +height -radius, radius)
// 矩形左邊線
ctx.lineTo(x, y + radius);
// 左上角圓弧,弧度從PI到3/2PI
ctx.arcTo(x,y, x+ radius, y, radius)
ctx.closePath();
}
背景色填充
function fillRoundRectPath(ctx, x, y, width, height, radius, color){
ctx.save();
this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
ctx.setFillStyle(color);
ctx.fill();
ctx.restore();
}
圖片填充
function drawRoundRectImg(ctx, x, y, width, height, radius, img) {
if(!img) return
ctx.save();
this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
// 剪切 原始畫布中剪切任意形狀和尺寸。一旦剪切了某個區(qū)域,則所有之后的繪圖都會被限制在被剪切的區(qū)域內(nèi)
ctx.clip();
ctx.drawImage(img, x, y, width, height);
ctx.restore();
}
虛線框
function strokeRoundRectPath(ctx, x, y, width, height, radius) {
this.drawRoundRectPathWithArc(ctx, x, y, width, height, radius);
ctx.strokeStyle = "#DDDDDD";
ctx.lineWidth = 0.5;
ctx.setLineDash([6, 5]);
ctx.stroke();
}
生成臨時圖片
wx.canvasToTempFilePath(Object object, Object this)
把當(dāng)前畫布指定區(qū)域的內(nèi)容導(dǎo)出生成指定大小的圖片。在 draw()
回調(diào)里調(diào)用該方法才能保證圖片導(dǎo)出成功。
ctx.draw(false, async () => {
// canvas畫布轉(zhuǎn)成圖片并返回圖片地址
const { tempFilePath } = await wx.canvasToTempFilePath(
{
x: 0, // 指定的畫布區(qū)域的左上角橫坐標(biāo)
y: 0, // 指定的畫布區(qū)域的左上角縱坐標(biāo)
width: posterImg_width, // 指定的畫布區(qū)域的寬度
height: posterImg_height, // 指定的畫布區(qū)域的高度
destWidth: posterImg_width * pixelRatio, // 輸出的圖片的寬度 導(dǎo)出大小為 canvas 的 pixelRatio 倍
destHeight: posterImg_height * pixelRatio, // 輸出的圖片的高度
canvasId: "posterCanvas",
},
this
);
this.posterTempFilePath = tempFilePath;
});
不同像素手機(jī)的顯示適配問題
由于只是一張圖片的展示,所以顯示適配的問題久很好解決。
- 設(shè)置圖片父層容器的側(cè)邊距,使容器自動撐開。
- 圖片寬度設(shè)置為
width:100%
, 設(shè)置mode="widthFix"
讓圖片自動縮放。
微信本地保存臨時圖片
function savePoster(tempFilePath) {
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
}).then(()=> {
wx.showToast({
title: "保存成功", // 提示的內(nèi)容,
icon: "success", // 圖標(biāo),
duration: 2000, // 延遲時間,
mask: true, // 顯示透明蒙層,防止觸摸穿透,
});
},
(err) => {
wx.showToast({
title: "保存失敗", // 提示的內(nèi)容,
icon: "none", // 圖標(biāo),
duration: 2000, // 延遲時間,
mask: true, // 顯示透明蒙層,防止觸摸穿透,
});
},);
}
主題切換
通過替換不同的背景圖片來切換不同的主題。
參考文章
說說如何使用 Canvas 繪制弧線與曲線文章來源:http://www.zghlxwxcb.cn/news/detail-598159.html
canvas生成分享海報文章來源地址http://www.zghlxwxcb.cn/news/detail-598159.html
到了這里,關(guān)于微信小程序使用canvas生成分享海報功能復(fù)盤的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!