前端图形引擎架构设计:双引擎架构设计

ECS渲染引擎架构文档

写在前面

之前写过一篇ECS文章,为什么还要再写一个,本质上因为之前的文档,截止到目前来说,变化巨大,底层已经改了很多很多,所以有必要把一些内容拎出来单独去说。

由于字体文件较大,加载时间会比较久😞

另外如果有性能问题,我会及时修复,引擎改造时间太仓促,只要不是内存泄漏,暂时没去处理。

还有很多东西要做。

体验地址:baiyuze.github.io/design/#/ca...

项目概览

Duck-Core 是一个基于 ECS(Entity-Component-System)架构 构建的高性能 Canvas 渲染引擎,专为复杂图形编辑场景设计。引擎的核心特色在于双渲染后端架构插件化系统设计极致的渲染性能优化

核心技术栈

  • CanvasKit-WASM - Google Skia 图形库的 WebAssembly 移植版
  • Canvas2D API - 浏览器原生渲染接口

架构核心亮点

ECS 架构模式 - 数据驱动的实体组件系统,实现逻辑与数据完全解耦

双引擎架构 - Canvas2D 与 CanvasKit 双渲染后端,运行时无缝切换

插件化设计 - 开放式扩展点,支持自定义渲染器、系统和组件

极致性能 - 颜色编码拾取、离屏渲染、渲染节流等多重优化


整体架构设计

整个引擎采用分层架构,从底层的渲染抽象到顶层的用户交互,每一层职责清晰且可独立替换。

graph TB subgraph "应用层" A[React 组件] --> B[Canvas 画布组件] end subgraph "引擎核心层" B --> C[Engine 引擎实例] C --> D[Core 状态管理器] C --> E[Camera 相机控制] C --> F[Entity Manager 实体管理] end subgraph "系统层 - System" C --> G[EventSystem 事件系统] G --> H[InputSystem 输入系统] G --> I[RenderSystem 渲染系统] G --> J[PickingSystem 拾取系统] G --> K[DragSystem 拖拽系统] G --> L[SelectionSystem 选择系统] G --> M[ZoomSystem 缩放系统] G --> M[FpsSystem FPS] end subgraph "渲染层 - Renderer" I --> N[RendererManager 渲染管理器] N --> O{选择渲染后端} O -->|Canvas2D| P[Canvas2D 渲染器组] O -->|CanvasKit| Q[CanvasKit 渲染器组] P --> R[RectRender] P --> S[EllipseRender] P --> T[TextRender] Q --> U[RectRender] Q --> V[EllipseRender] Q --> W[TextRender] end subgraph "数据层 - Component" X[StateStore 状态仓库] X --> Y[Position] X --> Z[Size] X --> AA[Color] X --> AB[Rotation] X --> AC[Selected] end D <--> X I --> X J --> X K --> X L --> X style C fill:#4A90E2,color:#fff style N fill:#E94B3C,color:#fff style X fill:#6ECB63,color:#fff style G fill:#F39C12,color:#fff

ECS 架构深度解析

什么是 ECS 架构?

ECS(Entity-Component-System)是一种源自游戏引擎的设计模式,它彻底改变了传统面向对象的继承体系,转而采用组合优于继承的理念。

三大核心概念:

  1. Entity(实体) - 仅是一个唯一 ID,不包含任何数据和逻辑
  2. Component(组件) - 纯数据结构,描述实体的属性(如位置、颜色、大小)
  3. System(系统) - 纯逻辑处理单元,操作特定组件组合的实体
graph TB subgraph "传统 OOP 继承方式" A1[GameObject] A1 --> A2[Rectangle] A1 --> A3[Circle] A1 --> A4[Text] A2 --> A5[DraggableRectangle] A3 --> A6[SelectableCircle] style A1 fill:#ff9999 end subgraph "ECS 组合方式" B1[Entity 123] -.拥有.-> B2[Position] B1 -.拥有.-> B3[Size] B1 -.拥有.-> B4[Color] B5[Entity 456] -.拥有.-> B6[Position] B5 -.拥有.-> B7[Font] B5 -.拥有.-> B8[Selected] B9[RenderSystem] --> B2 & B3 & B4 B10[DragSystem] --> B2 B11[SelectionSystem] --> B8 style B1 fill:#99ccff style B5 fill:#99ccff style B9 fill:#99ff99 style B10 fill:#99ff99 style B11 fill:#99ff99 end

ECS 架构的核心优势

1. 极致的解耦性

传统 OOP 中,功能通过继承链紧密耦合。而 ECS 中,系统只依赖组件接口,实体的行为完全由组件组合决定。

typescript 复制代码
// ❌ 传统方式:紧耦合的继承链
class Shape {
  render() { /* ... */ }
}
class DraggableShape extends Shape {
  drag() { /* ... */ }
}
class SelectableDraggableShape extends DraggableShape {
  select() { /* ... */ }
}

// ✅ ECS 方式:组件自由组合
const rect = createEntity()
addComponent(rect, Position, { x: 100, y: 100 })
addComponent(rect, Size, { width: 200, height: 150 })
addComponent(rect, Draggable, {})  // 可拖拽
addComponent(rect, Selected, {})   // 可选中
2. 强大的可扩展性

新增功能无需修改现有代码,只需添加新的组件和系统:

3. 天然的并行处理能力

系统之间无共享状态,可以安全地并行执行:

typescript 复制代码
// 多个系统可以同时读取同一个组件
async function updateFrame() {
  await Promise.all([
    physicsSystem.update(),   // 读取 Position
    renderSystem.update(),    // 读取 Position
    collisionSystem.update(), // 读取 Position
  ])
}
System 系统架构

系统负责处理逻辑,通过查询 StateStore 获取需要的组件数据:

typescript 复制代码
abstract class System {
  abstract update(stateStore: StateStore): void
}

class RenderSystem extends System {
  update(stateStore: StateStore) {
    // 查询所有拥有 Position 组件的实体
    for (const [entityId, position] of stateStore.position) {
      const size = stateStore.size.get(entityId)
      const color = stateStore.color.get(entityId)
      const type = stateStore.type.get(entityId)
      
      // 根据类型调用对应的渲染器
      this.renderMap.get(type)?.draw(entityId)
    }
  }
}

系统完整列表:

graph TB A[EventSystem
事件总线] --> B[InputSystem
输入捕获] A --> C[HoverSystem
悬停检测] A --> D[ClickSystem
点击处理] A --> E[DragSystem
拖拽逻辑] A --> F[SelectionSystem
选择管理] A --> G[ZoomSystem
缩放控制] A --> H[ScrollSystem
滚动平移] A --> I[PickingSystem
图形拾取] A --> J[RenderSystem
渲染绘制] A --> K[FpsSystem
性能监控] style A fill:#F39C12,color:#fff style J fill:#E74C3C,color:#fff style I fill:#3498DB,color:#fff

双引擎架构设计

架构设计理念

不同的应用场景对渲染引擎有不同的需求:

  • 简单场景:需要快速启动、体积小、兼容性好
  • 复杂场景:需要高性能、丰富特效、大量图形

传统方案通常只支持单一渲染后端,难以兼顾两者。本引擎采用双引擎可切换架构,在运行时动态选择最优渲染后端。

graph TB A[应用启动] --> B{检测场景复杂度} B -->|简单场景
< 100 图形| C[Canvas2D 引擎] B -->|复杂场景
> 100 图形| D[CanvasKit 引擎] B -->|用户手动指定| E[用户选择] C --> F[浏览器原生 API] D --> G[Skia WASM 引擎] C --> H[渲染输出] D --> H I[运行时切换] -.->|热切换| C I -.->|热切换| D style C fill:#90EE90 style D fill:#87CEEB style H fill:#FFD700

渲染后端对比

特性 Canvas2D CanvasKit (Skia)
启动速度 ⚡️ 即时(0ms) 🐢 需加载 WASM(~2s)
包体积 ✅ 0 KB ⚠️ ~1.5 MB
浏览器兼容性 ✅ 100% ⚠️ 需支持 WASM
渲染性能 🟡 中等 🟢 优秀
复杂路径渲染 🟡 一般 🟢 优秀
文字渲染 🟡 质量一般 🟢 亚像素级
滤镜特效 ❌ 有限 ✅ 丰富
离屏渲染 ✅ 支持 ✅ 支持
最佳场景 简单图形、快速原型 复杂设计、高性能需求

RendererManager 渲染管理器

RendererManager 是双引擎架构的核心枢纽,负责渲染器的注册、切换和调度:

typescript 复制代码
class RendererManager {
  rendererName: 'Canvas2D' | 'Canvaskit' = 'Canvaskit'
  
  // 渲染器映射表
  renderer: {
    rect: typeof RectRender
    ellipse: typeof EllipseRender
    text: typeof TextRender
    img: typeof ImgRender
    polygon: typeof PolygonRender
  }
  
  // 切换渲染后端
  setRenderer(name: 'Canvas2D' | 'Canvaskit') {
    this.rendererName = name
    
    if (name === 'Canvas2D') {
      this.renderer = Canvas2DRenderers
    } else {
      this.renderer = CanvaskitRenderers
    }
  }
}

渲染器切换流程:

sequenceDiagram participant U as 用户操作 participant E as Engine participant RM as RendererManager participant RS as RenderSystem participant R1 as Canvas2D Renderer participant R2 as CanvasKit Renderer U->>E: setRenderer('Canvas2D') E->>RM: setRenderer('Canvas2D') RM->>RM: 加载 Canvas2D 渲染器组 RM-->>E: 切换完成 E->>RS: 触发重新渲染 RS->>RM: 获取 rect 渲染器 RM-->>RS: 返回 Canvas2D.RectRender RS->>R1: 调用 draw() 方法 R1->>R1: 使用 ctx.fillRect() Note over U,R2: 用户再次切换引擎 U->>E: setRenderer('Canvaskit') E->>RM: setRenderer('Canvaskit') RM->>RM: 加载 CanvasKit 渲染器组 RM-->>E: 切换完成 E->>RS: 触发重新渲染 RS->>RM: 获取 rect 渲染器 RM-->>RS: 返回 CanvasKit.RectRender RS->>R2: 调用 draw() 方法 R2->>R2: 使用 canvas.drawRect()

渲染器统一接口

所有渲染器实现相同的接口,保证可替换性:

typescript 复制代码
abstract class BaseRenderer extends System {
  constructor(protected engine: Engine) {
    super()
  }
  
  // 统一的渲染接口
  abstract draw(entityId: string): void
  
}

自定义渲染器扩展

引擎支持用户自定义渲染器,只需实现 System 接口:

typescript 复制代码
// 1. 创建自定义渲染器
class CustomStarRender extends System {
  draw(entityId: string) {
    const points = this.getComponent<Polygon>(entityId, 'polygon')
    const color = this.getComponent<Color>(entityId, 'color')
    
    // 自定义绘制逻辑
    const ctx = this.engine.ctx
    ctx.beginPath()
    points.points.forEach((p, i) => {
      i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)
    })
    ctx.closePath()
    ctx.fillStyle = color.fill
    ctx.fill()
  }
}
const customRenderMap = {
  star: CustomStarRender
}
// 2. 注册到引擎
new RendererRegistry().register({
  "custom": customRenderMap
})

字体渲染优化

CanvasKit 需要预加载字体文件,引擎实现了字体管理器:

typescript 复制代码
async function loadFonts(CanvasKit: any) {
  const fontsBase = import.meta.env?.MODE === 'production' 
    ? '/design/fonts/' 
    : '/fonts/'

  const [robotoFont, notoSansFont] = await Promise.all([
    fetch(`${fontsBase}Roboto-Regular.ttf`).then(r => r.arrayBuffer()),
    fetch(`${fontsBase}NotoSansSC-VariableFont_wght_2.ttf`).then(r => r.arrayBuffer()),
  ])

  const fontMgr = CanvasKit.FontMgr.FromData(robotoFont, notoSansFont)
  return fontMgr
}

// 在 CanvasKit 初始化时调用
export async function createCanvasKit() {
  const CanvasKit = await initCanvasKit()
  const FontMgr = await loadFonts(CanvasKit)
  return { CanvasKit, FontMgr }
}

引擎工厂模式

使用工厂函数创建不同配置的引擎实例:

typescript 复制代码
export function createCanvasRenderer(engine: Engine) {
  // Canvas2D 引擎创建器
  const createCanvas2D = (config: DefaultConfig) => {
    const canvas = document.createElement('canvas')
    const dpr = window.devicePixelRatio || 1
    canvas.style.width = config.width + 'px'
    canvas.style.height = config.height + 'px'
    canvas.width = config.width * dpr
    canvas.height = config.height * dpr
    
    const ctx = canvas.getContext('2d', {
      willReadFrequently: true,
    }) as CanvasRenderingContext2D
    ctx.scale(dpr, dpr)
    
    config.container.appendChild(canvas)
    
    return { canvasDom: canvas, canvas: ctx, ctx }
  }

  // CanvasKit 引擎创建器
  const createCanvasKitSkia = async (config: DefaultConfig) => {
    const { CanvasKit, FontMgr } = await createCanvasKit()
    const canvasDom = document.createElement('canvas')
    const dpr = window.devicePixelRatio || 1
    
    canvasDom.style.width = config.width + 'px'
    canvasDom.style.height = config.height + 'px'
    canvasDom.width = config.width * dpr
    canvasDom.height = config.height * dpr
    canvasDom.id = 'canvasKitCanvas'
    
    config.container.appendChild(canvasDom)
    
    const surface = CanvasKit.MakeWebGLCanvasSurface('canvasKitCanvas')
    const canvas = surface!.getCanvas()
    
    return {
      canvasDom,
      surface,
      canvas: canvas,
      FontMgr: FontMgr,
      ck: CanvasKit,
    }
  }

  return {
    createCanvas2D,
    createCanvasKitSkia,
  }
}

Engine 引擎核心

Engine 类是整个渲染系统的中枢,协调所有子系统的运行:

typescript 复制代码
class Engine implements EngineContext {
  camera: Camera = new Camera()
  entityManager: Entity = new Entity()
  SystemMap: Map<string, System> = new Map()
  rendererManager: RendererManager = new RendererManager()
  
  canvas!: Canvas  // 渲染画布(类型取决于渲染后端)
  ctx!: CanvasRenderingContext2D
  ck!: CanvasKit
  
  constructor(public core: Core, rendererName?: string) {
    // 初始化渲染器
    this.rendererManager.rendererName = rendererName || 'Canvaskit'
    this.rendererManager.setRenderer(this.rendererManager.rendererName)
  }
  
  // 添加系统
  addSystem(system: System) {
    this.system.push(system)
    this.SystemMap.set(system.constructor.name, system)
  }
  
  // 获取系统
  getSystemByName<T extends System>(name: string): T | undefined {
    return this.SystemMap.get(name) as T
  }
  
  // 清空画布(适配双引擎)
  clear() {
    const canvas = this.canvas as any
    if (canvas?.clearRect) {
      // Canvas2D 清空方式
      canvas.clearRect(0, 0, this.defaultSize.width, this.defaultSize.height)
    } else {
      // CanvasKit 清空方式
      this.canvas.clear(this.ck.WHITE)
    }
  }
}

插件化系统设计

系统即插件

引擎的所有功能都以 System 形式实现,每个 System 都是独立的插件。这种设计带来极高的灵活性:

graph TB A[Engine 核心] --> B{System Manager} B --> C[核心系统] B --> D[可选系统] B --> E[自定义系统] C --> C1[EventSystem
必需] C --> C2[RenderSystem
必需] D --> D1[DragSystem
拖拽功能] D --> D2[ZoomSystem
缩放功能] D --> D3[FpsSystem
性能监控] E --> E1[UndoRedoSystem
撤销重做] E --> E2[SnappingSystem
吸附对齐] E --> E3[AnimationSystem
动画播放] style C1 fill:#e74c3c,color:#fff style C2 fill:#e74c3c,color:#fff style D1 fill:#3498db,color:#fff style D2 fill:#3498db,color:#fff style D3 fill:#3498db,color:#fff style E1 fill:#2ecc71,color:#fff style E2 fill:#2ecc71,color:#fff style E3 fill:#2ecc71,color:#fff

核心系统详解

1. EventSystem - 事件总线

EventSystem 是整个引擎的调度中枢,协调所有其他系统的执行:

typescript 复制代码
class EventSystem extends System {
  private eventQueue: Event[] = []
  
  update(stateStore: StateStore) {
    // 执行系统更新顺序
    this.executeSystem('InputSystem')      // 1. 捕获输入
    this.executeSystem('HoverSystem')      // 2. 检测悬停
    this.executeSystem('ClickSystem')      // 3. 处理点击
    this.executeSystem('DragSystem')       // 4. 处理拖拽
    this.executeSystem('ZoomSystem')       // 5. 处理缩放
    this.executeSystem('SelectionSystem')  // 6. 更新选择
    this.executeSystem('PickingSystem')    // 7. 更新拾取缓存
    this.executeSystem('RenderSystem')     // 8. 最后渲染
  }

}
2. RenderSystem - 渲染系统

RenderSystem 负责将实体绘制到画布:

typescript 复制代码
class RenderSystem extends System {
  private renderMap = new Map<string, BaseRenderer>()
  
  constructor(engine: Engine) {
    super()
    this.engine = engine
    this.initRenderMap()
  }
  
  // 初始化渲染器映射
  initRenderMap() {
    Object.entries(this.engine.rendererManager.renderer).forEach(
      ([type, RendererClass]) => {
        this.renderMap.set(type, new RendererClass(this.engine))
      }
    )
  }
  
  async update(stateStore: StateStore) {
    // 清空画布
    this.engine.clear()
    
    // 应用相机变换
    this.engine.canvas.save()
    this.engine.canvas.translate(
      this.engine.camera.translateX,
      this.engine.camera.translateY
    )
    this.engine.canvas.scale(
      this.engine.camera.zoom,
      this.engine.camera.zoom
    )
    
    // 遍历所有实体进行渲染
    for (const [entityId, pos] of stateStore.position) {
      this.engine.canvas.save()
      this.engine.canvas.translate(pos.x, pos.y)
      
      const type = stateStore.type.get(entityId)
      await this.renderMap.get(type)?.draw(entityId)
      
      this.engine.canvas.restore()
    }
    
    this.engine.canvas.restore()
  }
}

DSL 配置系统


DSL 配置系统

设计目标

DSL(Domain Specific Language)模块的目标是将图形场景序列化为 JSON 格式,实现:

  1. 场景持久化 - 保存到数据库或本地存储
  2. 场景传输 - 前后端数据交换
  3. 场景快照 - 撤销/重做功能的基础
  4. 模板复用 - 创建可复用的图形模板

配置结构

typescript 复制代码
interface DSLParams {
  type: 'rect' | 'ellipse' | 'text' | 'img' | 'polygon'
  id?: string
  position: { x: number; y: number }
  size?: { width: number; height: number }
  color?: { fill: string; stroke: string }
  rotation?: { value: number }
  scale?: { value: number }
  zIndex?: { value: number }
  selected?: { isSelected: boolean }
  // 形状特定属性
  font?: { family: string; size: number; weight: string }
  radius?: { value: number }
  polygon?: { points: Point[] }
}

DSL 解析器

typescript 复制代码
class DSL {
  constructor(params: DSLParams) {
    this.type = params.type
    this.id = params.id || this.generateId()
    this.position = new Position(params.position)
    this.size = params.size ? new Size(params.size) : new Size()
    this.color = params.color ? new Color(params.color) : new Color()
    // ... 初始化其他组件
  }
  
  // 转换为纯数据对象
  toJSON(): DSLParams {
    return {
      type: this.type,
      id: this.id,
      position: { x: this.position.x, y: this.position.y },
      size: { width: this.size.width, height: this.size.height },
      color: { fill: this.color.fill, stroke: this.color.stroke },
      // ...
    }
  }
}

低耦合架构实践

依赖方向

整个引擎严格遵循依赖倒置原则:

graph TB A[应用层
React 组件] --> B[引擎接口
Engine API] B --> C[系统层
System] C --> D[组件层
Component] C --> E[实体层
Entity] F[渲染层
Renderer] --> G[渲染接口
BaseRenderer] C --> G style B fill:#f39c12,color:#fff style G fill:#f39c12,color:#fff

关键设计:

  • 上层依赖接口,不依赖具体实现
  • System 不直接依赖 Renderer,通过 RendererManager 解耦
  • Component 纯数据,零依赖

总结

Duck-Core 前端渲染引擎通过以下设计实现了高性能、高扩展性:

核心优势

  1. ECS 架构 - 数据与逻辑完全分离,组件自由组合
  2. 双引擎架构 - Canvas2D 与 CanvasKit 可热切换,兼顾兼容性与性能
  3. 插件化系统 - 所有功能以 System 形式实现,按需加载
  4. 低耦合设计 - 接口隔离、依赖倒置、事件驱动
  5. 极致性能 - 渲染节流、离屏缓存、视口裁剪、内存优化
相关推荐
宋辰月2 小时前
学习react第三天
前端·学习·react.js
bug总结2 小时前
更新原生小程序封装(新增缓存订阅)完美解决
前端·缓存·小程序
GISer_Jing2 小时前
Node.js 开发实战:从入门到精通
javascript·后端·node.js
5335ld2 小时前
后端给的post 方法但是要求传表单数据格式(没有{})
开发语言·前端·javascript·vue.js·ecmascript
二川bro2 小时前
第33节:程序化生成与无限地形算法
前端·算法·3d·threejs
q***51892 小时前
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
java·spring boot·后端
QDKuz2 小时前
掌握Vue2转Vue3, Options API 转 Composition API
前端·javascript·vue.js
老前端的功夫3 小时前
前端Echarts性能优化:从卡顿到流畅的百万级数据可视化
前端·javascript
进击的野人3 小时前
深入解析localStorage:前端数据持久化的核心技术
前端·javascript