Canvas绘制可变换矩形需要了解的知识点及思路

809 阅读5分钟

从手机上删除了游戏,玩了一周游戏,也该重新把晚上的时间利用起来,去思考一些问题了。

能够拖拽变换的矩形

这个功能很常见,比如手机中的照片裁剪,如图: 如上图:当鼠标位于图片区域四个角时或上下左右四条边时,鼠标样式会变成一个重置大小的样式。此时,我们可以移动鼠标,对该区域进行变换。

功能实现前需要了解的内容

clientX,offsetX,pageX的区别

clientX:返回触点相对于可见视区(visual viewport)左边沿的的 X 坐标. 不包括任何滚动偏移.这个值会根据用户对可见视区的缩放行为而发生变化。

offsetX:MouseEvent 接口的只读属性 offsetX 规定了事件对象与目标节点的内填充边(padding edge)在 X 轴方向上的偏移量。

pageX:触点相对于 HTML 文档左边沿的的 X 坐标. 和 clientX 属性不同, 这个值是相对于整个 html 文档的坐标, 和用户滚动位置无关. 因此当存在水平滚动的偏移时, 这个值包含了水平滚动的偏移.

获取鼠标位置信息

  • 按下鼠标时鼠标的位置
// 按下鼠标
  down = (self, e) => {
    const { offsetX, offsetY, layerX, layerY } = e;
    this.mouseX = offsetX || layerX;
    this.mouseY = offsetY || layerY;
    console.log('mouseX,mouseY', this.mouseX, this.mouseY);
    this.isMove = true;
  };
  • 移动鼠标时鼠标的位置
// 移动鼠标
  move = (self, e) => {
    const { offsetX, offsetY, layerX, layerY } = e;
    // console.log('e----current-mouse-pos---->', e)
    console.log('posNo----', this.posNo);
    // console.log('矩形实例----', this.rect)
    let cur_x_point = offsetX || layerX;
    let cur_y_point = offsetY || layerY;
    // console.log('当前鼠标位置', cur_x_point, cur_y_point)
    ...
    }
  • 鼠标偏移量deltaX,deltaY
  let deltaX = cur_x_point - this.mouseX;
  let deltaY = cur_y_point - this.mouseY;

注: 偏移量主要用来计算矩形的位置改变及宽高改变,非常重要。

检测当前路径中是否包含检测点

我们需要将举行四个角及四条边的路径信息存下来,并检测当前鼠标位置是否在该路径中,用来展示对应的鼠标指针样式。

检测方法需要用到canvas的isPointInPath()方法。

鼠标指针样式

鼠标指针样式对于很多前端来说并不陌生,因为用的cursor:pointer比较多。但实际上鼠标指针样式大致分5种类型。链接及状态|选择|拖拽|重置大小|缩放

  • 链接及状态
    • context-menu 指针下有可用内容目录。
    • help 指示帮助。
    • pointer 悬浮于连接上时,通常为手。
    • progress 程序后台繁忙,用户仍可交互 (与 wait 相反)。
    • wait 程序繁忙,用户不可交互 (与 progress 相反).图标一般为沙漏或者表。
  • 选择
    • cell 指示单元格可被选中
    • crosshair 交叉指针,通常指示位图中的框选
    • text 指示文字可被选中
    • vertical-text 指示垂直文字可被选中
  • 拖拽
    • alias 复制或快捷方式将要被创建
    • copy 指示可复制
    • move 被悬浮的物体可被移动
    • no-drop 当前位置不能扔下
    • not-allowed 不能执行
    • grab 可抓取
    • grabbing 抓取中
  • 重设大小及滚动
    • all-scroll 元素可任意方向滚动 (平移).
    • col-resize 元素可被重设宽度。通常被渲染为中间有一条竖线分割的左右两个箭头
    • row-resize 元素可被重设高度。通常被渲染为中间有一条横线分割的上下两个箭头
    • n-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • e-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • s-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • w-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • ne-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • nw-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • se-resize 某条边将被移动。例如元素盒的东南角被移动时使用 se-resize
    • ew-resize 指示双向重新设置大小
    • ns-resize 指示双向重新设置大小
    • nesw-resize 指示双向重新设置大小
    • nwse-resize 指示双向重新设置大小
  • 缩放
    • zoom-in 放大
    • zoom-out 缩小

变换过程的大致逻辑

  1. 在canvas中添加一个矩形。
  2. 给canvas添加mousedown,mousemove,mouseup,mouseout事件。
  3. mousedown鼠标按下试记录当前鼠标位置,mousemove移动鼠标时计算偏移量,该偏移量同时也是矩形的偏移量。
  4. mousemove移动鼠标时更新矩形四个角及四条边的路径信息,以便鼠标移到对应位置时设置对应的指针样式。
  5. mousemove移动鼠标时进行各种判断(拖动的是左上角?右上角?顶边?底边?等等),同时基于偏移量,重新设置矩形的位置及宽高。

具体代码大致有200-300行,贴个核心move()方法出来,有兴趣的可以研究一下。

move = (self, e) => {
    const { offsetX, offsetY, layerX, layerY } = e;
    // console.log('e----current-mouse-pos---->', e)
    console.log('posNo----', this.posNo);
    // console.log('矩形实例----', this.rect)
    let cur_x_point = offsetX || layerX;
    let cur_y_point = offsetY || layerY;
    // console.log('当前鼠标位置', cur_x_point, cur_y_point)
    let cur_ctrl_posNo = -1; // 当前控制区编号
    if (this.isMove) {
      let deltaX = cur_x_point - this.mouseX;
      let deltaY = cur_y_point - this.mouseY;
      if (this.posNo == 0) {
        // 拖动
        console.log('移动------');
        console.log('deltaX,deltaY', deltaX, deltaY);
        console.log('移动------');
        this.rect.rectInit(
          deltaX,
          deltaY,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 1) {
        // 左上角
        if (this.options.rect_w <= this.min && this.options.rect_h > this.min) {
          cur_ctrl_posNo = 2;
        } else if (
          this.options.rect_h <= this.min &&
          this.options.rect_w > this.min
        ) {
          cur_ctrl_posNo = 3;
        } else if (
          this.options.rect_h <= this.min &&
          this.options.rect_w <= this.min
        ) {
          cur_ctrl_posNo = 4;
        } else {
          cur_ctrl_posNo = 1;
        }
        this.options.rect_w -= deltaX;
        this.options.rect_h -= deltaY;
        this.rect.rectInit(
          deltaX,
          deltaY,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 4) {
        // 右下角
        if (this.options.rect_w <= this.min && this.options.rect_h > this.min) {
          cur_ctrl_posNo = 3;
        } else if (
          this.options.rect_h <= this.min &&
          this.options.rect_w > this.min
        ) {
          cur_ctrl_posNo = 2;
        } else if (
          this.options.rect_w <= this.min &&
          this.options.rect_h > this.min
        ) {
          cur_ctrl_posNo = 1;
        } else {
          cur_ctrl_posNo = 4;
        }
        this.options.rect_w += deltaX;
        this.options.rect_h += deltaY;
        this.rect.rectInit(
          0,
          0,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 2) {
        // 左上角
        if (this.options.rect_w <= this.min && this.rect_h > this.min) {
          cur_ctrl_posNo = 1;
        } else if (this.options.rect_h <= this.min && this.rect_w > this.min) {
          cur_ctrl_posNo = 4;
        } else if (this.options.rect_h <= this.min && this.rect_w <= this.min) {
          cur_ctrl_posNo = 3;
        } else {
          cur_ctrl_posNo = 2;
        }
        this.options.rect_w += deltaX;
        this.options.rect_h -= deltaY;
        this.rect.rectInit(
          0,
          deltaY,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 3) {
        if (this.options.rect_w <= this.min && this.options.rect_h > this.min) {
          cur_ctrl_posNo = 4;
        } else if (
          this.options.rect_h <= this.min &&
          this.options.rect_w > this.min
        ) {
          cur_ctrl_posNo = 1;
        } else if (
          this.options.rect_w <= this.min &&
          this.options.rect_h <= this.min
        ) {
          cur_ctrl_posNo = 2;
        } else {
          cur_ctrl_posNo = 3;
        }
        this.options.rect_w -= deltaX;
        this.options.rect_h += deltaY;
        this.rect.rectInit(
          deltaX,
          0,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 5) {
        // 上
        this.options.rect_h < 0 ? (cur_ctrl_posNo = 6) : (cur_ctrl_posNo = 5);
        this.options.rect_h -= deltaY;
        this.rect.rectInit(
          0,
          deltaY,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 6) {
        // 下
        this.options.rect_h < 0 ? (cur_ctrl_posNo = 5) : (cur_ctrl_posNo = 6);
        this.options.rect_h += deltaY;
        this.rect.rectInit(
          0,
          0,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 7) {
        // 左
        this.options.rect_w < 0 ? (cur_ctrl_posNo = 8) : (cur_ctrl_posNo = 7);
        this.options.rect_w -= deltaX;
        this.rect.rectInit(
          deltaX,
          0,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      } else if (this.posNo == 8) {
        // 右
        this.options.rect_w < 0 ? (cur_ctrl_posNo = 7) : (cur_ctrl_posNo = 8);
        this.options.rect_w += deltaX;
        this.rect.rectInit(
          0,
          0,
          this.options.canvas_w,
          this.options.canvas_h,
          this.options.rect_w,
          this.options.rect_h
        );
      }
      changeMouse(this.canvas, cur_ctrl_posNo);
      this.mouseX = cur_x_point;
      this.mouseY = cur_y_point;
      this.rect.drawRect(
        this.ctx,
        this.options.canvas_w,
        this.options.canvas_h
      );
      this.pathes = changePath(
        this.rect.startX,
        this.rect.startY,
        this.rect.rect_w,
        this.rect_h,
        this.options.dis,
        this.pathes
      );
    } else {
      this.posNo = getPos(cur_x_point, cur_y_point, this.ctx, this.pathes); //移动的时候就不能再重新获取位置了
      console.log('this.posNo=------>', this.posNo);
      changeMouse(this.canvas, this.posNo);
    }
  };

总结

canvas的API看起来都很简单,但是真正想做好一个东西,确是需要花费不少功夫的,希望我能坚持下去,将它的API都过一遍最好。

最后说两句

  1. 动一动您发财的小手,「点个赞吧」
  2. 动一动您发财的小手,「点个在看」
  3. 都看到这里了,不妨 「加个关注」
  4. 不妨 「转发一下」,好东西要记得分享

@@@点击加个关注呗@@@