pixi-interact-kit
Version:
PixiJS拖拽交互管理器 - 用于管理PixiJS场景中的拖拽、缩放、旋转操作
316 lines (294 loc) • 11.8 kB
JavaScript
import { Container, Graphics } from 'pixi.js'
/**
* Drager 拖拽交互管理器
* 用于管理 PixiJS 场景中的拖拽对象
* @example
* const drager = new Drager(app) //传入你的application实例
*/
export class Dragger {
constructor(app) {
this.targetMap = new Map() // 用于存储目标对象与拖动框的映射,双向关联的
this.app = app // 引用
this.draggerContainer = null // 用来快速定位,存放拖拽对象的容器
this.handleSize = 10 // 控制框的尺寸
this.lastMoveTime = 0; // 记录上次执行move的时间
this.moveInterval = 1000 / 60; // 限制为30FPS(毫秒)
this.init()
}
/**
* 设置各种属性
* @param {number} handleSize - 控制框的尺寸,默认10
* @param {number} fps - 移动时的帧率,默认60
* @example
* drager.setConfig(20,30) // 设置控制框尺寸为20,移动时的帧率为30
*/
setConfig(handleSize = 10, fps = 60) {
this.handleSize = handleSize // 控制框的尺寸
this.moveInterval = 1000 / fps; // fps
}
// 初始化方法
init() {
this.draggerContainer = new Container()
this.draggerContainer.label = 'draggerContainer '
this.draggerContainer.zIndex = 9999 // 确保在最上层
this.app.stage.addChild(this.draggerContainer)
this.app.ticker.add(this.update, this) // 传递方法引用和上下文
this.app.renderer.events.domElement.addEventListener('pointerup', this.interaction.loosen) // 监听全局的松开事件
}
/**
* 销毁方法
* 清理所有资源和事件监听器
* @example
* drager.destroy()
* drager = null // 之后将引用置空
*/
destroy() {
// 移除全局 pointerup 监听器
this.app.renderer.events.domElement.removeEventListener('pointerup', this.interaction.loosen);
// 移除 ticker 的 update 方法
this.app.ticker.remove(this.update, this);
// 销毁所有拖拽对象和容器
this.removeAll();
if (this.draggerContainer) {
this.app.stage.removeChild(this.draggerContainer);
this.draggerContainer.destroy({ children: true });
this.draggerContainer = null;
}
// 清空映射
this.targetMap.clear();
}
/**
* 添加拖拽对象到draggerContainer
* add(对象),最好是添加精灵或者图形
* @param {Object} obj - 要添加的对象
* @examplef
* // 传入对象本身
* drager.add(sprite)
* 本插件旋转只会更改角度,要绕轴心旋转请在添加对象时设置好锚点
* sprite.anchor.set(0.5) // 设置锚点为中心
*/
add(obj) {
const newContainer = new Container()
newContainer.label = obj.label
this.draggerContainer.addChild(newContainer)
this.targetMap.set(obj, newContainer)
this.targetMap.set(newContainer, obj) // 双向关联
this.ControlBoxGraphics(obj) // 添加控制框图形
newContainer.interactive = true
newContainer.on('pointerdown', this.interaction.click)
}
/**
* 移除拖拽对象,在draggerContainer 里面
* @param {Object|string} obj - 要移除的对象
* @example
* // 传入对象本身
* drager.remove(sprite)
*/
remove(obj) {
const target = this.targetMap.get(obj)
if (target) {
target.removeAllListeners(); // 移除所有事件监听器
this.targetMap.delete(obj)
this.targetMap.delete(target) // 删除双向关联
this.draggerContainer.removeChild(target) // 从容器中移除
target.destroy({ children: true }) // 彻底销毁
} else {
console.warn(`对于删除${obj}对应的元素的操作,`, '未找到对应的拖拽对象')
}
}
/**
* 移除所有拖拽对象
*/
removeAll() {
this.draggerContainer.children.forEach(child => {
child.removeAllListeners(); // 移除所有事件监听器
this.targetMap.delete(this.targetMap.get(child));
this.targetMap.delete(child); // 删除双向关联
child.destroy({ children: true }); // 彻底销毁
});
this.draggerContainer.removeChildren(); // 清空容器
}
/**
* 开关方法
* @param {Object|string} obj - 要切换的对象
* @example
* // 传入对象本身
* drager.toggle(sprite)
*/
toggle(obj) {
const target = this.targetMap.get(obj)
if (target) {
this.remove(obj)
} else {
this.add(obj)
}
}
Math = {
pointToPerpendicularLineDistance(center, ref, target) {
// 计算从点 target 到(经过center,并且和center和ref垂直的线)线的距离
const dirX = ref.x - center.x;
const dirY = ref.y - center.y;
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
if (dirLength === 0) {
return 0; // Handle zero vector case
}
const normDirX = dirX / dirLength;
const normDirY = dirY / dirLength;
const toTargetX = target.x - center.x;
const toTargetY = target.y - center.y;
const projection = toTargetX * normDirX + toTargetY * normDirY;
return Math.abs(projection);
}
}
// 交互方法
interaction = {
status_: 'none', // 交互状态,none,scaleXY,scaleX,scaleY,rotate
click: (e) => {
this.interaction.record(e)
switch (e.target.label) {
case 'UpperLeft':
this.interaction.status_ = 'scaleXY'
break
case 'UpperRight':
this.interaction.status_ = 'scaleXY'
break
case 'LowerLeft':
this.interaction.status_ = 'scaleXY'
break
case 'LowerRight':
this.interaction.status_ = 'scaleXY'
break
case 'Upper':
this.interaction.status_ = 'scaleY'
break
case 'Lower':
this.interaction.status_ = 'scaleY'
break
case 'Left':
this.interaction.status_ = 'scaleX'
break
case 'Right':
this.interaction.status_ = 'scaleX'
break
case 'Rotate':
this.interaction.status_ = 'rotate'
break
default:
this.interaction.status_ = 'none'
}
},
// 记录点击时的值
record: (e) => {
this.interaction.goalEvent = e.target.parent // 记录当前点击事件的目标的父亲
this.interaction.obj = this.targetMap.get(this.interaction.goalEvent); // 记录当前点击事件绑定的对象
this.interaction.globXY = this.interaction.obj.getGlobalPosition()
this.interaction.scaleStart = { x: this.interaction.obj.scale.x, y: this.interaction.obj.scale.y } // 记录初始缩放
this.interaction.rotationStart = this.interaction.obj.rotation; // 记录全局旋转角度
this.interaction.startClick = e.data.getLocalPosition(this.app.stage) // 记录初始点击位置
},
// 必须绑定到整个canvas画板,不然的话一旦脱出画框外就不生效就会看起来一卡一卡的
loosen: () => {
this.interaction.status_ = 'none' // 松开鼠标后重置状态
this.interaction.goalEvent = null // 清除目标事件
},
// 使用全局坐标系
move: () => {
if (this.interaction.status_ === 'none') return // 如果没有交互状态则不处理
const rect = this.app.canvas.getBoundingClientRect();
// 将PixiJS坐标转换为DOM坐标系
const mouseX = this.app.renderer.events.pointer.x - rect.left;
const mouseY = this.app.renderer.events.pointer.y - rect.top;
const globXY = this.interaction.globXY
const start = this.interaction.startClick;
switch (this.interaction.status_) {
case 'scaleXY': // 缩放
const startDist = Math.hypot(start.x - globXY.x, start.y - globXY.y);
const currentDist = Math.hypot(mouseX - globXY.x, mouseY - globXY.y);
const scaleRatio = currentDist / startDist;
this.interaction.obj.scale.x = this.interaction.scaleStart.x * scaleRatio; // 逆向缩放
this.interaction.obj.scale.y = this.interaction.scaleStart.y * scaleRatio; // 逆向缩放
break
case 'scaleX': // 水平缩放
const startDistX = this.Math.pointToPerpendicularLineDistance(globXY, start, start)
const currentDistX = this.Math.pointToPerpendicularLineDistance(globXY, start, { x: mouseX, y: mouseY })
const scaleRatioX = currentDistX / startDistX;
this.interaction.obj.scale.x = this.interaction.scaleStart.x * scaleRatioX;
break
case 'scaleY': // 垂直缩放
const startDistY = this.Math.pointToPerpendicularLineDistance(globXY, start, start)
const currentDistY = this.Math.pointToPerpendicularLineDistance(globXY, start, { x: mouseX, y: mouseY })
const scaleRatioY = currentDistY / startDistY;
this.interaction.obj.scale.y = this.interaction.scaleStart.y * scaleRatioY;
break
case 'rotate': // 旋转
// 计算起始角度和当前角度
const startAngle = Math.atan2(start.y - globXY.y, start.x - globXY.x);
const currentAngle = Math.atan2(mouseY - globXY.y, mouseX - globXY.x);
const deltaAngle = currentAngle - startAngle;
this.interaction.obj.rotation = this.interaction.rotationStart + deltaAngle;
break
}
},
}
/**
* 添加控制框图形到draggerContainer
* @param {Object} obj - 要添加控制框的对象
*/
ControlBoxGraphics(obj) {
const objWidth = obj.width
const objHeight = obj.height
const handleSize = this.handleSize // 控制框的尺寸
function CreateTemplateSquare(x, y) {
const square = new Graphics()
square
.rect(0, 0, handleSize, handleSize) // 绘制小方块
.stroke({ width: 2, color: 0x000000 }) // 设置边框宽度和颜色(2像素黑色)
.fill({ color: 0xffffff }) // 设置填充颜色为白色
square.x = x - handleSize / 2 // 居中对齐
square.y = y - handleSize / 2
square.interactive = true // 使小方块可交互
return square
}
const UpperLeft = CreateTemplateSquare(0, 0)
UpperLeft.label = 'UpperLeft'
const UpperRight = CreateTemplateSquare(objWidth, 0)
UpperRight.label = 'UpperRight'
const LowerLeft = CreateTemplateSquare(0, objHeight)
LowerLeft.label = 'LowerLeft'
const LowerRight = CreateTemplateSquare(objWidth, objHeight)
LowerRight.label = 'LowerRight'
const Upper = CreateTemplateSquare(objWidth / 2, 0)
Upper.label = 'Upper'
const Lower = CreateTemplateSquare(objWidth / 2, objHeight)
Lower.label = 'Lower'
const Left = CreateTemplateSquare(0, objHeight / 2)
Left.label = 'Left'
const Right = CreateTemplateSquare(objWidth, objHeight / 2)
Right.label = 'Right'
const Rotate = CreateTemplateSquare(objWidth * 1.2, objHeight / 2)
Rotate.label = 'Rotate'
this.targetMap.get(obj).addChild(UpperLeft, UpperRight, LowerLeft, LowerRight,
Upper, Lower, Left, Right, Rotate)
}
// 更新方法
update() {
this.draggerContainer.children.forEach((child) => {
const target = this.targetMap.get(child);
const wt = target.worldTransform;
child.setFromMatrix(wt); // 同步变换矩阵
child.children.forEach((handle) => { // 逆向缩放控制框
const parentScaleX = child.scale.x;
const parentScaleY = child.scale.y;
if (parentScaleX !== 0 && parentScaleY !== 0) {
handle.scale.set(1 / parentScaleX, 1 / parentScaleY);
}
});
// 帧率限制:只有到达指定间隔才执行 move
const now = performance.now();
if (now - this.lastMoveTime > this.moveInterval) {
this.interaction.move();
this.lastMoveTime = now;
}
});
}
}