duai36D 2025-08-22 14:18 采纳率: 0%
浏览 22

微信小程序真机调试Canvas绘制失败

uniapp 画布Canvas绘制失败
在开发者工具中保存图片成功没有问题,真机调试就不行,打印到console.log('开始绘制');后面的ctx.draw就不执行了,这是什么原因导致的,该怎么解决。

const downloadPoster = async () => {
        uni.showLoading({ title: '生成中', mask: true })
        try {
            const ctx = uni.createCanvasContext('myCanvas', proxy)
            if (!ctx) {
                return uni.showToast({ title: 'canvas 未初始化', icon: 'none' })
            }
            const scale = 2; // 设计稿转canvas的比例
            const poster = state.posterList[state.selectPoster];
            const components = JSON.parse(poster?.pageComponents || '[]');

            const [bgInfo, avatarImg, levelIconImg, qrcodeImg] = await Promise.all([
                getImageInfoAsync(poster?.picUrl),
                getImageInfoAsync(info.value.avatar),
                getImageInfoAsync(state.member.picUrl),
                getImageInfoAsync(state.qrcode),
            ]) as any

            ctx.drawImage(bgInfo.path, 0, 0, 554, 970);// 背景图

            for (const comp of components) {
                if (comp.type === 1) {
                    const avatarSize = 65;
                    const avatarRadius = avatarSize / 2;
                    const nickname = info.value.nickname || '匿名';
                    const gradeText = info.value.memberName || '游客';

                    // 用户信息组件的起点位置
                    const startX = comp.x * scale;
                    const startY = comp.y * scale;

                    // 头像绘制
                    const avatarX = startX;
                    const avatarY = startY + 6;
                    ctx.save();
                    ctx.beginPath();
                    ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, 2 * Math.PI);
                    ctx.clip();
                    ctx.drawImage(avatarImg.path, avatarX, avatarY, avatarSize, avatarSize);
                    ctx.restore();

                    // 昵称绘制
                    const nameX = avatarX + avatarSize + 8; // 头像右侧间距
                    const nameY = avatarY + 4;

                    ctx.setFontSize(24);
                    ctx.setFillStyle('#804500');
                    ctx.setTextAlign('left');
                    ctx.setTextBaseline('top');
                    ctx.fillText(nickname, nameX, nameY);

                    // 等级背景 + 图标 + 文字绘制
                    const iconWidth = 28;
                    const iconHeight = 28;
                    const iconMarginRight = 4;
                    const paddingX = 6;
                    const paddingY = 6;

                    ctx.setFontSize(20);
                    const textWidth = ctx.measureText(gradeText).width;
                    const gradeWidth = iconWidth + iconMarginRight + textWidth + paddingX * 2;
                    const gradeHeight = 20 + paddingY * 2;

                    const gradeX = nameX;
                    const gradeY = nameY + 30;

                    const radius = 18;
                    ctx.beginPath();
                    ctx.setFillStyle(state.member?.labelColor);

                    // 左上角圆角
                    ctx.moveTo(gradeX + radius, gradeY);
                    ctx.quadraticCurveTo(gradeX, gradeY, gradeX, gradeY + radius);
                    // 左边
                    ctx.lineTo(gradeX, gradeY + gradeHeight - radius);
                    // 左下角(直角)
                    ctx.lineTo(gradeX, gradeY + gradeHeight);
                    ctx.lineTo(gradeX + gradeWidth - radius, gradeY + gradeHeight);
                    // 右下角圆角 
                    ctx.quadraticCurveTo(gradeX + gradeWidth, gradeY + gradeHeight, gradeX + gradeWidth, gradeY + gradeHeight - radius);
                    // 右边
                    ctx.lineTo(gradeX + gradeWidth, gradeY + radius);
                    // 右上角(直角)
                    ctx.lineTo(gradeX + gradeWidth, gradeY);
                    ctx.lineTo(gradeX + radius, gradeY); // 顶部回到起点
                    ctx.closePath();
                    ctx.fill();

                    // 等级图标绘制(垂直居中)
                    const iconX = gradeX + paddingX;
                    const iconY = gradeY + (gradeHeight - iconHeight) / 2;
                    ctx.drawImage(levelIconImg.path, iconX, iconY, iconWidth, iconHeight);
                    // 等级文字绘制
                    const textX = iconX + iconWidth + iconMarginRight;
                    const textY = gradeY + gradeHeight / 2;

                    ctx.setFillStyle(state.member.fontColor);
                    ctx.setTextAlign('left');
                    ctx.setTextBaseline('middle');
                    ctx.fillText(gradeText, textX, textY);
                }

                // 二维码
                if (comp.type === 2) {
                    ctx.drawImage(qrcodeImg?.path, comp.x * scale, comp.y * scale, 170, 170);
                }
            }
            console.log('开始绘制');

            // 确保所有绘制完成再导出
            ctx.draw(true, () => {
                console.log('draw 完成,开始导出')
                uni.canvasToTempFilePath({
                    canvasId: 'myCanvas',
                    destWidth: 554 * scale,
                    destHeight: 970 * scale,
                    success: res => {
                        state.previewImgUrl = res.tempFilePath
                        uni.hideLoading()
                        if (process.env.UNI_PLATFORM === 'h5') {
                            state.showImgUrl = true
                            uni.showToast({ title: '请长按图片保存', icon: 'none' })
                        } else {
                            saveToAlbum(res.tempFilePath)
                        }
                    },
                    fail: err => {
                        console.error('导出失败', err);
                        uni.hideLoading();
                        uni.showToast({ title: '导出失败' + err, icon: 'none' });
                    },
                    complete() {

                    }
                }, proxy)
            })

        } catch (e) {
            console.error('生成出错', e)
            uni.showToast({ title: '生成失败', icon: 'none' })
        } finally {
            uni.hideLoading()
        }
    }

  • 写回答

4条回答 默认 最新

  • 檀越@新空间 2025-08-22 14:19
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    在使用 微信小程序UniApp 进行 Canvas 绘制 时,真机调试失败 是一个常见的问题。你提到的 console.log('开始绘制'); 后面的 ctx.draw() 不执行,说明代码在真机上可能没有正确执行到 ctx.draw(),或者存在某些兼容性或性能限制


    🚨 原因分析

    1. Canvas 上下文未正确初始化

      • 在部分机型中,uni.createCanvasContext('myCanvas', proxy) 可能返回 null,导致后续绘制不执行。
      • 注意: uni.createCanvasContext 需要确保 <canvas> 标签已渲染完成。
    2. 异步加载图片未完成就调用绘制

      • 使用了 Promise.all([...]) 加载图片,但可能在图片未完全加载时就开始绘制,导致绘制失败。
    3. Canvas 绘制方法不支持

      • 某些 Canvas 方法(如 ctx.arc, ctx.clip, ctx.quadraticCurveTo)在部分平台上可能存在兼容性问题。
    4. Canvas 宽高设置不当

      • 如果 <canvas> 的宽高设置为 0 或未指定,可能导致绘制失败。
    5. 真机环境限制(如内存/性能)

      • 真机对 Canvas 的绘制有性能限制,尤其当图像较大或复杂时。

    ✅ 解决方案

    1. 确保 Canvas 初始化成功

    const ctx = uni.createCanvasContext('myCanvas', proxy);
    if (!ctx) {
        console.error('Canvas 上下文创建失败');
        return uni.showToast({ title: 'Canvas 初始化失败', icon: 'none' });
    }
    

    重点: 必须检查 ctx 是否为 null,避免后续操作报错。


    2. 确保图片加载完成后再进行绘制

    const [bgInfo, avatarImg, levelIconImg, qrcodeImg] = await Promise.all([
        getImageInfoAsync(poster?.picUrl),
        getImageInfoAsync(info.value.avatar),
        getImageInfoAsync(state.member.picUrl),
        getImageInfoAsync(state.qrcode),
    ]);
    

    重点: 使用 await 确保所有图片加载完成后才执行绘制。


    3. 设置 Canvas 的宽高属性

    <canvas 
        style="width: 554px; height: 970px;" 
        canvas-id="myCanvas" 
        id="myCanvas"
    ></canvas>
    

    重点: 在页面中明确设置 canvas 的宽度和高度,否则可能无法正常绘制。


    4. 检查并调整 Canvas 方法兼容性

    • 尝试简化绘制逻辑,例如:
      • 使用 ctx.fillRect() 替代复杂的路径绘制。
      • 避免使用 ctx.clip()ctx.quadraticCurveTo() 等复杂方法。
      • 如果必须使用,建议在真机上测试是否有兼容性问题。

    5. 添加日志排查执行流程

    console.log('开始绘制');
    ctx.drawImage(bgInfo.path, 0, 0, 554, 970);
    console.log('背景图绘制完成');
    
    for (const comp of components) {
        // ...
        console.log('组件绘制完成');
    }
    
    // 最后调用 draw
    ctx.draw(false, () => {
        console.log('Canvas 绘制完成');
    });
    

    重点: 通过日志判断绘制是否执行到某一步,定位具体失败点。


    6. 修改后的完整示例代码

    const downloadPoster = async () => {
        uni.showLoading({ title: '生成中', mask: true });
        try {
            const ctx = uni.createCanvasContext('myCanvas', proxy);
            if (!ctx) {
                return uni.showToast({ title: 'Canvas 初始化失败', icon: 'none' });
            }
    
            console.log('Canvas 初始化成功');
    
            const scale = 2;
            const poster = state.posterList[state.selectPoster];
            const components = JSON.parse(poster?.pageComponents || '[]');
    
            const [bgInfo, avatarImg, levelIconImg, qrcodeImg] = await Promise.all([
                getImageInfoAsync(poster?.picUrl),
                getImageInfoAsync(info.value.avatar),
                getImageInfoAsync(state.member.picUrl),
                getImageInfoAsync(state.qrcode),
            ]);
    
            console.log('图片加载完成');
    
            ctx.drawImage(bgInfo.path, 0, 0, 554, 970);
            console.log('背景图绘制完成');
    
            for (const comp of components) {
                if (comp.type === 1) {
                    // ... 其他绘制逻辑
                    console.log('组件绘制完成');
                }
            }
    
            ctx.draw(false, () => {
                console.log('Canvas 绘制完成');
                uni.canvasToTempFilePath({
                    canvasId: 'myCanvas',
                    success: (res) => {
                        console.log('保存图片成功', res.tempFilePath);
                        uni.hideLoading();
                        uni.saveImageToPhotosAlbum({
                            filePath: res.tempFilePath,
                            success: () => {
                                uni.showToast({ title: '保存成功' });
                            },
                            fail: () => {
                                uni.showToast({ title: '保存失败', icon: 'none' });
                            }
                        });
                    },
                    fail: (err) => {
                        console.error('Canvas 转换失败', err);
                        uni.showToast({ title: '生成失败', icon: 'none' });
                    }
                });
            });
    
        } catch (error) {
            console.error('绘制失败', error);
            uni.showToast({ title: '生成失败', icon: 'none' });
        } finally {
            uni.hideLoading();
        }
    };
    

    🔍 其他建议

    • 使用 uni.canvasToTempFilePath 替代 ctx.draw()
      在 UniApp 中,有些情况下 ctx.draw() 可能不生效,可以尝试直接使用 uni.canvasToTempFilePath() 来获取图片。

    • 检查设备权限
      如果是保存图片到相册,需确保用户已授权 writePhotosAlbum 权限。

    • 使用 uni.getSystemInfoSync() 检查设备信息
      了解当前设备的系统、分辨率等信息,便于优化 Canvas 绘制。


    ✅ 总结

    | 步骤 | 内容 | |------|------| | 1 | 检查 uni.createCanvasContext 是否返回有效上下文 | | 2 | 确保所有图片加载完成再进行绘制 | | 3 | 设置 Canvas 的宽高属性 | | 4 | 简化或替换复杂 Canvas 方法 | | 5 | 添加详细日志,定位绘制失败位置 | | 6 | 使用 uni.canvasToTempFilePath 替代 ctx.draw() |


    如果你能提供完整的 getImageInfoAsync 函数或 canvas 的 HTML 结构,我可以进一步帮你排查问题。

    评论

报告相同问题?

问题事件

  • 创建了问题 8月22日