能够拖拽变换的矩形
这个功能很常见,比如手机中的照片裁剪,如图:
如上图:当鼠标位于图片区域四个角时或上下左右四条边时,鼠标样式会变成一个重置大小的样式。此时,我们可以移动鼠标,对该区域进行变换。
功能实现前需要了解的内容
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-resizee-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizes-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizew-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizene-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizenw-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizese-resize
某条边将被移动。例如元素盒的东南角被移动时使用 se-resizeew-resize
指示双向重新设置大小ns-resize
指示双向重新设置大小nesw-resize
指示双向重新设置大小nwse-resize
指示双向重新设置大小
- 缩放
zoom-in
放大zoom-out
缩小
变换过程的大致逻辑
- 在canvas中添加一个矩形。
- 给canvas添加
mousedown
,mousemove
,mouseup
,mouseout
事件。 mousedown
鼠标按下时记录当前鼠标位置,mousemove
移动鼠标时计算偏移量,该偏移量同时也是矩形的偏移量。mousemove
移动鼠标时更新矩形四个角及四条边的路径信息,以便鼠标移到对应位置时设置对应的指针样式。mousemove
移动鼠标时进行各种判断(拖动的是左上角?右上角?顶边?底边?等等),同时基于偏移量,重新设置矩形的位置及宽高。
具体代码大致有200-300行,贴个核心move()
方法出来,有兴趣的可以研究一下。
posNo 代表当前拖动的位置。
代码语言:javascript复制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都过一遍最好。