前言
学习目标
- 创建Group对象
知识点
- 集合
前情回顾
之前我们创建了Img对象,接下来我们建立Group对象。
1-Group对象的功能
Group 是二维图形的集合,它可以对这些图形进行管理,比如增删改查。
2-Group对象的代码实现
其整体代码如下:
- /src/lmm/objects/Group.ts
import { Object2D, Object2DType } from './Object2D'
class Group extends Object2D {
// 子集
children: Object2D[] = []
// 类型
readonly isGroup = true
constructor(attr: Object2DType = {}) {
super()
this.setOption(attr)
}
/* 设置属性 */
setOption(attr: Object2DType) {
Object.assign(this, attr)
}
/* 添加元素 */
add(...objs: Object2D[]) {
for (let obj of objs) {
if (obj === this) {
return this
}
obj.parent && obj.remove()
obj.parent = this
this.children.push(obj)
this.dispatchEvent({ type: 'add', obj })
}
this.sort()
return this
}
/* 删除元素 */
remove(...objs: Object2D[]) {
const { children } = this
for (let obj of objs) {
const index = children.indexOf(obj)
if (index !== -1) {
obj.parent = undefined
this.children.splice(index, 1)
this.dispatchEvent({ type: 'remove', obj })
} else {
for (let child of children) {
if (child instanceof Group) {
child.remove(obj)
}
}
}
}
return this
}
/* 清空children */
clear() {
for (let obj of this.children) {
obj.parent = undefined
this.dispatchEvent({ type: 'removed', obj })
}
this.children = []
return this
}
/* 排序 */
sort() {
const { children } = this
children.sort((a, b) => {
return a.index - b.index
})
for (let child of children) {
child instanceof Group && child.sort()
}
}
/* 根据名称获取元素 */
getObjectByName(name: string) {
return this.getObjectByProperty('name', name)
}
/* 根据某个属性的值获取子对象 */
getObjectByProperty<T>(name: string, value: T): Object2D | undefined {
const { children } = this
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
if (child[name] === value) {
return child
} else if (child instanceof Group) {
const obj = child.getObjectByProperty<T>(name, value)
if (obj) {
return obj
}
}
}
return undefined
}
/* 遍历元素 */
traverse(callback: (obj: Object2D) => void) {
callback(this)
const { children } = this
for (let child of children) {
if (child instanceof Group) {
child.traverse(callback)
} else {
callback(child)
}
}
}
/* 遍历可见元素 */
traverseVisible(callback: (obj: Object2D) => void) {
if (!this.visible) {
return
}
callback(this)
const { children } = this
for (let child of children) {
if (!child.visible) {
continue
}
if (child instanceof Group) {
child.traverse(callback)
} else {
callback(child)
}
}
}
/* 绘图 */
drawShape(ctx: CanvasRenderingContext2D) {
const { children } = this
/* 绘制子对象 */
for (let obj of children) {
obj.draw(ctx)
}
}
}
export { Group }
Img 对象的属性
- children:二维对象的集合。
Img 对象的方法
-
add(…objs: Object2D[]) :添加子对象,其中可以添加多个子对象,并且做2个判断:
- 子对象是否被添加到其它集合中,若是从那个集合中删除此对象。
- 子对象是否是当前对象,若是便取消添加。
-
remove(…objs: Object2D[]):删除元素
-
clear():清空children。
-
sort():基于图形的index排序,影响图形的渲染属性。
-
getObjectByName(name):根据名称获取元素。
-
getObjectByProperty(name,value):根据属性获取元素。
-
traverse(callback):深度遍历元素。
-
traverseVisible(callback):深度遍历可见元素。
-
drawShape(ctx):绘制子对象。
3-Group对象测试
3-1-vitest测试
我们可以先用vitest测试一下Group中的增删改查逻辑。
- /src/test/Group.spec.ts
import { describe, it, expect } from 'vitest'
import { Group } from '../lmm/objects/Group'
import { Object2D } from '../lmm/objects/Object2D'
describe('EventDispatcher', () => {
const group1 = new Group()
group1.addEventListener('add', (event) => {
console.log('add obj:', event.obj.name)
})
const obj1 = new Object2D()
obj1.name = 'obj1'
group1.add(obj1)
const group2 = new Group()
group2.addEventListener('remove', (event) => {
console.log('remove obj:', event.obj.name)
})
group2.name = 'group2'
const obj2 = new Object2D()
obj2.name = 'obj2'
group2.add(obj2)
group1.add(group2)
it('增删改查', () => {
expect(group1.children.length).toBe(2)
expect(group1.children[0].uuid).toBe(obj1.uuid)
expect(group1.getObjectByName('obj2')?.uuid).toBe(obj2.uuid)
expect(group1.getObjectByProperty('uuid', obj2.uuid)?.uuid).toBe(obj2.uuid)
let i = 0
group1.traverse((obj) => {
expect(obj.name).toBe(['', 'obj1', 'group2', 'obj2'][i])
i++
})
obj1.index = 1
group1.sort()
expect(group1.children[1].uuid).toBe(obj1.uuid)
group1.remove(obj2)
expect(group1.getObjectByName('obj2')).toBe(undefined)
group1.clear()
expect(group1.children.length).toBe(0)
})
})
在上面的代码中,我把Group对象除drawShape() 之外的方法都测试了一遍。
其中的方法比较简单,我就不再多说了。
3-2-绘图测试
在examples中建立一个Group.vue文件。
- /src/lmm/examples/Group.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Vector2 } from '../lmm/math/Vector2'
import { Group } from '../lmm/objects/Group'
import { Img } from '../lmm/objects/Img'
import { ImagePromises } from '../lmm/objects/ObjectUtils'
// 获取父级属性
defineProps({
size: { type: Object, default: { width: 0, height: 0 } },
})
// 对应canvas 画布的Ref对象
const canvasRef = ref<HTMLCanvasElement>()
const images: HTMLImageElement[] = []
for (let i = 1; i < 8; i++) {
const image = new Image()
image.src = `https://yxyy-pandora.oss-cn-beijing.aliyuncs.com/stamp-images/${i}.png`
images.push(image)
}
const group = new Group()
/* 渲染 */
function render(ctx: CanvasRenderingContext2D) {
group.add(
...images.map((image, i) => {
return new Img({
image,
position: new Vector2(200, 80 * i + 50),
size: new Vector2(image.width, image.height).multiplyScalar(0.3),
style: {
shadowColor: 'rgba(0,0,0,0.5)',
shadowBlur: 5,
shadowOffsetY: 20,
},
})
})
)
group.draw(ctx)
}
onMounted(() => {
const canvas = canvasRef.value
const ctx = canvas?.getContext('2d')
if (!ctx) {
return
}
Promise.all(ImagePromises(images)).then(() => {
render(ctx)
})
})
</script>
<template>
<canvas ref="canvasRef" :width="size.width" :height="size.height"></canvas>
</template>
<style scoped></style>
路由的配置我就省略了。
效果如下:
在判断图片是否全部加载成功时,我是把图片的加载封装到Promise后,放到Promise.all() 进行监听的。
Promise.all(ImagePromises(images)).then(() => {
render(ctx)
})
上面的ImagePromises() 方法返回的就是图片的Promise 集合。
- /src/lmm/objects/ObjectUtils.ts
import { Matrix3 } from '../math/Matrix3'
import { Vector2 } from '../math/Vector2'
……
function ImagePromise(image: HTMLImageElement) {
return new Promise<HTMLImageElement>((resolve) => {
image.onload = () => {
resolve(image)
}
})
}
function ImagePromises(images: HTMLImageElement[]) {
return images.map((image) => ImagePromise(image))
}
export { crtPathByMatrix, ImagePromise, ImagePromises }
总结
这一章没啥难点,其中的增删改查都是比较基础的业务逻辑,下一章我们会基于Group对象封装一个Scene 场景对象。
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END