這段時間一個canvas 庫所實現的元素拖拽控制,以爲很不錯。因而本身用js + div 來實現一個。用了react 框架,練練手。
在被控制的元素的四條邊和四個角添加8個控制點控制點。拖拽控制點時判斷拖拽的方向,計算偏移量。修改元素的top、left、width、height。
旋轉功能是經過三角函數計算鼠標拖動後的角度。動態修改元素的rotatecss
想要對元素進行控制。 咱們先定義一個畫板,規定元素只能在指定的範圍內變化。
而後在畫板內插入一個被控制的 div 元素,就定義爲drawing-item
類名吧。drawing-item
須要絕對定位於畫板
以及八個方向的控制點。這是最簡單的結構了html
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;
給他們都加上樣式react
<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>
效果圖:
canvas
元素結構安排好後就來準備寫功能了。 先來分析下拖拽縮放最主要的功能是什麼,拖拽嘛!拖拽算是常見的簡單功能了,須要綁定三個事件: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
監聽鼠標按下的事件。svg
// 鼠標被按下 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; }
看下效果。
this
能夠拖着跑了,可是再拖一下, 哎,拖出界了
範圍限制還沒加上呢, 加一下限制
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 } }
測試下。
漂亮~ ,到這就完成了與元素的拖拽、縮放、旋轉功能了 。
最後,若是本文對你有任何幫助的話,感謝關注點個贊 ?