Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
Author: kanno <[email protected]>
Date:   Tue Jan 21 11:38:00 2025 +0800

    feat: done font evaluate

commit 0a8ad56a2429e01aa4bf7f8b17c135ade3db9e69
Author: kanno <[email protected]>
Date:   Mon Jan 20 17:27:56 2025 +0800

    chore: some comment
  • Loading branch information
nonzzz committed Jan 21, 2025
1 parent c75f4c6 commit 903f5f5
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 55 deletions.
19 changes: 19 additions & 0 deletions src/etoile/etoile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,22 @@ export function traverse(graphs: Display[], handler: (graph: S) => void) {
}
}
}

// https://jhildenbiddle.github.io/canvas-size/#/?id=maxheight
function getCanvasBoundarySize() {
const ua = navigator.userAgent
let size = 16384

if (/Firefox\/(\d+)/.test(ua)) {
const version = parseInt(RegExp.$1, 10)
if (version >= 122) {
size = 23168
} else {
size = 11180
}
}

return { size }
}

export const canvasBoundarySize = getCanvasBoundarySize()
3 changes: 3 additions & 0 deletions src/etoile/graph/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,14 @@ export abstract class S extends Display {
export abstract class Graph extends S {
instruction: ReturnType<typeof createInstruction>
__options__: Partial<LocOptions>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
__widget__: any
abstract style: GraphStyleSheet
constructor(options: Partial<GraphOptions> = {}) {
super(options)
this.instruction = createInstruction()
this.__options__ = options
this.__widget__ = null
}
abstract create(): void
abstract clone(): Graph
Expand Down
2 changes: 2 additions & 0 deletions src/etoile/native/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export function createEffectScope() {
return { run, stop }
}

// Some thoughts DOMEvent was designed this way intentionally. I don't have any idea of splitting the general libray yet.
// The follow captureBoxXy matrix a and d be 1 is because of the scaled canvas (without zoomed) is with a new layout.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bindDOMEvent(el: HTMLElement, evt: DOMEventType | (string & {}), dom: DOMEvent<any>) {
const handler = (e: unknown) => {
Expand Down
99 changes: 99 additions & 0 deletions src/primitives/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Canvas, asserts, canvasBoundarySize, drawGraphIntoCanvas, traverse } from '../etoile'
import type { RenderViewportOptions } from '../etoile'
import { Matrix2D } from '../etoile/native/matrix'
import { TreemapLayout, resetLayout } from './component'
import type { NativeModule } from './struct'

export abstract class Cache {
abstract key: string
abstract get state(): boolean
abstract flush(...args: never): void
abstract destroy(): void
}

// The following is my opinionated.
// For better performance, we desgin a cache system to store the render result.
// two step
// 1. draw current canvas into a cache canvas (offscreen canvas)
// 2. draw cache canvas into current canvas (note we should respect the dpi)
export class RenderCache extends Canvas implements Cache {
key: string
private $memory: boolean
constructor(opts: RenderViewportOptions) {
super(opts)
this.key = 'render-cache'
this.$memory = false
}
get state() {
return this.$memory
}
flush(treemap: TreemapLayout, matrix = new Matrix2D()) {
const { devicePixelRatio, width, height } = treemap.render.options
const { a, d } = matrix
const { size } = canvasBoundarySize
// Check outof boundary
if (width * a >= size || height * d >= size) {
return
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.setOptions({ width: width * a, height: height * d, devicePixelRatio })
resetLayout(treemap, width * a, height * d)
drawGraphIntoCanvas(treemap, { c: this.canvas, ctx: this.ctx, dpr: devicePixelRatio })
this.$memory = true
}
destroy() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.$memory = false
}
}

export class FontCache implements Cache {
key: string
private fonts: Record<string, number>
ellispsis: Record<string, number>
constructor() {
this.key = 'font-cache'
this.fonts = {}
this.ellispsis = {}
}
get state() {
return true
}
flush(treemap: TreemapLayout, matrix = new Matrix2D()) {
const { width, height } = treemap.render.options
const { a, d } = matrix

const zoomedWidth = width * a
const zoomedHeight = height * d
if (zoomedWidth <= width || zoomedHeight <= height) {
return
}
traverse([treemap.elements[0]], (graph) => {
if (asserts.isRoundRect(graph)) {
const { x, y, height: graphHeight, width: graphWidth } = graph
if (!graphHeight || !graphWidth) {
return
}
if (x >= 0 && y >= 0 && (x + graphWidth) <= width && (y + graphHeight) <= height) {
if (graph.__widget__) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const node = graph.__widget__.node as unknown as NativeModule
delete this.fonts[node.id]
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
delete this.ellispsis[node.label]
}
}
}
})
}
destroy() {
this.fonts = {}
this.ellispsis = {}
}
queryFontById(id: string, byDefault: () => number) {
if (!(id in this.fonts)) {
this.fonts[id] = byDefault()
}
return this.fonts[id]
}
}
66 changes: 14 additions & 52 deletions src/primitives/component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable no-use-before-define */
import { Bitmap, Box, Canvas, Schedule, drawGraphIntoCanvas } from '../etoile'
import type { RenderViewportOptions } from '../etoile'
import { Bitmap, Box, Schedule } from '../etoile'
import type { DOMEventDefinition } from '../etoile/native/dom'
import { log } from '../etoile/native/log'
import { Matrix2D } from '../etoile/native/matrix'
import { createRoundBlock, createTitleText } from '../shared'
import { FontCache, RenderCache } from './cache'
import type { RenderDecorator, Series } from './decorator'
import type { ExposedEventMethods, InternalEventDefinition } from './event'
import { INTERNAL_EVENT_MAPPINGS, TreemapEvent } from './event'
Expand Down Expand Up @@ -132,21 +131,19 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
decorator: RenderDecorator
bgBox: Box
fgBox: Box
fontsCaches: Record<string, number>
ellispsisWidthCache: Record<string, number>
highlight: Highlight
renderCache: RenderCache
fontCache: FontCache
constructor(...args: ConstructorParameters<typeof Schedule>) {
super(...args)
this.data = []
this.layoutNodes = []
this.bgBox = new Box()
this.fgBox = new Box()
this.decorator = Object.create(null) as RenderDecorator
this.fontsCaches = Object.create(null) as Record<string, number>
this.ellispsisWidthCache = Object.create(null) as Record<string, number>
this.highlight = new Highlight(this.to, { width: this.render.options.width, height: this.render.options.height })
this.renderCache = new RenderCache(this.render.options)
this.fontCache = new FontCache()
}

drawBackgroundNode(node: LayoutModule) {
Expand All @@ -156,7 +153,9 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
return
}
const fill = this.decorator.color.mappings[node.node.id]
this.bgBox.add(createRoundBlock(x, y, w, h, { fill, padding, radius: 2 }))
const rect = createRoundBlock(x, y, w, h, { fill, padding, radius: 2 })
rect.__widget__ = node
this.bgBox.add(rect)
for (const child of node.children) {
this.drawBackgroundNode(child)
}
Expand All @@ -167,11 +166,8 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
if (!w || !h) { return }
const { titleHeight, rectGap } = node.decorator
const { fontSize, fontFamily, color } = this.decorator.font
let optimalFontSize
if (node.node.id in this.fontsCaches) {
optimalFontSize = this.fontsCaches[node.node.id]
} else {
optimalFontSize = evaluateOptimalFontSize(
const optimalFontSize = this.fontCache.queryFontById(node.node.id, () =>
evaluateOptimalFontSize(
this.render.ctx,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
node.node.label,
Expand All @@ -181,12 +177,12 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
},
w - (rectGap * 2),
node.children.length ? Math.round(titleHeight / 2) + rectGap : h
)
this.fontsCaches[node.node.id] = optimalFontSize
}
))

this.render.ctx.font = `${optimalFontSize}px ${fontFamily}`

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const result = getSafeText(this.render.ctx, node.node.label, w - (rectGap * 2), this.ellispsisWidthCache)
const result = getSafeText(this.render.ctx, node.node.label, w - (rectGap * 2), this.fontCache.ellispsis)
if (!result) { return }
if (result.width >= w || optimalFontSize >= h) { return }
const { text, width } = result
Expand All @@ -201,7 +197,6 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
reset(refresh = false) {
this.remove(this.bgBox, this.fgBox)
this.bgBox.destory()

if (this.renderCache.state) {
this.fgBox.destory()
this.bgBox.add(new Bitmap({ bitmap: this.renderCache.canvas }))
Expand All @@ -219,7 +214,6 @@ export class TreemapLayout extends Schedule<InternalEventDefinition> {
this.fgBox = this.fgBox.clone()
}
}

this.add(this.bgBox, this.fgBox)
}

Expand Down Expand Up @@ -271,11 +265,10 @@ export function createTreemap() {
function resize() {
if (!treemap || !root) { return }
treemap.renderCache.destroy()
treemap.fontCache.destroy()
const { width, height } = root.getBoundingClientRect()
treemap.render.initOptions({ height, width, devicePixelRatio: window.devicePixelRatio })
treemap.render.canvas.style.position = 'absolute'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
treemap.fontsCaches = Object.create(null)
treemap.event.emit(INTERNAL_EVENT_MAPPINGS.ON_CLEANUP)
treemap.highlight.render.initOptions({ height, width, devicePixelRatio: window.devicePixelRatio })
treemap.highlight.reset()
Expand Down Expand Up @@ -319,34 +312,3 @@ export function createTreemap() {
}

export type TreemapInstanceAPI = TreemapLayout['api']

// The following is my opinionated.
// For better performance, we desgin a cache system to store the render result.
// two step
// 1. draw current canvas into a cache canvas (offscreen canvas)
// 2. draw cache canvas into current canvas (note we should respect the dpi)
export class RenderCache extends Canvas {
key: string
private $memory: boolean
constructor(opts: RenderViewportOptions) {
super(opts)
this.key = 'render-cache'
this.$memory = false
}
get state() {
return this.$memory
}
flush(treemap: TreemapLayout, matrix = new Matrix2D({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 })) {
const { devicePixelRatio, width, height } = treemap.render.options
const { a, d } = matrix
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.setOptions({ width: width * a, height: height * d, devicePixelRatio })
resetLayout(treemap, width * a, height * d)
drawGraphIntoCanvas(treemap, { c: this.canvas, ctx: this.ctx, dpr: devicePixelRatio })
this.$memory = true
}
destroy() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.$memory = false
}
}
2 changes: 1 addition & 1 deletion src/primitives/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const defaultLayoutOptions = {
export const defaultFontOptions = {
color: '#000',
fontSize: {
max: 38,
max: 70,
min: 0
},
fontFamily: 'sans-serif'
Expand Down
6 changes: 4 additions & 2 deletions src/primitives/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export class TreemapEvent extends DOMEvent {
const translateX = offsetX - (offsetX - this.matrix.e) * delta
const translateY = offsetY - (offsetY - this.matrix.f) * delta
runEffect((progress) => {
treemap.fontCache.flush(treemap, this.matrix)
this.state.isWheeling = true
const easedProgress = easing.quadraticOut(progress)
const scale = (targetScaleRatio - this.matrix.a) * easedProgress
Expand All @@ -308,6 +309,7 @@ export class TreemapEvent extends DOMEvent {
onStop: () => {
this.state.forceDestroy = false
this.state.isWheeling = false
treemap.fontCache.flush(treemap, this.matrix)
}
})
}
Expand Down Expand Up @@ -380,8 +382,8 @@ function createOnZoom(treemap: TreemapLayout, evt: TreemapEvent) {
const c = treemap.render.canvas
const boundingClientRect = c.getBoundingClientRect()
const [w, h] = estimateZoomingArea(node, root, boundingClientRect.width, boundingClientRect.height)
delete treemap.fontsCaches[node.node.id]
delete treemap.ellispsisWidthCache[node.node.id]
// delete treemap.fontsCaches[node.node.id]
// delete treemap.ellispsisWidthCache[node.node.id]
resetLayout(treemap, w, h)
const module = findRelativeNodeById(node.node.id, treemap.layoutNodes)
if (module) {
Expand Down

0 comments on commit 903f5f5

Please sign in to comment.