Files
shoot-miniprograms/src/util.js
2026-01-05 09:23:26 +08:00

857 lines
24 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.

import { isGamingAPI, getGameAPI } from "@/apis";
export const formatTimestamp = (timestamp) => {
const date = new Date(timestamp * 1000);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}-${month}-${day}`;
};
export const debounce = (fn, delay = 300) => {
let timer = null;
return async (...args) => {
if (timer) clearTimeout(timer);
return new Promise((resolve) => {
timer = setTimeout(async () => {
try {
const result = await fn(...args);
resolve(result);
} finally {
timer = null;
}
}, delay);
});
};
};
export function renderScores(ctx, arrows = []) {
let rowIndex = 0;
arrows.forEach((item, i) => {
rowIndex = i;
if (arrows.length >= 36 && i < 36) {
ctx.drawImage(
"../static/score-bg.png",
16 + (i % 9) * 30,
290 + Math.ceil((i + 1) / 9) * 30,
27,
27,
"center"
);
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;
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();
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 function renderText(
ctx,
text,
size,
color,
x,
y,
textAlign = "left",
rotateDeg = 0
) {
ctx.setFontSize(size);
ctx.setFillStyle(color);
ctx.setTextAlign(textAlign);
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 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);
// 设置背景颜色并填充
ctx.fillStyle = "#5F51FF";
ctx.fill();
ctx.setFontSize(fontSize);
ctx.setTextAlign("center");
ctx.setFillStyle("rgba(255, 255, 255, 0.9)");
ctx.fillText(text, textX, textY); // 绘制文字
}
export const drawRoundImage = async (
ctx,
imgPath,
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(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();
}
};
export 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);
}
});
export async function generateCanvasImage(canvasId, type, user, data) {
try {
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);
const lvlImage = await loadImage(user.lvlImage);
ctx.drawImage(lvlImage, 12, 15, 42, 42);
renderText(ctx, user.nickName, 13, "#fff", 58, 34);
renderRankTitle(ctx, user.lvlName);
let titleImage = "../static/first-try-title.png";
let subTitle = "正式开启弓箭手之路";
if (type > 1) {
subTitle = `今日弓箭练习打卡 ${data.createdAt
.split(" ")[0]
.replaceAll("-", ".")}`;
}
if (type == 2) {
titleImage = "../static/practise-one-title.png";
} else if (type == 3) {
titleImage = "../static/practise-two-title.png";
}
ctx.drawImage(titleImage, (width - 160) / 2, 160, 160, 40);
const subTitleWidth = ctx.measureText(subTitle).width;
renderText(
ctx,
subTitle,
18,
"#fff",
width / 2 - subTitleWidth - (type > 1 ? 15 : 9),
220
);
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);
ctx.drawImage("../static/qr-code.png", width * 0.06, height * 0.87, 52, 52);
renderText(ctx, "射灵平台", 12, "#fff", width * 0.25, height * 0.9);
renderText(
ctx,
"快加入我们一起玩吧~",
9,
"rgba(255, 255, 255, 0.5)",
width * 0.25,
height * 0.93
);
renderText(
ctx,
"后羿就是这样练成的",
9,
"rgba(255, 255, 255, 0.5)",
width * 0.25,
height * 0.955
);
// ctx.drawImage("../static/qr-code.png", width * 0.75, height * 0.86, 56, 56);
ctx.draw();
} catch (err) {
console.log(err);
}
}
export const wxShare = async (canvasId = "shareCanvas") => {
try {
// 先尝试按 id 查找 <canvas type="2d"> 节点
const { pixelRatio: dpr = 1 } = uni.getSystemInfoSync() || {};
const info = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => resolve(res && res[0]));
});
const canvas = info?.node;
const cssWidth = info?.width;
const cssHeight = info?.height;
if (canvas && typeof canvas.getContext === "function") {
// 2D 画布导出:传入 canvas 节点
const tempPath = await new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvas,
width: cssWidth,
height: cssHeight,
// 按 DPR 导出更清晰的图片
destWidth: Math.round(cssWidth * dpr),
destHeight: Math.round(cssHeight * dpr),
fileType: "png",
quality: 1,
success: (r) => {
const p = r.tempFilePath || r.apFilePath || r.filePath;
resolve(p);
},
fail: reject,
});
});
wx.showShareImageMenu({
entrancePath: "pages/index",
path: tempPath,
});
return tempPath;
}
// 回退:旧版非 2D 画布(通过 canvasId 导出)
const res = await uni.canvasToTempFilePath({
canvasId,
fileType: "png",
quality: 1,
});
wx.showShareImageMenu({
entrancePath: "pages/index",
path: res.tempFilePath,
});
return res.tempFilePath;
} catch (error) {
console.log("生成图片失败:", error);
uni.showToast({
title: "生成图片失败",
icon: "error",
});
throw error;
}
};
export const isGameEnded = async (battleId) => {
const isGaming = await isGamingAPI();
if (!isGaming) {
const result = await getGameAPI(battleId);
if (result.mode) {
uni.redirectTo({
url: `/pages/battle-result?battleId=${battleId}`,
});
} else {
uni.showToast({
title: "比赛已结束",
icon: "none",
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
}
}
return !isGaming;
};
// 获取元素尺寸和位置信息
export const getElementRect = (classname) => {
return new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(classname)
.boundingClientRect((rect) => {
resolve(rect);
})
.exec();
});
};
const calcNormalBowTarget = (x, y, diameter, arrowRadius) => {
// 将弓箭左上角坐标转换为圆心坐标
const arrowCenterX = x + arrowRadius;
const arrowCenterY = y + arrowRadius;
// 计算靶心坐标(靶纸中心)
const centerX = (diameter + arrowRadius * 2) / 2;
const centerY = (diameter + arrowRadius * 2) / 2;
// 计算弓箭圆心到靶心的距离
const deltaX = arrowCenterX - centerX;
const deltaY = arrowCenterY - centerY;
const distanceToCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 计算弓箭边缘到靶心的最近距离
const distance = Math.max(0, distanceToCenter - arrowRadius);
// 计算靶纸半径(取宽高中较小值的一半)
const targetRadius = diameter / 2;
// 计算相对距离0-1之间
let relativeDistance = distance / targetRadius;
// 全环靶有10个环每个环占半径的10%
// 从外到内1环到10环
// 距离越近靶心,环数越高
if (relativeDistance <= 0.05) return "X";
if (relativeDistance <= 0.1) return 10;
if (relativeDistance <= 0.2) return 9;
if (relativeDistance <= 0.3) return 8;
if (relativeDistance <= 0.4) return 7;
if (relativeDistance <= 0.5) return 6;
if (relativeDistance <= 0.6) return 5;
if (relativeDistance <= 0.7) return 4;
if (relativeDistance <= 0.8) return 3;
if (relativeDistance <= 0.9) return 2;
if (relativeDistance <= 1) return 1;
return 0; // 脱靶
};
const calcHalfBowTarget = (x, y, diameter, arrowRadius, noX = false) => {
// 将弓箭左上角坐标转换为圆心坐标
const arrowCenterX = x + arrowRadius;
const arrowCenterY = y + arrowRadius;
// 计算靶心坐标(靶纸中心)
const centerX = (diameter + arrowRadius * 2) / 2;
const centerY = (diameter + arrowRadius * 2) / 2;
// 计算弓箭圆心到靶心的距离
const deltaX = arrowCenterX - centerX;
const deltaY = arrowCenterY - centerY;
const distanceToCenter = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 计算弓箭边缘到靶心的最近距离
const distance = Math.max(0, distanceToCenter - arrowRadius);
// 计算靶纸半径(取宽高中较小值的一半)
const targetRadius = diameter / 2;
// 计算相对距离0-1之间
let relativeDistance = distance / targetRadius;
if (relativeDistance <= 0.1) return noX ? 10 : "X";
if (relativeDistance <= 0.2) return noX ? 9 : 10;
if (relativeDistance <= 0.4) return 9;
if (relativeDistance <= 0.6) return 8;
if (relativeDistance <= 0.8) return 7;
if (relativeDistance <= 1) return 6;
return 0; // 脱靶
};
export const calcTripleBowTarget = (
x,
y,
diameter,
arrowRadius,
noX = false
) => {
const side = diameter * 0.325;
if (y / diameter >= 0.661) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.675,
side,
arrowRadius,
noX
);
}
if (y / diameter >= 0.323) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.337,
side,
arrowRadius,
noX
);
}
if (y / diameter >= -0.026) {
return calcHalfBowTarget(
x - diameter * 0.336,
y - diameter * 0.0,
side,
arrowRadius,
noX
);
}
return 0;
};
export const calcPinBowTarget = (x, y, diameter, arrowRadius, noX = false) => {
const side = diameter * 0.482;
let r1 = 0;
let r2 = 0;
let r3 = 0;
if (x / diameter >= 0.23 && y / diameter >= 0.005) {
r1 = calcHalfBowTarget(
x - diameter * 0.26,
y - diameter * 0.034,
side,
arrowRadius,
noX
);
}
if (x / diameter >= -0.03 && y / diameter >= 0.456) {
r2 = calcHalfBowTarget(x, y - diameter * 0.484, side, arrowRadius, noX);
}
if (x / diameter >= 0.49 && y / diameter >= 0.456) {
r3 = calcHalfBowTarget(
x - diameter * 0.52,
y - diameter * 0.485,
side,
arrowRadius,
noX
);
}
return r1 || r2 || r3;
};
export const calcRing = (bowtargetId, x, y, diameter, arrowRadius = 5) => {
if (bowtargetId < 4) {
return calcNormalBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId < 7) {
return calcHalfBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 7) {
return calcTripleBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 8) {
return calcPinBowTarget(x, y, diameter, arrowRadius);
} else if (bowtargetId === 9) {
return calcTripleBowTarget(x, y, diameter, arrowRadius, true);
} else if (bowtargetId === 10) {
return calcPinBowTarget(x, y, diameter, arrowRadius, true);
}
return 0;
};
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;
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);
}
};
export const getDirectionText = (angle = 0) => {
if (angle < 0) return "";
if (angle >= 337.5 || angle < 22.5) {
return "下";
} else if (angle >= 22.5 && angle < 67.5) {
return "左下";
} else if (angle >= 67.5 && angle < 112.5) {
return "左";
} else if (angle >= 112.5 && angle < 157.5) {
return "左上";
} else if (angle >= 157.5 && angle < 202.5) {
return "上";
} else if (angle >= 202.5 && angle < 247.5) {
return "右上";
} else if (angle >= 247.5 && angle < 292.5) {
return "右";
} else if (angle >= 292.5 && angle < 337.5) {
return "右下";
}
};
export const wxLogin = () => {
return new Promise((resolve, reject) => {
uni.login({
provider: "weixin",
success: resolve,
fail: (err) => {
uni.showToast({
title: "登录失败",
icon: "none",
});
console.error("登录失败:", err);
reject(err);
},
});
});
};
export const canEenter = (user, device, online, page = "") => {
const { miniProgram } = uni.getAccountInfoSync();
if (!device.deviceId) {
uni.showToast({
title: "请先绑定设备",
icon: "none",
});
return false;
}
if (miniProgram.envVersion !== "release") return true;
if (!online) {
uni.showToast({
title: "智能弓未连接",
icon: "none",
});
return false;
}
if (!user.trio && page !== "/pages/first-try") {
uni.showToast({
title: "请先完成新手试炼",
icon: "none",
});
return false;
}
return true;
};