市面上成熟的2d render engine
数不胜数例如konva.js
,pixi.js
,zrender
等其他。他们都十分的优秀 在学习他们之前 我们可以编写个简单的render engine
去感受这些上层的抽象,我在写这篇blog
的时候是因为我刚刚完成了一个新的作品squarified。
在我们使用比如konva.js
这样的渲染引擎的时候其实不难发现框架会把画布进行一个拆分stage
,group
,graphics
,event
,animation
…。这里我不会详细展开他们的细节。 但是我们需要关注的是如何为我们自己的引擎定义一个简单的抽象。在一个极小的需求场景下我们的设计可以随意一点比如schedule
,graphics
,render
。我的渲染引擎只做了这3部分 剩下的内容都是采用动态拓展的方式去扩展整个的引擎而不是内聚这样的好处是他是可以拆卸的。
Schedule
相比其他的渲染引擎schedule
的职责非常简单只需要管理图形的更新就行比如变换平移以及一些原生canvas
指令的抽象转译。举个例子比如我们创建了一个graphics
如下:
import { Rect, Schedule } from 'lib'
const rectangle =const new Rect({
style: {
fill: 'red',
lineWidth: 2
}
})
const schedule = new Schedule()
schedule.add(rectangle)
schedule.update()
这时候呢Schedule
里就会存在一个图形然后在调用update
的时候把这个图形的比如style
属性转换成对应的canvas
指令 在这个示例中我们会把style
对象转换成如下指令
// ctx 指的是canvas的 context
ctx.fillStyle = 'red'
ctx.lineWidth = 2
// update
// render 指的是 canvas
function update() {
ctx.save()
// ... traverse 图形 做matrix矩阵变换和上面的指令
ctx.restore()
}
这些其实就是Schedule
的核心伪代码了。
Box
既然我们有了Schedule
那么我们的图形怎么组织管理是一个问题,因此我们需要抽象出一个容器的概念去存放我们的图形 让相关联的图形作为一个组这样我们在更新或者后续的优化都是可以更加方便的。
class Box extends Display {
constructor() {
this.elements = []
this.parent = null
}
add() {}
destory() {}
remove() {}
removeAt() {}
}
有了Box
我们就可以把我们刚才的rectangle
放到这个容器里(注意容器是可以嵌套的) 为了简单我们不会给graphics
设计parent
和child
的概念,所有具备父子关系的图形都应该被装到Box
里。
到这里我们就实现了渲染引擎所需要的基本功能了,关于cache
,hit testing
,aabb
,nesting check
这些功能作为一个简单的教程不会涉及(或许哪天我有兴趣了会单独开一篇)。
为了模块化的设计我们需要实现混入的功能相比extends
他更加强大。
export interface InheritedCollections<T = {}> {
name: string
fn: (instance: T) => void
}
export function mixin<T>(applyTo: T, methods: InheritedCollections<T>[]) {
methods.forEach(({ name, fn }) => {
Object.defineProperty(app, name, {
value: fn(app),
writable: false
})
})
}
有了mixin
我们就可以设计和event
模块了
type EventCallback<P = any[]> = P extends any[] ? (...args: P) => any : never
export type DefaultEventDefinition = Record<string, EventCallback>
export type BindThisParameter<T, C = unknown> = T extends (...args: infer P) => infer R ? (this: C, ...args: P) => R
: never
export interface EventCollectionData<EvtDefinition extends DefaultEventDefinition, C = unknown> {
name: string
handler: BindThisParameter<EvtDefinition[keyof EvtDefinition], C>
ctx: C
}
export type EventCollections<EvtDefinition extends DefaultEventDefinition> = Record<
keyof EvtDefinition,
EventCollectionData<EvtDefinition>[]
>
export class Event<EvtDefinition extends DefaultEventDefinition = DefaultEventDefinition> {
eventCollections: EventCollections<EvtDefinition>
constructor() {
this.eventCollections = Object.create(null)
}
on<C, Evt extends keyof EvtDefinition>(evt: Evt, handler: BindThisParameter<EvtDefinition[Evt], unknown extends C ? this : C>, c?: C) {
if (!(evt in this.eventCollections)) {
this.eventCollections[evt] = []
}
const data = <EventCollectionData<EvtDefinition>> {
name: evt,
handler,
ctx: c || this
}
this.eventCollections[evt].push(data)
}
off(evt: keyof EvtDefinition, handler?: BindThisParameter<EvtDefinition[keyof EvtDefinition], unknown>) {
if (evt in this.eventCollections) {
if (!handler) {
this.eventCollections[evt] = []
return
}
this.eventCollections[evt] = this.eventCollections[evt].filter((d) => d.handler !== handler)
}
}
emit(evt: keyof EvtDefinition, ...args: Parameters<EvtDefinition[keyof EvtDefinition]>) {
if (!this.eventCollections[evt]) { return }
const handlers = this.eventCollections[evt]
if (handlers.length) {
handlers.forEach((d) => {
d.handler.call(d.ctx, ...args)
})
}
}
bindWithContext<C>(
c: C
) {
return (evt: keyof EvtDefinition, handler: BindThisParameter<EvtDefinition[keyof EvtDefinition], unknown extends C ? this : C>) =>
this.on(evt, handler, c)
}
}
// Schedule.ts
export type PrimitiveEvent = typeof primitiveEvents[number]
export interface PrimitiveEventMetadata<T extends keyof HTMLElementEventMap> {
native: HTMLElementEventMap[T]
module: LayoutModule
}
export type PrimitiveEventCallback<T extends PrimitiveEvent> = (metadata: PrimitiveEventMetadata<T>) => void
type SelfEventCallback<T extends PrimitiveEvent | 'wheel'> = (metadata: PrimitiveEventMetadata<T>) => void
export type PrimitiveEventDefinition = {
[key in PrimitiveEvent]: BindThisParameter<PrimitiveEventCallback<key>, TreemapInstanceAPI>
}
const schedule = new Schedule()
const event = new Event()
const methods: InheritedCollections[] = [
{
name: 'on',
fn: () => event.bindWithContext(treemap.api).bind(event)
},
{
name: 'off',
fn: () => event.off.bind(event)
},
{
name: 'emit',
fn: () => event.emit.bind(event)
}
]
function bindPrimitiveEvent(
c: HTMLCanvasElement,
evt: PrimitiveEvent,
bus: Event<SelfEventDefinition>
) {
const handler = (e: unknown) => {
const { x, y } = captureBoxXY(
c,
e,
self.scaleRatio,
self.scaleRatio,
self.translateX,
self.translateY
)
const event = <PrimitiveEventMetadata<PrimitiveEvent>> {
native: e,
module: findRelativeNode(c, { x, y }, treemap.layoutNodes)
}
bus.emit(evt, event)
}
c.addEventListener(evt, handler)
return handler
}
const events = ['click', 'mousemove']
for (const evt of events) {
bindPrimitiveEvent(c, evt, event)
}
mixin(schedule, methods)
// ... 自己实现一些dom的绑定
到这为止我们就实现了一个简单的render engine
的骨架了。