从零实现2D绘图引擎:0.整体结构设计与实现规划

54 阅读5分钟

MiniRender仓库地址参考

这是一个非常宏大的工程,但也是深入理解图形渲染引擎的最佳途径。为了从零实现一个精简版 MiniRender,我们需要先跳出具体的代码细节,从架构设计类型定义入手。

这将是我们 Mini-Render 的蓝图。我们将采用 TypeScript 进行设计,利用其强类型特性来规范各模块的接口。

一、 整体目录结构设计

我们将项目分为核心层(Core)、图形层(Graphic)、渲染层(Painter)、工具层(Utils)四个主要部分。

src/
├── core/
│   ├── MiniRender.ts       # 入口文件,外观模式,协调各模块
│   ├── Eventful.ts      # 事件中心基类 (Observer Pattern)
│   └── env.ts           # 环境检测 (Browser, Node, etc.)
├── storage/
│   └── Storage.ts       # 图形仓库,管理 DisplayList
├── painter/
│   └── Painter.ts       # 渲染器,管理 Canvas 和绘图循环
├── handler/
│   └── Handler.ts       # 交互控制器,处理 DOM 事件到图形事件的映射
├── graphic/
│   ├── Element.ts       # 所有节点的基类 (变换矩阵、父子关系)
│   ├── Displayable.ts   # 可绘制对象的基类 (样式、层级)
│   ├── Group.ts         # 组,容器
│   ├── Path.ts          # 路径基类 (具体的形状继承此类)
│   ├── style.ts         # 样式接口定义
│   └── shape/           # 具体形状实现
│       ├── Circle.ts
│       ├── Rect.ts
│       └── ...
└── utils/
    ├── matrix.ts        # 3x2 仿射变换矩阵运算
    ├── vector.ts        # 向量运算
    ├── bounding.ts      # 包围盒计算
    └── guid.ts          # 唯一 ID 生成器

二、 核心模块详细设计与类型定义

下面我们将逐个模块定义其 TypeScript 接口和核心类的结构。

1. 基础类型与工具 (Utils & Common)

首先定义最基础的数据结构,这是图形学的基石。

// src/utils/types.ts

// 二维向量/点
export type Point = [number, number];

// 3x2 仿射变换矩阵: [a, b, c, d, tx, ty]
// x' = ax + cy + tx
// y' = bx + dy + ty
export type MatrixArray = [number, number, number, number, number, number];

// 包围盒
export interface BoundingRect {
    x: number;
    y: number;
    width: number;
    height: number;
}

// 字典类型
export interface Dictionary<T> {
    [key: string]: T;
}

2. 事件系统 (Eventful)

所有的图形元素、MiniRender 实例都需要支持 .on, .off, .trigger

// src/core/Eventful.ts

export type EventHandler = (...args: any[]) => void;

export interface IEventful {
    on(event: string, handler: EventHandler, context?: any): this;
    off(event?: string, handler?: EventHandler): this;
    trigger(event: string, ...args: any[]): this;
}

// 实现类将作为一个 Mixin 或者基类使用
export class Eventful implements IEventful { 
    // ... 具体实现省略,标准观察者模式
}

3. 图形层 (Graphic System)

这是最复杂的继承体系。我们需要区分“节点结构”和“渲染属性”。

继承链: Element (变换) -> Displayable (样式) -> Path (几何路径) -> Rect/Circle (具体形状)。

A. 元素基类 (Element) 负责场景图(Scene Graph)的树形结构和坐标变换。

// src/graphic/Element.ts

import { MatrixArray, Point, BoundingRect } from '../utils/types';
import { Eventful } from '../core/Eventful';

export interface ElementProps {
    id?: string;
    position?: Point; // [x, y]
    rotation?: number; // 弧度
    scale?: Point;    // [sx, sy]
    origin?: Point;   // 变换中心点 [ox, oy]
}

export abstract class Element extends Eventful {
    id: string;
    
    // 变换属性
    x: number = 0;
    y: number = 0;
    rotation: number = 0;
    scaleX: number = 1;
    scaleY: number = 1;
    originX: number = 0;
    originY: number = 0;

    // 变换矩阵
    transform: MatrixArray | null = null;      // 局部矩阵
    globalTransform: MatrixArray | null = null; // 全局矩阵 (世界坐标)

    // 层级关系
    parent: Element | null = null;

    // 标记是否需要更新矩阵
    dirty(): void; 
    
    // 计算包围盒 (抽象方法)
    abstract getBoundingRect(): BoundingRect;
    
    // 坐标转换
    // 将全局坐标转换为局部坐标(用于事件检测)
    globalToLocal(x: number, y: number): Point;
}

B. 可绘制对象 (Displayable) 负责具体的渲染样式、绘制顺序 (z-index)。

// src/graphic/Displayable.ts

import { Element } from './Element';

export interface PathStyle {
    fill?: string;
    stroke?: string;
    lineWidth?: number;
    opacity?: number;
    // ... lineCap, lineJoin, shadowBlur 等
}

export abstract class Displayable extends Element {
    // 决定 Canvas 的叠加顺序 (CSS z-index 概念)
    zLevel: number = 0; 
    // 决定同一个 Canvas 内的叠加顺序
    z: number = 0;
    
    // 样式
    style: PathStyle = {};
    
    // 是否可见
    invisible: boolean = false;

    // 核心绘制流程
    // ctx: CanvasRenderingContext2D
    brush(ctx: any): void {
        // 1. 保存 context
        // 2. 设置样式 (fill, stroke)
        // 3. 应用变换 (setTransform)
        // 4. 调用具体形状的 buildPath (构建路径)
        // 5. 绘制 (fill(), stroke())
        // 6. 恢复 context
    }
    
    // 抽象方法:构建路径(由子类实现)
    abstract buildPath(ctx: any): void;
    
    // 抽象方法:判断点是否在图形内(用于事件拾取)
    abstract contain(x: number, y: number): boolean;
}

C. 组 (Group) 容器,不直接绘制,但传递变换。

// src/graphic/Group.ts

import { Element } from './Element';

export class Group extends Element {
    readonly isGroup = true;
    children: Element[] = [];

    add(child: Element): void;
    remove(child: Element): void;
    removeAll(): void;
    
    // 遍历子元素
    traverse(cb: (el: Element) => void): void;
}

4. 仓库层 (Storage)

管理内存中的对象,负责排序。

// src/storage/Storage.ts

import { Element } from '../graphic/Element';
import { Displayable } from '../graphic/Displayable';

export class Storage {
    // 根节点列表 (Group 或 Displayable)
    private _roots: Element[] = [];
    
    // 扁平化的显示列表 (用于渲染循环)
    // 只有 Displayable 会进入此列表,Group 不会
    private _displayList: Displayable[] = [];

    addRoot(el: Element): void;
    delRoot(el: Element): void;

    // 获取按 zLevel -> z 排序后的列表
    getDisplayList(update?: boolean): Displayable[];

    // 标记全部更新
    dispose(): void;
}

5. 渲染层 (Painter)

负责操作 DOM 和 Canvas Context。

// src/painter/Painter.ts

import { Storage } from '../storage/Storage';
import { Displayable } from '../graphic/Displayable';

export class Painter {
    private _dom: HTMLElement; // 容器 DOM
    private _storage: Storage;
    
    // 可能有多个 Canvas 层 (zLevel)
    // Map<zLevel, CanvasContext>
    private _layers: Map<number, CanvasRenderingContext2D> = new Map();

    constructor(dom: HTMLElement, storage: Storage);

    // 核心渲染方法
    refresh(): void;
    
    // 清除指定层
    clear(): void;

    // 获取视口宽高
    getWidth(): number;
    getHeight(): number;
    
    // 内部方法:创建或获取层
    private _getLayer(zLevel: number): CanvasRenderingContext2D;
}

6. 交互层 (Handler)

处理鼠标事件,进行图形拾取 (Picking)。

// src/handler/Handler.ts

import { Storage } from '../storage/Storage';
import { Painter } from '../painter/Painter';
import { Element } from '../graphic/Element';

export class Handler {
    private _storage: Storage;
    private _painter: Painter;
    private _dom: HTMLElement;

    // 当前鼠标悬停的图形
    private _hovered: Element | null = null;

    constructor(storage: Storage, painter: Painter, proxyDom: HTMLElement);

    // 绑定 DOM 事件监听器
    private _initDomEvents(): void;

    // 事件分发逻辑
    private _dispatch(eventName: string, e: MouseEvent): void;

    // 核心:寻找鼠标下方的图形
    private _findHover(x: number, y: number): Element | null;
}

7. 入口类 (MiniRender)

整合以上所有模块。

// src/core/MiniRender.ts

import { Storage } from '../storage/Storage';
import { Painter } from '../painter/Painter';
import { Handler } from '../handler/Handler';
import { Element } from '../graphic/Element';
import { Group } from '../graphic/Group';

export interface ZROptions {
    renderer?: 'canvas' | 'svg';
    devicePixelRatio?: number;
}

export class MiniRender {
    id: string;
    dom: HTMLElement;
    
    storage: Storage;
    painter: Painter;
    handler: Handler;
    
    // 动画循环 ID
    private _animationId: number;

    constructor(dom: HTMLElement, opts?: ZROptions);

    // API: 添加元素
    add(el: Element): void;
    
    // API: 删除元素
    remove(el: Element): void;

    // 触发刷新 (通常在下一帧执行)
    refresh(): void;
    
    // 销毁实例
    dispose(): void;
}

// 工厂函数
export function init(dom: HTMLElement, opts?: ZROptions): MiniRender;

是的,我们的目标就是从零制作一个mini版本的ZRender,从而学习到ZRender这一著名的库的核心设计内容