canvas 踩坑記
前言
原來以為還不想這么快接觸canvas的,可是最近因為業(yè)務(wù)需要,不得不提前開始接觸canvas了。
需求是根據(jù)后臺傳回來的圖片和文字進行合成圖片,然后保存到本地
雖然可以根據(jù)現(xiàn)有的插件html2canvas,但是畢竟第一次接觸,也想了解下canvas的 api,所以還是自己動手寫寫咯
要實現(xiàn)的最后的樣子如下,就大概畫了個草圖:

實現(xiàn)這樣的內(nèi)容,其實也還算簡單,使用到的 api 也不多,主要就兩個,一個是繪制文本的fillText,一個是繪制圖片的drawImage。
踩坑過程
圖片位置計算
因為需要依次獲取圖片,好計算下一張圖片的顯示位置,所以這里簡單使用promise去加載圖片,并且將當(dāng)前的圖片對象返回,之后好記錄些數(shù)據(jù):
function loadImageSync(src) {
return new Promise(resolve => {
let img = new Image()
img.onload = () => {
resolve(img)
}
img.src = src
})
}
然后就可以這樣使用了:
// 假設(shè)img_src1和img_src2是后臺獲取到的
let { img_src1, img_src2 } = this.data
let img1 = null
loadImageSync(img_src1)
.then(img => {
img1 = img
ctx.drawImage(img1, 0, 0)
return loadImageSync(img_src2)
})
.then(img => {
ctx.drawImage(img, 0, img1.height)
})
這樣每張圖片的繪制高度就容易計算了。
文本換行
因為bgImage部分還要顯示一段文本內(nèi)容,而文本內(nèi)容也是從后臺獲取后來的,具體有多少字都是不清楚的,然后也不能全部放在一行,畢竟canvas沒有提供文字換行的 api。。不過好在有measureText這個方法,這個方法可以返回文字的寬度,可以利用這點做個計算:
function textAutoLine(text, canvas, initX, initY, lineHeight) {
let ctx = canvas.getContext('2d')
// 等同于css的font
ctx.font = 'bold 18px Microsoft Yahei'
// 初始化一行字體的寬度為0
let lineWidth = 0
let canvasWidth = canvas.width
// 初始化截取位置為0
let lastSubStrIndex = 0
for (let i = 0; i < text.length; i++) {
// 循環(huán)計算每個字的寬度 并累加到行寬上
lineWidth += ctx.measureText(text[i]).width
// 判斷行寬是否到達一個臨界值 也就是: 容器寬 - 文字第一個字的位置 - 臨界值(可自定義調(diào)整)
if (lineWidth > canvasWidth - (initX + 40)) {
// 一行達到臨界,開始繪制,并且重置行寬,另起一行,開始繪制下一行
ctx.fillText(text.substring(lastSubStrIndex, i), initX, initY)
initY += lineHeight
lineWidth = 0
lastSubStrIndex = i
}
if (i === text.length - 1) {
// 第二行字 再做偏移
ctx.fillText(text.substring(lastSubStrIndex, i + 1), initX + 40, initY)
}
}
}
繪制 dom
底部內(nèi)容是后臺返回的一段html代碼,因為是個dom片段,所以使用canvas繪制起來就相當(dāng)麻煩了,經(jīng)資料查找后發(fā)現(xiàn)可以通過特殊的方式,將dom繪制成圖片:
function drawDomToImg(dom) {
let domData = `<svg xmlns="http://www.w3.org/2000/svg" width="500" height="200">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">${dom}</div>
</foreignObject></svg>`
let DOMURL = window.URL || window.webkitURL || window
let svg = new Blob([domData], { type: 'image/svg+xml;charset=utf-8' })
let src = DOMURL.createObjectURL(svg)
return loadImageSync(src)
}
這樣就能將一段dom也繪制成圖片了,最后我們將繪制后的路徑傳到loadImageSync中,也能得到這個圖片的信息了,比如寬和高。
圖片跨域
整個內(nèi)容都繪制完成之后,要將其生成圖片,使用canvas的toDataURL就可以完成,不過這個操作出現(xiàn)了個問題,就是出現(xiàn)了圖片跨域的問題。
經(jīng)查找資料后發(fā)現(xiàn),在使用canvas繪制的時候,圖片的資源是本地或者外鏈都是可以的,但是生成圖片的時候,使用外鏈的圖片無法繪制進canvas中,可能是因為canvas的同源策略的保護機制吧。
解決方式需要兩點,一個是改造我們之前的loadImageSync的方法,加入一句代碼:
function loadImageSync(src) {
return new Promise(resolve => {
let img = new Image()
// 加入這句
img.setAttribute('crossOrigin', 'Anonymous')
img.onload = () => {
resolve(img)
}
img.src = src
})
}
還有一個就是需要后臺設(shè)置CORS了,就是設(shè)置Access-Control-Allow-Origin: *,允許跨域。這樣就可以使用canvas生成圖片了。