- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
import { Container } from '@pixi/display';
import { SVGTextEngineImpl } from './SVGTextEngineImpl';
import { parseMeasurement } from './utils/parseMeasurement';
import type { DisplayObject } from '@pixi/display';
import type { IPointData } from '@pixi/math';
import type { SVGTextEngine } from './SVGTextEngine';
import type { TextStyle, TextStyleFontWeight } from '@pixi/text';
/**
* Draws SVG <text /> elements.
*
* @public
*/
export class SVGTextNode extends Container
{
/**
* The SVG text rendering engine to be used by default in `SVGTextNode`. This API is not stable and
* can change anytime.
*
* @alpha
*/
static defaultEngine: { new(): SVGTextEngine & DisplayObject } = SVGTextEngineImpl;
/**
* An instance of a SVG text engine used to layout and render text.
*/
protected engine: SVGTextEngine & DisplayObject;
/**
* The current text position, where the next glyph will be placed.
*/
protected currentTextPosition: IPointData;
constructor()
{
super();
this.currentTextPosition = { x: 0, y: 0 };
this.engine = new (SVGTextNode.defaultEngine)();
this.addChild(this.engine);
// Listen to nodetransformdirty on the engine so bounds are updated
// when the text is rendered.
this.engine.on('nodetransformdirty', () => {
this.emit('nodetransformdirty');
});
}
/**
* Embeds a `SVGTextElement` in this node.
*
* @param {SVGTextElement} element - The `SVGTextElement` to embed.
*/
async embedText(element: SVGTextElement | SVGTSpanElement, style: Partial<TextStyle> = {}): Promise<void>
{
const engine = this.engine;
if (element instanceof SVGTextElement)
{
await engine.clear();
this.currentTextPosition.x = element.x.baseVal.length > 0
? element.x.baseVal.getItem(0).value
: 0;
this.currentTextPosition.y = element.y.baseVal.length > 0
? element.y.baseVal.getItem(0).value
: 0;
}
const fill = element.getAttribute('fill');
const fontFamily = element.getAttribute('font-family');
const fontSize = parseFloat(element.getAttribute('font-size'));
const fontWeight = element.getAttribute('font-weight');
const letterSpacing = parseMeasurement(element.getAttribute('letter-spacing'), fontSize);
style.fill = fill || style.fill || 'black';
style.fontFamily = fontFamily || !style.fontFamily ? `${fontFamily || 'serif'}, serif` : style.fontFamily;
style.fontSize = !isNaN(fontSize) ? fontSize : style.fontSize;
style.fontWeight = (fontWeight as TextStyleFontWeight) || style.fontWeight || 'normal';
style.letterSpacing = !isNaN(letterSpacing) ? letterSpacing : (style.letterSpacing || 0);
style.wordWrap = true;
style.wordWrapWidth = 400;
const childNodes = element.childNodes;
for (let i = 0, j = childNodes.length; i < j; i++)
{
const childNode = childNodes.item(i);
let textContent: string;
let textStyle: Partial<TextStyle>;
/* eslint-disable-next-line no-undef */
if (childNode instanceof globalThis.Text)
{
textContent = childNode.data;
textStyle = style;
this.currentTextPosition = await engine.put(
childNode,
{
x: this.currentTextPosition.x,
y: this.currentTextPosition.y,
},
textContent,
textStyle,
);
// Ensure transforms are updated as new text phrases are loaded.
this.emit('nodetransformdirty');
}
else if (childNode instanceof SVGTSpanElement)
{
if (childNode.x.baseVal.length > 0)
{
this.currentTextPosition.x = childNode.x.baseVal.getItem(0).value;
}
if (childNode.y.baseVal.length > 0)
{
this.currentTextPosition.y = childNode.y.baseVal.getItem(0).value;
}
await this.embedText(childNode, { ...style });
}
}
}
}