10
收藏
评论

canvas画图随记

canvas画图时是否遇到了和我一样的问题?有的话进来看看吧,希望对你有的小小的帮助。

最近画了一张分享图,在此记录一下遇到的问题及解决方法。

  • 画布尺寸自适应

微信小程序尺寸为rpx,会自适应各种机型,但canvas的方法参数默认为px,所以需要对画布上的每一项参数乘以(画布宽度/设备屏幕宽度),将rpx换算成px,达到尺寸自适应的目的,所以将此系数设置为全局变量。代码如下:

var app = getApp();
const device = wx.getSystemInfoSync();
const width = device.windowWidth;//设备屏幕宽度
const xs = width / 375;

调用:

createCard: function() {
    var context = wx.createCanvasContext('myCanvas');
    context.fillText('内容', 100 * xs , 100 * xs)
}
  • 长文本换行

由于fillText只能画一行,但很多情况下是需要将长文本自动换行展示的,这个时候则需要对文本进行处理。
方法:遍历该文本,计算出每一字宽度之和,当该宽度大于文本最大宽度时绘制当前截取部分,并将绘制高度加上行高,宽度置0,重新计算并绘制下一行。当只剩最后一字时,绘制剩余部分。
缺陷:当文本内有换行符时,绘制会换行,但当前计算宽度不会增加,导致格式混乱。所以需要在计算宽度之和前判断该字符是否为换行符,若果是,则绘制当前部分,开始下一行的计算。
完善:如果需要知道绘制文本的总高度,设置初始文本高度为0,在绘制一行时加上行高则可。代码如下:

  /**
   * context:当前画布对象
   * text:文本内容
   * leftWidth:文本左上角x坐标
   * initHeight:文本左上角y坐标
   * canvasWidth:一行文本最大宽度
   */
  drawText: function(context, text, leftWidth, initHeight, canvasWidth) {
    var lineWidth = 0; //文本宽度
    var textHeight = 0; //文本总高度
    var lastSubStrIndex = 0; //每次开始截取的字符串的索引
    for (let i = 0; i < text.length; i++) {
      if (text[i] == "\n") { //如遇换行
        context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth); //绘制截取部分
        initHeight += 17.5 * xs; //17.5为字体高度
        lineWidth = 0;
        lastSubStrIndex = i + 1; //截取字符串时跳过换行符
        textHeight += 17.5 * xs;
      } else {
        lineWidth += context.measureText(text[i]).width; //计算每个字的宽度之和
        if (lineWidth > canvasWidth) {
          context.fillText(text.substring(lastSubStrIndex, i), leftWidth, initHeight, canvasWidth);
          initHeight += 17.5 * xs;
          lineWidth = 0;
          lastSubStrIndex = i;
          textHeight += 17.5 * xs;
        }
      }
      if (i == text.length - 1) { //绘制剩余部分
        context.fillText(text.substring(lastSubStrIndex, i + 1), leftWidth, initHeight, canvasWidth);
        textHeight += 17.5 * xs;
      }
    }
    return textHeight;
  },

调用:

 var text = '新建项目选择小程序项目,选择代码存放的硬盘路径,填入刚刚申请到的小程序的 AppID,给你的项目起一个好听的名字,最后,勾选 "创建 QuickStart 项目" (注意: 你要选择一个空的目录才会有这个选项),点击确定,你就得到了你的第一个小程序了,点击顶部菜单编译就可以在微信开发者工具中预览你的第一个小程序。';
 context.setFontSize(15 * xs)
 that.drawText(context, text, 30 * xs, 100 * xs, 320 * xs)
  • 高度自适应

如碰到画布高度需要根据内容高度不同而不同,或者某元素与可变化高度的元素固定距离的情况,则需要计算出可变化元素高度,再根据该高度进行计算其他高度。例如:

微信图标始终距离文本30px,而该文本高度可变,所以图标的左上角y轴坐标=文本y轴坐标+文本高度+下边距,代码如下:

var textHeight = that.drawText(context, text, 30 * xs 100 * xs, 320 * xs)
 context.drawImage('/images/wx.png', 68 * xs, (100 + 30) * xs + textHeight, 80 * xs, 80 * xs)

注意:因为计算文本高度的方法里已经乘过系数,所以这里不需要乘。宽度自适应同理。

  • 绘制圆角矩形框

由于没有绘制圆角矩形的方法,所以需要将圆角矩形分开绘制。
方法:将四个圆角当成四分之一圆绘制,然后分别画四条边,坐标如下图所示。

代码:

  /**
   * context:当前画布对象
   * x:圆角矩形左上角x坐标
   * y:圆角矩形左上角y坐标
   * w:宽度
   * h:高度
   * r:border-radius
   * color:填充颜色
   */
  roundRect(ctx, x, y, w, h, r, color) {
    ctx.beginPath()
    // 左上角
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
    // 上边框
    ctx.moveTo(x + r, y)
    ctx.lineTo(x + w - r, y)
    ctx.lineTo(x + w, y + r)
    // 右上角
    ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
    // 右边框
    ctx.lineTo(x + w, y + h - r)
    ctx.lineTo(x + w - r, y + h)
    // 右下角
    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5)
    // 下边框
    ctx.lineTo(x + r, y + h)
    ctx.lineTo(x, y + h - r)
    // 左下角
    ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI)
    // 左边框
    ctx.lineTo(x, y + r)
    ctx.lineTo(x + r, y)
    //填充颜色
    ctx.setFillStyle(color);
    ctx.fill()
    ctx.closePath()
  }

调用:

that.roundRect(context, 15 * xs, 60 * xs, 350* xs, 200 * xs, 14 * xs, '#ffffff')
  • 文本加粗

官方文档里有说到font的使用规则与css语法一致,有几个需要注意的地方,否则可能会导致设置无效。

调用:

 context.font = "normal bold 27px sans-serif";
 context.setFontSize(27 * xs)
 context.fillText('加粗字体', 100 * xs , 145 * xs)

效果:

注意:在真机上若没有写第一个normal参数,则不能成功设置。
字体大小可以在下面重新赋值。
如果没有效果可以注意console有没有如下图所示 设置无效的警告,原因很大可能是因为参数写的不对。

  • 圆形头像绘制

方法:在画布上剪切一个圆,然后在圆上画头像,最后恢复即可。有一个需要注意的地方,drawImage方法只能绘制本地图片,如果需要绘制网络图片需下载完成之后再画。代码如下:

    context.save()
    context.beginPath()
    context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false)
    context.clip()
    var headimg = '/images/headimg.jpg'
    context.drawImage(headimg, 150 * xs, 73 * xs, 77 * xs, 77 * xs)
    context.restore()
    context.draw();

遇到的问题:当图片为长方形时,强行将图片压缩为正方形会导致头像变形。
解决办法:image组件里,参数mode有一个值为aspectFill,即保持纵横比缩放图片,只保证图片的短边能完全显示出来,我们参考这种思路来截取图片。

这里以宽比高长的图为例。如上图所示,圆为头像显示位置,线为中线,矩形框为一张宽大于高的图片。矩形左上角即为画图时的左上角坐标。截部分如图所示,得到图片宽高后,短边固定为头像尺寸,长边根据短边缩放比计算得到。图片宽=原图宽 /(头像高 / 原图高)。左上角的x轴坐标为:中线x坐标 - 图片宽 / 2。代码如下所示:

    context.save()
    context.beginPath()
    context.arc(77 / 2 * xs + 150 * xs, 77 / 2 * xs + 73 * xs, 77 / 2 * xs, 0, Math.PI * 2, false)
    context.clip()
    var headimg = '/images/headimg.jpg'; //头像路径
    var headimgHeight = 0;
    var headimgWidth = 0;
    wx.getImageInfo({
      src: headimg,
      success(res) {
        headimgHeight = res.height; //原图高度
        headimgWidth = res.width; //原图宽度
        //当宽 > 高时
        if (headimgWidth > headimgHeight) {
          var width = headimgWidth / (headimgHeight / (77 * xs)); //图片宽度
          var x = (150 + 77 / 2) * xs - width / 2; //x轴坐标
          context.drawImage(headimg, x, 73 * xs, width, 77 * xs)
        } else {
          //当高>=宽时
          var height = headimgHeight / (headimgWidth / (77 * xs)); //图片高度
          var h = (73 + 77 / 2) * xs - height / 2; //y轴坐标
          context.drawImage(headimg, 150 * xs, h, 77 * xs, height)
          context.restore()
          context.draw();
        }
      }
    })

注意:这里得到的图片宽高已经是px为单位,所以不乘系数。

最后一次编辑于  03-18  (未经腾讯允许,不得转载)
复制链接赞 10收藏投诉评论

12 个评论

  • 明明
    明明
    03-18

    假装已经看完了

    03-18
    赞同 2
    回复 1
  • Mr.G
    Mr.G
    03-18

    我发现有个问题 ,draw如果不擦除的话,重复调用性能会越来越低,也就是 context.draw(true), 而如果只是 context.draw()的话,则不受影响可以接受

    03-18
    赞同 1
    回复 5
    • 痛快科技
      痛快科技
      03-18

      以前还真没考虑过这个问题,都是在最后draw()了。context.draw(true)是可以避免画的太久等待时间过长,让它画一点展现一点出来是吗?感觉还是有优点的,需要考虑一下平衡。

      03-18
      回复
    • Mr.G
      Mr.G
      03-18回复痛快科技

      我的是动态绘图,也就是用户编写的代码会控制canvas中的一个小点移动。小点移动的轨迹就不能采用擦除的办法了,所以就采用了动画绘制的方法,定时器调用 draw,  就产生了上面的奇怪现象,按照我的理解,原生的canvas,底层是在操作系统原生组件下实现的,重复绘制仅仅实在bitmap 下,渲染了像素而已,怎么会慢了呢

      03-18
      回复
    • 痛快科技
      痛快科技
      03-18回复Mr.G

      我刚刚简单试了下,没什么大的性能问题呀,点的轨迹还是均匀的,也没有越画越慢的迹象。

        createCard: function() {
          var that = this;
          var context = wx.createCanvasContext('myCanvas');
          var height = 43 * xs
          context.setFontSize(50 * xs)
          context.setFillStyle('#02B488')
          context.setTextAlign('center')
          context.fillText('.', 375 * xs / 2, height)
          context.draw(true, setInterval(function() {
            height = height + 10 * xs;
            context.fillText('.', 375 * xs / 2, height)
            context.draw(true)
          }, 500))
        }
      })


      03-18
      回复
    • Mr.G
      Mr.G
      03-18回复痛快科技

      500太慢了,我的是设置的30,

      03-18
      回复
    • Mr.G
      Mr.G
      03-18回复痛快科技

      在虚拟CNC下有 代码范例,可以点一个进去,然后选择保留路径,然后就会看到卡了,掉帧,

      03-18
      回复
  • 兰昊
    兰昊
    03-19

    老哥 你这canvas教程能教动画不?

    能解决requestAnimation pollify 的问题不?

    在android下需要300ms延迟渲染image吗?

    03-19
    赞同
    回复
  • 沫笺
    沫笺
    04-12

    感谢分享,慢慢干货呢~画canvans时基本都会遇到的问题~

    04-12
    赞同
    回复
  • 仗剑走天涯
    仗剑走天涯
    06-22

    666

    06-22
    赞同
    回复
  • 子非鱼LX
    子非鱼LX
    06-25

    这个我开发游戏时也考虑过,如果只有一层canvas的话,一般draw多少次基本不影响性能,当然死循环除外(死循环其实是逻辑层崩溃跟画布没有太大关系)。这是我个人的看法,呵呵

    06-25
    赞同
    回复