uni-app之canvas實現(xiàn)海報分享

用uni-app實現(xiàn)微信小程序海報分享功能

微信小程序基礎庫2.9.0以后canvas新api實現(xiàn),本文實現(xiàn)是用2.9.0新api旷祸,簡單講一下2.9.0以下用法:
效果圖:


IMG_4789.PNG
  1. 2.9.0以下版本創(chuàng)建canvas
xml:
<view>
   <canvas style="width: 100px; height: 100px" canvas-id="test"></canvas>
 </view> 

js:
// 在組件中使用canvas这难,如果是在組件中使用canvas需要在創(chuàng)建圖形上下文是加this枫甲,在頁面中使用可不加
var cxt = uni.createCanvasContext('test', this);
cxt.setFillStyle('#fd0000');
cxt.fillRect(0, 0 , 50, 50);
cxt.draw();
  1. 2.9.0以上版本創(chuàng)建canvas装黑,主要代碼片段如下:

xml

<template>
  <view class="page-share-poster" @touchmove.stop="">
    <view class="view-bg" :class="[bgColor]" @click="onClickBg">
      <view class="view-canvas" @click.stop="">
        <!-- style是設置canvas樣式的大小 -->
        <canvas style="width: 100%; height: 100%;" type="2d" id="share-poster"></canvas>
      </view>
    </view>
  </view>
</template>

js

獲取canvas實例

// canvas實例
getCtx() {
  return new Promise((resolve, reject) => {
    const query = uni.createSelectorQuery().in(this);
    query.select('#share-poster')
     .fields({ node: true, size: true })
     .exec((res) => {
        const canvas = res[0].node
        const ctx = canvas.getContext('2d')
              
        // 設置canvas大小
        const dpr = uni.getSystemInfoSync().pixelRatio
        canvas.width = res[0].width * dpr
        canvas.height = res[0].height * dpr
        ctx.scale(dpr, dpr)
              
        resolve(canvas);
     })
  });
},

canvas生成圖片

// 生成圖片
canvasToTempFilePath(canvas) {
  uni.canvasToTempFilePath({
    canvas: canvas,
    success: (res) => {
      this.shareImg = res.tempFilePath;
      uni.hideLoading();
    },
    fail: (res) => {
      uni.hideLoading();
      this.$emit('onHideSharePoster');
      uni.showToast({
        title: '海報生成失敗',
      })
    }
  })
},

canvas加載網(wǎng)絡圖片之前恢恼,
加載網(wǎng)絡圖片需要先調用uni.getImageInfo把圖片下載下來民傻,生成一個臨時地址,需在微信管理平臺設置圖片域名白名單,圖片必須是https的漓踢。

// 返回的是微信存儲的臨時圖片路徑
 getImageInfo(src) {
  return new Promise((resolve, reject) => {
     uni.getImageInfo({
        src: src,
        success: (res) => {
          resolve(res.path);
        },
        fail: (err) => {
          uni.hideLoading();
            reject(err);
        }
     })
  })
},

canvas加載網(wǎng)絡圖片

 // canvas加載網(wǎng)絡圖片
onLoadImage(canvas, src) {
  return new Promise((resolve, reject) => {
    let img = canvas.createImage();
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      uni.hideLoading();
        reject('');
    }
    img.src = src;
  });
},
  1. 基礎庫版本判斷
// 獲取當前基礎庫版本號
const { SDKVersion } = uni.getSystemInfoSync();
// 版本號比較
isFastShare = compareVersion(SDKVersion, '2.9.0') < 0 ? true : false

export function compareVersion(v1, v2) {
  v1 = v1.split('.')
  v2 = v2.split('.')
  const len = Math.max(v1.length, v2.length)

  while (v1.length < len) {
    v1.push('0')
  }
  while (v2.length < len) {
    v2.push('0')
  }

  for (let i = 0; i < len; i++) {
    const num1 = parseInt(v1[I])
    const num2 = parseInt(v2[I])

    if (num1 > num2) {
      return 1
    } else if (num1 < num2) {
      return -1
    }
  }

  return 0
}
  1. 詳細代碼如下:
<template>
  <view class="page-share-poster" @touchmove.stop="">
    <view class="view-bg" :class="[bgColor]" @click="onClickBg">
        <view class="view-canvas" @click.stop="">
        <!-- style是設置canvas樣式的大小 -->
            <canvas style="width: 100%; height: 100%;" type="2d" id="share-poster"></canvas>
        </view>
    </view>
  </view>
</template>

<script>
    export default {
        props: {
            model: {
                type: Object,
                default() {
                    return {}
                }
            }
        },
    data() {
      return {
        bgImage: '', // canvas背景圖
        bottomImage: '', // canvas底部圖
        hotImage: '', // 已搶小圖標
        tagImage: '', // 驚喜價tag標簽
        goodsImage: '', // 商品圖片
        avatar: '', // 用戶頭像
        qrCode: '', // 二維碼
        shareImg: '', // 分享圖片
        bgColor: '', // 背景色
      }
    },
    mounted() {
       this.fxDetail();
    },
    methods: {
      // 點擊黑色透明背景隱藏頁面
      onClickBg() {
        this.$emit('onHideSharePoster');
      },
      // 保存圖片
      onSavePoster() {
        uni.saveImageToPhotosAlbum({
          filePath:this.shareImg,
          success: (res) => {
            this.$emit('onHideSharePoster');
            uni.showToast({
              title: '圖片保存成功'
            });
          },
          fail: (res) => {
            uni.showToast({
              title: '圖片保存失敗'
            });
          },
        });
      },
      // 獲取網(wǎng)絡圖片
      getImageInfo(src) {
        return new Promise((resolve, reject) => {
          uni.getImageInfo({
            src: src,
            success: (res) => {
              resolve(res.path);
            },
            fail: (err) => {
              uni.hideLoading();
              reject(err);
            }
          })
        })
      },
      // 加載本地圖片
      onLoadImage(canvas, src) {
        return new Promise((resolve, reject) => {
          let img = canvas.createImage();
          img.onload = () => {
            resolve(img);
          };
          img.onerror = () => {
            uni.hideLoading();
            reject('');
          }
          img.src = src;
        });
      },
      // canvas實例
      getCtx() {
        return new Promise((resolve, reject) => {
          const query = uni.createSelectorQuery().in(this);
          query.select('#share-poster')
            .fields({ node: true, size: true })
            .exec((res) => {
              const canvas = res[0].node
              const ctx = canvas.getContext('2d')
              
              // 設置canvas大小
              const dpr = uni.getSystemInfoSync().pixelRatio
              canvas.width = res[0].width * dpr
              canvas.height = res[0].height * dpr
              ctx.scale(dpr, dpr)
              
              resolve(canvas);
            })
        });
      },
      // 生成圖片
      canvasToTempFilePath(canvas) {
        uni.canvasToTempFilePath({
          canvas: canvas,
          success: (res) => {
            this.shareImg = res.tempFilePath;
            uni.hideLoading();
          },
          fail: (res) => {
            uni.hideLoading();
            this.$emit('onHideSharePoster');
            uni.showToast({
              title: '海報生成失敗',
            })
          }
        })
      },

      async fxDetail() {
        uni.showLoading({
          title: '海報生成中...',
        });
        
        const [bgImage, bottomImage, hotImage, tagImage, goodsImage, avatar, qrCode, canvas] = await Promise.all([
          this.getImageInfo('https://img1.iqianggou.com/miniapp/poster_bg.TIeCtE.png').catch(e => e),
          this.getImageInfo('https://img1.iqianggou.com/miniapp/poster_bottom.3C8Awt.png').catch(e => e),
          this.getImageInfo('https://img1.iqianggou.com/miniapp/huo.Jb1aAH.png').catch(e => e),
          this.getImageInfo('https://img1.iqianggou.com/miniapp/qian.8t3Rt5.png').catch(e => e),
          this.getImageInfo(this.model.image).catch(e => e),
          this.getImageInfo(this.userInfo.avatar).catch(e => e),
          this.getImageInfo(this.model.qrimg_url).catch(e => e),
          this.getCtx().catch(e => e),
        ]);
        
        this.bgImage = bgImage;
        this.bottomImage = bottomImage;
        this.hotImage = hotImage;
        this.tagImage = tagImage;
        this.goodsImage = goodsImage;
        this.avatar = avatar;
        this.qrCode = qrCode;
        this.drawFxDetialCanvas(canvas);
      },
      // 開始繪圖
      async drawFxDetialCanvas(canvas) {
        // 獲取canvas的繪圖上下文
        const ctx = canvas.getContext('2d');
        
        const [bgImg, goodsImg, bottomImg, avatar, qrCode, hotImage, tagImage] = await Promise.all([
          this.onLoadImage(canvas, this.bgImage).catch(e => e),
          this.onLoadImage(canvas, this.goodsImage).catch(e => e),
          this.onLoadImage(canvas, this.bottomImage).catch(e => e),
          this.onLoadImage(canvas, this.userInfo.avatar).catch(e => e),
          this.onLoadImage(canvas, this.qrCode).catch(e => e),
          this.onLoadImage(canvas, this.hotImage).catch(e => e),
          this.onLoadImage(canvas, this.tagImage).catch(e => e),
        ]);
        
        // 背景圖
        ctx.drawImage(bgImg, 0, 0, 255, 434);
        
        const cardWidth = 225
        
        // 商品區(qū)域背景色
        ctx.save();
        this.roundRect(ctx, 15, 15, cardWidth, 300, 10, '#FFFFFF', '#FFFFFF');
        ctx.restore();
        
        // 商品圖片
        ctx.save();
        this.roundRect(ctx, 15, 15, cardWidth, cardWidth, 10, '', '', true);
        ctx.drawImage(goodsImg, 15, 15, cardWidth, cardWidth);
        ctx.restore();
        
        // 商品名字
        ctx.textAlign = 'left';
        ctx.font = 'normal bold 11px sans-serif';
        ctx.fillStyle = '#333333';
        this.wordsWrap(ctx, this.model.name, 215, 20, 28 + cardWidth, 16, 2);
        
        // 價格
        // measureText計算文字寬高牵署,要在font之后
        ctx.font = 'normal normal 11px sans-serif';
        ctx.fillStyle = '#FF6D00';
        ctx.fillText('¥', 20, cardWidth + 69, 12);
        ctx.font = 'normal normal 22px sans-serif';
        const priceWidth = Math.min(ctx.measureText(this.model.sale_price).width, 100);
        ctx.fillStyle = '#FF6D00';
        ctx.fillText(this.model.sale_price, 32, cardWidth + 70, priceWidth);
        
        // 驚喜價標簽
        ctx.drawImage(tagImage, priceWidth + 42, cardWidth + 57, 46, 14);
        ctx.font = 'normal normal 9px sans-serif';
        ctx.fillStyle = '#FFFFFF';
        ctx.fillText('驚喜價', priceWidth + 54, cardWidth + 67, 40);
        
        // 正常價
        ctx.font = 'normal normal 8px sans-serif';
        ctx.fillStyle = '#999999';
        ctx.fillText(this.model.market_price, 20, cardWidth + 83, 200);
        // 刪除線
        ctx.beginPath();
        ctx.moveTo(20, cardWidth + 80);
        ctx.lineTo(ctx.measureText(this.model.market_price).width + 20, cardWidth + 80);
        ctx.strokeStyle = '#999999';
        ctx.lineWidth = 1;
        ctx.stroke();
        
        // 已搶x份
        ctx.font = 'normal normal 7px sans-serif';
        const salesSize = ctx.measureText(this.model.sales_volume);
        const maxSaleWidth = Math.min(salesSize.width, 100);
        const salesBgMinX = 235 - (maxSaleWidth + 30);
        // 背景色
        ctx.save();
        this.roundRect(ctx, salesBgMinX, cardWidth + 70, maxSaleWidth + 25, 14, 7, '#FFF6EA', 'transparent');
        ctx.restore();
        // 小圖標
        ctx.drawImage(hotImage, salesBgMinX + 6, cardWidth + 71, 9, 12);
        // 文字
        ctx.fillStyle = '#333333';
        ctx.fillText(this.model.sales_volume, salesBgMinX + 18, cardWidth + 80, maxSaleWidth);
        
        // 底部背景
        ctx.drawImage(bottomImg, 0, 327, 255, 107);
        
        // 微信頭像
        ctx.save();
        ctx.beginPath();
        ctx.arc(47, 353, 20, 0, 2 * Math.PI);
        ctx.strokeStyle = '#FFFFFF';
        ctx.stroke();
        ctx.clip();
        ctx.drawImage(avatar, 27, 333, 40, 40);
        ctx.restore();
        
        // 昵稱
        ctx.font = 'normal normal 9px sans-serif';
        const userNameWidth = Math.min(ctx.measureText(this.userInfo.username).width, 70);
        ctx.fillStyle = '#333333';        
        this.wordsWrap(ctx, this.userInfo.username, 70, 30, 388, 16, 1);
        
        // 描述
        ctx.font = 'normal normal 9px sans-serif';
        ctx.fillStyle = '#666666';
        ctx.fillText('發(fā)現(xiàn)一家好店', 40 + userNameWidth, 388, 60);
        
        // 二維碼
        ctx.save();
        this.roundRect(ctx, 173, 358, 68, 68, 2, 'transparent', '#E1E1E1');
        ctx.restore();
        ctx.drawImage(qrCode, 177, 362, 60, 60);
        
        // 生成圖片
        this.canvasToTempFilePath(canvas);
      },
      // 畫圓角 ctx、x起點喧半、y起點奴迅、w寬度、y高度挺据、r圓角半徑取具、fillColor填充顏色、strokeColor邊框顏色
      roundRect(ctx, x, y, w, h, r, fillColor, strokeColor, isTop) {
        // 開始繪制
        ctx.beginPath()
        // 繪制左上角圓弧 Math.PI = 180度
        // 圓心x起點吴菠、圓心y起點者填、半徑、以3點鐘方向順時針旋轉后確認的起始弧度做葵、以3點鐘方向順時針旋轉后確認的終止弧度
        ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
        // 繪制border-top
        // 移動起點位置 x終點占哟、y終點
        ctx.moveTo(x + r, y)
        // 畫一條線 x終點、y終點
        ctx.lineTo(x + w - r, y)
        // self.data.ctx.lineTo(x + w, y + r)
        // 繪制右上角圓弧
        ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
        // 繪制border-right
        ctx.lineTo(x + w, y + h - r)
        // 繪制右下角圓弧
        const newR = isTop ? 0 : r;
        ctx.arc(x + w - newR, y + h - newR, newR, 0, Math.PI * 0.5)
        // 繪制border-bottom
        ctx.lineTo(x + r, y + h)
        // 繪制左下角圓弧
        ctx.arc(x + newR, y + h - newR, newR, Math.PI * 0.5, Math.PI)
        // 繪制border-left
        ctx.lineTo(x, y + r)
        if (fillColor) {
          // 因為邊緣描邊存在鋸齒酿矢,最好指定使用 transparent 填充
          ctx.fillStyle = fillColor
          // 對繪畫區(qū)域填充
          ctx.fill()
        }
        if (strokeColor) {
          // 因為邊緣描邊存在鋸齒榨乎,最好指定使用 transparent 填充
          ctx.strokeStyle = strokeColor
          // 畫出當前路徑的邊框
          ctx.stroke()
        }
        // 剪切,剪切之后的繪畫繪制剪切區(qū)域內進行瘫筐,需要save與restore
        ctx.clip()
      },
      // 文字換行處理
      // canvas 標題超出換行處理
      wordsWrap(ctx, name, maxWidth, startX, srartY, wordsHight, maxLine) {
        let lineWidth = 0;
        let lastSubStrIndex = 0;
        let arr = [];
        for (let i = 0; i < name.length; i++) {
          lineWidth += ctx.measureText(name[i]).width;
          if (lineWidth > maxWidth) {
            arr.push({text: name.substring(lastSubStrIndex, i), startX, srartY});
            srartY += wordsHight;
            lineWidth = 0;
            lastSubStrIndex = I;
          } else if (i == name.length - 1) {
            arr.push({text: name.substring(lastSubStrIndex, i + 1), startX, srartY});
          }
        }
        let textArr = arr.slice(0, maxLine);
        if (arr.length > maxLine) {
          let text = textArr[maxLine - 1]['text'];
          textArr[maxLine - 1]['text'] = text.substring(0, text.length - 2) + '...';
        }
        for (let item of textArr) {
          ctx.fillText(item.text, item.startX, item.srartY);
        }
      },
    },
    }
</script>

<style lang="scss" scoped>
  .page-share-poster {
    .bg-white {
      background-color: #FFFFFF;
    }
    
    .bg-black {
      background-color: rgba(0, 0, 0, 0.5);
    }
    
    .view-bg {
      position: fixed;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
    }
    
    .view-canvas {
      position: absolute;
      left: 50%;
      bottom: 60%;
      // 這邊寬高不能用rpx蜜暑, 不然會影響canvas的樣式,要跟canvas(px)的單位保持一致
      width: 255px;
      height: 434px;
      margin-left: -127.5px;
      margin-bottom: -217px;
    }
  }
</style>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末策肝,一起剝皮案震驚了整個濱河市肛捍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌之众,老刑警劉巖拙毫,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棺禾,居然都是意外死亡缀蹄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門膘婶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缺前,“玉大人,你說我怎么就攤上這事悬襟⌒坡耄” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵脊岳,是天一觀的道長逝段。 經(jīng)常有香客問我筛璧,道長,這世上最難降的妖魔是什么惹恃? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮棺牧,結果婚禮上巫糙,老公的妹妹穿的比我還像新娘。我一直安慰自己颊乘,他們只是感情好参淹,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乏悄,像睡著了一般浙值。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檩小,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天开呐,我揣著相機與錄音,去河邊找鬼规求。 笑死筐付,一個胖子當著我的面吹牛,可吹牛的內容都是我干的阻肿。 我是一名探鬼主播瓦戚,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丛塌!你這毒婦竟也來了较解?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赴邻,失蹤者是張志新(化名)和其女友劉穎印衔,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乍楚,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡当编,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了徒溪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忿偷。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖臊泌,靈堂內的尸體忽然破棺而出鲤桥,到底是詐尸還是另有隱情,我是刑警寧澤渠概,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布茶凳,位于F島的核電站嫂拴,受9級特大地震影響,放射性物質發(fā)生泄漏贮喧。R本人自食惡果不足惜筒狠,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望箱沦。 院中可真熱鬧辩恼,春花似錦、人聲如沸谓形。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寒跳。三九已至聘萨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間童太,已是汗流浹背米辐。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留康愤,地道東北人儡循。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像征冷,于是被迫代替她去往敵國和親择膝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內容