UzumakiItachi
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

UzumakiItachi

起风了,唯有努力生存。
首页
  • JavaSript
  • Vue

    • Vue2
    • Vue3
  • React

    • React_18
  • WebPack
  • 浏览器相关
  • 工程化相关
  • 工作中遇到的问题以及解决方案
  • Git
  • 面试
  • 学习
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
  • 个人产出
  • 实用工具
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • WebPack

  • 浏览器相关

  • 工程化相关

  • 工作中遇到的问题以及解决方案

    • 解决浏览器返回页面不刷新的问题
    • 前端如何下载文件流
    • uniapp APP端实现更新最新安装包
    • GitHub Actions Process completed with exit code 128 的解决方案
    • 保留文字输入的空格和换行
    • 缩放适配大屏页
    • 一次性加载n多张图片的性能优化方案
    • NOT-Cool 低代码页面架构思路
    • h5下载vcard快捷保存联系人信息
    • 结合elementui实现的动态主题
    • 解决monorepo场景下子包作为依赖项在开发和生产暴露文件的问题
    • 给npm配置github令牌
    • 移动端电子印章解决方案
    • 小程序canvas绘制海报中遇到的一些坑
      • 前言
      • 实现思路
      • 开始
      • 优化
      • 总结
      • 后续
    • 超详细的虚拟列表实现过程
    • 小程序实现一个事件中心
    • 请求超时后如何优雅的重新请求
    • 超详细的大文件上传实现方案
    • 仿抖音短视频组件实现方案
    • swiper渲染大量数据的优化方案
    • 前端检测更新,自动刷新网页
  • Git

  • Vite

  • 一些小工具

  • 算法

  • 服务器

  • HTTP

  • 技术
  • 工作中遇到的问题以及解决方案
hanhanbuku
2024-04-22
目录

小程序canvas绘制海报中遇到的一些坑

# 前言

最近接到个需求在小程序要实现一个拍照打卡的功能,拍下的照片需要合成上我们小程序的logo还有专属边框。稍微思考了一下,这不就是获取用户拍的照片然后合成几张图嘛简简单单!直接开干!

# 实现思路

说到合成图片,那就不得不提到canvas了。利用canvas绘制图片然后调整好图片位置后导出图片临时路径就ok啦,但是微信小程序果然从不让人失望,文档写的跟屎一样坑一大堆官方不填,下面就从0到1实现一下来细数其中的坑。

# 开始

这里为了方便逻辑清晰 所以将合成图片这一步抽成了一个组件 问题开始了! 首先看一下基本代码

<ImageToPoster  ref="ImageToPoster" :width="473" :height="652" />

// ImageToPoster

<template>
  <view class="imgtop">
    <canvas ref="Canvas" :style="{'width':width+'px','height':height+'px'}" canvas-id="Canvas" id="Canvas"></canvas>
  </view>
</template>

<script>
export default {
  props: {
    width: {
      type: Number,
      default: 0
    },
    height: {
      type: Number,
      default: 0
    },
    step: {
      type: Number,
      default: 1
    }
  },
  data() {
    return {
      imagePath: '',
      bgImageTempPath: '', // 背景图的临时路径
      logoDetail:{},// logo图的信息
      logoImageTempPath1: '', // logo的临时路径
      logoImageTempPath2: '', // logo的临时路径
      logoImageTempPath3: '', // logo的临时路径
    }
  },
  mounted() {
    this.handleLoadImage()
  },
  methods:{
    // 提前加载素材图片
    handleLoadImage() {
      let that = this
      uni.getImageInfo({
        src: loadImage('hckdt.png'),
        success: function(image) {
          that.bgImageTempPath = image.path
        }
      });
      uni.getImageInfo({
        src: loadImage('logo1.png'),
        success: function(image) {
          that.logoDetail[1] = {
            path:image.path,
            width:image.width,
            height:image.height
          }
        }
      });
      uni.getImageInfo({
        src: loadImage('logo2.png'),
        success: function(image) {
          that.logoDetail[2] = {
            path:image.path,
            width:image.width,
            height:image.height
          }
        }
      });
      uni.getImageInfo({
        src: loadImage('logo3.png'),
        success: function(image) {
          that.logoDetail[3] = {
            path:image.path,
            width:image.width,
            height:image.height
          }
        }
      });
    },
    /**
     * 通过canvas合成图片
     * @param path 拍照的图片的临时路径
     * @param width1 拍照的图的宽度
     * @param height1 拍照的图的高度
     */
    handleCanvasToImage(path, width1, height1){
      const ctx = uni.createCanvasContext('Canvas', this)
      ctx.beginPath()
    }
  }
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

上文代码中是一个简易过的组件,里面一些重要代码保留了。这个组件主要就是提供一个canvas然后将传入的图片与素材图合成到一起最后导出临时路径。

  • 问题一 为什么要这么设计?

首先小程序不支持canvas被隐藏,所以我们的canvas画板一定得出现在页面上,那怎么办只能离屏(将canvas定位到超出屏幕之外的地方去)。

  • 问题二 获取不到canvas实例?

小程序中canvas 如果放在自定义子组件中,那么获取实例的时候必须传入子组件的this。否则会找不到canvas

解决两个基本的问题下面又有一个新的问题,那就是如何在canvas中对图片进行截取呢?

因为用户手机拍摄的照片大小都是不一样的,所以我们必须指定一个尺寸,然后在用户拍摄的照片上截取一个目标尺寸的图下来。但是要保证以中心区域为主。也就是说我们需要手动去实现一个img标签的cover功能了。

具体截取逻辑如下

handleCanvasToImage(path, width1, height1){
    const ctx = uni.createCanvasContext('Canvas', this)
    ctx.beginPath()
    // 获取目标图片的原始尺寸
    const originalWidth = width1;
    const originalHeight = height1;
    // 计算目标图片的宽高比和canvas的宽高比
    const imageRatio = originalWidth / originalHeight;
    const canvasRatio = 406 / 432;
    // 定义绘制区域的起点坐标和宽高
    let x = 0;
    let y = 0;
    let width = 0;
    let height = 0;
    // 判断以宽度为基准进行裁切还是以高度为基准进行裁切
    if (imageRatio > canvasRatio) {
        // 以宽度为基准进行裁切
        width = originalHeight * canvasRatio;
        height = originalHeight;
        x = (originalWidth - width) / 2;
    } else {
        // 以高度为基准进行裁切
        width = originalWidth;
        height = originalWidth / canvasRatio;
        y = (originalHeight - height) / 2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

获取到两个图片的宽高比后根据宽高比大小的不同来选择是以宽度为基准裁切还是以高度为基准裁切,最终得出裁切的起始坐标和裁切的图的宽度以及高度

ok,有了这个之后我们就可以开始着手绘制啦

handleCanvasToImage(path, width1, height1){
    const ctx = uni.createCanvasContext('Canvas', this)
    ctx.beginPath()
    // 获取目标图片的原始尺寸
    const originalWidth = width1;
    const originalHeight = height1;
    // 计算目标图片的宽高比和canvas的宽高比
    const imageRatio = originalWidth / originalHeight;
    const canvasRatio = 406 / 432;
    // 定义绘制区域的起点坐标和宽高
    let x = 0;
    let y = 0;
    let width = 0;
    let height = 0;
    // 判断以宽度为基准进行裁切还是以高度为基准进行裁切
    if (imageRatio > canvasRatio) {
        // 以宽度为基准进行裁切
        width = originalHeight * canvasRatio;
        height = originalHeight;
        x = (originalWidth - width) / 2;
    } else {
        // 以高度为基准进行裁切
        width = originalWidth;
        height = originalWidth / canvasRatio;
        y = (originalHeight - height) / 2;
    }
    // 绘制边框背景
    ctx.drawImage(that.bgImageTempPath, 0, 0, posterWidth, posterHeight)
    ctx.draw(true)
    // 绘制拍摄的照片
    ctx.drawImage(path, 0, 0, width, height, 33.5, 60, 406, 432);
    ctx.draw(true)
    // 绘制logo
    ctx.drawImage(that.logoDetail[that.step].path, 10, 462, that.logoDetail[that.step].width/3*2, that.logoDetail[that.step].height/3*2)
    ctx.draw(true)
    ctx.save()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

这里需要注意要调用ctx.draw才会绘制canvas,并且需要传入第一个参数true,画布才能保留上一次绘制的内容。 绘制完了之后再次调用小程序提供的Api来导出合成图的临时路径

uni.canvasToTempFilePath({
    x: 0,
    y: 0,
    width: posterWidth,
    height: posterHeight,
    destWidth: posterWidth,
    destHeight: posterHeight,
    canvasId: 'Canvas',
    success: function(res) {
        // 在H5平台下,tempFilePath 为 base64
        that.imagePath = res.tempFilePath
        resolve(that.imagePath)
    },
    fail(err) {
        reject('海报生成失败...')
        console.log(err)
    }
}, this)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

同样的这个api也需要传入this,不然也会报错。代码写到这里,掏出我的手机一测,嗯!不错但是图片好像有点糊。别急,一番搜索后发现导出图的宽高*手机的缩放比可以解决这个问题。

// 获取当前设备的图片缩放比
const pixelRatio = uni.getSystemInfo().pixelRatio || 2

uni.canvasToTempFilePath({
    x: 0,
    y: 0,
    width: posterWidth,
    height: posterHeight,
    destWidth: posterWidth * pixelRatio,
    destHeight: posterHeight * pixelRatio,
    canvasId: 'Canvas',
    success: function(res) {
        // 在H5平台下,tempFilePath 为 base64
        that.imagePath = res.tempFilePath
        resolve(that.imagePath)
    },
    fail(err) {
        reject('海报生成失败...')
        console.log(err)
    }
}, this)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

ok啦这下是真的ok啦!提交测试。测试一通操作后传来噩耗,有些机型导出了空白的图。

我开始慌了,这种兼容性问题是最让人头疼的。但是没办法 该调试还是得调试。于是我连上了第一个手机返现确实是这样偶尔会导出空白的图片, 于是我立马联想到会不会是因为canvas还没有画完就执行了导出方法。于是乎我给导出函数加了个定时器,延迟300ms执行。

联调过后发现没问题,提交给测试。几分钟后测试又说偶尔还是会出现那个问题,这个时候我确定了,就是因为canvas没有画完就执行了导出函数。确定了问题之后就比较好解决了,直接打开wx的文档

文档上有提到draw函数可以接收第二个参数,是一个回调函数,会在绘制完毕后调用。于是我又快马加鞭的加上

ctx.drawImage(that.bgImageTempPath, 0, 0, posterWidth, posterHeight)
    ctx.fillText(" ", 0, 0);
    ctx.draw(true,()=>{
        // 绘制拍摄的照片
        ctx.drawImage(path, 0, 0, width, height, 33.5, 60, 406, 432);
        ctx.fillText(" ", 0, 0);
        ctx.draw(true,()=>{
            // 绘制logo
            ctx.drawImage(that.logoDetail[that.step].path, 10, 462, that.logoDetail[that.step].width/3*2, that.logoDetail[that.step].height/3*2)
            ctx.fillText(" ", 0, 0);
            ctx.draw(true,()=>{
                const pixelRatio = uni.getSystemInfo().pixelRatio || 2
                setTimeout(()=>{
                    uni.canvasToTempFilePath({
                        x: 0,
                        y: 0,
                        width: posterWidth,
                        height: posterHeight,
                        destWidth: posterWidth * pixelRatio,
                        destHeight: posterHeight * pixelRatio,
                        canvasId: 'Canvas',
                        success: function(res) {
                            // 在H5平台下,tempFilePath 为 base64
                            that.imagePath = res.tempFilePath
                            resolve(that.imagePath)
                        },
                        fail(err) {
                            reject('海报生成失败...')
                            console.log(err)
                        }
                    }, that)
                },300)
            })
            ctx.save()
        }) // 需要传入true 才会保留上次绘制的内容
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

这下总没问题了吧。蛮喜欢喜的我准备下班了,结果测试又又又传来噩耗。这下更严重了,连空白的图都导不出来了。我立马掏出我的手机测试了一番,发现没问题。 依旧是丝滑流畅。那这可就令人头大了,api的兼容性问题,按照文档上写还没有用。文档也没说解决办法(这里真的忍不住吐槽微信的文档,真真真依托答辩)!

没办法,只能在社区找找找。翻了个底朝天终于看到一位大哥说的,遇到这种不执行的情况,第二个参数需要传入一个立即执行函数。我仿佛抓住了救民稻草一般。 加上之后发现果然没问题了。

    ctx.drawImage(that.bgImageTempPath, 0, 0, posterWidth, posterHeight)
    ctx.fillText(" ", 0, 0);
    ctx.draw(true,(()=>{
        // 绘制拍摄的照片
        ctx.drawImage(path, 0, 0, width, height, 33.5, 60, 406, 432);
        ctx.fillText(" ", 0, 0);
        ctx.draw(true,(()=>{
            // 绘制logo
            ctx.drawImage(that.logoDetail[that.step].path, 10, 462, that.logoDetail[that.step].width/3*2, that.logoDetail[that.step].height/3*2)
            ctx.fillText(" ", 0, 0);
            ctx.draw(true,(()=>{
                const pixelRatio = uni.getSystemInfo().pixelRatio || 2
                setTimeout(()=>{
                    uni.canvasToTempFilePath({
                        x: 0,
                        y: 0,
                        width: posterWidth,
                        height: posterHeight,
                        destWidth: posterWidth * pixelRatio,
                        destHeight: posterHeight * pixelRatio,
                        canvasId: 'Canvas',
                        success: function(res) {
                            // 在H5平台下,tempFilePath 为 base64
                            that.imagePath = res.tempFilePath
                            resolve(that.imagePath)
                        },
                        fail(err) {
                            reject('海报生成失败...')
                            console.log(err)
                        }
                    }, that)
                },300)
            })())
            ctx.save()
        })()) // 需要传入true 才会保留上次绘制的内容
    })())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

事情到这里就结束了,在过程中还有看到关于canvasToTempFilePath这个Api的问题,如果canvas采用的是2d模式,那在调用这个api的时候还需要传入canvas实例。不然会报错!

# 优化

由于合成的图片有点太大了,渲染的时候会出现一行一行渲染的情况。测试那边不满意,没办法只能改。一开始我以为是加载太慢,于是采取了先加载图然后再渲染的方式。后面发现无济于事 问题可能还是出在渲染上,之前有一次做web项目的时候也出现过图片太大导致页面非常卡顿,经过性能调试后发现是渲染过程导致的卡顿,图片太大导致解码耗费了很多时间。当时采取了渲染缩略图的方式来规避这个问题, 但是这里很显然没办法用缩略图,因为需求是用户要长按合成出来的图片保存。所以渲染的必须是最终的高清图。

那怎么办!就在我一筹莫展的时候,我看到了离屏渲染的canvas,诶!对了 图片我是不是也可以离屏渲染,等他渲染过一次之后第二次渲染就不会再出现卡顿的情况了。 具体的实现步骤这里就不贴代码了,很简单。就是用一个img标签再屏幕外先渲染一次图片然后再把图片渲染到用户可视区域中。

# 总结

一次简单的需求,炸出来这么多坑,关于wx小程序的canvas我只能说真的坑多,同一个api在不同手机上的运行结果不一致也就算了,也不报错啥也没有。他就在那不起作用,真的很让人无语。 下面总结一下几个点

  • canvas必须在页面中,不可以display:none也不可以透明度为0

  • canvas在自定义组件中使用的话 获取实例一定要传this

  • canvas的绘制函数需要调用draw才能绘制到画布中

  • draw需要传入true才能保留上一次绘制的结果,不然画布中就只有最后一次的结果

  • draw是支持回调的,有些需要等待绘制的场景可以用到,但是这个回调在某些机型上不执行,需要改为立即执行函数的方式才能解决

  • canvasToTempFilePath Api导出的图片糊的话,可以将实际图片宽高*手机的缩放比,能让图片更清晰。不要再盲目的设置2了,每个手机都不一样

  • canvasToTempFilePath Api在自定义组件中使用也需要传入this,否则会报错

# 后续

原本以为问题已经得到解决,后面有些机型依然存在这个问题。经过一番排查我发现一开始我的思路就错了,我把所有精力都放在canvas的绘制上,以为是这里出了问题。但后面发现是因为有些机型在拉起相机之后再回到小程序会导致小程序页面重载, 重载的过程中调用绘制函数导致了问题的发生(调用函数的时候canvas还没有加载完,这个时候绘制就会出现很多问题。)经过一番讨论后决定还是由服务端去合成图片,对于客户端来说这种兼容性问题太多了。如果是日活非常大的程序上这种需求都 应该由服务端去完成,最大程度的去降低兼容性带来的毁灭性bug。

编辑 (opens new window)
上次更新: 2024/05/29, 17:24:40
移动端电子印章解决方案
超详细的虚拟列表实现过程

← 移动端电子印章解决方案 超详细的虚拟列表实现过程→

最近更新
01
前端检测更新,自动刷新网页
06-09
02
swiper渲染大量数据的优化方案
06-06
03
仿抖音短视频组件实现方案
02-28
更多文章>
Theme by Vdoing | Copyright © 2023-2025 UzumakiItachi | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式