emeraldengine 2.2.1 → 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 -582
  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/Emerald.js CHANGED
@@ -1,459 +1,1150 @@
1
- import { STANDARD_FRAGMENT_SHADER, STANDARD_VERTEX_SHADER } from "./Shaders.js";
2
- import { initShaderProgram } from "./GLUtils.js";
3
- import { mat4 } from "gl-matrix";
4
- import Color from "./Color.js";
5
- import Transform from "./Transform.js";
6
- import { Vector2, Vector3 } from "./Physics.js";
7
- import Time from "./Time.js";
8
- import GLManager from "./managers/GLManager.js";
9
- import CameraManager from "./managers/CameraManager.js";
10
- import PointLight from "./lights/PointLight.js";
11
- import DirectionalLight from "./lights/DirectionalLight.js";
12
-
13
- /**
14
- * @class Emerald
15
- * @description A class that represents the Emerald engine
16
- * @param {HTMLCanvasElement} canvas - The canvas element
17
- */
18
- class Emerald {
19
- constructor(canvas) {
20
- const gl = canvas.getContext("webgl2", {
21
- antialias: true,
22
- powerPreference: "high-performance",
23
- desynchronized: false,
24
- });
25
- if (!gl) {
26
- console.error("[Emerald.js] > No WebGL context found");
27
- return undefined;
28
- }
29
-
30
- const shaderProgram = initShaderProgram(
31
- gl,
32
- STANDARD_VERTEX_SHADER,
33
- STANDARD_FRAGMENT_SHADER
34
- );
35
- this.shaderProgram = shaderProgram;
36
- this.gl = gl;
37
- this.programInfo = {
38
- program: shaderProgram,
39
- attribLocations: {
40
- vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
41
- aTexCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
42
- instanceMatrix: gl.getAttribLocation(shaderProgram, "aInstanceMatrix0"),
43
- instanceTexCoord0: gl.getAttribLocation(
44
- shaderProgram,
45
- "aInstanceTexCoord0"
46
- ),
47
- instanceTexCoord1: gl.getAttribLocation(
48
- shaderProgram,
49
- "aInstanceTexCoord1"
50
- ),
51
- instanceTexCoord2: gl.getAttribLocation(
52
- shaderProgram,
53
- "aInstanceTexCoord2"
54
- ),
55
- instanceTexCoord3: gl.getAttribLocation(
56
- shaderProgram,
57
- "aInstanceTexCoord3"
58
- ),
59
- },
60
- uniformLocations: {
61
- projectionMatrix: gl.getUniformLocation(
62
- shaderProgram,
63
- "uProjectionMatrix"
64
- ),
65
- globalViewMatrix: gl.getUniformLocation(
66
- shaderProgram,
67
- "uModelViewMatrix"
68
- ),
69
- instancedModelViewMatrix: gl.getUniformLocation(
70
- shaderProgram,
71
- "uInstancedModelViewMatrix"
72
- ),
73
- uModelMatrix: gl.getUniformLocation(shaderProgram, "uModelMatrix"),
74
- uInstancedModelMatrix: gl.getUniformLocation(
75
- shaderProgram,
76
- "uInstancedModelMatrix"
77
- ),
78
- uAmbientLightValues: gl.getUniformLocation(
79
- shaderProgram,
80
- "uAmbientLightValues"
81
- ),
82
- uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
83
- color: gl.getUniformLocation(shaderProgram, "uColor"),
84
- useTexture: gl.getUniformLocation(shaderProgram, "useTexture"),
85
- useInstances: gl.getUniformLocation(shaderProgram, "useInstances"),
86
- useText: gl.getUniformLocation(shaderProgram, "useText"),
87
- useLighting: gl.getUniformLocation(shaderProgram, "uUseLighting"),
88
- uOpacity: gl.getUniformLocation(shaderProgram, "uOpacity"),
89
- // Point light uniforms
90
- uLightPosition: gl.getUniformLocation(shaderProgram, "uLightPosition"),
91
- uLightColor: gl.getUniformLocation(shaderProgram, "uLightColor"),
92
- uLightIntensity: gl.getUniformLocation(
93
- shaderProgram,
94
- "uLightIntensity"
95
- ),
96
- uLightRadius: gl.getUniformLocation(shaderProgram, "uLightRadius"),
97
- uActiveLights: gl.getUniformLocation(shaderProgram, "uActiveLights"),
98
- // Directional light uniforms
99
- uDirLightPosition: gl.getUniformLocation(
100
- shaderProgram,
101
- "uDirLightPosition"
102
- ),
103
- uDirLightDirection: gl.getUniformLocation(
104
- shaderProgram,
105
- "uDirLightDirection"
106
- ),
107
- uDirLightColor: gl.getUniformLocation(shaderProgram, "uDirLightColor"),
108
- uDirLightIntensity: gl.getUniformLocation(
109
- shaderProgram,
110
- "uDirLightIntensity"
111
- ),
112
- uDirLightWidth: gl.getUniformLocation(shaderProgram, "uDirLightWidth"),
113
- uActiveDirLights: gl.getUniformLocation(
114
- shaderProgram,
115
- "uActiveDirLights"
116
- ),
117
- },
118
- };
119
- GLManager.setGL(gl);
120
- GLManager.setProgramInfo(this.programInfo);
121
- this.camera = {
122
- transform: new Transform(new Vector3(0, 0, 0), 0, new Vector2(1, 1)),
123
- setPosition(x, y, z) {
124
- this.transform.position.x = -x;
125
- this.transform.position.y = -y;
126
- this.transform.position.z = -z;
127
- },
128
- };
129
-
130
- CameraManager.setCamera(this.camera);
131
-
132
- this.ambientLight = new Vector3(1.0, 1.0, 1.0);
133
- this.pointLights = [];
134
- this.directionalLights = [];
135
- this.debugLogged = false; // Flag to prevent spam in console
136
- this.backgroundColor = {
137
- r: 0.0,
138
- g: 0.0,
139
- b: 0.0,
140
- a: 1.0,
141
- };
142
- }
143
-
144
- /**
145
- * @method setAmbientLight
146
- * @description Sets the ambient light for the scene
147
- * @param {Vector3} vector3 - The ambient light color as a Vector3 instance
148
- */
149
- setAmbientLight(vector3) {
150
- if (vector3.x && vector3.y && vector3.z) {
151
- this.ambientLight = vector3;
152
- } else {
153
- console.error(
154
- "[Emerald.js] > vector3 is not an instance of Vector3 class."
155
- );
156
- }
157
- }
158
-
159
- /**
160
- * @method resize
161
- * @description Resizes the canvas
162
- * @param {number} width - The width of the canvas
163
- * @param {number} height - The height of the canvas
164
- */
165
- resize(width, height) {
166
- // Update canvas dimensions
167
- this.gl.canvas.width = width;
168
- this.gl.canvas.height = height;
169
-
170
- // Update WebGL viewport
171
- this.gl.viewport(0, 0, width, height);
172
- }
173
-
174
- /**
175
- * @method addPointLight
176
- * @description Adds a point light to the scene
177
- * @param {PointLight} pointLight - The point light to add
178
- */
179
- addPointLight(pointLight) {
180
- if (!(pointLight instanceof PointLight)) {
181
- console.error(
182
- "[Emerald.js] > Passed argument is not an instance of PointLight class."
183
- );
184
- return;
185
- }
186
- if (this.pointLights.length < 4) {
187
- // Maximum 4 lights as defined in shader
188
- this.pointLights.push(pointLight);
189
- } else {
190
- console.warn("Maximum number of point lights reached (4)");
191
- }
192
- }
193
-
194
- /**
195
- * @method addDirectionalLight
196
- * @description Adds a directional light to the scene
197
- * @param {DirectionalLight} directionalLight - The directional light to add
198
- */
199
- addDirectionalLight(directionalLight) {
200
- if (!(directionalLight instanceof DirectionalLight)) {
201
- console.error(
202
- "[Emerald.js] > Passed argument is not an instance of DirectionalLight class."
203
- );
204
- return;
205
- }
206
- if (this.directionalLights.length < 4) {
207
- // Maximum 4 directional lights as defined in shader
208
- this.directionalLights.push(directionalLight);
209
- } else {
210
- console.warn("Maximum number of directional lights reached (4)");
211
- }
212
- }
213
-
214
- /**
215
- * @method removeDirectionalLight
216
- * @description Removes a directional light from the scene
217
- * @param {DirectionalLight} directionalLight - The directional light to remove
218
- */
219
- removeDirectionalLight(directionalLight) {
220
- const index = this.directionalLights.indexOf(directionalLight);
221
- if (index > -1) {
222
- this.directionalLights.splice(index, 1);
223
- }
224
- }
225
-
226
- /**
227
- * @method setBackgroundColor
228
- * @description Sets the background color of the scene
229
- * @param {Color} color - The background color
230
- */
231
- setBackgroundColor(color) {
232
- if (color instanceof Color) {
233
- this.backgroundColor = {
234
- r: color.r / 255,
235
- g: color.g / 255,
236
- b: color.b / 255,
237
- a: color.a / 255,
238
- };
239
- } else {
240
- console.error("[Emerald.js] > color is not an instance of Color class.");
241
- }
242
- }
243
- // Buggy (events don't work correctly when zoom changes from 1)
244
- // setZoom(float) {
245
- // vec3.set(this.camera.scale, float, float, float);
246
- // }
247
-
248
- // getZoom() {
249
- // return this.camera.scale[0];
250
- // }
251
-
252
- /**
253
- * @method drawScene
254
- * @description Draws the scene
255
- * @param {Scene} scene - The scene to draw
256
- * @param {number} deltaTime - The delta time
257
- */
258
- drawScene(scene, deltaTime) {
259
- Time.setDeltaTime(deltaTime);
260
- if (this.backgroundColor) {
261
- this.gl.clearColor(
262
- this.backgroundColor.r,
263
- this.backgroundColor.g,
264
- this.backgroundColor.b,
265
- this.backgroundColor.a
266
- );
267
- } else {
268
- this.gl.clearColor(0.0, 0.0, 0.0, 1.0); // Default to black if no background color is set
269
- }
270
- // 2D rendering: disable depth testing to allow proper alpha blending with back-to-front draw order
271
- this.gl.clearDepth(1.0);
272
- this.gl.disable(this.gl.DEPTH_TEST);
273
-
274
- this.gl.enable(this.gl.BLEND);
275
- // Keep framebuffer alpha at 1.0 to avoid page compositing artifacts while blending RGB correctly
276
- // newRGB = srcRGB * srcA + dstRGB * (1 - srcA)
277
- // newA = 1 * srcA + (1 - srcA) * 1 = 1
278
- this.gl.blendFuncSeparate(
279
- this.gl.SRC_ALPHA,
280
- this.gl.ONE_MINUS_SRC_ALPHA,
281
- this.gl.ONE,
282
- this.gl.ONE_MINUS_SRC_ALPHA
283
- );
284
- this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
285
-
286
- const projectionMatrix = mat4.create();
287
- const left = -this.gl.canvas.clientWidth / 2;
288
- const right = this.gl.canvas.clientWidth / 2;
289
- const bottom = -this.gl.canvas.clientHeight / 2;
290
- const top = this.gl.canvas.clientHeight / 2;
291
- const near = -100;
292
- const far = 100;
293
- mat4.ortho(projectionMatrix, left, right, bottom, top, near, far);
294
-
295
- const globalViewMatrix = mat4.create();
296
- mat4.translate(globalViewMatrix, globalViewMatrix, [
297
- this.camera.transform.position.x,
298
- this.camera.transform.position.y,
299
- this.camera.transform.position.z,
300
- ]); // Translation
301
- mat4.rotate(globalViewMatrix, globalViewMatrix, 0, [1, 0, 0]); // X-axis rotation
302
- mat4.rotate(globalViewMatrix, globalViewMatrix, 0, [0, 1, 0]); // Y-axis rotation
303
- mat4.rotate(
304
- globalViewMatrix,
305
- globalViewMatrix,
306
- this.camera.transform.rotation,
307
- [0, 0, 1]
308
- ); // Z-axis rotation
309
- mat4.scale(globalViewMatrix, globalViewMatrix, [
310
- this.camera.transform.scale.x,
311
- this.camera.transform.scale.y,
312
- 1,
313
- ]); // Scale
314
-
315
- this.gl.useProgram(this.programInfo.program);
316
-
317
- this.gl.uniform3fv(
318
- this.programInfo.uniformLocations.uAmbientLightValues,
319
- new Float32Array([
320
- this.ambientLight.x,
321
- this.ambientLight.y,
322
- this.ambientLight.z,
323
- ])
324
- );
325
- this.gl.uniformMatrix4fv(
326
- this.programInfo.uniformLocations.projectionMatrix,
327
- false,
328
- projectionMatrix
329
- );
330
- this.gl.uniformMatrix4fv(
331
- this.programInfo.uniformLocations.globalViewMatrix,
332
- false,
333
- globalViewMatrix
334
- );
335
-
336
- this.gl.uniformMatrix4fv(
337
- this.programInfo.uniformLocations.instancedModelViewMatrix,
338
- false,
339
- globalViewMatrix
340
- );
341
-
342
- const lightPositions = [];
343
- const lightColors = [];
344
- const lightIntensities = [];
345
- const lightRadii = [];
346
-
347
- this.pointLights.forEach((light) => {
348
- // Get original world position
349
- const worldPos = new Vector2(light.position.x, light.position.y);
350
-
351
- // This transforms the world position to account for camera movement
352
- const adjustedPos = new Vector2(
353
- worldPos.x + this.camera.transform.position.x,
354
- worldPos.y + this.camera.transform.position.y
355
- );
356
-
357
- // Push the adjusted position to the array
358
- lightPositions.push(adjustedPos.x, adjustedPos.y);
359
- lightColors.push(
360
- light.color.r / 255,
361
- light.color.g / 255,
362
- light.color.b / 255
363
- );
364
- lightIntensities.push(light.intensity);
365
- lightRadii.push(light.radius);
366
- });
367
-
368
- // Get uniform locations
369
- const uLightPositionLoc = this.programInfo.uniformLocations.uLightPosition;
370
- const uLightColorLoc = this.programInfo.uniformLocations.uLightColor;
371
- const uLightIntensityLoc =
372
- this.programInfo.uniformLocations.uLightIntensity;
373
- const uLightRadiusLoc = this.programInfo.uniformLocations.uLightRadius;
374
- const uActiveLightsLoc = this.programInfo.uniformLocations.uActiveLights;
375
-
376
- // Set uniform values
377
- if (lightPositions.length > 0) {
378
- this.gl.uniform2fv(uLightPositionLoc, new Float32Array(lightPositions));
379
- this.gl.uniform3fv(uLightColorLoc, new Float32Array(lightColors));
380
- this.gl.uniform1fv(
381
- uLightIntensityLoc,
382
- new Float32Array(lightIntensities)
383
- );
384
- this.gl.uniform1fv(uLightRadiusLoc, new Float32Array(lightRadii));
385
- }
386
- this.gl.uniform1i(uActiveLightsLoc, this.pointLights.length);
387
-
388
- // Prepare directional light data
389
- const dirLightPositions = [];
390
- const dirLightDirections = [];
391
- const dirLightColors = [];
392
- const dirLightIntensities = [];
393
- const dirLightWidths = [];
394
-
395
- this.directionalLights.forEach((light) => {
396
- // Apply camera transformation to directional light position
397
- const worldPos = new Vector2(light.position.x, light.position.y);
398
- const adjustedPos = new Vector2(
399
- worldPos.x + this.camera.transform.position.x,
400
- worldPos.y + this.camera.transform.position.y
401
- );
402
-
403
- dirLightPositions.push(adjustedPos.x, adjustedPos.y);
404
- dirLightDirections.push(light.direction.x, light.direction.y);
405
- dirLightColors.push(
406
- light.color.r / 255,
407
- light.color.g / 255,
408
- light.color.b / 255
409
- );
410
- dirLightIntensities.push(light.intensity);
411
- dirLightWidths.push(light.width);
412
- });
413
-
414
- // Get directional light uniform locations
415
- const uDirLightPositionLoc =
416
- this.programInfo.uniformLocations.uDirLightPosition;
417
- const uDirLightDirectionLoc =
418
- this.programInfo.uniformLocations.uDirLightDirection;
419
- const uDirLightColorLoc = this.programInfo.uniformLocations.uDirLightColor;
420
- const uDirLightIntensityLoc =
421
- this.programInfo.uniformLocations.uDirLightIntensity;
422
- const uDirLightWidthLoc = this.programInfo.uniformLocations.uDirLightWidth;
423
- const uActiveDirLightsLoc =
424
- this.programInfo.uniformLocations.uActiveDirLights;
425
-
426
- // Set directional light uniform values
427
- if (dirLightPositions.length > 0) {
428
- this.gl.uniform2fv(
429
- uDirLightPositionLoc,
430
- new Float32Array(dirLightPositions)
431
- );
432
- this.gl.uniform2fv(
433
- uDirLightDirectionLoc,
434
- new Float32Array(dirLightDirections)
435
- );
436
- this.gl.uniform3fv(uDirLightColorLoc, new Float32Array(dirLightColors));
437
- this.gl.uniform1fv(
438
- uDirLightIntensityLoc,
439
- new Float32Array(dirLightIntensities)
440
- );
441
- this.gl.uniform1fv(uDirLightWidthLoc, new Float32Array(dirLightWidths));
442
- }
443
- this.gl.uniform1i(uActiveDirLightsLoc, this.directionalLights.length);
444
-
445
- // Draw objects back-to-front based on their z to ensure correct transparency blending
446
- const sortedObjects = [...scene.objects].sort(
447
- (a, b) => a.transform.position.z - b.transform.position.z
448
- );
449
- for (const object of sortedObjects) {
450
- object.draw(
451
- globalViewMatrix,
452
- this.programInfo.uniformLocations.globalViewMatrix,
453
- performance.now()
454
- );
455
- }
456
- }
457
- }
458
-
459
- export default Emerald;
1
+ import { STANDARD_FRAGMENT_SHADER, STANDARD_VERTEX_SHADER } from "./Shaders.js";
2
+ import { initShaderProgram } from "./GLUtils.js";
3
+ import { mat4 } from "gl-matrix";
4
+ import Color from "./Color.js";
5
+ import Transform from "./Transform.js";
6
+ import Camera from "./Camera.js";
7
+ import { Vector2, Vector3 } from "./Physics.js";
8
+ import Time from "./Time.js";
9
+ import GLManager from "./managers/GLManager.js";
10
+ import GLState from "./managers/GLState.js";
11
+ import TextureManager from "./managers/TextureManager.js";
12
+ import RenderStats from "./managers/RenderStats.js";
13
+ import CameraManager from "./managers/CameraManager.js";
14
+ import PointLight from "./lights/PointLight.js";
15
+ import DirectionalLight from "./lights/DirectionalLight.js";
16
+ import InstancedTexture from "./InstancedTexture.js";
17
+ import Tween from "./Tween.js";
18
+ import Timer from "./Timer.js";
19
+ import Coroutine from "./Coroutine.js";
20
+ import PostProcessor from "./PostProcessor.js";
21
+ import RenderTarget from "./RenderTarget.js";
22
+
23
+ /**
24
+ * @class Emerald
25
+ * @description A class that represents the Emerald engine
26
+ * @param {HTMLCanvasElement} canvas - The canvas element
27
+ */
28
+ class Emerald {
29
+ constructor(canvas) {
30
+ const gl = canvas.getContext("webgl2", {
31
+ antialias: true,
32
+ powerPreference: "high-performance",
33
+ desynchronized: false,
34
+ });
35
+ if (!gl) {
36
+ throw new Error(
37
+ "[Emerald.js] > Could not acquire a WebGL2 context. Ensure the canvas is valid and WebGL2 is supported."
38
+ );
39
+ }
40
+
41
+ this.gl = gl;
42
+ this._initProgram(gl);
43
+ GLManager.setGL(gl);
44
+ GLManager.setProgramInfo(this.programInfo);
45
+ this._bindContextGuards(canvas);
46
+ this.camera = new Camera();
47
+ this.cameras = [this.camera];
48
+
49
+ CameraManager.setCamera(this.camera);
50
+
51
+ this.ambientLight = new Vector3(1.0, 1.0, 1.0);
52
+ this.pointLights = [];
53
+ this.directionalLights = [];
54
+ this.debugLogged = false;
55
+
56
+ this.cullingEnabled = true;
57
+
58
+ /** @private */
59
+ this._drawOrder = [];
60
+ /** @private */
61
+ this._projection = mat4.create();
62
+ /** @private */
63
+ this._view = mat4.create();
64
+ /** @private */
65
+ this._identityView = mat4.create();
66
+ /** @private */
67
+ this._ambient = new Float32Array(3);
68
+ /** @private */
69
+ this._lightPositions = new Float32Array(4 * 2);
70
+ /** @private */
71
+ this._lightColors = new Float32Array(4 * 3);
72
+ /** @private */
73
+ this._lightIntensities = new Float32Array(4);
74
+ /** @private */
75
+ this._lightRadii = new Float32Array(4);
76
+ /** @private */
77
+ this._dirLightPositions = new Float32Array(4 * 2);
78
+ /** @private */
79
+ this._dirLightDirections = new Float32Array(4 * 2);
80
+ /** @private */
81
+ this._dirLightColors = new Float32Array(4 * 3);
82
+ /** @private */
83
+ this._dirLightIntensities = new Float32Array(4);
84
+ /** @private */
85
+ this._dirLightWidths = new Float32Array(4);
86
+ this.backgroundColor = {
87
+ r: 0.0,
88
+ g: 0.0,
89
+ b: 0.0,
90
+ a: 1.0,
91
+ };
92
+
93
+ this.postProcessor = null;
94
+ /** @private */
95
+ this._sceneRT = null;
96
+
97
+ this.designResolution = null;
98
+ /** @private */
99
+ this._paused = false;
100
+ }
101
+
102
+ /**
103
+ * @method _initProgram
104
+ * @description Compiles the standard shader program and caches every attrib
105
+ * and uniform location. Called from the constructor and again after a WebGL
106
+ * context loss is restored (all programs die with the context).
107
+ * @private
108
+ */
109
+ _initProgram(gl) {
110
+ const shaderProgram = initShaderProgram(
111
+ gl,
112
+ STANDARD_VERTEX_SHADER,
113
+ STANDARD_FRAGMENT_SHADER
114
+ );
115
+ this.shaderProgram = shaderProgram;
116
+ this.programInfo = {
117
+ program: shaderProgram,
118
+ attribLocations: {
119
+ vertexPosition: gl.getAttribLocation(shaderProgram, "aVertexPosition"),
120
+ aTexCoord: gl.getAttribLocation(shaderProgram, "aTextureCoord"),
121
+ instanceMatrix: gl.getAttribLocation(shaderProgram, "aInstanceMatrix0"),
122
+ instanceTexCoord0: gl.getAttribLocation(
123
+ shaderProgram,
124
+ "aInstanceTexCoord0"
125
+ ),
126
+ instanceTexCoord1: gl.getAttribLocation(
127
+ shaderProgram,
128
+ "aInstanceTexCoord1"
129
+ ),
130
+ instanceTexCoord2: gl.getAttribLocation(
131
+ shaderProgram,
132
+ "aInstanceTexCoord2"
133
+ ),
134
+ instanceTexCoord3: gl.getAttribLocation(
135
+ shaderProgram,
136
+ "aInstanceTexCoord3"
137
+ ),
138
+ instanceColor: gl.getAttribLocation(shaderProgram, "aInstanceColor"),
139
+ },
140
+ uniformLocations: {
141
+ projectionMatrix: gl.getUniformLocation(
142
+ shaderProgram,
143
+ "uProjectionMatrix"
144
+ ),
145
+ globalViewMatrix: gl.getUniformLocation(
146
+ shaderProgram,
147
+ "uModelViewMatrix"
148
+ ),
149
+ instancedModelViewMatrix: gl.getUniformLocation(
150
+ shaderProgram,
151
+ "uInstancedModelViewMatrix"
152
+ ),
153
+ uModelMatrix: gl.getUniformLocation(shaderProgram, "uModelMatrix"),
154
+ uInstancedModelMatrix: gl.getUniformLocation(
155
+ shaderProgram,
156
+ "uInstancedModelMatrix"
157
+ ),
158
+ uAmbientLightValues: gl.getUniformLocation(
159
+ shaderProgram,
160
+ "uAmbientLightValues"
161
+ ),
162
+ uSampler: gl.getUniformLocation(shaderProgram, "uSampler"),
163
+ color: gl.getUniformLocation(shaderProgram, "uColor"),
164
+ useTexture: gl.getUniformLocation(shaderProgram, "useTexture"),
165
+ useInstances: gl.getUniformLocation(shaderProgram, "useInstances"),
166
+ useText: gl.getUniformLocation(shaderProgram, "useText"),
167
+ useLighting: gl.getUniformLocation(shaderProgram, "uUseLighting"),
168
+ uOpacity: gl.getUniformLocation(shaderProgram, "uOpacity"),
169
+ uLightPosition: gl.getUniformLocation(shaderProgram, "uLightPosition"),
170
+ uLightColor: gl.getUniformLocation(shaderProgram, "uLightColor"),
171
+ uLightIntensity: gl.getUniformLocation(
172
+ shaderProgram,
173
+ "uLightIntensity"
174
+ ),
175
+ uLightRadius: gl.getUniformLocation(shaderProgram, "uLightRadius"),
176
+ uActiveLights: gl.getUniformLocation(shaderProgram, "uActiveLights"),
177
+ uDirLightPosition: gl.getUniformLocation(
178
+ shaderProgram,
179
+ "uDirLightPosition"
180
+ ),
181
+ uDirLightDirection: gl.getUniformLocation(
182
+ shaderProgram,
183
+ "uDirLightDirection"
184
+ ),
185
+ uDirLightColor: gl.getUniformLocation(shaderProgram, "uDirLightColor"),
186
+ uDirLightIntensity: gl.getUniformLocation(
187
+ shaderProgram,
188
+ "uDirLightIntensity"
189
+ ),
190
+ uDirLightWidth: gl.getUniformLocation(shaderProgram, "uDirLightWidth"),
191
+ uActiveDirLights: gl.getUniformLocation(
192
+ shaderProgram,
193
+ "uActiveDirLights"
194
+ ),
195
+ },
196
+ };
197
+ }
198
+
199
+ /**
200
+ * @method _bindContextGuards
201
+ * @description Installs webglcontextlost/restored handlers: on loss the
202
+ * default (context is gone for good) is prevented and rendering pauses; on
203
+ * restore every GPU resource is rebuilt and rendering resumes.
204
+ * @private
205
+ */
206
+ _bindContextGuards(canvas) {
207
+ /** @private */
208
+ this._contextLost = false;
209
+ /** @private */
210
+ this._contextLostHandlers = [];
211
+ /** @private */
212
+ this._contextRestoredHandlers = [];
213
+ /** @private */
214
+ this._onCtxLost = (e) => {
215
+ e.preventDefault();
216
+ this._contextLost = true;
217
+ console.warn(
218
+ "[Emerald] > WebGL context lost — rendering paused until restore."
219
+ );
220
+ for (const h of this._contextLostHandlers) h();
221
+ };
222
+ /** @private */
223
+ this._onCtxRestored = () => {
224
+ try {
225
+ this._restoreContext();
226
+ console.info("[Emerald] > WebGL context restored.");
227
+ } catch (err) {
228
+ console.error("[Emerald] > Context restore failed:", err);
229
+ }
230
+ };
231
+ canvas.addEventListener("webglcontextlost", this._onCtxLost, false);
232
+ canvas.addEventListener("webglcontextrestored", this._onCtxRestored, false);
233
+ }
234
+
235
+ /**
236
+ * @method getRenderStats
237
+ * @description Render counters for the most recent completed frame:
238
+ * { drawCalls, quads, textureBinds }. DebugOverlay shows these automatically.
239
+ * @returns {{drawCalls:number, quads:number, textureBinds:number}}
240
+ */
241
+ getRenderStats() {
242
+ return RenderStats.frame;
243
+ }
244
+
245
+ /**
246
+ * @method onContextLost
247
+ * @description Registers a callback fired when the WebGL context is lost
248
+ * (e.g. to show a "please wait" overlay).
249
+ * @param {Function} cb
250
+ * @returns {Emerald} - this
251
+ */
252
+ onContextLost(cb) {
253
+ if (typeof cb === "function") this._contextLostHandlers.push(cb);
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * @method onContextRestored
259
+ * @description Registers a callback fired after the context and all GPU
260
+ * resources have been rebuilt.
261
+ * @param {Function} cb
262
+ * @returns {Emerald} - this
263
+ */
264
+ onContextRestored(cb) {
265
+ if (typeof cb === "function") this._contextRestoredHandlers.push(cb);
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * @method _restoreContext
271
+ * @description Rebuilds everything the GPU forgot: the standard program,
272
+ * cached textures (from the surviving image cache), every registered
273
+ * drawable's buffers, custom Materials, post-processing programs, and the
274
+ * scene render target. Runs on the webglcontextrestored event.
275
+ * @private
276
+ */
277
+ _restoreContext() {
278
+ const gl = this.gl;
279
+ this._initProgram(gl);
280
+ GLManager.setGL(gl);
281
+ GLManager.setProgramInfo(this.programInfo);
282
+ GLState.reset();
283
+ TextureManager.restoreAll();
284
+ GLManager.restoreAll();
285
+ if (this._sceneRT) {
286
+ const canvas = gl.canvas;
287
+ this._sceneRT = new RenderTarget(
288
+ Math.max(2, canvas.width),
289
+ Math.max(2, canvas.height)
290
+ );
291
+ }
292
+ this._contextLost = false;
293
+ for (const h of this._contextRestoredHandlers) h();
294
+ }
295
+
296
+ /**
297
+ * @method setDesignResolution
298
+ * @description Makes the world render at a fixed design resolution that is
299
+ * scaled to fit any screen, so the game looks consistent across devices and
300
+ * aspect ratios. Modes:
301
+ * - "fit" letterbox: preserve aspect, whole design visible, bars on the sides
302
+ * - "fill" cover: preserve aspect, fill the screen, crop the overflow
303
+ * - "stretch" ignore aspect, stretch the design to the screen
304
+ * - "pixel" like "fit" but snapped to an integer scale for crisp pixel art
305
+ * Call this once (and again on orientation change if desired); it composes with
306
+ * `resize`. Use `screenToWorld` for correct picking under any mode.
307
+ * @param {number} width - Design width in world units
308
+ * @param {number} height - Design height in world units
309
+ * @param {string} [mode="fit"] - "fit" | "fill" | "stretch" | "pixel"
310
+ */
311
+ setDesignResolution(width, height, mode = "fit") {
312
+ this.designResolution = { width, height, mode };
313
+ }
314
+
315
+ /**
316
+ * @method clearDesignResolution
317
+ * @description Disables design-resolution scaling; the world reverts to using
318
+ * the canvas's CSS pixel size as world units.
319
+ */
320
+ clearDesignResolution() {
321
+ this.designResolution = null;
322
+ }
323
+
324
+ /**
325
+ * @method _viewportFor
326
+ * @description Computes the device-pixel GL viewport rect and the world-space
327
+ * extents (`worldW` x `worldH`) for a camera, honoring the active design
328
+ * resolution + fit mode. Without a design resolution it returns the camera's
329
+ * screen sub-rect at the canvas CSS size (legacy behavior).
330
+ * @private
331
+ */
332
+ _viewportFor(camera) {
333
+ const canvas = this.gl.canvas;
334
+ const vp = camera.viewport;
335
+ const rx = vp.x * canvas.width;
336
+ const ry = vp.y * canvas.height;
337
+ const rw = vp.width * canvas.width;
338
+ const rh = vp.height * canvas.height;
339
+
340
+ const design = this.designResolution;
341
+ if (!design) {
342
+ return {
343
+ px: Math.floor(rx),
344
+ py: Math.floor(ry),
345
+ pw: Math.floor(rw),
346
+ ph: Math.floor(rh),
347
+ worldW: canvas.clientWidth * vp.width,
348
+ worldH: canvas.clientHeight * vp.height,
349
+ };
350
+ }
351
+
352
+ const dw = design.width;
353
+ const dh = design.height;
354
+ const mode = design.mode;
355
+
356
+ if (mode === "stretch") {
357
+ return {
358
+ px: Math.floor(rx),
359
+ py: Math.floor(ry),
360
+ pw: Math.floor(rw),
361
+ ph: Math.floor(rh),
362
+ worldW: dw,
363
+ worldH: dh,
364
+ };
365
+ }
366
+ if (mode === "fill") {
367
+ const scale = Math.max(rw / dw, rh / dh);
368
+ return {
369
+ px: Math.floor(rx),
370
+ py: Math.floor(ry),
371
+ pw: Math.floor(rw),
372
+ ph: Math.floor(rh),
373
+ worldW: rw / scale,
374
+ worldH: rh / scale,
375
+ };
376
+ }
377
+
378
+ let scale = Math.min(rw / dw, rh / dh);
379
+ if (mode === "pixel") scale = Math.max(1, Math.floor(scale));
380
+ const vw = dw * scale;
381
+ const vh = dh * scale;
382
+ return {
383
+ px: Math.floor(rx + (rw - vw) / 2),
384
+ py: Math.floor(ry + (rh - vh) / 2),
385
+ pw: Math.floor(vw),
386
+ ph: Math.floor(vh),
387
+ worldW: dw,
388
+ worldH: dh,
389
+ };
390
+ }
391
+
392
+ /**
393
+ * @method _clientToView
394
+ * @description Maps a client/CSS pixel coordinate to pre-camera view space
395
+ * (origin center, +Y up), honoring DPR, design resolution and fit mode
396
+ * (including letterbox offset). This is also the screen-space coordinate used
397
+ * for `screenSpace` objects. Camera transform is applied separately by
398
+ * `screenToWorld`.
399
+ * @private
400
+ * @returns {{x:number, y:number}}
401
+ */
402
+ _clientToView(clientX, clientY, camera = this.camera) {
403
+ const canvas = this.gl.canvas;
404
+ const rect =
405
+ typeof canvas.getBoundingClientRect === "function"
406
+ ? canvas.getBoundingClientRect()
407
+ : {
408
+ left: 0,
409
+ top: 0,
410
+ width: canvas.clientWidth,
411
+ height: canvas.clientHeight,
412
+ };
413
+
414
+ const sx = (clientX - rect.left) * (canvas.width / (rect.width || 1));
415
+ const sy = (clientY - rect.top) * (canvas.height / (rect.height || 1));
416
+ const gy = canvas.height - sy;
417
+
418
+ const { px, py, pw, ph, worldW, worldH } = this._viewportFor(camera);
419
+ const nx = (sx - px) / (pw || 1);
420
+ const ny = (gy - py) / (ph || 1);
421
+
422
+ return { x: (nx - 0.5) * worldW, y: (ny - 0.5) * worldH };
423
+ }
424
+
425
+ /**
426
+ * @method screenToWorld
427
+ * @description Converts a client/CSS pixel coordinate (e.g. `event.clientX/Y`)
428
+ * to world space for a camera, accounting for device pixel ratio, the active
429
+ * design resolution + fit mode (including letterbox offset), and the camera's
430
+ * position / zoom / rotation. Use this for mouse picking instead of ad-hoc math.
431
+ * @param {number} clientX - Client X (relative to the viewport)
432
+ * @param {number} clientY - Client Y (relative to the viewport)
433
+ * @param {Camera} [camera] - The camera to unproject through (default primary)
434
+ * @returns {Vector2} - World-space position
435
+ */
436
+ screenToWorld(clientX, clientY, camera = this.camera) {
437
+ const { x: vx, y: vy } = this._clientToView(clientX, clientY, camera);
438
+
439
+ const zoomX = camera.transform.scale.x || 1;
440
+ const zoomY = camera.transform.scale.y || 1;
441
+ const ux = vx / zoomX;
442
+ const uy = vy / zoomY;
443
+
444
+ const rot = -camera.transform.rotation;
445
+ const c = Math.cos(rot);
446
+ const s = Math.sin(rot);
447
+ const wx = ux * c - uy * s - camera.transform.position.x;
448
+ const wy = ux * s + uy * c - camera.transform.position.y;
449
+ return new Vector2(wx, wy);
450
+ }
451
+
452
+ /**
453
+ * @method enablePostProcessing
454
+ * @description Turns on the post-processing pipeline (lazily creating the
455
+ * scene render target and processor). Add effects with addPostEffect.
456
+ * @returns {PostProcessor} - The processor (to add/remove effects)
457
+ */
458
+ enablePostProcessing() {
459
+ if (!this.postProcessor) {
460
+ const canvas = this.gl.canvas;
461
+ this._sceneRT = new RenderTarget(canvas.width, canvas.height);
462
+ this.postProcessor = new PostProcessor();
463
+ }
464
+ this.postProcessor.enabled = true;
465
+ return this.postProcessor;
466
+ }
467
+
468
+ /**
469
+ * @method disablePostProcessing
470
+ * @description Disables the pipeline (scene renders straight to the canvas).
471
+ */
472
+ disablePostProcessing() {
473
+ if (this.postProcessor) this.postProcessor.enabled = false;
474
+ }
475
+
476
+ /**
477
+ * @method addPostEffect
478
+ * @description Appends a post-processing effect (enabling the pipeline if
479
+ * needed). See PostEffects for the built-ins.
480
+ * @param {PostEffect} effect
481
+ * @returns {Emerald} - this
482
+ */
483
+ addPostEffect(effect) {
484
+ this.enablePostProcessing();
485
+ this.postProcessor.addEffect(effect);
486
+ return this;
487
+ }
488
+
489
+ /**
490
+ * @method removePostEffect
491
+ * @description Removes a previously added post-processing effect.
492
+ * @param {PostEffect} effect
493
+ * @returns {Emerald} - this
494
+ */
495
+ removePostEffect(effect) {
496
+ if (this.postProcessor) this.postProcessor.removeEffect(effect);
497
+ return this;
498
+ }
499
+
500
+ /**
501
+ * @method setAmbientLight
502
+ * @description Sets the ambient light for the scene
503
+ * @param {Vector3} vector3 - The ambient light color as a Vector3 instance
504
+ */
505
+ setAmbientLight(vector3) {
506
+ if (
507
+ vector3 &&
508
+ typeof vector3.x === "number" &&
509
+ typeof vector3.y === "number" &&
510
+ typeof vector3.z === "number"
511
+ ) {
512
+ this.ambientLight = vector3;
513
+ } else {
514
+ console.error(
515
+ "[Emerald.js] > vector3 is not an instance of Vector3 class."
516
+ );
517
+ }
518
+ }
519
+
520
+ /**
521
+ * @method resize
522
+ * @description Resizes the canvas
523
+ * @param {number} width - The width of the canvas
524
+ * @param {number} height - The height of the canvas
525
+ * @param {number} [dpr] - Device pixel ratio override; defaults to
526
+ * `window.devicePixelRatio` (or 1 outside a browser)
527
+ */
528
+ resize(width, height, dpr) {
529
+ const ratio =
530
+ dpr || (typeof window !== "undefined" && window.devicePixelRatio) || 1;
531
+ const canvas = this.gl.canvas;
532
+
533
+ if (canvas.style) {
534
+ canvas.style.width = width + "px";
535
+ canvas.style.height = height + "px";
536
+ }
537
+ canvas.width = Math.round(width * ratio);
538
+ canvas.height = Math.round(height * ratio);
539
+
540
+ this.gl.viewport(0, 0, canvas.width, canvas.height);
541
+ }
542
+
543
+ /**
544
+ * @method addPointLight
545
+ * @description Adds a point light to the scene
546
+ * @param {PointLight} pointLight - The point light to add
547
+ */
548
+ addPointLight(pointLight) {
549
+ if (!(pointLight instanceof PointLight)) {
550
+ console.error(
551
+ "[Emerald.js] > Passed argument is not an instance of PointLight class."
552
+ );
553
+ return;
554
+ }
555
+ if (this.pointLights.length < 4) {
556
+ this.pointLights.push(pointLight);
557
+ } else {
558
+ console.warn("Maximum number of point lights reached (4)");
559
+ }
560
+ }
561
+
562
+ /**
563
+ * @method addDirectionalLight
564
+ * @description Adds a directional light to the scene
565
+ * @param {DirectionalLight} directionalLight - The directional light to add
566
+ */
567
+ addDirectionalLight(directionalLight) {
568
+ if (!(directionalLight instanceof DirectionalLight)) {
569
+ console.error(
570
+ "[Emerald.js] > Passed argument is not an instance of DirectionalLight class."
571
+ );
572
+ return;
573
+ }
574
+ if (this.directionalLights.length < 4) {
575
+ this.directionalLights.push(directionalLight);
576
+ } else {
577
+ console.warn("Maximum number of directional lights reached (4)");
578
+ }
579
+ }
580
+
581
+ /**
582
+ * @method removeDirectionalLight
583
+ * @description Removes a directional light from the scene
584
+ * @param {DirectionalLight} directionalLight - The directional light to remove
585
+ */
586
+ removeDirectionalLight(directionalLight) {
587
+ const index = this.directionalLights.indexOf(directionalLight);
588
+ if (index > -1) {
589
+ this.directionalLights.splice(index, 1);
590
+ }
591
+ }
592
+
593
+ /**
594
+ * @method setBackgroundColor
595
+ * @description Sets the background color of the scene
596
+ * @param {Color} color - The background color
597
+ */
598
+ setBackgroundColor(color) {
599
+ if (color instanceof Color) {
600
+ this.backgroundColor = {
601
+ r: color.r / 255,
602
+ g: color.g / 255,
603
+ b: color.b / 255,
604
+ a: color.a / 255,
605
+ };
606
+ } else {
607
+ console.error("[Emerald.js] > color is not an instance of Color class.");
608
+ }
609
+ }
610
+
611
+ /**
612
+ * @method run
613
+ * @description Starts an engine-owned game loop on requestAnimationFrame. Each
614
+ * frame it computes a delta time (clamped so a tab-switch stall can't produce
615
+ * a huge jump), optionally advances a fixed-timestep simulation, and calls
616
+ * your `update(dt, alpha)`. `alpha` is the 0..1 interpolation factor between
617
+ * the last and next fixed step (1 when no fixed step is configured), so you
618
+ * can render smoothly between simulation ticks.
619
+ *
620
+ * @param {Function} update - `(dt, alpha) => void`, called once per frame
621
+ * @param {Object} [options]
622
+ * @param {number} [options.maxDelta=0.25] - Max dt (seconds) per frame
623
+ * @param {number} [options.fixedStep=0] - Fixed simulation step in seconds
624
+ * (e.g. 1/60). 0 disables the fixed loop.
625
+ * @param {Function} [options.fixedUpdate] - `(step) => void`, called 0..N
626
+ * times per frame to advance the fixed-timestep simulation
627
+ * @param {number} [options.maxSubSteps=5] - Cap on fixed steps per frame
628
+ * @param {boolean} [options.pauseOnBlur=true] - Auto-pause when the tab is
629
+ * hidden/minimized (and on window blur if `pauseOnWindowBlur` is set)
630
+ * @param {boolean} [options.pauseOnWindowBlur=false] - Also pause on window blur
631
+ * @param {Function} [options.onPause] - Called when the loop pauses
632
+ * @param {Function} [options.onResume] - Called when the loop resumes
633
+ * @returns {Function} - A stop function (same as calling `stop()`)
634
+ */
635
+ run(update, options = {}) {
636
+ const raf =
637
+ typeof globalThis !== "undefined" && globalThis.requestAnimationFrame
638
+ ? globalThis.requestAnimationFrame.bind(globalThis)
639
+ : null;
640
+ if (!raf) {
641
+ throw new Error(
642
+ "[Emerald.js] > run() requires requestAnimationFrame (browser environment)."
643
+ );
644
+ }
645
+
646
+ const maxDelta = options.maxDelta ?? 0.25;
647
+ const fixedStep = options.fixedStep ?? 0;
648
+ const fixedUpdate = options.fixedUpdate || null;
649
+ const maxSubSteps = options.maxSubSteps ?? 5;
650
+
651
+ /** @private */
652
+ this._running = true;
653
+ this._paused = false;
654
+ /** @private */
655
+ this._loopLastTime = 0;
656
+ /** @private */
657
+ this._fixedAccumulator = 0;
658
+ /** @private */
659
+ this._onPause = options.onPause || null;
660
+ /** @private */
661
+ this._onResume = options.onResume || null;
662
+ this._bindLifecycle(options);
663
+
664
+ const frame = (now) => {
665
+ if (!this._running) return;
666
+ if (this._paused || this._contextLost) {
667
+ this._loopLastTime = now;
668
+ /** @private */
669
+ this._rafId = raf(frame);
670
+ return;
671
+ }
672
+ if (this._loopLastTime === 0) this._loopLastTime = now;
673
+ let dt = (now - this._loopLastTime) / 1000;
674
+ this._loopLastTime = now;
675
+ if (!isFinite(dt) || dt < 0) dt = 0;
676
+ if (dt > maxDelta) dt = maxDelta;
677
+
678
+ let alpha = 1;
679
+ if (fixedStep > 0 && fixedUpdate) {
680
+ this._fixedAccumulator += dt;
681
+ let steps = 0;
682
+ while (this._fixedAccumulator >= fixedStep && steps < maxSubSteps) {
683
+ fixedUpdate(fixedStep);
684
+ this._fixedAccumulator -= fixedStep;
685
+ steps++;
686
+ }
687
+ if (steps === maxSubSteps) this._fixedAccumulator = 0;
688
+ alpha = this._fixedAccumulator / fixedStep;
689
+ }
690
+
691
+ update(dt, alpha);
692
+ this._rafId = raf(frame);
693
+ };
694
+
695
+ this._rafId = raf(frame);
696
+ return () => this.stop();
697
+ }
698
+
699
+ /**
700
+ * @method stop
701
+ * @description Stops a loop previously started with run().
702
+ */
703
+ stop() {
704
+ this._running = false;
705
+ this._unbindLifecycle();
706
+ if (
707
+ this._rafId != null &&
708
+ typeof globalThis !== "undefined" &&
709
+ globalThis.cancelAnimationFrame
710
+ ) {
711
+ globalThis.cancelAnimationFrame(this._rafId);
712
+ }
713
+ this._rafId = null;
714
+ }
715
+
716
+ /**
717
+ * @method pause
718
+ * @description Pauses the loop: `update`/`fixedUpdate` stop being called while
719
+ * the RAF keeps spinning. Time doesn't accumulate, so resuming produces no dt
720
+ * spike. Fires the `onPause` callback once.
721
+ */
722
+ pause() {
723
+ if (this._paused) return;
724
+ this._paused = true;
725
+ if (this._onPause) this._onPause();
726
+ }
727
+
728
+ /**
729
+ * @method resume
730
+ * @description Resumes a paused loop and resyncs the clock so the first frame
731
+ * after resuming has ~0 dt. Fires the `onResume` callback once.
732
+ */
733
+ resume() {
734
+ if (!this._paused) return;
735
+ this._paused = false;
736
+ this._loopLastTime = 0;
737
+ if (this._onResume) this._onResume();
738
+ }
739
+
740
+ /**
741
+ * @method _bindLifecycle
742
+ * @description Wires visibility/blur listeners that auto-pause the loop when
743
+ * the page is backgrounded, per the `run` options.
744
+ * @private
745
+ */
746
+ _bindLifecycle(options) {
747
+ this._unbindLifecycle();
748
+ const pauseOnBlur = options.pauseOnBlur ?? true;
749
+ if (!pauseOnBlur) return;
750
+
751
+ if (typeof document !== "undefined" && document.addEventListener) {
752
+ /** @private */
753
+ this._onVisibility = () => {
754
+ if (document.hidden) this.pause();
755
+ else this.resume();
756
+ };
757
+ document.addEventListener("visibilitychange", this._onVisibility);
758
+ }
759
+ if (
760
+ options.pauseOnWindowBlur &&
761
+ typeof window !== "undefined" &&
762
+ window.addEventListener
763
+ ) {
764
+ /** @private */
765
+ this._onWinBlur = () => this.pause();
766
+ /** @private */
767
+ this._onWinFocus = () => this.resume();
768
+ window.addEventListener("blur", this._onWinBlur);
769
+ window.addEventListener("focus", this._onWinFocus);
770
+ }
771
+ }
772
+
773
+ /**
774
+ * @method _unbindLifecycle
775
+ * @description Removes any listeners registered by `_bindLifecycle`.
776
+ * @private
777
+ */
778
+ _unbindLifecycle() {
779
+ if (this._onVisibility && typeof document !== "undefined") {
780
+ document.removeEventListener("visibilitychange", this._onVisibility);
781
+ this._onVisibility = null;
782
+ }
783
+ if (this._onWinBlur && typeof window !== "undefined") {
784
+ window.removeEventListener("blur", this._onWinBlur);
785
+ window.removeEventListener("focus", this._onWinFocus);
786
+ this._onWinBlur = null;
787
+ this._onWinFocus = null;
788
+ }
789
+ }
790
+
791
+ /**
792
+ * @method drawScene
793
+ * @description Draws the scene
794
+ * @param {Scene} scene - The scene to draw
795
+ * @param {number} deltaTime - The delta time
796
+ */
797
+ drawScene(scene, deltaTime) {
798
+ if (this._contextLost) return;
799
+ RenderStats.beginFrame();
800
+ Time.setDeltaTime(deltaTime);
801
+
802
+ const scaledDelta = Time.getDeltaTime();
803
+ Tween.update(scaledDelta);
804
+ Timer.update(scaledDelta);
805
+ Coroutine.update(scaledDelta);
806
+
807
+ for (let i = 0; i < scene.objects.length; i++) {
808
+ const object = scene.objects[i];
809
+ if (object && typeof object.syncPhysics === "function") {
810
+ object.syncPhysics();
811
+ }
812
+ }
813
+
814
+ const gl = this.gl;
815
+ const canvas = gl.canvas;
816
+
817
+ const usePost = this.postProcessor && this.postProcessor.hasEffects();
818
+ if (usePost) {
819
+ this._sceneRT.resize(canvas.width, canvas.height);
820
+ this._sceneRT.bind();
821
+ }
822
+
823
+ gl.clearDepth(1.0);
824
+ gl.disable(gl.DEPTH_TEST);
825
+ gl.enable(gl.BLEND);
826
+ gl.blendFuncSeparate(
827
+ gl.SRC_ALPHA,
828
+ gl.ONE_MINUS_SRC_ALPHA,
829
+ gl.ONE,
830
+ gl.ONE_MINUS_SRC_ALPHA
831
+ );
832
+
833
+ const bg = this.backgroundColor || { r: 0, g: 0, b: 0, a: 1 };
834
+ gl.disable(gl.SCISSOR_TEST);
835
+ gl.viewport(0, 0, canvas.width, canvas.height);
836
+ gl.clearColor(bg.r, bg.g, bg.b, bg.a);
837
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
838
+
839
+ gl.useProgram(this.programInfo.program);
840
+ GLState.reset();
841
+
842
+ const order = this._drawOrder;
843
+ order.length = scene.objects.length;
844
+ for (let i = 0; i < scene.objects.length; i++) {
845
+ order[i] = scene.objects[i];
846
+ }
847
+ order.sort(
848
+ (a, b) =>
849
+ (a.layer || 0) - (b.layer || 0) ||
850
+ a.transform.position.z - b.transform.position.z
851
+ );
852
+
853
+ const now = performance.now();
854
+
855
+ const postExcluded = [];
856
+ for (const camera of this.cameras) {
857
+ if (!camera || !camera.active) continue;
858
+ if (usePost && camera.excludeFromPost) {
859
+ postExcluded.push(camera);
860
+ continue;
861
+ }
862
+ this._renderCamera(camera, order, now);
863
+ }
864
+
865
+ gl.disable(gl.SCISSOR_TEST);
866
+ gl.viewport(0, 0, canvas.width, canvas.height);
867
+
868
+ if (usePost) {
869
+ this._sceneRT.unbind();
870
+ this.postProcessor.process(
871
+ this._sceneRT.texture,
872
+ canvas.width,
873
+ canvas.height,
874
+ now / 1000
875
+ );
876
+
877
+ if (postExcluded.length) {
878
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
879
+ gl.viewport(0, 0, canvas.width, canvas.height);
880
+ gl.disable(gl.DEPTH_TEST);
881
+ gl.enable(gl.BLEND);
882
+ gl.blendFuncSeparate(
883
+ gl.SRC_ALPHA,
884
+ gl.ONE_MINUS_SRC_ALPHA,
885
+ gl.ONE,
886
+ gl.ONE_MINUS_SRC_ALPHA
887
+ );
888
+ gl.useProgram(this.programInfo.program);
889
+ GLState.reset();
890
+ for (const camera of postExcluded) {
891
+ this._renderCamera(camera, order, now);
892
+ }
893
+ gl.disable(gl.SCISSOR_TEST);
894
+ gl.viewport(0, 0, canvas.width, canvas.height);
895
+ }
896
+ }
897
+ }
898
+
899
+ /**
900
+ * @method _renderCamera
901
+ * @description Renders the sorted draw order through a single camera into its
902
+ * viewport rectangle.
903
+ * @private
904
+ */
905
+ _renderCamera(camera, order, now) {
906
+ const gl = this.gl;
907
+ const canvas = gl.canvas;
908
+
909
+ const { px, py, pw, ph, worldW, worldH } = this._viewportFor(camera);
910
+ gl.viewport(px, py, pw, ph);
911
+ gl.enable(gl.SCISSOR_TEST);
912
+ gl.scissor(px, py, pw, ph);
913
+
914
+ if (camera.clearColor) {
915
+ const c = camera.clearColor;
916
+ gl.clearColor(c.r / 255, c.g / 255, c.b / 255, c.a / 255);
917
+ gl.clear(gl.COLOR_BUFFER_BIT);
918
+ }
919
+
920
+ const cssW = worldW;
921
+ const cssH = worldH;
922
+ const projectionMatrix = this._projection;
923
+ mat4.ortho(
924
+ projectionMatrix,
925
+ -cssW / 2,
926
+ cssW / 2,
927
+ -cssH / 2,
928
+ cssH / 2,
929
+ -100,
930
+ 100
931
+ );
932
+ GLManager.setProjection(projectionMatrix);
933
+
934
+ const view = this._view;
935
+ mat4.identity(view);
936
+ mat4.scale(view, view, [
937
+ camera.transform.scale.x,
938
+ camera.transform.scale.y,
939
+ 1,
940
+ ]);
941
+ mat4.rotate(view, view, camera.transform.rotation, [0, 0, 1]);
942
+ let camTX = camera.transform.position.x;
943
+ let camTY = camera.transform.position.y;
944
+ if (camera.pixelSnap && camera.transform.rotation === 0) {
945
+ const zx = camera.transform.scale.x || 1;
946
+ const zy = camera.transform.scale.y || 1;
947
+ camTX = Math.round(camTX * zx) / zx;
948
+ camTY = Math.round(camTY * zy) / zy;
949
+ }
950
+ mat4.translate(view, view, [camTX, camTY, camera.transform.position.z]);
951
+
952
+ this._ambient[0] = this.ambientLight.x;
953
+ this._ambient[1] = this.ambientLight.y;
954
+ this._ambient[2] = this.ambientLight.z;
955
+ gl.uniform3fv(
956
+ this.programInfo.uniformLocations.uAmbientLightValues,
957
+ this._ambient
958
+ );
959
+ gl.uniformMatrix4fv(
960
+ this.programInfo.uniformLocations.projectionMatrix,
961
+ false,
962
+ projectionMatrix
963
+ );
964
+ gl.uniformMatrix4fv(
965
+ this.programInfo.uniformLocations.globalViewMatrix,
966
+ false,
967
+ view
968
+ );
969
+ gl.uniformMatrix4fv(
970
+ this.programInfo.uniformLocations.instancedModelViewMatrix,
971
+ false,
972
+ view
973
+ );
974
+
975
+ const camX = camera.transform.position.x;
976
+ const camY = camera.transform.position.y;
977
+ const zoom = camera.transform.scale.x || 1;
978
+
979
+ const pointCount = Math.min(this.pointLights.length, 4);
980
+ for (let i = 0; i < pointCount; i++) {
981
+ const light = this.pointLights[i];
982
+ this._lightPositions[i * 2] = (light.position.x + camX) * zoom;
983
+ this._lightPositions[i * 2 + 1] = (light.position.y + camY) * zoom;
984
+ this._lightColors[i * 3] = light.color.r / 255;
985
+ this._lightColors[i * 3 + 1] = light.color.g / 255;
986
+ this._lightColors[i * 3 + 2] = light.color.b / 255;
987
+ this._lightIntensities[i] = light.intensity;
988
+ this._lightRadii[i] = light.radius * zoom;
989
+ }
990
+ if (pointCount > 0) {
991
+ gl.uniform2fv(
992
+ this.programInfo.uniformLocations.uLightPosition,
993
+ this._lightPositions
994
+ );
995
+ gl.uniform3fv(
996
+ this.programInfo.uniformLocations.uLightColor,
997
+ this._lightColors
998
+ );
999
+ gl.uniform1fv(
1000
+ this.programInfo.uniformLocations.uLightIntensity,
1001
+ this._lightIntensities
1002
+ );
1003
+ gl.uniform1fv(
1004
+ this.programInfo.uniformLocations.uLightRadius,
1005
+ this._lightRadii
1006
+ );
1007
+ }
1008
+ gl.uniform1i(this.programInfo.uniformLocations.uActiveLights, pointCount);
1009
+
1010
+ const dirCount = Math.min(this.directionalLights.length, 4);
1011
+ for (let i = 0; i < dirCount; i++) {
1012
+ const light = this.directionalLights[i];
1013
+ this._dirLightPositions[i * 2] = (light.position.x + camX) * zoom;
1014
+ this._dirLightPositions[i * 2 + 1] = (light.position.y + camY) * zoom;
1015
+ this._dirLightDirections[i * 2] = light.direction.x;
1016
+ this._dirLightDirections[i * 2 + 1] = light.direction.y;
1017
+ this._dirLightColors[i * 3] = light.color.r / 255;
1018
+ this._dirLightColors[i * 3 + 1] = light.color.g / 255;
1019
+ this._dirLightColors[i * 3 + 2] = light.color.b / 255;
1020
+ this._dirLightIntensities[i] = light.intensity;
1021
+ this._dirLightWidths[i] = light.width * zoom;
1022
+ }
1023
+ if (dirCount > 0) {
1024
+ gl.uniform2fv(
1025
+ this.programInfo.uniformLocations.uDirLightPosition,
1026
+ this._dirLightPositions
1027
+ );
1028
+ gl.uniform2fv(
1029
+ this.programInfo.uniformLocations.uDirLightDirection,
1030
+ this._dirLightDirections
1031
+ );
1032
+ gl.uniform3fv(
1033
+ this.programInfo.uniformLocations.uDirLightColor,
1034
+ this._dirLightColors
1035
+ );
1036
+ gl.uniform1fv(
1037
+ this.programInfo.uniformLocations.uDirLightIntensity,
1038
+ this._dirLightIntensities
1039
+ );
1040
+ gl.uniform1fv(
1041
+ this.programInfo.uniformLocations.uDirLightWidth,
1042
+ this._dirLightWidths
1043
+ );
1044
+ }
1045
+ gl.uniform1i(this.programInfo.uniformLocations.uActiveDirLights, dirCount);
1046
+
1047
+ const halfW = cssW / 2 / zoom;
1048
+ const halfH = cssH / 2 / zoom;
1049
+ const viewLeft = -camX - halfW;
1050
+ const viewRight = -camX + halfW;
1051
+ const viewBottom = -camY - halfH;
1052
+ const viewTop = -camY + halfH;
1053
+
1054
+ const ignoreLayers = camera.ignoreLayers;
1055
+ const onlyLayers = camera.onlyLayers;
1056
+ for (const object of order) {
1057
+ const objLayer = object.layer || 0;
1058
+ if (onlyLayers && !onlyLayers.has(objLayer)) {
1059
+ continue;
1060
+ }
1061
+ if (ignoreLayers && ignoreLayers.size && ignoreLayers.has(objLayer)) {
1062
+ continue;
1063
+ }
1064
+ if (
1065
+ this.cullingEnabled &&
1066
+ !object.screenSpace &&
1067
+ !this._isVisible(object, viewLeft, viewRight, viewBottom, viewTop)
1068
+ ) {
1069
+ continue;
1070
+ }
1071
+ const objView = object.screenSpace ? this._identityView : view;
1072
+ object.draw(
1073
+ objView,
1074
+ this.programInfo.uniformLocations.globalViewMatrix,
1075
+ now
1076
+ );
1077
+ }
1078
+ }
1079
+
1080
+ /**
1081
+ * @method addCamera
1082
+ * @description Adds a camera to the render list (for split-screen, etc.).
1083
+ * @param {Camera} camera - The camera to add
1084
+ */
1085
+ addCamera(camera) {
1086
+ if (this.cameras.indexOf(camera) === -1) this.cameras.push(camera);
1087
+ }
1088
+
1089
+ /**
1090
+ * @method removeCamera
1091
+ * @description Removes a camera from the render list.
1092
+ * @param {Camera} camera - The camera to remove
1093
+ */
1094
+ removeCamera(camera) {
1095
+ const idx = this.cameras.indexOf(camera);
1096
+ if (idx !== -1) this.cameras.splice(idx, 1);
1097
+ }
1098
+
1099
+ /**
1100
+ * @method setCameras
1101
+ * @description Replaces the camera list. The first camera becomes the primary
1102
+ * `this.camera`.
1103
+ * @param {Camera[]} cameras - The cameras to render
1104
+ */
1105
+ setCameras(cameras) {
1106
+ this.cameras = cameras.slice();
1107
+ if (this.cameras.length > 0) {
1108
+ this.camera = this.cameras[0];
1109
+ CameraManager.setCamera(this.camera);
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * @method setCullingEnabled
1115
+ * @description Enables or disables off-screen object culling.
1116
+ * @param {boolean} enabled - Whether culling is enabled
1117
+ */
1118
+ setCullingEnabled(enabled) {
1119
+ this.cullingEnabled = enabled;
1120
+ }
1121
+
1122
+ /**
1123
+ * @method _isVisible
1124
+ * @description Conservative AABB visibility test against the view rectangle.
1125
+ * Instanced objects (whose instances spread beyond the owner transform) and
1126
+ * objects flagged `alwaysVisible` are never culled.
1127
+ * @private
1128
+ */
1129
+ _isVisible(object, left, right, bottom, top) {
1130
+ if (!object || !object.transform) return true;
1131
+ if (object.alwaysVisible) return true;
1132
+ if (object.transform.parent) return true;
1133
+ if (object.getComponent && object.getComponent(InstancedTexture)) {
1134
+ return true;
1135
+ }
1136
+
1137
+ const pos = object.transform.position;
1138
+ const scale = object.transform.scale;
1139
+ const half = Math.max(Math.abs(scale.x), Math.abs(scale.y)) * 1.5;
1140
+
1141
+ return !(
1142
+ pos.x + half < left ||
1143
+ pos.x - half > right ||
1144
+ pos.y + half < bottom ||
1145
+ pos.y - half > top
1146
+ );
1147
+ }
1148
+ }
1149
+
1150
+ export default Emerald;