diff --git a/src/heatmap.js b/src/heatmap.js
new file mode 100644
index 0000000..47bd0f0
--- /dev/null
+++ b/src/heatmap.js
@@ -0,0 +1,105 @@
+/**
+ * 在 uni-app 小程序里画弓箭热力图
+ * @param {String} canvasId 画布 id
+ * @param {Number} width 画布宽(px)
+ * @param {Number} height 画布高(px)
+ * @param {Array} arrowData [{x, y, count}, ...]
+ */
+export function generateHeatmapImage(canvasId, width, height, arrowData) {
+ return new Promise((resolve, reject) => {
+ // 1. 创建绘图上下文
+ const ctx = uni.createCanvasContext(canvasId);
+
+ // 3. 计算最大 count,用于归一化
+ const maxCount = Math.max(...arrowData.map((p) => p.count), 1);
+ // 4. 热点半径:可按实际靶子大小调,这里取画布短边的 6%
+ const radius = Math.min(width, height) * 0.12;
+
+ // 5. 按count从小到大排序,count越大越后面
+ arrowData.sort((a, b) => a.count - b.count);
+
+ // 6. 画每个点
+ arrowData.forEach((item) => {
+ const intensity = item.count / maxCount; // 0-1
+ // console.log(item.count, maxCount, intensity);
+ const r = radius * (1.2 - intensity * 0.8);
+ // 创建径向渐变
+ const grd = ctx.createCircularGradient(
+ item.x * width,
+ item.y * height,
+ r
+ );
+ grd.addColorStop(0, heatColor(intensity, 1));
+ grd.addColorStop(0.5, heatColor(intensity, 0.6));
+ grd.addColorStop(1, "rgba(0,0,0,0)");
+
+ ctx.save();
+ ctx.fillStyle = grd;
+ ctx.globalCompositeOperation = "screen"; // 叠加变亮
+ ctx.beginPath();
+ ctx.arc(item.x * width, item.y * height, r, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+ });
+
+ // 6. 可选:整体蒙版,让非热点区域暗下去
+ // ctx.save()
+ // ctx.fillStyle = 'rgba(0,0,0,0.35)'
+ // ctx.fillRect(0, 0, width, height)
+ // ctx.restore()
+
+ // 7. 把指令一次性推送到 canvas
+ ctx.draw(false, () => {
+ // Canvas绘制完成后生成图片
+ uni.canvasToTempFilePath({
+ canvasId: "heatMapCanvas",
+ width: width,
+ height: height,
+ destWidth: width * 2, // 提高图片质量
+ destHeight: height * 2,
+ success: (res) => {
+ console.log("热力图图片生成成功:", res.tempFilePath);
+ resolve(res.tempFilePath);
+ },
+ fail: (error) => {
+ console.error("热力图图片生成失败:", error);
+ reject(error);
+ },
+ });
+ });
+ });
+}
+
+/**
+ * 把强度 0-1 映射成红-黄-绿渐变,返回 rgba 字符串
+ * @param {Number} v 0-1
+ * @param {Number} a 透明度 0-1
+ */
+function heatColor(v, a) {
+ // v 从 0→1,重新映射:极低值绿色,低值黄色,中到高值红色
+ let red, green;
+
+ if (v < 0.2) {
+ // 极低值:纯绿色
+ red = 0;
+ green = 200; // 柔和的绿
+ } else if (v < 0.4) {
+ // 低值:绿色到黄色
+ const t = (v - 0.2) / 0.2;
+ red = Math.round(255 * t);
+ green = 255;
+ } else if (v < 0.6) {
+ // 中低值:黄色到橙色
+ const t = (v - 0.4) / 0.2;
+ red = 255;
+ green = Math.round(255 * (1 - t * 0.5));
+ } else {
+ // 中到高值:橙色到红色
+ const t = (v - 0.6) / 0.4;
+ red = 255;
+ green = Math.round(128 * (1 - t));
+ }
+
+ const blue = 0;
+ return `rgba(${red}, ${green}, ${blue}, ${a})`;
+}
diff --git a/src/pages/point-book.vue b/src/pages/point-book.vue
index 745c875..0e042d1 100644
--- a/src/pages/point-book.vue
+++ b/src/pages/point-book.vue
@@ -16,7 +16,8 @@ import {
getPointBookStatisticsAPI,
} from "@/apis";
-import { getElementRect, drawHeatMap } from "@/util";
+import { getElementRect } from "@/util";
+import { generateHeatmapImage } from "@/heatmap";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -38,6 +39,7 @@ const data = ref({
const list = ref([]);
const bowTargetSrc = ref("");
const heatMapImageSrc = ref(""); // 存储热力图图片地址
+const canvasVisible = ref(false); // 控制canvas显示状态
const toListPage = () => {
uni.navigateTo({
@@ -59,10 +61,54 @@ const startScoring = () => {
}
};
+// 生成热力图测试数据
+const generateHeatMapData = (width, height, amount = 100) => {
+ const data = [];
+ const centerX = 0.5; // 中心点X坐标
+ const centerY = 0.5; // 中心点Y坐标
+
+ // 生成500条记录
+ for (let i = 0; i < amount; i++) {
+ let x, y, count;
+
+ // 30%的数据集中在中心区域(高斯分布)
+ if (Math.random() < 0.3) {
+ // 使用正态分布生成中心区域的数据
+ const angle = Math.random() * 2 * Math.PI;
+ const radius = Math.sqrt(-2 * Math.log(Math.random())) * 0.15; // 标准差0.15
+ x = centerX + radius * Math.cos(angle);
+ y = centerY + radius * Math.sin(angle);
+ count = Math.floor(Math.random() * 20);
+ } else {
+ x = Math.random() * 0.8 + 0.1; // 0.1-0.9范围
+ y = Math.random() * 0.8 + 0.1;
+ count = Math.floor(Math.random() * 20);
+ }
+
+ // 确保坐标在0-1范围内
+ x = Math.max(0.05, Math.min(0.95, x));
+ y = Math.max(0.05, Math.min(0.95, y));
+
+ data.push({
+ x: parseFloat(x.toFixed(3)),
+ y: parseFloat(y.toFixed(3)),
+ ring: Math.floor(Math.random() * 5) + 6, // 6-10环
+ count: count,
+ });
+ }
+
+ return data;
+};
+
const loadData = async () => {
const result = await getPointBookListAPI(1);
list.value = result.slice(0, 3);
const result2 = await getPointBookStatisticsAPI();
+
+ const rect = await getElementRect(".heat-map");
+ // const testWeekArrows = generateHeatMapData(rect.width, rect.height);
+ // result2.weekArrows = testWeekArrows;
+ // console.log(result2.weekArrows)
data.value = result2;
let hot = 0;
if (result2.checkInCount > -3 && result2.checkInCount < 3) hot = 1;
@@ -70,14 +116,13 @@ const loadData = async () => {
else if (result2.checkInCount >= 5) hot = 3;
else if (result2.checkInCount === 7) hot = 4;
uni.$emit("update-hot", hot);
- const rect = await getElementRect(".heat-map");
- // 延迟绘制热力图确保Canvas准备就绪
setTimeout(async () => {
try {
- const imagePath = await drawHeatMap(
+ const imagePath = await generateHeatmapImage(
+ "heatMapCanvas",
rect.width,
rect.height,
- result2.weekArrows
+ result2.weekArrows.filter((item) => item.x && item.y)
);
heatMapImageSrc.value = imagePath; // 存储生成的图片地址
console.log("热力图图片地址:", imagePath);
@@ -247,7 +292,17 @@ onShareTimeline(() => {
mode="widthFix"
/>
-
+