小程序 canvas2d 趟坑記錄

一、關於canvas繪圖,小程序官方文檔目前有新舊兩種接口,分別如下:

1、Canvas 2D 示例代碼

<!-- canvas.wxml -->
  <canvas type="2d" id="myCanvas"></canvas>

onReady() {
    const query = wx.createSelectorQuery()
    query.select('#myCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvas = res[0].node
        const ctx = canvas.getContext('2d')

        const dpr = wx.getSystemInfoSync().pixelRatio
        canvas.width = res[0].width * dpr
        canvas.height = res[0].height * dpr
        ctx.scale(dpr, dpr)

        ctx.fillRect(0, 0, 100, 100)
      })
  }

2、示例代碼(舊的接口)

<!-- canvas.wxml -->
<canvas style="width: 300px; height: 200px;" canvas-id="firstCanvas"></canvas>

onReady() {
    // 使用 wx.createContext 獲取繪圖上下文 context
    var context = wx.createCanvasContext('firstCanvas')

    context.setStrokeStyle("#00ff00")
    context.setLineWidth(5)
    context.rect(0, 0, 200, 200)
    context.stroke()
    context.setStrokeStyle("#ff0000")
    context.setLineWidth(2)
    context.moveTo(160, 100)
    context.arc(100, 100, 60, 0, 2 * Math.PI, true)
    context.moveTo(140, 100)
    context.arc(100, 100, 40, 0, Math.PI, false)
    context.moveTo(85, 80)
    context.arc(80, 80, 5, 0, 2 * Math.PI, true)
    context.moveTo(125, 80)
    context.arc(120, 80, 5, 0, 2 * Math.PI, true)
    context.stroke()
    context.draw()
  }

目前網上教程大部分是針對舊的api(第二種示例)的解析,相比於第一種示例(canvas2d)舊版本效率低下且官方明示不在維護。

二、鑑於canvas2d官方文檔的某種特性(混亂不堪,投訴無門,秉承鵝廠愛答不理的一貫作風),故整理以下內容:

小程序分享海報案例

標題示例圖

1、官網文檔地址 https://developers.weixin.qq.com/miniprogram/dev/component/canvas.html

2、

   , 特別注意第6、7條:

    1. 在ios中不設置寬高生成圖片大小會錯亂;

    2. 安卓系統中dpr超過3時會報如下錯誤:  canvasToTempFilePath:fail:convert native buffer parameter fail. native buffer exceed size       limit,解決辦法把dpr設置爲1即可;

3.繪圖初始化 ,由於繪圖分先後順序 所有方法用Promise,繪製文本時特別注意 字體一定要是系統中有的字體 否則文字繪製不上

 /**
   * 繪圖初始化
   * @constructor
   */
  public DrawInit() {
    const that = this;
    const query = wx.createSelectorQuery().in(that); //如果是在組件中,則改成             
    this.createSelectorQuery()
    (query as any)
      .select("#sharePic")
      .fields({ node: true, size: true })
      .exec((res) => {
        const canvas = res[0].node;
        const ctx = canvas.getContext("2d");
        //dpr大於3 安卓繪製失敗 此處設爲1
        const dpr = wx.getSystemInfoSync().pixelRatio;
        canvas.width = 750 * 1;
        canvas.height = 1220 * 1;
        ctx.scale(1, 1);

        const CanvasD = new CanvasDraw(canvas, ctx);
        that.CanvasD = CanvasD;
        CanvasD.DrawImg2d(that.shareInfo.backImg).then(() => {
          CanvasD.drawLogo().then(() => {
            CanvasD.drawSiteInfo().then(() => {
              CanvasD.roundRect(40, 268, 670, 912, 10).then(() => {
                CanvasD.drawTXT(that.shareInfo.txtArr).then(() => {
                  CanvasD.drawQrCode(that.shareInfo.qrCode).then(() => {
                    CanvasD.DrawImg2d(that.shareInfo.liveIcons, 60, 1018, 92, 36).then(() => {
                      CanvasD.roundRect(40, 268, 670, 670, 0).then(() => {
                        that.covertImageAsround(that.shareInfo.actImg).then((actImg) => {
                          CanvasD.DrawImg2d(actImg, 40, 268, 670, 670).then(() => {
                            CanvasD._canvasToPath(that).then((res: string) => {
                              console.error(res, "-----------");
                               //七牛圖片倒角方法
                              // that.canvasImage = `${res}?roundPic/radius/10`;
                              that.canvasImage = res;
                              wx.hideLoading();
                            });
                          });
                        });
                      });
                    });
                  });
                });
              });
            });
          });
        });
      });
    // });
  }

分裝的canvasDwaw方法

import { AppUrls } from "@/utils/consts";
import { CacheManager } from "@/services/cache/cacheManger";
import { IPSMainService } from "@/services/ipsService/ipsMainService";
import { WXAppSDK } from "@/services/weixin/wxAppSDK";
// const debug = require("debug")("log:Canvas2d");
const getSiteInCache = CacheManager.getSiteInCache;
export default class CanvasDraw {
  protected WIDTH: number;
  protected HEIGHT: number;
  protected rate: number;
  protected ctx: any = null;
  protected dpr = 0;
  protected canvasNode: any = null;
  protected canvasId = "";
  protected siteInfo: SiteInfo = {};
  protected canvasImage = "";
  constructor(canvasNode, ctx, WIDTH?: number, HEIGHT?: number, rate?: number) {
    // super(canvasId);
    this.ctx = ctx;
    this.canvasNode = canvasNode;
    // debug(canvasId);
    this.WIDTH = WIDTH || 750;
    this.HEIGHT = HEIGHT || 1220;
    this.rate = rate || 1;
    this.siteInfo = getSiteInCache() || {};
    this.init();
  }
  public init() {
    this.dpr = wx.getSystemInfoSync().pixelRatio;
  }

  public DrawImg2d(url, x?, y?, w?, h?) {
    console.error(url);
    const that = this;
    return new Promise((resolve, reject) => {
      this.ctx.beginPath();
      this.ctx.save();
      this.ctx.restore();

      if (!url) {
        resolve(1);
        return;
      }

      const img = this.canvasNode.createImage(); //canvas 2d 通過此函數創建一個圖片對象

      img.onload = (e) => {
        console.log("img loaded");
        console.log("drawImage", (x || 0) * that.rate, (y || 0) * that.rate, (w || that.WIDTH) * that.rate, (h || that.HEIGHT) * that.rate);
        that.ctx.drawImage(img, (x || 0) * that.rate, (y || 0) * that.rate, (w || that.WIDTH) * that.rate, (h || that.HEIGHT) * that.rate);
        that.ctx.restore();
        // that.ctx.drawImage(res.path, (x || 0) * that.rate, (y || 0) * that.rate, res.width * that.rate, res.height * that.rate);
        resolve(that.ctx);
        console.log("getImageInfo", e);
      };
      img.onerror = (e) => {
        console.error("err:", e);
      };
      img.src = url;

      console.log("繪製圖片公共方法", url);
    });
  }

  
  /**
   * 獲取siteLogo
   * (轉換爲七牛地址)
   */
  public getSiteLogo() {
    return new Promise((resolve) => {
      IPSMainService.getSiteLog({
        postParams: { siteId: this.siteInfo.siteId }
      })
        .then((siteLogo) => {
          resolve(siteLogo);
        })
        .catch((errorMessage) => {
          throw errorMessage;
        });
    });
  }

  /**
   * 繪製店鋪LOGO
   * @param shareCanvas
   * @param res
   * @param ctx
   * @param rate
   */
  public drawLogo() {
    const that = this;
    return new Promise((resolve, reject) => {
      that.getSiteLogo().then((siteLogo) => {
        that.ctx.save();
        // let siteLogo = shareCanvas.createImage();
        //繪製外層圓
        const avatarurl_width = 112 * that.rate, //繪製的頭像寬度
          avatarurl_heigth = 112 * that.rate, //繪製的頭像高度
          avatarurl_x = (that.WIDTH * that.rate - avatarurl_width) / 2, //繪製的頭像在畫布上的位置
          avatarurl_y = 32 * that.rate, //繪製的頭像在畫布上的位置
          //繪製內層圓
          s_avatarurl_width = 100 * that.rate, //繪製的頭像寬度
          s_avatarurl_heigth = 100 * that.rate, //繪製的頭像高度
          s_avatarurl_x = (that.WIDTH * that.rate - s_avatarurl_width) / 2, //繪製的頭像在畫布上的位置
          s_avatarurl_y = 40 * that.rate; //繪製的頭像在畫布上的位置

        that.ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2);
        that.ctx.fillStyle = "#e4e4e4";
        that.ctx.fill();

        that.ctx.restore();
        that.ctx.save();
        that.ctx.beginPath();

        that.ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, s_avatarurl_width / 2, 0, Math.PI * 2);
        that.ctx.fillStyle = "#fff";
        that.ctx.fill();

        that.ctx.clip(); //畫了圓 再剪切  原始畫布中剪切任意形狀和尺寸。一旦剪切了某個區域,則所有之後的繪圖都會被限制在被剪切的區域內
        console.log("繪製店鋪LOGO", (siteLogo as any).siteLog);
        that.DrawImg2d((siteLogo as any).siteLog, s_avatarurl_x, s_avatarurl_y, s_avatarurl_width, s_avatarurl_heigth).then(() => {
          resolve(that.ctx);
        });
      });
    });
  }

  /**
   * 繪製店鋪信息
   * @param res
   * @param ctx
   * @param rate
   */
  public drawSiteInfo() {
    return new Promise((resolve, reject) => {
      //siteName
      this.ctx.font = `${32 * this.rate}px Arial`;
      this.ctx.fillStyle = "#f7f7f7";
      this.ctx.textAlign = "center";
      this.ctx.fillText(this.siteInfo.siteName, (750 * this.rate) / 2, 186 * this.rate);
      // ctx.fillText('139南東 JORDAN', (this.ComputedFontOffsetLeft('139南東 JORDAN', 32 * rate, res[0].width)), 186 * rate);

      //siteAddr
      this.ctx.font = `${24 * this.rate}px Arial`;
      this.ctx.fillStyle = "#DFDFDF";
      // ctx.fillText(this.shareInfo.siteInfo.siteAddress, (this.ComputedFontOffsetLeft(this.shareInfo.siteInfo.siteAddress, 24 * rate, res[0].width)), 230 * rate);
      this.ctx.fillText(this.siteInfo.siteAddress, (750 * this.rate) / 2, 230 * this.rate);
      resolve(this.ctx);
      console.log("繪製店鋪信息");
    });
  }

  /**
   * 圓角矩形
   * @param x X軸
   * @param y y軸
   * @param w 寬
   * @param h 高
   * @param r 半徑
   * @param ctx
   */
  public roundRect(x, y, w, h, r) {
    x = x * this.rate;
    y = y * this.rate;
    w = w * this.rate;
    h = h * this.rate;
    r = r * this.rate;
    return new Promise((resolve) => {
      if (w < 2 * r) r = w / 2;
      if (h < 2 * r) r = h / 2;
      this.ctx.beginPath();

      this.ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
      this.ctx.moveTo(x + r, y);
      this.ctx.lineTo(x + w - r, y);
      this.ctx.lineTo(x + w, y + r);

      this.ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2);
      this.ctx.lineTo(x + w, y + h - r);
      this.ctx.lineTo(x + w - r, y + h);

      this.ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5);
      this.ctx.lineTo(x + r, y + h);
      this.ctx.lineTo(x, y + h - r);

      this.ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI);
      this.ctx.lineTo(x, y + r);
      this.ctx.lineTo(x + r, y);

      this.ctx.closePath();

      this.ctx.fillStyle = "#fff";
      this.ctx.fill();
      this.ctx.clip();
      resolve(this.ctx);
      this.ctx.save();
      this.ctx.restore();
      console.log("圓角矩形");
    });
  }

  /**
   * 繪製海報文本
   * @param shareCanvas
   * @param res
   * @param ctx
   * @param rate
   */
  public drawTXT(arr) {
    const TXTArr = arr.filter((item) => {
      return item.title;
    });
    console.log("繪製海報文本", TXTArr);
    const that = this;
    return new Promise((resolve, reject) => {
      for (let index = 0; index < TXTArr.length; index++) {
        const item = TXTArr[index];
        if (item.title) {
          const title = that.workSpace(item.title);

          //自定義行數
          const len = title.length > item.line ? item.line : title.length || 1;
          for (let i = 0; i < len; i++) {
            console.log(title[i], that.ctx.measureText(title[i]).width);
            let str = title[i];
            if (i === len - 1 && str && that.ctx.measureText(str).width > 280 * that.rate) {
              str += "...";
            }

            that.ctx.font = `normal ${item.fontWeight} ${item.fontSize * that.rate}px Arial`; // 設置字體大小
            that.ctx.fillStyle = item.color; // 設置文字顏色
            that.ctx.textAlign = item.textAlign;
            that.ctx.fillText(str, item.x * that.rate, (item.y + (item.fontSize + 10) * i) * that.rate);
            // that.ctx.fillText(title[i], item.x * that.rate, item.y * that.rate);

            // 是否顯示刪除線
            if (item.isDelLine) {
              that.ctx.beginPath();
              that.ctx.moveTo(item.x * that.rate, (item.y - item.fontSize / 2 + 2) * that.rate);
              that.ctx.lineTo(item.x * that.rate + (item.title.length - 1) * item.fontSize * that.rate, (item.y - item.fontSize / 2 + 2) * that.rate);
              that.ctx.lineWidth = 2;
              that.ctx.strokeStyle = item.color;
              that.ctx.stroke();
            }

            that.ctx.save();
            that.ctx.restore();
          }
        }
      }

      resolve(this.ctx);
      console.log("繪製海報文本");
    });
  }

  /**
   * 文字自動換行
   */
  public workSpace(txt) {
    const text = this.ctx.measureText(txt);
    console.log("txt", txt, text.width);
    const chr = txt.split("");
    // 保存計算換行後字符
    let newTxt = "";
    // 保存結果字符數組
    const resArr = [];

    for (let i = 0; i <= chr.length + 1; i++) {
      if (this.ctx.measureText(newTxt).width < 300 * this.rate) {
        if (i == chr.length && newTxt) {
          resArr.push(newTxt);
        } else {
          newTxt += chr[i];
        }
      } else {
        i--;
        newTxt && resArr.push(newTxt);
        newTxt = "";
      }
    }
    console.log("文字自動換行--------------", resArr);
    return resArr;
  }


  /**
   * 繪製二維碼
   * @param shareCanvas
   * @param res
   * @param ctx
   * @param rate
   */
  public drawQrCode(url) {
    const that = this;
    return new Promise((resolve) => {
      that.ctx.save();
      that.ctx.restore();

      that.DrawImg2d(url, 522 * that.rate, (938 + 20) * that.rate, 154 * that.rate, 154 * that.rate).then(() => {
        resolve(that.ctx);
        console.log("繪製二維碼", url);
      });
    });
  }

 

  /**
   * 轉圖片格式
   */
  _canvasToPath(evt) {
    const that = this;
    return new Promise((resolve) => {
      (wx as any).canvasToTempFilePath(
        {
          x: 0,
          y: 0,
          width: that.WIDTH * that.rate,
          height: that.HEIGHT * that.rate,
          destWidth: that.WIDTH * that.rate,
          destHeight: that.HEIGHT * that.rate,
          fileType: "jpg",
          quality: 1,
          canvas: that.canvasNode,
          success(res) {
            that.canvasImage = res.tempFilePath;
            resolve(res.tempFilePath);
            console.log(res, "canvasToTempFilePath");
          },
          fail(err) {
            console.log(err, "canvasToTempFilePath-fail");
          }
        },
        that
      );
    });
  }

  
  /**
   * @param qrCode 保存圖片
   */
  public saveImage(qrCode) {
        wx.saveImageToPhotosAlbum({
          filePath: this.canvasImage,
          success: (img) => {
            console.error(img);
            WXAppSDK.errorMessage("圖片保存成功");
          },
          fail: () => {
            WXAppSDK.errorMessage("圖片保存失敗");
          },
          complete: () => {
           
          }
        });
  }
}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章