【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();   
        }

这里,需要注意的是,一般在网页中,坐标原点为左上角。绘制圆的方向是顺时针方向。

【js】 canvas绘图和拖拽效果实现

 

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。但是,该方法只能检测鼠标是否落在最后一个绘制的封闭路径之内。

最后,实例的效果和完整的代码如下。

【js】 canvas绘图和拖拽效果实现

【js】 canvas绘图和拖拽效果实现

<!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>