Skip to content

Commit 6808056

Browse files
authored
feat: using culling render (#22)
* chore: fix typo * feat: add quard tree * feat: perf render
1 parent 1a61683 commit 6808056

10 files changed

Lines changed: 205 additions & 44 deletions

File tree

src/component.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class Component extends Schedule {
142142
)
143143
}
144144

145-
private drawBroundRect(node: LayoutModule) {
145+
private drawRoundRect(node: LayoutModule) {
146146
const [x, y, w, h] = node.layout
147147
const { rectRadius } = node.config
148148

@@ -157,11 +157,11 @@ export class Component extends Schedule {
157157
fill,
158158
padding: 0,
159159
radius: effectiveRadius
160-
})
160+
}, { x, y, w, h })
161161

162162
this.rectLayer.add(rect)
163163
for (const child of node.children) {
164-
this.drawBroundRect(child)
164+
this.drawRoundRect(child)
165165
}
166166
}
167167
private drawText(node: LayoutModule) {
@@ -203,7 +203,7 @@ export class Component extends Schedule {
203203
const textY = y + (node.children && node.children.length > 0
204204
? Math.round(titleAreaHeight / 2)
205205
: Math.round(h / 2))
206-
const textComponent = createTitleText(text, textX, textY, font, config.color)
206+
const textComponent = createTitleText(text, textX, textY, font, config.color, { textX, textY })
207207
this.textLayer.add(textComponent)
208208
for (const child of node.children) {
209209
this.drawText(child)
@@ -224,7 +224,7 @@ export class Component extends Schedule {
224224
}
225225
}
226226
for (const node of this.layoutNodes) {
227-
this.drawBroundRect(node)
227+
this.drawRoundRect(node)
228228
}
229229

230230
for (const node of this.layoutNodes) {
@@ -293,8 +293,8 @@ interface TextLayoutResult {
293293
}
294294

295295
export function getTextLayout(c: CanvasRenderingContext2D, text: string, width: number, height: number): TextLayoutResult {
296-
const textWidth = c.measureText(text).width
297296
const metrics = c.measureText(text)
297+
const textWidth = metrics.width
298298
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
299299

300300
if (textHeight > height) {

src/etoile/graph/box.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { asserts } from './types'
33

44
export abstract class C extends Display {
55
elements: Display[]
6-
constructor() {
7-
super()
6+
constructor(id?: number) {
7+
super(id)
88
this.elements = []
99
}
1010
abstract get __instanceOf__(): DisplayType
@@ -43,8 +43,8 @@ export abstract class C extends Display {
4343
export class Box extends C {
4444
elements: Display[]
4545

46-
constructor() {
47-
super()
46+
constructor(id?: number) {
47+
super(id)
4848
this.elements = []
4949
}
5050

@@ -83,7 +83,7 @@ export class Box extends C {
8383
}
8484

8585
clone() {
86-
const box = new Box()
86+
const box = new Box(this.id)
8787
if (this.elements.length) {
8888
const stack: { elements: Display[], parent: Box }[] = [{ elements: this.elements, parent: box }]
8989

src/etoile/graph/display.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export abstract class Display {
2121
id: number
2222
matrix: Matrix2D
2323
abstract get __instanceOf__(): DisplayType
24-
constructor() {
24+
constructor(id?: number) {
2525
this.parent = null
26-
this.id = SELF_ID.get()
26+
this.id = id ?? SELF_ID.get()
2727
this.matrix = new Matrix2D()
2828
}
2929

@@ -39,7 +39,11 @@ export interface GraphStyleSheet {
3939
lineWidth: number
4040
}
4141

42-
export interface LocOptions {
42+
interface RequestID {
43+
__id__?: number
44+
}
45+
46+
export interface LocOptions extends RequestID {
4347
width: number
4448
height: number
4549
x: number
@@ -166,7 +170,7 @@ export abstract class S extends Display {
166170
skewY: number
167171

168172
constructor(options: Partial<LocOptions> = {}) {
169-
super()
173+
super(options.__id__)
170174
this.width = options.width || 0
171175
this.height = options.height || 0
172176
this.x = options.x || 0
@@ -181,14 +185,16 @@ export abstract class S extends Display {
181185

182186
// For performance. we need impl AABB Check for render.
183187

184-
export abstract class Graph extends S {
188+
export abstract class Graph<T extends Any = Any> extends S {
185189
instruction: ReturnType<typeof createInstruction>
186190
__options__: Partial<LocOptions>
191+
__widget__: T
187192
abstract style: GraphStyleSheet
188-
constructor(options: Partial<GraphOptions> = {}) {
193+
constructor(options: Partial<GraphOptions> = {}, widget?: T) {
189194
super(options)
190195
this.instruction = createInstruction()
191196
this.__options__ = options
197+
this.__widget__ = widget as T
192198
}
193199
abstract create(): void
194200
abstract clone(): Graph

src/etoile/graph/rect.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export type RoundRectStyleOptions = RectStyleOptions & { radius: number }
1111

1212
export type RoundRectOptions = RectOptions & { style: Partial<RoundRectStyleOptions> }
1313

14-
export class RoundRect extends Graph {
14+
export class RoundRect<T extends Any = Any> extends Graph {
1515
style: Required<RoundRectStyleOptions>
16-
constructor(options: Partial<RoundRectOptions> = {}) {
17-
super(options)
16+
constructor(options: Partial<RoundRectOptions> = {}, widget?: T) {
17+
super(options, widget)
1818
this.style = (options.style || Object.create(null)) as Required<RoundRectStyleOptions>
1919
}
2020

@@ -51,6 +51,6 @@ export class RoundRect extends Graph {
5151
}
5252

5353
clone() {
54-
return new RoundRect({ ...this.style, ...this.__options__ })
54+
return new RoundRect({ ...this.style, ...this.__options__, __id__: this.id }, this.__widget__)
5555
}
5656
}

src/etoile/graph/text.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ export interface TextOptions extends Omit<GraphOptions, 'style'> {
1414
>
1515
}
1616

17-
export class Text extends Graph {
17+
export class Text<T extends Any = Any> extends Graph {
1818
text: string
1919
style: Required<TextOptions['style']>
20-
constructor(options: Partial<TextOptions> = {}) {
21-
super(options)
20+
constructor(options: Partial<TextOptions> = {}, widget?: T) {
21+
super(options, widget)
2222
this.text = options.text || ''
2323
this.style = (options.style || Object.create(null)) as Required<TextOptions['style']>
2424
}
@@ -35,7 +35,7 @@ export class Text extends Graph {
3535
}
3636

3737
clone() {
38-
return new Text({ ...this.style, ...this.__options__ })
38+
return new Text({ ...this.style, ...this.__options__, __id__: this.id }, this.__widget__)
3939
}
4040

4141
get __shape__() {

src/etoile/graph/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function isRoundRect(display: Display): display is RoundRect {
1515
return isGraph(display) && display.__shape__ === DisplayType.RoundRect
1616
}
1717

18-
export function isText(display: Display): display is Text {
18+
export function isText<T extends Any = Any>(display: Display): display is Text<T> {
1919
return isGraph(display) && display.__shape__ === DisplayType.Text
2020
}
2121

src/etoile/native/matrix.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ export class Matrix2D implements MatrixLoc {
5151
return this
5252
}
5353

54+
transformPoint(x: number, y: number) {
55+
return {
56+
x: this.a * x + this.c * y + this.e,
57+
y: this.b * x + this.d * y + this.f
58+
}
59+
}
60+
5461
translation(x: number, y: number) {
5562
this.e += x
5663
this.f += y

src/etoile/schedule/index.ts

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
/* eslint-disable no-use-before-define */
12
import { applyCanvasTransform } from '../../shared'
23
import { Box, asserts } from '../graph'
34
import { Display } from '../graph/display'
45
import { Event } from '../native/event'
56
import type { DefaultEventDefinition } from '../native/event'
67
import { log } from '../native/log'
8+
import { Matrix2D } from '../native/matrix'
79
import { Render } from './render'
810

911
import type { RenderViewportOptions } from './render'
@@ -19,33 +21,141 @@ export interface DrawGraphIntoCanvasOptions {
1921
// First cleanup canvas
2022
export function drawGraphIntoCanvas(
2123
graph: Display,
22-
opts: DrawGraphIntoCanvasOptions
24+
opts: DrawGraphIntoCanvasOptions,
25+
visibleSet?: Set<number>
2326
) {
2427
const { ctx, dpr } = opts
25-
ctx.save()
28+
if (asserts.isGraph(graph) && visibleSet && !visibleSet.has(graph.id)) {
29+
return
30+
}
31+
2632
if (asserts.isBox(graph)) {
2733
const elements = graph.elements
28-
const cap = elements.length
29-
30-
for (let i = 0; i < cap; i++) {
34+
for (let i = 0; i < elements.length; i++) {
3135
const element = elements[i]
32-
drawGraphIntoCanvas(element, opts)
36+
if (asserts.isGraph(element) && visibleSet && !visibleSet.has(element.id)) {
37+
continue
38+
}
39+
drawGraphIntoCanvas(element, opts, visibleSet)
3340
}
41+
return
3442
}
3543
if (asserts.isGraph(graph)) {
44+
ctx.save()
3645
const matrix = graph.matrix.create({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 })
3746
matrix.transform(graph.x, graph.y, graph.scaleX, graph.scaleY, graph.rotation, graph.skewX, graph.skewY)
38-
3947
applyCanvasTransform(ctx, matrix, dpr)
4048
graph.render(ctx)
49+
ctx.restore()
50+
}
51+
}
52+
53+
type BBox = { x: number, y: number, width: number, height: number }
54+
55+
function bboxIntersect(a: BBox, b: BBox) {
56+
return !(
57+
a.x + a.width < b.x ||
58+
a.x > b.x + b.width ||
59+
a.y + a.height < b.y ||
60+
a.y > b.y + b.height
61+
)
62+
}
63+
64+
class QuadTree<T> {
65+
boundary: BBox
66+
capacity: number
67+
objects: Array<{ bbox: BBox, obj: T }>
68+
divided: boolean
69+
northeast?: QuadTree<T>
70+
northwest?: QuadTree<T>
71+
southeast?: QuadTree<T>
72+
southwest?: QuadTree<T>
73+
constructor(boundary: BBox, capacity: number = 8) {
74+
this.boundary = boundary
75+
this.capacity = capacity
76+
this.objects = []
77+
this.divided = false
78+
}
79+
insert(bbox: BBox, obj: T): boolean {
80+
if (!bboxIntersect(this.boundary, bbox)) {
81+
return false
82+
}
83+
if (this.objects.length < this.capacity) {
84+
this.objects.push({ bbox, obj })
85+
return true
86+
}
87+
if (!this.divided) {
88+
this.subdivide()
89+
}
90+
return (
91+
this.northeast!.insert(bbox, obj) ||
92+
this.northwest!.insert(bbox, obj) ||
93+
this.southeast!.insert(bbox, obj) ||
94+
this.southwest!.insert(bbox, obj)
95+
)
96+
}
97+
subdivide() {
98+
const { x, y, width, height } = this.boundary
99+
const hw = width / 2
100+
const hh = height / 2
101+
this.northeast = new QuadTree({ x: x + hw, y, width: hw, height: hh }, this.capacity)
102+
this.northwest = new QuadTree({ x, y, width: hw, height: hh }, this.capacity)
103+
this.southeast = new QuadTree({ x: x + hw, y: y + hh, width: hw, height: hh }, this.capacity)
104+
this.southwest = new QuadTree({ x, y: y + hh, width: hw, height: hh }, this.capacity)
105+
this.divided = true
106+
}
107+
query(range: BBox, found: T[] = []): T[] {
108+
if (!bboxIntersect(this.boundary, range)) { return found }
109+
for (const { bbox, obj } of this.objects) {
110+
if (bboxIntersect(bbox, range)) { found.push(obj) }
111+
}
112+
if (this.divided) {
113+
this.northeast!.query(range, found)
114+
this.northwest!.query(range, found)
115+
this.southeast!.query(range, found)
116+
this.southwest!.query(range, found)
117+
}
118+
return found
41119
}
42-
ctx.restore()
120+
}
121+
122+
function collectBoundingGraphics(graph: Display, parentMatrix?: Matrix2D) {
123+
const results: Array<{ bbox: BBox, obj: Display }> = []
124+
const matrix = parentMatrix ?? new Matrix2D()
125+
if (asserts.isGraph(graph)) {
126+
const x = graph.x ?? 0
127+
const y = graph.y ?? 0
128+
const width = graph.width ?? 0
129+
const height = graph.height ?? 0
130+
131+
const p1 = matrix.transformPoint(x, y)
132+
const p2 = matrix.transformPoint(x + width, y + height)
133+
134+
const bbox = <BBox> {
135+
x: Math.min(p1.x, p2.x),
136+
y: Math.min(p1.y, p2.y),
137+
width: Math.abs(p2.x - p1.x),
138+
height: Math.abs(p2.y - p1.y)
139+
}
140+
results.push({ bbox, obj: graph })
141+
}
142+
if (asserts.isBox(graph)) {
143+
const elements = graph.elements
144+
const cap = elements.length
145+
146+
for (let i = 0; i < cap; i++) {
147+
const element = elements[i]
148+
results.push(...collectBoundingGraphics(element, matrix))
149+
}
150+
}
151+
return results
43152
}
44153

45154
export class Schedule<D extends DefaultEventDefinition = DefaultEventDefinition> extends Box {
46155
render: Render
47156
to: HTMLElement
48157
event: Event<D>
158+
quadTree?: QuadTree<Display>
49159
constructor(to: ApplyTo, renderOptions: Partial<RenderViewportOptions> = {}) {
50160
super()
51161
this.to = typeof to === 'string' ? document.querySelector(to)! : to
@@ -60,13 +170,27 @@ export class Schedule<D extends DefaultEventDefinition = DefaultEventDefinition>
60170

61171
update() {
62172
this.render.clear(this.render.options.width, this.render.options.height)
63-
this.execute(this.render, this)
173+
const all = collectBoundingGraphics(this)
174+
const viewport = { x: 0, y: 0, width: this.render.options.width, height: this.render.options.height }
175+
this.quadTree = new QuadTree<Display>(viewport)
176+
const cap = all.length
177+
for (let i = 0; i < cap; i++) {
178+
const { bbox, obj } = all[i]
179+
this.quadTree.insert(bbox, obj)
180+
}
181+
let visibleSet = undefined
182+
if (this.quadTree) {
183+
const visible = this.quadTree.query(viewport)
184+
visibleSet = new Set(visible.map((g) => g.id))
185+
}
186+
this.execute(this.render, this, visibleSet)
187+
64188
const matrix = this.matrix.create({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 })
65189
applyCanvasTransform(this.render.ctx, matrix, this.render.options.devicePixelRatio)
66190
}
67191

68192
// execute all graph elements
69-
execute(render: Render, graph: Display = this) {
70-
drawGraphIntoCanvas(graph, { c: render.canvas, ctx: render.ctx, dpr: render.options.devicePixelRatio })
193+
execute(render: Render, graph: Display = this, visibleSet?: Set<number>) {
194+
drawGraphIntoCanvas(graph, { c: render.canvas, ctx: render.ctx, dpr: render.options.devicePixelRatio }, visibleSet)
71195
}
72196
}

0 commit comments

Comments
 (0)