From bc587391b82569397e490b87457a837206ed5cbb Mon Sep 17 00:00:00 2001 From: Mikey Gower Date: Thu, 2 Nov 2023 11:18:39 -0400 Subject: [PATCH] add in some comparison perf gains for label updates --- examples/native/src/labels/index.ts | 4 +- src/renderers/webgl/node.ts | 2 +- .../webgl/objects/label/background.ts | 74 ++++--- src/renderers/webgl/objects/label/index.ts | 189 ++++++++---------- src/renderers/webgl/objects/label/utils.ts | 73 ++++--- 5 files changed, 170 insertions(+), 172 deletions(-) diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts index 054c5701..feca6d38 100644 --- a/examples/native/src/labels/index.ts +++ b/examples/native/src/labels/index.ts @@ -22,7 +22,7 @@ const NODE_STYLE: Graph.NodeStyle = { label: { position: 'right', fontName: 'NodeLabel', - fontFamily: ['Arial', 'sans-serif'], + fontFamily: 'Arial, sans-serif', background: { color: GREEN_LIGHT }, margin: 4 } @@ -35,7 +35,7 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = { label: { position: 'right', fontName: 'NodeLabelHover', - fontFamily: ['Arial', 'sans-serif'], + fontFamily: 'Arial, sans-serif', background: { color: DARK_GREEN }, color: '#FFF', margin: 4 diff --git a/src/renderers/webgl/node.ts b/src/renderers/webgl/node.ts index 5af2a8bb..cbffdc25 100644 --- a/src/renderers/webgl/node.ts +++ b/src/renderers/webgl/node.ts @@ -50,7 +50,7 @@ export class NodeRenderer { this.renderer.labelObjectManager.delete(this.label) this.labelMounted = false this.label = undefined - } else { + } else if (!this.label.equals(node.label, node.style?.label)) { this.label.update(node.label, node.style?.label) } diff --git a/src/renderers/webgl/objects/label/background.ts b/src/renderers/webgl/objects/label/background.ts index f7b7e036..6bae1b0d 100644 --- a/src/renderers/webgl/objects/label/background.ts +++ b/src/renderers/webgl/objects/label/background.ts @@ -1,6 +1,6 @@ -import utils, { RESOLUTION, STYLE_DEFAULTS } from './utils' -import type { BackgroundPadding, LabelBackgroundStyle } from './utils' -import { BitmapText, ColorSource, Container, Rectangle, Sprite, Text, Texture } from 'pixi.js' +import utils, { STYLE_DEFAULTS } from './utils' +import type { LabelBackgroundStyle } from './utils' +import { BitmapText, ColorSource, Container, Point, Rectangle, Sprite, Text, Texture } from 'pixi.js' import { equals } from '../../../..' export class LabelBackground { @@ -12,28 +12,39 @@ export class LabelBackground { private sprite: Sprite private label: Text | BitmapText private container: Container - private style: Required private rect: Rectangle + private _style: LabelBackgroundStyle constructor(container: Container, label: Text | BitmapText, style: LabelBackgroundStyle) { this.label = label this.container = container - this.style = utils.mergeBackgroundDefaults(style) - this.sprite = Sprite.from(Texture.WHITE, { resolution: RESOLUTION }) + this._style = style + this.rect = this.label.getLocalBounds() + + const { width, height } = this.size + + this.sprite = Sprite.from(Texture.WHITE) + this.sprite.height = height + this.sprite.width = width this.sprite.anchor.set(this.label.anchor.x, this.label.anchor.y) this.sprite.alpha = this.style.opacity this.sprite.tint = this.style.color - this.rect = label.getLocalBounds() - this.resize() } update(label: Text | BitmapText, style: LabelBackgroundStyle) { - this.label = label - this.color = style.color + this.dirty = !equals(style.padding, this._style.padding) this.bounds = label.getLocalBounds() - this.opacity = style.opacity ?? STYLE_DEFAULTS.OPACITY - this.padding = style.padding ?? STYLE_DEFAULTS.PADDING - this.sprite.anchor.set(this.label.anchor.x, this.label.anchor.y) + this.anchor = label.anchor.clone() + + if (this.label !== label) { + this.label = label + } + + if (this._style !== style) { + this._style = style + this.color = style.color + this.opacity = style.opacity ?? STYLE_DEFAULTS.OPACITY + } if (this.dirty) { this.dirty = false @@ -81,42 +92,45 @@ export class LabelBackground { } private resize() { - const [vertical, horizontal] = utils.getBackgroundPadding(this.style.padding) - - const height = this.rect.height + vertical + const { height, width } = this.size if (height !== this.sprite.height) { this.sprite.height = height } - - const width = this.rect.width + horizontal if (width !== this.sprite.width) { this.sprite.width = width } - return this } + private get style() { + return utils.mergeBackgroundDefaults(this._style) + } + + private get size() { + const [top, right, bottom, left] = utils.getBackgroundPadding(this._style.padding) + const height = this.rect.height + top + bottom + const width = this.rect.width + right + left + return { width, height } + } + + private set anchor(anchor: Point) { + if (!this.sprite.anchor.equals(anchor)) { + this.sprite.anchor.copyFrom(anchor) + } + } + private set color(color: ColorSource) { - if (color !== this.style.color) { - this.style.color = color + if (this.sprite.tint !== color) { this.sprite.tint = color } } private set opacity(opacity: number) { - if (opacity !== this.style.opacity) { - this.style.opacity = opacity + if (this.sprite.alpha !== opacity) { this.sprite.alpha = opacity } } - private set padding(padding: BackgroundPadding) { - if (!equals(padding, this.style.padding)) { - this.style.padding = padding - this.dirty = true - } - } - private set bounds(bounds: Rectangle) { if (this.rect.width !== bounds.width || this.rect.height !== bounds.height) { this.rect = bounds diff --git a/src/renderers/webgl/objects/label/index.ts b/src/renderers/webgl/objects/label/index.ts index 0f124800..a41334d9 100644 --- a/src/renderers/webgl/objects/label/index.ts +++ b/src/renderers/webgl/objects/label/index.ts @@ -1,9 +1,8 @@ import utils, { STYLE_DEFAULTS } from './utils' -import type { StyleWithDefaults, LabelPosition, LabelStyle, LabelBackgroundStyle } from './utils' +import type { LabelPosition, LabelStyle, LabelBackgroundStyle } from './utils' import type { Stroke } from '../../../../types' import { BitmapText, Container, Text, TextStyleAlign, TextStyleFill, TextStyleFontWeight } from 'pixi.js' import { LabelBackground } from './background' -import { equals } from '../../../..' /** * TODO @@ -21,17 +20,18 @@ export class Label { private label: string private container: Container private text: BitmapText | Text - private style: StyleWithDefaults private labelBackground: LabelBackground | null = null + private _style: LabelStyle | undefined constructor(container: Container, label: string, style: LabelStyle | undefined) { - this.label = label this.container = container - this.style = utils.mergeDefaults(style) + this.label = label + this._style = style if (utils.isASCII(this.label)) { utils.loadFont(this.style) this.text = new BitmapText(this.label, utils.getBitmapStyle(this.style)) + this.text.resolution = 2 } else { this.text = new Text(this.label, utils.getTextStyle(this.style)) } @@ -43,32 +43,45 @@ export class Label { } update(label: string, style: LabelStyle | undefined) { - this.value = label + const labelHasChanged = this.label !== label + const styleHasChanged = this._style !== style + + this._style = style - const isBitmapText = this.isBitmapText() - const isASCII = utils.isASCII(this.label) - // if the text type has changed, regenerate a new text object - if ((isBitmapText && !isASCII) || (!isBitmapText && isASCII)) { - this.transformText(this.label, utils.mergeDefaults(style)) + if (labelHasChanged) { + this.label = label + this.text.text = label + + const isBitmapText = this.isBitmapText() + const isASCII = utils.isASCII(label) + // if the text type has changed, regenerate a new text object + if ((isBitmapText && !isASCII) || (!isBitmapText && isASCII)) { + this.transformText() + } } - this.wordWrap = style?.wordWrap - this.color = style?.color - this.stroke = style?.stroke - this.fontWeight = style?.fontWeight - this.letterSpacing = style?.letterSpacing - this.position = style?.position ?? STYLE_DEFAULTS.POSITION - this.fontSize = style?.fontSize ?? STYLE_DEFAULTS.FONT_SIZE - this.fontFamily = style?.fontFamily ?? STYLE_DEFAULTS.FONT_FAMILY - this.fontName = style?.fontName ?? STYLE_DEFAULTS.FONT_NAME - this.background = style?.background + if (styleHasChanged) { + this.stroke = style?.stroke + this.wordWrap = style?.wordWrap + this.color = style?.color ?? STYLE_DEFAULTS.COLOR + this.fontWeight = style?.fontWeight ?? STYLE_DEFAULTS.FONT_WEIGHT + this.letterSpacing = style?.letterSpacing ?? STYLE_DEFAULTS.LETTER_SPACING + this.position = style?.position ?? STYLE_DEFAULTS.POSITION + this.fontSize = style?.fontSize ?? STYLE_DEFAULTS.FONT_SIZE + this.fontFamily = style?.fontFamily ?? STYLE_DEFAULTS.FONT_FAMILY + this.fontName = style?.fontName ?? STYLE_DEFAULTS.FONT_NAME + } if (this.dirty) { this.dirty = false this.updateText() } - this.transformed = false + if (labelHasChanged || styleHasChanged) { + this.transformed = false + this.background = style?.background + } + return this } @@ -117,6 +130,10 @@ export class Label { return undefined } + equals(label: string, style: LabelStyle | undefined) { + return this.label === label && this._style === style + } + private isBitmapText(text: Text | BitmapText = this.text): text is BitmapText { return text instanceof BitmapText } @@ -129,20 +146,21 @@ export class Label { } } - private transformText(label: string, style: StyleWithDefaults) { + private transformText() { this.transformed = true const isMounted = this.mounted this.delete() - if (utils.isASCII(label)) { - utils.loadFont(style) - this.text = new BitmapText(label, utils.getBitmapStyle(style)) + if (utils.isASCII(this.label)) { + utils.loadFont(this.style) + this.text = new BitmapText(this.label, utils.getBitmapStyle(this.style)) + this.text.resolution = 2 } else { - this.text = new Text(label, utils.getTextStyle(style)) + this.text = new Text(this.label, utils.getTextStyle(this.style)) } - this.text.anchor.set(...utils.getAnchorPoint(style.position)) + this.anchor = utils.getAnchorPoint(this.style.position) this.text.x = this.x ?? 0 this.text.y = this.y ?? 0 @@ -151,17 +169,13 @@ export class Label { } } - private set value(label: string) { - if (label !== this.label) { - this.text.text = label - this.label = label - } + private get style() { + return utils.mergeDefaults(this._style) } private set position(position: LabelPosition) { this.align = utils.getTextAlign(position) this.anchor = utils.getAnchorPoint(position) - this.style.position = position } private set align(align: TextStyleAlign) { @@ -185,51 +199,40 @@ export class Label { } private set fontSize(fontSize: number) { - if (fontSize !== this.style.fontSize) { - this.style.fontSize = fontSize - if (this.isBitmapText(this.text)) { - if (this.text.fontSize !== fontSize) { - this.dirty = true - this.text.fontSize = fontSize - } - } else { - if (this.text.style.fontSize !== fontSize) { - this.dirty = true - this.text.style.fontSize = fontSize - } + if (this.isBitmapText(this.text)) { + if (this.text.fontSize !== fontSize) { + this.dirty = true + this.text.fontSize = fontSize + } + } else { + if (this.text.style.fontSize !== fontSize) { + this.dirty = true + this.text.style.fontSize = fontSize } } } private set wordWrap(wordWrap: number | undefined) { - if (wordWrap !== this.style.wordWrap) { - this.style.wordWrap = wordWrap - const wordWrapWidth = wordWrap ?? 0 - if (!this.isBitmapText(this.text) && this.text.style.wordWrapWidth !== wordWrapWidth) { - this.dirty = true - this.text.style.wordWrap = wordWrap !== undefined - this.text.style.wordWrapWidth = wordWrapWidth - } + const wordWrapWidth = wordWrap ?? 0 + if (!this.isBitmapText(this.text) && this.text.style.wordWrapWidth !== wordWrapWidth) { + this.dirty = true + this.text.style.wordWrap = wordWrap !== undefined + this.text.style.wordWrapWidth = wordWrapWidth } } - private set color(value: TextStyleFill | undefined) { - if (!equals(value, this.style.color)) { - this.style.color = value - const color = value ?? STYLE_DEFAULTS.COLOR - if (!this.isBitmapText(this.text) && this.text.style.fill !== color) { - this.dirty = true - this.text.style.fill = color - } + private set color(color: TextStyleFill) { + if (!this.isBitmapText(this.text) && this.text.style.fill !== color) { + this.dirty = true + this.text.style.fill = color } } private set stroke(value: Stroke | undefined) { - if (!equals(value, this.style.stroke)) { - this.style.stroke = value + if (!this.isBitmapText(this.text)) { const stroke = value?.color ?? STYLE_DEFAULTS.STROKE const strokeThickness = value?.width ?? STYLE_DEFAULTS.STROKE_THICKNESS - if (!this.isBitmapText(this.text) && (this.text.style.stroke !== stroke || this.text.style.strokeThickness !== strokeThickness)) { + if (this.text.style.stroke !== stroke || this.text.style.strokeThickness !== strokeThickness) { this.dirty = true this.text.style.stroke = stroke this.text.style.strokeThickness = strokeThickness @@ -237,40 +240,28 @@ export class Label { } } - private set fontFamily(fontFamily: string | string[]) { - if (!equals(fontFamily, this.style.fontFamily)) { - this.style.fontFamily = fontFamily - if (!this.isBitmapText(this.text) && !equals(this.text.style.fontFamily, fontFamily)) { - this.dirty = true - this.text.style.fontFamily = fontFamily - } + private set fontFamily(fontFamily: string) { + if (!this.isBitmapText(this.text) && fontFamily !== this.text.style.fontFamily) { + this.dirty = true + this.text.style.fontFamily = fontFamily } } private set fontName(fontName: string) { - if (fontName !== this.style.fontName) { - this.style.fontName = fontName - if (this.isBitmapText(this.text) && this.text.fontName !== fontName) { - this.dirty = true - this.text.fontName = fontName - } + if (this.isBitmapText(this.text) && this.text.fontName !== fontName) { + this.dirty = true + this.text.fontName = fontName } } - private set fontWeight(value: TextStyleFontWeight | undefined) { - if (value !== this.style.fontWeight) { - this.style.fontWeight = value - const fontWeight = value ?? STYLE_DEFAULTS.FONT_WEIGHT - if (!this.isBitmapText(this.text) && this.text.style.fontWeight !== fontWeight) { - this.dirty = true - this.text.style.fontWeight = fontWeight - } + private set fontWeight(fontWeight: TextStyleFontWeight) { + if (!this.isBitmapText(this.text) && this.text.style.fontWeight !== fontWeight) { + this.dirty = true + this.text.style.fontWeight = fontWeight } } private set background(background: LabelBackgroundStyle | undefined) { - this.style.background = background - if (this.labelBackground === null && background !== undefined) { this.labelBackground = new LabelBackground(this.container, this.text, background) } else if (this.labelBackground && background !== undefined) { @@ -281,21 +272,15 @@ export class Label { } } - private set letterSpacing(value: number | undefined) { - if (value !== this.style.letterSpacing) { - this.style.letterSpacing = value - const letterSpacing = value ?? STYLE_DEFAULTS.LETTER_SPACING - if (!this.isBitmapText(this.text)) { - if (this.text.style.letterSpacing !== letterSpacing) { - this.dirty = true - this.text.style.letterSpacing = letterSpacing - } - } else { - if (this.text.letterSpacing !== letterSpacing) { - this.dirty = true - this.text.letterSpacing = letterSpacing - } + private set letterSpacing(letterSpacing: number) { + if (!this.isBitmapText(this.text)) { + if (this.text.style.letterSpacing !== letterSpacing) { + this.dirty = true + this.text.style.letterSpacing = letterSpacing } + } else if (this.text.letterSpacing !== letterSpacing) { + this.dirty = true + this.text.letterSpacing = letterSpacing } } } diff --git a/src/renderers/webgl/objects/label/utils.ts b/src/renderers/webgl/objects/label/utils.ts index 11458342..c00d99aa 100644 --- a/src/renderers/webgl/objects/label/utils.ts +++ b/src/renderers/webgl/objects/label/utils.ts @@ -1,3 +1,4 @@ +import { MIN_ZOOM } from '../..' import type { Stroke } from '../../../../types' import { Text, @@ -8,17 +9,16 @@ import { TextStyleAlign, BitmapFont, ColorSource, - TextStyleFontWeight + TextStyleFontWeight, + LINE_JOIN } from 'pixi.js' export type LabelPosition = 'bottom' | 'left' | 'top' | 'right' -export type BackgroundPadding = number | [vertical: number, horizontal: number] - export type LabelBackgroundStyle = { color: ColorSource opacity?: number - padding?: BackgroundPadding + padding?: number | number[] } export type LabelStyle = Partial<{ @@ -27,7 +27,7 @@ export type LabelStyle = Partial<{ margin: number wordWrap: number letterSpacing: number - fontFamily: string | string[] + fontFamily: string fontWeight: TextStyleFontWeight stroke: Stroke color: TextStyleFill @@ -35,12 +35,13 @@ export type LabelStyle = Partial<{ background: LabelBackgroundStyle }> -type _StyleDefaults = 'fontSize' | 'position' | 'fontFamily' | 'fontName' +type _StyleDefaults = 'fontSize' | 'position' | 'fontFamily' | 'fontName' | 'margin' export type StyleWithDefaults = Omit & { fontSize: number position: LabelPosition fontFamily: string | string[] fontName: string + margin: number } export const RESOLUTION = 2 @@ -57,9 +58,8 @@ export const STYLE_DEFAULTS = { COLOR: '#000000', ALIGN: 'center' as const, POSITION: 'bottom' as const, - LINE_JOIN: 'round' as const, FONT_WEIGHT: 'normal' as const, - FONT_FAMILY: ['Arial', 'sans-serif'] + FONT_FAMILY: 'Arial, sans-serif' } // install text defaults @@ -70,7 +70,7 @@ TextStyle.defaultStyle = { align: STYLE_DEFAULTS.ALIGN, fill: STYLE_DEFAULTS.COLOR, stroke: STYLE_DEFAULTS.STROKE, - lineJoin: STYLE_DEFAULTS.LINE_JOIN, + lineJoin: LINE_JOIN.ROUND, wordWrap: STYLE_DEFAULTS.WORD_WRAP, fontSize: STYLE_DEFAULTS.FONT_SIZE, fontFamily: STYLE_DEFAULTS.FONT_FAMILY, @@ -84,12 +84,14 @@ const mergeDefaults = ({ fontSize = STYLE_DEFAULTS.FONT_SIZE, fontFamily = STYLE_DEFAULTS.FONT_FAMILY, fontName = STYLE_DEFAULTS.FONT_NAME, + margin = STYLE_DEFAULTS.MARGIN, ...style }: LabelStyle = {}): StyleWithDefaults => ({ position, fontSize, fontFamily, fontName, + margin, ...style }) @@ -170,15 +172,17 @@ const getBitmapStyle = (style: StyleWithDefaults): Partial => const loadFont = (style: StyleWithDefaults) => { if (BitmapFont.available[style.fontName] === undefined) { - BitmapFont.from(style.fontName, getTextStyle(style), { - resolution: RESOLUTION, + BitmapFont.from(style.fontName, getTextStyle({ ...style, fontSize: style.fontSize * RESOLUTION * MIN_ZOOM }), { chars: BitmapFont.ASCII }) } } -const getBackgroundPadding = (padding: BackgroundPadding = STYLE_DEFAULTS.PADDING): [vertical: number, horizontal: number] => { - return typeof padding === 'number' ? [padding, padding] : padding +const getBackgroundPadding = ( + padding: number | number[] = STYLE_DEFAULTS.PADDING +): [top: number, right: number, bottom: number, left: number] => { + const [top, right = top, bottom = top, left = right]: number[] = typeof padding === 'number' ? [padding] : padding + return [top, right, bottom, left] } const getLabelCoordinates = ( @@ -186,50 +190,45 @@ const getLabelCoordinates = ( y: number, offset: number, isBitmapText: boolean, - { position, background, margin = STYLE_DEFAULTS.MARGIN }: StyleWithDefaults + { position, background, margin }: StyleWithDefaults ) => { const shift = margin + offset const label = { x, y } const bg = { x, y } - let vertical = 0 - let horizontal = 0 + let top = 0 + let right = 0 + let bottom = 0 + let left = 0 if (background !== undefined) { - const [v, h] = getBackgroundPadding(background.padding) - vertical += v / 2 - horizontal += h / 2 + const [t, r, b, l] = getBackgroundPadding(background.padding) + top += t + right += r + bottom += b + left += l + } + + if (isBitmapText && (position === 'left' || position === 'right')) { + label.y -= 1 + bg.y -= 1 } switch (position) { case 'bottom': - label.y += shift + vertical + label.y += shift + top bg.y += shift - break case 'left': - label.x -= shift + horizontal + label.x -= shift + right bg.x -= shift - - if (isBitmapText) { - label.y -= 1 - bg.y -= 1 - } - break case 'top': - label.y -= shift + vertical + label.y -= shift + bottom bg.y -= shift - break case 'right': - label.x += shift + horizontal + label.x += shift + left bg.x += shift - - if (isBitmapText) { - label.y -= 1 - bg.y -= 1 - } - break }