pixi-glyphs 4.1.7 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,7 @@ The style objects are modified versions (supersets) of `PIXI.TextStyle` (referre
107
107
  - `lineThroughThickness` - Sets the thickness of the line-through. Default is `1`.
108
108
  - `lineThroughOffset` - Positions the line-through above or below the default location. Default is `0`.
109
109
  - `adjustBaseline` - Adjusts the position of the text above or below the baseline. Default is `0`. Also see the `adjustFontBaseline` property in the options.
110
+ - `topTrim` - Adjusts the line height of text by trimming (or expanding) from the top of the text. Positive values reduce the line height by trimming from the top (useful for reducing gaps above large text), while negative values increase the line height. This property only affects the segments that have it set, and the overall line height is determined by the tallest effective segment after all individual `topTrim` adjustments. Default is `0`. Example: `<big topTrim="50">Big Text</big>` reduces the line height of "Big Text" by 50 pixels from the top.
110
111
  - `highlightColor` - Adds a background highlight color behind the text. Can be a hex number like `0xFFEB3B` or a hex string like `"#FFEB3B"`. The highlight appears as a continuous solid color box behind the text with no borders, spanning across spaces between words for a seamless highlight effect. When the same highlight color is applied to consecutive text segments, they will be rendered as a single continuous highlight box. Perfect for emphasizing important text, creating visual hierarchy, inline code blocks, or implementing syntax highlighting. Example: `<highlight>This entire phrase is highlighted</highlight>` will create one continuous highlight box.
111
112
  - `color` - An alias for `fill`. It's recommended you just use either `fill` or `color`, but if both are set, `fill` will be used. If tags are nested, `color` on an inner tag can override `fill` in an outer tag.
112
113
 
@@ -321,34 +321,17 @@ const isOnlyWhitespace = s => s.search(/^\s+$/) === 0;
321
321
  const PX_PER_EM = 16;
322
322
  const PX_PER_PERCENT = 16 / 100;
323
323
  const PX_PER_PT = 1.3281472327365;
324
- const fontMetricsCache = new Map();
325
- const measureFont = font => {
326
- if (fontMetricsCache.has(font)) {
327
- return fontMetricsCache.get(font);
328
- }
329
- const canvas = document.createElement('canvas');
330
- const context = canvas.getContext('2d');
331
- if (!context) throw new Error('Cannot get 2D context');
332
- context.font = font;
333
- const measureString = "Mgpjy";
334
- const metrics = context.measureText(measureString);
335
- const fontSize = parseInt(font.match(/(\d+)px/)?.['1'] || '16');
336
- const result = {
337
- ascent: metrics.actualBoundingBoxAscent || fontSize * 0.88,
338
- descent: metrics.actualBoundingBoxDescent || fontSize * 0.12,
339
- fontSize: fontSize
340
- };
341
- fontMetricsCache.set(font, result);
342
- return result;
343
- };
344
324
  const getFontPropertiesOfText = (textField, forceUpdate = false) => {
325
+ const text = textField.text;
345
326
  const style = textField.style;
346
- const fontSize = typeof style.fontSize === 'number' ? style.fontSize : 16;
347
- const fontFamily = style.fontFamily || 'Arial';
348
- const fontWeight = style.fontWeight || 'normal';
349
- const fontStyle = style.fontStyle || 'normal';
350
- const font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
351
- return measureFont(font);
327
+ const measureText = text && text.trim().length > 0 ? text : 'M';
328
+ const metrics = PIXI__namespace.CanvasTextMetrics.measureText(measureText, style);
329
+ const fontSize = metrics.fontProperties?.fontSize || (typeof style.fontSize === 'number' ? style.fontSize : 16);
330
+ return {
331
+ ascent: metrics.fontProperties?.ascent || fontSize * 0.88,
332
+ descent: metrics.fontProperties?.descent || fontSize * 0.12,
333
+ fontSize: fontSize
334
+ };
352
335
  };
353
336
  const cloneSprite = sprite => new PIXI__namespace.Sprite(sprite.texture);
354
337
  const fontSizeStringToNumber = size => {
@@ -872,7 +855,7 @@ const getTallestToken = line => {
872
855
  }
873
856
  }
874
857
  const relevantWords = lastNonWhitespaceIndex >= 0 ? line.slice(0, lastNonWhitespaceIndex + 1) : line;
875
- return flatReduce((tallest, current) => {
858
+ const tallest = flatReduce((tallest, current) => {
876
859
  if (isWhitespaceToken(current) || isNewlineToken(current)) {
877
860
  return tallest;
878
861
  }
@@ -886,6 +869,14 @@ const getTallestToken = line => {
886
869
  }
887
870
  return tallest;
888
871
  }, createEmptySegmentToken())(relevantWords);
872
+ if ((tallest.bounds?.height ?? 0) === 0) {
873
+ const allTokens = line.flat(2);
874
+ const firstNewline = allTokens.find(t => isNewlineToken(t));
875
+ if (firstNewline) {
876
+ return firstNewline;
877
+ }
878
+ }
879
+ return tallest;
889
880
  };
890
881
  const verticalAlignInLines = (lines, lineSpacing, overrideValign) => {
891
882
  let previousTallestToken = createEmptySegmentToken();
@@ -898,12 +889,29 @@ const verticalAlignInLines = (lines, lineSpacing, overrideValign) => {
898
889
  let tallestToken = getTallestToken(line);
899
890
  let baseHeight = tallestToken.bounds?.height ?? 0;
900
891
  let baseTallestAscent = 0;
892
+ const hasRealContent = line.flat(2).some(seg => !isWhitespaceToken(seg) && !isNewlineToken(seg));
901
893
  for (const word of line) {
902
894
  for (const segment of word) {
903
- if (isWhitespaceToken(segment) || isNewlineToken(segment)) {
895
+ let segAscent = segment.fontProperties?.ascent ?? 0;
896
+ const style = segment.style;
897
+ const strokeWidth = typeof style?.stroke === 'object' ? style.stroke.width : 0;
898
+ const legacyStrokeThickness = style.strokeThickness || 0;
899
+ const strokeThickness = strokeWidth || legacyStrokeThickness;
900
+ if (strokeThickness && strokeThickness > 0) {
901
+ segAscent += strokeThickness / 2;
902
+ }
903
+ const topTrim = segment.style.topTrim ?? 0;
904
+ if (topTrim !== 0) {
905
+ segAscent = Math.max(0, segAscent - topTrim);
906
+ }
907
+ const isNewline = isNewlineToken(segment);
908
+ isWhitespaceToken(segment);
909
+ const skipNewline = isNewlineToken(segment) && hasRealContent;
910
+ const skipWhitespace = isWhitespaceToken(segment) && !isNewline && !(strokeThickness && strokeThickness > 0);
911
+ const isEmptyLineNewline = isNewline && !hasRealContent;
912
+ if ((skipNewline || skipWhitespace) && !isEmptyLineNewline) {
904
913
  continue;
905
914
  }
906
- const segAscent = segment.fontProperties?.ascent ?? 0;
907
915
  if (segAscent > baseTallestAscent) {
908
916
  baseTallestAscent = segAscent;
909
917
  }
@@ -947,13 +955,17 @@ const verticalAlignInLines = (lines, lineSpacing, overrideValign) => {
947
955
  ...bounds
948
956
  };
949
957
  const valign = overrideValign ?? style.valign;
958
+ const strokeWidth = typeof style?.stroke === 'object' ? style.stroke.width : 0;
959
+ const legacyStrokeThickness = style.strokeThickness || 0;
960
+ const strokeThickness = strokeWidth || legacyStrokeThickness;
961
+ const hasStroke = style?.stroke && strokeThickness > 0;
950
962
  let {
951
963
  ascent
952
964
  } = fontProperties;
953
965
  if (isSpriteToken(segment)) {
954
966
  const imgDisplay = segment.style[IMG_DISPLAY_PROPERTY];
955
967
  if (imgDisplay === 'icon') {
956
- ascent = segment.bounds.height - 4;
968
+ ascent = fontProperties.ascent;
957
969
  } else {
958
970
  ascent = segment.bounds.height;
959
971
  }
@@ -967,10 +979,6 @@ const verticalAlignInLines = (lines, lineSpacing, overrideValign) => {
967
979
  continue;
968
980
  }
969
981
  let newY = previousLineBottom;
970
- const strokeWidth = typeof style?.stroke === 'object' ? style.stroke.width : 0;
971
- const legacyStrokeThickness = style.strokeThickness || 0;
972
- const strokeThickness = strokeWidth || legacyStrokeThickness;
973
- const hasStroke = style?.stroke && strokeThickness > 0;
974
982
  switch (valign) {
975
983
  case "bottom":
976
984
  newY += tallestHeight - height;
@@ -1212,14 +1220,6 @@ const calculateTokens = (styledTokens, splitStyle = "words", scaleIcons = true,
1212
1220
  fontProperties = {
1213
1221
  ...getFontPropertiesOfText(localSizer, true)
1214
1222
  };
1215
- if (isOnlyWhitespace(token) === false) {
1216
- const stroke = localSizer.style.stroke ?? 0;
1217
- if (stroke > 0) {
1218
- fontProperties.descent += stroke / 2;
1219
- fontProperties.ascent += stroke / 2;
1220
- fontProperties.fontSize = fontProperties.ascent + fontProperties.descent;
1221
- }
1222
- }
1223
1223
  const sw = style.fontScaleWidth ?? 1.0;
1224
1224
  const sh = style.fontScaleHeight ?? 1.0;
1225
1225
  const scaleWidth = isNaN(sw) || sw < 0 ? 0.0 : sw;
@@ -1232,7 +1232,10 @@ const calculateTokens = (styledTokens, splitStyle = "words", scaleIcons = true,
1232
1232
  if (isOnlyWhitespace(str)) {
1233
1233
  const fontSize = typeof localSizer.style.fontSize === 'string' ? parseInt(localSizer.style.fontSize) : localSizer.style.fontSize || 24;
1234
1234
  const spaceWidth = fontSize * 0.3 * str.length;
1235
- const height = fontProperties.fontSize;
1235
+ let height = fontProperties.fontSize;
1236
+ if (strokeThickness && strokeThickness > 0) {
1237
+ height += strokeThickness;
1238
+ }
1236
1239
  bounds = new PIXI__namespace.Rectangle(0, 0, spaceWidth, height);
1237
1240
  } else {
1238
1241
  const measureSizer = new PIXI__namespace.Text({
@@ -1286,11 +1289,6 @@ const calculateTokens = (styledTokens, splitStyle = "words", scaleIcons = true,
1286
1289
  fontProperties = {
1287
1290
  ...getFontPropertiesOfText(localSizer, true)
1288
1291
  };
1289
- if (strokeThickness && strokeThickness > 0) {
1290
- fontProperties.ascent += strokeThickness / 2;
1291
- fontProperties.descent += strokeThickness / 2;
1292
- fontProperties.fontSize = fontProperties.ascent + fontProperties.descent;
1293
- }
1294
1292
  if (isIcon) {
1295
1293
  const h = Math.max(sprite.height, 1);
1296
1294
  if (h > 1 && sprite.scale.y === 1) {
@@ -1906,6 +1904,8 @@ class Glyphs extends PIXI__namespace.Container {
1906
1904
  color: cleanedStyle.stroke,
1907
1905
  width: cleanedStyle.strokeThickness || 0
1908
1906
  };
1907
+ }
1908
+ if (cleanedStyle.strokeThickness !== undefined) {
1909
1909
  delete cleanedStyle.strokeThickness;
1910
1910
  }
1911
1911
  const textField = new PIXI__namespace.Text({
@@ -2020,11 +2020,11 @@ class Glyphs extends PIXI__namespace.Container {
2020
2020
  const g = this._debugGraphics;
2021
2021
  g.clear();
2022
2022
  let lineHeightGraphics = new PIXI__namespace.Graphics();
2023
- lineHeightGraphics.name = 'lineHeightGraphics';
2023
+ lineHeightGraphics.label = 'lineHeightGraphics';
2024
2024
  debugContainer.addChild(lineHeightGraphics);
2025
2025
  lineHeightGraphics.clear();
2026
2026
  let baselineGraphics = new PIXI__namespace.Graphics();
2027
- baselineGraphics.name = 'baselineGraphics';
2027
+ baselineGraphics.label = 'baselineGraphics';
2028
2028
  baselineGraphics.visible = true;
2029
2029
  baselineGraphics.alpha = 1;
2030
2030
  debugContainer.addChild(baselineGraphics);
@@ -2041,13 +2041,38 @@ class Glyphs extends PIXI__namespace.Container {
2041
2041
  }
2042
2042
  for (let lineNumber = 0; lineNumber < paragraph.length; lineNumber++) {
2043
2043
  const line = paragraph[lineNumber];
2044
- const lineBounds = getBoundsNested(line);
2045
- let lineBoxY = lineBounds.y + 2;
2046
- let lineBoxHeight = lineBounds.height;
2044
+ getBoundsNested(line);
2045
+ for (const word of line) {
2046
+ for (const segment of word) {
2047
+ }
2048
+ }
2049
+ let tallestEffectiveHeight = 0;
2050
+ let tallestEffectiveY = Number.POSITIVE_INFINITY;
2051
+ for (const word of line) {
2052
+ for (const segment of word) {
2053
+ const topTrim = segment.style.topTrim ?? 0;
2054
+ let segmentAscent = segment.fontProperties.ascent;
2055
+ const segmentDescent = segment.fontProperties.descent;
2056
+ if (topTrim !== 0) {
2057
+ segmentAscent = Math.max(0, segmentAscent - topTrim);
2058
+ }
2059
+ const effectiveHeight = segmentAscent + segmentDescent;
2060
+ const effectiveY = segment.bounds.y + segment.fontProperties.ascent - segmentAscent;
2061
+ if (effectiveHeight > tallestEffectiveHeight) {
2062
+ tallestEffectiveHeight = effectiveHeight;
2063
+ tallestEffectiveY = effectiveY;
2064
+ }
2065
+ }
2066
+ }
2067
+ let lineBoxY = tallestEffectiveY;
2068
+ let lineBoxHeight = tallestEffectiveHeight;
2047
2069
  if (this.defaultStyle.wordWrap) {
2048
2070
  const w = this.defaultStyle.wordWrapWidth ?? this.width;
2049
- g.lineStyle(0.5, DEBUG.LINE_COLOR, 0.2);
2050
- g.drawRect(0, lineBoxY, w, lineBoxHeight);
2071
+ g.rect(0, lineBoxY, w, lineBoxHeight).stroke({
2072
+ width: 0.5,
2073
+ color: DEBUG.LINE_COLOR,
2074
+ alpha: 0.2
2075
+ });
2051
2076
  }
2052
2077
  for (let wordNumber = 0; wordNumber < line.length; wordNumber++) {
2053
2078
  const word = line[wordNumber];
@@ -2058,49 +2083,31 @@ class Glyphs extends PIXI__namespace.Container {
2058
2083
  y,
2059
2084
  width
2060
2085
  } = segmentToken.bounds;
2061
- let adjustedY = y;
2062
- const fontSize = segmentToken.fontProperties?.fontSize || 24;
2063
- let adjustment = 0;
2064
- if (fontSize <= 24) {
2065
- adjustment = -2;
2066
- } else if (fontSize <= 28) {
2067
- adjustment = -3;
2068
- } else if (fontSize === 36) {
2069
- adjustment = 5;
2070
- } else if (fontSize > 36 && fontSize <= 47) {
2071
- adjustment = 5 + (fontSize - 36) * 0.2;
2072
- } else if (fontSize > 47) {
2073
- adjustment = 7 + (fontSize - 47) * 0.1;
2074
- }
2075
- adjustedY -= adjustment;
2076
2086
  let baseline;
2077
2087
  if (isSprite) {
2078
- baseline = adjustedY + segmentToken.bounds.height;
2088
+ baseline = y + segmentToken.bounds.height;
2079
2089
  } else {
2080
- baseline = adjustedY + segmentToken.fontProperties.ascent;
2081
- const strokeWidth = typeof segmentToken.style?.stroke === 'object' ? segmentToken.style.stroke.width : 0;
2082
- const legacyStrokeThickness = segmentToken.style.strokeThickness || 0;
2083
- const strokeThickness = strokeWidth || legacyStrokeThickness;
2084
- const hasStroke = segmentToken.style?.stroke && strokeThickness > 0;
2085
- if (fontSize === 36) {
2086
- baseline += 15;
2087
- } else if (hasStroke && fontSize <= 28) {
2088
- baseline += 6;
2089
- } else if (fontSize <= 24) {
2090
- baseline += 4;
2091
- } else {
2092
- baseline += 4;
2090
+ baseline = y + segmentToken.fontProperties.ascent;
2091
+ const actualHeight = segmentToken.bounds.height;
2092
+ const metricsHeight = segmentToken.fontProperties.ascent + segmentToken.fontProperties.descent;
2093
+ const heightDifference = actualHeight - metricsHeight;
2094
+ if (heightDifference > 1) {
2095
+ baseline += heightDifference / 2;
2093
2096
  }
2094
2097
  }
2095
- let boxY = adjustedY;
2098
+ let boxY = y;
2096
2099
  let boxHeight = segmentToken.bounds.height;
2097
2100
  if (!isSprite) {
2098
- const ascent = segmentToken.fontProperties.ascent;
2101
+ let ascent = segmentToken.fontProperties.ascent;
2099
2102
  const descent = segmentToken.fontProperties.descent;
2103
+ const segmentTopTrim = segmentToken.style.topTrim ?? 0;
2104
+ if (segmentTopTrim !== 0) {
2105
+ ascent = Math.max(0, ascent - segmentTopTrim);
2106
+ }
2100
2107
  boxY = baseline - ascent;
2101
2108
  boxHeight = ascent + descent;
2102
2109
  } else {
2103
- boxHeight += segmentToken.fontProperties.descent;
2110
+ boxHeight = segmentToken.bounds.height + segmentToken.fontProperties.descent;
2104
2111
  }
2105
2112
  const strokeColor = isWhitespaceToken(segmentToken) && this.options.drawWhitespace === false ? DEBUG.WHITESPACE_STROKE_COLOR : DEBUG.WORD_STROKE_COLOR;
2106
2113
  const fillColor = isWhitespaceToken(segmentToken) && this.options.drawWhitespace === false ? DEBUG.WHITESPACE_COLOR : DEBUG.WORD_FILL_COLOR;
@@ -2141,15 +2148,16 @@ class Glyphs extends PIXI__namespace.Container {
2141
2148
  }
2142
2149
  }
2143
2150
  if (baselines.length > 0) {
2144
- baselineGraphics.beginFill(DEBUG.BASELINE_COLOR, 0.8);
2145
2151
  for (const {
2146
2152
  x,
2147
2153
  baseline,
2148
2154
  width
2149
2155
  } of baselines) {
2150
- baselineGraphics.drawRect(x, baseline - 1, width, 2);
2156
+ baselineGraphics.rect(x, baseline - 1, width, 2).fill({
2157
+ color: DEBUG.BASELINE_COLOR,
2158
+ alpha: 0.8
2159
+ });
2151
2160
  }
2152
- baselineGraphics.endFill();
2153
2161
  }
2154
2162
  }
2155
2163
  }