好的,我们开始 数学工具与基础图形类 的实现。
这是整个引擎的基石。如果这里的矩阵运算或者父子坐标变换写错了,后续所有的交互判定和渲染位置都会错乱。
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.验证
虽然我们还没有 Storage 和 Painter,但我们可以写一段模拟代码来验证 变换矩阵 是否生效。
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);
