Published on

PixiJS 源码揭秘 - 6. 探索复杂图形 Graphics 的设计与实现

Authors
  • avatar
    Name
    青雲
    Twitter

概述

Graphics类是PixiJS中的一个核心渲染类,它提供了一套完整的2D图形绘制系统。主要用途包括:

  1. 绘制基础图形,如线条、圆形、矩形等
  2. 为这些图形添加颜色和填充效果
  3. 创建复杂的遮罩(mask)
  4. 定义复杂的点击区域(hitArea)

其设计理念包括:

  1. 链式调用:所有绘图方法都返回this,支持方法链式调用
  2. 分离关注点:将绘图指令(Graphics)和实际渲染逻辑(GraphicsContext)分离
  3. 资源共享:支持多个Graphics实例共享同一个GraphicsContext,优化性能
  4. 灵活性:支持基础图形绘制、复杂路径创建、填充和描边样式自定义

核心架构详解

目录结构

主要类文件

  1. Graphics.ts
    • Graphics类的主要实现
    • 提供用户级API接口
  2. GraphicsContext.ts
    • 图形上下文的核心实现
    • 负责实际的绘图操作
  3. BatchableGraphics.ts
    • 可批处理的图形实现
    • 优化渲染性能

系统文件

  1. GraphicsContextSystem.ts
    • 图形上下文系统
    • 管理图形上下文的生命周期
  2. GraphicsPipe.ts
    • 图形渲染管道
    • 处理图形的渲染流程

类型定义

  1. FillTypes.ts
    • 填充样式相关类型定义
    • 定义了填充和描边的样式接口
  2. const.ts
    • 定义模块使用的常量
    • 配置参数等

构建命令 (buildCommands/)

  • 负责将高级绘图命令转换为底层渲染指令
buildCommands/
├── buildLine.ts         // 线条构建
├── buildPixelLine.ts    // 像素线条构建
├── buildCircle.ts       // 圆形构建
└── ...                  // 其他图形构建命令

路径系统 (path/)

  • 处理路径的创建和管理
  • 实现各种路径绘制算法
path/
├── GraphicsPath.ts      // 图形路径实现
├── ShapePath.ts        // 形状路径
└── roundShape.ts       // 圆角形状实现

SVG支持 (svg/)

  • 提供SVG路径解析和渲染支持
svg/
├── SVGParser.ts        // SVG解析器
└── ...                 // 其他SVG相关实现

填充系统 (fill/)

  • 处理各种填充样式和模式
fill/
├── FillPattern.ts      // 填充模式
└── ...                 // 其他填充相关实现

工具函数 (utils/)

  • 提供各种辅助功能和工具函数
utils/
├── convertFillInputToFillStyle.ts  // 填充样式转换
└── ...                            // 其他工具函数

Graphics的组成与设计

核心组件

  1. Graphics类
export class Graphics extends ViewContainer implements Instruction {
  public readonly renderPipeId: string = 'graphics'
  private _context: GraphicsContext
  private readonly _ownedContext: GraphicsContext
}
  • 继承自ViewContainer,获得视图容器的基本能力
  • 实现Instruction接口,支持渲染指令系统
  • 提供用户友好的绘图API
  • 管理绘图上下文的生命周期
  1. GraphicsContext
export class GraphicsContext extends EventEmitter {
  public static defaultFillStyle: ConvertedFillStyle
  public static defaultStrokeStyle: ConvertedStrokeStyle

  // 事件系统
  update: GraphicsContext
  destroy: GraphicsContext
}
  • 继承自EventEmitter,提供事件通知机制
  • 实际执行绘图指令的核心引擎
  • 管理图形的状态和生命周期

架构特点

双Context设计

private _context: GraphicsContext;        // 当前使用的上下文
private readonly _ownedContext: GraphicsContext;  // 实例拥有的上下文
  • 支持上下文共享机制
  • 允许多个Graphics实例共用同一个GraphicsContext
  • 优化性能和内存使用

事件驱动更新

this._context.on('update', this.onViewUpdate, this)
  • 通过事件系统实现状态同步
  • 当GraphicsContext发生变化时自动触发视图更新

可配置性

export interface GraphicsOptions extends ContainerOptions {
  context?: GraphicsContext
  roundPixels?: boolean
}
  • 支持自定义GraphicsContext
  • 提供像素对齐等渲染选项

GraphicsContext详解

GraphicsContext是实际执行绘图操作的核心类。

核心功能

样式管理

public static defaultFillStyle: ConvertedFillStyle;
public static defaultStrokeStyle: ConvertedStrokeStyle;
  • 提供默认填充和描边样式
  • 支持颜色、透明度、纹理等属性

指令系统

export type GraphicsInstructions = FillInstruction | StrokeInstruction | TextureInstruction
  • 支持填充、描边、纹理等多种绘图指令
  • 使用类型联合确保指令类型安全

优化机制

状态管理

  • 使用脏标记机制(dirty flags)追踪状态变化
  • 维护状态栈用于保存和恢复绘图状态
  • 支持共享上下文以复用绘图指令

批处理系统

// 'auto':根据上下文自动决定是否进行批处理
// 'batch':强制进行批处理
// 'no-batch':禁用批处理
export type BatchMode = 'auto' | 'batch' | 'no-batch'
  • 支持自动/手动/禁用三种批处理模式
  • 使用 BatchableGraphics 管理可批处理的图形
  • 通过 GraphicsContextSystem 统一管理渲染数据

资源共享

  • 支持多个Graphics实例共享同一个GraphicsContext
  • 类似于精灵共享纹理的概念
  • 减少GPU资源占用和内存消耗

几何数据管理

  • 在 GpuGraphicsContext 中集中管理几何数据
  • 支持顶点、UV和索引数据的复用
  • 通过 GraphicsContextRenderData 管理渲染指令

渲染流程

指令转换流程

Graphics类接收API调用

// Graphics类提供用户友好的API
export class Graphics extends ViewContainer implements Instruction {
  private _context: GraphicsContext

  // 通过_callContextMethod方法将API调用转发给Context
  private _callContextMethod(method: keyof GraphicsContext, args: any[]): this {
    ;(this._context[method] as any)(...args)
    return this
  }
}

转换为标准化指令

// GraphicsContext中的指令类型定义
export type GraphicsInstructions =
  | FillInstruction // 填充指令
  | StrokeInstruction // 描边指令
  | TextureInstruction // 纹理指令

// 指令示例
interface FillInstruction {
  action: 'fill' | 'cut'
  data: {
    style: ConvertedFillStyle
    path: GraphicsPath
    hole?: GraphicsPath
  }
}

几何处理

GraphicsContextSystem处理

export class GraphicsContextSystem {
  // 管理GPU上下文
  private _gpuContextHash: Record<number, GpuGraphicsContext> = {}
  // 管理渲染数据
  private _graphicsDataContextHash: Record<number, GraphicsContextRenderData> = Object.create(null)
}

路径计算

export class ShapePath {
  // 存储图形基元
  public shapePrimitives: { shape: ShapePrimitive; transform?: Matrix }[] = []

  // 用于碰撞检测和边界计算
  private readonly _bounds = new Bounds()

  // 当前多边形
  private _currentPoly: Polygon | null = null
}

渲染优化

批处理系统

export class GraphicsPipe implements RenderPipe<Graphics> {
  // 批处理哈希表
  private _graphicsBatchesHash: Record<number, BatchableGraphics[]> = Object.create(null)

  public validateRenderable(graphics: Graphics): boolean {
    const context = graphics.context
    const gpuContext = this.renderer.graphicsContext.updateGpuContext(context)

    // 检查是否可以批处理
    if (gpuContext.isBatchable) {
      return true
    }
    return false
  }

  private _addToBatcher(graphics: Graphics, instructionSet: InstructionSet) {
    // 实现批处理逻辑
    const batches = (this._graphicsBatchesHash[graphics.uid] ||= [] as BatchableGraphics[])
  }
}

状态缓存

export class GraphicsContext {
  // 缓存变换矩阵
  private _transform: Matrix = new Matrix()

  // 缓存样式
  private _fillStyle: ConvertedFillStyle
  private _strokeStyle: ConvertedStrokeStyle

  // 状态栈用于保存和恢复状态
  private _stateStack: {
    fillStyle: ConvertedFillStyle
    strokeStyle: ConvertedStrokeStyle
    transform: Matrix
  }[] = []
}

渲染模式优化

export type BatchMode = 'auto' | 'batch' | 'no-batch'

export class GraphicsContext {
  public batchMode: BatchMode = 'auto'

  // 脏标记用于优化更新
  public dirty = true
  private _boundsDirty = true
}

总结

  1. Graphics组件
    • User API Layer:提供用户友好的绘图API
    • Event System:处理更新和状态变化事件
  2. GraphicsContext组件
    • Instruction Manager:管理绘图指令
    • State Manager:管理绘图状态
    • 两个数据存储:Instructions和State Stack
  3. GraphicsPipe组件
    • Batch Manager:处理批处理逻辑
    • Validation:验证可渲染对象
    • Batch Hash:存储批处理数据
  4. GraphicsContextSystem组件
    • GPU Context Manager:管理GPU上下文
    • Render Data Manager:管理渲染数据
    • 两个数据存储:GPU Context Hash和Render Data Hash
  5. Renderer组件
    • 支持多种渲染管线:WebGL、WebGPU和Canvas

下图展示了用户API如何转换为底层指令、状态管理和批处理的优化过程和各个组件之间的交互关系。

Graphics的设计具有以下特点:

  1. 分层设计
    1. Graphics层:提供用户API
    2. GraphicsContext层:管理绘图指令
    3. GraphicsPipe层:处理渲染和批处理
    4. GraphicsContextSystem层:管理GPU资源
  2. 性能优化
    1. 使用批处理减少绘制调用
    2. 状态缓存避免重复计算
    3. 脏标记机制优化更新
    4. 支持自动/手动批处理模式
  3. 灵活性
    1. 支持多种渲染后端(WebGL/WebGPU/Canvas)
    2. 可自定义渲染适配器
    3. 支持复杂的图形操作和样式

绘图功能全解析

路径系统

Graphics提供了完整的路径绘制系统。

基础路径操作

beginPath() // 开始新路径
moveTo(x, y) // 移动到指定点
lineTo(x, y) // 绘制直线到指定点
closePath() // 闭合路径

曲线绘制

// 圆弧
arc(x, y, radius, startAngle, endAngle, counterclockwise?)
arcTo(x1, y1, x2, y2, radius)

// 三次贝塞尔曲线
public bezierCurveTo(
    cp1x: number, cp1y: number,  // 第一控制点
    cp2x: number, cp2y: number,  // 第二控制点
    x: number, y: number,        // 终点
    smoothness?: number          // 平滑度
): this {
    this._ensurePoly();
    const currentPoly = this._currentPoly;

    // 处理曲线生成
    buildAdaptiveBezier(
        currentPoly.points,
        currentPoly.lastX, currentPoly.lastY,
        cp1x, cp1y, cp2x, cp2y,
        x, y,
        smoothness,
    );

    return this;
}
// 二次贝塞尔曲线
public quadraticCurveTo(
    cpx: number, cpy: number,    // 控制点
    x: number, y: number,        // 终点
    smoothing?: number           // 平滑度
): this {
    buildAdaptiveQuadratic(
        this._currentPoly.points,
        currentPoly.lastX, currentPoly.lastY,
        cpx, cpy, x, y,
        smoothing,
    );
    return this;
}

SVG路径支持

arcToSvg(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y)

Pixel Line

Pixel Line是PixiJS中的一种特殊线条绘制方式,它主要用于绘制像素精确的线条。与普通线条不同,Pixel Line不会进行抗锯齿处理,适合需要精确像素控制的场景。

export function buildPixelLine(
  points: number[],
  closed: boolean,
  vertices: number[],
  indices: number[]
): void
  1. points: 存储线条各个点的坐标
    • 偶数索引存储x坐标
    • 奇数索引存储y坐标
  2. closed: 控制线条是否闭合
    • true: 首尾相连
    • false: 保持开放
  3. vertices: 存储处理后的顶点信息
  4. indices: 存储顶点的连接顺序

pixelLine 的处理流程:

if (!lineStyle.pixelLine) {
  buildLine(points, lineStyle, false, close, vertices, indices)
} else {
  buildPixelLine(points, close, vertices, indices)
  topology = 'line-list' // 使用 line-list 拓扑
}
  • 当 pixelLine: true 时,使用 buildPixelLine 和 line-list 拓扑
  • 这确保了线条以精确的像素级别渲染
  • 不会受到变换和缩放的影响

网格绘制

function drawGrid(graphics: Graphics, width: number, height: number, cellSize: number) {
  graphics.setStrokeStyle({
    width: 1,
    color: 0xcccccc,
    pixelLine: true, // 关键是这个属性
  })

  // 先画所有的路径
  // 绘制垂直线
  for (let x = 0; x <= width; x += cellSize) {
    graphics.moveTo(x, 0)
    graphics.lineTo(x, height)
  }

  // 绘制水平线
  for (let y = 0; y <= height; y += cellSize) {
    graphics.moveTo(0, y)
    graphics.lineTo(width, y)
  }

  // 最后一次性stroke所有路径
  graphics.stroke()
}

像素艺术画

// 像素画绘制函数
function drawPixelArt(graphics, pixelData, pixelSize, offsetX = 0, offsetY = 0) {
  // 设置填充样式
  graphics.setFillStyle({
    color: 0x000000, // 黑色填充
  })

  // 设置描边样式
  graphics.setStrokeStyle({
    width: 1,
    color: 0x333333,
    pixelLine: true,
  })

  // 遍历像素数据
  for (let y = 0; y < pixelData.length; y++) {
    for (let x = 0; x < pixelData[y].length; x++) {
      if (pixelData[y][x]) {
        const px = offsetX + x * pixelSize
        const py = offsetY + y * pixelSize

        // 绘制一个填充的矩形
        graphics.beginPath()
        graphics.rect(px, py, pixelSize, pixelSize)
        graphics.fill()
        graphics.stroke()
      }
    }
  }
}

图形绘制系统

提供了丰富的预定义图形。

基础图形

rect(x, y, w, h) // 矩形
circle(x, y, radius) // 圆形
ellipse(x, y, radiusX, radiusY) // 椭圆

高级图形

roundRect(x, y, w, h, radius)  // 圆角矩形
regularPoly(x, y, radius, sides, rotation?, transform?) // 正多边形
roundPoly(x, y, radius, sides, corner, rotation?) // 圆角多边形

自定义图形

poly(points, close?)           // 自定义多边形
roundShape(points, radius, useQuadratic?, smoothness?) // 圆角自定义图形

样式系统

填充样式(FillStyle)

const fillStyle = {
  color: 0xffffff, // 颜色
  alpha: 1, // 透明度
  texture: Texture.WHITE, // 纹理填充
  matrix: null, // 变换矩阵
  fill: null, // 自定义填充
}

描边样式(StrokeStyle)

const strokeStyle = {
  width: 1, // 线宽
  color: 0xffffff, // 颜色
  alpha: 1, // 透明度
  alignment: 0.5, // 线条对齐
  miterLimit: 10, // 斜接限制
  cap: 'butt', // 线帽样式
  join: 'miter', // 连接样式
}

变换系统

Graphics类提供了强大的变换系统,允许开发者对图形进行旋转、缩放、平移等操作。

变换矩阵基础

Matrix类结构

在PixiJS中,所有的变换操作都基于Matrix类:

class Matrix {
  a: number // 水平缩放
  b: number // 水平倾斜
  c: number // 垂直倾斜
  d: number // 垂直缩放
  tx: number // 水平移动
  ty: number // 垂直移动
}

矩阵变换原理

2D变换矩阵是一个3x3的矩阵,用于执行以下操作:

  • 平移(Translation)
  • 旋转(Rotation)
  • 缩放(Scale)
  • 倾斜(Skew)

变换矩阵的数学表达式:

[a  b  0]
[c  d  0]
[tx ty 1]

矩阵组合

  • 多个变换可以通过矩阵乘法组合
  • 变换的顺序会影响最终结果
  • 组合顺序:从右到左应用变换

核心变换方法

// 位置变换
graphics.position.set(x, y)
// 旋转变换
graphics.rotation = angle

// 缩放变换
graphics.scale.set(scaleX, scaleY)

// 倾斜变换
graphics.skew.set(skewX, skewY)

状态管理

// 保存变换状态
const savedState = {
  position: graphics.position.clone(),
  rotation: graphics.rotation,
  scale: graphics.scale.clone(),
  skew: graphics.skew.clone(),
}

// 恢复变换状态
graphics.position.copyFrom(savedState.position)
graphics.rotation = savedState.rotation
graphics.scale.copyFrom(savedState.scale)
graphics.skew.copyFrom(savedState.skew)

最佳实践与使用建议

性能优化建议

Context复用

// 对于相同的图形,使用共享Context
const sharedContext = new GraphicsContext()
const instances = Array.from({ length: 10 }, () => new Graphics({ context: sharedContext }))

批处理使用

// 大量简单图形时启用批处理
const graphics = new Graphics()
graphics.batched = true
graphics.beginFill(0xff0000)
for (let i = 0; i < 1000; i++) {
  graphics.drawRect(i * 10, 0, 8, 8)
}

资源管理

// 及时清理不需要的资源
graphics.destroy({ context: true })

常见使用场景

UI元素绘制

const button = new Graphics().beginFill(0x0000ff).roundRect(0, 0, 100, 40, 5).endFill()

复杂形状

const star = new Graphics()
  .beginFill(0xffff00)
  .regularPoly(100, 100, 50, 5, Math.PI / 2)
  .endFill()

动画效果

const animatedCircle = new Graphics()
app.ticker.add((delta) => {
  animatedCircle
    .clear()
    .beginFill(0xff0000)
    .circle(100, 100, Math.sin(Date.now() / 1000) * 20 + 50)
    .endFill()
})

综合示例:时钟

// 创建时钟图形
const clockGraphics = new Graphics()
app.stage.addChild(clockGraphics)

// 绘制时钟刻度
function drawClockFace(graphics) {
  // 绘制外圈
  graphics
    .setStrokeStyle({
      width: 4,
      color: 0x000000,
    })
    .beginFill(0xffffff)
    .drawCircle(0, 0, 200)
    .endFill()

  // 绘制刻度
  for (let i = 0; i < 12; i++) {
    const angle = (i * Math.PI * 2) / 12

    // 绘制小时刻度
    graphics.setStrokeStyle({
      width: 4,
      color: 0x000000,
    })

    const startX = Math.sin(angle) * 180
    const startY = -Math.cos(angle) * 180
    const endX = Math.sin(angle) * 200

    const endY = -Math.cos(angle) * 200

    graphics.moveTo(startX, startY).lineTo(endX, endY)

    // 绘制分钟刻度
    for (let j = 1; j < 5; j++) {
      const minuteAngle = angle + (j * Math.PI * 2) / 60
      const minStartX = Math.sin(minuteAngle) * 190
      const minStartY = -Math.cos(minuteAngle) * 190
      const minEndX = Math.sin(minuteAngle) * 200
      const minEndY = -Math.cos(minuteAngle) * 200

      graphics.moveTo(minStartX, minStartY).lineTo(minEndX, minEndY)
    }
  }

  // 绘制中心点
  graphics.beginFill(0x000000).drawCircle(0, 0, 8).endFill()
}

// 绘制时针
function drawHourHand(graphics, hours, minutes) {
  const angle = ((hours + minutes / 60) * Math.PI * 2) / 12
  const endX = Math.sin(angle) * 100
  const endY = -Math.cos(angle) * 100

  graphics
    .setStrokeStyle({
      width: 8,
      color: 0x000000,
      cap: 'round',
    })
    .moveTo(0, 0)
    .lineTo(endX, endY)
    .stroke() // 添加 stroke() 调用
}

// 绘制分针
function drawMinuteHand(graphics, minutes, seconds) {
  const angle = ((minutes + seconds / 60) * Math.PI * 2) / 60
  const endX = Math.sin(angle) * 140
  const endY = -Math.cos(angle) * 140

  graphics
    .setStrokeStyle({
      width: 4,
      color: 0x000000,
      cap: 'round',
    })
    .moveTo(0, 0)
    .lineTo(endX, endY)
    .stroke()
}

// 绘制秒针
function drawSecondHand(graphics, seconds, milliseconds) {
  const angle = ((seconds + milliseconds / 1000) * Math.PI * 2) / 60
  const backX = Math.sin(angle) * -20
  const backY = -Math.cos(angle) * -20
  const endX = Math.sin(angle) * 160
  const endY = -Math.cos(angle) * 160

  graphics
    .setStrokeStyle({
      width: 2,
      color: 0xff0000,
      cap: 'round',
    })
    .moveTo(backX, backY)
    .lineTo(endX, endY)
    .stroke()
}

// 绘制数字
function drawNumbers(graphics) {
  const radius = 160
  const fontSize = 24

  for (let i = 1; i <= 12; i++) {
    const angle = (i * Math.PI * 2) / 12
    const x = Math.sin(angle) * radius
    const y = -Math.cos(angle) * radius

    const text = new Text({
      text: i.toString(),
      style: {
        fontFamily: 'Arial',
        fontSize: fontSize,
        fill: 0x000000,
        align: 'center',
      },
    })

    text.anchor.set(0.5)
    text.position.set(x, y)
    graphics.addChild(text)
  }
}

// 更新时钟
function updateClock() {
  const now = new Date()
  const hours = now.getHours() % 12
  const minutes = now.getMinutes()
  const seconds = now.getSeconds()
  const milliseconds = now.getMilliseconds()

  clockGraphics.clear()

  // 设置时钟位置到画布中心
  clockGraphics.position.set(app.screen.width / 2, app.screen.height / 2)

  // 绘制时钟面
  drawClockFace(clockGraphics)

  // 绘制指针
  drawHourHand(clockGraphics, hours, minutes)
  drawMinuteHand(clockGraphics, minutes, seconds)
  drawSecondHand(clockGraphics, seconds, milliseconds)

  // 绘制数字
  drawNumbers(clockGraphics)
}

// 添加阴影效果
function addShadowEffect() {
  const shadow = new Graphics()
  shadow.beginFill(0x000000, 0.1).drawCircle(0, 0, 205)

  app.stage.addChildAt(shadow, 0)
  shadow.position.set(app.screen.width / 2 + 5, app.screen.height / 2 + 5)
}

// 初始化
addShadowEffect()

// 启动动画循环
app.ticker.add(updateClock)

总结

PixiJS的Graphics类是一个功能强大、设计优雅的2D图形绘制系统。通过合理的架构设计和性能优化,它既保证了API的易用性,又确保了渲染性能。开发者可以根据具体需求,选择适当的API和优化策略,创建高效的图形渲染应用。