基于 TypeScript 的轻量级 2D渲染引擎
✅ 所有代码为纯逻辑层封装,适用于 WebWorker 或与任意 UI 框架集成
📦 安装依赖(提示)
bash
npm install gl-matrix
🔧 基础类型定义(common.ts)
ts
import { mat4, vec2, vec3 } from 'gl-matrix';
export type Vec2 = vec2;
export type Vec3 = vec3;
export type Mat4 = mat4;
export interface CanvasRect {
x: number;
y: number;
width: number;
height: number;
}
🎨 CanvasConfig.ts
ts
import { CanvasRect } from './common';
export class CanvasConfig {
public canvasClientRect: CanvasRect;
constructor(rect: CanvasRect) {
this.canvasClientRect = rect;
}
}
🖼️ View.ts
ts
import { mat4, vec2, vec3 } from 'gl-matrix';
import { CanvasRect, Vec2, Mat4 } from './common';
export class View {
public viewMatrix: Mat4 = mat4.create();
public projectionMatrix: Mat4 = mat4.create();
private canvasRect: CanvasRect;
constructor(canvasRect: CanvasRect) {
this.canvasRect = canvasRect;
this.updateProjectionMatrix(); // 默认正交投影
}
updateViewMatrix(eye: Vec2 = [0, 0], center: Vec2 = [0, 0], up: Vec2 = [0, 1]): void {
const eye3: Vec3 = [eye[0], eye[1], 0];
const center3: Vec3 = [center[0], center[1], 0];
const up3: Vec3 = [up[0], up[1], 1]; // z 轴轻微上扬避免退化
mat4.lookAt(this.viewMatrix, eye3, center3, up3);
}
updateProjectionMatrix(near: number = -1, far: number = 1): void {
const { width, height } = this.canvasRect;
mat4.ortho(this.projectionMatrix, 0, width, height, 0, near, far);
}
// 屏幕坐标 → NDC (-1 ~ 1)
screenToNDC(screenPos: Vec2): Vec2 {
const { width, height } = this.canvasRect;
return [
(screenPos[0] / width) * 2 - 1,
(screenPos[1] / height) * 2 - 1
];
}
// NDC → 世界坐标(假设 Z=0)
ndcToWorld(ndc: Vec2): Vec2 {
const invViewProj = mat4.create();
mat4.multiply(invViewProj, this.projectionMatrix, this.viewMatrix);
mat4.invert(invViewProj, invViewProj);
const pt4 = vec3.fromValues(ndc[0], ndc[1], 0);
vec3.transformMat4(pt4, pt4, invViewProj);
return [pt4[0], pt4[1]];
}
// 屏幕坐标 → 世界坐标
screenToWorld(screenPos: Vec2): Vec2 {
const ndc = this.screenToNDC(screenPos);
return this.ndcToWorld(ndc);
}
}
⚙️ GLBuffer.ts
ts
export enum BufferType {
ARRAY_BUFFER = 0x8892, // position, color, etc.
ELEMENT_ARRAY_BUFFER = 0x8893 // indices
}
export enum UsageHint {
STATIC_DRAW = 0x88e4,
DYNAMIC_DRAW = 0x88e8,
STREAM_DRAW = 0x88e0
}
export class GLBuffer {
private buffer: WebGLBuffer | null = null;
private gl: WebGL2RenderingContext;
constructor(
gl: WebGL2RenderingContext,
type: BufferType,
usage: UsageHint = UsageHint.STATIC_DRAW
) {
this.gl = gl;
this.buffer = gl.createBuffer();
this.bind(type);
this.unbind(type);
}
bind(type: BufferType): void {
this.gl.bindBuffer(type, this.buffer);
}
unbind(type: BufferType): void {
this.gl.bindBuffer(type, null);
}
data(data: Float32Array | Uint16Array | Uint32Array, type: BufferType, usage: UsageHint): void {
this.bind(type);
this.gl.bufferData(type, data, usage);
this.unbind(type);
}
destroy(): void {
if (this.buffer) {
this.gl.deleteBuffer(this.buffer);
this.buffer = null;
}
}
}
🧱 VertexLayout.ts
ts
export interface AttributeInfo {
name: string;
size: number; // components per vertex (e.g., 2 for vec2)
type: number; // gl.FLOAT, etc.
normalized: boolean;
stride: number;
offset: number;
}
export class VertexLayout {
private attributes: AttributeInfo[] = [];
addAttribute(info: Omit<AttributeInfo, 'stride' | 'offset'>): this {
this.attributes.push({ ...info, stride: 0, offset: 0 });
return this;
}
computeOffsetsAndStride(): number {
let offset = 0;
let stride = 0;
this.attributes.forEach(attr => {
attr.offset = offset;
const typeSize = this.getTypeSize(attr.type);
offset += attr.size * typeSize;
});
stride = offset;
this.attributes.forEach(attr => {
attr.stride = stride;
});
return stride;
}
getAttributes(): readonly AttributeInfo[] {
return this.attributes;
}
private getTypeSize(type: number): number {
switch (type) {
case 0x1406: return 4; // FLOAT
case 0x1404: return 4; // INT
case 0x1405: return 4; // UNSIGNED_INT
default: throw new Error(`Unsupported GL type: ${type}`);
}
}
}
🧩 VertexArray.ts
ts
export class VertexArray {
private vao: WebGLVertexArrayObjectOES;
private vbo: GLBuffer;
private ibo: GLBuffer | null = null;
private layout: VertexLayout;
private gl: WebGL2RenderingContext;
constructor(
gl: WebGL2RenderingContext,
vbo: GLBuffer,
layout: VertexLayout,
ibo?: GLBuffer
) {
this.gl = gl;
const ext = gl.getExtension('OES_vertex_array_object');
if (!ext) throw new Error('OES_vertex_array_object not supported');
this.vao = ext.createVertexArray()!;
this.vbo = vbo;
this.layout = layout;
this.ibo = ibo || null;
this.bind();
this.vbo.bind(BufferType.ARRAY_BUFFER);
const stride = layout.computeOffsetsAndStride();
for (const attr of layout.getAttributes()) {
this.gl.enableVertexAttribArray(this.gl.getAttribLocation(this.getCurrentProgram(), attr.name));
this.gl.vertexAttribPointer(
this.gl.getAttribLocation(this.getCurrentProgram(), attr.name),
attr.size,
attr.type,
attr.normalized,
attr.stride,
attr.offset
);
}
if (this.ibo) {
this.ibo.bind(BufferType.ELEMENT_ARRAY_BUFFER);
}
this.unbind();
}
bind(): void {
const ext = this.gl.getExtension('OES_vertex_array_object');
ext!.bindVertexArray(this.vao);
}
unbind(): void {
const ext = this.gl.getExtension('OES_vertex_array_object');
ext!.bindVertexArray(null);
}
drawElements(mode: number, count: number, type: number, offset: number): void {
this.bind();
this.gl.drawElements(mode, count, type, offset);
this.unbind();
}
destroy(): void {
const ext = this.gl.getExtension('OES_vertex_array_object');
ext!.deleteVertexArray(this.vao);
this.vbo.destroy();
this.ibo?.destroy();
}
private getCurrentProgram(): WebGLProgram {
return this.gl.getParameter(this.gl.CURRENT_PROGRAM);
}
}
🎯 Shader.ts
ts
export enum ShaderType {
VERTEX = 0x8b31,
FRAGMENT = 0x8b30
}
export class Shader {
private shader: WebGLShader;
private gl: WebGL2RenderingContext;
constructor(gl: WebGL2RenderingContext, source: string, type: ShaderType) {
this.gl = gl;
this.shader = gl.createShader(type)!;
gl.shaderSource(this.shader, source);
gl.compileShader(this.shader);
if (!gl.getShaderParameter(this.shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(this.shader);
console.error(`Shader compilation failed:\n${log}`);
throw new Error(`Shader compile error: ${log}`);
}
}
getRaw(): WebGLShader {
return this.shader;
}
destroy(): void {
this.gl.deleteShader(this.shader);
}
}
🧩 ShaderProgram.ts
ts
export class ShaderProgram {
private program: WebGLProgram;
private gl: WebGL2RenderingContext;
constructor(gl: WebGL2RenderingContext, vertexShader: Shader, fragmentShader: Shader) {
this.gl = gl;
this.program = gl.createProgram()!;
gl.attachShader(this.program, vertexShader.getRaw());
gl.attachShader(this.program, fragmentShader.getRaw());
gl.linkProgram(this.program);
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(this.program);
console.error(`Program link failed: ${log}`);
throw new Error(`Program link error: ${log}`);
}
}
use(): void {
this.gl.useProgram(this.program);
}
getUniformLocation(name: string): WebGLUniformLocation | null {
return this.gl.getUniformLocation(this.program, name);
}
setUniformMatrix4fv(name: string, value: Float32Array): void {
const loc = this.getUniformLocation(name);
if (loc) this.gl.uniformMatrix4fv(loc, false, value);
}
destroy(): void {
this.gl.deleteProgram(this.program);
}
}
🧱 RenderGeometry.ts
ts
export abstract class RenderGeometry {
protected positions: Float32Array = new Float32Array(0);
protected indices: Uint16Array = new Uint16Array(0);
getPositions(): Float32Array {
return this.positions;
}
getIndices(): Uint16Array {
return this.indices;
}
getCount(): number {
return this.indices.length;
}
}
// 示例:用两个三角形模拟一条线段(加宽)
export class RenderSegment extends RenderGeometry {
constructor(start: [number, number], end: [number, number], thickness: number = 2) {
super();
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const len = Math.hypot(dx, dy);
if (len === 0) return;
const nx = dy / len * thickness * 0.5;
const ny = -dx / len * thickness * 0.5;
const p0 = [start[0] - nx, start[1] - ny];
const p1 = [start[0] + nx, start[1] + ny];
const p2 = [end[0] + nx, end[1] + ny];
const p3 = [end[0] - nx, end[1] - ny];
this.positions = new Float32Array([
p0[0], p0[1],
p1[0], p1[1],
p2[0], p2[1],
p3[0], p3[1]
]);
this.indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
}
}
🖌️ Renderer2D.ts
ts
import { mat4 } from 'gl-matrix';
export class Renderer2D {
private program: ShaderProgram;
private uProjection: WebGLUniformLocation | null = null;
private uView: WebGLUniformLocation | null = null;
constructor(program: ShaderProgram) {
this.program = program;
this.program.use();
this.uProjection = program.getUniformLocation('uProjection');
this.uView = program.getUniformLocation('uView');
}
render(geometries: RenderGeometry[], viewMatrix: mat4, projectionMatrix: mat4): void {
this.program.use();
if (this.uProjection) this.program.setUniformMatrix4fv('uProjection', projectionMatrix);
if (this.uView) this.program.setUniformMatrix4fv('uView', viewMatrix);
for (const geom of geometries) {
const vbo = new GLBuffer(this.program.gl, BufferType.ARRAY_BUFFER);
vbo.data(geom.getPositions(), BufferType.ARRAY_BUFFER, UsageHint.STATIC_DRAW);
const ibo = new GLBuffer(this.program.gl, BufferType.ELEMENT_ARRAY_BUFFER);
ibo.data(geom.getIndices(), BufferType.ELEMENT_ARRAY_BUFFER, UsageHint.STATIC_DRAW);
const layout = new VertexLayout()
.addAttribute({
name: 'aPosition',
size: 2,
type: this.program.gl.FLOAT,
normalized: false
});
const vao = new VertexArray(this.program.gl, vbo, layout, ibo);
vao.drawElements(this.program.gl.TRIANGLES, geom.getCount(), this.program.gl.UNSIGNED_SHORT, 0);
vao.destroy(); // 简单示例中每帧重建,实际可用对象池优化
}
}
}
🧠 CanvasCore.ts
ts
export class CanvasCore {
public readonly gl: WebGL2RenderingContext;
public readonly renderer: Renderer2D;
public readonly config: CanvasConfig;
private geometries: RenderGeometry[] = [];
public isDrawing: boolean = false;
constructor(
gl: WebGL2RenderingContext,
renderer: Renderer2D,
config: CanvasConfig
) {
this.gl = gl;
this.renderer = renderer;
this.config = config;
}
renderFrame(viewMatrix: Float32Array, projectionMatrix: Float32Array): void {
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
this.renderer.render(this.geometries, viewMatrix, projectionMatrix);
}
addGeometry(geom: RenderGeometry): void {
if (this.isDrawing) {
this.geometries.push(geom);
}
}
clearGeometries(): void {
this.geometries = [];
}
}
🖱️ EventModule.ts(支持 zoom/pan)
ts
import { View } from './View';
import { CanvasCore } from './CanvasCore';
import { RenderSegment } from './RenderGeometry';
export class EventModule {
private view: View;
private core: CanvasCore;
private lastPanPoint: [number, number] | null = null;
constructor(view: View, core: CanvasCore) {
this.view = view;
this.core = core;
}
onPanStart(x: number, y: number): void {
this.lastPanPoint = [x, y];
}
onPanMove(x: number, y: number): void {
if (!this.lastPanPoint) return;
const delta = [x - this.lastPanPoint[0], y - this.lastPanPoint[1]];
const startWorld = this.view.screenToWorld(this.lastPanPoint);
const currentWorld = this.view.screenToWorld([x, y]);
const panDelta = [startWorld[0] - currentWorld[0], startWorld[1] - currentWorld[1]];
const translation = [panDelta[0], panDelta[1]];
const translateMat = mat4.create();
mat4.translate(translateMat, this.view.viewMatrix, translation);
mat4.copy(this.view.viewMatrix, translateMat);
this.lastPanPoint = [x, y];
}
onZoom(scale: number, centerX: number, centerY: number): void {
const worldCenterBefore = this.view.screenToWorld([centerX, centerY]);
// 缩放 view matrix(可改为修改投影)
const scaleMat = mat4.create();
mat4.scale(scaleMat, this.view.viewMatrix, [scale, scale, 1]);
mat4.copy(this.view.viewMatrix, scaleMat);
const worldCenterAfter = this.view.screenToWorld([centerX, centerY]);
const fix = [
worldCenterBefore[0] - worldCenterAfter[0],
worldCenterBefore[1] - worldCenterAfter[1]
];
mat4.translate(this.view.viewMatrix, this.view.viewMatrix, [fix[0], fix[1], 0]);
}
onMouseDown(x: number, y: number): void {
this.core.isDrawing = true;
this.onPanStart(x, y);
}
onMouseMove(x: number, y: number): void {
if (this.core.isDrawing && this.lastPanPoint) {
const seg = new RenderSegment(this.lastPanPoint, [x, y], 2);
this.core.addGeometry(seg);
this.lastPanPoint = [x, y];
} else {
this.onPanMove(x, y);
}
}
onMouseUp(): void {
this.core.isDrawing = false;
this.lastPanPoint = null;
}
}
✅ 使用示例(外部调用)
ts
// 假设你已获取 gl 上下文
const canvas = document.getElementById('render-canvas') as HTMLCanvasElement;
const gl = canvas.getContext('webgl2')!;
// 配置
const config = new CanvasConfig({ x: 0, y: 0, width: canvas.width, height: canvas.height });
const view = new View(config.canvasClientRect);
// 着色器
const vs = `
attribute vec2 aPosition;
uniform mat4 uView;
uniform mat4 uProjection;
void main() {
gl_Position = uProjection * uView * vec4(aPosition, 0, 1);
}
`;
const fs = `
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0, 0, 1);
}
`;
const vertexShader = new Shader(gl, vs, ShaderType.VERTEX);
const fragmentShader = new Shader(gl, fs, ShaderType.FRAGMENT);
const program = new ShaderProgram(gl, vertexShader, fragmentShader);
const renderer = new Renderer2D(program);
const core = new CanvasCore(gl, renderer, config);
const eventModule = new EventModule(view, core);
// 绑定事件(此处仅为示意)
canvas.addEventListener('mousedown', e => {
const rect = canvas.getBoundingClientRect();
eventModule.onMouseDown(e.clientX - rect.left, e.clientY - rect.top);
});
// 动画循环
function animate() {
core.renderFrame(view.viewMatrix, view.projectionMatrix);
requestAnimationFrame(animate);
}
animate();
🌟 特点总结
| 模块 | 职责说明 |
|---|---|
CanvasCore |
核心协调器,持有状态 |
Renderer2D |
渲染几何体列表 |
RenderGeometry |
几何数据生成(如线段) |
GLBuffer/VAO |
GPU 数据管理 |
ShaderProgram |
着色器编译与使用 |
View |
相机控制,坐标转换 |
EventModule |
支持交互(zoom/pan/draw) |