uni-app之canvas实现海报分享

用uni-app实现微信小程序海报分享功能

微信小程序基础库2.9.0以后canvas新api实现,本文实现是用2.9.0新api,简单讲一下2.9.0以下用法:
效果图:


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

js:
// 在组件中使用canvas,如果是在组件中使用canvas需要在创建图形上下文是加this,在页面中使用可不加
var cxt = uni.createCanvasContext('test', this);
cxt.setFillStyle('#fd0000');
cxt.fillRect(0, 0 , 50, 50);
cxt.draw();
  1. 2.9.0以上版本创建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加载网络图片之前,
加载网络图片需要先调用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加载网络图片

 // canvas加载网络图片
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: '图片保存失败'
            });
          },
        });
      },
      // 获取网络图片
      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
        
        // 商品区域背景色
        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('发现一家好店', 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
          // 对绘画区域填充
          ctx.fill()
        }
        if (strokeColor) {
          // 因为边缘描边存在锯齿,最好指定使用 transparent 填充
          ctx.strokeStyle = strokeColor
          // 画出当前路径的边框
          ctx.stroke()
        }
        // 剪切,剪切之后的绘画绘制剪切区域内进行,需要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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容