前言
源码
学习目标
- 对图案编辑器进行整体架构
- 认识各种常见的矩阵
知识点
- 图形架构
- 矩阵
1-架构思想
图形项目的基本架构思想是面向对象。
我在架构图形项目的时候,一般会架构两棵树,一个棵是用于渲染的元素树,一棵是类的继承树。
元素树是类的继承树的实例化,其中的元素一般有两种:
- 显性元素:具备绘图接口,可以直接在场景中显示。比如Img对象。
- 辅助元素:功能对象,辅助绘图,不显示。比如相机,它不会显示出来,但它会变换视图;除此之外,我们后面还会说一个ImgTransformer 对象,它封装了变换图案的方法。
类的继承树与元素树相辅相成的,它会基于元素树的关系和规律,抽离出其共性,建立类对象。
2-关系树
我们可以通过两棵关系树说一下图案编辑器的架构:
- 蓝色节点的是图案编辑器中用于显示或者辅助显示的元素树。
- 绿色节点的是类的继承树。
在类的继承树中:
- Scene 对象是场景对象,是渲染所有元素的舞台,继承自Group对象。其children属性是装载所有Object2D 对象的集合,camera对象是负责变换视图的。
- Object2D 是所有二维对象的基类,其中的属性是所有二维对象都具备的,比如模型矩阵相关的属性。
- Img 对象是负责显示图像的,具备CanvasRenderingContext2D 对象的drawImage() 功能。
- Group 对象是装载Object2D 对象的集合,它可以对其中的元素进行增删改查。
- ImgControler 对象是图像控制器,负责变换图案,并绘制图案的控制框frame,以及显示不同变换状态的鼠标图案mouseShape。
- EventDispatcher 是一个事件调度器,负责事件的捕获、传递与分发,其使用的设计模式是监听者模式。EventDispatcher 是一个应用非常广泛的对象,所以它处于继承链的底层。
- OrbitControler 是相机轨道控制器,负责控制相机的位移和缩放,从而实现对视图的二维变换。
接下来看一下图案编辑器的元素树。
scene 对象的概念我们上面说过了,其children 中的元素如下图所示:
关于图案编辑器的基本架构逻辑就是这样,在后面的章节里,我们会自下而上的建立相应对象。
3-矩阵架构
首先大家要知道,矩阵和坐标系都是一回事,当我在说坐标系的时候也是在说矩阵。
从我们之前的图形树中,可以看到相机、二维对象、集合对象,有一定图形学基础的同学肯定会想到,矩阵架构是少不了的了。
所以我会自上而下的说一下我们所需要矩阵。
3-1-clinet 坐标系
clinet 坐标系就是鼠标事件里的event.clientX和event.clientY。
这对clientX,clientY构成的坐标系,我们就叫它client坐标系了。
具备前端基础的同学应该知道,client坐标就是我们鼠标在整个窗口中的位置。
3-2-canvas 坐标系
默认canvas 坐标系的原点在左上角,x轴朝右,y轴朝下,以像素为单位,如下图所示:
width,height 是画布的像素宽高。
要获取鼠标在canvas坐标系里的位置,可以通过以下方法实现:
clientToCanvas(clientX: number, clientY: number) {
const { canvas } = this
const { left, top } = canvas.getBoundingClientRect()
return new Vector2(clientX - left, clientY - top)
}
3-3-裁剪空间
当有了相机的时候,裁剪空间是必不可少的。
canvas 默认的坐标系也可以理解为一种裁剪空间,反正就是图形出了这个空间就会被裁掉,看不见了。
然而,canvas 默认的坐标原点在左上方,这是不适合直接做裁剪空间的。因为它在和相机视口做映射的时候不太方便。
所以我们可以把canvas 默认的坐标原点放在画布中间,以此坐标系作为裁剪空间。
此裁剪空间的宽高就是canvas画布的像素宽高,这和webgl中(-1,1)的裁剪空间是不一样的。
canvas坐标到裁剪空间的转换方法如下:
canvastoClip({ x, y }: Vector2) {
const {
canvas: { width, height },
} = this
return new Vector2(x - width / 2, y - height / 2)
}
3-4-视图投影矩阵
视图投影矩阵的作用就是把相机在场景中看到的东西投射到裁剪空间中。
视图投影矩阵的概念我在WebGL课程中说过,但这里没那么复杂,大家感兴趣可以看一下。
我们这里的视图投影矩阵只会用相机控制视图的位移和缩放,其实现方法很简单:
transformInvert(ctx: CanvasRenderingContext2D) {
const {
position: { x, y },
zoom,
} = this
const scale = 1 / zoom
ctx.translate(-x, -y)
ctx.scale(scale, scale)
}
3-5-模型矩阵
模型矩阵有本地模型矩阵和世界模型矩阵之分,其作用就是控制物体变换的,比如缩放、旋转和位移。
模型矩阵的获取方法如下:
get matrix(): Matrix3 {
const { position, rotate, scale } = this
return new Matrix3()
.scale(scale.x, scale.y)
.rotate(rotate)
.translate(position.x, position.y)
}
我们可以用canvas内置的矩阵方法实现图形的变换:
transform(ctx: CanvasRenderingContext2D) {
const { position, rotate, scale } = this
ctx.translate(position.x, position.y)
ctx.rotate(rotate)
ctx.scale(scale.x, scale.y)
}
这些方法大家不理解也没事,这里主要给大家看一个概念,之后会详解。
3-6-偏移矩阵
这个偏移矩阵里只有位移数据,是在本地模型矩阵之下的一个矩阵,用于改变图形的变换基点。
在本课程里改变图案的变换基点的时候,我会将drawImage(image, dx, dy, dWidth, dHeight)中的 dx, dy 作为偏移值。
总结
我们在架构初期,一定要做好取舍,舍弃一些东西和抓住一些东西同样重要。
比如我当前在图案编辑器中架构的这两棵树,我是忽略掉了很多细节和周边的,我要抓的是一条主脉,这样有助于我们掌控全场,逐步深入,有条不紊的开发项目。