如何在PixiJS中用Graphics重绘ERASE混合模式擦除的区域?
解决方案:基于矢量步骤记录的可擦除绘图
你当前用双Graphics+ERASE混合模式的方案,核心问题是擦除操作直接永久删除了画布像素,导致无法重绘。要实现可编辑的擦除功能,必须放弃直接修改像素的思路,转而记录所有绘制操作的矢量步骤,通过重绘步骤来实现可逆编辑。
方案1:纯矢量步骤重绘(适合中小画布)
这种方案把每一笔绘制(画笔/擦除)都记录为独立的步骤数据,每次操作后重新绘制整个画布,擦除操作只是添加一条擦除步骤,撤销时移除即可恢复。
import { Graphics, Point, BLEND_MODES, Container } from 'pixi.js' // 定义单步绘制数据结构 interface DrawStep { mode: 'pen' | 'eraser' points: { x: number; y: number }[] style: { width: number; color: number; cap: string } } class ClearableGraphics extends Container { private canvas: Graphics private steps: DrawStep[] = [] private currentStep: DrawStep | null = null constructor() { super() this.canvas = new Graphics() this.addChild(this.canvas) } // 开始绘制(鼠标按下时调用) public startDraw(pt: { x: number; y: number }, mode: 'pen' | 'eraser' = 'pen') { const localPt = this.convertLocal(pt) // 画笔用黑色,擦除用白色(配合SRC_OUT混合模式实现擦除) const style = mode === 'pen' ? { width: 8, color: 0x000000, cap: 'round' } : { width: 8, color: 0xffffff, cap: 'round' } this.currentStep = { mode, points: [localPt], style } } // 继续绘制(鼠标拖拽时调用) public continueDraw(pt: { x: number; y: number }) { if (!this.currentStep) return const localPt = this.convertLocal(pt) this.currentStep.points.push(localPt) this.redraw() } // 结束绘制(鼠标松开时调用) public endDraw() { if (this.currentStep) { this.steps.push(this.currentStep) this.currentStep = null } } // 重绘整个画布 private redraw() { this.canvas.clear() // 绘制所有已完成的步骤 this.steps.forEach(step => this.drawStep(step)) // 绘制当前正在进行的步骤(实时预览) if (this.currentStep) { this.drawStep(this.currentStep) } } // 绘制单步路径 private drawStep(step: DrawStep) { const { mode, points, style } = step this.canvas.lineStyle(style) // 设置混合模式:画笔用正常覆盖,擦除用SRC_OUT(与底层像素混合实现擦除) this.canvas.blendMode = mode === 'pen' ? BLEND_MODES.SRC_OVER : BLEND_MODES.SRC_OUT if (points.length === 0) return this.canvas.moveTo(points[0].x, points[0].y) for (let i = 1; i < points.length; i++) { this.canvas.lineTo(points[i].x, points[i].y) } // 重置混合模式,避免影响后续绘制 this.canvas.blendMode = BLEND_MODES.SRC_OVER } public convertLocal(pt: { x: number; y: number }) { return this.parent.toLocal(new Point(pt.x, pt.y)) } // 撤销上一步操作(支持重绘擦除的区域) public undo() { if (this.steps.length > 0) { this.steps.pop() this.redraw() } } // 清空所有绘制内容 public clearAll() { this.steps = [] this.currentStep = null this.canvas.clear() } }
方案优势
- 所有操作都是可逆的,擦除后只需撤销对应步骤就能重绘区域
- 基于矢量数据,缩放画布不会出现模糊
- 逻辑简单,容易扩展(比如添加画笔颜色、粗细调整)
方案2:结合RenderTexture缓存(适合大画布)
如果画布尺寸较大,纯矢量重绘可能会有性能问题,这时候可以用RenderTexture缓存最终绘制结果,只在步骤变化时重新生成纹理,兼顾性能和可编辑性。
import { Graphics, Point, BLEND_MODES, Container, RenderTexture, Sprite, Application } from 'pixi.js' interface DrawStep { mode: 'pen' | 'eraser' points: { x: number; y: number }[] style: { width: number; color: number; cap: string } } class ClearableGraphics extends Container { private renderTexture: RenderTexture private canvasSprite: Sprite private tempGraphics: Graphics private steps: DrawStep[] = [] private currentStep: DrawStep | null = null private app: Application constructor(app: Application, width: number, height: number) { super() this.app = app this.renderTexture = RenderTexture.create({ width, height }) this.canvasSprite = new Sprite(this.renderTexture) this.tempGraphics = new Graphics() this.addChild(this.canvasSprite) } public startDraw(pt: { x: number; y: number }, mode: 'pen' | 'eraser' = 'pen') { const localPt = this.convertLocal(pt) const style = mode === 'pen' ? { width: 8, color: 0x000000, cap: 'round' } : { width: 8, color: 0xffffff, cap: 'round' } this.currentStep = { mode, points: [localPt], style } } public continueDraw(pt: { x: number; y: number }) { if (!this.currentStep) return const localPt = this.convertLocal(pt) this.currentStep.points.push(localPt) // 实时预览当前绘制的步骤 this.tempGraphics.clear() this.drawStep(this.tempGraphics, this.currentStep) this.canvasSprite.texture = this.renderTexture } public endDraw() { if (this.currentStep) { this.steps.push(this.currentStep) this.currentStep = null // 将临时绘制的内容合并到RenderTexture this.app.renderer.render(this.tempGraphics, { renderTexture: this.renderTexture, clear: false }) this.tempGraphics.clear() } } private drawStep(graphics: Graphics, step: DrawStep) { const { mode, points, style } = step graphics.lineStyle(style) graphics.blendMode = mode === 'pen' ? BLEND_MODES.SRC_OVER : BLEND_MODES.SRC_OUT if (points.length === 0) return graphics.moveTo(points[0].x, points[0].y) for (let i = 1; i < points.length; i++) { graphics.lineTo(points[i].x, points[i].y) } graphics.blendMode = BLEND_MODES.SRC_OVER } public convertLocal(pt: { x: number; y: number }) { return this.parent.toLocal(new Point(pt.x, pt.y)) } public undo() { if (this.steps.length > 0) { this.steps.pop() // 重新生成整个RenderTexture this.renderTexture = RenderTexture.create({ width: this.renderTexture.width, height: this.renderTexture.height }) this.tempGraphics.clear() this.steps.forEach(step => { this.drawStep(this.tempGraphics, step) }) this.app.renderer.render(this.tempGraphics, { renderTexture: this.renderTexture }) this.canvasSprite.texture = this.renderTexture } } public clearAll() { this.steps = [] this.currentStep = null this.renderTexture = RenderTexture.create({ width: this.renderTexture.width, height: this.renderTexture.height }) this.canvasSprite.texture = this.renderTexture this.tempGraphics.clear() } }
方案优势
- 用RenderTexture缓存最终结果,大幅提升大画布下的绘制性能
- 保留步骤记录,依然支持撤销/重绘操作
- 实时预览用临时Graphics,不影响缓存的纹理数据
关键原理说明
你之前用的BLEND_MODES.ERASE是直接修改目标像素的Alpha通道,删除后无法恢复。而上面两种方案用BLEND_MODES.SRC_OUT混合模式,擦除时绘制的白色路径会与底层黑色路径混合,实现视觉上的擦除效果,但所有操作都以矢量步骤的形式保留,随时可以通过重绘步骤来恢复被擦除的区域。
内容的提问来源于stack exchange,提问作者Philip Chain
相关产品推荐
相关产品推荐

