前言:学习自用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.jsfunction 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.jsimport { 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;
};
效果图: