diff --git a/examples/native/src/labels/index.ts b/examples/native/src/labels/index.ts index 87d02579..054c5701 100644 --- a/examples/native/src/labels/index.ts +++ b/examples/native/src/labels/index.ts @@ -20,7 +20,7 @@ const NODE_STYLE: Graph.NodeStyle = { icon: TEXT_ICON, stroke: [{ width: 2, color: GREEN_LIGHT }], label: { - position: 'bottom', + position: 'right', fontName: 'NodeLabel', fontFamily: ['Arial', 'sans-serif'], background: { color: GREEN_LIGHT }, @@ -33,11 +33,11 @@ const NODE_HOVER_STYLE: Graph.NodeStyle = { icon: TEXT_ICON, stroke: [{ width: 2, color: GREEN_LIGHT }], label: { - position: 'bottom', + position: 'right', fontName: 'NodeLabelHover', fontFamily: ['Arial', 'sans-serif'], - background: { color: GREEN_LIGHT }, - color: DARK_GREEN, + background: { color: DARK_GREEN }, + color: '#FFF', margin: 4 } } diff --git a/src/renderers/webgl/objects/label/Label.ts b/src/renderers/webgl/objects/label/Label.ts index 9cd9998e..580226ab 100644 --- a/src/renderers/webgl/objects/label/Label.ts +++ b/src/renderers/webgl/objects/label/Label.ts @@ -14,6 +14,7 @@ export class Label { mounted = false private dirty = false + private transformed = false private label: string private container: Container private text: BitmapText | Text @@ -35,7 +36,17 @@ export class Label { } update(label: string, coords: LabelCoords, style?: LabelStyle) { + const previous = this.text.getLocalBounds() + this.value = 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(label, utils.mergeDefaults(style)) + } + this.style.margin = style?.margin this.coordinates = coords this.wordWrap = style?.wordWrap @@ -48,25 +59,19 @@ export class Label { this.fontName = style?.fontName ?? STYLE_DEFAULTS.FONT_NAME this.background = style?.background - const isBitmapText = this.isBitmapText() - const isASCII = utils.isASCII(label) - - if (isASCII) { - // conditionally load font if BitmapFont is unavailable - utils.loadFont(this.style) - } - - if ((isBitmapText && !isASCII) || (!isBitmapText && isASCII)) { - // if the text type has changed, regenerate a new text object - this.dirty = false - this.transformText() - } - if (this.dirty) { this.dirty = false this.updateText() } + if (this.backgroundSprite) { + const bounds = this.text.getLocalBounds() + if (previous.width !== bounds.width || previous.height !== bounds.height) { + utils.setBackgroundSize(this.backgroundSprite, bounds, this.style.background?.padding) + } + } + + this.transformed = false return this } @@ -115,27 +120,17 @@ export class Label { } } - private transformText() { - if (this.isBitmapText()) { - const isMounted = this.mounted - this.delete() - this.text = new Text(this.label, utils.getTextStyle(this.style)) - this.position = this.style.position - this.x = undefined - this.y = undefined - if (isMounted) { - this.mount() - } - } else { - const isMounted = this.mounted - this.delete() - this.text = new BitmapText(this.label, utils.getBitmapStyle(this.style)) - this.position = this.style.position - this.x = undefined - this.y = undefined - if (isMounted) { - this.mount() - } + private transformText(label: string, style: StyleWithDefaults) { + const isMounted = this.mounted + + this.delete() + this.text = utils.createTextObject(label, style) + this.transformed = true + this.x = undefined + this.y = undefined + + if (isMounted) { + this.mount() } } @@ -172,7 +167,7 @@ export class Label { this.align = utils.getPositionAlign(position) this.anchor = utils.getPositionAnchor(position) if (position !== this.style.position) { - this.dirty = true + this.dirty = !this.transformed this.style.position = position } } @@ -199,12 +194,14 @@ export class Label { private set fontSize(fontSize: number) { if (fontSize !== this.style.fontSize) { - this.dirty = true this.style.fontSize = fontSize - if (this.isBitmapText(this.text)) { - this.text.fontSize = fontSize - } else { - this.text.style.fontSize = fontSize + if (!this.transformed) { + this.dirty = true + if (this.isBitmapText(this.text)) { + this.text.fontSize = fontSize + } else { + this.text.style.fontSize = fontSize + } } } } @@ -212,7 +209,7 @@ export class Label { private set wordWrap(wordWrap: number | undefined) { if (wordWrap !== this.style.wordWrap) { this.style.wordWrap = wordWrap - if (!this.isBitmapText(this.text)) { + if (!this.transformed && !this.isBitmapText(this.text)) { this.dirty = true this.text.style.wordWrap = wordWrap !== undefined this.text.style.wordWrapWidth = wordWrap ?? 0 @@ -223,7 +220,7 @@ export class Label { private set color(color: TextStyleFill | undefined) { if (!equals(color, this.style.color)) { this.style.color = color - if (!this.isBitmapText(this.text)) { + if (!this.transformed && !this.isBitmapText(this.text)) { this.dirty = true this.text.style.fill = color ?? STYLE_DEFAULTS.COLOR } @@ -233,7 +230,7 @@ export class Label { private set stroke(stroke: Stroke | undefined) { if (!equals(stroke, this.style.stroke)) { this.style.stroke = stroke - if (!this.isBitmapText(this.text)) { + if (!this.transformed && !this.isBitmapText(this.text)) { this.dirty = true this.text.style.stroke = stroke?.color ?? STYLE_DEFAULTS.STROKE this.text.style.strokeThickness = stroke?.width ?? STYLE_DEFAULTS.STROKE_THICKNESS @@ -243,10 +240,12 @@ export class Label { private set fontFamily(fontFamily: string | string[]) { if (!equals(fontFamily, this.style.fontFamily)) { - this.dirty = true this.style.fontFamily = fontFamily - if (!this.isBitmapText(this.text)) { - this.text.style.fontFamily = fontFamily + if (!this.transformed) { + this.dirty = true + if (!this.isBitmapText(this.text)) { + this.text.style.fontFamily = fontFamily + } } } } @@ -254,7 +253,7 @@ export class Label { private set fontName(fontName: string) { if (fontName !== this.style.fontName) { this.style.fontName = fontName - if (this.isBitmapText(this.text)) { + if (!this.transformed && this.isBitmapText(this.text)) { this.dirty = true this.text.fontName = fontName } @@ -264,7 +263,7 @@ export class Label { private set fontWeight(fontWeight: TextStyleFontWeight | undefined) { if (fontWeight !== this.style.fontWeight) { this.style.fontWeight = fontWeight - if (!this.isBitmapText(this.text)) { + if (!this.transformed && !this.isBitmapText(this.text)) { this.dirty = true this.text.style.fontWeight = fontWeight ?? STYLE_DEFAULTS.FONT_WEIGHT } diff --git a/src/renderers/webgl/objects/label/utils.ts b/src/renderers/webgl/objects/label/utils.ts index 4402d09f..ce0abcec 100644 --- a/src/renderers/webgl/objects/label/utils.ts +++ b/src/renderers/webgl/objects/label/utils.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import type { Stroke } from '../../../../types' import { Text, @@ -12,7 +11,8 @@ import { Sprite, Texture, BitmapText, - TextStyleFontWeight + TextStyleFontWeight, + Rectangle } from 'pixi.js' export type LabelPosition = 'bottom' | 'left' | 'top' | 'right' @@ -53,8 +53,9 @@ const RESOLUTION = 2 export const STYLE_DEFAULTS = { FONT_SIZE: 10, STROKE_THICKNESS: 0, - PADDING: [4, 8] as [number, number], + LETTER_SPACING: 0.5, WORD_WRAP: false, + PADDING: [4, 8] as [number, number], STROKE: '#FFF', FONT_NAME: 'Label', COLOR: '#000000', @@ -77,7 +78,8 @@ TextStyle.defaultStyle = { wordWrap: STYLE_DEFAULTS.WORD_WRAP, fontSize: STYLE_DEFAULTS.FONT_SIZE, fontFamily: STYLE_DEFAULTS.FONT_FAMILY, - strokeThickness: STYLE_DEFAULTS.STROKE_THICKNESS + strokeThickness: STYLE_DEFAULTS.STROKE_THICKNESS, + letterSpacing: STYLE_DEFAULTS.LETTER_SPACING } // utils @@ -156,31 +158,19 @@ const getTextStyle = ({ color, fontFamily, fontSize, fontWeight, wordWrap, strok const getBitmapStyle = (style: StyleWithDefaults): Partial => ({ fontName: style.fontName, fontSize: style.fontSize, - align: getPositionAlign(style.position) + align: getPositionAlign(style.position), + letterSpacing: style.letterSpacing ?? STYLE_DEFAULTS.LETTER_SPACING }) -const bitmapFontIsAvailable = (fontName: string) => BitmapFont.available[fontName] !== undefined - const loadFont = (style: StyleWithDefaults) => { - if (!bitmapFontIsAvailable(style.fontName)) { - BitmapFont.from(style.fontName, { ...getTextStyle(style), letterSpacing: 1 }, { resolution: RESOLUTION, chars: BitmapFont.ASCII }) + if (BitmapFont.available[style.fontName] === undefined) { + BitmapFont.from(style.fontName, getTextStyle(style), { + resolution: RESOLUTION, + chars: BitmapFont.ASCII + }) } } -const getBackgroundPadding = (padding: BackgroundPadding = STYLE_DEFAULTS.PADDING): [vertical: number, horizontal: number] => { - return typeof padding === 'number' ? [padding, padding] : padding -} - -const setBackgroundStyle = (sprite: Sprite, text: Text | BitmapText, { color, opacity = 1, padding }: LabelBackgroundStyle) => { - const [vertical, horizontal] = getBackgroundPadding(padding) - sprite.anchor.set(text.anchor.x, text.anchor.y) - sprite.height = text.height + vertical - sprite.width = text.width + horizontal - sprite.alpha = opacity - sprite.tint = color - return sprite -} - const createTextObject = (label: string, style: StyleWithDefaults) => { let text: BitmapText | Text @@ -195,6 +185,24 @@ const createTextObject = (label: string, style: StyleWithDefaults) => { return text } +const getBackgroundPadding = (padding: BackgroundPadding = STYLE_DEFAULTS.PADDING): [vertical: number, horizontal: number] => { + return typeof padding === 'number' ? [padding, padding] : padding +} + +const setBackgroundSize = (sprite: Sprite, bounds: Rectangle, padding?: BackgroundPadding) => { + const [vertical, horizontal] = getBackgroundPadding(padding) + sprite.height = bounds.height + vertical + sprite.width = bounds.width + horizontal + return sprite +} + +const setBackgroundStyle = (sprite: Sprite, text: Text | BitmapText, { color, opacity = 1, padding }: LabelBackgroundStyle) => { + sprite.anchor.set(text.anchor.x, text.anchor.y) + sprite.alpha = opacity + sprite.tint = color + return setBackgroundSize(sprite, text.getLocalBounds(), padding) +} + const createBackgroundSprite = (text: Text | BitmapText, style: LabelBackgroundStyle) => { const sprite = Sprite.from(Texture.WHITE, { resolution: RESOLUTION }) return setBackgroundStyle(sprite, text, style) @@ -206,9 +214,8 @@ const getLabelCoordinates = ( isBitmapText: boolean ) => { const shift = margin + offset - // BitmapText shifts text down 2px - const label = { x, y: isBitmapText ? y - 1 : y + 1 } - const bg = { x, y: isBitmapText ? y - 1 : y + 1 } + const label = { x, y } + const bg = { x, y } let vertical = 0 let horizontal = 0 @@ -222,18 +229,32 @@ const getLabelCoordinates = ( case 'bottom': label.y += shift + vertical bg.y += shift + break case 'left': label.x -= shift + horizontal bg.x -= shift + + if (isBitmapText) { + label.y -= 1 + bg.y -= 1 + } + break case 'top': label.y -= shift + vertical bg.y -= shift + break case 'right': label.x += shift + horizontal bg.x += shift + + if (isBitmapText) { + label.y -= 1 + bg.y -= 1 + } + break } @@ -248,9 +269,9 @@ export default { getPositionAnchor, getTextStyle, getBitmapStyle, - bitmapFontIsAvailable, loadFont, createTextObject, + setBackgroundSize, setBackgroundStyle, createBackgroundSprite, getBackgroundPadding