更新一版热力图
This commit is contained in:
105
src/heatmap.js
Normal file
105
src/heatmap.js
Normal file
@@ -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})`;
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
getPointBookStatisticsAPI,
|
getPointBookStatisticsAPI,
|
||||||
} from "@/apis";
|
} from "@/apis";
|
||||||
|
|
||||||
import { getElementRect, drawHeatMap } from "@/util";
|
import { getElementRect } from "@/util";
|
||||||
|
import { generateHeatmapImage } from "@/heatmap";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -38,6 +39,7 @@ const data = ref({
|
|||||||
const list = ref([]);
|
const list = ref([]);
|
||||||
const bowTargetSrc = ref("");
|
const bowTargetSrc = ref("");
|
||||||
const heatMapImageSrc = ref(""); // 存储热力图图片地址
|
const heatMapImageSrc = ref(""); // 存储热力图图片地址
|
||||||
|
const canvasVisible = ref(false); // 控制canvas显示状态
|
||||||
|
|
||||||
const toListPage = () => {
|
const toListPage = () => {
|
||||||
uni.navigateTo({
|
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 loadData = async () => {
|
||||||
const result = await getPointBookListAPI(1);
|
const result = await getPointBookListAPI(1);
|
||||||
list.value = result.slice(0, 3);
|
list.value = result.slice(0, 3);
|
||||||
const result2 = await getPointBookStatisticsAPI();
|
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;
|
data.value = result2;
|
||||||
let hot = 0;
|
let hot = 0;
|
||||||
if (result2.checkInCount > -3 && result2.checkInCount < 3) hot = 1;
|
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 >= 5) hot = 3;
|
||||||
else if (result2.checkInCount === 7) hot = 4;
|
else if (result2.checkInCount === 7) hot = 4;
|
||||||
uni.$emit("update-hot", hot);
|
uni.$emit("update-hot", hot);
|
||||||
const rect = await getElementRect(".heat-map");
|
|
||||||
// 延迟绘制热力图确保Canvas准备就绪
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const imagePath = await drawHeatMap(
|
const imagePath = await generateHeatmapImage(
|
||||||
|
"heatMapCanvas",
|
||||||
rect.width,
|
rect.width,
|
||||||
rect.height,
|
rect.height,
|
||||||
result2.weekArrows
|
result2.weekArrows.filter((item) => item.x && item.y)
|
||||||
);
|
);
|
||||||
heatMapImageSrc.value = imagePath; // 存储生成的图片地址
|
heatMapImageSrc.value = imagePath; // 存储生成的图片地址
|
||||||
console.log("热力图图片地址:", imagePath);
|
console.log("热力图图片地址:", imagePath);
|
||||||
@@ -247,7 +292,17 @@ onShareTimeline(() => {
|
|||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
<image v-if="heatMapImageSrc" :src="heatMapImageSrc" mode="widthFix" />
|
<image v-if="heatMapImageSrc" :src="heatMapImageSrc" mode="widthFix" />
|
||||||
<canvas canvas-id="heatMapCanvas" style="width: 100%; height: 100%" />
|
<canvas
|
||||||
|
canvas-id="heatMapCanvas"
|
||||||
|
style="
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
<view class="reward" v-if="data.totalArrow">
|
<view class="reward" v-if="data.totalArrow">
|
||||||
<button hover-class="none" @click="showTip = true">
|
<button hover-class="none" @click="showTip = true">
|
||||||
@@ -392,8 +447,8 @@ onShareTimeline(() => {
|
|||||||
|
|
||||||
.heat-map > canvas {
|
.heat-map > canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: -1000px;
|
||||||
left: -1000px;
|
left: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
72
src/util.js
72
src/util.js
@@ -445,75 +445,3 @@ export const calcRing = (bowtargetId, x, y, diameter) => {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 绘制热力图并生成图片
|
|
||||||
export const drawHeatMap = (width, height, arrows) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
const ctx = uni.createCanvasContext("heatMapCanvas");
|
|
||||||
|
|
||||||
// ctx.setFillStyle("rgba(255, 255, 255, 0.4)");
|
|
||||||
// ctx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
let minCount = 0;
|
|
||||||
let maxCount = 0;
|
|
||||||
// 计算最大和最小频次
|
|
||||||
arrows.forEach((point) => {
|
|
||||||
if (point.count > maxCount) {
|
|
||||||
maxCount = point.count;
|
|
||||||
}
|
|
||||||
if (point.count < minCount) {
|
|
||||||
minCount = point.count;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 绘制热力点
|
|
||||||
arrows.forEach((point) => {
|
|
||||||
if (point.x === 0 && point.y === 0) return;
|
|
||||||
// 数量越多,半径越小(反比关系)
|
|
||||||
const radius = 20;
|
|
||||||
|
|
||||||
// 根据频次设置同一种颜色的5个深浅度
|
|
||||||
let color = "rgba(71, 254, 102, 0.2)";
|
|
||||||
// if (point.count >= maxCount * 0.5) {
|
|
||||||
// color = "rgba(255, 232, 143, 0.7)"; // 最深 - 频次5次及以上
|
|
||||||
// } else if (point.count >= maxCount * 0.4) {
|
|
||||||
// color = "rgba(255, 232, 143, 0.5)"; // 较深 - 频次4次
|
|
||||||
// } else if (point.count >= maxCount * 0.3) {
|
|
||||||
// color = "rgba(255, 232, 143, 0.3)"; // 中等 - 频次3次
|
|
||||||
// } else if (point.count >= maxCount * 0.2) {
|
|
||||||
// color = "rgba(255, 232, 143, 0.2)"; // 较浅 - 频次2次
|
|
||||||
// } else {
|
|
||||||
// color = "rgba(255, 232, 143, 0.1)"; // 最浅 - 频次1次
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 绘制圆形热力点
|
|
||||||
ctx.setFillStyle(color);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(point.x * width, point.y * height, radius, 0, 2 * Math.PI);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("绘制热力图失败:", error);
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user