Galan小屋

welcome my blog

小程序canvas生成图片

By galan99

发表于 2018-11-14

微信小程序上canvas绘制图片助手,一个json就制作分享朋友圈图片

有以下注意事项

  • 网络图片要是安全域名下才能生成,并且要先下载在本地(wx.getImageInfo/wx.uploadFile)
  • 开发者工具里的项目设置关闭所有的勾选,要不然体验版手机不能生成图片

引入组件,把canvas.wpy放到components目录下,painting 是需要传入的 json


//components/canvans.wpy

<template>
    <canvas canvas-id="canvasdrawer"
        style="width:px; height:px;"
        class="board"
        wx:if="">
    </canvas>
</template>

<script>
import wepy from "wepy";

export default class Drawer extends wepy.component {
      /**
    * @argument
    * painting -- json数据
    * @event
    * getImage 渲染图片完成后的回调
      **/
    props = {
        painting: {
            type: Object,
            default: {}
        }
    };

    data = {
        showCanvas: false,

        width: 100,
        height: 100,

        index: 0,
        imageList: [],
        tempFileList: [],

        isPainting: false,
        ctx: null,
        cache: {}
    };

    watch = {
        painting(newVal, oldVal) {
            if (!this.isPainting) {
                if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
                    if (newVal && newVal.width && newVal.height) {
                        this.showCanvas = true;
                        this.isPainting = true;
                        this.$apply();
                        this.readyPigment();
                    }
                } else {
                    if (newVal && newVal.mode !== "same") {
                        this.$emit("getImage", {
                            errMsg: "canvasdrawer:samme params"
                        });
                    }
                }
            }
        }
    };

    methods = {};
    readyPigment() {
        const {
            width,
            height,
            views
        } = this.painting;
        this.width = width;
        this.height = height;

        const inter = setInterval(() => {
            if (this.ctx) {
                clearInterval(inter);
                this.ctx.clearActions();
                this.ctx.save();
                this.getImageList(views);
                this.downLoadImages(0);
            }
        }, 100);
    }
    getImageList(views) {
        const imageList = [];
        for (let i = 0; i < views.length; i++) {
            if (views[i].type === "image") {
                imageList.push(views[i].url);
            }
        }
        this.imageList = imageList;
    }
    downLoadImages(index) {
        const {
            imageList,
            tempFileList
        } = this;
        if (index < imageList.length) {
            this.getImageInfo(imageList[index]).then(file => {
                tempFileList.push(file);
                this.tempFileList = tempFileList;
                this.downLoadImages(index + 1);
            });
        } else {
            this.startPainting();
        }
    }
    startPainting() {
        const {
            tempFileList,
            painting: {
                views
            }
        } = this;
        for (let i = 0, imageIndex = 0; i < views.length; i++) {
            if (views[i].type === "image") {
                this.drawImage({
                    ...views[i],
                    url: tempFileList[imageIndex]
                });
                imageIndex++;
            } else if (views[i].type === "text") {
                if (!this.ctx.measureText) {
                    wepy.showModal({
                        title: "提示",
                        content: "当前微信版本过低,无法使用 measureText 功能,请升级到最新微信版本后重试。"
                    });
                    this.$emit("getImage", {
                        errMsg: "canvasdrawer:version too low"
                    });
                    return;
                } else {
                    this.drawText(views[i]);
                }
            } else if (views[i].type === "rect") {
                this.drawRect(views[i]);
            }
        }
        this.ctx.draw(false, () => {
            wx.setStorageSync('canvasdrawer_pic_cache', this.cache)
            const system = wx.getSystemInfoSync().system
            if (/ios/i.test(system)) {
                this.saveImageToLocal()
            } else {
                // 延迟保存图片,解决安卓生成图片错位bug。
                setTimeout(() => {
                    this.saveImageToLocal()
                }, 100);
            }
        });
    }
    drawImage(params) {
        this.ctx.save();
        const {
            url,
            top = 0,
            left = 0,
            width = 0,
            height = 0,
            radius = 0
        } = params;
        if(radius != 0){
            this.ctx.arc(left + radius, top + radius, radius, 0, 2 * Math.PI)
            this.ctx.clip();
        }
        this.ctx.drawImage(url, left, top, width, height);
        this.ctx.restore();
    }
    drawText(params) {
        this.ctx.save();
        const {
            MaxLineNumber = 2,
            breakWord = false,
            color = "black",
            content = "",
            fontSize = 16,
            top = 0,
            left = 0,
            lineHeight = 20,
            textAlign = "left",
            width,
            bolder = false,
            textDecoration = "none"
        } = params;

        this.ctx.beginPath();
        this.ctx.setTextBaseline("top");
        this.ctx.setTextAlign(textAlign);
        this.ctx.setFillStyle(color);
        this.ctx.setFontSize(fontSize);

        if (!breakWord) {
            this.ctx.fillText(content, left, top);
            this.drawTextLine(left, top, textDecoration, color, fontSize, content);
        } else {
            let fillText = "";
            let fillTop = top;
            let lineNum = 1;
            for (let i = 0; i < content.length; i++) {
                fillText += [content[i]];
                if (this.ctx.measureText(fillText).width > width) {
                    if (lineNum === MaxLineNumber) {
                        if (i !== content.length) {
                            fillText = `${fillText.substring(0, fillText.length - 1)}...`;
                            this.ctx.fillText(fillText, left, fillTop);
                            this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText);
                            fillText = "";
                            break;
                        }
                    }
                    this.ctx.fillText(fillText, left, fillTop);
                    this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText);
                    fillText = "";
                    fillTop += lineHeight;
                    lineNum++;
                }
            }
            this.ctx.fillText(fillText, left, fillTop);
            this.drawTextLine(left, fillTop, textDecoration, color, fontSize, fillText);
        }

        this.ctx.restore();

        if (bolder) {
            this.drawText({
                ...params,
                left: left + 0.3,
                top: top + 0.3,
                bolder: false,
                textDecoration: "none"
            });
        }
    }
    drawTextLine(left, top, textDecoration, color, fontSize, content) {
        if (textDecoration === "underline") {
            this.drawRect({
                background: color,
                top: top + fontSize * 1.2,
                left: left - 1,
                width: this.ctx.measureText(content).width + 3,
                height: 1
            });
        } else if (textDecoration === "line-through") {
            this.drawRect({
                background: color,
                top: top + fontSize * 0.6,
                left: left - 1,
                width: this.ctx.measureText(content).width + 3,
                height: 1
            });
        }
    }
    drawRect(params) {
        this.ctx.save();
        const {
            background,
            top = 0,
            left = 0,
            width = 0,
            height = 0
        } = params;
        this.ctx.setFillStyle(background);
        this.ctx.fillRect(left, top, width, height);
        this.ctx.restore();
    }
    getImageInfo(url) {  
        return new Promise((resolve, reject) => {
            if (this.cache[url]) {
                resolve(this.cache[url])
            } else {
                const objExp = new RegExp(/^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- .\/?%&=]*)?/)
                if (objExp.test(url)) {
                    wx.getImageInfo({
                        src: url,
                        complete: res => {
                            if (res.errMsg === 'getImageInfo:ok') {
                                this.cache[url] = res.path
                                resolve(res.path)
                            } else {
                                this.triggerEvent('getImage', {errMsg: 'canvasdrawer:download fail'})
                                reject(new Error('getImageInfo fail'))
                            }
                        }
                    })
                } else {
                    this.cache[url] = url
                    resolve(url)
                }
            }
      })
    }
    async saveImageToLocal() {
        const {
            width,
            height
        } = this;
        const res = await wepy.canvasToTempFilePath({
            x: 0,
            y: 0,
            width,
            height,
            canvasId: "canvasdrawer"
        }, this);
        if (res.errMsg === "canvasToTempFilePath:ok") {
            this.showCanvas = false;
            this.isPainting = false;
            this.imageList = [];
            this.tempFileList = [];
            this.$apply();
            this.$emit("getImage", {
                tempFilePath: res.tempFilePath,
                errMsg: "canvasdrawer:ok"
            });
        } else {
            this.$emit("getImage", {
                errMsg: "canvasdrawer:fail"
            });
        }
    }
    onLoad() {
        wepy.removeStorageSync("canvasdrawer_pic_cache");
        this.cache = wepy.getStorageSync("canvasdrawer_pic_cache") || {};
        this.ctx = wepy.createCanvasContext("canvasdrawer", this);
    }
}
</script>
<style lang="less">
    .board {
        position: fixed;
        top: 2000rpx;
    }
</style>

在使用页面注册组件,代码如下,getImage 方法是绘图完成之后的回调函数,在 event.detail 中返回绘制完成的图片地址。


<view>
    <image src="" class="down"></image>
    <canvasdrawer :painting.sync="painting" @getImage.user="eventGetImage"></canvasdrawer>
    <button type='primary' class='openSetting' open-type="openSetting" wx:if="">去授权</button>
    <view class="longtap" wx:if="" @tap="eventSave">保存至手机相册</view>
</view>


import canvasdrawer from '@/components/canvas';
export default class Index extends wepy.page {
  components = {
    canvasdrawer,
  };
  data = {
    shareImage: '',//生成的图片地址
    painting: null,//canvas图片信息
    show: false,
    userInfo: {},//用户信息
    openSetBtn: false,//授权相册
  };

  watch = {
    shareImage: function(newVal) {
      if (newVal) {
        this.show = true;
        wx.hideLoading();
      }
    }
  };
  methods = {
    async eventSave() {
      // 保存图片至本地
      let that = this;
      try {
        const res = await wepy.saveImageToPhotosAlbum({
          filePath: this.shareImage
        });
        that.openSetBtn = false;
        that.$apply();
        if (res.errMsg === 'saveImageToPhotosAlbum:ok') {
          wepy.showToast({
            title: '保存图片成功',
            icon: 'success',
            duration: 2000
          });
        }
      } catch (err) {
        console.log(err);
        if (err.errMsg != 'saveImageToPhotosAlbum:fail cancel') {
          wx.showModal({
            title: '警告',
            content: '若不打开授权,则无法将图片保存在相册中!',
            showCancel: false
          });
          that.openSetBtn = true;
          that.$apply();
        } else {
          that.openSetBtn = false;
          that.$apply();
        }
      }
    },
    //生成图片
    eventGetImage(event) {
      const { tempFilePath, errMsg } = event;
      if (errMsg === 'canvasdrawer:ok') this.shareImage = tempFilePath;
    },
  };

  getData(){
    let that = this;
    let gameName = '游戏名字-5';
    let num = 55; //礼包个数
    let pre = 0;
    if (gameName) {
      gameName.split('').forEach((val, i) => {
        if (val.charCodeAt(0) > 127 || val.charCodeAt(0) == 94) {
          pre++;
        } else {
          pre += 0.5;
        }
      });
    }
    this.painting = {
        width: 586,
        height: 742,
        views: [
            {
                type: 'image',
                url: 'https://dl.gamdream.com/activity/douwan/20181108/share2.png',
                top: 0,
                left: 0,
                width: 586,
                height: 742
            },
            {
                type: 'image',
                url: 'https://dl.gamdream.com/activity/douwan/20181108/share2.png',
                top: 152,
                left: 222,
                width: 149,
                height: 149,
                radius: Math.floor(149 / 2)
            },
            {
                type: 'image',
                url: 'https://dl.gamdream.com/activity/douwan/20181108/radius.png',
                top: 152,
                left: 222,
                width: 149,
                height: 149
            },
            {
                type: 'text',
                content: '微信名字',
                fontSize: 26,
                color: '#333333',
                textAlign: 'center',
                top: 307,
                left: 298
            },
            {
                type: 'text',
                content: '我在逗玩城已经领取',
                fontSize: 28,
                color: '#333333',
                textAlign: 'center',
                top: 350,
                left: 298
            },
            {
                type: 'text',
                content: num,
                fontSize: 48,
                color: '#333333',
                textAlign: 'left',
                top: 397,
                left: Math.floor((600 - String(num).length * 48 * 0.586 - 30) / 2)
            },
            {
                type: 'text',
                content: '个',
                fontSize: 30,
                color: '#333333',
                textAlign: 'left',
                top: 412,
                left: Math.floor(
                    (600 - String(num).length * 48 * 0.586 - 30) / 2 +
                    String(num).length * 48 * 0.586
                )
            },
            {
                type: 'text',
                content: gameName,
                fontSize: 30,
                color: '#8A2C1B',
                textAlign: 'left',
                top: 465,
                left: Math.floor((600 - (30 * pre + 20 + 60)) / 2)
            },
            {
                type: 'text',
                content: '礼包',
                fontSize: 30,
                color: '#333333',
                textAlign: 'left',
                top: 465,
                left: Math.floor((600 - (30 * pre + 20 + 60)) / 2 + (30 * pre + 20))
            }, 
        ]
    };
    that.$apply();
  }

  onLoad(){
    this.getData()
  }
}