uniapp canvas 生成海报并保存到相册
前言:
之前写过一篇canvas小程序画图只要是canvas各种方法的实际应用,有兴趣的小伙伴也可以看看
微信小程序:使用canvas 生成图片 并分享_小程序canvas生成图片-CSDN博客
上一篇文章是小试牛刀,这次是更加全面的记录生成海报的实战应用
一.实现核心原理
1.uni.createCanvasContext----创建canvas画布
uni.createCanvasContext(canvasId, componentInstance) | uni-app官网
2.uni.canvasToTempFilePath ---- 保存临时图片uni.canvasToTempFilePath(object, componentInstance) | uni-app官网
3.uni.saveImageToPhotosAlbum ----下载图片到本地
uni-app官网
利用canvas 先画出二倍图(这样导出的图片不会模糊),canvas容器本身通过visibility: hidden 进行隐藏,实际用image展示画好的海报,并通过uni.saveImageToPhotosAlbum 可进行下载。
二.实现代码
模板部分:
<!-- canvas 绘制海报 -->
<view class="poster_box" v-if="showPoster">
<view class="poster_title_box">
<view class="poster_title">生成海报</view>
<image @click="() => showPoster = false" class="poster_close"
src="close.png"
mode="scaleToFill" />
</view>
<image class="canvas_image" :src="lastGeneratedImage" alt="" show-menu-by-longpress/>
<view class="poster_save_box" @click="savePosterByQR">
<image class="poster_save"
src="https://bhk-cms.oss-accelerate.aliyuncs.com/wechat/static/image/poster/poster_save.png"
mode="scaleToFill" />
<view class="poster_save_text">保存海报到本地</view>
</view>
<canvas canvas-id="positionPoster" class="canvas" :style="{width: canvasW,height:canvasH}">
</canvas>
</view>
<!-- 海报遮罩 -->
<view v-if="showPoster" class="mask" @click="() => showPoster = false"></view>
样式部分:
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #00000066;
z-index: 120;
}
.poster_box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
z-index: 121;
width: 606rpx;
.poster_title_box {
width: 100%;
text-align: center;
position: relative;
font-family: PingFang SC;
margin-bottom: 48rpx;
.poster_title {
font-size: 32rpx;
font-weight: 500;
line-height: 48rpx;
color: #fff;
}
.poster_close {
position: absolute;
top: 0;
bottom: 0;
margin: auto;
right: 0;
width: 48rpx;
height: 48rpx;
}
}
.poster_save_box {
margin-top: 40rpx;
.poster_save {
width: 96rpx;
height: 96rpx;
margin-bottom: 8rpx;
}
.poster_save_text {
font-family: PingFang SC;
font-size: 28rpx;
font-weight: 500;
line-height: 44rpx;
text-align: center;
color: #fff;
}
}
.canvas_image {
width: 606rpx;
height: 924rpx;
}
.canvas {
visibility: hidden;
z-index: -999;
position: absolute;
}
}
vue部分:
function fillBg(ctx, height, color1, color2 = color1) {
const gradient = ctx.createLinearGradient(0, height, 0, 0); // 从下往上渐变
gradient.addColorStop(0, color1); // 起点颜色
gradient.addColorStop(1, color2); // 终点颜色
ctx.fillStyle = gradient;
}
function useCanvas() {
const showPoster = ref(false);
const canvas_finished = ref(true);
const lastGeneratedImage = ref(null);
const showPosterContent = ref(false);
// 获取屏幕宽度和设备像素比
const dpr = uni.getWindowInfo().pixelRatio; // 设备像素比
const logicalWidth = toPx(303); // 逻辑宽度
const logicalHeight = toPx(462); // 逻辑高度
const canvasWidth = logicalWidth * dpr; // 实际宽度
const canvasHeight = logicalHeight * dpr; // 实际高度
// const canvasWidth = logicalWidth; // 实际宽度
// const canvasHeight = logicalHeight; // 实际高度
//绘制海报
async function createPoster(options) {
canvas_finished.value = true;
showPosterContent.value = false;
const { originData, houseData, rmb_price, price } = options;
const width = toPx(303);
const height = toPx(462);
// 获取屏幕宽度(单位为 px)
const screenWidth = uni.getWindowInfo().screenWidth;
const screenHeight = uni.getWindowInfo().screenHeight;
// 检查是否已经生成过图像
if (lastGeneratedImage.value) {
// 如果已经生成过图像,直接使用上次的图像数据
const ctx = uni.createCanvasContext("positionPoster");
ctx.clearRect(0, 0, width, height); // 清除画布
ctx.drawImage(lastGeneratedImage.value, 0, 0); // 将上次的图像绘制回canvas
ctx.draw();
showPosterContent.value = true;
uni.hideLoading();
return; // 结束,不再重新生成
}
//初始化画布
const ctx = uni.createCanvasContext("positionPoster");
// ctx.scale(dpr, dpr);
// 提取圆角值
const bgRadius = [toPx(8), toPx(8), toPx(48), toPx(8)];
/* 背景 */
// 创建线性渐变(垂直方向,模拟 360deg)
fillBg(ctx, height, "#669CFF", "#1F7CFF");
/* 外层-圆角矩形开始 */
drawRoundedRect(ctx, 0, 0, width, height, toPx(8));
/* 外层-圆角矩形结束 */
//画logo和背景图
await drawImageWithCache(
ctx,
posterLogo,
toPx(14),
toPx(16),
toPx(130),
toPx(32)
);
await drawImageWithCache(
ctx,
posterBg,
toPx(229),
toPx(7),
toPx(56),
toPx(56)
);
//主要内容-content
// /* content-圆角矩形开始 */
ctx.setFillStyle("#fff");
drawRoundedRect(ctx, toPx(14), toPx(60), toPx(275), toPx(316), toPx(8));
/* content-圆角矩形结束 */
// banner
/* banner-圆角矩形开始 */
ctx.fillStyle = "#fff";
ctx.save(); // 保存当前绘制状态
drawRoundedRect(ctx, toPx(26), toPx(72), toPx(251), toPx(154), toPx(4));
ctx.clip(); // 限制绘制区域为圆角矩形
/* banner-圆角矩形结束 */
/* banner图绘制开始*/
const firstImg =
originData.thumbnail + "?x-oss-process=image/resize,w_251/quality,q_85";
await drawImageWithCache(
ctx,
firstImg,
toPx(26),
toPx(72),
toPx(251),
toPx(154)
);
// 恢复
ctx.restore();
/* banner图绘制结束*/
//title
ctx.fillStyle = "#000";
ctx.font = "18px PingFang SC";
ctx.setFontSize(toPx(18));
//多行显示
toFormateStr(
ctx,
originData.title,
toPx(26),
toPx(250),
toPx(26),
toPx(250)
);
//两室两厅一卫 |90㎡| 芭提雅
ctx.fillStyle = "#555";
ctx.font = "10px PingFang SC";
ctx.setFontSize(toPx(10));
const room =
houseData.bedroom === 0
? "开间"
: `${houseData.bedroom || "-"}卧${houseData.toilet || "-"}卫`;
const area = houseData.building || "-";
const location = houseData.area === 168 ? "芭提雅" : "曼谷";
const info = `${room} | ${area}㎡ | ${location} `;
ctx.fillText(info, toPx(26), toPx(302), toPx(250));
//tag标签
await drawImagesInRow(ctx, houseData);
//价格
drawPriceText(ctx, houseData, rmb_price, price);
//画图二维码
/* 二维码-圆角矩形开始 */
ctx.fillStyle = "#fff";
ctx.save(); // 保存当前绘制状态
drawRoundedRect(ctx, toPx(233), toPx(390), toPx(56), toPx(56), toPx(4));
ctx.clip();
await drawImageWithCache(
ctx,
originData.wx_qr_code,
toPx(237),
toPx(394),
toPx(48),
toPx(48)
);
// // 恢复
ctx.restore();
/* 二维码-圆角矩形结束 */
/* 右侧-文字部分 */
ctx.fillStyle = "#fff";
ctx.font = "14px PingFang SC";
ctx.setFontSize(toPx(14));
ctx.fillText("找大管家做省心业主", toPx(14), toPx(414), toPx(420));
ctx.fillStyle = "#e3eeff";
ctx.font = "8px PingFang SC";
ctx.setFontSize(toPx(8));
ctx.fillText("长按扫描二维码,查看详情", toPx(14), toPx(430), toPx(432));
// 渲染
// 保存本次生成的海报图为缓存
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "positionPoster",
width: canvasWidth,
height: canvasHeight,
destWidth: canvasWidth,
destHeight: canvasHeight,
success: function (res) {
lastGeneratedImage.value = res.tempFilePath; // 缓存图片
canvas_finished.value = false;
showPosterContent.value = true;
uni.showToast({
title: "绘制成功",
});
},
fail: function (error) {
uni.showToast({
title: "绘制失败",
});
},
complete: function complete() {
uni.hideLoading();
uni.hideToast();
},
});
});
}
//保存海报
let isSaving = false; // 用于标记当前操作是否正在进行
function savePosterByQR() {
// 如果正在保存,直接返回,不执行后续操作
if (isSaving) {
return;
}
isSaving = true; // 设置标记,表示正在保存操作
// 如果已缓存海报,直接返回
if (lastGeneratedImage.value) {
saveToUser(lastGeneratedImage.value);
} else {
isSaving = false; // 结束操作,重置标记
}
function saveToUser(tempFilePath) {
// 画板路径保存成功后,调用方法把图片保存到用户相册
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: (res) => {
showPoster.value = false;
uni.showToast({
title: "保存成功",
icon: "success",
});
isSaving = false; // 操作完成,重置标记
},
fail: (err) => {
uni.showToast({
title: "保存失败",
icon: "fail",
});
showPoster.value = false;
isSaving = false; // 操作完成,重置标记
},
});
}
}
//隐藏海报
function cancelPosterContent() {
showPoster.value = false;
showPosterContent.value = false;
}
return {
showPoster,
canvas_finished,
showPosterContent,
dpr,
lastGeneratedImage,
savePosterByQR,
createPoster,
cancelPosterContent,
};
}
//多行文字绘制
/**
* 将字符串格式化为适合在画布上显示的格式
*
* @param ctx 画布上下文
* @param str 需要格式化的字符串
* @param axisX 文本在画布上的起始x坐标
* @param axisY 文本在画布上的起始y坐标
* @param titleHeight 每行文本的高度
* @param maxWidth 每行文本的最大宽度
*/
//多行文本
export function toFormateStr(ctx, str, axisX, axisY, titleHeight, maxWidth) {
// 文本处理
let strArr = str.split("");
let row = [];
let temp = "";
for (let i = 0; i < strArr.length; i++) {
if (ctx.measureText(temp).width < maxWidth) {
temp += strArr[i];
} else {
i--; //这里添加了i-- 是为了防止字符丢失,效果图中有对比
row.push(temp);
temp = "";
}
}
row.push(temp); // row有多少项则就有多少行
//如果数组长度大于2,现在只需要显示两行则只截取前两项,把第二行结尾设置成'...'
if (row.length > 2) {
let rowCut = row.slice(0, 2);
let rowPart = rowCut[1];
let test = "";
let empty = [];
for (let i = 0; i < rowPart.length; i++) {
if (ctx.measureText(test).width < maxWidth) {
test += rowPart[i];
} else {
break;
}
}
empty.push(test);
let group = empty[0] + "..."; //这里只显示两行,超出的用...表示
rowCut.splice(1, 1, group);
row = rowCut;
}
// 把文本绘制到画布中
for (let i = 0; i < row.length; i++) {
// 一次渲染一行
ctx.fillText(row[i], axisX, axisY + i * titleHeight, maxWidth);
}
// // 保存当前画布状态
// ctx.save();
// // 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中。
// ctx.draw();
}
//绘制图片
export function drawImageWithCache(context, imgUrl, x, y, width, height) {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imgUrl,
success: function (res) {
context.drawImage(res.path, x, y, width, height);
resolve();
},
fail: function (err) {
console.error("图片加载失败:", err);
reject(err);
},
});
});
}
//绘制圆角矩形
export function drawRoundedRect(
ctx,
x,
y,
width,
height,
r1,
r2 = r1,
r3 = r1,
r4 = r1
) {
ctx.beginPath();
// 左上角
ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5);
// 上边线
ctx.lineTo(x + width - r2, y);
ctx.arc(x + width - r2, y + r2, r2, Math.PI * 1.5, Math.PI * 2);
// 右边线
ctx.lineTo(x + width, y + height - r3);
ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI * 0.5);
// 下边线
ctx.lineTo(x + r4, y + height);
ctx.arc(x + r4, y + height - r4, r4, Math.PI * 0.5, Math.PI);
// 左边线
ctx.lineTo(x, y + r1);
ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5);
ctx.closePath(); // 闭合路径
ctx.fill(); // 填充路径
}
//多行tag
export async function drawImagesInRow(ctx, houseData) {
// 设置起始位置和间距
let currentX = toPx(26); // 起始位置,x 坐标
const yPosition = toPx(312); // 图片绘制的 y 坐标
const spacing = toPx(4); // 图片之间的间距
// 遍历每个图像 URL,依次绘制图片
for (let i = 0; i < imageUrls.length; i++) {
const image = imageUrls[i];
// 异步加载并绘制图片
await drawImageWithCache(
ctx,
image.src,
currentX,
yPosition,
image.width,
image.height
);
// 更新当前的 x 坐标,图像绘制完成后将 x 坐标向右移动,考虑到间距
currentX += image.width + spacing;
}
}
//单位转换
export function toPx(px) {
const devicePi = uni.getWindowInfo().pixelRatio;
const screenWidth = uni.getWindowInfo().screenWidth;
return (px / 375) * screenWidth;
}