完成生成靶子数据图片的头部

This commit is contained in:
kron
2025-11-14 11:59:21 +08:00
parent 7871544f01
commit 0745c4ba9f
2 changed files with 275 additions and 9 deletions

View File

@@ -81,11 +81,152 @@ export function renderLine(ctx, from) {
ctx.stroke();
}
export function renderText(ctx, text, size, color, x, y, textAlign = "left") {
export function renderText(
ctx,
text,
size,
color,
x,
y,
textAlign = "left",
rotateDeg = 0
) {
ctx.setFontSize(size);
ctx.setFillStyle(color);
ctx.setTextAlign(textAlign);
ctx.fillText(text, x, y);
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 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);
// 当圆角为 0 时,避免使用 arcTo 导致平台差异,直接绘制普通矩形
if (r === 0) {
ctx.beginPath();
if (typeof ctx.rect === "function") {
ctx.rect(x, y, width, height);
} else {
ctx.moveTo(x, y);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
}
if (fillColor) {
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle(fillColor);
} else {
ctx.fillStyle = fillColor;
}
ctx.fill();
}
if (strokeColor) {
if (typeof ctx.setLineWidth === "function") {
ctx.setLineWidth(lineWidth);
} else {
ctx.lineWidth = lineWidth;
}
if (typeof ctx.setStrokeStyle === "function") {
ctx.setStrokeStyle(strokeColor);
} else {
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);
// 显式闭合路径,避免某些 Android WebView/Canvas 在左上角出现缺口
ctx.closePath();
if (fillColor) {
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle(fillColor);
} else {
ctx.fillStyle = fillColor;
}
ctx.fill();
}
if (strokeColor) {
if (typeof ctx.setLineWidth === "function") {
ctx.setLineWidth(lineWidth);
} else {
ctx.lineWidth = lineWidth;
}
if (typeof ctx.setStrokeStyle === "function") {
ctx.setStrokeStyle(strokeColor);
} else {
ctx.strokeStyle = strokeColor;
}
ctx.stroke();
}
}
// 将文本按字符拆分并依次向右绘制,每个字符有白色背景,背景宽度根据字符宽度+内边距动态计算
export function drawTextBoxesLine(
ctx,
text,
startX,
baselineY,
fontSize = 9,
padding = 10,
bgColor = "#fff",
textColor = "#000",
radius = 4,
gap = 0
) {
const str = String(text || "");
// 确保测量宽度使用一致的字号
ctx.setFontSize(fontSize);
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;
}
export function renderRankTitle(ctx, text) {
@@ -133,7 +274,9 @@ export const drawRoundImage = async (
y,
width,
height,
radius
radius,
borderColor = null,
borderWidth = 0
) => {
ctx.save();
// 创建圆角路径
@@ -160,18 +303,56 @@ export const drawRoundImage = async (
// 绘制图片
ctx.drawImage(imgPath, 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();
}
};
const loadImage = (src) =>
new Promise((resolve) => {
new Promise((resolve, reject) => {
try {
uni.getImageInfo({
src,
success: (res) => resolve(res.path || res.tempFilePath || src),
fail: () => resolve(src),
fail: (e) => reject(e),
});
} catch (e) {
resolve(src);
reject(src);
}
});
@@ -180,6 +361,15 @@ export async function generateCanvasImage(canvasId, type, user, data) {
var ctx = uni.createCanvasContext(canvasId);
const width = 300;
const height = 534;
// 先填充整体背景色
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle("#F5F5F5");
} else {
ctx.fillStyle = "#F5F5F5";
}
if (typeof ctx.fillRect === "function") {
ctx.fillRect(0, 0, width, height);
}
ctx.drawImage("../static/share-bg.png", 0, 0, width, height);
const avatar = await loadImage(user.avatar);
drawRoundImage(ctx, avatar, 17, 20, 32, 32, 20);
@@ -461,6 +651,15 @@ export const generateShareCard = (canvasId, date, actual, total) => {
"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;
@@ -675,7 +874,74 @@ export const generateShareCard = (canvasId, date, actual, total) => {
}
};
export const generateShareImage = (canvasId) => {};
export const generateShareImage = async (canvasId, data) => {
try {
const pointBook = uni.getStorageSync("point-book");
console.log("generate data:", data, pointBook);
const ctx = uni.createCanvasContext(canvasId);
const width = 375;
const height = 800;
ctx.setFillStyle("#F5F5F5");
ctx.fillRect(0, 0, width, height);
const bgUrl = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-11-14/de82iol96zxgwxbjhb.png"
);
ctx.drawImage(bgUrl, 0, 0, width, height);
const avatarUrl = await loadImage(data.user.avatar);
drawRoundImage(ctx, avatarUrl, 13, 13, 54, 54, 27, "#000", 1);
renderText(ctx, data.user.name, 18, "#000", 84, 36);
renderText(ctx, data.recordDate, 9, "#fff", 350, 24, "center", 38);
renderText(ctx, "今日打卡", 13, "#fff", 338, 36, "center", 38);
// 以第一个字的 x 为起点,右侧字符依次向右排列,每个字符有白色背景并留 5 像素内边距
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
);
// const pointBookQrcodeUrl = await loadImage(
// "https://static.shelingxingqiu.com/attachment/2025-11-13/de7fzgghsfgqu0ytu6.png"
// );
// ctx.drawImage(pointBookQrcodeUrl, 45, 777, 68, 68);
ctx.draw();
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
}
};
export const getDirectionText = (angle = 0) => {
if (angle < 0) return "";