@lightningjs/renderer 0.7.5 → 0.7.6

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 (218) hide show
  1. package/LICENSE +202 -202
  2. package/NOTICE +3 -3
  3. package/README.md +229 -221
  4. package/dist/src/common/CommonTypes.d.ts +6 -0
  5. package/dist/src/core/CoreNode.d.ts +21 -4
  6. package/dist/src/core/CoreNode.js +145 -34
  7. package/dist/src/core/CoreNode.js.map +1 -1
  8. package/dist/src/core/CoreTextNode.d.ts +1 -1
  9. package/dist/src/core/CoreTextNode.js +18 -10
  10. package/dist/src/core/CoreTextNode.js.map +1 -1
  11. package/dist/src/core/CoreTextureManager.d.ts +3 -1
  12. package/dist/src/core/CoreTextureManager.js +9 -2
  13. package/dist/src/core/CoreTextureManager.js.map +1 -1
  14. package/dist/src/core/Stage.d.ts +2 -0
  15. package/dist/src/core/Stage.js +9 -1
  16. package/dist/src/core/Stage.js.map +1 -1
  17. package/dist/src/core/lib/ImageWorker.d.ts +0 -1
  18. package/dist/src/core/lib/ImageWorker.js +55 -40
  19. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  20. package/dist/src/core/lib/RenderCoords.d.ts +13 -0
  21. package/dist/src/core/lib/RenderCoords.js +63 -0
  22. package/dist/src/core/lib/RenderCoords.js.map +1 -0
  23. package/dist/src/core/lib/WebGlContext.d.ts +414 -0
  24. package/dist/src/core/lib/WebGlContext.js +640 -0
  25. package/dist/src/core/lib/WebGlContext.js.map +1 -0
  26. package/dist/src/core/lib/utils.d.ts +1 -0
  27. package/dist/src/core/lib/utils.js +6 -0
  28. package/dist/src/core/lib/utils.js.map +1 -1
  29. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js +4 -1
  30. package/dist/src/core/renderers/webgl/WebGlCoreCtxTexture.js.map +1 -1
  31. package/dist/src/core/renderers/webgl/internal/RendererUtils.d.ts +9 -0
  32. package/dist/src/core/renderers/webgl/internal/RendererUtils.js +14 -0
  33. package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
  34. package/dist/src/core/renderers/webgl/shaders/DefaultShader.js +47 -47
  35. package/dist/src/core/renderers/webgl/shaders/DefaultShaderBatched.js +61 -61
  36. package/dist/src/core/renderers/webgl/shaders/DynamicShader.js +93 -93
  37. package/dist/src/core/renderers/webgl/shaders/RoundedRectangle.js +63 -63
  38. package/dist/src/core/renderers/webgl/shaders/SdfShader.js +49 -49
  39. package/dist/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.js +15 -15
  40. package/dist/src/core/renderers/webgl/shaders/effects/BorderEffect.js +5 -5
  41. package/dist/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.js +15 -15
  42. package/dist/src/core/renderers/webgl/shaders/effects/BorderRightEffect.js +15 -15
  43. package/dist/src/core/renderers/webgl/shaders/effects/BorderTopEffect.js +15 -15
  44. package/dist/src/core/renderers/webgl/shaders/effects/FadeOutEffect.js +42 -42
  45. package/dist/src/core/renderers/webgl/shaders/effects/GlitchEffect.js +44 -44
  46. package/dist/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.js +3 -3
  47. package/dist/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.js +31 -31
  48. package/dist/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.js +13 -13
  49. package/dist/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.js +37 -37
  50. package/dist/src/core/renderers/webgl/shaders/effects/RadiusEffect.js +19 -19
  51. package/dist/src/core/scene/Scene.d.ts +59 -0
  52. package/dist/src/core/scene/Scene.js +106 -0
  53. package/dist/src/core/scene/Scene.js.map +1 -0
  54. package/dist/src/core/text-rendering/TrFontManager.js +30 -25
  55. package/dist/src/core/text-rendering/TrFontManager.js.map +1 -1
  56. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js +8 -2
  57. package/dist/src/core/text-rendering/renderers/CanvasTextRenderer.js.map +1 -1
  58. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js +8 -2
  59. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js.map +1 -1
  60. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.d.ts +8 -0
  61. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.js +29 -0
  62. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/findNearestMultiple.js.map +1 -0
  63. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.d.ts +19 -0
  64. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.js +84 -0
  65. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/SdfBufferHelper.js.map +1 -0
  66. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.d.ts +8 -0
  67. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.js +40 -0
  68. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutLine.js.map +1 -0
  69. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.d.ts +2 -0
  70. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.js +41 -0
  71. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/layoutText2.js.map +1 -0
  72. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.d.ts +1 -0
  73. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.js +4 -0
  74. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2/utils.js.map +1 -0
  75. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2.d.ts +1 -0
  76. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2.js +2 -0
  77. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText2.js.map +1 -0
  78. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.d.ts +20 -0
  79. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.js +55 -0
  80. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/makeRenderWindow.js.map +1 -0
  81. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.d.ts +9 -0
  82. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.js +32 -0
  83. package/dist/src/core/text-rendering/renderers/SdfTextRenderer/internal/roundUpToMultiple.js.map +1 -0
  84. package/dist/src/core/text-rendering/renderers/TextRenderer.d.ts +12 -0
  85. package/dist/src/core/text-rendering/renderers/TextRenderer.js.map +1 -1
  86. package/dist/src/core/textures/ImageTexture.js +16 -2
  87. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  88. package/dist/src/core/textures/Texture.d.ts +1 -1
  89. package/dist/src/core/textures/Texture.js.map +1 -1
  90. package/dist/src/core/utils.d.ts +1 -1
  91. package/dist/src/main-api/RendererMain.d.ts +6 -0
  92. package/dist/src/main-api/RendererMain.js +1 -0
  93. package/dist/src/main-api/RendererMain.js.map +1 -1
  94. package/dist/src/render-drivers/main/MainCoreDriver.js +1 -0
  95. package/dist/src/render-drivers/main/MainCoreDriver.js.map +1 -1
  96. package/dist/src/render-drivers/main/MainOnlyNode.d.ts +4 -0
  97. package/dist/src/render-drivers/main/MainOnlyNode.js +22 -0
  98. package/dist/src/render-drivers/main/MainOnlyNode.js.map +1 -1
  99. package/dist/src/render-drivers/threadx/ThreadXCoreDriver.js +1 -0
  100. package/dist/src/render-drivers/threadx/ThreadXCoreDriver.js.map +1 -1
  101. package/dist/src/render-drivers/threadx/ThreadXRendererMessage.d.ts +1 -0
  102. package/dist/src/render-drivers/threadx/ThreadXRendererMessage.js.map +1 -1
  103. package/dist/src/render-drivers/threadx/worker/renderer.js +1 -0
  104. package/dist/src/render-drivers/threadx/worker/renderer.js.map +1 -1
  105. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  106. package/exports/core-api.ts +102 -102
  107. package/exports/main-api.ts +60 -60
  108. package/exports/utils.ts +41 -41
  109. package/package.json +1 -1
  110. package/scripts/please-use-pnpm.js +13 -13
  111. package/src/common/CommonTypes.ts +125 -113
  112. package/src/common/EventEmitter.ts +77 -77
  113. package/src/common/IAnimationController.ts +29 -29
  114. package/src/core/CoreExtension.ts +32 -32
  115. package/src/core/CoreNode.ts +1114 -955
  116. package/src/core/CoreShaderManager.ts +243 -243
  117. package/src/core/CoreTextNode.ts +399 -391
  118. package/src/core/CoreTextureManager.ts +337 -326
  119. package/src/core/Stage.ts +365 -354
  120. package/src/core/animations/AnimationManager.ts +38 -38
  121. package/src/core/animations/CoreAnimation.ts +181 -181
  122. package/src/core/animations/CoreAnimationController.ts +148 -148
  123. package/src/core/lib/ContextSpy.ts +41 -41
  124. package/src/core/lib/ImageWorker.ts +149 -135
  125. package/src/core/lib/Matrix3d.ts +290 -290
  126. package/src/core/lib/RenderCoords.ts +86 -0
  127. package/src/core/lib/WebGlContextWrapper.ts +992 -992
  128. package/src/core/lib/textureCompression.ts +152 -152
  129. package/src/core/lib/utils.ts +250 -241
  130. package/src/core/platform.ts +46 -46
  131. package/src/core/renderers/CoreContextTexture.ts +30 -30
  132. package/src/core/renderers/CoreRenderOp.ts +22 -22
  133. package/src/core/renderers/CoreRenderer.ts +63 -63
  134. package/src/core/renderers/CoreShader.ts +41 -41
  135. package/src/core/renderers/webgl/WebGlCoreCtxSubTexture.ts +37 -37
  136. package/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +233 -230
  137. package/src/core/renderers/webgl/WebGlCoreRenderOp.ts +107 -107
  138. package/src/core/renderers/webgl/WebGlCoreRenderer.ts +520 -520
  139. package/src/core/renderers/webgl/WebGlCoreShader.ts +337 -337
  140. package/src/core/renderers/webgl/internal/BufferCollection.ts +54 -54
  141. package/src/core/renderers/webgl/internal/RendererUtils.ts +148 -131
  142. package/src/core/renderers/webgl/internal/ShaderUtils.ts +136 -136
  143. package/src/core/renderers/webgl/internal/WebGlUtils.ts +35 -35
  144. package/src/core/renderers/webgl/shaders/DefaultShader.ts +95 -95
  145. package/src/core/renderers/webgl/shaders/DefaultShaderBatched.ts +132 -132
  146. package/src/core/renderers/webgl/shaders/DynamicShader.ts +474 -474
  147. package/src/core/renderers/webgl/shaders/RoundedRectangle.ts +161 -161
  148. package/src/core/renderers/webgl/shaders/SdfShader.ts +174 -174
  149. package/src/core/renderers/webgl/shaders/effects/BorderBottomEffect.ts +101 -101
  150. package/src/core/renderers/webgl/shaders/effects/BorderEffect.ts +86 -86
  151. package/src/core/renderers/webgl/shaders/effects/BorderLeftEffect.ts +101 -101
  152. package/src/core/renderers/webgl/shaders/effects/BorderRightEffect.ts +101 -101
  153. package/src/core/renderers/webgl/shaders/effects/BorderTopEffect.ts +101 -101
  154. package/src/core/renderers/webgl/shaders/effects/EffectUtils.ts +33 -33
  155. package/src/core/renderers/webgl/shaders/effects/FadeOutEffect.ts +135 -135
  156. package/src/core/renderers/webgl/shaders/effects/GlitchEffect.ts +145 -145
  157. package/src/core/renderers/webgl/shaders/effects/GrayscaleEffect.ts +67 -67
  158. package/src/core/renderers/webgl/shaders/effects/LinearGradientEffect.ts +176 -176
  159. package/src/core/renderers/webgl/shaders/effects/RadialGradientEffect.ts +159 -159
  160. package/src/core/renderers/webgl/shaders/effects/RadialProgressEffect.ts +186 -186
  161. package/src/core/renderers/webgl/shaders/effects/RadiusEffect.ts +121 -121
  162. package/src/core/renderers/webgl/shaders/effects/ShaderEffect.ts +114 -114
  163. package/src/core/text-rendering/TextTextureRendererUtils.ts +189 -189
  164. package/src/core/text-rendering/TrFontManager.ts +170 -166
  165. package/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts +141 -141
  166. package/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/FontShaper.ts +139 -139
  167. package/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.test.ts +173 -173
  168. package/src/core/text-rendering/font-face-types/SdfTrFontFace/internal/SdfFontShaper.ts +169 -169
  169. package/src/core/text-rendering/font-face-types/TrFontFace.ts +105 -105
  170. package/src/core/text-rendering/font-face-types/WebTrFontFace.ts +77 -77
  171. package/src/core/text-rendering/renderers/CanvasTextRenderer.ts +757 -751
  172. package/src/core/text-rendering/renderers/LightningTextTextureRenderer.ts +741 -741
  173. package/src/core/text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.ts +784 -778
  174. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.test.ts +48 -48
  175. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/PeekableGenerator.ts +66 -66
  176. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/SpecialCodepoints.ts +52 -52
  177. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/constants.ts +32 -32
  178. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/getStartConditions.ts +84 -84
  179. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.test.ts +133 -133
  180. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/getUnicodeCodepoints.ts +38 -38
  181. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/layoutText.ts +393 -393
  182. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.test.ts +49 -49
  183. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/measureText.ts +51 -51
  184. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.test.ts +205 -205
  185. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/setRenderWindow.ts +93 -93
  186. package/src/core/text-rendering/renderers/SdfTextRenderer/internal/util.ts +40 -40
  187. package/src/core/text-rendering/renderers/TextRenderer.ts +516 -504
  188. package/src/core/textures/ColorTexture.ts +86 -86
  189. package/src/core/textures/ImageTexture.ts +154 -140
  190. package/src/core/textures/NoiseTexture.ts +96 -96
  191. package/src/core/textures/SubTexture.ts +143 -143
  192. package/src/core/textures/Texture.ts +224 -218
  193. package/src/core/utils.ts +224 -224
  194. package/src/env.d.ts +7 -7
  195. package/src/main-api/ICoreDriver.ts +66 -66
  196. package/src/main-api/INode.ts +499 -499
  197. package/src/main-api/Inspector.ts +439 -439
  198. package/src/main-api/RendererMain.ts +659 -652
  199. package/src/main-api/texture-usage-trackers/FinalizationRegistryTextureUsageTracker.ts +45 -45
  200. package/src/main-api/texture-usage-trackers/ManualCountTextureUsageTracker.ts +154 -154
  201. package/src/main-api/texture-usage-trackers/TextureUsageTracker.ts +54 -54
  202. package/src/render-drivers/main/MainCoreDriver.ts +149 -148
  203. package/src/render-drivers/main/MainOnlyNode.ts +494 -466
  204. package/src/render-drivers/main/MainOnlyTextNode.ts +261 -261
  205. package/src/render-drivers/threadx/NodeStruct.ts +300 -300
  206. package/src/render-drivers/threadx/SharedNode.ts +97 -97
  207. package/src/render-drivers/threadx/TextNodeStruct.ts +211 -211
  208. package/src/render-drivers/threadx/ThreadXCoreDriver.ts +286 -285
  209. package/src/render-drivers/threadx/ThreadXMainAnimationController.ts +99 -99
  210. package/src/render-drivers/threadx/ThreadXMainNode.ts +192 -192
  211. package/src/render-drivers/threadx/ThreadXMainTextNode.ts +85 -85
  212. package/src/render-drivers/threadx/ThreadXRendererMessage.ts +111 -110
  213. package/src/render-drivers/threadx/worker/ThreadXRendererNode.ts +238 -238
  214. package/src/render-drivers/threadx/worker/ThreadXRendererTextNode.ts +149 -149
  215. package/src/render-drivers/threadx/worker/renderer.ts +152 -151
  216. package/src/render-drivers/utils.ts +97 -97
  217. package/src/utils.ts +207 -207
  218. package/COPYING +0 -1
@@ -1,778 +1,784 @@
1
- /*
2
- * If not stated otherwise in this file or this component's LICENSE file the
3
- * following copyright and licenses apply:
4
- *
5
- * Copyright 2023 Comcast Cable Communications Management, LLC.
6
- *
7
- * Licensed under the Apache License, Version 2.0 (the License);
8
- * you may not use this file except in compliance with the License.
9
- * You may obtain a copy of the License at
10
- *
11
- * http://www.apache.org/licenses/LICENSE-2.0
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
-
20
- import {
21
- type Bound,
22
- type Rect,
23
- createBound,
24
- type BoundWithValid,
25
- intersectRect,
26
- type RectWithValid,
27
- copyRect,
28
- boundsOverlap,
29
- convertBoundToRect,
30
- } from '../../../lib/utils.js';
31
- import {
32
- TextRenderer,
33
- type TrProps,
34
- type TextRendererState,
35
- type TrFontProps,
36
- type TrPropSetters,
37
- } from '../TextRenderer.js';
38
- import { SdfTrFontFace } from '../../font-face-types/SdfTrFontFace/SdfTrFontFace.js';
39
- import { FLOATS_PER_GLYPH } from './internal/constants.js';
40
- import { getStartConditions } from './internal/getStartConditions.js';
41
- import { layoutText } from './internal/layoutText.js';
42
- import {
43
- setRenderWindow,
44
- type SdfRenderWindow,
45
- } from './internal/setRenderWindow.js';
46
- import type { TrFontFace } from '../../font-face-types/TrFontFace.js';
47
- import { TrFontManager, type FontFamilyMap } from '../../TrFontManager.js';
48
- import { assertTruthy, mergeColorAlpha } from '../../../../utils.js';
49
- import type { Stage } from '../../../Stage.js';
50
- import { WebGlCoreRenderOp } from '../../../renderers/webgl/WebGlCoreRenderOp.js';
51
- import { BufferCollection } from '../../../renderers/webgl/internal/BufferCollection.js';
52
- import type {
53
- SdfShader,
54
- SdfShaderProps,
55
- } from '../../../renderers/webgl/shaders/SdfShader.js';
56
- import type { WebGlCoreCtxTexture } from '../../../renderers/webgl/WebGlCoreCtxTexture.js';
57
- import { EventEmitter } from '../../../../common/EventEmitter.js';
58
- import type { Matrix3d } from '../../../lib/Matrix3d.js';
59
-
60
- declare module '../TextRenderer.js' {
61
- interface TextRendererMap {
62
- sdf: SdfTextRenderer;
63
- }
64
-
65
- // Add prefixed SDF-specific props to TextRendererDebugProps
66
- interface TextRendererDebugProps {
67
- sdfShaderDebug: boolean;
68
- }
69
- }
70
-
71
- export interface LineCacheItem {
72
- codepointIndex: number;
73
- maxY: number;
74
- maxX: number;
75
- }
76
-
77
- export interface SdfTextRendererState extends TextRendererState {
78
- /**
79
- * Cache for layout resume points indexed by the `curY` for each line
80
- * in the render sequence.
81
- *
82
- * Allows faster rendering by skipping parts of the layout loop that are
83
- * outside of the renderWindow.
84
- */
85
- lineCache: LineCacheItem[];
86
-
87
- renderWindow: SdfRenderWindow;
88
-
89
- elementBounds: BoundWithValid;
90
-
91
- clippingRect: RectWithValid;
92
-
93
- bufferNumFloats: number;
94
-
95
- bufferNumQuads: number;
96
-
97
- vertexBuffer: Float32Array | undefined;
98
-
99
- webGlBuffers: BufferCollection | null;
100
-
101
- bufferUploaded: boolean;
102
-
103
- distanceRange: number;
104
-
105
- trFontFace: SdfTrFontFace | undefined;
106
- }
107
-
108
- /**
109
- * Ephemeral rect object used for calculations
110
- */
111
- const tmpRect: Rect = {
112
- x: 0,
113
- y: 0,
114
- width: 0,
115
- height: 0,
116
- };
117
-
118
- /**
119
- * Singleton class for rendering text using signed distance fields.
120
- *
121
- * @remarks
122
- * SdfTextRenderer supports both single-channel and multi-channel signed distance fields.
123
- */
124
- export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
125
- /**
126
- * Map of font family names to a set of font faces.
127
- */
128
- private ssdfFontFamilies: FontFamilyMap = {};
129
- private msdfFontFamilies: FontFamilyMap = {};
130
- private fontFamilyArray: FontFamilyMap[] = [
131
- this.ssdfFontFamilies,
132
- this.msdfFontFamilies,
133
- ];
134
- private sdfShader: SdfShader;
135
- private rendererBounds: Bound;
136
-
137
- constructor(stage: Stage) {
138
- super(stage);
139
- this.sdfShader = this.stage.shManager.loadShader('SdfShader').shader;
140
- this.rendererBounds = {
141
- x1: 0,
142
- y1: 0,
143
- x2: this.stage.options.appWidth,
144
- y2: this.stage.options.appHeight,
145
- };
146
- }
147
-
148
- //#region Overrides
149
- getPropertySetters(): Partial<TrPropSetters<SdfTextRendererState>> {
150
- return {
151
- fontFamily: (state, value) => {
152
- state.props.fontFamily = value;
153
- state.trFontFace = undefined;
154
- this.invalidateLayoutCache(state);
155
- },
156
- fontWeight: (state, value) => {
157
- state.props.fontWeight = value;
158
- state.trFontFace = undefined;
159
- this.invalidateLayoutCache(state);
160
- },
161
- fontStyle: (state, value) => {
162
- state.props.fontStyle = value;
163
- state.trFontFace = undefined;
164
- this.invalidateLayoutCache(state);
165
- },
166
- fontStretch: (state, value) => {
167
- state.props.fontStretch = value;
168
- state.trFontFace = undefined;
169
- this.invalidateLayoutCache(state);
170
- },
171
- fontSize: (state, value) => {
172
- state.props.fontSize = value;
173
- this.invalidateLayoutCache(state);
174
- },
175
- text: (state, value) => {
176
- state.props.text = value;
177
- this.invalidateLayoutCache(state);
178
- },
179
- textAlign: (state, value) => {
180
- state.props.textAlign = value;
181
- this.invalidateLayoutCache(state);
182
- },
183
- color: (state, value) => {
184
- state.props.color = value;
185
- },
186
- x: (state, value) => {
187
- state.props.x = value;
188
- if (state.elementBounds.valid) {
189
- this.setElementBoundsX(state);
190
- // Only schedule an update if the text is not already rendered
191
- // (renderWindow is invalid) and the element possibly overlaps the screen
192
- // This is to avoid unnecessary updates when we know text is off-screen
193
- if (
194
- !state.renderWindow.valid &&
195
- boundsOverlap(state.elementBounds, this.rendererBounds)
196
- ) {
197
- this.scheduleUpdateState(state);
198
- }
199
- }
200
- },
201
- y: (state, value) => {
202
- state.props.y = value;
203
- if (state.elementBounds.valid) {
204
- this.setElementBoundsY(state);
205
- // See x() for explanation
206
- if (
207
- !state.renderWindow.valid &&
208
- boundsOverlap(state.elementBounds, this.rendererBounds)
209
- ) {
210
- this.scheduleUpdateState(state);
211
- }
212
- }
213
- },
214
- contain: (state, value) => {
215
- state.props.contain = value;
216
- this.invalidateLayoutCache(state);
217
- },
218
- width: (state, value) => {
219
- state.props.width = value;
220
- this.invalidateLayoutCache(state);
221
- },
222
- height: (state, value) => {
223
- state.props.height = value;
224
- this.invalidateLayoutCache(state);
225
- },
226
- offsetY: (state, value) => {
227
- state.props.offsetY = value;
228
- this.invalidateLayoutCache(state);
229
- },
230
- scrollable: (state, value) => {
231
- state.props.scrollable = value;
232
- this.invalidateLayoutCache(state);
233
- },
234
- scrollY: (state, value) => {
235
- state.props.scrollY = value;
236
- // Scrolling doesn't need to invalidate any caches, but it does need to
237
- // schedule an update
238
- this.scheduleUpdateState(state);
239
- },
240
- letterSpacing: (state, value) => {
241
- state.props.letterSpacing = value;
242
- this.invalidateLayoutCache(state);
243
- },
244
- lineHeight: (state, value) => {
245
- state.props.lineHeight = value;
246
- this.invalidateLayoutCache(state);
247
- },
248
- maxLines: (state, value) => {
249
- state.props.maxLines = value;
250
- this.invalidateLayoutCache(state);
251
- },
252
- textBaseline: (state, value) => {
253
- state.props.textBaseline = value;
254
- this.invalidateLayoutCache(state);
255
- },
256
- verticalAlign: (state, value) => {
257
- state.props.verticalAlign = value;
258
- this.invalidateLayoutCache(state);
259
- },
260
- overflowSuffix: (state, value) => {
261
- state.props.overflowSuffix = value;
262
- this.invalidateLayoutCache(state);
263
- },
264
- debug: (state, value) => {
265
- state.props.debug = value;
266
- },
267
- };
268
- }
269
-
270
- override canRenderFont(props: TrFontProps): boolean {
271
- // TODO: Support matching on font stretch, weight and style (if/when needed)
272
- // For now we just match on the font family name
273
- // '$$SDF_FAILURE_TEST$$' is used to test the 'failure' event coming from text
274
- const { fontFamily } = props;
275
- return (
276
- fontFamily in this.ssdfFontFamilies ||
277
- fontFamily in this.msdfFontFamilies ||
278
- fontFamily === '$$SDF_FAILURE_TEST$$'
279
- );
280
- }
281
-
282
- override isFontFaceSupported(fontFace: TrFontFace): boolean {
283
- return fontFace instanceof SdfTrFontFace;
284
- }
285
-
286
- override addFontFace(fontFace: TrFontFace): void {
287
- // Make sure the font face is an SDF font face (it should have already passed
288
- // the `isFontFaceSupported` check)
289
- assertTruthy(fontFace instanceof SdfTrFontFace);
290
- const familyName = fontFace.fontFamily;
291
- const fontFamiles =
292
- fontFace.type === 'ssdf'
293
- ? this.ssdfFontFamilies
294
- : fontFace.type === 'msdf'
295
- ? this.msdfFontFamilies
296
- : undefined;
297
- if (!fontFamiles) {
298
- console.warn(`Invalid font face type: ${fontFace.type as string}`);
299
- return;
300
- }
301
- let faceSet = fontFamiles[familyName];
302
- if (!faceSet) {
303
- faceSet = new Set();
304
- fontFamiles[familyName] = faceSet;
305
- }
306
- faceSet.add(fontFace);
307
- }
308
-
309
- override createState(props: TrProps): SdfTextRendererState {
310
- return {
311
- props,
312
- status: 'initialState',
313
- updateScheduled: false,
314
- emitter: new EventEmitter(),
315
- lineCache: [],
316
- forceFullLayoutCalc: false,
317
- renderWindow: {
318
- screen: {
319
- x1: 0,
320
- y1: 0,
321
- x2: 0,
322
- y2: 0,
323
- },
324
- sdf: {
325
- x1: 0,
326
- y1: 0,
327
- x2: 0,
328
- y2: 0,
329
- },
330
- firstLineIdx: 0,
331
- numLines: 0,
332
- valid: false,
333
- },
334
- elementBounds: {
335
- x1: 0,
336
- y1: 0,
337
- x2: 0,
338
- y2: 0,
339
- valid: false,
340
- },
341
- clippingRect: {
342
- x: 0,
343
- y: 0,
344
- width: 0,
345
- height: 0,
346
- valid: false,
347
- },
348
- bufferNumFloats: 0,
349
- bufferNumQuads: 0,
350
- vertexBuffer: undefined,
351
- webGlBuffers: null,
352
- bufferUploaded: false,
353
- textH: undefined,
354
- textW: undefined,
355
- distanceRange: 0,
356
- trFontFace: undefined,
357
- debugData: {
358
- updateCount: 0,
359
- layoutCount: 0,
360
- lastLayoutNumCharacters: 0,
361
- layoutSum: 0,
362
- drawSum: 0,
363
- drawCount: 0,
364
- bufferSize: 0,
365
- },
366
- };
367
- }
368
-
369
- override updateState(state: SdfTextRendererState): void {
370
- let { trFontFace } = state;
371
- const { textH, lineCache, debugData, forceFullLayoutCalc } = state;
372
- debugData.updateCount++;
373
-
374
- // On the first update call we need to set the status to loading
375
- if (state.status === 'initialState') {
376
- this.setStatus(state, 'loading');
377
- }
378
-
379
- // Resolve font face if we haven't yet
380
- if (!trFontFace) {
381
- trFontFace = this.resolveFontFace(state.props);
382
- state.trFontFace = trFontFace;
383
- if (!trFontFace) {
384
- const msg = `SdfTextRenderer: Could not resolve font face for family: '${state.props.fontFamily}'`;
385
- console.error(msg);
386
- this.setStatus(state, 'failed', new Error(msg));
387
- return;
388
- }
389
- }
390
-
391
- // If the font hasn't been loaded yet, stop here.
392
- // Listen for the 'loaded' event and forward fontLoaded event
393
- if (!trFontFace.loaded) {
394
- trFontFace.once('loaded', () => {
395
- this.scheduleUpdateState(state);
396
- });
397
- return;
398
- }
399
-
400
- // If the font is loaded then so should the data
401
- assertTruthy(trFontFace.data, 'Font face data should be loaded');
402
-
403
- const {
404
- text,
405
- fontSize,
406
- x,
407
- y,
408
- contain,
409
- width,
410
- height,
411
- lineHeight,
412
- verticalAlign,
413
- scrollable,
414
- overflowSuffix,
415
- maxLines,
416
- } = state.props;
417
-
418
- // scrollY only has an effect when contain === 'both' and scrollable === true
419
- const scrollY = contain === 'both' && scrollable ? state.props.scrollY : 0;
420
-
421
- const { renderWindow } = state;
422
-
423
- /**
424
- * The font size of the SDF font face (the basis for SDF space units)
425
- */
426
- const sdfFontSize = trFontFace.data.info.size;
427
-
428
- /**
429
- * Divide screen space units by this to get the SDF space units
430
- * Mulitple SDF space units by this to get screen space units
431
- */
432
- const fontSizeRatio = fontSize / sdfFontSize;
433
-
434
- // Needed in renderWindow calculation
435
- const sdfLineHeight = lineHeight / fontSizeRatio;
436
-
437
- state.distanceRange =
438
- fontSizeRatio * trFontFace.data.distanceField.distanceRange;
439
-
440
- // Allocate buffers if needed
441
- const neededLength = text.length * FLOATS_PER_GLYPH;
442
- let vertexBuffer = state.vertexBuffer;
443
- if (!vertexBuffer || vertexBuffer.length < neededLength) {
444
- vertexBuffer = new Float32Array(neededLength * 2);
445
- }
446
-
447
- const elementBounds = state.elementBounds;
448
- if (!elementBounds.valid) {
449
- this.setElementBoundsX(state);
450
- this.setElementBoundsY(state);
451
- elementBounds.valid = true;
452
- }
453
-
454
- // Return early if we're still viewing inside the established render window
455
- // No need to re-render what we've already rendered
456
- // (Only if there's an established renderWindow and we're not suppressing early exit)
457
- if (!forceFullLayoutCalc && renderWindow.valid) {
458
- const rwScreen = renderWindow.screen;
459
- if (
460
- x + rwScreen.x1 <= elementBounds.x1 &&
461
- x + rwScreen.x2 >= elementBounds.x2 &&
462
- y - scrollY + rwScreen.y1 <= elementBounds.y1 &&
463
- y - scrollY + rwScreen.y2 >= elementBounds.y2
464
- ) {
465
- this.setStatus(state, 'loaded');
466
- return;
467
- }
468
- // Otherwise invalidate the renderWindow so it can be redone
469
- renderWindow.valid = false;
470
- this.setStatus(state, 'loading');
471
- }
472
-
473
- const { offsetY, textAlign } = state.props;
474
-
475
- // Create a new renderWindow if needed
476
- if (!renderWindow.valid) {
477
- const isPossiblyOnScreen = boundsOverlap(
478
- elementBounds,
479
- this.rendererBounds,
480
- );
481
-
482
- if (!isPossiblyOnScreen) {
483
- // If the element is not possibly on screen, we can skip the layout and rendering completely
484
- return;
485
- }
486
-
487
- setRenderWindow(
488
- renderWindow,
489
- x,
490
- y,
491
- scrollY,
492
- lineHeight,
493
- contain === 'both' ? elementBounds.y2 - elementBounds.y1 : 0,
494
- elementBounds,
495
- fontSizeRatio,
496
- );
497
- // console.log('newRenderWindow', renderWindow);
498
- }
499
-
500
- const start = getStartConditions(
501
- sdfFontSize,
502
- sdfLineHeight,
503
- lineHeight,
504
- verticalAlign,
505
- offsetY,
506
- fontSizeRatio,
507
- renderWindow,
508
- lineCache,
509
- textH,
510
- );
511
-
512
- if (!start) {
513
- // Nothing to render, return early, but still mark as loaded (since the text is just scrolled
514
- // out of view)
515
- this.setStatus(state, 'loaded');
516
- return;
517
- }
518
-
519
- const { letterSpacing } = state.props;
520
-
521
- const out2 = layoutText(
522
- start.lineIndex,
523
- start.sdfX,
524
- start.sdfY,
525
- text,
526
- textAlign,
527
- width,
528
- height,
529
- fontSize,
530
- lineHeight,
531
- letterSpacing,
532
- vertexBuffer,
533
- contain,
534
- lineCache,
535
- renderWindow.sdf,
536
- trFontFace,
537
- forceFullLayoutCalc,
538
- scrollable,
539
- overflowSuffix,
540
- maxLines,
541
- );
542
-
543
- state.bufferUploaded = false;
544
- state.bufferNumFloats = out2.bufferNumFloats;
545
- state.bufferNumQuads = out2.bufferNumQuads;
546
- state.vertexBuffer = vertexBuffer;
547
- state.renderWindow = renderWindow;
548
- debugData.lastLayoutNumCharacters = out2.layoutNumCharacters;
549
- debugData.bufferSize = vertexBuffer.byteLength;
550
-
551
- // If we didn't exit early, we know we have completely computed w/h
552
- if (out2.fullyProcessed) {
553
- state.textW = out2.maxX * fontSizeRatio;
554
- state.textH = out2.maxY * fontSizeRatio;
555
- }
556
-
557
- // if (state.props.debug.printLayoutTime) {
558
- // debugData.layoutSum += performance.now() - updateStartTime;
559
- // debugData.layoutCount++;
560
- // }
561
- this.setStatus(state, 'loaded');
562
- }
563
-
564
- override renderQuads(
565
- state: SdfTextRendererState,
566
- transform: Matrix3d,
567
- clippingRect: Readonly<RectWithValid>,
568
- alpha: number,
569
- ): void {
570
- if (!state.vertexBuffer) {
571
- // Nothing to draw
572
- return;
573
- }
574
-
575
- const { renderer } = this.stage;
576
-
577
- const { fontSize, color, contain, scrollable, zIndex, debug } = state.props;
578
-
579
- // scrollY only has an effect when contain === 'both' and scrollable === true
580
- const scrollY = contain === 'both' && scrollable ? state.props.scrollY : 0;
581
-
582
- const {
583
- textW = 0,
584
- textH = 0,
585
- distanceRange,
586
- vertexBuffer,
587
- bufferUploaded,
588
- trFontFace,
589
- elementBounds,
590
- } = state;
591
-
592
- let { webGlBuffers } = state;
593
-
594
- if (!webGlBuffers) {
595
- const glw = renderer.glw;
596
- const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
597
- const webGlBuffer = glw.createBuffer();
598
- assertTruthy(webGlBuffer);
599
- state.webGlBuffers = new BufferCollection([
600
- {
601
- buffer: webGlBuffer,
602
- attributes: {
603
- a_position: {
604
- name: 'a_position',
605
- size: 2, // 2 components per iteration
606
- type: glw.FLOAT, // the data is 32bit floats
607
- normalized: false, // don't normalize the data
608
- stride, // 0 = move forward size * sizeof(type) each iteration to get the next position
609
- offset: 0, // start at the beginning of the buffer
610
- },
611
- a_textureCoordinate: {
612
- name: 'a_textureCoordinate',
613
- size: 2,
614
- type: glw.FLOAT,
615
- normalized: false,
616
- stride,
617
- offset: 2 * Float32Array.BYTES_PER_ELEMENT,
618
- },
619
- },
620
- },
621
- ]);
622
- state.bufferUploaded = false;
623
- assertTruthy(state.webGlBuffers);
624
- webGlBuffers = state.webGlBuffers;
625
- }
626
-
627
- if (!bufferUploaded) {
628
- const glw = renderer.glw;
629
-
630
- const buffer = webGlBuffers?.getBuffer('a_textureCoordinate') ?? null;
631
- glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW);
632
- state.bufferUploaded = true;
633
- }
634
-
635
- assertTruthy(trFontFace);
636
- if (scrollable && contain === 'both') {
637
- assertTruthy(elementBounds.valid);
638
- const elementRect = convertBoundToRect(elementBounds, tmpRect);
639
-
640
- if (clippingRect.valid) {
641
- state.clippingRect.valid = true;
642
- clippingRect = intersectRect(
643
- clippingRect,
644
- elementRect,
645
- state.clippingRect,
646
- );
647
- } else {
648
- state.clippingRect.valid = true;
649
- clippingRect = copyRect(elementRect, state.clippingRect);
650
- }
651
- }
652
-
653
- const renderOp = new WebGlCoreRenderOp(
654
- renderer.glw,
655
- renderer.options,
656
- webGlBuffers,
657
- this.sdfShader,
658
- {
659
- transform: transform.data,
660
- // IMPORTANT: The SDF Shader expects the color NOT to be premultiplied
661
- // for the best blending results. Which is why we use `mergeColorAlpha`
662
- // instead of `mergeColorAlphaPremultiplied` here.
663
- color: mergeColorAlpha(color, alpha),
664
- size: fontSize / (trFontFace.data?.info.size || 0),
665
- scrollY,
666
- distanceRange,
667
- debug: debug.sdfShaderDebug,
668
- } satisfies SdfShaderProps,
669
- alpha,
670
- clippingRect,
671
- { height: textH, width: textW },
672
- 0,
673
- zIndex,
674
- );
675
-
676
- const texture = state.trFontFace?.texture;
677
- assertTruthy(texture);
678
- const ctxTexture = this.stage.txManager.getCtxTexture(texture);
679
-
680
- renderOp.addTexture(ctxTexture as WebGlCoreCtxTexture);
681
- renderOp.length = state.bufferNumFloats;
682
- renderOp.numQuads = state.bufferNumQuads;
683
-
684
- renderer.addRenderOp(renderOp);
685
-
686
- // if (!debug.disableScissor) {
687
- // renderer.enableScissor(
688
- // visibleRect.x,
689
- // visibleRect.y,
690
- // visibleRect.w,
691
- // visibleRect.h,
692
- // );
693
- // }
694
-
695
- // Draw the arrays
696
- // gl.drawArrays(
697
- // gl.TRIANGLES, // Primitive type
698
- // 0,
699
- // bufferNumVertices, // Number of verticies
700
- // );
701
-
702
- // renderer.disableScissor();
703
-
704
- // if (debug.showElementRect) {
705
- // this.renderer.drawBorder(
706
- // Colors.Blue,
707
- // elementRect.x,
708
- // elementRect.y,
709
- // elementRect.w,
710
- // elementRect.h,
711
- // );
712
- // }
713
-
714
- // if (debug.showVisibleRect) {
715
- // this.renderer.drawBorder(
716
- // Colors.Green,
717
- // visibleRect.x,
718
- // visibleRect.y,
719
- // visibleRect.w,
720
- // visibleRect.h,
721
- // );
722
- // }
723
-
724
- // if (debug.showRenderWindow && renderWindow) {
725
- // this.renderer.drawBorder(
726
- // Colors.Red,
727
- // x + renderWindow.x1,
728
- // y + renderWindow.y1 - scrollY,
729
- // x + renderWindow.x2 - (x + renderWindow.x1),
730
- // y + renderWindow.y2 - scrollY - (y + renderWindow.y1 - scrollY),
731
- // );
732
- // }
733
- // if (debug.printLayoutTime) {
734
- // debugData.drawSum += performance.now() - drawStartTime;
735
- // debugData.drawCount++;
736
- // }
737
- }
738
- //#endregion Overrides
739
-
740
- public resolveFontFace(props: TrFontProps): SdfTrFontFace | undefined {
741
- return TrFontManager.resolveFontFace(this.fontFamilyArray, props) as
742
- | SdfTrFontFace
743
- | undefined;
744
- }
745
-
746
- /**
747
- * Invalidate the layout cache stored in the state. This will cause the text
748
- * to be re-layed out on the next update.
749
- *
750
- * @remarks
751
- * This also invalidates the visible window cache.
752
- *
753
- * @param state
754
- */
755
- protected invalidateLayoutCache(state: SdfTextRendererState): void {
756
- state.renderWindow.valid = false;
757
- state.elementBounds.valid = false;
758
- state.textH = undefined;
759
- state.textW = undefined;
760
- state.lineCache = [];
761
- this.setStatus(state, 'loading');
762
- this.scheduleUpdateState(state);
763
- }
764
-
765
- protected setElementBoundsX(state: SdfTextRendererState): void {
766
- const { x, contain, width } = state.props;
767
- const { elementBounds } = state;
768
- elementBounds.x1 = x;
769
- elementBounds.x2 = contain !== 'none' ? x + width : Infinity;
770
- }
771
-
772
- protected setElementBoundsY(state: SdfTextRendererState): void {
773
- const { y, contain, height } = state.props;
774
- const { elementBounds } = state;
775
- elementBounds.y1 = y;
776
- elementBounds.y2 = contain === 'both' ? y + height : Infinity;
777
- }
778
- }
1
+ /*
2
+ * If not stated otherwise in this file or this component's LICENSE file the
3
+ * following copyright and licenses apply:
4
+ *
5
+ * Copyright 2023 Comcast Cable Communications Management, LLC.
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the License);
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import {
21
+ type Bound,
22
+ type Rect,
23
+ createBound,
24
+ type BoundWithValid,
25
+ intersectRect,
26
+ type RectWithValid,
27
+ copyRect,
28
+ boundsOverlap,
29
+ convertBoundToRect,
30
+ } from '../../../lib/utils.js';
31
+ import {
32
+ TextRenderer,
33
+ type TrProps,
34
+ type TextRendererState,
35
+ type TrFontProps,
36
+ type TrPropSetters,
37
+ } from '../TextRenderer.js';
38
+ import { SdfTrFontFace } from '../../font-face-types/SdfTrFontFace/SdfTrFontFace.js';
39
+ import { FLOATS_PER_GLYPH } from './internal/constants.js';
40
+ import { getStartConditions } from './internal/getStartConditions.js';
41
+ import { layoutText } from './internal/layoutText.js';
42
+ import {
43
+ setRenderWindow,
44
+ type SdfRenderWindow,
45
+ } from './internal/setRenderWindow.js';
46
+ import type { TrFontFace } from '../../font-face-types/TrFontFace.js';
47
+ import { TrFontManager, type FontFamilyMap } from '../../TrFontManager.js';
48
+ import { assertTruthy, mergeColorAlpha } from '../../../../utils.js';
49
+ import type { Stage } from '../../../Stage.js';
50
+ import { WebGlCoreRenderOp } from '../../../renderers/webgl/WebGlCoreRenderOp.js';
51
+ import { BufferCollection } from '../../../renderers/webgl/internal/BufferCollection.js';
52
+ import type {
53
+ SdfShader,
54
+ SdfShaderProps,
55
+ } from '../../../renderers/webgl/shaders/SdfShader.js';
56
+ import type { WebGlCoreCtxTexture } from '../../../renderers/webgl/WebGlCoreCtxTexture.js';
57
+ import { EventEmitter } from '../../../../common/EventEmitter.js';
58
+ import type { Matrix3d } from '../../../lib/Matrix3d.js';
59
+
60
+ declare module '../TextRenderer.js' {
61
+ interface TextRendererMap {
62
+ sdf: SdfTextRenderer;
63
+ }
64
+
65
+ // Add prefixed SDF-specific props to TextRendererDebugProps
66
+ interface TextRendererDebugProps {
67
+ sdfShaderDebug: boolean;
68
+ }
69
+ }
70
+
71
+ export interface LineCacheItem {
72
+ codepointIndex: number;
73
+ maxY: number;
74
+ maxX: number;
75
+ }
76
+
77
+ export interface SdfTextRendererState extends TextRendererState {
78
+ /**
79
+ * Cache for layout resume points indexed by the `curY` for each line
80
+ * in the render sequence.
81
+ *
82
+ * Allows faster rendering by skipping parts of the layout loop that are
83
+ * outside of the renderWindow.
84
+ */
85
+ lineCache: LineCacheItem[];
86
+
87
+ renderWindow: SdfRenderWindow;
88
+
89
+ elementBounds: BoundWithValid;
90
+
91
+ clippingRect: RectWithValid;
92
+
93
+ bufferNumFloats: number;
94
+
95
+ bufferNumQuads: number;
96
+
97
+ vertexBuffer: Float32Array | undefined;
98
+
99
+ webGlBuffers: BufferCollection | null;
100
+
101
+ bufferUploaded: boolean;
102
+
103
+ distanceRange: number;
104
+
105
+ trFontFace: SdfTrFontFace | undefined;
106
+ }
107
+
108
+ /**
109
+ * Ephemeral rect object used for calculations
110
+ */
111
+ const tmpRect: Rect = {
112
+ x: 0,
113
+ y: 0,
114
+ width: 0,
115
+ height: 0,
116
+ };
117
+
118
+ /**
119
+ * Singleton class for rendering text using signed distance fields.
120
+ *
121
+ * @remarks
122
+ * SdfTextRenderer supports both single-channel and multi-channel signed distance fields.
123
+ */
124
+ export class SdfTextRenderer extends TextRenderer<SdfTextRendererState> {
125
+ /**
126
+ * Map of font family names to a set of font faces.
127
+ */
128
+ private ssdfFontFamilies: FontFamilyMap = {};
129
+ private msdfFontFamilies: FontFamilyMap = {};
130
+ private fontFamilyArray: FontFamilyMap[] = [
131
+ this.ssdfFontFamilies,
132
+ this.msdfFontFamilies,
133
+ ];
134
+ private sdfShader: SdfShader;
135
+ private rendererBounds: Bound;
136
+
137
+ constructor(stage: Stage) {
138
+ super(stage);
139
+ this.sdfShader = this.stage.shManager.loadShader('SdfShader').shader;
140
+ this.rendererBounds = {
141
+ x1: 0,
142
+ y1: 0,
143
+ x2: this.stage.options.appWidth,
144
+ y2: this.stage.options.appHeight,
145
+ };
146
+ }
147
+
148
+ //#region Overrides
149
+ getPropertySetters(): Partial<TrPropSetters<SdfTextRendererState>> {
150
+ return {
151
+ fontFamily: (state, value) => {
152
+ state.props.fontFamily = value;
153
+ state.trFontFace = undefined;
154
+ this.invalidateLayoutCache(state);
155
+ },
156
+ fontWeight: (state, value) => {
157
+ state.props.fontWeight = value;
158
+ state.trFontFace = undefined;
159
+ this.invalidateLayoutCache(state);
160
+ },
161
+ fontStyle: (state, value) => {
162
+ state.props.fontStyle = value;
163
+ state.trFontFace = undefined;
164
+ this.invalidateLayoutCache(state);
165
+ },
166
+ fontStretch: (state, value) => {
167
+ state.props.fontStretch = value;
168
+ state.trFontFace = undefined;
169
+ this.invalidateLayoutCache(state);
170
+ },
171
+ fontSize: (state, value) => {
172
+ state.props.fontSize = value;
173
+ this.invalidateLayoutCache(state);
174
+ },
175
+ text: (state, value) => {
176
+ state.props.text = value;
177
+ this.invalidateLayoutCache(state);
178
+ },
179
+ textAlign: (state, value) => {
180
+ state.props.textAlign = value;
181
+ this.invalidateLayoutCache(state);
182
+ },
183
+ color: (state, value) => {
184
+ state.props.color = value;
185
+ },
186
+ x: (state, value) => {
187
+ state.props.x = value;
188
+ if (state.elementBounds.valid) {
189
+ this.setElementBoundsX(state);
190
+ // Only schedule an update if the text is not already rendered
191
+ // (renderWindow is invalid) and the element possibly overlaps the screen
192
+ // This is to avoid unnecessary updates when we know text is off-screen
193
+ if (
194
+ !state.renderWindow.valid &&
195
+ boundsOverlap(state.elementBounds, this.rendererBounds)
196
+ ) {
197
+ this.scheduleUpdateState(state);
198
+ }
199
+ }
200
+ },
201
+ y: (state, value) => {
202
+ state.props.y = value;
203
+ if (state.elementBounds.valid) {
204
+ this.setElementBoundsY(state);
205
+ // See x() for explanation
206
+ if (
207
+ !state.renderWindow.valid &&
208
+ boundsOverlap(state.elementBounds, this.rendererBounds)
209
+ ) {
210
+ this.scheduleUpdateState(state);
211
+ }
212
+ }
213
+ },
214
+ contain: (state, value) => {
215
+ state.props.contain = value;
216
+ this.invalidateLayoutCache(state);
217
+ },
218
+ width: (state, value) => {
219
+ state.props.width = value;
220
+ // Only invalidate layout cache if we're containing in the horizontal direction
221
+ if (state.props.contain !== 'none') {
222
+ this.invalidateLayoutCache(state);
223
+ }
224
+ },
225
+ height: (state, value) => {
226
+ state.props.height = value;
227
+ // Only invalidate layout cache if we're containing in the vertical direction
228
+ if (state.props.contain === 'both') {
229
+ this.invalidateLayoutCache(state);
230
+ }
231
+ },
232
+ offsetY: (state, value) => {
233
+ state.props.offsetY = value;
234
+ this.invalidateLayoutCache(state);
235
+ },
236
+ scrollable: (state, value) => {
237
+ state.props.scrollable = value;
238
+ this.invalidateLayoutCache(state);
239
+ },
240
+ scrollY: (state, value) => {
241
+ state.props.scrollY = value;
242
+ // Scrolling doesn't need to invalidate any caches, but it does need to
243
+ // schedule an update
244
+ this.scheduleUpdateState(state);
245
+ },
246
+ letterSpacing: (state, value) => {
247
+ state.props.letterSpacing = value;
248
+ this.invalidateLayoutCache(state);
249
+ },
250
+ lineHeight: (state, value) => {
251
+ state.props.lineHeight = value;
252
+ this.invalidateLayoutCache(state);
253
+ },
254
+ maxLines: (state, value) => {
255
+ state.props.maxLines = value;
256
+ this.invalidateLayoutCache(state);
257
+ },
258
+ textBaseline: (state, value) => {
259
+ state.props.textBaseline = value;
260
+ this.invalidateLayoutCache(state);
261
+ },
262
+ verticalAlign: (state, value) => {
263
+ state.props.verticalAlign = value;
264
+ this.invalidateLayoutCache(state);
265
+ },
266
+ overflowSuffix: (state, value) => {
267
+ state.props.overflowSuffix = value;
268
+ this.invalidateLayoutCache(state);
269
+ },
270
+ debug: (state, value) => {
271
+ state.props.debug = value;
272
+ },
273
+ };
274
+ }
275
+
276
+ override canRenderFont(props: TrFontProps): boolean {
277
+ // TODO: Support matching on font stretch, weight and style (if/when needed)
278
+ // For now we just match on the font family name
279
+ // '$$SDF_FAILURE_TEST$$' is used to test the 'failure' event coming from text
280
+ const { fontFamily } = props;
281
+ return (
282
+ fontFamily in this.ssdfFontFamilies ||
283
+ fontFamily in this.msdfFontFamilies ||
284
+ fontFamily === '$$SDF_FAILURE_TEST$$'
285
+ );
286
+ }
287
+
288
+ override isFontFaceSupported(fontFace: TrFontFace): boolean {
289
+ return fontFace instanceof SdfTrFontFace;
290
+ }
291
+
292
+ override addFontFace(fontFace: TrFontFace): void {
293
+ // Make sure the font face is an SDF font face (it should have already passed
294
+ // the `isFontFaceSupported` check)
295
+ assertTruthy(fontFace instanceof SdfTrFontFace);
296
+ const familyName = fontFace.fontFamily;
297
+ const fontFamiles =
298
+ fontFace.type === 'ssdf'
299
+ ? this.ssdfFontFamilies
300
+ : fontFace.type === 'msdf'
301
+ ? this.msdfFontFamilies
302
+ : undefined;
303
+ if (!fontFamiles) {
304
+ console.warn(`Invalid font face type: ${fontFace.type as string}`);
305
+ return;
306
+ }
307
+ let faceSet = fontFamiles[familyName];
308
+ if (!faceSet) {
309
+ faceSet = new Set();
310
+ fontFamiles[familyName] = faceSet;
311
+ }
312
+ faceSet.add(fontFace);
313
+ }
314
+
315
+ override createState(props: TrProps): SdfTextRendererState {
316
+ return {
317
+ props,
318
+ status: 'initialState',
319
+ updateScheduled: false,
320
+ emitter: new EventEmitter(),
321
+ lineCache: [],
322
+ forceFullLayoutCalc: false,
323
+ renderWindow: {
324
+ screen: {
325
+ x1: 0,
326
+ y1: 0,
327
+ x2: 0,
328
+ y2: 0,
329
+ },
330
+ sdf: {
331
+ x1: 0,
332
+ y1: 0,
333
+ x2: 0,
334
+ y2: 0,
335
+ },
336
+ firstLineIdx: 0,
337
+ numLines: 0,
338
+ valid: false,
339
+ },
340
+ elementBounds: {
341
+ x1: 0,
342
+ y1: 0,
343
+ x2: 0,
344
+ y2: 0,
345
+ valid: false,
346
+ },
347
+ clippingRect: {
348
+ x: 0,
349
+ y: 0,
350
+ width: 0,
351
+ height: 0,
352
+ valid: false,
353
+ },
354
+ bufferNumFloats: 0,
355
+ bufferNumQuads: 0,
356
+ vertexBuffer: undefined,
357
+ webGlBuffers: null,
358
+ bufferUploaded: false,
359
+ textH: undefined,
360
+ textW: undefined,
361
+ distanceRange: 0,
362
+ trFontFace: undefined,
363
+ debugData: {
364
+ updateCount: 0,
365
+ layoutCount: 0,
366
+ lastLayoutNumCharacters: 0,
367
+ layoutSum: 0,
368
+ drawSum: 0,
369
+ drawCount: 0,
370
+ bufferSize: 0,
371
+ },
372
+ };
373
+ }
374
+
375
+ override updateState(state: SdfTextRendererState): void {
376
+ let { trFontFace } = state;
377
+ const { textH, lineCache, debugData, forceFullLayoutCalc } = state;
378
+ debugData.updateCount++;
379
+
380
+ // On the first update call we need to set the status to loading
381
+ if (state.status === 'initialState') {
382
+ this.setStatus(state, 'loading');
383
+ }
384
+
385
+ // Resolve font face if we haven't yet
386
+ if (!trFontFace) {
387
+ trFontFace = this.resolveFontFace(state.props);
388
+ state.trFontFace = trFontFace;
389
+ if (!trFontFace) {
390
+ const msg = `SdfTextRenderer: Could not resolve font face for family: '${state.props.fontFamily}'`;
391
+ console.error(msg);
392
+ this.setStatus(state, 'failed', new Error(msg));
393
+ return;
394
+ }
395
+ }
396
+
397
+ // If the font hasn't been loaded yet, stop here.
398
+ // Listen for the 'loaded' event and forward fontLoaded event
399
+ if (!trFontFace.loaded) {
400
+ trFontFace.once('loaded', () => {
401
+ this.scheduleUpdateState(state);
402
+ });
403
+ return;
404
+ }
405
+
406
+ // If the font is loaded then so should the data
407
+ assertTruthy(trFontFace.data, 'Font face data should be loaded');
408
+
409
+ const {
410
+ text,
411
+ fontSize,
412
+ x,
413
+ y,
414
+ contain,
415
+ width,
416
+ height,
417
+ lineHeight,
418
+ verticalAlign,
419
+ scrollable,
420
+ overflowSuffix,
421
+ maxLines,
422
+ } = state.props;
423
+
424
+ // scrollY only has an effect when contain === 'both' and scrollable === true
425
+ const scrollY = contain === 'both' && scrollable ? state.props.scrollY : 0;
426
+
427
+ const { renderWindow } = state;
428
+
429
+ /**
430
+ * The font size of the SDF font face (the basis for SDF space units)
431
+ */
432
+ const sdfFontSize = trFontFace.data.info.size;
433
+
434
+ /**
435
+ * Divide screen space units by this to get the SDF space units
436
+ * Mulitple SDF space units by this to get screen space units
437
+ */
438
+ const fontSizeRatio = fontSize / sdfFontSize;
439
+
440
+ // Needed in renderWindow calculation
441
+ const sdfLineHeight = lineHeight / fontSizeRatio;
442
+
443
+ state.distanceRange =
444
+ fontSizeRatio * trFontFace.data.distanceField.distanceRange;
445
+
446
+ // Allocate buffers if needed
447
+ const neededLength = text.length * FLOATS_PER_GLYPH;
448
+ let vertexBuffer = state.vertexBuffer;
449
+ if (!vertexBuffer || vertexBuffer.length < neededLength) {
450
+ vertexBuffer = new Float32Array(neededLength * 2);
451
+ }
452
+
453
+ const elementBounds = state.elementBounds;
454
+ if (!elementBounds.valid) {
455
+ this.setElementBoundsX(state);
456
+ this.setElementBoundsY(state);
457
+ elementBounds.valid = true;
458
+ }
459
+
460
+ // Return early if we're still viewing inside the established render window
461
+ // No need to re-render what we've already rendered
462
+ // (Only if there's an established renderWindow and we're not suppressing early exit)
463
+ if (!forceFullLayoutCalc && renderWindow.valid) {
464
+ const rwScreen = renderWindow.screen;
465
+ if (
466
+ x + rwScreen.x1 <= elementBounds.x1 &&
467
+ x + rwScreen.x2 >= elementBounds.x2 &&
468
+ y - scrollY + rwScreen.y1 <= elementBounds.y1 &&
469
+ y - scrollY + rwScreen.y2 >= elementBounds.y2
470
+ ) {
471
+ this.setStatus(state, 'loaded');
472
+ return;
473
+ }
474
+ // Otherwise invalidate the renderWindow so it can be redone
475
+ renderWindow.valid = false;
476
+ this.setStatus(state, 'loading');
477
+ }
478
+
479
+ const { offsetY, textAlign } = state.props;
480
+
481
+ // Create a new renderWindow if needed
482
+ if (!renderWindow.valid) {
483
+ const isPossiblyOnScreen = boundsOverlap(
484
+ elementBounds,
485
+ this.rendererBounds,
486
+ );
487
+
488
+ if (!isPossiblyOnScreen) {
489
+ // If the element is not possibly on screen, we can skip the layout and rendering completely
490
+ return;
491
+ }
492
+
493
+ setRenderWindow(
494
+ renderWindow,
495
+ x,
496
+ y,
497
+ scrollY,
498
+ lineHeight,
499
+ contain === 'both' ? elementBounds.y2 - elementBounds.y1 : 0,
500
+ elementBounds,
501
+ fontSizeRatio,
502
+ );
503
+ // console.log('newRenderWindow', renderWindow);
504
+ }
505
+
506
+ const start = getStartConditions(
507
+ sdfFontSize,
508
+ sdfLineHeight,
509
+ lineHeight,
510
+ verticalAlign,
511
+ offsetY,
512
+ fontSizeRatio,
513
+ renderWindow,
514
+ lineCache,
515
+ textH,
516
+ );
517
+
518
+ if (!start) {
519
+ // Nothing to render, return early, but still mark as loaded (since the text is just scrolled
520
+ // out of view)
521
+ this.setStatus(state, 'loaded');
522
+ return;
523
+ }
524
+
525
+ const { letterSpacing } = state.props;
526
+
527
+ const out2 = layoutText(
528
+ start.lineIndex,
529
+ start.sdfX,
530
+ start.sdfY,
531
+ text,
532
+ textAlign,
533
+ width,
534
+ height,
535
+ fontSize,
536
+ lineHeight,
537
+ letterSpacing,
538
+ vertexBuffer,
539
+ contain,
540
+ lineCache,
541
+ renderWindow.sdf,
542
+ trFontFace,
543
+ forceFullLayoutCalc,
544
+ scrollable,
545
+ overflowSuffix,
546
+ maxLines,
547
+ );
548
+
549
+ state.bufferUploaded = false;
550
+ state.bufferNumFloats = out2.bufferNumFloats;
551
+ state.bufferNumQuads = out2.bufferNumQuads;
552
+ state.vertexBuffer = vertexBuffer;
553
+ state.renderWindow = renderWindow;
554
+ debugData.lastLayoutNumCharacters = out2.layoutNumCharacters;
555
+ debugData.bufferSize = vertexBuffer.byteLength;
556
+
557
+ // If we didn't exit early, we know we have completely computed w/h
558
+ if (out2.fullyProcessed) {
559
+ state.textW = out2.maxX * fontSizeRatio;
560
+ state.textH = out2.maxY * fontSizeRatio;
561
+ }
562
+
563
+ // if (state.props.debug.printLayoutTime) {
564
+ // debugData.layoutSum += performance.now() - updateStartTime;
565
+ // debugData.layoutCount++;
566
+ // }
567
+ this.setStatus(state, 'loaded');
568
+ }
569
+
570
+ override renderQuads(
571
+ state: SdfTextRendererState,
572
+ transform: Matrix3d,
573
+ clippingRect: Readonly<RectWithValid>,
574
+ alpha: number,
575
+ ): void {
576
+ if (!state.vertexBuffer) {
577
+ // Nothing to draw
578
+ return;
579
+ }
580
+
581
+ const { renderer } = this.stage;
582
+
583
+ const { fontSize, color, contain, scrollable, zIndex, debug } = state.props;
584
+
585
+ // scrollY only has an effect when contain === 'both' and scrollable === true
586
+ const scrollY = contain === 'both' && scrollable ? state.props.scrollY : 0;
587
+
588
+ const {
589
+ textW = 0,
590
+ textH = 0,
591
+ distanceRange,
592
+ vertexBuffer,
593
+ bufferUploaded,
594
+ trFontFace,
595
+ elementBounds,
596
+ } = state;
597
+
598
+ let { webGlBuffers } = state;
599
+
600
+ if (!webGlBuffers) {
601
+ const glw = renderer.glw;
602
+ const stride = 4 * Float32Array.BYTES_PER_ELEMENT;
603
+ const webGlBuffer = glw.createBuffer();
604
+ assertTruthy(webGlBuffer);
605
+ state.webGlBuffers = new BufferCollection([
606
+ {
607
+ buffer: webGlBuffer,
608
+ attributes: {
609
+ a_position: {
610
+ name: 'a_position',
611
+ size: 2, // 2 components per iteration
612
+ type: glw.FLOAT, // the data is 32bit floats
613
+ normalized: false, // don't normalize the data
614
+ stride, // 0 = move forward size * sizeof(type) each iteration to get the next position
615
+ offset: 0, // start at the beginning of the buffer
616
+ },
617
+ a_textureCoordinate: {
618
+ name: 'a_textureCoordinate',
619
+ size: 2,
620
+ type: glw.FLOAT,
621
+ normalized: false,
622
+ stride,
623
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT,
624
+ },
625
+ },
626
+ },
627
+ ]);
628
+ state.bufferUploaded = false;
629
+ assertTruthy(state.webGlBuffers);
630
+ webGlBuffers = state.webGlBuffers;
631
+ }
632
+
633
+ if (!bufferUploaded) {
634
+ const glw = renderer.glw;
635
+
636
+ const buffer = webGlBuffers?.getBuffer('a_textureCoordinate') ?? null;
637
+ glw.arrayBufferData(buffer, vertexBuffer, glw.STATIC_DRAW);
638
+ state.bufferUploaded = true;
639
+ }
640
+
641
+ assertTruthy(trFontFace);
642
+ if (scrollable && contain === 'both') {
643
+ assertTruthy(elementBounds.valid);
644
+ const elementRect = convertBoundToRect(elementBounds, tmpRect);
645
+
646
+ if (clippingRect.valid) {
647
+ state.clippingRect.valid = true;
648
+ clippingRect = intersectRect(
649
+ clippingRect,
650
+ elementRect,
651
+ state.clippingRect,
652
+ );
653
+ } else {
654
+ state.clippingRect.valid = true;
655
+ clippingRect = copyRect(elementRect, state.clippingRect);
656
+ }
657
+ }
658
+
659
+ const renderOp = new WebGlCoreRenderOp(
660
+ renderer.glw,
661
+ renderer.options,
662
+ webGlBuffers,
663
+ this.sdfShader,
664
+ {
665
+ transform: transform.data,
666
+ // IMPORTANT: The SDF Shader expects the color NOT to be premultiplied
667
+ // for the best blending results. Which is why we use `mergeColorAlpha`
668
+ // instead of `mergeColorAlphaPremultiplied` here.
669
+ color: mergeColorAlpha(color, alpha),
670
+ size: fontSize / (trFontFace.data?.info.size || 0),
671
+ scrollY,
672
+ distanceRange,
673
+ debug: debug.sdfShaderDebug,
674
+ } satisfies SdfShaderProps,
675
+ alpha,
676
+ clippingRect,
677
+ { height: textH, width: textW },
678
+ 0,
679
+ zIndex,
680
+ );
681
+
682
+ const texture = state.trFontFace?.texture;
683
+ assertTruthy(texture);
684
+ const ctxTexture = this.stage.txManager.getCtxTexture(texture);
685
+
686
+ renderOp.addTexture(ctxTexture as WebGlCoreCtxTexture);
687
+ renderOp.length = state.bufferNumFloats;
688
+ renderOp.numQuads = state.bufferNumQuads;
689
+
690
+ renderer.addRenderOp(renderOp);
691
+
692
+ // if (!debug.disableScissor) {
693
+ // renderer.enableScissor(
694
+ // visibleRect.x,
695
+ // visibleRect.y,
696
+ // visibleRect.w,
697
+ // visibleRect.h,
698
+ // );
699
+ // }
700
+
701
+ // Draw the arrays
702
+ // gl.drawArrays(
703
+ // gl.TRIANGLES, // Primitive type
704
+ // 0,
705
+ // bufferNumVertices, // Number of verticies
706
+ // );
707
+
708
+ // renderer.disableScissor();
709
+
710
+ // if (debug.showElementRect) {
711
+ // this.renderer.drawBorder(
712
+ // Colors.Blue,
713
+ // elementRect.x,
714
+ // elementRect.y,
715
+ // elementRect.w,
716
+ // elementRect.h,
717
+ // );
718
+ // }
719
+
720
+ // if (debug.showVisibleRect) {
721
+ // this.renderer.drawBorder(
722
+ // Colors.Green,
723
+ // visibleRect.x,
724
+ // visibleRect.y,
725
+ // visibleRect.w,
726
+ // visibleRect.h,
727
+ // );
728
+ // }
729
+
730
+ // if (debug.showRenderWindow && renderWindow) {
731
+ // this.renderer.drawBorder(
732
+ // Colors.Red,
733
+ // x + renderWindow.x1,
734
+ // y + renderWindow.y1 - scrollY,
735
+ // x + renderWindow.x2 - (x + renderWindow.x1),
736
+ // y + renderWindow.y2 - scrollY - (y + renderWindow.y1 - scrollY),
737
+ // );
738
+ // }
739
+ // if (debug.printLayoutTime) {
740
+ // debugData.drawSum += performance.now() - drawStartTime;
741
+ // debugData.drawCount++;
742
+ // }
743
+ }
744
+ //#endregion Overrides
745
+
746
+ public resolveFontFace(props: TrFontProps): SdfTrFontFace | undefined {
747
+ return TrFontManager.resolveFontFace(this.fontFamilyArray, props) as
748
+ | SdfTrFontFace
749
+ | undefined;
750
+ }
751
+
752
+ /**
753
+ * Invalidate the layout cache stored in the state. This will cause the text
754
+ * to be re-layed out on the next update.
755
+ *
756
+ * @remarks
757
+ * This also invalidates the visible window cache.
758
+ *
759
+ * @param state
760
+ */
761
+ protected invalidateLayoutCache(state: SdfTextRendererState): void {
762
+ state.renderWindow.valid = false;
763
+ state.elementBounds.valid = false;
764
+ state.textH = undefined;
765
+ state.textW = undefined;
766
+ state.lineCache = [];
767
+ this.setStatus(state, 'loading');
768
+ this.scheduleUpdateState(state);
769
+ }
770
+
771
+ protected setElementBoundsX(state: SdfTextRendererState): void {
772
+ const { x, contain, width } = state.props;
773
+ const { elementBounds } = state;
774
+ elementBounds.x1 = x;
775
+ elementBounds.x2 = contain !== 'none' ? x + width : Infinity;
776
+ }
777
+
778
+ protected setElementBoundsY(state: SdfTextRendererState): void {
779
+ const { y, contain, height } = state.props;
780
+ const { elementBounds } = state;
781
+ elementBounds.y1 = y;
782
+ elementBounds.y2 = contain === 'both' ? y + height : Infinity;
783
+ }
784
+ }