每日一练-threejs实现三维动态热力图

前言:学习自用Three.js搞个炫酷热力山丘图,作者讲解的十分详细,在这里不再过多赘述,直接上代码!

<template>
  <div class="map" ref="map"></div>
</template>

<script setup>
import * as THREE from "three"
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"
import { Tween, Easing } from 'three/examples/jsm/libs/tween.module.js';
import { ref, onMounted } from "vue"
import axios from 'axios'
import { latlng2px } from "@/lib/index.js"
import { createHeatmap } from "./heatmap.js"
let map = ref(null);

onMounted(() => {
  // 创建三维场景
  let scene = new THREE.Scene();
  let width = map.value.offsetWidth;
  let height = map.value.offsetHeight;
  
  let camera = new THREE.PerspectiveCamera(45, width/height, 1, 10000);
  camera.position.set(100, 100, 100);
  camera.lookAt(0, 0, 0);
  let renderer = new THREE.WebGLRenderer({
    antialias: true
  });
  renderer.setSize(width, height);
  map.value.appendChild(renderer.domElement)

  let orbitControls = new OrbitControls(camera, renderer.domElement);
  orbitControls.position0.set(100, 100, 100);
  orbitControls.target.set(0, 0, 0);

  // 生成二维热力图
  function init2DMap(geojson) { 
    let option = {
      max: Number.MIN_SAFE_INTEGER,
      min: Number.MAX_SAFE_INTEGER,
      maxlon: Number.MIN_SAFE_INTEGER,
      minlon: Number.MAX_SAFE_INTEGER,
      maxlat: Number.MIN_SAFE_INTEGER,
      minlat: Number.MAX_SAFE_INTEGER,
      data: []
    }

    geojson.features.forEach(item => {
      let position = latlng2px(item.geometry.coordinates);  // 经纬度转像素
      let newItem = {
        lon: position[0],
        lat: position[1],
        value: item.properties.avg  // 决定热力图高度的属性值
      };
      option.max = Math.max(newItem.value, option.max);
      option.maxlon = Math.max(newItem.lon, option.maxlon);
      option.maxlat = Math.max(newItem.lat, option.maxlat);

      option.min = Math.min(newItem.value, option.min);
      option.minlon = Math.min(newItem.lon, option.minlon);
      option.minlat = Math.min(newItem.lat, option.minlat);
      option.data.push(newItem);
    });
    option.radius = 8;
    option.width = option.maxlon - option.minlon + option.radius * 2;
    option.height = option.maxlat - option.minlat + option.radius * 2;
    option.colors = {
      0.1: '#2A85B8',
      0.2: '#16B0A9',
      0.3: '#29CF6F',
      0.4: '#5CE182',
      0.5: '#7DF675',
      0.6: '#FFF100',
      0.7: '#FAA53F',
      1: '#D04343'
    }    
    let canvas = createHeatmap(option)
    return { canvas, option }
  }
  // 生成三维热力图
  function initHeatmap(data) {
    let { canvas: heatmapCanvas, option } = init2DMap(data);
    const map = new THREE.CanvasTexture(heatmapCanvas);
    map.wrapS = THREE.RepeatWrapping;
    map.wrapT = THREE.RepeatWrapping;
    let geometry = new THREE.PlaneGeometry(option.width, option.height, 500, 500);
    let material = new THREE.ShaderMaterial({
      transparent: true,
      side: THREE.DoubleSide,
      uniforms: {
        map: { value: map },
        uHeight: { value: 20 },
        uOpacity: { value: 2.0 }
      },
      vertexShader: `uniform sampler2D map;
                    uniform float uHeight;
                    varying vec2 v_texcoord;
        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 );
        }`,
      fragmentShader: `precision mediump float;
        uniform sampler2D map;
        uniform float uOpacity;
        varying vec2 v_texcoord;
        void main (void)
        {
          vec4 color= texture2D(map, v_texcoord);
          float a=color.a*uOpacity;
        gl_FragColor.rgb =color.rgb;
        gl_FragColor.a=a>1.0?1.0:a;
        }`
    })

    let mesh = new THREE.Mesh(geometry, material);
    mesh.rotateX( -Math.PI / 2);
    scene.add(mesh);

    tween.to({ v: 20 }, 4000)
        .onUpdate((obj) => { material.uniforms.uHeight.value = obj.v;})
        .easing(Easing.Quadratic.Out);
    tween.start();
  }

  // 动画
  let tween = new Tween({ v: 0 })
  // 基于高德地图交通数据生成的热力图
  axios.get("https://a.amap.com/Loca/static/loca-v2/demos/mock_data/traffic.json")
        .then(res => {
          if (res.status == 200 && res.data) {
            initHeatmap(res.data);
          }
        })
        
  // 渲染
  function render() {
    tween.update();
    renderer.render(scene, camera);
    requestAnimationFrame(render);
  }
  render()
})

</script>

<style scoped>
.map {
  width: 100%;
  height: 100%;
}
</style>
// heatmap.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);

  return ctx.getImageData(0, 0, canvas.width, 1).data;
}
function drawCircle(ctx, option, item) {
  let { lon, lat, value } = item;
  let x = lon - option.minlon + option.radius;
  let y = lat - option.minlat + option.radius;
  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();
}
export function createHeatmap(option) {
  const canvas = document.createElement('canvas');
  // document.body.appendChild(canvas);
  canvas.width = option.width;
  canvas.height = option.height;
  const ctx = canvas.getContext('2d');

  option.size = option.max - option.min;
  option.data.forEach((item) => {
    drawCircle(ctx, option, item);
  });
  const colorData = createColors(option);
  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;
    imageData.data[i - 3] = colorData[offset];
    imageData.data[i - 2] = colorData[offset + 1];
    imageData.data[i - 1] = colorData[offset + 2];
  }

  ctx.putImageData(imageData, 0, 0);
  return canvas;
}
// lib/index.js

import { geoMercator } from 'd3-geo';

export const latlng2px = (pos) => {
  if (pos[0] >= -180 && pos[0] <= 180 && pos[1] >= -90 && pos[1] <= 90) {
    return geoMercator()(pos);
  }
  return pos;
};

效果图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值