丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
技术选型背景:taro3.0-vue
先来十一个问题压压惊,相信你做绘制海报过程中,一定会遇到
①taro-vuecreateCanvasContext获取canvas实例无效问题,绘制不出来效果?
②taro-vue初始化获取不到canvas上下文怎么办,完全绘制不出来图片?
③关于canvas宽高以及缩放比问题,绘制的元素变形,画布的高度真得等于cavans标签设置的宽高么?
④canvas怎么绘制叠在一起的两张图片,并控制层级?
⑤如何用canvas绘制,多行文本?
⑥如何根据设计稿,精确还原海报各个元素位置问题。
⑦canvas怎么绘制base64的图片
⑧如何绘制网络的图片,两种canvas画布api,绘制图片有什么区别完成
⑨如何正确选型生成二维码工具?
⑩生成的二维码,识别不出来怎么办,
如何绘制二维码上的logo
我们即将解决的问题
②taro-vue初始化获取不到canvas上下文怎么办?
wxml
js中这么写
onReady(){/*使用wx.createContext获取绘图上下文context,firstCanvas与canvas属性中的canvas-id一一对应*/constcontext=wx.createCanvasContext('firstCanvas')/*设置字体大小*/context.setFontSize(20)/*设置字体颜色*/context.setFillStyle('pink')/*设置文本内容,位置*/context.fillText('hello,world',0,0)context.draw()}老版本是使用createCanvasContext传入canvas标签中的canvas-id属性,来获取canvas实例,老版本的使用起来说实话,不够灵活,很多对canvas线条,颜色的设置,都封装成方法了,每次改变需要调用方法。
新的方式,则是先通过createSelectorQuery获取canvas元素节点,然后通过getContext获取上下文。
constquery=wx.createSelectorQuery()query.select('#myCanvas').fields({node:true,size:true}).exec((res)=>{const{node}=res[0]if(!node)return/*获取canvas实例*/constcontext=node.getContext('2d')context.fillStyle='pink'/*设置字体样式大小字体类别*/context.font='normal40012pxPingFangSC-Regular',context.fillText('hello,world',0,0)})这种方式和第一种createSelectorQuery方式,在api使用方式上会有微妙的差别,这种写法更像原生的DOM写法,设置颜色,样式,直接改变context属性,而不再需要调用对应的api。
解决问题:①taro-vuecreateCanvasContext获取canvas实例无效问题,绘制不出来效果?
因为我们小程序技术选择是taro-vue2,所以我这里重点将一下,在taro-vue中,目前使用createCanvasContext方式获取canvas实例,绘制画布从来没有成功过,即便是createCanvasContext能够创建上下文,但是任何东西也画不出来(传this之类的方案试了一个遍)。要是问我为什么?实际我也不知道,只有凹凸实验室的同学应该更清楚,GitHub上也有issue,希望taro团队能够重视起来。
解决方案就是采用最新的api,就是上述讲的第二个方案。代码如下:
importTarofrom'@tarojs/taro'constquery=Taro.createSelectorQuery()query.select('#myCanvas').fields({node:true,size:true}).exec(res=>{//TODO:....})②taro-vue初始化获取不到canvas上下文怎么办?
在使用taro-vue的过程中,会面临一个问题,就是小程序node节点获取不到的问题,这个有可能是小程序本身的生命周期,和vue生命周期混乱造成的。尤其当我们选择的是组件而不是页面的情况。对于这样的情况,官方文档给出了答案。页面首次渲染完毕时执行,此生命周期在小程序端对应小程序页面的onReady生命周期。从此生命周期开始可以使用createCanvasContext或createselectorquery等API访问真实DOM。
也就是说如果想要获取真是dom节点,我们可以这么做,
组件中
mounted(){eventCenter.once(getCurrentInstance().router.onReady,()=>{constquery=Taro.createSelectorQuery()query.select('#myCanvas').fields({node:true,size:true}).exec(res=>{//TODO:....})})}尴尬的是,这种情况下,有的时候会造成eventCenter.once()回调函数不执行的情况,比如说当前组件的是收到v-if控制的情况。那么怎么样解决呢,对于这种情况,我教大家一种解决方案。
我们可以用taro中,通过Taro.nextTick方法,将获取元素的任务放在下一次nextTick执行。
mounted(){Taro.nextTick(()=>{//获取元素})}2初始化canvas设置宽高百分比我们即将解决的问题:
我们期望将整个屏幕作为画布,对于不同手机,屏幕尺寸都会有差别,所以要动态获取设备的宽高。这里有一个问题是容器宽高等于画布宽高吗,答案是否定的,为什么这么说呢,原因如下小程序的canvas画布有一个原始的画布宽高,以及缩放比,而且是按照一倍像素来的,当我们给canvas容器设定容器宽高之后,如果没有对应设置canvas画布的画布宽高以及scale,画出的画布就会严重的变形,我们用一个例子来解释。
比如我们想再画布上半部分区域,画一个图片,当我们期望正常比例画canvas,如果我们只给cavans标签加宽高,而不给画布设置宽高的时候。会按照原始画布的宽高比去绘制。
期望结果,画布充满屏幕,图片按照正常比列展示。当我们不给cavnas画布设置画布宽高以及缩放比的时候。会发生下面的情况。
实际效果:
importTaro,{eventCenter,getCurrentInstance}from'@tarojs/taro'exportdefault{name:'myPoster',data(){const{windowHeight,windowWidth,pixelRatio}=Taro.getSystemInfoSync()/*动态获取设备的宽和高*/return{canvasStyle:{/*cavnas的宽高*/width:windowWidth+'px',height:windowHeight+'px',},windowWidth,pixelRatio,/*屏幕缩放比*/windowHeight,scale:1}},mounted(){Taro.nextTick(()=>{constquery=Taro.createSelectorQuery()query.select('#myPoster').fields({node:true,size:true}).exec(res=>{let{node,}=res[0]if(!node)return/*第一步:canvas画布的宽高和元素的宽高必须保持相同的长宽比列,否则会变形*/constdpr=this.pixelRatioconstcontext=node.getContext('2d')node.width=windowWidth*dprnode.height=windowHeight*dprcontext.scale(dpr,dpr)context.fillStyle='#fff'context.fillRect(0,0,windowWidth,windowHeight)})})}}当我们设置好画布宽高,以及缩放比之后,就能按照正常比列进行绘制了。让我们一起看看设置完缩放比之后的图片效果,变成了我们想要的效果。
接下来就是绘制阶段。
在讲解canvas如何生成海报,完美还原设计稿的问题之前,我们应该想一个问题,因为canvas画布,毕竟不是dom模型,可以使用div或者view,通过自定义设置样式来进行布局。cavnas需要我们画出元素的布局效果,这里就要精确获取画布上每一个元素相对与画布的x,y值。那么首先想到的是如何获取每一个元素精确的x,y值。
解决问题:⑥如何根据设计稿,精确还原海报各个元素位置问题。针对完美还原设计稿的问题,比较靠谱的方案就是,先1:1正常挂在dom元素,然后通过获取元素的位置,来绘制canvas画布的元素位置。我们用一幅图来表示其原理。
这里打一个比方,我们在dom元素中可能存在这样的结构。
还有一个问题,就是尽量不要给需要绘制的元素,增加paddingmarign等属性,如果是绘制纯文本,不要设置lineHeight,如图下示例:
我们期望在获取a点的位置信息,但是最终却获取b点的位置信息。如果用b点位置来绘制canvas,势必不能完美还原设计稿,所以我们在用这种方式绘制canvas的时候,应该注意这些细节问题。
我们需要绘制海报上的每一个点位,首先想到的就是获取小程序元素位置方法,并封装该方法。我们用promise来防止深层次的回调,并且方便使用asyncawait语法糖。废话不多说,一言不合上代码。
/*获取元素位置*/geDomPostion(dom,isAll){returnnewPromise((resolve)=>{Taro.createSelectorQuery().select(dom).boundingClientRect(rect=>{const{top,left}=rect/*isAll是否获取设备宽高等信息*/resolve(isAllrect:{top,left})}).exec()})},小提示:如果用wx原生,或者其他跨端框架mpvuewepyuniapp是的同学,把Taro换成wx即可。
接下来我们要解决的问题:⑨如何绘制网络的图片,两种通过canvas画布api,绘制图片有什么区别
我们在用canvas绘制图片的时候,对于本地图片可以直接通过canvas提供的drawImage进行绘制,但是对于网络图片是不能这么绘制的,我们首先需要通过getImageInfo来获取图片的临时路径。用getImageInfo绘制网络资源的时候请注意配置一下合法的下载域名,要不然我们是无法成功获取图片信息的。我们首先需要在小程序后台配置downloadFile合法域名。
具体步骤如下:第一步:
第二步:
第三步:
接下来我们要做的就是读取图片的临时路径,绘制到canvas画布上来。
/*backGroundImageUrl是我们要画的网络图片的地址*/this.getImageInfo(this.backGroundImageUrl).then(res=>{const{width,/*宽度*/height,path/*临时路径*/}=res1/*第二步:绘制banner图*/constbannerImage=awaitthis.geDomPostion('#bannerImage')this.startTop=bannerImage.top-30this.drawImage(context,node,path,0,0,width,height,0,this.startTop,windowWidth,windowWidth)context.save()})this.drawImage是我们封装好的方法,之前说过对于小程序获取context两种接口方式,两种方式绘制canvas图片,有一些差别,我们马上道来。
老版本绘制方法
老版本apicreateCanvasContext可以直接使用drawImage绘制图片。如下
/*绘制图片*/context.drawImage(url,x,y,width,height,dx,dy,dwidth,dheight)当时我们项目用的是第二种新apigetContext当时获取上下文,所以在图片绘制方式上,会有所改变。
新版本绘制方法
constimage=node.createImage()image.src=urlimage.onload=()=>{context.drawImage(image,x,y,width,height,dx,dy,dwidth,dheight)}用新版本的API绘制图片的同学请注意,这个onload回调是在图片加载完成时候执行的,所以说明是异步的。还有一个注意的地方,相比老版本的drawImage第一个参数是图片的路径,而新版本的drawImage第一个参数是image元素。
封装绘制图片方法
刚才在绘制网络图片最后一步,我们调用了this.drawImage方法。因为整个海报生成过程中,内部会画入多张图片,所以我们单独封装了一个绘制图片的方法。
/*绘制图片*/drawImage(context,node,url,...arg){returnnewPromise((resolve)=>{constimage=node.createImage()image.src=urlimage.onload=()=>{context.drawImage(image,...arg)resolve()}})},这样我们就可以通过,async,await判断图片是否加载完成。
我这里简单给大家介绍一下context.drawImage用法,
CanvasContext.drawImage(imageResource/dom,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight)绘制图像到画布,第一个参数,在老api中代表路径,在新版本api中代表imagDom元素,
sx需要绘制到画布中的,imageResource/dom的矩形(裁剪)选择框的左上角x坐标
sy需要绘制到画布中的,imageResource/dom的矩形(裁剪)选择框的左上角y坐标
sWidth需要绘制到画布中的,imageResource/dom的矩形(裁剪)选择框的宽度
sHeight需要绘制到画布中的,imageResource/dom的矩形(裁剪)选择框的高度
dximageResource的左上角在目标canvas上x轴的位置
dyimageResource的左上角在目标canvas上y轴的位置
dWidth在目标画布上绘制imageResource的宽度,允许对绘制的imageResource进行缩放
dHeight在目标画布上绘制imageResource的高度,允许对绘制的imageResource进行缩放
我们用一幅图表示各个属性的对应什么。
解决问题:④canvas怎么绘制叠在一起的两张图片,并控制层级?
如果我们绘制叠在一起的两张图片,需要我们做一些什么样的工作呢?首先想到的是层级问题,我们期望背景图片放在下面,例如头像之类的图片放在上面,但是在画布中没有控制zIndex层级的属性,那么怎么样处理这个问题呢?答案是实际在canvas中,绘制的先后顺序就是画布层级顺序,后画的在先画的上层,那么对于这种层级问题呢,我们只要保证层级高的元素后画,层级低的元素先画就可以完美解决,接下来我们在海报中,画上头像,文字等信息。
效果:
我们完美解决了片文本的层级问题,接下来,我们就要绘制海报的主要的内容了。在我们绘制海报的时候,可能会遇到多行文本的情况,那么多对多行文本,我们是怎么解决的呢?
解决问题:⑤如何用canvas绘制,多行文本?
canvas画的文本,并不能像我们的dom元素下的文本一样,可以自动换行,我们如何还原,多行文本的效果呢。这这里教大家一种方法,我们可以一个一个字的绘制到canvas中,然后把每个字的宽度相加,如果总宽度大于容器的宽度,那么就另外起一行,增加每一行的高度,从头开始画。,我们直接上代码。
/**画多行文本*@paramctxcanvas上下文*@paramstr多行文本*@paraminitHeight容器初始top值*@paraminitWidth容器初始left值*@paramcanvasWidth容器宽度*/drawRanksTexts(ctx,str,initHeight,initWidth,canvasWidth){letlineWidth=0;letlastSubStrIndex=0;/*设置文字样式*/ctx.fillStyle="#303133"ctx.font='normal40015pxPingFangSC-Regular'for(leti=0;i
/*TODO:复制多行文本*/constrowsText=awaitthis.geDomPostion('#context',true)this.drawRanksTexts(context,this.skuName,rowsText.top,rowsText.left,rowsText.width)效果
接下来我们做的是绘制二维码,绘制二维码过程,笔者踩了不少的坑,尤其taro-vue不支持createCanvasContext方式,希望我能用自己踩的坑,让大家避开相同的错误,避免大家少走很多弯路。绘制二维码实际并没有想象的复杂,实际就是将链接转换成二维码,让手机扫码或者长按可以识别即可,虽然原理很简单,但是还是有很多注意的细节。
绘制二维码无异于二种方式,第一种方式就是用canvas画出来。第二种将链接转成base64的链接,然后让图片展示链接。接下来我们针对这两种方式,进行二维码库的技术选型。
解决问题⑨如何正确选型生成二维码工具?
使用
这种方式下,最后确实成功了,因为在做demo的时候,我用的是github短链接。但是一回归笔者公司的项目,很长的链接,奈何生成的二维码特别密集,手机根本识别不出来,无奈前功尽弃了,只能换其他的技术方案,所以笔者选择了第二种比较稳的方式,形成base64文件。
qrcode-base64是将二维码的链接,转成base64的链接,并把这个链接作为src属性赋值给图片。我们先介绍一下基本用法。
下载
npminstallqrcode-base64使用
importQRfrom'qrcode-base64'varimgData=QR.drawImg(this.data.codeText,{typeNumber:4,errorCorrectLevel:'M',size:500})//返回输出base64编码imgData如上述代码块所示,imgData就是生成的base64链接,我们可以直接把它作为图片的src,然后让canvas将图片绘制到我们的海报中去,但是又来了一个问题,canvas是不支持绘制base64的链接图片的,真机上没有任何效果,真实一步十个坑啊,我们还得想办法解决这个问题。
解决问题⑦canvas怎么绘制base64的图片
对于上面说到的canvas不支持base64的图片,那么我们还要把二维码绘制到海报中,那么并不是没有办法,我们可以用小程序提供的文件系统来解决问题。
wx.getFileSystemManager获取全局唯一的文件管理器,返回值类似于node中的fs.
writeFile写入文件,可以将图片写入系统中。
constfs=wx.getFileSystemManager()fs.writeFile(/*写入文件*/)封装方法封装绘制二维码方法
我们在wxml上写一个元素,作为占位,方便我们可以获取二维码的位置。
扫描成功
解决问题:⑩生成的二维码,识别不出来怎么办。
在线调整二维码
微调整有的时候,我们需要对二维码大小进行微调整,我这里建议在调试阶段,建立起常量控制,并调整写好调整方法或公式。这样做的好处是,每当我们作出微调整的时候,不会影响因为当前调整而再计算,如下。
上代码:
/*生成海报*/makePc(node){const{startTop,/*截取canvas画布的顶部*/endTop,/*截取canvas画布的底部*/windowWidth/*屏幕宽度*/}=thisconst_this=thisTaro.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注意事项还是回到最初的那个问题,在调用canvasToTempFilePath方法的时候,新老api传递的参数不同。
在做这个功能的时候,真是遇到了很多坑,甚至于有一种欲哭无泪的感觉,不过踩着坑一路走来,确实也收获蛮多。
我把整个生成海报的全流程的demo项目,上传到github,感兴趣的同学,可以看看,尤其是正在做这个功能的同学们,加油~
新的一年,祝大家新的一年心想事成,万事如意,祝福掘金社区越来越好。