canvas渲染熱力圖的一種方式
今天早上看了下 ofollow,noindex">heatMap.js 的原始碼,瞭解了他是如何繪製熱力圖的,這裡我們拋開其資料處理的部分,聚焦熱力圖的繪製。
如果要繪製一個點的熱力圖,可以簡單是的使用 createRadialGradient
來實現,但是如果兩個點的熱力圖發生了重疊,重疊部分當然不是簡單的覆蓋。這種情況下我們當然可以使用畫素級的操作,結合兩個點的熱力圖通過複雜的計算得到覆蓋之後的熱力圖,但顯然過於複雜。
我們仔細觀察下熱力圖,他其實就是一些顏色的漸變產生的效果,中間部分顏色深一點,外圍淺一點,我們實際上就是根據權重的大小來著色。比如我們在[80, 80]的地方有一個點,像半徑10的周圍輻射,我們把重心的權重設為100,最外圍設為10,我們很容易想到,使用一個單色繪製。最方便就是使用灰色,只需要使用透明度就可,其畫素點的rgb值都是0,這樣的資料就方便處理,如下圖。

所以步驟就是先使用這種灰度先繪製到一個 canvas
上,其每一個點的rgba都是 (0, 0, 0, 0)
到 (0, 0, 0, 255)
之間。現在就可以根據其alpha值將其著色。現在有一個漸變色卡如下,其對應關係就是alpha的值為0,對應色卡的左邊,255對應右邊。

一種簡單的方式就是使用漸變色繪製一個寬為256的canvas,取得這256個點的顏色,然後與canvas進行一一對應。比如,我們的主canvas中某個畫素點的alpha值為100,那麼就將該店的顏色修改為色卡中第100(程式員計數)個點的顏色。
具體實現過程如下:
getImageData
注:1. 每一個點根據值得大小設定顏色深度可以根據值得大小修改相應的 globalAlpha
。 2. 灰度canvas的繪製也不一定必須的繪製到主canvas,也可以使用離屏canvas,最後一步在講結果繪製到主canvas(heatMap.js就是如此)。 3. 灰度資料可以使用Uint8ClampedArray來運算,不一定非得畫出灰色的canvas來獲取資料,計算並不複雜。
思路就是如此,下面就是一個簡單的實現方式。

interface HeatMapConfig { gradient?: object; radius?: number; width?: number; height?: number; min?: number; max?: number; container: HTMLElement } interface PointData{ x: number; y: number; value: number; } class HeatMap { static defaultConfig = { gradient: { 0.3: "blue", 0.5: "lime", 0.7: "yellow", 1: "red" }, min: 0, max: 100, radius: 40, width: 400, height: 400 } private config: HeatMapConfig; private canvas = this.createCanvas(); private ctx = this.canvas.getContext('2d'); private data: PointData[] = []; constructor(config: HeatMapConfig) { this.initConfig(config); } private initConfig(config: HeatMapConfig) { if(!config.container) { throw Error('no container'); } this.config = { ...HeatMap.defaultConfig, ...config }; const {width, height} = this.config; this.canvas.width = width; this.canvas.height = height; this.config.container.appendChild(this.canvas); } initData(data: PointData[]) { this.data = data; this.render(); } private render() { this.renderAlpha(); this.putColor() } // 繪製alpha通道的圓 private renderAlpha(){ const shadowCanvas = this.createShadowTpl(); const {min, max} = this.config; for(let point of this.data) { const alpha = (point.value - min) / (max - min); this.ctx.globalAlpha = alpha; this.ctx.drawImage(shadowCanvas, point.x, point.y); } } // 為alpha通道的圓著色 private putColor() { const colorData = this.createColordata(); const imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); const {data} = imgData for(let i = 0; i < data.length; i++) { const value = data[i]; if(value) { data[i - 3] = colorData[4 * value]; data[i - 2] = colorData[4 * value + 1]; data[i - 1] = colorData[4 * value + 2]; } } this.ctx.putImageData(imgData, 0, 0); } private createCanvas(){ return document.createElement('canvas') } private createColordata(){ const cCanvas = this.createCanvas(); const cCtx = cCanvas.getContext('2d'); cCanvas.width = 256; cCanvas.height = 1; const tuple: [number, number, number, number] = [0, 0, cCanvas.width, cCanvas.height] const grd = cCtx.createLinearGradient(...tuple); const {gradient} = this.config; for(let key in gradient) { grd.addColorStop(parseFloat(key), gradient[key]); } cCtx.fillStyle = grd; cCtx.fillRect(0, 0, cCanvas.width, cCanvas.height); return cCtx.getImageData(...tuple).data; } /** * 離屏canvas繪製一個黑色(rgb都是0,方便處理)的alpha通道的圓 */ private createShadowTpl() { const tCanvas = this.createCanvas(); const tCtx = tCanvas.getContext('2d'); const blur = 0; const radius = this.config.radius; tCanvas.width = 2 * radius; tCanvas.height = 2 * radius; const grd = tCtx.createRadialGradient(radius, radius, blur, radius, radius, radius); grd.addColorStop(0, 'rgba(0,0,0,1)'); grd.addColorStop(1, 'rgba(0,0,0,0)'); tCtx.fillStyle = grd; tCtx.fillRect(0, 0, 2 * radius, 2 * radius); return tCanvas; } } const heatmap = new HeatMap({ container: document.body }); const data: PointData[] = []; for(var i = 0; i < 100; i++) { data.push({ x: Math.random() * 400, y : Math.random() * 400, value: Math.random() * 100 }) } heatmap.initData(data); 複製程式碼