@meonode/canvas 1.2.0 → 1.3.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 +320 -225
- package/dist/cjs/canvas/canvas.type.d.ts +38 -7
- package/dist/cjs/canvas/canvas.type.d.ts.map +1 -1
- package/dist/cjs/canvas/grid.canvas.util.d.ts +25 -18
- package/dist/cjs/canvas/grid.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/grid.canvas.util.js +304 -210
- package/dist/cjs/canvas/grid.canvas.util.js.map +1 -1
- package/dist/cjs/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/text.canvas.util.js +84 -51
- package/dist/cjs/canvas/text.canvas.util.js.map +1 -1
- package/dist/esm/canvas/canvas.type.d.ts +38 -7
- package/dist/esm/canvas/canvas.type.d.ts.map +1 -1
- package/dist/esm/canvas/grid.canvas.util.d.ts +25 -18
- package/dist/esm/canvas/grid.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/grid.canvas.util.js +304 -210
- package/dist/esm/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/text.canvas.util.js +84 -51
- package/package.json +22 -22
|
@@ -807,16 +807,6 @@ class TextNode extends BoxNode {
|
|
|
807
807
|
*/
|
|
808
808
|
_renderContent(ctx, x, y, width, height) {
|
|
809
809
|
super._renderContent(ctx, x, y, width, height);
|
|
810
|
-
const linesToRender = this.getLinesToMeasureOrRender();
|
|
811
|
-
const numLinesToRender = linesToRender.length;
|
|
812
|
-
// Validate required data is available
|
|
813
|
-
if (numLinesToRender === 0 ||
|
|
814
|
-
this.segments.length === 0 ||
|
|
815
|
-
this.lineHeights.length !== numLinesToRender ||
|
|
816
|
-
this.lineAscents.length !== numLinesToRender ||
|
|
817
|
-
this.lineContentHeights.length !== numLinesToRender) {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
810
|
ctx.save();
|
|
821
811
|
ctx.textBaseline = 'alphabetic';
|
|
822
812
|
ctx.letterSpacing = this.formatSpacing(this.props.letterSpacing);
|
|
@@ -836,9 +826,80 @@ class TextNode extends BoxNode {
|
|
|
836
826
|
ctx.restore();
|
|
837
827
|
return;
|
|
838
828
|
}
|
|
829
|
+
// Re-calculate lines based on the actual render width to ensure consistency
|
|
830
|
+
// This fixes issues where Yoga Layout might use a cached measurement from a different
|
|
831
|
+
// width constraint (e.g., during a flex shrink pass) but final layout is wider.
|
|
832
|
+
const spaceWidth = this.measureSpaceWidth(ctx);
|
|
833
|
+
// Use a small epsilon for float precision issues
|
|
834
|
+
const epsilon = 0.01;
|
|
835
|
+
const allLines = this.wrapTextRich(ctx, this.segments, contentWidth + epsilon, parsedWordSpacingPx);
|
|
836
|
+
const needsEllipsis = this.props.ellipsis && this.props.maxLines !== undefined && allLines.length > this.props.maxLines;
|
|
837
|
+
// Apply maxLines constraint to get the visible lines
|
|
838
|
+
const visibleLines = this.props.maxLines !== undefined && this.props.maxLines > 0 ? allLines.slice(0, this.props.maxLines) : allLines;
|
|
839
|
+
const numLinesToRender = visibleLines.length;
|
|
840
|
+
// Recalculate line metrics for the rendered lines
|
|
841
|
+
// We cannot rely on this.lineHeights from measureText because it might correspond to different wrapping
|
|
842
|
+
const lineHeights = [];
|
|
843
|
+
const lineAscents = [];
|
|
844
|
+
const lineContentHeights = [];
|
|
845
|
+
const defaultLineHeightMultiplier = 1.2;
|
|
846
|
+
let totalTextHeight = 0;
|
|
847
|
+
for (const line of visibleLines) {
|
|
848
|
+
let maxAscent = 0;
|
|
849
|
+
let maxDescent = 0;
|
|
850
|
+
let maxFontSizeOnLine = 0;
|
|
851
|
+
if (line.length === 0) {
|
|
852
|
+
ctx.font = this.getFontString();
|
|
853
|
+
if (this.props.fontVariant)
|
|
854
|
+
ctx.fontVariant = typeof this.props.fontVariant === 'string' ? this.props.fontVariant : 'normal';
|
|
855
|
+
const metrics = ctx.measureText(this.metricsString);
|
|
856
|
+
maxAscent = metrics.actualBoundingBoxAscent ?? baseFontSize * 0.8;
|
|
857
|
+
maxDescent = metrics.actualBoundingBoxDescent ?? baseFontSize * 0.2;
|
|
858
|
+
maxFontSizeOnLine = baseFontSize;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
for (const segment of line) {
|
|
862
|
+
if (/^\s+$/.test(segment.text))
|
|
863
|
+
continue;
|
|
864
|
+
const segmentSize = segment.size || baseFontSize;
|
|
865
|
+
maxFontSizeOnLine = Math.max(maxFontSizeOnLine, segmentSize);
|
|
866
|
+
// Style context for accurate metrics
|
|
867
|
+
ctx.font = this.getFontString(segment);
|
|
868
|
+
if (this.props.fontVariant)
|
|
869
|
+
ctx.fontVariant = typeof this.props.fontVariant === 'string' ? this.props.fontVariant : 'normal';
|
|
870
|
+
const metrics = ctx.measureText(this.metricsString);
|
|
871
|
+
const ascent = metrics.actualBoundingBoxAscent ?? segmentSize * 0.8;
|
|
872
|
+
const descent = metrics.actualBoundingBoxDescent ?? segmentSize * 0.2;
|
|
873
|
+
maxAscent = Math.max(maxAscent, ascent);
|
|
874
|
+
maxDescent = Math.max(maxDescent, descent);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (maxAscent === 0 && maxDescent === 0 && line.length > 0) {
|
|
878
|
+
// Fallback
|
|
879
|
+
ctx.font = this.getFontString();
|
|
880
|
+
if (this.props.fontVariant)
|
|
881
|
+
ctx.fontVariant = typeof this.props.fontVariant === 'string' ? this.props.fontVariant : 'normal';
|
|
882
|
+
const metrics = ctx.measureText(this.metricsString);
|
|
883
|
+
maxAscent = metrics.actualBoundingBoxAscent ?? baseFontSize * 0.8;
|
|
884
|
+
maxDescent = metrics.actualBoundingBoxDescent ?? baseFontSize * 0.2;
|
|
885
|
+
maxFontSizeOnLine = maxFontSizeOnLine || baseFontSize;
|
|
886
|
+
}
|
|
887
|
+
maxFontSizeOnLine = maxFontSizeOnLine || baseFontSize;
|
|
888
|
+
const actualContentHeight = maxAscent + maxDescent;
|
|
889
|
+
const targetLineBoxHeight = typeof this.props.lineHeight === 'number' && this.props.lineHeight > 0 ? this.props.lineHeight : maxFontSizeOnLine * defaultLineHeightMultiplier;
|
|
890
|
+
const finalLineHeight = Math.max(actualContentHeight, targetLineBoxHeight);
|
|
891
|
+
lineHeights.push(finalLineHeight);
|
|
892
|
+
lineAscents.push(maxAscent);
|
|
893
|
+
lineContentHeights.push(actualContentHeight);
|
|
894
|
+
totalTextHeight += finalLineHeight;
|
|
895
|
+
}
|
|
896
|
+
if (numLinesToRender === 0) {
|
|
897
|
+
ctx.restore();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
839
900
|
// Calculate vertical alignment offset
|
|
840
901
|
const lineGapValue = this.props.lineGap;
|
|
841
|
-
const totalCalculatedTextHeight =
|
|
902
|
+
const totalCalculatedTextHeight = totalTextHeight + Math.max(0, numLinesToRender - 1) * lineGapValue;
|
|
842
903
|
let blockStartY;
|
|
843
904
|
switch (this.props.verticalAlign) {
|
|
844
905
|
case 'middle':
|
|
@@ -858,13 +919,12 @@ class TextNode extends BoxNode {
|
|
|
858
919
|
ctx.clip();
|
|
859
920
|
// Configure ellipsis if needed
|
|
860
921
|
const ellipsisChar = typeof this.props.ellipsis === 'string' ? this.props.ellipsis : '...';
|
|
861
|
-
const needsEllipsis = this.props.ellipsis && this.lines.length > numLinesToRender;
|
|
862
922
|
let ellipsisWidth = 0;
|
|
863
923
|
let ellipsisStyle = undefined;
|
|
864
924
|
if (needsEllipsis) {
|
|
865
|
-
const lastRenderedLine =
|
|
925
|
+
const lastRenderedLine = visibleLines[visibleLines.length - 1];
|
|
926
|
+
// ... ellipsis calculation ...
|
|
866
927
|
const lastTextStyleSegment = [...lastRenderedLine].reverse().find(seg => !/^\s+$/.test(seg.text));
|
|
867
|
-
// Inherit styles from last non-whitespace segment
|
|
868
928
|
ellipsisStyle = lastTextStyleSegment
|
|
869
929
|
? {
|
|
870
930
|
color: lastTextStyleSegment.color,
|
|
@@ -874,38 +934,20 @@ class TextNode extends BoxNode {
|
|
|
874
934
|
i: lastTextStyleSegment.i,
|
|
875
935
|
}
|
|
876
936
|
: undefined;
|
|
877
|
-
|
|
878
|
-
const originalVariant = ctx.fontVariant;
|
|
937
|
+
ctx.save();
|
|
879
938
|
ctx.font = this.getFontString(ellipsisStyle);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
ctx.fontVariant = this.props.fontVariant;
|
|
883
|
-
}
|
|
884
|
-
else if (this.props.fontVariant !== undefined) {
|
|
885
|
-
console.warn(`[TextNode ${this.key || ''}] Invalid fontVariant prop type in _renderContent (ellipsis measure):`, this.props.fontVariant);
|
|
886
|
-
if (ctx.fontVariant !== 'normal')
|
|
887
|
-
ctx.fontVariant = 'normal';
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
if (ctx.fontVariant !== 'normal')
|
|
891
|
-
ctx.fontVariant = 'normal';
|
|
939
|
+
if (this.props.fontVariant) {
|
|
940
|
+
ctx.fontVariant = typeof this.props.fontVariant === 'string' ? this.props.fontVariant : 'normal';
|
|
892
941
|
}
|
|
893
942
|
ellipsisWidth = ctx.measureText(ellipsisChar).width;
|
|
894
|
-
ctx.
|
|
895
|
-
if (originalVariant !== 'normal') {
|
|
896
|
-
ctx.fontVariant = originalVariant;
|
|
897
|
-
}
|
|
898
|
-
else if (ctx.fontVariant !== 'normal') {
|
|
899
|
-
ctx.fontVariant = 'normal';
|
|
900
|
-
}
|
|
943
|
+
ctx.restore();
|
|
901
944
|
}
|
|
902
|
-
const spaceWidth = this.measureSpaceWidth(ctx);
|
|
903
945
|
// Render text content line by line
|
|
904
946
|
for (let i = 0; i < numLinesToRender; i++) {
|
|
905
|
-
const lineSegments =
|
|
906
|
-
const currentLineFinalHeight =
|
|
907
|
-
const currentLineMaxAscent =
|
|
908
|
-
const currentLineContentHeight =
|
|
947
|
+
const lineSegments = visibleLines[i];
|
|
948
|
+
const currentLineFinalHeight = lineHeights[i];
|
|
949
|
+
const currentLineMaxAscent = lineAscents[i];
|
|
950
|
+
const currentLineContentHeight = lineContentHeights[i];
|
|
909
951
|
// Calculate line spacing metrics
|
|
910
952
|
const currentLineLeading = currentLineFinalHeight - currentLineContentHeight;
|
|
911
953
|
const currentLineSpaceAbove = Math.max(0, currentLineLeading / 2);
|
|
@@ -1063,9 +1105,7 @@ class TextNode extends BoxNode {
|
|
|
1063
1105
|
if (applyEllipsisAfter) {
|
|
1064
1106
|
const ellipsisRemainingWidth = contentX + contentWidth - currentX;
|
|
1065
1107
|
if (ellipsisRemainingWidth >= ellipsisWidth) {
|
|
1066
|
-
|
|
1067
|
-
const originalVariant = ctx.fontVariant;
|
|
1068
|
-
const originalFill = ctx.fillStyle;
|
|
1108
|
+
ctx.save();
|
|
1069
1109
|
ctx.font = this.getFontString(ellipsisStyle);
|
|
1070
1110
|
if (typeof this.props.fontVariant === 'string') {
|
|
1071
1111
|
ctx.fontVariant = this.props.fontVariant;
|
|
@@ -1081,14 +1121,7 @@ class TextNode extends BoxNode {
|
|
|
1081
1121
|
}
|
|
1082
1122
|
ctx.fillStyle = ellipsisStyle?.color || this.props.color || 'black';
|
|
1083
1123
|
ctx.fillText(ellipsisChar, currentX, lineY, Math.max(0, ellipsisRemainingWidth + 1));
|
|
1084
|
-
ctx.
|
|
1085
|
-
if (originalVariant !== 'normal') {
|
|
1086
|
-
ctx.fontVariant = originalVariant;
|
|
1087
|
-
}
|
|
1088
|
-
else if (ctx.fontVariant !== 'normal') {
|
|
1089
|
-
ctx.fontVariant = 'normal';
|
|
1090
|
-
}
|
|
1091
|
-
ctx.fillStyle = originalFill;
|
|
1124
|
+
ctx.restore();
|
|
1092
1125
|
}
|
|
1093
1126
|
break;
|
|
1094
1127
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meonode/canvas",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A declarative, component-based library for server-side generation
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|
|
7
7
|
"server-side",
|
|
@@ -39,46 +39,46 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "rm -rf ./dist && rollup -c --bundleConfigAsCjs && tsc-alias -p tsconfig.esm.json && tsc-alias -p tsconfig.cjs.json",
|
|
41
41
|
"test": "node --no-experimental-webstorage --experimental-vm-modules $(yarn bin jest)",
|
|
42
|
-
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.mjs",
|
|
42
|
+
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.mjs --fix",
|
|
43
43
|
"format": "prettier --write .",
|
|
44
|
-
"generate:samples": "npx tsx scripts/
|
|
44
|
+
"generate:samples": "npx tsx scripts/generate_sample_charts.ts && npx tsx scripts/generate_sample_grids.ts",
|
|
45
45
|
"prepare": "husky"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@eslint/js": "^9.39.
|
|
49
|
-
"@jest/globals": "^30.
|
|
50
|
-
"@rollup/plugin-commonjs": "^29.0.
|
|
48
|
+
"@eslint/js": "^9.39.4",
|
|
49
|
+
"@jest/globals": "^30.3.0",
|
|
50
|
+
"@rollup/plugin-commonjs": "^29.0.2",
|
|
51
51
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
52
52
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
53
53
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
54
54
|
"@semantic-release/exec": "^7.1.0",
|
|
55
|
-
"@semantic-release/gitlab": "^13.2
|
|
56
|
-
"@semantic-release/npm": "^13.1.
|
|
55
|
+
"@semantic-release/gitlab": "^13.3.2",
|
|
56
|
+
"@semantic-release/npm": "^13.1.5",
|
|
57
57
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
58
58
|
"@types/jest": "^30.0.0",
|
|
59
59
|
"@types/sharp": "^0.32.0",
|
|
60
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
61
|
-
"@typescript-eslint/parser": "^8.
|
|
62
|
-
"eslint": "^9.39.
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
62
|
+
"eslint": "^9.39.4",
|
|
63
63
|
"eslint-config-prettier": "^10.1.8",
|
|
64
|
-
"eslint-plugin-jsdoc": "^
|
|
65
|
-
"eslint-plugin-prettier": "^5.5.
|
|
66
|
-
"eslint-plugin-unused-imports": "^4.
|
|
64
|
+
"eslint-plugin-jsdoc": "^62.7.1",
|
|
65
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
66
|
+
"eslint-plugin-unused-imports": "^4.4.1",
|
|
67
67
|
"husky": "^9.1.7",
|
|
68
|
-
"jest": "^30.
|
|
69
|
-
"prettier": "^3.
|
|
70
|
-
"rollup": "^4.
|
|
68
|
+
"jest": "^30.3.0",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
70
|
+
"rollup": "^4.59.0",
|
|
71
71
|
"rollup-plugin-tsconfig-paths": "^1.5.2",
|
|
72
|
-
"semantic-release": "^25.0.
|
|
72
|
+
"semantic-release": "^25.0.3",
|
|
73
73
|
"ts-jest": "^29.4.6",
|
|
74
74
|
"tsc-alias": "^1.8.16",
|
|
75
75
|
"typescript": "^5.9.3",
|
|
76
|
-
"typescript-eslint": "^8.
|
|
76
|
+
"typescript-eslint": "^8.57.0"
|
|
77
77
|
},
|
|
78
78
|
"packageManager": "yarn@4.11.0",
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"file-type": "^21.
|
|
81
|
-
"lodash-es": "^4.17.
|
|
80
|
+
"file-type": "^21.3.1",
|
|
81
|
+
"lodash-es": "^4.17.23",
|
|
82
82
|
"sharp": "^0.34.5",
|
|
83
83
|
"skia-canvas": "^3.0.8",
|
|
84
84
|
"tinycolor2": "^1.6.0",
|