pixijs 实现了一个具有拖放、旋转、缩放、多选和编辑功能的记事本。
2023.12
更新:1. 把项目的pixijs升级到了v7,分支是chore/update-pixijs-7
,现在不再在renderer上监听事件,而是在stage上监听事件。2. 由于不再在renderer上监听事件了,所以让stage代替以前的renderer,并加入了一个rootContainer,代替以前的stage,现在的缩放、平移操作,不再是操作stage了,而是操作rootContainer,stage处于不变的状态。3. 解决了一些小bug
1.前言
1.1 编写本文的目的
本文旨在分享一些前端常用的拖拽、旋转、缩放、框选等操作的实现方法,重心会放在核心思想上(当然也会有代码实现),虽然项目采用pixijs实现,但是核心思想并不仅仅局限于pixijs,读者了解了核心思想之后,也可以用其他方式来实现类似的效果。
1.2 项目介绍
本项目是一个基于pixijs的记事板,每一个重要功能的实现,都会有对应的核心思想阐述以及代码实现,整体代码在这里 -> 代码
整体效果如下:
拖拽&缩放画布
旋转&编辑节点
框选&旋转多个节点
1.3前置知识
- 了解pixijs的API以及思想(stage、Container等), 可以看一下pixijs的文档 -> pixijs官网,或者可以看一下这篇文章 -> pixijs教程
- 有基本的线性代数知识,知道矩阵相乘的几何意义,知道旋转、缩放、平移对应的变换矩阵是怎样的。可以去b站看一下3Blue1Brown的这个视频 ->线性代数的本质
OK!我们可以开始了!
2.开始
2.1 随机生成Text
2.1.1生成Text
function createText() {
const fontSize = 14;
const text = new Text(
'床前明月光\n疑是地上霜123\n举头望明月\n低头思故乡asdasd',
{
fill: 0x4ca486,
fontFamily: `OpenSans, Arial, sans-serif, "Noto Sans Hebrew", "Noto Sans", "Noto Sans JP", "Noto Sans KR"`,
fontSize,
lineHeight: 1.2 * fontSize,
}
);
text.cursor = 'pointer';
text.interactive = true;
return text;
}
2.1.2 将生成的Text添加到stage上,并指定一个随机位置
for (let i = 0; i < 500; i++) {
const randomX = Math.random();
const randomY = Math.random();
const text = createText();
pixiApp.stage.addChild(text);
text.position.set(800 * 10 * randomX, 600 * 10 * randomY);
}
接下来我们就可以在屏幕上看到如下的效果:
2.2 拖拽单个Text对象
2.2.1 核心思想以及代码实现
我们会在pixi的renderer上监听pointerdown、pointermove、pointerup这3个事件(pixijs的事件有自己的一套叫法,这三个事件分别对应浏览器标准事件的mousedown、mousemove、mouseup)。
- 触发pointerdown时,我们会判断鼠标是否点到了画布上的Text,如果点到了Text,那么我们会用一个变量来标记当前点到了Text,并记录下鼠标down的位置信息:
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
mouseDownPoint = event.target.position
curDragTarget = event.target;
curDragTargetOriginalPos = event.target.position;
}
);
- 触发pointermove时,我们会根据第2步中设置的变量来判断pointerdown时是否点到了Text,如果是的话,则开始移动Text,具体做法是:计算出当前鼠标的位置和pointerdown时的鼠标位置的差值,然后设置Text的position:
pixiApp.renderer.plugins.interaction.on(
'pointermove',
(event: InteractionEvent) => {
const globalPos = event.data.global;
if (curDragTarget) {
// 拖拽起始点位置在stage上的坐标
const startPoint = pixiApp.stage.localTransform.applyInverse(mouseDownPoint);
// 鼠标当前位置在stage上的坐标
const curPoint = pixiApp.stage.localTransform.applyInverse(globalPos);
const dx = curPoint.x - startPoint.x;
const dy = curPoint.y - startPoint.y;
const { x: originalX, y: originalY } = curDragTargetOriginalPos;
curDragTarget.position.set(originalX + dx, originalY + dy);
}
}
);
- 触发pointerup时,将第2步设置的标记变量置为空值:
pixiApp.renderer.plugins.interaction.on(
'pointerup',
(event: InteractionEvent) => {
curDragTarget = undefined;
}
);
2.2.2 最终效果:
2.3 坐标映射
在上面的2.2.1第2步中,使用了pixiApp.stage.localTransform.applyInverse这个函数,将renderer上的坐标映射成了stage上的坐标,这里讲一下原理。
2.3.1 几个概念
- renderer
可以理解为canvas视窗,是固定不变的,从event对象上拿到的event.global,是事件在renderer上面的坐标,我们可以理解为全局坐标或global坐标,有点类似DOM Event的event.clientX/Y。
- stage
pixijs有一个Container的概念,用户可以自己创建Container,将一些元素放进去,这样的话,放进去的元素就会随着Container的移动而一起移动,相当于形成了一个‘组’,pixijs的stage也是一个Container,用户需要将内容放到stage上,才会被pixijs渲染出来。
2.3.2 不变的stage以及会变动的stage
上面说了,renderer是不会动的,这里说的动包括(平移、旋转、缩放),而stage是会动的,事实上,我们要实现对画布的缩放、平移,正是通过缩放、平移stage来实现的。
一开始,我们没有对stage进行缩放,这个时候,stage的原点和renderer的原点重合,鼠标落下的点在renderer上的坐标等于在stage上的坐标:
如果我们了平移了stage:
这样的话,鼠标落下点在renderer上的坐标和在stage上的坐标就不一样了
我们的Text对象是放在stage上的,所以,必须要将global坐标转换成stage坐标,才能正确的让Text对象在stage上移动
2.3.3 逆变换(逆矩阵)
上面说了,我们需要将global坐标映射成stage坐标,那么到底该怎么做呢?
其实,对stage进行平移或者缩放,其实就是进行了一个线性变换,也就是左乘了一个变换矩阵,这个变换矩阵,我们可以通过pixiApp.stage.localTransform拿到,我们可以输出看一下这个矩阵的样子:
这是一个齐次坐标,tx和ty是stage的水平移动量和垂直移动量
左乘这个变换矩阵之后,stage上的所有点都会被映射到另一个点,所以stage看起来就被缩放、平移了。现在,鼠标落下的点的global坐标是[a,b],a、b是已知数;鼠标落下的点的stage坐标是[x,y],x、y是未知数,令M=变换矩阵,可以得知,以下的等式是成立的:
进一步推出:
也就是说,我们让global坐标左乘stage的变换矩阵的逆矩阵就得到了stage坐标,pixiApp.stage.localTransform.applyInverse函数就相当于:让某个点左乘pixiApp.stage.localTransform这个矩阵的逆矩阵,所以,这也解释了为什么我们用下面这种方式进行坐标的映射:
// 拖拽起始点位置在stage上的坐标
const startPoint = pixiApp.stage.localTransform.applyInverse(mouseDownPoint);
// 鼠标当前位置在stage上的坐标
const curPoint = pixiApp.stage.localTransform.applyInverse(globalPos);
2.4 拖拽画布(stage)
其实理解了上面的拖拽Text对象之后,拖拽画布就不在话下了
2.4.1 核心思想
我们依然会在renderer上监听pointerdown、pointermove、pointerup,在触发pointerdown的时候,如果enent对象的target属性为空,说明点到了画布的空白区域,这个时候,如果再触发pointermove,就会开始移动画布了。
2.4.2 代码实现
- 触发pointerdown时,用touchBlank来标记点中了画布的空白区域:
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
const globalPos = event.data.global;
// 记录下stage原来的位置
stageOriginalPos = copyPoint(pixiApp.stage.position);
// 记录下mouse down的位置
mouseDownPoint = copyPoint(globalPos);
if (!event.target) {
// 点到了画布的空白位置
touchBlank = true;
}
}
);
- 触发pointermove时,给stage设置新的position:
pixiApp.renderer.plugins.interaction.on(
'pointermove',
(event: InteractionEvent) => {
const globalPos = event.data.global;
if (touchBlank) {
// 拖拽画布
const dx = globalPos.x - mouseDownPoint.x;
const dy = globalPos.y - mouseDownPoint.y;
pixiApp.stage.position.set(
stageOriginalPos.x + dx,
stageOriginalPos.y + dy
);
}
}
);
这里就不需要将global坐标转成stage坐标了,因为stage是相对renderer定位的,所以直接用global坐标就好
- 触发pointerup时,将touchBlank变量设置为false:
pixiApp.renderer.plugins.interaction.on(
'pointerup',
(event: InteractionEvent) => {
touchBlank = false;
}
);
2.4.3 最终效果
2.5 缩放画布(stage)
2.5.1 几个注意点
- 画布是x/y等比例缩放的
- 我们不会旋转画布
- 要以鼠标的位置为锚点进行画布的缩放
综合1、2点,我们可以很容易写出stage的变换矩阵,假设我们将画布放大了2倍,并且向右平移了100px,向下平移了200px,那么变换矩阵是Matrix(2, 0, 0, 2, 100, 200)。
这里的Matrix和CSS3的transform的matrix属性一样,也是6个参数
2.5.2 核心思想
先来思考一个问题,我们现在要对stage做一些变换,变换前的stage的状态是:放大了1.2倍,向右平移了100px,向下平移了200px,如下:
我们很容易写出变换前的stage的变换矩阵:Matrix(1.2, 0, 0, 1.2, 100, 200)
我们对stage做的变换是:以鼠标所在的点[a,b]为锚点,缩小成最初的大小的0.8倍,下图中,蓝色的框框代表缩小后的stage:
这个时候,stage的变换矩阵是什么呢?
首先,前4位很容易写出来,因为现在的缩放倍数是0.8,所以前4位我们是知道的:Matrix(0.8, 0, 0, 0.8, ?, ?),那么后面2位该怎么得到呢?我们先来求后面2位的第一位,我们给它取个名字,就叫x吧,另外,我加了几根辅助线,方便理解:
先来捋一下:红色框框是变换前的stage;蓝色框框是变换后的stage;x(蓝色虚线的长度)就是我们要求的那个x,它是变换后的stage的左边框到y轴的距离;p(绿色虚线的长度)是mouse point到变换前的stage的左边框的距离;q(橙色虚线)是mouse point到变换后的stage的左边框的距离
显然,这个等式是成立的: x = 100 + (p - q)
p就是mouse point在stage(before)上的横坐标,stage(before)的变换矩阵是已知的,通过应用它的逆矩阵,我们可以得到p的值,但是得到的值是相对于stage的值,并不是相对于renderer的值,我们现在要求的x是相对于renderer的值,所以p也要要转化成相对于renderer的值,具体做法是我们需要将其乘以1.2,得到的就是相对于renderer的值,还记得吗,stage(before)是放大了1.2倍的。
现在我们求出了p,只剩下q了,我们的变换是以mouse point为锚点的变换,所以p和q是成比例的,可以得知这个等式是成立的:p/q = 1.2/0.8,这样的话,q也求出来了,那么x的值就求出来了,我们得到了stage(after)的横向移动的距离,同理,可以得到stage(after)的纵向移动距离。
综上所述,我们求出了变换后的stage的变换矩阵。
2.5.3 代码实现
获取当前的缩放比例
getZoom(): number {
// stage是宽高等比例缩放的,所以取x或者取y是一样的
return pixiApp.stage.scale.x;
}
在canvas元素上监听鼠标滚轮事件
pixiApp.view.addEventListener('wheel', (event) => {
// 因为画布是充满视窗的,所以clientX等于mouse point在renderer上的x坐标
const globalPos = new Point(event.clientX, event.clientY);
const delta = event.deltaY;
const oldZoom = getZoom();
let newZoom = zoom * 0.999 ** delta;
applyZoom(oldZoom, newZoom, globalPos);
});
applyZoom函数
applyZoom = (oldZoom: number, newZoom: number, pointerGlobalPos: Point) => {
const oldStageMatrix = pixiApp.stage.localTransform.clone();
const oldStagePos = oldStageMatrix.applyInverse(pointerGlobalPos);
const dx = oldStagePos.x * oldZoom - oldStagePos.x * newZoom;
const dy = oldStagePos.y * oldZoom - oldStagePos.y * newZoom;
pixiApp.stage.setTransform(
pixiApp.stage.position.x + dx,
pixiApp.stage.position.y + dy,
newZoom,
newZoom,
0,
0,
0,
0,
0
);
}
2.5.4 最终效果
2.6 给点击的对象添加一个选中效果
2.6.1 核心思想以及代码实现
- 当我们点击了一个Text对象之后,将该对象设置为activeObj,然后获取该Text对象的4个顶点,然后根据这4个顶点画一个矩形,添加到stage上
设置activeObj:
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
activeObject = event.target;
addActiveTargetBorder()
}
);
获取Text的4个顶点:
getObjectStageBound(obj: DisplayObject) {
const localBounds = obj.getLocalBounds();
const tl = new Point(localBounds.x, localBounds.y);
const tr = new Point(localBounds.x + localBounds.width, localBounds.y);
const br = new Point(
localBounds.x + localBounds.width,
localBounds.y + localBounds.height
);
const bl = new Point(localBounds.x, localBounds.y + localBounds.height);
const localPoints = [tl, tr, br, bl];
return localPoints.map((p) => obj.localTransform.apply(p));
}
根据4个顶点画一个矩形,并添加到stage上:
addActiveTargetBorder() {
const bound = getObjectStageBound(activeObject);
const border = new Graphics();
border.lineStyle(3 / getZoom(), 0x5b97fc);
border.drawPolygon(bound);
pixiApp.stage.addChild(border);
}
- 现在,选中效果也就是border已经添加到了stage上,但是,我们在对Text对象进行拖拽时,Text对象相对于stage的位置就改变了,这个时候,border也要随着Text对象的位置的改变而改变自己的位置
在添加border后,要将其引用记录下来,我们稍微修改一下addActiveTargetBorder函数:
addActiveTargetBorder() {
const bound = getObjectStageBound(activeObject);
const border = new Graphics();
border.lineStyle(3 / getZoom(), 0x5b97fc);
border.drawPolygon(bound);
pixiApp.stage.addChild(border);
activeObjBorder = border;
}
不断更新border,让其跟随activeObj移动
updateActiveTargetBorder = () => {
if (activeObject && activeObjBorder) {
const bound = getObjectStageBound(activeObject);
activeObjBorder.clear(); // 清除border
activeObjBorder.lineStyle(3 / getZoom(), 0x5b97fc);
activeObjBorder.drawPolygon(bound); // 重新画draw border
}
};
updateActiveTargetBorder函数要添加到ticker列表中,这样的话,pixi就会不断执行这个函数
pixiApp.ticker.add(updateActiveTargetBorder);
2.6.2 最终效果
2.7 旋转Text对象
2.7.1 添加控制点
在实现旋转之前,先给activeObj添加一个控制点
控制点对象继承于pixijs的Graphics对象:
class ControlPoint extends Graphics {
controlTarget: DisplayObject;
constructor(target: DisplayObject) {
super();
this.controlTarget = target;
}
}
添加activeObj的border时,一并添加控制点:
addActiveTargetControlPoint(activeObj: DisplayObject) {
const controlPoint = new ControlPoint(activeObj);
activeObjControlPoint = controlPoint;
controlPoint.interactive = true;
controlPoint.cursor = 'pointer';
pixiApp.stage.addChild(controlPoint);
controlPoint.lineStyle(2 / getZoom(), 0xc66965);
const radius = 5 / getZoom();
controlPoint.beginFill(0xffffff);
controlPoint.drawCircle(0, 0, radius);
controlPoint.endFill();
const bound = getObjectStageBound(activeObject!);
const [tl, tr] = bound;
controlPoint.position.set((tl.x + tr.x) / 2, (tl.y + tr.y) / 2);
}
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
activeObject = event.target;
addActiveTargetBorder()
addActiveTargetControlPoint(activeObject)
}
);
更新activeObj的border时,一并更新控制点的位置信息
updateActiveTargetControlPoint = () => {
if (activeObject && activeObjControlPoint) {
activeObjControlPoint.clear();
activeObjControlPoint.lineStyle(2 / getZoom(), 0xc66965);
const radius = 5 / getZoom();
activeObjControlPoint.beginFill(0xffffff);
activeObjControlPoint.drawCircle(0, 0, radius);
activeObjControlPoint.endFill();
const bound = getObjectStageBound(activeObject);
const [tl, tr] = bound;
activeObjControlPoint.position.set(
(tl.x + tr.x) / 2,
(tl.y + tr.y) / 2
);
}
};
pixiApp.ticker.add(updateActiveTargetControlPoint);
控制点的效果如下:
2.7.2拖拽控制点以旋转Text对象
2.7.2.1核心思想
先思考一个问题,现在有2个向量:向量v1和向量v2,我们要对向量v1做什么样的操作,才能让v1向量和v2向量共线呢?
想必各位已经有了答案,就是旋转一个角度θ就行了,这个角度θ可以这样求:根据公式v1⋅v2=||v1||||v2||cosθ,可以得到:θ=acos(v1⋅v2/||v1||||v2||),这样我们就求出了这个角度。
求出了这个角度还不够,还有个问题就是:v1向量应该加这个角度,还是减这个角度,才能达到与v2共线呢?
这里可以通过两个向量的叉积来判断:v1和v2的叉积为正,那么v1在v2的顺时针方向,但是有个问题,现在我们的坐标系跟数学里的那个坐标系是不同的,数学里的那个坐标系,y轴是朝上的,现在我们的坐标系的y轴是朝下的,所以真正的结论应该是:v1和v2的叉积为正,那么v1在v2的逆时针方向。
2.7.2.2代码实现
鼠标点到了控制点时,我们要记录一些信息,为拖拽做准备:
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
const globalPos = event.data.global;
mouseDownPoint = new Point(globalPos.x, globalPos.y);
if (event.target instanceof ControlPoint) {
rotatingActiveObject = true;
originalAngle = event.target.controlTarget.angle;
const bound = getObjectStageBound(activeObject);
const [tl, _tr, br, _bl] = bound;
originalCenter = new Point((tl.x + br.x) / 2, (tl.y + br.y) / 2);
}
}
);
拖拽控制点时,以Text对象中心点为锚点旋转Text对象:
setActiveObjAngle(angle: number) {
activeObject.angle = angle;
const newBound = getObjectStageBound(activeObject);
const [tl, _tr, br, _bl] = newBound;
const newCenter = new Point((tl.x + br.x) / 2, (tl.y + br.y) / 2);
const dx = newCenter.x - originalCenter.x;
const dy = newCenter.y - originalCenter.y;
activeObject.position.set(
activeObject.position.x - dx,
activeObject.position.y - dy
);
}
pixiApp.renderer.plugins.interaction.on(
'pointermove',
(event: InteractionEvent) => {
const globalPos = event.data.global;
if (rotatingActiveObject) {
// 拖拽控制点
const pointerDownStagePos =
pixiApp.stage.localTransform.applyInverse(mouseDownPoint);
const curPointerStagePos =
pixiApp.stage.localTransform.applyInverse(globalPos);
const v1 = new Point(
pointerDownStagePos.x - originalCenter.x,
pointerDownStagePos.y - originalCenter.y
);
const v2 = new Point(
curPointerStagePos.x - originalCenter.x,
curPointerStagePos.y - originalCenter.y
);
// 计算v1向量和v2向量的夹角
const v1mv2 = v1.x * v2.x + v1.y * v2.y; // v1和v2的点积
const modV1V2 =
Math.sqrt(Math.pow(v1.x, 2) + Math.pow(v1.y, 2)) *
Math.sqrt(Math.pow(v2.x, 2) + Math.pow(v2.y, 2));
const cos = v1mv2 / modV1V2; // v1向量和v2向量的夹角的cos值
const angle = (180 * Math.acos(cos)) / Math.PI;
// 判断应该顺时针 旋转还是逆时针旋转(这里注意:坐标系是倒过来的)
const v2xv1 = v2.x * v1.y - v2.y * v1.x; // v1向量和v2向量的叉积
const dAngle = v2xv1 > 0 ? -angle : angle; // 叉积为正说明v1向量在v2向量的顺时针方向
setActiveObjAngle((originalAngle + dAngle) % 360);
}
}
);
2.7.3 最终效果
2.8 多选
2.8.1 实现一个类似Windows桌面上画矩形框的效果
2.8.1.1 核心思想
当触发pointerdown事件时,我们会在stage上添加一个Graphics对象,并记录下pointerdown时鼠标的坐标,作为矩形框的左上角(或者右下角);接下来,随着pointermove事件的触发,我们会不断地重新绘制这个Graphics,以达到矩形框的右下角(或者左上角)的顶点追随鼠标的效果;最后,触发pointerup事件时,将这个Graphics对象从stage上移除掉。
2.8.1.2 代码实现
SelectorTool类:
class SelectorTool {
private pixiApp: Application;
private p1: Point;
private p2: Point;
private rect: Graphics;
constructor(pixiApp: Application, startPoint: Point) {
this.pixiApp = pixiApp;
this.p1 = startPoint;
this.p2 = startPoint;
this.rect = new Graphics();
this.pixiApp.stage.addChild(this.rect);
}
getRect(): Box {
const xMin = Math.min(this.p1.x, this.p2.x);
const yMin = Math.min(this.p1.y, this.p2.y);
const xMax = Math.max(this.p1.x, this.p2.x);
const yMax = Math.max(this.p1.y, this.p2.y);
return {
tl: new Point(xMin, yMin),
br: new Point(xMax, yMax),
};
}
drawRect() {
const rect = this.getRect();
this.rect.clear();
this.rect.beginFill(0x8888ff, 0.5);
this.rect.drawRect(
rect.tl.x,
rect.tl.y,
rect.br.x - rect.tl.x,
rect.br.y - rect.tl.y
);
this.rect.endFill();
}
move(point: Point) {
this.p2 = point;
this.drawRect();
}
end() {
this.pixiApp.stage.removeChild(this.rect);
}
}
pointerdown时new一个SelectorTool对象:
pixiApp.renderer.plugins.interaction.on(
'pointerdown',
(event: InteractionEvent) => {
const globalPos = event.data.global;
if (!event.target) {
// 点到了画布的空白位置
touchBlank = true;
if (curTool === Tool.Selector) {
const stagePos = pixiApp.stage.localTransform
.clone()
.applyInverse(globalPos);
selectorTool = new SelectorTool(pixiApp, stagePos);
}
}
}
);
pointermove时,执行selectorTool对象的move函数:
pixiApp.renderer.plugins.interaction.on(
'pointermove',
(event: InteractionEvent) => {
const globalPos = event.data.global;
const stagePos = pixiApp.stage.localTransform.applyInverse(globalPos);
if (touchBlank && curTool === Tool.Selector) {
selectorTool.move(stagePos);
}
}
);
pointerup时执行selectorTool对象的end函数:
pixiApp.renderer.plugins.interaction.on(
'pointerup',
(event: InteractionEvent) => {
touchBlank = false;
if (selectorTool) {
selectorTool.end();
selectorTool = undefined;
}
}
);
2.8.1.3 最终效果
2.8.2 在pointerup时,选中矩形框内的元素
2.8.2.1 核心思想
在pointerup时,我们将遍历stage上的所有元素,判断是否有元素在矩形框内,如果有,我们就会新建一个Container,将这些元素放到Container里面,再将这个Container放到stage上,并将这个Container设置成activeObj(在第5步中,我们实现了给activeObj添加border和控制点的效果,这个效果在这里也适用)
2.8.2.2 注意
在将选中的元素放到container里时,这些元素就会相对于container进行定位了,这个时候,我们要重新设置这些元素的位置,让其相对于stage的位置不变。
2.8.2.3 代码实现
给SelectorTool类的end函数添加一些逻辑:
end() {
this.pixiApp.stage.removeChild(this.rect);
const selected: DisplayObject[] = [];
this.pixiApp.stage.children
.filter((child) => {
if (child instanceof Text) {
return true;
}
})
.forEach((child) => {
const objRect = getObjectStageAABB(child);
if (AABBRectTest(this.getRect(), objRect)) {
selected.push(child);
}
});
if (selected.length > 1) {
// 创建一个GroupSelector对象,并将选中的元素放到这个对象里
createGroupSelector(selected);
}
}
将选中的Text元素放入GroupSelector对象里:
addChildrenFromStage(objList: DisplayObject[]) {
this.addChild(...objList);
// 重新设置这些Text元素的位置
objList.forEach((obj) => {
obj.position.set(
obj.position.x - this.position.x,
obj.position.y - this.position.y
);
});
const localBounds = this.getLocalBounds();
this.hitArea = new Polygon([
0,
0,
localBounds.width,
0,
localBounds.width,
localBounds.height,
0,
localBounds.height,
]);
}
2.8.2.4 最终效果
2.8.3 在鼠标点击activeObj之外的区域时,移除GroupSelector,并将其子元素放回到stage上
2.8.3.1 核心思想
当Text对象从stage移到GroupSelector对象上时,就相对于GroupSelector对象定位了,如果要将Text对象再从GroupSelector对象上放回stage上,那么需要让他的position加上一个值,因为,将Text对象从stage移到GroupSelector对象上时,我们让它的position减少了一个值,就是这一段代码:
// 重新设置这些Text元素的位置
objList.forEach((obj) => {
obj.position.set(
obj.position.x - this.position.x