@lightningjs/renderer 3.0.0-beta16 → 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 (247) 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 +13 -0
  5. package/dist/src/core/CoreTextNode.js +68 -17
  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/CoreTransition.d.ts +24 -0
  13. package/dist/src/core/animations/CoreTransition.js +63 -0
  14. package/dist/src/core/animations/CoreTransition.js.map +1 -0
  15. package/dist/src/core/animations/Playback.d.ts +62 -0
  16. package/dist/src/core/animations/Playback.js +155 -0
  17. package/dist/src/core/animations/Playback.js.map +1 -0
  18. package/dist/src/core/animations/Transition.d.ts +25 -0
  19. package/dist/src/core/animations/Transition.js +63 -0
  20. package/dist/src/core/animations/Transition.js.map +1 -0
  21. package/dist/src/core/animations/utils.d.ts +2 -0
  22. package/dist/src/core/animations/utils.js +137 -0
  23. package/dist/src/core/animations/utils.js.map +1 -0
  24. package/dist/src/core/lib/collectionUtils.d.ts +5 -0
  25. package/dist/src/core/lib/collectionUtils.js +100 -0
  26. package/dist/src/core/lib/collectionUtils.js.map +1 -0
  27. package/dist/src/core/platforms/Platform.d.ts +5 -0
  28. package/dist/src/core/platforms/Platform.js.map +1 -1
  29. package/dist/src/core/platforms/web/WebPlatform.d.ts +1 -0
  30. package/dist/src/core/platforms/web/WebPlatform.js +3 -0
  31. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  32. package/dist/src/core/text-rendering/CanvasFontHandler.js +1 -1
  33. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  34. package/dist/src/core/text-rendering/CanvasTextRenderer.js +3 -3
  35. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  36. package/dist/src/core/text-rendering/SdfTextRenderer.js +3 -3
  37. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  38. package/dist/src/core/text-rendering/TextLayoutEngine.d.ts +12 -13
  39. package/dist/src/core/text-rendering/TextLayoutEngine.js +239 -181
  40. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  41. package/dist/src/core/text-rendering/TextRenderer.d.ts +22 -7
  42. package/dist/src/core/utils.d.ts +1 -1
  43. package/dist/src/main-api/Renderer.js +3 -2
  44. package/dist/src/main-api/Renderer.js.map +1 -1
  45. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  46. package/package.json +1 -1
  47. package/src/common/CommonTypes.ts +16 -0
  48. package/src/core/CoreNode.test.ts +50 -1
  49. package/src/core/CoreNode.ts +11 -0
  50. package/src/core/CoreTextNode.ts +78 -17
  51. package/src/core/Stage.ts +2 -1
  52. package/src/core/platforms/Platform.ts +6 -0
  53. package/src/core/platforms/web/WebPlatform.ts +11 -0
  54. package/src/core/text-rendering/CanvasFontHandler.ts +1 -7
  55. package/src/core/text-rendering/CanvasTextRenderer.ts +2 -4
  56. package/src/core/text-rendering/SdfTextRenderer.ts +2 -3
  57. package/src/core/text-rendering/TextLayoutEngine.ts +393 -223
  58. package/src/core/text-rendering/TextRenderer.ts +22 -7
  59. package/src/core/text-rendering/tests/{SdfTests.test.ts → TextLayoutEngine.test.ts} +103 -64
  60. package/src/main-api/Renderer.ts +3 -2
  61. package/dist/src/core/TextureError.d.ts +0 -11
  62. package/dist/src/core/TextureError.js +0 -37
  63. package/dist/src/core/TextureError.js.map +0 -1
  64. package/dist/src/core/platform.d.ts +0 -10
  65. package/dist/src/core/platform.js +0 -81
  66. package/dist/src/core/platform.js.map +0 -1
  67. package/dist/src/core/renderers/CoreShader.d.ts +0 -9
  68. package/dist/src/core/renderers/CoreShader.js +0 -28
  69. package/dist/src/core/renderers/CoreShader.js.map +0 -1
  70. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.d.ts +0 -33
  71. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.js +0 -250
  72. package/dist/src/core/renderers/canvas/CanvasCoreRenderer.js.map +0 -1
  73. package/dist/src/core/renderers/canvas/CanvasCoreTexture.d.ts +0 -17
  74. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js +0 -125
  75. package/dist/src/core/renderers/canvas/CanvasCoreTexture.js.map +0 -1
  76. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.d.ts +0 -14
  77. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.js +0 -138
  78. package/dist/src/core/renderers/canvas/internal/C2DShaderUtils.js.map +0 -1
  79. package/dist/src/core/renderers/canvas/internal/ColorUtils.d.ts +0 -19
  80. package/dist/src/core/renderers/canvas/internal/ColorUtils.js +0 -58
  81. package/dist/src/core/renderers/canvas/internal/ColorUtils.js.map +0 -1
  82. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.d.ts +0 -10
  83. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.js +0 -43
  84. package/dist/src/core/renderers/canvas/shaders/UnsupportedShader.js.map +0 -1
  85. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.d.ts +0 -12
  86. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.js +0 -58
  87. package/dist/src/core/renderers/webgl/WebGlCoreCtxRenderTexture.js.map +0 -1
  88. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.d.ts +0 -9
  89. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.js +0 -38
  90. package/dist/src/core/renderers/webgl/WebGlCoreCtxSubTexture.js.map +0 -1
  91. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.d.ts +0 -69
  92. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js +0 -272
  93. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js.map +0 -1
  94. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.d.ts +0 -34
  95. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.js +0 -114
  96. package/dist/src/core/renderers/webgl/WebGlCoreRenderOp.js.map +0 -1
  97. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.d.ts +0 -133
  98. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js +0 -641
  99. package/dist/src/core/renderers/webgl/WebGlCoreRenderer.js.map +0 -1
  100. package/dist/src/core/renderers/webgl/WebGlCoreShader.d.ts +0 -78
  101. package/dist/src/core/renderers/webgl/WebGlCoreShader.js +0 -202
  102. package/dist/src/core/renderers/webgl/WebGlCoreShader.js.map +0 -1
  103. package/dist/src/core/renderers/webgl/shaders/DefaultShader.d.ts +0 -9
  104. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js +0 -84
  105. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js.map +0 -1
  106. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.d.ts +0 -10
  107. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js +0 -108
  108. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js.map +0 -1
  109. package/dist/src/core/renderers/webgl/shaders/DynamicShader.d.ts +0 -29
  110. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js +0 -408
  111. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js.map +0 -1
  112. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.d.ts +0 -28
  113. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js +0 -126
  114. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js.map +0 -1
  115. package/dist/src/core/renderers/webgl/shaders/SdfShader.d.ts +0 -47
  116. package/dist/src/core/renderers/webgl/shaders/SdfShader.js +0 -148
  117. package/dist/src/core/renderers/webgl/shaders/SdfShader.js.map +0 -1
  118. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.d.ts +0 -31
  119. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js +0 -71
  120. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js.map +0 -1
  121. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.d.ts +0 -30
  122. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js +0 -58
  123. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js.map +0 -1
  124. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.d.ts +0 -31
  125. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js +0 -71
  126. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js.map +0 -1
  127. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.d.ts +0 -31
  128. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js +0 -71
  129. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js.map +0 -1
  130. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.d.ts +0 -31
  131. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js +0 -71
  132. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js.map +0 -1
  133. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.d.ts +0 -9
  134. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.js +0 -136
  135. package/dist/src/core/renderers/webgl/shaders/effects/EffectUtils.js.map +0 -1
  136. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.d.ts +0 -36
  137. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js +0 -85
  138. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js.map +0 -1
  139. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.d.ts +0 -45
  140. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.js +0 -104
  141. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.js.map +0 -1
  142. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.d.ts +0 -22
  143. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.js +0 -45
  144. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.js.map +0 -1
  145. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.d.ts +0 -58
  146. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.js +0 -80
  147. package/dist/src/core/renderers/webgl/shaders/effects/HolePunchEffect.js.map +0 -1
  148. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.d.ts +0 -35
  149. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js +0 -134
  150. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js.map +0 -1
  151. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.d.ts +0 -40
  152. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js +0 -143
  153. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js.map +0 -1
  154. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.d.ts +0 -61
  155. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js +0 -127
  156. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js.map +0 -1
  157. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.d.ts +0 -40
  158. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js +0 -71
  159. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js.map +0 -1
  160. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.d.ts +0 -115
  161. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.js +0 -61
  162. package/dist/src/core/renderers/webgl/shaders/effects/ShaderEffect.js.map +0 -1
  163. package/dist/src/core/text-rendering/CanvasFont.d.ts +0 -14
  164. package/dist/src/core/text-rendering/CanvasFont.js +0 -111
  165. package/dist/src/core/text-rendering/CanvasFont.js.map +0 -1
  166. package/dist/src/core/text-rendering/CoreFont.d.ts +0 -33
  167. package/dist/src/core/text-rendering/CoreFont.js +0 -48
  168. package/dist/src/core/text-rendering/CoreFont.js.map +0 -1
  169. package/dist/src/core/text-rendering/FontManager.d.ts +0 -11
  170. package/dist/src/core/text-rendering/FontManager.js +0 -42
  171. package/dist/src/core/text-rendering/FontManager.js.map +0 -1
  172. package/dist/src/core/text-rendering/SdfFont.d.ts +0 -29
  173. package/dist/src/core/text-rendering/SdfFont.js +0 -142
  174. package/dist/src/core/text-rendering/SdfFont.js.map +0 -1
  175. package/dist/src/core/text-rendering/TextRenderingUtils.d.ts +0 -12
  176. package/dist/src/core/text-rendering/TextRenderingUtils.js +0 -14
  177. package/dist/src/core/text-rendering/TextRenderingUtils.js.map +0 -1
  178. package/dist/src/core/text-rendering/TextTextureRendererUtils.d.ts +0 -72
  179. package/dist/src/core/text-rendering/TextTextureRendererUtils.js +0 -217
  180. package/dist/src/core/text-rendering/TextTextureRendererUtils.js.map +0 -1
  181. package/dist/src/core/text-rendering/TrFontManager.d.ts +0 -26
  182. package/dist/src/core/text-rendering/TrFontManager.js +0 -131
  183. package/dist/src/core/text-rendering/TrFontManager.js.map +0 -1
  184. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.d.ts +0 -39
  185. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js +0 -125
  186. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.js.map +0 -1
  187. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.d.ts +0 -103
  188. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.js +0 -21
  189. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.js.map +0 -1
  190. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.d.ts +0 -62
  191. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.js +0 -88
  192. package/dist/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.js.map +0 -1
  193. package/dist/src/core/text-rendering/font-face-types/TrFontFace.d.ts +0 -118
  194. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js +0 -63
  195. package/dist/src/core/text-rendering/font-face-types/TrFontFace.js.map +0 -1
  196. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.d.ts +0 -14
  197. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js +0 -66
  198. package/dist/src/core/text-rendering/font-face-types/WebTrFontFace.js.map +0 -1
  199. package/dist/src/core/text-rendering/font-face-types/utils.d.ts +0 -1
  200. package/dist/src/core/text-rendering/font-face-types/utils.js +0 -38
  201. package/dist/src/core/text-rendering/font-face-types/utils.js.map +0 -1
  202. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.d.ts +0 -59
  203. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js +0 -397
  204. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js.map +0 -1
  205. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.d.ts +0 -120
  206. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js +0 -551
  207. package/dist/src/core/text-rendering/renderers/LightningTextTextureRenderer.js.map +0 -1
  208. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.d.ts +0 -92
  209. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +0 -607
  210. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +0 -1
  211. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.d.ts +0 -12
  212. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.js +0 -61
  213. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.js.map +0 -1
  214. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.d.ts +0 -33
  215. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.js +0 -52
  216. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.js.map +0 -1
  217. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.d.ts +0 -13
  218. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.js +0 -32
  219. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.js.map +0 -1
  220. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.d.ts +0 -23
  221. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js +0 -84
  222. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.js.map +0 -1
  223. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.d.ts +0 -4
  224. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.js +0 -34
  225. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.js.map +0 -1
  226. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.d.ts +0 -20
  227. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js +0 -308
  228. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.js.map +0 -1
  229. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.d.ts +0 -10
  230. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.js +0 -40
  231. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.js.map +0 -1
  232. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.d.ts +0 -26
  233. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.js +0 -70
  234. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.js.map +0 -1
  235. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.d.ts +0 -16
  236. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.js +0 -39
  237. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.js.map +0 -1
  238. package/dist/src/core/text-rendering/renderers/TextRenderer.d.ts +0 -373
  239. package/dist/src/core/text-rendering/renderers/TextRenderer.js +0 -178
  240. package/dist/src/core/text-rendering/renderers/TextRenderer.js.map +0 -1
  241. package/dist/src/main-api/DynamicShaderController.d.ts +0 -29
  242. package/dist/src/main-api/DynamicShaderController.js +0 -58
  243. package/dist/src/main-api/DynamicShaderController.js.map +0 -1
  244. package/dist/src/main-api/ShaderController.d.ts +0 -31
  245. package/dist/src/main-api/ShaderController.js +0 -37
  246. package/dist/src/main-api/ShaderController.js.map +0 -1
  247. 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,23 +97,26 @@ 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
 
@@ -95,7 +125,7 @@ export const mapTextLayout = (
95
125
  for (let i = 0; i < effectiveLineAmount; i++) {
96
126
  const line = lines[i]!;
97
127
  const w = line[1];
98
- line[2] =
128
+ line[3] =
99
129
  textAlign === 'right'
100
130
  ? effectiveMaxWidth - w
101
131
  : (effectiveMaxWidth - w) / 2;
@@ -109,7 +139,7 @@ export const mapTextLayout = (
109
139
  const startY = firstBaseLine;
110
140
  for (let i = 0; i < effectiveLineAmount; i++) {
111
141
  const line = lines[i] as TextLineStruct;
112
- line[3] = startY + lineHeightPx * i;
142
+ line[4] = startY + lineHeightPx * i;
113
143
  }
114
144
 
115
145
  return [
@@ -142,7 +172,7 @@ export const measureLines = (
142
172
  continue;
143
173
  }
144
174
  const width = measureText(line, fontFamily, letterSpacing);
145
- measuredLines.push([line, width, 0, 0]);
175
+ measuredLines.push([line, width, false, 0, 0]);
146
176
  }
147
177
 
148
178
  return [
@@ -167,9 +197,10 @@ export const wrapText = (
167
197
 
168
198
  // Calculate space width for line wrapping
169
199
  const spaceWidth = measureText(' ', fontFamily, letterSpacing);
200
+ const overflowWidth = measureText(overflowSuffix, fontFamily, letterSpacing);
170
201
 
171
202
  let wrappedLine: TextLineStruct[] = [];
172
- let remainingLines = maxLines;
203
+ let remainingLines = maxLines > 0 ? maxLines : 1000;
173
204
  let hasRemainingText = true;
174
205
  let hasMaxLines = maxLines > 0;
175
206
 
@@ -189,11 +220,11 @@ export const wrapText = (
189
220
  letterSpacing,
190
221
  spaceWidth,
191
222
  overflowSuffix,
223
+ overflowWidth,
192
224
  wordBreak,
193
225
  remainingLines,
194
- hasMaxLines,
195
226
  )
196
- : [[['', 0, 0, 0]], remainingLines, i < lines.length - 1];
227
+ : [[['', 0, false, 0, 0]], remainingLines, i < lines.length - 1];
197
228
 
198
229
  remainingLines--;
199
230
  wrappedLines.push(...wrappedLine);
@@ -201,15 +232,23 @@ export const wrapText = (
201
232
  if (hasMaxLines === true && remainingLines <= 0) {
202
233
  const lastLine = wrappedLines[wrappedLines.length - 1]!;
203
234
  if (i < lines.length - 1) {
204
- if (lastLine[0].endsWith(overflowSuffix) === false) {
205
- lastLine[0] = truncateLineWithSuffix(
235
+ //check if line is truncated already
236
+ if (lastLine[2] === false) {
237
+ let remainingText = '';
238
+ const [line, lineWidth] = truncateLineEnd(
206
239
  measureText,
207
- lastLine[0],
208
240
  fontFamily,
209
- maxWidth,
210
241
  letterSpacing,
242
+ lastLine[0],
243
+ lastLine[1],
244
+ remainingText,
245
+ maxWidth,
211
246
  overflowSuffix,
247
+ overflowWidth,
212
248
  );
249
+ lastLine[0] = line;
250
+ lastLine[1] = lineWidth;
251
+ lastLine[2] = true;
213
252
  }
214
253
  }
215
254
  break;
@@ -227,12 +266,10 @@ export const wrapLine = (
227
266
  letterSpacing: number,
228
267
  spaceWidth: number,
229
268
  overflowSuffix: string,
269
+ overflowWidth: number,
230
270
  wordBreak: string,
231
271
  remainingLines: number,
232
- hasMaxLines: boolean,
233
272
  ): WrappedLinesStruct => {
234
- // Use the same space regex as Canvas renderer to handle ZWSP
235
- const spaceRegex = / |\u200B/g;
236
273
  const words = line.split(spaceRegex);
237
274
  const spaces = line.match(spaceRegex) || [];
238
275
  const wrappedLines: TextLineStruct[] = [];
@@ -240,263 +277,396 @@ export const wrapLine = (
240
277
  let currentLineWidth = 0;
241
278
  let hasRemainingText = true;
242
279
 
243
- let i = 0;
244
-
245
- for (; i < words.length; i++) {
246
- const word = words[i];
247
- if (word === undefined) {
248
- continue;
249
- }
250
- const space = spaces[i - 1] || '';
251
- const wordWidth = measureText(word, fontFamily, letterSpacing);
252
- // For width calculation, treat ZWSP as having 0 width but regular space functionality
253
- const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
254
- const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
255
-
256
- if (
257
- (i === 0 && wordWidth <= maxWidth) ||
258
- (i > 0 && totalWidth <= maxWidth)
259
- ) {
260
- // Word fits on current line
261
- if (currentLine.length > 0) {
262
- // Add space - for ZWSP, don't add anything to output (it's invisible)
263
- if (space !== '\u200B') {
264
- currentLine += space;
265
- currentLineWidth += effectiveSpaceWidth;
266
- }
267
- }
268
- currentLine += word;
269
- currentLineWidth += wordWidth;
270
- } else {
271
- if (remainingLines === 1) {
272
- if (currentLine.length > 0) {
273
- // Add space - for ZWSP, don't add anything to output (it's invisible)
274
- if (space !== '\u200B') {
275
- currentLine += space;
276
- currentLineWidth += effectiveSpaceWidth;
277
- }
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);
278
316
  }
279
- currentLine += word;
280
- currentLineWidth += wordWidth;
281
- remainingLines = 0;
282
- hasRemainingText = i < words.length;
283
- break;
284
- }
285
-
286
- if (wordBreak !== 'break-all' && currentLine.length > 0) {
287
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
288
- }
289
-
290
- 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) {
291
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 {
292
324
  currentLine = word;
293
325
  currentLineWidth = wordWidth;
294
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;
295
333
 
296
- if (wordBreak === 'break-word') {
297
- const [lines, rl, rt] = breakWord(
298
- measureText,
299
- word,
300
- fontFamily,
301
- maxWidth,
302
- letterSpacing,
303
- remainingLines,
304
- );
305
- remainingLines = rl;
306
- hasRemainingText = rt;
307
- if (lines.length === 1) {
308
- [currentLine, currentLineWidth] = lines[lines.length - 1]!;
309
- } else {
310
- for (let j = 0; j < lines.length; j++) {
311
- [currentLine, currentLineWidth] = lines[j]!;
312
- if (j < lines.length - 1) {
313
- wrappedLines.push(lines[j]!);
314
- }
315
- }
316
- }
317
- } else if (wordBreak === 'break-all') {
318
- const firstLetterWidth = measureText(
319
- word.charAt(0),
320
- fontFamily,
321
- letterSpacing,
322
- );
323
- let linebreak = false;
324
- if (
325
- currentLineWidth + firstLetterWidth + effectiveSpaceWidth >
326
- maxWidth
327
- ) {
328
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
329
- remainingLines -= 1;
330
- currentLine = '';
331
- currentLineWidth = 0;
332
- linebreak = true;
333
- }
334
- const initial = maxWidth - currentLineWidth;
335
- const [lines, rl, rt] = breakAll(
336
- measureText,
337
- word,
338
- fontFamily,
339
- initial,
340
- maxWidth,
341
- letterSpacing,
342
- remainingLines,
343
- );
344
- remainingLines = rl;
345
- hasRemainingText = rt;
346
- if (linebreak === false) {
347
- const [text, width] = lines[0]!;
348
- currentLine += text;
349
- currentLineWidth += width;
350
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
351
- }
352
-
353
- for (let j = 1; j < lines.length; j++) {
354
- [currentLine, currentLineWidth] = lines[j]!;
355
- if (j < lines.length - 1) {
356
- wrappedLines.push([currentLine, currentLineWidth, 0, 0]);
357
- }
358
- }
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--;
359
341
 
360
- if (i < words.length - 1 && currentLine.endsWith(' ') === false) {
361
- currentLine += ' ';
362
- currentLineWidth += effectiveSpaceWidth;
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
  };