前端图形引擎架构设计:双引擎架构设计
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)是一种源自游戏引擎的设计模式,它彻底改变了传统面向对象的继承体系,转而采用组合优于继承的理念。
三大核心概念:
- Entity(实体) - 仅是一个唯一 ID,不包含任何数据和逻辑
- Component(组件) - 纯数据结构,描述实体的属性(如位置、颜色、大小)
- 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 中,系统只依赖组件接口,实体的行为完全由组件组合决定。
// ❌ 传统方式:紧耦合的继承链
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. 天然的并行处理能力
系统之间无共享状态,可以安全地并行执行:
// 多个系统可以同时读取同一个组件
async function updateFrame() {
await Promise.all([
physicsSystem.update(), // 读取 Position
renderSystem.update(), // 读取 Position
collisionSystem.update(), // 读取 Position
])
}
System 系统架构
系统负责处理逻辑,通过查询 StateStore 获取需要的组件数据:
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<br/>事件总线] --> B[InputSystem<br/>输入捕获]
A --> C[HoverSystem<br/>悬停检测]
A --> D[ClickSystem<br/>点击处理]
A --> E[DragSystem<br/>拖拽逻辑]
A --> F[SelectionSystem<br/>选择管理]
A --> G[ZoomSystem<br/>缩放控制]
A --> H[ScrollSystem<br/>滚动平移]
A --> I[PickingSystem<br/>图形拾取]
A --> J[RenderSystem<br/>渲染绘制]
A --> K[FpsSystem<br/>性能监控]
style A fill:#F39C12,color:#fff
style J fill:#E74C3C,color:#fff
style I fill:#3498DB,color:#fff
双引擎架构设计
架构设计理念
不同的应用场景对渲染引擎有不同的需求:
- 简单场景:需要快速启动、体积小、兼容性好
- 复杂场景:需要高性能、丰富特效、大量图形
传统方案通常只支持单一渲染后端,难以兼顾两者。本引擎采用双引擎可切换架构,在运行时动态选择最优渲染后端。
graph TB
A[应用启动] --> B{检测场景复杂度}
B -->|简单场景<br/>< 100 图形| C[Canvas2D 引擎]
B -->|复杂场景<br/>> 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 是双引擎架构的核心枢纽,负责渲染器的注册、切换和调度:
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()
渲染器统一接口
所有渲染器实现相同的接口,保证可替换性:
abstract class BaseRenderer extends System {
constructor(protected engine: Engine) {
super()
}
// 统一的渲染接口
abstract draw(entityId: string): void
}
自定义渲染器扩展
引擎支持用户自定义渲染器,只需实现 System 接口:
// 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 需要预加载字体文件,引擎实现了字体管理器:
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 }
}
引擎工厂模式
使用工厂函数创建不同配置的引擎实例:
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 类是整个渲染系统的中枢,协调所有子系统的运行:
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<br/>必需]
C --> C2[RenderSystem<br/>必需]
D --> D1[DragSystem<br/>拖拽功能]
D --> D2[ZoomSystem<br/>缩放功能]
D --> D3[FpsSystem<br/>性能监控]
E --> E1[UndoRedoSystem<br/>撤销重做]
E --> E2[SnappingSystem<br/>吸附对齐]
E --> E3[AnimationSystem<br/>动画播放]
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 是整个引擎的调度中枢,协调所有其他系统的执行:
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 负责将实体绘制到画布:
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 格式,实现:
- 场景持久化 - 保存到数据库或本地存储
- 场景传输 - 前后端数据交换
- 场景快照 - 撤销/重做功能的基础
- 模板复用 - 创建可复用的图形模板
配置结构
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 解析器
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[应用层<br/>React 组件] --> B[引擎接口<br/>Engine API]
B --> C[系统层<br/>System]
C --> D[组件层<br/>Component]
C --> E[实体层<br/>Entity]
F[渲染层<br/>Renderer] --> G[渲染接口<br/>BaseRenderer]
C --> G
style B fill:#f39c12,color:#fff
style G fill:#f39c12,color:#fff
关键设计:
- 上层依赖接口,不依赖具体实现
- System 不直接依赖 Renderer,通过 RendererManager 解耦
- Component 纯数据,零依赖
总结
Duck-Core 前端渲染引擎通过以下设计实现了高性能、高扩展性:
核心优势
- ECS 架构 - 数据与逻辑完全分离,组件自由组合
- 双引擎架构 - Canvas2D 与 CanvasKit 可热切换,兼顾兼容性与性能
- 插件化系统 - 所有功能以 System 形式实现,按需加载
- 低耦合设计 - 接口隔离、依赖倒置、事件驱动
- 极致性能 - 渲染节流、离屏缓存、视口裁剪、内存优化