emeraldengine 3.0.0 → 3.1.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 (103) hide show
  1. package/README.md +1498 -1659
  2. package/dist/types/index.d.ts +4 -1
  3. package/dist/types/src/Animator.d.ts +1 -1
  4. package/dist/types/src/CollisionLayers.d.ts +2 -2
  5. package/dist/types/src/Color.d.ts +1 -0
  6. package/dist/types/src/Drawable.d.ts +1 -1
  7. package/dist/types/src/EmeraldDB.d.ts +2 -2
  8. package/dist/types/src/InstancedTexture.d.ts +17 -2
  9. package/dist/types/src/Material.d.ts +2 -2
  10. package/dist/types/src/MathUtils.d.ts +2 -1
  11. package/dist/types/src/ParticleEmitter.d.ts +1 -1
  12. package/dist/types/src/Physics.d.ts +148 -18
  13. package/dist/types/src/Scene.d.ts +1 -1
  14. package/dist/types/src/Shaders.d.ts +2 -2
  15. package/dist/types/src/Tilemap.d.ts +1 -1
  16. package/dist/types/src/UI.d.ts +1 -1
  17. package/dist/types/src/components/Behaviour.d.ts +2 -2
  18. package/dist/types/src/components/Collider.d.ts +7 -1
  19. package/dist/types/src/components/GameObject.d.ts +2 -2
  20. package/dist/types/src/components/PolygonCollider.d.ts +33 -0
  21. package/dist/types/src/components/RigidBody.d.ts +281 -8
  22. package/dist/types/src/importers/Aseprite.d.ts +2 -2
  23. package/dist/types/src/importers/ForgeLevel.d.ts +97 -0
  24. package/dist/types/src/importers/TiledMap.d.ts +1 -1
  25. package/dist/types/src/managers/EventManager.d.ts +1 -1
  26. package/dist/types/src/managers/Gamepad.d.ts +102 -0
  27. package/dist/types/src/managers/InputManager.d.ts +93 -2
  28. package/dist/types/src/managers/NetworkManager.d.ts +2 -2
  29. package/dist/types/src/managers/RenderStats.d.ts +1 -1
  30. package/dist/types/src/managers/TextureManager.d.ts +1 -1
  31. package/dist/types/src/physics/AABB.d.ts +92 -0
  32. package/dist/types/src/physics/Body.d.ts +435 -0
  33. package/dist/types/src/physics/BodyType.d.ts +6 -0
  34. package/dist/types/src/physics/BroadPhase.d.ts +210 -0
  35. package/dist/types/src/physics/Collision.d.ts +102 -0
  36. package/dist/types/src/physics/Contact.d.ts +206 -0
  37. package/dist/types/src/physics/ContactSolver.d.ts +108 -0
  38. package/dist/types/src/physics/Distance.d.ts +54 -0
  39. package/dist/types/src/physics/DistanceJoint.d.ts +90 -0
  40. package/dist/types/src/physics/Fixture.d.ts +221 -0
  41. package/dist/types/src/physics/Island.d.ts +52 -0
  42. package/dist/types/src/physics/Joint.d.ts +59 -0
  43. package/dist/types/src/physics/Math2D.d.ts +371 -0
  44. package/dist/types/src/physics/RevoluteJoint.d.ts +119 -0
  45. package/dist/types/src/physics/Settings.d.ts +22 -0
  46. package/dist/types/src/physics/Shapes.d.ts +207 -0
  47. package/dist/types/src/physics/TimeOfImpact.d.ts +22 -0
  48. package/dist/types/src/physics/World.d.ts +274 -0
  49. package/dist/types/src/physics/index.d.ts +34 -0
  50. package/index.js +6 -0
  51. package/package.json +2 -3
  52. package/src/Animator.js +1 -1
  53. package/src/CollisionLayers.js +3 -3
  54. package/src/Color.js +8 -0
  55. package/src/Drawable.js +1 -1
  56. package/src/Emerald.js +1 -1
  57. package/src/EmeraldDB.js +2 -2
  58. package/src/InstancedTexture.js +57 -9
  59. package/src/Material.js +2 -2
  60. package/src/MathUtils.js +2 -1
  61. package/src/ParticleEmitter.js +1 -1
  62. package/src/Physics.js +270 -60
  63. package/src/Scene.js +1 -1
  64. package/src/Shaders.js +20 -20
  65. package/src/Tilemap.js +1 -1
  66. package/src/UI.js +1 -1
  67. package/src/components/Behaviour.js +2 -2
  68. package/src/components/BoxCollider.js +7 -9
  69. package/src/components/BoxColliderDebug.js +3 -4
  70. package/src/components/CircleCollider.js +7 -9
  71. package/src/components/CircleColliderDebug.js +3 -2
  72. package/src/components/Collider.js +13 -3
  73. package/src/components/GameObject.js +2 -2
  74. package/src/components/PolygonCollider.js +55 -0
  75. package/src/components/RigidBody.js +441 -14
  76. package/src/importers/Aseprite.js +2 -2
  77. package/src/importers/ForgeLevel.js +581 -0
  78. package/src/importers/TiledMap.js +1 -1
  79. package/src/managers/EventManager.js +1 -1
  80. package/src/managers/Gamepad.js +126 -0
  81. package/src/managers/InputManager.js +129 -3
  82. package/src/managers/NetworkManager.js +2 -2
  83. package/src/managers/RenderStats.js +1 -1
  84. package/src/managers/TextureManager.js +1 -1
  85. package/src/physics/AABB.js +207 -0
  86. package/src/physics/Body.js +862 -0
  87. package/src/physics/BodyType.js +16 -0
  88. package/src/physics/BroadPhase.js +641 -0
  89. package/src/physics/Collision.js +534 -0
  90. package/src/physics/Contact.js +500 -0
  91. package/src/physics/ContactSolver.js +526 -0
  92. package/src/physics/Distance.js +403 -0
  93. package/src/physics/DistanceJoint.js +227 -0
  94. package/src/physics/Fixture.js +346 -0
  95. package/src/physics/Island.js +203 -0
  96. package/src/physics/Joint.js +78 -0
  97. package/src/physics/Math2D.js +573 -0
  98. package/src/physics/RevoluteJoint.js +278 -0
  99. package/src/physics/Settings.js +78 -0
  100. package/src/physics/Shapes.js +549 -0
  101. package/src/physics/TimeOfImpact.js +87 -0
  102. package/src/physics/World.js +731 -0
  103. package/src/physics/index.js +79 -0
package/README.md CHANGED
@@ -2,55 +2,18 @@
2
2
 
3
3
  Emerald is a comprehensive 2D graphics engine that can help you create games easier than ever.
4
4
 
5
- **New to the engine?** Start with the [Getting Started guide](docs/getting-started.md)
6
- it walks from an empty page to a playable sprite with input, tiles, audio,
7
- saves, and a debug overlay.
5
+ **New to the engine?** The [Getting Started guide](docs/getting-started.md) walks from an empty page to a playable sprite with input, tiles, audio, saves, and a debug overlay.
8
6
 
9
7
  ## Table of Contents
10
8
 
11
9
  - [Emerald](#emerald)
12
10
  - [Table of Contents](#table-of-contents)
13
11
  - [Getting Started](#getting-started)
14
- - [Core Systems](#core-systems)
15
- - [Sprites: flipping, pivot & anchor](#sprites-flipping-pivot--anchor)
16
- - [Rendering Pipeline (post-processing, materials, batching)](#rendering-pipeline-post-processing-materials-batching)
17
- - [Post-processing](#post-processing)
18
- - [PostEffects (built-in)](#posteffects-built-in)
19
- - [RenderTarget](#rendertarget)
20
- - [Material (custom shaders)](#material-custom-shaders)
21
- - [SpriteBatch](#spritebatch)
22
- - [In-Engine UI](#in-engine-ui)
23
- - [Game Loop & Scene Transitions](#game-loop--scene-transitions)
24
- - [ScreenEffects (transitions)](#screeneffects-transitions)
25
- - [Coroutines](#coroutines)
26
- - [Gamepads & Controllers](#gamepads--controllers)
27
- - [Per-instance color & tinting](#per-instance-color--tinting)
28
- - [Physics: collision layers & continuous detection](#physics-collision-layers--continuous-detection)
29
- - [RigidBody velocity helpers](#rigidbody-velocity-helpers)
30
- - [Tilemap colliders & auto-tiling](#tilemap-colliders--auto-tiling)
31
- - [Camera follow deadzone](#camera-follow-deadzone)
32
- - [ParticleEmitter (pooled sprites)](#particleemitter-pooled-sprites)
33
- - [Advanced Particles](#advanced-particles)
34
- - [Positional (spatial) Audio](#positional-spatial-audio)
35
- - [Audio buses & fades](#audio-buses--fades)
36
- - [AssetManager](#assetmanager-loading)
37
- - [Asset importers (Tiled & Aseprite)](#asset-importers-tiled--aseprite)
38
- - [Networking (NetworkManager + Interpolator)](#networking-networkmanager--interpolator)
39
- - [DebugOverlay (upgraded)](#debugoverlay-upgraded)
40
- - [Storage (versioned saves)](#storage-versioned-saves)
41
- - [EmeraldDB (IndexedDB saves)](#emeralddb-indexeddb-saves)
42
- - [Resolution independence](#resolution-independence-design-resolution)
43
- - [Auto-pause & lifecycle](#auto-pause--lifecycle)
44
- - [Production hardening](#production-hardening-resource-lifecycle-context-loss-render-stats)
45
- - [Usage](#usage)
46
- - [Basic Setup](#basic-setup)
47
- - [Set the background color for the engine](#set-the-background-color-for-the-engine)
48
- - [Drawing the scene](#drawing-the-scene)
49
12
  - [Scene](#scene)
50
- - [Adding and removing items from the scene](#adding-and-removing-items-from-the-scene)
51
- - [Set Active Scene](#set-active-scene)
52
13
  - [Game Objects](#game-objects)
53
14
  - [Creating a new GameObject](#creating-a-new-gameobject)
15
+ - [Behaviour (component lifecycle)](#behaviour-component-lifecycle)
16
+ - [Transform hierarchy](#transform-hierarchy)
54
17
  - [Components](#components)
55
18
  - [Texture](#texture)
56
19
  - [InstancedTexture](#instancedtexture)
@@ -60,849 +23,826 @@ saves, and a debug overlay.
60
23
  - [RigidBody](#rigidbody)
61
24
  - [BoxCollider](#boxcollider)
62
25
  - [CircleCollider](#circlecollider)
26
+ - [PolygonCollider](#polygoncollider)
63
27
  - [Object methods](#object-methods)
64
28
  - [Position](#position)
65
29
  - [Rotation](#rotation)
66
30
  - [Scale](#scale)
67
31
  - [Change color](#change-color)
68
- - [Set texture frame](#set-texture-frame)
32
+ - [Sprite flipping, pivot & anchor](#sprite-flipping-pivot--anchor)
69
33
  - [Animations](#animations)
70
34
  - [Instance System](#instance-system)
71
35
  - [Creating Instances](#creating-instances)
72
36
  - [Instance Management](#instance-management)
73
37
  - [Instance Events](#instance-events)
38
+ - [Per-instance color](#per-instance-color)
39
+ - [Per-instance atlas regions](#per-instance-atlas-regions)
40
+ - [Per-instance animation](#per-instance-animation)
74
41
  - [Physics Engine](#physics-engine)
75
42
  - [Setting up Physics](#setting-up-physics)
76
- - [Physics Bodies](#physics-bodies)
43
+ - [Time stepping](#time-stepping)
44
+ - [RigidBody methods](#rigidbody-methods)
77
45
  - [Collision Detection](#collision-detection)
78
- - [Collision Events](#collision-events)
46
+ - [Collision layers](#collision-layers)
47
+ - [Continuous collision detection](#continuous-collision-detection)
48
+ - [Raycasting & point queries](#raycasting--point-queries)
49
+ - [Tilemap colliders & auto-tiling](#tilemap-colliders--auto-tiling)
79
50
  - [Particle System](#particle-system)
80
51
  - [Particle Settings](#particle-settings)
81
52
  - [Creating Particle Systems](#creating-particle-systems)
82
53
  - [Particle System Methods](#particle-system-methods)
54
+ - [ParticleEmitter (simple pooled bursts)](#particleemitter-simple-pooled-bursts)
83
55
  - [Lighting System](#lighting-system)
84
56
  - [Ambient Light](#ambient-light)
85
57
  - [Point Light](#point-light)
86
58
  - [Directional Light](#directional-light)
87
59
  - [Text Rendering](#text-rendering)
88
60
  - [BitmapText](#bitmaptext)
89
- - [EventManager](#eventmanager)
61
+ - [CanvasText](#canvastext)
62
+ - [Input](#input)
90
63
  - [Keyboard Events](#keyboard-events)
91
64
  - [Mouse Events](#mouse-events)
92
65
  - [Object Events](#object-events)
93
66
  - [Event Cleanup](#event-cleanup)
67
+ - [InputManager (actions)](#inputmanager-actions)
68
+ - [Gamepads & Controllers](#gamepads--controllers)
94
69
  - [AudioManager](#audiomanager)
95
70
  - [Adding Audio](#adding-audio)
96
71
  - [Playing Audio](#playing-audio)
97
72
  - [Audio Control](#audio-control)
73
+ - [Buses & Fades](#buses--fades)
74
+ - [Positional Audio](#positional-audio)
98
75
  - [Camera](#camera)
99
76
  - [FPSCounter](#fpscounter)
100
- - [Scene Management](#scene-management)
101
77
  - [Time Management](#time-management)
78
+ - [Tween, Timer & StateMachine](#tween-timer--statemachine)
79
+ - [SpatialGrid & Pool](#spatialgrid--pool)
80
+ - [MathUtils](#mathutils)
81
+ - [Coroutines](#coroutines)
82
+ - [Rendering Pipeline](#rendering-pipeline)
83
+ - [Post-processing](#post-processing)
84
+ - [PostEffects (built-in)](#posteffects-built-in)
85
+ - [RenderTarget](#rendertarget)
86
+ - [Material (custom shaders)](#material-custom-shaders)
87
+ - [SpriteBatch](#spritebatch)
88
+ - [In-Engine UI](#in-engine-ui)
89
+ - [ScreenEffects (transitions)](#screeneffects-transitions)
90
+ - [Scene Transitions & the Game Loop](#scene-transitions--the-game-loop)
91
+ - [DebugOverlay](#debugoverlay)
92
+ - [Serializer (save/load scenes)](#serializer-saveload-scenes)
93
+ - [Storage (versioned saves)](#storage-versioned-saves)
94
+ - [EmeraldDB (IndexedDB saves)](#emeralddb-indexeddb-saves)
95
+ - [AssetManager](#assetmanager)
96
+ - [Asset importers (Tiled, Aseprite & Forge)](#asset-importers-tiled-aseprite--forge)
97
+ - [Networking (NetworkManager + Interpolator)](#networking-networkmanager--interpolator)
102
98
  - [Advanced Features](#advanced-features)
103
99
  - [Resize Handling](#resize-handling)
100
+ - [Resolution independence](#resolution-independence)
101
+ - [Auto-pause & lifecycle](#auto-pause--lifecycle)
102
+ - [Production hardening](#production-hardening)
103
+ - [NPM scripts](#npm-scripts)
104
104
 
105
105
  ## Getting Started
106
106
 
107
107
  To get started with Emerald, you need to have a canvas element in your HTML and import the necessary classes.
108
108
 
109
- ## Core Systems
109
+ ```javascript
110
+ import { Emerald, Scene, Color, SceneManager } from "emeraldengine";
110
111
 
111
- Everything below ships from the package root (`import { ... } from "emeraldengine"`).
112
+ const emerald = new Emerald(canvas); // You should pass your own canvas element here
113
+ const scene = new Scene();
114
+ SceneManager.setScene(scene);
112
115
 
113
- ### Game loop & Time
116
+ emerald.setBackgroundColor(new Color(20, 20, 30, 255)); // color = new Color(r, g, b, a = 255)
117
+ ```
114
118
 
115
- `Emerald.drawScene(scene, dt)` renders the scene and also advances global `Tween`/`Timer` updates using the time-scaled delta. `Time` is updated for you each call.
119
+ To draw items on the screen you need some sort of animation loop. You can drive one yourself with `window.requestAnimationFrame`:
116
120
 
117
121
  ```javascript
118
- import { Time } from "emeraldengine";
119
- Time.getDeltaTime(); // time-scaled seconds since last frame
120
- Time.getUnscaledDeltaTime(); // raw delta, ignores timeScale
121
- Time.getElapsedTime(); // total accumulated time
122
- Time.setTimeScale(0.5); // 0 = paused, 1 = normal, 2 = double speed
122
+ let lastTime = 0;
123
+ const animate = (currentTime) => {
124
+ const deltaTime = (currentTime - lastTime) / 1000;
125
+ lastTime = currentTime;
126
+ emerald.drawScene(scene, deltaTime); // You need this line to tell the engine what to draw
127
+ window.requestAnimationFrame(animate);
128
+ };
129
+ animate(0);
123
130
  ```
124
131
 
125
- ### Behaviour (component lifecycle)
132
+ Or let `emerald.run()` own the loop for you. It computes a clamped delta time, pauses automatically when the tab is hidden, and optionally drives a fixed-timestep simulation alongside your rendering:
126
133
 
127
134
  ```javascript
128
- import { Behaviour } from "emeraldengine";
129
-
130
- class Spinner extends Behaviour {
131
- start() {
132
- this.speed = 2;
133
- } // once, before first update
134
- update(dt) {
135
- this.gameObject.transform.rotation += this.speed * dt;
135
+ const stop = emerald.run(
136
+ (dt, alpha) => {
137
+ world.update(dt);
138
+ emerald.drawScene(scene, dt);
139
+ },
140
+ {
141
+ maxDelta: 0.25, // clamp dt after a tab-switch stall
142
+ fixedStep: 1 / 60, // optional fixed simulation step (0 = off)
143
+ fixedUpdate: (step) => physics.process(step),
136
144
  }
137
- onCollisionEnter(other, contact) {} // requires physics ticking
138
- onCollisionExit(other, contact) {}
139
- onDestroy() {}
140
- }
141
- obj.addComponent(new Spinner());
142
- scene.update(dt); // ticks all Behaviours on active objects
145
+ );
146
+ // later: stop(); // or emerald.stop();
143
147
  ```
144
148
 
145
- ### Transform hierarchy
149
+ See [Auto-pause & lifecycle](#auto-pause--lifecycle) for the pause/resume hooks `run()` accepts.
146
150
 
147
- ```javascript
148
- parent.addChild(child); // or child.setParent(parent)
149
- child.setParent(null); // detach
150
- // child.transform composes on top of the parent's position/rotation/scale
151
- ```
151
+ ## Scene
152
152
 
153
- ### Cameras & split-screen
153
+ Emerald has multiple scenes support. In order to render any object it has to be added to the scene using the `add` method.
154
154
 
155
155
  ```javascript
156
- import { Camera, CameraController } from "emeraldengine";
156
+ // Adding an object to the scene
157
+ scene.add(gameObject);
157
158
 
158
- // Multiple cameras, each with a normalized viewport (origin bottom-left)
159
- const top = new Camera({ viewport: { x: 0, y: 0.5, width: 1, height: 0.5 } });
160
- const bottom = new Camera({ viewport: { x: 0, y: 0, width: 1, height: 0.5 } });
161
- emerald.setCameras([top, bottom]); // or emerald.addCamera(cam) / removeCamera(cam)
159
+ // Removing an object from the scene
160
+ scene.remove(gameObject);
162
161
 
163
- camera.setZoom(1.5);
164
- camera.setPosition(x, y);
165
- camera.clearColor = new Color(10, 14, 20); // optional per-viewport clear
162
+ // Freeing it for good (buffers + texture reference), instead of just removing it
163
+ scene.remove(gameObject, { dispose: true });
164
+ ```
166
165
 
167
- // Per-camera layer visibility (e.g. translucent opponent in 2-player)
168
- top.setIgnoreLayers([11, 20]); // skip these object layers in this camera
166
+ When changing a scene you should deactivate the current scene to not mess up the event manager:
169
167
 
170
- // Smooth follow / bounds / shake
171
- const cam = new CameraController(emerald.camera);
172
- cam.follow(player, 0.12).setBounds(-1000, -1000, 1000, 1000);
173
- cam.shake(10, 0.3);
174
- // each frame: cam.update(dt);
168
+ ```javascript
169
+ scene.setIsActive(true); // Activate
170
+ scene.setIsActive(false); // Deactivate
175
171
  ```
176
172
 
177
- ### Input
173
+ `SceneManager` keeps track of which scene is currently active:
178
174
 
179
175
  ```javascript
180
- import { InputManager } from "emeraldengine";
181
- const input = new InputManager();
182
- input.mapAction("jump", ["Space", " ", "pad:0:south"]); // keyboard + gamepad
183
- input.mapAction("left", ["a", "ArrowLeft", "pad:0:dpadLeft"]);
184
- // in the loop:
185
- if (input.justPressed("jump")) player.jump();
186
- const move = input.getAxis("left", "right"); // -1 / 0 / +1
187
- input.update(); // call once per frame (edge detection + gamepad polling)
176
+ import { SceneManager } from "emeraldengine";
177
+
178
+ SceneManager.setScene(scene);
179
+ const currentScene = SceneManager.getScene();
188
180
  ```
189
181
 
190
- Gamepad tokens (`pad:0:south`, sticks, triggers, rumble, mapping for non-standard
191
- pads) are documented in [Gamepads & Controllers](#gamepads--controllers).
182
+ It can also switch scenes behind a fade, wired to `ScreenEffects`. See [Scene Transitions & the Game Loop](#scene-transitions--the-game-loop).
192
183
 
193
- `EventManager` handles per-object click/hover; it now hits only the **topmost** object and `screenToWorld(clientX, clientY)` accounts for camera zoom, DPR, and viewport.
184
+ ## Game Objects
194
185
 
195
- ### Tween & Easing
186
+ ### Creating a new GameObject
196
187
 
197
188
  ```javascript
198
- import { Tween, Easing } from "emeraldengine";
199
- Tween.to(sprite.transform.position, { x: 200, y: -50 }, 0.6, {
200
- easing: Easing.outBack, // linear, inOutQuad, outCubic, outBounce, outElastic, ...
201
- delay: 0,
202
- loop: false,
203
- yoyo: false,
204
- onUpdate: (t) => {},
205
- onComplete: () => {},
206
- }).then(() => console.log("done"));
207
- Tween.killOf(target);
208
- Tween.killAll();
209
- // driven automatically by drawScene
189
+ import { GameObject, Vector3, Vector2 } from "emeraldengine";
190
+ /*
191
+ ARGUMENTS:
192
+ 1. name: string = Name of the new GameObject
193
+ 2. position: Vector3 = Position of the new GameObject
194
+ 3. rotation: number = Rotation of the new GameObject
195
+ 4. scale: Vector2 = Scale of the new GameObject
196
+ */
197
+ const gameObject = new GameObject(name, position, rotation, scale);
210
198
  ```
211
199
 
212
- ### Timer
200
+ This will create a new empty GameObject. At this stage you will not see anything on the screen until you add some components.
213
201
 
214
- ```javascript
215
- import { Timer } from "emeraldengine";
216
- Timer.after(2, () => spawnEnemy()); // once
217
- const h = Timer.every(0.5, () => tick(), 10); // 10 times (omit count = forever)
218
- Timer.clear(h);
219
- Timer.clearAll();
220
- ```
202
+ ### Behaviour (component lifecycle)
221
203
 
222
- ### StateMachine
204
+ For your own game logic (rather than rendering/physics), add a `Behaviour` component. It's ticked automatically every frame the object is active.
223
205
 
224
206
  ```javascript
225
- import { StateMachine } from "emeraldengine";
226
- const fsm = new StateMachine();
227
- fsm.add("idle", {
228
- update: (dt, sm) => {
229
- if (seen) sm.set("chase");
230
- },
231
- });
232
- fsm.add("chase", { enter: () => roar(), update: (dt) => move(dt) });
233
- fsm.set("idle");
234
- // in update(dt): fsm.update(dt); -> fsm.is("chase")
207
+ import { Behaviour } from "emeraldengine";
208
+
209
+ class Spinner extends Behaviour {
210
+ start() {
211
+ this.speed = 2;
212
+ } // once, before the first update
213
+ update(dt) {
214
+ this.gameObject.transform.rotation += this.speed * dt;
215
+ }
216
+ onCollisionEnter(other, contact) {} // requires physics ticking
217
+ onCollisionExit(other, contact) {}
218
+ onDestroy() {}
219
+ }
220
+ gameObject.addComponent(new Spinner());
235
221
  ```
236
222
 
237
- ### SpatialGrid
223
+ ### Transform hierarchy
224
+
225
+ GameObjects can be parented to one another. A child's transform composes on top of its parent's position/rotation/scale, so moving the parent moves the whole group.
238
226
 
239
227
  ```javascript
240
- import { SpatialGrid } from "emeraldengine";
241
- const grid = new SpatialGrid(64);
242
- grid.clear();
243
- for (const e of enemies)
244
- grid.insert(e, e.transform.position.x, e.transform.position.y);
245
- const near = grid.queryRadius(px, py, 100); // or grid.queryRect(...)
228
+ parent.addChild(child); // or child.setParent(parent)
229
+ child.setParent(null); // detach
246
230
  ```
247
231
 
248
- ### Pool
232
+ ### Components
233
+
234
+ There are currently 9 components: Texture, InstancedTexture, Square2D, Circle2D, Triangle2D, RigidBody, BoxCollider, CircleCollider, PolygonCollider
235
+
236
+ #### Texture
249
237
 
250
238
  ```javascript
251
- import { Pool } from "emeraldengine";
252
- const bullets = new Pool(
253
- () => new Bullet(),
254
- (b, x, y) => b.spawn(x, y),
255
- 50
239
+ import { Texture } from "emeraldengine";
240
+ /*
241
+ ARGUMENTS:
242
+ 1. texturePath = Specify the path for the texture that you want to use.
243
+ 2. frameWidth: number = The width of each frame.
244
+ 3. frameHeight: number = The height of each frame.
245
+ 4. framesPerRow: number = How many frames are in one row in your spritesheet.
246
+ 5. totalFrames: number = How many total frames does your spritesheet have.
247
+ 6. animationSpeed: number = Speed of change of every frame.
248
+ 7. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
249
+ 8. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
250
+ 9. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
251
+ */
252
+ const texture = new Texture(
253
+ texturePath,
254
+ frameWidth,
255
+ frameHeight,
256
+ framesPerRow,
257
+ totalFrames,
258
+ animationSpeed,
259
+ autoPlay,
260
+ (pixelart = true),
261
+ (useLighting = true)
256
262
  );
257
- const b = bullets.acquire(px, py);
258
- bullets.release(b); // bullets.releaseAll()
263
+
264
+ // Add the texture to a game object
265
+ gameObject.addComponent(texture);
259
266
  ```
260
267
 
261
- ### MathUtils
268
+ ![Texture](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/texture.png?raw=true)
262
269
 
263
- ```javascript
264
- import { MathUtils } from "emeraldengine";
265
- MathUtils.clamp(v, 0, 1);
266
- MathUtils.lerp(a, b, t);
267
- MathUtils.map(v, 0, 10, 0, 100);
268
- MathUtils.degToRad(90);
269
- MathUtils.randomRange(0, 5);
270
- MathUtils.randomInt(1, 6);
271
- MathUtils.distance(a, b);
272
- MathUtils.normalize(v);
273
- MathUtils.angleBetween(a, b);
274
- ```
270
+ #### InstancedTexture
275
271
 
276
- ### Physics: fixed timestep, per-object collisions, raycast
272
+ InstancedTexture is perfect for rendering many objects with the same texture efficiently, such as tiles, particles, or repeating elements. The whole batch is drawn in a single draw call. See the [Instance System](#instance-system) section for how to add, manage, and animate instances.
277
273
 
278
274
  ```javascript
279
- physics.setFixedTimeStep(1 / 60); // stable, frame-rate independent
280
- physics.process(dt); // accumulator-based stepping
275
+ import { InstancedTexture } from "emeraldengine";
276
+ /*
277
+ ARGUMENTS:
278
+ 1. texturePath = Specify the path for the texture that you want to use.
279
+ 2. instanceCount: number = How many instances of the texture you want to create.
280
+ 3. frameWidth: number = The width of each frame.
281
+ 4. frameHeight: number = The height of each frame.
282
+ 5. framesPerRow: number = How many frames are in one row in your spritesheet.
283
+ 6. totalFrames: number = How many total frames does your spritesheet have.
284
+ 7. animationSpeed: number = Speed of change of every frame.
285
+ 8. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
286
+ 9. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
287
+ 10. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
288
+ */
289
+ const instancedTexture = new InstancedTexture(
290
+ texturePath,
291
+ instanceCount,
292
+ frameWidth,
293
+ frameHeight,
294
+ framesPerRow,
295
+ totalFrames,
296
+ animationSpeed,
297
+ autoPlay,
298
+ (pixelart = true),
299
+ (useLighting = true)
300
+ );
281
301
 
282
- // Per-object events fire on Behaviour components automatically (see Behaviour).
283
- const hit = physics.raycast({ x, y }, { x: 1, y: 0 }, 500);
284
- // -> { object, rigidBody, point, normal, fraction } | null
285
- const objects = physics.queryPoint({ x, y }); // owners whose collider contains the point
302
+ // Add the instanced texture to a game object
303
+ gameObject.addComponent(instancedTexture);
286
304
  ```
287
305
 
288
- ### Rendering extras
306
+ ![InstancedTexture](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/instancedtexture.png?raw=true)
307
+
308
+ #### Square2D
289
309
 
290
310
  ```javascript
291
- // Layers (sorted before z) and screen-space HUD
292
- obj.setLayer(10);
293
- hudObj.setScreenSpace(true); // ignores camera; position is pixels from viewport center
311
+ import { Square2D } from "emeraldengine";
312
+ let square = new Square2D();
313
+ gameObject.addComponent(square);
314
+ ```
294
315
 
295
- // Blend modes (per drawable)
296
- drawable.setBlendMode("additive"); // "normal" | "additive" | "multiply"
316
+ ![Square2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/square2d.png?raw=true)
297
317
 
298
- // Off-screen culling (on by default): objects outside the view are skipped.
299
- emerald.setCullingEnabled(true);
300
- particles.alwaysVisible = true; // opt an object out (e.g. emitters with spread)
318
+ #### Triangle2D
301
319
 
302
- // Texture atlas
303
- import { TextureAtlas } from "emeraldengine";
304
- const atlas = await TextureAtlas.load("sheet.png", sheetJson, true);
305
- atlas.applyTo(sprite, "player_idle_0"); // sets a UV sub-rect on the Drawable
320
+ ```javascript
321
+ import { Triangle2D } from "emeraldengine";
322
+ let triangle = new Triangle2D();
323
+ gameObject.addComponent(triangle);
324
+ ```
306
325
 
307
- // Dynamic system-font text
308
- import { CanvasText } from "emeraldengine";
309
- const label = CanvasText.create("Score: 0", {
310
- font: "bold 28px monospace",
311
- color: "#6ee7b7",
312
- screenSpace: true,
313
- });
314
- scene.add(label);
315
- label.getComponent(CanvasText).setText("Score: 120");
316
-
317
- // Shared/cached GL textures
318
- import { TextureManager } from "emeraldengine";
319
- await TextureManager.preload(["a.png", "b.png"], true);
320
- ```
326
+ ![Triangle2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/triangle2d.png?raw=true)
321
327
 
322
- ### Animator, Tilemap, Particles
328
+ #### Circle2D
323
329
 
324
330
  ```javascript
325
- import { Animator, Tilemap } from "emeraldengine";
331
+ import { Circle2D } from "emeraldengine";
332
+ /*
333
+ ARGUMENTS:
334
+ 1. segments = number of segments that the circle will have. Default is 32.
335
+ */
336
+ let circle = new Circle2D(segments);
337
+ gameObject.addComponent(circle);
338
+ ```
326
339
 
327
- const anim = new Animator();
328
- anim
329
- .addClip("run", [0, 1, 2, 3], { speed: 100 })
330
- .addClip("jump", [8, 9], { loop: false });
331
- obj.addComponent(texture);
332
- obj.addComponent(anim);
333
- anim.play("run");
340
+ ![Circle2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/circle2d.png?raw=true)
334
341
 
335
- const map = new Tilemap("level", "tiles.png", {
336
- tileSize: 32,
337
- frameWidth: 16,
338
- frameHeight: 16,
339
- framesPerRow: 4,
340
- totalFrames: 16,
341
- });
342
- map.setMap([
343
- [0, 0, 0],
344
- [1, -1, 1],
345
- ]); // -1 = empty; one instanced draw call
346
- scene.add(map.gameObject);
347
- ```
342
+ #### RigidBody
348
343
 
349
- ### AudioManager
344
+ RigidBody is a component that allows you to add physics to your game objects. However, it won't work until you create a Physics instance at the top of your code. `RigidBody` itself is the body: every physics operation (position, velocity, forces, sleep state, mass) is a method on it directly; see [RigidBody methods](#rigidbody-methods) in the Physics Engine section for the full list.
350
345
 
351
346
  ```javascript
352
- audio.add("jump.wav", "jump", { volume: 0.8, loop: false });
353
- audio.play("jump"); // restart from 0
354
- audio.playOverlap("jump"); // overlapping copies for rapid SFX
355
- audio.setMasterVolume(0.5);
356
- audio.setBusVolume("music", 0.6); // mix buses: master * bus * sound
357
- audio.crossfade("theme", "boss", 2.0); // timed fades: fadeIn/fadeOut/fadeTo too
358
- ```
347
+ import { RigidBody, Physics, Vector2 } from "emeraldengine";
359
348
 
360
- Full mixing details: [Audio buses & fades](#audio-buses--fades).
349
+ // Create physics engine first
350
+ const physics = new Physics(-70, 32, 2); // gravity, scale, velocityThreshold
361
351
 
362
- ### Serializer (save/load scenes)
352
+ /*
353
+ ARGUMENTS:
354
+ 1. physics: Physics = Instance of the Physics class that you created at the top of your code.
355
+ 2. type: string = Type of the rigid body. It can be "dynamic", "kinematic", or "static".
356
+ 3. position: Vector2 = Position of the rigid body is Vector2 because it doesn't need any Z index.
357
+ 4. fixedRotation: boolean = Specify whether the rigid body should have a fixed rotation or not. Default is false.
358
+ 5. parentObject: GameObject = (OPTIONAL) If you want to attach the rigid body to a GameObject you can pass it here. If you don't want to attach it to any GameObject then pass null.
359
+ 6. offset: Vector2 = (OPTIONAL) Offset from the GameObject's position.
360
+ */
361
+ const rigidBody = new RigidBody(
362
+ physics,
363
+ "dynamic",
364
+ new Vector2(0, 0),
365
+ false,
366
+ gameObject,
367
+ new Vector2(0, 0)
368
+ );
363
369
 
364
- ```javascript
365
- import { Serializer } from "emeraldengine";
366
- Serializer.register("coin", (data) => makeCoin(data.value));
367
- coin.prefabType = "coin";
368
- coin.serialize = () => ({ value: 5 });
369
- const json = Serializer.toJSON(scene); // save
370
- Serializer.fromJSON(json, new Scene()); // load
370
+ gameObject.addComponent(rigidBody);
371
371
  ```
372
372
 
373
- ### DebugOverlay
373
+ #### BoxCollider
374
374
 
375
375
  ```javascript
376
- import { DebugOverlay } from "emeraldengine";
377
- const debug = new DebugOverlay();
378
- // after drawScene: debug.update(emerald, scene); // FPS / objects / cameras
379
- ```
376
+ import { BoxCollider } from "emeraldengine";
377
+
378
+ /*
379
+ ARGUMENTS:
380
+ 1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
381
+ 2. size: Vector2 = Size of the box collider.
382
+ 3. density: number = Density of the collider.
383
+ 4. friction: number = Friction of the collider.
384
+ 5. restitution: number = Restitution (bounciness) of the collider.
385
+ 6. isSensor: boolean = Whether this collider is a sensor (triggers events but doesn't collide physically).
386
+ 7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
387
+ 8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
388
+ */
389
+ const boxCollider = new BoxCollider(
390
+ rigidBody,
391
+ new Vector2(1, 1),
392
+ 1,
393
+ 0.3,
394
+ 0.1,
395
+ false,
396
+ gameObject
397
+ );
380
398
 
381
- ### NPM scripts
399
+ gameObject.addComponent(boxCollider);
400
+ ```
382
401
 
383
- | Script | Purpose |
384
- | ----------------- | -------------------------------------------- |
385
- | `npm test` | Node test suite (`node --test test/`) |
386
- | `npm run types` | Regenerate `dist/types` from JSDoc via `tsc` |
387
- | `npm run format` | Prettier |
402
+ ![BoxCollider](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/boxcollider.png?raw=true)
388
403
 
389
- ## Sprites: flipping, pivot & anchor
404
+ The `BoxCollider` is specifically made bigger than the `Square2D` component in this image to demonstrate how it works. You can adjust the size of the collider to fit your needs.
390
405
 
391
- Any `Texture` (or other `Drawable`) can be mirrored and re-pivoted without
392
- touching the GameObject's scale — handy for characters that face left/right and
393
- for putting a sprite's origin at its feet.
406
+ #### CircleCollider
394
407
 
395
408
  ```javascript
396
- const tex = gameObject.getComponent(Texture);
397
-
398
- tex.setFlipX(facing < 0); // mirror horizontally (e.g. face left)
399
- tex.setFlipY(true); // mirror vertically
409
+ import { CircleCollider } from "emeraldengine";
400
410
 
401
- // Pivot: which local point sits on the GameObject's position and acts as the
402
- // rotation/scale center. (0,0) = center (default); x in [-1,1] left..right,
403
- // y in [-1,1] bottom..top.
404
- tex.setPivot(0, -1); // bottom-center feet on the ground
411
+ /*
412
+ ARGUMENTS:
413
+ 1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
414
+ 2. radius: number = Radius of the circle collider.
415
+ 3. density: number = Density of the collider.
416
+ 4. friction: number = Friction of the collider.
417
+ 5. restitution: number = Restitution (bounciness) of the collider.
418
+ 6. isSensor: boolean = Whether this collider is a sensor.
419
+ 7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
420
+ 8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
421
+ */
422
+ const circleCollider = new CircleCollider(
423
+ rigidBody,
424
+ 1.5,
425
+ 1,
426
+ 0.3,
427
+ 0.8,
428
+ false,
429
+ gameObject
430
+ );
405
431
 
406
- // Anchor: the same thing in 0..1 with a top-left origin (CSS-style).
407
- tex.setAnchor(0.5, 1); // bottom-center
408
- tex.setAnchor(0.5, 0.5); // back to center
432
+ gameObject.addComponent(circleCollider);
409
433
  ```
410
434
 
411
- ## Rendering Pipeline (post-processing, materials, batching)
435
+ ![CircleCollider](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/circlecollider.png?raw=true)
412
436
 
413
- Everything in this section ships from the package root: `import { ... } from "emeraldengine"`.
437
+ The `CircleCollider` is specifically made bigger than the `Circle2D` component in this image to demonstrate how it works. You can adjust the radius of the collider to fit your needs.
414
438
 
415
- ### Post-processing
439
+ #### PolygonCollider
416
440
 
417
- When post-processing is enabled, Emerald renders the whole scene into an offscreen
418
- texture and then runs a chain of full-screen shader passes (ping-ponging between two
419
- render targets) before drawing the final image to the canvas. You manage it entirely
420
- through the `Emerald` instance:
441
+ For a collision shape a box or circle can't approximate, like ramps, wedges, or arbitrary outlines.
421
442
 
422
443
  ```javascript
423
- emerald.enablePostProcessing(); // allocate the scene render target + processor
424
- emerald.disablePostProcessing(); // turn it back off
444
+ import { PolygonCollider } from "emeraldengine";
425
445
 
426
- const bloom = emerald.addPostEffect(PostEffects.bloom()); // returns the effect
427
- emerald.removePostEffect(bloom);
446
+ /*
447
+ ARGUMENTS:
448
+ 1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
449
+ 2. points: Array<{x:number, y:number}> = Local-space points, in physics units, in any order. The convex hull of these points is used, so a concave outline needs more than one collider.
450
+ 3. density: number = Density of the collider.
451
+ 4. friction: number = Friction of the collider.
452
+ 5. restitution: number = Restitution (bounciness) of the collider.
453
+ 6. isSensor: boolean = (OPTIONAL) Whether this collider is a sensor. Default is false.
454
+ 7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
455
+ 8. filter: Object = (OPTIONAL) Collision filter spec (see Collision layers below).
456
+ */
457
+ const polygonCollider = new PolygonCollider(
458
+ rigidBody,
459
+ [
460
+ { x: -1, y: -0.5 },
461
+ { x: 1, y: -0.5 },
462
+ { x: 0, y: 1 },
463
+ ],
464
+ 1,
465
+ 0.3,
466
+ 0.1,
467
+ false,
468
+ gameObject
469
+ );
428
470
 
429
- // Effects run in the order they were added. Toggle one without removing it:
430
- bloom.enabled = false;
471
+ gameObject.addComponent(polygonCollider);
431
472
  ```
432
473
 
433
- `drawScene` automatically routes through the processor while any **enabled** effect
434
- exists; if none do, it draws straight to the screen with zero overhead.
474
+ ## Object methods
435
475
 
436
- Write a custom pass by constructing a `PostEffect`. Your fragment shader gets
437
- `vUV` (0–1 screen UV), `uScene` (the previous pass), `uResolution`, and `uTime`
438
- for free — declare any extra uniforms and set them in `setUniforms`:
476
+ ### Position
439
477
 
440
478
  ```javascript
441
- import { PostEffect } from "emeraldengine";
479
+ // Set position
480
+ gameObject.transform.position.x = 100;
481
+ gameObject.transform.position.y = 200;
482
+ gameObject.transform.position.z = 0;
442
483
 
443
- const tint = new PostEffect(
444
- "tint",
445
- `
446
- uniform vec3 uTint;
447
- void main() {
448
- gl_FragColor = texture2D(uScene, vUV) * vec4(uTint, 1.0);
449
- }`,
450
- {
451
- setUniforms: (gl, loc) => gl.uniform3f(loc("uTint"), 1.0, 0.85, 0.7),
452
- enabled: true,
453
- }
454
- );
455
- emerald.addPostEffect(tint);
484
+ // Or set all at once
485
+ gameObject.transform.position = new Vector3(100, 200, 0);
456
486
  ```
457
487
 
458
- Keep UI crisp by rendering it on a camera **excluded from post-processing** —
459
- it draws straight to the screen after the effect chain, so bloom never blows
460
- out your buttons and text:
488
+ ![Position](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/position.gif?raw=true)
489
+
490
+ ### Rotation
461
491
 
462
492
  ```javascript
463
- const uiCam = new Camera({ excludeFromPost: true }); // or uiCam.setExcludeFromPost(true)
464
- emerald.addCamera(uiCam);
465
- hudObject.setLayer(100); // and restrict cameras via setOnlyLayers/ignoreLayers
493
+ // Set rotation (in radians)
494
+ gameObject.transform.rotation = Math.PI / 4; // 45 degrees
466
495
  ```
467
496
 
468
- ### PostEffects (built-in)
497
+ ![Rotation](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/rotation.gif?raw=true)
469
498
 
470
- Factory functions on the `PostEffects` namespace return a ready `PostEffect`:
499
+ ### Scale
471
500
 
472
501
  ```javascript
473
- import { PostEffects } from "emeraldengine";
502
+ // Set scale
503
+ gameObject.transform.scale.x = 2;
504
+ gameObject.transform.scale.y = 2;
474
505
 
475
- emerald.enablePostProcessing();
476
- emerald.addPostEffect(
477
- PostEffects.bloom({ threshold: 0.6, intensity: 1.2, spread: 1.1 })
478
- );
479
- emerald.addPostEffect(
480
- PostEffects.vignette({ intensity: 0.5, radius: 0.75, softness: 0.45 })
481
- );
482
- emerald.addPostEffect(
483
- PostEffects.colorGrade({ brightness: 0.02, contrast: 1.08, saturation: 1.15 })
484
- );
485
- emerald.addPostEffect(PostEffects.chromaticAberration({ amount: 0.003 }));
486
- emerald.addPostEffect(PostEffects.scanlines({ intensity: 0.15, count: 480 }));
487
- emerald.addPostEffect(
488
- PostEffects.crt({ curvature: 4.0, scanlineIntensity: 0.2, vignette: 0.3 })
489
- );
490
- emerald.addPostEffect(PostEffects.grayscale());
506
+ // Or set both at once
507
+ gameObject.transform.scale = new Vector2(2, 2);
491
508
  ```
492
509
 
493
- | Effect | Options (defaults) |
494
- | --------------------- | ----------------------------------------------------------- |
495
- | `bloom` | `threshold 0.7`, `intensity 1.0`, `spread 1.0` (multi-pass) |
496
- | `vignette` | `intensity 0.5`, `radius 0.75`, `softness 0.45` |
497
- | `colorGrade` | `brightness 0`, `contrast 1`, `saturation 1` |
498
- | `chromaticAberration` | `amount 0.003` |
499
- | `scanlines` | `intensity 0.15`, `count 480` |
500
- | `crt` | `curvature 4.0`, `scanlineIntensity 0.2`, `vignette 0.3` |
501
- | `grayscale` | — |
502
-
503
- `bloom` is exported as a class too (`BloomEffect`) if you want to subclass it.
504
-
505
- ### RenderTarget
510
+ ![Scale](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/scale.gif?raw=true)
506
511
 
507
- An offscreen framebuffer backed by a color texture (and an optional depth buffer).
508
- Used internally by the post-processor, but useful on its own for minimaps, mirrors,
509
- or picture-in-picture.
512
+ ### Change color
510
513
 
511
514
  ```javascript
512
- import { RenderTarget } from "emeraldengine";
513
-
514
- const rt = new RenderTarget(512, 512, { depth: false, pixelart: false });
515
- rt.bind(); // binds the FBO and sets the viewport to its size
516
- // ...draw...
517
- rt.unbind(); // restore the canvas framebuffer
518
- // rt.texture now holds the rendered image (a WebGLTexture)
519
- rt.resize(1024, 1024); // reallocates only if the size changed
520
- rt.dispose(); // free GL resources
515
+ // For textures
516
+ const texture = gameObject.getComponent(Texture);
517
+ texture.setColor(new Color(255, 0, 0)); // Red
521
518
  ```
522
519
 
523
- ### Material (custom shaders)
520
+ ![Change Color](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/changecolor.gif?raw=true)
524
521
 
525
- A `Material` replaces a Drawable's fragment shader while reusing the engine's
526
- standard vertex shader, so transforms, the camera, and instancing keep working.
527
- Your fragment program automatically has `vTexCoord`, `vFragPos`, `vInstanceColor`,
528
- `uSampler`, `uColor`, `uOpacity`, and `uTime` — **don't redeclare them**; declare
529
- any extra uniforms and push values with `set(name, value)`.
522
+ ### Sprite flipping, pivot & anchor
530
523
 
531
- ```javascript
532
- import { Material, Square2D } from "emeraldengine";
524
+ Any `Texture` (or other `Drawable`) can be mirrored and re-pivoted without touching the GameObject's scale, handy for characters that face left/right and for putting a sprite's origin at its feet.
533
525
 
534
- const dissolve = new Material(
535
- `
536
- uniform float uAmount;
537
- void main() {
538
- vec4 c = texture2D(uSampler, vTexCoord);
539
- if (c.a < uAmount) discard;
540
- gl_FragColor = c * uColor * uOpacity;
541
- }
542
- `,
543
- { uniforms: { uAmount: 0.0 } }
544
- );
526
+ ```javascript
527
+ tex.setFlipX(facing < 0); // mirror horizontally (e.g. face left)
528
+ tex.setFlipY(true); // mirror vertically
545
529
 
546
- const shape = new Square2D();
547
- shape.setMaterial(dissolve); // any Drawable: Texture, Square2D, Circle2D…
548
- gameObject.addComponent(shape);
530
+ // Pivot: which local point sits on the GameObject's position and acts as the
531
+ // rotation/scale center. (0,0) = center (default); x in [-1,1] left..right,
532
+ // y in [-1,1] bottom..top.
533
+ tex.setPivot(0, -1); // bottom-center, feet on the ground
549
534
 
550
- // Animate a uniform (numbers, vec2/3/4 arrays, or functions are accepted):
551
- dissolve.set("uAmount", 0.5);
552
- dissolve.set("uPulse", () => 0.5 + 0.5 * Math.sin(performance.now() / 300));
535
+ // Anchor: the same thing in 0..1 with a top-left origin (CSS-style).
536
+ tex.setAnchor(0.5, 1); // bottom-center
537
+ tex.setAnchor(0.5, 0.5); // back to center
553
538
  ```
554
539
 
555
- A pulsing glow material (as used by the race example's finish line):
540
+ ## Animations
541
+
542
+ For animated textures, `setFrame` jumps straight to a specific frame:
556
543
 
557
544
  ```javascript
558
- const glow = new Material(`
559
- void main() {
560
- float pulse = 0.5 + 0.5 * sin(uTime * 4.0 + vTexCoord.y * 6.2831);
561
- vec3 col = mix(vec3(0.40, 1.0, 0.70), vec3(1.0, 0.9, 0.4), pulse);
562
- gl_FragColor = vec4(col, 0.5 * pulse);
563
- }
564
- `);
565
- square.setMaterial(glow);
566
- square.setBlendMode("additive");
545
+ const texture = gameObject.getComponent(Texture);
546
+ texture.setFrame(2); // Set to frame 2
567
547
  ```
568
548
 
569
- ### SpriteBatch
570
-
571
- A dynamic batched renderer with its own minimal shader. Instead of one draw call
572
- per sprite, it accumulates sprites that share a texture into a single interleaved
573
- buffer and submits them in one `drawElements` call — ideal for many same-atlas
574
- quads (bullets, tiles, text glyphs).
549
+ Or play through a sequence of frames:
575
550
 
576
551
  ```javascript
577
- import { SpriteBatch } from "emeraldengine";
552
+ // Play animation
553
+ texture.playAnimation([0, 1, 2, 3], 200); // frames array, speed in ms
578
554
 
579
- const batch = new SpriteBatch({ maxQuads: 2000 });
580
- batch.begin(projectionMatrix, viewMatrix); // gl-matrix mat4 / Float32Array(16)
581
- for (const e of entities) {
582
- batch.draw({
583
- texture: atlasTexture, // a WebGLTexture; changing it flushes the batch
584
- x: e.x,
585
- y: e.y,
586
- w: 32,
587
- h: 32,
588
- rotation: e.angle,
589
- originX: 0.5,
590
- originY: 0.5,
591
- u0: e.u0,
592
- v0: e.v0,
593
- u1: e.u1,
594
- v1: e.v1, // UV sub-rect (defaults 0..1)
595
- r: 1,
596
- g: 1,
597
- b: 1,
598
- a: 1, // per-vertex tint
599
- });
555
+ // Play once, then stop instead of looping
556
+ texture.playAnimationOnce([0, 1, 2, 3], null, 200, () => console.log("done"));
557
+
558
+ // Stop animation
559
+ texture.stopAnimation();
560
+
561
+ // Check if playing
562
+ if (texture.isPlaying) {
563
+ // Animation is currently playing
600
564
  }
601
- batch.end(); // flushes remaining sprites
602
- console.log(batch.drawCalls); // GL draw calls emitted this frame
603
565
  ```
604
566
 
605
- ## In-Engine UI
606
-
607
- `UI` is a retained-mode toolkit drawn entirely by the engine — no DOM/HTML overlay.
608
- Elements are screen-space objects on a dedicated high layer with their own pointer
609
- and keyboard hit-testing. Positions are **pixels from the viewport center (y up)**,
610
- either a literal `{x, y}` or a responsive `(viewW, viewH) => ({x, y})` function;
611
- call `relayout()` after a resize.
567
+ ![Animations](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/animations.gif?raw=true)
612
568
 
613
- Pair it with a UI camera so the UI draws over the game and the game cameras skip the
614
- UI layer:
569
+ For named clips instead of raw frame arrays, use `Animator`:
615
570
 
616
571
  ```javascript
617
- import { UI } from "emeraldengine";
618
-
619
- const uiCam = UI.createCamera(); // a full-screen camera that renders ONLY UI.LAYER
620
- gameCamera.ignoreLayer(UI.LAYER); // keep the UI out of the game viewport(s)
621
- emerald.setCameras([gameCamera, uiCam]); // add the UI camera last
572
+ import { Animator } from "emeraldengine";
622
573
 
623
- const ui = new UI(scene, canvas, { accent: [120, 200, 255] });
574
+ const anim = new Animator();
575
+ anim
576
+ .addClip("run", [0, 1, 2, 3], { speed: 100 })
577
+ .addClip("jump", [8, 9], { loop: false });
578
+ gameObject.addComponent(texture);
579
+ gameObject.addComponent(anim);
580
+ anim.play("run");
581
+ ```
624
582
 
625
- // Labels (return a handle with setText)
626
- const score = ui.label(() => ({ x: 0, y: 200 }), "Score: 0", {
627
- font: "700 30px system-ui, sans-serif",
628
- color: "#eaf2ff",
629
- });
630
- score.setText("Score: 120");
583
+ ## Instance System
631
584
 
632
- // Buttons (panel + centered label, hover highlight, click handler)
633
- ui.button(() => ({ x: 0, y: 0 }), "START", 240, 56, {
634
- accent: [120, 220, 160],
635
- onClick: () => startGame(),
636
- });
585
+ The Instance system allows you to efficiently manage multiple copies of the same texture through `InstancedTexture` (see [Components](#instancedtexture)).
637
586
 
638
- // Panels and a modal dimmer behind a dialog
639
- ui.dim(0.6); // full-screen backdrop
640
- ui.panel(() => ({ x: 0, y: 0 }), 480, 320, { opacity: 0.94 });
587
+ ### Creating Instances
641
588
 
642
- // Editable single-line text field
643
- const name = ui.textField(() => ({ x: 0, y: -80 }), 280, 44, {
644
- placeholder: "Your name",
645
- maxLength: 16,
646
- onChange: (v) => console.log(v),
647
- });
648
- name.getValue();
649
- name.setValue("P1");
589
+ ```javascript
590
+ import { Instance } from "emeraldengine";
650
591
 
651
- ui.relayout(); // after creating/anchoring or on window resize
652
- ui.isOver(clientX, clientY); // true if an interactive element is under the pointer
653
- ui.destroy(); // remove all UI objects + detach listeners
592
+ // Create an instance
593
+ const instance = new Instance(
594
+ "InstanceName",
595
+ new Vector3(x, y, z),
596
+ new Vector2(width, height),
597
+ rotation,
598
+ frame
599
+ );
654
600
 
655
- UI.LAYER; // 100000 the default UI render layer
601
+ // Add to InstancedTexture
602
+ const instancedTexture = gameObject.getComponent(InstancedTexture);
603
+ instancedTexture.addInstance(instance);
656
604
  ```
657
605
 
658
- ## Game Loop & Scene Transitions
659
-
660
- `emerald.run(update, options)` owns the `requestAnimationFrame` loop for you: it
661
- computes a clamped delta time, optionally advances a fixed-timestep simulation,
662
- and calls your `update(dt, alpha)` each frame. `alpha` is the 0..1 interpolation
663
- factor between fixed steps (1 when no fixed step is configured), so you can render
664
- smoothly between simulation ticks.
606
+ ### Instance Management
665
607
 
666
608
  ```javascript
667
- const stop = emerald.run(
668
- (dt, alpha) => {
669
- world.update(dt); // variable-step game logic
670
- emerald.drawScene(scene, dt);
671
- },
672
- {
673
- maxDelta: 0.25, // clamp dt after a tab-switch stall
674
- fixedStep: 1 / 60, // optional fixed simulation step (0 = off)
675
- fixedUpdate: (step) => physics.process(step),
676
- maxSubSteps: 5,
677
- }
678
- );
679
- // later: stop(); // or emerald.stop();
680
- ```
609
+ // Remove instance
610
+ instancedTexture.removeInstance(instanceId);
681
611
 
682
- Switch scenes behind a fade with `SceneManager.transitionTo` (wired to
683
- `ScreenEffects`), or drive the fade directly with `ScreenEffects.transition`:
612
+ // Get instance by ID
613
+ const instance = instancedTexture.getInstanceWithId(instanceId);
684
614
 
685
- ```javascript
686
- import { SceneManager, ScreenEffects, Color } from "emeraldengine";
615
+ // Get instance at position
616
+ const instance = instancedTexture.getInstanceAtPosition(position, tolerance);
687
617
 
688
- const fx = new ScreenEffects(overlayScene); // update()'d each frame by your loop
618
+ // Clear all instances
619
+ instancedTexture.clearInstances();
620
+ ```
689
621
 
690
- await SceneManager.transitionTo(nextScene, {
691
- screenEffects: fx,
692
- duration: 0.4,
693
- color: new Color(0, 0, 0, 255),
694
- onSwap: (scene) => buildLevel(scene), // runs while the screen is covered
622
+ ### Instance Events
623
+
624
+ ```javascript
625
+ // Add click event to specific instance
626
+ instancedTexture.addInstanceClickEvent(instanceId, (event) => {
627
+ console.log("Instance clicked!");
695
628
  });
696
629
 
697
- // Or lower-level: fade out -> swap -> fade in
698
- await fx.transition(() => swapScenes(), { duration: 0.4 });
630
+ // Add hover events to specific instance
631
+ instancedTexture.addInstanceHoverEvent(
632
+ instanceId,
633
+ (event) => console.log("Mouse entered"),
634
+ (event) => console.log("Mouse left")
635
+ );
699
636
  ```
700
637
 
701
- ## ScreenEffects (transitions)
638
+ ### Per-instance color
702
639
 
703
- Full-screen camera transitions drawn with the engine's own screen-space quads
704
- (no CSS overlay), so they survive resolution changes, post-processing and
705
- split-screen. `fadeOut`/`fadeIn`/`flash` return promises. Call `update(dt)` each
706
- frame before `drawScene`.
640
+ Each instance can have an independent RGBA tint (white = unchanged, so existing scenes render identically). The tint multiplies the texture in the shader.
707
641
 
708
642
  ```javascript
709
- import { ScreenEffects, Color } from "emeraldengine";
710
-
711
- const fx = new ScreenEffects(scene, { layer: 100000, size: 5000 });
712
-
713
- await fx.fadeOut(0.4, new Color(0, 0, 0, 255)); // fade to black
714
- loadNextLevel();
715
- await fx.fadeIn(0.4); // fade back in
643
+ import { Color } from "emeraldengine";
716
644
 
717
- fx.flash(new Color(255, 255, 255, 255), 0.25); // quick screen flash
718
- fx.setLetterbox(80); // animate cinematic bars to 80px; pass 0 to retract
645
+ // On the Instance directly (Color uses 0..255 channels; raw form is 0..1):
646
+ instance.setColor(new Color(255, 120, 60)); // warm tint
647
+ instance.setColor(1.0, 0.4, 0.2, 1.0); // same, as raw 0..1 RGBA
719
648
 
720
- // in the loop:
721
- fx.update(dt);
722
- // when leaving the scene:
723
- fx.destroy();
649
+ // Or drive it through the InstancedTexture by index:
650
+ instancedTexture.updateInstanceColor(0); // re-read instance 0's tint
651
+ instancedTexture.updateAllInstanceColors(); // re-read every instance's tint
724
652
  ```
725
653
 
726
- ## Coroutines
727
-
728
- Generator-based sequencing layered on the same per-frame delta the rest of the
729
- engine uses. It's driven automatically from `Emerald.drawScene` (so coroutines
730
- honor pause/slow-mo via `Time.timeScale`).
731
-
732
- ```javascript
733
- import { Coroutine } from "emeraldengine";
654
+ Tip: for additive sparkle/coin glows, set the instanced texture's blend mode: `instancedTexture.setBlendMode("additive")`.
734
655
 
735
- const handle = Coroutine.start(function* () {
736
- big.setText("3");
737
- audio.beep();
738
- yield 0.7; // wait 0.7 seconds
739
- big.setText("2");
740
- audio.beep();
741
- yield 0.7;
742
- yield Coroutine.waitFrames(3); // wait 3 frames
743
- yield Coroutine.waitUntil(() => player.ready); // block until predicate is truthy
744
- yield Coroutine.waitWhile(() => paused); // block while predicate is truthy
745
- yield fetch("/level.json"); // await any promise
746
- yield Coroutine.tween(0, 1, 0.5, (v) => (door.openAmount = v)); // drive a value
747
- yield otherCoroutineHandle; // wait for a nested coroutine
748
- start();
749
- });
656
+ ### Per-instance atlas regions
750
657
 
751
- handle.cancel(); // stop it early
752
- handle.isRunning(); // boolean
753
- await handle.promise; // resolves when the coroutine finishes or is cancelled
658
+ Normally every instance samples the shared frame grid (`instance.frame`). With `setTexCoords` an instance carries its own UV quad instead, so a single InstancedTexture (one draw call) can batch tiles from an atlas with margins and spacing, apply per-instance flips, or mix arbitrary sprite regions:
754
659
 
755
- Coroutine.count(); // number of running coroutines
756
- Coroutine.clearAll(); // cancel + remove every coroutine (e.g. on scene exit)
660
+ ```javascript
661
+ const tile = new Instance(
662
+ "tile",
663
+ new Vector3(x, y, 0),
664
+ new Vector2(32, 32),
665
+ rotation
666
+ );
667
+ // 8 floats, one vec2 per corner in getFrameTexCoords order: (R,B) (L,B) (R,T) (L,T)
668
+ tile.setTexCoords([right, bottom, left, bottom, right, top, left, top]);
669
+ instancedTexture.addInstance(tile);
670
+ instancedTexture.setStatic(true); // non-moving batch: matrices upload once
757
671
  ```
758
672
 
759
- ## Gamepads & Controllers
673
+ This is exactly how the Tile Forge level loader renders a whole layer of sliced, rotated, flipped tiles as one draw call. Pass `null` to return an instance to the frame grid.
760
674
 
761
- Full controller support is built into `InputManager`: analog sticks/triggers,
762
- **semantic button names** that resolve through each pad's mapping, rumble,
763
- connect/disconnect events, and a registry for non-standard controllers. Gamepad
764
- input flows through the same `isDown` / `justPressed` / `getAxis` machinery as the
765
- keyboard, so a token like `"pad:0:south"` works anywhere a key token does
766
- (including `justPressed` edge detection).
675
+ ### Per-instance animation
767
676
 
768
- ### Button tokens & names
677
+ Instancing and sprite animation aren't mutually exclusive: each `Instance` can run its own independent frame sequence, still batched into the same single draw call.
769
678
 
770
- `pad:<i>:<name>` targets pad index `<i>`. Names resolve through the pad's mapping,
771
- so `south` is always the bottom face button whether the pad reports Xbox or
772
- PlayStation ordering:
679
+ ```javascript
680
+ // Every instance can animate on its own, at its own pace:
681
+ instancedTexture.animateInstance(zombie1.id, [0, 1, 2, 3], 150);
682
+ instancedTexture.animateInstance(zombie2.id, [4, 5, 6], 250); // a different clip, different speed
683
+ instancedTexture.stopInstanceAnimation(zombie1.id, true); // stop, and revert to its original frame
773
684
 
774
- | Tokens | Buttons |
775
- | ------------------------------------------------------------------------------------- | -------------------------------------- |
776
- | `south`/`a`/`cross`, `east`/`b`/`circle`, `west`/`x`/`square`, `north`/`y`/`triangle` | face buttons |
777
- | `l1`/`lb`, `r1`/`rb`, `l2`/`lt`, `r2`/`rt` | shoulders / triggers |
778
- | `select`/`back`/`view`/`share`, `start`/`menu`/`options`, `guide`/`home` | center |
779
- | `l3`/`leftStick`, `r3`/`rightStick` | stick clicks |
780
- | `dpadUp`/`up`, `dpadDown`/`down`, `dpadLeft`/`left`, `dpadRight`/`right` | d-pad |
781
- | `pad:<i>:<n>` | raw button index (mapping-independent) |
782
- | `pad:<i>:axis<n>+` / `axis<n>-` | analog axis past the deadzone |
783
- | `pad:<i>:leftStickUp/Down/Left/Right`, `rightStick...` | analog stick as a d-pad |
685
+ // Or animate every instance together, in lockstep: current ones immediately,
686
+ // and any added later automatically join in:
687
+ instancedTexture.playAnimation([0, 1, 2, 3], 150); // loops
688
+ instancedTexture.playAnimationOnce([10, 11, 12], 150); // plays once, holds the last frame
689
+ instancedTexture.getAnimation(); // -> the frames array currently set this way
690
+ instancedTexture.stopAnimation(); // stop the shared animation on every instance
691
+ ```
784
692
 
785
- ### Analog feel: radial deadzone, rescaling, response curve
693
+ `playAnimation`/`playAnimationOnce` set the animation every instance plays by default; `animateInstance` overrides that for one instance specifically (a boss that should stay in its own attack animation while the rest of the horde keeps walking, say).
786
694
 
787
- `getGamepadStick` applies a **radial** deadzone (on the stick's distance from
788
- center, not per axis), so diagonals aren't clipped square and direction is
789
- preserved exactly. Values are **rescaled** — 0 at the deadzone edge, 1 at full
790
- deflection — so there's no jump at the threshold and the full range stays
791
- reachable. An optional response curve shapes the middle:
695
+ ## Physics Engine
792
696
 
793
- ```javascript
794
- input.setGamepadDeadzone(0.25);
795
- input.setGamepadCurve(2); // finer control near center (great for camera sticks)
796
- const { x, y, magnitude, angle } = input.getGamepadStick("left", 0, { invertY: true });
797
- ```
697
+ Emerald ships its own 2D rigid-body physics engine, with no external dependency. It has a dynamic AABB tree broadphase, a separating-axis narrowphase, an impulse solver with warm starting so stacks settle instead of sinking or jittering, island-based sleeping, and continuous collision detection for fast bodies.
798
698
 
799
- Semantic stick-direction tokens (`pad:0:leftStickUp`...) fire once the rescaled
800
- value passes `stickPressThreshold` (default 0.5, configurable), so drift never
801
- triggers menus. Common **DirectInput** pads — Logitech Dual Action, generic
802
- Twin-USB PS2 adapters, 8BitDo in D-input mode — are recognized out of the box;
803
- `InputManager.registerGamepadMapping()` overrides always win.
699
+ ### Setting up Physics
804
700
 
805
701
  ```javascript
806
- input.mapAction("jump", ["Space", "pad:0:south"]);
807
- input.mapAction("dash", ["Shift", "pad:0:west", "pad:0:r2"]); // X or right trigger
808
- if (input.justPressed("jump")) player.jump();
702
+ import { Physics } from "emeraldengine";
703
+ /*
704
+ ARGUMENTS:
705
+ 1. gravity: number = Gravity force (negative for downward)
706
+ 2. scale: number = Scale factor for physics units to pixels
707
+ 3. velocityThreshold: number = Minimum velocity threshold
708
+ */
709
+ const physics = new Physics(-70, 32, 2);
809
710
  ```
810
711
 
811
- ### Analog sticks, triggers & rumble
712
+ The simulation runs in physics units (meters, radians, seconds); `scale` is the pixels-per-meter conversion. Aim for bodies roughly 0.1–10 units in size; that's the range the solver tolerances are tuned for. The world itself lives at `physics.world`, and the raw classes are in `src/physics` if you want a bare world without the pixel wrapper.
812
713
 
813
- ```javascript
814
- const { x, y } = input.getGamepadStick("left"); // deadzoned -1..1
815
- const aim = input.getGamepadStick("right");
816
- const t = input.getGamepadTrigger("right"); // 0..1
817
- input.getGamepadButton("south").pressed; // also .value, .index
818
- input.setGamepadDeadzone(0.25);
714
+ You need to process physics in your own update loop:
819
715
 
820
- input.rumble(0, { duration: 120, strong: 0.6, weak: 0.4 }); // where supported
716
+ ```javascript
717
+ const animate = (currentTime) => {
718
+ physics.process(deltaTime);
719
+ };
821
720
  ```
822
721
 
823
- ### Connection events & diagnostics
722
+ ### Time stepping
824
723
 
825
- ```javascript
826
- input.onGamepadConnected((info) =>
827
- console.log(info.id, info.mapping, info.buttonCount, info.axesCount)
828
- );
829
- input.onGamepadDisconnected((info) => pauseFor(info.index));
724
+ `process(dt)` turns a frame's elapsed time into simulation steps, in one of two modes.
830
725
 
831
- input.isGamepadConnected(0);
832
- input.getGamepadInfo(0); // { id, mapping, buttonCount, axesCount, standard }
833
- input.getPressedButtons(0); // raw indices currently pressed (layout discovery)
726
+ ```javascript
727
+ physics.setFixedTimeStep(1 / 60, 5); // step, max substeps (default)
728
+ physics.setVariableTimeStep(1 / 30, 5); // max step, max substeps
834
729
  ```
835
730
 
836
- ### Non-standard controllers
731
+ **Fixed** banks real time and simulates constant-size slices. It's deterministic: the same inputs give the same result on every machine, and a slow frame can't destabilise the solver. The catch is that motion updates at the step rate, not the display rate, so on a high-refresh screen anything moved in the render frame can slide against sprites that only move every other step.
837
732
 
838
- Most pads (and anything via XInput / Steam Input) report `mapping === "standard"`
839
- and work out of the box. For a controller that reports a non-standard mapping (its
840
- raw button indices differ), register a mapping once — it only applies to pads
841
- whose id matches **and** that aren't already standard, so a correctly-reporting
842
- pad is never remapped:
733
+ **Variable** advances once per call using the frame's own delta, so physics runs at exactly the rendering rate. It's not deterministic, and a long frame is a coarser solve. Frames longer than `maxStep` are split into equal steps rather than simulated in one lump, up to `maxSubSteps`.
843
734
 
844
735
  ```javascript
845
- InputManager.registerGamepadMapping("my-controller-id", {
846
- buttons: { south: 1, east: 2, west: 0, north: 3, start: 9 },
847
- });
736
+ const steps = physics.process(dt); // how many steps actually ran
737
+ physics.getTimeStepMode(); // "fixed" | "variable"
738
+ physics.getInterpolationAlpha(); // 0..1 through the current fixed step, for interpolating renderables
848
739
  ```
849
740
 
850
- D-pads reported as a hat axis (instead of buttons 12–15) are decoded into the
851
- `dpad*` tokens automatically.
852
-
853
- ## Per-instance color & tinting
741
+ ### RigidBody methods
854
742
 
855
- `InstancedTexture` now supports an independent RGBA tint per instance (white =
856
- unchanged, so existing scenes render identically). The tint multiplies the
857
- texture in the shader.
743
+ Position, velocity, forces, sleep state, mass: every operation the physics engine supports is a method on `RigidBody` itself, in world (pixel) units; the engine converts to/from physics units internally, so you never touch the scale factor.
858
744
 
859
745
  ```javascript
860
- import { Instance, Color } from "emeraldengine";
861
-
862
- const inst = new Instance("coin", position, scale, rotation, frame);
863
- instancedTexture.addInstance(inst);
864
-
865
- // On the Instance directly (Color uses 0..255 channels; raw form is 0..1):
866
- inst.setColor(new Color(255, 120, 60)); // warm tint
867
- inst.setColor(1.0, 0.4, 0.2, 1.0); // same, as raw 0..1 RGBA
746
+ // Position, rotation, velocity
747
+ rigidBody.setPosition(new Vector2(100, 200));
748
+ rigidBody.setTransform(new Vector2(100, 200), Math.PI / 2); // position + angle, atomically
749
+ rigidBody.getPosition(); // Vector2, current position (not the spawn point)
750
+ rigidBody.getInitialPosition(); // Vector2, the position it was created at
751
+ rigidBody.setRotation(Math.PI);
752
+ rigidBody.setLinearVelocity(380, 0); // world units per second
753
+ rigidBody.getLinearVelocity(); // { x, y } in world units/sec
754
+ rigidBody.setAngularVelocity(2); // radians/sec
755
+ rigidBody.getAngularVelocity();
756
+ rigidBody.getLinearVelocityFromWorldPoint({ x, y }); // velocity at a point on the body, spin included
757
+
758
+ // Forces and impulses (world units)
759
+ rigidBody.applyForce(fx, fy); // accumulates; cleared automatically each step
760
+ rigidBody.applyForce(fx, fy, { x, y }); // applied off-center, adds torque
761
+ rigidBody.applyForceToCenter(fx, fy); // never adds torque
762
+ rigidBody.applyTorque(torque);
763
+ rigidBody.applyImpulse(ix, iy); // instantaneous, at the center: jumps, knockback
764
+ rigidBody.applyImpulse(ix, iy, { x, y }); // instantaneous, off-center, adds spin
765
+ rigidBody.applyAngularImpulse(impulse);
766
+
767
+ // Sleeping, activity, rotation lock
768
+ rigidBody.setAwake(true); // wake (or sleep) the body
769
+ rigidBody.isAwake();
770
+ rigidBody.setSleepingAllowed(false); // this body should never sleep
771
+ rigidBody.isSleepingAllowed();
772
+ rigidBody.setActive(false); // pull out of collision detection without destroying it
773
+ rigidBody.isActive();
774
+ rigidBody.setFixedRotation(true); // lock rotation at runtime
775
+ rigidBody.isFixedRotation();
776
+ rigidBody.setType("kinematic"); // change body type at runtime
777
+
778
+ // Damping, gravity, mass
779
+ rigidBody.setLinearDamping(0.5);
780
+ rigidBody.getLinearDamping();
781
+ rigidBody.setAngularDamping(0.2);
782
+ rigidBody.getAngularDamping();
783
+ rigidBody.setGravityScale(2); // 0 disables gravity for this body, 2 doubles it
784
+ rigidBody.getGravityScale();
785
+ rigidBody.getMass();
786
+ rigidBody.getInertia();
787
+ rigidBody.resetMassData(); // re-derive from fixtures after changing a density
788
+ rigidBody.setMassData({ mass, center, I }); // override directly; center is world units
789
+
790
+ // Local/world point and vector conversions (world units in and out)
791
+ rigidBody.getWorldPoint(localPoint);
792
+ rigidBody.getLocalPoint(worldPoint);
793
+ rigidBody.getWorldVector(localVector);
794
+ rigidBody.getLocalVector(worldVector);
795
+
796
+ // Fixtures, without going through a Collider component
797
+ rigidBody.createFixture(shape, { density, friction, restitution });
798
+ rigidBody.destroyFixture(fixture);
799
+
800
+ // Your own data, and the world/contacts this body belongs to
801
+ rigidBody.setUserData({ kind: "crate", hp: 3 });
802
+ rigidBody.getUserData();
803
+ rigidBody.getWorld();
804
+ rigidBody.getContactList();
805
+ ```
806
+
807
+ `setUserData`/`getUserData` are entirely separate from the physics engine's own internal bookkeeping (which is how collisions get routed back to this RigidBody), so setting your own data can never interfere with that.
868
808
 
869
- // Or drive it through the InstancedTexture by index:
870
- instancedTexture.updateInstanceColor(0); // re-read instance 0's tint
871
- instancedTexture.updateAllInstanceColors(); // re-read every instance's tint
872
- ```
809
+ ### Collision Detection
873
810
 
874
- Tip: for additive sparkle/coin glows, set the instanced texture's blend mode:
875
- `instancedTexture.setBlendMode("additive")`.
811
+ ```javascript
812
+ // Handle collision enter
813
+ physics.onCollisionEnter((bodyA, bodyB, contact) => {
814
+ console.log("Collision started!");
876
815
 
877
- ### Per-instance atlas regions (setTexCoords)
816
+ // Get collision normal. It always points from fixture A's body towards
817
+ // fixture B's, so read it relative to the body you care about:
818
+ const manifold = contact.getWorldManifold();
819
+ const normal = manifold.normal;
820
+ const facingPlayer = contact.getFixtureA().getBody() === playerBody;
821
+ const n = facingPlayer ? normal : { x: -normal.x, y: -normal.y };
822
+ // n.y = -1 the player is standing on something (with y-down gravity)
823
+ // n.y = 1 the player hit a ceiling
824
+ // n.x = -1 / 1 the player hit a wall on that side
825
+ // manifold.separations[i] is how deep contact point i is (negative = overlap)
878
826
 
879
- Normally every instance samples the shared frame grid (`instance.frame`). With
880
- `setTexCoords` an instance carries its **own UV quad**, so a single
881
- InstancedTexture one draw call — can batch tiles from an atlas with margins
882
- and spacing, apply per-instance flips, or mix arbitrary sprite regions:
827
+ // Check if bodies are sensors
828
+ const fixtureA = contact.getFixtureA();
829
+ const fixtureB = contact.getFixtureB();
830
+ if (fixtureA.isSensor() || fixtureB.isSensor()) {
831
+ // Handle sensor collision
832
+ }
833
+ });
883
834
 
884
- ```javascript
885
- const inst = new InstancedTexture("atlas.png", count, 0, 0, 1, 1, 0, false, true, false);
886
- const tile = new Instance("tile", new Vector3(x, y, 0), new Vector2(32, 32), rotation);
887
- // 8 floats, one vec2 per corner in getFrameTexCoords order: (R,B) (L,B) (R,T) (L,T)
888
- tile.setTexCoords([right, bottom, left, bottom, right, top, left, top]);
889
- inst.addInstance(tile);
890
- inst.setStatic(true); // non-moving batch: matrices upload once
835
+ // Handle collision exit
836
+ physics.onCollisionExit((bodyA, bodyB, contact) => {
837
+ console.log("Collision ended!");
838
+ });
891
839
  ```
892
840
 
893
- This is exactly how the Tile Forge level loader renders a whole layer of
894
- sliced, rotated, flipped tiles as one draw call. Pass `null` to return an
895
- instance to the frame grid.
841
+ Per-object collision events also fire automatically on `Behaviour` components (`onCollisionEnter`/`onCollisionExit`) for any GameObject with a RigidBody. See [Behaviour](#behaviour-component-lifecycle).
896
842
 
897
- ## Physics: collision layers & continuous detection
843
+ ### Collision layers
898
844
 
899
- ### Collision filtering (layers)
900
-
901
- `CollisionLayers` maps human-readable layer names to the category bits planck uses
902
- for filtering, so you can express "players collide with ground and enemies, but
903
- not each other" without juggling bitmasks. Two fixtures collide only when each
904
- one's category is in the other's mask (symmetric by construction; up to 16
905
- layers).
845
+ `CollisionLayers` maps human-readable layer names to the category bits the physics engine uses for filtering, so you can express "players collide with ground and enemies, but not each other" without juggling bitmasks. Two fixtures collide only when each one's category is in the other's mask.
906
846
 
907
847
  ```javascript
908
848
  import { CollisionLayers } from "emeraldengine";
@@ -914,7 +854,7 @@ playerCollider
914
854
  .setCollidesWith(["ground", "enemy", "pickup"]);
915
855
  enemyCollider.setCategory("enemy").setCollidesWith(["ground", "player"]); // ignore each other
916
856
 
917
- // Or up front, in the collider constructor (8th arg):
857
+ // Or up front, in the collider constructor's filter argument:
918
858
  new BoxCollider(body, size, 1, 0.2, 0, false, gameObject, {
919
859
  category: "pickup",
920
860
  collidesWith: ["player"],
@@ -924,44 +864,29 @@ new BoxCollider(body, size, 1, 0.2, 0, false, gameObject, {
924
864
  collider.setFilter({ category: 0x0004, mask: 0xffff, group: 0 });
925
865
  ```
926
866
 
927
- ### Continuous collision detection (CCD)
867
+ ### Continuous collision detection
928
868
 
929
- Fast bodies (a dash, a projectile, a hard fall) can move far enough in one physics
930
- step to tunnel through thin walls. Mark them continuous so planck solves the swept
931
- path against static geometry instead:
869
+ Fast bodies (a dash, a projectile, a hard fall) can move far enough in one physics step to tunnel through thin walls. Mark them continuous so the engine sweeps their path against static geometry instead of testing only where they ended up:
932
870
 
933
871
  ```javascript
934
872
  projectile.setContinuous(true); // bullet-mode CCD
935
873
  projectile.isContinuous(); // boolean
936
874
  ```
937
875
 
938
- Reserve it for the handful of bodies that actually move fast it costs more per
939
- step.
940
-
941
- ## RigidBody velocity helpers
876
+ Reserve it for the handful of bodies that actually move fast; it costs more per step.
942
877
 
943
- `RigidBody` gained direct velocity/impulse control in **world (pixel) units** —
944
- the engine converts to/from physics units internally, so you never touch the
945
- scale factor.
878
+ ### Raycasting & point queries
946
879
 
947
880
  ```javascript
948
- rigidBody.setLinearVelocity(380, 0); // world units per second
949
- const v = rigidBody.getLinearVelocity(); // { x, y } in world units/sec
950
- rigidBody.applyImpulse(0, 900); // impulse at the body's center
951
- rigidBody.setAwake(true); // wake (or sleep) the body
881
+ const hit = physics.raycast({ x, y }, { x: 1, y: 0 }, 500);
882
+ // -> { object, rigidBody, point, normal, fraction } | null
952
883
 
953
- // Live world-space position/rotation (kept in sync with the simulation):
954
- rigidBody.getPosition(); // Vector2 — current position (not the spawn point)
955
- rigidBody.getInitialPosition(); // Vector2 — the position it was created at
884
+ const objects = physics.queryPoint({ x, y }); // owners whose collider contains the point
956
885
  ```
957
886
 
958
- These make velocity-driven movement (player controllers, knockback, dashes)
959
- straightforward without reaching into the underlying planck body.
960
-
961
- ## Tilemap colliders & auto-tiling
887
+ ### Tilemap colliders & auto-tiling
962
888
 
963
- A `Tilemap` can now generate physics colliders from its map and auto-pick tile
964
- frames from a solidity grid.
889
+ A `Tilemap` can generate physics colliders from its map and auto-pick tile frames from a solidity grid.
965
890
 
966
891
  ```javascript
967
892
  // 1) Build solid colliders from the current map.
@@ -987,35 +912,111 @@ const frames = Tilemap.computeAutoTile(solidGrid, {
987
912
  map.setAutoTiledMap(solidGrid, { base: 0, originX: 0, originY: 0 }); // compute + setMap
988
913
  ```
989
914
 
990
- ## Camera follow deadzone
915
+ ## Particle System
916
+
917
+ Emerald includes a powerful particle system for creating visual effects.
918
+
919
+ ![Particles](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/particles.gif?raw=true)
991
920
 
992
- `CameraController` now supports a centered follow deadzone: the camera only scrolls
993
- once the target leaves a box around the current focus, so small movements don't jitter
994
- the view. Chainable with the existing follow/bounds/shake API.
921
+ ### Particle Settings
995
922
 
996
923
  ```javascript
997
- const ctrl = new CameraController(camera);
998
- ctrl
999
- .follow(player.gameObject, 0.12)
1000
- .setDeadzone(90, 60) // half-extents in world units; 0/0 or null disables
1001
- .setBounds(280, cy, finishX + 200, cy);
1002
- // each frame: ctrl.update(dt);
924
+ import { ParticleSettings, Vector2, Color } from "emeraldengine";
925
+
926
+ const particleSettings = new ParticleSettings({
927
+ lifetime: 1.2,
928
+ velocity: new Vector2(200, 300),
929
+ gravity: new Vector2(0, -400),
930
+ amount: 16,
931
+ direction: new Vector2(0, 1), // upward
932
+ spread: Math.PI * 2,
933
+ emissionRate: Infinity, // one-shot emission
934
+ frame: 0,
935
+ offset: 5,
936
+ rotation: 0,
937
+ scale: new Vector2(5, 5),
938
+ animation: { frames: [0, 1, 2], speed: 200 },
939
+
940
+ // Emitter shape: where new particles spawn relative to the emit point:
941
+ // "point" | "circle" | "ring" | "box" | "cone" (default)
942
+ shape: "ring",
943
+ shapeRadius: 24, // used by circle/ring
944
+ shapeSize: new Vector2(40, 10), // used by box
945
+
946
+ // Over-lifetime curves ({ from, to } interpolated by normalized age):
947
+ scaleOverLife: { from: 1.4, to: 0.0 }, // size multiplier
948
+ alphaOverLife: { from: 1.0, to: 0.0 }, // opacity
949
+ colorOverLife: {
950
+ from: new Color(255, 240, 180),
951
+ to: new Color(255, 90, 60),
952
+ },
953
+
954
+ rotationSpeed: Math.PI, // radians/sec per particle
955
+ drag: 1.2, // velocity damping per second (0 = none)
956
+ });
957
+ ```
958
+
959
+ ### Creating Particle Systems
960
+
961
+ ```javascript
962
+ import { Particles } from "emeraldengine";
963
+
964
+ /*
965
+ ARGUMENTS:
966
+ 1. name: string = Name of the particle system
967
+ 2. texturePath: string = Path to the texture
968
+ 3. frameWidth: number = Width of each frame
969
+ 4. frameHeight: number = Height of each frame
970
+ 5. framesPerRow: number = Frames per row in spritesheet
971
+ 6. totalFrames: number = Total frames in spritesheet
972
+ 7. duration: number = Duration of the effect
973
+ 8. settings: ParticleSettings = Particle settings object
974
+ */
975
+ const particles = new Particles(
976
+ "explosion",
977
+ texturePath,
978
+ 16,
979
+ 16,
980
+ 9,
981
+ 27,
982
+ 1.2,
983
+ particleSettings
984
+ );
985
+
986
+ // Add to scene
987
+ scene.add(particles.gameObject);
988
+ ```
989
+
990
+ ### Particle System Methods
991
+
992
+ ```javascript
993
+ // Play particle effect at position
994
+ particles.play(new Vector3(x, y, z));
995
+
996
+ // Stop particle system
997
+ particles.stop();
998
+
999
+ // Reset particle system
1000
+ particles.reset();
1001
+
1002
+ // Update particles (call in your animation loop)
1003
+ particles.update(deltaTime);
1004
+
1005
+ // Check if active
1006
+ if (particles.active) {
1007
+ // Particles are currently active
1008
+ }
1003
1009
  ```
1004
1010
 
1005
- ## ParticleEmitter (pooled sprites)
1011
+ ### ParticleEmitter (simple pooled bursts)
1006
1012
 
1007
- `ParticleEmitter` is a reliable, allocation-free particle system built from a
1008
- fixed pool of ordinary textured GameObjects. Every live particle's transform, tint
1009
- and opacity are driven by hand each frame — there's no instanced-draw buffer
1010
- lifecycle to desync, so it keeps rendering for the whole session. Spawn with
1011
- `burst(n, cfg)` / `emit(cfg)` and advance with `update(dt)`; every `cfg` field is
1012
- optional.
1013
+ If you don't need per-particle curves, `ParticleEmitter` is a simpler, allocation-free system built from a fixed pool of textured GameObjects, good for one-off bursts (dust, sparkles, confetti, hit effects). Spawn with `burst(n, cfg)` / `emit(cfg)`; every `cfg` field is optional.
1013
1014
 
1014
1015
  ```javascript
1015
1016
  import { ParticleEmitter } from "emeraldengine";
1016
1017
 
1017
1018
  const fx = new ParticleEmitter(scene, {
1018
- texture: "spark.png", // sprite source (data URL / path / image)
1019
+ texture: "spark.png",
1019
1020
  capacity: 256, // pool size (max live particles)
1020
1021
  layer: 50,
1021
1022
  });
@@ -1028,7 +1029,7 @@ fx.burst(12, {
1028
1029
  speed: 180, // emission cone
1029
1030
  gx: 0,
1030
1031
  gy: -300,
1031
- drag: 2, // forces
1032
+ drag: 2,
1032
1033
  life: 0.4,
1033
1034
  size: 8,
1034
1035
  sFrom: 1,
@@ -1038,7 +1039,7 @@ fx.burst(12, {
1038
1039
  cr: 255,
1039
1040
  cg: 220,
1040
1041
  cb: 120,
1041
- additive: true, // tint + blend
1042
+ additive: true,
1042
1043
  rotSpeed: 6,
1043
1044
  shape: "ring",
1044
1045
  radius: 12, // "point" | "ring" | "circle" | "box"
@@ -1051,1309 +1052,1147 @@ fx.reset(); // kill all immediately
1051
1052
  fx.destroy(); // remove pooled objects from the scene
1052
1053
  ```
1053
1054
 
1054
- This is the go-to for one-off bursts (dust, sparkles, confetti, hit effects). For
1055
- the curve-driven, instanced system see [Advanced Particles](#advanced-particles)
1056
- below.
1055
+ ## Lighting System
1057
1056
 
1058
- ## Advanced Particles
1057
+ Emerald supports ambient, point, and directional lighting.
1059
1058
 
1060
- `ParticleSettings` gained emitter shapes and over-lifetime curves, plus per-particle
1061
- `rotationSpeed` and `drag`. These layer on top of the existing particle fields.
1059
+ ### Ambient Light
1062
1060
 
1063
1061
  ```javascript
1064
- import { ParticleSettings, Vector2, Color } from "emeraldengine";
1062
+ // Set ambient light
1063
+ emerald.setAmbientLight(new Vector3(0.3, 0.3, 0.3)); // RGB values 0-1
1064
+ ```
1065
1065
 
1066
- const settings = new ParticleSettings({
1067
- lifetime: 1.0,
1068
- amount: 24,
1069
- velocity: new Vector2(0, 260),
1070
- gravity: new Vector2(0, -300),
1066
+ ### Point Light
1071
1067
 
1072
- // Emitter shape — where new particles spawn relative to the emit point:
1073
- // "point" | "circle" | "ring" | "box" | "cone" (default)
1074
- shape: "ring",
1075
- shapeRadius: 24, // used by circle/ring
1076
- shapeSize: new Vector2(40, 10), // used by box
1068
+ ```javascript
1069
+ import { PointLight } from "emeraldengine";
1077
1070
 
1078
- // Over-lifetime curves ({ from, to } interpolated by normalized age):
1079
- scaleOverLife: { from: 1.4, to: 0.0 }, // size multiplier
1080
- alphaOverLife: { from: 1.0, to: 0.0 }, // opacity
1081
- colorOverLife: {
1082
- // 0..255 channels
1083
- from: new Color(255, 240, 180),
1084
- to: new Color(255, 90, 60),
1085
- },
1071
+ /*
1072
+ ARGUMENTS:
1073
+ 1. position: Vector2 = Position of the light
1074
+ 2. color: Color = Color of the light
1075
+ 3. intensity: number = Light intensity
1076
+ 4. radius: number = Light radius
1077
+ */
1078
+ const pointLight = new PointLight(
1079
+ new Vector2(100, 0),
1080
+ new Color(255, 204, 153),
1081
+ 1.5,
1082
+ 400
1083
+ );
1086
1084
 
1087
- rotationSpeed: Math.PI, // radians/sec per particle
1088
- drag: 1.2, // velocity damping per second (0 = none)
1089
- });
1090
- ```
1085
+ // Add to engine
1086
+ emerald.addPointLight(pointLight);
1091
1087
 
1092
- ## Positional (spatial) Audio
1088
+ // Update position
1089
+ pointLight.position.x = newX;
1090
+ pointLight.position.y = newY;
1091
+ ```
1093
1092
 
1094
- `AudioManager` can attenuate and pan sounds based on a listener position. It uses
1095
- the Web Audio `StereoPanner` when available and falls back to volume-only panning
1096
- otherwise.
1093
+ ### Directional Light
1097
1094
 
1098
1095
  ```javascript
1099
- audio.setListener(player.x, player.y); // usually the camera/player each frame
1100
- audio.setSpatialRange(100, 800); // full volume <100px, silent >800px
1096
+ import { DirectionalLight } from "emeraldengine";
1097
+
1098
+ /*
1099
+ ARGUMENTS:
1100
+ 1. position: Vector2 = Position of the light
1101
+ 2. direction: Vector2 = Direction vector
1102
+ 3. color: Color = Color of the light
1103
+ 4. intensity: number = Light intensity
1104
+ 5. width: number = Width of the light beam
1105
+ */
1106
+ const directionalLight = new DirectionalLight(
1107
+ new Vector2(0, 300),
1108
+ new Vector2(0, -1), // pointing down
1109
+ new Color(255, 255, 255),
1110
+ 3.0,
1111
+ 200
1112
+ );
1101
1113
 
1102
- audio.playSpatial("explosion", { x: 1200, y: 50 }); // one-shot, positioned
1114
+ // Add to engine
1115
+ emerald.addDirectionalLight(directionalLight);
1103
1116
 
1104
- // Pure helper (also used internally) — handy for custom routing/tests:
1105
- const { volume, pan, distance } = audio.computeSpatial({ x, y });
1117
+ // Rotate direction
1118
+ const angle = 0.1;
1119
+ const newX =
1120
+ directionalLight.direction.x * Math.cos(angle) -
1121
+ directionalLight.direction.y * Math.sin(angle);
1122
+ const newY =
1123
+ directionalLight.direction.x * Math.sin(angle) +
1124
+ directionalLight.direction.y * Math.cos(angle);
1125
+ directionalLight.direction.x = newX;
1126
+ directionalLight.direction.y = newY;
1106
1127
  ```
1107
1128
 
1108
- ## Audio buses & fades
1129
+ ## Text Rendering
1130
+
1131
+ ### BitmapText
1132
+
1133
+ Emerald supports bitmap font rendering using the BitmapText component. This allows you to display text with custom fonts and styles.
1109
1134
 
1110
- Every sound belongs to a named **mix bus** — `"music"` and `"sfx"` exist by
1111
- default (new sounds land on `"sfx"`), and any name you use creates a bus on the
1112
- fly. Effective volume is `master × bus × sound × fade`, so one slider mutes all
1113
- music without touching the SFX:
1135
+ ![BitmapText](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/bitmaptext.png?raw=true)
1114
1136
 
1115
1137
  ```javascript
1116
- audio.add("theme.mp3", "theme", { bus: "music", loop: true });
1117
- audio.add("jump.wav", "jump"); // default bus: "sfx"
1138
+ import { BitmapText } from "emeraldengine";
1118
1139
 
1119
- audio.setBusVolume("music", 0.5); // the settings-menu "music volume" slider
1120
- audio.setBusVolume("sfx", 0.8);
1121
- audio.setSoundBus("thunder", "ambience"); // move a sound, creating the bus
1122
- audio.getBusVolume("music"); // 0.5
1123
- ```
1140
+ /*
1141
+ ARGUMENTS:
1142
+ 1. text: string = Text to display
1143
+ 2. texturePath: string = Path to bitmap font texture
1144
+ 3. letters: string = String containing all available characters
1145
+ 4. letterSpacing: number = Spacing between letters
1146
+ 5. frameWidth: number = Width of each character frame
1147
+ 6. frameHeight: number = Height of each character frame
1148
+ 7. framesPerRow: number = Characters per row in font texture
1149
+ 8. totalFrames: number = Total character frames
1150
+ 9. pixelArt: boolean = Whether to use pixel art rendering
1151
+ 10. fontSize: number = Font size
1152
+ 11. color: Color = Text color
1153
+ 12. position: Vector3 = Text position
1154
+ 13. rotation: number = Text rotation
1155
+ 14. useLighting: boolean = Whether text should react to lighting
1156
+ */
1157
+ const bitmapText = new BitmapText(
1158
+ "Hello World!",
1159
+ fontTexturePath,
1160
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?.",
1161
+ 16,
1162
+ 32,
1163
+ 32,
1164
+ 10,
1165
+ 95,
1166
+ true,
1167
+ 24,
1168
+ new Color(255, 255, 255),
1169
+ new Vector3(0, 200, 0),
1170
+ 0,
1171
+ false
1172
+ );
1124
1173
 
1125
- Fades run on the manager's clock (self-driven via rAF by default; pass
1126
- `{ autoTick: false }` and call `audio.update(dt)` yourself to tie them to the
1127
- game loop):
1174
+ // Add to scene
1175
+ scene.add(bitmapText.gameObject);
1128
1176
 
1129
- ```javascript
1130
- audio.fadeIn("theme", 1.5); // play from silence to full over 1.5s
1131
- audio.fadeOut("theme", 2.0); // fade to silence, then stop
1132
- audio.fadeTo("theme", 0.2, 0.5); // duck under dialogue
1133
- audio.crossfade("theme", "boss", 2.0); // level -> boss music, one call
1177
+ // Update text
1178
+ bitmapText.setText("New Text!");
1179
+ bitmapText.setColor(new Color(255, 0, 0));
1180
+ bitmapText.setFontSize(32);
1181
+ bitmapText.setLetterSpacing(20);
1134
1182
  ```
1135
1183
 
1136
- ## AssetManager (loading)
1184
+ ### CanvasText
1137
1185
 
1138
- One async loader for everything a game needs at startup images/textures, audio,
1139
- JSON, text, and web fonts — with deduplication and aggregate progress for a
1140
- loading bar. Images are routed through `TextureManager`, so the GL upload cache is
1141
- shared with the rest of the engine.
1186
+ `CanvasText` renders any CSS font (including loaded webfonts) into a texture, at the device pixel ratio so text is crisp on Retina/HiDPI displays, with support for multi-line strings, word-wrapping, and alignment. Use `BitmapText` for retro/pixel fonts from a glyph sheet, `CanvasText` for everything else (UI, dialogue, any real font).
1142
1187
 
1143
1188
  ```javascript
1144
- import { AssetManager } from "emeraldengine";
1189
+ import { CanvasText } from "emeraldengine";
1145
1190
 
1146
- const assets = new AssetManager();
1147
- assets
1148
- .image("player", "player.png", { pixelart: true })
1149
- .audio("jump", "jump.wav")
1150
- .json("level1", "levels/1.json")
1151
- .text("credits", "credits.txt")
1152
- .font("Press Start 2P", "fonts/press-start.woff2");
1191
+ // Factory: returns a GameObject already sized to the text
1192
+ const label = CanvasText.create("Score: 0", {
1193
+ font: "700 24px 'Pixelify Sans', sans-serif",
1194
+ color: "#8fe0ff",
1195
+ screenSpace: true, // HUD: fixed on screen, position in px from center
1196
+ position: new Vector3(0, 240, 0),
1197
+ });
1198
+ scene.add(label);
1153
1199
 
1154
- assets.onProgress((loaded, total) => bar.set(loaded / total));
1155
- await assets.load({ continueOnError: false }); // rejects on a failed asset unless true
1200
+ // Multi-line + wrapping
1201
+ const dialog = CanvasText.create(
1202
+ "A long line of dialogue that wraps automatically.\nExplicit breaks work too.",
1203
+ { font: "16px system-ui", maxWidth: 320, align: "left", lineHeight: 22 }
1204
+ );
1156
1205
 
1157
- assets.get("player"); // HTMLImageElement
1158
- assets.get("level1"); // parsed JSON
1159
- assets.has("jump"); // boolean
1160
- assets.progress(); // 0..1
1161
- await assets.getTexture("player"); // { texture, width, height } from the GL cache
1162
- assets.clear();
1206
+ // Updating (re-renders the texture; attached GameObject rescales to fit)
1207
+ const text = label.getComponent(CanvasText);
1208
+ text.setText("Score: 120");
1209
+ text.setColor("#ffd166");
1210
+ text.setMaxWidth(400);
1211
+ text.setAlign("center"); // "left" | "center" | "right"
1163
1212
  ```
1164
1213
 
1165
- ## Asset importers (Tiled & Aseprite)
1214
+ ## Input
1166
1215
 
1167
- Import maps from [Tiled](https://www.mapeditor.org) and sprite-sheet animations
1168
- from [Aseprite](https://www.aseprite.org). Both are pure parsers — hand them the
1169
- already-parsed JSON (load it with `AssetManager.json` or `fetch`).
1216
+ Emerald supports keyboard, mouse, click, and hover events through the built-in `EventManager` class, plus a higher-level action-mapping `InputManager` for gameplay input (keyboard, mouse and gamepad through one API).
1170
1217
 
1171
- ### Tiled maps
1218
+ ![EventManager](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/eventmanager.gif?raw=true)
1172
1219
 
1173
1220
  ```javascript
1174
- import { TiledMap } from "emeraldengine";
1175
-
1176
- // Build a ready-to-render Tilemap from a Tiled JSON map + its tile sheet.
1177
- const map = TiledMap.toTilemap(mapJson, "tiles.png", { layer: "ground" });
1178
- scene.add(map.gameObject);
1179
- map.buildColliders(physics);
1180
-
1181
- // Or just the frame grid (for your own Tilemap.setMap call):
1182
- const grid = TiledMap.toFrameGrid(mapJson, { layer: "ground" });
1221
+ import { EventManager } from "emeraldengine";
1183
1222
 
1184
- // Object layers (spawn points, triggers) as plain data; Tiled `properties` are
1185
- // flattened into `props`, and flipY converts to a y-up world.
1186
- const spawns = TiledMap.objects(mapJson, { layer: "spawns", flipY: true });
1187
- // -> [{ name, type, x, y, width, height, gid, props, ... }]
1223
+ let eventManager = new EventManager(canvas, scene, emerald.camera);
1188
1224
  ```
1189
1225
 
1190
- ### Aseprite sheets
1226
+ `EventManager` hits only the topmost object under the pointer, and its `screenToWorld(clientX, clientY)` accounts for camera zoom, DPR, and viewport.
1227
+
1228
+ ### Keyboard Events
1191
1229
 
1192
1230
  ```javascript
1193
- import { Aseprite, Texture, Animator } from "emeraldengine";
1231
+ // Key down events
1232
+ eventManager.addKeyDown("w", () => {
1233
+ console.log("W key pressed");
1234
+ });
1194
1235
 
1195
- const cfg = Aseprite.spriteConfig(sheetJson); // { frameWidth, frameHeight, framesPerRow, totalFrames }
1196
- const tex = new Texture(
1197
- "hero.png",
1198
- cfg.frameWidth,
1199
- cfg.frameHeight,
1200
- cfg.framesPerRow,
1201
- cfg.totalFrames,
1202
- 0,
1203
- false
1204
- );
1205
- obj.addComponent(tex);
1236
+ // Key up events
1237
+ eventManager.addKeyUp("w", () => {
1238
+ console.log("W key released");
1239
+ });
1206
1240
 
1207
- const anim = new Animator();
1208
- obj.addComponent(anim);
1209
- Aseprite.applyTo(anim, sheetJson); // registers a clip per frame-tag
1210
- anim.play("run");
1241
+ // Check if key is currently pressed
1242
+ if (eventManager.isKeyPressed("w")) {
1243
+ // W key is currently held down
1244
+ }
1211
1245
 
1212
- // Or inspect the clips yourself (handles forward/reverse/pingpong):
1213
- Aseprite.toClips(sheetJson); // -> [{ name, frames:[...], speed }]
1246
+ // Remove key events
1247
+ eventManager.removeKeyDown("w", callbackFunction);
1248
+ eventManager.removeKeyUp("w", callbackFunction);
1214
1249
  ```
1215
1250
 
1216
- ## Networking (NetworkManager + Interpolator)
1217
-
1218
- A thin, **optional** multiplayer layer over [Colyseus](https://colyseus.io).
1219
- `colyseus.js` is a peer dependency imported dynamically, so games that don't use
1220
- networking never load it. It bundles an `Interpolator` for smooth remote entities.
1251
+ ### Mouse Events
1221
1252
 
1222
1253
  ```javascript
1223
- import { NetworkManager } from "emeraldengine";
1254
+ // Get mouse position
1255
+ const mousePos = eventManager.getMousePosition();
1256
+ console.log(mousePos.x, mousePos.y);
1224
1257
 
1225
- const net = new NetworkManager({ interpolation: { delay: 0.1 } });
1226
- await net.connect("wss://my-server:2567"); // dynamically imports colyseus.js
1227
- const room = await net.join("arena", { name: "P1" });
1258
+ // Check if camera was moved
1259
+ if (eventManager.wasCameraMoved()) {
1260
+ // Camera was moved by dragging
1261
+ eventManager.resetCameraMoved();
1262
+ }
1263
+ ```
1228
1264
 
1229
- net.onMessage("hit", (msg) => applyHit(msg));
1230
- net.onStateChange((state) => {
1231
- for (const [id, p] of state.players) net.interpolator.push(id, p, net.now());
1265
+ ### Object Events
1266
+
1267
+ ```javascript
1268
+ // Click events
1269
+ eventManager.addClickEvent(gameObject, (event, object) => {
1270
+ console.log("Object clicked!");
1232
1271
  });
1233
- net.onLeave((code) => showDisconnected(code));
1234
1272
 
1235
- net.send("move", { dir: 1 });
1236
- net.sessionId; // this client's id
1237
- await net.leave();
1273
+ // Hover events
1274
+ eventManager.addHoverEvent(
1275
+ gameObject,
1276
+ (event) => {
1277
+ console.log("Mouse entered object");
1278
+ },
1279
+ (event) => {
1280
+ console.log("Mouse left object");
1281
+ }
1282
+ );
1238
1283
 
1239
- // each frame, render remote entities "in the past" for smoothness:
1240
- const pos = net.interpolator.sample(remoteId, net.now()); // { x, y } | null
1284
+ // Remove events
1285
+ eventManager.removeClickEvent(gameObject, callbackFunction);
1286
+ eventManager.removeHoverEvent(gameObject, enterCallback, leaveCallback);
1241
1287
  ```
1242
1288
 
1243
- `Interpolator` is also exported standalone and is pure (no network/DOM), so you can
1244
- use it with any transport or in tests:
1289
+ ### Event Cleanup
1245
1290
 
1246
1291
  ```javascript
1247
- import { Interpolator } from "emeraldengine";
1292
+ // Clean up all events when done
1293
+ eventManager.clean();
1248
1294
 
1249
- const interp = new Interpolator({ delay: 0.1, maxBuffer: 60 });
1250
- interp.push(entityId, { x, y }, serverTimeSeconds); // on each authoritative update
1251
- const smoothed = interp.sample(entityId, nowSeconds); // each frame
1252
- interp.prune(nowSeconds); // bound memory for long-lived entities
1253
- interp.remove(entityId); // when an entity leaves
1254
- interp.clear();
1295
+ // Change scene
1296
+ eventManager.changeScene(newScene);
1255
1297
  ```
1256
1298
 
1257
- ## DebugOverlay (upgraded)
1299
+ ### InputManager (actions)
1258
1300
 
1259
- The overlay now shows a frame-time sparkline with min/avg/max milliseconds and heap
1260
- usage, accepts custom metric rows, and can visualize physics colliders.
1301
+ For gameplay, `InputManager` lets you bind named actions once and read them everywhere: keyboard keys, mouse buttons, gamepad buttons and analog stick directions are all just tokens:
1261
1302
 
1262
1303
  ```javascript
1263
- import { DebugOverlay } from "emeraldengine";
1264
-
1265
- const debug = new DebugOverlay();
1266
- debug.setVisible(true); // toggle (e.g. bind to F3)
1267
- debug.setMetric("enemies", enemies.length); // add/refresh a custom row
1268
- debug.showColliders(scene, true); // overlay collider shapes for the scene
1269
- // after drawScene each frame:
1270
- debug.update(emerald, scene); // FPS / frame-time graph / objects / cameras
1271
- debug.destroy();
1304
+ import { InputManager } from "emeraldengine";
1305
+ const input = new InputManager();
1306
+ input.mapAction("jump", ["Space", " ", "pad:0:south"]); // keyboard + gamepad
1307
+ input.mapAction("left", ["a", "ArrowLeft", "pad:0:dpadLeft"]);
1308
+ // in the loop:
1309
+ if (input.justPressed("jump")) player.jump();
1310
+ const move = input.getAxis("left", "right"); // -1 / 0 / +1
1311
+ input.update(); // call once per frame (edge detection + gamepad polling)
1272
1312
  ```
1273
1313
 
1274
- ## Storage (versioned saves)
1314
+ ### Gamepads & Controllers
1275
1315
 
1276
- `Storage.save`/`Storage.load` wrap your data in a versioned envelope
1277
- (`{ v, t, data }`) with an automatic `.bak` mirror, so saves survive both
1278
- corrupted writes (a torn write recovers from backup) and **schema changes**
1279
- (old saves migrate forward instead of being discarded):
1316
+ Full controller support is built into `InputManager`: analog sticks/triggers, semantic button names that resolve through each pad's mapping, rumble, connect/disconnect events, and a registry for non-standard controllers. Gamepad input flows through the same `isDown`/`justPressed`/`getAxis` machinery as the keyboard, so a token like `"pad:0:south"` works anywhere a key token does.
1280
1317
 
1281
- ```javascript
1282
- import { Storage } from "emeraldengine";
1318
+ `pad:<i>:<name>` targets pad index `<i>`. Names resolve through the pad's mapping, so `south` is always the bottom face button whether the pad reports Xbox or PlayStation ordering:
1283
1319
 
1284
- // Write: version + timestamp envelope, plus a .bak backup by default.
1285
- Storage.save("profile", { level: 3, coins: 120 }, { version: 2 });
1320
+ | Tokens | Buttons |
1321
+ | -------------------------------------------------------------------------------------- | --------------------------------------- |
1322
+ | `south`/`a`/`cross`, `east`/`b`/`circle`, `west`/`x`/`square`, `north`/`y`/`triangle` | face buttons |
1323
+ | `l1`/`lb`, `r1`/`rb`, `l2`/`lt`, `r2`/`rt` | shoulders / triggers |
1324
+ | `select`/`back`/`view`/`share`, `start`/`menu`/`options`, `guide`/`home` | center |
1325
+ | `l3`/`leftStick`, `r3`/`rightStick` | stick clicks |
1326
+ | `dpadUp`/`up`, `dpadDown`/`down`, `dpadLeft`/`left`, `dpadRight`/`right` | d-pad |
1327
+ | `pad:<i>:<n>` | raw button index (mapping-independent) |
1328
+ | `pad:<i>:axis<n>+` / `axis<n>-` | analog axis past the deadzone |
1329
+ | `pad:<i>:leftStickUp/Down/Left/Right`, `rightStick...` | analog stick as a d-pad |
1286
1330
 
1287
- // Read: falls back, recovers from backup, and migrates old versions.
1288
- const profile = Storage.load("profile", {
1289
- version: 2,
1290
- fallback: { level: 1, coins: 0 },
1291
- migrate: (old, fromVersion) => {
1292
- // v1 saves had no coins field — upgrade them instead of losing progress
1293
- return { ...old, coins: old.coins ?? 0 };
1294
- },
1295
- // rewrite: true (default) re-saves migrated data in the new format
1296
- });
1331
+ `getGamepadStick` applies a radial deadzone (on the stick's distance from center, not per axis), rescaled so there's no jump at the threshold, with an optional response curve:
1297
1332
 
1298
- Storage.hasSave("profile"); // true
1299
- Storage.removeSave("profile"); // deletes the save AND its backup
1333
+ ```javascript
1334
+ input.setGamepadDeadzone(0.25);
1335
+ input.setGamepadCurve(2); // finer control near center (great for camera sticks)
1336
+ const { x, y, magnitude, angle } = input.getGamepadStick("left", 0, {
1337
+ invertY: true,
1338
+ });
1300
1339
  ```
1301
1340
 
1302
- Plain pre-versioning values load as version 0, so adopting the envelope on an
1303
- existing game is safe. (For whole-scene snapshots, see
1304
- [Serializer](#serializer-saveload-scenes); for raw key/value access,
1305
- `Storage.saveToLocalStorage` / `readFromLocalStorage` still exist.)
1341
+ Sticks, triggers and rumble:
1306
1342
 
1307
- ## EmeraldDB (IndexedDB saves)
1343
+ ```javascript
1344
+ const { x, y } = input.getGamepadStick("left"); // deadzoned -1..1
1345
+ const aim = input.getGamepadStick("right");
1346
+ const t = input.getGamepadTrigger("right"); // 0..1
1347
+ input.getGamepadButton("south").pressed; // also .value, .index
1348
+
1349
+ input.rumble(0, { duration: 120, strong: 0.6, weak: 0.4 }); // where supported
1350
+ ```
1308
1351
 
1309
- `Storage` lives on localStorage, which caps out around **5MB** — plenty for
1310
- settings and high scores, not for a big persistent world (every tile's state,
1311
- chests, NPC relationships...). `EmeraldDB` is the async, big-world companion:
1312
- same versioned envelope, `.bak` backup, and migration semantics, backed by
1313
- IndexedDB — effectively unlimited, and values are **structured-cloned** (no
1314
- JSON round-trip), so Maps, Sets, Dates, and typed arrays save as-is and large
1315
- saves stay fast.
1352
+ Connection events and diagnostics:
1316
1353
 
1317
1354
  ```javascript
1318
- import { EmeraldDB } from "emeraldengine";
1355
+ input.onGamepadConnected((info) =>
1356
+ console.log(info.id, info.mapping, info.buttonCount, info.axesCount)
1357
+ );
1358
+ input.onGamepadDisconnected((info) => pauseFor(info.index));
1319
1359
 
1320
- // Versioned world save — mirrors Storage.save/load, but async:
1321
- await EmeraldDB.save("world", world, { version: 3 });
1322
- const world = await EmeraldDB.load("world", {
1323
- version: 3,
1324
- fallback: makeNewWorld(),
1325
- migrate: (old, fromVersion) => upgradeWorld(old, fromVersion),
1326
- });
1327
- await EmeraldDB.hasSave("world"); // true (checks the .bak too)
1328
- await EmeraldDB.removeSave("world"); // deletes save + backup
1360
+ input.isGamepadConnected(0);
1361
+ input.getGamepadInfo(0); // { id, mapping, buttonCount, axesCount, standard }
1362
+ input.getPressedButtons(0); // raw indices currently pressed (layout discovery)
1363
+ ```
1329
1364
 
1330
- // Plain async key/value (no envelope):
1331
- await EmeraldDB.set("settings", { volume: 0.8, keybinds: new Map() });
1332
- const settings = await EmeraldDB.get("settings", {});
1333
- await EmeraldDB.keys(); // every key in the store
1365
+ Most pads (and anything via XInput / Steam Input) report `mapping === "standard"` and work out of the box. Common DirectInput pads (Logitech Dual Action, generic Twin-USB PS2 adapters, 8BitDo in D-input mode) are recognized out of the box too. For anything else, register a mapping once; it only applies to pads whose id matches and that aren't already standard:
1334
1366
 
1335
- // Optional setup:
1336
- EmeraldDB.configure({ name: "my-game", store: "saves" }); // before first use
1337
- EmeraldDB.isSupported(); // feature-detect (falls back to Storage if false)
1338
- await EmeraldDB.importFromStorage("profile"); // one-time upgrade of an old localStorage save
1367
+ ```javascript
1368
+ InputManager.registerGamepadMapping("my-controller-id", {
1369
+ buttons: { south: 1, east: 2, west: 0, north: 3, start: 9 },
1370
+ });
1339
1371
  ```
1340
1372
 
1341
- The backup write happens **in the same IndexedDB transaction** as the save, so
1342
- a crash mid-write can never leave you with both copies torn. Rule of thumb:
1343
- `Storage` for small synchronous bits (settings, best times), `EmeraldDB` for
1344
- the world.
1373
+ D-pads reported as a hat axis (instead of buttons 12–15) are decoded into the `dpad*` tokens automatically.
1374
+
1375
+ ## AudioManager
1345
1376
 
1346
- ## Resolution independence (design resolution)
1377
+ Emerald includes a comprehensive audio management system.
1347
1378
 
1348
- Author your game at one fixed resolution and let the engine scale it to any
1349
- screen. The world renders at the design size and is fitted per mode; input
1350
- helpers convert back, so gameplay code never sees the difference:
1379
+ ### Adding Audio
1351
1380
 
1352
1381
  ```javascript
1353
- // Design at 960x540, letterboxed onto whatever screen the player has:
1354
- emerald.setDesignResolution(960, 540, "fit");
1382
+ import { AudioManager } from "emeraldengine";
1355
1383
 
1356
- // Modes:
1357
- // "fit" letterbox — whole design visible, bars if aspect differs
1358
- // "fill" cover — fills the screen, crops the overflow
1359
- // "stretch" distorts to fill exactly (no bars, no crop)
1360
- // "pixel" integer scaling — crisp for pixel art
1361
- emerald.clearDesignResolution(); // back to 1:1 CSS pixels
1384
+ const audioManager = new AudioManager();
1362
1385
 
1363
- // Mouse/touch coordinates -> world space (accounts for the design scale,
1364
- // letterbox offset, camera zoom/position, and DPR):
1365
- const world = emerald.screenToWorld(input.mouse.x, input.mouse.y);
1386
+ // Add audio files
1387
+ audioManager.add("path/to/sound.wav", "soundName", { volume: 0.8, loop: false });
1388
+ audioManager.add("path/to/music.mp3", "backgroundMusic", { bus: "music", loop: true });
1366
1389
  ```
1367
1390
 
1368
- ## Auto-pause & lifecycle
1369
-
1370
- `run()` pauses the loop when the tab is hidden (stops audio-desync, timer
1371
- pileups, and giant delta-time spikes on return). Hooks let you pause music or
1372
- show an overlay; you can also pause manually:
1391
+ ### Playing Audio
1373
1392
 
1374
1393
  ```javascript
1375
- emerald.run(update, {
1376
- pauseOnBlur: true, // default: pause when the tab is hidden
1377
- pauseOnWindowBlur: false, // stricter: also pause when the window loses focus
1378
- onPause: () => audio.setMasterVolume(0),
1379
- onResume: () => audio.setMasterVolume(1),
1380
- });
1394
+ // Play audio
1395
+ audioManager.play("soundName"); // restarts from 0
1381
1396
 
1382
- emerald.pause(); // e.g. from your own pause menu
1383
- emerald.resume();
1397
+ // Play overlapping copies, for rapid SFX
1398
+ audioManager.playOverlap("soundName");
1399
+
1400
+ // Play exclusively (stops all other audio first)
1401
+ audioManager.playExclusive("soundName");
1384
1402
  ```
1385
1403
 
1386
- The first `dt` after resuming is clamped (`maxDelta`, default 0.25s), so
1387
- physics never explodes after a long background stint.
1404
+ ### Audio Control
1388
1405
 
1389
- ## Production hardening (resource lifecycle, context loss, render stats)
1406
+ ```javascript
1407
+ // Stop specific audio
1408
+ audioManager.stop("soundName");
1390
1409
 
1391
- ### Freeing GPU memory
1410
+ // Stop all audio
1411
+ audioManager.stopAll();
1392
1412
 
1393
- Removing an object from a scene keeps its GPU resources alive so it can be
1394
- re-added. When something is gone for good, dispose it — shared textures are
1395
- reference-counted and freed when their last user disposes:
1413
+ // Remove audio
1414
+ audioManager.remove("soundName");
1396
1415
 
1397
- ```javascript
1398
- scene.remove(enemy, { dispose: true }); // buffers + texture reference freed
1399
- gameObject.destroy(); // same, plus physics bodies + Behaviour.onDestroy
1400
- scene.dispose(); // tear down an entire level/screen
1401
- drawable.dispose(); // lowest level, safe to call twice
1416
+ // Get audio object
1417
+ const sound = audioManager.getSound("soundName");
1402
1418
  ```
1403
1419
 
1404
- ### WebGL context loss
1420
+ ### Buses & Fades
1405
1421
 
1406
- Lost contexts (mobile tab switches, GPU resets, laptops waking) are survived
1407
- automatically: rendering pauses on loss, and on restore the engine recompiles
1408
- shaders, re-uploads every cached texture from the surviving image cache,
1409
- rebuilds all drawable buffers, custom `Material`s, post effects, and render
1410
- targets, then resumes. Optional hooks:
1422
+ Every sound belongs to a named mix bus. `"music"` and `"sfx"` exist by default (new sounds land on `"sfx"`), and any name you use creates a bus on the fly. Effective volume is `master × bus × sound × fade`, so one slider mutes all music without touching the SFX:
1411
1423
 
1412
1424
  ```javascript
1413
- emerald.onContextLost(() => overlay.show("Recovering graphics..."));
1414
- emerald.onContextRestored(() => overlay.hide());
1425
+ audioManager.setMasterVolume(0.5);
1426
+ audioManager.setBusVolume("music", 0.5); // the settings-menu "music volume" slider
1427
+ audioManager.setBusVolume("sfx", 0.8);
1428
+ audioManager.setSoundBus("thunder", "ambience"); // move a sound, creating the bus
1429
+ audioManager.getBusVolume("music"); // 0.5
1415
1430
  ```
1416
1431
 
1417
- ### Render stats
1418
-
1419
- Every draw site reports into per-frame counters, shown automatically by
1420
- `DebugOverlay` (`draws` / `quads` / `binds`) and readable in code:
1432
+ Fades run on the manager's clock (self-driven via rAF by default; pass `{ autoTick: false }` and call `audioManager.update(dt)` yourself to tie them to the game loop):
1421
1433
 
1422
1434
  ```javascript
1423
- const { drawCalls, quads, textureBinds } = emerald.getRenderStats();
1435
+ audioManager.fadeIn("theme", 1.5); // play from silence to full over 1.5s
1436
+ audioManager.fadeOut("theme", 2.0); // fade to silence, then stop
1437
+ audioManager.fadeTo("theme", 0.2, 0.5); // duck under dialogue
1438
+ audioManager.crossfade("theme", "boss", 2.0); // level -> boss music, one call
1424
1439
  ```
1425
1440
 
1426
- Draw calls growing with level size means something isn't batched — use
1427
- `Tilemap`, `SpriteBatch`, or the level loader's instanced tile path.
1441
+ ### Positional Audio
1428
1442
 
1429
- ### Pixel-perfect rendering
1430
-
1431
- Spritesheet frame UVs are inset half a texel everywhere, so frames never bleed
1432
- into neighboring cells (the classic "white seams between tiles" artifact). For
1433
- pixel-art games also snap the camera to whole pixels:
1443
+ `AudioManager` can also attenuate and pan sounds based on a listener position, using the Web Audio `StereoPanner` where available and falling back to volume-only panning otherwise.
1434
1444
 
1435
1445
  ```javascript
1436
- emerald.camera.setPixelSnap(true); // rendered position rounds; stored position stays smooth
1446
+ audioManager.setListener(player.x, player.y); // usually the camera/player each frame
1447
+ audioManager.setSpatialRange(100, 800); // full volume <100px, silent >800px
1448
+
1449
+ audioManager.playSpatial("explosion", { x: 1200, y: 50 }); // one-shot, positioned
1450
+
1451
+ // Pure helper (also used internally), handy for custom routing/tests:
1452
+ const { volume, pan, distance } = audioManager.computeSpatial({ x, y });
1437
1453
  ```
1438
1454
 
1439
- ## Usage
1455
+ ## Camera
1440
1456
 
1441
- ### Basic Setup
1457
+ The engine has simple controls for the camera. The camera is stored in the emerald variable.
1442
1458
 
1443
1459
  ```javascript
1444
- import { Emerald } from "emeraldengine";
1445
- import { Scene } from "emeraldengine";
1446
- import { Color } from "emeraldengine";
1447
- import { SceneManager } from "emeraldengine";
1460
+ // Set camera position
1461
+ emerald.camera.setPosition(x, y, z);
1462
+ emerald.camera.setZoom(1.5);
1448
1463
 
1449
- const emerald = new Emerald(canvas); // You should pass your own canvas element here
1450
- const scene = new Scene();
1451
- SceneManager.setScene(scene);
1464
+ // Access camera transform directly
1465
+ emerald.camera.transform.position.x = 100;
1466
+ emerald.camera.transform.position.y = 200;
1467
+ emerald.camera.transform.scale.x = 1.5;
1468
+ emerald.camera.transform.scale.y = 1.5;
1452
1469
  ```
1453
1470
 
1454
- ### Set the background color for the engine
1471
+ Multiple cameras are supported, each with a normalized viewport (origin bottom-left), useful for split-screen:
1455
1472
 
1456
1473
  ```javascript
1457
- emerald.setBackgroundColor(color); // color = new Color(r, g, b, a = 255);
1458
- ```
1459
-
1460
- ### Drawing the scene
1474
+ import { Camera, CameraController } from "emeraldengine";
1461
1475
 
1462
- To draw items in the screen you need some sort of animation loop. I use `window.requestAnimationFrame` for this. Here is a basic example:
1476
+ const top = new Camera({ viewport: { x: 0, y: 0.5, width: 1, height: 0.5 } });
1477
+ const bottom = new Camera({ viewport: { x: 0, y: 0, width: 1, height: 0.5 } });
1478
+ emerald.setCameras([top, bottom]); // or emerald.addCamera(cam) / removeCamera(cam)
1463
1479
 
1464
- ```javascript
1465
- let lastTime = 0;
1466
- const animate = (currentTime) => {
1467
- const deltaTime = (currentTime - lastTime) / 1000;
1468
- lastTime = currentTime;
1469
- emerald.drawScene(scene, deltaTime); // You need this line to tell the engine what to draw
1470
- window.requestAnimationFrame(animate);
1471
- };
1472
- animate(0);
1480
+ top.clearColor = new Color(10, 14, 20); // optional per-viewport clear
1481
+ top.setIgnoreLayers([11, 20]); // skip these object layers in this camera
1473
1482
  ```
1474
1483
 
1475
- ## Scene
1476
-
1477
- Emerald has multiple scenes support. In order to render any object it has to be added to the scene using the `add` method.
1478
-
1479
- ### Adding and removing items from the scene
1484
+ `CameraController` adds smooth follow, bounds, a deadzone, and shake:
1480
1485
 
1481
1486
  ```javascript
1482
- // Adding an object to the scene
1483
- scene.add(gameObject);
1484
-
1485
- // Removing an object from the scene
1486
- scene.remove(gameObject);
1487
+ const cam = new CameraController(emerald.camera);
1488
+ cam
1489
+ .follow(player.gameObject, 0.12)
1490
+ .setDeadzone(90, 60) // half-extents in world units; camera only scrolls once the target leaves this box; 0/0 or null disables
1491
+ .setBounds(-1000, -1000, 1000, 1000);
1492
+ cam.shake(10, 0.3);
1493
+ // each frame: cam.update(dt);
1487
1494
  ```
1488
1495
 
1489
- ### Set Active Scene
1496
+ ## FPSCounter
1497
+
1498
+ Emerald has a built-in FPS counter.
1490
1499
 
1491
1500
  ```javascript
1492
- // When changing a scene you should deactivate current scene to not mess up the event manager.
1493
-
1494
- // Activate Scene
1495
- scene.setIsActive(true);
1501
+ import { FPSCounter } from "emeraldengine";
1502
+ let fpsCounter = new FPSCounter();
1496
1503
 
1497
- // Deactivate Scene
1498
- scene.setIsActive(false);
1504
+ const animate = (currentTime) => {
1505
+ emerald.drawScene(scene, deltaTime);
1506
+ fpsCounter.update(); // Call this in your animation loop
1507
+ window.requestAnimationFrame(animate);
1508
+ };
1509
+ animate();
1499
1510
  ```
1500
1511
 
1501
- ## Game Objects
1502
-
1503
- ### Creating a new GameObject
1512
+ ## Time Management
1504
1513
 
1505
1514
  ```javascript
1506
- import { GameObject } from "emeraldengine";
1507
- import { Vector3, Vector2 } from "emeraldengine";
1508
- /*
1509
- ARGUMENTS:
1510
- 1. name: string = Name of the new GameObject
1511
- 2. position: Vector3 = Position of the new GameObject
1512
- 3. rotation: number = Rotation of the new GameObject
1513
- 4. scale: Vector2 = Scale of the new GameObject
1514
- */
1515
- const gameObject = new GameObject(name, position, rotation, scale);
1516
- ```
1515
+ import { Time } from "emeraldengine";
1517
1516
 
1518
- This will create a new empty GameObject. At this stage you will not see anything on the screen until you add some components.
1517
+ // Get delta time
1518
+ const deltaTime = Time.deltaTime; // or Time.getDeltaTime()
1519
+ Time.getUnscaledDeltaTime(); // raw delta, ignores timeScale
1520
+ Time.getElapsedTime(); // total accumulated time
1519
1521
 
1520
- ### Components
1522
+ // Time is automatically updated when you call emerald.drawScene()
1523
+ // You can also manually set it
1524
+ Time.setDeltaTime(deltaTime);
1521
1525
 
1522
- There are currently 8 components: Texture, InstancedTexture, Square2D, Circle2D, Triangle2D, RigidBody, BoxCollider, CircleCollider
1526
+ // Slow down / speed up / pause the whole game:
1527
+ Time.setTimeScale(0.5); // 0 = paused, 1 = normal, 2 = double speed
1528
+ Time.getTimeScale();
1529
+ ```
1523
1530
 
1524
- #### Texture
1531
+ ## Tween, Timer & StateMachine
1525
1532
 
1526
1533
  ```javascript
1527
- import { Texture } from "emeraldengine";
1528
- /*
1529
- ARGUMENTS:
1530
- 1. texturePath = Specify the path for the texture that you want to use.
1531
- 2. frameWidth: number = The width of each frame.
1532
- 3. frameHeight: number = The height of each frame.
1533
- 4. framesPerRow: number = How many frames are in one row in your spritesheet.
1534
- 5. totalFrames: number = How many total frames does your spritesheet have.
1535
- 6. animationSpeed: number = Speed of change of every frame.
1536
- 7. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
1537
- 8. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
1538
- 9. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
1539
- */
1540
- const texture = new Texture(
1541
- texturePath,
1542
- frameWidth,
1543
- frameHeight,
1544
- framesPerRow,
1545
- totalFrames,
1546
- animationSpeed,
1547
- autoPlay,
1548
- (pixelart = true),
1549
- (useLighting = true)
1550
- );
1534
+ import { Tween, Easing } from "emeraldengine";
1535
+ Tween.to(sprite.transform.position, { x: 200, y: -50 }, 0.6, {
1536
+ easing: Easing.outBack, // linear, inOutQuad, outCubic, outBounce, outElastic, ...
1537
+ delay: 0,
1538
+ loop: false,
1539
+ yoyo: false,
1540
+ onUpdate: (t) => {},
1541
+ onComplete: () => {},
1542
+ }).then(() => console.log("done"));
1543
+ Tween.killOf(target);
1544
+ Tween.killAll();
1545
+ // driven automatically by drawScene
1546
+ ```
1551
1547
 
1552
- // Add the texture to a game object
1553
- gameObject.addComponent(texture);
1548
+ ```javascript
1549
+ import { Timer } from "emeraldengine";
1550
+ Timer.after(2, () => spawnEnemy()); // once
1551
+ const h = Timer.every(0.5, () => tick(), 10); // 10 times (omit count = forever)
1552
+ Timer.clear(h);
1553
+ Timer.clearAll();
1554
1554
  ```
1555
1555
 
1556
- ![Texture](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/texture.png?raw=true)
1556
+ ```javascript
1557
+ import { StateMachine } from "emeraldengine";
1558
+ const fsm = new StateMachine();
1559
+ fsm.add("idle", {
1560
+ update: (dt, sm) => {
1561
+ if (seen) sm.set("chase");
1562
+ },
1563
+ });
1564
+ fsm.add("chase", { enter: () => roar(), update: (dt) => move(dt) });
1565
+ fsm.set("idle");
1566
+ // in update(dt): fsm.update(dt); -> fsm.is("chase")
1567
+ ```
1557
1568
 
1558
- #### InstancedTexture
1569
+ ## SpatialGrid & Pool
1559
1570
 
1560
- InstancedTexture is perfect for rendering many objects with the same texture efficiently, such as tiles, particles, or repeating elements.
1571
+ ```javascript
1572
+ import { SpatialGrid } from "emeraldengine";
1573
+ const grid = new SpatialGrid(64);
1574
+ grid.clear();
1575
+ for (const e of enemies)
1576
+ grid.insert(e, e.transform.position.x, e.transform.position.y);
1577
+ const near = grid.queryRadius(px, py, 100); // or grid.queryRect(...)
1578
+ ```
1561
1579
 
1562
1580
  ```javascript
1563
- import { InstancedTexture } from "emeraldengine";
1564
- /*
1565
- ARGUMENTS:
1566
- 1. texturePath = Specify the path for the texture that you want to use.
1567
- 2. instanceCount: number = How many instances of the texture you want to create.
1568
- 3. frameWidth: number = The width of each frame.
1569
- 4. frameHeight: number = The height of each frame.
1570
- 5. framesPerRow: number = How many frames are in one row in your spritesheet.
1571
- 6. totalFrames: number = How many total frames does your spritesheet have.
1572
- 7. animationSpeed: number = Speed of change of every frame.
1573
- 8. autoPlay: boolean = Specify if you want the animation to play automatically. If you don't want any animation then pass false for it.
1574
- 9. pixelart: boolean = Specify whether the texture should be rendered in pixel art style. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
1575
- 10. useLighting: boolean = Specify whether the texture should react to lighting or not. If you don't want any lighting then pass false for it. (THIS IS OPTIONAL. If you don't specify it then it will be defaulted to true)
1576
- */
1577
- const instancedTexture = new InstancedTexture(
1578
- texturePath,
1579
- instanceCount,
1580
- frameWidth,
1581
- frameHeight,
1582
- framesPerRow,
1583
- totalFrames,
1584
- animationSpeed,
1585
- autoPlay,
1586
- (pixelart = true),
1587
- (useLighting = true)
1581
+ import { Pool } from "emeraldengine";
1582
+ const bullets = new Pool(
1583
+ () => new Bullet(),
1584
+ (b, x, y) => b.spawn(x, y),
1585
+ 50
1588
1586
  );
1589
-
1590
- // Add the instanced texture to a game object
1591
- gameObject.addComponent(instancedTexture);
1587
+ const b = bullets.acquire(px, py);
1588
+ bullets.release(b); // bullets.releaseAll()
1592
1589
  ```
1593
1590
 
1594
- ![InstancedTexture](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/instancedtexture.png?raw=true)
1595
-
1596
- #### Square2D
1591
+ ## MathUtils
1597
1592
 
1598
1593
  ```javascript
1599
- import { Square2D } from "emeraldengine";
1600
- let square = new Square2D();
1601
- gameObject.addComponent(square);
1594
+ import { MathUtils } from "emeraldengine";
1595
+ MathUtils.clamp(v, 0, 1);
1596
+ MathUtils.lerp(a, b, t);
1597
+ MathUtils.map(v, 0, 10, 0, 100);
1598
+ MathUtils.degToRad(90);
1599
+ MathUtils.randomRange(0, 5);
1600
+ MathUtils.randomInt(1, 6);
1601
+ MathUtils.distance(a, b);
1602
+ MathUtils.normalize(v);
1603
+ MathUtils.angleBetween(a, b);
1602
1604
  ```
1603
1605
 
1604
- ![Square2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/square2d.png?raw=true)
1606
+ ## Coroutines
1605
1607
 
1606
- #### Triangle2D
1608
+ Generator-based sequencing layered on the same per-frame delta the rest of the engine uses. It's driven automatically from `Emerald.drawScene`, so coroutines honor pause/slow-mo via `Time.timeScale`.
1607
1609
 
1608
1610
  ```javascript
1609
- import { Triangle2D } from "emeraldengine";
1610
- let triangle = new Triangle2D();
1611
- gameObject.addComponent(triangle);
1612
- ```
1611
+ import { Coroutine } from "emeraldengine";
1613
1612
 
1614
- ![Triangle2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/triangle2d.png?raw=true)
1613
+ const handle = Coroutine.start(function* () {
1614
+ big.setText("3");
1615
+ audio.beep();
1616
+ yield 0.7; // wait 0.7 seconds
1617
+ big.setText("2");
1618
+ audio.beep();
1619
+ yield 0.7;
1620
+ yield Coroutine.waitFrames(3); // wait 3 frames
1621
+ yield Coroutine.waitUntil(() => player.ready); // block until predicate is truthy
1622
+ yield Coroutine.waitWhile(() => paused); // block while predicate is truthy
1623
+ yield fetch("/level.json"); // await any promise
1624
+ yield Coroutine.tween(0, 1, 0.5, (v) => (door.openAmount = v)); // drive a value
1625
+ yield otherCoroutineHandle; // wait for a nested coroutine
1626
+ start();
1627
+ });
1615
1628
 
1616
- #### Circle2D
1629
+ handle.cancel(); // stop it early
1630
+ handle.isRunning(); // boolean
1631
+ await handle.promise; // resolves when the coroutine finishes or is cancelled
1617
1632
 
1618
- ```javascript
1619
- import { Circle2D } from "emeraldengine";
1620
- /*
1621
- ARGUMENTS:
1622
- 1. segments = number of segments that the circle will have. Default is 32.
1623
- */
1624
- let circle = new Circle2D(segments);
1625
- gameObject.addComponent(circle);
1633
+ Coroutine.count(); // number of running coroutines
1634
+ Coroutine.clearAll(); // cancel + remove every coroutine (e.g. on scene exit)
1626
1635
  ```
1627
1636
 
1628
- ![Circle2D](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/circle2d.png?raw=true)
1637
+ ## Rendering Pipeline
1629
1638
 
1630
- #### RigidBody
1639
+ ### Post-processing
1631
1640
 
1632
- RigidBody is a component that allows you to add physics to your game objects. However, it won't work until you create a Physics instance at the top of your code.
1641
+ When post-processing is enabled, Emerald renders the whole scene into an offscreen texture and then runs a chain of full-screen shader passes before drawing the final image to the canvas. You manage it entirely through the `Emerald` instance:
1633
1642
 
1634
1643
  ```javascript
1635
- import { RigidBody } from "emeraldengine";
1636
- import { Physics, Vector2 } from "emeraldengine";
1637
- // Create physics engine first
1638
- const physics = new Physics(-70, 32, 2); // gravity, scale, velocityThreshold
1644
+ emerald.enablePostProcessing(); // allocate the scene render target + processor
1645
+ emerald.disablePostProcessing(); // turn it back off
1639
1646
 
1640
- /*
1641
- ARGUMENTS:
1642
- 1. physics: Physics = Instance of the Physics class that you created at the top of your code.
1643
- 2. type: string = Type of the rigid body. It can be "dynamic", "kinematic", or "static".
1644
- 3. position: Vector2 = Position of the rigid body is Vector2 because it doesn't need any Z index.
1645
- 4. fixedRotation: boolean = Specify whether the rigid body should have a fixed rotation or not. Default is false.
1646
- 5. parentObject: GameObject = (OPTIONAL) If you want to attach the rigid body to a GameObject you can pass it here. If you don't want to attach it to any GameObject then pass null.
1647
- 6. offset: Vector2 = (OPTIONAL) Offset from the GameObject's position.
1648
- */
1649
- const rigidBody = new RigidBody(
1650
- physics,
1651
- "dynamic",
1652
- new Vector2(0, 0),
1653
- false,
1654
- gameObject,
1655
- new Vector2(0, 0)
1656
- );
1647
+ const bloom = emerald.addPostEffect(PostEffects.bloom()); // returns the effect
1648
+ emerald.removePostEffect(bloom);
1657
1649
 
1658
- gameObject.addComponent(rigidBody);
1650
+ // Effects run in the order they were added. Toggle one without removing it:
1651
+ bloom.enabled = false;
1659
1652
  ```
1660
1653
 
1661
- #### BoxCollider
1654
+ `drawScene` automatically routes through the processor while any enabled effect exists; if none do, it draws straight to the screen with zero overhead.
1655
+
1656
+ Write a custom pass by constructing a `PostEffect`. Your fragment shader gets `vUV` (0–1 screen UV), `uScene` (the previous pass), `uResolution`, and `uTime` for free; declare any extra uniforms and set them in `setUniforms`:
1662
1657
 
1663
1658
  ```javascript
1664
- import { BoxCollider } from "emeraldengine";
1659
+ import { PostEffect } from "emeraldengine";
1665
1660
 
1666
- /*
1667
- ARGUMENTS:
1668
- 1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
1669
- 2. size: Vector2 = Size of the box collider.
1670
- 3. density: number = Density of the collider.
1671
- 4. friction: number = Friction of the collider.
1672
- 5. restitution: number = Restitution (bounciness) of the collider.
1673
- 6. isSensor: boolean = Whether this collider is a sensor (triggers events but doesn't collide physically).
1674
- 7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
1675
- */
1676
- const boxCollider = new BoxCollider(
1677
- rigidBody,
1678
- new Vector2(1, 1),
1679
- 1,
1680
- 0.3,
1681
- 0.1,
1682
- false,
1683
- gameObject
1661
+ const tint = new PostEffect(
1662
+ "tint",
1663
+ `
1664
+ uniform vec3 uTint;
1665
+ void main() {
1666
+ gl_FragColor = texture2D(uScene, vUV) * vec4(uTint, 1.0);
1667
+ }`,
1668
+ {
1669
+ setUniforms: (gl, loc) => gl.uniform3f(loc("uTint"), 1.0, 0.85, 0.7),
1670
+ enabled: true,
1671
+ }
1684
1672
  );
1685
-
1686
- gameObject.addComponent(boxCollider);
1673
+ emerald.addPostEffect(tint);
1687
1674
  ```
1688
1675
 
1689
- ![BoxCollider](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/boxcollider.png?raw=true)
1676
+ Keep UI crisp by rendering it on a camera excluded from post-processing: it draws straight to the screen after the effect chain, so bloom never blows out your buttons and text:
1690
1677
 
1691
- The `BoxCollider` is specifically made bigger than the `Square2D` component in this image to demonstrate how it works. You can adjust the size of the collider to fit your needs.
1678
+ ```javascript
1679
+ const uiCam = new Camera({ excludeFromPost: true }); // or uiCam.setExcludeFromPost(true)
1680
+ emerald.addCamera(uiCam);
1681
+ hudObject.setLayer(100); // and restrict cameras via setOnlyLayers/ignoreLayers
1682
+ ```
1692
1683
 
1693
- #### CircleCollider
1684
+ ### PostEffects (built-in)
1685
+
1686
+ Factory functions on the `PostEffects` namespace return a ready `PostEffect`:
1694
1687
 
1695
1688
  ```javascript
1696
- import { CircleCollider } from "emeraldengine";
1689
+ import { PostEffects } from "emeraldengine";
1697
1690
 
1698
- /*
1699
- ARGUMENTS:
1700
- 1. rigidBody: RigidBody = The rigid body component that this collider will be attached to.
1701
- 2. radius: number = Radius of the circle collider.
1702
- 3. density: number = Density of the collider.
1703
- 4. friction: number = Friction of the collider.
1704
- 5. restitution: number = Restitution (bounciness) of the collider.
1705
- 6. isSensor: boolean = Whether this collider is a sensor.
1706
- 7. parentObject: GameObject = (OPTIONAL) Parent GameObject.
1707
- */
1708
- const circleCollider = new CircleCollider(
1709
- rigidBody,
1710
- 1.5,
1711
- 1,
1712
- 0.3,
1713
- 0.8,
1714
- false,
1715
- gameObject
1691
+ emerald.enablePostProcessing();
1692
+ emerald.addPostEffect(
1693
+ PostEffects.bloom({ threshold: 0.6, intensity: 1.2, spread: 1.1 })
1716
1694
  );
1717
-
1718
- gameObject.addComponent(circleCollider);
1695
+ emerald.addPostEffect(
1696
+ PostEffects.vignette({ intensity: 0.5, radius: 0.75, softness: 0.45 })
1697
+ );
1698
+ emerald.addPostEffect(
1699
+ PostEffects.colorGrade({ brightness: 0.02, contrast: 1.08, saturation: 1.15 })
1700
+ );
1701
+ emerald.addPostEffect(PostEffects.chromaticAberration({ amount: 0.003 }));
1702
+ emerald.addPostEffect(PostEffects.scanlines({ intensity: 0.15, count: 480 }));
1703
+ emerald.addPostEffect(
1704
+ PostEffects.crt({ curvature: 4.0, scanlineIntensity: 0.2, vignette: 0.3 })
1705
+ );
1706
+ emerald.addPostEffect(PostEffects.grayscale());
1719
1707
  ```
1720
1708
 
1721
- ![CircleCollider](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/circlecollider.png?raw=true)
1709
+ | Effect | Options (defaults) |
1710
+ | --------------------- | ------------------------------------------------------------ |
1711
+ | `bloom` | `threshold 0.7`, `intensity 1.0`, `spread 1.0` (multi-pass) |
1712
+ | `vignette` | `intensity 0.5`, `radius 0.75`, `softness 0.45` |
1713
+ | `colorGrade` | `brightness 0`, `contrast 1`, `saturation 1` |
1714
+ | `chromaticAberration` | `amount 0.003` |
1715
+ | `scanlines` | `intensity 0.15`, `count 480` |
1716
+ | `crt` | `curvature 4.0`, `scanlineIntensity 0.2`, `vignette 0.3` |
1717
+ | `grayscale` | none |
1722
1718
 
1723
- The `CircleCollider` is specifically made bigger than the `Circle2D` component in this image to demonstrate how it works. You can adjust the radius of the collider to fit your needs.
1719
+ `bloom` is exported as a class too (`BloomEffect`) if you want to subclass it.
1724
1720
 
1725
- ## Object methods
1721
+ ### RenderTarget
1726
1722
 
1727
- ### Position
1723
+ An offscreen framebuffer backed by a color texture (and an optional depth buffer). Used internally by the post-processor, but useful on its own for minimaps, mirrors, or picture-in-picture.
1728
1724
 
1729
1725
  ```javascript
1730
- // Set position
1731
- gameObject.transform.position.x = 100;
1732
- gameObject.transform.position.y = 200;
1733
- gameObject.transform.position.z = 0;
1726
+ import { RenderTarget } from "emeraldengine";
1734
1727
 
1735
- // Or set all at once
1736
- gameObject.transform.position = new Vector3(100, 200, 0);
1728
+ const rt = new RenderTarget(512, 512, { depth: false, pixelart: false });
1729
+ rt.bind(); // binds the FBO and sets the viewport to its size
1730
+ // ...draw...
1731
+ rt.unbind(); // restore the canvas framebuffer
1732
+ // rt.texture now holds the rendered image (a WebGLTexture)
1733
+ rt.resize(1024, 1024); // reallocates only if the size changed
1734
+ rt.dispose(); // free GL resources
1737
1735
  ```
1738
1736
 
1739
- ![Position](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/position.gif?raw=true)
1737
+ ### Material (custom shaders)
1740
1738
 
1741
- ### Rotation
1739
+ A `Material` replaces a Drawable's fragment shader while reusing the engine's standard vertex shader, so transforms, the camera, and instancing keep working. Your fragment program automatically has `vTexCoord`, `vFragPos`, `vInstanceColor`, `uSampler`, `uColor`, `uOpacity`, and `uTime`. Don't redeclare them; declare any extra uniforms and push values with `set(name, value)`.
1742
1740
 
1743
1741
  ```javascript
1744
- // Set rotation (in radians)
1745
- gameObject.transform.rotation = Math.PI / 4; // 45 degrees
1746
- ```
1747
-
1748
- ![Rotation](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/rotation.gif?raw=true)
1742
+ import { Material, Square2D } from "emeraldengine";
1749
1743
 
1750
- ### Scale
1744
+ const dissolve = new Material(
1745
+ `
1746
+ uniform float uAmount;
1747
+ void main() {
1748
+ vec4 c = texture2D(uSampler, vTexCoord);
1749
+ if (c.a < uAmount) discard;
1750
+ gl_FragColor = c * uColor * uOpacity;
1751
+ }
1752
+ `,
1753
+ { uniforms: { uAmount: 0.0 } }
1754
+ );
1751
1755
 
1752
- ```javascript
1753
- // Set scale
1754
- gameObject.transform.scale.x = 2;
1755
- gameObject.transform.scale.y = 2;
1756
+ const shape = new Square2D();
1757
+ shape.setMaterial(dissolve); // any Drawable: Texture, Square2D, Circle2D…
1758
+ gameObject.addComponent(shape);
1756
1759
 
1757
- // Or set both at once
1758
- gameObject.transform.scale = new Vector2(2, 2);
1760
+ // Animate a uniform (numbers, vec2/3/4 arrays, or functions are accepted):
1761
+ dissolve.set("uAmount", 0.5);
1762
+ dissolve.set("uPulse", () => 0.5 + 0.5 * Math.sin(performance.now() / 300));
1759
1763
  ```
1760
1764
 
1761
- ![Scale](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/scale.gif?raw=true)
1765
+ ### SpriteBatch
1762
1766
 
1763
- ### Change color
1767
+ A dynamic batched renderer with its own minimal shader. Instead of one draw call per sprite, it accumulates sprites that share a texture into a single interleaved buffer and submits them in one `drawElements` call, ideal for many same-atlas quads (bullets, tiles, text glyphs).
1764
1768
 
1765
1769
  ```javascript
1766
- // For textures
1767
- const texture = gameObject.getComponent(Texture);
1768
- texture.setColor(new Color(255, 0, 0)); // Red
1770
+ import { SpriteBatch } from "emeraldengine";
1771
+
1772
+ const batch = new SpriteBatch({ maxQuads: 2000 });
1773
+ batch.begin(projectionMatrix, viewMatrix); // gl-matrix mat4 / Float32Array(16)
1774
+ for (const e of entities) {
1775
+ batch.draw({
1776
+ texture: atlasTexture, // a WebGLTexture; changing it flushes the batch
1777
+ x: e.x,
1778
+ y: e.y,
1779
+ w: 32,
1780
+ h: 32,
1781
+ rotation: e.angle,
1782
+ originX: 0.5,
1783
+ originY: 0.5,
1784
+ u0: e.u0,
1785
+ v0: e.v0,
1786
+ u1: e.u1,
1787
+ v1: e.v1, // UV sub-rect (defaults 0..1)
1788
+ r: 1,
1789
+ g: 1,
1790
+ b: 1,
1791
+ a: 1, // per-vertex tint
1792
+ });
1793
+ }
1794
+ batch.end(); // flushes remaining sprites
1795
+ console.log(batch.drawCalls); // GL draw calls emitted this frame
1769
1796
  ```
1770
1797
 
1771
- ![Change Color](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/changecolor.gif?raw=true)
1798
+ Also worth knowing about: `obj.setLayer(10)` (layers sort before z), `hudObj.setScreenSpace(true)` (ignore the camera, position in pixels from viewport center), `drawable.setBlendMode("additive" | "normal" | "multiply")`, off-screen culling (`emerald.setCullingEnabled(true)`, `particles.alwaysVisible = true` to opt out), `TextureAtlas.load`/`applyTo` for atlas sub-rects, and `TextureManager.preload([...])` for shared/cached GL textures.
1772
1799
 
1773
- ### Set texture frame
1800
+ ## In-Engine UI
1774
1801
 
1775
- ```javascript
1776
- // For animated textures
1777
- const texture = gameObject.getComponent(Texture);
1778
- texture.setFrame(2); // Set to frame 2
1779
- ```
1802
+ `UI` is a retained-mode toolkit drawn entirely by the engine, with no DOM/HTML overlay. Elements are screen-space objects on a dedicated high layer with their own pointer and keyboard hit-testing. Positions are pixels from the viewport center (y up), either a literal `{x, y}` or a responsive `(viewW, viewH) => ({x, y})` function; call `relayout()` after a resize.
1780
1803
 
1781
- ## Animations
1804
+ Pair it with a UI camera so the UI draws over the game and the game cameras skip the UI layer:
1782
1805
 
1783
1806
  ```javascript
1784
- // Play animation
1785
- texture.playAnimation([0, 1, 2, 3], 200); // frames array, speed in ms
1786
-
1787
- // Stop animation
1788
- texture.stopAnimation();
1807
+ import { UI } from "emeraldengine";
1789
1808
 
1790
- // Check if playing
1791
- if (texture.isPlaying) {
1792
- // Animation is currently playing
1793
- }
1794
- ```
1809
+ const uiCam = UI.createCamera(); // a full-screen camera that renders ONLY UI.LAYER
1810
+ gameCamera.ignoreLayer(UI.LAYER); // keep the UI out of the game viewport(s)
1811
+ emerald.setCameras([gameCamera, uiCam]); // add the UI camera last
1795
1812
 
1796
- ![Animations](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/animations.gif?raw=true)
1813
+ const ui = new UI(scene, canvas, { accent: [120, 200, 255] });
1797
1814
 
1798
- ## Instance System
1815
+ // Labels (return a handle with setText)
1816
+ const score = ui.label(() => ({ x: 0, y: 200 }), "Score: 0", {
1817
+ font: "700 30px system-ui, sans-serif",
1818
+ color: "#eaf2ff",
1819
+ });
1820
+ score.setText("Score: 120");
1799
1821
 
1800
- The Instance system allows you to efficiently manage multiple copies of the same object.
1822
+ // Buttons (panel + centered label, hover highlight, click handler)
1823
+ ui.button(() => ({ x: 0, y: 0 }), "START", 240, 56, {
1824
+ accent: [120, 220, 160],
1825
+ onClick: () => startGame(),
1826
+ });
1801
1827
 
1802
- ### Creating Instances
1828
+ // Panels and a modal dimmer behind a dialog
1829
+ ui.dim(0.6); // full-screen backdrop
1830
+ ui.panel(() => ({ x: 0, y: 0 }), 480, 320, { opacity: 0.94 });
1803
1831
 
1804
- ```javascript
1805
- import { Instance } from "emeraldengine";
1832
+ // Editable single-line text field
1833
+ const name = ui.textField(() => ({ x: 0, y: -80 }), 280, 44, {
1834
+ placeholder: "Your name",
1835
+ maxLength: 16,
1836
+ onChange: (v) => console.log(v),
1837
+ });
1838
+ name.getValue();
1839
+ name.setValue("P1");
1806
1840
 
1807
- // Create an instance
1808
- const instance = new Instance(
1809
- "InstanceName",
1810
- new Vector3(x, y, z),
1811
- new Vector2(width, height),
1812
- rotation,
1813
- frame
1814
- );
1841
+ ui.relayout(); // after creating/anchoring or on window resize
1842
+ ui.isOver(clientX, clientY); // true if an interactive element is under the pointer
1843
+ ui.destroy(); // remove all UI objects + detach listeners
1815
1844
 
1816
- // Add to InstancedTexture
1817
- const instancedTexture = gameObject.getComponent(InstancedTexture);
1818
- instancedTexture.addInstance(instance);
1845
+ UI.LAYER; // 100000, the default UI render layer
1819
1846
  ```
1820
1847
 
1821
- ### Instance Management
1848
+ ## ScreenEffects (transitions)
1849
+
1850
+ Full-screen camera transitions drawn with the engine's own screen-space quads (no CSS overlay), so they survive resolution changes, post-processing and split-screen. `fadeOut`/`fadeIn`/`flash` return promises. Call `update(dt)` each frame before `drawScene`.
1822
1851
 
1823
1852
  ```javascript
1824
- // Remove instance
1825
- instancedTexture.removeInstance(instanceId);
1853
+ import { ScreenEffects, Color } from "emeraldengine";
1826
1854
 
1827
- // Get instance by ID
1828
- const instance = instancedTexture.getInstanceWithId(instanceId);
1855
+ const fx = new ScreenEffects(scene, { layer: 100000, size: 5000 });
1829
1856
 
1830
- // Get instance at position
1831
- const instance = instancedTexture.getInstanceAtPosition(position, tolerance);
1857
+ await fx.fadeOut(0.4, new Color(0, 0, 0, 255)); // fade to black
1858
+ loadNextLevel();
1859
+ await fx.fadeIn(0.4); // fade back in
1832
1860
 
1833
- // Clear all instances
1834
- instancedTexture.clearInstances();
1861
+ fx.flash(new Color(255, 255, 255, 255), 0.25); // quick screen flash
1862
+ fx.setLetterbox(80); // animate cinematic bars to 80px; pass 0 to retract
1863
+
1864
+ // in the loop:
1865
+ fx.update(dt);
1866
+ // when leaving the scene:
1867
+ fx.destroy();
1835
1868
  ```
1836
1869
 
1837
- ### Instance Events
1870
+ ## Scene Transitions & the Game Loop
1871
+
1872
+ `emerald.run(update, options)` (see [Getting Started](#getting-started)) computes a clamped delta time, optionally advances a fixed-timestep simulation, and calls your `update(dt, alpha)` each frame. `alpha` is the 0..1 interpolation factor between fixed steps (1 when no fixed step is configured).
1873
+
1874
+ Switch scenes behind a fade with `SceneManager.transitionTo` (wired to `ScreenEffects`), or drive the fade directly with `ScreenEffects.transition`:
1838
1875
 
1839
1876
  ```javascript
1840
- // Add click event to specific instance
1841
- instancedTexture.addInstanceClickEvent(instanceId, (event) => {
1842
- console.log("Instance clicked!");
1843
- });
1877
+ import { SceneManager, ScreenEffects, Color } from "emeraldengine";
1844
1878
 
1845
- // Add hover events to specific instance
1846
- instancedTexture.addInstanceHoverEvent(
1847
- instanceId,
1848
- (event) => console.log("Mouse entered"),
1849
- (event) => console.log("Mouse left")
1850
- );
1851
- ```
1852
-
1853
- ## Physics Engine
1854
-
1855
- Emerald includes a comprehensive physics engine built on top of Planck.js.
1856
-
1857
- ### Setting up Physics
1858
-
1859
- ```javascript
1860
- import { Physics } from "emeraldengine";
1861
- /*
1862
- ARGUMENTS:
1863
- 1. gravity: number = Gravity force (negative for downward)
1864
- 2. scale: number = Scale factor for physics units to pixels
1865
- 3. velocityThreshold: number = Minimum velocity threshold
1866
- */
1867
- const physics = new Physics(-70, 32, 2);
1868
- ```
1869
-
1870
- ### Physics Bodies
1871
-
1872
- ```javascript
1873
- // Get the physics body from a RigidBody component
1874
- const body = rigidBody.getBody();
1875
-
1876
- // Set velocity
1877
- body.setLinearVelocity(new Vector2(10, 0));
1878
-
1879
- // Get velocity
1880
- const velocity = body.getLinearVelocity();
1881
-
1882
- // Get position
1883
- const position = body.getPosition();
1884
- ```
1885
-
1886
- ### Collision Detection
1887
-
1888
- ```javascript
1889
- // Handle collision enter
1890
- physics.onCollisionEnter((bodyA, bodyB, contact) => {
1891
- console.log("Collision started!");
1892
-
1893
- // Get collision normal
1894
- const normal = contact.getWorldManifold().normal;
1895
- //normal.y = -1 when player is on the ground
1896
- //normal.y = 1 when player hits the ceiling
1897
- //normal.x = -1 when player hits the left wall
1898
- //normal.x = 1 when player hits the right wall
1899
-
1900
- // Check if bodies are sensors
1901
- const fixtureA = contact.getFixtureA();
1902
- const fixtureB = contact.getFixtureB();
1903
- if (fixtureA.isSensor() || fixtureB.isSensor()) {
1904
- // Handle sensor collision
1905
- }
1906
- });
1907
-
1908
- // Handle collision exit
1909
- physics.onCollisionExit((bodyA, bodyB, contact) => {
1910
- console.log("Collision ended!");
1911
- });
1912
- ```
1913
-
1914
- ### Collision Events
1915
-
1916
- ```javascript
1917
- // Process physics in your update loop
1918
- const animate = (currentTime) => {
1919
- physics.process(deltaTime);
1920
- };
1921
- ```
1922
-
1923
- ## Particle System
1924
-
1925
- Emerald includes a powerful particle system for creating visual effects.
1926
-
1927
- ![Particles](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/particles.gif?raw=true)
1928
-
1929
- ### Particle Settings
1930
-
1931
- ```javascript
1932
- import { ParticleSettings } from "emeraldengine";
1879
+ const fx = new ScreenEffects(overlayScene); // update()'d each frame by your loop
1933
1880
 
1934
- const particleSettings = new ParticleSettings({
1935
- lifetime: 1.2,
1936
- velocity: new Vector2(200, 300),
1937
- gravity: new Vector2(0, -400),
1938
- amount: 16,
1939
- direction: new Vector2(0, 1), // upward
1940
- spread: Math.PI * 2,
1941
- emissionRate: Infinity, // one-shot emission
1942
- frame: 0,
1943
- offset: 5,
1944
- rotation: 0,
1945
- scale: new Vector2(5, 5),
1946
- animation: { frames: [0, 1, 2], speed: 200 },
1881
+ await SceneManager.transitionTo(nextScene, {
1882
+ screenEffects: fx,
1883
+ duration: 0.4,
1884
+ color: new Color(0, 0, 0, 255),
1885
+ onSwap: (scene) => buildLevel(scene), // runs while the screen is covered
1947
1886
  });
1948
- ```
1949
-
1950
- ### Creating Particle Systems
1951
1887
 
1952
- ```javascript
1953
- import { Particles } from "emeraldengine";
1954
-
1955
- /*
1956
- ARGUMENTS:
1957
- 1. name: string = Name of the particle system
1958
- 2. texturePath: string = Path to the texture
1959
- 3. frameWidth: number = Width of each frame
1960
- 4. frameHeight: number = Height of each frame
1961
- 5. framesPerRow: number = Frames per row in spritesheet
1962
- 6. totalFrames: number = Total frames in spritesheet
1963
- 7. duration: number = Duration of the effect
1964
- 8. settings: ParticleSettings = Particle settings object
1965
- */
1966
- const particles = new Particles(
1967
- "explosion",
1968
- texturePath,
1969
- 16,
1970
- 16,
1971
- 9,
1972
- 27,
1973
- 1.2,
1974
- particleSettings
1975
- );
1976
-
1977
- // Add to scene
1978
- scene.add(particles.gameObject);
1979
- ```
1980
-
1981
- ### Particle System Methods
1982
-
1983
- ```javascript
1984
- // Play particle effect at position
1985
- particles.play(new Vector3(x, y, z));
1986
-
1987
- // Stop particle system
1988
- particles.stop();
1989
-
1990
- // Reset particle system
1991
- particles.reset();
1992
-
1993
- // Update particles (call in your animation loop)
1994
- particles.update(deltaTime);
1995
-
1996
- // Check if active
1997
- if (particles.active) {
1998
- // Particles are currently active
1999
- }
2000
- ```
2001
-
2002
- ## Lighting System
2003
-
2004
- Emerald supports ambient, point, and directional lighting.
2005
-
2006
- ### Ambient Light
2007
-
2008
- ```javascript
2009
- // Set ambient light
2010
- emerald.setAmbientLight(new Vector3(0.3, 0.3, 0.3)); // RGB values 0-1
1888
+ // Or lower-level: fade out -> swap -> fade in
1889
+ await fx.transition(() => swapScenes(), { duration: 0.4 });
2011
1890
  ```
2012
1891
 
2013
- ### Point Light
1892
+ ## DebugOverlay
2014
1893
 
2015
1894
  ```javascript
2016
- import { PointLight } from "emeraldengine";
2017
-
2018
- /*
2019
- ARGUMENTS:
2020
- 1. position: Vector2 = Position of the light
2021
- 2. color: Color = Color of the light
2022
- 3. intensity: number = Light intensity
2023
- 4. radius: number = Light radius
2024
- */
2025
- const pointLight = new PointLight(
2026
- new Vector2(100, 0),
2027
- new Color(255, 204, 153),
2028
- 1.5,
2029
- 400
2030
- );
2031
-
2032
- // Add to engine
2033
- emerald.addPointLight(pointLight);
1895
+ import { DebugOverlay } from "emeraldengine";
2034
1896
 
2035
- // Update position
2036
- pointLight.position.x = newX;
2037
- pointLight.position.y = newY;
1897
+ const debug = new DebugOverlay();
1898
+ debug.setVisible(true); // toggle (e.g. bind to F3)
1899
+ debug.setMetric("enemies", enemies.length); // add/refresh a custom row
1900
+ debug.showColliders(scene, true); // overlay collider shapes for the scene
1901
+ // after drawScene each frame:
1902
+ debug.update(emerald, scene); // FPS / frame-time graph / objects / cameras
1903
+ debug.destroy();
2038
1904
  ```
2039
1905
 
2040
- ### Directional Light
1906
+ It shows a frame-time sparkline with min/avg/max milliseconds and heap usage, and its `draws`/`quads`/`binds` numbers come from the same render stats you can read yourself:
2041
1907
 
2042
1908
  ```javascript
2043
- import { DirectionalLight } from "emeraldengine";
2044
-
2045
- /*
2046
- ARGUMENTS:
2047
- 1. position: Vector2 = Position of the light
2048
- 2. direction: Vector2 = Direction vector
2049
- 3. color: Color = Color of the light
2050
- 4. intensity: number = Light intensity
2051
- 5. width: number = Width of the light beam
2052
- */
2053
- const directionalLight = new DirectionalLight(
2054
- new Vector2(0, 300),
2055
- new Vector2(0, -1), // pointing down
2056
- new Color(255, 255, 255),
2057
- 3.0,
2058
- 200
2059
- );
2060
-
2061
- // Add to engine
2062
- emerald.addDirectionalLight(directionalLight);
2063
-
2064
- // Rotate direction
2065
- const angle = 0.1;
2066
- const newX =
2067
- directionalLight.direction.x * Math.cos(angle) -
2068
- directionalLight.direction.y * Math.sin(angle);
2069
- const newY =
2070
- directionalLight.direction.x * Math.sin(angle) +
2071
- directionalLight.direction.y * Math.cos(angle);
2072
- directionalLight.direction.x = newX;
2073
- directionalLight.direction.y = newY;
1909
+ const { drawCalls, quads, textureBinds } = emerald.getRenderStats();
2074
1910
  ```
2075
1911
 
2076
- ## Text Rendering
1912
+ Draw calls growing with level size means something isn't batched. Use `Tilemap`, `SpriteBatch`, or the level loader's instanced tile path.
2077
1913
 
2078
- ### BitmapText
2079
-
2080
- Emerald supports bitmap font rendering using the BitmapText component. This allows you to display text with custom fonts and styles.
2081
-
2082
- ![BitmapText](https://github.com/vahan-gev/emeralddocs/blob/main/github/screenshots/bitmaptext.png?raw=true)
1914
+ ## Serializer (save/load scenes)
2083
1915
 
2084
1916
  ```javascript
2085
- import { BitmapText } from "emeraldengine";
2086
-
2087
- /*
2088
- ARGUMENTS:
2089
- 1. text: string = Text to display
2090
- 2. texturePath: string = Path to bitmap font texture
2091
- 3. letters: string = String containing all available characters
2092
- 4. letterSpacing: number = Spacing between letters
2093
- 5. frameWidth: number = Width of each character frame
2094
- 6. frameHeight: number = Height of each character frame
2095
- 7. framesPerRow: number = Characters per row in font texture
2096
- 8. totalFrames: number = Total character frames
2097
- 9. pixelArt: boolean = Whether to use pixel art rendering
2098
- 10. fontSize: number = Font size
2099
- 11. color: Color = Text color
2100
- 12. position: Vector3 = Text position
2101
- 13. rotation: number = Text rotation
2102
- 14. useLighting: boolean = Whether text should react to lighting
2103
- */
2104
- const bitmapText = new BitmapText(
2105
- "Hello World!",
2106
- fontTexturePath,
2107
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?.",
2108
- 16,
2109
- 32,
2110
- 32,
2111
- 10,
2112
- 95,
2113
- true,
2114
- 24,
2115
- new Color(255, 255, 255),
2116
- new Vector3(0, 200, 0),
2117
- 0,
2118
- false
2119
- );
2120
-
2121
- // Add to scene
2122
- scene.add(bitmapText.gameObject);
2123
-
2124
- // Update text
2125
- bitmapText.setText("New Text!");
2126
- bitmapText.setColor(new Color(255, 0, 0));
2127
- bitmapText.setFontSize(32);
2128
- bitmapText.setLetterSpacing(20);
1917
+ import { Serializer } from "emeraldengine";
1918
+ Serializer.register("coin", (data) => makeCoin(data.value));
1919
+ coin.prefabType = "coin";
1920
+ coin.serialize = () => ({ value: 5 });
1921
+ const json = Serializer.toJSON(scene); // save
1922
+ Serializer.fromJSON(json, new Scene()); // load
2129
1923
  ```
2130
1924
 
2131
- ### CanvasText (system fonts, word-wrap, Retina-crisp)
1925
+ ## Storage (versioned saves)
2132
1926
 
2133
- `CanvasText` renders any CSS font (including loaded webfonts) into a texture.
2134
- It renders at the device pixel ratio, so text is crisp on Retina/HiDPI
2135
- displays, and it supports multi-line strings with word-wrapping and alignment:
1927
+ `Storage.save`/`Storage.load` wrap your data in a versioned envelope (`{ v, t, data }`) with an automatic `.bak` mirror, so saves survive both corrupted writes (a torn write recovers from backup) and schema changes (old saves migrate forward instead of being discarded):
2136
1928
 
2137
1929
  ```javascript
2138
- import { CanvasText } from "emeraldengine";
1930
+ import { Storage } from "emeraldengine";
2139
1931
 
2140
- // Factory: returns a GameObject already sized to the text
2141
- const label = CanvasText.create("Score: 0", {
2142
- font: "700 24px 'Pixelify Sans', sans-serif",
2143
- color: "#8fe0ff",
2144
- screenSpace: true, // HUD: fixed on screen, position in px from center
2145
- position: new Vector3(0, 240, 0),
2146
- });
2147
- scene.add(label);
1932
+ // Write: version + timestamp envelope, plus a .bak backup by default.
1933
+ Storage.save("profile", { level: 3, coins: 120 }, { version: 2 });
2148
1934
 
2149
- // Multi-line + wrapping
2150
- const dialog = CanvasText.create(
2151
- "A long line of dialogue that wraps automatically.\nExplicit breaks work too.",
2152
- { font: "16px system-ui", maxWidth: 320, align: "left", lineHeight: 22 }
2153
- );
1935
+ // Read: falls back, recovers from backup, and migrates old versions.
1936
+ const profile = Storage.load("profile", {
1937
+ version: 2,
1938
+ fallback: { level: 1, coins: 0 },
1939
+ migrate: (old, fromVersion) => {
1940
+ // v1 saves had no coins field, upgrade them instead of losing progress
1941
+ return { ...old, coins: old.coins ?? 0 };
1942
+ },
1943
+ // rewrite: true (default) re-saves migrated data in the new format
1944
+ });
2154
1945
 
2155
- // Updating (re-renders the texture; attached GameObject rescales to fit)
2156
- const text = label.getComponent(CanvasText);
2157
- text.setText("Score: 120");
2158
- text.setColor("#ffd166");
2159
- text.setMaxWidth(400);
2160
- text.setAlign("center"); // "left" | "center" | "right"
1946
+ Storage.hasSave("profile"); // true
1947
+ Storage.removeSave("profile"); // deletes the save AND its backup
2161
1948
  ```
2162
1949
 
2163
- Rule of thumb: `BitmapText` for retro/pixel fonts from a glyph sheet,
2164
- `CanvasText` for everything else (UI, dialogue, any real font).
2165
-
2166
- ## EventManager
1950
+ Plain pre-versioning values load as version 0, so adopting the envelope on an existing game is safe. For raw key/value access, `Storage.saveToLocalStorage`/`readFromLocalStorage` still exist.
2167
1951
 
2168
- Emerald supports keyboard, mouse, click, and hover events. All events are handled using the built-in EventManager class.
1952
+ ## EmeraldDB (IndexedDB saves)
2169
1953
 
2170
- ![EventManager](https://github.com/vahan-gev/emeralddocs/blob/main/github/videos/eventmanager.gif?raw=true)
1954
+ `Storage` lives on localStorage, which caps out around 5MB: plenty for settings and high scores, not for a big persistent world. `EmeraldDB` is the async, big-world companion: same versioned envelope, `.bak` backup, and migration semantics, backed by IndexedDB (effectively unlimited), and values are structured-cloned (no JSON round-trip), so Maps, Sets, Dates, and typed arrays save as-is.
2171
1955
 
2172
1956
  ```javascript
2173
- import { EventManager } from "emeraldengine";
1957
+ import { EmeraldDB } from "emeraldengine";
2174
1958
 
2175
- let eventManager = new EventManager(canvas, scene, emerald.camera);
1959
+ // Versioned world save, mirrors Storage.save/load, but async:
1960
+ await EmeraldDB.save("world", world, { version: 3 });
1961
+ const world = await EmeraldDB.load("world", {
1962
+ version: 3,
1963
+ fallback: makeNewWorld(),
1964
+ migrate: (old, fromVersion) => upgradeWorld(old, fromVersion),
1965
+ });
1966
+ await EmeraldDB.hasSave("world"); // true (checks the .bak too)
1967
+ await EmeraldDB.removeSave("world"); // deletes save + backup
1968
+
1969
+ // Plain async key/value (no envelope):
1970
+ await EmeraldDB.set("settings", { volume: 0.8, keybinds: new Map() });
1971
+ const settings = await EmeraldDB.get("settings", {});
1972
+ await EmeraldDB.keys(); // every key in the store
1973
+
1974
+ // Optional setup:
1975
+ EmeraldDB.configure({ name: "my-game", store: "saves" }); // before first use
1976
+ EmeraldDB.isSupported(); // feature-detect (falls back to Storage if false)
1977
+ await EmeraldDB.importFromStorage("profile"); // one-time upgrade of an old localStorage save
2176
1978
  ```
2177
1979
 
2178
- ### Keyboard Events
1980
+ Rule of thumb: `Storage` for small synchronous bits (settings, best times), `EmeraldDB` for the world.
1981
+
1982
+ ## AssetManager
1983
+
1984
+ One async loader for everything a game needs at startup: images/textures, audio, JSON, text, and web fonts, with deduplication and aggregate progress for a loading bar. Images are routed through `TextureManager`, so the GL upload cache is shared with the rest of the engine.
2179
1985
 
2180
1986
  ```javascript
2181
- // Key down events
2182
- eventManager.addKeyDown("w", () => {
2183
- console.log("W key pressed");
2184
- });
1987
+ import { AssetManager } from "emeraldengine";
2185
1988
 
2186
- // Key up events
2187
- eventManager.addKeyUp("w", () => {
2188
- console.log("W key released");
2189
- });
1989
+ const assets = new AssetManager();
1990
+ assets
1991
+ .image("player", "player.png", { pixelart: true })
1992
+ .audio("jump", "jump.wav")
1993
+ .json("level1", "levels/1.json")
1994
+ .text("credits", "credits.txt")
1995
+ .font("Press Start 2P", "fonts/press-start.woff2");
2190
1996
 
2191
- // Check if key is currently pressed
2192
- if (eventManager.isKeyPressed("w")) {
2193
- // W key is currently held down
2194
- }
1997
+ assets.onProgress((loaded, total) => bar.set(loaded / total));
1998
+ await assets.load({ continueOnError: false }); // rejects on a failed asset unless true
2195
1999
 
2196
- // Remove key events
2197
- eventManager.removeKeyDown("w", callbackFunction);
2198
- eventManager.removeKeyUp("w", callbackFunction);
2000
+ assets.get("player"); // HTMLImageElement
2001
+ assets.get("level1"); // parsed JSON
2002
+ assets.has("jump"); // boolean
2003
+ assets.progress(); // 0..1
2004
+ await assets.getTexture("player"); // { texture, width, height } from the GL cache
2005
+ assets.clear();
2199
2006
  ```
2200
2007
 
2201
- ### Mouse Events
2008
+ ## Asset importers (Tiled, Aseprite & Forge)
2009
+
2010
+ Import maps from [Tiled](https://www.mapeditor.org), sprite-sheet animations from [Aseprite](https://www.aseprite.org), and levels from Emerald's own Tile Forge editor. All three are pure parsers/builders: hand them the already-parsed JSON (load it with `AssetManager.json` or `fetch`).
2202
2011
 
2203
2012
  ```javascript
2204
- // Get mouse position
2205
- const mousePos = eventManager.getMousePosition();
2206
- console.log(mousePos.x, mousePos.y);
2013
+ import { TiledMap } from "emeraldengine";
2207
2014
 
2208
- // Check if camera was moved
2209
- if (eventManager.wasCameraMoved()) {
2210
- // Camera was moved by dragging
2211
- eventManager.resetCameraMoved();
2212
- }
2213
- ```
2015
+ // Build a ready-to-render Tilemap from a Tiled JSON map + its tile sheet.
2016
+ const map = TiledMap.toTilemap(mapJson, "tiles.png", { layer: "ground" });
2017
+ scene.add(map.gameObject);
2018
+ map.buildColliders(physics);
2214
2019
 
2215
- ### Object Events
2020
+ // Or just the frame grid (for your own Tilemap.setMap call):
2021
+ const grid = TiledMap.toFrameGrid(mapJson, { layer: "ground" });
2022
+
2023
+ // Object layers (spawn points, triggers) as plain data; Tiled `properties` are
2024
+ // flattened into `props`, and flipY converts to a y-up world.
2025
+ const spawns = TiledMap.objects(mapJson, { layer: "spawns", flipY: true });
2026
+ // -> [{ name, type, x, y, width, height, gid, props, ... }]
2027
+ ```
2216
2028
 
2217
2029
  ```javascript
2218
- // Click events
2219
- eventManager.addClickEvent(gameObject, (event, object) => {
2220
- console.log("Object clicked!");
2221
- });
2030
+ import { Aseprite, Texture, Animator } from "emeraldengine";
2222
2031
 
2223
- // Hover events
2224
- eventManager.addHoverEvent(
2225
- gameObject,
2226
- (event) => {
2227
- console.log("Mouse entered object");
2228
- },
2229
- (event) => {
2230
- console.log("Mouse left object");
2231
- }
2032
+ const cfg = Aseprite.spriteConfig(sheetJson); // { frameWidth, frameHeight, framesPerRow, totalFrames }
2033
+ const tex = new Texture(
2034
+ "hero.png",
2035
+ cfg.frameWidth,
2036
+ cfg.frameHeight,
2037
+ cfg.framesPerRow,
2038
+ cfg.totalFrames,
2039
+ 0,
2040
+ false
2232
2041
  );
2042
+ gameObject.addComponent(tex);
2233
2043
 
2234
- // Remove events
2235
- eventManager.removeClickEvent(gameObject, callbackFunction);
2236
- eventManager.removeHoverEvent(gameObject, enterCallback, leaveCallback);
2237
- ```
2044
+ const anim = new Animator();
2045
+ gameObject.addComponent(anim);
2046
+ Aseprite.applyTo(anim, sheetJson); // registers a clip per frame-tag
2047
+ anim.play("run");
2238
2048
 
2239
- ### Event Cleanup
2049
+ // Or inspect the clips yourself (handles forward/reverse/pingpong):
2050
+ Aseprite.toClips(sheetJson); // -> [{ name, frames:[...], speed }]
2051
+ ```
2240
2052
 
2241
2053
  ```javascript
2242
- // Clean up all events when done
2243
- eventManager.clean();
2054
+ import { ForgeLevel } from "emeraldengine";
2244
2055
 
2245
- // Change scene
2246
- eventManager.changeScene(newScene);
2056
+ /*
2057
+ ARGUMENTS (options object):
2058
+ 1. scene: Scene = Scene to add the layer objects to.
2059
+ 2. physics: Physics = (OPTIONAL) Physics engine; omit to skip colliders.
2060
+ 3. filter: Object = (OPTIONAL) Collision filter spec for the colliders.
2061
+ 4. ownerObject: GameObject = (OPTIONAL) Owner reported by collision events.
2062
+ 5. pixelart: boolean = (OPTIONAL) NEAREST filtering for the atlas. Default is true.
2063
+ 6. layerOrder: string = (OPTIONAL) "top-first" or "bottom-first": whether layers[0] is the topmost or bottommost layer. Default is "top-first".
2064
+ */
2065
+ const map = ForgeLevel.load(levelJson, {
2066
+ scene,
2067
+ physics,
2068
+ filter: LAYERS.ground,
2069
+ });
2070
+
2071
+ emerald.setBackgroundColor(Color.fromHex(map.background));
2072
+ // map -> { tileSize, cols, rows, width, height, background, bounds,
2073
+ // layers, colliders, objects, entityTypes, toWorld }
2247
2074
  ```
2248
2075
 
2249
- ## AudioManager
2076
+ Each tile layer is drawn as one draw call (an `InstancedTexture` per tileset, batched by atlas). Solid tiles become static bodies: full-tile runs are merged into single `BoxCollider`s, and a tile whose collider is a shape other than the full tile (a ramp, a wedge) gets a real `PolygonCollider` built from that shape's own points, not a bounding-box approximation. Object layers (spawns, pickups, triggers) come back as plain data in `map.objects`, already converted from grid cells to world space.
2250
2077
 
2251
- Emerald includes a comprehensive audio management system.
2078
+ ## Networking (NetworkManager + Interpolator)
2252
2079
 
2253
- ### Adding Audio
2080
+ A thin, optional multiplayer layer over [Colyseus](https://colyseus.io). `colyseus.js` is a peer dependency imported dynamically, so games that don't use networking never load it.
2254
2081
 
2255
2082
  ```javascript
2256
- import { AudioManager } from "emeraldengine";
2083
+ import { NetworkManager } from "emeraldengine";
2257
2084
 
2258
- const audioManager = new AudioManager();
2085
+ const net = new NetworkManager({ interpolation: { delay: 0.1 } });
2086
+ await net.connect("wss://my-server:2567"); // dynamically imports colyseus.js
2087
+ const room = await net.join("arena", { name: "P1" });
2259
2088
 
2260
- // Add audio files
2261
- audioManager.add("path/to/sound.wav", "soundName");
2262
- audioManager.add("path/to/music.mp3", "backgroundMusic");
2089
+ net.onMessage("hit", (msg) => applyHit(msg));
2090
+ net.onStateChange((state) => {
2091
+ for (const [id, p] of state.players) net.interpolator.push(id, p, net.now());
2092
+ });
2093
+ net.onLeave((code) => showDisconnected(code));
2094
+
2095
+ net.send("move", { dir: 1 });
2096
+ net.sessionId; // this client's id
2097
+ await net.leave();
2098
+
2099
+ // each frame, render remote entities "in the past" for smoothness:
2100
+ const pos = net.interpolator.sample(remoteId, net.now()); // { x, y } | null
2263
2101
  ```
2264
2102
 
2265
- ### Playing Audio
2103
+ `Interpolator` is also exported standalone and is pure (no network/DOM), so you can use it with any transport or in tests:
2266
2104
 
2267
2105
  ```javascript
2268
- // Play audio
2269
- audioManager.play("soundName");
2106
+ import { Interpolator } from "emeraldengine";
2270
2107
 
2271
- // Play exclusively (stops all other audio first)
2272
- audioManager.playExclusive("soundName");
2108
+ const interp = new Interpolator({ delay: 0.1, maxBuffer: 60 });
2109
+ interp.push(entityId, { x, y }, serverTimeSeconds); // on each authoritative update
2110
+ const smoothed = interp.sample(entityId, nowSeconds); // each frame
2111
+ interp.prune(nowSeconds); // bound memory for long-lived entities
2112
+ interp.remove(entityId); // when an entity leaves
2113
+ interp.clear();
2273
2114
  ```
2274
2115
 
2275
- ### Audio Control
2276
-
2277
- ```javascript
2278
- // Stop specific audio
2279
- audioManager.stop("soundName");
2116
+ ## Advanced Features
2280
2117
 
2281
- // Stop all audio
2282
- audioManager.stopAll();
2118
+ ### Resize Handling
2283
2119
 
2284
- // Remove audio
2285
- audioManager.remove("soundName");
2120
+ ```javascript
2121
+ // Handle window resize
2122
+ const handleResize = () => {
2123
+ const { width, height } = getCanvasDimensions();
2124
+ emerald.resize(width, height);
2125
+ };
2286
2126
 
2287
- // Get audio object
2288
- const sound = audioManager.getSound("soundName");
2127
+ window.addEventListener("resize", handleResize);
2289
2128
  ```
2290
2129
 
2291
- ## Camera
2130
+ ### Resolution independence
2292
2131
 
2293
- The engine has simple controls for the camera. The camera is stored in the emerald variable.
2132
+ Author your game at one fixed resolution and let the engine scale it to any screen:
2294
2133
 
2295
2134
  ```javascript
2296
- // Set camera position
2297
- emerald.camera.setPosition(x, y, z);
2135
+ // Design at 960x540, letterboxed onto whatever screen the player has:
2136
+ emerald.setDesignResolution(960, 540, "fit");
2298
2137
 
2299
- // Access camera transform directly
2300
- emerald.camera.transform.position.x = 100;
2301
- emerald.camera.transform.position.y = 200;
2302
- emerald.camera.transform.scale.x = 1.5;
2303
- emerald.camera.transform.scale.y = 1.5;
2138
+ // Modes:
2139
+ // "fit" letterbox: whole design visible, bars if aspect differs
2140
+ // "fill" cover: fills the screen, crops the overflow
2141
+ // "stretch" distorts to fill exactly (no bars, no crop)
2142
+ // "pixel" integer scaling, crisp for pixel art
2143
+ emerald.clearDesignResolution(); // back to 1:1 CSS pixels
2144
+
2145
+ // Mouse/touch coordinates -> world space (accounts for the design scale,
2146
+ // letterbox offset, camera zoom/position, and DPR):
2147
+ const world = emerald.screenToWorld(input.mouse.x, input.mouse.y);
2304
2148
  ```
2305
2149
 
2306
- ## FPSCounter
2150
+ ### Auto-pause & lifecycle
2307
2151
 
2308
- Emerald has a built-in FPS counter.
2152
+ `run()` pauses the loop when the tab is hidden (stops audio-desync, timer pileups, and giant delta-time spikes on return). Hooks let you pause music or show an overlay; you can also pause manually:
2309
2153
 
2310
2154
  ```javascript
2311
- import { FPSCounter } from "emeraldengine";
2312
- let fpsCounter = new FPSCounter();
2155
+ emerald.run(update, {
2156
+ pauseOnBlur: true, // default: pause when the tab is hidden
2157
+ pauseOnWindowBlur: false, // stricter: also pause when the window loses focus
2158
+ onPause: () => audio.setMasterVolume(0),
2159
+ onResume: () => audio.setMasterVolume(1),
2160
+ });
2313
2161
 
2314
- const animate = (currentTime) => {
2315
- emerald.drawScene(scene, deltaTime);
2316
- fpsCounter.update(); // Call this in your animation loop
2317
- window.requestAnimationFrame(animate);
2318
- };
2319
- animate();
2162
+ emerald.pause(); // e.g. from your own pause menu
2163
+ emerald.resume();
2320
2164
  ```
2321
2165
 
2322
- ## Scene Management
2166
+ The first `dt` after resuming is clamped (`maxDelta`, default 0.25s), so physics never explodes after a long background stint.
2323
2167
 
2324
- ```javascript
2325
- import { SceneManager } from "emeraldengine";
2168
+ ### Production hardening
2326
2169
 
2327
- // Set active scene
2328
- SceneManager.setScene(scene);
2170
+ Removing an object from a scene keeps its GPU resources alive so it can be re-added. When something is gone for good, dispose it: shared textures are reference-counted and freed when their last user disposes:
2329
2171
 
2330
- // Get current scene
2331
- const currentScene = SceneManager.getScene();
2172
+ ```javascript
2173
+ scene.remove(enemy, { dispose: true }); // buffers + texture reference freed
2174
+ gameObject.destroy(); // same, plus physics bodies + Behaviour.onDestroy
2175
+ scene.dispose(); // tear down an entire level/screen
2176
+ drawable.dispose(); // lowest level, safe to call twice
2332
2177
  ```
2333
2178
 
2334
- ## Time Management
2179
+ Lost WebGL contexts (mobile tab switches, GPU resets, laptops waking) are survived automatically: rendering pauses on loss, and on restore the engine recompiles shaders, re-uploads every cached texture, rebuilds all drawable buffers, custom `Material`s, post effects, and render targets, then resumes. Optional hooks:
2335
2180
 
2336
2181
  ```javascript
2337
- import { Time } from "emeraldengine";
2338
-
2339
- // Get delta time
2340
- const deltaTime = Time.deltaTime;
2341
-
2342
- // Time is automatically updated when you call emerald.drawScene()
2343
- // You can also manually set it
2344
- Time.setDeltaTime(deltaTime);
2182
+ emerald.onContextLost(() => overlay.show("Recovering graphics..."));
2183
+ emerald.onContextRestored(() => overlay.hide());
2345
2184
  ```
2346
2185
 
2347
- ## Advanced Features
2348
-
2349
- ### Resize Handling
2186
+ Spritesheet frame UVs are inset half a texel everywhere, so frames never bleed into neighboring cells. For pixel-art games also snap the camera to whole pixels:
2350
2187
 
2351
2188
  ```javascript
2352
- // Handle window resize
2353
- const handleResize = () => {
2354
- const { width, height } = getCanvasDimensions();
2355
- emerald.resize(width, height);
2356
- };
2357
-
2358
- window.addEventListener("resize", handleResize);
2189
+ emerald.camera.setPixelSnap(true); // rendered position rounds; stored position stays smooth
2359
2190
  ```
2191
+
2192
+ ## NPM scripts
2193
+
2194
+ | Script | Purpose |
2195
+ | ---------------- | --------------------------------------------- |
2196
+ | `npm test` | Node test suite (`node --test test/`) |
2197
+ | `npm run types` | Regenerate `dist/types` from JSDoc via `tsc` |
2198
+ | `npm run format` | Prettier |