完成生成靶子数据图片的头部
This commit is contained in:
280
src/util.js
280
src/util.js
@@ -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 "";
|
||||
|
||||
Reference in New Issue
Block a user