【js】 canvas绘图和拖拽效果实现
Canvas 是 html5 中的一个非常重要的元素,和以往的 html 标签不同的是,我们虽然可以通过 Canvas 的 API 进行图像的绘制,但是,却不能够为绘制出的东西添加事件。也就是说,在 Canvas 画布中所有的移动,都是通过清空画布重新绘制的,并不是像 DOM 元素一样,可以通过设置某一个具体对象的left、top值让其移动,而只能通过每次清空画布,再次绘制。
那么,如果我们确实有需要说让 Canvas 绘制的东西动起来,又该怎么做呢?当然,有很多框架已经可以让你像以往的 DOM 一样的方式操作 Canvas 中的对象,并对其设置一些事件。现在,我们先暂时不用框架,讨论一下该如何实现对绘图对象的操作。
实现鼠标拖拽的基本思路是:给Canvas元素绑定鼠标点击事件 onmousedown,当事件发生时,检查鼠标的位置。接着,判断鼠标的位置是否处于待检测区域。若是,就可以视为点击了该检测区域,同时,注册鼠标移动事件 onmousemove,以重绘不变的背景和更新位置后的物体。另外,注册鼠标松开事件 onmouseup,确保鼠标松开后不再执行触发鼠标移动事件。
1. 绘制背景
因为背景是不变的,并且每次都需要重绘,因此,需要将背景绘图部分总结在一起。
1.1 绘制圆角矩形
通过 Canvas API 可以实现矩形,线段,圆等形状的绘制。但是并没有提供绘制圆角矩形的方法,这里,参考别人的实现,给出了下述实现。
//绘制圆角矩形
function drawRoundRect(cxt, x, y, width, height, radius){
ctx.strokeStyle = "#262626";//线条颜色
//开始绘制(从左上角开始顺时针绘制)
cxt.beginPath();
//左上角
cxt.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2);
cxt.lineTo(x + width - radius, y);
//右上角
cxt.arc(x + width - radius, y + radius, radius, Math.PI * 3 / 2, Math.PI * 2);
cxt.lineTo(x + width, y + height - radius);
//右下角
cxt.arc(x + width - radius, y + height - radius, radius, 0, Math.PI * 1 / 2);
cxt.lineTo(x + radius, y + height);
//左下角
cxt.arc(x + radius, y + height - radius, radius, Math.PI * 1 / 2, Math.PI);
cxt.lineTo(x, y + radius);
cxt.closePath();
//绘制路径
ctx.stroke();
}
这里,需要注意的是,一般在网页中,坐标原点为左上角。绘制圆的方向是顺时针方向。
1.2 绘制带有箭头的直线
带箭头的直线的绘制需要经过数学集合的计算,具体的原理可以移步至:https://www.w3cplus.com/canvas/drawing-arrow.html。
//绘制箭头
function drawArrow(ctx, fromX, fromY, toX, toY,theta,headlen,width,color) {
theta = typeof(theta) != 'undefined' ? theta : 30;
headlen = typeof(theta) != 'undefined' ? headlen : 10;
width = typeof(width) != 'undefined' ? width : 1;
color = typeof(color) != 'color' ? color : '#000'; // 计算各角度和对应的P2,P3坐标
var angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI, angle1 = (angle + theta) * Math.PI / 180, angle2 = (angle - theta) * Math.PI / 180, topX = headlen * Math.cos(angle1), topY = headlen * Math.sin(angle1), botX = headlen * Math.cos(angle2), botY = headlen * Math.sin(angle2);
ctx.save();
ctx.beginPath();
var arrowX = fromX - topX, arrowY = fromY - topY;
ctx.moveTo(arrowX, arrowY);
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
arrowX = toX + topX;
arrowY = toY + topY;
ctx.moveTo(arrowX, arrowY);
ctx.lineTo(toX, toY); arrowX = toX + botX;
arrowY = toY + botY;
ctx.lineTo(arrowX, arrowY);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
ctx.restore();
}
1.3 注意事项
Canvas 绘制的对象在发生冲突时会有不同的效果,有时部分图形会变得不可见。因此,需要尽量避免不同部分的重叠。具体发生冲突时不同的显示效果的控制请移步:HTML5 Canvas 绘图方法整理 【十七、Canvas透明度 / 图形叠加、层次、冲突】。
2. 绘制待拖拽的内容
2.1 定义并绘制待拖拽物体对象
//创建圆滑块
function createBlob(x,y,width,height,color,alpha){
//定义对象
var blob = {};
blob.alpha = alpha;
blob.color = color;
blob.x = x;
blob.y = y;
blob.width = width;
blob.height = height;
blob.InnerWidth = 0;
blob.InnerHeight = 0;
return blob;
}
//绘制圆滑块
function DrawBlob(blob){
ctx.globalAlpha = blob.alpha;
ctx.beginPath();
ctx.fillStyle = blob.color;
ctx.rect(blob.x,blob.y,blob.width,blob.height);
ctx.fill();
ctx.closePath();
ctx.globalAlpha = 1;
}
这里主要考虑到,可能需要创建多个待拖拽的对象,因此,为了方便地将其组织起来,笔者定义了上述的对象。
2.2 获取检测区域
这里需要解释一下,这里的检测区域是笔者为了形象而提出的概念。其实,它就是每次待拖拽的物体的位置。getBounds方法将会返回该物体的矩形区域。然后,再通过containsPoint方法,判断鼠标位置(x,y)是否落在该区域。
//获取检测区域
function getBounds(blob){
return {
x: blob.x,
y: blob.y,
width: blob.width,
height: blob.height
};
}
//判断鼠标是否点击在指定检测区域
function containsPoint(rect, x, y){
return !(x<rect.x || x>rect.x + rect.width ||
y<rect.y || y>rect.y + rect.height);
}
2.3 注册鼠标事件
2.3.1 注册鼠标按下事件
//鼠标按下,将鼠标按下坐标保存在x,y中
c.onmousedown = function(e){
//记录鼠标所在位置的坐标
x = e.clientX - c.getBoundingClientRect().left;
y = e.clientY - c.getBoundingClientRect().top;
//记录所在检测区域内坐标
blob1.InnerWidth = x - blob1.x;
blob1.InnerHeight = y - blob1.y;
drag(blob1,x,y);
};
2.3.2 注册鼠标移动和鼠标松开事件
//拖拽函数
function drag(blob,x,y) {
// 判断鼠标是否在检测区域
if(containsPoint(getBounds(blob), x, y)){
//注册鼠标移动事件
c.onmousemove = function(e){
var x = e.clientX - c.getBoundingClientRect().left;
var y = e.clientY - c.getBoundingClientRect().top;
//清除画布内容
ctx.clearRect(0,0,this.width,this.height);
//重绘
drawBackground(ctx);
DrawBlob(blob1);
//更新块所在的位置
blob1.x = x - blob1.InnerWidth;
blob1.y = y - blob1.InnerHeight;
};
//注册鼠标松开事件
c.onmouseup = function(){
this.onmousemove = null;
this.onmouseup = null;
};
};
}
最后,需要提的是,有很多人会用 isPointInPath() 方法来判断指定的坐标点是否在检测区域内,如果在返回true,否则,返回false。但是,该方法只能检测鼠标是否落在最后一个绘制的封闭路径之内。
最后,实例的效果和完整的代码如下。
<!DOCTYPE html>
<html>
<head>
<title>绘制解码图</title>
</head>
<body>
<canvas id="myCanvas" width="1200" height="600" style="border:1px solid #000000;" >您的浏览器不支持canvas!</canvas>
<div id="cover" style="left: 100px; top: 100px"></div>
<script>
//绘制染色体
function createChromosome(ctx,x,y,color,data){
//基因的宽度和高度
var rect_width = 20;//基因位的宽度
var rect_height = 10;//染色体的高度
var dis = 10;//基因位之间的距离
var first_x = 20;//第一个基因位距离开始的水平距离
var first_y = 1;//第一个基因位距离开始的水平距离
var radius = 4;//圆角矩形的半径
//计算染色体的长度
var len = first_x * 2 + data.length * rect_width + (data.length - 1) * dis;
//绘制圆角矩形
drawRoundRect(ctx, x, y, len, rect_height, radius);
for(i=0;i<data.length;i++){
//绘制基因位位置
ctx.fillStyle="#FFFFFF";
ctx.fillStyle=color[i];
ctx.fillRect(x + first_x + dis * i + i * rect_width, y + first_y, rect_width, rect_height - 2 * first_y);
//绘制基因编码
ctx.fillStyle="#808080";
ctx.fillText(data[i], x + first_x + dis * i + i * rect_width + rect_width / 2 - 2, y - 2 * first_y);
}
return x + len;
}
//绘制圆角矩形
function drawRoundRect(cxt, x, y, width, height, radius){
ctx.strokeStyle = "#262626";//线条颜色
//开始绘制(从左上角开始顺时针绘制)
cxt.beginPath();
//左上角
cxt.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2);
cxt.lineTo(x + width - radius, y);
//右上角
cxt.arc(x + width - radius, y + radius, radius, Math.PI * 3 / 2, Math.PI * 2);
cxt.lineTo(x + width, y + height - radius);
//右下角
cxt.arc(x + width - radius, y + height - radius, radius, 0, Math.PI * 1 / 2);
cxt.lineTo(x + radius, y + height);
//左下角
cxt.arc(x + radius, y + height - radius, radius, Math.PI * 1 / 2, Math.PI);
cxt.lineTo(x, y + radius);
cxt.closePath();
//绘制路径
ctx.stroke();
}
//绘制箭头
function drawArrow(ctx, fromX, fromY, toX, toY,theta,headlen,width,color) {
theta = typeof(theta) != 'undefined' ? theta : 30;
headlen = typeof(theta) != 'undefined' ? headlen : 10;
width = typeof(width) != 'undefined' ? width : 1;
color = typeof(color) != 'color' ? color : '#000'; // 计算各角度和对应的P2,P3坐标
var angle = Math.atan2(fromY - toY, fromX - toX) * 180 / Math.PI, angle1 = (angle + theta) * Math.PI / 180, angle2 = (angle - theta) * Math.PI / 180, topX = headlen * Math.cos(angle1), topY = headlen * Math.sin(angle1), botX = headlen * Math.cos(angle2), botY = headlen * Math.sin(angle2);
ctx.save();
ctx.beginPath();
var arrowX = fromX - topX, arrowY = fromY - topY;
ctx.moveTo(arrowX, arrowY);
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
arrowX = toX + topX;
arrowY = toY + topY;
ctx.moveTo(arrowX, arrowY);
ctx.lineTo(toX, toY); arrowX = toX + botX;
arrowY = toY + botY;
ctx.lineTo(arrowX, arrowY);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
ctx.restore();
}
//绘制静态对象
function drawBackground(ctx){
var startx_1 = 50;
var startx_2 = 30;
var starty_1 = 20;
var starty_2 = 50;
var starty_3 = 150;
var starty_4 = 200;
var dis1 = 50;
var dis2 = 100;
//第一步
var color = ["#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5"];
//编码
//绘制罐染色体
var c1 = createChromosome(ctx,startx_1,starty_1,color,[5,0,1,1,0,6,9,0,1]);
//绘制塔染色体
var c2 = createChromosome(ctx,c1 + dis1,starty_1,color,[1,0,2,2,0,2,3,0,1]);
//绘制速度染色体
var c3 = createChromosome(ctx,c2 + dis1,starty_1,color,[1,0,1,1,0,1,1,0,1]);
var color1 = ["#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#262626","#262626","#262626","#262626"];
var color2 = ["#262626","#262626","#262626"];
var color3 = ["#262626","#262626","#262626"];
//解码
//选择供油罐
c1 = createChromosome(ctx,startx_2,dis2,color1,[0,1,2,3,4,5,6,7,8,9]);
//选塔
c2 = createChromosome(ctx,c1 + dis2,dis2,color2,[0,1,2]);
//选择速度
c3 = createChromosome(ctx,c2 + dis2,dis2,color3,[0,1,2]);
//绘制箭头
drawArrow(ctx, 50, 35, 170,70,30,10,2,'#f36');
drawArrow(ctx, 50, 35, 220,70,30,10,2,'#f36');
drawArrow(ctx, 50, 35, 250,70,30,10,2,'#f36');
//第二步
var color = ["#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5"];
//编码
//绘制罐染色体
c1 = createChromosome(ctx,startx_1,starty_3,color,[5,0,1,1,0,6,9,0,1]);
//绘制塔染色体
c2 = createChromosome(ctx,c1 + dis1,starty_3,color,[1,0,2,2,0,2,3,0,1]);
//绘制速度染色体
c3 = createChromosome(ctx,c2 + dis1,starty_3,color,[1,0,1,1,0,1,1,0,1]);
var color1 = ["#262626","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#E5E5E5","#262626","#262626","#262626"];
var color2 = ["#262626","#262626","#262626"];
var color3 = ["#262626","#262626","#262626"];
//解码
//选择供油罐
c1 = createChromosome(ctx,startx_2,starty_4,color1,[0,1,2,3,4,5,6,7,8,9]);
//选塔
c2 = createChromosome(ctx,c1 + dis2,starty_4,color2,[0,1,2]);
//选择速度
c3 = createChromosome(ctx,c2 + dis2,starty_4,color3,[0,1,2]);
}
//创建圆滑块
function createBlob(x,y,width,height,color,alpha){
//定义对象
var blob = {};
blob.alpha = alpha;
blob.color = color;
blob.x = x;
blob.y = y;
blob.width = width;
blob.height = height;
blob.InnerWidth = 0;
blob.InnerHeight = 0;
return blob;
}
//绘制圆滑块
function DrawBlob(blob){
ctx.globalAlpha = blob.alpha;
ctx.beginPath();
ctx.fillStyle = blob.color;
ctx.rect(blob.x,blob.y,blob.width,blob.height);
ctx.fill();
ctx.closePath();
ctx.globalAlpha = 1;
}
//获取绘图环境
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
//绘制背景
drawBackground(ctx);
//创建滑块
var blob1 = createBlob(70,18,20,14,"#F00",0.5);
DrawBlob(blob1);
//鼠标按下,将鼠标按下坐标保存在x,y中
c.onmousedown = function(e){
//记录鼠标所在位置的坐标
x = e.clientX - c.getBoundingClientRect().left;
y = e.clientY - c.getBoundingClientRect().top;
//记录所在检测区域内坐标
blob1.InnerWidth = x - blob1.x;
blob1.InnerHeight = y - blob1.y;
drag(blob1,x,y);
};
//获取检测区域
function getBounds(blob){
return {
x: blob.x,
y: blob.y,
width: blob.width,
height: blob.height
};
}
//判断鼠标是否点击在指定检测区域
function containsPoint(rect, x, y){
return !(x<rect.x || x>rect.x + rect.width ||
y<rect.y || y>rect.y + rect.height);
}
//拖拽函数
function drag(blob,x,y) {
// 判断鼠标是否在检测区域
if(containsPoint(getBounds(blob), x, y)){
//注册鼠标移动事件
c.onmousemove = function(e){
var x = e.clientX - c.getBoundingClientRect().left;
var y = e.clientY - c.getBoundingClientRect().top;
//清除画布内容
ctx.clearRect(0,0,this.width,this.height);
//重绘
drawBackground(ctx);
DrawBlob(blob1);
//更新块所在的位置
blob1.x = x - blob1.InnerWidth;
blob1.y = y - blob1.InnerHeight;
};
//注册鼠标松开事件
c.onmouseup = function(){
this.onmousemove = null;
this.onmouseup = null;
};
};
}
</script>
</body>
</html>