@meonode/canvas 1.4.0 → 1.5.1

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.
@@ -73,6 +73,30 @@ class ChartNode extends BoxNode {
73
73
  break;
74
74
  }
75
75
  }
76
+ getSmartYAxisFormatter(maxValue) {
77
+ const absMax = Math.abs(maxValue);
78
+ // Thresholds with corresponding decimal places, divisors, and suffixes
79
+ const thresholds = [
80
+ { min: 1000000, decimals: 1, divisor: 1000000, suffix: 'M' },
81
+ { min: 1000, decimals: 0, divisor: 1, suffix: '' },
82
+ { min: 100, decimals: 1, divisor: 1, suffix: '' },
83
+ { min: 1, decimals: 2, divisor: 1, suffix: '' },
84
+ { min: 0, decimals: 4, divisor: 1, suffix: '' },
85
+ ];
86
+ let config = thresholds[thresholds.length - 1];
87
+ for (const threshold of thresholds) {
88
+ if (absMax >= threshold.min) {
89
+ config = threshold;
90
+ break;
91
+ }
92
+ }
93
+ return (v) => {
94
+ const scaled = v / config.divisor;
95
+ const factor = Math.pow(10, config.decimals);
96
+ const rounded = Math.round(scaled * factor) / factor;
97
+ return rounded.toString() + config.suffix;
98
+ };
99
+ }
76
100
  getLegendLayout(ctx, totalWidth, totalHeight) {
77
101
  if (!this.chartOptions?.showLegend) {
78
102
  return { x: 0, y: 0, width: 0, height: 0, chartWidth: totalWidth, chartHeight: totalHeight, chartX: 0, chartY: 0 };
@@ -180,7 +204,7 @@ class ChartNode extends BoxNode {
180
204
  if (chartOptions?.showYAxis) {
181
205
  const fontSize = chartOptions.yAxisFontSize || 12;
182
206
  ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
183
- const formatter = chartOptions.yAxisLabelFormatter || ((v) => v.toString());
207
+ const formatter = chartOptions.yAxisLabelFormatter || this.getSmartYAxisFormatter(maxValue);
184
208
  const maxLabel = formatter(maxValue);
185
209
  const yAxisWidth = ctx.measureText(maxLabel).width + 10;
186
210
  chartX += yAxisWidth;
@@ -215,7 +239,7 @@ class ChartNode extends BoxNode {
215
239
  ctx.stroke();
216
240
  if (chartOptions?.showYAxis) {
217
241
  const value = maxValue - (maxValue / 5) * i;
218
- const formatter = chartOptions.yAxisLabelFormatter || ((v) => (Math.round(v * 100) / 100).toString());
242
+ const formatter = chartOptions.yAxisLabelFormatter || this.getSmartYAxisFormatter(maxValue);
219
243
  const label = formatter(value);
220
244
  TextNode.renderSimpleText(ctx, label, chartX - 5, gridY, {
221
245
  color: chartOptions.yAxisColor || chartOptions.axisColor || '#000',
@@ -265,7 +289,8 @@ class ChartNode extends BoxNode {
265
289
  });
266
290
  // Render labels
267
291
  if (chartOptions?.showLabels) {
268
- const { renderLabelItem } = chartOptions;
292
+ const { renderLabelItem, xAxisLabelFormatter } = chartOptions;
293
+ const displayLabel = xAxisLabelFormatter ? xAxisLabelFormatter(label, index) : label;
269
294
  if (renderLabelItem) {
270
295
  const labelNode = renderLabelItem({ item: label, index });
271
296
  if (labelNode) {
@@ -276,7 +301,7 @@ class ChartNode extends BoxNode {
276
301
  }
277
302
  }
278
303
  else {
279
- TextNode.renderSimpleText(ctx, label, groupX + (groupWidth - barSpacing) / 2, chartY + finalChartHeight + labelHeight / 2, {
304
+ TextNode.renderSimpleText(ctx, displayLabel, groupX + (groupWidth - barSpacing) / 2, chartY + finalChartHeight + labelHeight / 2, {
280
305
  color: chartOptions.labelColor || chartOptions.axisColor,
281
306
  fontSize: chartOptions.labelFontSize,
282
307
  fontFamily: this.props.fontFamily,
@@ -306,7 +331,7 @@ class ChartNode extends BoxNode {
306
331
  if (chartOptions?.showYAxis) {
307
332
  const fontSize = chartOptions.yAxisFontSize || 12;
308
333
  ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
309
- const formatter = chartOptions.yAxisLabelFormatter || ((v) => v.toString());
334
+ const formatter = chartOptions.yAxisLabelFormatter || this.getSmartYAxisFormatter(maxValue);
310
335
  const maxLabel = formatter(maxValue);
311
336
  const yAxisWidth = ctx.measureText(maxLabel).width + 10;
312
337
  chartX += yAxisWidth;
@@ -339,7 +364,7 @@ class ChartNode extends BoxNode {
339
364
  ctx.stroke();
340
365
  if (chartOptions?.showYAxis) {
341
366
  const value = maxValue - (maxValue / 5) * i;
342
- const formatter = chartOptions.yAxisLabelFormatter || ((v) => (Math.round(v * 100) / 100).toString());
367
+ const formatter = chartOptions.yAxisLabelFormatter || this.getSmartYAxisFormatter(maxValue);
343
368
  const label = formatter(value);
344
369
  TextNode.renderSimpleText(ctx, label, chartX - 5, gridY, {
345
370
  color: chartOptions.yAxisColor || chartOptions.axisColor || '#000',
@@ -380,9 +405,10 @@ class ChartNode extends BoxNode {
380
405
  });
381
406
  // Render labels
382
407
  if (chartOptions?.showLabels) {
383
- const { renderLabelItem } = chartOptions;
408
+ const { renderLabelItem, xAxisLabelFormatter } = chartOptions;
384
409
  labels.forEach((label, index) => {
385
410
  const pointX = chartX + index * pointSpacing;
411
+ const displayLabel = xAxisLabelFormatter ? xAxisLabelFormatter(label, index) : label;
386
412
  if (renderLabelItem) {
387
413
  const labelNode = renderLabelItem({ item: label, index });
388
414
  if (labelNode) {
@@ -393,7 +419,7 @@ class ChartNode extends BoxNode {
393
419
  }
394
420
  }
395
421
  else {
396
- TextNode.renderSimpleText(ctx, label, pointX, chartY + finalChartHeight + labelHeight / 2, {
422
+ TextNode.renderSimpleText(ctx, displayLabel, pointX, chartY + finalChartHeight + labelHeight / 2, {
397
423
  color: chartOptions.labelColor || chartOptions.axisColor,
398
424
  fontSize: chartOptions.labelFontSize,
399
425
  fontFamily: this.props.fontFamily,
@@ -1 +1 @@
1
- {"version":3,"file":"grid.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/grid.canvas.util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAiB,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACtF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAIjE;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,OAAO;gBAC3B,KAAK,EAAE,aAAa;CAMjC;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,aAAa,iBAA4B,CAAA;AAEzE;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC;;;OAGG;gBACS,KAAK,EAAE,SAAS;IAQ5B;;OAEG;IACH,OAAO,CAAC,UAAU;IAqBlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAmBpB;;OAEG;cACgB,+BAA+B;IAwRlD;;OAEG;IACH,OAAO,CAAC,aAAa;CAkCtB;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,aAAwB,CAAA"}
1
+ {"version":3,"file":"grid.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/grid.canvas.util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAiB,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACtF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAIjE;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,OAAO;gBAC3B,KAAK,EAAE,aAAa;CAMjC;AAED;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,aAAa,iBAA4B,CAAA;AAEzE;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC;;;OAGG;gBACS,KAAK,EAAE,SAAS;IAQ5B;;OAEG;IACH,OAAO,CAAC,UAAU;IAwBlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAmBpB;;OAEG;cACgB,+BAA+B;IA0SlD;;OAEG;IACH,OAAO,CAAC,aAAa;CAkCtB;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,SAAS,aAAwB,CAAA"}
@@ -35,6 +35,9 @@ class GridNode extends RowNode {
35
35
  if (track.endsWith('%')) {
36
36
  return { type: '%', value: parsePercentage(track, availableSpace) };
37
37
  }
38
+ if (track.endsWith('px')) {
39
+ return { type: 'px', value: parseFloat(track) };
40
+ }
38
41
  // Try parsing as number (px) if just string "100"
39
42
  const num = parseFloat(track);
40
43
  if (!isNaN(num))
@@ -68,7 +71,25 @@ class GridNode extends RowNode {
68
71
  */
69
72
  updateLayoutBasedOnComputedSize() {
70
73
  // 1. Get Container Dimensions
71
- const width = this.node.getComputedWidth();
74
+ let width = this.node.getComputedWidth();
75
+ // Handle case where computed width is NaN or 0 (e.g., when parent uses alignItems: Center and child has minWidth)
76
+ // Fall back to minWidth if available
77
+ if ((isNaN(width) || width === 0) && this.node.getParent()) {
78
+ const minWidth = this.node.getMinWidth();
79
+ if (!isNaN(minWidth.value) && minWidth.value > 0) {
80
+ if (minWidth.unit === Style.Unit.Percent) {
81
+ // For percentage minWidth, calculate based on parent's computed width
82
+ const parentWidth = this.node.getParent().getComputedWidth();
83
+ if (!isNaN(parentWidth) && parentWidth > 0) {
84
+ width = (minWidth.value / 100) * parentWidth;
85
+ }
86
+ }
87
+ else {
88
+ // For pixel minWidth, use the value directly
89
+ width = minWidth.value;
90
+ }
91
+ }
92
+ }
72
93
  const paddingLeft = this.node.getComputedPadding(Style.Edge.Left);
73
94
  const paddingRight = this.node.getComputedPadding(Style.Edge.Right);
74
95
  const paddingTop = this.node.getComputedPadding(Style.Edge.Top);
@@ -76,6 +76,12 @@ export declare class TextNode extends BoxNode {
76
76
  private parseRichText;
77
77
  private formatSpacing;
78
78
  private parseSpacingToPx;
79
+ /**
80
+ * Adds manual letter spacing compensation to a measured text width.
81
+ * Needed because skia-canvas ctx.measureText() does not include letterSpacing in the returned width,
82
+ * even though letterSpacing IS applied during rendering (fillText/strokeText).
83
+ */
84
+ private addLetterSpacingExtra;
79
85
  /**
80
86
  * Generates a CSS font string by combining base TextProps with optional TextSegment styling.
81
87
  * Follows browser font string format: "font-style font-weight font-size font-family"
@@ -1 +1 @@
1
- {"version":3,"file":"text.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.util.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,yBAAyB,CAAA;AACrE,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAGxD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA8NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IAuKpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAiXrH;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,aAA8B,CAAA"}
1
+ {"version":3,"file":"text.canvas.util.d.ts","sourceRoot":"","sources":["../../../src/canvas/text.canvas.util.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,yBAAyB,CAAA;AACrE,OAAO,EAAU,KAAK,wBAAwB,EAA2B,MAAM,aAAa,CAAA;AAC5F,OAAO,EAAE,OAAO,EAAE,MAAM,gCAAgC,CAAA;AAGxD;;;GAGG;AACH,qBAAa,QAAS,SAAQ,OAAO;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAwC;IACzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,kBAAkB,CAAe;IAEjC,KAAK,EAAE,SAAS,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;gBAElC,IAAI,GAAE,MAAM,GAAG,MAAW,EAAE,KAAK,GAAE,SAAc;IAuB7D;;;;;;;;OAQG;WACW,gBAAgB,CAC5B,GAAG,EAAE,wBAAwB,EAC7B,IAAI,EAAE,MAAM,EACZ,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,KAAK,GAAE;QACL,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAA;QACpC,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAClC,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,SAAS,CAAC,EAAE,wBAAwB,CAAC,WAAW,CAAC,CAAA;QACjD,YAAY,CAAC,EAAE,wBAAwB,CAAC,cAAc,CAAC,CAAA;KACnD;cAwBW,aAAa,IAAI,IAAI;IAoDxC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,sBAAsB;IA8B9B;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+ErB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAyBxB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,WAAW;IA+NnB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IA6KpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAmErB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;;;;;;;;;;OAgBG;cACgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAkXrH;AAED;;GAEG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,GAAG,MAAM,EAAE,QAAQ,SAAS,aAA8B,CAAA"}
@@ -260,6 +260,17 @@ class TextNode extends BoxNode {
260
260
  }
261
261
  return 0; // Default fallback
262
262
  }
263
+ /**
264
+ * Adds manual letter spacing compensation to a measured text width.
265
+ * Needed because skia-canvas ctx.measureText() does not include letterSpacing in the returned width,
266
+ * even though letterSpacing IS applied during rendering (fillText/strokeText).
267
+ */
268
+ addLetterSpacingExtra(text, measuredWidth, letterSpacingPx) {
269
+ if (letterSpacingPx === 0 || text.length === 0)
270
+ return measuredWidth;
271
+ const charCount = [...text].length;
272
+ return measuredWidth + (charCount > 1 ? (charCount - 1) * letterSpacingPx : 0);
273
+ }
263
274
  /**
264
275
  * Generates a CSS font string by combining base TextProps with optional TextSegment styling.
265
276
  * Follows browser font string format: "font-style font-weight font-size font-family"
@@ -338,6 +349,7 @@ class TextNode extends BoxNode {
338
349
  ctx.letterSpacing = this.formatSpacing(this.props.letterSpacing);
339
350
  ctx.wordSpacing = 'normal'; // Handled manually via parsedWordSpacingPx
340
351
  const parsedWordSpacingPx = this.parseSpacingToPx(this.props.wordSpacing, baseFontSize);
352
+ const parsedLetterSpacingPx = this.parseSpacingToPx(this.props.letterSpacing, baseFontSize);
341
353
  // Pre-measure each text segment width with its specific styling
342
354
  for (const segment of this.segments) {
343
355
  ctx.font = this.getFontString(segment);
@@ -353,13 +365,13 @@ class TextNode extends BoxNode {
353
365
  if (ctx.fontVariant !== 'normal')
354
366
  ctx.fontVariant = 'normal';
355
367
  }
356
- segment.width = ctx.measureText(segment.text).width;
368
+ segment.width = this.addLetterSpacingExtra(segment.text, ctx.measureText(segment.text).width, parsedLetterSpacingPx);
357
369
  }
358
370
  // Calculate available layout width
359
371
  const availableWidthForContent = widthMode === Style.MeasureMode.Undefined ? Infinity : Math.max(0, widthConstraint);
360
372
  const epsilon = 0.001; // Float precision compensation
361
373
  // Wrap text into lines based on available width
362
- this.lines = this.wrapTextRich(ctx, this.segments, availableWidthForContent + epsilon, parsedWordSpacingPx);
374
+ this.lines = this.wrapTextRich(ctx, this.segments, availableWidthForContent + epsilon, parsedWordSpacingPx, parsedLetterSpacingPx);
363
375
  // Initialize line metrics arrays
364
376
  this.lineHeights = []; // Final heights including leading
365
377
  this.lineAscents = []; // Text ascent heights
@@ -479,7 +491,7 @@ class TextNode extends BoxNode {
479
491
  if (ctx.fontVariant !== 'normal')
480
492
  ctx.fontVariant = 'normal';
481
493
  }
482
- const wordWidth = ctx.measureText(word).width;
494
+ const wordWidth = this.addLetterSpacingExtra(word, ctx.measureText(word).width, parsedLetterSpacingPx);
483
495
  if (!firstWordInSingleLine) {
484
496
  singleLineWidth += spaceWidth + parsedWordSpacingPx;
485
497
  }
@@ -554,7 +566,7 @@ class TextNode extends BoxNode {
554
566
  * @param parsedWordSpacingPx Additional spacing to add between words in pixels
555
567
  * @returns Array of lines, where each line contains styled text segments
556
568
  */
557
- wrapTextRich(ctx, segments, maxWidth, parsedWordSpacingPx) {
569
+ wrapTextRich(ctx, segments, maxWidth, parsedWordSpacingPx, parsedLetterSpacingPx = 0) {
558
570
  const lines = [];
559
571
  if (segments.length === 0 || maxWidth <= 0)
560
572
  return lines;
@@ -605,7 +617,7 @@ class TextNode extends BoxNode {
605
617
  ctx.font = this.getFontString(segmentStyle);
606
618
  if (this.props.fontVariant)
607
619
  ctx.fontVariant = this.props.fontVariant;
608
- wordWidth = ctx.measureText(wordOrSpace).width;
620
+ wordWidth = this.addLetterSpacingExtra(wordOrSpace, ctx.measureText(wordOrSpace).width, parsedLetterSpacingPx);
609
621
  wordSegment = { text: wordOrSpace, ...segmentStyle, width: wordWidth };
610
622
  }
611
623
  const needsSpace = currentLineSegments.length > 0 && !/^\s+$/.test(currentLineSegments[currentLineSegments.length - 1].text);
@@ -624,7 +636,7 @@ class TextNode extends BoxNode {
624
636
  }
625
637
  if (!isSpace) {
626
638
  if (wordWidth > maxWidth && maxWidth > 0) {
627
- const brokenParts = this.breakWordRich(ctx, wordSegment, maxWidth);
639
+ const brokenParts = this.breakWordRich(ctx, wordSegment, maxWidth, parsedLetterSpacingPx);
628
640
  if (brokenParts.length > 0) {
629
641
  for (let k = 0; k < brokenParts.length - 1; k++) {
630
642
  lines.push([brokenParts[k]]);
@@ -667,7 +679,7 @@ class TextNode extends BoxNode {
667
679
  ctx.font = this.getFontString(segmentStyle);
668
680
  if (this.props.fontVariant)
669
681
  ctx.fontVariant = this.props.fontVariant;
670
- wordWidth = ctx.measureText(wordOrSpace).width;
682
+ wordWidth = this.addLetterSpacingExtra(wordOrSpace, ctx.measureText(wordOrSpace).width, parsedLetterSpacingPx);
671
683
  wordSegment = { text: wordOrSpace, ...segmentStyle, width: wordWidth };
672
684
  }
673
685
  const needsSpace = currentLineSegments.length > 0 && !/^\s+$/.test(currentLineSegments[currentLineSegments.length - 1].text);
@@ -686,7 +698,7 @@ class TextNode extends BoxNode {
686
698
  }
687
699
  if (!isSpace) {
688
700
  if (wordWidth > maxWidth && maxWidth > 0) {
689
- const brokenParts = this.breakWordRich(ctx, wordSegment, maxWidth);
701
+ const brokenParts = this.breakWordRich(ctx, wordSegment, maxWidth, parsedLetterSpacingPx);
690
702
  if (brokenParts.length > 0) {
691
703
  for (let k = 0; k < brokenParts.length - 1; k++) {
692
704
  lines.push([brokenParts[k]]);
@@ -719,7 +731,7 @@ class TextNode extends BoxNode {
719
731
  * @param maxWidth Maximum width allowed for each resulting segment
720
732
  * @returns Array of TextSegments, each fitting maxWidth, or original segment if no breaking needed
721
733
  */
722
- breakWordRich(ctx, segmentToBreak, maxWidth) {
734
+ breakWordRich(ctx, segmentToBreak, maxWidth, parsedLetterSpacingPx = 0) {
723
735
  const word = segmentToBreak.text;
724
736
  // Copy all style properties to maintain consistent styling across broken segments
725
737
  const style = {
@@ -740,14 +752,14 @@ class TextNode extends BoxNode {
740
752
  // Process word character by character to find valid break points
741
753
  for (const char of word) {
742
754
  const testPartText = currentPartText + char;
743
- const testPartWidth = ctx.measureText(testPartText).width;
755
+ const testPartWidth = this.addLetterSpacingExtra(testPartText, ctx.measureText(testPartText).width, parsedLetterSpacingPx);
744
756
  if (testPartWidth > maxWidth) {
745
757
  // Current accumulated text exceeds width - create new segment
746
758
  if (currentPartText) {
747
759
  brokenSegments.push({
748
760
  text: currentPartText,
749
761
  ...style,
750
- width: ctx.measureText(currentPartText).width,
762
+ width: this.addLetterSpacingExtra(currentPartText, ctx.measureText(currentPartText).width, parsedLetterSpacingPx),
751
763
  });
752
764
  }
753
765
  // Handle current character that caused overflow
@@ -773,7 +785,7 @@ class TextNode extends BoxNode {
773
785
  brokenSegments.push({
774
786
  text: currentPartText,
775
787
  ...style,
776
- width: ctx.measureText(currentPartText).width,
788
+ width: this.addLetterSpacingExtra(currentPartText, ctx.measureText(currentPartText).width, parsedLetterSpacingPx),
777
789
  });
778
790
  }
779
791
  return brokenSegments.length > 0 ? brokenSegments : [segmentToBreak];
@@ -832,7 +844,8 @@ class TextNode extends BoxNode {
832
844
  const spaceWidth = this.measureSpaceWidth(ctx);
833
845
  // Use a small epsilon for float precision issues
834
846
  const epsilon = 0.01;
835
- const allLines = this.wrapTextRich(ctx, this.segments, contentWidth + epsilon, parsedWordSpacingPx);
847
+ const parsedLetterSpacingPx = this.parseSpacingToPx(this.props.letterSpacing, baseFontSize);
848
+ const allLines = this.wrapTextRich(ctx, this.segments, contentWidth + epsilon, parsedWordSpacingPx, parsedLetterSpacingPx);
836
849
  const needsEllipsis = this.props.ellipsis && this.props.maxLines !== undefined && allLines.length > this.props.maxLines;
837
850
  // Apply maxLines constraint to get the visible lines
838
851
  const visibleLines = this.props.maxLines !== undefined && this.props.maxLines > 0 ? allLines.slice(0, this.props.maxLines) : allLines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meonode/canvas",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "A declarative, component-based library for server-side canvas image generation. Write complex visuals with simple functions, similar to the composition style of @meonode/ui.",
5
5
  "keywords": [
6
6
  "canvas",