惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
S
Secure Thoughts
Application and Cybersecurity Blog
Application and Cybersecurity Blog
AWS News Blog
AWS News Blog
GbyAI
GbyAI
腾讯CDC
WordPress大学
WordPress大学
V
V2EX
小众软件
小众软件
C
CXSECURITY Database RSS Feed - CXSecurity.com
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
H
Hacker News: Front Page
Scott Helme
Scott Helme
The Hacker News
The Hacker News
Schneier on Security
Schneier on Security
K
Kaspersky official blog
S
Security @ Cisco Blogs
N
News | PayPal Newsroom
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Threatpost
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
P
Privacy International News Feed
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
O
OpenAI News
P
Proofpoint News Feed
J
Java Code Geeks
B
Blog RSS Feed
Attack and Defense Labs
Attack and Defense Labs
L
Lohrmann on Cybersecurity
T
Threat Research - Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
阮一峰的网络日志
阮一峰的网络日志
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
AI
AI
Google DeepMind News
Google DeepMind News
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
T
Tailwind CSS Blog
G
GRAHAM CLULEY
A
Arctic Wolf
N
Netflix TechBlog - Medium
L
LINUX DO - 最新话题
爱范儿
爱范儿
博客园 - 聂微东

轻笑Chuckle

NestJS实践杂记 React-记-3 React-记-2 React-记-1 闭包与内存泄漏 Vue3.5 更新了啥 Git lint 相关 集成 lint 代码规范工具 Monorepo 单仓库多应用 Vue+TS 实现虚拟列表 Canvas 2D 事件 Canvas 2D 贝塞尔曲线 Canvas 2D 进阶 Canvas 2D 基础 Git 规范与实践 Git 熟知熟用 解决System占CPU过高 浏览器HTTP缓存 浏览器渲染流程
开发了一个 Canvas 2D 渲染引擎
『轻笑Chuckle』 · 2024-05-23 · via 轻笑Chuckle

发表于更新于

字数总计:2.9k阅读时长:11分钟阅读量:

前言

写一个 Canvas 2D 渲染引擎,让绘制更简单些。

参考:
Canvas的高级绘制技巧和事件系统
canvas进阶——如何实现canvas的事件系统
聊聊Canvas事件机制相关 (非API层,偏框架设计方面)
【前端】从零开始的Canvas事件处理系统(上)
从0-1入门数据可视化-canvas事件系统
如何实现一个canvas渲染引擎
canvas核心技术-如何实现碰撞检测
二、Canvas进阶-9、碰撞检测
“等一下,我碰!”——常见的2D碰撞检测
浅谈 Canvas 渲染引擎
手把手教你打造一款轻量级canvas渲染引擎
leaferjs,全新的 Canvas 渲染引擎

基本架构

Canvas 的原生 API 是非常底层的,所有图形不分层级、父子关系依次覆盖绘制。

为了方便绘制,一个 Canvas 引擎应该如下设计:

  1. 数据结构:仿照 DOM 树结构,设计出层级关系的图形树结构。
  2. 层级关系:抽象出节点,具有坐标位置、样式等基础属性,节点之间可以有父子、兄弟关系。
  3. 组:一个节点及其子节点的集合,可以看作一个组。组也是一个节点,可以作为其他组的子节点。各种图形类都继承自组。
  4. 绘制顺序:祖先节点最先绘制,后代节点后绘制,兄弟节点由 z-index 控制,视觉上子元素在上层。
  5. 定位:子节点完全依赖父节点进行定位,相当于 position:static。
  6. 锚点:在canvas中一切变换都是相对于原点进行的,所以需要实现锚点来确定变换的中心。
  7. 封装:将 Canvas API 的封装成更简单、清晰的形式,贴近于我们熟悉的 DOM,屏蔽底层绘制细节。
  8. 性能:异步批量渲染、离屏渲染、脏区渲染。
  9. 包围盒:AABB、OBB、碰撞检测。这是为了知道每个节点的位置和大小,以便处理图形相交、事件等。
  10. 排版系统:成熟的 canvas 库一般具有排版系统,可以脱离坐标系进行布局。
  11. 多画布:大型的应用可能需要多个画布,就如 Konva,它抽象出了一个舞台 Stage,包含多个画布 Layer,每个 Layer 可以包含多个节点 Node。

qx-canvas

项目地址:qx-canvas

花了一星期时间完成,虽然还没用过 pixijs,但 api 格式是仿照其设计的。

实现了基础图形和路径的绘制、以组为基本渲染单元的树结构、完善的碰撞检测、具有传播机制的事件系统。可以像 DOM 一样操作 Canvas 图形。

项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
├───📁 display/ # 布局管理,树结构
│ ├───📄 group.ts # 组
│ ├───📄 index.ts
│ └───📄 node.ts # 节点
├───📁 events/ # 事件
│ ├───📄 eventAdmin.ts # 事件管理器
│ ├───📄 eventClient.ts # 事件中心
│ ├───📄 eventObject.ts # 事件对象
│ ├───📄 eventSystem.ts # 事件系统
│ └───📄 index.ts
├───📁 graphics/ # 图案
│ ├───📁 style/ # 上下文样式
│ │ ├───📄 fill.ts # 填充样式
│ │ ├───📄 index.ts
│ │ └───📄 line.ts # 描边样式
│ ├───📄 graphics.ts # 图案类
│ ├───📄 graphicsData.ts # 图形数据
│ └───📄 index.ts
├───📁 math/ # 数学相关
│ ├───📄 bezier.ts # 贝塞尔曲线
│ ├───📄 constant.ts # 常量
│ ├───📄 index.ts
│ ├───📄 matrix.ts # 3*3矩阵
│ ├───📄 point.ts # 点
│ └───📄 transform.ts # 变换矩阵
├───📁 renderer/ # 渲染相关
│ ├───📄 canvasRenderer.ts # canvas2d渲染器
│ ├───📄 index.ts
│ └───📄 renderer.ts # 渲染器
├───📁 shapes/ # 各种可绘制的基础图形
│ ├───📄 circle.ts # 圆
│ ├───📄 ellipse.ts # 椭圆
│ ├───📄 image.ts # 图像
│ ├───📄 index.ts
│ ├───📄 path.ts # 路径
│ ├───📄 polygon.ts # 多边形
│ ├───📄 rectangle.ts # 矩形
│ ├───📄 roundRect.ts # 圆角矩形
│ ├───📄 shape.ts # 图形抽象类
│ └───📄 text.ts # 文本
├───📁 test/ # 测试
│ └───📄 index.ts
├───📁 types/ # 类型
│ ├───......
├───📁 utils/ # 工具函数
│ └───📄 index.ts
├───📄 app.ts # 库入口
└───📄 index.ts # 打包入口

快速开始

安装:

1
pnpm i qx-canvas -S

canvas 元素传入 App 类,即可创建一个 App 实例。App 用于管理整个 canvas 及其事件系统。

1
2
3
4
import * as QxCanvas from "qx-canvas";
const app = new QxCanvas.App({
canvas: document.querySelector("canvas")!,
});

接着使用 Graphics 类创建图案对象,并挂载到 app.stage 舞台(根节点)上。

绘制图形:
Graphics 具有一系列绘制基础图形的方法,它们以 draw 开头,如 drawRectdrawCircledrawLine 等。
但你应该先调用 beginFillbeginLine 方法,以表明开始填充描边,并传入样式参数。

添加事件:
你可以为图案对象添加事件监听器,如 clickmousedownmousemove 等,并具有和 DOM 一样的事件传播机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const rect1 = new QxCanvas.Graphics()

.beginFill({
color: "blue",
})
.drawRect(200, 0, 100, 100)
.setPivot(200, 0)
.setRotation(45)
.setScale(2, 2)
.setAlpha(0.5)
.setCursor("pointer")
.addEventListener("click", (e) => {

console.log(e.clone());
});
app.stage.add(rect1);

app.stage舞台、根节点,所有 Graphics 图形都是其后代节点,其类型为 Group 表示的概念。

Graphics 继承自 Group,也就是说,所有图形节点本身也是,可以添加子节点,以此形成树结构。

1
2
3
4
5
6
7
const circle1 = new QxCanvas.Graphics()
.beginLine({
width: 5,
})
.setPosition(0, 100)
.drawCircle(200, 0, 20);
rect1.add(circle1);

在同一个组中,其透明度、变换(旋转、缩放等)是共享叠加的,如:父节点的变换叠加上子节点自身的局部变换,形成子节点最终的全局变换效果。

Graphics 除了可以绘制基础图形,还可以绘制路径,这和原生 Canvas 类似,不过和绘制图形一样,需要提前调用 beginFillbeginLine 方法。

1
2
3
4
5
6
7
const path1 = new QxCanvas.Graphics()
.beginLine()
.moveTo(100, 100)
.lineTo(200, 200)
.lineTo(300, 100)
.closePath();
app.stage.add(path1);

API 文档

较为详细的 API 文档。

App

App 类是整个引擎的入口,用于管理整个 canvas 及其事件系统。

1
2
3
const app = new QxCanvas.App({
canvas: document.querySelector("canvas")!,
});

参数:

1
2
3
4
5
6
7
8
9
10
11
export interface IAppOptions {

canvas: HTMLCanvasElement;

width?: number;
height?: number;

backgroundColor?: string;

backgroundAlpha?: number;
}

属性和方法:

1
2
3
4
5
6
7
8
declare class App<T extends IContext["ctx"] = CanvasRenderingContext2D> {
readonly stage: Group;
constructor(options: IAppOptions);
clear(): void;
resize(width: number, height: number): void;
get ctx(): T;
get canvas(): HTMLCanvasElement;
}

Node

Node 类是所有节点的基类,具有一些基础的属性和方法。(内部类,未向外暴露)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
declare abstract class Node extends EventClient {
visible: boolean;
readonly transform: Transform;
cursor: Cursor;
hitArea: Shape | null;
parent: this | null;
readonly children: this[];
id: string;
class: string[];
get zIndex(): number;
set zIndex(value: number);


setZIndex(index: number): this;
setAlpha(alpha: number): this;
setVisible(visible: boolean): this;
setCursor(cursor: Cursor): this;
setHitArea(hitArea: Shape): this;
setId(id: string): this;
setClass(className: string): this;
addClass(className: string): this;
removeClass(className: string): this;
setScale(x: number, y: number): this;
setRotation(rotation: number): this;
setPosition(x: number, y: number): this;
setPivot(x: number, y: number): this;
setSkew(x: number, y: number): this;


addEventListener(type: EventType, listener: EventListener, options?: boolean | EventOptions): this;

removeEventListener(type: EventType, listener: EventListener, capture?: boolean): this;


findById(id: string): this | null;
findByClass(className: string): this[];
}

Group

Group 类是对象,继承自 Node,用于管理子节点。(内部类,未向外暴露)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
declare class Group extends Node {
add(child: this | this[]): this;
remove(child: this): this;
removeChildren(): this;
removeSelf(): this;
destroy(): void;
getSpreadPath(): Group[];


onBeforeMount(handler: (item: this) => void): this;
onMounted(handler: (item: this) => void): this;
onBeforeRender(handler: (item: this, renderer: CanvasRenderer) => void): this;
onRendering(handler: (item: this, renderer: CanvasRenderer) => void): this;
onRendered(handler: (item: this, renderer: CanvasRenderer) => void): this;
onBeforeUnmount(handler: (item: this) => void): this;
onUnmounted(handler: (item: this) => void): this;
}

Graphics

Graphics 类是图案对象,继承自 Group,用于绘制图形、路径,以及绑定事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
declare class Graphics extends Group {
readonly graphicsDataList: GraphicsData[];

beginFill(style?: Partial<FillStyleType>): this;
beginLine(style?: Partial<LineStyleType>): this;

endFill(): this;
endLine(): this;


contains(p: Point): boolean;



drawRect(x: number, y: number, width: number, height: number): this;

drawCircle(x: number, y: number, radius: number): this;

drawEllipse(x: number, y: number, radiusX: number, radiusY: number): this;

drawRoundRect(x: number, y: number, width: number, height: number, radius: number): this;

drawPolygon(points: number[]): this;

drawText(text: string, x: number, y: number, textStyle?: TextStyle): this;


moveTo(x: number, y: number): this;
lineTo(x: number, y: number): this;

bezierCurveTo(controlPoints: number[], t?: number, accuracy?: number): this;
closePath(): this;

beginPath(): this;

clearPath(): this;

beginClip(): this;

endClip(): this;



setMask(x: number, y: number, width: number, height: number, graphics: Graphics, type?: GlobalCompositeOperation): this;
}

生命周期

每个图案对象 GraphicsGroup)都具有生命周期,以便在特定的时机执行特定的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const s = new QxCanvas.Graphics()
s.onBeforeMount((item) => {
console.log("before mount");
})
.onMounted((item) => {
console.log("mounted");
})
.onBeforeRender((item, renderer) => {
console.log("before render");
})
.onRendering((item, renderer) => {
console.log("rendering");
})
.onRendered((item, renderer) => {
console.log("after render");
app.stage.remove(s);
})
.onBeforeUnmount((item) => {
console.log("before unmount");
})
.onUnmounted((item) => {
console.log("unmounted");
});

顺序如下:

  1. onBeforeMount:挂载到父节点前触发。
  2. onMounted:挂载到父节点后触发。
  3. onBeforeRender:每帧渲染前触发。
  4. onRendering:每帧渲染时触发,此时节点自身已经渲染完毕,但子节点还未渲染。
  5. onRendered:每帧渲染后触发,此时节点及其子节点都已经渲染完毕。
  6. onBeforeUnmount:从父节点移除前触发。
  7. onUnmounted:从父节点移除后触发。

案例

使用 qx-canvas 实现一些简单的案例。

有了渲染引擎,写这些小Demo就非常方便且简单了。

绘画板

可以选择画笔颜色的绘画板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const color = document.querySelector("#color") as HTMLInputElement;

const s1 = new QxCanvas.Graphics();

app.stage.add(s1).addEventListener("mousedown", (e) => {
s1.beginLine({ width: 2, color: color.value });
const onMove = (e: any) => {
s1.lineTo(e.global.x, e.global.y);
};
app.stage.addEventListener("mousemove", onMove);
app.stage.addEventListener(
"mouseup",
() => {
app.stage.removeEventListener("mousemove", onMove);
},
{ once: true }
);
});

任意图形拖动

使用 setPosition 设置位置偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a.addEventListener("mousedown", (e) => {
const mouseDownPoint = e.global.clone();
const { x, y } = a.transform.position;
const onMove = (e: any) => {
const movePoint = e.global.clone();
const dx = movePoint.x - mouseDownPoint.x;
const dy = movePoint.y - mouseDownPoint.y;
a.setPosition(x + dx, y + dy);
};
app.stage.addEventListener("mousemove", onMove);
app.stage.addEventListener(
"mouseup",
() => {
app.stage.removeEventListener("mousemove", onMove);
},
{ once: true }
);
});

刮刮乐

利用遮罩和自由绘画实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mask = new QxCanvas.Graphics().beginLine({ width: 12 });

const bg = new QxCanvas.Graphics()
.beginFill({ color: "#ccc" })
.drawRect(0, 0, app.canvas.width, app.canvas.height)
.setMask(0, 0, app.canvas.width, app.canvas.height, mask, "destination-out")
.addEventListener("mousedown", (e) => {
mask.moveTo(e.global.x, e.global.y);
const onMove = (e: any) => {
mask.lineTo(e.global.x, e.global.y);
};
bg.addEventListener("mousemove", onMove);
bg.addEventListener("mouseup", () => {
bg.removeEventListener("mousemove", onMove);
});
});

app.stage.add(bg);