# 参考资料
# 目录结构
- heatmao.tar.gz
- img 文件夹
- 相关的区域图片
- ...
mockData.js
测试数据util.js
相关辅助方法index.js
index.html
mockData.js
// 设备热力图区域点
export const heamapAreaPoints = [
[42, 162],
[470, 162],
[470, 160],
[538, 160],
[540, 162],
[927, 162],
[930, 160],
[990, 160],
[993, 162],
[1150, 162],
[1150, 384],
[1142, 384],
[1142, 410],
[1150, 410],
[1150, 757],
[1140, 757],
[1140, 776],
[1000, 776],
[996, 779],
[954, 779],
[948, 776],
[803, 776],
[792, 756],
[771, 756],
[771, 775],
[530, 775],
[521, 779],
[475, 779],
[475, 775],
[454, 775],
[454, 768],
[416, 768],
[417, 756],
[398, 756],
[384, 776],
[80, 776],
[80, 757],
[74, 757],
[74, 410],
[80, 410],
[80, 385],
[42, 385],
[42, 162],
];
/**
* 设备测试的温度测点
*/
export const tempPointList = [
{
"x": 712,
"y": 662,
"value": 28.2
},
{
"x": 712,
"y": 486,
"value": 28.2
},
{
"x": 712,
"y": 310,
"value": 28.1
},
{
"x": 90,
"y": 299,
"value": 28
},
{
"x": 90,
"y": 470,
"value": 28.1
},
{
"x": 90,
"y": 689,
"value": 28.1
},
{
"x": 286,
"y": 678,
"value": 28
},
{
"x": 286,
"y": 474,
"value": 28
},
{
"x": 286,
"y": 282,
"value": 28
},
{
"x": 492,
"y": 694,
"value": 27.8
},
{
"x": 498,
"y": 480,
"value": 27.7
},
{
"x": 498,
"y": 291,
"value": 27.7
},
{
"x": 932,
"y": 300,
"value": 27.6
},
{
"x": 932,
"y": 489,
"value": 27.6
},
{
"x": 932,
"y": 650,
"value": 27.6
}
];
util.js
/**
* 齐次坐标(四维齐次坐标) 统一处理平移、旋转、缩放等变换
* ` (x,y,z,w) `
* - z 值代表深度测试, 来确定物体的前后关系
* - w 值通常用来表示 位置 还是 方向; `(x, y, z, 1.0) 表示一个位置` `(x, y, z, 0.0) 表示一个方向`
*/
/**
* 坐标系转换-Canvas转WebGL (Canvas TO WebGL)返回的数值的精度很关键!!!
* @param {number} 画布上x轴的点位坐标
* @param {number} 画布上y轴的点位坐标
* @param {number} 画布宽度
* @param {number} 画布高度
* @returns {number[]} ndc坐标(三维, 浮点数, 最多保留4位小数)
* - Canvas 二维坐标系
* (像素级别)默认的坐标系的原点 (0, 0) 位于画布的左上角。坐标系中的 x 值沿水平方向增加,y 值沿垂直方向增加,即 x 向右增大,y 向下增大
* - WebGL 三维坐标系(z轴默认填充0.0, z轴的分量代表深度测试, 来确定物体的前后关系)
* 原点是 (0, 0, 0),位于视口的中心。WebGL 使用的是一个标准化坐标系,范围通常是 [-1, 1],这意味着 x 和 y 的值在 NDC 中会被标准化到这个范围内
*/
export function coordCanvasToWebGL(x_canvas, y_canvas, canvasWidth, canvasHeight) {
const x_ndc = +((x_canvas / (canvasWidth / 2)) - 1).toFixed(6);
const y_ndc = +(1 - (y_canvas / (canvasHeight / 2))).toFixed(6);
return [x_ndc, y_ndc, 0.0];
}
/**
* 坐标系转换-Canvas转WebGL (WebGL TO Canvas)
* @param {number} 画布上x轴的点位坐标
* @param {number} 画布上y轴的点位坐标
* @param {number} 画布宽度
* @param {number} 画布高度
* @returns {number[]} 画布坐标(二维, 整数)
* - Canvas 二维坐标系
* (像素级别)默认的坐标系的原点 (0, 0) 位于画布的左上角。坐标系中的 x 值沿水平方向增加,y 值沿垂直方向增加,即 x 向右增大,y 向下增大
* - WebGL 三维坐标系(z轴默认填充0.0, z轴的分量代表深度测试, 来确定物体的前后关系)
* 原点是 (0, 0),位于视口的中心。WebGL 使用的是一个标准化坐标系,范围通常是 [-1, 1],这意味着 x 和 y 的值在 NDC 中会被标准化到这个范围内
*/
export function coordWebGLToCanvas(x_ndc, y_ndc, canvasWidth, canvasHeight) {
const x_canvas = Math.ceil((x_ndc + 1) * (canvasWidth / 2));
const y_canvas = Math.ceil((1 - y_ndc) * (canvasHeight / 2));
return [x_canvas, y_canvas];
}
/**
* 加载图片资源
* @param {*} url
* @returns
*/
export function loadImageUrl(url) {
return new Promise((resolve, reject) => {
const image = new Image();
image.src = url;
image.onload = () => {
resolve({ url, image });
};
image.onerror = reject;
});
}
/**
* 根据多边形的点构建矩形-用于计算热力图形状的顶点信息
*/
export function genHeatmapAreaByPoints(areaPoints = []) {
let left = areaPoints[0][0];
let right = areaPoints[0][0];
let top = areaPoints[0][1];
let bottom = areaPoints[0][1];
for (const [x, y] of areaPoints) {
if (x < left) {
left = x; // 更新最左点
}
if (x > right) {
right = x; // 更新最右点
}
if (y < top) {
top = y; // 更新最上点
}
if (y > bottom) {
bottom = y; // 更新最下点
}
}
const width = right - left;
const height = bottom - top;
return {
/**
* 左上角
*/
leftTop: [left, top],
rightTop: [right, top],
rightBottom: [right, bottom],
/**
* 左下角
*/
leftBottom: [left, bottom],
width,
height,
};
}
index.js
import { coordCanvasToWebGL } from "./util.js";
/**
* 顶点着色器代码
*/
const vertexShaderSource = `#version 300 es
in vec4 a_position;
// out vec4 cur_position;
void main() {
// float curPointSize = 60.0;
// gl_PointSize = curPointSize;
// cur_position = a_position;
gl_Position = a_position;
}
`;
/**
* 片元着色器代码
*/
const fragmentShaderSource = `#version 300 es
precision mediump float;
// in vec4 cur_position;
out vec4 color;
uniform vec3 iResolution;
// test data
// uniform int PointCount;
const int PointCount = 30;
uniform vec3 Points[30];
uniform float HEAT_MAX;
const float PointRadius = .75;
vec4 gradient(float w, vec2 uv) {
w = pow(clamp(w, 0., 1.) * 3.14159 * .5, .9);
vec3 c = vec3(sin(w), sin(w * 2.), cos(w)) * 1.1;
float a = clamp(w, 0.5, 1.);
return mix(vec4(0.25, 1.0, 0.0, 0.5), vec4(c, a), w*0.2);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2. * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
float d = 0.;
for (int i = 0; i < PointCount; i++) {
vec3 v = Points[i];
float intensity = v.z / HEAT_MAX;
float dis = length(uv - v.xy);
float pd = (1.0 - dis / PointRadius) * intensity;
d += pow(max(0., pd), 2.);
}
fragColor = gradient(d, uv);
}
void main() {
vec4 col = vec4(1.0, 0.0, 0.0, 1.0);
mainImage(col, gl_FragCoord.xy);
color = col;
}
`;
/**
* webgl2绘制上下文
*/
var gl;
/**
* 获取着色器源码-fn
* @param {string} 文件名称
* @returns {Promise<string>} 着色器源码
*/
function getShaderSource(filename) {
return fetch(filename).then((res) => res.text());
}
/**
* 创建着色器-fn
* @param {GLenum} 着色器类型
* @param {string} 着色器源码
* @returns {WebGLShader} shader着色器
*/
function createShaderFn(shaderType, shaderSource) {
// 创建着色器
const shader = gl.createShader(shaderType);
// 着色器添加源码
gl.shaderSource(shader, shaderSource);
// 编译着色器源码
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const shaderTypeText = gl.VERTEX_SHADER === shaderType ? 'vertexShader' : 'fragmentShader';
console.error(`Error compiling ${shaderTypeText}:`, gl.getShaderInfoLog(shader)); // 输出编译错误信息
}
return shader;
}
/**
* 创建shader程序-fn
* @param {WebGLShader} 顶点着色器
* @param {WebGLShader} 片元着色器
* @returns {WebGLProgram} shader程序
*/
function createProgramFn(vertexShader, fragmentShader) {
// 创建shader程序
const shaderProgram = gl.createProgram();
// shader程序添加着色器
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
// 链接shader程序
gl.linkProgram(shaderProgram);
// 使用shader程序
gl.useProgram(shaderProgram);
return shaderProgram;
}
/**
*
* @param {*} pCanvasInfo 主画布的信息(parent canvas宽度、高度等信息)
* @param {*} tempPointList 测试的温度测点
* @returns
*/
function main(pCanvasInfo = {}, tempPointList = []) {
// 获取顶点着色器和片元着色器的GLSL代码
// const vertexShaderSource = await getShaderSource('./vertShader.vs');
// const fragmentShaderSource = await getShaderSource('./fragShader.fs');
const canvasDom = document.createElement('canvas');
canvasDom.width = pCanvasInfo.width || 1200;
canvasDom.height = pCanvasInfo.height || 804;
gl = canvasDom.getContext('webgl2');
// 创建着色器(顶点、片元着色器)
const vertexShader = createShaderFn(gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShaderFn(gl.FRAGMENT_SHADER, fragmentShaderSource);
// 创建shader程序
const shaderProgram = createProgramFn(vertexShader, fragmentShader);
const canvasNdcList = [
-1.0, 1.0,
1.0, 1.0,
-1.0, -1.0,
1.0, -1.0,
];
const triangleVertices = new Float32Array(canvasNdcList);
// 创建缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, triangleVertices, gl.STATIC_DRAW);
// 获取a_position属性的位置
var a_Position = gl.getAttribLocation(shaderProgram, 'a_position');
// 将缓冲区的数据分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 开启a_Position变量
gl.enableVertexAttribArray(a_Position);
const iResolutionAddr = gl.getUniformLocation(shaderProgram, "iResolution");
//给iResolutionAddr 字段设置数据 ,是canvas的像素宽高
gl.uniform3f(iResolutionAddr, gl.canvas.width, gl.canvas.height, 1.0);
const points = [];
for (const item of tempPointList) {
const point = coordCanvasToWebGL(item.x, item.y, gl.canvas.width, gl.canvas.height);
point[2] = item.value;
points.push(point);
}
// 获取 uniform 位置
const uniformAddr = gl.getUniformLocation(shaderProgram, "Points");
gl.uniform3fv(uniformAddr, new Float32Array(points.flat()));
const HEATMAXAddr = gl.getUniformLocation(shaderProgram, "HEAT_MAX");
gl.uniform1f(HEATMAXAddr, 35.5);
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// gl.clearColor(0.0, 1.0, 0.0, 0.75); // 设置canvas的清除颜色
// gl.clear(gl.COLOR_BUFFER_BIT); // 清除canvas
// 执行绘制
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
return gl.canvas;
}
function generateHeatmapCanvas(pCanvasInfo = {}, tempPointList = []) {
const canvas = main(pCanvasInfo, tempPointList);
return canvas;
}
async function getHeatmapImage(pCanvasInfo = {}, tempPointList = []) {
const canvas = main(pCanvasInfo, tempPointList);
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
resolve(img);
};
img.src = canvas.toDataURL('image/png', 1.0);
});
}
export {
generateHeatmapCanvas,
getHeatmapImage,
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Heatmap</title>
</head>
<body>
<canvas id="canvasDom" width="1200" height="804"></canvas>
</body>
<script type="module">
import { generateHeatmapCanvas } from "./index.js";
import { loadImageUrl, genHeatmapAreaByPoints } from "./util.js";
import { heamapAreaPoints, tempPointList } from "./mockData.js";
let ctx;
/***
* 画底层
*/
async function drawBaseLayer() {
const { image }= await loadImageUrl('http://cdn.xiaomaike.space/img/%E5%BA%95%E5%9B%BE5.png');
const canvasDom = document.getElementById('canvasDom'); // 尝试获取canvas dom节点
const width = canvasDom.width || 1200; // 尝试获取canvas的宽度
const height = canvasDom.height || 804;
ctx = canvasDom.getContext('2d');
ctx.save();
ctx.beginPath();
ctx.drawImage(image, 0, 0, width, height);
ctx.restore();
}
/**
* 画热力图层
* heatmapArea 热力图形状的顶点信息(包括左上角、右上角、右下角、左下角; 宽度、高度的顶点信息)
*/
function drawHeatMapLayer() {
const heatmapArea = genHeatmapAreaByPoints(heamapAreaPoints);
const mainCanvasInfo = { width: canvasDom.width || 1200, height: canvasDom.height || 804 };
const heatmapCanvas = generateHeatmapCanvas(mainCanvasInfo, tempPointList);
// console.log(heatmapCanvas);
ctx.save();
ctx.beginPath();
const firstX = heamapAreaPoints[0][0];
const firstY = heamapAreaPoints[0][1];
ctx.moveTo(firstX, firstY); // 移动到第一个点
// 绘制从第一个点到其他点的线
for (let i = 1; i < heamapAreaPoints.length; i++) {
const x = heamapAreaPoints[i][0];
const y = heamapAreaPoints[i][1];
ctx.lineTo(x, y);
}
ctx.clip();
ctx.drawImage(
heatmapCanvas,
heatmapArea.leftTop[0],
heatmapArea.leftTop[1],
heatmapArea.width,
heatmapArea.height,
);
ctx.restore();
}
/**
* 画顶层(突出强调设备的效果)
*/
async function drawEmphasizeLayer() {
const { image } = await loadImageUrl('http://cdn.xiaomaike.space/img/%E5%BA%95%E5%9B%BE5_a.png');
ctx.save();
ctx.beginPath();
const width = ctx.canvas.width || 1200;
const height = ctx.canvas.height || 804;
ctx.drawImage(image, 0, 0, width, height);
ctx.restore();
}
/*** 主函数 ***/
async function run() {
await drawBaseLayer();
drawHeatMapLayer();
drawEmphasizeLayer();
}
run();
</script>
</html>