扫码阅读
手机扫码阅读

Odoo|当我在Odoo用画布创建流程图

90 2024-03-14

本期摘要

Odoo是一款开源的企业管理软件,可以用于制作步骤图。通过使用Odoo的表单视图和相关的初始化方法,可以实现步骤图的制作。

除此之外,我还尝试了一种新的形式,利用Canvas技术来实现流程图的制作。本次分享是此次尝试的结果,供大家参考。

作者

沈童 | 前端开发工程师

默默无闻,走向人生巅峰,激流勇进,退居幕后黑手

快要七夕了,牛郎与织女的爱情故事,还在民间传播,口口相传,最近我遇到了一个需求,需要绘制一个特殊的步骤图,它采用上下两层分列式流转的形式。我在考虑使用哪种前端技术来实现这个功能。

首先我想到了传统的css+html+js组合,通过结构事件和组件化来完成。虽然这种方法是可行的,但我觉得有点无趣。于是我决定进行调研,看看能否在Odoo14中使用Canvas技术来实现这个功能。

01

什么是帆布

Canvas可以用于创建图表、游戏、图像编辑器、数据可视化等各种交互式和动态的网页应用程序。它具有灵活性和高性能,可以实现各种复杂的绘图操作,并且可以在不同设备和浏览器上运行。Canvas更偏向于创建图形,复杂的图形,通过代码实现而不是gif或者简单的图片切换。

Canvas具有以下的优点:

  • 灵活性:Canvas可以绘制各种形状和图像,可以通过JavaScript动态创建和修改图形,使得开发者可以自由地实现各种效果和交互。

  • 高性能:Canvas使用GPU加速绘制,可以在浏览器中实现流畅的动画效果。同时,由于Canvas只需要绘制一次,而不需要维护DOM结构,所以在处理大量图形或动画时性能更好。

  • 可扩展性:Canvas可以与其他Web技术(如CSS、JavaScript)结合使用,可以通过CSS样式控制Canvas的外观,通过JavaScript控制Canvas的行为,从而实现更复杂的功能和交互。

  • 跨平台兼容性:Canvas是HTML5的一部分,几乎所有现代浏览器都支持Canvas,包括移动设备上的浏览器。这意味着开发者可以使用Canvas创建跨平台的Web应用程序和游戏。

  • 绘制能力强大:Canvas支持绘制2D和3D图形,可以绘制直线、曲线、多边形、文本、图像等各种元素。通过使用Canvas的API,开发者可以实现各种复杂的绘图效果,如渐变、阴影、变形等。

02

实施步骤

在Odoo14中,如何在form表单中最上面插入一个Canvas的画布控件呢?

首先,我发现在Odoo中,form表单会在每次重置后只进入一次form视图的init和renderButtons等相关的初始化视图方法。但是在二次渲染视图时,会出现"不触发"和"找不到相关DOM元素"的问题。

为了解决这个问题,我们需要使用form视图的Renderer配置属性,并改写Renderer中的内容,因为Renderer是百分百会进入和触发的。


ExtendFromController = FormController.extend({ events: Object.assign({}, FormController.prototype.events, { 'click .bump-form-search': '_bump_form_search', 'click .reset-form-bump': '_reset_form_bump', 'click .add-bump-form': '_add_bump_inventory',
    }), /**
 * @override
*/ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.initialState.context
 
},
 
 
}) var bump_formView = FormView.extend({ config: _.extend({}, FormView.prototype.config, { Controller: ExtendFromController, Renderer: GroupList
    }), _extractParamsFromAction:function (){ var params = this._super.apply(this, arguments); return params
    }
    view_registry.add('borrow_bump_form_list', bump_formView); return bump_formView;
})

代码仅仅作为示范,大家可以自己尝试检验一下

改写Renderer部分的代码,记得引入相关模块

/**记得引入*/ var FormController = require('web.FormController'); var FormRenderer = require('web.FormRenderer'); var GroupList = FormRenderer.extend({ /**
    * @private
    * @param {Object} node
    * @returns {jQueryElement}
    */ _renderTagNotebook: function (node) { var self = this; var $headers = $('
'); var $pages = $('
'); // renderedTabs is used to aggregate the generated $headers and $pages // alongside their node, so that their modifiers can be registered once // all tabs have been rendered, to ensure that the first visible tab // is correctly activated var renderedTabs = _.map(node.children, function (child, index) { var pageID = _.uniqueId('notebook_page_'); var $header = self._renderTabHeader(child, pageID); var $page = self._renderTabPage(child, pageID);             self._handleAttributes($header, child);             $headers.append($header);             $pages.append($page); return { $header: $header, $page: $page, node: child,             };         }); // register the modifiers for each tab _.each(renderedTabs, function (tab) {             self._registerModifiers(tab.node, self.state, tab.$header, { callback: function (element, modifiers) { // if the active tab is invisible, activate the first visible tab instead var $link = element.$el.find('.nav-link'); if (modifiers.invisible && $link.hasClass('active')) {                         $link.removeClass('active');                         tab.$page.removeClass('active');                         self.inactiveNotebooks.push(renderedTabs);                     } if (!modifiers.invisible) { // make first page active if there is only one page to display var $visibleTabs = $headers.find('li:not(.o_invisible_modifier)'); if ($visibleTabs.length === 1) {                             self.inactiveNotebooks.push(renderedTabs);                         }                     }                 },             });         }); this._activateFirstVisibleTab(renderedTabs); var $notebookHeaders = $('
').append($headers); var $notebook = $('
').append($notebookHeaders, $pages);         $notebook[0].dataset.name = node.attrs.name || '_default_'; this._registerModifiers(node, this.state, $notebook); this._handleAttributes($notebook, node);         bump_form_process_status = this.state.data.process_status;         states = this.state.data         apply_id = this.state.data.apply_id.res_id /**在这里找相关的DOM和触发相关是业务操作,记得代码方法写在*/ $(document).ready(() => {             _this.$buttons.find('.my_bump_buttons').hide() this.beforeCanvas();//在这里调用创建Canvas的视图 }); return $notebook;     }, /**代码方法写在这里*/ var nodes = [             { x: 100, y: 100, text: '开始' },             { x: 300, y: 100, text: '步骤1' },             { x: 300, y: 200, text: '步骤2' },             { x: 100, y: 200, text: '步骤3' },             { x: 200, y: 300, text: '结束' }         ];     beforeCanvas:function (){ let canvasWidth = parseInt($('.o_form_sheet').innerWidth())-400 let canvasBox = `
${canvasWidth}" height="400">
` $('.o_form_sheet').before(canvasBox) this.canvasInit();         }, canvasInit:function (){ var canvas = this.$('#flowchartCanvas')[0]; var context = canvas.getContext('2d'); this.drawFlowchart();         }, // 绘制节点 drawNode:function(x, y, text) { var canvas = this.$('#flowchartCanvas')[0]; var context = canvas.getContext('2d');             context.beginPath();             context.arc(x, y, 30, 0, 2 * Math.PI);             context.stroke();               context.font = '14px Arial';             context.textAlign = 'center';             context.fillText(text, x, y + 5);         }, // 绘制连线 drawConnections:function() { var canvas = this.$('#flowchartCanvas')[0]; var context = canvas.getContext('2d');             context.beginPath();             context.moveTo(nodesCanvas[0].x, nodesCanvas[0].y); for (var i = 1; i < nodesCanvas.length; i++) { context.lineTo(nodesCanvas[i].x, nodesCanvas[i].y); } context.lineWidth = 5;             context.strokeStyle = 'blue' context.lineCap = 'round';               context.stroke();         }, // 绘制节点和连线 drawFlowchart:function() { let self =this; var canvas = this.$('#flowchartCanvas')[0]; var context = canvas.getContext('2d'); // 定义流程图的节点 // 清空画布 context.clearRect(0, 0, canvas.width, canvas.height); // 绘制节点 nodesCanvas.forEach(function (node) {                self.drawNode(node.x, node.y, node.text);             }); // 绘制连线 self.drawConnections();         }, // 在节点上添加点击事件,点击时移动节点位置这个功能是尝试给相关节点添加事件,就和流程图一样。记得和上面一样封装在function中 canvas.addEventListener('mousedown', function (e) { var mouseX = e.clientX - canvas.offsetLeft; var mouseY = e.clientY - canvas.offsetTop; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var distance = Math.sqrt(Math.pow(mouseX - node.x, 2) + Math.pow(mouseY - node.y, 2)); if (distance <= 30) { var offsetX = mouseX - node.x; var offsetY = mouseY - node.y;                       canvas.addEventListener('mousemove', moveNode);                     canvas.addEventListener('mouseup', stopMovingNode);                     canvas.addEventListener('mouseout', stopMovingNode); function moveNode(e) {                         node.x = e.clientX - canvas.offsetLeft - offsetX;                         node.y = e.clientY - canvas.offsetTop - offsetY;                         drawFlowchart();                     } function stopMovingNode() {                         canvas.removeEventListener('mousemove', moveNode);                         canvas.removeEventListener('mouseup', stopMovingNode);                         canvas.removeEventListener('mouseout', stopMovingNode);                     } break;                 }             }         });           })

会得到一个类似这样的canvas视图。这里我只是举个例子,关于如何绘制canvas,大家需要自己多下功夫,查看API和案例。

添加了事件后,是可以拖拽任意节点变形的。

你也可以使用别人封装好的JS代码。记得将它命名封装到一个单独的.js文件中,然后在你的文件中提前引入。

代码如下

/**
 *流向图组件,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;i2;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;i2;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;iMath.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;iMath.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;iMath.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;iMath.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;iMath.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;iMath.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;iMath.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;iMath.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);
};;i++){>;i++){>;i++){>;i++){>;i++){>;i++){>;i++){>;i++){>

然后可以在你的form的js中调用


//调用的时候可以直接放到上面的beforeCanvas这个方法中,如果找不到对应的dom记得修改上面封装的js中的dom获取方式,断点查看 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 }
        ]
    });
 
 
感谢博主「Programmer boy」的原创文章
原文链接:https://blog.csdn.net/m0_37631322/article/details/91128968

完成后可以得到一个的步骤图

剩下如何修改样式,代码事件等相关操作需要自己行根据自身的需求进行修改。

03

附赠:贪吃蛇小游戏

这里给大家附赠一个用Canvas制作的贪吃蛇小游戏,大家可以自己去尝试一下。

代码如下

 <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>canvastitle> head> <style> .container{ width:800px; height:800px; margin:50px auto; /* border:1px solid #ccc; */ } .statistics{ height:100px; display: flex; background:green; font-size:32px; text-align: center; line-height: 100px; color: #fff;
        } .statistics div{ width:50%;
        } canvas{ background: #000; overflow: hidden; vertical-align:middle
        } .controller{ display: flex; height:100px; font-size:32px; text-align: center; line-height: 100px; color:#fff;
        } .controller div{ width:100%; cursor: pointer;
        } .controller div:active{ background:green!important;
        } .controller div:nth-child(1){ background: #8fd229;
        } .controller div:nth-child(2){ background: #9bd83c;
        } .controller div:nth-child(3){ background: #96ce42;
        } style> <body> <div class="container"> <div class="statistics"> <div id="score">得分:0分div> <div id="timer">用时:0秒div> div> <canvas id="game" width="800" height="600">canvas> <div class="controller"> <div onclick="start()">开始div> <div id="stop" onclick="stop()">暂停div> <div onclick="reset()">重置div> div> div> body> <script> var canvas = document.getElementById('game'); var game = { length:3, positions:[[360,500], [380,500],[400,500]], direction:'right', foodPositionInSnack:null, foodPosition:null, score:0 }; var timer; var timeSpend = 0; var startTime = 0; var operateTime = 0; var speedFactor = 300; var willGrowUp = false; var gameStatus = true; var operateTimer; var operateQueue = []; function start(){ console.log('game_ start__'); if( !game.content ){
                refreshTime();
                gameInit();
            } 
        } function stop(){
            gameStatus = ! gameStatus; if( gameStatus ){ document.getElementById('stop').innerText = '暂停' refreshTime(); console.log('game_ restart');
            }else{ document.getElementById('stop').innerText = '继续' clearInterval(timer); console.log('game_ stop');
            }
             
        } function reset(){ console.log('game_ reset'); if( !!game.content ){
                timeSpend = 0;
                game = { length:3, positions:[[360,500], [380,500],[400,500]], direction:'right', foodPositionInSnack:null, foodPosition:null, score:0 };
                gameStatus = true; document.getElementById('stop').innerText = '暂停' gameInit();
            }
        } function gameInit(){ var ctx = canvas.getContext('2d');
            game.content = ctx;
 
            generateFood();
 
            renderGame();
            operateEventloop();
        } function renderGame(timestamp){ var currentTime = performance.now(); if( (currentTime - startTime) < speedFactor || !gameStatus ){ requestAnimationFrame(renderGame); return false;
            }
            operateEventloop();
            startTime = currentTime;
 
            game.content.clearRect(0,0,canvas.width,canvas.height); var x = 0,y = 0; switch (game.direction){ case 'left':
                    x -=20; break; case 'right':
                    x +=20; break; case 'top':
                    y -= 20; break; case 'bottom':
                    y += 20; break;
            } //判断吃掉食物 const newPosition = [game.positions[game.positions.length - 1][0] +x, game.positions[game.positions.length - 1][1] +y]; //如果头部位置和食物位置重合,则吃掉,分数+1, 并重新生成食物 if( newPosition[0] == game.foodPosition[0] && newPosition[1] == game.foodPosition[1]){
                game.foodPositionInSnack = [...game.foodPosition];
                generateFood();
                game.score ++; document.getElementById('score').innerText = `得分:${game.score}分`; var accCoeff = Math.floor(game.score / 10);
                speedFactor = 300 - accCoeff * 40 > 50 ? 300 - accCoeff * 40 : 50;
            } //判断游戏失败 var isFailture = false; //1.出界 if(newPosition[0] < 0 || newPosition[0] >= 800 || newPosition[1] <0 || newPosition[1] >=600 ){
                isFailture = true;
            } //2.吃到自己 game.positions.forEach(position => { if(position[0] == newPosition[0] && position[1] == newPosition[1]){
                    isFailture = true;
                }
            }) if(isFailture){
                alert(`Game Over \n 最终得分${game.score}`); return false;
            }
 
            game.positions.push(newPosition); // 如果食物到达尾部,则变长一格 if( !willGrowUp ){
                game.positions.shift();
            }else{
                game.foodPositionInSnack = null;
            }
            willGrowUp = false; if( game.foodPositionInSnack ){ if( game.positions[0][0] == game.foodPositionInSnack[0] && game.positions[0][1] == game.foodPositionInSnack[1] ){
                    willGrowUp = true;
                }
            }
             
            drawSnack();
            drawFood();
 
            requestAnimationFrame(renderGame);
        } function generateFood(){ var randomPos = randomPosition(); var hasRepeat = false;
            game.positions.forEach(position => { if( randomPos[0] == position[0] && randomPos[1] == [1]){
                    hasRepeat = true;
                }
            }) if( hasRepeat ){
                generateFood(); return;
            }
 
            game.foodPosition = randomPos;
        } function randomPosition(){ return [Math.floor(Math.random() * 40) * 20, Math.floor(Math.random() * 30) *20];
        } function drawSnack(){ var ctx = game.content;
            ctx.beginPath();
            ctx.fillStyle = '#ccc';
            game.positions.forEach(position => {
                ctx.fillRect(position[0] + 2.5, position[1] + 2.5, 15,15);
            })
        } function drawFood(){ var ctx = game.content;
            ctx.beginPath();
            ctx.fillStyle = '#ccc';
            ctx.arc(game.foodPosition[0] + 10, game.foodPosition[1] + 10, 8, 0, Math.PI * 2, true);
            ctx.fill();
        } function refreshTime(){
            timer = setInterval(() => {
                timeSpend ++ ; document.getElementById('timer').innerText = `用时:${timeSpend}秒`;
            }, 1000);
        } function operateEventloop(){ if( operateQueue.length !=0 ){
                    triggerOperate(operateQueue[0]);
                    operateQueue.shift();
                }
        } function triggerOperate(e){ switch (e.keyCode){ case 87: if( game.direction != 'bottom' ){
                        game.direction = 'top';
                    } break; case 83: if( game.direction != 'top' ){
                        game.direction = 'bottom';
                    } break; case 65: if( game.direction != 'right' ){
                        game.direction = 'left';
                    } break; case 68: if( game.direction != 'left' ){
                        game.direction = 'right';
                    } break;
            }
        } document.addEventListener('keyup', function(e){
            operateQueue.push(e);
        }) script> html>

本次分享就结束啦,感兴趣的朋友可以自己动手尝试一下。

原文链接: http://mp.weixin.qq.com/s?__biz=Mzg5MzUyOTgwMQ==&mid=2247524302&idx=1&sn=cb8493f5199c57fb906445d2ec1b8465&chksm=c02fa468f7582d7e99a488986072a9e5dbfc1d4ea379a3dc91e8abaa7d57fb48ffb14a50a833#rd