从零实现2D绘图引擎:1.实现数学工具库与基础图形类

MiniRender仓库地址参考

好的,我们开始 数学工具与基础图形类 的实现。

这是整个引擎的基石。如果这里的矩阵运算或者父子坐标变换写错了,后续所有的交互判定和渲染位置都会错乱。

1. 基础类型定义 (src/utils/types.ts)

为了代码清晰,我们先统一定义一些类型。

typescript 复制代码
// src/utils/types.ts

// 向量/点: [x, y]
export type Point = [number, number];

// 3x2 仿射变换矩阵
// index: [0, 1, 2, 3, 4, 5] -> [a, b, c, d, tx, ty]
// 数学表示:
// | a c tx |
// | b d ty |
// | 0 0 1  |
export type MatrixArray = Float32Array | number[];

export interface BoundingRect {
    x: number;
    y: number;
    width: number;
    height: number;
}

2. 矩阵运算库 (src/utils/matrix.ts)

这是处理复杂层级和动画的核心。我们只需要实现最关键的几个方法:创建、乘法(级联)、合成(属性转矩阵)。

typescript 复制代码
// src/utils/matrix.ts
import { MatrixArray } from './types';

// 创建单位矩阵
export function create(): MatrixArray {
    return [1, 0, 0, 1, 0, 0];
}

// 矩阵乘法: out = m1 * m2
// 用于计算 父级矩阵 * 子级局部矩阵 = 子级全局矩阵
export function mul(out: MatrixArray, m1: MatrixArray, m2: MatrixArray): MatrixArray {
    const out0 = m1[0] * m2[0] + m1[2] * m2[1];
    const out1 = m1[1] * m2[0] + m1[3] * m2[1];
    const out2 = m1[0] * m2[2] + m1[2] * m2[3];
    const out3 = m1[1] * m2[2] + m1[3] * m2[3];
    const out4 = m1[0] * m2[4] + m1[2] * m2[5] + m1[4];
    const out5 = m1[1] * m2[4] + m1[3] * m2[5] + m1[5];
    
    out[0] = out0; out[1] = out1;
    out[2] = out2; out[3] = out3;
    out[4] = out4; out[5] = out5;
    return out;
}

// 核心:将平移、缩放、旋转属性合成为一个矩阵
// 变换顺序:Translate -> Rotate -> Scale (标准顺序)
export function compose(
    out: MatrixArray,
    x: number, y: number,
    scaleX: number, scaleY: number,
    rotation: number
): MatrixArray {
    const sr = Math.sin(rotation);
    const cr = Math.cos(rotation);

    // 矩阵公式推导结果
    out[0] = cr * scaleX;
    out[1] = sr * scaleX;
    out[2] = -sr * scaleY;
    out[3] = cr * scaleY;
    out[4] = x;
    out[5] = y;
    
    return out;
}

// 克隆矩阵
export function clone(m: MatrixArray): MatrixArray {
    return Array.from(m); // 简易版
}

3. 元素基类 (src/graphic/Element.ts)

Element 不负责画画,只负责"我在哪"和"我的父级是谁"。它是场景图(Scene Graph)的节点。

核心逻辑updateTransform 方法。它负责先算自己的局部矩阵,然后看有没有爸爸。如果有,乘上爸爸的矩阵。

typescript 复制代码
// src/graphic/Element.ts
import * as matrix from '../utils/matrix';
import { MatrixArray } from '../utils/types';

// 用一个简单的 GUID 生成器
let idBase = 0;

export abstract class Element {
    id: string;
    
    // --- 变换属性 (Transform Props) ---
    x: number = 0;
    y: number = 0;
    scaleX: number = 1;
    scaleY: number = 1;
    rotation: number = 0; // 弧度制

    // --- 矩阵状态 (Matrix State) ---
    // 局部变换矩阵 (相对于父级)
    localTransform: MatrixArray = matrix.create();
    // 全局变换矩阵 (相对于 Canvas 左上角)
    globalTransform: MatrixArray = matrix.create();

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

    constructor() {
        this.id = `el_${idBase++}`;
    }

    /**
     * 核心方法:更新变换矩阵
     * 递归更新:通常由渲染器从根节点开始调用
     */
    updateTransform() {
        // 1. 根据属性计算局部矩阵
        // 优化:如果没有任何变换,保持单位矩阵 (此处省略优化,直接计算)
        matrix.compose(
            this.localTransform,
            this.x, this.y,
            this.scaleX, this.scaleY,
            this.rotation
        );

        // 2. 计算全局矩阵
        const parentTransform = this.parent && this.parent.globalTransform;
        
        if (parentTransform) {
            // 有父级:全局 = 父级全局 * 自身局部
            matrix.mul(this.globalTransform, parentTransform, this.localTransform);
        } else {
            // 无父级:全局 = 自身局部
            // 注意:这里需要拷贝,防止引用错乱
            for(let i = 0; i < 6; i++) {
                this.globalTransform[i] = this.localTransform[i];
            }
        }
    }
}

4. 样式接口 (src/graphic/Style.ts)

定义简单的 Canvas 样式。

typescript 复制代码
// src/graphic/Style.ts
export interface CommonStyle {
    fill?: string;       // 填充颜色
    stroke?: string;     // 描边颜色
    lineWidth?: number;  // 线宽
    opacity?: number;    // 透明度 0-1
    shadowBlur?: number;
    shadowColor?: string;
    // ... 其他 Canvas 样式
}

5. 可绘制对象基类 (src/graphic/Displayable.ts)

Displayable 继承自 Element,负责将对象真正"画"到 Context 上。

核心逻辑brush 方法。它是渲染管线的核心步骤。

typescript 复制代码
// src/graphic/Displayable.ts
import { Element } from './Element';
import { CommonStyle } from './Style';

export abstract class Displayable extends Element {
    
    style: CommonStyle = {};
    
    // 绘制顺序,类似于 CSS z-index
    z: number = 0;
    
    // 层级,不同的 zLevel 会被绘制在不同的 Canvas 实例上 (Layer)
    zLevel: number = 0;

    /**
     * 绘制入口
     * @param ctx 原生 CanvasContext
     */
    brush(ctx: CanvasRenderingContext2D) {
        const style = this.style;
        
        // 1. 保存当前 Context 状态
        ctx.save();

        // 2. 应用样式
        if (style.fill) ctx.fillStyle = style.fill;
        if (style.stroke) ctx.strokeStyle = style.stroke;
        if (style.lineWidth) ctx.lineWidth = style.lineWidth;
        // ... 其他样式应用

        // 3. 应用变换 (关键!)
        // setTransform(a, b, c, d, e, f)
        // 使用 globalTransform,这样 Canvas 原点就变到了图形的坐标系下
        const m = this.globalTransform;
        ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);

        // 4. 开始路径
        ctx.beginPath();
        
        // 5. 调用具体形状的路径构建逻辑
        this.buildPath(ctx);

        // 6. 绘制
        ctx.closePath(); // 可选
        if (style.fill) ctx.fill();
        if (style.stroke) ctx.stroke();

        // 7. 恢复 Context 状态 (弹出 save 的状态)
        ctx.restore();
    }

    /**
     * 抽象方法:由子类实现具体的路径
     * 例如 Circle 会调用 ctx.arc
     */
    abstract buildPath(ctx: CanvasRenderingContext2D): void;
}

6. 具体图形实现:圆形 (src/graphic/shape/Circle.ts)

最后,我们实现一个具体的图形来验证这一套逻辑。

typescript 复制代码
// src/graphic/shape/Circle.ts
import { Displayable } from '../Displayable';

interface CircleShape {
    cx?: number;
    cy?: number;
    r?: number;
}

export class Circle extends Displayable {
    
    // 图形特有的几何属性
    shape: CircleShape;

    constructor(opts?: { shape?: CircleShape, style?: any, z?: number }) {
        super();
        this.shape = { cx: 0, cy: 0, r: 0, ...opts?.shape };
        if (opts?.style) this.style = opts.style;
        if (opts?.z) this.z = opts.z;
    }

    buildPath(ctx: CanvasRenderingContext2D) {
        const shape = this.shape;
        // 直接调用 Canvas API
        // 注意:因为我们在 Displayable.brush 中已经做了 setTransform
        // 这里的 cx, cy 是相对于图形自身坐标系的位置
        ctx.arc(shape.cx!, shape.cy!, shape.r!, 0, Math.PI * 2);
    }
}

7.验证

虽然我们还没有 StoragePainter,但我们可以写一段模拟代码来验证 变换矩阵 是否生效。

typescript 复制代码
// test.ts (模拟运行)
import { Circle } from './graphic/shape/Circle';

// 1. 模拟一个 Canvas
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d')!;

// 2. 创建一个圆
const circle = new Circle({
    shape: { cx: 0, cy: 0, r: 50 }, // 圆心在 0,0
    style: { fill: 'red' }
});

// 3. 设置变换属性
circle.x = 200;      // 移到 x=200
circle.y = 200;      // 移到 y=200
circle.scaleX = 2;   // 宽度放大 2 倍
circle.rotation = Math.PI / 4; // 旋转 45 度

// 4. 手动更新矩阵 (正常这是由 MiniRender 系统做的)
circle.updateTransform();

console.log('Global Matrix:', circle.globalTransform);
// 预期:tx=200, ty=200, 且 a,b,c,d 有值(因为有缩放和旋转)

// 5. 手动绘制 (正常这是由 Painter 做的)
// 清空画布
ctx.clearRect(0, 0, 500, 500);
// 绘制
circle.brush(ctx);
相关推荐
葡萄城技术团队1 小时前
SpreadJS 自定义函数实战指南:从入门到避坑
前端
m0_740043731 小时前
v-bind 和 v-model 的核心区别
前端·javascript·vue.js
魂祈梦1 小时前
页面出现莫名其妙的滚动条
前端·css
重铸码农荣光1 小时前
从零实现一个「就地编辑」组件:深入理解 OOP 封装与复用的艺术
前端·javascript·前端框架
攻心的子乐1 小时前
redission 分布式锁
前端·bootstrap·mybatis
前端老宋Running1 小时前
拒绝“无效焦虑”:为什么你 80% 的 useMemo 都在做负优化?
前端·javascript·react.js
品克缤1 小时前
vue项目配置代理,解决跨域问题
前端·javascript·vue.js
m0_740043731 小时前
Vue简介
前端·javascript·vue.js
我叫张小白。1 小时前
Vue3 v-model:组件通信的语法糖
开发语言·前端·javascript·vue.js·elementui·前端框架·vue