Files
shoot-miniprograms/src/canvas.js
2026-01-08 10:30:41 +08:00

1013 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const loadImage = (src) =>
new Promise((resolve, reject) => {
try {
uni.getImageInfo({
src,
success: (res) => resolve(res.path || res.tempFilePath || src),
fail: (e) => reject(e),
});
} catch (e) {
reject(src);
}
});
function drawLine(ctx, x1, y1, x2, y2, color = "#000", width = 1) {
ctx.beginPath();
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
const drawRoundImage = async (
ctx,
img,
x,
y,
width,
height,
radius,
borderColor = null,
borderWidth = 0
) => {
ctx.save();
// 创建圆角路径
ctx.beginPath();
ctx.moveTo(x + radius, y);
// 右上角
ctx.lineTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
// 右下角
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
// 左下角
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
// 左上角
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.clip();
// 绘制图片
ctx.drawImage(img, x, y, width, height);
ctx.restore();
// 绘制边框(可选)
if (borderWidth > 0 && borderColor) {
// 内收路径,使描边完全落在给定宽高内
const inset = borderWidth / 2;
const bx = x + inset;
const by = y + inset;
const bw = width - inset * 2;
const bh = height - inset * 2;
const br = Math.max(radius - inset, 0);
ctx.beginPath();
ctx.moveTo(bx + br, by);
// 右上角
ctx.lineTo(bx + bw - br, by);
ctx.arcTo(bx + bw, by, bx + bw, by + br, br);
// 右下角
ctx.lineTo(bx + bw, by + bh - br);
ctx.arcTo(bx + bw, by + bh, bx + bw - br, by + bh, br);
// 左下角
ctx.lineTo(bx + br, by + bh);
ctx.arcTo(bx, by + bh, bx, by + bh - br, br);
// 左上角
ctx.lineTo(bx, by + br);
ctx.arcTo(bx, by, bx + br, by, br);
if (typeof ctx.setLineWidth === "function") {
ctx.setLineWidth(borderWidth);
} else {
ctx.lineWidth = borderWidth;
}
if (typeof ctx.setStrokeStyle === "function") {
ctx.setStrokeStyle(borderColor);
} else {
ctx.strokeStyle = borderColor;
}
ctx.stroke();
}
};
function drawRingCircle(ctx, x, y, text, diameter = 9) {
const fillColor = "#00bf04";
const borderColor = "#ffffff";
const borderWidth = 1;
const r = diameter / 2;
const cx = x - r - borderWidth;
const cy = y - r - borderWidth;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fillStyle = fillColor;
ctx.shadowColor = "rgba(0,0,0,0.25)";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 1;
ctx.fill();
// 重置阴影
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.stroke();
const fontSize = 7;
ctx.save();
ctx.translate(cx, cy);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `${fontSize}px sans-serif`;
ctx.fillStyle = "#ffffff";
ctx.scale(0.7, 1);
ctx.fillText(String(text ?? ""), 0, 0);
ctx.restore();
}
function drawRoundedRect(
ctx,
x,
y,
width,
height,
radius,
fillColor = "#fff",
strokeColor = null,
lineWidth = 1
) {
const r = Math.max(Math.min(radius, Math.min(width, height) / 2), 0);
if (r === 0) {
ctx.beginPath();
ctx.rect(x, y, width, height);
if (fillColor) {
ctx.fillStyle = fillColor;
ctx.fill();
}
if (strokeColor) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
return;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + width - r, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
ctx.lineTo(x + width, y + height - r);
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
ctx.lineTo(x + r, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
if (fillColor) {
ctx.fillStyle = fillColor;
ctx.fill();
}
if (strokeColor) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeColor;
ctx.stroke();
}
}
function drawTextBoxesLine(
ctx,
text,
startX,
baselineY,
fontSize = 9,
padding = 10,
bgColor = "#fff",
textColor = "#000",
radius = 4,
gap = 0
) {
const str = String(text || "");
ctx.font = `${fontSize}px sans-serif`;
const w = ctx.measureText(str).width; // 整段文本宽度
const hPadding = padding + 4;
const rectW = Math.ceil(w) + hPadding * 2;
const rectH = fontSize + padding * 2;
const rectY = baselineY - fontSize - padding / 1.3;
drawRoundedRect(ctx, startX, rectY, rectW, rectH, radius, bgColor, null, 0);
renderText(
ctx,
str,
fontSize,
textColor,
startX + rectW / 2,
baselineY,
"center"
);
return startX + rectW + gap;
}
function renderText(
ctx,
text,
size,
color,
x,
y,
textAlign = "left",
rotateDeg = 0
) {
ctx.font = `${size}px sans-serif`;
ctx.fillStyle = color;
ctx.textAlign = textAlign;
ctx.textBaseline = "alphabetic";
if (rotateDeg) {
ctx.save();
ctx.translate(x, y);
ctx.rotate((Math.PI / 180) * rotateDeg);
ctx.fillText(text, 0, 0);
ctx.restore();
} else {
ctx.fillText(text, x, y);
}
}
export const generateShareImage = async (canvasId, data) => {
try {
const pointBook = uni.getStorageSync("point-book");
const arrowData = data.groups && data.groups[0] ? data.groups[0] : {};
const hasPoint =
Array.isArray(arrowData.list) &&
arrowData.list.some((arrow) => arrow?.x && arrow?.y);
const width = 375;
const height = hasPoint ? 800 : 440;
// 获取 Canvas 2D 上下文并按 DPR 设置
const { canvas, ctx } = await getCanvas2DContext(canvasId, width, height);
// 背景填充
ctx.fillStyle = "#F5F5F5";
ctx.fillRect(0, 0, width, height);
// 背景图
const bgSrc = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88ugdgqwecnmsqfv.png"
);
const bgImg = await loadCanvasImage(canvas, bgSrc);
ctx.drawImage(bgImg, 0, 0, width, 300);
// 头像
const avatarSrc = await loadImage(data.user.avatar);
const avatarImg = await loadCanvasImage(canvas, avatarSrc);
await drawRoundImage(ctx, avatarImg, 13, 13, 54, 54, 27, "#000", 1);
renderText(ctx, data.user.name, 18, "#000", 84, 36);
renderText(ctx, data.recordDate, 9, "#fff", 348, 23, "center", 39);
renderText(ctx, "今日打卡", 14, "#fff", 336, 38, "center", 39);
// 文本标签
let cursorX = 84;
cursorX = drawTextBoxesLine(
ctx,
pointBook.bowType.name,
cursorX,
58,
9,
5,
"#fff",
"#000",
6,
2
);
cursorX = drawTextBoxesLine(
ctx,
String(pointBook.distance) + " 米",
cursorX + 6,
58,
9,
5,
"#fff",
"#000",
6,
2
);
cursorX = drawTextBoxesLine(
ctx,
pointBook.bowtargetType.name,
cursorX + 6,
58,
9,
5,
"#fff",
"#000",
6,
2
);
renderText(ctx, "落点稳定性", 13, "#999", 25, 110);
renderText(ctx, "黄心率", 13, "#999", 145, 110);
renderText(ctx, "10环数", 13, "#999", 262, 110);
renderText(
ctx,
Number(arrowData.stability.toFixed(2)),
15,
"#000",
25,
133
);
renderText(
ctx,
Number((arrowData.yellowRate * 100).toFixed(2)) + "%",
15,
"#000",
145,
133
);
renderText(ctx, arrowData.tenRings, 15, "#000", 262, 133);
renderText(ctx, "平均环数", 13, "#999", 25, 175);
renderText(ctx, "总环数", 13, "#999", 145, 175);
renderText(
ctx,
Number(arrowData.averageRing.toFixed(2)),
15,
"#000",
25,
200
);
renderText(
ctx,
`${arrowData.userTotalRing}/${arrowData.totalRing}`,
15,
"#000",
145,
200
);
if (hasPoint) {
drawRoundedRect(ctx, 20, 230, 5, 15, 5, "#FED847");
renderText(ctx, "落点分布", 13, "#999", 32, 242);
const bowSrc = await loadImage(pointBook.bowtargetType.iconPng);
const bowImg = await loadCanvasImage(canvas, bowSrc);
ctx.drawImage(bowImg, 375 * 0.08, 250, 375 * 0.84, 375 * 0.84);
arrowData.list
.filter((arrow) => arrow?.x && arrow?.y)
.forEach((arrow, index) => {
const px = 375 * 0.08 + (375 * 0.84 + 20) * arrow.x;
const py = 250 + (375 * 0.84 + 20) * arrow.y;
drawRingCircle(ctx, px, py, index + 1);
});
}
const ringNames = ["X", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, "M"];
const ringBarHeight = hasPoint ? 700 : 340;
let rings = new Array(12).fill(0);
arrowData.list.forEach((arrow) => {
if (arrow.ring === 0) rings[0]++;
else if (arrow.ring === -1) rings[11]++;
else rings[arrow.ring]++;
});
[rings[0], rings[11]] = [rings[11], rings[0]];
rings = rings.reverse().map((count) => ({
count,
rate: count / arrowData.list.length,
}));
const barColor = (rate) => {
if (rate >= 0.4) return "#FDC540";
if (rate >= 0.2) return "#FED847";
return "#ffe88f";
};
const barWidth = 20;
const barSpacing = 7.1;
const startX = 38;
const startY = ringBarHeight - 15;
rings.forEach((ring, index) => {
const barHeight = ring.rate * 90;
if (ring.rate > 0) {
renderText(
ctx,
Number((ring.rate * 100).toFixed(1)) + "%",
9,
"#333",
startX + index * (barWidth + barSpacing),
startY - barHeight - (hasPoint ? 6 : 3)
);
}
ctx.fillStyle = barColor(ring.rate);
ctx.fillRect(
startX + index * (barWidth + barSpacing),
startY - barHeight,
barWidth,
barHeight
);
});
renderText(ctx, "环值", 9, "#999", 15, ringBarHeight - 1);
ringNames.forEach((ring, index) => {
renderText(
ctx,
ring,
11,
"#333",
48 + index * 27,
ringBarHeight,
"center"
);
});
drawLine(ctx, 15, ringBarHeight - 15, 362, ringBarHeight - 15, "#333");
const qrcodeSrc = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-12-04/dep7lfqhpelmerjle4.png"
);
const qrcodeImg = await loadCanvasImage(canvas, qrcodeSrc);
ctx.drawImage(qrcodeImg, 40, hasPoint ? 715 : 358, 68, 68);
renderText(ctx, "射灵星球", 18, "#333", 120, hasPoint ? 734 : 380);
renderText(
ctx,
"高效记录每一箭,快来一起打卡吧!",
13,
"#999",
120,
hasPoint ? 756 : 402
);
renderText(ctx, "扫码打卡", 13, "#FFA118", 142, hasPoint ? 777 : 422);
const pointImg = await loadCanvasImage(canvas, "../static/point.png");
ctx.drawImage(pointImg, 120, hasPoint ? 765 : 410, 18, 14);
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
}
};
// 顶部导入与工具方法
async function getCanvas2DContext(canvasId, targetWidth, targetHeight) {
return new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
const { node: canvas } = res[0] || {};
const ctx = canvas.getContext("2d");
const dpr = uni.getSystemInfoSync().pixelRatio || 1;
const w = targetWidth || res[0].width;
const h = targetHeight || res[0].height;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.scale(dpr, dpr);
resolve({ canvas, ctx, dpr, cssWidth: w, cssHeight: h });
});
});
}
async function loadCanvasImage(canvas, src) {
return new Promise((resolve, reject) => {
try {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = src;
} catch (e) {
reject(e);
}
});
}
export const sharePointData = async (canvasId, data) => {
try {
const width = 375;
const height = 460;
// 获取 Canvas 2D 上下文并按 DPR 设置
const { canvas, ctx } = await getCanvas2DContext(canvasId, width, height);
// 背景填充
ctx.fillStyle = "#F5F5F5";
ctx.fillRect(0, 0, width, height);
// 背景图
const bgSrc = await loadImage(
"https://static.shelingxingqiu.com/attachment/2026-01-05/dfgirwfuz5htwfd4sd.png"
);
const bgImg = await loadCanvasImage(canvas, bgSrc);
ctx.drawImage(bgImg, 0, 0, width, width);
// 头像
const avatarSrc = await loadImage(data.avatar);
const avatarImg = await loadCanvasImage(canvas, avatarSrc);
await drawRoundImage(ctx, avatarImg, 13, 13, 54, 54, 27, "#fff", 1);
renderText(ctx, data.name, 20, "#fff", 84, 50);
const bubble1Src = await loadImage(
"https://static.shelingxingqiu.com/attachment/2026-01-05/dfgirwcxugdsenlnud.png"
);
const bubble2Src = await loadImage(
"https://static.shelingxingqiu.com/attachment/2026-01-05/dfgirwcxujhysg0vfq.png"
);
const bubble3Src = await loadImage(
"https://static.shelingxingqiu.com/attachment/2026-01-05/dfgirwfa33spdori3p.png"
);
const bubble1Img = await loadCanvasImage(canvas, bubble1Src);
const bubble2Img = await loadCanvasImage(canvas, bubble2Src);
const bubble3Img = await loadCanvasImage(canvas, bubble3Src);
ctx.drawImage(bubble1Img, 10, 88, 143, 87);
renderText(ctx, "本周箭数", 14, "#FDA103", 84, 116, "center");
renderText(ctx, data.weekArrow, 36, "#FA2A2A", 84, 152, "center");
ctx.drawImage(bubble2Img, 65, 220, 143, 87);
renderText(ctx, "本周消耗", 14, "#FDA103", 139, 248, "center");
renderText(
ctx,
Math.round(data.weekArrow * 1.6),
36,
"#FA2A2A",
139,
284,
"center"
);
ctx.drawImage(bubble3Img, 255, 52, 114, 92);
renderText(ctx, "我的名次", 14, "#FDA103", 312, 80, "center");
renderText(ctx, data.rank, 36, "#FA2A2A", 312, 116, "center");
const qrcodeSrc = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-12-04/dep7lfqhpelmerjle4.png"
);
const qrcodeImg = await loadCanvasImage(canvas, qrcodeSrc);
ctx.drawImage(qrcodeImg, 40, 383, 68, 68);
renderText(ctx, "射灵星球", 18, "#333", 120, 412);
renderText(ctx, "高效记录每一箭,快来一起打卡吧!", 13, "#999", 120, 435);
// 2D 即时绘制,无需 ctx.draw()
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
}
};
export function renderRankTitle(ctx, text) {
const fontSize = 8;
const textWidth = ctx.measureText(text).width;
const padding = 8; // 文字与背景边缘的间距
const radius = 8; // 圆角半径
const textX = 76;
const textY = 50;
const x = textX - padding - 10; // 文字 x 坐标减去内边距
const y = textY - fontSize - padding / 2 + 2; // 文字 y 坐标减去字体大小和内边距
const width = textWidth + padding * 2 - 25; // 背景宽度
const height = fontSize + padding - 2; // 背景高度
// 开始绘制圆角矩形
ctx.beginPath();
// 从左上角开始,顺时针绘制
ctx.moveTo(x + radius, y);
// 上边框
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
// 右边框
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
// 下边框
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
// 左边框
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
// 设置背景颜色并填充
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle("#5F51FF");
} else {
ctx.fillStyle = "#5F51FF";
}
ctx.fill();
if (typeof ctx.setFontSize === "function") {
ctx.setFontSize(fontSize);
ctx.setTextAlign("center");
ctx.setFillStyle("rgba(255, 255, 255, 0.9)");
} else {
ctx.font = `${fontSize}px sans-serif`;
ctx.textAlign = "center";
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
}
ctx.fillText(text, textX, textY); // 绘制文字
}
export function renderScores(ctx, arrows = [], bgImg) {
let rowIndex = 0;
arrows.forEach((item, i) => {
rowIndex = i;
if (arrows.length >= 36 && i < 36) {
if (bgImg) {
ctx.drawImage(
bgImg,
16 + (i % 9) * 30,
290 + Math.ceil((i + 1) / 9) * 30,
27,
27
);
} else {
ctx.drawImage(
"../static/score-bg.png",
16 + (i % 9) * 30,
290 + Math.ceil((i + 1) / 9) * 30,
27,
27
);
}
renderText(
ctx,
item.ring,
18,
"#fed847",
29.5 + (i % 9) * 30,
310 + Math.ceil((i + 1) / 9) * 30,
"center"
);
} else if (arrows.length >= 12 && i < 12) {
if (i > 5) rowIndex = i - 6;
if (bgImg) {
ctx.drawImage(bgImg, 24 + rowIndex * 42, i > 5 ? 362 : 320, 38, 38);
} else {
ctx.drawImage(
"../static/score-bg.png",
24 + rowIndex * 42,
i > 5 ? 362 : 320,
38,
38
);
}
renderText(
ctx,
item.ring,
23,
"#fed847",
43 + rowIndex * 42,
i > 5 ? 389 : 347,
"center"
);
}
});
}
export function renderLine(ctx, from) {
ctx.beginPath();
if (typeof ctx.setLineWidth === "function") {
ctx.setLineWidth(1);
ctx.setStrokeStyle("rgba(255, 255, 255, 0.3)");
} else {
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
}
const length = 35;
ctx.moveTo(from, 295);
ctx.lineTo(from + length, 295);
ctx.stroke();
}
export async function sharePractiseData(canvasId, type, user, data) {
try {
const width = 300;
const height = 530;
const { canvas, ctx } = await getCanvas2DContext(canvasId, width, height);
// 预加载所有图片
const bgImgSrc = await loadImage(
"https://static.shelingxingqiu.com/attachment/2026-01-07/dfia8qlexbwoqaezyd.png"
);
const bgImg = await loadCanvasImage(canvas, bgImgSrc);
const avatarImgPromise = loadImage(user.avatar).then((path) =>
loadCanvasImage(canvas, path)
);
const lvlImgPromise = loadImage(user.lvlImage).then((path) =>
loadCanvasImage(canvas, path)
);
let titleImageSrc = "../static/first-try-title.png";
if (type == 2) {
titleImageSrc = "../static/practise-one-title.png";
} else if (type == 3) {
titleImageSrc = "../static/practise-two-title.png";
}
const titleImgPromise = loadCanvasImage(canvas, titleImageSrc);
const scoreBgImgPromise = loadCanvasImage(canvas, "../static/score-bg.png");
const qrCodeImgPromise = loadCanvasImage(canvas, "../static/qr-code.png");
const [avatarImg, lvlImg, titleImg, scoreBgImg, qrCodeImg] =
await Promise.all([
avatarImgPromise,
lvlImgPromise,
titleImgPromise,
scoreBgImgPromise,
qrCodeImgPromise,
]);
// 先填充整体背景色
ctx.fillStyle = "#F5F5F5";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(bgImg, 0, 0, width, height);
drawRoundImage(ctx, avatarImg, 17, 20, 32, 32, 20);
ctx.drawImage(lvlImg, 12, 15, 42, 42);
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
renderRankTitle(ctx, user.lvlName);
let subTitle = "正式开启弓箭手之路";
if (type > 1) {
subTitle = `今日弓箭练习打卡 ${data.createdAt
.split(" ")[0]
.replaceAll("-", ".")}`;
}
ctx.drawImage(titleImg, (width - 160) / 2, 160, 160, 40);
ctx.font = "18px sans-serif";
renderText(ctx, subTitle, 18, "#fff", width / 2, 224, "center");
renderText(ctx, "共", 14, "#fff", 122, 300);
const totalRing = data.arrows.reduce((last, next) => last + next.ring, 0);
renderText(ctx, totalRing, 14, "#fed847", 148, 300, "center");
renderText(ctx, "环", 14, "#fff", 161, 300);
renderLine(ctx, 77);
renderLine(ctx, 185);
renderScores(ctx, data.arrows, scoreBgImg);
ctx.drawImage(qrCodeImg, width * 0.06, height * 0.87, 52, 52);
renderText(ctx, "射灵平台", 12, "#fff", width * 0.26, height * 0.9);
renderText(
ctx,
"快加入我们一起玩吧~",
9,
"rgba(255, 255, 255, 0.5)",
width * 0.26,
height * 0.93
);
renderText(
ctx,
"后羿就是这样练成的",
9,
"rgba(255, 255, 255, 0.5)",
width * 0.26,
height * 0.955
);
// 2D 模式下无需 ctx.draw()
} catch (err) {
console.log(err);
}
}
export const generateShareCard = (canvasId, date, actual, total) => {
try {
const ctx = uni.createCanvasContext(canvasId);
const imgUrl =
"https://static.shelingxingqiu.com/attachment/2025-11-04/ddzpm4tyh5vunyacsr.png";
const drawWidth = 375;
const drawHeight = 300;
// 先填充整体背景色
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle("#F5F5F5");
} else {
ctx.fillStyle = "#F5F5F5";
}
if (typeof ctx.fillRect === "function") {
ctx.fillRect(0, 0, drawWidth, drawHeight);
}
// 兼容第三个参数传入 "actual/total" 的情况
let a = actual;
let t = total;
if (
(t === undefined || t === null) &&
typeof a === "string" &&
a.includes("/")
) {
const parts = a.split("/");
a = parts[0];
t = parts[1] || "";
}
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: imgUrl,
success: (res) => {
const path = res.path || res.tempFilePath || imgUrl;
if (typeof ctx.clearRect === "function") {
ctx.clearRect(0, 0, drawWidth, drawHeight);
}
ctx.drawImage(path, 0, 0, drawWidth, drawHeight);
// 绘制左上角日期白色22px
renderText(ctx, String(date || ""), 18, "#FFFFFF", 6, 20, "left");
// 居中绘制第三个参数为圆角黑底标签
const rectW = 200;
const rectH = 78;
const radius = 39;
const rectX = (drawWidth - rectW) / 2;
const rectY = (drawHeight - rectH) / 2 - 12;
ctx.save();
ctx.beginPath();
ctx.moveTo(rectX + radius, rectY);
ctx.lineTo(rectX + rectW - radius, rectY);
ctx.quadraticCurveTo(
rectX + rectW,
rectY,
rectX + rectW,
rectY + radius
);
ctx.lineTo(rectX + rectW, rectY + rectH - radius);
ctx.quadraticCurveTo(
rectX + rectW,
rectY + rectH,
rectX + rectW - radius,
rectY + rectH
);
ctx.lineTo(rectX + radius, rectY + rectH);
ctx.quadraticCurveTo(
rectX,
rectY + rectH,
rectX,
rectY + rectH - radius
);
ctx.lineTo(rectX, rectY + radius);
ctx.quadraticCurveTo(rectX, rectY, rectX + radius, rectY);
ctx.closePath();
ctx.setFillStyle("rgba(0,0,0,0.5)");
ctx.fill();
ctx.restore();
// 居中排版:左侧显示 actual/,右侧显示 total再追加“环”
ctx.save();
if (typeof ctx.setTextBaseline === "function")
ctx.setTextBaseline("middle");
const centerX = rectX + rectW / 2;
const centerY = rectY + rectH / 2;
// 左半部:右对齐 actual/
renderText(ctx, `${a}/`, 40, "#FFFFFF", centerX, centerY, "right");
// 右半部:左对齐 total
renderText(
ctx,
`${t || ""}`,
30,
"#FFFFFF",
centerX,
centerY,
"left"
);
// 追加单位“环”:放在 total 文本之后 16px
const totalWidth = ctx.measureText(`${t || ""}`).width || 0;
renderText(
ctx,
"环",
20,
"#FFFFFF",
centerX + totalWidth + 5,
centerY,
"left"
);
ctx.restore();
ctx.draw(false, () => {
try {
uni.canvasToTempFilePath({
canvasId,
width: drawWidth,
height: drawHeight,
destWidth: drawWidth,
destHeight: drawHeight,
fileType: "png",
quality: 1,
success: (r) => {
const path = r.tempFilePath || r.apFilePath || r.filePath;
resolve(path);
},
fail: (err) => reject(err),
});
} catch (err) {
reject(err);
}
});
},
fail: () => {
try {
ctx.drawImage(imgUrl, 0, 0, drawWidth, drawHeight);
// 绘制左上角日期白色22px
renderText(ctx, String(date || ""), 22, "#FFFFFF", 10, 22, "left");
// 居中绘制第三个参数为圆角黑底标签
const rectW = 200;
const rectH = 78;
const radius = 39;
const rectX = (drawWidth - rectW) / 2;
const rectY = (drawHeight - rectH) / 2;
ctx.save();
ctx.beginPath();
ctx.moveTo(rectX + radius, rectY);
ctx.lineTo(rectX + rectW - radius, rectY);
ctx.quadraticCurveTo(
rectX + rectW,
rectY,
rectX + rectW,
rectY + radius
);
ctx.lineTo(rectX + rectW, rectY + rectH - radius);
ctx.quadraticCurveTo(
rectX + rectW,
rectY + rectH,
rectX + rectW - radius,
rectY + rectH
);
ctx.lineTo(rectX + radius, rectY + rectH);
ctx.quadraticCurveTo(
rectX,
rectY + rectH,
rectX,
rectY + rectH - radius
);
ctx.lineTo(rectX, rectY + radius);
ctx.quadraticCurveTo(rectX, rectY, rectX + radius, rectY);
ctx.closePath();
ctx.setFillStyle("rgba(0,0,0,0.5)");
ctx.fill();
ctx.restore();
// 居中排版:左侧显示 actual/,右侧显示 total再追加“环”
ctx.save();
if (typeof ctx.setTextBaseline === "function")
ctx.setTextBaseline("middle");
const centerX = rectX + rectW / 2;
const centerY = rectY + rectH / 2;
// 左半部:右对齐 actual/
renderText(ctx, `${a}/`, 40, "#FFFFFF", centerX, centerY, "right");
// 右半部:左对齐 total
renderText(
ctx,
`${t || ""}`,
35,
"#FFFFFF",
centerX,
centerY,
"left"
);
// 追加单位“环”:放在 total 文本之后 16px
const totalWidth = ctx.measureText(`${t || ""}`).width || 0;
renderText(
ctx,
"环",
20,
"#FFFFFF",
centerX + totalWidth + 5,
centerY,
"left"
);
ctx.restore();
ctx.draw(false, () => {
try {
uni.canvasToTempFilePath({
canvasId,
width: drawWidth,
height: drawHeight,
destWidth: drawWidth,
destHeight: drawHeight,
fileType: "png",
quality: 1,
success: (r) => {
const path = r.tempFilePath || r.apFilePath || r.filePath;
resolve(path);
},
fail: (err) => reject(err),
});
} catch (err) {
reject(err);
}
});
} catch (e) {
reject(e);
}
},
});
});
} catch (e) {
console.error("generateShareCardImage 绘制失败:", e);
}
};