小程序动态生成canvas海报

news/2024/7/20 3:26:00 标签: 小程序, Canvas, Promise, 微信小程序

先写wxml,再根据 wxml 来动态地画canvas有诸多好处:提升体验(渲染快)、便于升级迭代(后期瓜皮改需求)、方便维护等。

文章目录

  • 先睹为快
  • canvas海报
    • 1、优雅地授权 (Be elegant.)
      • 1.1、封装
      • 1.2、使用
    • 2、动态获取wxml显示信息
    • 3、绘制canvas (Be patient.)
      • 3.1、缓存图片
      • 3.2、开始绘制
      • 3.3、从缓存存入本地
  • 踩坑小记
    • 绘制出的海报没有二维码

先睹为快

通过本篇博客你可以掌握的东西

  • 小程序canvas绘制海报的完备解耦流程
  • 利用Promise来控制异步流程的方法(!important)
  • 优雅地处理瓜皮授权问题
  • 避免小程序canvas的坑坑们

canvas海报

“学习一个东西,认知很重要,这有益于把控整体及系统掌握。”
——光

大抵流程也就是按大小标题的顺序来的,可以前往文章目录回顾下。

为防止用户疑惑,下文提到的page均为页面的实例
总步骤:

// 点击生成海报按钮
async tapSave() {
        try {
            await page.beforeSave(); // 生成前的校验
            await page.askAuth(); // 处理授权
            await page.drawPoster(); // 绘制海报
            await page.saveFile(); // 保存到本地
        } catch (e) {
            console.error(e);
        }
    },

1、优雅地授权 (Be elegant.)

毋庸置疑,canvas绘制海报需要保存到用户本地,因此需要用户的授权;而小程序中需要判断用户是否授权的业务不少,对此功能封装一下可以增强项目的模块性,方便维护。

1.1、封装

/utils/util.js

module.exports = {
    getAuthStatus(scopeName) {
       return new Promise((resolve, reject) => {
           wx.getSetting({
               success(res) {
                       // 已拒绝过,弹设置
                       if (res.authSetting[`scope.${scopeName}`] === false) {
                           resolve(false);
                           // 已同意
                       } else if (res.authSetting[`scope.${scopeName}`]) {
                           resolve(true);
                       } else {
                           resolve("none");
                       }
                   }
               });
       });
   }
}

1.2、使用

海报页面 /pages/spread_poster/spread_poster中,封装一个判断及授权步骤的方法

const utils = require("../../utils/util");
// ...
// 获取授权
askAuth() {
       return new Promise((resolve, reject) => {
           utils.getAuthStatus("writePhotosAlbum").then(isAuthed => {
               if (isAuthed === "none") {
                   // 无信息,弹授权
                   wx.authorize({
                       scope: "scope.writePhotosAlbum",
                       success() {
                           resolve();
                       },
                       fail(e) {
                           page.initAuthStatus();
                           wx.showToast({
                               title: "保存需要您的授权哦~",
                               icon: "none"
                           });
                           reject(e);
                       }
                   });
               } else if (isAuthed) {
                   // 已同意,直接保存
                   resolve();
               } else {
                   // 已拒绝过,弹设置
                   reject(res);
                   page.openSetting();
               }
           });
       });
   },
   // 弹出授权设置
    openSetting() {
       wx.openSetting({
           success(e) {
               console.log(e);
           },
           fail(e) {
               console.error(e);
           }
       });
   },

2、动态获取wxml显示信息

在wxml内开发完成后,就可以开始绘制canvas了,言之动态即在于此,为了方便取节点,可以对需要查询的节点id取名,方便后面处理(见3.2)

// 获取界面dom节点的位置和尺寸
 nodeRect(selector, root) {
       if (page.query === undefined) {
           page.query = wx.createSelectorQuery();
       }
       return new Promise((resolve, reject) => {
           try {
               page.query
                   .select(selector)
                   .boundingClientRect(rect => {
                       if (!root) {
                           rect.left = rect.left - page.data.originX;
                           rect.top = rect.top - page.data.originY;
                       }
                       resolve(rect);
                   })
                   .exec();
           } catch (e) {
               console.error("获取节点" + selector + "信息失败");
               reject(e);
           }
       });
   },

3、绘制canvas (Be patient.)

至此,我们就可以正式开始使用cavans绘制了。

3.1、缓存图片

如果绘制canvas的过程中用到了图片,直接使用URI是不行的,需要先将图片缓存到本地,对此可以封装一个方法:

 // 获取任意网络图片的本地url
   _getLocalSrc(url) {
       console.log("url", url);

       return new Promise((resolve, reject) => {
           // 保存网络图片到本地缓存
           wx.downloadFile({
               url,
               success(res) {
                   if (res.statusCode !== 200) {
                       wx.showToast({
                           title: "保存二维码到本地失败",
                           icon: "none"
                       });
                       reject(res);
                   } else {
                       resolve(res.tempFilePath);
                   }
               },
               fail(e) {
                   reject(e);
               }
           });
       });
   },

需要多张图片的情况下,再封装一层

 getGoodsNeedImgSrc() {
       const { QrcodeUrl, p } = page.data;
       return Promise.all([
           page._getLocalSrc(p.imageList[0]),
           page._getLocalSrc(posterConfig.titleBgUrl),
           page._getLocalSrc(QrcodeUrl)
       ]);
   },

3.2、开始绘制

 drawGoodsPoster() {
       return new Promise(async (resolve, reject) => {
           try {
               let [
                   goodsImgSrc,
                   titleBgSrc,
                   QrcodeSrc
               ] = await page.getGoodsNeedImgSrc();

               const p = page.data.p;

               let ratio = page.ratio;
               if (!ratio) {
                   ratio = 750 / wx.getSystemInfoSync().windowWidth;
                   page.ratio = ratio;
               }

               console.log("开始画画");

               const ctx = wx.createCanvasContext("myCanvas");

               /**
                * r:获取到的dom节点的位置大小信息
                * 后面覆盖前面
                */
               // 画背景
               let r = await page.nodeRect("#goods");
               // 设原点坐标
               page.setData({
                   originX: r.left,
                   originY: r.top
               });
               // console.log('背景rect', r);

               const canvasWidth = r.width;
               const canvasHeight = r.height;
               ctx.setFillStyle(posterConfig.bgColor);
               ctx.fillRect(0, 0, r.width, r.height);

               // 画商品
               r = await page.nodeRect("#goodsImg");
               ctx.drawImage(goodsImgSrc, r.left, r.top, r.width, r.height);

               // 画标题一
               // 标题背景
               r = await page.nodeRect("#titleBg");
               ctx.drawImage(titleBgSrc, r.left, r.top, r.width, r.height);
               ctx.setTextBaseline("top");

               // 标题一文字
               // ctx.save()

               r = await page.nodeRect("#p1-1");
               // ctx.font = `sans-serif ${62/ratio}px bold`
               ctx.setFontSize(54 / ratio);
               ctx.setFillStyle("#fff");
               ctx.fillText(p.couponPriceUi + "元", r.left, r.top);
               r = await page.nodeRect("#p1-2");
               ctx.setFontSize(36 / ratio);
               ctx.fillText("券", r.left, r.top);

               r = await page.nodeRect("#p1-3");
               ctx.setFontSize(54 / ratio);
               // ctx.font = `${62/ratio}px sans-serif bold;`
               ctx.fillText("+" + p.rebatePriceUi + "元", r.left, r.top);

               r = await page.nodeRect("#p1-4");
               ctx.setFillStyle("#fff");
               ctx.setFontSize(36 / ratio);
               ctx.fillText("返利", r.left, r.top);
               // ctx.restore()

               // 画标题二
               // 标题二背景
               r = await page.nodeRect("#goodsDetail");
               ctx.setFillStyle("#fff");
               ctx.fillRect(r.left, r.top, r.width, r.height);
               // 标题二文字
               r = await page.nodeRect("#p2-1");
               // ctx.font = `sans-serif ${28/ratio}px bold`
               ctx.setFontSize(28 / ratio);
               ctx.setFillStyle("#000");
               ctx.fillText(p.couponName, r.left, r.top);

               r = await page.nodeRect("#p2-2");
               // ctx.font = `${28/ratio} sans-serif`
               ctx.setFontSize(28 / ratio);
               ctx.fillText(p.goodsName1, r.left, r.top);

               r = await page.nodeRect("#p2-3");
               ctx.fillText(p.goodsName2, r.left, r.top);

               // 画标题三
               ctx.setTextBaseline("bottom");
               r = await page.nodeRect("#p3-1");
               ctx.setFontSize(26 / ratio);
               ctx.setFillStyle("#F92E0C");
               ctx.fillText("到手价:¥", r.left, r.top + r.height);

               r = await page.nodeRect("#p3-2");
               ctx.setFontSize(38 / ratio);
               ctx.setFillStyle("#F92E0C");
               ctx.fillText(p.showPriceIntUi, r.left, r.top + r.height);

               r = await page.nodeRect("#p3-3");
               ctx.setFontSize(26 / ratio);
               ctx.setFillStyle("#F92E0C");
               ctx.fillText(p.showPriceFloatUi, r.left, r.top + r.height);

               r = await page.nodeRect("#p3-4");
               ctx.setFontSize(26 / ratio);
               ctx.setFillStyle("#999");
               ctx.fillText(p.marketPriceUi, r.left, r.top + r.height);
               // 画删除线
               ctx.fillRect(r.left, r.top + r.height / 2, r.width, 1);

               r = await page.nodeRect("#p3-5");
               ctx.setFontSize(26 / ratio);
               ctx.setFillStyle("#999");
               ctx.fillText(p.saleTextUi, r.left, r.top + r.height);

               // 画tips
               ctx.setTextBaseline("top");
               r = await page.nodeRect("#p4-1");
               ctx.setTextAlign("center");
               ctx.setFontSize(30 / ratio);
               ctx.setFillStyle("#fff");
               ctx.fillText(
                   page.data.QrcodeText1,
                   r.left + r.width / 2,
                   r.top
               );

               r = await page.nodeRect("#p4-2");
               ctx.setFontSize(30 / ratio);
               ctx.fillText(
                   page.data.QrcodeText2,
                   r.left + r.width / 2,
                   r.top
               );

               // 画二维码
               r = await page.nodeRect("#Qrcode");
               ctx.save();
               ctx.beginPath(); //开始绘制
               ctx.arc(
                   r.width / 2 + r.left,
                   r.width / 2 + r.top,
                   r.width / 2,
                   0,
                   Math.PI * 2,
                   false
               );
               ctx.setLineWidth(0);
               // ctx.stroke(); //画空心圆
               ctx.closePath();
               ctx.clip();
               ctx.drawImage(QrcodeSrc, r.left, r.top, r.width, r.height);
               ctx.restore(); //恢复之前保存的绘图上下文 恢复之前保存的绘图问下文即状态

               console.log("开始绘制...");

               await page._drawCanvas({
                   canvasWidth,
                   canvasHeight,
                   ctx
               });
               wx.hideLoading();
               resolve();
           } catch (e) {
               reject(e);
           }
       });
   },

3.3、从缓存存入本地


   _drawCanvas({ canvasWidth = 680, canvasHeight = 1030, ctx }) {
       return new Promise((resolve, reject) => {
           // 绘制到canvas
           ctx.draw(true, () => {
               setTimeout(() => {
                   cb();
               }, 300);
               const cb = () => {
                   page.setData({
                       isCanvasDrawed: true
                   });
                   // 将canvas存入本地
                   wx.canvasToTempFilePath({
                       x: 0,
                       y: 0,
                       width: canvasWidth,
                       height: canvasHeight,
                       destWidth: canvasWidth * 2,
                       destHeight: canvasHeight * 2,
                       canvasId: "myCanvas",
                       success(res) {
                           console.log("canvasToTempFilePath res", res);
                           if (res.errMsg !== "canvasToTempFilePath:ok") {
                               reject();
                               wx.showToast({
                                   title: "保存canvas到缓存失败,请稍后再试",
                                   icon: "none"
                               });
                           } else {
                               resolve();
                               page.posterPath = res.tempFilePath;
                           }
                       },
                       fail(e) {
                           console.error(e);
                       }
                   });
               };
           });
       });
   },

踩坑小记

绘制出的海报没有二维码

  • 基于业务需要,海报通常包含小程序二维码,忽略控制二维码加载与绘制海报的运行顺序的话会出现绘制出的海报没有二维码等异常情况,解决方法是将步骤解耦顺序执行,结imgae组件的bindload
// 生成前校验
 beforeSave() {
       return new Promise((resove, reject) => {
           if (!page.data.isQrcodeLoaded) {
               reject("二维码未加载完成");
               wx.showToast({
                   title: "请等待二维码加载哦~",
                   icon: "none"
               });
           } else {
               resove();
           }
       });
   },
  // 二维码image bindload事件
    QrcodeLoaded(e) {
       page.setData({
           isQrcodeLoaded: true
       });
   },

http://www.niftyadmin.cn/n/913994.html

相关文章

网络对抗技术 -网络侦查与网络扫描

中国人民公安大学 Chinese people’ public security university 网络对抗技术 实验报告 实验一 网络侦查与网络扫描 学生姓名 翟一鸣 年级 2014级 区队 2区 指导教师 高见 信息技术与网络安全学院 2017年7月7日 实验任务总纲 2017—2018 学年 第 一 学期 一、实验…

解决银河麒麟kylin.desktop-generic编译生成的程序执行报错“权限不够”

问题复现 编写一个简单的a.c程序 #include <stdio.h>void main(){printf("zzz\n"); } 编译该程序&#xff0c;生成a.out可执行程序 greatwallgreatwall-KVM-Virtual-Machine:~/cproj$ gcc a.c greatwallgreatwall-KVM-Virtual-Machine:~/cproj$ ll 总用量…

I am come back

好久没写&#xff0c;这段时间好忙&#xff0c;忙工作&#xff0c;忙着玩&#xff0c;忙着发呆&#xff0c;生活中无事不忙&#xff0c;事事忙。突然又想起“不做无益之事&#xff0c;何以遣有崖之生&#xff1f;”。上周末在深圳的朋友们小聚&#xff0c;爽歪歪。转载于:https…

微信小程序“无埋点”式封装上报

无论什么业务&#xff0c;c端产品的数据统计、埋点上报是很重要的&#xff0c;它可以帮助产品人更好地了解用户行为&#xff0c;以快速响应迭代。 所谓“无埋点”式是指不在业务代码里插入上报代码&#xff0c;将业务代码与上报代码分开&#xff0c;提高项目可维护性和可读性。…

java set list 区别

set :是有序不可重复的集合 list:无序可以重复的集合 eg:set public class Test1 {public static void main(String[] args){Set s new HashSet();s.add(1);s.add(2);s.add(3);s.add(5);s.add(5);s.add(4);s.add(8);s.add(6);Iterator<Integer> it s.iterator();while(…

taro微信小程序云开发-实现用户信息增改查

趁周末空闲时&#xff0c;&#xff08;没jojo看我要死了&#xff09;捣鼓了一下taro和小程序的云开发。 体验很好&#xff0c;作博一篇抛砖引玉&#xff0c;也给想有些想自己做小程序又不会写后端的人略做引导&#xff0c;以求少走些弯路。 几个月后看自己的这篇博文&#xff0…

JAVA泛型使用方法总结

1. 基本概念&#xff1a; &#xff08;1&#xff09;什么是泛型&#xff1f;   泛型&#xff0c;即“参数化类型”。即将类型由原来的具体的类型参数化&#xff0c;类似于方法中的变量参数&#xff0c;此时类型也定义成参数形式&#xff08;可以称之为类型形参&#xff09;&a…

.Net 2005 中通过MasterPage来更方便实现网站模板替换

前面曾在.net2003下做一个blog&#xff0c;由于也是要用到模板技术。多方查找&#xff0c;找了一个第三方自己实现的MasterPage组件来实现对每天blog用户模板更换的实现。不过&#xff0c;比使用&#xff0c;实现方面还是比较订烦的。制作一个新的模块必须有程序员的大量参于。…