[技術博客]海報圖片生成——小程序canvas畫布


背景介紹

目標:利用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM