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>
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>