编写一个简单的2D渲染引擎

编写一个简单的2D渲染引擎

市面上成熟的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 {

name: string

fn: (instance: T) => void

}

export function mixin(applyTo: T, methods: InheritedCollections[]) {

methods.forEach(({ name, fn }) => {

Object.defineProperty(app, name, {

value: fn(app),

writable: false

})

})

}有了mixin我们就可以设计和event模块了

type EventCallback

= P extends any[] ? (...args: P) => any : never

export type DefaultEventDefinition = Record

export type BindThisParameter = T extends (...args: infer P) => infer R ? (this: C, ...args: P) => R

: never

export interface EventCollectionData {

name: string

handler: BindThisParameter

ctx: C

}

export type EventCollections = Record<

keyof EvtDefinition,

EventCollectionData[]

>

export class Event {

eventCollections: EventCollections

constructor() {

this.eventCollections = Object.create(null)

}

on(evt: Evt, handler: BindThisParameter, c?: C) {

if (!(evt in this.eventCollections)) {

this.eventCollections[evt] = []

}

const data = > {

name: evt,

handler,

ctx: c || this

}

this.eventCollections[evt].push(data)

}

off(evt: keyof EvtDefinition, handler?: BindThisParameter) {

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) {

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

) {

return (evt: keyof EvtDefinition, handler: BindThisParameter) =>

this.on(evt, handler, c)

}

}

// Schedule.ts

export type PrimitiveEvent = typeof primitiveEvents[number]

export interface PrimitiveEventMetadata {

native: HTMLElementEventMap[T]

module: LayoutModule

}

export type PrimitiveEventCallback = (metadata: PrimitiveEventMetadata) => void

type SelfEventCallback = (metadata: PrimitiveEventMetadata) => void

export type PrimitiveEventDefinition = {

[key in PrimitiveEvent]: BindThisParameter, 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

) {

const handler = (e: unknown) => {

const { x, y } = captureBoxXY(

c,

e,

self.scaleRatio,

self.scaleRatio,

self.translateX,

self.translateY

)

const event = > {

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的骨架了。

上一篇: 为什么大多数台湾女生说话语气都感觉嗲嗲的?
下一篇: ‎装裱大师:作品加画框装裱工具。画家、艺术家、摄影师 VOUN App

相关文章

科普小知识丨中华鲟为什么叫中华鲟?恋家到拒绝“移民”
草莓酱的小说有哪些
号称动物界的攀爬大师,岩羊为何能在近乎垂直的峭壁上行走自如?
钉钉是哪家公司开发的
如何通过共享玩法打造高效汉堡店营销模式
医学上R是什么意思