更新一版热力图
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,
|
||||
} 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"
|
||||
/>
|
||||
<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 class="reward" v-if="data.totalArrow">
|
||||
<button hover-class="none" @click="showTip = true">
|
||||
@@ -392,8 +447,8 @@ onShareTimeline(() => {
|
||||
|
||||
.heat-map > canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -1000px;
|
||||
top: -1000px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
|
||||
72
src/util.js
72
src/util.js
@@ -445,75 +445,3 @@ export const calcRing = (bowtargetId, x, y, diameter) => {
|
||||
}
|
||||
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