作者:沈童 | 前端开发工程师
快要七夕了,牛郎与织女的爱情故事,还在民间传播,口口相传,最近我遇到了一个需求,需要绘制一个特殊的步骤图,它采用上下两层分列式流转的形式。我在考虑使用哪种前端技术来实现这个功能。
首先我想到了传统的css+html+js组合,通过结构事件和组件化来完成。虽然这种方法是可行的,但我觉得有点无趣。于是我决定进行调研,看看能否在Odoo14中使用Canvas技术来实现这个功能。
一、什么是帆布
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,开发者可以实现各种复杂的绘图效果,如渐变、阴影、变形等。
二、实施步骤
在Odoo14中,如何在form表单中最上面插入一个Canvas的画布控件呢?
首先,我发现在Odoo中,form表单会在每次重置后只进入一次form视图的init和renderButtons等相关的初始化视图方法。但是在二次渲染视图时,会出现"不触发"和"找不到相关DOM元素"的问题。
为了解决这个问题,我们需要使用form视图的Renderer配置属性,并改写Renderer中的内容,因为Renderer是百分百会进入和触发的。
javascript">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部分的代码,记得引入相关模块
javascript">/**记得引入*/
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 = $('<ul class="nav nav-tabs">');
var $pages = $('<div class="tab-content">');
// 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 = $('<div class="o_notebook_headers">').append($headers);
var $notebook = $('<div class="o_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 = `<div class="step-line step-box"><canvas id="flowchartCanvas" width="${canvasWidth}" height="400"></canvas></div>`
$('.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文件中,然后在你的文件中提前引入。
代码如下
javascript">/**
*流向图组件,mouyao
*/
varopsDirectionMap = 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(){
varmyCanvas=document.getElementById(this.config.placeId);
this.resolveVagueProblem(myCanvas);
this.render(myCanvas);
};
/*
*判定是否数组
*/
opsDirectionMap.prototype.isArrayFn =function(o) {
returnObject.prototype.toString.call(o) === '[object Array]';
};
/*
*根据当前节点的执行状态,渲染圆点前的线条的颜色
*/
opsDirectionMap.prototype.drawDashLine =function(ctx, x1, y1, x2, y2,data,index){
ctx.lineWidth=1;
ctx.beginPath();
varx=(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);
}elseif(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";
}elseif(data.isExcuted===false&&index!==0&&this.data[index-1].isExcuted===true&&!this.isArrayFn(this.data[index-1])){
ctx.strokeStyle="#009aff";
}elseif(data.isExcuted===false&&this.isArrayFn(this.data[index-1])){
//如果上一个元素是数组
vararr=[];
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){
varthis_ = this;
this_.canvas = canvas;
varctx = canvas.getContext("2d");//上下文
this_.ctx = ctx;
varx = 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)
vary =50;
this_.data.forEach(function(item, index){
if(!(item instanceofArray)){
x = index == 0?x:(x + this_.space);
if((index-1)>=0&& this_.data[index-1] instanceofArray){
vararr = this_.data[index-1];
if(arr.length % 2==0){
varitemY = this_.angle;
for(vari=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;
}
varitemY = this_.angle;
for(vari=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{
varitemY = 0;
for(vari=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;
}
varitemY = this_.angle;
for(vari=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] instanceofArray)){
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] instanceofArray)){//上一级不是数组
varitemY = 0;
if(item.length%2==0){//偶数
itemY = this_.angle;
vardataArr = item.slice(0,item.length/2).reverse();
for(vari=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;
vardataArr = item.slice(item.length/2,item.length);
for(vari=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{//奇数
vardataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
for(vari=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;
vardataArr = item.slice(parseInt(item.length/2)+1,item.length);
for(vari=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){//偶数
varitemY = this_.angle;
vardataArr = item.slice(0,item.length/2).reverse();
for(vari=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;
}
varitemY = this_.angle;
vardataArr = item.slice(item.length/2,item.length);
for(vari=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{//奇数
varitemY = 0;
vardataArr = item.slice(0,parseInt(item.length/2)+1).reverse();
for(vari=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;
}
varitemY = this_.angle;
vardataArr = item.slice(parseInt(item.length/2)+1,item.length);
for(vari=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) {
vargetPixelRatio = function(context) {
varbackingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return(window.devicePixelRatio || 1) / backingStore;
};
//画文字
myCanvas.style.border = "1px solid silver";
varcontext = myCanvas.getContext("2d");
varratio = 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);
};
然后可以在你的form的js中调用
javascript">//调用的时候可以直接放到上面的beforeCanvas这个方法中,如果找不到对应的dom记得修改上面封装的js中的dom获取方式,断点查看
vardemo=newopsDirectionMap({
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
完成后可以得到一个的步骤图
剩下如何修改样式,代码事件等相关操作需要自己行根据自身的需求进行修改。
三、附赠:贪吃蛇小游戏
这里给大家附赠一个用Canvas制作的贪吃蛇小游戏,大家可以自己去尝试一下。
代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas</title>
</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>
本次分享就结束啦,感兴趣的朋友可以自己动手尝试一下。
本次分享就到这里啦,更多odoo小知识欢迎关注“神州数码云基地”公众号,回复“odoo”进入社群交流
版权声明:文章由神州数码武汉云基地团队实践整理输出,转载请注明出处。