这段时间一个 canvas 库所实现的元素拖拽管制,感觉很不错。于是本人用 js + div 来实现一个。用了 react 框架,练练手。
思路
在被管制的元素的四条边和四个角增加 8 个控制点控制点。拖拽控制点时判断拖拽的方向,计算偏移量。批改元素的 top、left、width、height。
旋转性能是通过三角函数计算鼠标拖动后的角度。动静批改元素的 rotate
画板(舞台)
想要对元素进行管制。咱们先定义一个画板,规定元素只能在指定的范畴内变动。
而后在画板内插入一个被管制的 div 元素,就定义为 drawing-item
类名吧。drawing-item
须要相对定位于画板
以及八个方向的控制点。这是最简略的构造了
import "./Drawing.css"
// 东南西北,西南、东南、西北、东北
const points = ['e', 'w', 's', 'n', 'ne', 'nw', 'se', 'sw']
function Drawing() {// const data = useState()
return <div className="drawing-wrap">
<div className="drawing-item">
{points.map(item => <div className={`control-point point-${item}`}></div>)}
</div>
</div>
}
export default Drawing;
给他们都加上款式
<style>
.drawing-wrap{
width: 500px;
height: 500px;
border: 1px solid red ;
position: relative;
top: 100px ;
left: 100px;
}
.drawing-item {
cursor: move;
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 100px;
left: 100px;
box-sizing: border-box;
}
.control-point{
position: absolute;
box-sizing: border-box;
display: inline-block;
background: #fff;
border: 1px solid #c0c5cf;
box-shadow: 0 0 2px 0 rgba(86, 90, 98, .2);
border-radius: 6px;
padding: 8px;
margin-top: -8px !important;
margin-left: -8px !important;
user-select: none; // 留神禁止鼠标选中控制点元素,不然拖拽事件可能会因而被中断
}
.control-point.point-e{
cursor: ew-resize;
left: 100%;
top: 50%;
margin-left: 1px
}
.control-point.point-n{
cursor: ns-resize;
left: 50%;
margin-top: -1px
}
.control-point.point-s{
cursor: ns-resize;
left: 50%;
top: 100%;
margin-top: 1px
}
.control-point.point-w{
cursor: ew-resize;
top: 50%;
left: 0;
margin-left: -1px
}
.control-point.point-ne {
cursor: nesw-resize;
left: 100%;
margin-top: -1px;
margin-left: 1px
}
.control-point.point-nw {
cursor: nwse-resize;
margin-left: -1px;
margin-top: -1px
}
.control-point.point-se {
cursor: nwse-resize;
left: 100%;
top: 100%;
margin-left: 1px;
margin-top: 1px
}
.control-point.point-sw {
cursor: nesw-resize;
top: 100%;
margin-left: -1px;
margin-top: 1px
}
</style>
效果图:
拖拽
元素构造安顿好后就来筹备写性能了。先来剖析下拖拽缩放最次要的性能是什么,拖拽嘛!拖拽算是常见的简略性能了,须要绑定三个事件:onMouseDown(鼠标按下)、onMouseMove(挪动)、onMouseUp (抬起)。
先来写拖拽的性能,以实现元素在画板内位移。元素的地位挪动只须要动静批改 left 和 top,定义一个 style 对象给 drawing-item
加上
const [style, setStyle] = useState({
left: 100,
top: 100,
width: 100,
height: 100
})
// html
<div className="drawing-item" style={style}>
咱们给画板 drawing-wrap
绑定监听鼠标挪动和抬起的事件,给 drawing-item
监听鼠标按下的事件。
// 鼠标被按下
function onMouseDown(e) {}
// 鼠标挪动
function onMouseMove() {}
// 鼠标被抬起
function onMouseUp() {}
return <div className="drawing-wrap" onMouseUp={onMouseUp} onMouseMove={onMouseMove}>
<div className="drawing-item" style={style}>
{points.map(item => <div className={`control-point point-${item}`} ></div>)}
</div>
</div>
// 咱们给每个控制点加了 `onMouseDown` 事件,当鼠标按下时将以后控制点的方向传进去。
当鼠标放在drawing-item
上按下时。就能获取到以后元素的以及鼠标的地位。
偏移量
偏移量指的是元素绝对于父元素的偏移间隔
获取元素绝对于画板的偏移量。
// 元素绝对于画板的以后地位。const top = e.target.offsetTop;
const left = e.target.offsetLeft;
// 而后鼠标坐标是
const cY = e.clientY; // clientX 绝对于可视化区域
const cX = e.clientX;
鼠标按下时,须要将以后鼠标的地位和元素的地位保存起来。每当鼠标挪动时。计算鼠标挪动了多少间隔。
// 画板的
const wrapStyle = {
left: 100,
top: 100,
width: 500,
height: 500
}
const [style, setStyle] = useState({
left: 100,
top: 100,
width: 100,
height: 100
})
// 初始数据,因为不须要从新 render 所以用 useRef
const oriPos = useRef({
top: 0, // 元素的坐标
left: 0,
cX: 0, // 鼠标的坐标
cY: 0
})
const isDown = useRef(false)
// 鼠标被按下
function onMouseDown(e) {
// 阻止事件冒泡
e.stopPropagation();
isDown.current = true;
// 元素绝对于画板的以后地位。const top = e.target.offsetTop;
const left = e.target.offsetLeft;
// 而后鼠标坐标是
const cY = e.clientY; // clientX 绝对于可视化区域
const cX = e.clientX;
oriPos.current = {top, left, cX, cY}
}
// 鼠标挪动
function onMouseMove(e) {
// 判断鼠标是否按住
if (!isDown.current) return
// 元素地位 = 初始地位 + 鼠标偏移量
const top = oriPos.current.top + (e.clientY - oriPos.current.cY)
const left = oriPos.current.left + (e.clientX - oriPos.current.cX)
setStyle({
top,
left
})
}
// 鼠标被抬起
function onMouseUp(e) {console.log(e, 'onMouseUp');
isDown.current = false;
}
看下成果。
能够拖着跑了,然而再拖一下,哎,拖出界了
范畴限度还没加上呢, 加一下限度
function onMouseMove(e) {
// 判断鼠标是否按住
if (!isDown.current) return
let newStyle = {...style};
// 元素以后地位 + 偏移量
const top = oriPos.current.top + e.clientY - oriPos.current.cY;
const left = oriPos.current.left + e.clientX - oriPos.current.cX;
// 限度必须在这个范畴内挪动 画板的高度 - 元素的高度
newStyle.top = Math.max(0, Math.min(top, wrapStyle.height - style.height));
newStyle.left = Math.max(0, Math.min(left, wrapStyle.width - style.width));
setStyle(newStyle)
}
这下就拖不进来了。
下面的代码还有些小坑。咱们定义的 三个办法 onMouseMove
、onMouseUp
、onMouseDown
是间接通过 function
定义的,这回存在一些性能上的问题,每次设置style
state 时会从新渲染组件,导致从新定义这三个办法。这是没必要的性能节约。
通过应用 react 的 useCallback
语法糖 定义方法,能够防止一直的从新定义。与下面的useRef
一样
const onMouseDown = useCallback((e) => {/*...*/},[])
const onMouseMove = useCallback((e) => {/*...*/},[])
const onMouseUp = useCallback((e) => {/*...*/},[])
缩放
接下来封装一个办法。来计算元素的缩放。
咱们在某个控制点上按下鼠标,将以后控制点的方向保存起来,鼠标拖动后依据以后方向计算元素地位和宽高
先将原先的 拖拽办法也封装进去。顺便也将 onMouseMove 改一下。
/**
* 元素变动。办法放在组件内部或者其余中央。* @param direction 方向 // move 挪动 / 'e', 'w', 's', 'n', 'ne', 'nw', 'se', 'sw'
* @param oriStyle 元素的属性 width height top left
* @param oriPos 鼠标按下时所记录的坐标
* @param e 事件 event
*/
function transform(direction, oriPos, e) {const style = {...oriPos.current}
const offsetX = e.clientX - oriPos.current.cX;
const offsetY = e.clientY - oriPos.current.cY;
switch (direction.current) {
// 拖拽挪动
case 'move' :
// 元素以后地位 + 偏移量
const top = oriPos.current.top + offsetY;
const left = oriPos.current.left + offsetX;
// 限度必须在这个范畴内挪动 画板的高度 - 元素的高度
style.top = Math.max(0, Math.min(top, wrapStyle.height - style.height));
style.left = Math.max(0, Math.min(left, wrapStyle.width - style.width));
break
// 东
case 'e':
// 向右拖拽增加宽度
style.width += offsetX;
return style
// 西
case 'w':
// 减少宽度、地位同步左移
style.width -= offsetX;
style.left += offsetX;
return style
// 南
case 's':
style.height += offsetY;
return style
// 北
case 'n':
style.height -= offsetY;
style.top += offsetY;
break
// 西南
case 'ne':
style.height -= offsetY;
style.top += offsetY;
style.width += offsetX;
break
// 东南
case 'nw':
style.height -= offsetY;
style.top += offsetY;
style.width -= offsetX;
style.left += offsetX;
break
// 西北
case 'se':
style.height += offsetY;
style.width += offsetX;
break
// 东北
case 'sw':
style.height += offsetY;
style.width -= offsetX;
style.left += offsetX;
break
}
return style
}
// 鼠标被按下
const onMouseDown = useCallback((dir, e) => {
// 阻止事件冒泡
e.stopPropagation();
// 保留方向。direction.current = dir;
isDown.current = true;
// 而后鼠标坐标是
const cY = e.clientY; // clientX 绝对于可视化区域
const cX = e.clientX;
oriPos.current = {
...style,
cX, cY
}
})
// 鼠标挪动
const onMouseMove = useCallback((e) => {
// 判断鼠标是否按住
if (!isDown.current) return
let newStyle = transform(direction, oriPos, e);
setStyle(newStyle)
}, [])
这就实现了对元素的拖拽缩放性能了。
旋转
给drawing-item
加一个 旋转按钮吧。
<style>
.control-point.control-rotator{
cursor: pointer;
position: absolute;
left: 50%;
top: 130%;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='24'height='24'xmlns='http://www.w3.org/2000/svg'fill='%23757575'%3E%3Cg fill='none'fill-rule='evenodd'%3E%3Ccircle stroke='%23CCD1DA'fill='%23FFF'cx='12'cy='12'r='11.5'/%3E%3Cpath d='M16.242 12.012a4.25 4.25 0 00-5.944-4.158L9.696 6.48a5.75 5.75 0 018.048 5.532h1.263l-2.01 3.002-2.008-3.002h1.253zm-8.484-.004a4.25 4.25 0 005.943 3.638l.6 1.375a5.75 5.75 0 01-8.046-5.013H5.023L7.02 9.004l1.997 3.004h-1.26z'fill='%23000'fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E");
width: 22px;
height: 22px;
background-size: 100% 100%;
z-index: 4;
box-shadow: none;
border: none;
transform: translateX(-3px);
}
</style>
<div className="drawing-item" ...>
// ....
<div className="control-point control-rotator" onMouseDown={onMouseDown.bind(this, 'rotate')}></div>
</div>
OK,剩下的就只须要在 transform 办法内加 计算角度的代码就 OK 了
function transform(direction, oriPos, e) {
// ... 省略
switch (direction.current) {
// ... 省略
// 拖拽挪动
case 'rotate':
// 先计算下元素的中心点, x,y 作为坐标原点
const x = style.width / 2 + style.left;
const y = style.height / 2 + style.top;
// 以后的鼠标坐标
const x1 = e.clientX;
const y1 = e.clientY;
// 使用高中的三角函数
style.transform = `rotate(${(Math.atan2((y1 - y), (x1 - x))) * (180 / Math.PI) - 90}deg)`;
break
}
}
测试下。
丑陋~,到这就实现了与元素的拖拽、缩放、旋转性能了。
最初,如果本文对你有任何帮忙的话,感激关注点个赞 ?