用Three.js搞个炫酷热力山丘图

1.画热力canvas图

画黑白渐变圆圈

js 复制代码
//option.radius半径,(x,y)圆心坐标
 const grad = ctx.createRadialGradient(x, y, 0, x, y, option.radius);
  grad.addColorStop(0.0, 'rgba(0,0,0,1)');//纯黑
  grad.addColorStop(1.0, 'rgba(0,0,0,0)');//透明黑
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.arc(x, y, option.radius, 0, 2 * Math.PI);
  ctx.closePath();
  //根据数据设置当前圆圈的透明度
  ctx.globalAlpha = (value - option.min) / option.size;
  ctx.fill();

根据数据画多个黑白渐变圆圈

处理经纬度数据,经纬度数据转像素坐标要用到d3-geo.geoMercator墨卡托投影,

js 复制代码
//geoUtil.js
import d3geo from './d3-geo.min.js';

let geoFun;
export function initGeoFun(size) {
  //放大倍数
  geoFun = d3geo.geoMercator().scale(size || 100);
}
//经纬度转px
export const latlng2px = (pos) => {
  if (pos[0] >= -180 && pos[0] <= 180 && pos[1] >= -90 && pos[1] <= 90) {
    return geoFun(pos);
  }
  return pos;
};

计算数据信息,并进行整理转换,为画圆做准备

js 复制代码
initGeoFun(1000);
      fetch('./assets/traffic.json')
        .then((res) => res.json())
        .then((res) => {
         
          let info = {
            max: Number.MIN_SAFE_INTEGER,
            min: Number.MAX_SAFE_INTEGER,
            maxlng: Number.MIN_SAFE_INTEGER,
            minlng: Number.MAX_SAFE_INTEGER,
            maxlat: Number.MIN_SAFE_INTEGER,
            minlat: Number.MAX_SAFE_INTEGER,
            data: []
          };
          res.features.forEach((item) => {
            let pos = latlng2px(item.geometry.coordinates);
            let newitem = {
              lng: pos[0],
              lat: pos[1],
              value: item.properties.avg
            };
            info.max = Math.max(newitem.value, info.max);
            info.maxlng = Math.max(newitem.lng, info.maxlng);
            info.maxlat = Math.max(newitem.lat, info.maxlat);

            info.min = Math.min(newitem.value, info.min);
            info.minlng = Math.min(newitem.lng, info.minlng);
            info.minlat = Math.min(newitem.lat, info.minlat);
            info.data.push(newitem);
          });
          //数值范围
          info.size = info.max - info.min;
          //经纬度范围
          info.sizelng = info.maxlng - info.minlng;
          info.sizelat = info.maxlat - info.minlat;
          console.log(info);
           const radius = 50;
          createHeatmap({
              //圆心在边界的情况,圆会显示不全,预留半径的空间
            width: info.sizelng + radius * 2,
            height: info.sizelng + radius * 2,
            //颜色列表
            colors: {
              0.1: '#2A85B8',
              0.2: '#16B0A9',
              0.3: '#29CF6F',
              0.4: '#5CE182',
              0.5: '#7DF675',
              0.6: '#FFF100',
              0.7: '#FAA53F',
              1: '#D04343'
            },
            radius,
            ...info
             
          });
        });

注意:

  • 画圆时,圆心可能会出现在边界,这时会导致圆显示不全,因此画布要预留半径的空间+radius * 2
  • 同理,在画单个圆时,要让圆心整体向右下方偏移radius
js 复制代码
 let { lng, lat, value } = item;
  let x = lng - option.minlng + option.radius;
  let y = lat - option.minlat + option.radius;

绘制多个黑白渐变圆,形成热力图

js 复制代码
 const colorData = createColors(option);
  const canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  canvas.width = option.width;
  canvas.height = option.height;
  const ctx = canvas.getContext('2d');  
  option.data.forEach((item) => {
    drawCircle(ctx, option, item);
  });

数据来源于:高德地图-普通热力图-全国交通事故增长率

将黑白热力图转换成彩色

绘制渐变颜色列表,用于取色

js 复制代码
function createColors(option) {
  const canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  const ctx = canvas.getContext('2d');
  canvas.width = 256;
  canvas.height = 1;
  //从左到右渐变
  const grad = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
  for (let k in option.colors) {
    grad.addColorStop(k, option.colors[k]);
  }
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);
   //返回canvas图片数据256个像素颜色
  return ctx.getImageData(0, 0, canvas.width, 1).data;
}

根据黑白热力图的绘制流程可知透明度就是对应的热力值,不同热力值要取颜色列表上的不同颜色。而canvas的ImageData的数值范围是[0-255],四个元素为一个单位对应rgba,那么我们只要根据透明度作为索引对应颜色列表256个颜色,赋值到ImageData的对应色值即可

js 复制代码
const colorData = createColors(option);
//获取canvas图片颜色值
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  for (let i = 3; i < imageData.data.length; i = i + 4) {
    let opacity = imageData.data[i];
    let offset = opacity * 4;
    //设置颜色映射
    //red
    imageData.data[i - 3] = colorData[offset];
    //green
    imageData.data[i - 2] = colorData[offset + 1];
    //blue
    imageData.data[i - 1] = colorData[offset + 2];
  }
//颜色值重新赋值到canvas
  ctx.putImageData(imageData, 0, 0);

这样2D热力图绘制大功告成了!

  • 其中可以通过调整option.colors颜色列表的不同透明度的对应颜色和option.radius半径大小来调节热力图的样式。上图是option.radius=50,下图是option.radius=30的情况,可见红色的热力明显变小了

画3D热力图

将上面绘制的2D热力canvas作为贴图覆盖到Plane平面上,可以得到热力平面图

js 复制代码
const { canvas: heatmapCanvas, option } = await this.createHeatmap();
          const map = new THREE.CanvasTexture(heatmapCanvas);      
          map.wrapS = THREE.RepeatWrapping;
          map.wrapT = THREE.RepeatWrapping;
          //平面大小跟canvas的比例对应
          const geometry = new THREE.PlaneGeometry(
            option.width * 0.5,
            option.height * 0.5,
             500,
            500
          );

          const material = new THREE.MeshBasicMaterial({
            map: map,
            side: THREE.DoubleSide,//双面可见
            transparent: true//开启透明
          });
      const plane = new THREE.Mesh(geometry, material);
      //旋转90度
          plane.rotateX(-Math.PI * 0.5);
          this.scene.add(plane);
  • 顶点着色器:因为热力贴图透明度对应热力值,所以可以通过texture2D获取透明度来计算position的高度值,从而形成山丘一样的3D热力
c++ 复制代码
uniform sampler2D map;//热力贴图
uniform float uHeight; //高度
varying vec2 v_texcoord;//传递贴图uv变量
void main(void)
{
    v_texcoord = uv;
    float h=texture2D(map, v_texcoord).a*uHeight;//获取透明度,计算坐标高度
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position.x,position.y,h, 1.0 );
}
  • 片元着色器,直接
c++ 复制代码
precision mediump float;
uniform float uOpacity;//透明度设置
uniform sampler2D map;//热力贴图
varying vec2 v_texcoord;//传递贴图uv变量
void main (void)
{
//获取贴图颜色
vec4 color= texture2D(map, v_texcoord);
//设置为贴图颜色值
gl_FragColor.rgb =color.rgb;
//计算新的透明度
float a=color.a*uOpacity;
gl_FragColor.a=a>1.0?1.0:a;
}

使用ShaderMaterial材质

js 复制代码
const material = new THREE.ShaderMaterial({
              transparent: true,//开启透明
              side: THREE.DoubleSide,//双面
              uniforms: {
              //热力贴图
                map: { value: map },
                //高度
                uHeight: { value: 50 },
                //透明度设置
                 uOpacity: { value: 2.0 }
              },
              vertexShader: ``,
              fragmentShader: ``
            });

注意:

  • PlaneGeometry一定要设置足够的widthSegmentsheightSegments,这样才有足够的面数形成高度山丘,否则就是一平面。

加个山丘长高的小动画

js 复制代码
let tween = new this.TWEEN.Tween({ v: 0 })
              .to({ v: 50 }, 1000)

              .onUpdate((obj) => {
                material.uniforms.uHeight.value = obj.v;
              })
              .easing(this.TWEEN.Easing.Quadratic.Out)
              .start();
              this.TWEEN.add(tween);

噔噔噔噔!3D热力山丘图搞定了!

GitHub地址

https://github.com/xiaolidan00/my-earth

参考

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   7 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web8 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery