react 实现div缩放、旋转、拖拽的9个控制点

react 实现div缩放、旋转、拖拽的9个控制点

这段时间一个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>

效果图:


DraggedImage.png

拖拽

元素结构安排好后就来准备写功能了。 先来分析下拖拽缩放最主要的功能是什么,拖拽嘛!拖拽算是常见的简单功能了,需要绑定三个事件: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;

}

看下效果。


DraggedImage-1.png

可以拖着跑了,但是再拖一下, 哎,拖出界了


DraggedImage-2.png

范围限制还没加上呢, 加一下限制


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)

}

这下就拖不出去了。

上面的代码还有些小坑。我们定义的 三个方法onMouseMoveonMouseUponMouseDown 是直接通过 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)

}, [])

这就完成了对元素的拖拽缩放功能了。

2020-12-22 14.08.12.gif

旋转

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

}

}

测试下。

2020-12-22 14.52.36.gif

漂亮~ ,到这就完成了与元素的拖拽、缩放、旋转功能了 。

最后,如果本文对你有任何帮助的话,感谢关注点个赞 ?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354