@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.
- package/README.md +280 -252
- package/dist/cjs/canvas/canvas.type.d.ts +2 -1
- package/dist/cjs/canvas/canvas.type.d.ts.map +1 -1
- package/dist/cjs/canvas/chart.canvas.util.d.ts +1 -0
- package/dist/cjs/canvas/chart.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/chart.canvas.util.js +34 -8
- package/dist/cjs/canvas/chart.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/grid.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/grid.canvas.util.js +22 -1
- package/dist/cjs/canvas/grid.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/text.canvas.util.d.ts +6 -0
- package/dist/cjs/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/text.canvas.util.js +26 -13
- package/dist/cjs/canvas/text.canvas.util.js.map +1 -1
- package/dist/esm/canvas/canvas.type.d.ts +2 -1
- package/dist/esm/canvas/canvas.type.d.ts.map +1 -1
- package/dist/esm/canvas/chart.canvas.util.d.ts +1 -0
- package/dist/esm/canvas/chart.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/chart.canvas.util.js +34 -8
- package/dist/esm/canvas/grid.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/grid.canvas.util.js +22 -1
- package/dist/esm/canvas/text.canvas.util.d.ts +6 -0
- package/dist/esm/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/text.canvas.util.js +26 -13
- package/package.json +1 -1
|
@@ -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 ||
|
|
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 ||
|
|
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,
|
|
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 ||
|
|
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 ||
|
|
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,
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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
|
|
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.
|
|
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",
|