背景介绍

目标:利用canvas画布生成社团活动的海报图片,便于社团活动宣传以及对小程序的推广。

场景:用户A觉得某个社团的活动很不错,因此点击“分享”按钮,生成一个该活动的海报图片,接着,用户A把该图片发到某个群或者朋友圈进行传播,用户B看到该图片后对这个活动蛮感兴趣,通过长按识别图片中的小程序码,能够进入“北航社团帮”小程序的相应活动详情页。

UI设计:我们将海报内容需求进行了详细描述后,联系了社联宣传部帮忙制作了海报原型的ps图,即本人将按照这个目标去生成海报:

(附:社联设计的长宽比不大合适,过长,本人将进行调整)

canvas简介

代码实现

首先需要在wxml中使用canvas组件,注意需要把组件位置设置到屏幕之外,因为canvas画布画出来很丑。(虽然保存为图片的时候很好看)

<view>
  <canvas style="width: 375px;height: 690px;position:fixed;top:9999px" canvas-id="mycanvas" />
  <!-- canvas画布画出来很丑,不能用让用户看见 -->
</view>

接着需要在js中编写canvas绘制函数

    //在canvas上进行具体绘画的函数
  drawCanvas: function () {
    console.log("开始draw convas")
    var context = wx.createCanvasContext('mycanvas');
    var greycolor = '#969696';
    var maincolor = '#eda874';

    //0.绘制背景图片和原竖版海报
    console.log("在画海报时,原海报下载的临时地址为:")
    console.log(_this.data.poster_old)
    context.drawImage(_this.data.poster_old, 69, 120, 237, 333);
    context.drawImage("/images/bg4.png", 0, 0, 375, 690);

    context.save();
    //1.绘制头像
    var radius = 20;
    var center_x = 79;
    var center_y = 30;
    context.arc(center_x, center_y, radius, 0, 2 * Math.PI) //画出圆
    context.clip(); //裁剪上面的圆形
    console.log("在画海报时,原头像下载的临时地址为:")
    console.log(_this.data.touxiang)
    context.drawImage(_this.data.touxiang, center_x - radius, center_y - radius, radius * 2, radius * 2); // 在刚刚裁剪的园上画图
    context.restore();

    //2.绘制昵称
    console.log(_this.data.userInfo)
    var nickname = _this.data.userInfo.nickname.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&apos;/g, "'").replace(/&ensp;/g, " ").replace(/&emsp;/g, " ") //特殊字符转义
    // nickname = "分解分解红红火火恍恍惚惚a";
    var max_nickname_width = 120;//一个汉字的宽度为10
    context.font = 'normal 10px sans-serif';
    var nickname_len_by_10 = context.measureText(nickname).width;
    if (nickname_len_by_10 > max_nickname_width) {//昵称宽度大于最大显示宽度,则只取前8个字符
      console.log("昵称太长,cut");
      nickname = nickname.slice(0, 12) + '...';
    }
    var width = 17;//昵称的字号
    var position_x_name = 60;
    var position_y_name = 75;
    context.setFontSize(width);
    context.setTextAlign('left');
    context.setFillStyle(greycolor);
    context.fillText(nickname, position_x_name, position_y_name);

    //3.绘制活动标题
    var actname = _this.data.activity_dict.name;
    // actname = "分解分解红红火火恍恍惚惚什么什么分解分解红红火火恍恍惚惚什么什么";
    var max_actname_width = 130;
    context.font = 'normal 10px sans-serif';
    var actname_len_by_10 = context.measureText(actname).width;
    if (actname_len_by_10 > max_actname_width) {//昵称宽度大于最大显示宽度,则只取前8个字符
      console.log("活动名称太长,cut");
      actname = actname.slice(0, 13) + '...';
    }
    var width = 19;//活动名称的字号
    var position_x_actname = 62;
    var position_y_actname = 495;
    context.font = 'normal bold 10px sans-serif';
    context.setFontSize(width);
    context.setTextAlign('left');
    context.setFillStyle(maincolor);
    context.fillText(actname, position_x_actname, position_y_actname);

    //4.绘制活动社团名称、活动地点
    //4.1.关于内容
    var club_const = "社团名称";
    var place_const = "活动地点";
    var club = _this.data.club_info.name;
    var place = _this.data.activity_dict.place;
    var max_width = 130;//你体会一下
    context.font = 'normal 10px sans-serif';
    // club = "解分解红红火火恍恍惚惚什么什么解分解红红火火恍恍惚惚什么什么";
    // place = "分解分解红红火火恍恍惚惚什";
    var club_by_10 = context.measureText(club).width;
    if (club_by_10 > max_width) {//宽度大于最大显示宽度,则只取前8个字符
      console.log("社团名称太长,cut");
      club = club.slice(0, 12) + '...';
    }
    var place_by_10 = context.measureText(place).width;
    if (place_by_10 > max_width) {//宽度大于最大显示宽度,则只取前8个字符
      console.log("地点太长,cut");
      place = place.slice(0, 12) + '...';
    }
    //4.2.关于样式
    var start_y = 527;
    var x = 64;
    var dis = 20;//"行间距"
    //4.3.画它
    context.setTextAlign('left');
    context.font = 'normal 12px sans-serif';
    context.setFillStyle(maincolor);
    context.fillText(club_const, x, start_y);
    context.setFillStyle(greycolor);
    context.fillText(club, x, start_y + dis);
    context.setFillStyle(maincolor);
    context.fillText(place_const, x, start_y + dis * 2);
    context.setFillStyle(greycolor);
    context.fillText(place, x, start_y + dis * 3);

    //5.绘制活动时间相关信息
    //5.1.关于内容
    var start_time = _this.data.activity_dict.start_time;//形如"2019-05-26 18:00 周日"
    var year = start_time.slice(0, 4);
    var month = start_time.slice(5, 7);
    var day = start_time.slice(8, 10);
    var hour_min_start = start_time.slice(11, 16);
    var hour_min_end = "(´-ω-`)";// (´-ω-`)   o(≧v≦)o  (≧v≦)
    var end_time = _this.data.activity_dict.end_time;
    var act_in_one_day = (start_time.slice(0, 10) == end_time.slice(0, 10));//判断该活动是否在一天内结束
    console.log("是否在一天内结束活动:")
    console.log(act_in_one_day);
    //仅当且仅当该活动end_time不为空,且该活动在一天内结束时,才显示活动结束时间,否则显示颜表情
    if (end_time != "" && act_in_one_day) {
      hour_min_end = end_time.slice(11, 16);
    }
    //5.2.画它
    // context.setFillStyle(maincolor);//为什么要在下面每个地方都放一个,因为真机第一次生成时有点问题
    context.setTextAlign('left');
    context.setFillStyle(maincolor);
    context.font = 'normal bold 25px sans-serif';//这个字体还行,不换了...
    context.fillText(month, 238, 548);
    context.setFillStyle(maincolor);
    context.font = 'normal bold 25px sans-serif';
    context.fillText(day, 238, 589);
    context.setFillStyle(maincolor);
    context.setTextAlign('right');
    context.font = 'normal 11px sans-serif';
    context.fillText(year, 316, 516);
    context.setFillStyle(maincolor);
    context.setTextAlign('right');
    context.font = 'normal 14px sans-serif';
    context.fillText(hour_min_start, 316, 556);
    context.setFillStyle(maincolor);
    context.setTextAlign('right');
    context.font = 'normal 14px sans-serif';
    context.fillText(hour_min_end, 316, 585);

    //6.绘制小程序码
    context.drawImage(_this.data.minicode, 255, 600, 60, 60);

    //7.将canvas生成好的图片下载到临时文件夹
    console.log("画好了")
    context.draw(false, function () {
      wx.canvasToTempFilePath({
        canvasId: 'mycanvas',
        success: function (res) {
          var tempFilePath = res.tempFilePath;
          _this.setData({
            imagePath: tempFilePath,
            hide_poster: false
          });
          console.log("图片下载到临时文件夹了")
        },
        fail: function (res) {
          console.log(res);
        }
      });
    });
  }

其中涉及到许多小程序中canvas画布的接口,读者请自行在微信官方文档中查看。比较重要的几点将在下文说明。

下面先展示一下代码中所用到的固定图片"/images/bg4.png",是本人利用PS生成的,是png图片,中间给海报图片留了空:(论善用PS的重要性)

下面展示下最后的海报生成效果:

点击活动详情页面的“分享”按钮:

就会生成活动的海报:

点击“保存相册”,即可将图片保存到相册。保存到相册的图片如下:

难点讲解

圆角矩形裁剪失败之PS的妙用

  • 第0步,绘制背景图片和原竖版海报时,善用了PS,在本地图片bg4.png中放置竖版海报的地方,裁出一个透明的圆角矩形。
  • 这么做的目的主要是,如果先画背景图片再画海报的话,由于海报需要实现圆角矩形,因此需要先通过canvas绘制路径并裁剪后再drawImage,我一开始确实是这么做的,并且参考了这篇博客写出了下面的代码:
    //3.绘制原竖版海报
    //3.1.绘制圆角矩形。首先定义圆角矩形的左上角点坐标,圆的半径,矩形的长宽。
    var x = 70;
    var y = 122;
    var r = 8;
    var w = 235;
    var h = 329;
    context.beginPath()
    // 因为边缘描边存在锯齿,最好指定使用 transparent 填充
    context.setFillStyle('transparent');// 这里是使用 fill 还是 stroke都可以,二选一即可
    // 左上角,border-top
    context.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5)
    context.moveTo(x + r, y)
    context.lineTo(x + w - r, y)
    context.lineTo(x + w, y + r)
    // 右上角,border-right
    context.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2)
    context.lineTo(x + w, y + h - r)
    context.lineTo(x + w - r, y + h)
    // 右下角,border-bottom
    context.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5)
    context.lineTo(x + r, y + h)
    context.lineTo(x, y + h - r)
    // 左下角,border-left
    context.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI)
    context.lineTo(x, y + r)
    context.lineTo(x + r, y)
    // 这里是使用 fill 还是 stroke都可以,二选一即可,但是需要与上面对应
    context.fill()
    context.closePath() //关闭路径
    //3.2.裁剪圆角矩形,绘制竖版海报
    context.clip(); //裁剪上面的圆角矩形
    console.log("在画海报时,原海报下载的临时地址为:")
    console.log(_this.data.poster_old)
    context.drawImage(_this.data.poster_old, x, y, w, h); // 在刚刚裁剪的园上画图
    context.restore();

最后在模拟器上确实实现了想要的效果,但是真机调试时,却发现无法看到竖版海报,只能看到圆角矩形。经过了艰苦卓绝的debug,仍然没有找到原因(没有试出原因),抱着结果比过程更重要的决心,我使用PS奇淫巧技地实现了想要的效果。

也就是:先将竖版海报图片放在底层,然后用顶层图片进行覆盖,顶层图片是通过PS留下中间透明的圆角矩形,从而实现对竖版海报图片的圆角矩形裁剪效果。

编码不要过硬

canvas绘图许多地方需要定位,如果将所有位置都进行硬编码的话,不论是在开发还是后期调整的过程中,都会难以调整,对此,笔者倡导适当进行硬编码,一旦你发现某个位置的坐标需要使用计算器计算/心算时,就表明你差不多应该使用变量来进行软编码的。

对过长的文字进行截取

以对过长的昵称进行截取为例:

    var max_nickname_width = 120;//一个汉字的宽度为10
    context.font = 'normal 10px sans-serif';
    var nickname_len_by_10 = context.measureText(nickname).width;
    if (nickname_len_by_10 > max_nickname_width) {//昵称宽度大于最大显示宽度,则只取前8个字符
      console.log("昵称太长,cut");
      nickname = nickname.slice(0, 12) + '...';
    }

首先,设置字体大小为10px,然后使用measureText函数(具体自行查看文档),即可得到所测量文字在10px字体下的宽度(一个汉字的宽度是10px,字母和标点符号会小一些)。

接着,设置max_width为120,表明昵称最长应是120px,也就是12个汉字的宽度,如果昵称过长将会进行截取,截取是通过slice函数进行。

真机首次生成时字体不对

在真机测试时,发现首次生成的海报的字号不对,解决方案是:

在绘制文字filltext前,都设置一下文字的样式(大小、颜色、字体等)。

drawImage只能使用本地图片

这点将在下一篇技术博客进行详解:https://www.cnblogs.com/buaareadsun/p/11020314.html

 posted on 2019-06-12 08:27  BuaaRedSun  阅读(3177)  评论(1编辑  收藏  举报