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

484 阅读12分钟

大家好,我是青雲,专注于前端开发领域,尤其在可视化技术和编辑器方面有着深入的研究和实践经验。欢迎关注我的公众号【青雲老哥】,一起交流、学习、共建。

概述

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 renderPipeIdstring = 'graphics';
    private _contextGraphicsContext;
    private readonly _ownedContextGraphicsContext;
}
  • 继承自ViewContainer,获得视图容器的基本能力
  • 实现Instruction接口,支持渲染指令系统
  • 提供用户友好的绘图API
  • 管理绘图上下文的生命周期
  1. GraphicsContext
export class GraphicsContext extends EventEmitter {
    public static defaultFillStyleConvertedFillStyle;
    public static defaultStrokeStyleConvertedStrokeStyle;
    
    // 事件系统
    updateGraphicsContext
    destroyGraphicsContext
}
  • 继承自EventEmitter,提供事件通知机制
  • 实际执行绘图指令的核心引擎
  • 管理图形的状态和生命周期

架构特点

双Context设计
private _contextGraphicsContext;        // 当前使用的上下文
private readonly _ownedContextGraphicsContext;  // 实例拥有的上下文
  • 支持上下文共享机制
  • 允许多个Graphics实例共用同一个GraphicsContext
  • 优化性能和内存使用
事件驱动更新
this._context.on('update'this.onViewUpdatethis);
  • 通过事件系统实现状态同步
  • 当GraphicsContext发生变化时自动触发视图更新
可配置性
export interface GraphicsOptions extends ContainerOptions {
    context?: GraphicsContext;
    roundPixels?: boolean;
}
  • 支持自定义GraphicsContext
  • 提供像素对齐等渲染选项

GraphicsContext详解

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

核心功能

样式管理
public static defaultFillStyleConvertedFillStyle;
public static defaultStrokeStyleConvertedStrokeStyle;
  • 提供默认填充和描边样式
  • 支持颜色、透明度、纹理等属性
指令系统
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 _contextGraphicsContext;
    
    // 通过_callContextMethod方法将API调用转发给Context
    private _callContextMethod(method: keyof GraphicsContextargsany[]): this {
        (this._context[method] as any)(...args);
        return this;
    }
}
转换为标准化指令
// GraphicsContext中的指令类型定义
export type GraphicsInstructions = 
    FillInstruction |   // 填充指令
    StrokeInstruction | // 描边指令
    TextureInstruction// 纹理指令

// 指令示例
interface FillInstruction {
    action'fill' | 'cut'
    data: { 
        styleConvertedFillStyle, 
        pathGraphicsPath, 
        hole?: GraphicsPath 
    }
}

几何处理

GraphicsContextSystem处理
export class GraphicsContextSystem {
    // 管理GPU上下文
    private _gpuContextHashRecord<numberGpuGraphicsContext> = {};
    // 管理渲染数据
    private _graphicsDataContextHashRecord<numberGraphicsContextRenderData> = Object.create(null);
}
路径计算
export class ShapePath {
    // 存储图形基元
    public shapePrimitives: { shapeShapePrimitive, transform?: Matrix }[] = [];
    
    // 用于碰撞检测和边界计算
    private readonly _bounds = new Bounds();
    
    // 当前多边形
    private _currentPolyPolygon | null = null;
}

渲染优化

批处理系统
export class GraphicsPipe implements RenderPipe<Graphics> {
    // 批处理哈希表
    private _graphicsBatchesHashRecord<numberBatchableGraphics[]> = Object.create(null);
    
    public validateRenderable(graphicsGraphics): 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 _transformMatrix = new Matrix();
    
    // 缓存样式
    private _fillStyleConvertedFillStyle;
    private _strokeStyleConvertedStrokeStyle;
    
    // 状态栈用于保存和恢复状态
    private _stateStack: { 
        fillStyleConvertedFillStyle; 
        strokeStyleConvertedStrokeStyle, 
        transformMatrix 
    }[] = [];
}
渲染模式优化
export type BatchMode = 'auto' | 'batch' | 'no-batch';

export class GraphicsContext {
    public batchModeBatchMode = '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(
    cp1xnumbercp1ynumber,  // 第一控制点
    cp2xnumbercp2ynumber,  // 第二控制点
    xnumberynumber,        // 终点
    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(
    cpxnumbercpynumber,    // 控制点
    xnumberynumber,        // 终点
    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({
        width1,
        color0xCCCCCC,
        pixelLinetrue// 关键是这个属性
    });

    // 先画所有的路径
    // 绘制垂直线
    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({
            color0x000000,  // 黑色填充
        });

        // 设置描边样式
        graphics.setStrokeStyle({
            width1,
            color0x333333,
            pixelLinetrue,
        });

        // 遍历像素数据
        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 = {
    color0xffffff,    // 颜色
    alpha1,           // 透明度
    textureTexture.WHITE// 纹理填充
    matrixnull,       // 变换矩阵
    fillnull         // 自定义填充
};

描边样式(StrokeStyle)

const strokeStyle = {
    width1,          // 线宽
    color0xffffff,   // 颜色
    alpha1,          // 透明度
    alignment0.5,    // 线条对齐
    miterLimit10,    // 斜接限制
    cap'butt',       // 线帽样式
    join'miter'      // 连接样式
};

变换系统

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

变换矩阵基础

Matrix类结构

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

class Matrix {
    anumber;    // 水平缩放
    bnumber;    // 水平倾斜
    cnumber;    // 垂直倾斜
    dnumber;    // 垂直缩放
    txnumber;   // 水平移动
    tynumber;   // 垂直移动
}
矩阵变换原理

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({ length10 }, () => new Graphics({ context: sharedContext }));

批处理使用

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

资源管理

// 及时清理不需要的资源
graphics.destroy({ contexttrue });

常见使用场景

UI元素绘制

const button = new Graphics()
    .beginFill(0x0000ff)
    .roundRect(00100405)
    .endFill();

复杂形状

const star = new Graphics()
    .beginFill(0xffff00)
    .regularPoly(100100505Math.PI / 2)
    .endFill();

动画效果

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

综合示例:时钟

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

// 绘制时钟刻度
function drawClockFace(graphics) {
    // 绘制外圈
    graphics
        .setStrokeStyle({
            width4,
            color0x000000,
        })
        .beginFill(0xffffff)
        .drawCircle(00200)
        .endFill();

    // 绘制刻度
    for (let i = 0; i < 12; i++) {
        const angle = i * Math.PI * 2 / 12;
        
        // 绘制小时刻度
        graphics.setStrokeStyle({
            width4,
            color0x000000,
        });

        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(008)
        .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({
            width8,
            color0x000000,
            cap'round',
        })
        .moveTo(00)
        .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({
            width4,
            color0x000000,
            cap'round',
        })
        .moveTo(00)
        .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({
            width2,
            color0xff0000,
            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,
                fill0x000000,
                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(0x0000000.1)
        .drawCircle(00205);
    
    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和优化策略,创建高效的图形渲染应用。