基于ECS架构的Canvas画布编辑器

概述

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

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

核心功能模块

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 核心逻辑

typescript 复制代码
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
矩形渲染] C[EllipseRenderer
椭圆渲染] D[TextRenderer
文本渲染] E[ImageRenderer
图片渲染] F[PolygonRenderer
多边形渲染] 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

矩形渲染器

typescript 复制代码
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();
  }
}

椭圆渲染器

typescript 复制代码
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();
    }
  }
}

文本渲染器

typescript 复制代码
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();
  }
}

图片渲染器

typescript 复制代码
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 实现

typescript 复制代码
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 实现

typescript 复制代码
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 实现

typescript 复制代码
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 实现

typescript 复制代码
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 类实现

typescript 复制代码
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 使用示例

typescript 复制代码
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 实现

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

离屏 Canvas

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

图片缓存

typescript 复制代码
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 使用 Map O(1) 查询,内存高效
图片缓存 ImageCache Map 避免重复加载,提升速度
事件队列 EventQueue 批量处理 解耦系统,灵活扩展
DSL 配置 JSON 声明式配置 易于序列化,可视化编辑
DPR 适配 Canvas 高清适配 支持 Retina 屏幕

最后

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

相关推荐
sjin3 小时前
React源码 - 关键数据结构
前端·react.js
旺仔牛仔QQ糖3 小时前
IntersectionObserver 异步交叉观察器
前端
不如喫茶去4 小时前
VUE查询-历史记录功能
前端·javascript·vue.js
持梦远方4 小时前
重生之我拿捏Linux——《三、shell脚本使用》
前端·chrome
行走在顶尖4 小时前
代码截断运行逻辑
前端
一枚前端小能手4 小时前
「周更第8期」实用JS库推荐:decimal.j
前端·javascript
草莓熊Lotso4 小时前
《C++ Web 自动化测试实战:常用函数全解析与场景化应用指南》
前端·c++·python·dubbo
Tech_Lin4 小时前
JavaScript Date时间对象的常用操作方法总结
前端·javascript
温宇飞4 小时前
JavaScript 异常处理
前端