微信小程序生成朋友圈分享图/海报

  • 效果图

  • 源码用demo下载,海报内容根据实际调整,

  • 海报生成原文链接:https://developers.weixin.qq.com/community/develop/article/doc/000ac686c5c5506f18b87ee825b013

  • demo 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5

  • 小程序海报生成工具链接:https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813

  • npm 链接: https://www.npmjs.com/package/mina-painter

  • 使用组件:Painter

  • Painter 一款轻量级的小程序海报生成组件

  • pages 文件夹

  • pages/index/index.wxml

      <button class="intro" bindtap="createShareImage" >点我生成分享图</button>
      <share-box isCanDraw="{{isCanDraw}}" detailObj="{{detailObj}}" bind:initData="createShareImage" />
    
  • pages/index/index.json

      {
          "navigationBarTitleText":"生成朋友圈分享图",
        "usingComponents": {
          "share-box": "/components/shareBox/index"
        }
      }
    
  • pages/index/index.js

      const app = getApp()
    
      Page({
        data: {
          isCanDraw: false,
          detailObj: {}
        },
        onLoad() {
            const res = {"errCode":0,"status":200,"data":{"broker":{"name":"经纪人","brokerid":"19","storename":"余杭店","headurl":"https://broker.dissai.com.cn/webapiformal//UploadFilesformal/2023-09-28/8646b899-71b8-44ca-8ec0-1e5cc6edb482.jpg","phone":"188xxxx0000"},"result":[{"id":199.0,"type":"新房","propertyIntro":"绿创·溪山赋呼应时代所想,以“山水、低密、城心、别墅”为元素,以0.59容积率为底气,构建“山水别墅+城市别墅=半山墅院”的全新生活,为嵊州居者提供“城心桃花源”的非凡体验。","imgpath":"https://broker.dissai.com.cn/webapiformal//UploadFilesformal/Broker/2023-09-24/c4837043-b5fd-4af2-9216-499e85ac1d041.jpg","chamber":0,"office":0,"defend":0,"area":295.00,"price":737.00,"averagePrice":25000.00,"facilitesList":["别墅"],"labelList":["人车分流","低密居所","绿化率高"],"rentalMethod":"","city":"嵊州市","propertyName":"溪山赋","state":"在售","bShouCang":false,"propertyType":"住宅","mianJiMax":295.00,"mianJiMin":216.00,"juShiMax":6.0,"juShiMin":5.0,"priceMax":25000.0,"priceMin":24983.0,"allPriceMax":737.0,"allPriceMin":540.0,"mobileShowType":2,"houseid":"199","iscollect":false,"brokerid":"25"}]}}
            this.setData({
              detailObj: res.data
            })
            console.log(res.data.result[0])
        },
    
        createShareImage() {
          this.setData({
            isCanDraw: !this.data.isCanDraw
          })
        }
      })
    
  • pages/index/index.wxss

      .intro {
        width: 686rpx;
        height: 88rpx;
        background: #00cc88;
        color: #FFF;
        border-radius: 16rpx;
        font-size: 32rpx;
        text-align: center;
        margin: 200rpx auto;
      }
    
  • components 文件夹

  • components/shareBox/index.wxml

      <view class="share-wrap" wx:if="{{visible}}" catchtouchmove="preventDefault">
        <view class="share-back"></view>
        <view class="share-container">
          <view class="close" bindtap="handleClose" data-ptpid="ebe9-1656-ad6a-462e"></view>
          <image mode="widthFix" src="{{sharePath}}" class="share-image" />
          <view class="share-tips"></view>
          <view class="save-btn" bindtap="handlePhotoSaved" data-ptpid="4095-16fd-bc97-4868"></view>
        </view>
      </view>
      <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" widthPixels="1000" />
      <dialog-modal isShow="{{isModal}}" title="提示" content="您未开启保存图片到相册的权限,请点击确定去开启权限!" confirmType="openSetting" />
    
  • components/shareBox/index.json

      {
        "component": true,
        "usingComponents": {
          "painter": "../painter/painter",
          "dialog-modal": "../dialogModal/index"
        }
      }
    
  • components/shareBox/index.js

      Component({
        properties: {
          //属性值可以在组件使用时指定
          isCanDraw: {
            type: Boolean,
            value: false,
            observer(newVal, oldVal) {
              newVal && this.drawPic()
            }
          },
          detailObj: {
              type: Object,
              value: {},
            }
        },
        data: {
          isModal: false, //是否显示拒绝保存图片后的弹窗
          imgDraw: {}, //绘制图片的大对象
          sharePath: '', //生成的分享图
          visible: false
        },
        lifetimes: {
          attached() {
          }
        },
        methods: {
          handlePhotoSaved() {
            this.savePhoto(this.data.sharePath)
          },
          handleClose() {
            this.setData({
              visible: false
            })
          },
          drawPic() {
          const detailObj= this.properties.detailObj
            if (this.data.sharePath) { //如果已经绘制过了本地保存有图片不需要重新绘制
              this.setData({
                visible: true
              })
              this.triggerEvent('initData')
              return
            }
            wx.showLoading({
              title: '生成中'
            })
            const result = detailObj.result[0];
          const list = result.labelList;
          const newViews = [];
          for(let index = 0; index < list.length ; index ++) {
              newViews.push({
                  type: 'text',
                  text: `${list[index]}`,
                  css: [{
                      color: '#e90820',
                      top: '1160rpx',
                      left: `${50 + 120 * index}rpx`,
                      fontSize: '20rpx',
                      borderRadius: '6rpx',
                      padding: '4px',
                      background: '#f3e6e6',
                  }],
              })
          }
    
            this.setData({
              imgDraw: {
                  width: '654rpx',
                  height: '1390rpx',
                  background: 'https://broker.dissai.com.cn/h5/img/fission-bg.png',
                  views: [
                    {
                        type: 'rect',
                        css: {
                            width: '610rpx',
                            height: '990rpx',
                            top: '220rpx',
                            right: '20rpx',
                            color: '#fff',
                            borderRadius: '20rpx',
                          },
                    },
                    {
                        type: 'image',
                        url: `${result.imgpath}`,
                        css: {
                            width: '610rpx',
                            height: '750rpx',
                          top: '220rpx',
                            right: '20rpx',
                            scalable: true,
                        },
                    },
                    {
                        type: 'text',
                        text:  `${result.city} | ${result.propertyName}`,
                        css: [{
                            color: '#fff',
                            top: '920rpx',
                            left: '40rpx',
                            fontSize: '24rpx',
                        }],
                      },
                      {
                        type: 'text',
                        text: `${result.type === '新房' ?   result.propertyName : result.propertyIntro ? result.propertyIntro : result.propertyName}`,
                        css: [{
                            color: '#000',
                            top: '990rpx',
                            left: '40rpx',
                            fontSize: '26rpx',
                            width: '500rpx',
                            maxLines: 1,
                        }],
                      },
                      {
                        type: 'text',
                        text: `${result.type === '新房' ? result.state : ''}`,
                        css: [{
                            color: '#e90820',
                            top: '990rpx',
                            right: '40rpx',
                            fontSize: '24rpx',
                        }],
                      },
                      {
                        type: 'text',
                        text: `${result.type === '新房' ? result.propertyType  +'  |  '+result.mianJiMin +' - '+ result.mianJiMax +'㎡  |  '+ result.juShiMin+ ' / '+result.juShiMax+ '居' :  result.chamber + `室 `+ result.office  || 0  + `厅 `+ result.defend || 0  + `卫 `}`,
                        css: [{
                            color: '#333',
                            top: '1050rpx',
                            left: '40rpx',
                            fontSize: '24rpx',
                        }],
                      },
                      {
                        type: 'text',
                        text:`${(result.averagePrice == 0 || result.allPriceMin == 0) ? '待定' : result.mobileShowType === 1 ? result.averagePrice : result.allPriceMin  }`,
                        css: [{
                            color: '#e90820',
                            top: '1100rpx',
                            left: '40rpx',
                            fontSize: '26rpx',
                        }],
                      },
                      {
                        type: 'text',
                        text: `${result.mobileShowType === 1 ? ' 元/㎡均价' : ' 万元起'}`,
                        css: [{
                            color: '#e90820',
                            top: '1100rpx',
                            left: '90rpx',
                            fontSize: '22rpx',
                        }],
                      },
                      ...newViews,
                    {
                        type: 'image',
                        url: `${detailObj.broker.headurl}`,
                        css: {
                            bottom: '40rpx',
                            left: '40rpx',
                            borderRadius: '100rpx',
                            borderWidth: '4rpx',
                            borderColor: '#fff',
                            width: '96rpx',
                            height: '96rpx',
                        },
                    },
                    {
                        type: 'text',
                        text: `${detailObj.broker.name}为您推荐`,
                        css: [{
                            color: '#fff',
                            bottom: '100rpx',
                            left: '160rpx',
                            fontSize: '32rpx',
                        }],
                      },
                      {
                        type: 'image',
                        url: 'https://broker.dissai.com.cn/h5/img/phone.png',
                        css: {
                            bottom: '50rpx',
                            left: '160rpx',
                            width: '28rpx',
                            height: '28rpx',
                        },
                    },
                      {
                        type: 'text',
                        text: `点击拨打 ${detailObj.broker.phone}`,
                        css: [{
                            color: '#fff',
                            bottom: '50rpx',
                            left: '200rpx',
                            fontSize: '26rpx',
                            fontWight: 'bold'
                        }],
                      },
                      {
                          type: 'image',
                          url: 'https://broker.dissai.com.cn/h5/img/code.jpg',
                          css: {
                              bottom: '40rpx',
                              right: '40rpx',
                              width: '100rpx',
                              height: '100rpx',
                          },
                      },
                  ],
              }
            })
          },
          onImgErr(e) {
            wx.hideLoading()
            wx.showToast({
              title: '生成分享图失败,请刷新页面重试'
            })
          },
          onImgOK(e) {
            wx.hideLoading()
            this.setData({
              sharePath: e.detail.path,
              visible: true,
            })
            //通知外部绘制完成,重置isCanDraw为false
            this.triggerEvent('initData')
          },
          preventDefault() { },
          // 保存图片
          savePhoto(path) {
            wx.showLoading({
              title: '正在保存...',
              mask: true
            })
            this.setData({
              isDrawImage: false
            })
            wx.saveImageToPhotosAlbum({
              filePath: path,
              success: (res) => {
                wx.showToast({
                  title: '保存成功',
                  icon: 'none'
                })
                setTimeout(() => {
                  this.setData({
                    visible: false
                  })
                }, 300)
              },
              fail: (res) => {
                wx.getSetting({
                  success: res => {
                    let authSetting = res.authSetting
                    if (!authSetting['scope.writePhotosAlbum']) {
                      this.setData({
                        isModal: true
                      })
                    }
                  }
                })
                setTimeout(() => {
                  wx.hideLoading()
                  this.setData({
                    visible: false
                  })
                }, 300)
              }
            })
          }
        }
      })
    
  • components/shareBox/index.wxss

      .share-wrap {
        width: 100%;
      }
    
      .share-back {
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.6);
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 888;
      }
    
      .share-container {
        width: 100%;
        background: #FFF;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 999;
      }
    
      .close {
        width: 30rpx;
        height: 30rpx;
        overflow: hidden;
        position: absolute;
        right: 64rpx;
        top: 64rpx;
      }
    
      .close::after {
        transform: rotate(-45deg);
      }
    
      .close::before {
        transform: rotate(45deg);
      }
    
      .close::before,
      .close::after {
        content: '';
        position: absolute;
        height: 3rpx;
        width: 100%;
        top: 50%;
        left: 0;
        margin-top: -2rpx;
        background: #9C9C9C;
      }
    
      .share-image {
        width: 420rpx;
        margin: 66rpx auto 0;
        display: block;
        border-radius: 16rpx;
        box-shadow: 0px 4rpx 8px 0px rgba(0, 0, 0, 0.1);
      }
    
      .share-tips {
        width: 100%;
        text-align: center;
        color: #3C3C3C;
        font-size: 28rpx;
        margin: 32rpx 0;
        height: 40rpx;
      }
    
      .save-btn {
        width: 336rpx;
        height: 96rpx;
        margin: 0 auto 94rpx;
        background: url('https://qiniu-image.qtshe.com/20190506save-btn.png') center center;
        background-size: 100% 100%;
      }
    
  • components/dialogModal/index.wxml

      <view class="container" wx:if="{{isShow}}" catchtouchmove="preventTouchMove">
        <view class="back-model"></view>
        <view class="conent-model">
          <text class="title">{{title}}</text>
          <text class="content">{{content}}</text>
          <view class="quickBtn">
            <button class="cancel-btn" open-type="{{cancelType}}" bindtap="cancel">{{cancelText}}</button>
            <button class="confirm-btn" open-type="{{confirmType}}" bindtap="confirm">{{confirmText}}</button>
          </view>
        </view>
      </view>
    
  • components/dialogModal/index.json

      {
        "component": true,
        "usingComponents": {}
      }
    
  • components/dialogModal/index.js

      var app = getApp()
      Component({
        data: {
    
        },
        properties: {
          isShow: {
            type: Boolean,
            value: false
          },
          title: {
            type: String,
            value: '提示'
          },
          content: {
            type: String,
            value: ''
          },
          cancelText: {
            type: String,
            value: '取消'
          },
          confirmText: {
            type: String,
            value: '确定'
          },
          isNeedAuth: {
            type: Boolean,
            value: false
          },
          cancelType: {
            type: String,
            value: ''
          },
          confirmType: {
            type: String,
            value: ''
          }
        },
        methods: {
          preventTouchMove() { },
          cancel() {
            this.setData({
              isShow: false
            })
            this.triggerEvent('cancel')
          },
          confirm() {
            this.setData({
              isShow: false
            })
            this.triggerEvent('confirm')
          }
        }
      })
    
  • components/dialogModal/index.wxss

      .container {
        width: 100%;
        height: 100%;
      }
    
      .back-model {
        width: 100%;
        height: 100%;
        position: fixed;
        z-index: 999;
        background-color: rgba(0, 0, 0, 0.6);
        top: 0;
      }
    
      .conent-model {
        position: fixed;
        left: 50%;
        top: 50%;
        width: 622rpx;
        margin-left: -311rpx;
        margin-top: -200rpx;
        z-index: 999;
        background: #fff;
        border-radius: 8rpx;
        padding-top: 32rpx;
      }
    
      .title {
        display: block;
        text-align: center;
        font-size: 36rpx;
        color: #3c3c3c;
      }
    
      .content {
        display: block;
        text-align: center;
        font-size: 30rpx;
        padding: 32rpx;
        color: #999;
      }
    
      .quickBtn {
        width: 100%;
        height: 96rpx;
        border-top: 2rpx solid #EEE;
        line-height: 96rpx;
      }
    
      .cancel-btn {
        width: 50%;
        display: inline-block;
        color: #3c3c3c;
        font-size: 32rpx;
        text-align: center;
        height: 96rpx;
        line-height: 96rpx;
        border-right: 1rpx solid #EEE;
      }
    
      .confirm-btn {
        width: 50%;
        display: inline-block;
        color: #00cc88;
        font-size: 32rpx;
        height: 96rpx;
        line-height: 96rpx;
        text-align: center;
        border-left: 1rpx solid #EEE;
      }
    
  • components/painter/painter.wxml

      <view style='position: relative;{{customStyle}};{{painterStyle}}'>
        <block wx:if="{{!use2D}}">
          <canvas canvas-id="photo" style="{{photoStyle}};position: absolute; left: -9999px; top: -9999rpx;" />
          <block wx:if="{{dancePalette}}">
            <canvas canvas-id="bottom" style="{{painterStyle}};position: absolute;" />
            <canvas canvas-id="k-canvas" style="{{painterStyle}};position: absolute;" />
            <canvas canvas-id="top" style="{{painterStyle}};position: absolute;" />
            <canvas
              canvas-id="front"
              style="{{painterStyle}};position: absolute;"
              bindtouchstart="onTouchStart"
              bindtouchmove="onTouchMove"
              bindtouchend="onTouchEnd"
              bindtouchcancel="onTouchCancel"
              disable-scroll="{{true}}" />
            </block>
        </block>
        <block wx:if="{{use2D}}">
          <canvas type="2d" id="photo" style="{{photoStyle}};" />
        </block>
      </view>
    
  • components/painter/painter.json

      {
        "component": true,
        "usingComponents": {}
      }
    
  • components/painter/painter.js

      import Pen, { penCache, clearPenCache } from './lib/pen';
      import Downloader from './lib/downloader';
      import WxCanvas from './lib/wx-canvas';
    
      const util = require('./lib/util');
      const calc = require('./lib/calc');
    
      const downloader = new Downloader();
    
      // 最大尝试的绘制次数
      const MAX_PAINT_COUNT = 5;
      const ACTION_DEFAULT_SIZE = 24;
      const ACTION_OFFSET = '2rpx';
      Component({
        canvasWidthInPx: 0,
        canvasHeightInPx: 0,
        canvasNode: null,
        paintCount: 0,
        currentPalette: {},
        outterDisabled: false,
        isDisabled: false,
        needClear: false,
        /**
        * 组件的属性列表
        */
        properties: {
          use2D: {
            type: Boolean,
          },
          customStyle: {
            type: String,
          },
          // 运行自定义选择框和删除缩放按钮
          customActionStyle: {
            type: Object,
          },
          palette: {
            type: Object,
            observer: function (newVal, oldVal) {
              if (this.isNeedRefresh(newVal, oldVal)) {
                this.paintCount = 0;
                clearPenCache();
                this.startPaint();
              }
            },
          },
          dancePalette: {
            type: Object,
            observer: function (newVal, oldVal) {
              if (!this.isEmpty(newVal) && !this.properties.use2D) {
                clearPenCache();
                this.initDancePalette(newVal);
              }
            },
          },
          // 缩放比,会在传入的 palette 中统一乘以该缩放比
          scaleRatio: {
            type: Number,
            value: 1,
          },
          widthPixels: {
            type: Number,
            value: 0,
          },
          // 启用脏检查,默认 false
          dirty: {
            type: Boolean,
            value: false,
          },
          LRU: {
            type: Boolean,
            value: false,
          },
          action: {
            type: Object,
            observer: function (newVal, oldVal) {
              if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
                this.doAction(newVal, null, false, true);
              }
            },
          },
          disableAction: {
            type: Boolean,
            observer: function (isDisabled) {
              this.outterDisabled = isDisabled;
              this.isDisabled = isDisabled;
            },
          },
          clearActionBox: {
            type: Boolean,
            observer: function (needClear) {
              if (needClear && !this.needClear) {
                if (this.frontContext) {
                  setTimeout(() => {
                    this.frontContext.draw();
                  }, 100);
                  this.touchedView = {};
                  this.prevFindedIndex = this.findedIndex;
                  this.findedIndex = -1;
                }
              }
              this.needClear = needClear;
            },
          },
        },
    
        data: {
          picURL: '',
          showCanvas: true,
          painterStyle: '',
        },
    
        methods: {
          /**
          * 判断一个 object 是否为 空
          * @param {object} object
          */
          isEmpty(object) {
            for (const i in object) {
              return false;
            }
            return true;
          },
    
          isNeedRefresh(newVal, oldVal) {
            if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
              return false;
            }
            return true;
          },
    
          getBox(rect, type) {
            const boxArea = {
              type: 'rect',
              css: {
                height: `${rect.bottom - rect.top}px`,
                width: `${rect.right - rect.left}px`,
                left: `${rect.left}px`,
                top: `${rect.top}px`,
                borderWidth: '4rpx',
                borderColor: '#1A7AF8',
                color: 'transparent',
              },
            };
            if (type === 'text') {
              boxArea.css = Object.assign({}, boxArea.css, {
                borderStyle: 'dashed',
              });
            }
            if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
              boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border);
            }
            Object.assign(boxArea, {
              id: 'box',
            });
            return boxArea;
          },
    
          getScaleIcon(rect, type) {
            let scaleArea = {};
            const { customActionStyle } = this.properties;
            if (customActionStyle && customActionStyle.scale) {
              scaleArea = {
                type: 'image',
                url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
                css: {
                  height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
                },
              };
            } else {
              scaleArea = {
                type: 'rect',
                css: {
                  height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
                  color: '#0000ff',
                },
              };
            }
            scaleArea.css = Object.assign({}, scaleArea.css, {
              align: 'center',
              left: `${rect.right + ACTION_OFFSET.toPx()}px`,
              top:
                type === 'text'
                  ? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
                  : `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`,
            });
            Object.assign(scaleArea, {
              id: 'scale',
            });
            return scaleArea;
          },
    
          getDeleteIcon(rect) {
            let deleteArea = {};
            const { customActionStyle } = this.properties;
            if (customActionStyle && customActionStyle.scale) {
              deleteArea = {
                type: 'image',
                url: customActionStyle.delete.icon,
                css: {
                  height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
                },
              };
            } else {
              deleteArea = {
                type: 'rect',
                css: {
                  height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
                  borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
                  color: '#0000ff',
                },
              };
            }
            deleteArea.css = Object.assign({}, deleteArea.css, {
              align: 'center',
              left: `${rect.left - ACTION_OFFSET.toPx()}px`,
              top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`,
            });
            Object.assign(deleteArea, {
              id: 'delete',
            });
            return deleteArea;
          },
    
          doAction(action, callback, isMoving, overwrite) {
            if (this.properties.use2D) {
              return;
            }
            let newVal = null;
            if (action) {
              newVal = action.view;
            }
            if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
              // 带 id 的动作给撤回时使用,不带 id,表示对当前选中对象进行操作
              const { views } = this.currentPalette;
              for (let i = 0; i < views.length; i++) {
                if (views[i].id === newVal.id) {
                  // 跨层回撤,需要重新构建三层关系
                  this.touchedView = views[i];
                  this.findedIndex = i;
                  this.sliceLayers();
                  break;
                }
              }
            }
    
            const doView = this.touchedView;
    
            if (!doView || this.isEmpty(doView)) {
              return;
            }
            if (newVal && newVal.css) {
              if (overwrite) {
                doView.css = newVal.css;
              } else if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
                doView.css = Object.assign({}, ...doView.css, ...newVal.css);
              } else if (Array.isArray(doView.css)) {
                doView.css = Object.assign({}, ...doView.css, newVal.css);
              } else if (Array.isArray(newVal.css)) {
                doView.css = Object.assign({}, doView.css, ...newVal.css);
              } else {
                doView.css = Object.assign({}, doView.css, newVal.css);
              }
            }
            if (newVal && newVal.rect) {
              doView.rect = newVal.rect;
            }
            if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
              downloader
                .download(newVal.url, this.properties.LRU)
                .then(path => {
                  if (newVal.url.startsWith('https')) {
                    doView.originUrl = newVal.url;
                  }
                  doView.url = path;
                  wx.getImageInfo({
                    src: path,
                    success: res => {
                      doView.sHeight = res.height;
                      doView.sWidth = res.width;
                      this.reDraw(doView, callback, isMoving);
                    },
                    fail: () => {
                      this.reDraw(doView, callback, isMoving);
                    },
                  });
                })
                .catch(error => {
                  // 未下载成功,直接绘制
                  console.error(error);
                  this.reDraw(doView, callback, isMoving);
                });
            } else {
              newVal && newVal.text && doView.text && newVal.text !== doView.text && (doView.text = newVal.text);
              newVal &&
                newVal.content &&
                doView.content &&
                newVal.content !== doView.content &&
                (doView.content = newVal.content);
              this.reDraw(doView, callback, isMoving);
            }
          },
    
          reDraw(doView, callback, isMoving) {
            const draw = {
              width: this.currentPalette.width,
              height: this.currentPalette.height,
              views: this.isEmpty(doView) ? [] : [doView],
            };
            const pen = new Pen(this.globalContext, draw);
    
            pen.paint(callbackInfo => {
              callback && callback(callbackInfo);
              this.triggerEvent('viewUpdate', {
                view: this.touchedView,
              });
            });
    
            const { rect, css, type } = doView;
    
            this.block = {
              width: this.currentPalette.width,
              height: this.currentPalette.height,
              views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)],
            };
            if (css && css.scalable) {
              this.block.views.push(this.getScaleIcon(rect, type));
            }
            if (css && css.deletable) {
              this.block.views.push(this.getDeleteIcon(rect));
            }
            const topBlock = new Pen(this.frontContext, this.block);
            topBlock.paint();
          },
    
          isInView(x, y, rect) {
            return x > rect.left && y > rect.top && x < rect.right && y < rect.bottom;
          },
    
          isInDelete(x, y) {
            for (const view of this.block.views) {
              if (view.id === 'delete') {
                return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
              }
            }
            return false;
          },
    
          isInScale(x, y) {
            for (const view of this.block.views) {
              if (view.id === 'scale') {
                return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
              }
            }
            return false;
          },
    
          touchedView: {},
          findedIndex: -1,
          onClick() {
            const x = this.startX;
            const y = this.startY;
            const totalLayerCount = this.currentPalette.views.length;
            let canBeTouched = [];
            let isDelete = false;
            let deleteIndex = -1;
            for (let i = totalLayerCount - 1; i >= 0; i--) {
              const view = this.currentPalette.views[i];
              const { rect } = view;
              if (this.touchedView && this.touchedView.id && this.touchedView.id === view.id && this.isInDelete(x, y, rect)) {
                canBeTouched.length = 0;
                deleteIndex = i;
                isDelete = true;
                break;
              }
              if (this.isInView(x, y, rect)) {
                canBeTouched.push({
                  view,
                  index: i,
                });
              }
            }
            this.touchedView = {};
            if (canBeTouched.length === 0) {
              this.findedIndex = -1;
            } else {
              let i = 0;
              const touchAble = canBeTouched.filter(item => Boolean(item.view.id));
              if (touchAble.length === 0) {
                this.findedIndex = canBeTouched[0].index;
              } else {
                for (i = 0; i < touchAble.length; i++) {
                  if (this.findedIndex === touchAble[i].index) {
                    i++;
                    break;
                  }
                }
                if (i === touchAble.length) {
                  i = 0;
                }
                this.touchedView = touchAble[i].view;
                this.findedIndex = touchAble[i].index;
                this.triggerEvent('viewClicked', {
                  view: this.touchedView,
                });
              }
            }
            if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
              // 证明点击了背景 或无法移动的view
              this.frontContext.draw();
              if (isDelete) {
                this.triggerEvent('touchEnd', {
                  view: this.currentPalette.views[deleteIndex],
                  index: deleteIndex,
                  type: 'delete',
                });
                this.doAction();
              } else if (this.findedIndex < 0) {
                this.triggerEvent('viewClicked', {});
              }
              this.findedIndex = -1;
              this.prevFindedIndex = -1;
            } else if (this.touchedView && this.touchedView.id) {
              this.sliceLayers();
            }
          },
    
          sliceLayers() {
            const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex);
            const topLayers = this.currentPalette.views.slice(this.findedIndex + 1);
            const bottomDraw = {
              width: this.currentPalette.width,
              height: this.currentPalette.height,
              background: this.currentPalette.background,
              views: bottomLayers,
            };
            const topDraw = {
              width: this.currentPalette.width,
              height: this.currentPalette.height,
              views: topLayers,
            };
            if (this.prevFindedIndex < this.findedIndex) {
              new Pen(this.bottomContext, bottomDraw).paint();
              this.doAction();
              new Pen(this.topContext, topDraw).paint();
            } else {
              new Pen(this.topContext, topDraw).paint();
              this.doAction();
              new Pen(this.bottomContext, bottomDraw).paint();
            }
            this.prevFindedIndex = this.findedIndex;
          },
    
          startX: 0,
          startY: 0,
          startH: 0,
          startW: 0,
          isScale: false,
          startTimeStamp: 0,
          onTouchStart(event) {
            if (this.isDisabled) {
              return;
            }
            const { x, y } = event.touches[0];
            this.startX = x;
            this.startY = y;
            this.startTimeStamp = new Date().getTime();
            if (this.touchedView && !this.isEmpty(this.touchedView)) {
              const { rect } = this.touchedView;
              if (this.isInScale(x, y, rect)) {
                this.isScale = true;
                this.startH = rect.bottom - rect.top;
                this.startW = rect.right - rect.left;
              } else {
                this.isScale = false;
              }
            } else {
              this.isScale = false;
            }
          },
    
          onTouchEnd(e) {
            if (this.isDisabled) {
              return;
            }
            const current = new Date().getTime();
            if (current - this.startTimeStamp <= 500 && !this.hasMove) {
              !this.isScale && this.onClick(e);
            } else if (this.touchedView && !this.isEmpty(this.touchedView)) {
              this.triggerEvent('touchEnd', {
                view: this.touchedView,
              });
            }
            this.hasMove = false;
          },
    
          onTouchCancel(e) {
            if (this.isDisabled) {
              return;
            }
            this.onTouchEnd(e);
          },
    
          hasMove: false,
          onTouchMove(event) {
            if (this.isDisabled) {
              return;
            }
            this.hasMove = true;
            if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
              return;
            }
            const { x, y } = event.touches[0];
            const offsetX = x - this.startX;
            const offsetY = y - this.startY;
            const { rect, type } = this.touchedView;
            let css = {};
            if (this.isScale) {
              clearPenCache(this.touchedView.id);
              const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1;
              if (this.touchedView.css && this.touchedView.css.minWidth) {
                if (newW < this.touchedView.css.minWidth.toPx()) {
                  return;
                }
              }
              if (this.touchedView.rect && this.touchedView.rect.minWidth) {
                if (newW < this.touchedView.rect.minWidth) {
                  return;
                }
              }
              const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1;
              css = {
                width: `${newW}px`,
              };
              if (type !== 'text') {
                if (type === 'image') {
                  css.height = `${(newW * this.startH) / this.startW}px`;
                } else {
                  css.height = `${newH}px`;
                }
              }
            } else {
              this.startX = x;
              this.startY = y;
              css = {
                left: `${rect.x + offsetX}px`,
                top: `${rect.y + offsetY}px`,
                right: undefined,
                bottom: undefined,
              };
            }
            this.doAction(
              {
                view: {
                  css,
                },
              },
              null,
              !this.isScale,
            );
          },
    
          initScreenK() {
            if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
              try {
                // getApp().systemInfo = wx.getSystemInfoSync();
                getApp().systemInfo = wx.getAppAuthorizeSetting();
              } catch (e) {
                console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
                return;
              }
            }
            this.screenK = 0.5;
            if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
              this.screenK = getApp().systemInfo.screenWidth / 750;
            }
            setStringPrototype(this.screenK, this.properties.scaleRatio);
          },
    
          initDancePalette() {
            if (this.properties.use2D) {
              return;
            }
            this.isDisabled = true;
            this.initScreenK();
            this.downloadImages(this.properties.dancePalette).then(async palette => {
              this.currentPalette = palette;
              const { width, height } = palette;
    
              if (!width || !height) {
                console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
                return;
              }
              this.setData({
                painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
              });
              this.frontContext || (this.frontContext = await this.getCanvasContext(this.properties.use2D, 'front'));
              this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
              this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
              this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
              new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
                this.isDisabled = false;
                this.isDisabled = this.outterDisabled;
                this.triggerEvent('didShow');
              });
              this.globalContext.draw();
              this.frontContext.draw();
              this.topContext.draw();
            });
            this.touchedView = {};
          },
    
          startPaint() {
            this.initScreenK();
            const { width, height } = this.properties.palette;
    
            if (!width || !height) {
              console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
              return;
            }
    
            let needScale = false;
            // 生成图片时,根据设置的像素值重新绘制
            if (width.toPx() !== this.canvasWidthInPx) {
              this.canvasWidthInPx = width.toPx();
              needScale = this.properties.use2D;
            }
            if (this.properties.widthPixels) {
              setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx);
              this.canvasWidthInPx = this.properties.widthPixels;
            }
    
            if (this.canvasHeightInPx !== height.toPx()) {
              this.canvasHeightInPx = height.toPx();
              needScale = needScale || this.properties.use2D;
            }
            this.setData(
              {
                photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
              },
              function () {
                this.downloadImages(this.properties.palette).then(async palette => {
                  if (!this.photoContext) {
                    this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
                  }
                  if (needScale) {
                    const scale = getApp().systemInfo.pixelRatio;
                    this.photoContext.width = this.canvasWidthInPx * scale;
                    this.photoContext.height = this.canvasHeightInPx * scale;
                    this.photoContext.scale(scale, scale);
                  }
                  new Pen(this.photoContext, palette).paint(() => {
                    this.saveImgToLocal();
                  });
                  setStringPrototype(this.screenK, this.properties.scaleRatio);
                });
              },
            );
          },
    
          downloadImages(palette) {
            return new Promise((resolve, reject) => {
              let preCount = 0;
              let completeCount = 0;
              const paletteCopy = JSON.parse(JSON.stringify(palette));
              if (paletteCopy.background) {
                preCount++;
                downloader.download(paletteCopy.background, this.properties.LRU).then(
                  path => {
                    paletteCopy.background = path;
                    completeCount++;
                    if (preCount === completeCount) {
                      resolve(paletteCopy);
                    }
                  },
                  () => {
                    completeCount++;
                    if (preCount === completeCount) {
                      resolve(paletteCopy);
                    }
                  },
                );
              }
              if (paletteCopy.views) {
                for (const view of paletteCopy.views) {
                  if (view && view.type === 'image' && view.url) {
                    preCount++;
                    /* eslint-disable no-loop-func */
                    downloader.download(view.url, this.properties.LRU).then(
                      path => {
                        view.originUrl = view.url;
                        view.url = path;
                        wx.getImageInfo({
                          src: path,
                          success: res => {
                            // 获得一下图片信息,供后续裁减使用
                            view.sWidth = res.width;
                            view.sHeight = res.height;
                          },
                          fail: error => {
                            // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
                            console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
                            view.url = '';
                          },
                          complete: () => {
                            completeCount++;
                            if (preCount === completeCount) {
                              resolve(paletteCopy);
                            }
                          },
                        });
                      },
                      () => {
                        completeCount++;
                        if (preCount === completeCount) {
                          resolve(paletteCopy);
                        }
                      },
                    );
                  }
                }
              }
              if (preCount === 0) {
                resolve(paletteCopy);
              }
            });
          },
    
          saveImgToLocal() {
            const that = this;
            const optionsOf2d = {
              canvas: that.canvasNode,
            }
            const optionsOfOld = {
              canvasId: 'photo',
              destWidth: that.canvasWidthInPx,
              destHeight: that.canvasHeightInPx,
            }
            setTimeout(() => {
              wx.canvasToTempFilePath(
                {
                  ...(that.properties.use2D ? optionsOf2d : optionsOfOld),
                  success: function (res) {
                    that.getImageInfo(res.tempFilePath);
                  },
                  fail: function (error) {
                    console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
                    that.triggerEvent('imgErr', {
                      error: error,
                    });
                  },
                },
                this,
              );
            }, 300);
          },
    
          getCanvasContext(use2D, id) {
            const that = this;
            return new Promise(resolve => {
              if (use2D) {
                const query = wx.createSelectorQuery().in(that);
                const selectId = `#${id}`;
                query
                  .select(selectId)
                  .fields({ node: true, size: true })
                  .exec(res => {
                    that.canvasNode = res[0].node;
                    const ctx = that.canvasNode.getContext('2d');
                    const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
                    resolve(wxCanvas);
                  });
              } else {
                const temp = wx.createCanvasContext(id, that);
                resolve(new WxCanvas('mina', temp, id, true));
              }
            });
          },
    
          getImageInfo(filePath) {
            const that = this;
            wx.getImageInfo({
              src: filePath,
              success: infoRes => {
                if (that.paintCount > MAX_PAINT_COUNT) {
                  const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
                  console.error(error);
                  that.triggerEvent('imgErr', {
                    error: error,
                  });
                  return;
                }
                // 比例相符时才证明绘制成功,否则进行强制重绘制
                if (
                  Math.abs(
                    (infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) /
                      (infoRes.height * that.canvasHeightInPx),
                  ) < 0.01
                ) {
                  that.triggerEvent('imgOK', {
                    path: filePath,
                  });
                } else {
                  that.startPaint();
                }
                that.paintCount++;
              },
              fail: error => {
                console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
                that.triggerEvent('imgErr', {
                  error: error,
                });
              },
            });
          },
        },
      });
    
      function setStringPrototype(screenK, scale) {
        /* eslint-disable no-extend-native */
        /**
        * string 到对应的 px
        * @param {Number} baseSize 当设置了 % 号时,设置的基准值
        */
        String.prototype.toPx = function toPx(_, baseSize) {
          if (this === '0') {
            return 0;
          }
          const REG = /-?[0-9]+(\.[0-9]+)?(rpx|px|%)/;
    
          const parsePx = origin => {
            const results = new RegExp(REG).exec(origin);
            if (!origin || !results) {
              console.error(`The size: ${origin} is illegal`);
              return 0;
            }
            const unit = results[2];
            const value = parseFloat(origin);
    
            let res = 0;
            if (unit === 'rpx') {
              res = Math.round(value * (screenK || 0.5) * (scale || 1));
            } else if (unit === 'px') {
              res = Math.round(value * (scale || 1));
            } else if (unit === '%') {
              res = Math.round((value * baseSize) / 100);
            }
            return res;
          };
          const formula = /^calc\((.+)\)$/.exec(this);
          if (formula && formula[1]) {
            // 进行 calc 计算
            const afterOne = formula[1].replace(/([^\s\(\+\-\*\/]+)\.(left|right|bottom|top|width|height)/g, word => {
              const [id, attr] = word.split('.');
              return penCache.viewRect[id][attr];
            });
            const afterTwo = afterOne.replace(new RegExp(REG, 'g'), parsePx);
            return calc(afterTwo);
          } else {
            return parsePx(this);
          }
        };
      }
    
  • components/painter/lib/calc.js

      /* eslint-disable */
      // 四则运算
    
      !(function () {
        var calculate = function (s) {
          s = s.trim();
          const stack = new Array();
          let preSign = '+';
          let numStr = '';
          const n = s.length;
          for (let i = 0; i < n; ++i) {
            if (s[i] === '.' || (!isNaN(Number(s[i])) && s[i] !== ' ')) {
              numStr += s[i];
            } else if (s[i] === '(') {
              let isClose = 1;
              let j = i;
              while (isClose > 0) {
                j += 1;
                if (s[j] === '(') isClose += 1;
                if (s[j] === ')') isClose -= 1;
              }
              numStr = `${calculate(s.slice(i + 1, j))}`;
              i = j;
            }
            if ((isNaN(Number(s[i])) && s[i] !== '.') || i === n - 1) {
              let num = parseFloat(numStr);
              switch (preSign) {
                case '+':
                  stack.push(num);
                  break;
                case '-':
                  stack.push(-num);
                  break;
                case '*':
                  stack.push(stack.pop() * num);
                  break;
                case '/':
                  stack.push(stack.pop() / num);
                  break;
                default:
                  break;
              }
              preSign = s[i];
              numStr = '';
            }
          }
          let ans = 0;
          while (stack.length) {
            ans += stack.pop();
          }
          return ans;
        };
        module.exports = calculate;
      })();
    
  • components/painter/lib/downloader.js

      /**
      * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
      * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
      */
      const util = require('./util');
      const sha1 = require('./sha1');
    
      const SAVED_FILES_KEY = 'savedFiles';
      const KEY_TOTAL_SIZE = 'totalSize';
      const KEY_PATH = 'path';
      const KEY_TIME = 'time';
      const KEY_SIZE = 'size';
    
      // 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
      let MAX_SPACE_IN_B = 6 * 1024 * 1024;
      let savedFiles = {};
    
      export default class Dowloader {
        constructor() {
          // app 如果设置了最大存储空间,则使用 app 中的
          if (getApp().PAINTER_MAX_LRU_SPACE) {
            MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
          }
          wx.getStorage({
            key: SAVED_FILES_KEY,
            success: function (res) {
              if (res.data) {
                savedFiles = res.data;
              }
            },
          });
        }
    
        /**
        * 下载文件,会用 lru 方式来缓存文件到本地
        * @param {String} url 文件的 url
        */
        download(url, lru) {
          return new Promise((resolve, reject) => {
            if (!(url && util.isValidUrl(url))) {
              resolve(url);
              return;
            }
            const fileName = getFileName(url);
            if (!lru) {
              // 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
              wx.getFileInfo({
                filePath: fileName,
                success: () => {
                  resolve(url);
                },
                fail: () => {
                  if (util.isOnlineUrl(url)) {
                    downloadFile(url, lru).then((path) => {
                      resolve(path);
                    }, () => {
                      reject();
                    });
                  } else if (util.isDataUrl(url)) {
                    transformBase64File(url, lru).then(path => {
                      resolve(path);
                    }, () => {
                      reject();
                    });
                  }
                },
              })
              return
            }
    
            const file = getFile(fileName);
    
            if (file) {
              if (file[KEY_PATH].indexOf('//usr/') !== -1) {
                wx.getFileInfo({
                  filePath: file[KEY_PATH],
                  success() {
                    resolve(file[KEY_PATH]);
                  },
                  fail(error) {
                    console.error(`base64 file broken, ${JSON.stringify(error)}`);
                    transformBase64File(url, lru).then(path => {
                      resolve(path);
                    }, () => {
                      reject();
                    });
                  }
                })
              } else {
                // 检查文件是否正常,不正常需要重新下载
                wx.getSavedFileInfo({
                  filePath: file[KEY_PATH],
                  success: (res) => {
                    resolve(file[KEY_PATH]);
                  },
                  fail: (error) => {
                    console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
                    downloadFile(url, lru).then((path) => {
                      resolve(path);
                    }, () => {
                      reject();
                    });
                  },
                });
              }
            } else {
              if (util.isOnlineUrl(url)) {
                downloadFile(url, lru).then((path) => {
                  resolve(path);
                }, () => {
                  reject();
                });
              } else if (util.isDataUrl(url)) {
                transformBase64File(url, lru).then(path => {
                  resolve(path);
                }, () => {
                  reject();
                });
              }
            }
          });
        }
      }
    
      function getFileName(url) {
        if (util.isDataUrl(url)) {
          const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
          const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
          return fileName;
        } else {
          return url;
        }
      }
    
      function transformBase64File(base64data, lru) {
        return new Promise((resolve, reject) => {
          const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
          if (!format) {
            console.error('base parse failed');
            reject();
            return;
          }
          const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
          const path = `${wx.env.USER_DATA_PATH}/${fileName}`;
          const buffer = wx.base64ToArrayBuffer(bodyData.replace(/[\r\n]/g, ""));
          wx.getFileSystemManager().writeFile({
            filePath: path,
            data: buffer,
            encoding: 'binary',
            success() {
              wx.getFileInfo({
                filePath: path,
                success: (tmpRes) => {
                  const newFileSize = tmpRes.size;
                  lru ? doLru(newFileSize).then(() => {
                    saveFile(fileName, newFileSize, path, true).then((filePath) => {
                      resolve(filePath);
                    });
                  }, () => {
                    resolve(path);
                  }) : resolve(path);
                },
                fail: (error) => {
                // 文件大小信息获取失败,则此文件也不要进行存储
                  console.error(`getFileInfo ${path} failed, ${JSON.stringify(error)}`);
                  resolve(path);
                },
              });
            },
            fail(err) {
              console.log(err)
            }
          })
        });
      }
    
      function downloadFile(url, lru) {
        return new Promise((resolve, reject) => {
          const downloader = url.startsWith('cloud://')?wx.cloud.downloadFile:wx.downloadFile
          downloader({
            url: url,
            fileID: url,
            success: function (res) {
              if (res.statusCode !== 200) {
                console.error(`downloadFile ${url} failed res.statusCode is not 200`);
                reject();
                return;
              }
              const {
                tempFilePath
              } = res;
              wx.getFileInfo({
                filePath: tempFilePath,
                success: (tmpRes) => {
                  const newFileSize = tmpRes.size;
                  lru ? doLru(newFileSize).then(() => {
                    saveFile(url, newFileSize, tempFilePath).then((filePath) => {
                      resolve(filePath);
                    });
                  }, () => {
                    resolve(tempFilePath);
                  }) : resolve(tempFilePath);
                },
                fail: (error) => {
                  // 文件大小信息获取失败,则此文件也不要进行存储
                  console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
                  resolve(res.tempFilePath);
                },
              });
            },
            fail: function (error) {
              console.error(`downloadFile failed, ${JSON.stringify(error)} `);
              reject();
            },
          });
        });
      }
    
      function saveFile(key, newFileSize, tempFilePath, isDataUrl = false) {
        return new Promise((resolve, reject) => {
          if (isDataUrl) {
            const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
            savedFiles[key] = {};
            savedFiles[key][KEY_PATH] = tempFilePath;
            savedFiles[key][KEY_TIME] = new Date().getTime();
            savedFiles[key][KEY_SIZE] = newFileSize;
            savedFiles['totalSize'] = newFileSize + totalSize;
            wx.setStorage({
              key: SAVED_FILES_KEY,
              data: savedFiles,
            });
            resolve(tempFilePath);
            return;
          }
          wx.saveFile({
            tempFilePath: tempFilePath,
            success: (fileRes) => {
              const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
              savedFiles[key] = {};
              savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
              savedFiles[key][KEY_TIME] = new Date().getTime();
              savedFiles[key][KEY_SIZE] = newFileSize;
              savedFiles['totalSize'] = newFileSize + totalSize;
              wx.setStorage({
                key: SAVED_FILES_KEY,
                data: savedFiles,
              });
              resolve(fileRes.savedFilePath);
            },
            fail: (error) => {
              console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
              // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
              resolve(tempFilePath);
              // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
              reset();
            },
          });
        });
      }
    
      /**
      * 清空所有下载相关内容
      */
      function reset() {
        wx.removeStorage({
          key: SAVED_FILES_KEY,
          success: () => {
            wx.getSavedFileList({
              success: (listRes) => {
                removeFiles(listRes.fileList);
              },
              fail: (getError) => {
                console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
              },
            });
          },
        });
      }
    
      function doLru(size) {
        if (size > MAX_SPACE_IN_B) {
          return Promise.reject()
        }
        return new Promise((resolve, reject) => {
          let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
    
          if (size + totalSize <= MAX_SPACE_IN_B) {
            resolve();
            return;
          }
          // 如果加上新文件后大小超过最大限制,则进行 lru
          const pathsShouldDelete = [];
          // 按照最后一次的访问时间,从小到大排序
          const allFiles = JSON.parse(JSON.stringify(savedFiles));
          delete allFiles[KEY_TOTAL_SIZE];
          const sortedKeys = Object.keys(allFiles).sort((a, b) => {
            return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
          });
    
          for (const sortedKey of sortedKeys) {
            totalSize -= savedFiles[sortedKey].size;
            pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
            delete savedFiles[sortedKey];
            if (totalSize + size < MAX_SPACE_IN_B) {
              break;
            }
          }
    
          savedFiles['totalSize'] = totalSize;
    
          wx.setStorage({
            key: SAVED_FILES_KEY,
            data: savedFiles,
            success: () => {
              // 保证 storage 中不会存在不存在的文件数据
              if (pathsShouldDelete.length > 0) {
                removeFiles(pathsShouldDelete);
              }
              resolve();
            },
            fail: (error) => {
              console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
              reject();
            },
          });
        });
      }
    
      function removeFiles(pathsShouldDelete) {
        for (const pathDel of pathsShouldDelete) {
          let delPath = pathDel;
          if (typeof pathDel === 'object') {
            delPath = pathDel.filePath;
          }
          if (delPath.indexOf('//usr/') !== -1) {
            wx.getFileSystemManager().unlink({
              filePath: delPath,
              fail(error) {
                console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
              }
            })
          } else {
            wx.removeSavedFile({
              filePath: delPath,
              fail: (error) => {
                console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
              },
            });
          }
        }
      }
    
      function getFile(key) {
        if (!savedFiles[key]) {
          return;
        }
        savedFiles[key]['time'] = new Date().getTime();
        wx.setStorage({
          key: SAVED_FILES_KEY,
          data: savedFiles,
        });
        return savedFiles[key];
      }
    
  • components/painter/lib/gradient.js

      /* eslint-disable */
      // 当ctx传入当前文件,const grd = ctx.createCircularGradient() 和
      // const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理
      // 先分析,在外部创建grd,再传入使用就可以
    
      !(function () {
    
        var api = {
          isGradient: function(bg) {
            if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) {
              return true;
            }
            return false;
          },
    
          doGradient: function(bg, width, height, ctx) {
            if (bg.startsWith('linear')) {
              linearEffect(width, height, bg, ctx);
            } else if (bg.startsWith('radial')) {
              radialEffect(width, height, bg, ctx);
            }
          },
        }
    
        function analizeGrad(string) {
          const colorPercents = string.substring(0, string.length - 1).split("%,");
          const colors = [];
          const percents = [];
          for (let colorPercent of colorPercents) {
            colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(" ")).trim());
            percents.push(colorPercent.substring(colorPercent.lastIndexOf(" "), colorPercent.length) / 100);
          }
          return {colors: colors, percents: percents};
        }
    
        function radialEffect(width, height, bg, ctx) {
          const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]);
          const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, width < height ? height / 2 : width / 2);
          for (let i = 0; i < colorPer.colors.length; i++) {
            grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
          }
          ctx.fillStyle = grd;
          //ctx.fillRect(-(width / 2), -(height / 2), width, height);
        }
    
        function analizeLinear(bg, width, height) {
          const direction = bg.match(/([-]?\d{1,3})deg/);
          const dir = direction && direction[1] ? parseFloat(direction[1]) : 0;
          let coordinate;
          switch (dir) {
            case 0: coordinate = [0, -height / 2, 0, height / 2]; break;
            case 90: coordinate = [width / 2, 0, -width / 2, 0]; break;
            case -90: coordinate = [-width / 2, 0, width / 2, 0]; break;
            case 180: coordinate = [0, height / 2, 0, -height / 2]; break;
            case -180: coordinate = [0, -height / 2, 0, height / 2]; break;
            default:
              let x1 = 0;
              let y1 = 0;
              let x2 = 0;
              let y2 = 0;
              if (direction[1] > 0 && direction[1] < 90) {
                x1 = (width / 2) - ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
                y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
                x2 = -x1;
                y1 = -y2;
              } else if (direction[1] > -180 && direction[1] < -90) {
                x1 = -(width / 2) + ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
                y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
                x2 = -x1;
                y1 = -y2;
              } else if (direction[1] > 90 && direction[1] < 180) {
                x1 = (width / 2) + (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
                y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
                x2 = -x1;
                y1 = -y2;
              } else {
                x1 = -(width / 2) - (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
                y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
                x2 = -x1;
                y1 = -y2;
              }
              coordinate = [x1, y1, x2, y2];
            break;
          }
          return coordinate;
        }
    
        function linearEffect(width, height, bg, ctx) {
          const param = analizeLinear(bg, width, height);
          const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]);
          const content = bg.match(/linear-gradient\((.+)\)/)[1];
          const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1));
          for (let i = 0; i < colorPer.colors.length; i++) {
            grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
          }
          ctx.fillStyle = grd
          //ctx.fillRect(-(width / 2), -(height / 2), width, height);
        }
    
        module.exports = { api }
    
      })();
    
  • components/painter/lib/pen.js

      const QR = require('./qrcode.js');
      const GD = require('./gradient.js');
      require('./string-polyfill.js');
    
      export const penCache = {
        // 用于存储带 id 的 view 的 rect 信息
        viewRect: {},
        textLines: {},
      };
      export const clearPenCache = id => {
        if (id) {
          penCache.viewRect[id] = null;
          penCache.textLines[id] = null;
        } else {
          penCache.viewRect = {};
          penCache.textLines = {};
        }
      };
      export default class Painter {
        constructor(ctx, data) {
          this.ctx = ctx;
          this.data = data;
        }
    
        paint(callback) {
          this.style = {
            width: this.data.width.toPx(),
            height: this.data.height.toPx(),
          };
    
          this._background();
          for (const view of this.data.views) {
            this._drawAbsolute(view);
          }
          this.ctx.draw(false, () => {
            callback && callback();
          });
        }
    
        _background() {
          this.ctx.save();
          const { width, height } = this.style;
          const bg = this.data.background;
          this.ctx.translate(width / 2, height / 2);
    
          this._doClip(this.data.borderRadius, width, height);
          if (!bg) {
            // 如果未设置背景,则默认使用透明色
            this.ctx.fillStyle = 'transparent';
            this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
          } else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') {
            // 背景填充颜色
            this.ctx.fillStyle = bg;
            this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
          } else if (GD.api.isGradient(bg)) {
            GD.api.doGradient(bg, width, height, this.ctx);
            this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
          } else {
            // 背景填充图片
            this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height);
          }
          this.ctx.restore();
        }
    
        _drawAbsolute(view) {
          if (!(view && view.type)) {
            // 过滤无效 view
            return;
          }
          // 证明 css 为数组形式,需要合并
          if (view.css && view.css.length) {
            /* eslint-disable no-param-reassign */
            view.css = Object.assign(...view.css);
          }
          switch (view.type) {
            case 'image':
              this._drawAbsImage(view);
              break;
            case 'text':
              this._fillAbsText(view);
              break;
            case 'inlineText':
              this._fillAbsInlineText(view);
              break;
            case 'rect':
              this._drawAbsRect(view);
              break;
            case 'qrcode':
              this._drawQRCode(view);
              break;
            default:
              break;
          }
        }
    
        _border({ borderRadius = 0, width, height, borderWidth = 0, borderStyle = 'solid' }) {
          let r1 = 0,
            r2 = 0,
            r3 = 0,
            r4 = 0;
          const minSize = Math.min(width, height);
          if (borderRadius) {
            const border = borderRadius.split(/\s+/);
            if (border.length === 4) {
              r1 = Math.min(border[0].toPx(false, minSize), width / 2, height / 2);
              r2 = Math.min(border[1].toPx(false, minSize), width / 2, height / 2);
              r3 = Math.min(border[2].toPx(false, minSize), width / 2, height / 2);
              r4 = Math.min(border[3].toPx(false, minSize), width / 2, height / 2);
            } else {
              r1 = r2 = r3 = r4 = Math.min(borderRadius && borderRadius.toPx(false, minSize), width / 2, height / 2);
            }
          }
          const lineWidth = borderWidth && borderWidth.toPx(false, minSize);
          this.ctx.lineWidth = lineWidth;
          if (borderStyle === 'dashed') {
            this.ctx.setLineDash([(lineWidth * 4) / 3, (lineWidth * 4) / 3]);
            // this.ctx.lineDashOffset = 2 * lineWidth
          } else if (borderStyle === 'dotted') {
            this.ctx.setLineDash([lineWidth, lineWidth]);
          }
          const notSolid = borderStyle !== 'solid';
          this.ctx.beginPath();
    
          notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
          r1 !== 0 && this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1 + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI); //左上角圆弧
          this.ctx.lineTo(
            r2 === 0 ? (notSolid ? width / 2 : width / 2 + lineWidth / 2) : width / 2 - r2,
            -height / 2 - lineWidth / 2,
          ); // 顶边线
    
          notSolid && r2 === 0 && this.ctx.moveTo(width / 2 + lineWidth / 2, -height / 2 - lineWidth); // 右边虚线规避重叠规则
          r2 !== 0 && this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2 + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI); // 右上角圆弧
          this.ctx.lineTo(
            width / 2 + lineWidth / 2,
            r3 === 0 ? (notSolid ? height / 2 : height / 2 + lineWidth / 2) : height / 2 - r3,
          ); // 右边线
    
          notSolid && r3 === 0 && this.ctx.moveTo(width / 2 + lineWidth, height / 2 + lineWidth / 2); // 底边虚线规避重叠规则
          r3 !== 0 && this.ctx.arc(width / 2 - r3, height / 2 - r3, r3 + lineWidth / 2, 0, 0.5 * Math.PI); // 右下角圆弧
          this.ctx.lineTo(
            r4 === 0 ? (notSolid ? -width / 2 : -width / 2 - lineWidth / 2) : -width / 2 + r4,
            height / 2 + lineWidth / 2,
          ); // 底边线
    
          notSolid && r4 === 0 && this.ctx.moveTo(-width / 2 - lineWidth / 2, height / 2 + lineWidth); // 左边虚线规避重叠规则
          r4 !== 0 && this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4 + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI); // 左下角圆弧
          this.ctx.lineTo(
            -width / 2 - lineWidth / 2,
            r1 === 0 ? (notSolid ? -height / 2 : -height / 2 - lineWidth / 2) : -height / 2 + r1,
          ); // 左边线
          notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
    
          if (!notSolid) {
            this.ctx.closePath();
          }
        }
    
        /**
        * 根据 borderRadius 进行裁减
        */
        _doClip(borderRadius, width, height, borderStyle) {
          if (borderRadius && width && height) {
            // 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
            // globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white,相对默认的 black 要好点
            this.ctx.globalAlpha = 0;
            this.ctx.fillStyle = 'white';
            this._border({
              borderRadius,
              width,
              height,
              borderStyle,
            });
            this.ctx.fill();
            // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性
            if (!(getApp().systemInfo && getApp().systemInfo.version <= '6.6.6' && getApp().systemInfo.platform === 'ios')) {
              this.ctx.clip();
            }
            this.ctx.globalAlpha = 1;
          }
        }
    
        /**
        * 画边框
        */
        _doBorder(view, width, height) {
          if (!view.css) {
            return;
          }
          const { borderRadius, borderWidth, borderColor, borderStyle } = view.css;
          if (!borderWidth) {
            return;
          }
          this.ctx.save();
          this._preProcess(view, true);
          this.ctx.strokeStyle = borderColor || 'black';
          this._border({
            borderRadius,
            width,
            height,
            borderWidth,
            borderStyle,
          });
          this.ctx.stroke();
          this.ctx.restore();
        }
    
        _preProcess(view, notClip) {
          let width = 0;
          let height;
          let extra;
          const paddings = this._doPaddings(view);
          switch (view.type) {
            case 'inlineText': {
              {
                // 计算行数
                let lines = 0;
                // 文字总长度
                let textLength = 0;
                // 行高
                let lineHeight = 0;
                const textList = view.textList || [];
                for (let i = 0; i < textList.length; i++) {
                  let subView = textList[i];
                  const fontWeight = subView.css.fontWeight || '400';
                  const textStyle = subView.css.textStyle || 'normal';
                  if (!subView.css.fontSize) {
                    subView.css.fontSize = '20rpx';
                  }
                  this.ctx.font = `${textStyle} ${fontWeight} ${subView.css.fontSize.toPx()}px "${subView.css.fontFamily || 'sans-serif'}"`;
                  textLength += this.ctx.measureText(subView.text).width;
                  let tempLineHeight = subView.css.lineHeight ? subView.css.lineHeight.toPx() : subView.css.fontSize.toPx();
                  lineHeight = Math.max(lineHeight, tempLineHeight);
                }
                width = view.css.width ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3] : textLength;;
                const calLines = Math.ceil(textLength / width);
    
                lines += calLines;
                // lines = view.css.maxLines < lines ? view.css.maxLines : lines;
                height = lineHeight * lines;
                extra = {
                  lines: lines,
                  lineHeight: lineHeight,
                  // textArray: textArray,
                  // linesArray: linesArray,
                };
              }
              break;
            }
            case 'text': {
              const textArray = String(view.text).split('\n');
              // 处理多个连续的'\n'
              for (let i = 0; i < textArray.length; ++i) {
                if (textArray[i] === '') {
                  textArray[i] = ' ';
                }
              }
              const fontWeight = view.css.fontWeight || '400';
              const textStyle = view.css.textStyle || 'normal';
              if (!view.css.fontSize) {
                view.css.fontSize = '20rpx';
              }
              this.ctx.font = `${textStyle} ${fontWeight} ${view.css.fontSize.toPx()}px "${
                view.css.fontFamily || 'sans-serif'
              }"`;
              // 计算行数
              let lines = 0;
              const linesArray = [];
              for (let i = 0; i < textArray.length; ++i) {
                const textLength = this.ctx.measureText(textArray[i]).width;
                const minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
                let partWidth = view.css.width
                  ? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3]
                  : textLength;
                if (partWidth < minWidth) {
                  partWidth = minWidth;
                }
                const calLines = Math.ceil(textLength / partWidth);
                // 取最长的作为 width
                width = partWidth > width ? partWidth : width;
                lines += calLines;
                linesArray[i] = calLines;
              }
              lines = view.css.maxLines < lines ? view.css.maxLines : lines;
              const lineHeight = view.css.lineHeight ? view.css.lineHeight.toPx() : view.css.fontSize.toPx();
              height = lineHeight * lines;
              extra = {
                lines: lines,
                lineHeight: lineHeight,
                textArray: textArray,
                linesArray: linesArray,
              };
              break;
            }
            case 'image': {
              // image的长宽设置成auto的逻辑处理
              const ratio = getApp().systemInfo.pixelRatio ? getApp().systemInfo.pixelRatio : 2;
              // 有css却未设置width或height,则默认为auto
              if (view.css) {
                if (!view.css.width) {
                  view.css.width = 'auto';
                }
                if (!view.css.height) {
                  view.css.height = 'auto';
                }
              }
              if (!view.css || (view.css.width === 'auto' && view.css.height === 'auto')) {
                width = Math.round(view.sWidth / ratio);
                height = Math.round(view.sHeight / ratio);
              } else if (view.css.width === 'auto') {
                height = view.css.height.toPx(false, this.style.height);
                width = (view.sWidth / view.sHeight) * height;
              } else if (view.css.height === 'auto') {
                width = view.css.width.toPx(false, this.style.width);
                height = (view.sHeight / view.sWidth) * width;
              } else {
                width = view.css.width.toPx(false, this.style.width);
                height = view.css.height.toPx(false, this.style.height);
              }
              break;
            }
            default:
              if (!(view.css.width && view.css.height)) {
                console.error('You should set width and height');
                return;
              }
              width = view.css.width.toPx(false, this.style.width);
              height = view.css.height.toPx(false, this.style.height);
              break;
          }
          let x;
          if (view.css && view.css.right) {
            if (typeof view.css.right === 'string') {
              x = this.style.width - view.css.right.toPx(true, this.style.width);
            } else {
              // 可以用数组方式,把文字长度计算进去
              // [right, 文字id, 乘数(默认 1)]
              const rights = view.css.right;
              x =
                this.style.width -
                rights[0].toPx(true, this.style.width) -
                penCache.viewRect[rights[1]].width * (rights[2] || 1);
            }
          } else if (view.css && view.css.left) {
            if (typeof view.css.left === 'string') {
              x = view.css.left.toPx(true, this.style.width);
            } else {
              const lefts = view.css.left;
              x = lefts[0].toPx(true, this.style.width) + penCache.viewRect[lefts[1]].width * (lefts[2] || 1);
            }
          } else {
            x = 0;
          }
          //const y = view.css && view.css.bottom ? this.style.height - height - view.css.bottom.toPx(true) : (view.css && view.css.top ? view.css.top.toPx(true) : 0);
          let y;
          if (view.css && view.css.bottom) {
            y = this.style.height - height - view.css.bottom.toPx(true, this.style.height);
          } else {
            if (view.css && view.css.top) {
              if (typeof view.css.top === 'string') {
                y = view.css.top.toPx(true, this.style.height);
              } else {
                const tops = view.css.top;
                y = tops[0].toPx(true, this.style.height) + penCache.viewRect[tops[1]].height * (tops[2] || 1);
              }
            } else {
              y = 0;
            }
          }
    
          const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0;
          // 当设置了 right 时,默认 align 用 right,反之用 left
          const align = view.css && view.css.align ? view.css.align : view.css && view.css.right ? 'right' : 'left';
          const verticalAlign = view.css && view.css.verticalAlign ? view.css.verticalAlign : 'top';
          // 记录绘制时的画布
          let xa = 0;
          switch (align) {
            case 'center':
              xa = x;
              break;
            case 'right':
              xa = x - width / 2;
              break;
            default:
              xa = x + width / 2;
              break;
          }
          let ya = 0;
          switch (verticalAlign) {
            case 'center':
              ya = y;
              break;
            case 'bottom':
              ya = y - height / 2;
              break;
            default:
              ya = y + height / 2;
              break;
          }
          this.ctx.translate(xa, ya);
          // 记录该 view 的有效点击区域
          // TODO ,旋转和裁剪的判断
          // 记录在真实画布上的左侧
          let left = x;
          if (align === 'center') {
            left = x - width / 2;
          } else if (align === 'right') {
            left = x - width;
          }
          var top = y;
          if (verticalAlign === 'center') {
            top = y - height / 2;
          } else if (verticalAlign === 'bottom') {
            top = y - height;
          }
          if (view.rect) {
            view.rect.left = left;
            view.rect.top = top;
            view.rect.right = left + width;
            view.rect.bottom = top + height;
            view.rect.x = view.css && view.css.right ? x - width : x;
            view.rect.y = y;
          } else {
            view.rect = {
              left: left,
              top: top,
              right: left + width,
              bottom: top + height,
              x: view.css && view.css.right ? x - width : x,
              y: y,
            };
          }
    
          view.rect.left = view.rect.left - paddings[3];
          view.rect.top = view.rect.top - paddings[0];
          view.rect.right = view.rect.right + paddings[1];
          view.rect.bottom = view.rect.bottom + paddings[2];
          if (view.type === 'text') {
            view.rect.minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
          }
    
          this.ctx.rotate(angle);
          if (!notClip && view.css && view.css.borderRadius && view.type !== 'rect') {
            this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
          }
          this._doShadow(view);
          if (view.id) {
            penCache.viewRect[view.id] = {
              width,
              height,
              left: view.rect.left,
              top: view.rect.top,
              right: view.rect.right,
              bottom: view.rect.bottom,
            };
          }
          return {
            width: width,
            height: height,
            x: x,
            y: y,
            extra: extra,
          };
        }
    
        _doPaddings(view) {
          const { padding } = view.css ? view.css : {};
          let pd = [0, 0, 0, 0];
          if (padding) {
            const pdg = padding.split(/\s+/);
            if (pdg.length === 1) {
              const x = pdg[0].toPx();
              pd = [x, x, x, x];
            }
            if (pdg.length === 2) {
              const x = pdg[0].toPx();
              const y = pdg[1].toPx();
              pd = [x, y, x, y];
            }
            if (pdg.length === 3) {
              const x = pdg[0].toPx();
              const y = pdg[1].toPx();
              const z = pdg[2].toPx();
              pd = [x, y, z, y];
            }
            if (pdg.length === 4) {
              const x = pdg[0].toPx();
              const y = pdg[1].toPx();
              const z = pdg[2].toPx();
              const a = pdg[3].toPx();
              pd = [x, y, z, a];
            }
          }
          return pd;
        }
    
        // 画文字的背景图片
        _doBackground(view) {
          this.ctx.save();
          const { width: rawWidth, height: rawHeight } = this._preProcess(view, true);
    
          const { background } = view.css;
          let pd = this._doPaddings(view);
          const width = rawWidth + pd[1] + pd[3];
          const height = rawHeight + pd[0] + pd[2];
    
          this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
          if (GD.api.isGradient(background)) {
            GD.api.doGradient(background, width, height, this.ctx);
          } else {
            this.ctx.fillStyle = background;
          }
          this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
    
          this.ctx.restore();
        }
    
        _drawQRCode(view) {
          this.ctx.save();
          const { width, height } = this._preProcess(view);
          QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color);
          this.ctx.restore();
          this._doBorder(view, width, height);
        }
    
        _drawAbsImage(view) {
          if (!view.url) {
            return;
          }
          this.ctx.save();
          const { width, height } = this._preProcess(view);
          // 获得缩放到图片大小级别的裁减框
          let rWidth = view.sWidth;
          let rHeight = view.sHeight;
          let startX = 0;
          let startY = 0;
          // 绘画区域比例
          const cp = width / height;
          // 原图比例
          const op = view.sWidth / view.sHeight;
          if (cp >= op) {
            rHeight = rWidth / cp;
            startY = Math.round((view.sHeight - rHeight) / 2);
          } else {
            rWidth = rHeight * cp;
            startX = Math.round((view.sWidth - rWidth) / 2);
          }
          if (view.css && view.css.mode === 'scaleToFill') {
            this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height);
          } else {
            this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height);
            view.rect.startX = startX / view.sWidth;
            view.rect.startY = startY / view.sHeight;
            view.rect.endX = (startX + rWidth) / view.sWidth;
            view.rect.endY = (startY + rHeight) / view.sHeight;
          }
          this.ctx.restore();
          this._doBorder(view, width, height);
        }
        /**
        *
        * @param {*} view
        * @description 一行内文字多样式的方法
        *
        * 暂不支持配置 text-align,默认left
        * 暂不支持配置 maxLines
        */
        _fillAbsInlineText(view) {
          if (!view.textList) {
            return;
          }
          if (view.css.background) {
            // 生成背景
            this._doBackground(view);
          }
          this.ctx.save();
          const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius);
          const { lines, lineHeight } = extra;
          let staticX = -(width / 2);
          let lineIndex = 0; // 第几行
          let x = staticX; // 开始x位置
          let leftWidth = width; // 当前行剩余多少宽度可以使用
    
          let getStyle = css => {
            const fontWeight = css.fontWeight || '400';
            const textStyle = css.textStyle || 'normal';
            if (!css.fontSize) {
              css.fontSize = '20rpx';
            }
            return `${textStyle} ${fontWeight} ${css.fontSize.toPx()}px "${css.fontFamily || 'sans-serif'}"`;
          }
    
          // 遍历行内的文字数组
          for (let j = 0; j < view.textList.length; j++) {
            const subView = view.textList[j];
    
            // 某个文字开始位置
            let start = 0;
            // 文字已使用的数量
            let alreadyCount = 0;
            // 文字总长度
            let textLength = subView.text.length;
            // 文字总宽度
            let textWidth = this.ctx.measureText(subView.text).width;
            // 每个文字的平均宽度
            let preWidth = Math.ceil(textWidth / textLength);
    
            // 循环写文字
            while (alreadyCount < textLength) {
              // alreadyCount - start + 1 -> 当前摘取出来的文字
              // 比较可用宽度,寻找最大可写文字长度
              while ((alreadyCount - start + 1) * preWidth < leftWidth && alreadyCount < textLength) {
                alreadyCount++;
              }
    
              // 取出文字
              let text = subView.text.substr(start, alreadyCount - start);
    
              const y = -(height / 2) + subView.css.fontSize.toPx() + lineIndex * lineHeight;
    
              // 设置文字样式
              this.ctx.font = getStyle(subView.css);
    
              this.ctx.fillStyle = subView.css.color || 'black';
              this.ctx.textAlign = 'left';
    
              // 执行画布操作
              if (subView.css.textStyle === 'stroke') {
                this.ctx.strokeText(text, x, y);
              } else {
                this.ctx.fillText(text, x, y);
              }
    
              // 当次已使用宽度
              let currentUsedWidth = this.ctx.measureText(text).width;
    
              const fontSize = subView.css.fontSize.toPx();
    
              // 画 textDecoration
              let textDecoration;
              if (subView.css.textDecoration) {
                this.ctx.lineWidth = fontSize / 13;
                this.ctx.beginPath();
                if (/\bunderline\b/.test(subView.css.textDecoration)) {
                  this.ctx.moveTo(x, y);
                  this.ctx.lineTo(x + currentUsedWidth, y);
                  textDecoration = {
                    moveTo: [x, y],
                    lineTo: [x + currentUsedWidth, y],
                  };
                }
                if (/\boverline\b/.test(subView.css.textDecoration)) {
                  this.ctx.moveTo(x, y - fontSize);
                  this.ctx.lineTo(x + currentUsedWidth, y - fontSize);
                  textDecoration = {
                    moveTo: [x, y - fontSize],
                    lineTo: [x + currentUsedWidth, y - fontSize],
                  };
                }
                if (/\bline-through\b/.test(subView.css.textDecoration)) {
                  this.ctx.moveTo(x, y - fontSize / 3);
                  this.ctx.lineTo(x + currentUsedWidth, y - fontSize / 3);
                  textDecoration = {
                    moveTo: [x, y - fontSize / 3],
                    lineTo: [x + currentUsedWidth, y - fontSize / 3],
                  };
                }
                this.ctx.closePath();
                this.ctx.strokeStyle = subView.css.color;
                this.ctx.stroke();
              }
    
              // 重置数据
              start = alreadyCount;
              leftWidth -= currentUsedWidth;
              x += currentUsedWidth;
              // 如果剩余宽度 小于等于0 或者小于一个字的平均宽度,换行
              if (leftWidth <= 0 || leftWidth < preWidth) {
                leftWidth = width;
                x = staticX;
                lineIndex++;
              }
            }
          }
    
          this.ctx.restore();
          this._doBorder(view, width, height);
        }
    
        _fillAbsText(view) {
          if (!view.text) {
            return;
          }
          if (view.css.background) {
            // 生成背景
            this._doBackground(view);
          }
          this.ctx.save();
          const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius);
          this.ctx.fillStyle = view.css.color || 'black';
          if (view.id && penCache.textLines[view.id]) {
            this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
            for (const i of penCache.textLines[view.id]) {
              const { measuredWith, text, x, y, textDecoration } = i;
              if (view.css.textStyle === 'stroke') {
                this.ctx.strokeText(text, x, y, measuredWith);
              } else {
                this.ctx.fillText(text, x, y, measuredWith);
              }
              if (textDecoration) {
                const fontSize = view.css.fontSize.toPx();
                this.ctx.lineWidth = fontSize / 13;
                this.ctx.beginPath();
                this.ctx.moveTo(...textDecoration.moveTo);
                this.ctx.lineTo(...textDecoration.lineTo);
                this.ctx.closePath();
                this.ctx.strokeStyle = view.css.color;
                this.ctx.stroke();
              }
            }
          } else {
            const { lines, lineHeight, textArray, linesArray } = extra;
            // 如果设置了id,则保留 text 的长度
            if (view.id) {
              let textWidth = 0;
              for (let i = 0; i < textArray.length; ++i) {
                const _w = this.ctx.measureText(textArray[i]).width;
                textWidth = _w > textWidth ? _w : textWidth;
              }
              penCache.viewRect[view.id].width = width ? (textWidth < width ? textWidth : width) : textWidth;
            }
            let lineIndex = 0;
            for (let j = 0; j < textArray.length; ++j) {
              const preLineLength = Math.ceil(textArray[j].length / linesArray[j]);
              let start = 0;
              let alreadyCount = 0;
    
              for (let i = 0; i < linesArray[j]; ++i) {
                // 绘制行数大于最大行数,则直接跳出循环
                if (lineIndex >= lines) {
                  break;
                }
                alreadyCount = preLineLength;
                let text = textArray[j].substr(start, alreadyCount);
                let measuredWith = this.ctx.measureText(text).width;
                // 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除
                // 如果已经到文本末尾,也不要进行该循环
                while (
                  start + alreadyCount <= textArray[j].length &&
                  (width - measuredWith > view.css.fontSize.toPx() || measuredWith - width > view.css.fontSize.toPx())
                ) {
                  if (measuredWith < width) {
                    text = textArray[j].substr(start, ++alreadyCount);
                  } else {
                    if (text.length <= 1) {
                      // 如果只有一个字符时,直接跳出循环
                      break;
                    }
                    text = textArray[j].substr(start, --alreadyCount);
                    // break;
                  }
                  measuredWith = this.ctx.measureText(text).width;
                }
                start += text.length;
                // 如果是最后一行了,发现还有未绘制完的内容,则加...
                if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) {
                  while (this.ctx.measureText(`${text}...`).width > width) {
                    if (text.length <= 1) {
                      // 如果只有一个字符时,直接跳出循环
                      break;
                    }
                    text = text.substring(0, text.length - 1);
                  }
                  text += '...';
                  measuredWith = this.ctx.measureText(text).width;
                }
                this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
                let x;
                let lineX;
                switch (view.css.textAlign) {
                  case 'center':
                    x = 0;
                    lineX = x - measuredWith / 2;
                    break;
                  case 'right':
                    x = width / 2;
                    lineX = x - measuredWith;
                    break;
                  default:
                    x = -(width / 2);
                    lineX = x;
                    break;
                }
    
                const y =
                  -(height / 2) +
                  (lineIndex === 0 ? view.css.fontSize.toPx() : view.css.fontSize.toPx() + lineIndex * lineHeight);
                lineIndex++;
                if (view.css.textStyle === 'stroke') {
                  this.ctx.strokeText(text, x, y, measuredWith);
                } else {
                  this.ctx.fillText(text, x, y, measuredWith);
                }
                const fontSize = view.css.fontSize.toPx();
                let textDecoration;
                if (view.css.textDecoration) {
                  this.ctx.lineWidth = fontSize / 13;
                  this.ctx.beginPath();
                  if (/\bunderline\b/.test(view.css.textDecoration)) {
                    this.ctx.moveTo(lineX, y);
                    this.ctx.lineTo(lineX + measuredWith, y);
                    textDecoration = {
                      moveTo: [lineX, y],
                      lineTo: [lineX + measuredWith, y],
                    };
                  }
                  if (/\boverline\b/.test(view.css.textDecoration)) {
                    this.ctx.moveTo(lineX, y - fontSize);
                    this.ctx.lineTo(lineX + measuredWith, y - fontSize);
                    textDecoration = {
                      moveTo: [lineX, y - fontSize],
                      lineTo: [lineX + measuredWith, y - fontSize],
                    };
                  }
                  if (/\bline-through\b/.test(view.css.textDecoration)) {
                    this.ctx.moveTo(lineX, y - fontSize / 3);
                    this.ctx.lineTo(lineX + measuredWith, y - fontSize / 3);
                    textDecoration = {
                      moveTo: [lineX, y - fontSize / 3],
                      lineTo: [lineX + measuredWith, y - fontSize / 3],
                    };
                  }
                  this.ctx.closePath();
                  this.ctx.strokeStyle = view.css.color;
                  this.ctx.stroke();
                }
                if (view.id) {
                  penCache.textLines[view.id]
                    ? penCache.textLines[view.id].push({
                        text,
                        x,
                        y,
                        measuredWith,
                        textDecoration,
                      })
                    : (penCache.textLines[view.id] = [
                        {
                          text,
                          x,
                          y,
                          measuredWith,
                          textDecoration,
                        },
                      ]);
                }
              }
            }
          }
          this.ctx.restore();
          this._doBorder(view, width, height);
        }
    
        _drawAbsRect(view) {
          this.ctx.save();
          const { width, height } = this._preProcess(view);
          if (GD.api.isGradient(view.css.color)) {
            GD.api.doGradient(view.css.color, width, height, this.ctx);
          } else {
            this.ctx.fillStyle = view.css.color;
          }
          const { borderRadius, borderStyle, borderWidth } = view.css;
          this._border({
            borderRadius,
            width,
            height,
            borderWidth,
            borderStyle,
          });
          this.ctx.fill();
          this.ctx.restore();
          this._doBorder(view, width, height);
        }
    
        // shadow 支持 (x, y, blur, color), 不支持 spread
        // shadow:0px 0px 10px rgba(0,0,0,0.1);
        _doShadow(view) {
          if (!view.css || !view.css.shadow) {
            return;
          }
          const box = view.css.shadow.replace(/,\s+/g, ',').split(/\s+/);
          if (box.length > 4) {
            console.error("shadow don't spread option");
            return;
          }
          this.ctx.shadowOffsetX = parseInt(box[0], 10);
          this.ctx.shadowOffsetY = parseInt(box[1], 10);
          this.ctx.shadowBlur = parseInt(box[2], 10);
          this.ctx.shadowColor = box[3];
        }
    
        _getAngle(angle) {
          return (Number(angle) * Math.PI) / 180;
        }
      }
    
  • components/painter/lib/qrcode.js

      /* eslint-disable */
      !(function () {
    
        // alignment pattern
        var adelta = [
          0, 11, 15, 19, 23, 27, 31,
          16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
          26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
        ];
    
        // version block
        var vpat = [
          0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
          0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
          0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
          0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
          0x541, 0xc69
        ];
    
        // final format bits with mask: level << 3 | mask
        var fmtword = [
          0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976,    //L
          0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0,    //M
          0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed,    //Q
          0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b    //H
        ];
    
        // 4 per version: number of blocks 1,2; data width; ecc width
        var eccblocks = [
          1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17,
          1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28,
          1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22,
          1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16,
          1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22,
          2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28,
          2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26,
          2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26,
          2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24,
          2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28,
          4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24,
          2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28,
          4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22,
          3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24,
          5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24,
          5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30,
          1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28,
          5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28,
          3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26,
          3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
          4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30,
          2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24,
          4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
          6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
          8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
          10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
          8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
          3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
          7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
          5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
          13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
          17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
          17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
          13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
          12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
          6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
          17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
          4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
          20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
          19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
        ];
    
        // Galois field log table
        var glog = [
          0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
          0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
          0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
          0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
          0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
          0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
          0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
          0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
          0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
          0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
          0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
          0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
          0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
          0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
          0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
          0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
        ];
    
        // Galios field exponent table
        var gexp = [
          0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
          0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
          0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
          0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
          0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
          0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
          0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
          0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
          0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
          0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
          0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
          0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
          0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
          0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
          0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
          0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
        ];
    
        // Working buffers:
        // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
        var strinbuf = [], eccbuf = [], qrframe = [], framask = [], rlens = [];
        // Control values - width is based on version, last 4 are from table.
        var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
        var ecclevel = 2;
        // set bit to indicate cell in qrframe is immutable.  symmetric around diagonal
        function setmask(x, y) {
          var bt;
          if (x > y) {
            bt = x;
            x = y;
            y = bt;
          }
          // y*y = 1+3+5...
          bt = y;
          bt *= y;
          bt += y;
          bt >>= 1;
          bt += x;
          framask[bt] = 1;
        }
    
        // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
        function putalign(x, y) {
          var j;
    
          qrframe[x + width * y] = 1;
          for (j = -2; j < 2; j++) {
            qrframe[(x + j) + width * (y - 2)] = 1;
            qrframe[(x - 2) + width * (y + j + 1)] = 1;
            qrframe[(x + 2) + width * (y + j)] = 1;
            qrframe[(x + j + 1) + width * (y + 2)] = 1;
          }
          for (j = 0; j < 2; j++) {
            setmask(x - 1, y + j);
            setmask(x + 1, y - j);
            setmask(x - j, y - 1);
            setmask(x + j, y + 1);
          }
        }
    
        //========================================================================
        // Reed Solomon error correction
        // exponentiation mod N
        function modnn(x) {
          while (x >= 255) {
            x -= 255;
            x = (x >> 8) + (x & 255);
          }
          return x;
        }
    
        var genpoly = [];
    
        // Calculate and append ECC data to data block.  Block is in strinbuf, indexes to buffers given.
        function appendrs(data, dlen, ecbuf, eclen) {
          var i, j, fb;
    
          for (i = 0; i < eclen; i++)
            strinbuf[ecbuf + i] = 0;
          for (i = 0; i < dlen; i++) {
            fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
            if (fb != 255)     /* fb term is non-zero */
              for (j = 1; j < eclen; j++)
                strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
            else
              for (j = ecbuf; j < ecbuf + eclen; j++)
                strinbuf[j] = strinbuf[j + 1];
            strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
          }
        }
    
        //========================================================================
        // Frame data insert following the path rules
    
        // check mask - since symmetrical use half.
        function ismasked(x, y) {
          var bt;
          if (x > y) {
            bt = x;
            x = y;
            y = bt;
          }
          bt = y;
          bt += y * y;
          bt >>= 1;
          bt += x;
          return framask[bt];
        }
    
        //========================================================================
        //  Apply the selected mask out of the 8.
        function applymask(m) {
          var x, y, r3x, r3y;
    
          switch (m) {
            case 0:
              for (y = 0; y < width; y++)
                for (x = 0; x < width; x++)
                  if (!((x + y) & 1) && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
              break;
            case 1:
              for (y = 0; y < width; y++)
                for (x = 0; x < width; x++)
                  if (!(y & 1) && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
              break;
            case 2:
              for (y = 0; y < width; y++)
                for (r3x = 0, x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3)
                    r3x = 0;
                  if (!r3x && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              break;
            case 3:
              for (r3y = 0, y = 0; y < width; y++ , r3y++) {
                if (r3y == 3)
                  r3y = 0;
                for (r3x = r3y, x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3)
                    r3x = 0;
                  if (!r3x && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              }
              break;
            case 4:
              for (y = 0; y < width; y++)
                for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3) {
                    r3x = 0;
                    r3y = !r3y;
                  }
                  if (!r3y && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              break;
            case 5:
              for (r3y = 0, y = 0; y < width; y++ , r3y++) {
                if (r3y == 3)
                  r3y = 0;
                for (r3x = 0, x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3)
                    r3x = 0;
                  if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              }
              break;
            case 6:
              for (r3y = 0, y = 0; y < width; y++ , r3y++) {
                if (r3y == 3)
                  r3y = 0;
                for (r3x = 0, x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3)
                    r3x = 0;
                  if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              }
              break;
            case 7:
              for (r3y = 0, y = 0; y < width; y++ , r3y++) {
                if (r3y == 3)
                  r3y = 0;
                for (r3x = 0, x = 0; x < width; x++ , r3x++) {
                  if (r3x == 3)
                    r3x = 0;
                  if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
                    qrframe[x + y * width] ^= 1;
                }
              }
              break;
          }
          return;
        }
    
        // Badness coefficients.
        var N1 = 3, N2 = 3, N3 = 40, N4 = 10;
    
        // Using the table of the length of each run, calculate the amount of bad image
        // - long runs or those that look like finders; called twice, once each for X and Y
        function badruns(length) {
          var i;
          var runsbad = 0;
          for (i = 0; i <= length; i++)
            if (rlens[i] >= 5)
              runsbad += N1 + rlens[i] - 5;
          // BwBBBwB as in finder
          for (i = 3; i < length - 1; i += 2)
            if (rlens[i - 2] == rlens[i + 2]
              && rlens[i + 2] == rlens[i - 1]
              && rlens[i - 1] == rlens[i + 1]
              && rlens[i - 1] * 3 == rlens[i]
              // white around the black pattern? Not part of spec
              && (rlens[i - 3] == 0 // beginning
                || i + 3 > length  // end
                || rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
            )
              runsbad += N3;
          return runsbad;
        }
    
        // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
        function badcheck() {
          var x, y, h, b, b1;
          var thisbad = 0;
          var bw = 0;
    
          // blocks of same color.
          for (y = 0; y < width - 1; y++)
            for (x = 0; x < width - 1; x++)
              if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y]
                && qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
                || !(qrframe[x + width * y] || qrframe[(x + 1) + width * y]
                  || qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
                thisbad += N2;
    
          // X runs
          for (y = 0; y < width; y++) {
            rlens[0] = 0;
            for (h = b = x = 0; x < width; x++) {
              if ((b1 = qrframe[x + width * y]) == b)
                rlens[h]++;
              else
                rlens[++h] = 1;
              b = b1;
              bw += b ? 1 : -1;
            }
            thisbad += badruns(h);
          }
    
          // black/white imbalance
          if (bw < 0)
            bw = -bw;
    
          var big = bw;
          var count = 0;
          big += big << 2;
          big <<= 1;
          while (big > width * width)
            big -= width * width, count++;
          thisbad += count * N4;
    
          // Y runs
          for (x = 0; x < width; x++) {
            rlens[0] = 0;
            for (h = b = y = 0; y < width; y++) {
              if ((b1 = qrframe[x + width * y]) == b)
                rlens[h]++;
              else
                rlens[++h] = 1;
              b = b1;
            }
            thisbad += badruns(h);
          }
          return thisbad;
        }
    
        function genframe(instring) {
          var x, y, k, t, v, i, j, m;
    
          // find the smallest version that fits the string
          t = instring.length;
          version = 0;
          do {
            version++;
            k = (ecclevel - 1) * 4 + (version - 1) * 16;
            neccblk1 = eccblocks[k++];
            neccblk2 = eccblocks[k++];
            datablkw = eccblocks[k++];
            eccblkwid = eccblocks[k];
            k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
            if (t <= k)
              break;
          } while (version < 40);
    
          // FIXME - insure that it fits insted of being truncated
          width = 17 + 4 * version;
    
          // allocate, clear and setup data structures
          v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
          for (t = 0; t < v; t++)
            eccbuf[t] = 0;
          strinbuf = instring.slice(0);
    
          for (t = 0; t < width * width; t++)
            qrframe[t] = 0;
    
          for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
            framask[t] = 0;
    
          // insert finders - black to frame, white to mask
          for (t = 0; t < 3; t++) {
            k = 0;
            y = 0;
            if (t == 1)
              k = (width - 7);
            if (t == 2)
              y = (width - 7);
            qrframe[(y + 3) + width * (k + 3)] = 1;
            for (x = 0; x < 6; x++) {
              qrframe[(y + x) + width * k] = 1;
              qrframe[y + width * (k + x + 1)] = 1;
              qrframe[(y + 6) + width * (k + x)] = 1;
              qrframe[(y + x + 1) + width * (k + 6)] = 1;
            }
            for (x = 1; x < 5; x++) {
              setmask(y + x, k + 1);
              setmask(y + 1, k + x + 1);
              setmask(y + 5, k + x);
              setmask(y + x + 1, k + 5);
            }
            for (x = 2; x < 4; x++) {
              qrframe[(y + x) + width * (k + 2)] = 1;
              qrframe[(y + 2) + width * (k + x + 1)] = 1;
              qrframe[(y + 4) + width * (k + x)] = 1;
              qrframe[(y + x + 1) + width * (k + 4)] = 1;
            }
          }
    
          // alignment blocks
          if (version > 1) {
            t = adelta[version];
            y = width - 7;
            for (; ;) {
              x = width - 7;
              while (x > t - 3) {
                putalign(x, y);
                if (x < t)
                  break;
                x -= t;
              }
              if (y <= t + 9)
                break;
              y -= t;
              putalign(6, y);
              putalign(y, 6);
            }
          }
    
          // single black
          qrframe[8 + width * (width - 8)] = 1;
    
          // timing gap - mask only
          for (y = 0; y < 7; y++) {
            setmask(7, y);
            setmask(width - 8, y);
            setmask(7, y + width - 7);
          }
          for (x = 0; x < 8; x++) {
            setmask(x, 7);
            setmask(x + width - 8, 7);
            setmask(x, width - 8);
          }
    
          // reserve mask-format area
          for (x = 0; x < 9; x++)
            setmask(x, 8);
          for (x = 0; x < 8; x++) {
            setmask(x + width - 8, 8);
            setmask(8, x);
          }
          for (y = 0; y < 7; y++)
            setmask(8, y + width - 7);
    
          // timing row/col
          for (x = 0; x < width - 14; x++)
            if (x & 1) {
              setmask(8 + x, 6);
              setmask(6, 8 + x);
            }
            else {
              qrframe[(8 + x) + width * 6] = 1;
              qrframe[6 + width * (8 + x)] = 1;
            }
    
          // version block
          if (version > 6) {
            t = vpat[version - 7];
            k = 17;
            for (x = 0; x < 6; x++)
              for (y = 0; y < 3; y++ , k--)
                if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
                  qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
                  qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
                }
                else {
                  setmask(5 - x, 2 - y + width - 11);
                  setmask(2 - y + width - 11, 5 - x);
                }
          }
    
          // sync mask bits - only set above for white spaces, so add in black bits
          for (y = 0; y < width; y++)
            for (x = 0; x <= y; x++)
              if (qrframe[x + width * y])
                setmask(x, y);
    
          // convert string to bitstream
          // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
          v = strinbuf.length;
    
          // string to array
          for (i = 0; i < v; i++)
            eccbuf[i] = strinbuf.charCodeAt(i);
          strinbuf = eccbuf.slice(0);
    
          // calculate max string length
          x = datablkw * (neccblk1 + neccblk2) + neccblk2;
          if (v >= x - 2) {
            v = x - 2;
            if (version > 9)
              v--;
          }
    
          // shift and repack to insert length prefix
          i = v;
          if (version > 9) {
            strinbuf[i + 2] = 0;
            strinbuf[i + 3] = 0;
            while (i--) {
              t = strinbuf[i];
              strinbuf[i + 3] |= 255 & (t << 4);
              strinbuf[i + 2] = t >> 4;
            }
            strinbuf[2] |= 255 & (v << 4);
            strinbuf[1] = v >> 4;
            strinbuf[0] = 0x40 | (v >> 12);
          }
          else {
            strinbuf[i + 1] = 0;
            strinbuf[i + 2] = 0;
            while (i--) {
              t = strinbuf[i];
              strinbuf[i + 2] |= 255 & (t << 4);
              strinbuf[i + 1] = t >> 4;
            }
            strinbuf[1] |= 255 & (v << 4);
            strinbuf[0] = 0x40 | (v >> 4);
          }
          // fill to end with pad pattern
          i = v + 3 - (version < 10);
          while (i < x) {
            strinbuf[i++] = 0xec;
            // buffer has room    if (i == x)      break;
            strinbuf[i++] = 0x11;
          }
    
          // calculate and append ECC
    
          // calculate generator polynomial
          genpoly[0] = 1;
          for (i = 0; i < eccblkwid; i++) {
            genpoly[i + 1] = 1;
            for (j = i; j > 0; j--)
              genpoly[j] = genpoly[j]
                ? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
            genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
          }
          for (i = 0; i <= eccblkwid; i++)
            genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
    
          // append ecc to data buffer
          k = x;
          y = 0;
          for (i = 0; i < neccblk1; i++) {
            appendrs(y, datablkw, k, eccblkwid);
            y += datablkw;
            k += eccblkwid;
          }
          for (i = 0; i < neccblk2; i++) {
            appendrs(y, datablkw + 1, k, eccblkwid);
            y += datablkw + 1;
            k += eccblkwid;
          }
          // interleave blocks
          y = 0;
          for (i = 0; i < datablkw; i++) {
            for (j = 0; j < neccblk1; j++)
              eccbuf[y++] = strinbuf[i + j * datablkw];
            for (j = 0; j < neccblk2; j++)
              eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
          }
          for (j = 0; j < neccblk2; j++)
            eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
          for (i = 0; i < eccblkwid; i++)
            for (j = 0; j < neccblk1 + neccblk2; j++)
              eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
          strinbuf = eccbuf;
    
          // pack bits into frame avoiding masked area.
          x = y = width - 1;
          k = v = 1;         // up, minus
          /* inteleaved data and ecc codes */
          m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
          for (i = 0; i < m; i++) {
            t = strinbuf[i];
            for (j = 0; j < 8; j++ , t <<= 1) {
              if (0x80 & t)
                qrframe[x + width * y] = 1;
              do {        // find next fill position
                if (v)
                  x--;
                else {
                  x++;
                  if (k) {
                    if (y != 0)
                      y--;
                    else {
                      x -= 2;
                      k = !k;
                      if (x == 6) {
                        x--;
                        y = 9;
                      }
                    }
                  }
                  else {
                    if (y != width - 1)
                      y++;
                    else {
                      x -= 2;
                      k = !k;
                      if (x == 6) {
                        x--;
                        y -= 8;
                      }
                    }
                  }
                }
                v = !v;
              } while (ismasked(x, y));
            }
          }
    
          // save pre-mask copy of frame
          strinbuf = qrframe.slice(0);
          t = 0;           // best
          y = 30000;         // demerit
          // for instead of while since in original arduino code
          // if an early mask was "good enough" it wouldn't try for a better one
          // since they get more complex and take longer.
          for (k = 0; k < 8; k++) {
            applymask(k);      // returns black-white imbalance
            x = badcheck();
            if (x < y) { // current mask better than previous best?
              y = x;
              t = k;
            }
            if (t == 7)
              break;       // don't increment i to a void redoing mask
            qrframe = strinbuf.slice(0); // reset for next pass
          }
          if (t != k)         // redo best mask - none good enough, last wasn't t
            applymask(t);
    
          // add in final mask/ecclevel bytes
          y = fmtword[t + ((ecclevel - 1) << 3)];
          // low byte
          for (k = 0; k < 8; k++ , y >>= 1)
            if (y & 1) {
              qrframe[(width - 1 - k) + width * 8] = 1;
              if (k < 6)
                qrframe[8 + width * k] = 1;
              else
                qrframe[8 + width * (k + 1)] = 1;
            }
          // high byte
          for (k = 0; k < 7; k++ , y >>= 1)
            if (y & 1) {
              qrframe[8 + width * (width - 7 + k)] = 1;
              if (k)
                qrframe[(6 - k) + width * 8] = 1;
              else
                qrframe[7 + width * 8] = 1;
            }
          return qrframe;
        }
    
    
    
    
        var _canvas = null;
    
        var api = {
    
          get ecclevel() {
            return ecclevel;
          },
    
          set ecclevel(val) {
            ecclevel = val;
          },
    
          get size() {
            return _size;
          },
    
          set size(val) {
            _size = val
          },
    
          get canvas() {
            return _canvas;
          },
    
          set canvas(el) {
            _canvas = el;
          },
    
          getFrame: function (string) {
            return genframe(string);
          },
          //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
          utf16to8: function (str) {
            var out, i, len, c;
    
            out = "";
            len = str.length;
            for (i = 0; i < len; i++) {
              c = str.charCodeAt(i);
              if ((c >= 0x0001) && (c <= 0x007F)) {
                out += str.charAt(i);
              } else if (c > 0x07FF) {
                out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
                out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
                out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
              } else {
                out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
                out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
              }
            }
            return out;
          },
          /**
          * 新增$this参数,传入组件的this,兼容在组件中生成
          * @param bg 目前只能设置颜色值
          */
          draw: function (str, ctx, startX, startY, cavW, cavH, bg, color, $this, ecc) {
            var that = this;
            ecclevel = ecc || ecclevel;
            if (!ctx) {
              console.warn('No canvas provided to draw QR code in!')
              return;
            }
            var size = Math.min(cavW, cavH);
            str = that.utf16to8(str);//增加中文显示
    
            var frame = that.getFrame(str);
            var px = size / width;
            if (bg) {
              ctx.fillStyle = bg;
              ctx.fillRect(startX, startY, cavW, cavW);
            }
            ctx.fillStyle = color || 'black';
            for (var i = 0; i < width; i++) {
              for (var j = 0; j < width; j++) {
                if (frame[j * width + i]) {
                  ctx.fillRect(startX + px * i, startY + px * j, px, px);
                }
              }
            }
          }
        }
        module.exports = { api }
        // exports.draw = api;
    
      })();
    
  • components/painter/lib/sha1.js

      var hexcase = 0;
      var chrsz = 8;
    
      function hex_sha1(s) {
        return binb2hex(core_sha1(str2binb(s), s.length * chrsz));
      }
    
      function core_sha1(x, len) {
        x[len >> 5] |= 0x80 << (24 - (len % 32));
        x[(((len + 64) >> 9) << 4) + 15] = len;
    
        var w = Array(80);
        var a = 1732584193;
        var b = -271733879;
        var c = -1732584194;
        var d = 271733878;
        var e = -1009589776;
    
        for (var i = 0; i < x.length; i += 16) {
          var olda = a;
          var oldb = b;
          var oldc = c;
          var oldd = d;
          var olde = e;
    
          for (var j = 0; j < 80; j++) {
            if (j < 16) w[j] = x[i + j];
            else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
            var t = safe_add(
              safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
              safe_add(safe_add(e, w[j]), sha1_kt(j))
            );
            e = d;
            d = c;
            c = rol(b, 30);
            b = a;
            a = t;
          }
    
          a = safe_add(a, olda);
          b = safe_add(b, oldb);
          c = safe_add(c, oldc);
          d = safe_add(d, oldd);
          e = safe_add(e, olde);
        }
        return Array(a, b, c, d, e);
      }
    
      function sha1_ft(t, b, c, d) {
        if (t < 20) return (b & c) | (~b & d);
        if (t < 40) return b ^ c ^ d;
        if (t < 60) return (b & c) | (b & d) | (c & d);
        return b ^ c ^ d;
      }
    
      function sha1_kt(t) {
        return t < 20
          ? 1518500249
          : t < 40
          ? 1859775393
          : t < 60
          ? -1894007588
          : -899497514;
      }
    
      function safe_add(x, y) {
        var lsw = (x & 0xffff) + (y & 0xffff);
        var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
        return (msw << 16) | (lsw & 0xffff);
      }
    
      function rol(num, cnt) {
        return (num << cnt) | (num >>> (32 - cnt));
      }
    
      function str2binb(str) {
        var bin = Array();
        var mask = (1 << chrsz) - 1;
        for (var i = 0; i < str.length * chrsz; i += chrsz)
          bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - (i % 32));
        return bin;
      }
    
      function binb2hex(binarray) {
        var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
        var str = "";
        for (var i = 0; i < binarray.length * 4; i++) {
          str +=
            hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8 + 4)) & 0xf) +
            hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8)) & 0xf);
        }
        return str;
      }
    
      module.exports = {
        hex_sha1,
      }
    
  • components/painter/lib/string-polyfill.js

      String.prototype.substr = function (start, length) {
        if (start === undefined) {
          return this.toString()
        }
        if (typeof start !== 'number' || (typeof length !== 'number' && length !== undefined) ) {
          return ''
        }
        const strArr = [...this]
        const _length = strArr.length
        if (_length + start < 0) {
          start = 0
        }
        if (length === undefined || (start < 0 && start + length > 0)) {
          return strArr.slice(start).join('')
        } else {
          return strArr.slice(start, start + length).join('')
        }
      }
    
    
      String.prototype.substring = function (start, end) {
        if (start === undefined) {
          return this.toString()
        }
        if (typeof start !== 'number' || (typeof end !== 'number' && end !== undefined) ) {
          return ''
        }
        if (!(start > 0)) {
          start = 0
        }
        if (!(end > 0) && end !== undefined) {
          end = 0
        }
        const strArr = [...this]
        const _length = strArr.length
        if (start > _length) {
          start = _length
        }
        if (end > _length) {
          end = _length
        }
        if (end < start) {
          [start, end] = [end, start]
        }
        return strArr.slice(start, end).join('')
      }
    
  • components/painter/lib/util.js

      function isValidUrl(url) {
        return isOnlineUrl(url) || isDataUrl(url);
      }
    
      function isOnlineUrl(url) {
        return /((ht|f)tp(s?)|cloud):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url)
      }
    
      function isDataUrl(url) {
        return /data:image\/(\w+);base64,(.*)/.test(url);
      }
    
      /**
      * 深度对比两个对象是否一致
      * from: https://github.com/epoberezkin/fast-deep-equal
      * @param  {Object} a 对象a
      * @param  {Object} b 对象b
      * @return {Boolean}   是否相同
      */
      /* eslint-disable */
      function equal(a, b) {
        if (a === b) return true;
    
        if (a && b && typeof a == 'object' && typeof b == 'object') {
          var arrA = Array.isArray(a)
            , arrB = Array.isArray(b)
            , i
            , length
            , key;
    
          if (arrA && arrB) {
            length = a.length;
            if (length != b.length) return false;
            for (i = length; i-- !== 0;)
              if (!equal(a[i], b[i])) return false;
            return true;
          }
    
          if (arrA != arrB) return false;
    
          var dateA = a instanceof Date
            , dateB = b instanceof Date;
          if (dateA != dateB) return false;
          if (dateA && dateB) return a.getTime() == b.getTime();
    
          var regexpA = a instanceof RegExp
            , regexpB = b instanceof RegExp;
          if (regexpA != regexpB) return false;
          if (regexpA && regexpB) return a.toString() == b.toString();
    
          var keys = Object.keys(a);
          length = keys.length;
    
          if (length !== Object.keys(b).length)
            return false;
    
          for (i = length; i-- !== 0;)
            if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
    
          for (i = length; i-- !== 0;) {
            key = keys[i];
            if (!equal(a[key], b[key])) return false;
          }
    
          return true;
        }
    
        return a!==a && b!==b;
      }
    
      module.exports = {
        isValidUrl,
        isOnlineUrl,
        isDataUrl,
        equal
      };
    
  • components/painter/lib/wx-canvas.js

      // @ts-check
      export default class WxCanvas {
        ctx;
        type;
        canvasId;
        canvasNode;
        stepList = [];
        canvasPrototype = {};
    
        constructor(type, ctx, canvasId, isNew, canvasNode) {
          this.ctx = ctx;
          this.canvasId = canvasId;
          this.type = type;
          if (isNew) {
            this.canvasNode = canvasNode || {};
          }
        }
    
        set width(w) {
          if (this.canvasNode) {
            this.canvasNode.width = w;
            // 经测试,在 2d 接口中如果不设置这个值,IOS 端有一定几率会出现图片显示不全的情况。
            this.canvasNode._width = w;
          }
        }
    
        get width() {
          if (this.canvasNode) return this.canvasNode.width;
          return 0;
        }
    
        set height(h) {
          if (this.canvasNode) {
            this.canvasNode.height = h;
            // 经测试,在 2d 接口中如果不设置这个值,IOS 端有一定几率会出现图片显示不全的情况。
            this.canvasNode._height = h;
          }
        }
    
        get height() {
          if (this.canvasNode) return this.canvasNode.height;
          return 0;
        }
    
        set lineWidth(args) {
          this.canvasPrototype.lineWidth = args;
          this.stepList.push({
            action: "lineWidth",
            args,
            actionType: "set",
          });
        }
    
        get lineWidth() {
          return this.canvasPrototype.lineWidth;
        }
    
        set lineCap(args) {
          this.canvasPrototype.lineCap = args;
          this.stepList.push({
            action: "lineCap",
            args,
            actionType: "set",
          });
        }
    
        get lineCap() {
          return this.canvasPrototype.lineCap;
        }
    
        set lineJoin(args) {
          this.canvasPrototype.lineJoin = args;
          this.stepList.push({
            action: "lineJoin",
            args,
            actionType: "set",
          });
        }
    
        get lineJoin() {
          return this.canvasPrototype.lineJoin;
        }
    
        set miterLimit(args) {
          this.canvasPrototype.miterLimit = args;
          this.stepList.push({
            action: "miterLimit",
            args,
            actionType: "set",
          });
        }
    
        get miterLimit() {
          return this.canvasPrototype.miterLimit;
        }
    
        set lineDashOffset(args) {
          this.canvasPrototype.lineDashOffset = args;
          this.stepList.push({
            action: "lineDashOffset",
            args,
            actionType: "set",
          });
        }
    
        get lineDashOffset() {
          return this.canvasPrototype.lineDashOffset;
        }
    
        set font(args) {
          this.canvasPrototype.font = args;
          this.ctx.font = args;
          this.stepList.push({
            action: "font",
            args,
            actionType: "set",
          });
        }
    
        get font() {
          return this.canvasPrototype.font;
        }
    
        set textAlign(args) {
          this.canvasPrototype.textAlign = args;
          this.stepList.push({
            action: "textAlign",
            args,
            actionType: "set",
          });
        }
    
        get textAlign() {
          return this.canvasPrototype.textAlign;
        }
    
        set textBaseline(args) {
          this.canvasPrototype.textBaseline = args;
          this.stepList.push({
            action: "textBaseline",
            args,
            actionType: "set",
          });
        }
    
        get textBaseline() {
          return this.canvasPrototype.textBaseline;
        }
    
        set fillStyle(args) {
          this.canvasPrototype.fillStyle = args;
          this.stepList.push({
            action: "fillStyle",
            args,
            actionType: "set",
          });
        }
    
        get fillStyle() {
          return this.canvasPrototype.fillStyle;
        }
    
        set strokeStyle(args) {
          this.canvasPrototype.strokeStyle = args;
          this.stepList.push({
            action: "strokeStyle",
            args,
            actionType: "set",
          });
        }
    
        get strokeStyle() {
          return this.canvasPrototype.strokeStyle;
        }
    
        set globalAlpha(args) {
          this.canvasPrototype.globalAlpha = args;
          this.stepList.push({
            action: "globalAlpha",
            args,
            actionType: "set",
          });
        }
    
        get globalAlpha() {
          return this.canvasPrototype.globalAlpha;
        }
    
        set globalCompositeOperation(args) {
          this.canvasPrototype.globalCompositeOperation = args;
          this.stepList.push({
            action: "globalCompositeOperation",
            args,
            actionType: "set",
          });
        }
    
        get globalCompositeOperation() {
          return this.canvasPrototype.globalCompositeOperation;
        }
    
        set shadowColor(args) {
          this.canvasPrototype.shadowColor = args;
          this.stepList.push({
            action: "shadowColor",
            args,
            actionType: "set",
          });
        }
    
        get shadowColor() {
          return this.canvasPrototype.shadowColor;
        }
    
        set shadowOffsetX(args) {
          this.canvasPrototype.shadowOffsetX = args;
          this.stepList.push({
            action: "shadowOffsetX",
            args,
            actionType: "set",
          });
        }
    
        get shadowOffsetX() {
          return this.canvasPrototype.shadowOffsetX;
        }
    
        set shadowOffsetY(args) {
          this.canvasPrototype.shadowOffsetY = args;
          this.stepList.push({
            action: "shadowOffsetY",
            args,
            actionType: "set",
          });
        }
    
        get shadowOffsetY() {
          return this.canvasPrototype.shadowOffsetY;
        }
    
        set shadowBlur(args) {
          this.canvasPrototype.shadowBlur = args;
          this.stepList.push({
            action: "shadowBlur",
            args,
            actionType: "set",
          });
        }
    
        get shadowBlur() {
          return this.canvasPrototype.shadowBlur;
        }
    
        save() {
          this.stepList.push({
            action: "save",
            args: null,
            actionType: "func",
          });
        }
    
        restore() {
          this.stepList.push({
            action: "restore",
            args: null,
            actionType: "func",
          });
        }
    
        setLineDash(...args) {
          this.canvasPrototype.lineDash = args;
          this.stepList.push({
            action: "setLineDash",
            args,
            actionType: "func",
          });
        }
    
        moveTo(...args) {
          this.stepList.push({
            action: "moveTo",
            args,
            actionType: "func",
          });
        }
    
        closePath() {
          this.stepList.push({
            action: "closePath",
            args: null,
            actionType: "func",
          });
        }
    
        lineTo(...args) {
          this.stepList.push({
            action: "lineTo",
            args,
            actionType: "func",
          });
        }
    
        quadraticCurveTo(...args) {
          this.stepList.push({
            action: "quadraticCurveTo",
            args,
            actionType: "func",
          });
        }
    
        bezierCurveTo(...args) {
          this.stepList.push({
            action: "bezierCurveTo",
            args,
            actionType: "func",
          });
        }
    
        arcTo(...args) {
          this.stepList.push({
            action: "arcTo",
            args,
            actionType: "func",
          });
        }
    
        arc(...args) {
          this.stepList.push({
            action: "arc",
            args,
            actionType: "func",
          });
        }
    
        rect(...args) {
          this.stepList.push({
            action: "rect",
            args,
            actionType: "func",
          });
        }
    
        scale(...args) {
          this.stepList.push({
            action: "scale",
            args,
            actionType: "func",
          });
        }
    
        rotate(...args) {
          this.stepList.push({
            action: "rotate",
            args,
            actionType: "func",
          });
        }
    
        translate(...args) {
          this.stepList.push({
            action: "translate",
            args,
            actionType: "func",
          });
        }
    
        transform(...args) {
          this.stepList.push({
            action: "transform",
            args,
            actionType: "func",
          });
        }
    
        setTransform(...args) {
          this.stepList.push({
            action: "setTransform",
            args,
            actionType: "func",
          });
        }
    
        clearRect(...args) {
          this.stepList.push({
            action: "clearRect",
            args,
            actionType: "func",
          });
        }
    
        fillRect(...args) {
          this.stepList.push({
            action: "fillRect",
            args,
            actionType: "func",
          });
        }
    
        strokeRect(...args) {
          this.stepList.push({
            action: "strokeRect",
            args,
            actionType: "func",
          });
        }
    
        fillText(...args) {
          this.stepList.push({
            action: "fillText",
            args,
            actionType: "func",
          });
        }
    
        strokeText(...args) {
          this.stepList.push({
            action: "strokeText",
            args,
            actionType: "func",
          });
        }
    
        beginPath() {
          this.stepList.push({
            action: "beginPath",
            args: null,
            actionType: "func",
          });
        }
    
        fill() {
          this.stepList.push({
            action: "fill",
            args: null,
            actionType: "func",
          });
        }
    
        stroke() {
          this.stepList.push({
            action: "stroke",
            args: null,
            actionType: "func",
          });
        }
    
        drawFocusIfNeeded(...args) {
          this.stepList.push({
            action: "drawFocusIfNeeded",
            args,
            actionType: "func",
          });
        }
    
        clip() {
          this.stepList.push({
            action: "clip",
            args: null,
            actionType: "func",
          });
        }
    
        isPointInPath(...args) {
          this.stepList.push({
            action: "isPointInPath",
            args,
            actionType: "func",
          });
        }
    
        drawImage(...args) {
          this.stepList.push({
            action: "drawImage",
            args,
            actionType: "func",
          });
        }
    
        addHitRegion(...args) {
          this.stepList.push({
            action: "addHitRegion",
            args,
            actionType: "func",
          });
        }
    
        removeHitRegion(...args) {
          this.stepList.push({
            action: "removeHitRegion",
            args,
            actionType: "func",
          });
        }
    
        clearHitRegions(...args) {
          this.stepList.push({
            action: "clearHitRegions",
            args,
            actionType: "func",
          });
        }
    
        putImageData(...args) {
          this.stepList.push({
            action: "putImageData",
            args,
            actionType: "func",
          });
        }
    
        getLineDash() {
          return this.canvasPrototype.lineDash;
        }
    
        createLinearGradient(...args) {
          return this.ctx.createLinearGradient(...args);
        }
    
        createRadialGradient(...args) {
          if (this.type === "2d") {
            return this.ctx.createRadialGradient(...args);
          } else {
            return this.ctx.createCircularGradient(...args.slice(3, 6));
          }
        }
    
        createPattern(...args) {
          return this.ctx.createPattern(...args);
        }
    
        measureText(...args) {
          return this.ctx.measureText(...args);
        }
    
        createImageData(...args) {
          return this.ctx.createImageData(...args);
        }
    
        getImageData(...args) {
          return this.ctx.getImageData(...args);
        }
    
        async draw(reserve, func) {
          const realstepList = this.stepList.slice();
          this.stepList.length = 0;
          if (this.type === "mina") {
            if (realstepList.length > 0) {
              for (const step of realstepList) {
                this.implementMinaStep(step);
              }
              realstepList.length = 0;
            }
            this.ctx.draw(reserve, func);
          } else if (this.type === "2d") {
            if (!reserve) {
              this.ctx.clearRect(0, 0, this.canvasNode.width, this.canvasNode.height);
            }
            if (realstepList.length > 0) {
              for (const step of realstepList) {
                await this.implement2DStep(step);
              }
              realstepList.length = 0;
            }
            if (func) {
              func();
            }
          }
          realstepList.length = 0;
        }
    
        implementMinaStep(step) {
          switch (step.action) {
            case "textAlign": {
              this.ctx.setTextAlign(step.args);
              break;
            }
            case "textBaseline": {
              this.ctx.setTextBaseline(step.args);
              break;
            }
            default: {
              if (step.actionType === "set") {
                this.ctx[step.action] = step.args;
              } else if (step.actionType === "func") {
                if (step.args) {
                  this.ctx[step.action](...step.args);
                } else {
                  this.ctx[step.action]();
                }
              }
              break;
            }
          }
        }
    
        implement2DStep(step) {
          return new Promise((resolve) => {
            if (step.action === "drawImage") {
              const img = this.canvasNode.createImage();
              img.src = step.args[0];
              img.onload = () => {
                this.ctx.drawImage(img, ...step.args.slice(1));
                resolve();
              };
            } else {
              if (step.actionType === "set") {
                this.ctx[step.action] = step.args;
              } else if (step.actionType === "func") {
                if (step.args) {
                  this.ctx[step.action](...step.args);
                } else {
                  this.ctx[step.action]();
                }
              }
              resolve();
            }
          });
        }
      }
    
posted @ 2024-11-25 11:02  不完美的完美  阅读(2)  评论(0编辑  收藏  举报