基於canvas實現一個多分支流程圖組件

綜述:使用canvas實現了一個多分支流向圖,總結下主要的實現思路,有需要的朋友可直接使用,假設如果不使用canvas來繪製,使用svg來繪製該怎樣來實現?

1.效果展示

2.實現思路解析

  • 需求分析

一個流程圖,主要由橫線,空心圓,空心圓中的實心圓三部分組成,並且隨着狀態的變化能控制其顏色

可以支持多分支,多分支中還包括奇數個分支和偶數個分支,實現思路上兩者稍微不同

  • 核心思路說明

通過配置的數組解析繪製,如果是非數組元素,則就是畫直線和圓圈,實心圓,並分別計算各自的位置(x,y)座標,依次類推。

如果是一個數組的元素,則說明是一個多分支的元素,需要向上和向下開分支,繪製多條直線和圓圈,實心圓,依次類推即可,並分別計算各自的位置(x,y)座標,依次類推。

3.遇到的問題總結

  • canvas是基於矢量繪製的圖像,存在模糊的問題(參考連接),細節見js代碼種的最後一個模塊
  • 對於繪製直線的顏色控制邏輯,沒有理清楚,廢了些時間

4.代碼展示

  • javascript代碼(組件實現代碼),render方法過於冗餘,但是好處是比較好理解,後期應該優化下
/**
 *流向圖組件,mouyao
 */
var opsDirectionMap = function(option){
    this.const(option);
    this.init();
};
/*
*配置項引入
*/
opsDirectionMap.prototype.const=function(option){
    this.r=option.r||4;//節點半徑
    this.config=option;
    this.data = option.data||[];
    this.mLeft = option.mLeft||-20;//起點距左邊距離
    this.space = option.space||18*this.r;//節點之間距離
    this.angle =2*this.r;//分支上下之間的高度
    this.nodeArr = []; //存儲所有的圓點的信息和座標
};
/*
*配置項引入
*/
opsDirectionMap.prototype.init =function(){
    var myCanvas=document.getElementById(this.config.placeId);
    this.resolveVagueProblem(myCanvas);
    this.render(myCanvas);
};
/*
*判定是否數組
*/
opsDirectionMap.prototype.isArrayFn =function(o) {
    return Object.prototype.toString.call(o) === '[object Array]';
};
/*
*根據當前節點的執行狀態,渲染圓點前的線條的顏色
*/
opsDirectionMap.prototype.drawDashLine =function(ctx, x1, y1, x2, y2,data,index){
    ctx.lineWidth=1;
    ctx.beginPath();
    var x=(x2-x1)/2;
    if(index>0&&!this.isArrayFn(this.data[index-1])){
        ctx.moveTo(x1,y1);
        ctx.lineTo(x1+x ,y1);
        ctx.moveTo(x1+x,y1);
        ctx.lineTo(x1+x ,y2);
        ctx.moveTo(x1+x,y2);
        ctx.lineTo(x2 ,y2);
    }else if(index>0&&this.isArrayFn(this.data[index-1])){
        ctx.moveTo(x1,y1);
        ctx.lineTo(x1+x ,y1);
        ctx.moveTo(x1+x,y1);
        ctx.lineTo(x1+x ,y2);
        ctx.moveTo(x1+x,y2);
        ctx.lineTo(x2 ,y2);
    }else{
        if(index!==0){ //刪除第一個圓點的連接線
            ctx.moveTo(x1,y1);
            ctx.lineTo(x2 ,y2);
        }
    }
    if(data.isExcuted===true){
        ctx.strokeStyle="#009aff";
    }else if(data.isExcuted===false&&index!==0&&this.data[index-1].isExcuted===true&&!this.isArrayFn(this.data[index-1])){
        ctx.strokeStyle="#009aff";
    }else if(data.isExcuted===false&&this.isArrayFn(this.data[index-1])){
        //如果上一個元素是數組
        var arr=[];
        this.data[index-1].some(function(item){
            if(item.isExcuted===true){
                arr.push(true);
            }
        });
        if(arr.length===(this.data[index-1]).length){
            ctx.strokeStyle="#009aff";
        }else{
            ctx.strokeStyle="#959595";
        }
    }else{
        ctx.strokeStyle="#959595";
    }
    ctx.stroke();
};
/*
*繪製線條,圓點,圓心,和說明文字
*/
opsDirectionMap.prototype.render =function(canvas){
        var this_ = this;
        this_.canvas = canvas;
        var ctx = canvas.getContext("2d");//上下文
        this_.ctx = ctx;
        var x = this_.mLeft; //每個操作的對象的座標
        //var y = canvas.height/2;
        //x偏移量:this_.r*Math.sin((90-itemY)*Math.PI/180)
        //y偏移量:this_.r*Math.cos((90-itemY)*Math.PI/180)
        var y =50;
        this_.data.forEach(function(item, index){
            if(!(item instanceof Array)){
                x = index == 0?x:(x + this_.space);
                if((index-1)>=0 && this_.data[index-1] instanceof Array){
                    var arr = this_.data[index-1];
                  
                    if(arr.length % 2==0){
                        var itemY = this_.angle;
                        for(var i=0;i<arr.length/2;i++){
                            this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y-Math.tan(itemY*Math.PI/180)*this_.space+this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
                            itemY = itemY + this_.angle;
                        }
                        var itemY = this_.angle;
                        for(var i=0;i<arr.length/2;i++){
                            this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y+Math.tan(itemY*Math.PI/180)*this_.space-this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
                            itemY = itemY + this_.angle;
                        }
                    }else{
                        var itemY = 0;
                        for(var i=0;i<parseInt(arr.length/2)+1;i++){
                            console.log(arr[i]);
                            this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y-Math.tan(itemY*Math.PI/180)*this_.space+this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
                            itemY = itemY + this_.angle;
                        }
                        var itemY = this_.angle;
                        for(var i=0;i<parseInt(arr.length/2);i++){
                            this_.drawDashLine(ctx, x - this_.space - this_.r+this_.r*Math.sin((90-itemY)*Math.PI/180), y+Math.tan(itemY*Math.PI/180)*this_.space-this_.r*Math.cos((90-itemY)*Math.PI/180), x, y,item,index);
                            itemY = itemY + this_.angle;
                        }
                    }
                }

                if(index == 0){
                    ctx.moveTo(x,y);
                    x = x + this_.space;
                }
                //繪製非數組直線
                if(!((index-1)>=0 && this_.data[index-1] instanceof Array)){
                    this_.drawDashLine(ctx,x-this_.space, y, x, y,item,index);
                }
                ctx.moveTo(x + 2*this_.r,y);

                //繪製節點,畫圓
                ctx.arc(x + this_.r,y,this_.r,0,2*Math.PI);
                this_.nodeArr.push({x:x + this_.r,y:y,data:item});
                ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                ctx.fill();

                //節點標題note
                ctx.textAlign ="center";
                ctx.textBaseline = "middle";
                ctx.font = "bold 10px 宋體";//字體大小
                ctx.fillStyle =item.noteColor;//字體顏色
                //節點的名稱設置
                ctx.fillText(item.noteName,x + this_.r,y-this_.r-10);
                ctx.fillStyle = "black";//字體顏色
              
                x = x + 2*this_.r;
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(x,y);
            }else{//數組
                if(!(this_.data[index-1] instanceof Array)){//上一級不是數組
                    var itemY = 0;
                    if(item.length%2==0){//偶數
                        itemY = this_.angle;
                        var dataArr = item.slice(0,item.length/2).reverse();
                        for(var i=0;i<dataArr.length;i++){
                            this_.drawDashLine(ctx, x, y, x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            ctx.beginPath();
                            ctx.arc(x + this_.space, y-Math.tan(itemY*Math.PI/180)*(this_.space),this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x + this_.space,y:y-Math.tan(itemY*Math.PI/180)*(this_.space),data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle =dataArr[i].isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x + this_.space, y-Math.tan(itemY*Math.PI/180)*(this_.space)-this_.r-10);
                            ctx.fill();
                            ctx.moveTo(x+this_.r,y);
                            itemY = itemY + this_.angle;
                        }
                        itemY = this_.angle;
                        var dataArr = item.slice(item.length/2,item.length);
                        for(var i=0;i<dataArr.length;i++){
                            this_.drawDashLine(ctx, x, y, x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            ctx.beginPath();
                            ctx.arc(x + this_.space, y+Math.tan(itemY*Math.PI/180)*(this_.space),this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x + this_.space,y:y+Math.tan(itemY*Math.PI/180)*(this_.space),data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle =dataArr[i].isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x + this_.space, y+Math.tan(itemY*Math.PI/180)*(this_.space)+this_.r+10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                    }else{//奇數
                        var dataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
                        for(var i=0;i<dataArr.length;i++){
                            this_.drawDashLine(ctx, x, y, x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);

                            ctx.beginPath();
                            ctx.arc(x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x + this_.space,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;;//填充顏色
                            ctx.fill();
                            ctx.stroke();
                            
                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x + this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                        itemY = this_.angle;
                        var dataArr = item.slice(parseInt(item.length/2)+1,item.length);
                        for(var i=0;i<dataArr.length;i++){
                            this_.drawDashLine(ctx, x, y, x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            ctx.beginPath();
                            ctx.arc(x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x + this_.space,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x + this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                    }
                    ctx.stroke();

                    ctx.beginPath();
                    x = x+this_.space+this_.r;
                    ctx.moveTo(x,y);
                }else{//上一級是數組
                    if(item.length%2==0){//偶數
                        var itemY = this_.angle;
                        var dataArr = item.slice(0,item.length/2).reverse();
                        for(var i=0;i<dataArr.length;i++){
                            ctx.moveTo(x,y-Math.tan(itemY*Math.PI/180)*this_.space);
                            this_.drawDashLine(ctx, x, y-Math.tan(itemY*Math.PI/180)*this_.space,
                                x+this_.r+ this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            ctx.beginPath();
                            ctx.arc(x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x+ this_.space+this_.r,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                        var itemY = this_.angle;
                        var dataArr = item.slice(item.length/2,item.length);
                        for(var i=0;i<dataArr.length;i++){
                            ctx.moveTo(x,y+Math.tan(itemY*Math.PI/180)*this_.space);
                            this_.drawDashLine(ctx, x, y+Math.tan(itemY*Math.PI/180)*this_.space,
                                x+this_.r+ this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            ctx.beginPath();
                            ctx.arc(x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x+ this_.space+this_.r,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                    }else{//奇數
                        var itemY = 0;
                        var dataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
                        for(var i=0;i<dataArr.length;i++){
                            ctx.moveTo(x,y-Math.tan(itemY*Math.PI/180)*this_.space);
                            this_.drawDashLine(ctx, x, y-Math.tan(itemY*Math.PI/180)*this_.space,
                                x+this_.r+ this_.space, y-Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);

                            ctx.beginPath();
                            ctx.arc(x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x+ this_.space+this_.r,y:y-Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});

                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y-Math.tan(itemY*Math.PI/180)*this_.space-this_.r-10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                        var itemY = this_.angle;
                        var dataArr = item.slice(parseInt(item.length/2)+1,item.length);
                        for(var i=0;i<dataArr.length;i++){
                            ctx.moveTo(x,y+Math.tan(itemY*Math.PI/180)*this_.space);
                            this_.drawDashLine(ctx, x, y+Math.tan(itemY*Math.PI/180)*this_.space,
                                x+this_.r+ this_.space, y+Math.tan(itemY*Math.PI/180)*this_.space,dataArr[i],index);
                            
                            ctx.beginPath();
                            ctx.arc(x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space,this_.r,0,2*Math.PI);
                            this_.nodeArr.push({x:x+ this_.space+this_.r,y:y+Math.tan(itemY*Math.PI/180)*this_.space,data:dataArr[i]});
                            //節點信息
                            ctx.textAlign ="center";
                            ctx.textBaseline = "middle";
                            ctx.font = "bold 10px 宋體";//字體大小
                            ctx.fillStyle = item.isExcuted===true?this_.config.excutedCirclePointColor:this_.config.circlePointColor;//填充顏色
                            ctx.fill();
                            ctx.stroke();

                            ctx.beginPath();
                            ctx.fillStyle = dataArr[i].noteColor;//字體顏色
                            ctx.fillText(dataArr[i].noteName,x+ this_.space+this_.r, y+Math.tan(itemY*Math.PI/180)*this_.space+this_.r+10);
                            ctx.fill();
                            itemY = itemY + this_.angle;
                        }
                    }
                    ctx.stroke();
                    ctx.beginPath();
                    x = x+this_.space+2*this_.r;
                    ctx.moveTo(x,y);
                }
            }
        });
};
/*
*因爲canvas繪製的是矢量圖,會出現模糊問題,使用下邊的方法解決
* 參考鏈接:https://zhuanlan.zhihu.com/p/31426945
*/
opsDirectionMap.prototype.resolveVagueProblem=function(myCanvas) {
    var getPixelRatio = function (context) {
        var backingStore = context.backingStorePixelRatio ||
            context.webkitBackingStorePixelRatio ||
            context.mozBackingStorePixelRatio ||
            context.msBackingStorePixelRatio ||
            context.oBackingStorePixelRatio ||
            context.backingStorePixelRatio || 1;
        return (window.devicePixelRatio || 1) / backingStore;
    };
    //畫文字
    myCanvas.style.border = "1px solid silver";
    var context = myCanvas.getContext("2d");
    var ratio = getPixelRatio(context);
    myCanvas.style.width = myCanvas.width + 'px';
    myCanvas.style.height =myCanvas.height+ 'px';
    myCanvas.width = myCanvas.width * ratio;
    myCanvas.height = myCanvas.height * ratio;
    context.scale(ratio,ratio); 
};
  • html調用代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
</head>
<body>
<canvas id="renderArea" width="600" height="100">瀏覽器不支持canvas</canvas>
<script type="text/javascript" src="ops-direction-map.js"></script>
<script>
     var demo=new opsDirectionMap({
        placeId:"renderArea",
        excutedCirclePointColor:"#009aff",//執行的節點的圓心顏色
        circlePointColor:"#ffffff",//未執行的的節點的圓心顏色
        data:[{
            noteName:'節點1',
            noteColor:'#000000', //說明文字的顏色
            isExcuted:true//如果這裏爲true,則其前邊的線條爲藍色,圓點爲實現,否在爲白色
        },{
            noteName:'節點2',
            noteColor:'#000000',
            isExcuted:true
        },[
            {
                noteName:'節點3-1',
                noteColor:'#000000',
                isExcuted:true
            },
            {
                noteName:'節點3-2',
                noteColor:'#000000',
                isExcuted:false
            }
        ],{
            noteName:'節點4',
            noteColor:'#000000',
            isExcuted:false
        },{
            noteName:'節點5',
            noteColor:'#000000',
            isExcuted:false
        },[
            {
                noteName:'節點6-1',
                noteColor:'#000000',
                isExcuted:false
            },{
                noteName:'節點6-2',
                noteColor:'#000000',
                isExcuted:false,
            }
        ],{
            noteName:'節點7',
            noteColor:'#000000',
            isExcuted:false
        }
        ]
    });
</script>
</body>
</html>

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章