前端图形引擎架构设计:基于ECS架构的可扩展渲染系统

1,171 阅读6分钟

系列

《前端图形引擎架构设计:AI生成设计稿落地实践》

《前端图形引擎架构设计:可扩展双渲染引擎架构设计-支持自定义渲染器》

GitHub

概述

本文档详细介绍前端画布系统的核心功能实现,基于 ECS(Entity-Component-System)架构,提供高性能的图形渲染和交互能力。

项目处于早期开发阶段,代码可能会存在和文章不符的情况,以最新代码为准。

项目地址:github.com/baiyuze/duc…

渲染效果:

录屏2025-10-31 15.02.09.gif

d881e431f261ba2aca0ed90f9ca7635f.png

核心功能模块

graph TB
    A[前端核心功能] --> B[ECS渲染引擎]
    A --> C[图形拾取系统]
    A --> D[选择交互系统]
    A --> E[输入处理系统]
    A --> F[事件管理系统]
    A --> G[DSL解析系统]
    
    B --> B1[RenderSystem]
    B --> B2[渲染器注册表]
    B --> B3[多种图形渲染器]
    
    C --> C1[PickingSystem]
    C --> C2[颜色编码算法]
    C --> C3[离屏Canvas]
    
    D --> D1[SelectionSystem]
    D --> D2[单选/多选]
    D --> D3[拖拽功能]
    
    E --> E1[InputSystem]
    E --> E2[鼠标事件]
    E --> E3[键盘事件]
    
    F --> F1[EventSystem]
    F --> F2[事件队列]
    F --> F3[事件分发]
    
    G --> G1[DSL类]
    G --> G2[配置验证]
    G --> G3[组件实例化]

ECS 渲染引擎

渲染流程架构

sequenceDiagram
    participant M as 主循环
    participant E as EventSystem
    participant R as RenderSystem
    participant RR as RenderRegistry
    participant RE as Renderer
    participant C as Canvas
    
    M->>E: 1. 处理事件队列
    E->>E: 处理用户交互事件
    M->>R: 2. 触发渲染更新
    R->>R: 节流检查(100ms)
    R->>C: 3. 清空画布
    
    loop 遍历所有实体
        R->>R: 获取实体type
        R->>RR: 查找对应渲染器
        RR->>RE: 返回渲染器实例
        RE->>RE: 读取组件数据
        RE->>C: 绘制图形
    end
    
    M->>M: requestAnimationFrame

渲染系统实现

RenderSystem 架构

graph LR
    A[RenderSystem] --> B[RenderMap]
    B --> C[rect: RectRenderer]
    B --> D[ellipse: EllipseRenderer]
    B --> E[text: TextRenderer]
    B --> F[img: ImageRenderer]
    B --> G[polygon: PolygonRenderer]
    
    H[StateStore] --> A
    A --> I[throttledRender]
    I --> J[render方法]
    J --> K[drawShape]
    K --> C
    K --> D
    K --> E
    K --> F
    K --> G

RenderSystem 核心逻辑

export class RenderSystem extends System {
  core: Core;
  ctx: CanvasRenderingContext2D;
  renderMap = new Map<string, System>();

  constructor(ctx: CanvasRenderingContext2D, core: Core) {
    super();
    this.core = core;
    this.ctx = ctx;
    this.initRenderMap();
  }

  // 初始化渲染器映射表
  initRenderMap() {
    Object.entries(renderRegistry).forEach(([key, SystemClass]) => {
      this.renderMap.set(key, new SystemClass(this.ctx, this.core));
    });
  }

  // 节流渲染(100ms)
  throttledRender = throttle((stateStore: StateStore) => {
    this.render(stateStore, this.ctx);
  }, 100);

  // 绘制单个图形
  drawShape(stateStore: StateStore, entityId: string) {
    const type = stateStore.type.get(entityId);
    if (!type) return;
    
    const renderer = this.renderMap.get(type);
    renderer?.draw(entityId);
  }

  // 主渲染方法
  render(stateStore: StateStore, ctx: CanvasRenderingContext2D) {
    // 清空画布
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

    // 遍历所有实体并渲染
    stateStore.position.forEach((pos, entityId) => {
      ctx.save();
      this.drawShape(stateStore, entityId);
      ctx.restore();
    });
  }

  // 每帧更新
  update(stateStore: StateStore) {
    this.throttledRender(stateStore);
  }
}

渲染器实现

渲染器架构设计

graph TB
    subgraph "渲染器基类"
        A[System基类]
    end
    
    subgraph "具体渲染器"
        B[RectRenderer<br/>矩形渲染]
        C[EllipseRenderer<br/>椭圆渲染]
        D[TextRenderer<br/>文本渲染]
        E[ImageRenderer<br/>图片渲染]
        F[PolygonRenderer<br/>多边形渲染]
    end
    
    subgraph "渲染流程"
        G[读取Position]
        H[读取Size]
        I[读取Color]
        J[读取其他组件]
        K[Canvas绘制API]
    end
    
    A --> B
    A --> C
    A --> D
    A --> E
    A --> F
    
    B --> G
    B --> H
    B --> I
    B --> K
    
    C --> G
    C --> H
    C --> I
    C --> K
    
    D --> G
    D --> J
    D --> K
    
    E --> G
    E --> H
    E --> J
    E --> K

矩形渲染器

export class RectRenderer extends System {
  ctx: CanvasRenderingContext2D;
  core: Core;

  constructor(ctx: CanvasRenderingContext2D, core: Core) {
    super();
    this.ctx = ctx;
    this.core = core;
  }

  draw(entityId: string) {
    const position = this.core.stateStore.position.get(entityId);
    const size = this.core.stateStore.size.get(entityId);
    const color = this.core.stateStore.color.get(entityId);
    const rotation = this.core.stateStore.rotation.get(entityId);

    if (!position || !size || !color) return;

    this.ctx.save();

    // 应用旋转
    if (rotation) {
      const centerX = position.x + size.width / 2;
      const centerY = position.y + size.height / 2;
      this.ctx.translate(centerX, centerY);
      this.ctx.rotate((rotation.value * Math.PI) / 180);
      this.ctx.translate(-centerX, -centerY);
    }

    // 填充
    if (color.fillColor) {
      this.ctx.fillStyle = color.fillColor;
      this.ctx.fillRect(position.x, position.y, size.width, size.height);
    }

    // 描边
    if (color.strokeColor) {
      const lineWidth = this.core.stateStore.lineWidth.get(entityId);
      this.ctx.strokeStyle = color.strokeColor;
      this.ctx.lineWidth = lineWidth?.value || 1;
      this.ctx.strokeRect(position.x, position.y, size.width, size.height);
    }

    this.ctx.restore();
  }
}

椭圆渲染器

export class EllipseRenderer extends System {
  draw(entityId: string) {
    const position = this.core.stateStore.position.get(entityId);
    const size = this.core.stateStore.size.get(entityId);
    const color = this.core.stateStore.color.get(entityId);

    if (!position || !size || !color) return;

    const centerX = position.x + size.width / 2;
    const centerY = position.y + size.height / 2;
    const radiusX = size.width / 2;
    const radiusY = size.height / 2;

    this.ctx.beginPath();
    this.ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, 2 * Math.PI);

    if (color.fillColor) {
      this.ctx.fillStyle = color.fillColor;
      this.ctx.fill();
    }

    if (color.strokeColor) {
      this.ctx.strokeStyle = color.strokeColor;
      this.ctx.stroke();
    }
  }
}

文本渲染器

export class TextRenderer extends System {
  draw(entityId: string) {
    const position = this.core.stateStore.position.get(entityId);
    const font = this.core.stateStore.font.get(entityId);

    if (!position || !font) return;

    this.ctx.save();

    // 设置字体样式
    this.ctx.font = `${font.weight} ${font.size}px ${font.family}`;
    this.ctx.fillStyle = font.fillColor;
    this.ctx.textBaseline = 'top';

    // 绘制文本
    this.ctx.fillText(font.text, position.x, position.y);

    this.ctx.restore();
  }
}

图片渲染器

export class ImageRenderer extends System {
  private imageCache = new Map<string, HTMLImageElement>();

  draw(entityId: string) {
    const position = this.core.stateStore.position.get(entityId);
    const size = this.core.stateStore.size.get(entityId);
    const img = this.core.stateStore.img.get(entityId);

    if (!position || !size || !img) return;

    let image = this.imageCache.get(img.src);

    if (!image) {
      image = new Image();
      image.src = img.src;
      this.imageCache.set(img.src, image);

      image.onload = () => {
        this.ctx.drawImage(image!, position.x, position.y, size.width, size.height);
      };
    } else if (image.complete) {
      this.ctx.drawImage(image, position.x, position.y, size.width, size.height);
    }
  }
}

图形拾取系统

拾取系统架构

graph TB
    subgraph "拾取系统设计"
        A[PickingSystem]
        B[离屏Canvas]
        C[颜色映射表]
    end
    
    subgraph "拾取流程"
        D[1. 为实体分配唯一颜色]
        E[2. 在离屏Canvas绘制]
        F[3. 读取点击位置像素]
        G[4. 颜色反查实体ID]
    end
    
    subgraph "颜色编码算法"
        H[实体索引 index]
        I[转RGB颜色]
        J[绘制到离屏]
        K[点击获取RGB]
        L[RGB转索引]
        M[返回实体ID]
    end
    
    A --> B
    A --> C
    A --> D
    D --> E
    E --> F
    F --> G
    
    H --> I
    I --> J
    K --> L
    L --> M

拾取原理图

sequenceDiagram
    participant U as 用户点击
    participant P as PickingSystem
    participant O as 离屏Canvas
    participant M as ColorMap
    
    Note over P,O: 准备阶段
    P->>P: 为每个实体分配唯一颜色ID
    P->>M: 建立颜色→实体映射表
    
    Note over U,M: 点击阶段
    U->>P: 鼠标点击(x, y)
    P->>O: 渲染所有实体到离屏Canvas
    Note over O: 使用唯一颜色填充
    P->>O: getImageData(x, y, 1, 1)
    O->>P: 返回像素RGB值
    P->>M: 查询RGB对应的实体
    M->>P: 返回实体ID
    P->>U: 返回被点击的实体

PickingSystem 实现

export class PickingSystem extends System {
  core: Core;
  ctx: CanvasRenderingContext2D;
  offscreenCanvas: HTMLCanvasElement;
  offscreenCtx: CanvasRenderingContext2D;
  colorToEntityMap = new Map<string, string>();

  constructor(ctx: CanvasRenderingContext2D, core: Core) {
    super();
    this.core = core;
    this.ctx = ctx;

    // 创建离屏 Canvas
    this.offscreenCanvas = document.createElement('canvas');
    this.offscreenCanvas.width = ctx.canvas.width;
    this.offscreenCanvas.height = ctx.canvas.height;
    this.offscreenCtx = this.offscreenCanvas.getContext('2d')!;

    this.generateColorMap();
  }

  // 为每个实体生成唯一颜色
  generateColorMap() {
    let colorIndex = 1;
    this.core.stateStore.position.forEach((_, entityId) => {
      const color = this.indexToColor(colorIndex);
      this.colorToEntityMap.set(color, entityId);
      colorIndex++;
    });
  }

  // 索引转颜色
  indexToColor(index: number): string {
    const r = (index & 0xFF0000) >> 16;
    const g = (index & 0x00FF00) >> 8;
    const b = (index & 0x0000FF);
    return `rgb(${r},${g},${b})`;
  }

  // 颜色转索引
  colorToIndex(r: number, g: number, b: number): number {
    return (r << 16) | (g << 8) | b;
  }

  // 渲染到离屏 Canvas
  renderOffscreen() {
    this.offscreenCtx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);

    let colorIndex = 1;
    this.core.stateStore.position.forEach((position, entityId) => {
      const size = this.core.stateStore.size.get(entityId);
      if (!size) return;

      const color = this.indexToColor(colorIndex);
      this.offscreenCtx.fillStyle = color;
      this.offscreenCtx.fillRect(position.x, position.y, size.width, size.height);

      colorIndex++;
    });
  }

  // 拾取实体
  pick(x: number, y: number): string | null {
    this.renderOffscreen();

    const pixel = this.offscreenCtx.getImageData(x, y, 1, 1).data;
    const color = `rgb(${pixel[0]},${pixel[1]},${pixel[2]})`;

    return this.colorToEntityMap.get(color) || null;
  }
}

选择系统

选择系统架构

graph TB
    subgraph "选择模式"
        A[SelectionSystem]
        B[单选模式]
        C[多选模式Ctrl/Cmd]
        D[框选模式拖拽]
    end
    
    subgraph "选择状态"
        E[未选中 value:false]
        F[选中 value:true]
        G[悬停 hovered:true]
    end
    
    subgraph "视觉反馈"
        H[选择框绘制]
        I[控制点绘制]
        J[高亮显示]
    end
    
    A --> B
    A --> C
    A --> D
    
    B --> E
    B --> F
    C --> F
    
    F --> H
    F --> I
    G --> J

选择状态流转

stateDiagram-v2
    [*] --> 未选中
    
    未选中 --> 悬停: 鼠标移入
    悬停 --> 未选中: 鼠标移出
    
    悬停 --> 选中: 点击
    未选中 --> 选中: 直接点击
    
    选中 --> 拖拽中: 按住并移动
    拖拽中 --> 选中: 释放鼠标
    
    选中 --> 未选中: 点击空白区域
    选中 --> 多选: Ctrl+点击其他实体
    多选 --> 选中: Ctrl+点击已选实体
    
    多选 --> 未选中: 点击空白区域

SelectionSystem 实现

export class SelectionSystem extends System {
  core: Core;
  ctx: CanvasRenderingContext2D;

  constructor(ctx: CanvasRenderingContext2D, core: Core) {
    super();
    this.core = core;
    this.ctx = ctx;
  }

  // 选中实体
  selectEntity(entityId: string) {
    const selected = this.core.stateStore.selected.get(entityId);
    if (selected) {
      selected.value = true;
    }
  }

  // 取消选中
  deselectEntity(entityId: string) {
    const selected = this.core.stateStore.selected.get(entityId);
    if (selected) {
      selected.value = false;
    }
  }

  // 取消所有选中
  deselectAll() {
    this.core.stateStore.selected.forEach((selected) => {
      selected.value = false;
    });
  }

  // 绘制选择框
  drawSelectionBox(entityId: string) {
    const position = this.core.stateStore.position.get(entityId);
    const size = this.core.stateStore.size.get(entityId);
    const selected = this.core.stateStore.selected.get(entityId);

    if (!position || !size || !selected?.value) return;

    this.ctx.save();

    // 绘制选择框
    this.ctx.strokeStyle = '#0078D4';
    this.ctx.lineWidth = 2;
    this.ctx.setLineDash([5, 5]);
    this.ctx.strokeRect(
      position.x - 2,
      position.y - 2,
      size.width + 4,
      size.height + 4
    );

    // 绘制控制点
    this.drawHandles(position, size);

    this.ctx.restore();
  }

  // 绘制控制点
  drawHandles(position: Position, size: Size) {
    const handleSize = 8;
    const handles = [
      { x: position.x, y: position.y }, // 左上
      { x: position.x + size.width, y: position.y }, // 右上
      { x: position.x, y: position.y + size.height }, // 左下
      { x: position.x + size.width, y: position.y + size.height }, // 右下
    ];

    handles.forEach(handle => {
      this.ctx.fillStyle = '#FFFFFF';
      this.ctx.strokeStyle = '#0078D4';
      this.ctx.lineWidth = 2;
      this.ctx.fillRect(
        handle.x - handleSize / 2,
        handle.y - handleSize / 2,
        handleSize,
        handleSize
      );
      this.ctx.strokeRect(
        handle.x - handleSize / 2,
        handle.y - handleSize / 2,
        handleSize,
        handleSize
      );
    });
  }

  update(stateStore: StateStore) {
    stateStore.selected.forEach((selected, entityId) => {
      if (selected.value) {
        this.drawSelectionBox(entityId);
      }
    });
  }
}

输入系统

输入系统架构

graph TB
    subgraph "输入源"
        A[鼠标事件]
        B[键盘事件]
        C[触摸事件]
    end
    
    subgraph "InputSystem"
        D[事件监听器]
        E[事件处理器]
        F[状态管理]
    end
    
    subgraph "鼠标事件处理"
        G[mousedown]
        H[mousemove]
        I[mouseup]
        J[click]
    end
    
    subgraph "交互功能"
        K[选择实体]
        L[拖拽移动]
        M[缩放控制]
        N[旋转操作]
    end
    
    A --> D
    B --> D
    C --> D
    
    D --> E
    E --> F
    
    E --> G
    E --> H
    E --> I
    E --> J
    
    G --> L
    H --> L
    I --> L
    J --> K

拖拽交互流程

sequenceDiagram
    participant U as 用户
    participant I as InputSystem
    participant P as PickingSystem
    participant S as StateStore
    participant R as RenderSystem
    
    U->>I: mousedown(x, y)
    I->>P: pick(x, y)
    P->>I: 返回entityId
    I->>I: 记录拖拽开始位置
    I->>I: isDragging = true
    
    loop 鼠标移动
        U->>I: mousemove(x, y)
        I->>I: 计算偏移量(dx, dy)
        I->>S: 更新Position组件
        S->>R: 触发重绘
    end
    
    U->>I: mouseup
    I->>I: isDragging = false
    I->>I: 清空拖拽状态

InputSystem 实现

export class InputSystem extends System {
  canvas: HTMLCanvasElement;
  core: Core;
  pickingSystem: PickingSystem;
  isDragging = false;
  dragStartPos: { x: number; y: number } | null = null;
  selectedEntity: string | null = null;

  constructor(canvas: HTMLCanvasElement, core: Core, pickingSystem: PickingSystem) {
    super();
    this.canvas = canvas;
    this.core = core;
    this.pickingSystem = pickingSystem;
    this.bindEvents();
  }

  bindEvents() {
    this.canvas.addEventListener('mousedown', this.handleMouseDown);
    this.canvas.addEventListener('mousemove', this.handleMouseMove);
    this.canvas.addEventListener('mouseup', this.handleMouseUp);
    this.canvas.addEventListener('click', this.handleClick);
  }

  handleClick = (e: MouseEvent) => {
    const rect = this.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    // 拾取实体
    const entityId = this.pickingSystem.pick(x, y);

    if (entityId) {
      // 如果按下 Ctrl/Cmd,则多选
      if (e.ctrlKey || e.metaKey) {
        const selected = this.core.stateStore.selected.get(entityId);
        if (selected) {
          selected.value = !selected.value;
        }
      } else {
        // 单选
        this.core.stateStore.selected.forEach((s) => (s.value = false));
        const selected = this.core.stateStore.selected.get(entityId);
        if (selected) {
          selected.value = true;
        }
      }
    } else {
      // 点击空白,取消所有选中
      this.core.stateStore.selected.forEach((s) => (s.value = false));
    }
  };

  handleMouseDown = (e: MouseEvent) => {
    const rect = this.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const entityId = this.pickingSystem.pick(x, y);

    if (entityId) {
      this.isDragging = true;
      this.selectedEntity = entityId;
      this.dragStartPos = { x, y };
    }
  };

  handleMouseMove = (e: MouseEvent) => {
    if (!this.isDragging || !this.selectedEntity || !this.dragStartPos) return;

    const rect = this.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const dx = x - this.dragStartPos.x;
    const dy = y - this.dragStartPos.y;

    // 更新位置
    const position = this.core.stateStore.position.get(this.selectedEntity);
    if (position) {
      position.x += dx;
      position.y += dy;
    }

    this.dragStartPos = { x, y };
  };

  handleMouseUp = () => {
    this.isDragging = false;
    this.selectedEntity = null;
    this.dragStartPos = null;
  };

  update(stateStore: StateStore) {
    // 输入系统主要是事件驱动,不需要每帧更新
  }
}

事件系统

事件系统架构

graph TB
    subgraph "事件源"
        A[InputSystem]
        B[SelectionSystem]
        C[业务逻辑]
    end
    
    subgraph "EventSystem"
        D[EventQueue事件队列]
        E[事件处理器]
        F[事件分发器]
    end
    
    subgraph "事件类型"
        G[entity:select 选中]
        H[entity:deselect 取消选中]
        I[entity:move 移动]
        J[entity:delete 删除]
        K[entity:resize 缩放]
        L[entity:rotate 旋转]
    end
    
    subgraph "系统响应"
        M[更新StateStore]
        N[触发重绘]
        O[执行业务逻辑]
    end
    
    A --> D
    B --> D
    C --> D
    
    D --> E
    E --> F
    
    F --> G
    F --> H
    F --> I
    F --> J
    F --> K
    F --> L
    
    G --> M
    H --> M
    I --> M
    J --> M
    
    M --> N

事件处理流程

sequenceDiagram
    participant I as InputSystem
    participant Q as EventQueue
    participant E as EventSystem
    participant H as EventHandler
    participant S as StateStore
    
    I->>Q: 添加事件
    Note over Q: {type: 'entity:move', data: {...}}
    
    loop 每帧更新
        E->>Q: 读取事件队列
        Q->>E: 返回事件列表
        
        loop 处理每个事件
            E->>E: switch(event.type)
            E->>H: 调用对应处理器
            H->>S: 更新组件数据
        end
        
        E->>Q: 清空已处理事件
    end

EventSystem 实现

import type { Core } from "../Core";
import { Entity } from "../Entity/Entity";
import type { StateStore } from "../types";
import type { ClickSystem } from "./ClickSystem";
import type { DragSystem } from "./DragSystem";
import type { HoverSystem } from "./HoverSystem";
import type { SelectionSystem } from "./SelectionSystem";
import { System } from "./System";
import { throttle } from "lodash";

export class EventSystem extends System {
  core: Core;
  ctx: CanvasRenderingContext2D;
  offCtx: CanvasRenderingContext2D | null = null;
  entityManager: Entity = new Entity();
  stateStore: StateStore | null = null;
  throttledMouseMove: ReturnType<typeof throttle>;

  constructor(ctx: CanvasRenderingContext2D, core: Core) {
    super();
    this.ctx = ctx;
    this.core = core;
    this.dispose();
    this.throttledMouseMove = throttle(this.onMouseMove.bind(this), 16);
    ctx.canvas.addEventListener("click", this.onClick.bind(this));
    ctx.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
    ctx.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
    document.addEventListener("mousemove", this.throttledMouseMove);
  }

  dispose() {
    this.ctx.canvas.removeEventListener("click", this.onClick.bind(this));
    document.removeEventListener("mousemove", this.throttledMouseMove);
    this.ctx.canvas.removeEventListener("mouseup", this.onMouseUp.bind(this));
    this.ctx.canvas.removeEventListener(
      "mousedown",
      this.onMouseDown.bind(this)
    );
    this.throttledMouseMove?.cancel();
  }

  onMouseUp(event: MouseEvent) {
    if (!this.stateStore) return;
    this.stateStore.eventQueue.push({
      type: "mouseup",
      event,
    });
    this.render();
  }
  onMouseDown(event: MouseEvent) {
    if (!this.stateStore) return;
    this.stateStore.eventQueue.push({
      type: "mousedown",
      event,
    });
    this.render();
  }

  nextTick(cb: () => void) {
    return Promise.resolve().then(cb);
  }

  update(stateStore: StateStore) {
    this.stateStore = stateStore;
  }

  render() {
    const core = this.core;
    const selectionSystem =
      core.getSystemByName<SelectionSystem>("SelectionSystem");
    const hoverSystem = core.getSystemByName<HoverSystem>("HoverSystem");
    const clickSystem = core.getSystemByName<ClickSystem>("ClickSystem");
    const dragSystem = core.getSystemByName<DragSystem>("DragSystem");

    if (!this.stateStore) return;
    if (hoverSystem) {
      hoverSystem.update(this.stateStore);
    }
    if (clickSystem) {
      clickSystem.update(this.stateStore);
    }
    if (selectionSystem) {
      selectionSystem.update(this.stateStore);
    }
    if (dragSystem) {
      dragSystem.update(this.stateStore);
    }
    this.stateStore.eventQueue = [];
  }
  /**
   * 点击
   * @param event MouseEvent
   * @returns
   */
  onClick(event: MouseEvent) {
    if (!this.stateStore) return;
    this.stateStore.eventQueue = [
      {
        type: "click",
        event,
      },
    ];
    this.render();
  }
  onMouseMove(event: MouseEvent) {
    if (!this.stateStore) return;
    if (this.stateStore.eventQueue.length) return;
    this.stateStore.eventQueue = [{ type: "mousemove", event }];
    this.render();
  }

  destroyed(): void {
    this.dispose();
    this.offCtx = null;
    this.stateStore = null;
    this.entityManager = null as any;
    this.core = null as any;
    this.ctx = null as any;
  }
}

DSL 解析器

DSL 解析架构

graph TB
    subgraph DSL配置
        A[JSON配置对象]
        B[必填字段]
        C[可选字段]
    end
    
    subgraph DSL解析器
        D[DSL构造器]
        E[字段验证]
        F[默认值填充]
        G[组件实例化]
    end
    
    subgraph 组件注册
        H[Position]
        I[Size]
        J[Color]
        K[Rotation]
        L[其他组件]
    end
    
    subgraph StateStore
        M[position Map]
        N[size Map]
        O[color Map]
        P[其他 Map]
    end
    
    A --> D
    B --> E
    C --> F
    D --> E
    E --> G
    
    G --> H
    G --> I
    G --> J
    G --> K
    G --> L
    
    H --> M
    I --> N
    J --> O
    L --> P

DSL 解析流程

sequenceDiagram
    participant C as JSON Config
    participant D as DSL Parser
    participant V as Validator
    participant S as StateStore
    participant E as Entity Manager
    
    C->>D: 传入配置对象
    D->>V: 验证必填字段
    
    alt 验证失败
        V->>D: 抛出错误
        D->>C: Error: 缺少必填字段
    else 验证成功
        V->>D: 验证通过
        D->>D: 填充默认值
        D->>D: 创建DSL实例
        
        loop 遍历组件属性
            D->>S: 将组件存入对应Map
            Note over S: position.set(id, {x, y})
            Note over S: size.set(id, {w, h})
            Note over S: color.set(id, {fill, stroke})
        end
        
        D->>E: 注册实体ID
        E->>D: 注册成功
        D->>C: 返回DSL实例
    end

DSL 类实现

export class DSL {
  id: string;
  type: string;
  position: Position;
  size: Size;
  color: Color;
  selected?: { value: boolean; hovered: boolean };
  rotation?: { value: number };
  font?: Font;
  lineWidth?: { value: number };
  img?: Img;
  scale?: Scale;
  polygon?: Polygon;
  ellipseRadius?: EllipseRadius;

  constructor(config: any) {
    this.id = config.id;
    this.type = config.type;
    this.position = config.position;
    this.size = config.size;
    this.color = config.color;
    this.selected = config.selected || { value: false, hovered: false };
    this.rotation = config.rotation || { value: 0 };
    this.font = config.font;
    this.lineWidth = config.lineWidth || { value: 1 };
    this.img = config.img;
    this.scale = config.scale;
    this.polygon = config.polygon;
    this.ellipseRadius = config.ellipseRadius;

    this.validate();
  }

  validate() {
    if (!this.id) throw new Error('DSL 缺少 id 字段');
    if (!this.type) throw new Error('DSL 缺少 type 字段');
    if (!this.position) throw new Error('DSL 缺少 position 字段');
    if (!this.size) throw new Error('DSL 缺少 size 字段');
    if (!this.color) throw new Error('DSL 缺少 color 字段');
  }
}

DSL 使用示例

const dsls = [
  {
    id: "rect-1",
    type: "rect",
    position: { x: 100, y: 100 },
    size: { width: 200, height: 100 },
    color: { 
      fillColor: "#FF5000", 
      strokeColor: "#000000" 
    },
    rotation: { value: 0 },
    selected: { value: false },
  },
  {
    id: "text-1",
    type: "text",
    position: { x: 120, y: 130 },
    size: { width: 160, height: 40 },
    color: { fillColor: "", strokeColor: "" },
    font: {
      family: "Arial",
      size: 24,
      weight: "bold",
      text: "Hello World",
      fillColor: "#FFFFFF",
    },
  },
  {
    id: "ellipse-1",
    type: "ellipse",
    position: { x: 350, y: 100 },
    size: { width: 120, height: 80 },
    color: { 
      fillColor: "#00BFFF", 
      strokeColor: "#000000" 
    },
  },
];

// 初始化 Core
const core = new Core(dsls);

Canvas 组件集成

Canvas 组件架构

graph TB
    subgraph "React组件"
        A[Canvas组件]
        B[canvasRef]
        C[useEffect钩子]
    end
    
    subgraph "Core初始化"
        D[创建Core实例]
        E[加载DSL配置]
        F[初始化Canvas]
    end
    
    subgraph "系统初始化"
        G[RenderSystem]
        H[PickingSystem]
        I[SelectionSystem]
        J[EventSystem]
        K[InputSystem]
    end
    
    subgraph "主循环"
        L[requestAnimationFrame]
        M[事件处理]
        N[渲染更新]
        O[选择框绘制]
    end
    
    A --> B
    A --> C
    C --> D
    C --> E
    C --> F
    
    F --> G
    F --> H
    F --> I
    F --> J
    F --> K
    
    G --> L
    J --> M
    G --> N
    I --> O
    
    L --> L

系统初始化流程

sequenceDiagram
    participant R as React
    participant C as Canvas组件
    participant Core as Core引擎
    participant S as Systems
    participant L as 主循环
    
    R->>C: 组件挂载
    C->>C: useEffect触发
    C->>Core: 创建Core实例(dsls)
    Core->>Core: 解析DSL
    Core->>Core: 初始化StateStore
    
    C->>Core: initCanvas(canvasRef)
    Core->>Core: 设置DPR
    Core->>C: 返回ctx
    
    C->>S: new RenderSystem(ctx, core)
    C->>S: new PickingSystem(ctx, core)
    C->>S: new SelectionSystem(ctx, core)
    C->>S: new EventSystem(core)
    C->>S: new InputSystem(canvas, core, picking)
    
    C->>L: 启动主循环loop()
    
    loop 每帧
        L->>S: eventSystem.update()
        L->>S: renderSystem.update()
        L->>S: selectionSystem.update()
        L->>L: requestAnimationFrame
    end

Canvas.tsx 实现

import { useEffect, useRef, useState } from "react";
import { Core } from "../Core/Core";
import { RenderSystem } from "../Core/System/RenderSystem/RenderSystem";
import { SelectionSystem } from "../Core/System/SelectionSystem";
import { PickingSystem } from "../Core/System/PickingSystem";
import { EventSystem } from "../Core/System/EventSystem";
import { InputSystem } from "../Core/System/InputSystem";

function Canvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [core, setCore] = useState<Core | null>(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    // 初始化 Core
    const dsls = []; // 从服务器或状态加载 DSL
    const coreInstance = new Core(dsls);
    const ctx = coreInstance.initCanvas(canvasRef.current);

    // 初始化系统
    const renderSystem = new RenderSystem(ctx, coreInstance);
    const pickingSystem = new PickingSystem(ctx, coreInstance);
    const selectionSystem = new SelectionSystem(ctx, coreInstance);
    const eventSystem = new EventSystem(coreInstance);
    const inputSystem = new InputSystem(
      canvasRef.current,
      coreInstance,
      pickingSystem
    );

    // 主循环
    function loop() {
      eventSystem.update(coreInstance.stateStore);
      renderSystem.update(coreInstance.stateStore);
      selectionSystem.update(coreInstance.stateStore);
      requestAnimationFrame(loop);
    }

    loop();
    setCore(coreInstance);

    return () => {
      // 清理事件监听
    };
  }, []);

  return (
    <div className="canvas-container">
      <canvas 
        ref={canvasRef} 
        width={800} 
        height={600}
        style={{ border: '1px solid #ccc' }}
      />
    </div>
  );
}

export default Canvas;

性能优化技巧

性能优化架构

graph TB
    subgraph "渲染优化"
        A[节流渲染<br/>Throttle 100ms]
        B[离屏Canvas<br/>拾取优化]
        C[增量更新<br/>只更新变化]
        D[可视区域裁剪<br/>只渲染可见]
    end
    
    subgraph "内存优化"
        E[Map数据结构<br/>O1查询]
        F[图片缓存<br/>避免重复加载]
        G[对象池模式<br/>复用对象]
    end
    
    subgraph "事件优化"
        H[事件委托<br/>Canvas统一监听]
        I[防抖处理<br/>resize事件]
        J[事件队列<br/>批量处理]
    end
    
    subgraph "Canvas优化"
        K[DPR适配<br/>高清显示]
        L[willReadFrequently<br/>频繁读取优化]
        M[save/restore<br/>状态管理]
    end

离屏 Canvas

// 用于图形拾取,不显示
const offscreenCanvas = document.createElement('canvas');
const offscreenCtx = offscreenCanvas.getContext('2d');

图片缓存

private imageCache = new Map<string, HTMLImageElement>();

loadImage(src: string): HTMLImageElement {
  if (this.imageCache.has(src)) {
    return this.imageCache.get(src)!;
  }
  
  const img = new Image();
  img.src = src;
  this.imageCache.set(src, img);
  return img;
}

完整数据流架构

端到端数据流

graph TB
    subgraph "用户交互层"
        A[用户操作]
        B[鼠标事件]
        C[键盘事件]
    end
    
    subgraph "输入处理层"
        D[InputSystem]
        E[事件绑定]
        F[事件转换]
    end
    
    subgraph "事件管理层"
        G[EventQueue]
        H[EventSystem]
        I[事件分发]
    end
    
    subgraph "状态管理层"
        J[StateStore]
        K[Position Map]
        L[Size Map]
        M[Color Map]
        N[Selected Map]
    end
    
    subgraph "拾取判断层"
        O[PickingSystem]
        P[离屏Canvas]
        Q[颜色编码]
    end
    
    subgraph "选择管理层"
        R[SelectionSystem]
        S[选中状态更新]
        T[选择框绘制]
    end
    
    subgraph "渲染输出层"
        U[RenderSystem]
        V[渲染器注册表]
        W[Canvas绘制]
    end
    
    A --> B
    A --> C
    B --> D
    C --> D
    
    D --> E
    E --> F
    F --> G
    
    G --> H
    H --> I
    I --> J
    
    D --> O
    O --> P
    O --> Q
    
    J --> R
    R --> S
    S --> T
    
    J --> U
    U --> V
    V --> W
    
    T --> W

完整交互流程

sequenceDiagram
    participant U as 用户
    participant I as InputSystem
    participant P as PickingSystem
    participant E as EventSystem
    participant S as StateStore
    participant Sel as SelectionSystem
    participant R as RenderSystem
    participant C as Canvas
    
    Note over U,C: 1. 点击选择阶段
    U->>I: 点击画布(x, y)
    I->>P: pick(x, y)
    P->>P: 读取离屏Canvas像素
    P->>I: 返回entityId
    I->>E: 添加'entity:select'事件
    
    Note over U,C: 2. 事件处理阶段
    E->>E: 处理事件队列
    E->>S: 更新selected组件
    S->>S: selected.set(id, true)
    
    Note over U,C: 3. 拖拽移动阶段
    U->>I: mousedown + mousemove
    I->>I: 计算偏移量(dx, dy)
    I->>S: 更新position组件
    S->>S: position.x += dx
    
    Note over U,C: 4. 渲染更新阶段
    R->>R: throttledRender触发
    R->>S: 读取所有组件数据
    R->>C: 清空画布
    
    loop 遍历所有实体
        R->>V: 查找渲染器
        V->>C: 绘制图形
    end
    
    Sel->>S: 读取selected组件
    Sel->>C: 绘制选择框

架构优势总结

设计优势

mindmap
  root((ECS架构优势))
    高性能
      数据局部性
      缓存友好
      Map O1查询
      节流渲染
    可扩展性
      添加新组件
      添加新系统
      添加新渲染器
      插件化设计
    可维护性
      数据逻辑分离
      单一职责
      模块化设计
      清晰的依赖关系
    灵活性
      组合优于继承
      动态添加删除组件
      运行时修改
      DSL配置驱动

技术亮点

特性实现方式优势
ECS 架构Entity-Component-System 模式数据与逻辑分离,高性能
颜色编码拾取离屏 Canvas + RGB 映射精确快速,支持复杂图形
节流渲染Lodash throttle 100ms降低 CPU 使用,提升性能
Map 数据结构StateStore 使用 MapO(1) 查询,内存高效
图片缓存ImageCache Map避免重复加载,提升速度
事件队列EventQueue 批量处理解耦系统,灵活扩展
DSL 配置JSON 声明式配置易于序列化,可视化编辑
DPR 适配Canvas 高清适配支持 Retina 屏幕

最后

项目在不断迭代中,后面可能存在代码和文章有差异的地方,具体可以看github.com/baiyuze/duc…