Canvas高级路径操作之贝塞尔曲线编辑

1,443 阅读4分钟

前言

为了追踪所画内容,诸如画图应用程序、计算机辅助设计系统(computer-aided design system 简称CAD系统)以及游戏等许多应用程序,都会维护一份包含当前显示对象的列表。通常来说,这些应用程序都允许用户对当前显示在屏幕上的物体进行操作。比方说,在CAD应用程序中,我们可以对设计中的元素进行选择、移动、缩放等操作
——《HTML5 Canvas核心技术》

在Canvas中也同样如此,Canvas提供了一个名为isPointInPath(x, y)的API,判断点(x, y)是否在路径之中。如果在路径之中,则返回true。于是我们可以有如下思路:

维护一个可以描述各个路径的数组,通过ispointInPath(x, y)判断点击位置是否在某一个路径之中,选中此路径,进行操作(移动、缩放等),再绘制图形

此前有同类型例子:Canvas高级路径操作之拖拽对象
本文记录如何使用Canvas拖拽一条贝塞尔曲线并进行编辑,例子来源于《HTML5 Canvas核心技术》,修改了部分代码,也放在CodePen之中
Demo如下(印子是录屏软件的原因👹):

CodePen打开
本文内容如下:

  • 思路讲解
    • ”橡皮带(rubber band)“与贝塞尔曲线
    • 实现逻辑思路
    • 伪代码思路
  • 代码分割
    • mousedown
    • mousemove
    • mouseup
  • 结语
  • 参考资料

思路讲解

思路奖讲解主要分为三部分,即”橡皮带(rubber band)“与贝塞尔曲线实现逻辑思路伪代码思路
”橡皮带(rubber band)“与贝塞尔曲线:阐述绘制Demo中如何贝塞尔曲线
实现逻辑思路:本Demo既有绘制编辑两种状态
伪代码思路:结合伪代码阐述思路

”橡皮带(rubber band)“与贝塞尔曲线

首先我们看下三次贝塞尔曲线的APIvoid ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
cp1与cp2代表两个控制点,x、y代表另一个端点的位置,还有一个端点用moveTo()作为起始位置即可

接下来是如何绘制

我们通过不断改变的rubberBandRect进行绘图,利用left、top、width、height来定位四个点

无论是绘制曲线还是编辑曲线,都是通过改变rubberBandRect,再绘制对应的点及曲线

实现逻辑思路

此阶段为编辑状态,发生在绘制完曲线后,即有点可拖拽:

此状态为可绘制状态,在初始状态或结束编辑状态之后(本Demo未做编辑”已定形曲线“的功能):

综上及Demo体验,我们可以把逻辑归纳为如下:

伪代码思路

这部分结合代码中部分关键变量及思维导图进行说明: 关键变量:

editing: //是否处于编辑状态
drawingCurve: //是否正在绘制曲线
draggingPoint: //记录拖拽点,可能的值有
               //null(表示绘制曲线后点击空白部分,无拖拽点,结束编辑阶段),
               //endPoint,controlPoint

接下来在代码分割中介绍各个阶段部分

代码分割

mousedown

    canvas.onmousedown = function (e) {
        const loc = windowToCanvas(e.clientX, e.clientY);//获取位置
        e.preventDefault();

        if (editing === false) { //画线
            saveDrawingSurface();
            mousedown.x = loc.x;
            mousedown.y = loc.y;
            updateRubberBandRectangle(loc);//更新rubberBandRect
            drawingCurve = true;//标记正在绘图
        } else {//编辑曲线阶段进入
          // 选择点击位置对应的点,三种可能null, endPoint, controlPoint
            draggingPoint = cursorInControlPoint(loc);
            if (draggingPoint === null) {
                draggingPoint = cursorInEndPoint(loc);
            }
        }
    }

mousedown主要判断是绘制曲线还是编辑曲线

mousemove

    canvas.onmousemove = function (e) {
        const loc = windowToCanvas(e.clientX, e.clientY);

        if (drawingCurve || draggingPoint) {//未使用应用情况下鼠标移动干扰
            e.preventDefault();
            restoreDrawingSurface();

            if (guideWires) {
                drawGuideWires(loc.x, loc.y);
            }
        }

        if (drawingCurve) {//绘制曲线
            updateRubberBand(loc); //更新rubberBand并绘制
            drawControlAndEndPoints(); 
        } else if (draggingPoint) {//mousedown编辑阶段中点击端点或控制点,开始拖拽
            updateDraggingPoint(loc);
            drawControlAndEndPoints();
            drawBezierCurve();
        }

    }

mousemove主要排除不使用功能时鼠标滑过的情况,及对应阶段的绘制

mouseup

    canvas.onmouseup = function (e) {
        const loc = windowToCanvas(e.clientX, e.clientY);

        restoreDrawingSurface();

        if (editing === false) {//画线阶段进入
            updateRubberBand(loc);
            drawControlAndEndPoints();
            drawingCurve = false;
            editing = true; //mouseup后可进入编辑曲线阶段
          
        } else {//编辑曲线阶段进入
            if (draggingPoint) {//有拖拽点,进行拖拽
                drawControlAndEndPoints();

            } else {//没拖拽点,后续结束编辑状态
                editing = false;
            }
          
            drawBezierCurve();
            draggingPoint = null;
        }
    }

mouseup主要是对每个阶段的后续处理,如果是画线阶段结束,则进入编辑阶段,如果是编辑阶段结束,则通过判断有无拖拽点继续编辑或结束编辑

结语

本例子只要理清了RubberBandRect绘制阶段与编辑阶段的控制后就会容易很多了
不足之处欢迎大家交流学习✨

参考资料

《HTML5 Canvas核心技术》