emeraldengine 2.2.0 → 3.0.0

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 (162) hide show
  1. package/README.md +2359 -968
  2. package/dist/types/index.d.ts +72 -32
  3. package/dist/types/src/Animator.d.ts +50 -0
  4. package/dist/types/{BitmapText.d.ts → src/BitmapText.d.ts} +19 -21
  5. package/dist/types/src/Camera.d.ts +122 -0
  6. package/dist/types/src/CameraController.d.ts +107 -0
  7. package/dist/types/src/CanvasText.d.ts +91 -0
  8. package/dist/types/src/CollisionLayers.d.ts +58 -0
  9. package/dist/types/{Color.d.ts → src/Color.d.ts} +5 -6
  10. package/dist/types/src/Coroutine.d.ts +111 -0
  11. package/dist/types/src/DebugOverlay.d.ts +86 -0
  12. package/dist/types/src/Drawable.d.ts +271 -0
  13. package/dist/types/src/Easing.d.ts +22 -0
  14. package/dist/types/src/Emerald.d.ts +420 -0
  15. package/dist/types/src/EmeraldDB.d.ts +159 -0
  16. package/dist/types/{FPSCounter.d.ts → src/FPSCounter.d.ts} +1 -3
  17. package/dist/types/src/GLUtils.d.ts +4 -0
  18. package/dist/types/{Instance.d.ts → src/Instance.d.ts} +37 -15
  19. package/dist/types/{InstancedTexture.d.ts → src/InstancedTexture.d.ts} +60 -23
  20. package/dist/types/src/Interpolator.d.ts +66 -0
  21. package/dist/types/src/Material.d.ts +87 -0
  22. package/dist/types/src/MathUtils.d.ts +80 -0
  23. package/dist/types/src/ParticleEmitter.d.ts +131 -0
  24. package/dist/types/{Physics.d.ts → src/Physics.d.ts} +88 -20
  25. package/dist/types/src/Pool.d.ts +53 -0
  26. package/dist/types/src/PostEffects.d.ts +68 -0
  27. package/dist/types/src/PostProcessor.d.ts +124 -0
  28. package/dist/types/src/RenderTarget.d.ts +56 -0
  29. package/dist/types/src/Scene.d.ts +62 -0
  30. package/dist/types/src/ScreenEffects.d.ts +111 -0
  31. package/dist/types/src/Serializer.d.ts +86 -0
  32. package/dist/types/src/Shaders.d.ts +2 -0
  33. package/dist/types/{Shapes.d.ts → src/Shapes.d.ts} +4 -6
  34. package/dist/types/src/SpatialGrid.d.ts +50 -0
  35. package/dist/types/src/SpriteBatch.d.ts +89 -0
  36. package/dist/types/src/StateMachine.d.ts +59 -0
  37. package/dist/types/src/Storage.d.ts +89 -0
  38. package/dist/types/{Texture.d.ts → src/Texture.d.ts} +3 -4
  39. package/dist/types/src/TextureAtlas.d.ts +55 -0
  40. package/dist/types/src/Tilemap.d.ts +91 -0
  41. package/dist/types/src/Time.d.ts +53 -0
  42. package/dist/types/src/Timer.d.ts +54 -0
  43. package/dist/types/src/Transform.d.ts +94 -0
  44. package/dist/types/src/Tween.d.ts +81 -0
  45. package/dist/types/src/UI.d.ts +121 -0
  46. package/dist/types/src/components/Behaviour.d.ts +71 -0
  47. package/dist/types/{components → src/components}/BoxCollider.d.ts +14 -14
  48. package/dist/types/src/components/BoxColliderDebug.d.ts +15 -0
  49. package/dist/types/{components → src/components}/CircleCollider.d.ts +14 -12
  50. package/dist/types/src/components/CircleColliderDebug.d.ts +15 -0
  51. package/dist/types/src/components/Collider.d.ts +96 -0
  52. package/dist/types/src/components/GameObject.d.ts +135 -0
  53. package/dist/types/src/components/RigidBody.d.ts +183 -0
  54. package/dist/types/src/importers/Aseprite.d.ts +79 -0
  55. package/dist/types/src/importers/TiledMap.d.ts +62 -0
  56. package/dist/types/{lights → src/lights}/DirectionalLight.d.ts +7 -9
  57. package/dist/types/{lights → src/lights}/PointLight.d.ts +6 -9
  58. package/dist/types/src/managers/AssetManager.d.ts +116 -0
  59. package/dist/types/src/managers/AudioManager.d.ts +259 -0
  60. package/dist/types/{managers → src/managers}/CameraManager.d.ts +8 -8
  61. package/dist/types/{managers → src/managers}/EventManager.d.ts +46 -32
  62. package/dist/types/src/managers/GLManager.d.ts +84 -0
  63. package/dist/types/src/managers/GLState.d.ts +37 -0
  64. package/dist/types/src/managers/IDManager.d.ts +31 -0
  65. package/dist/types/src/managers/InputManager.d.ts +290 -0
  66. package/dist/types/src/managers/NetworkManager.d.ts +93 -0
  67. package/dist/types/src/managers/RenderStats.d.ts +34 -0
  68. package/dist/types/src/managers/SceneManager.d.ts +44 -0
  69. package/dist/types/src/managers/ShaderManager.d.ts +55 -0
  70. package/dist/types/src/managers/TextureManager.d.ts +102 -0
  71. package/dist/types/src/particlesystem/Particle.d.ts +64 -0
  72. package/dist/types/{particlesystem → src/particlesystem}/ParticleSettings.d.ts +32 -24
  73. package/dist/types/{particlesystem → src/particlesystem}/Particles.d.ts +20 -12
  74. package/index.js +72 -0
  75. package/package.json +74 -60
  76. package/src/Animator.js +95 -0
  77. package/src/BitmapText.js +6 -5
  78. package/src/Camera.js +183 -0
  79. package/src/CameraController.js +192 -0
  80. package/src/CanvasText.js +281 -0
  81. package/src/CollisionLayers.js +86 -0
  82. package/src/Color.js +18 -18
  83. package/src/Coroutine.js +259 -0
  84. package/src/DebugOverlay.js +246 -0
  85. package/src/Drawable.js +842 -555
  86. package/src/Easing.js +57 -0
  87. package/src/Emerald.js +1150 -459
  88. package/src/EmeraldDB.js +328 -0
  89. package/src/FPSCounter.js +43 -43
  90. package/src/GLUtils.js +60 -67
  91. package/src/Instance.js +41 -5
  92. package/src/InstancedTexture.js +251 -118
  93. package/src/Interpolator.js +124 -0
  94. package/src/Material.js +202 -0
  95. package/src/MathUtils.js +133 -0
  96. package/src/ParticleEmitter.js +284 -0
  97. package/src/Physics.js +186 -5
  98. package/src/Pool.js +85 -0
  99. package/src/PostEffects.js +296 -0
  100. package/src/PostProcessor.js +304 -0
  101. package/src/RenderTarget.js +134 -0
  102. package/src/Scene.js +115 -83
  103. package/src/ScreenEffects.js +266 -0
  104. package/src/Serializer.js +131 -0
  105. package/src/Shaders.js +150 -165
  106. package/src/Shapes.js +118 -129
  107. package/src/SpatialGrid.js +111 -0
  108. package/src/SpriteBatch.js +299 -0
  109. package/src/StateMachine.js +82 -0
  110. package/src/Storage.js +175 -47
  111. package/src/Texture.js +58 -67
  112. package/src/TextureAtlas.js +96 -0
  113. package/src/Tilemap.js +274 -0
  114. package/src/Time.js +51 -6
  115. package/src/Timer.js +99 -0
  116. package/src/Transform.js +100 -7
  117. package/src/Tween.js +160 -0
  118. package/src/UI.js +394 -0
  119. package/src/components/Behaviour.js +90 -0
  120. package/src/components/BoxCollider.js +22 -3
  121. package/src/components/CircleCollider.js +19 -3
  122. package/src/components/CircleColliderDebug.js +24 -24
  123. package/src/components/Collider.js +140 -34
  124. package/src/components/GameObject.js +130 -21
  125. package/src/components/RigidBody.js +123 -2
  126. package/src/importers/Aseprite.js +142 -0
  127. package/src/importers/TiledMap.js +158 -0
  128. package/src/lights/DirectionalLight.js +6 -15
  129. package/src/lights/PointLight.js +4 -4
  130. package/src/managers/AssetManager.js +239 -0
  131. package/src/managers/AudioManager.js +565 -146
  132. package/src/managers/EventManager.js +488 -477
  133. package/src/managers/GLManager.js +57 -0
  134. package/src/managers/GLState.js +70 -0
  135. package/src/managers/IDManager.js +24 -2
  136. package/src/managers/InputManager.js +779 -0
  137. package/src/managers/NetworkManager.js +178 -0
  138. package/src/managers/RenderStats.js +34 -0
  139. package/src/managers/SceneManager.js +30 -0
  140. package/src/managers/ShaderManager.js +0 -2
  141. package/src/managers/TextureManager.js +218 -0
  142. package/src/particlesystem/Particle.js +82 -7
  143. package/src/particlesystem/ParticleSettings.js +21 -3
  144. package/src/particlesystem/Particles.js +80 -31
  145. package/dist/types/Drawable.d.ts +0 -157
  146. package/dist/types/Emerald.d.ts +0 -73
  147. package/dist/types/GLUtils.d.ts +0 -6
  148. package/dist/types/Scene.d.ts +0 -39
  149. package/dist/types/Shaders.d.ts +0 -4
  150. package/dist/types/Storage.d.ts +0 -46
  151. package/dist/types/Time.d.ts +0 -22
  152. package/dist/types/Transform.d.ts +0 -41
  153. package/dist/types/components/BoxColliderDebug.d.ts +0 -19
  154. package/dist/types/components/CircleColliderDebug.d.ts +0 -19
  155. package/dist/types/components/Collider.d.ts +0 -53
  156. package/dist/types/components/GameObject.d.ts +0 -72
  157. package/dist/types/components/RigidBody.d.ts +0 -104
  158. package/dist/types/managers/AudioManager.d.ts +0 -60
  159. package/dist/types/managers/GLManager.d.ts +0 -47
  160. package/dist/types/managers/IDManager.d.ts +0 -21
  161. package/dist/types/managers/SceneManager.d.ts +0 -22
  162. package/dist/types/particlesystem/Particle.d.ts +0 -42
package/src/Camera.js ADDED
@@ -0,0 +1,183 @@
1
+ import Transform from "./Transform.js";
2
+ import { Vector2, Vector3 } from "./Physics.js";
3
+
4
+ /**
5
+ * @class Camera
6
+ * @description A view into the scene with its own transform (position, zoom,
7
+ * rotation) and a viewport rectangle. Multiple cameras can be added to an
8
+ * Emerald instance to render split-screen or picture-in-picture. The viewport
9
+ * is expressed in normalized [0..1] coordinates with the origin at the
10
+ * bottom-left of the canvas (matching WebGL).
11
+ *
12
+ * Backward compatible with the previous inline camera: position is stored
13
+ * negated internally, and setPosition/getPosition/setZoom/getZoom behave the
14
+ * same, so EventManager and CameraController are unaffected.
15
+ *
16
+ * @example
17
+ * const left = new Camera({ viewport: { x: 0, y: 0, width: 0.5, height: 1 } });
18
+ * const right = new Camera({ viewport: { x: 0.5, y: 0, width: 0.5, height: 1 } });
19
+ * emerald.setCameras([left, right]);
20
+ */
21
+ class Camera {
22
+ constructor(options = {}) {
23
+ this.transform = new Transform(new Vector3(0, 0, 0), 0, new Vector2(1, 1));
24
+
25
+ const vp = options.viewport || {};
26
+ this.viewport = {
27
+ x: vp.x ?? 0,
28
+ y: vp.y ?? 0,
29
+ width: vp.width ?? 1,
30
+ height: vp.height ?? 1,
31
+ };
32
+
33
+ this.active = options.active !== false;
34
+ this.clearColor = options.clearColor || null;
35
+
36
+ this.ignoreLayers = new Set(options.ignoreLayers || []);
37
+
38
+ this.onlyLayers = options.onlyLayers ? new Set(options.onlyLayers) : null;
39
+
40
+ this.excludeFromPost = options.excludeFromPost === true;
41
+
42
+ this.pixelSnap = options.pixelSnap === true;
43
+ }
44
+
45
+ /**
46
+ * @method setPixelSnap
47
+ * @description When true, the camera's rendered position is rounded to whole
48
+ * screen pixels each frame (the stored position stays smooth). Use it for
49
+ * pixel-art games to eliminate sub-pixel shimmer and tile seams while a
50
+ * smooth-follow camera moves. Ignored while the camera is rotated.
51
+ * @param {boolean} snap
52
+ * @returns {Camera} - this
53
+ */
54
+ setPixelSnap(snap) {
55
+ this.pixelSnap = snap !== false;
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * @method setExcludeFromPost
61
+ * @description When true, this camera renders straight to the screen AFTER the
62
+ * post-processing pass instead of into the post-processed scene buffer. Use it
63
+ * for HUD/UI cameras so effects like bloom don't affect the interface.
64
+ * @param {boolean} exclude
65
+ * @returns {Camera} - this
66
+ */
67
+ setExcludeFromPost(exclude) {
68
+ this.excludeFromPost = exclude !== false;
69
+ return this;
70
+ }
71
+
72
+ /**
73
+ * @method setOnlyLayers
74
+ * @description Restricts the camera to rendering only the given layers (or
75
+ * pass null to clear the restriction).
76
+ * @param {number[]|null} layers
77
+ * @returns {Camera} - this
78
+ */
79
+ setOnlyLayers(layers) {
80
+ this.onlyLayers = layers ? new Set(layers) : null;
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * @method setIgnoreLayers
86
+ * @description Replaces the set of layers this camera skips when rendering.
87
+ * @param {number[]} layers - Layer indices to ignore
88
+ * @returns {Camera} - this
89
+ */
90
+ setIgnoreLayers(layers) {
91
+ this.ignoreLayers = new Set(layers);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * @method ignoreLayer
97
+ * @description Adds a single layer to the ignore set.
98
+ * @param {number} layer
99
+ * @returns {Camera} - this
100
+ */
101
+ ignoreLayer(layer) {
102
+ this.ignoreLayers.add(layer);
103
+ return this;
104
+ }
105
+
106
+ /**
107
+ * @method setPosition
108
+ * @description Sets the world-space center of the camera.
109
+ */
110
+ setPosition(x, y, z = 0) {
111
+ this.transform.position.x = -x;
112
+ this.transform.position.y = -y;
113
+ this.transform.position.z = -z;
114
+ }
115
+
116
+ /**
117
+ * @method getPosition
118
+ * @description Returns the world-space center of the camera.
119
+ */
120
+ getPosition() {
121
+ return {
122
+ x: -this.transform.position.x,
123
+ y: -this.transform.position.y,
124
+ z: -this.transform.position.z,
125
+ };
126
+ }
127
+
128
+ /**
129
+ * @method setZoom
130
+ * @description Sets the zoom (1 = default, >1 zooms in).
131
+ */
132
+ setZoom(zoom) {
133
+ const z = Math.max(0.0001, zoom);
134
+ this.transform.scale.x = z;
135
+ this.transform.scale.y = z;
136
+ }
137
+
138
+ /**
139
+ * @method getZoom
140
+ * @returns {number}
141
+ */
142
+ getZoom() {
143
+ return this.transform.scale.x;
144
+ }
145
+
146
+ /**
147
+ * @method setRotation
148
+ * @description Sets the camera rotation in radians.
149
+ */
150
+ setRotation(radians) {
151
+ this.transform.rotation = radians;
152
+ }
153
+
154
+ /**
155
+ * @method getRotation
156
+ * @returns {number}
157
+ */
158
+ getRotation() {
159
+ return this.transform.rotation;
160
+ }
161
+
162
+ /**
163
+ * @method setViewport
164
+ * @description Sets the normalized viewport rectangle (origin bottom-left).
165
+ * @returns {Camera} - this
166
+ */
167
+ setViewport(x, y, width, height) {
168
+ this.viewport = { x, y, width, height };
169
+ return this;
170
+ }
171
+
172
+ /**
173
+ * @method setActive
174
+ * @description Enables/disables rendering of this camera.
175
+ * @returns {Camera} - this
176
+ */
177
+ setActive(active) {
178
+ this.active = active;
179
+ return this;
180
+ }
181
+ }
182
+
183
+ export default Camera;
@@ -0,0 +1,192 @@
1
+ import MathUtils from "./MathUtils.js";
2
+
3
+ /**
4
+ * @class CameraController
5
+ * @description Drives an Emerald camera with follow, bounds clamping, zoom and
6
+ * screen shake. Construct it with `emerald.camera` and call `update(dt)` once
7
+ * per frame.
8
+ *
9
+ * @example
10
+ * const cam = new CameraController(emerald.camera);
11
+ * cam.follow(player.gameObject, 0.1).setBounds(-1000, -1000, 1000, 1000);
12
+ * // in the loop: cam.update(deltaTime);
13
+ */
14
+ class CameraController {
15
+ /**
16
+ * @param {Object} camera - The Emerald camera (e.g. emerald.camera)
17
+ */
18
+ constructor(camera) {
19
+ this.camera = camera;
20
+ this.target = null;
21
+ this.smoothing = 1;
22
+ this.bounds = null;
23
+
24
+ const start = camera.getPosition ? camera.getPosition() : { x: 0, y: 0 };
25
+ this.position = { x: start.x, y: start.y };
26
+
27
+ this.shakeIntensity = 0;
28
+ this.shakeDuration = 0;
29
+ this.shakeElapsed = Infinity;
30
+
31
+ this.deadzone = null;
32
+ }
33
+
34
+ /**
35
+ * @method setDeadzone
36
+ * @description Sets a centered follow deadzone. The camera only scrolls when
37
+ * the target moves outside this box, giving the player room to move without
38
+ * the camera reacting to every step. Pass 0/0 or null to disable.
39
+ * @param {number} halfWidth - Horizontal half-extent in world units
40
+ * @param {number} halfHeight - Vertical half-extent in world units
41
+ * @returns {CameraController} - this
42
+ */
43
+ setDeadzone(halfWidth, halfHeight) {
44
+ this.deadzone =
45
+ halfWidth || halfHeight
46
+ ? { x: Math.abs(halfWidth || 0), y: Math.abs(halfHeight || 0) }
47
+ : null;
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * @method follow
53
+ * @description Follows a target each frame.
54
+ * @param {Object} target - GameObject/Instance (uses transform.position) or a { x, y }
55
+ * @param {number} smoothing - 0..1, higher snaps faster (1 = instant)
56
+ * @returns {CameraController} - this
57
+ */
58
+ follow(target, smoothing = 0.1) {
59
+ this.target = target;
60
+ this.smoothing = smoothing;
61
+ return this;
62
+ }
63
+
64
+ /**
65
+ * @method stopFollow
66
+ * @description Stops following the current target.
67
+ * @returns {CameraController} - this
68
+ */
69
+ stopFollow() {
70
+ this.target = null;
71
+ return this;
72
+ }
73
+
74
+ /**
75
+ * @method setBounds
76
+ * @description Clamps the camera center to a world-space rectangle.
77
+ * @returns {CameraController} - this
78
+ */
79
+ setBounds(minX, minY, maxX, maxY) {
80
+ this.bounds = { minX, minY, maxX, maxY };
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * @method clearBounds
86
+ * @description Removes bounds clamping.
87
+ * @returns {CameraController} - this
88
+ */
89
+ clearBounds() {
90
+ this.bounds = null;
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * @method setZoom
96
+ * @description Sets the camera zoom (1 = default, >1 zooms in).
97
+ * @returns {CameraController} - this
98
+ */
99
+ setZoom(zoom) {
100
+ if (this.camera.setZoom) this.camera.setZoom(zoom);
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * @method getZoom
106
+ * @returns {number} - The current zoom
107
+ */
108
+ getZoom() {
109
+ return this.camera.getZoom ? this.camera.getZoom() : 1;
110
+ }
111
+
112
+ /**
113
+ * @method setPosition
114
+ * @description Immediately moves the camera center.
115
+ * @returns {CameraController} - this
116
+ */
117
+ setPosition(x, y) {
118
+ this.position.x = x;
119
+ this.position.y = y;
120
+ this.camera.setPosition(x, y, 0);
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * @method shake
126
+ * @description Triggers a screen shake that decays over its duration.
127
+ * @param {number} intensity - Max offset in world units
128
+ * @param {number} duration - Duration in seconds
129
+ * @returns {CameraController} - this
130
+ */
131
+ shake(intensity, duration) {
132
+ this.shakeIntensity = intensity;
133
+ this.shakeDuration = duration;
134
+ this.shakeElapsed = 0;
135
+ return this;
136
+ }
137
+
138
+ /** @private */
139
+ _targetPosition() {
140
+ const t = this.target;
141
+ if (!t) return null;
142
+ if (t.transform && t.transform.position) return t.transform.position;
143
+ return t;
144
+ }
145
+
146
+ /**
147
+ * @method update
148
+ * @description Advances follow/shake and applies the result to the camera.
149
+ * @param {number} dt - Seconds since the last frame
150
+ */
151
+ update(dt) {
152
+ const targetPos = this._targetPosition();
153
+ if (targetPos) {
154
+ let goalX = targetPos.x;
155
+ let goalY = targetPos.y;
156
+ if (this.deadzone) {
157
+ goalX = this.position.x;
158
+ goalY = this.position.y;
159
+ const dx = targetPos.x - this.position.x;
160
+ const dy = targetPos.y - this.position.y;
161
+ if (dx > this.deadzone.x) goalX = targetPos.x - this.deadzone.x;
162
+ else if (dx < -this.deadzone.x) goalX = targetPos.x + this.deadzone.x;
163
+ if (dy > this.deadzone.y) goalY = targetPos.y - this.deadzone.y;
164
+ else if (dy < -this.deadzone.y) goalY = targetPos.y + this.deadzone.y;
165
+ }
166
+
167
+ const s = MathUtils.clamp(this.smoothing, 0, 1);
168
+ const a = s >= 1 ? 1 : 1 - Math.pow(1 - s, dt * 60);
169
+ this.position.x = MathUtils.lerp(this.position.x, goalX, a);
170
+ this.position.y = MathUtils.lerp(this.position.y, goalY, a);
171
+ }
172
+
173
+ let x = this.position.x;
174
+ let y = this.position.y;
175
+
176
+ if (this.bounds) {
177
+ x = MathUtils.clamp(x, this.bounds.minX, this.bounds.maxX);
178
+ y = MathUtils.clamp(y, this.bounds.minY, this.bounds.maxY);
179
+ }
180
+
181
+ if (this.shakeElapsed < this.shakeDuration) {
182
+ this.shakeElapsed += dt;
183
+ const falloff = Math.max(0, 1 - this.shakeElapsed / this.shakeDuration);
184
+ x += (Math.random() * 2 - 1) * this.shakeIntensity * falloff;
185
+ y += (Math.random() * 2 - 1) * this.shakeIntensity * falloff;
186
+ }
187
+
188
+ this.camera.setPosition(x, y, 0);
189
+ }
190
+ }
191
+
192
+ export default CameraController;
@@ -0,0 +1,281 @@
1
+ import Drawable from "./Drawable.js";
2
+ import GameObject from "./components/GameObject.js";
3
+ import GLManager from "./managers/GLManager.js";
4
+ import { initVertexBuffer } from "./GLUtils.js";
5
+ import { Vector2, Vector3 } from "./Physics.js";
6
+
7
+ /**
8
+ * @class CanvasText
9
+ * @description Renders dynamic text using a system font by drawing to an
10
+ * offscreen 2D canvas and uploading it as a texture. Unlike BitmapText it needs
11
+ * no font sheet and supports any installed font. It's a Drawable component;
12
+ * `CanvasText.create()` returns a ready GameObject sized to the text.
13
+ *
14
+ * @example
15
+ * const label = CanvasText.create("Score: 0", {
16
+ * font: "bold 28px monospace", color: "#6ee7b7", screenSpace: true,
17
+ * });
18
+ * label.transform.position = new Vector3(-300, 260, 100);
19
+ * scene.add(label);
20
+ * label.getComponent(CanvasText).setText("Score: 120");
21
+ */
22
+ class CanvasText extends Drawable {
23
+ constructor(text, options = {}) {
24
+ const gl = GLManager.getGL();
25
+ const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
26
+ const verticesBuffer = initVertexBuffer(gl, vertices);
27
+ const texCoords = [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0];
28
+ const texCoordBuffer = initVertexBuffer(gl, texCoords);
29
+
30
+ super(
31
+ gl,
32
+ GLManager.getProgramInfo(),
33
+ verticesBuffer,
34
+ texCoordBuffer,
35
+ vertices,
36
+ false,
37
+ "",
38
+ 0,
39
+ 0,
40
+ 1,
41
+ 1,
42
+ 0,
43
+ false,
44
+ false,
45
+ options.useLighting === true
46
+ );
47
+
48
+ this.font = options.font || "24px sans-serif";
49
+ this.fillStyle = options.color || "#ffffff";
50
+ this.padding = options.padding != null ? options.padding : 6;
51
+ this.maxWidth = options.maxWidth || 0;
52
+ this.align = options.align || "center";
53
+ this.lineHeight = options.lineHeight || 0;
54
+ /** @private */
55
+ this._canvas = document.createElement("canvas");
56
+ /** @private */
57
+ this._ctx = this._canvas.getContext("2d");
58
+
59
+ this.useTexture = true;
60
+ this.texture = gl.createTexture();
61
+ this.width = 1;
62
+ this.height = 1;
63
+
64
+ this.setText(text);
65
+ }
66
+
67
+ /**
68
+ * @method setText
69
+ * @description Re-renders the text and re-uploads the texture. If attached to
70
+ * a GameObject, its scale is updated to match the new size.
71
+ * @param {string} text
72
+ */
73
+ setText(text) {
74
+ this.text = text;
75
+ const ctx = this._ctx;
76
+ const dpr = (typeof window !== "undefined" && window.devicePixelRatio) || 1;
77
+
78
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
79
+ ctx.font = this.font;
80
+ const fontSize = parseInt(this.font, 10) || 24;
81
+ const lineHeight = this.lineHeight || Math.ceil(fontSize * 1.4);
82
+
83
+ const lines = CanvasText.wrapText(
84
+ (s) => ctx.measureText(s).width,
85
+ String(text),
86
+ this.maxWidth
87
+ );
88
+
89
+ let maxLineW = 1;
90
+ for (const ln of lines) {
91
+ const m = ctx.measureText(ln);
92
+ let lw = m.width;
93
+ if (m.actualBoundingBoxLeft != null && m.actualBoundingBoxRight != null) {
94
+ lw = Math.max(lw, m.actualBoundingBoxLeft + m.actualBoundingBoxRight);
95
+ }
96
+ maxLineW = Math.max(maxLineW, lw);
97
+ }
98
+ const contentW = this.maxWidth
99
+ ? Math.max(maxLineW, this.maxWidth)
100
+ : maxLineW;
101
+ const w = Math.max(1, Math.ceil(contentW) + this.padding * 2);
102
+ const h = lines.length * lineHeight + this.padding * 2;
103
+
104
+ this._canvas.width = Math.max(1, Math.round(w * dpr));
105
+ this._canvas.height = Math.max(1, Math.round(h * dpr));
106
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
107
+ ctx.clearRect(0, 0, w, h);
108
+ ctx.font = this.font;
109
+ ctx.textBaseline = "middle";
110
+ ctx.textAlign = this.align;
111
+ ctx.fillStyle = this.fillStyle;
112
+
113
+ const x =
114
+ this.align === "left"
115
+ ? this.padding
116
+ : this.align === "right"
117
+ ? w - this.padding
118
+ : w / 2;
119
+ for (let i = 0; i < lines.length; i++) {
120
+ const y = this.padding + lineHeight * (i + 0.5);
121
+ ctx.fillText(lines[i], x, y);
122
+ }
123
+
124
+ const gl = this.gl;
125
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
126
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
127
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
128
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
129
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
130
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
131
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
132
+ gl.texImage2D(
133
+ gl.TEXTURE_2D,
134
+ 0,
135
+ gl.RGBA,
136
+ gl.RGBA,
137
+ gl.UNSIGNED_BYTE,
138
+ this._canvas
139
+ );
140
+
141
+ this.textureWidth = this._canvas.width;
142
+ this.textureHeight = this._canvas.height;
143
+ this.originalTexture = this.texture;
144
+ this.width = w;
145
+ this.height = h;
146
+ /** @private */
147
+ this._lastTextureWidth = -1;
148
+
149
+ if (this.parentObject && this.parentObject.transform) {
150
+ this.parentObject.transform.scale.x = w / 2;
151
+ this.parentObject.transform.scale.y = h / 2;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * @method wrapText
157
+ * @description Pure helper that breaks `text` into rendered lines. Honors
158
+ * explicit `\n` line breaks and, when `maxWidth > 0`, greedily word-wraps each
159
+ * paragraph so no line measures wider than `maxWidth`. A single word longer
160
+ * than `maxWidth` is kept on its own line rather than being split.
161
+ * @param {(s:string)=>number} measure - Returns the pixel width of a string
162
+ * @param {string} text - The text to lay out
163
+ * @param {number} maxWidth - Wrap width in pixels (0 disables wrapping)
164
+ * @returns {string[]} - The lines to render top to bottom
165
+ */
166
+ static wrapText(measure, text, maxWidth) {
167
+ const hardLines = text.split("\n");
168
+ if (!maxWidth || maxWidth <= 0) return hardLines;
169
+ const out = [];
170
+ for (const hard of hardLines) {
171
+ const words = hard.split(" ");
172
+ let line = "";
173
+ for (const word of words) {
174
+ const test = line ? line + " " + word : word;
175
+ if (line && measure(test) > maxWidth) {
176
+ out.push(line);
177
+ line = word;
178
+ } else {
179
+ line = test;
180
+ }
181
+ }
182
+ out.push(line);
183
+ }
184
+ return out;
185
+ }
186
+
187
+ /**
188
+ * @method setMaxWidth
189
+ * @description Sets the wrap width in pixels (0 disables wrapping) and
190
+ * re-renders.
191
+ * @param {number} px
192
+ */
193
+ setMaxWidth(px) {
194
+ this.maxWidth = px || 0;
195
+ this.setText(this.text);
196
+ }
197
+
198
+ /**
199
+ * @method setAlign
200
+ * @description Sets horizontal alignment ("left" | "center" | "right") and
201
+ * re-renders.
202
+ * @param {string} align
203
+ */
204
+ setAlign(align) {
205
+ this.align = align;
206
+ this.setText(this.text);
207
+ }
208
+
209
+ /**
210
+ * @method setColor
211
+ * @description Sets the text color (CSS color string) and re-renders.
212
+ * @param {string} cssColor
213
+ */
214
+ setColor(cssColor) {
215
+ this.fillStyle = cssColor;
216
+ this.setText(this.text);
217
+ }
218
+
219
+ /**
220
+ * @method getSize
221
+ * @returns {{width:number, height:number}} - The canvas pixel size
222
+ */
223
+ getSize() {
224
+ return { width: this.width, height: this.height };
225
+ }
226
+
227
+ /**
228
+ * @method _restoreGL
229
+ * @description After a context loss: rebuild the base buffers, recreate this
230
+ * text's own texture, and re-render the current string into it.
231
+ * @private
232
+ */
233
+ _restoreGL() {
234
+ if (this._disposed) return;
235
+ super._restoreGL();
236
+ if (!this.gl) return;
237
+ this._canvas = this._canvas || document.createElement("canvas");
238
+ this._ctx = this._canvas.getContext("2d");
239
+ this.texture = this.gl.createTexture();
240
+ this.setText(this.text);
241
+ }
242
+
243
+ /**
244
+ * @method dispose
245
+ * @description Frees the GL texture this text owns (CanvasText renders into
246
+ * its own texture, unlike path-loaded Textures which are shared) plus the
247
+ * base Drawable buffers. Safe to call twice.
248
+ */
249
+ dispose() {
250
+ if (this._disposed) return;
251
+ if (this.gl && this.texture) this.gl.deleteTexture(this.texture);
252
+ this.texture = null;
253
+ this.originalTexture = null;
254
+ this._canvas = null;
255
+ this._ctx = null;
256
+ super.dispose();
257
+ }
258
+
259
+ /**
260
+ * @method create
261
+ * @description Convenience factory returning a GameObject sized to the text.
262
+ * @param {string} text
263
+ * @param {Object} [options] - { name, position, screenSpace, font, color,
264
+ * maxWidth, align, lineHeight, padding }
265
+ * @returns {GameObject}
266
+ */
267
+ static create(text, options = {}) {
268
+ const component = new CanvasText(text, options);
269
+ const go = new GameObject(
270
+ options.name || `text:${text}`,
271
+ options.position || new Vector3(0, 0, 0),
272
+ 0,
273
+ new Vector2(component.width / 2, component.height / 2)
274
+ );
275
+ go.addComponent(component);
276
+ if (options.screenSpace) go.setScreenSpace(true);
277
+ return go;
278
+ }
279
+ }
280
+
281
+ export default CanvasText;