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
一定要设置足够的widthSegments
和heightSegments
,这样才有足够的面数形成高度山丘,否则就是一平面。
加个山丘长高的小动画
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