@lightningjs/renderer 3.0.0-beta15 → 3.0.0-beta17

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.
Files changed (245) hide show
  1. package/dist/src/common/CommonTypes.d.ts +11 -0
  2. package/dist/src/core/CoreNode.js +8 -0
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/CoreTextNode.d.ts +14 -0
  5. package/dist/src/core/CoreTextNode.js +90 -5
  6. package/dist/src/core/CoreTextNode.js.map +1 -1
  7. package/dist/src/core/Stage.js +2 -1
  8. package/dist/src/core/Stage.js.map +1 -1
  9. package/dist/src/core/animations/Animation.d.ts +16 -0
  10. package/dist/src/core/animations/Animation.js +111 -0
  11. package/dist/src/core/animations/Animation.js.map +1 -0
  12. package/dist/src/core/animations/CoreAnimation.d.ts +0 -1
  13. package/dist/src/core/animations/CoreAnimation.js +0 -1
  14. package/dist/src/core/animations/CoreAnimation.js.map +1 -1
  15. package/dist/src/core/animations/CoreTransition.d.ts +24 -0
  16. package/dist/src/core/animations/CoreTransition.js +63 -0
  17. package/dist/src/core/animations/CoreTransition.js.map +1 -0
  18. package/dist/src/core/animations/Playback.d.ts +62 -0
  19. package/dist/src/core/animations/Playback.js +155 -0
  20. package/dist/src/core/animations/Playback.js.map +1 -0
  21. package/dist/src/core/animations/Transition.d.ts +25 -0
  22. package/dist/src/core/animations/Transition.js +63 -0
  23. package/dist/src/core/animations/Transition.js.map +1 -0
  24. package/dist/src/core/animations/utils.d.ts +2 -0
  25. package/dist/src/core/animations/utils.js +137 -0
  26. package/dist/src/core/animations/utils.js.map +1 -0
  27. package/dist/src/core/lib/collectionUtils.d.ts +5 -0
  28. package/dist/src/core/lib/collectionUtils.js +100 -0
  29. package/dist/src/core/lib/collectionUtils.js.map +1 -0
  30. package/dist/src/core/platforms/Platform.d.ts +5 -0
  31. package/dist/src/core/platforms/Platform.js.map +1 -1
  32. package/dist/src/core/platforms/web/WebPlatform.d.ts +1 -0
  33. package/dist/src/core/platforms/web/WebPlatform.js +3 -0
  34. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  35. package/dist/src/core/text-rendering/CanvasFontHandler.js +4 -1
  36. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  37. package/dist/src/core/text-rendering/CanvasTextRenderer.js +7 -7
  38. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  39. package/dist/src/core/text-rendering/SdfFontHandler.js +3 -0
  40. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  41. package/dist/src/core/text-rendering/SdfTextRenderer.js +5 -5
  42. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  43. package/dist/src/core/text-rendering/TextLayoutEngine.d.ts +12 -13
  44. package/dist/src/core/text-rendering/TextLayoutEngine.js +242 -186
  45. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  46. package/dist/src/core/text-rendering/TextRenderer.d.ts +22 -7
  47. package/dist/src/core/utils.d.ts +1 -1
  48. package/dist/src/main-api/Inspector.js +9 -5
  49. package/dist/src/main-api/Inspector.js.map +1 -1
  50. package/dist/src/main-api/Renderer.js +3 -2
  51. package/dist/src/main-api/Renderer.js.map +1 -1
  52. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/common/CommonTypes.ts +16 -0
  55. package/src/core/CoreNode.test.ts +50 -1
  56. package/src/core/CoreNode.ts +11 -0
  57. package/src/core/CoreTextNode.ts +112 -5
  58. package/src/core/Stage.ts +2 -1
  59. package/src/core/animations/CoreAnimation.ts +0 -2
  60. package/src/core/platforms/Platform.ts +6 -0
  61. package/src/core/platforms/web/WebPlatform.ts +11 -0
  62. package/src/core/text-rendering/CanvasFontHandler.ts +4 -7
  63. package/src/core/text-rendering/CanvasTextRenderer.ts +6 -8
  64. package/src/core/text-rendering/SdfFontHandler.ts +3 -0
  65. package/src/core/text-rendering/SdfTextRenderer.ts +4 -5
  66. package/src/core/text-rendering/TextLayoutEngine.ts +396 -226
  67. package/src/core/text-rendering/TextRenderer.ts +22 -7
  68. package/src/core/text-rendering/tests/{SdfTests.test.ts → TextLayoutEngine.test.ts} +103 -64
  69. package/src/main-api/Inspector.ts +9 -5
  70. package/src/main-api/Renderer.ts +3 -2
  71. package/dist/src/core/TextureError.d.ts +0 -11
  72. package/dist/src/core/TextureError.js +0 -37
  73. package/dist/src/core/TextureError.js.map +0 -1
  74. package/dist/src/core/platform.d.ts +0 -10
  75. package/dist/src/core/platform.js +0 -81
  76. package/dist/src/core/platform.js.map +0 -1
  77. package/dist/src/core/renderers/CoreShader.d.ts +0 -9
  78. package/dist/src/core/renderers/CoreShader.js +0 -28
  79. package/dist/src/core/renderers/CoreShader.js.map +0 -1
  80. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.d.ts +0 -33
  81. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.js +0 -250
  82. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.js.map +0 -1
  83. package/dist/src/core/renderers/canvas/CanvasCoreTexture.d.ts +0 -17
  84. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js +0 -125
  85. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js.map +0 -1
  86. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.d.ts +0 -14
  87. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.js +0 -138
  88. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.js.map +0 -1
  89. package/dist/src/core/renderers/canvas/internal/ColorUtils.d.ts +0 -19
  90. package/dist/src/core/renderers/canvas/internal/ColorUtils.js +0 -58
  91. package/dist/src/core/renderers/canvas/internal/ColorUtils.js.map +0 -1
  92. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.d.ts +0 -10
  93. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.js +0 -43
  94. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.js.map +0 -1
  95. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.d.ts +0 -12
  96. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.js +0 -58
  97. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.js.map +0 -1
  98. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.d.ts +0 -9
  99. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.js +0 -38
  100. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.js.map +0 -1
  101. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.d.ts +0 -65
  102. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js +0 -269
  103. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js.map +0 -1
  104. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.d.ts +0 -34
  105. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.js +0 -114
  106. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.js.map +0 -1
  107. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.d.ts +0 -133
  108. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js +0 -616
  109. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js.map +0 -1
  110. package/dist/src/core/renderers/webgl/WebGlCoreShader.d.ts +0 -83
  111. package/dist/src/core/renderers/webgl/WebGlCoreShader.js +0 -233
  112. package/dist/src/core/renderers/webgl/WebGlCoreShader.js.map +0 -1
  113. package/dist/src/core/renderers/webgl/shaders/DefaultShader.d.ts +0 -9
  114. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js +0 -87
  115. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js.map +0 -1
  116. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.d.ts +0 -10
  117. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js +0 -119
  118. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js.map +0 -1
  119. package/dist/src/core/renderers/webgl/shaders/DynamicShader.d.ts +0 -29
  120. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js +0 -413
  121. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js.map +0 -1
  122. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.d.ts +0 -28
  123. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js +0 -131
  124. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js.map +0 -1
  125. package/dist/src/core/renderers/webgl/shaders/SdfShader.d.ts +0 -47
  126. package/dist/src/core/renderers/webgl/shaders/SdfShader.js +0 -160
  127. package/dist/src/core/renderers/webgl/shaders/SdfShader.js.map +0 -1
  128. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.d.ts +0 -31
  129. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js +0 -71
  130. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js.map +0 -1
  131. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.d.ts +0 -30
  132. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js +0 -58
  133. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js.map +0 -1
  134. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.d.ts +0 -31
  135. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js +0 -71
  136. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js.map +0 -1
  137. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.d.ts +0 -31
  138. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js +0 -71
  139. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js.map +0 -1
  140. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.d.ts +0 -31
  141. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js +0 -71
  142. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js.map +0 -1
  143. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.d.ts +0 -9
  144. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.js +0 -136
  145. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.js.map +0 -1
  146. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.d.ts +0 -36
  147. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js +0 -85
  148. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js.map +0 -1
  149. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.d.ts +0 -45
  150. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.js +0 -104
  151. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.js.map +0 -1
  152. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.d.ts +0 -22
  153. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.js +0 -45
  154. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.js.map +0 -1
  155. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.d.ts +0 -58
  156. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.js +0 -80
  157. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.js.map +0 -1
  158. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.d.ts +0 -35
  159. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js +0 -129
  160. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js.map +0 -1
  161. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.d.ts +0 -39
  162. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js +0 -116
  163. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js.map +0 -1
  164. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.d.ts +0 -61
  165. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js +0 -127
  166. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js.map +0 -1
  167. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.d.ts +0 -40
  168. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js +0 -71
  169. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js.map +0 -1
  170. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.d.ts +0 -115
  171. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.js +0 -61
  172. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.js.map +0 -1
  173. package/dist/src/core/text-rendering/TextRenderingUtils.d.ts +0 -12
  174. package/dist/src/core/text-rendering/TextRenderingUtils.js +0 -14
  175. package/dist/src/core/text-rendering/TextRenderingUtils.js.map +0 -1
  176. package/dist/src/core/text-rendering/TextTextureRendererUtils.d.ts +0 -72
  177. package/dist/src/core/text-rendering/TextTextureRendererUtils.js +0 -217
  178. package/dist/src/core/text-rendering/TextTextureRendererUtils.js.map +0 -1
  179. package/dist/src/core/text-rendering/TrFontManager.d.ts +0 -26
  180. package/dist/src/core/text-rendering/TrFontManager.js +0 -131
  181. package/dist/src/core/text-rendering/TrFontManager.js.map +0 -1
  182. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.d.ts +0 -39
  183. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js +0 -125
  184. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js.map +0 -1
  185. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.d.ts +0 -103
  186. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.js +0 -21
  187. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.js.map +0 -1
  188. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.d.ts +0 -62
  189. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.js +0 -88
  190. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.js.map +0 -1
  191. package/dist/src/core/text-rendering/font-face-types/TrFontFace.d.ts +0 -118
  192. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js +0 -63
  193. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js.map +0 -1
  194. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.d.ts +0 -14
  195. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js +0 -66
  196. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js.map +0 -1
  197. package/dist/src/core/text-rendering/font-face-types/utils.d.ts +0 -1
  198. package/dist/src/core/text-rendering/font-face-types/utils.js +0 -38
  199. package/dist/src/core/text-rendering/font-face-types/utils.js.map +0 -1
  200. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.d.ts +0 -59
  201. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js +0 -397
  202. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js.map +0 -1
  203. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.d.ts +0 -120
  204. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js +0 -551
  205. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js.map +0 -1
  206. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.d.ts +0 -92
  207. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +0 -607
  208. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +0 -1
  209. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.d.ts +0 -12
  210. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.js +0 -61
  211. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.js.map +0 -1
  212. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.d.ts +0 -33
  213. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.js +0 -52
  214. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.js.map +0 -1
  215. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.d.ts +0 -13
  216. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.js +0 -32
  217. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.js.map +0 -1
  218. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.d.ts +0 -23
  219. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js +0 -84
  220. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js.map +0 -1
  221. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.d.ts +0 -4
  222. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.js +0 -34
  223. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.js.map +0 -1
  224. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.d.ts +0 -20
  225. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js +0 -308
  226. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js.map +0 -1
  227. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.d.ts +0 -10
  228. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.js +0 -40
  229. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.js.map +0 -1
  230. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.d.ts +0 -26
  231. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.js +0 -70
  232. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.js.map +0 -1
  233. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.d.ts +0 -16
  234. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.js +0 -39
  235. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.js.map +0 -1
  236. package/dist/src/core/text-rendering/renderers/TextRenderer.d.ts +0 -373
  237. package/dist/src/core/text-rendering/renderers/TextRenderer.js +0 -178
  238. package/dist/src/core/text-rendering/renderers/TextRenderer.js.map +0 -1
  239. package/dist/src/main-api/DynamicShaderController.d.ts +0 -29
  240. package/dist/src/main-api/DynamicShaderController.js +0 -58
  241. package/dist/src/main-api/DynamicShaderController.js.map +0 -1
  242. package/dist/src/main-api/ShaderController.d.ts +0 -31
  243. package/dist/src/main-api/ShaderController.js +0 -37
  244. package/dist/src/main-api/ShaderController.js.map +0 -1
  245. package/src/core/text-rendering/tests/Canvas.test.ts +0 -378
@@ -7,6 +7,9 @@ import type {
7
7
  WrappedLinesStruct,
8
8
  } from './TextRenderer.js';
9
9
 
10
+ // Use the same space regex as Canvas renderer to handle ZWSP
11
+ const spaceRegex = /[ \u200B]+/g;
12
+
10
13
  export const defaultFontMetrics: FontMetrics = {
11
14
  ascender: 800,
12
15
  descender: -200,
@@ -14,6 +17,24 @@ export const defaultFontMetrics: FontMetrics = {
14
17
  unitsPerEm: 1000,
15
18
  };
16
19
 
20
+ type WrapStrategyFn = (
21
+ measureText: MeasureTextFn,
22
+ word: string,
23
+ wordWidth: number,
24
+ fontFamily: string,
25
+ letterSpacing: number,
26
+ wrappedLines: TextLineStruct[],
27
+ currentLine: string,
28
+ currentLineWidth: number,
29
+ remainingLines: number,
30
+ remainingWord: string,
31
+ maxWidth: number,
32
+ space: string,
33
+ spaceWidth: number,
34
+ overflowSuffix: string,
35
+ overflowWidth: number,
36
+ ) => [string, number, string];
37
+
17
38
  export const normalizeFontMetrics = (
18
39
  metrics: FontMetrics,
19
40
  fontSize: number,
@@ -31,7 +52,6 @@ export const mapTextLayout = (
31
52
  metrics: NormalizedFontMetrics,
32
53
  text: string,
33
54
  textAlign: string,
34
- verticalAlign: string,
35
55
  fontFamily: string,
36
56
  lineHeight: number,
37
57
  overflowSuffix: string,
@@ -51,13 +71,20 @@ export const mapTextLayout = (
51
71
  const halfDelta = lineHeightDelta * 0.5;
52
72
 
53
73
  let effectiveMaxLines = maxLines;
74
+
54
75
  if (maxHeight > 0) {
55
- const maxFromHeight = Math.floor(maxHeight / lineHeightPx);
76
+ let maxFromHeight = Math.floor(maxHeight / lineHeightPx);
77
+ //ensure at least 1 line
78
+ if (maxFromHeight < 1) {
79
+ maxFromHeight = 1;
80
+ }
56
81
  if (effectiveMaxLines === 0 || maxFromHeight < effectiveMaxLines) {
57
82
  effectiveMaxLines = maxFromHeight;
58
83
  }
59
84
  }
60
85
 
86
+ //trim start/end whitespace
87
+ // text = text.trim();
61
88
  const wrappedText = maxWidth > 0;
62
89
  //wrapText or just measureLines based on maxWidth
63
90
  const [lines, remainingLines, remainingText] =
@@ -70,51 +97,49 @@ export const mapTextLayout = (
70
97
  letterSpacing,
71
98
  overflowSuffix,
72
99
  wordBreak,
73
- maxLines,
100
+ effectiveMaxLines,
74
101
  )
75
102
  : measureLines(
76
103
  measureText,
77
104
  text.split('\n'),
78
105
  fontFamily,
79
106
  letterSpacing,
80
- maxLines,
107
+ effectiveMaxLines,
81
108
  );
82
109
 
83
110
  let effectiveLineAmount = lines.length;
84
- let effectiveMaxWidth = lines[0]![1];
85
-
86
- //check for longest line
87
- if (effectiveLineAmount > 1) {
88
- for (let i = 1; i < effectiveLineAmount; i++) {
89
- effectiveMaxWidth = Math.max(effectiveMaxWidth, lines[i]![1]);
111
+ let effectiveMaxWidth = 0;
112
+
113
+ if (effectiveLineAmount > 0) {
114
+ effectiveMaxWidth = lines[0]![1];
115
+ //check for longest line
116
+ if (effectiveLineAmount > 1) {
117
+ for (let i = 1; i < effectiveLineAmount; i++) {
118
+ effectiveMaxWidth = Math.max(effectiveMaxWidth, lines[i]![1]);
119
+ }
90
120
  }
91
121
  }
92
122
 
93
123
  //update line x offsets
94
124
  if (textAlign !== 'left') {
95
- const maxW = wrappedText === true ? maxWidth : effectiveMaxWidth;
96
125
  for (let i = 0; i < effectiveLineAmount; i++) {
97
126
  const line = lines[i]!;
98
127
  const w = line[1];
99
- line[2] = textAlign === 'right' ? maxW - w : (maxW - w) / 2;
128
+ line[3] =
129
+ textAlign === 'right'
130
+ ? effectiveMaxWidth - w
131
+ : (effectiveMaxWidth - w) / 2;
100
132
  }
101
133
  }
102
134
 
103
135
  const effectiveMaxHeight = effectiveLineAmount * lineHeightPx;
104
136
 
105
137
  let firstBaseLine = halfDelta;
106
- if (maxHeight > 0 && verticalAlign !== 'top') {
107
- if (verticalAlign === 'middle') {
108
- firstBaseLine += (maxHeight - effectiveMaxHeight) / 2;
109
- } else {
110
- firstBaseLine += maxHeight - effectiveMaxHeight;
111
- }
112
- }
113
138
 
114
139
  const startY = firstBaseLine;
115
140
  for (let i = 0; i < effectiveLineAmount; i++) {
116
141
  const line = lines[i] as TextLineStruct;
117
- line[3] = startY + lineHeightPx * i;
142
+ line[4] = startY + lineHeightPx * i;
118
143
  }
119
144
 
120
145
  return [
@@ -147,7 +172,7 @@ export const measureLines = (
147
172
  continue;
148
173
  }
149
174
  const width = measureText(line, fontFamily, letterSpacing);
150
- measuredLines.push([line, width, 0, 0]);
175
+ measuredLines.push([line, width, false, 0, 0]);
151
176
  }
152
177
 
153
178
  return [
@@ -172,9 +197,10 @@ export const wrapText = (
172
197
 
173
198
  // Calculate space width for line wrapping
174
199
  const spaceWidth = measureText(' ', fontFamily, letterSpacing);
200
+ const overflowWidth = measureText(overflowSuffix, fontFamily, letterSpacing);
175
201
 
176
202
  let wrappedLine: TextLineStruct[] = [];
177
- let remainingLines = maxLines;
203
+ let remainingLines = maxLines > 0 ? maxLines : 1000;
178
204
  let hasRemainingText = true;
179
205
  let hasMaxLines = maxLines > 0;
180
206
 
@@ -194,11 +220,11 @@ export const wrapText = (
194
220
  letterSpacing,
195
221
  spaceWidth,
196
222
  overflowSuffix,
223
+ overflowWidth,
197
224
  wordBreak,
198
225
  remainingLines,
199
- hasMaxLines,
200
226
  )
201
- : [[['', 0, 0, 0]], remainingLines, i < lines.length - 1];
227
+ : [[['', 0, false, 0, 0]], remainingLines, i < lines.length - 1];
202
228
 
203
229
  remainingLines--;
204
230
  wrappedLines.push(...wrappedLine);
@@ -206,15 +232,23 @@ export const wrapText = (
206
232
  if (hasMaxLines === true && remainingLines <= 0) {
207
233
  const lastLine = wrappedLines[wrappedLines.length - 1]!;
208
234
  if (i < lines.length - 1) {
209
- if (lastLine[0].endsWith(overflowSuffix) === false) {
210
- lastLine[0] = truncateLineWithSuffix(
235
+ //check if line is truncated already
236
+ if (lastLine[2] === false) {
237
+ let remainingText = '';
238
+ const [line, lineWidth] = truncateLineEnd(
211
239
  measureText,
212
- lastLine[0],
213
240
  fontFamily,
214
- maxWidth,
215
241
  letterSpacing,
242
+ lastLine[0],
243
+ lastLine[1],
244
+ remainingText,
245
+ maxWidth,
216
246
  overflowSuffix,
247
+ overflowWidth,
217
248
  );
249
+ lastLine[0] = line;
250
+ lastLine[1] = lineWidth;
251
+ lastLine[2] = true;
218
252
  }
219
253
  }
220
254
  break;
@@ -232,12 +266,10 @@ export const wrapLine = (
232
266
  letterSpacing: number,
233
267
  spaceWidth: number,
234
268
  overflowSuffix: string,
269
+ overflowWidth: number,
235
270
  wordBreak: string,
236
271
  remainingLines: number,
237
- hasMaxLines: boolean,
238
272
  ): WrappedLinesStruct => {
239
- // Use the same space regex as Canvas renderer to handle ZWSP
240
- const spaceRegex = / |\u200B/g;
241
273
  const words = line.split(spaceRegex);
242
274
  const spaces = line.match(spaceRegex) || [];
243
275
  const wrappedLines: TextLineStruct[] = [];
@@ -245,258 +277,396 @@ export const wrapLine = (
245
277
  let currentLineWidth = 0;
246
278
  let hasRemainingText = true;
247
279
 
248
- let i = 0;
249
-
250
- for (; i < words.length; i++) {
251
- const word = words[i];
252
- if (word === undefined) {
253
- continue;
254
- }
255
- const space = spaces[i - 1] || '';
256
- const wordWidth = measureText(word, fontFamily, letterSpacing);
257
- // For width calculation, treat ZWSP as having 0 width but regular space functionality
258
- const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
259
- const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
260
-
261
- if (
262
- (i === 0 && wordWidth <= maxWidth) ||
263
- (i > 0 && totalWidth <= maxWidth)
264
- ) {
265
- // Word fits on current line
266
- if (currentLine.length > 0) {
267
- // Add space - for ZWSP, don't add anything to output (it's invisible)
268
- if (space !== '\u200B') {
269
- currentLine += space;
270
- currentLineWidth += effectiveSpaceWidth;
271
- }
272
- }
273
- currentLine += word;
274
- currentLineWidth += wordWidth;
275
- } else {
276
- if (remainingLines === 1) {
277
- if (currentLine.length > 0) {
278
- // Add space - for ZWSP, don't add anything to output (it's invisible)
279
- if (space !== '\u200B') {
280
- currentLine += space;
281
- currentLineWidth += effectiveSpaceWidth;
282
- }
280
+ const wrapFn = getWrapStrategy(wordBreak);
281
+ while (words.length > 0 && remainingLines > 0) {
282
+ let word = words.shift()!;
283
+ let wordWidth = measureText(word, fontFamily, letterSpacing);
284
+ let remainingWord = '';
285
+
286
+ //handle first word of new line separately to avoid empty line issues
287
+ if (currentLineWidth === 0) {
288
+ // Word doesn't fit on current line
289
+ //if first word doesn't fit on empty line
290
+ if (wordWidth > maxWidth) {
291
+ remainingLines--;
292
+ //truncate word to fit
293
+ [word, remainingWord, wordWidth] =
294
+ remainingLines === 0
295
+ ? truncateWord(
296
+ measureText,
297
+ word,
298
+ wordWidth,
299
+ maxWidth,
300
+ fontFamily,
301
+ letterSpacing,
302
+ overflowSuffix,
303
+ overflowWidth,
304
+ )
305
+ : splitWord(
306
+ measureText,
307
+ word,
308
+ wordWidth,
309
+ maxWidth,
310
+ fontFamily,
311
+ letterSpacing,
312
+ );
313
+
314
+ if (remainingWord.length > 0) {
315
+ words.unshift(remainingWord);
283
316
  }
284
- currentLine += word;
285
- currentLineWidth += wordWidth;
286
- remainingLines = 0;
287
- hasRemainingText = i < words.length;
288
- break;
289
- }
290
-
291
- if (wordBreak !== 'break-all' && currentLine.length > 0) {
292
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
293
- }
294
-
295
- if (wordBreak !== 'break-all') {
317
+ // first word doesn't fit on an empty line
318
+ wrappedLines.push([word, wordWidth, false, 0, 0]);
319
+ } else if (wordWidth + spaceWidth >= maxWidth) {
296
320
  remainingLines--;
321
+ // word with space doesn't fit, but word itself fits - put on new line
322
+ wrappedLines.push([word, wordWidth, false, 0, 0]);
323
+ } else {
297
324
  currentLine = word;
298
325
  currentLineWidth = wordWidth;
299
326
  }
327
+ continue;
328
+ }
329
+ const space = spaces.shift() || '';
330
+ // For width calculation, treat ZWSP as having 0 width but regular space functionality
331
+ const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
332
+ const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
300
333
 
301
- if (wordBreak === 'break-word') {
302
- const [lines, rl, rt] = breakWord(
303
- measureText,
304
- word,
305
- fontFamily,
306
- maxWidth,
307
- letterSpacing,
308
- remainingLines,
309
- );
310
- remainingLines = rl;
311
- hasRemainingText = rt;
312
- if (lines.length === 1) {
313
- [currentLine, currentLineWidth] = lines[lines.length - 1]!;
314
- } else {
315
- for (let j = 0; j < lines.length; j++) {
316
- [currentLine, currentLineWidth] = lines[j]!;
317
- if (j < lines.length - 1) {
318
- wrappedLines.push(lines[j]!);
319
- }
320
- }
321
- }
322
- } else if (wordBreak === 'break-all') {
323
- const firstLetterWidth = measureText(
324
- word.charAt(0),
325
- fontFamily,
326
- letterSpacing,
327
- );
328
- let linebreak = false;
329
- if (
330
- currentLineWidth + firstLetterWidth + effectiveSpaceWidth >
331
- maxWidth
332
- ) {
333
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
334
- remainingLines -= 1;
335
- currentLine = '';
336
- currentLineWidth = 0;
337
- linebreak = true;
338
- }
339
- const initial = maxWidth - currentLineWidth;
340
- const [lines, rl, rt] = breakAll(
341
- measureText,
342
- word,
343
- fontFamily,
344
- initial,
345
- maxWidth,
346
- letterSpacing,
347
- remainingLines,
348
- );
349
- remainingLines = rl;
350
- hasRemainingText = rt;
351
- if (linebreak === false) {
352
- const [text, width] = lines[0]!;
353
- currentLine += ' ' + text;
354
- currentLineWidth = width;
355
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
356
- }
334
+ if (totalWidth < maxWidth) {
335
+ currentLine += effectiveSpaceWidth > 0 ? space + word : word;
336
+ currentLineWidth = totalWidth;
337
+ continue;
338
+ }
339
+ // Will move to next line after loop finishes
340
+ remainingLines--;
357
341
 
358
- for (let j = 1; j < lines.length; j++) {
359
- [currentLine, currentLineWidth] = lines[j]!;
360
- if (j < lines.length - 1) {
361
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
362
- }
363
- }
364
- }
342
+ if (totalWidth === maxWidth) {
343
+ currentLine += effectiveSpaceWidth > 0 ? space + word : word;
344
+ currentLineWidth = totalWidth;
345
+ wrappedLines.push([currentLine, currentLineWidth, false, 0, 0]);
346
+ currentLine = '';
347
+ currentLineWidth = 0;
348
+ continue;
365
349
  }
366
- }
367
350
 
368
- // Add the last line if it has content
369
- if (currentLine.length > 0 && hasMaxLines === true && remainingLines === 0) {
370
- currentLine = truncateLineWithSuffix(
351
+ [currentLine, currentLineWidth, remainingWord] = wrapFn(
371
352
  measureText,
372
- currentLine,
353
+ word,
354
+ wordWidth,
373
355
  fontFamily,
374
- maxWidth,
375
356
  letterSpacing,
357
+ wrappedLines,
358
+ currentLine,
359
+ currentLineWidth,
360
+ remainingLines,
361
+ remainingWord,
362
+ maxWidth,
363
+ space,
364
+ spaceWidth,
376
365
  overflowSuffix,
366
+ overflowWidth,
377
367
  );
368
+
369
+ if (remainingWord.length > 0) {
370
+ words.unshift(remainingWord);
371
+ }
378
372
  }
379
373
 
380
- if (currentLine.length > 0) {
381
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
374
+ if (currentLineWidth > 0 && remainingLines > 0) {
375
+ wrappedLines.push([currentLine, currentLineWidth, false, 0, 0]);
382
376
  }
377
+
383
378
  return [wrappedLines, remainingLines, hasRemainingText];
384
379
  };
385
380
 
381
+ const getWrapStrategy = (wordBreak: string): WrapStrategyFn => {
382
+ //** default so probably first out */
383
+ if (wordBreak === 'break-word') {
384
+ return breakWord;
385
+ }
386
+ //** second most used */
387
+ if (wordBreak === 'break-all') {
388
+ return breakAll;
389
+ }
390
+ //** most similar to html/CSS 'normal' not really used in TV apps */
391
+ if (wordBreak === 'overflow') {
392
+ return overflow;
393
+ }
394
+ //fallback
395
+ return breakWord;
396
+ };
397
+
398
+ //break strategies
399
+
386
400
  /**
387
- * Truncate a line with overflow suffix to fit within width
401
+ * Overflow wordBreak strategy, if a word partially fits add it to the line, start new line if necessary or add overflowSuffix.
402
+ *
403
+ * @remarks This strategy is similar to 'normal' in html/CSS. However
388
404
  */
389
- export const truncateLineWithSuffix = (
405
+ export const overflow = (
390
406
  measureText: MeasureTextFn,
391
- line: string,
407
+ word: string,
408
+ wordWidth: number,
392
409
  fontFamily: string,
393
- maxWidth: number,
394
410
  letterSpacing: number,
411
+ wrappedLines: TextLineStruct[],
412
+ currentLine: string,
413
+ currentLineWidth: number,
414
+ remainingLines: number,
415
+ remainingWord: string,
416
+ maxWidth: number,
417
+ space: string,
418
+ spaceWidth: number,
395
419
  overflowSuffix: string,
396
- ): string => {
397
- const suffixWidth = measureText(overflowSuffix, fontFamily, letterSpacing);
398
-
399
- if (suffixWidth >= maxWidth) {
400
- return overflowSuffix.substring(0, Math.max(1, overflowSuffix.length - 1));
401
- }
402
-
403
- let truncatedLine = line;
404
- while (truncatedLine.length > 0) {
405
- const lineWidth = measureText(truncatedLine, fontFamily, letterSpacing);
406
- if (lineWidth + suffixWidth <= maxWidth) {
407
- return truncatedLine + overflowSuffix;
408
- }
409
- truncatedLine = truncatedLine.substring(0, truncatedLine.length - 1);
420
+ overflowWidth: number,
421
+ ): [string, number, string] => {
422
+ currentLine += space + word;
423
+ currentLineWidth += spaceWidth + wordWidth;
424
+
425
+ if (remainingLines === 0) {
426
+ currentLine += overflowSuffix;
427
+ currentLineWidth += overflowWidth;
410
428
  }
411
429
 
412
- return overflowSuffix;
430
+ wrappedLines.push([currentLine, currentLineWidth, true, 0, 0]);
431
+ return ['', 0, ''];
413
432
  };
414
433
 
415
- /**
416
- * wordbreak function: https://developer.mozilla.org/en-US/docs/Web/CSS/word-break#break-word
417
- */
418
434
  export const breakWord = (
419
435
  measureText: MeasureTextFn,
420
436
  word: string,
437
+ wordWidth: number,
421
438
  fontFamily: string,
439
+ letterSpacing: number,
440
+ wrappedLines: TextLineStruct[],
441
+ currentLine: string,
442
+ currentLineWidth: number,
443
+ remainingLines: number,
444
+ remainingWord: string,
422
445
  maxWidth: number,
446
+ space: string,
447
+ spaceWidth: number,
448
+ overflowSuffix: string,
449
+ overflowWidth: number,
450
+ ): [string, number, string] => {
451
+ remainingWord = word;
452
+ if (remainingLines === 0) {
453
+ [currentLine, currentLineWidth, remainingWord] = truncateLineEnd(
454
+ measureText,
455
+ fontFamily,
456
+ letterSpacing,
457
+ currentLine,
458
+ currentLineWidth,
459
+ remainingWord,
460
+ maxWidth,
461
+ overflowSuffix,
462
+ overflowWidth,
463
+ );
464
+ wrappedLines.push([currentLine, currentLineWidth, true, 0, 0]);
465
+ } else {
466
+ wrappedLines.push([currentLine, currentLineWidth, false, 0, 0]);
467
+ currentLine = '';
468
+ currentLineWidth = 0;
469
+ }
470
+ return [currentLine, currentLineWidth, remainingWord];
471
+ };
472
+
473
+ export const breakAll = (
474
+ measureText: MeasureTextFn,
475
+ word: string,
476
+ wordWidth: number,
477
+ fontFamily: string,
423
478
  letterSpacing: number,
479
+ wrappedLines: TextLineStruct[],
480
+ currentLine: string,
481
+ currentLineWidth: number,
424
482
  remainingLines: number,
425
- ): WrappedLinesStruct => {
426
- const lines: TextLineStruct[] = [];
427
- let currentPart = '';
428
- let currentWidth = 0;
429
- let i = 0;
483
+ remainingWord: string,
484
+ maxWidth: number,
485
+ space: string,
486
+ spaceWidth: number,
487
+ overflowSuffix: string,
488
+ overflowWidth: number,
489
+ ): [string, number, string] => {
490
+ let remainingSpace = maxWidth - currentLineWidth;
491
+ if (currentLineWidth > 0) {
492
+ remainingSpace -= spaceWidth;
493
+ }
494
+ const truncate = remainingLines === 0;
495
+ [word, remainingWord, wordWidth] = truncate
496
+ ? truncateWord(
497
+ measureText,
498
+ word,
499
+ wordWidth,
500
+ remainingSpace,
501
+ fontFamily,
502
+ letterSpacing,
503
+ overflowSuffix,
504
+ overflowWidth,
505
+ )
506
+ : splitWord(
507
+ measureText,
508
+ word,
509
+ wordWidth,
510
+ remainingSpace,
511
+ fontFamily,
512
+ letterSpacing,
513
+ );
514
+ currentLine += space + word;
515
+ currentLineWidth += spaceWidth + wordWidth;
516
+
517
+ // first word doesn't fit on an empty line
518
+ wrappedLines.push([currentLine, currentLineWidth, truncate, 0, 0]);
519
+
520
+ currentLine = '';
521
+ currentLineWidth = 0;
522
+
523
+ return [currentLine, currentLineWidth, remainingWord];
524
+ };
430
525
 
431
- for (let i = 0; i < word.length; i++) {
432
- const char = word.charAt(i);
433
- const codepoint = char.codePointAt(0);
434
- if (codepoint === undefined) continue;
526
+ export const truncateLineEnd = (
527
+ measureText: MeasureTextFn,
528
+ fontFamily: string,
529
+ letterSpacing: number,
530
+ currentLine: string,
531
+ currentLineWidth: number,
532
+ remainingWord: string,
533
+ maxWidth: number,
534
+ overflowSuffix: string,
535
+ overflowWidth: number,
536
+ ): [string, number, string] => {
537
+ if (currentLineWidth + overflowWidth <= maxWidth) {
538
+ currentLine += overflowSuffix;
539
+ currentLineWidth += overflowWidth;
540
+ remainingWord = '';
541
+ return [currentLine, currentLineWidth, remainingWord];
542
+ }
435
543
 
544
+ let truncated = false;
545
+ for (let i = currentLine.length - 1; i > 0; i--) {
546
+ const char = currentLine.charAt(i);
436
547
  const charWidth = measureText(char, fontFamily, letterSpacing);
437
-
438
- if (currentWidth + charWidth > maxWidth && currentPart.length > 0) {
439
- remainingLines--;
440
- if (remainingLines === 0) {
441
- break;
442
- }
443
- lines.push([currentPart, currentWidth, 0, 0]);
444
- currentPart = char;
445
- currentWidth = charWidth;
446
- } else {
447
- currentPart += char;
448
- currentWidth += charWidth;
548
+ currentLineWidth -= charWidth;
549
+ if (currentLineWidth + overflowWidth <= maxWidth) {
550
+ currentLine = currentLine.substring(0, i) + overflowSuffix;
551
+ currentLineWidth += overflowWidth;
552
+ remainingWord = currentLine.substring(i) + ' ' + remainingWord;
553
+ truncated = true;
554
+ break;
449
555
  }
450
556
  }
451
557
 
452
- if (currentPart.length > 0) {
453
- lines.push([currentPart, currentWidth, 0, 0]);
558
+ if (truncated === false) {
559
+ currentLine = overflowSuffix;
560
+ currentLineWidth = overflowWidth;
561
+ remainingWord = currentLine;
454
562
  }
455
-
456
- return [lines, remainingLines, i < word.length - 1];
563
+ return [currentLine, currentLineWidth, remainingWord];
457
564
  };
458
565
 
459
- /**
460
- * wordbreak function: https://developer.mozilla.org/en-US/docs/Web/CSS/word-break#break-word
461
- */
462
- export const breakAll = (
566
+ export const truncateWord = (
463
567
  measureText: MeasureTextFn,
464
568
  word: string,
465
- fontFamily: string,
466
- initial: number,
569
+ wordWidth: number,
467
570
  maxWidth: number,
571
+ fontFamily: string,
468
572
  letterSpacing: number,
469
- remainingLines: number,
470
- ): WrappedLinesStruct => {
471
- const lines: TextLineStruct[] = [];
472
- let currentPart = '';
473
- let currentWidth = 0;
474
- let max = initial;
475
- let i = 0;
476
- let hasRemainingText = false;
573
+ overflowSuffix: string,
574
+ overflowWidth: number,
575
+ ): [string, string, number] => {
576
+ const targetWidth = maxWidth - overflowWidth;
477
577
 
478
- for (; i < word.length; i++) {
479
- if (remainingLines === 0) {
480
- hasRemainingText = true;
481
- break;
578
+ if (targetWidth <= 0) {
579
+ return ['', word, 0];
580
+ }
581
+
582
+ const excessWidth = wordWidth - targetWidth;
583
+ // If excess is small (< 50%), we're keeping most - start from back and remove
584
+ // If excess is large (>= 50%), we're removing most - start from front and add
585
+ const shouldStartFromBack = excessWidth < wordWidth / 2;
586
+
587
+ if (shouldStartFromBack === false) {
588
+ // Start from back - remove characters until it fits (keeping most of word)
589
+ let currentWidth = wordWidth;
590
+ for (let i = word.length - 1; i > 0; i--) {
591
+ const char = word.charAt(i);
592
+ const charWidth = measureText(char, fontFamily, letterSpacing);
593
+ currentWidth -= charWidth;
594
+ if (currentWidth <= targetWidth) {
595
+ const remainingWord = word.substring(i);
596
+ return [
597
+ word.substring(0, i) + overflowSuffix,
598
+ remainingWord,
599
+ currentWidth + overflowWidth,
600
+ ];
601
+ }
482
602
  }
603
+ // Even first character doesn't fit
604
+ return [overflowSuffix, word, overflowWidth];
605
+ }
606
+
607
+ // Start from front - add characters until we exceed limit (removing most of word)
608
+ let currentWidth = 0;
609
+ for (let i = 0; i < word.length; i++) {
483
610
  const char = word.charAt(i);
484
611
  const charWidth = measureText(char, fontFamily, letterSpacing);
485
- if (currentWidth + charWidth > max && currentPart.length > 0) {
486
- lines.push([currentPart, currentWidth, 0, 0]);
487
- currentPart = char;
488
- currentWidth = charWidth;
489
- max = maxWidth;
490
- remainingLines--;
491
- } else {
492
- currentPart += char;
493
- currentWidth += charWidth;
612
+ if (currentWidth + charWidth > targetWidth) {
613
+ const remainingWord = word.substring(i);
614
+ return [
615
+ word.substring(0, i) + overflowSuffix,
616
+ remainingWord,
617
+ currentWidth + overflowWidth,
618
+ ];
494
619
  }
620
+ currentWidth += charWidth;
621
+ }
622
+ // Entire word fits (shouldn't happen, but safe fallback)
623
+ return [word + overflowSuffix, '', wordWidth + overflowWidth];
624
+ };
625
+
626
+ export const splitWord = (
627
+ measureText: MeasureTextFn,
628
+ word: string,
629
+ wordWidth: number,
630
+ maxWidth: number,
631
+ fontFamily: string,
632
+ letterSpacing: number,
633
+ ): [string, string, number] => {
634
+ if (maxWidth <= 0) {
635
+ return ['', word, 0];
495
636
  }
496
637
 
497
- if (currentPart.length > 0) {
498
- lines.push([currentPart, currentWidth, 0, 0]);
638
+ const excessWidth = wordWidth - maxWidth;
639
+ // If excess is small (< 50%), we're keeping most - start from back and remove
640
+ // If excess is large (>= 50%), we're removing most - start from front and add
641
+ const shouldStartFromBack = excessWidth < wordWidth / 2;
642
+
643
+ if (shouldStartFromBack === false) {
644
+ // Start from back - remove characters until it fits (keeping most of word)
645
+ let currentWidth = wordWidth;
646
+ for (let i = word.length - 1; i > 0; i--) {
647
+ const char = word.charAt(i);
648
+ const charWidth = measureText(char, fontFamily, letterSpacing);
649
+ currentWidth -= charWidth;
650
+ if (currentWidth <= maxWidth) {
651
+ const remainingWord = word.substring(i);
652
+ return [word.substring(0, i), remainingWord, currentWidth];
653
+ }
654
+ }
655
+ // Even first character doesn't fit
656
+ return ['', word, 0];
499
657
  }
500
658
 
501
- return [lines, remainingLines, hasRemainingText];
659
+ // Start from front - add characters until we exceed limit (removing most of word)
660
+ let currentWidth = 0;
661
+ for (let i = 0; i < word.length; i++) {
662
+ const char = word.charAt(i);
663
+ const charWidth = measureText(char, fontFamily, letterSpacing);
664
+ if (currentWidth + charWidth > maxWidth) {
665
+ const remainingWord = word.substring(i);
666
+ return [word.substring(0, i), remainingWord, currentWidth];
667
+ }
668
+ currentWidth += charWidth;
669
+ }
670
+ // Entire word fits (shouldn't happen, but safe fallback)
671
+ return [word, '', wordWidth];
502
672
  };