diff --git a/timeline-chart/src/components/time-graph-state.ts b/timeline-chart/src/components/time-graph-state.ts index 7518a5e..d4809cc 100644 --- a/timeline-chart/src/components/time-graph-state.ts +++ b/timeline-chart/src/components/time-graph-state.ts @@ -12,13 +12,33 @@ export interface TimeGraphStateStyle { borderColor?: number } +/** + * Using BitMapText.getLocalBounds() to measure the full label width is expensive, because + * we need to create a BitMapText object to use it. This means that we need to create a BitMapText + * for every state, even if we don't use it. When we remove the states, these objects need to + * be removed as well. In the case where we have a lot states, this cause multiple garbage collection + * and hogs the performance of the timeline chart. + * + * An alternative to measure the width of the label is to use PIXI.TextMetrics.measureText(). + * However, this method applies only for Text objects, and not BitMapText; therefore there is a + * slight difference between the values returned by the two methods. PIXI.TextMetrics returns a + * slightly smaller value. Currently, PIXI does not support measureText() for BitMapText. + * + * Through some trials, it looks like the ratio between the text width returned by the two methods is + * consistent. Hence, we can use PIXI.TextMetrics.measureText() to get a good estimation of the text width, + * then multiply with the SCALING_FACTOR to determine the actual width of the label when rendered + * with BitMapText. + * + * SCALING_FACTOR = BitMapText.getLocalBounds() / PIXI.TextMetrics.measureText() + */ +const SCALING_FACTOR = 1.04; + export class TimeGraphStateComponent extends TimeGraphComponent { static fontController: FontController = new FontController(); protected _options: TimeGraphStyledRect; private textLabelObject: PIXI.BitmapText | undefined; - private textWidth: number; constructor( id: string, @@ -55,8 +75,8 @@ export class TimeGraphStateComponent extends TimeGraphComponent this.textWidth) { - textObjX = position.x + (displayWidth - this.textWidth) / 2; - displayLabel = labelText; - } - else { - const textScaler = displayWidth / this.textWidth; - const index = Math.min(Math.floor(textScaler * labelText.length), labelText.length - 1) - const partialLabel = labelText.substr(0, Math.max(index - 1, 0)); - if (partialLabel.length > 0) { - displayLabel = partialLabel.concat("…"); + if (displayWidth > textWidth) { + textObjX = position.x + (displayWidth - textWidth) / 2; + displayLabel = labelText; + } + else { + const textScaler = displayWidth / textWidth; + const index = Math.min(Math.floor(textScaler * labelText.length), labelText.length - 1); + const partialLabel = labelText.substr(0, Math.max(index - 1, 0)); + if (partialLabel.length > 0) { + displayLabel = partialLabel.concat("…"); + } } - } - this.textLabelObject.text = displayLabel; + if (displayLabel === "") { + this.removeLabel(); + return; + } - if (displayLabel === "") { - return; - } + if (!this.textLabelObject) { + this.textLabelObject = new PIXI.BitmapText(displayLabel, { fontName: fontName }); + this.displayObject.addChild(this.textLabelObject); + } else { + this.textLabelObject.text = displayLabel; + } - this.textLabelObject.alpha = this._options.opacity ?? 1; - this.textLabelObject.x = textObjX; - this.textLabelObject.y = textObjY; + this.textLabelObject.alpha = this._options.opacity ?? 1; + this.textLabelObject.x = textObjX; + this.textLabelObject.y = textObjY; + } + } - if (addLabel) { - this.displayObject.addChild(this.textLabelObject); + private removeLabel() { + if (this.textLabelObject) { + this.textLabelObject.destroy(); + this.textLabelObject = undefined; } } diff --git a/timeline-chart/src/time-graph-font-controller.ts b/timeline-chart/src/time-graph-font-controller.ts index 2e1656a..201d222 100644 --- a/timeline-chart/src/time-graph-font-controller.ts +++ b/timeline-chart/src/time-graph-font-controller.ts @@ -1,25 +1,37 @@ import * as PIXI from "pixi.js-legacy"; +const DEFAULT_FONT_SIZE = 8; +const DEFAULT_FONT_NAME = "LabelFont8White"; +const DEFAULT_FONT_STYLE = { + fontFamily: "monospace", + fontSize: 8, + fill: "white", + fontWeight: "bold" +}; + export class FontController { private fontFamily: string; + private fontStyleMap: Map>; private fontNameMap: Map>; private fontColorMap: Map; - private defaultFontName: string = "LabelFont8White"; constructor(fontFamily: string = "monospace") { this.fontFamily = fontFamily; + this.fontStyleMap = new Map>(); this.fontNameMap = new Map>(); this.fontColorMap = new Map(); - const defaultFontSize = 8; - this.updateFontNameMap(defaultFontSize); + this.updateFontNameMapAndFontStyleMap(DEFAULT_FONT_SIZE); } - getDefaultFontName(): string { - return this.defaultFontName; + getDefaultFont(): { fontName: string, fontStyle: PIXI.TextStyle} { + return { + fontName: DEFAULT_FONT_NAME, + fontStyle: new PIXI.TextStyle(DEFAULT_FONT_STYLE) + } } - createFontName(fontColor: string, fontSize: number): string { + createFont(fontColor: string, fontSize: number): { fontName: string, fontStyle: PIXI.TextStyle } { const fontName = "LabelFont" + fontSize.toString() + fontColor; const fontStyle = { fontFamily: this.fontFamily, @@ -28,17 +40,30 @@ export class FontController { fontWeight: "bold" }; PIXI.BitmapFont.from(fontName, fontStyle, { chars: this.getCharacterSet() }); - return fontName; + + return { + fontName: fontName, + fontStyle: new PIXI.TextStyle(fontStyle) + } } - updateFontNameMap(size: number) { + updateFontNameMapAndFontStyleMap(size: number) { let color2FontMap = new Map(); - color2FontMap.set("black", this.createFontName("Black", size)); - color2FontMap.set("white", this.createFontName("White", size)); + let style2FontMap = new Map(); + + const blackFont = this.createFont("Black", size); + color2FontMap.set("black", blackFont.fontName); + style2FontMap.set("black", blackFont.fontStyle); + + const whiteFont = this.createFont("White", size); + color2FontMap.set("white", whiteFont.fontName); + style2FontMap.set("white", blackFont.fontStyle); + this.fontNameMap.set(size, color2FontMap); + this.fontStyleMap.set(size, style2FontMap); } - getFontName(color: number, size: number): string { + getFont(color: number, size: number): { fontName: string, fontStyle: PIXI.TextStyle | undefined} { let fontColor = this.fontColorMap.get(color); if (!fontColor) { let colorInHex = color.toString(16); @@ -61,16 +86,23 @@ export class FontController { } let fontName: string | undefined; + let fontStyle: PIXI.TextStyle | undefined; if (size) { const MIN_FONT_SIZE = 6; size = Math.max(size, MIN_FONT_SIZE); if (!this.fontNameMap.has(size)) { - this.updateFontNameMap(size); + this.updateFontNameMapAndFontStyleMap(size); } const size2FontMap = this.fontNameMap.get(size); fontName = size2FontMap ? size2FontMap.get(fontColor) : undefined; + + const size2StyleMap = this.fontStyleMap.get(size); + fontStyle = size2StyleMap ? size2StyleMap.get(fontColor) : undefined; + } + return { + fontName: fontName ? fontName : "", + fontStyle: fontStyle } - return fontName ? fontName : ""; } private getCharacterSet(): string[][] {