Canvas 实现一张分享海报?

前言

Canvas 实现分享海报,这个需求现在已经非常多见了。因为它方便用户分享是一个很好的产品推广方式。

我也写过了好几次这个需求了,不同需求会遇到一些不同的问题。所以我写了一个 Demo 来总结一下我遇到的问题,希望别的小伙伴们可以避坑,同时如果小伙伴遇到我这里没提到的问题也欢迎小伙伴们给我说一下,互相学习😄~~~

开发工具:  uni-app 

 

Part.1  效果展示

 

Part.2  可能遇到的问题

1.  Canvas 内容过多,文字换行

2.  Canvas 内容不确认,从而 Canvas 高度不确定

3.  Canvas 绘入网络图片

4.  产出的 Canvas  图片模糊不清

5.  绘制一个圆形图像 (这个在另外一篇文章做了详述~)https://www.cnblogs.com/langxiyu/p/13226941.html

 

Part.3  解决思路

问题一: 计算每个文字的宽度与可绘入的宽度作比较,大于可绘入宽度则换行绘入

问题二: 先给 Canvas 一个足够高的高度,例如:5000px 。通过计算已经绘入画布元素高度的加总重新给 Canvas 赋值高度

问题三: 绘入网络图片时,需先下载图片进而拿到该图片的临时地址后再绘入  Canvas ,否则可能会导致 Canvas 绘入空白

问题四: Canvas  产出的图片很模糊,只需将输出值扩大原来的两倍即可 ,如:200px 输出就为 400px

 

Part.4  代码展示

HTML

 1 <template>
 2     <view class="layout">
 3          <view class="layout-header">
 4              <canvas v-if="!status"
 5                      canvas-id='myCanvas' 
 6                      class="header-canvas"
 7                      :style="{top: '-' + canvasHeight + 'px', height: canvasHeight + 'px'}"></canvas>
 8             <uniTransition :duration="500" :mode-class="['slide-bottom']" :show="status">
 9                 <image class="header-img"
10                        :class="status? 'active':''"
11                        :src="src"
12                        :style="{top: '-' + canvasHeight + 'px', height: canvasHeight + 'px'}"
13                        @load="placardLoad"  
14                        mode=""></image>
15             </uniTransition>         
16          </view>
17          
18          <view class="main-occupancy"></view>
19          
20          <view class="footer-ope-box">
21              <view class="ope-box">
22                  <button class='btn-seller btn bg-green' hover-class="btn-hover2" @click="saveAlbum">保存到相册</button>
23              </view>
24          </view>
25     </view>
26 </template>

 

JS

  1 <script>
  2     import uniTransition from '@/components/uni-transition/uni-transition.vue'
  3     export default {
  4         components: {
  5             uniTransition
  6         },
  7         data() {
  8             return {
  9                 src: '',  // 生成图片地址
 10                 status: false, // 展示状态
 11                 
 12                 canvasHeight: 5000, // canvas 默认高度
 13                 
 14                 // 绘入 Canvas 信息
 15                 obj: {
 16                     avatar: 'https://pic.liesio.com/2020/06/23/9246b58199bbe.png',
 17                     nickname: '孙悟空',
 18                     oldName: '卡卡罗特',
 19                     race: '被遗弃的赛亚人',
 20                     magnumOpus : '《龙珠》、《龙珠Z》、《龙珠GT》、《龙珠改》、《龙珠超》等作品中的男主角',
 21                     summary: '来自贝吉塔行星的赛亚人,幼时以“下级战士”之身份被送往地球,并被武道家孙悟饭收养,因失控变为巨猿将孙悟饭踩死后独自生活在深山,后因结识布尔玛从而踏上寻找龙珠之旅。梦想是不断变强,为追求力量而刻苦修行。基于该角色之影响力,自2015年起日本纪念日协会正式认定每年5月9日为“悟空纪念日”'
 22                 }
 23             }
 24         },
 25         onLoad() {
 26             uni.showLoading({
 27                 title: '我正在努力...',
 28                 mask: true
 29             });
 30             
 31             // 开始绘制海报
 32             this.createPlacard()
 33         },
 34         methods: {
 35             // 开始绘制海报
 36             createPlacard() {
 37                 uni.getImageInfo({
 38                     src: this.obj.avatar, // 网络图片需先下载,得到临时本地路径,否则绘入 Canvas 可能会出现空白 
 39                     success: (img)=> {
 40 
 41                         const ctx = wx.createCanvasContext('myCanvas', this);
 42                         ctx.fillStyle = "#FFFFFF";
 43                         ctx.fillRect(0, 0, uni.upx2px(750), this.canvasHeight);
 44                         
 45                         // 背景图片
 46                         ctx.drawImage('/static/create-placard-bg.jpg', uni.upx2px(40), uni.upx2px(40), uni.upx2px(670), uni.upx2px(500));
 47                         
 48                         // 悟空艳照
 49                         ctx.drawImage(img.path, uni.upx2px(200), uni.upx2px(95), uni.upx2px(350), uni.upx2px(350));
 50                         
 51                         // 设置绘入文本基线
 52                         ctx.textBaseline = "top";
 53                         
 54                         // 姓名
 55                         ctx.font = `${uni.upx2px(40)}px bold`;
 56                         ctx.setFillStyle('#333333');
 57                         ctx.fillText('姓名:', uni.upx2px(40), uni.upx2px(570));
 58                         ctx.setFillStyle('#1468FF');
 59                         ctx.fillText(this.obj.nickname || '', uni.upx2px(150), uni.upx2px(570));
 60                         
 61                         // 原名
 62                         ctx.setFillStyle('#333333');
 63                         ctx.fillText('原名:', uni.upx2px(40), uni.upx2px(630));
 64                         ctx.setFillStyle('#1468FF');
 65                         ctx.fillText(this.obj.oldName || '', uni.upx2px(150), uni.upx2px(630));
 66                         
 67                         // 种族
 68                         ctx.setFillStyle('#333333');
 69                         ctx.fillText('种族:', uni.upx2px(40), uni.upx2px(690));
 70                         ctx.setFillStyle('#1468FF');
 71                         ctx.fillText(this.obj.race || '', uni.upx2px(150), uni.upx2px(690));
 72                         
 73                         // 作品
 74                         ctx.setFillStyle('#333333');
 75                         ctx.fillText('作品:', uni.upx2px(40), uni.upx2px(750));
 76                         ctx.setFillStyle('#1468FF');
 77                         if (this.obj.magnumOpus.length > 15) {
 78                             this.obj.magnumOpus = this.obj.magnumOpus.substring(0, 13) + '...'
 79                         };
 80                         ctx.fillText(this.obj.magnumOpus || '', uni.upx2px(150), uni.upx2px(750));
 81                         
 82                         // 简介
 83                         ctx.setFillStyle('#333333');
 84                         ctx.fillText('简介:', uni.upx2px(40), uni.upx2px(810));
 85                         // 绘入简介
 86                         ctx.setFillStyle('#1468FF');
 87                         this.setSummary(ctx, this.obj.summary, uni.upx2px(520));
 88                             
 89                         ctx.draw(true, ()=> {
 90                             uni.canvasToTempFilePath({
 91                               x: 0,
 92                               y: 0,
 93                               width: uni.upx2px(750),
 94                               height: this.canvasHeight,
 95                               destWidth: uni.upx2px(1500),
 96                               destHeight: this.canvasHeight * 2,
 97                               canvasId: 'myCanvas',
 98                               success: (res)=> {
 99                                  this.src = res.tempFilePath
100                               },fail(err) {
101                                 console.log(err)
102                               } 
103                             },this)
104                         },200); 
105                     }
106                 })    
107             },
108             
109             // 绘入简介
110             setSummary(ctx, text, w) {
111                 /*
112                 * ctx: Canvas 实例
113                 * text: 简介
114                 *  w :  文字可输入的最大宽度
115                 * */
116 
117                 let textArr = text.split(''); // 将简介内容每个字符进行切割
118                 let textArrLen = textArr.length; // 切割字符数组长度
119                 let str = ''; // 每一行的字符
120                 let row = []; // 得到全部行
121                 let lastRowY = null; // 最后一行 Y 坐标
122                 
123                 for (let i = 0; i < textArrLen; i++) {
124                     // if 每一个字符的宽度 < 当前可绘入宽度  && 当前字符 + 已经累积字符的宽度 <= 当前可绘入宽度
125                     // 将字符进行累加
126                     // else 将字符 push 进每一行的数组中,并且 str 要从每一行宽度溢出的字符开始累计
127                     if (ctx.measureText(textArr[i]).width < w && ctx.measureText(textArr[i] + str).width <= w) {
128                         str += textArr[i]
129                     } else {
130                         row.push(str);
131                         str = textArr[i];
132                     }
133                 };
134                 
135                 // 最后一行
136                 row.push(str); 
137                 
138                 // 根据宽度会分好每一行需要 push 的字符
139                 // 循环绘入 Canvas 中
140                 // lastRowY :每一行的 Y 坐标, 自定义为 60
141                 for (let j = 0, rowLen = row.length; j < rowLen; j++) {
142                     lastRowY = uni.upx2px(810) + j * uni.upx2px(60);
143                     ctx.fillText(row[j], uni.upx2px(150), lastRowY)
144                 };
145                 
146                 // 得到最后一行的 Y 坐标,由于设置了文本基线为 ‘top’ (ctx.textBaseline = "top")
147                 // 所以 Canvas 高度 = 最后一行的 Y 坐标 + 一行的行距 60 + 背景图片距离顶部距离 40 (为了头部距离和底部距离相等)
148                 this.canvasHeight = lastRowY + uni.upx2px(60) + uni.upx2px(40);
149             },
150             
151             // 图片加载完成
152             placardLoad() {
153                 this.status = true;
154                 uni.hideLoading();
155             },
156             
157             // 保存到相册
158             saveAlbum() {
159                 let that = this;
160                 
161                 uni.authorize({
162                     scope: 'scope.writePhotosAlbum',
163                     success() {
164                        uni.saveImageToPhotosAlbum({
165                           filePath: that.src,
166                           success: ()=> {
167                               uni.showModal({
168                                  title: '保存成功',
169                                  content: '已保存到相册,快去看看吧',
170                                  showCancel: false
171                               })
172                           }
173                        })
174                     },fail() {
175                         uni.showModal({
176                           title: '提示',
177                           content: '您点击了拒绝授权,将无法正常保存图片,点击确定可重新获取授权',
178                           success:(res)=> {
179                             if (res.confirm) {
180                               uni.openSetting({
181                                 success: (res) => {
182                                   if (res.authSetting["scope.writePhotosAlbum"]) {////如果用户重新同意了授权登录
183                                     uni.saveImageToPhotosAlbum({
184                                         filePath: that.src,
185                                         success:()=> {
186                                             uni.showModal({
187                                                 title: '保存成功',
188                                                 content: '已保存到相册,快去看看吧',
189                                                 showCancel: false
190                                             })
191                                         }
192                                     })
193                                   }
194                                 }
195                               })
196                             }
197                           }
198                         })                
199                      }
200                 })    
201             }
202         }
203     }
204 </script>
View Code

 

 

CSS

 1 <style lang="scss" scoped>
 2     .layout {    
 3         .layout-header {
 4             width: 750upx;
 5             background:rgba(255,255,255,1);
 6             .header-canvas, .header-img {
 7                 width: 750upx;
 8                 position: fixed;
 9                 box-shadow:0 5upx 16upx 0 rgba(20,104,255,0.07);
10                 &.active {
11                     position: relative;
12                     top: 0upx !important;
13                 }
14             }
15         }
16         
17         .main-occupancy {
18             height: 170upx;
19         }
20         
21         .footer-ope-box {
22             position: fixed;
23             bottom: 0;
24             left: 0;
25             right: 0;
26             padding-bottom: env(safe-area-inset-bottom);
27             background-color: #FFFFFF;
28             box-shadow:0 5upx 16upx 0 rgba(20,104,255,0.07);
29             z-index: 999;
30             .ope-box {
31                 padding: 10upx 20upx;
32                 .btn-seller {
33                     width: 100%;
34                     height: 90upx;
35                     line-height: 90upx;
36                     color: #FFFFFF; 
37                     background-color: #1468FF;
38                     border-radius:10upx;
39                 }
40             }
41         }
42     
43     }
44 </style>

 

posted @ 2020-04-13 19:05  鲁肉饭  阅读(489)  评论(0编辑  收藏  举报
UP