拖拉拽基础
2维向量类
创建一个2维向量vector2
,用来存放鼠标位置信息
export class Vector2 {
x: number;
y: number;
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
绑定事件
暂时只存两个值 x
和y
,后续再丰富内容,接下来就是在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;
}
}
效果
缩放元素
创建角标和关键点
<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
}
...
效果
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
}
···
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
效果
改变手势
在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;
}
效果
实际应用中很常见,不管是div画的框还是canvas画的框,拖拽的逻辑不变,无非就是调用canvas改变rect的方法,截图,绘制有效区,都可以使用
可以搞下来代码运行一下,另:vector2 封装的方法抄的 [threejs
](threejs.org/docs/index.…)
历史文章
# three.js 打造游戏小场景(拾取武器、领取任务、刷怪)