@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.
@@ -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 = this.lineHeights.reduce((sum, h) => sum + h, 0) + Math.max(0, numLinesToRender - 1) * lineGapValue;
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 = linesToRender[numLinesToRender - 1];
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
- const originalFont = ctx.font;
878
- const originalVariant = ctx.fontVariant;
937
+ ctx.save();
879
938
  ctx.font = this.getFontString(ellipsisStyle);
880
- // Handle font variant setting and validation
881
- if (typeof this.props.fontVariant === 'string') {
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.font = originalFont;
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 = linesToRender[i];
906
- const currentLineFinalHeight = this.lineHeights[i];
907
- const currentLineMaxAscent = this.lineAscents[i];
908
- const currentLineContentHeight = this.lineContentHeights[i];
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
- const originalFont = ctx.font;
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.font = originalFont;
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.2.0",
4
- "description": "A declarative, component-based library for server-side generation of high-quality images on a canvas, inspired by the MeoNode UI library for React. It uses skia-canvas for drawing and yoga-layout for flexbox-based layouts.",
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/manual/generate_sample_charts.ts",
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.2",
49
- "@jest/globals": "^30.2.0",
50
- "@rollup/plugin-commonjs": "^29.0.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.9",
56
- "@semantic-release/npm": "^13.1.3",
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.50.0",
61
- "@typescript-eslint/parser": "^8.50.0",
62
- "eslint": "^9.39.2",
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": "^61.5.0",
65
- "eslint-plugin-prettier": "^5.5.4",
66
- "eslint-plugin-unused-imports": "^4.3.0",
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.2.0",
69
- "prettier": "^3.7.4",
70
- "rollup": "^4.53.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.2",
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.50.0"
76
+ "typescript-eslint": "^8.57.0"
77
77
  },
78
78
  "packageManager": "yarn@4.11.0",
79
79
  "dependencies": {
80
- "file-type": "^21.1.1",
81
- "lodash-es": "^4.17.21",
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",