# 参考资料

# 目录结构

  • 相关的区域图片
  • ...
  • 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>

# 效果

1-1设备热力图.png

Last Updated: 4/27/2025, 3:47:33 PM
起风了
宋姿璇