《前端圖形引擎架構設計:基於ECS模式的可擴展渲染系統》
《前端圖形引擎架構設計:AI生成設計稿落地實踐》
GitHub
之前寫過一篇ECS文章,為什麼還要再寫一個,本質上因為之前的文檔,截至到目前為止,變化巨大,底層已經改了很多很多,所以有必要把一些內容提取出來單獨去說明。
由於字體文件較大,載入時間會比較久😞,可以把項目clone下來本地運行會比較快。
另外如果有性能問題,我會及時修復,引擎改造時間太倉促,只要不是內存洩漏,暫時沒去處理。
還有很多東西要做。
體驗地址:baiyuze.github.io/design/#/ca…


Duck-Core 是一個基於 ECS(Entity-Component-System)架構構建的高性能 Canvas 渲染引擎,專為複雜圖形編輯場景設計。引擎的核心特色在於雙渲染後端架構、插件化系統設計和極致的渲染性能優化。
整個引擎採用分層架構,從底層的渲染抽象到頂層的用戶互動,每一層職責清晰且可獨立替換。
ECS(Entity-Component-System)是一種源自遊戲引擎的設計模式,它徹底改變了傳統面向對象的繼承體系,轉而採用組合優於繼承的理念。
三大核心概念:
傳統 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, {}) // 可選中
新增功能無需修改現有程式碼,只需添加新的元件和系統:

系統之間無共享狀態,可以安全地並行執行:
// 多個系統可以同時讀取同一個元件
async function updateFrame() {
await Promise.all([
physicsSystem.update(), // 讀取 Position
renderSystem.update(), // 讀取 Position
collisionSystem.update(), // 讀取 Position
])
}
系統負責處理邏輯,通過查詢 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)
}
}
}
系統完整列表:
不同的應用場景對渲染引擎有不同的需求:
傳統方案通常只支持單一渲染後端,難以兼顧兩者。本引擎採用雙引擎可切換架構,在運行時動態選擇最優渲染後端。
| 特性 | Canvas2D | CanvasKit (Skia) |
|---|---|---|
| 啟動速度 | ⚡️ 即時(0ms) | 🐢 需載入 WASM(~2s) |
| 包體積 | ✅ 0 KB | ⚠️ ~1.5 MB |
| 瀏覽器兼容性 | ✅ 100% | ⚠️ 需支持 WASM |
| 渲染性能 | 🟡 中等 | 🟢 優秀 |
| 複雜路徑渲染 | 🟡 一般 | 🟢 優秀 |
| 文字渲染 | 🟡 質量一般 | 🟢 亞像素級 |
| 濾鏡特效 | ❌ 有限 | ✅ 豐富 |
| 離屏渲染 | ✅ 支持 | ✅ 支持 |
| 最佳場景 | 簡單圖形、快速原型 | 複雜設計、高性能需求 |
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
}
}
}
渲染器切換流程:
所有渲染器實現相同的介面,保證可替換性:
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,
FontMgr,
ck: CanvasKit,
}
}
return {
createCanvas2D,
createCanvasKitSkia,
}
}
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 都是獨立的插件。這種設計帶來極高的靈活性:
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. 最後渲染
}
}
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(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[] }
}
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 },
// ...
}
}
}
整個引擎嚴格遵循依賴倒置原則:
關鍵設計:
Duck-Core 前端渲染引擎通過以下設計實現了高性能、高擴展性: