使用 uniapp + canvas 在小程序端生成海报,并解决图片模糊的问题
使用 uniapp + canvas 在小程序端生成海报,并解决图片模糊的问题
背景
想为答题小程序中的模拟考试功能,增加一个弹出显示考试成绩海报的功能,可以让用户下载图片。使用了原生的 canvas 绘制,但是发现图片模糊。
参考了网上其他网友的方法,在画布上进行绘制的时候,先放大尺寸,然后在 scale 缩小,或者 setTransform 缩小,发现都没解决。另辟蹊径,最终使用 image 图片显示,解决了这个问题。
模糊的原因
这就牵扯出一个概念,物理分辨率 和 逻辑分辨率。现在设备基本上都是高分屏,通常说的我们说的某个手机分辨率是物理分辨率,比如 iPhone 13 Pro 的物理分辨率是 1170 x 2532,但是逻辑分辨率是 390 x 844,也就是说,物理分辨率是 3 倍于逻辑分辨率( dpr = 3)。我们一般操作都是基于逻辑分辨率,但是实际显示的时候,会根据 dpr 进行放大或者缩小。
假设我们现在 canvas 的大小都是 10 * 10,那么 canvas 画完的照片中就会有 100 个(像素)点,也就是只有 100 个点的信息;但是到了高清屏中(如 dpr = 3),我们需要 400 个点的信息,原来的点不够用怎么办?于是就会有一套算法来自动生成这些点的信息,从而造成了模糊。
canvas 尺寸概念
Canvas 有两种 width、height:
一种是 width、height 属性,一般称其为画布尺寸,即图形绘制的地方。默认值分别为 300px、150px。
<canvas id="canvas" width="300" height="300"></canvas>
另一种是 css 样式里的 width、height 属性,可通过内联样式、内部样式表或外部样式表设置。一般称其为画板尺寸,用于渲染绘制完成的图形。默认值为空。
<canvas id="canvas" style="width:300px; height:300px;"></canvas>
我自己的理解,使用 style 设置的宽高,相当于一个视窗,透过它可以看到画布的内容。比如我上边背景提到的,canvas 绘制的图片模糊,可以在绘制的时候对画布进行放大,但是若是超过了 style 设置的宽高,那么图片就会显示不全。
解决办法
一开始想的是,画布在绘制图片的时候,直接对尺寸进行放大,但是这样会超过 style 的宽高,图片显示不完整。如果再进行缩放,图片就又会模糊。
其实有个好办法可以解决,可以在要展示海报的位置放一个 image,然后通过 image 的 src 属性,将放大后的 canvas 画布的内容转为图片,这样就可以避免图片模糊的问题了。
<template>
<view class="painter" >
<!-- 用来制作海报的 canvas -->
<canvas
canvas-id="canvasShareId"
:style="{
width: 360 * pixelRatio + 'px',
height: 640 * pixelRatio + 'px',
}"></canvas>
<u-popup
:round="10"
closeable
:show="show"
@close="close"
@open="open"
:bgColor="'transparent'"
mode="center">
<!-- 担心有的机型太老,海报显示不完整,做一个溢出滚动 -->
<view
style="overflow: scroll"
:style="{ height: Number(screenHeight) - 120 + 'px' }">
<view>
<!-- 使用 image 图片显示,解决图片模糊的问题 -->
<!-- 图片尺寸,是我根据海报尺寸定的,比例要对 -->
<u--image
width="680rpx"
height="1280rpx"
:src="tempFilePath"
mode="aspectFit"></u--image>
</view>
<view class="canvas-btn-box">
<view class="btn-item"
><u-button icon="weixin-fill" class="btn-item" open-type="share"
>分享好友</u-button
></view
>
<view class="btn-item">
<u-button icon="download" class="btn-item" @click="downLoad"
>保存图片</u-button
></view
>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
data() {
return {
canvasInstance: null, //海报实例
currentImg:
'https://***.png', // 海报背景图
show: false,
tempFilePath: '',
pixelRatio: 1,
screenHeight: null,
level: null,
}
},
onLoad(){
uni.getSystemInfo({
success: (res) => {
this.pixelRatio = res.pixelRatio; //获取设备像素比,就是上文中的 dpr
this.screenHeight = res.screenHeight; //屏幕宽度
},
});
this.painterInit()
},
methods: {
open() {},
close() {
this.show = false;
},
painterInit() {
this.show = true;
this.canvasInstance = uni.createCanvasContext('canvasShareId');
// 开始绘制
this.drawBackgroundImage();
},
drawBackgroundImage() {
uni.showLoading({ title: 'loading...', icon: 'none' });
// 因为用的是线上图片,所以需要先下载图片。如果是本地图片,可以直接 drawImage
uni.downloadFile({
url: this.currentImg, // https图片要在微信后台配置downloadFile合法域名
success: (res) => {
if (res) {
// 绘制背景图
this.canvasInstance.drawImage(
res.tempFilePath, // 图片源 本地资源直接传入路径 "/static/images/xx.png"
0, // 图像的左上角在目标canvas上 X 轴的位置
0, // 图像的左上角在目标canvas上 Y 轴的位置
360 * this.pixelRatio, // 在目标画布上绘制图像的宽度,允许对绘制的图像进行缩放
640 * this.pixelRatio // 在目标画布上绘制图像的高度,允许对绘制的图像进行缩放
);
// 绘制文字
let scoreFontSize = Number(this.pixelRatio) * 14 + 'px';
this.canvasInstance.font = `${scoreFontSize} '黑体' `;
this.canvasInstance.fillText(
`生产知识成绩:100分`,
40 * this.pixelRatio,
424 * this.pixelRatio
);
this.canvasInstance.fillText(
`生产管理能力成绩:100分`,
40 * this.pixelRatio,
444 * this.pixelRatio
);
let levelFontSize = Number(this.pixelRatio) * 28 + 'px';
this.canvasInstance.font = `${levelFontSize} 'STXingkai' `;
this.canvasInstance.fillText(
'优秀',
115 * this.pixelRatio,
492 * this.pixelRatio
);
let timeFontSize = Number(this.pixelRatio) * 10 + 'px';
this.canvasInstance.font = `${timeFontSize} '黑体' `;
this.canvasInstance.fillText(
`${formatDate(new Date())}`,
240 * this.pixelRatio,
632 * this.pixelRatio
);
// 开始生成
this.canvasInstance.draw(false, () => {
// 把绘制好的画布转为临时路径
uni.canvasToTempFilePath({
x: 0, // 画布x轴起点
y: 0, // 画布y轴起点
width: 360 * this.pixelRatio,
height: 640 * this.pixelRatio,
destWidth: 360 * this.pixelRatio, // 输出图片宽度(默认为 width * 屏幕像素密度)
destHeight: 640 * this.pixelRatio, // 输出图片高度(默认为 height * 屏幕像素密度)
quality: 1, // 图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
fileType: 'png',
canvasId: 'canvasShareId',
success: (res) => {
this.tempFilePath = res.tempFilePath;
},
});
});
}
},
complete() {
uni.hideLoading();
},
});
},
onShareAppMessage() {
return {
title: '邀请好友一起来拿证!',
path: '/pages/index/index', //页面 path ,必须是以 / 开头的完整路径
imageUrl: this.tempFilePath,
desc: '赶快跟我一起来拿证!',
};
},
onShareTimeline() {
return {
title: '邀请好友一起来拿证!',
path: '/pages/index/index', //页面 path ,必须是以 / 开头的完整路径
imageUrl: this.tempFilePath,
desc: '赶快跟我一起来拿证!',
};
},
// 保存图片
downLoad() {
uni.showLoading({ title: 'loading...', icon: 'none' });
uni.canvasToTempFilePath({
x: 0, // 画布x轴起点
y: 0, // 画布y轴起点
width: 360 * this.pixelRatio,
height: 640 * this.pixelRatio,
destWidth: 360 * this.pixelRatio, // 输出图片宽度(默认为 width * 屏幕像素密度)
destHeight: 640 * this.pixelRatio, // 输出图片高度(默认为 height * 屏幕像素密度)
quality: 1, // 图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理
canvasId: 'canvasShareId',
success: function (res) {
// 在H5平台下,tempFilePath 为 base64
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath, // 图片文件路径,临时文件路径或者是永久文件路径,不支持网络图片路径
success: function () {
setTimeout(() => {
// success 触发 => complete 触发(hideLoading 会关闭 showToast) => 所以使用异步;
uni.showToast({
title: '下载成功!',
duration: 2000,
});
}, 100);
},
fail() {
// 未开启授权
uni.authorize({
scope: 'scope.writePhotosAlbum', // 发起授权
success() {
uni.showToast({
title: '授权成功,请重新下载!',
icon: 'none',
duration: 2000,
});
},
// 如果以前拒绝过这个授权 那么直接进入 fail 回调 使用 openSetting 打开设置进行重新授权
fail() {
uni.showToast({
title: '请允许保存图片授权!',
icon: 'none',
duration: 2000,
});
uni.openSetting({
success() {
uni.showToast({
title: '授权成功,请重新下载!',
icon: 'none',
duration: 2000,
});
},
fail() {
uni.showToast({
title: '授权失败',
icon: 'none',
duration: 2000,
});
},
});
},
});
},
complete() {
uni.hideLoading();
},
});
},
});
},
}
}
</script>
<style>
.painter {
margin-top: 500px;
}
.canvas-btn-box {
display: flex;
justify-content: space-around;
}
.canvas-btn-box .btn-item {
width: 32%;
}
</style>
如果使用了下载图片功能,千万注意
如果出现了开发环境、或者真机调试环境可以下载,但是上线后无法下载图片的情况,请检查以下两点:
- 一定要在小程序微信公众平台配置 downloadFile 合法域名;
- 一定要在小程序微信公众平台《服务内容声明》中,去完善《用户隐私保护指引》;