Skip to content

第9章:交互与事件系统

9.1 章节概述

PixiJS 提供了强大的交互系统,支持鼠标、触摸、指针等多种输入方式。理解事件系统对于构建交互式应用至关重要。

本章将深入讲解:

  • 事件系统基础:eventMode、事件类型
  • 指针事件:点击、移动、拖拽
  • 事件传播:冒泡、捕获、阻止传播
  • 命中测试:hitArea、containsPoint
  • 拖拽实现:基本拖拽、边界限制
  • 手势识别:缩放、旋转手势

9.2 事件系统基础

9.2.1 事件系统架构

PixiJS 事件系统架构

┌─────────────────────────────────────────────────────────────┐
│                    EventSystem                              │
├─────────────────────────────────────────────────────────────┤
│  - 监听 DOM 事件(mouse, touch, pointer)                   │
│  - 转换为 PixiJS 事件                                       │
│  - 执行命中测试                                             │
│  - 分发事件到显示对象                                       │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    EventBoundary                            │
├─────────────────────────────────────────────────────────────┤
│  - 管理事件边界                                             │
│  - 处理事件传播(冒泡、捕获)                               │
│  - 维护 hover 状态                                          │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    DisplayObject                            │
├─────────────────────────────────────────────────────────────┤
│  - eventMode: 事件模式                                      │
│  - hitArea: 命中区域                                        │
│  - cursor: 鼠标样式                                         │
│  - on/off/emit: 事件监听                                    │
└─────────────────────────────────────────────────────────────┘

9.2.2 eventMode 事件模式

typescript
/**
 * eventMode 事件模式
 */

// 事件模式类型
type EventMode = 'none' | 'passive' | 'auto' | 'static' | 'dynamic';

/*
none:
- 不接收任何事件
- 不参与命中测试
- 性能最好

passive:
- 不接收事件
- 但子对象可以接收事件
- 用于容器

auto:
- 自动检测是否有事件监听器
- 有监听器时参与命中测试
- 默认值

static:
- 总是参与命中测试
- 适合静态交互元素
- 推荐用于按钮等

dynamic:
- 总是参与命中测试
- 每帧更新命中区域
- 适合移动的交互元素
- 性能开销较大
*/


// 使用示例
const button = new PIXI.Sprite(texture);
button.eventMode = 'static';  // 推荐

const container = new PIXI.Container();
container.eventMode = 'passive';  // 容器不接收事件,但子对象可以

const decoration = new PIXI.Sprite(texture);
decoration.eventMode = 'none';  // 纯装饰,不需要交互

9.2.3 基本事件监听

typescript
/**
 * 基本事件监听
 */

const sprite = new PIXI.Sprite(texture);
sprite.eventMode = 'static';

// 添加事件监听
sprite.on('pointerdown', (event) => {
    console.log('按下', event.global);
});

sprite.on('pointerup', (event) => {
    console.log('释放');
});

sprite.on('pointermove', (event) => {
    console.log('移动', event.global);
});

// 一次性监听
sprite.once('pointerdown', (event) => {
    console.log('只触发一次');
});

// 移除监听
const handler = (event) => console.log('click');
sprite.on('pointerdown', handler);
sprite.off('pointerdown', handler);

// 移除所有监听
sprite.removeAllListeners();
sprite.removeAllListeners('pointerdown');  // 只移除特定事件

9.3 事件类型

9.3.1 指针事件

typescript
/**
 * 指针事件(推荐使用)
 * 统一处理鼠标和触摸
 */

sprite.eventMode = 'static';

// 按下
sprite.on('pointerdown', (event) => {
    console.log('按下');
});

// 释放
sprite.on('pointerup', (event) => {
    console.log('释放');
});

// 在对象外释放
sprite.on('pointerupoutside', (event) => {
    console.log('在对象外释放');
});

// 移动
sprite.on('pointermove', (event) => {
    console.log('移动');
});

// 进入
sprite.on('pointerover', (event) => {
    console.log('进入');
});

// 离开
sprite.on('pointerout', (event) => {
    console.log('离开');
});

// 进入(不冒泡)
sprite.on('pointerenter', (event) => {
    console.log('进入(不冒泡)');
});

// 离开(不冒泡)
sprite.on('pointerleave', (event) => {
    console.log('离开(不冒泡)');
});

// 取消
sprite.on('pointercancel', (event) => {
    console.log('取消');
});

// 点击(按下并释放)
sprite.on('pointertap', (event) => {
    console.log('点击');
});

9.3.2 鼠标事件

typescript
/**
 * 鼠标特定事件
 */

sprite.eventMode = 'static';

// 右键点击
sprite.on('rightdown', (event) => {
    console.log('右键按下');
});

sprite.on('rightup', (event) => {
    console.log('右键释放');
});

sprite.on('rightclick', (event) => {
    console.log('右键点击');
});

// 滚轮
sprite.on('wheel', (event) => {
    console.log('滚轮', event.deltaY);
});

// 全局鼠标事件
app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;

app.stage.on('globalpointermove', (event) => {
    console.log('全局移动', event.global);
});

9.3.3 触摸事件

typescript
/**
 * 触摸特定事件
 */

sprite.eventMode = 'static';

sprite.on('touchstart', (event) => {
    console.log('触摸开始');
});

sprite.on('touchend', (event) => {
    console.log('触摸结束');
});

sprite.on('touchmove', (event) => {
    console.log('触摸移动');
});

sprite.on('touchcancel', (event) => {
    console.log('触摸取消');
});

sprite.on('tap', (event) => {
    console.log('点击');
});

9.3.4 事件对象

typescript
/**
 * FederatedPointerEvent 事件对象
 */

sprite.on('pointerdown', (event: PIXI.FederatedPointerEvent) => {
    // 全局坐标(相对于画布)
    console.log('全局坐标:', event.global.x, event.global.y);
    
    // 本地坐标(相对于目标对象)
    console.log('本地坐标:', event.getLocalPosition(sprite));
    
    // 原始 DOM 事件
    console.log('原始事件:', event.nativeEvent);
    
    // 事件目标
    console.log('目标:', event.target);
    console.log('当前目标:', event.currentTarget);
    
    // 指针信息
    console.log('指针 ID:', event.pointerId);
    console.log('指针类型:', event.pointerType);  // mouse, touch, pen
    console.log('按钮:', event.button);  // 0=左, 1=中, 2=右
    console.log('按钮状态:', event.buttons);
    
    // 修饰键
    console.log('Shift:', event.shiftKey);
    console.log('Ctrl:', event.ctrlKey);
    console.log('Alt:', event.altKey);
    console.log('Meta:', event.metaKey);
    
    // 压力(触摸/笔)
    console.log('压力:', event.pressure);
    
    // 移动量
    console.log('移动:', event.movementX, event.movementY);
});

9.4 事件传播

9.4.1 冒泡与捕获

事件传播机制

捕获阶段(从外到内):
Stage → Container → Sprite

目标阶段:
Sprite(触发事件)

冒泡阶段(从内到外):
Sprite → Container → Stage


示例:

    Stage

      ▼ 捕获
  Container

      ▼ 捕获
   Sprite ← 目标

      ▼ 冒泡
  Container

      ▼ 冒泡
    Stage

9.4.2 事件传播控制

typescript
/**
 * 事件传播控制
 */

// 阻止冒泡
sprite.on('pointerdown', (event) => {
    event.stopPropagation();
    console.log('事件不会冒泡到父容器');
});

// 阻止默认行为
sprite.on('pointerdown', (event) => {
    event.preventDefault();
});

// 立即停止传播(同一对象的其他监听器也不执行)
sprite.on('pointerdown', (event) => {
    event.stopImmediatePropagation();
});


// 监听捕获阶段
container.addEventListener('pointerdown', (event) => {
    console.log('捕获阶段');
}, { capture: true });

// 监听冒泡阶段(默认)
container.on('pointerdown', (event) => {
    console.log('冒泡阶段');
});

9.4.3 事件委托

typescript
/**
 * 事件委托
 * 在父容器上监听子对象的事件
 */

const container = new PIXI.Container();
container.eventMode = 'static';

// 添加多个子对象
for (let i = 0; i < 10; i++) {
    const sprite = new PIXI.Sprite(texture);
    sprite.name = `item_${i}`;
    sprite.eventMode = 'static';
    container.addChild(sprite);
}

// 在容器上统一处理
container.on('pointerdown', (event) => {
    const target = event.target;
    if (target !== container) {
        console.log('点击了:', target.name);
    }
});


// 更复杂的事件委托
class EventDelegator {
    private container: PIXI.Container;
    private handlers: Map<string, (target: PIXI.DisplayObject, event: PIXI.FederatedPointerEvent) => void> = new Map();
    
    constructor(container: PIXI.Container) {
        this.container = container;
        this.container.eventMode = 'static';
        
        this.container.on('pointerdown', this.handleEvent.bind(this));
    }
    
    register(name: string, handler: (target: PIXI.DisplayObject, event: PIXI.FederatedPointerEvent) => void) {
        this.handlers.set(name, handler);
    }
    
    private handleEvent(event: PIXI.FederatedPointerEvent) {
        let target = event.target as PIXI.DisplayObject;
        
        while (target && target !== this.container) {
            if (target.name && this.handlers.has(target.name)) {
                this.handlers.get(target.name)!(target, event);
                break;
            }
            target = target.parent;
        }
    }
}

9.5 命中测试

9.5.1 hitArea 命中区域

typescript
/**
 * hitArea 自定义命中区域
 */

const sprite = new PIXI.Sprite(texture);
sprite.eventMode = 'static';

// 矩形命中区域
sprite.hitArea = new PIXI.Rectangle(0, 0, 100, 100);

// 圆形命中区域
sprite.hitArea = new PIXI.Circle(50, 50, 50);

// 椭圆命中区域
sprite.hitArea = new PIXI.Ellipse(50, 50, 60, 40);

// 多边形命中区域
sprite.hitArea = new PIXI.Polygon([
    0, 0,
    100, 0,
    100, 100,
    0, 100
]);

// 圆角矩形命中区域
sprite.hitArea = new PIXI.RoundedRectangle(0, 0, 100, 100, 10);


// 扩大命中区域(添加边距)
const padding = 10;
sprite.hitArea = new PIXI.Rectangle(
    -padding,
    -padding,
    sprite.width + padding * 2,
    sprite.height + padding * 2
);

9.5.2 自定义命中测试

typescript
/**
 * 自定义命中测试
 */

// 自定义 hitArea 类
class CustomHitArea implements PIXI.IHitArea {
    contains(x: number, y: number): boolean {
        // 自定义命中逻辑
        // 例如:环形区域
        const dx = x - 50;
        const dy = y - 50;
        const distance = Math.sqrt(dx * dx + dy * dy);
        return distance >= 30 && distance <= 50;
    }
}

sprite.hitArea = new CustomHitArea();


// 基于像素的命中测试
class PixelHitArea implements PIXI.IHitArea {
    private imageData: ImageData;
    private width: number;
    private height: number;
    
    constructor(texture: PIXI.Texture) {
        // 从纹理获取像素数据
        const canvas = document.createElement('canvas');
        canvas.width = texture.width;
        canvas.height = texture.height;
        
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(texture.baseTexture.resource.source as HTMLImageElement, 0, 0);
        
        this.imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        this.width = canvas.width;
        this.height = canvas.height;
    }
    
    contains(x: number, y: number): boolean {
        if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
            return false;
        }
        
        const index = (Math.floor(y) * this.width + Math.floor(x)) * 4;
        const alpha = this.imageData.data[index + 3];
        
        return alpha > 0;  // 非透明区域
    }
}

9.5.3 interactiveChildren

typescript
/**
 * interactiveChildren 控制子对象交互
 */

const container = new PIXI.Container();
container.eventMode = 'static';

// 禁用子对象的交互
container.interactiveChildren = false;

// 现在只有容器本身可以接收事件
// 子对象不参与命中测试


// 使用场景:模态对话框
class Modal extends PIXI.Container {
    constructor() {
        super();
        
        // 背景遮罩
        const overlay = new PIXI.Graphics();
        overlay.beginFill(0x000000, 0.5);
        overlay.drawRect(0, 0, 800, 600);
        overlay.endFill();
        overlay.eventMode = 'static';
        overlay.interactiveChildren = false;  // 阻止点击穿透
        
        // 对话框内容
        const dialog = new PIXI.Container();
        dialog.eventMode = 'static';
        // ... 添加对话框内容
        
        this.addChild(overlay, dialog);
    }
}

9.6 拖拽实现

9.6.1 基本拖拽

typescript
/**
 * 基本拖拽实现
 */

function makeDraggable(target: PIXI.DisplayObject) {
    let dragging = false;
    let dragOffset = new PIXI.Point();
    
    target.eventMode = 'static';
    target.cursor = 'pointer';
    
    target.on('pointerdown', (event: PIXI.FederatedPointerEvent) => {
        dragging = true;
        const localPos = event.getLocalPosition(target.parent);
        dragOffset.set(target.x - localPos.x, target.y - localPos.y);
        target.alpha = 0.8;
    });
    
    target.on('globalpointermove', (event: PIXI.FederatedPointerEvent) => {
        if (dragging) {
            const newPos = event.getLocalPosition(target.parent);
            target.x = newPos.x + dragOffset.x;
            target.y = newPos.y + dragOffset.y;
        }
    });
    
    target.on('pointerup', () => {
        dragging = false;
        target.alpha = 1;
    });
    
    target.on('pointerupoutside', () => {
        dragging = false;
        target.alpha = 1;
    });
}

// 使用
const sprite = new PIXI.Sprite(texture);
makeDraggable(sprite);

9.6.2 带边界限制的拖拽

typescript
/**
 * 带边界限制的拖拽
 */

function makeDraggableWithBounds(
    target: PIXI.DisplayObject,
    bounds: PIXI.Rectangle
) {
    let dragging = false;
    let dragOffset = new PIXI.Point();
    
    target.eventMode = 'static';
    target.cursor = 'pointer';
    
    target.on('pointerdown', (event: PIXI.FederatedPointerEvent) => {
        dragging = true;
        const localPos = event.getLocalPosition(target.parent);
        dragOffset.set(target.x - localPos.x, target.y - localPos.y);
    });
    
    target.on('globalpointermove', (event: PIXI.FederatedPointerEvent) => {
        if (dragging) {
            const newPos = event.getLocalPosition(target.parent);
            
            // 计算新位置
            let newX = newPos.x + dragOffset.x;
            let newY = newPos.y + dragOffset.y;
            
            // 限制在边界内
            newX = Math.max(bounds.x, Math.min(newX, bounds.right - target.width));
            newY = Math.max(bounds.y, Math.min(newY, bounds.bottom - target.height));
            
            target.x = newX;
            target.y = newY;
        }
    });
    
    target.on('pointerup', () => {
        dragging = false;
    });
    
    target.on('pointerupoutside', () => {
        dragging = false;
    });
}

// 使用
const bounds = new PIXI.Rectangle(0, 0, 800, 600);
makeDraggableWithBounds(sprite, bounds);

9.6.3 拖拽排序

typescript
/**
 * 拖拽排序
 */

class DraggableList {
    private container: PIXI.Container;
    private items: PIXI.DisplayObject[] = [];
    private draggingItem: PIXI.DisplayObject | null = null;
    private dragStartIndex: number = -1;
    private itemHeight: number;
    
    constructor(container: PIXI.Container, itemHeight: number) {
        this.container = container;
        this.itemHeight = itemHeight;
    }
    
    addItem(item: PIXI.DisplayObject) {
        item.eventMode = 'static';
        item.cursor = 'pointer';
        
        item.on('pointerdown', () => this.startDrag(item));
        item.on('globalpointermove', (e) => this.onDrag(e));
        item.on('pointerup', () => this.endDrag());
        item.on('pointerupoutside', () => this.endDrag());
        
        this.items.push(item);
        this.container.addChild(item);
        this.updatePositions();
    }
    
    private startDrag(item: PIXI.DisplayObject) {
        this.draggingItem = item;
        this.dragStartIndex = this.items.indexOf(item);
        item.alpha = 0.7;
        
        // 移到最上层
        this.container.setChildIndex(item, this.container.children.length - 1);
    }
    
    private onDrag(event: PIXI.FederatedPointerEvent) {
        if (!this.draggingItem) return;
        
        const localPos = event.getLocalPosition(this.container);
        this.draggingItem.y = localPos.y - this.itemHeight / 2;
        
        // 计算新索引
        const newIndex = Math.floor(localPos.y / this.itemHeight);
        const clampedIndex = Math.max(0, Math.min(newIndex, this.items.length - 1));
        
        // 重新排序
        const currentIndex = this.items.indexOf(this.draggingItem);
        if (clampedIndex !== currentIndex) {
            this.items.splice(currentIndex, 1);
            this.items.splice(clampedIndex, 0, this.draggingItem);
            this.updatePositions(this.draggingItem);
        }
    }
    
    private endDrag() {
        if (this.draggingItem) {
            this.draggingItem.alpha = 1;
            this.updatePositions();
            this.draggingItem = null;
        }
    }
    
    private updatePositions(except?: PIXI.DisplayObject) {
        this.items.forEach((item, index) => {
            if (item !== except) {
                item.y = index * this.itemHeight;
            }
        });
    }
}

9.7 手势识别

9.7.1 双指缩放

typescript
/**
 * 双指缩放(Pinch Zoom)
 */

class PinchZoom {
    private target: PIXI.Container;
    private touches: Map<number, PIXI.Point> = new Map();
    private initialDistance: number = 0;
    private initialScale: number = 1;
    
    constructor(target: PIXI.Container) {
        this.target = target;
        this.target.eventMode = 'static';
        
        this.target.on('pointerdown', this.onPointerDown.bind(this));
        this.target.on('pointermove', this.onPointerMove.bind(this));
        this.target.on('pointerup', this.onPointerUp.bind(this));
        this.target.on('pointerupoutside', this.onPointerUp.bind(this));
    }
    
    private onPointerDown(event: PIXI.FederatedPointerEvent) {
        this.touches.set(event.pointerId, event.global.clone());
        
        if (this.touches.size === 2) {
            this.initialDistance = this.getDistance();
            this.initialScale = this.target.scale.x;
        }
    }
    
    private onPointerMove(event: PIXI.FederatedPointerEvent) {
        if (!this.touches.has(event.pointerId)) return;
        
        this.touches.set(event.pointerId, event.global.clone());
        
        if (this.touches.size === 2) {
            const currentDistance = this.getDistance();
            const scale = (currentDistance / this.initialDistance) * this.initialScale;
            
            // 限制缩放范围
            const clampedScale = Math.max(0.5, Math.min(scale, 3));
            this.target.scale.set(clampedScale);
        }
    }
    
    private onPointerUp(event: PIXI.FederatedPointerEvent) {
        this.touches.delete(event.pointerId);
    }
    
    private getDistance(): number {
        const points = Array.from(this.touches.values());
        if (points.length < 2) return 0;
        
        const dx = points[1].x - points[0].x;
        const dy = points[1].y - points[0].y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

9.7.2 双指旋转

typescript
/**
 * 双指旋转
 */

class PinchRotate {
    private target: PIXI.Container;
    private touches: Map<number, PIXI.Point> = new Map();
    private initialAngle: number = 0;
    private initialRotation: number = 0;
    
    constructor(target: PIXI.Container) {
        this.target = target;
        this.target.eventMode = 'static';
        
        this.target.on('pointerdown', this.onPointerDown.bind(this));
        this.target.on('pointermove', this.onPointerMove.bind(this));
        this.target.on('pointerup', this.onPointerUp.bind(this));
        this.target.on('pointerupoutside', this.onPointerUp.bind(this));
    }
    
    private onPointerDown(event: PIXI.FederatedPointerEvent) {
        this.touches.set(event.pointerId, event.global.clone());
        
        if (this.touches.size === 2) {
            this.initialAngle = this.getAngle();
            this.initialRotation = this.target.rotation;
        }
    }
    
    private onPointerMove(event: PIXI.FederatedPointerEvent) {
        if (!this.touches.has(event.pointerId)) return;
        
        this.touches.set(event.pointerId, event.global.clone());
        
        if (this.touches.size === 2) {
            const currentAngle = this.getAngle();
            const deltaAngle = currentAngle - this.initialAngle;
            this.target.rotation = this.initialRotation + deltaAngle;
        }
    }
    
    private onPointerUp(event: PIXI.FederatedPointerEvent) {
        this.touches.delete(event.pointerId);
    }
    
    private getAngle(): number {
        const points = Array.from(this.touches.values());
        if (points.length < 2) return 0;
        
        return Math.atan2(
            points[1].y - points[0].y,
            points[1].x - points[0].x
        );
    }
}

9.7.3 滑动手势

typescript
/**
 * 滑动手势
 */

type SwipeDirection = 'left' | 'right' | 'up' | 'down';

class SwipeGesture {
    private target: PIXI.DisplayObject;
    private startPoint: PIXI.Point | null = null;
    private startTime: number = 0;
    private minDistance: number;
    private maxTime: number;
    private onSwipe: (direction: SwipeDirection) => void;
    
    constructor(
        target: PIXI.DisplayObject,
        onSwipe: (direction: SwipeDirection) => void,
        options: { minDistance?: number; maxTime?: number } = {}
    ) {
        this.target = target;
        this.onSwipe = onSwipe;
        this.minDistance = options.minDistance ?? 50;
        this.maxTime = options.maxTime ?? 300;
        
        this.target.eventMode = 'static';
        this.target.on('pointerdown', this.onPointerDown.bind(this));
        this.target.on('pointerup', this.onPointerUp.bind(this));
    }
    
    private onPointerDown(event: PIXI.FederatedPointerEvent) {
        this.startPoint = event.global.clone();
        this.startTime = Date.now();
    }
    
    private onPointerUp(event: PIXI.FederatedPointerEvent) {
        if (!this.startPoint) return;
        
        const endPoint = event.global;
        const elapsed = Date.now() - this.startTime;
        
        if (elapsed > this.maxTime) {
            this.startPoint = null;
            return;
        }
        
        const dx = endPoint.x - this.startPoint.x;
        const dy = endPoint.y - this.startPoint.y;
        const distance = Math.sqrt(dx * dx + dy * dy);
        
        if (distance < this.minDistance) {
            this.startPoint = null;
            return;
        }
        
        // 判断方向
        const angle = Math.atan2(dy, dx);
        let direction: SwipeDirection;
        
        if (angle > -Math.PI / 4 && angle <= Math.PI / 4) {
            direction = 'right';
        } else if (angle > Math.PI / 4 && angle <= 3 * Math.PI / 4) {
            direction = 'down';
        } else if (angle > -3 * Math.PI / 4 && angle <= -Math.PI / 4) {
            direction = 'up';
        } else {
            direction = 'left';
        }
        
        this.onSwipe(direction);
        this.startPoint = null;
    }
}

// 使用
new SwipeGesture(sprite, (direction) => {
    console.log('滑动方向:', direction);
});

9.8 鼠标样式

9.8.1 内置鼠标样式

typescript
/**
 * 鼠标样式
 */

sprite.eventMode = 'static';

// CSS 鼠标样式
sprite.cursor = 'pointer';
sprite.cursor = 'grab';
sprite.cursor = 'grabbing';
sprite.cursor = 'crosshair';
sprite.cursor = 'move';
sprite.cursor = 'not-allowed';
sprite.cursor = 'text';
sprite.cursor = 'wait';
sprite.cursor = 'help';
sprite.cursor = 'zoom-in';
sprite.cursor = 'zoom-out';

// 调整大小
sprite.cursor = 'n-resize';
sprite.cursor = 's-resize';
sprite.cursor = 'e-resize';
sprite.cursor = 'w-resize';
sprite.cursor = 'ne-resize';
sprite.cursor = 'nw-resize';
sprite.cursor = 'se-resize';
sprite.cursor = 'sw-resize';

9.8.2 自定义鼠标样式

typescript
/**
 * 自定义鼠标样式
 */

// 使用图片
sprite.cursor = 'url(cursor.png), auto';

// 使用图片并指定热点
sprite.cursor = 'url(cursor.png) 16 16, auto';


// 动态更改鼠标样式
sprite.on('pointerdown', () => {
    sprite.cursor = 'grabbing';
});

sprite.on('pointerup', () => {
    sprite.cursor = 'grab';
});


// 使用 PixiJS 自定义光标
const cursorTexture = PIXI.Texture.from('cursor.png');
const cursorSprite = new PIXI.Sprite(cursorTexture);
cursorSprite.anchor.set(0.5);

app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;

// 隐藏系统光标
app.renderer.events.cursorStyles.default = 'none';

app.stage.on('globalpointermove', (event) => {
    cursorSprite.position.copyFrom(event.global);
});

app.stage.addChild(cursorSprite);

9.9 本章小结

核心概念

概念说明
eventMode事件模式(none, passive, auto, static, dynamic)
hitArea自定义命中区域
cursor鼠标样式
pointerdown/up/move指针事件
事件冒泡从内到外传播
事件捕获从外到内传播
stopPropagation阻止事件传播

关键代码

typescript
// 启用交互
sprite.eventMode = 'static';
sprite.cursor = 'pointer';

// 事件监听
sprite.on('pointerdown', (event) => {
    console.log(event.global);
});

// 自定义命中区域
sprite.hitArea = new PIXI.Circle(50, 50, 50);

// 阻止传播
sprite.on('pointerdown', (event) => {
    event.stopPropagation();
});

// 拖拽
sprite.on('pointerdown', () => { dragging = true; });
sprite.on('globalpointermove', (e) => {
    if (dragging) sprite.position.copyFrom(e.global);
});
sprite.on('pointerup', () => { dragging = false; });

9.10 练习题

基础练习

  1. 实现一个可点击的按钮,带 hover 效果

  2. 实现基本的拖拽功能

  3. 实现右键菜单

进阶练习

  1. 实现拖拽排序列表

  2. 实现双指缩放和旋转

挑战练习

  1. 实现一个完整的画布交互系统(平移、缩放、选择、拖拽)

下一章预告:在第10章中,我们将深入学习纹理管理与资源加载。


文档版本:v1.0
字数统计:约 12,000 字
代码示例:50+ 个

如有转载或 CV 的请标注本站原文地址