Javascript基础之鼠标拖拉拽

673 阅读5分钟

拖拉拽基础

2维向量类

创建一个2维向量vector2,用来存放鼠标位置信息

export class Vector2 {
    x: number;
    y: number;
    constructor(x = 0, y = 0) { 
        this.x = x;
        this.y = y;
    }
}

绑定事件

暂时只存两个值 xy,后续再丰富内容,接下来就是在html中创建一个div,再监听window上的鼠标事件,

const box = document.querySelector('#box')

const mouseDown = (e: MouseEvent) => {
    console.log('按下', e);

}
const mouseUp = (e: MouseEvent) => {
    console.log('抬起', e);
}

const mouseMove = (e: MouseEvent) => {
    console.log('移动', e?.target)
}

window.addEventListener('mousemove', mouseMove)
window.addEventListener('mousedown', mouseDown)
window.addEventListener('mouseup', mouseUp)

移动元素的思路是 按下开始拖拽计算位置修改元素样式抬起,结束本次拖拽, 所以在移动之前添加一个flag,默认为false,按下时置为true,抬起时再置为false。

...

let flag = false
const mouseDown = (e: MouseEvent) => {
    flag = true
}
const mouseUp = (e: MouseEvent) => {
    flag = false
}

const mouseMove = (e: MouseEvent) => {
    if (flag) {
        console.log(e.clientX, e.clientY);
    }
}

...

绑定并修改元素

既然是要拖拽box元素,那就把mousedown方法挂载到box上,鼠标在box上按下,flag置为true

...
box.onmousedown = mouseDown
...

mouseMove 方法改造一下,获取到鼠标的x和y值,并存储为一个二维向量Vector2

box新的位置计算方法mouseposition - lastmouseposition + boxposition

mouseposition 为鼠标当前位置,lastmouseposition为上一个位置,boxposition为盒子位置,boxPadding是鼠标到盒子左上角的位置

所以我们在移动时候,需要获取到这三个变量,并改造一下mouseMove方法

...
if (flag) {
     // 盒子的位置
    const { offsetLeft: boxX, offsetTop: boxY } = box
    const boxPosition = new Vector2(boxX, boxY)
    // 鼠标位置
    const { clientX, clientY } = e;
    const mousePosition = new Vector2(clientX, clientY);
    // 鼠标与上一次记录的鼠标位置的差值
    let offset = new Vector2();
    offset = offset.subVectors(mousePosition, lastMousePosition);
    // 通过差值与盒子的位置,计算出盒子新的位置
    const LT = new Vector2().addVectors(offset, boxPosition)

    // 将当前鼠标位置记录一下
    lastMousePosition = mousePosition.clone();
    // 修改位置
    box.style.left = LT.x + 'px'
    box.style.top = LT.y + 'px'
...

扩展Vector2

以上的运算都是基于二维向量的,所以,扩展一下vector.js新增几个方法

export class Vector2 {
    x: number;
    y: number;
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
    set(x: Vector2['x'], y: Vector2['y']) {
        this.x = x;
        this.y = y
    }
    // 拷贝
    copy(v2: Vector2) {
        this.x = v2.x
        this.y = v2.y
    }
    // 克隆
    clone() {
        return new Vector2(this.x, this.y)
    }
    // 向量相减 返回新的向量
    subVectors(a: Vector2, b: Vector2) {
        this.x = a.x - b.x;
        this.y = a.y - b.y;

        return this;
    }
    向量相加 返回新的向量
    addVectors(a: Vector2, b: Vector2) {
        this.x = a.x + b.x;
        this.y = a.y + b.y;

        return this;
    }
}

效果

2023-05-24 16.02.35.gif

缩放元素

创建角标和关键点

image.png

 <div id="box" active="box">
    <div class="point lt" active="lt"></div>
    <div class="point rt" active="rt"></div>
    <div class="point lb" active="lb"></div>
    <div class="point rb" active="rb"></div>
</div>

active属性是为了区分每个区块的作用

@pointW: 12px;
@all: 100%;
.point {
    width: @pointW;
    height: @pointW;
    background-color: aquamarine;
    position: absolute;

    &.lt {
        top: calc(0px - @pointW / 2);
        left: calc(0px - @pointW / 2);
    }

    &.rt {
        top: calc(0px - @pointW / 2);
        left: calc(@all - @pointW / 2);
    }

    &.lb {
        top: calc(@all - @pointW / 2);
        left: calc(0px - @pointW / 2);
    }

    &.rb {
        top: calc(@all - @pointW / 2);
        left: calc(@all - @pointW / 2);
    }

}

区分点击的区域

现在区块变多了,而且每个区块的动作都不一样,所以在mousedown方法记录一下当前点击的是什么区块,

外部记录一下type

    let areaType = ''

mousedown添加以下代码

···
 try {
    areaType = e.target.getAttribute('active')
    console.log(areaType);

} catch (error) {
    console.error(error)
}
···

因为有兼容性问题,我这里没做处理,直接捕获错误,项目开发,可不能这么简单粗暴。

现在按下的时候记录区块,抬起时将区块改为''空字符串

接下来要做的就是根据不同的区块,设置不同的动作,比如rb区块,就是右下角,作用是改变尺寸,lt是左上角,改变尺寸和位置

前文计算出来offset的向量,为当前鼠标位置和上一次鼠标位置的差值,以右下角为例,boxPosition+boxSize+offset最终得到具体尺寸,所以现在定义一个方法,接受offset参数,再根据不同的作用区域进行不同的处理

所以接下来将mouseMove继续改造一下,将之前写的改变box位置的代码单独抽离出来

改造mouseMove方法

···
 // 鼠标位置
const { clientX, clientY } = e;
const mousePosition = new Vector2(clientX, clientY);
// 鼠标与上一次记录的鼠标位置的差值
let offset = new Vector2();
mouse.copy(mousePosition)
offset = offset.subVectors(mousePosition, lastMousePosition);
changeBox(offset)
// 将当前鼠标位置记录一下
lastMousePosition = mouse.clone();
···

changeBox方法是根据不同的areaType做的不同的动作

const changeBox = (offset: Vector2) => {
    // 盒子的位置
    const { offsetLeft: boxX, offsetTop: boxY, clientWidth, clientHeight } = box
    const boxPosition = new Vector2(boxX, boxY)
    const boxSize = new Vector2(clientWidth, clientHeight)
    const styleInfo: BoxStyleInfo = {}
    if (areaType === 'box') {
        // 通过差值与盒子的位置,计算出盒子新的位置
        const LT = new Vector2().addVectors(offset, boxPosition)
        styleInfo.left = LT.x
        styleInfo.top = LT.y
    }
    changeBoxStyle(styleInfo)
}

changeBoxStyle方法是修改box样式的

interface BoxStyleInfo {
    width?: number;
    height?: number;
    left?: number;
    top?: number;
}
const changeBoxStyle = (info: BoxStyleInfo) => {
    if (info.left !== undefined) box.style.left = info.left + 'px'
    if (info.top !== undefined) box.style.top = info.top + 'px'
    if (info.width !== undefined) box.style.width = info.width + 'px'
    if (info.height !== undefined) box.style.height = info.height + 'px'
}

rb动作

同样的方法,写一下rb的动作

...
else if (areaType === 'rb') {
    const size = new Vector2().addVectors(offset, boxSize)
    styleInfo.width = size.x
    styleInfo.height = size.y
}
...

效果

2023-05-24 17.27.21.gif

lt动作

除了右下角不需要改变box的位置,其他的作用区域都需要改变位置,所以其他的区域相对比较复杂,以lt为例 mouse的位置就是box的位置,mouse - offset就是盒子的尺寸,尺寸和位置需要同步修改,拖拽左上角后,右下角的尺寸是不变的

···
else if (areaType === 'lt') {
    const pos = mouse.clone()
    styleInfo.left = pos.x
    styleInfo.top = pos.y
    const size = boxSize.clone().sub(offset)
    styleInfo.width = size.x
    styleInfo.height = size.y
}
···

lt.gif

rt动作

右上角拖拽时,改变的是top,width,height,所以只对这几个属性进行改变即可

const top = boxPosition.y + offset.y
styleInfo.top = top

const width = boxSize.x + offset.x
styleInfo.width = width

const height = boxSize.y - offset.y
styleInfo.height = height

lb动作

左下角改变的是left, height, width

const height = boxSize.y + offset.y
styleInfo.height = height

const width = boxSize.x - offset.x
styleInfo.width = width
        
styleInfo.left = mouse.x

效果

2023-05-24 18.11.37.gif

改变手势

areaType改变的时候 调用一下changeCursor方法就可以,或者直接写一个监听函数

enum MouseType {
    NWSE = 'nwse-resize',
    NESW = 'nesw-resize',
    EW = 'ew-resize',
    NS = 'ns-resize',
    MOVE = 'move',
    DEFAULT = 'auto',
}

const MouseTypeDisplay = {
    'lt': MouseType.NWSE,
    'lb': MouseType.NESW,
    'rt': MouseType.NESW,
    'rb': MouseType.NWSE,
    'box': MouseType.MOVE,
    default: MouseType.DEFAULT,
};

const changeCursor = () => {
    const cursor = MouseTypeDisplay[areaType || 'default'];
    document.body.style.cursor = cursor;
}

效果

2023-05-24 18.25.45.gif

实际应用中很常见,不管是div画的框还是canvas画的框,拖拽的逻辑不变,无非就是调用canvas改变rect的方法,截图,绘制有效区,都可以使用

代码地址

可以搞下来代码运行一下,另:vector2 封装的方法抄的 [threejs](threejs.org/docs/index.…)

历史文章

# Javascript基础之写一个好玩的点击效果

# Javascript基础之鼠标拖拉拽

# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)

# threejs 打造 world.ipanda.com 同款3D首页

# three.js——物理引擎

# three.js——镜头跟踪

# threejs 笔记 03 —— 轨道控制器