「建議收藏」小程序canvas繪制海報全流程 - 新聞資訊 - 雲南小程序開發|雲南軟件開發|雲南網站建設-昆明融晨信息技術有限公司

159-8711-8523

雲南網建設/小程序開發/軟件開發

知識

不(bù)管是(shì)網站,軟件還是(shì)小程序,都要(yào / yāo)直接或間接能爲(wéi / wèi)您産生價值,我們在(zài)追求其視覺表現的(de)同時(shí),更側重于(yú)功能的(de)便捷,營銷的(de)便利,運營的(de)高效,讓網站成爲(wéi / wèi)營銷工具,讓軟件能切實提升企業内部管理水平和(hé / huò)效率。優秀的(de)程序爲(wéi / wèi)後期升級提供便捷的(de)支持!

您當前位置>首頁 » 新聞資訊 » 小程序相關 >

「建議收藏」小程序canvas繪制海報全流程

發表時(shí)間:2021-2-20

發布人(rén):融晨科技

浏覽次數:244

接下來(lái),我會把純前端實現生成海報全流程給大(dà)家講個(gè)明明白白,把我自己遇到(dào)的(de)坑,給大(dà)家詳細分享并講解,防止大(dà)家遇到(dào)相似問題,即使遇到(dào)問題,也(yě)會有一個(gè)明确的(de)方向,并且吐血建議大(dà)家收藏一波,以(yǐ)備不(bù)時(shí)之(zhī)需。(你不(bù)能保證以(yǐ)後的(de)需求,沒有類似的(de)吧,有的(de)話,記得翻出(chū)來(lái)看看)

一 寫在(zài)前面

1 canvas繪制帶二維碼的(de)海報,這(zhè)些坑總有一個(gè)你可能會踩到(dào),我會帶你一步步解決這(zhè)些坑

技術選型背景:taro3.0-vue

先來(lái)十一個(gè)問題壓壓驚,相信你做繪制海報過程中,一定會遇到(dào)

taro框架遇到(dào)的(de)坑

① taro-vue createCanvasContext 獲取canvas實例無效問題,繪制不(bù)出(chū)來(lái)效果??

② taro-vue 初始化獲取不(bù)到(dào)canvas上(shàng)下文怎麽辦,完全繪制不(bù)出(chū)來(lái)圖片??

小程序canvas遇到(dào)的(de)坑

③ 關于(yú)canvas 寬高以(yǐ)及縮放比問題,繪制的(de)元素變形,畫布的(de)高度真得等于(yú)cavans标簽設置的(de)寬高麽??

④ canvas怎麽繪制疊在(zài)一起的(de)兩張圖片,并控制層級??

⑤ 如何用canvas繪制,多行文本??

⑥ 如何根據設計稿,精确還原海報各個(gè)元素位置問題。?

⑦ canvas怎麽繪制base64的(de)圖片?

⑧ 如何繪制網絡的(de)圖片,兩種canvas畫布api,繪制圖片有什麽區别完成?

生成二維碼遇到(dào)的(de)坑

⑨ 如何正确選型生成二維碼工具??

⑩ 生成的(de)二維碼,識别不(bù)出(chū)來(lái)怎麽辦,?

? 如何繪制二維碼上(shàng)的(de)logo?


二 實戰一第一階段:小程序canva初始化

1 兩種cavnas獲取上(shàng)下文方式

我們即将解決的(de)問題

① taro-vue createCanvasContext 獲取canvas實例無效問題,繪制不(bù)出(chū)來(lái)效果?

② taro-vue 初始化獲取不(bù)到(dào)canvas上(shàng)下文怎麽辦?

微信官網上(shàng)介紹兩種 canvas 獲取上(shàng)下文方式,一種是(shì)老的(de)api ,一種是(shì)新的(de) api ,接下來(lái)我将講解一下這(zhè)兩api的(de)用法。

老版本 createCanvasContext 方式

createCanvasContext是(shì)微信提供的(de)獲取 canvas實例的(de)老得接口,使用方式如下。

wxml

<canvas style="width: 300px; height: 200px;" canvas-id="firstCanvas">canvas>
複制代碼

美好的(de)一天從寫一個(gè)hello,world開始。

js中這(zhè)麽寫

onReady(){
    /*  使用 wx.createContext 獲取繪圖上(shàng)下文 context , firstCanvas 與 canvas 屬性中的(de)canvas-id一一對應  */
    const context = wx.createCanvasContext('firstCanvas')
    /* 設置字體大(dà)小 */
    context.setFontSize(20) 
    /* 設置字體顔色 */
    context.setFillStyle('pink')
    /* 設置文本内容,位置 */
    context.fillText('hello,world', 0, 0)
    context.draw()
}

複制代碼

老版本是(shì)使用createCanvasContext傳入 canvas标簽中的(de) canvas-id屬性,來(lái)獲取canvas實例,老版本的(de)使用起來(lái)說(shuō)實話,不(bù)夠靈活,很多對canvas線條,顔色的(de)設置,都封裝成方法了(le/liǎo),每次改變需要(yào / yāo)調用方法。

新版本 getContext 上(shàng)下文方式

新的(de)方式,則是(shì)先通過 createSelectorQuery 獲取 canvas 元素節點, 然後通過 getContext 獲取上(shàng)下文。

wxml

 <canvas type="2d" id="myCanvas">canvas>
複制代碼

js

const query = wx.createSelectorQuery()
query.select('#myCanvas')
.fields({
    node: true,
    size: true
})
.exec((res)=>{
    const { node } = res[0]
    if (!node) return
    /* 獲取 canvas 實例 */
    const context = node.getContext('2d')
    context.fillStyle = 'pink'
    /* 設置字體樣式 大(dà)小 字體類别 */
    context.font = 'normal 400 12px PingFangSC-Regular',
    context.fillText('hello,world', 0, 0)
})
複制代碼

這(zhè)種方式和(hé / huò)第一種 createSelectorQuery 方式,在(zài)api使用方式上(shàng)會有微妙的(de)差别,這(zhè)種寫法更像原生的(de)DOM寫法,設置顔色,樣式,直接改變context屬性,而(ér)不(bù)再需要(yào / yāo)調用對應的(de)api

taro-vue 使用 canvas

解決問題: ① taro-vue createCanvasContext 獲取canvas實例無效問題,繪制不(bù)出(chū)來(lái)效果?

因爲(wéi / wèi)我們小程序技術選擇是(shì) taro-vue2,所以(yǐ)我這(zhè)裏重點将一下,在(zài)taro-vue中,目前使用 createCanvasContext 方式獲取 canvas 實例,繪制畫布從來(lái)沒有成功過,即便是(shì)createCanvasContext能夠創建上(shàng)下文,但是(shì)任何東西也(yě)畫不(bù)出(chū)來(lái)(傳this之(zhī)類的(de)方案試了(le/liǎo)一個(gè)遍)。要(yào / yāo)是(shì)問我爲(wéi / wèi)什麽?實際我也(yě)不(bù)知道(dào),隻有凹凸實驗室的(de)同學應該更清楚,GitHub上(shàng)也(yě)有issue,希望taro團隊能夠重視起來(lái)。

解決方案就(jiù)是(shì)采用最新的(de)api,就(jiù)是(shì)上(shàng)述講的(de)第二個(gè)方案。代碼如下:

import Taro  from '@tarojs/taro'
const query = Taro.createSelectorQuery()
query.select('#myCanvas')
.fields({
    node: true,
    size: true
})
.exec(res=>{
    //TODO:....
})
複制代碼

② taro-vue 初始化獲取不(bù)到(dào)canvas上(shàng)下文怎麽辦?

在(zài)使用taro-vue的(de)過程中,會面臨一個(gè)問題,就(jiù)是(shì)小程序node節點獲取不(bù)到(dào)的(de)問題,這(zhè)個(gè)有可能是(shì)小程序本身的(de)生命周期,和(hé / huò)vue生命周期混亂造成的(de)。尤其當我們選擇的(de)是(shì)組件而(ér)不(bù)是(shì)頁面的(de)情況。對于(yú)這(zhè)樣的(de)情況,官方文檔給出(chū)了(le/liǎo)答案。頁面首次渲染完畢時(shí)執行,此生命周期在(zài)小程序端對應小程序頁面的(de) onReady 生命周期。從此生命周期開始可以(yǐ)使用 createCanvasContext 或 createselectorquery 等 API 訪問真實 DOM。

也(yě)就(jiù)是(shì)說(shuō)如果想要(yào / yāo)獲取真是(shì)dom節點,我們可以(yǐ)這(zhè)麽做,

組件中

mounted () {
    eventCenter.once(getCurrentInstance().router.onReady, () => {
       const query = Taro.createSelectorQuery()
        query.select('#myCanvas')
        .fields({
           node: true,
           size: true
        })
        .exec(res=>{
        //TODO:....
        })         
    })
}

複制代碼

尴尬的(de)是(shì),這(zhè)種情況下,有的(de)時(shí)候會造成 eventCenter.once() 回調函數不(bù)執行的(de)情況,比如說(shuō)當前組件的(de)是(shì)收到(dào)v-if控制的(de)情況。那麽怎麽樣解決呢,對于(yú)這(zhè)種情況,我教大(dà)家一種解決方案。

我們可以(yǐ)用taro中,通過 Taro.nextTick 方法,将獲取元素的(de)任務放在(zài)下一次nextTick執行。

mounted(){
  Taro.nextTick(() => {
      // 獲取元素
  })   
}
複制代碼
2 初始化 canvas設置寬高百分比

我們即将解決的(de)問題:

③ 關于(yú)canvas 寬高以(yǐ)及縮放比問題,繪制的(de)元素變形,畫布的(de)高度真得等于(yú)cavans标簽設置的(de)寬高麽?

<template>
    <view>
        <canvas
            id="myPoster"
            type="2d"
            class="canves"
            :style="canvasStyle"
        />
    view>
<template>
複制代碼

在(zài)這(zhè)裏我們首先要(yào / yāo)明白二個(gè)概念, 容器寬高: 我們給canvas标簽設置的(de)寬高,就(jiù)是(shì)如上(shàng)代碼中的(de) canvasStyle,是(shì)canvas容器的(de)寬高。 畫布寬高: 而(ér)我們畫布的(de)寬高,在(zài)新版本api中,是(shì)通過獲取node節點,動态設置的(de)node.width 和(hé / huò) node.height的(de)值。

我們期望将整個(gè)屏幕作爲(wéi / wèi)畫布,對于(yú)不(bù)同手機,屏幕尺寸都會有差别,所以(yǐ)要(yào / yāo)動态獲取設備的(de)寬高。這(zhè)裏有一個(gè)問題是(shì) 容器寬高等于(yú)畫布寬高嗎 , 答案是(shì)否定的(de),爲(wéi / wèi)什麽這(zhè)麽說(shuō)呢,原因如下 小程序的(de)canvas畫布有一個(gè)原始的(de)畫布寬高,以(yǐ)及縮放比,而(ér)且是(shì)按照一倍像素來(lái)的(de),當我們給canvas容器設定容器寬高之(zhī)後,如果沒有對應設置canvas畫布的(de)畫布寬高以(yǐ)及scale,畫出(chū)的(de)畫布就(jiù)會嚴重的(de)變形,我們用一個(gè)例子(zǐ)來(lái)解釋。

比如我們想再畫布上(shàng)半部分區域,畫一個(gè)圖片,當我們期望正常比例畫 canvas ,如果我們隻給cavans标簽加寬高,而(ér)不(bù)給畫布設置寬高的(de)時(shí)候。會按照原始畫布的(de)寬高比去繪制。期望結果,畫布充滿屏幕,圖片按照正常比列展示。當我們不(bù)給 cavnas 畫布設置畫布寬高 以(yǐ)及縮放比的(de)時(shí)候。會發生下面的(de)情況。

實際效果:

所以(yǐ)我們初始化的(de)時(shí)候要(yào / yāo)給canvas如下操作。這(zhè)個(gè)在(zài)微信的(de)官方文檔中,都有說(shuō)明。

import Taro, {
    eventCenter,
    getCurrentInstance
} from '@tarojs/taro'

export default {
    
    name:'myPoster',
    data(){
        const {
            windowHeight,
            windowWidth,
            pixelRatio
        } = Taro.getSystemInfoSync() /* 動态獲取設備的(de)寬和(hé / huò)高  */
       return {
            canvasStyle: {           /* cavnas 的(de)寬高 */
                width: windowWidth + 'px',
                height: windowHeight + 'px',
            },
            windowWidth,
            pixelRatio,   /* 屏幕縮放比 */
            windowHeight,
            scale:1       
       }
    },
    mounted(){
        Taro.nextTick(() => {
            const query = Taro.createSelectorQuery()
            query.select('#myPoster').fields({
                node: true,
                size: true
            }).exec(res => {
                let {
                    node,
                } = res[0]
                if (!node) return
                 /* 第一步: canvas 畫布的(de)寬高 和(hé / huò) 元素的(de)寬高 必須保持相同的(de)長寬比列,否則會變形 */
                const dpr = this.pixelRatio
                const context = node.getContext('2d')
                node.width = windowWidth * dpr
                node.height = windowHeight * dpr
                context.scale(dpr, dpr)
                context.fillStyle = '#fff'
                context.fillRect(0, 0, windowWidth, windowHeight)
            })
        })
    }
}
複制代碼

當我們設置好畫布寬高,以(yǐ)及縮放比之(zhī)後,就(jiù)能按照正常比列進行繪制了(le/liǎo)。讓我們一起看看設置完縮放比之(zhī)後的(de)圖片效果,變成了(le/liǎo)我們想要(yào / yāo)的(de)效果。

接下來(lái)就(jiù)是(shì)繪制階段。


三 實戰第二階段: 虛拟點位繪制canvas階段

在(zài)講解canvas如何生成海報,完美還原設計稿的(de)問題之(zhī)前,我們應該想一個(gè)問題,因爲(wéi / wèi)canvas畫布,畢竟不(bù)是(shì) dom模型,可以(yǐ)使用div或者view,通過自定義設置樣式來(lái)進行布局。cavnas需要(yào / yāo)我們畫出(chū)元素的(de)布局效果,這(zhè)裏就(jiù)要(yào / yāo)精确獲取畫布上(shàng)每一個(gè)元素相對與畫布的(de)x,y值。那麽首先想到(dào)的(de)是(shì)如何獲取每一個(gè)元素精确的(de)x , y 值。

1 虛拟點位還原實際設計稿

解決問題: ⑥ 如何根據設計稿,精确還原海報各個(gè)元素位置問題。 針對完美還原設計稿的(de)問題,比較靠譜的(de)方案就(jiù)是(shì),先1:1正常挂在(zài)dom元素,然後通過獲取元素的(de)位置,來(lái)繪制canvas畫布的(de)元素位置。我們用一幅圖來(lái)表示其原理。

注意事項

注意事項1: 選擇正确的(de)元素獲取點

這(zhè)裏打一個(gè)比方,我們在(zài)dom元素中可能存在(zài)這(zhè)樣的(de)結構。

<view class="box" >
    <view class="parent" >
        <view class="son" > 這(zhè)裏是(shì)将要(yào / yāo)繪制到(dào)canvas中的(de)内容。 view>
    view>
view>
複制代碼

對于(yú)上(shàng)面的(de)結構,我們隻需要(yào / yāo)将 son中的(de)内容繪制到(dào) canvas 畫布中,那麽就(jiù)有一個(gè)問題,我們要(yào / yāo)獲取哪一層級的(de)元素信息(left,top,width,height),答案應該都能猜到(dào),應該是(shì)想要(yào / yāo)繪制的(de)内容最近的(de)一層,也(yě)就(jiù)是(shì)面的(de)son層級。如果我們選外層,可能收到(dào)父元素paddingmargin等影響,導緻真實的(de)位置不(bù)準确。

注意事項2: 盡量不(bù)要(yào / yāo)給獲取信息的(de)元素增加 padding marign,如果繪制文本内容,盡量容器高度等于(yú)文本高度

還有一個(gè)問題,就(jiù)是(shì)盡量不(bù)要(yào / yāo)給需要(yào / yāo)繪制的(de)元素,增加 padding marign等屬性,如果是(shì)繪制純文本,不(bù)要(yào / yāo)設置lineHeight,如圖下示例:

我們期望在(zài)獲取 a 點的(de)位置信息, 但是(shì)最終卻獲取 b點的(de)位置信息。如果用 b 點位置來(lái)繪制canvas,勢必不(bù)能完美還原設計稿,所以(yǐ)我們在(zài)用這(zhè)種方式繪制canvas的(de)時(shí)候,應該注意這(zhè)些細節問題。

封裝獲取位置信息方法

我們需要(yào / yāo)繪制海報上(shàng)的(de)每一個(gè)點位,首先想到(dào)的(de)就(jiù)是(shì)獲取小程序元素位置方法,并封裝該方法。我們用promise來(lái)防止深層次的(de)回調,并且方便使用async await語法糖。廢話不(bù)多說(shuō),一言不(bù)合上(shàng)代碼。

    /* 獲取元素位置 */
    geDomPostion(dom, isAll) {
        return new Promise((resolve) => {
            Taro.createSelectorQuery().select(dom).boundingClientRect(rect => {
                const {
                    top,
                    left
                } = rect
                /* isAll 是(shì)否獲取設備寬高等信息 */
                resolve(isAll ? rect : {
                    top,
                    left
                })
            }).exec()
        })
    },
複制代碼

小提示:如果用wx原生,或者其他(tā)跨端框架mpvue wepy uniapp是(shì)的(de)同學,把 Taro 換成 wx 即可。


2 繪制網絡圖片

繪制網絡圖片

接下來(lái)我們要(yào / yāo)解決的(de)問題: ⑨ 如何繪制網絡的(de)圖片,兩種通過canvas畫布api,繪制圖片有什麽區别?

我們在(zài)用canvas繪制圖片的(de)時(shí)候,對于(yú)本地(dì / de)圖片可以(yǐ)直接通過canvas提供的(de)drawImage進行繪制,但是(shì)對于(yú)網絡圖片是(shì)不(bù)能這(zhè)麽繪制的(de),我們首先需要(yào / yāo)通過getImageInfo來(lái)獲取圖片的(de)臨時(shí)路徑。用getImageInfo繪制網絡資源的(de)時(shí)候請注意配置一下合法的(de)下載域名,要(yào / yāo)不(bù)然我們是(shì)無法成功獲取圖片信息的(de)。我們首先需要(yào / yāo)在(zài)小程序後台配置downloadFile合法域名。

具體步驟如下: 第一步:

第二步:

第三步:

接下來(lái)我們要(yào / yāo)做的(de)就(jiù)是(shì)讀取圖片的(de)臨時(shí)路徑,繪制到(dào)canvas畫布上(shàng)來(lái)。

 /* backGroundImageUrl 是(shì)我們要(yào / yāo)畫的(de)網絡圖片的(de)地(dì / de)址  */
 this.getImageInfo(this.backGroundImageUrl).then(res=>{
      const {
        width,   /* 寬度 */
        height,  
        path     /* 臨時(shí)路徑 */
      } = res1
      /* 第二步: 繪制banner圖 */
    const bannerImage = await this.geDomPostion('#bannerImage')
    this.startTop = bannerImage.top - 30
    this.drawImage(context, node, path, 0, 0, width, height, 0, this.startTop, windowWidth, windowWidth)
    context.save()
 })
複制代碼

this.drawImage 是(shì)我們封裝好的(de)方法,之(zhī)前說(shuō)過對于(yú)小程序獲取 context兩種接口方式,兩種方式繪制canvas圖片,有一些差别,我們馬上(shàng)道(dào)來(lái)。

新老接口繪制圖片的(de)區别

老版本繪制方法

老版本api createCanvasContext可以(yǐ)直接使用 drawImage繪制圖片。如下

/* 繪制圖片 */
context.drawImage(url,x,y,width,height,dx,dy,dwidth,dheight)
複制代碼

當時(shí)我們項目用的(de)是(shì)第二種新api getContext當時(shí)獲取上(shàng)下文,所以(yǐ)在(zài)圖片繪制方式上(shàng),會有所改變。

新版本繪制方法

  const image = node.createImage()
  image.src = http://www.wxapp-union.com/url
  image.onload = () => {
    context.drawImage(image,x,y,width,height,dx,dy,dwidth,dheight)
  }
複制代碼

用新版本的(de)API 繪制圖片的(de)同學請注意,這(zhè)個(gè)onload回調是(shì)在(zài)圖片加載完成時(shí)候執行的(de),所以(yǐ)說(shuō)明是(shì)異步的(de)。還有一個(gè)注意的(de)地(dì / de)方,相比老版本的(de) drawImage 第一個(gè)參數是(shì)圖片的(de)路徑,而(ér)新版本的(de)drawImage第一個(gè)參數是(shì)image元素。

封裝繪制圖片方法

剛才在(zài)繪制網絡圖片最後一步,我們調用了(le/liǎo) this.drawImage 方法。因爲(wéi / wèi)整個(gè)海報生成過程中,内部會畫入多張圖片,所以(yǐ)我們單獨封裝了(le/liǎo)一個(gè)繪制圖片的(de)方法。

/* 繪制圖片 */
drawImage(context, node, url, ...arg) {
    return new Promise((resolve) => {
        const image = node.createImage()
        image.src = http://www.wxapp-union.com/url
        image.onload = () => {
            context.drawImage(image, ...arg)
            resolve()
        }
    })
},
複制代碼

這(zhè)樣我們就(jiù)可以(yǐ)通過,async,await判斷圖片是(shì)否加載完成。

簡介 context.drawImage

我這(zhè)裏簡單給大(dà)家介紹一下context.drawImage用法,

CanvasContext.drawImage(imageResource / dom, sx,  sy,  sWidth,  sHeight,  dx,  dy,  dWidth,  dHeight)
複制代碼

繪制圖像到(dào)畫布,第一個(gè)參數,在(zài)老api中代表路徑,在(zài)新版本api中代表imagDom元素,

sx 需要(yào / yāo)繪制到(dào)畫布中的(de),imageResource / dom 的(de)矩形(裁剪)選擇框的(de)左上(shàng)角 x 坐标

sy 需要(yào / yāo)繪制到(dào)畫布中的(de),imageResource / dom 的(de)矩形(裁剪)選擇框的(de)左上(shàng)角 y 坐标

sWidth 需要(yào / yāo)繪制到(dào)畫布中的(de),imageResource / dom 的(de)矩形(裁剪)選擇框的(de)寬度

sHeight 需要(yào / yāo)繪制到(dào)畫布中的(de),imageResource / dom 的(de)矩形(裁剪)選擇框的(de)高度

dx imageResource的(de)左上(shàng)角在(zài)目标 canvas 上(shàng) x 軸的(de)位置

dy imageResource的(de)左上(shàng)角在(zài)目标 canvas 上(shàng) y 軸的(de)位置

dWidth 在(zài)目标畫布上(shàng)繪制imageResource的(de)寬度,允許對繪制的(de)imageResource進行縮放

dHeight 在(zài)目标畫布上(shàng)繪制imageResource的(de)高度,允許對繪制的(de)imageResource進行縮放

我們用一幅圖表示各個(gè)屬性的(de)對應什麽。

3 繪制層級圖片

解決問題: ④ canvas怎麽繪制疊在(zài)一起的(de)兩張圖片,并控制層級?

如果我們繪制疊在(zài)一起的(de)兩張圖片,需要(yào / yāo)我們做一些什麽樣的(de)工作呢?首先想到(dào)的(de)是(shì)層級問題,我們期望背景圖片放在(zài)下面,例如頭像之(zhī)類的(de)圖片放在(zài)上(shàng)面,但是(shì)在(zài)畫布中沒有控制zIndex層級的(de)屬性,那麽怎麽樣處理這(zhè)個(gè)問題呢 ?答案是(shì)實際在(zài)canvas中,繪制的(de)先後順序 就(jiù)是(shì)畫布層級順序,後畫的(de)在(zài)先畫的(de)上(shàng)層,那麽對于(yú)這(zhè)種層級問題呢,我們隻要(yào / yāo)保證層級高的(de)元素後畫,層級低的(de)元素先畫就(jiù)可以(yǐ)完美解決,接下來(lái)我們在(zài)海報中,畫上(shàng)頭像,文字等信息。


<image  class="userheadImage"  id="userheadImage"  :src="headImage"  />

複制代碼
    /*TODO: 繪制頭像 */
    const userheadImage = await this.geDomPostion('#userheadImage',true)
    /* 圓形圖片 */
    let d = userheadImage.height / 2
    const cx = userheadImage.left + userheadImage.width / 2
    let cy = userheadImage.top + userheadImage.height / 2
    context.arc(cx, cy, d, 0, 2 * Math.PI)
    context.strokeStyle = '#FFFFFF'
    context.stroke()
    context.clip()
    await this.drawImage(context, node, this.headImage, userheadImage.left, userheadImage.top, userheadImage.width, userheadImage.height)
    context.restore()
    this.drawText(context,{ top: userheadImage.top + userheadImage.height + 40 ,left : userheadImage.left - 70 },'我不(bù)是(shì)外星人(rén)「前端Sharing」',18,'normal 600 20px PingFangSC-Regular','#fff')
複制代碼

在(zài)我們使用context.clip()之(zhī)後,記得使用context.restore()重置,否則将無法繪制其他(tā)元素。

效果:

我們完美解決了(le/liǎo)片文本的(de)層級問題,接下來(lái),我們就(jiù)要(yào / yāo)繪制海報的(de)主要(yào / yāo)的(de)内容了(le/liǎo)。在(zài)我們繪制海報的(de)時(shí)候,可能會遇到(dào)多行文本的(de)情況,那麽多對多行文本,我們是(shì)怎麽解決的(de)呢?

4 繪制多行文本

解決問題:⑤ 如何用canvas繪制,多行文本?

canvas畫的(de)文本,并不(bù)能像我們的(de)dom元素下的(de)文本一樣,可以(yǐ)自動換行,我們如何還原,多行文本的(de)效果呢。這(zhè)這(zhè)裏教大(dà)家一種方法,我們可以(yǐ)一個(gè)一個(gè)字的(de)繪制到(dào)canvas中,然後把每個(gè)字的(de)寬度相加,如果總寬度大(dà)于(yú)容器的(de)寬度,那麽就(jiù)另外起一行,增加每一行的(de)高度,從頭開始畫。,我們直接上(shàng)代碼。

       /** 畫多行文本
         * @param ctx          canvas 上(shàng)下文
         * @param str          多行文本
         * @param initHeight   容器初始 top值
         * @param initWidth    容器初始 left值
         * @param canvasWidth  容器寬度
         */
        drawRanksTexts(ctx, str, initHeight, initWidth, canvasWidth) {
            let lineWidth = 0;
            let lastSubStrIndex = 0;
            /* 設置文字樣式 */
            ctx.fillStyle = "#303133"
            ctx.font = 'normal 400 15px  PingFangSC-Regular'
            for (let i = 0; i < str.length; i++) {
                lineWidth += ctx.measureText(str[i]).width
                if (lineWidth > canvasWidth) { /* 換行 */
                    ctx.fillText(str.substring(lastSubStrIndex, i), initWidth, initHeight)
                    initHeight += 20
                    lineWidth = 0
                    lastSubStrIndex = i
                }
                if (i == str.length - 1) {  /* 無需換行 */
                    ctx.fillText(str.substring(lastSubStrIndex, i + 1), initWidth, initHeight)
                }
            }

        },
複制代碼

調用

/* TODO: 複制多行文本 */
const rowsText = await this.geDomPostion('#context', true)
this.drawRanksTexts(context, this.skuName, rowsText.top, rowsText.left, rowsText.width)
複制代碼

四 實戰第三階段: 生成二維碼

接下來(lái)我們做的(de)是(shì)繪制二維碼,繪制二維碼過程,筆者踩了(le/liǎo)不(bù)少的(de)坑,尤其taro-vue不(bù)支持createCanvasContext方式,希望我能用自己踩的(de)坑,讓大(dà)家避開相同的(de)錯誤,避免大(dà)家少走很多彎路。繪制二維碼實際并沒有想象的(de)複雜,實際就(jiù)是(shì)将鏈接轉換成二維碼,讓手機掃碼或者長按可以(yǐ)識别即可,雖然原理很簡單,但是(shì)還是(shì)有很多注意的(de)細節。

繪制二維碼無異于(yú)二種方式,第一種方式就(jiù)是(shì)用canvas畫出(chū)來(lái)。第二種将鏈接轉成base64的(de)鏈接,然後讓圖片展示鏈接。 接下來(lái)我們針對這(zhè)兩種方式,進行二維碼庫的(de)技術選型。

1 關于(yú)二維碼庫選型

解決問題 ⑨ 如何正确選型生成二維碼工具?

形成二維碼的(de)過程,我們肯定不(bù)能手撸算法,因爲(wéi / wèi)即便我們能手撸出(chū)來(lái),也(yě)會占用大(dà)量時(shí)間,還會有很多bug,因爲(wéi / wèi)現在(zài)生成二維碼的(de)生态已經很健全了(le/liǎo),比如 qrcode.js等等都是(shì)非常不(bù)錯的(de),但是(shì)唯一不(bù)好的(de)是(shì)不(bù)支持小程序端。我這(zhè)裏介紹幾個(gè)二維碼的(de)庫

weapp-qrcode

對于(yú)比如短鏈接,不(bù)必拼寫很長的(de)參數,這(zhè)種情況用 weapp-qrcode 綽綽有餘。這(zhè)種方式是(shì)基于(yú)第一種用canvas繪制的(de)。而(ér)且是(shì)采用老版本的(de)api , 這(zhè)樣的(de)話就(jiù)有一個(gè)問題,如果像用新的(de) getContext 方式,就(jiù)需要(yào / yāo)把源碼下載下來(lái),然後改動一下源碼,讓它支持 getContext 這(zhè)種方式。我們來(lái)簡介一下 weapp-qrcode的(de)使用。

使用

//  将 dist 目錄下,weapp.qrcode.esm.js 複制到(dào)項目目錄中  如果用 taro uniapp 等框架 ,可以(yǐ)用  npm install 
import drawQrcode from '../../utils/weapp.qrcode.esm.js'

drawQrcode({
  width: 200,
  height: 200,
  canvasId: 'myQrcode',
  // ctx: wx.createCanvasContext('myQrcode'),
  text: 'https://juejin.cn/user/2418581313687390',
  // v1.0.0+版本支持在(zài)二維碼上(shàng)繪制圖片
  image: {
    imageResource: '../../images/icon.png',
    dx: 70,
    dy: 70,
    dWidth: 60,
    dHeight: 60
  }
})
複制代碼

結果

這(zhè)種方式下,最後确實成功了(le/liǎo),因爲(wéi / wèi)在(zài)做demo的(de)時(shí)候,我用的(de)是(shì)github短鏈接。但是(shì)一回歸筆者公司的(de)項目,很長的(de)鏈接,奈何生成的(de)二維碼特别密集,手機根本識别不(bù)出(chū)來(lái),無奈前功盡棄了(le/liǎo),隻能換其他(tā)的(de)技術方案,所以(yǐ)筆者選擇了(le/liǎo)第二種比較穩的(de)方式,形成base64文件。

qrcode-base64

qrcode-base64 是(shì)将二維碼的(de)鏈接,轉成base64的(de)鏈接,并把這(zhè)個(gè)鏈接作爲(wéi / wèi)src屬性賦值給圖片。我們先介紹一下基本用法。

下載

npm install qrcode-base64
複制代碼

使用

import QR from 'qrcode-base64'

var imgData = http://www.wxapp-union.com/QR.drawImg(this.data.codeText, {
    typeNumber: 4,
    errorCorrectLevel: 'M',
    size: 500
})
// 返回輸出(chū)base64編碼imgData
複制代碼

如上(shàng)述代碼塊所示,imgData就(jiù)是(shì)生成的(de)base64鏈接,我們可以(yǐ)直接把它作爲(wéi / wèi)圖片的(de)src,然後讓canvas将圖片繪制到(dào)我們的(de)海報中去,但是(shì)又來(lái)了(le/liǎo)一個(gè)問題,canvas是(shì)不(bù)支持繪制base64的(de)鏈接圖片的(de),真機上(shàng)沒有任何效果,真實一步十個(gè)坑啊,我們還得想辦法解決這(zhè)個(gè)問題。

2 canvas 繪制 base64圖片

解決問題 ⑦ canvas怎麽繪制base64的(de)圖片

對于(yú)上(shàng)面說(shuō)到(dào)的(de)canvas不(bù)支持base64的(de)圖片,那麽我們還要(yào / yāo)把二維碼繪制到(dào)海報中,那麽并不(bù)是(shì)沒有辦法,我們可以(yǐ)用小程序提供的(de)文件系統來(lái)解決問題。

小程序文件系統

wx.getFileSystemManager 獲取全局唯一的(de)文件管理器,返回值 類似于(yú)node中的(de)fs.

writeFile 寫入文件,可以(yǐ)将圖片寫入系統中。

const fs = wx.getFileSystemManager()

fs.writeFile(/* 寫入文件 */)
複制代碼

封裝方法

封裝繪制二維碼方法

  /* 生成二維碼 */
        drawCode(ctx, node, x, y) {
            return new Promise((resolve) => {
                const codeImageWidth = 150   /* 繪制二維碼寬度 */
                const canvasImageWidth = 85 /* 二維碼繪制到(dào)canvas的(de)寬度 */
                const left = x - 15          /* left 值 */
                const top = y - 22           /* top 值 */
                const LogoWidth = 15       /* 二維碼logo寬度 */
                const url = 'https://juejin.cn/user/2418581313687390'
                
                const base64 = QR.drawImg(url, {
                    typeNumber: 4,
                    errorCorrectLevel: 'L',
                    size: codeImageWidth
                })
                /* 創建讀寫流 */
                const fs = Taro.getFileSystemManager()
                const times = new Date().getTime()
                const codeimg = Taro.env.USER_DATA_PATH + '/' + times + '.png'

                /* 将base64圖片寫入 */
                fs.writeFile({
                    filePath: codeimg,
                    data: base64.slice(22),  /* 數據流 */
                    encoding: 'base64',      
                    success: async () => {
                        const offset = (canvasImageWidth - LogoWidth) / 2 /* 偏移量 */
                         /* 繪制圖片 */
                        await this.drawImage(ctx, node, codeimg, 0, 0, codeImageWidth, codeImageWidth, left, top, canvasImageWidth, canvasImageWidth)
                        await this.drawImage(ctx, node, this.logoUrl, left + offset, top + offset, LogoWidth, LogoWidth)
                        resolve()

                    }
                })
            })

        },
複制代碼

如上(shàng)所示我們完成了(le/liǎo)二維碼的(de)繪制。讓我們來(lái)看一下如何使用。

使用

我們在(zài)wxml上(shàng)寫一個(gè)元素,作爲(wéi / wèi)占位,方便我們可以(yǐ)獲取二維碼的(de)位置。

<view id="qrCode" class="store-uscode" />
複制代碼
/*TODO: 第四步:繪制二維碼 */
const qrCode = await this.geDomPostion('#qrCode')
await this.drawCode(context, node, qrCode.left - 20, qrCode.top - this.cavnsOffsetop)
複制代碼
3 調試二維碼大(dà)小,如何讓二維碼可以(yǐ)識别,繪制二維碼logo

解決問題:⑩ 生成的(de)二維碼,識别不(bù)出(chū)來(lái)怎麽辦。

有的(de)時(shí)候我們展示的(de)二維碼比較小的(de)時(shí)候,因爲(wéi / wèi)色塊太密,手機也(yě)會有無法識别的(de)情況。那麽我們如何調整二維碼,有能讓頁面盡量高保真的(de)還原設計稿呢,這(zhè)裏教大(dà)家一個(gè)小技巧,可以(yǐ)去先去二維碼生成網站,先适配手機可以(yǐ)識别的(de)最佳比例,避免識别不(bù)出(chū)來(lái)的(de)情況。推薦網站:草料二維碼 https://cli.im/ 我們可以(yǐ)在(zài)線調試二維碼的(de)像素,和(hé / huò) logo的(de)大(dà)小,直到(dào)調整出(chū),能夠符合設計的(de)最佳大(dà)小。

在(zài)線調整二維碼

微調整 有的(de)時(shí)候,我們需要(yào / yāo)對二維碼大(dà)小進行微調整,我這(zhè)裏建議在(zài)調試階段,建立起常量控制,并調整寫好調整方法公式。這(zhè)樣做的(de)好處是(shì),每當我們作出(chū)微調整的(de)時(shí)候,不(bù)會影響因爲(wéi / wèi)當前調整而(ér)再計算,如下。

const codeImageWidth = 150   /* 繪制二維碼寬度 */
const canvasImageWidth = 85  /* 二維碼繪制到(dào)canvas的(de)寬度 */
const left = x - 15          /* left 值 */
const top = y - 22           /* top 值 */
const LogoWidth = 15         /* 二維碼logo寬度 */

const offset = (canvasImageWidth - LogoWidth) / 2 /* 偏移量 */
複制代碼

4 完事具備,生成海報圖片,轉發好友

我們已經跑完整個(gè)流程。就(jiù)剩下最後一步,生成海報圖片,轉發圖片了(le/liǎo)。生成海報可以(yǐ)用微信小程序canvas中的(de)canvasToTempFilePath生成圖片路徑,然後通過previewImage方法浏覽圖片,浏覽圖片時(shí)候就(jiù)可以(yǐ)喚醒微信小程序的(de)分享好友功能了(le/liǎo)。這(zhè)裏有一點我們應該注意,就(jiù)是(shì)要(yào / yāo)截取canvas的(de)有效高度。

上(shàng)代碼:

/* 生成海報 */
makePc(node) {
    const {
        startTop,    /* 截取canvas畫布的(de)頂部 */
        endTop,      /* 截取canvas畫布的(de)底部 */
        windowWidth  /* 屏幕寬度 */
    } = this
    const _this = this
    Taro.canvasToTempFilePath({
        x: 0,
        y: startTop,
        width: windowWidth,
        height: endTop - startTop,
        destWidth: windowWidth * 3,
        destHeight: (endTop - startTop) * 3,
        canvas: node,
        success: function (res) {
            Taro.hideLoading()
            Taro.previewImage({
                urls: [res.tempFilePath]
            })

        }
    })
}
複制代碼

canvasToTempFilePath 注意事項

還是(shì)回到(dào)最初的(de)那個(gè)問題,在(zài)調用 canvasToTempFilePath 方法的(de)時(shí)候,新老 api 傳遞的(de)參數不(bù)同。

在(zài)老版本API中 ,通過createCanvasContext 方式繪制的(de)canvascanvasToTempFilePath 的(de)配置屬性canvas, 微信開發者文檔是(shì)這(zhè)麽解釋的(de) canvas 畫布标識,傳入 canvas 組件實例 (canvas type="2d" 時(shí)使用該屬性), 也(yě)就(jiù)是(shì)canvas上(shàng)下文context

但是(shì)我們用的(de)是(shì)新版本 ,通過 getContext 方式繪制的(de)canvas ,當我們傳入的(de)是(shì)context,竟然沒有效果,what? 還有這(zhè)種事,難道(dào)是(shì)微信開發者文檔出(chū)現了(le/liǎo)問題嗎?後來(lái)發現在(zài)這(zhè)種方式下,傳入的(de)是(shì)通過 query.select獲取的(de)canvas 的(de)node節點,真是(shì)坑不(bù)少啊~~~。一口老血都要(yào / yāo)噴出(chū)來(lái)了(le/liǎo)

五 總結

在(zài)做這(zhè)個(gè)功能的(de)時(shí)候,真是(shì)遇到(dào)了(le/liǎo)很多坑,甚至于(yú)有一種欲哭無淚的(de)感覺,不(bù)過踩着坑一路走來(lái),确實也(yě)收獲蠻多。

相關案例查看更多