melonjs 20.8.0 → 20.9.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 (182) hide show
  1. package/README.md +1 -0
  2. package/build/application/application.d.ts +66 -8
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/defaultApplicationSettings.d.ts +1 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/resize.d.ts +25 -0
  7. package/build/application/resize.d.ts.map +1 -1
  8. package/build/application/scaleMethods.d.ts +13 -3
  9. package/build/application/scaleMethods.d.ts.map +1 -1
  10. package/build/application/settings.d.ts +56 -5
  11. package/build/application/settings.d.ts.map +1 -1
  12. package/build/audio/audio.d.ts +29 -5
  13. package/build/audio/audio.d.ts.map +1 -1
  14. package/build/audio/autopause.d.ts +36 -0
  15. package/build/audio/autopause.d.ts.map +1 -0
  16. package/build/audio/backend/engine.d.ts +10 -0
  17. package/build/audio/backend/engine.d.ts.map +1 -1
  18. package/build/audio/backend/sound.d.ts.map +1 -1
  19. package/build/audio/backend/spatial.d.ts.map +1 -1
  20. package/build/audio/context.d.ts +35 -0
  21. package/build/audio/context.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts.map +1 -1
  23. package/build/audio/procedural.d.ts.map +1 -1
  24. package/build/audio/spatial.d.ts +87 -0
  25. package/build/audio/spatial.d.ts.map +1 -0
  26. package/build/audio/state.d.ts +1 -0
  27. package/build/audio/state.d.ts.map +1 -1
  28. package/build/audio/types.d.ts +46 -6
  29. package/build/audio/types.d.ts.map +1 -1
  30. package/build/camera/camera2d.d.ts.map +1 -1
  31. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  32. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  33. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  34. package/build/geometries/box3d.d.ts +1 -1
  35. package/build/geometries/box3d.d.ts.map +1 -1
  36. package/build/geometries/ellipse.d.ts +1 -1
  37. package/build/geometries/ellipse.d.ts.map +1 -1
  38. package/build/geometries/line.d.ts +1 -1
  39. package/build/geometries/line.d.ts.map +1 -1
  40. package/build/geometries/point.d.ts +1 -1
  41. package/build/geometries/point.d.ts.map +1 -1
  42. package/build/geometries/polygon.d.ts +1 -1
  43. package/build/geometries/polygon.d.ts.map +1 -1
  44. package/build/geometries/rectangle.d.ts +1 -1
  45. package/build/geometries/rectangle.d.ts.map +1 -1
  46. package/build/geometries/roundrect.d.ts +1 -1
  47. package/build/geometries/roundrect.d.ts.map +1 -1
  48. package/build/geometries/sphere.d.ts +1 -1
  49. package/build/geometries/sphere.d.ts.map +1 -1
  50. package/build/index.d.ts +8 -2
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +36985 -34678
  53. package/build/index.js.map +4 -4
  54. package/build/input/gamepad.d.ts +129 -3
  55. package/build/input/gamepad.d.ts.map +1 -1
  56. package/build/level/gltf/GLTFModel.d.ts +5 -4
  57. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  58. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  59. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  60. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  61. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  62. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -4
  63. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  64. package/build/loader/cache.d.ts +220 -0
  65. package/build/loader/cache.d.ts.map +1 -1
  66. package/build/loader/loader.d.ts +20 -349
  67. package/build/loader/loader.d.ts.map +1 -1
  68. package/build/loader/parsers/gltf.d.ts +115 -0
  69. package/build/loader/parsers/gltf.d.ts.map +1 -1
  70. package/build/math/color.d.ts +1 -1
  71. package/build/math/color.d.ts.map +1 -1
  72. package/build/math/matrix2d.d.ts +1 -1
  73. package/build/math/matrix2d.d.ts.map +1 -1
  74. package/build/math/matrix3d.d.ts +1 -1
  75. package/build/math/matrix3d.d.ts.map +1 -1
  76. package/build/math/observableVector2d.d.ts +14 -2
  77. package/build/math/observableVector2d.d.ts.map +1 -1
  78. package/build/math/observableVector3d.d.ts +15 -1
  79. package/build/math/observableVector3d.d.ts.map +1 -1
  80. package/build/math/vector2d.d.ts +31 -1
  81. package/build/math/vector2d.d.ts.map +1 -1
  82. package/build/math/vector3d.d.ts +38 -1
  83. package/build/math/vector3d.d.ts.map +1 -1
  84. package/build/math/vertex.d.ts.map +1 -1
  85. package/build/particles/emitter.d.ts.map +1 -1
  86. package/build/particles/particle.d.ts +7 -1
  87. package/build/particles/particle.d.ts.map +1 -1
  88. package/build/particles/settings.d.ts +35 -0
  89. package/build/particles/settings.d.ts.map +1 -1
  90. package/build/physics/bounds.d.ts +1 -1
  91. package/build/physics/bounds.d.ts.map +1 -1
  92. package/build/physics/broadphase/octree.d.ts +24 -0
  93. package/build/physics/broadphase/octree.d.ts.map +1 -1
  94. package/build/physics/broadphase/quadtree.d.ts +31 -0
  95. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  96. package/build/physics/builtin/body.d.ts +14 -0
  97. package/build/physics/builtin/body.d.ts.map +1 -1
  98. package/build/pool.d.ts +4 -0
  99. package/build/pool.d.ts.map +1 -1
  100. package/build/renderable/colorlayer.d.ts +13 -0
  101. package/build/renderable/colorlayer.d.ts.map +1 -1
  102. package/build/renderable/container.d.ts.map +1 -1
  103. package/build/renderable/groundshadow.d.ts.map +1 -1
  104. package/build/renderable/instanced_mesh.d.ts +56 -0
  105. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  106. package/build/renderable/mesh.d.ts +97 -11
  107. package/build/renderable/mesh.d.ts.map +1 -1
  108. package/build/renderable/renderable.d.ts +41 -3
  109. package/build/renderable/renderable.d.ts.map +1 -1
  110. package/build/renderable/sprite.d.ts +2 -1
  111. package/build/renderable/sprite.d.ts.map +1 -1
  112. package/build/renderable/sprite3d.d.ts +6 -2
  113. package/build/renderable/sprite3d.d.ts.map +1 -1
  114. package/build/renderable/text/bitmaptext.d.ts +63 -2
  115. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  116. package/build/renderable/text/bitmaptextdata.d.ts +1 -1
  117. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  118. package/build/renderable/text/glypheffect.d.ts +44 -0
  119. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  120. package/build/renderable/text/text.d.ts +17 -0
  121. package/build/renderable/text/text.d.ts.map +1 -1
  122. package/build/renderable/trail.d.ts.map +1 -1
  123. package/build/renderable/trail3d.d.ts +174 -0
  124. package/build/renderable/trail3d.d.ts.map +1 -0
  125. package/build/renderable/trigger.d.ts +2 -1
  126. package/build/renderable/trigger.d.ts.map +1 -1
  127. package/build/renderable/ui/panelpaint.d.ts +5 -0
  128. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  129. package/build/renderable/ui/progressbar.d.ts +2 -4
  130. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  131. package/build/renderable/ui/uipanel.d.ts +245 -0
  132. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  133. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  134. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  135. package/build/state/state.d.ts.map +1 -1
  136. package/build/system/bootstrap.d.ts.map +1 -1
  137. package/build/system/legacy_pool.d.ts +22 -2
  138. package/build/system/legacy_pool.d.ts.map +1 -1
  139. package/build/system/pool.d.ts +5 -8
  140. package/build/system/pool.d.ts.map +1 -1
  141. package/build/tweens/tween.d.ts +1 -1
  142. package/build/tweens/tween.d.ts.map +1 -1
  143. package/build/video/effects/tintPulse.d.ts +0 -5
  144. package/build/video/effects/tintPulse.d.ts.map +1 -1
  145. package/build/video/effects/wave.d.ts +0 -5
  146. package/build/video/effects/wave.d.ts.map +1 -1
  147. package/build/video/gpu/batcher.d.ts +8 -2
  148. package/build/video/gpu/batcher.d.ts.map +1 -1
  149. package/build/video/gradient.d.ts +1 -0
  150. package/build/video/gradient.d.ts.map +1 -1
  151. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  152. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  153. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  154. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  155. package/build/video/rendertarget/webgpurendertarget.d.ts +6 -12
  156. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  157. package/build/video/texture/atlas.d.ts +1 -1
  158. package/build/video/texture/atlas.d.ts.map +1 -1
  159. package/build/video/texture/texture2d.d.ts +6 -0
  160. package/build/video/texture/texture2d.d.ts.map +1 -1
  161. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -11
  162. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  163. package/package.json +2 -2
  164. package/skills/AGENTS.md +1 -1
  165. package/skills/melonjs/SKILL.md +2 -0
  166. package/skills/melonjs-3d/SKILL.md +192 -18
  167. package/skills/melonjs-3d-assets/SKILL.md +36 -2
  168. package/skills/melonjs-audio/SKILL.md +229 -8
  169. package/skills/melonjs-camera-and-drawing/SKILL.md +73 -3
  170. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  171. package/skills/melonjs-effects-and-shaders/SKILL.md +4 -4
  172. package/skills/melonjs-events/SKILL.md +8 -6
  173. package/skills/melonjs-input/SKILL.md +100 -4
  174. package/skills/melonjs-math/SKILL.md +313 -0
  175. package/skills/melonjs-particles-and-trails/SKILL.md +88 -14
  176. package/skills/melonjs-performance/SKILL.md +50 -19
  177. package/skills/melonjs-physics/SKILL.md +2 -1
  178. package/skills/melonjs-renderables/SKILL.md +6 -1
  179. package/skills/melonjs-scenes-and-state/SKILL.md +14 -3
  180. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  181. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  182. package/skills/melonjs-ui-and-text/SKILL.md +382 -4
@@ -0,0 +1,313 @@
1
+ ---
2
+ name: melonjs-math
3
+ description: "Use this skill for vector and matrix maths in melonJS — Vector2d, Vector3d, their observable variants, Matrix2d, Matrix3d, and the math utility functions. Covers which methods mutate and which allocate, the vector pool and when clone() is the wrong tool, interpolation with lerp, slerp, damp and moveTowards, and the argument defaults that silently drop a component. Triggers on: Vector2d, Vector3d, ObservableVector2d, ObservableVector3d, Matrix2d, Matrix3d, vector, vector maths, dot, cross, normalize, length, distance, angle, lerp, slerp, damp, moveTowards, project, clamp, degToRad, radToDeg, clone, vector pool, vector3dPool, setMuted, setCallback, my vector is wrong, z is zero, scale ignores z., moveTowards ignores z, projectN, currentTransform, setTransform, matrix, transform matrix, column-major, val, Float32Array, basis matrix, orientation matrix."
4
+ license: MIT
5
+ ---
6
+
7
+ # Vectors, matrices and maths
8
+
9
+ ## The one rule that explains most surprises
10
+
11
+ **Methods mutate `this` and return it. They do not return a new vector.**
12
+
13
+ ```js
14
+ const a = new Vector3d(1, 0, 0);
15
+ const b = new Vector3d(0, 1, 0);
16
+
17
+ a.cross(b); // a IS NOW the cross product; the original a is gone
18
+ a.add(b).normalize().scale(3, 3, 3); // chains, all in place
19
+ ```
20
+
21
+ That is what makes the API allocation-free, and it is the opposite of what a
22
+ functional style expects. If you need to keep the original, copy into
23
+ something you own first:
24
+
25
+ ```js
26
+ out.copy(a).cross(b); // a untouched, nothing allocated
27
+ ```
28
+
29
+ **The exceptions are named.** A handful of methods return a NEW vector, and
30
+ each has a `*Self` sibling that does not:
31
+
32
+ | allocates a new vector | mutates in place |
33
+ |---|---|
34
+ | `negate()` | `negateSelf()` |
35
+ | `floor()` | `floorSelf()` |
36
+ | `ceil()` | `ceilSelf()` |
37
+ | `clamp(low, high)` | `clampSelf(low, high)` |
38
+ | `clone()` | `copy(v)` |
39
+
40
+ So `v.negate()` in a per-frame loop allocates every frame and `v.negateSelf()`
41
+ does not. The names are the only clue; nothing in the call site shows it.
42
+
43
+ ## `clone()` comes from a pool, and only pays off if you give it back
44
+
45
+ `clone()` is `vector3dPool.get(...)`. A pool is fast when instances come back:
46
+ `get()` takes a free one and resets it. Nothing comes back on its own, so a
47
+ clone that is simply dropped makes `get()` miss, construct, and write two
48
+ registry entries.
49
+
50
+ Measured, 3M operations:
51
+
52
+ | | |
53
+ |---|---|
54
+ | `out.copy(a).cross(b)` | 39ms |
55
+ | `new Vector3d(ax, ay, az).cross(b)` | 37ms |
56
+ | `a.clone().cross(b)` | **2919ms** |
57
+
58
+ So:
59
+
60
+ - **Transient value inside a function?** Use a module-level scratch vector, or
61
+ `new Vector3d()`. Not `clone()`.
62
+ - **Keeping it, then done with it?** `clone()` and `vector3dPool.release(v)`
63
+ when finished. Releasing is what makes the pool a pool.
64
+ - **Writing a helper that returns a vector?** Take an optional `out`:
65
+
66
+ ```js
67
+ const cross = (a, b, out = new Vector3d()) => out.copy(a).cross(b);
68
+ ```
69
+
70
+ That is the engine's own convention, and it lets a hot caller supply scratch
71
+ while a casual one ignores it.
72
+
73
+ **A returned value can never be shared scratch.** The next call overwrites
74
+ something the caller is still holding, and nothing fails loudly:
75
+
76
+ ```js
77
+ const _t = new Vector3d();
78
+ const bad = (v) => _t.copy(v).normalize(); // WRONG: two calls alias
79
+ ```
80
+
81
+ ## Argument defaults that drop a component
82
+
83
+ Three signatures quietly discard what you did not pass. All three are
84
+ reachable by writing the obvious thing.
85
+
86
+ **`scale(x, y = x, z = 1)`** — `z` defaults to **1**, not to `x`. A uniform
87
+ scale has to say so three times:
88
+
89
+ ```js
90
+ v.scale(2); // (1,2,3) -> (2,4,3) z UNTOUCHED
91
+ v.scale(2, 2, 2); // (1,2,3) -> (2,4,6) what you meant
92
+ ```
93
+
94
+ **`set(x, y, z = 0)`** — setting two components **zeroes the third**:
95
+
96
+ ```js
97
+ v.set(10, 20); // z is now 0, whatever it was
98
+ ```
99
+
100
+ **`Vector3d.pos` on a renderable is typed `Vector2d`.** `Renderable.pos` is an
101
+ `ObservableVector3d` at run time but is declared through `Polygon` as a
102
+ `Vector2d`, so `.z`, `.setMuted` and `.setCallback` need a cast from
103
+ TypeScript. See issue #1510.
104
+
105
+ ## `cross` is two different functions
106
+
107
+ ```js
108
+ v3.cross(other); // Vector3d: MUTATES v3, returns this
109
+ v2.cross(other); // Vector2d: returns a NUMBER, mutates nothing
110
+ ```
111
+
112
+ The 2D one is the z component of the 3D cross, which is the useful scalar for
113
+ "which side is this on". Same name, different shape, different return.
114
+
115
+ ## Interpolation: four of them, and they are not interchangeable
116
+
117
+ ```js
118
+ a.lerp(b, t); // straight line, t in 0..1
119
+ a.slerp(b, t); // along the ARC, constant angular rate
120
+ a.damp(b, lambda, dt); // frame-rate independent ease toward b
121
+ a.moveTowards(b, step); // fixed distance per call, never overshoots
122
+ ```
123
+
124
+ **`moveTowards` is X/Y ONLY, on `Vector3d` as well as `Vector2d`.** It is the
125
+ one of the four that is not 3D: it measures the distance from `dx` and `dy`,
126
+ steps along `atan2(dy, dx)`, and leaves `z` exactly where it was. The source
127
+ says so in a comment and the JSDoc says "on the x and y axis", but the name
128
+ gives no hint, and in a list like the one above it reads as the sibling of the
129
+ other three. Anything moving in three dimensions wants `damp` or an explicit
130
+ step; reach for `moveTowards` only when the motion really is planar.
131
+
132
+ **`lerp` between two directions is uneven**: it slows at the ends, runs fast
133
+ through the middle, and shortens the vector as it passes halfway. Anything
134
+ whose direction matters more than its components wants `slerp` — a camera
135
+ swinging to look somewhere, a turret tracking, a body moving over a sphere.
136
+ `slerp` carries length linearly, so two unit vectors give a unit vector and
137
+ nothing needs normalising afterwards.
138
+
139
+ **`damp` is the one to use for "ease toward a target every frame"**, because
140
+ `lerp(current, target, 0.1)` per frame is frame-rate dependent: it converges
141
+ twice as fast at 120fps as at 60.
142
+
143
+ ## `project` scales THIS, and `projectN` is the one without the divide
144
+
145
+ `a.project(b)` does not return the component of `a` along `b`. It scales **a**
146
+ by `a.b / |b|^2` and returns `a`, so the result points along `a`, not along
147
+ `b`. Worth knowing before you reach for it expecting the textbook projection.
148
+
149
+ `projectN(v)` is the same thing for a `v` that is already unit, so it skips
150
+ the `/ v.length2()`:
151
+
152
+ ```js
153
+ a.project(b); // a scaled by (a.b / |b|^2)
154
+ a.projectN(b); // a scaled by (a.b) -- only correct when |b| = 1
155
+ ```
156
+
157
+ Handing `projectN` a target that is NOT unit does not normalise anything, it
158
+ just scales by the raw dot product. That is the price of it being cheaper, and
159
+ it is the caller's job to meet the contract.
160
+
161
+ Until 20.9 both 3D classes got this wrong: `Vector3d.projectN` and
162
+ `ObservableVector3d.projectN` were copies of `project`, division and all,
163
+ under `projectN`'s name and its "slightly more efficient" doc comment. Nothing
164
+ in the engine called either one and nothing tested them, which is how it
165
+ survived. Both are fixed and all four classes are now pinned by tests.
166
+
167
+ ## `length()` is `sqrt(x*x + y*y + z*z)`
168
+
169
+ Fast and correct to 1 ULP for anything a game holds. It is NOT robust at
170
+ extremes: components above ~1e154 overflow to `Infinity` and below ~1e-162
171
+ underflow to `0`, because the squares are formed before the root. `Math.hypot`
172
+ avoids that and costs 35 to 65% more. The engine takes the speed, which is the
173
+ right trade for coordinates between 1e-3 and 1e6; nothing in a game is 148
174
+ orders of magnitude from unit scale.
175
+
176
+ `length2()` skips the square root. Use it for comparisons:
177
+
178
+ ```js
179
+ if (a.distance(b) < r) { } // a square root you did not need
180
+ if (a.sub(b).length2() < r * r) { } // same answer, cheaper
181
+ ```
182
+
183
+ ## Observable vectors
184
+
185
+ `ObservableVector2d` / `ObservableVector3d` call a callback on every write,
186
+ which is how a renderable knows its bounds went stale.
187
+
188
+ ```js
189
+ v.setCallback(() => { ... });
190
+ v.setMuted(true); // write without firing it
191
+ ```
192
+
193
+ **`setMuted` is what you want when writing several components**, so a
194
+ half-updated vector does not fire a callback that reads the other half.
195
+ `set()` already does this internally: it disables the callback, writes all
196
+ three, fires once, and re-enables.
197
+
198
+ Note the observable `set(x = 0, y = 0, z = 0)` defaults EVERY argument, so
199
+ `set(10, 20)` zeroes z exactly as the plain vector does.
200
+
201
+ ## Matrices
202
+
203
+ `Matrix2d` is 3x3 for 2D affine transforms; `Matrix3d` is 4x4. Both mutate and
204
+ chain, same as vectors, and both come from pools.
205
+
206
+ ```js
207
+ m.identity().translate(x, y).rotate(angle).scale(sx, sy);
208
+ ```
209
+
210
+ **Order is the reverse of how it reads.** These post-multiply, so the call
211
+ made LAST reaches the vertex FIRST. To rotate a thing about its own centre,
212
+ translate to the centre last.
213
+
214
+ ### Every renderable already HAS one
215
+
216
+ `Renderable.currentTransform` is a `Matrix3d`. If you are orienting something
217
+ on screen, the matrix exists: write into it rather than building a parallel
218
+ structure and copying.
219
+
220
+ ```js
221
+ // the whole transform of a renderable, written in place
222
+ mesh.currentTransform.identity().translate(x, y, z).rotate(angle, axis);
223
+ ```
224
+
225
+ ### The sixteen numbers are `val`, column-major
226
+
227
+ Composing with `translate`/`rotate`/`scale` is one way to fill a matrix. The
228
+ other is to write the numbers, which is what you want when the orientation
229
+ comes from a basis you have already computed — three orthonormal vectors, a
230
+ surface frame, a camera basis.
231
+
232
+ `val` is a `Float32Array(16)` in **column-major** order, the OpenGL
233
+ convention: each basis vector occupies a stride-4 run, and the translation is
234
+ the last column.
235
+
236
+ | | X column | Y column | Z column | translation |
237
+ |---|---|---|---|---|
238
+ | indices | `0,1,2` | `4,5,6` | `8,9,10` | `12,13,14` |
239
+
240
+ ```js
241
+ // an orientation from three axes, straight into the renderable's own matrix
242
+ const m = mesh.currentTransform.val;
243
+ m[0] = right.x; m[1] = right.y; m[2] = right.z; m[3] = 0;
244
+ m[4] = up.x; m[5] = up.y; m[6] = up.z; m[7] = 0;
245
+ m[8] = fwd.x; m[9] = fwd.y; m[10] = fwd.z; m[11] = 0;
246
+ m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
247
+ ```
248
+
249
+ A helper that builds one should take the destination, the same way the engine
250
+ takes an `out`, so it writes once into the matrix that will be used:
251
+
252
+ ```js
253
+ const orient = (frame, out) => { /* writes out.val */ return out; };
254
+ orient(frame, mesh.currentTransform);
255
+ ```
256
+
257
+ ### Getting numbers IN from somewhere else
258
+
259
+ `setTransform(...)` takes the sixteen as arguments, and `copy(m)` takes
260
+ another matrix:
261
+
262
+ ```js
263
+ m.setTransform(a, b, c, d, e, f, g, h, i, j, k, l, p, q, r, s);
264
+ m.copy(other); // Float32Array.set under the hood
265
+ ```
266
+
267
+ Both are fine occasionally. In a per-frame loop, prefer writing `val` or
268
+ `copy()` over spreading sixteen arguments, and note that **one matrix built
269
+ once and applied to many renderables** is a `copy()` per renderable, not a
270
+ rebuild per renderable.
271
+
272
+ **`val` is `Float32Array`, so it holds float32.** Numbers you put in are
273
+ rounded to single precision. That is where they were always heading, since
274
+ this is what reaches the GPU, but it means a value read back out is not
275
+ bit-identical to the double you wrote.
276
+
277
+ ## Utilities
278
+
279
+ ```js
280
+ import { math } from "melonjs";
281
+
282
+ math.clamp(v, lo, hi);
283
+ math.lerp(a, b, t); // scalars
284
+ math.damp(a, b, lambda, dt); // frame-rate independent
285
+ math.degToRad(deg); math.radToDeg(rad);
286
+ math.random(lo, hi); // integer
287
+ math.randomFloat(lo, hi);
288
+ math.weightedRandom(lo, hi); // integer, biased toward LO
289
+ math.round(n, decimals);
290
+ math.pow(n); // n squared
291
+ math.DEG_TO_RAD math.RAD_TO_DEG math.TAU math.ETA math.EPSILON
292
+ ```
293
+
294
+ ## Symptoms
295
+
296
+ | symptom | cause |
297
+ |---|---|
298
+ | z unchanged after scaling | `scale(s)` leaves z alone; pass `scale(s, s, s)` |
299
+ | z became 0 for no reason | `set(x, y)` defaults z to 0 |
300
+ | the original vector changed | the method mutates; `copy()` into something first |
301
+ | memory climbs in a loop | `clone()` or `negate()`/`floor()`/`ceil()` per frame |
302
+ | two helper results are the same object | a shared scratch vector was returned |
303
+ | `.z` is a type error | `Renderable.pos` is declared `Vector2d` (#1510) |
304
+ | rotation eases oddly between directions | `lerp` on directions; use `slerp` |
305
+ | you built your own 16-number array | `Renderable.currentTransform` is already a `Matrix3d`; write its `val` |
306
+ | a value read back from a matrix differs | `val` is `Float32Array`; it holds float32 |
307
+ | easing differs at 120fps | per-frame `lerp`; use `damp` |
308
+
309
+ ## Related skills
310
+
311
+ - `melonjs-3d` — `Camera3d`, meshes, and where these vectors are used in 3D
312
+ - `melonjs-physics` — bodies, velocities and collision response
313
+ - `melonjs-performance` — pooling in general, and what release() is for
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-particles-and-trails
3
- description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter."
3
+ description: "Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: Trail3d, ribbon, exhaust trail, speed streak, ski track, tyre mark, ribbonNormal, ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -28,6 +28,47 @@ emitter.burstParticles(); // one shot
28
28
  `duration` setting, which defaults to `Infinity` — a stream runs until
29
29
  `stopStream()`. `burstParticles(n)` launches `n`, or `totalParticles`.
30
30
 
31
+ ## Sprite-sheet particles
32
+
33
+ Use `framewidth` and `frameheight` (the same names as on `Sprite`) to divide
34
+ `image` into a regular grid. Each omitted dimension defaults to that of the
35
+ image. Frames are numbered from zero left-to-right, then top-to-bottom; only
36
+ whole cells count, so partial cells along the right/bottom edges are ignored.
37
+ Margins and spacing are not supported.
38
+
39
+ ```js
40
+ const explosion = new ParticleEmitter(x, y, {
41
+ image: loader.getImage("explosion-sheet"),
42
+ framewidth: 32, frameheight: 32,
43
+ frames: [0, 1, 2, 3],
44
+ minLife: 400, maxLife: 800,
45
+ });
46
+ world.addChild(explosion);
47
+ explosion.burstParticles();
48
+ ```
49
+
50
+ Omit `frames` to use every cell. Each particle traverses the sequence once
51
+ over its own lifetime, with equal time for each entry; repeated indices hold
52
+ a frame longer. Existing scale, opacity and rotation still apply. Bounds and
53
+ the rotation pivot use one frame's dimensions, rather than the whole sheet.
54
+
55
+ Set `randomFrame: true` for debris: a particle chooses one entry at birth and
56
+ keeps it until it dies. Different frames share the same image and GPU batch.
57
+ Without sheet settings, particles still draw the whole image as before, and
58
+ that includes `randomFrame` on its own: it needs a grid to choose from, so
59
+ pair it with `framewidth`/`frameheight` or it does nothing.
60
+
61
+ Note the variety is in SHAPE, not colour. `tint` belongs to the emitter, so
62
+ every particle from one emitter is tinted alike however many frames it picks
63
+ between.
64
+
65
+ Frame dimensions must be positive integers that fit inside the image, and an
66
+ explicit `frames` sequence must be nonempty and contain valid integer indices.
67
+ Invalid configurations throw `RangeError` before changing an emitter during
68
+ `reset()`. The frame sequence is copied on construction/reset; resetting changes
69
+ only future particles, so ones already alive finish with their original image,
70
+ frame size and sequence.
71
+
31
72
  ## Where particles are born
32
73
 
33
74
  `width` and `height` are the spawn AREA, and both default to `1` — so by
@@ -184,19 +225,48 @@ Omit `target` and feed it yourself with `trail.addPoint(x, y)` — the mode for
184
225
  sword slash, where the ribbon follows a weapon tip rather than an object. Points
185
226
  closer together than `minDistance` (default 4 px) are dropped.
186
227
 
187
- **`Trail` is 2D.** `addPoint` takes `(x, y)`, points are stored without a `z`,
188
- and the ribbon is drawn as connected screen-space quads. Under a `Camera3d` it
189
- draws a flat ribbon that does not recede with perspective — so it is the wrong
190
- tool for a trail behind an object moving *into* the scene. For that:
228
+ **`Trail` is 2D. Use `Trail3d` in a 3D scene** (20.9.0 and later). `Trail`
229
+ stores points without a `z` and draws connected screen-space quads, which carry
230
+ one depth for the whole renderable, so under a `Camera3d` the ribbon stays flat
231
+ and does not narrow along its own length.
232
+
233
+ ```js
234
+ const exhaust = new Trail3d(0, 0, {
235
+ target: ship, // samples pos.x / pos.y / pos.z
236
+ width: 6, // world units, and free to change per frame
237
+ lifetime: 260,
238
+ gradient: ["#eaffff", "#2ea8ff", "#2ea8ff00"],
239
+ });
240
+ exhaust.blendMode = "additive";
241
+ app.world.addChild(exhaust); // no z: it derives its own from its points
242
+ ```
191
243
 
192
- - **`ParticleEmitter` with `referenceSpace: "world"`** — particles stay where
193
- they were dropped while the emitter moves on, and each one sits at the
194
- emitter's depth slice so `Camera3d` projects it correctly. Add an `elevation`
195
- and they recede as well. This is the built-in answer for exhaust, ski tracks
196
- and speed trails in 3D.
197
- - **A procedural `Mesh`** built from the object's recent path, with
198
- `mesh.vertexColors` for a per-vertex gradient — more work, but a continuous
199
- ribbon rather than discrete puffs.
244
+ `Trail3d` is a `Mesh`, which is most of the difference:
245
+
246
+ - `billboard` is spelled and valued exactly as on `Sprite3d` —
247
+ `"spherical"` (the default here) faces the camera on all axes,
248
+ `true` / `"cylindrical"` faces it but stays upright, `false` uses the
249
+ `ribbonNormal` you give it. `billboard: false, ribbonNormal: [0, -1, 0]`
250
+ lays the ribbon in the ground plane for a tyre mark or a wake.
251
+ - **`tint` works**, and is the per-frame colour lever. On `Trail` it does
252
+ nothing at all, because the primitive batcher reads the draw colour and never
253
+ the tint. `setColor()` / `setGradient()` rebuild the gradient for a change of
254
+ state; they allocate, so they are not per-frame.
255
+
256
+ **If the colour is going to change, author the gradient WHITE.** `tint`
257
+ multiplies, so tinting a cyan ribbon red gives muddy brown rather than red. A
258
+ neutral gradient hands the hue entirely to the tint, which is what lets one
259
+ lever take a trail from white through to red as something overheats.
260
+ - A post effect reaches it properly, where `Trail` has to force a screen
261
+ capture.
262
+ - It is **cheaper**: one indexed draw call, against one filled path per segment.
263
+ - `length` is fixed at construction, because the geometry is sized once.
264
+ Assigning `maxPoints` throws rather than silently truncating the ribbon, and
265
+ a `length` that is not a finite number throws at construction.
266
+ - It needs a `Camera3d`. Under a 2D camera it draws nothing and warns once.
267
+
268
+ A world-space `ParticleEmitter` (`referenceSpace: "world"`) is still the right
269
+ answer when you want discrete puffs rather than a continuous ribbon.
200
270
 
201
271
  ## Performance
202
272
 
@@ -219,7 +289,11 @@ tool for a trail behind an object moving *into* the scene. For that:
219
289
 
220
290
  | symptom | cause |
221
291
  |---|---|
222
- | trail looks flat under a `Camera3d` | `Trail` is 2D — use a world-space emitter or a ribbon mesh |
292
+ | trail looks flat under a `Camera3d` | `Trail` is 2D; use `Trail3d` |
293
+ | `Trail3d` draws nothing, with a console warning | it needs a `Camera3d`; the app's `cameraClass` is still the 2D default |
294
+ | `trail.tint` does nothing | `Trail` cannot be tinted at all; `Trail3d` can |
295
+ | assigning `maxPoints` throws | `Trail3d` sizes its geometry once; pass `length` to the constructor |
296
+ | `Trail3d: \`length\` must be a finite number` | a computed `length` came out `NaN`; it is refused rather than silently rendering nothing |
223
297
  | emitter added but nothing appears | `streamParticles()` / `burstParticles()` never called |
224
298
  | the cloud follows a moving emitter | default `referenceSpace: "local"` — use `"world"` for trails |
225
299
  | particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
@@ -59,16 +59,28 @@ Anything spawned frequently — bullets, particles, pickups, damage numbers —
59
59
  should be pooled rather than allocated:
60
60
 
61
61
  ```js
62
- pool.register("bullet", Bullet, true); // third arg = enable recycling
63
- const b = pool.pull("bullet", x, y); // recycled: onResetEvent(x, y)
64
- // fresh: new Bullet(x, y)
65
- world.removeChild(b); // returns to the pool automatically
62
+ import { createPool } from "melonjs";
63
+
64
+ const bulletPool = createPool((x, y) => {
65
+ const instance = new Bullet(x, y);
66
+ return { instance, reset: (x, y) => instance.onResetEvent(x, y) };
67
+ });
68
+
69
+ const b = bulletPool.get(x, y); // recycled: reset(x, y)
70
+ // fresh: new Bullet(x, y)
71
+ world.removeChild(b); // returns to bulletPool automatically
66
72
  ```
67
73
 
68
- `register` also publishes the class as a Tiled object factory under that name,
69
- so an object with a matching class or name in a `.tmx` map instantiates it. Set
70
- `pool.autoRegisterTiled = false` around the call for classes that should stay
71
- programmatic.
74
+ The container returns a child to whichever pool built it, so removal is enough;
75
+ you only call `release()` yourself for something that was never added to the
76
+ world.
77
+
78
+ **`me.pool` is the older, string-keyed version and is deprecated since
79
+ 18.0.0.** `pool.register(name, Class, true)` / `pool.pull(name)` still work and
80
+ print a one-off console notice, but a recycled object is no longer returned on
81
+ removal, so pooling through it now costs an allocation per spawn. To register a
82
+ class so a Tiled map can name it, use `registerTiledObjectClass` — that is a
83
+ separate registry and is not deprecated.
72
84
 
73
85
  Two rules that cause subtle bugs when missed:
74
86
 
@@ -76,10 +88,11 @@ Two rules that cause subtle bugs when missed:
76
88
  alpha, tint, scale, animation, velocity. Anything you forget carries into the
77
89
  next use, which looks like a random visual glitch.
78
90
  - **Pooled objects do not fire `onDestroyEvent` on removal.** Removal always
79
- calls `onDeactivateEvent`, then tries `pool.push`; only when that *fails* does
80
- it fall through to `destroy()`, which is what calls `onDestroyEvent`. So a
81
- successfully recycled object never sees it. Pair event subscriptions with
82
- `onActivateEvent` / `onDeactivateEvent` instead, or you leak handlers.
91
+ calls `onDeactivateEvent`, then tries to return the object to the pool that
92
+ built it; only when nothing owns it does it fall through to `destroy()`,
93
+ which is what calls `onDestroyEvent`. So a successfully recycled object never
94
+ sees it. Pair event subscriptions with `onActivateEvent` /
95
+ `onDeactivateEvent` instead, or you leak handlers.
83
96
 
84
97
  - **`removeChild` is DEFERRED, which breaks a pool you re-lend in the same
85
98
  frame.** The removal is queued and runs after the update and draw stack has
@@ -98,14 +111,32 @@ Two rules that cause subtle bugs when missed:
98
111
  usual cause of a pooled effect that flickers out one frame after it is
99
112
  recycled.
100
113
 
101
- Engine classes are poolable too: `pool.pull("Tween", target)`. The canonical
102
- names are unprefixed — `Entity`, `Collectable`, `Trigger`, `Light2d`,
103
- `Particle`, `Sprite`, `NineSliceSprite`, `Renderable`, `Text`, `BitmapText`,
104
- `ImageLayer`, `Tween`, `ColorLayer`.
114
+ **The engine's own classes are NOT in this pool.** They were registered in it
115
+ once, which bought nothing: most had recycling off, so `pool.pull("Sprite")`
116
+ was a plain construction behind a string key. The ones worth recycling have
117
+ typed pools instead, reached by key and fully typed:
118
+
119
+ ```js
120
+ const tween = getPool("tween").get(target);
121
+ const label = getPool("text").get(x, y, { font: "Arial", size: 12 });
122
+ getPool("text").release(label); // yours to release, as with particles
123
+ ```
105
124
 
106
- `pool.register` additionally aliases every name under an `me.` prefix, pointing
107
- at the same entry, so `pool.pull("me.Tween")` resolves identically — and the
108
- same alias is registered with the Tiled object factory, which is why a map
125
+ A recycled label comes back with its **base-class** state reset as well as its
126
+ own: `floating`, `blendMode`, `alpha`, `tint`, the transform, the anchor, the
127
+ behaviour flags, any mask and any post effect, and `depth` back to 0. The
128
+ settings object names none of those, so set them again after `get()`. A pooled
129
+ HUD number that is not re-marked `floating` is drawn through the camera, i.e.
130
+ somewhere out in the world rather than on the screen, and `addChild(label, z)`
131
+ is what gives it its depth back.
132
+
133
+ `getPool` covers `vector2d`, `vector3d`, `point`, `matrix2d`, `matrix3d`,
134
+ `bounds`, `color`, `polygon`, `line`, `rectangle`, `roundedRectangle`,
135
+ `ellipse`, `tween`, `particle`, `text`, `colorLayer` and `bitmapTextData`.
136
+
137
+ `pool.register` still aliases every name you give it under an `me.` prefix,
138
+ pointing at the same entry, so `pool.pull("me.Bullet")` resolves identically —
139
+ and the same alias reaches the Tiled object factory, which is why a map
109
140
  authored against melonJS 1.x still finds its classes. Prefer the unprefixed
110
141
  name in new code; do not "correct" an `me.`-prefixed one, it is not broken.
111
142
 
@@ -500,7 +500,8 @@ onCollisionStart(response, other) {
500
500
  What actually happens per adapter:
501
501
 
502
502
  - **built-in** — callbacks fire inline during `step()`. `Container.removeChild`
503
- is deferred, so it is safe; `removeChildNow()` and `destroy()` are immediate,
503
+ is deferred, so it is safe; `removeChildNow()`, and the internal `destroy()`
504
+ it calls, are immediate,
504
505
  and the detector only survives them because it re-checks `body === undefined`
505
506
  after every user callback.
506
507
  - **planck** — `onCollisionStart` / `onCollisionEnd` are dispatched from
@@ -184,6 +184,11 @@ v.set(10, 20, v.z); // keep it
184
184
  `pos` is an `ObservableVector3d`, whose `set(x = 0, y = 0, z = 0)` does the same
185
185
  — so `this.pos.set(x, y)` wipes the object's depth.
186
186
 
187
+ `Renderable#setPosition(x, y, z?)` is the renderable-level form and does not:
188
+ omit `z` and the depth is left exactly as it was. Reach for it whenever you
189
+ are moving an object rather than editing a vector, and keep `v.set(x, y, v.z)`
190
+ for the vector itself.
191
+
187
192
  ## 7. A settings key the class never reads is silently ignored
188
193
 
189
194
  A settings object is a plain literal: nothing rejects a key, so a name that
@@ -279,7 +284,7 @@ Hand-rolled equivalents miss the batching and the multi-backend support.
279
284
  | off-screen object never ticks | `update()` is gated on `inViewport` — set `alwaysUpdate = true` |
280
285
  | custom draw offset by half the size | centred `anchorPoint` default not zeroed |
281
286
  | spatial query never finds an object | `isKinematic` left `true` — not in the broadphase |
282
- | `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 |
287
+ | `z` unexpectedly 0 after a `set` | `Vector3d.set(x, y)` defaults `z` to 0 — `setPosition(x, y)` on the renderable leaves the depth alone |
283
288
  | a settings option appears to do nothing | the class never reads that key — see section 7 |
284
289
  | `isRenderable = false` did not hide the object | it gates bounds, not drawing — use `alpha` |
285
290
 
@@ -194,6 +194,17 @@ timer.clearInterval(id); // in the Stage's onDestroyEvent
194
194
  `onActivateEvent` / `onDeactivateEvent` on a stage. Those two are `Renderable`
195
195
  hooks; put per-renderable teardown there instead.
196
196
 
197
+ **The outgoing stage is destroyed before the incoming one is reset.** That
198
+ ordering is what makes "each stage owns what it started" work: a scene can
199
+ release a resource in `onDestroyEvent` and the next scene can claim the same
200
+ one in `onResetEvent` without the teardown arriving late and undoing it.
201
+
202
+ What a stage starts, it must end: `input.registerPointerEvent` regions,
203
+ `event.on` subscriptions and running `Tween`s all outlive the stage that made
204
+ them. Music is the exception rather than the rule, since `audio.playTrack()`
205
+ stops the track it replaces, so `stopTrack()` in `onDestroyEvent` is only
206
+ needed where the next screen has no music of its own.
207
+
197
208
  Engine timers are **pause-aware**. `window.setTimeout` keeps firing while the
198
209
  game is paused, which produces enemies spawning behind a pause menu. Pass
199
210
  `false` as the third argument — `timer.setInterval(fn, 500, false)` — for one
@@ -257,9 +268,9 @@ instances.** All four, or it is not a tween.
257
268
 
258
269
 
259
270
  ```js
260
- import { Tween, pool } from "melonjs";
271
+ import { getPool, Tween } from "melonjs";
261
272
 
262
- const t = pool.pull("Tween", sprite.pos) // registered name is "Tween"
273
+ const t = getPool("tween").get(sprite.pos) // typed pool, release when done
263
274
  .to({ x: 300 }, { duration: 500 }) // options object, not a number
264
275
  .easing(Tween.Easing.Quadratic.Out)
265
276
  .onComplete(() => { /* … */ })
@@ -289,7 +300,7 @@ callback is invoked with the tweened object as `this` and the eased progress
289
300
 
290
301
  ```js
291
302
  const fade = { level: 1 };
292
- pool.pull("Tween", fade)
303
+ getPool("tween").get(fade)
293
304
  .to({ level: 0 }, { duration: 400 })
294
305
  .easing(Tween.Easing.Quadratic.Out)
295
306
  .onUpdate(function () {
@@ -103,7 +103,7 @@ Packing into one atlas keeps everything on a single binding.
103
103
  ## Visual properties
104
104
 
105
105
  ```js
106
- sprite.tint.setColor(255, 128, 0); // multiply tint
106
+ sprite.setTint("#ff8000"); // multiply tint
107
107
  sprite.alpha = 0.5;
108
108
  sprite.flipX(true); // mirror horizontally
109
109
  sprite.flipY(true);
@@ -141,9 +141,14 @@ The insets default to a quarter of the frame.
141
141
  Frequently spawned sprites should be pooled:
142
142
 
143
143
  ```js
144
- pool.register("bullet", Bullet, true); // true = recyclable
145
- const b = pool.pull("bullet", x, y); // new Bullet(x, y), or onResetEvent(x, y) on a reused one
146
- world.removeChild(b); // returns it to the pool automatically
144
+ import { createPool } from "melonjs";
145
+
146
+ const bulletPool = createPool((x, y) => { // me.pool is deprecated
147
+ const instance = new Bullet(x, y);
148
+ return { instance, reset: (x, y) => instance.onResetEvent(x, y) };
149
+ });
150
+ const b = bulletPool.get(x, y); // new Bullet(x, y), or reset(x, y) on a reused one
151
+ world.removeChild(b); // returns it to bulletPool automatically
147
152
  ```
148
153
 
149
154
  Two consequences: