canvas渲染熱力圖的一種方式

說明

今天早上看了下heatMap.js的源碼,瞭解了他是如何繪製熱力圖的,這裏咱們拋開其數據處理的部分,聚焦熱力圖的繪製。git

若是要繪製一個點的熱力圖,能夠簡單是的使用createRadialGradient來實現,可是若是兩個點的熱力圖發生了重疊,重疊部分固然不是簡單的覆蓋。這種狀況下咱們固然可使用像素級的操做,結合兩個點的熱力圖經過複雜的計算獲得覆蓋以後的熱力圖,但顯然過於複雜。程序員

咱們仔細觀察下熱力圖,他其實就是一些顏色的漸變產生的效果,中間部分顏色深一點,外圍淺一點,咱們實際上就是根據權重的大小來着色。好比咱們在[80, 80]的地方有一個點,像半徑10的周圍輻射,咱們把重心的權重設爲100,最外圍設爲10,咱們很容易想到,使用一個單色繪製。最方便就是使用灰色,只須要使用透明度就可,其像素點的rgb值都是0,這樣的數據就方便處理,以下圖。 github

grey

因此步驟就是先使用這種灰度先繪製到一個canvas上,其每個點的rgba都是(0, 0, 0, 0)(0, 0, 0, 255)之間。如今就能夠根據其alpha值將其着色。如今有一個漸變色卡以下,其對應關係就是alpha的值爲0,對應色卡的左邊,255對應右邊。 typescript

色卡

一種簡單的方式就是使用漸變色繪製一個寬爲256的canvas,取得這256個點的顏色,而後與canvas進行一一對應。好比,咱們的主canvas中某個像素點的alpha值爲100,那麼就將該店的顏色修改成色卡中第100(程序員計數)個點的顏色。canvas

具體實現過程以下:

  1. 一個離屏canvas,繪製一個黑色(rgb都是0,方便處理)的alpha通道的圓
  2. 將一個個點經過這個離屏canvas繪製到主canvas上
  3. 繪製一個寬256高1的離屏canvas,講漸變色繪製到這個canvas上面,獲得取色卡
  4. 經過getImageData方法獲取畫布數據,並經過數據中的alpha存在值的數據獲取取色卡中的對應的rgb,填入相應的rgb
  5. 最後將畫布數據填充到主畫布上;

注:1. 每個點根據值得大小設置顏色深度能夠根據值得大小修改相應的globalAlpha。 2. 灰度canvas的繪製也不必定必須的繪製到主canvas,也可使用離屏canvas,最後一步在將結果繪製到主canvas(heatMap.js就是如此)。 3. 灰度數據可使用Uint8ClampedArray來運算,不必定非得畫出灰色的canvas來獲取數據,計算並不複雜。app

思路就是如此,下面就是一個簡單的實現方式。 dom

heatmap

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);
複製代碼
相關文章
相關標籤/搜索