melonjs 20.12.0 → 20.14.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 (185) hide show
  1. package/README.md +66 -22
  2. package/build/application/application.d.ts +6 -1
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/resize.d.ts.map +1 -1
  6. package/build/application/settings.d.ts +13 -8
  7. package/build/application/settings.d.ts.map +1 -1
  8. package/build/audio/audio.d.ts +8 -4
  9. package/build/audio/audio.d.ts.map +1 -1
  10. package/build/audio/backend/engine.d.ts.map +1 -1
  11. package/build/audio/playback.d.ts +2 -2
  12. package/build/audio/playback.d.ts.map +1 -1
  13. package/build/audio/spatial.d.ts.map +1 -1
  14. package/build/audio/types.d.ts +15 -5
  15. package/build/audio/types.d.ts.map +1 -1
  16. package/build/camera/camera2d.d.ts +12 -9
  17. package/build/camera/camera2d.d.ts.map +1 -1
  18. package/build/camera/camera3d.d.ts +11 -8
  19. package/build/camera/camera3d.d.ts.map +1 -1
  20. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  21. package/build/geometries/ellipse.d.ts +3 -0
  22. package/build/geometries/ellipse.d.ts.map +1 -1
  23. package/build/geometries/line.d.ts.map +1 -1
  24. package/build/geometries/path2d.d.ts +9 -8
  25. package/build/geometries/path2d.d.ts.map +1 -1
  26. package/build/geometries/polygon.d.ts +2 -1
  27. package/build/geometries/polygon.d.ts.map +1 -1
  28. package/build/index.d.ts +14 -3
  29. package/build/index.d.ts.map +1 -1
  30. package/build/index.js +50215 -45051
  31. package/build/index.js.map +4 -4
  32. package/build/input/gamepad.d.ts.map +1 -1
  33. package/build/input/keyboard.d.ts.map +1 -1
  34. package/build/input/pointerevent.d.ts.map +1 -1
  35. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  36. package/build/level/level.d.ts.map +1 -1
  37. package/build/level/tiled/TMXLayer.d.ts +50 -15
  38. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  39. package/build/level/tiled/TMXTileMap.d.ts +14 -4
  40. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  41. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  42. package/build/level/tiled/TMXUtils.d.ts.map +1 -1
  43. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  44. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  45. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
  46. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  47. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  48. package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
  49. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  50. package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
  51. package/build/lighting/light3d.d.ts +55 -0
  52. package/build/lighting/light3d.d.ts.map +1 -1
  53. package/build/loader/cache.d.ts +146 -97
  54. package/build/loader/cache.d.ts.map +1 -1
  55. package/build/loader/internal-types.d.ts +69 -0
  56. package/build/loader/internal-types.d.ts.map +1 -0
  57. package/build/loader/loader.d.ts +589 -253
  58. package/build/loader/loader.d.ts.map +1 -1
  59. package/build/loader/parsers/audio.d.ts.map +1 -1
  60. package/build/loader/parsers/fontface.d.ts.map +1 -1
  61. package/build/loader/parsers/gltf.d.ts.map +1 -1
  62. package/build/loader/parsers/tmx.d.ts.map +1 -1
  63. package/build/math/color.d.ts +4 -0
  64. package/build/math/color.d.ts.map +1 -1
  65. package/build/math/math.d.ts +6 -1
  66. package/build/math/math.d.ts.map +1 -1
  67. package/build/math/noise.d.ts +16 -16
  68. package/build/math/noise.d.ts.map +1 -1
  69. package/build/math/observableVector2d.d.ts +6 -6
  70. package/build/math/observableVector2d.d.ts.map +1 -1
  71. package/build/math/observableVector3d.d.ts +8 -8
  72. package/build/math/observableVector3d.d.ts.map +1 -1
  73. package/build/particles/emitter.d.ts +15 -3
  74. package/build/particles/emitter.d.ts.map +1 -1
  75. package/build/particles/particle.d.ts +6 -0
  76. package/build/particles/particle.d.ts.map +1 -1
  77. package/build/particles/settings.d.ts +22 -5
  78. package/build/particles/settings.d.ts.map +1 -1
  79. package/build/physics/adapter.d.ts +64 -25
  80. package/build/physics/adapter.d.ts.map +1 -1
  81. package/build/physics/builtin/body.d.ts +23 -4
  82. package/build/physics/builtin/body.d.ts.map +1 -1
  83. package/build/physics/builtin/builtin-adapter.d.ts +25 -3
  84. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  85. package/build/physics/builtin/detector.d.ts.map +1 -1
  86. package/build/physics/builtin/raycast.d.ts.map +1 -1
  87. package/build/physics/world.d.ts +40 -10
  88. package/build/physics/world.d.ts.map +1 -1
  89. package/build/renderable/container.d.ts +41 -6
  90. package/build/renderable/container.d.ts.map +1 -1
  91. package/build/renderable/draggable.d.ts.map +1 -1
  92. package/build/renderable/frameAnimation.d.ts.map +1 -1
  93. package/build/renderable/instanced_mesh.d.ts +16 -14
  94. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  95. package/build/renderable/mesh.d.ts +62 -5
  96. package/build/renderable/mesh.d.ts.map +1 -1
  97. package/build/renderable/renderable.d.ts.map +1 -1
  98. package/build/renderable/sprite.d.ts +4 -2
  99. package/build/renderable/sprite.d.ts.map +1 -1
  100. package/build/renderable/text/bitmaptext.d.ts +12 -3
  101. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  102. package/build/renderable/text/text.d.ts +147 -4
  103. package/build/renderable/text/text.d.ts.map +1 -1
  104. package/build/renderable/text/textmetrics.d.ts +10 -2
  105. package/build/renderable/text/textmetrics.d.ts.map +1 -1
  106. package/build/renderable/text/textstyle.d.ts.map +1 -1
  107. package/build/renderable/trigger.d.ts +5 -0
  108. package/build/renderable/trigger.d.ts.map +1 -1
  109. package/build/renderable/ui/panelpaint.d.ts +5 -1
  110. package/build/renderable/ui/panelpaint.d.ts.map +1 -1
  111. package/build/renderable/ui/progressbar.d.ts +267 -95
  112. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  113. package/build/renderable/ui/uibaseelement.d.ts +443 -50
  114. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  115. package/build/renderable/ui/uicheckbox.d.ts +383 -0
  116. package/build/renderable/ui/uicheckbox.d.ts.map +1 -0
  117. package/build/renderable/ui/uifocus.d.ts +6 -0
  118. package/build/renderable/ui/uifocus.d.ts.map +1 -0
  119. package/build/renderable/ui/uilabel.d.ts +31 -0
  120. package/build/renderable/ui/uilabel.d.ts.map +1 -0
  121. package/build/renderable/ui/uipanel.d.ts +589 -116
  122. package/build/renderable/ui/uipanel.d.ts.map +1 -1
  123. package/build/renderable/ui/uiskin.d.ts +25 -0
  124. package/build/renderable/ui/uiskin.d.ts.map +1 -0
  125. package/build/renderable/ui/uislider.d.ts +518 -0
  126. package/build/renderable/ui/uislider.d.ts.map +1 -0
  127. package/build/renderable/ui/uispriteelement.d.ts +161 -85
  128. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  129. package/build/renderable/ui/uitextbutton.d.ts +369 -63
  130. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  131. package/build/state/stage.d.ts.map +1 -1
  132. package/build/system/device.d.ts +11 -2
  133. package/build/system/device.d.ts.map +1 -1
  134. package/build/system/fullscreen.d.ts.map +1 -1
  135. package/build/system/pool.d.ts.map +1 -1
  136. package/build/system/save.d.ts +2 -1
  137. package/build/system/save.d.ts.map +1 -1
  138. package/build/tweens/tween.d.ts +5 -2
  139. package/build/tweens/tween.d.ts.map +1 -1
  140. package/build/utils/fetchdata.d.ts.map +1 -1
  141. package/build/utils/utils.d.ts.map +1 -1
  142. package/build/video/canvas/canvas_renderer.d.ts +16 -19
  143. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  144. package/build/video/canvas_factory.d.ts.map +1 -1
  145. package/build/video/effects/shadereffect.d.ts +127 -0
  146. package/build/video/effects/shadereffect.d.ts.map +1 -1
  147. package/build/video/renderer.d.ts +20 -4
  148. package/build/video/renderer.d.ts.map +1 -1
  149. package/build/video/renderstate.d.ts +26 -3
  150. package/build/video/renderstate.d.ts.map +1 -1
  151. package/build/video/utils/tessellation.d.ts +11 -0
  152. package/build/video/utils/tessellation.d.ts.map +1 -1
  153. package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
  154. package/build/video/webgl/webgl_renderer.d.ts +3 -6
  155. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  156. package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
  157. package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
  158. package/build/video/webgpu/webgpu_renderer.d.ts +3 -6
  159. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  160. package/package.json +1 -1
  161. package/skills/melonjs/SKILL.md +41 -17
  162. package/skills/melonjs-20-migration/SKILL.md +50 -7
  163. package/skills/melonjs-3d/SKILL.md +138 -38
  164. package/skills/melonjs-3d-assets/SKILL.md +83 -33
  165. package/skills/melonjs-audio/SKILL.md +62 -19
  166. package/skills/melonjs-camera-and-drawing/SKILL.md +135 -8
  167. package/skills/melonjs-deployment/SKILL.md +92 -14
  168. package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
  169. package/skills/melonjs-effects-and-shaders/SKILL.md +47 -15
  170. package/skills/melonjs-events/SKILL.md +6 -3
  171. package/skills/melonjs-getting-started/SKILL.md +6 -2
  172. package/skills/melonjs-input/SKILL.md +102 -16
  173. package/skills/melonjs-lighting/SKILL.md +13 -1
  174. package/skills/melonjs-loading-assets/SKILL.md +255 -39
  175. package/skills/melonjs-math/SKILL.md +26 -8
  176. package/skills/melonjs-particles-and-trails/SKILL.md +77 -9
  177. package/skills/melonjs-performance/SKILL.md +18 -8
  178. package/skills/melonjs-physics/SKILL.md +435 -128
  179. package/skills/melonjs-plugins/SKILL.md +50 -11
  180. package/skills/melonjs-renderables/SKILL.md +31 -7
  181. package/skills/melonjs-renderer-backends/SKILL.md +38 -17
  182. package/skills/melonjs-scenes-and-state/SKILL.md +45 -5
  183. package/skills/melonjs-sprites-and-animation/SKILL.md +37 -2
  184. package/skills/melonjs-tilemaps/SKILL.md +149 -17
  185. package/skills/melonjs-ui-and-text/SKILL.md +674 -42
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-3d-assets
3
- description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options including the async flag, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, skinning, Blender export, 3D asset."
3
+ description: "Use this skill when loading 3D models into melonJS — glTF and GLB scenes, OBJ/MTL models, materials, imported lights, node animation, ground shadows and GPU instancing. Covers level.load options including the async flag, the rightHanded conversion, and exactly what the loader does and does not support. Triggers on: glTF, gltf, glb, OBJ, MTL, 3D model, getGLTF, getOBJ, getMTL, GLTFModel, GLTFScene, level.load glb, rightHanded, lightIntensityScale, castGroundShadow, shadowGroundY, EXT_mesh_gpu_instancing, KHR_lights_punctual, alphaMode, doubleSided, map_d, map_bump, normalize, animationspeed, skinning, Blender export, 3D asset."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -36,16 +36,8 @@ or it renders flat. See `melonjs-3d` for the camera.
36
36
  | `onLoaded` | `game.onLevelLoaded` | called with the **level id**, not the scene |
37
37
  | `async` | `false` | return a promise that settles once the scene is in the world, instead of a boolean |
38
38
  | `rightHanded` | `true` | see below |
39
- | `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` world children |
39
+ | `lights` | `true` | instantiate authored `KHR_lights_punctual` lights as `Light3d` children of `container`; `false` also builds the meshes unlit |
40
40
  | `lightIntensityScale` | — | keep authored intensity ratios instead of normalising every light to 1 |
41
-
42
- Those `game.` defaults are the engine's, not a recommendation. `game` is
43
- whichever `Application` finished `init()` last, so **pass `container`
44
- explicitly** from a `Stage`, where the right one is in your hand:
45
-
46
- ```js
47
- level.load("sceneId", { container: this.app.world, scale: 40 });
48
- ```
49
41
  | `castGroundShadow` | inherits the app setting (**on**) | blob shadows for this scene's meshes |
50
42
  | `shadowGroundY` | each object's own base | world Y of the floor the blobs land on |
51
43
  | `shadowOpacity` | `0.45` | how dark the blobs are |
@@ -56,6 +48,16 @@ level.load("sceneId", { container: this.app.world, scale: 40 });
56
48
  | `shadowGroundNormal` | world up | up normal of the floor, for a scene that is not level |
57
49
  | `shadowLight` | — | a `Light3d` to take the direction from, re-read every draw |
58
50
 
51
+ Those `game.` defaults are the engine's, not a recommendation. `game` is
52
+ whichever `Application` finished `init()` last, so **pass `container`
53
+ explicitly** from a `Stage`, where the right one is in your hand:
54
+
55
+ ```js
56
+ onResetEvent(app) {
57
+ level.load("sceneId", { container: app.world, scale: 40 });
58
+ }
59
+ ```
60
+
59
61
  `shadowGroundPoint` is **not** on that list, and a scene load drops it without
60
62
  a warning. One floor POINT shared by every part of a scene would stack the
61
63
  whole thing's blobs on a single spot, which is nonsense where one floor
@@ -102,9 +104,10 @@ world.addChild(new Trigger(x, y, {
102
104
 
103
105
  Every `level.load` option above travels, including the whole ground-shadow set,
104
106
  and so do `container`, `onLoaded`, `flatten` and `setViewportBounds`. The list is
105
- **derived** from `level.loadOptions` rather than restated, so an option added to
106
- a level format arrives here for free. It used to be hand-written and had fallen
107
- three releases behind the glTF path (#1649).
107
+ **derived** from the options each level format declares beside the code that
108
+ reads them, rather than restated, so an option added to a level format arrives
109
+ here for free. It used to be hand-written and had fallen three releases behind
110
+ the glTF path (#1649).
108
111
 
109
112
  **`async` is deliberately excluded.** A trigger with a `color` + `duration`
110
113
  sets it itself, because it needs the promise to sequence its fade and reveal; a
@@ -132,24 +135,34 @@ Do not add a flip of your own on top; two corrections cancel back to wrong.
132
135
  The importer covers a deliberate subset:
133
136
 
134
137
  - `pbrMetallicRoughness` **base colour texture and factor**
138
+ - `COLOR_0` **vertex colours**, multiplied with the factor and the texture
135
139
  - metallic/roughness **factors** converted to the engine's specular model
136
140
  - `emissiveFactor`, including `KHR_materials_emissive_strength`
137
141
  - `KHR_materials_unlit` — honoured, for baked/stylised workflows
138
142
  - `alphaMode: "MASK"` — the cutout threshold is applied
139
143
  - `doubleSided`
144
+ - the base colour texture's sampler: `wrapS` / `wrapT` (repeat by default, as
145
+ the spec says) and `magFilter`
146
+
147
+ `alphaMode: "BLEND"` is read as opaque: the loader does not set `transparent`,
148
+ because one loaded mesh can merge materials and the flag routes the whole mesh.
149
+ Set `transparent: true` on the part yourself (see `melonjs-3d`).
140
150
 
141
151
  Not imported: **normal maps**, metallic/roughness *textures*, occlusion maps,
142
152
  morph targets, `KHR_texture_transform`. Nothing errors — the material simply
143
153
  arrives without them, which reads as "flatter than the preview in my authoring
144
- tool". Bake what you need into the base colour.
154
+ tool". Bake what you need into the base colour. (OBJ/MTL does carry a normal
155
+ map, see below.)
145
156
 
146
157
  **Draco compression is the exception that does error.** Its geometry sits in
147
158
  accessors with no buffer view, which the parser rejects outright, so the whole
148
159
  asset fails to load. Export uncompressed.
149
160
 
150
- `textureFilter` on the Application settings (`"nearest"` / `"linear"` /
151
- `"auto"`) drives sampling; glTF materials that declare their own sampler carry
152
- it through. Pixel-art models want `"nearest"`.
161
+ `textureFilter` on the Application settings (`"auto"` / `"nearest"` /
162
+ `"linear"` / `"linear-mipmap"`) drives sampling; glTF materials that declare
163
+ their own sampler carry it through. Pixel-art models want `"nearest"`. A mesh
164
+ already samples mipmaps under `"linear"`, so `"linear-mipmap"` changes nothing
165
+ for one.
153
166
 
154
167
  ### `baseColorFactor` is LINEAR; a `tint` is sRGB
155
168
  **Pass the factor as a setting and the encoding is done for you.** `Mesh`
@@ -222,8 +235,10 @@ screen, a HUD tint or a glow, are already sRGB: `* 255` is right for those.
222
235
  ## Lights
223
236
 
224
237
  Authored `KHR_lights_punctual` lights — sun, point, spot — become `Light3d`
225
- world children, plus a soft ambient fill, and each keeps its authored name.
226
- A scene carrying any of them renders through the **lit** path automatically.
238
+ children of the container the scene loads into, plus a soft white ambient fill
239
+ at intensity 0.3, and each keeps its authored name. A scene carrying any of them
240
+ renders through the **lit** path automatically (except `KHR_materials_unlit`
241
+ materials); one carrying none renders unlit.
227
242
 
228
243
  glTF stores physical units — lux for suns, candela for lamps — and a Blender
229
244
  daylight sun is 1000+, which blows out the engine's stylised shading. So every
@@ -235,7 +250,10 @@ set, scale instead of normalising:
235
250
  level.load("diorama", { scale: 50, lightIntensityScale: 0.001 });
236
251
  ```
237
252
 
238
- Pass `lights: false` to manage lighting yourself.
253
+ Pass `lights: false` to leave the authored lights out. The meshes are then built
254
+ **unlit**, so a `Light3d` of your own does not shade them either. To light a
255
+ scene your own way, keep the authored lights and retune them by name, or remove
256
+ them once the load has finished and add yours: the meshes stay lit.
239
257
 
240
258
  ## Animation
241
259
 
@@ -258,13 +276,18 @@ model.pause();
258
276
 
259
277
  `speed` is a playback multiplier (1 = authored speed), not a frame delay. Only
260
278
  an explicit `loop: false` stops looping. `setCurrentAnimation` **throws** on a
261
- clip name the asset does not define — check `getAnimationNames()` first.
279
+ clip name the asset does not define — check `getAnimationNames()` first. Called
280
+ with the clip that is already playing it returns at once and ignores its
281
+ options, so retime a running clip through `model.animationspeed`, not a second
282
+ `setCurrentAnimation(name, { speed })`.
262
283
 
263
284
  ### Placing and moving a model
264
285
 
265
286
  A `GLTFModel` is placed like any other renderable — `pos`, `depth`, `rotate`
266
287
  and `scale` move the **whole rig**, and compose with whatever the active clip
267
- is doing, so a walk cycle plays wherever the character stands:
288
+ is doing, so a walk cycle plays wherever the character stands. They apply just
289
+ the same while the clip is paused, finished or was never played, and `scale()`
290
+ multiplies the `scale` option the model was built with:
268
291
 
269
292
  ```js
270
293
  const boat = new me.GLTFModel(me.loader.getGLTF("boat"), { scale: 40, lit: false });
@@ -295,6 +318,11 @@ hundred thousand. Export with instancing enabled where your tool supports it.
295
318
  Sparse accessors on the instancing attributes are rejected with a clear error
296
319
  rather than silently misplacing every instance.
297
320
 
321
+ Face culling is decided once per mesh, so instances with a **negative scale**
322
+ (mirrored linked duplicates) render inside-out. Building the scene or model
323
+ warns when it meets them; mark the material `doubleSided`, which turns culling
324
+ off for the set.
325
+
298
326
  ## Ground shadows
299
327
 
300
328
  ```js
@@ -353,7 +381,7 @@ world.addChild(mesh);
353
381
  ```
354
382
 
355
383
  **The MTL loader fetches the textures it references itself** — `map_Kd`,
356
- `map_d` and `map_bump` are resolved relative to the `.mtl` and preloaded for
384
+ `map_d` and the normal map are resolved relative to the `.mtl` and preloaded for
357
385
  you, so there is no image entry in the manifest above. Passing `texture:`
358
386
  explicitly overrides that and pins *one* binding over the whole model, which on
359
387
  a multi-material model suppresses the per-material texture split; leave it out
@@ -361,19 +389,23 @@ unless you mean that.
361
389
 
362
390
  MTL contributes `Kd` (diffuse tint), `d` / `Tr` (opacity), `Ke` (emissive),
363
391
  `Ks` + `Ns` (specular highlight), `map_Kd` (diffuse texture), `map_d`
364
- (per-texel opacity) and `map_bump` / `bump` / `norm` (tangent-space normal
365
- map — needs `lit: true`, and the tangent frame is derived per fragment, so the
366
- model needs no tangent attribute). All three normal-map spellings are read as
367
- normal maps even though the format specifies two of them as height maps, which
368
- is what every exporter actually writes. Specular *maps* (`map_Ks`) and the PBR
369
- extension's `map_Pr` / `map_Pm` are not supported and warn — the `Pr` / `Pm`
370
- *scalars* are read and approximated onto the specular terms, the maps would
371
- need a real PBR model.
392
+ (per-texel opacity) and `map_bump` / `map_Bump` / `bump` / `norm`
393
+ (tangent-space normal map — needs `lit: true`, and the tangent frame is derived
394
+ per fragment, so the model needs no tangent attribute). Every normal-map
395
+ spelling is read as a normal map even though the format specifies `bump` and
396
+ `map_bump` as height maps, because a normal map is what exporters actually
397
+ write there. Specular *maps* (`map_Ks`) and the PBR extension's `map_Pr` /
398
+ `map_Pm` are not supported and warn — the `Pr` / `Pm` *scalars* are read and
399
+ approximated onto the specular terms, the maps would need a real PBR model.
400
+
401
+ `map_d` cuts out only against a non-zero `alphaCutoff`, and the loader sets
402
+ none: pass one in the `Mesh` settings (`alphaCutoff: 0.5`), or nothing is cut
403
+ out.
372
404
 
373
405
  `Ka` (ambient) and `illum` (illumination model) are read onto the material and
374
406
  readable from `loader.getMTL(name)`, but take no part in shading. That is
375
407
  deliberate rather than pending: `Ka` predates scene-wide ambient light and would
376
- fight `Stage.ambientLightingColor`, and `illum` enumerates fixed-function
408
+ fight an `"ambient"` `Light3d`, and `illum` enumerates fixed-function
377
409
  behaviours this renderer has no equivalent for. In particular an `illum` of 0 or
378
410
  1 nominally means "no specular", and the engine does **not** honour that — `Ns`
379
411
  stays the only gate, so a model that renders correctly today is not restyled by
@@ -469,11 +501,19 @@ const node = me.loader.getGLTF("palm").nodes[0];
469
501
  const trees = new me.InstancedMesh(0, groundY, {
470
502
  vertices: node.vertices, uvs: node.uvs,
471
503
  normals: node.normals, indices: node.indices,
504
+ normalize: false, // keep the authored units; see below
472
505
  texture: palette, textureFilter: "nearest", scale: 40,
473
506
  instanceCount: count,
474
507
  });
475
508
  ```
476
509
 
510
+ **Pass `normalize: false` whenever the geometry comes off a descriptor.** The
511
+ default fits the geometry into a unit cube, centred, which throws away the
512
+ authored size and the base-on-the-floor origin. Before 20.14 it was worse: the
513
+ default normalized the loader's own array in place, so every later mesh built
514
+ from that node, and a `level.load` of the same asset, got the shrunken
515
+ geometry. A normalized mesh works on a copy now.
516
+
477
517
  `nodes` is one entry **per primitive**, each in its own local space with a
478
518
  separate `world` matrix — so this only works cleanly when the asset is a single
479
519
  merged primitive exported with an identity node transform. Model it at the
@@ -515,8 +555,16 @@ need a prefix.
515
555
  | the asset fails to load entirely | an external buffer/image `uri` that 404s, or Draco-compressed geometry |
516
556
  | everything is blown out white | authored lux/candela intensities; use `lightIntensityScale` |
517
557
  | materials flatter than the authoring-tool preview | no normal or metallic/roughness *textures* are imported |
558
+ | a glTF `alphaMode: "BLEND"` material draws opaque | the loader does not set `transparent`; set it on that part yourself |
559
+ | an MTL `map_d` cuts nothing out | it needs a non-zero `alphaCutoff`, which the loader does not set |
560
+ | a mesh built from `getGLTF(name).nodes` comes out unit-sized | `normalize` defaults to `true`; pass `normalize: false` to keep the authored units |
561
+ | after one normalized mesh of a model, the scene or other meshes of that model load shrunken (before 20.14) | the default normalized the loader's cached array in place; update, or pass `normalize: false` |
562
+ | some copies in an imported scatter render inside-out | negative-scale (mirrored) instances under per-mesh face culling; make the material `doubleSided` |
518
563
  | a character does not deform | vertex skinning is out of scope; rig hierarchically or billboard |
519
564
  | animation names come back empty | the asset has no node-TRS channels (skin-only rig) |
565
+ | `setCurrentAnimation(name, { speed })` changes nothing | that clip is already playing, so the call returns at once; set `animationspeed` |
566
+ | `model.scale(2)` throws "not a function" on a `GLTFModel` (before 20.14) | the `scale` option hid the method: update |
567
+ | a `GLTFModel` ignores `pos` / `rotate` once its clip is paused or finished (before 20.14) | it was only re-posed while a clip played: update |
520
568
  | a hundred copies tank the frame rate | exported without `EXT_mesh_gpu_instancing` |
521
569
  | a whole scatter looks like one object repeated | every instance shares the group transform — vary yaw/scale per instance, rotating *after* the translate |
522
570
  | three clips where you authored one | one action per object; use a single action with a slot each |
@@ -530,12 +578,14 @@ need a prefix.
530
578
  | a shadow smeared across the whole floor | a ground plane cast its own blob — use the scene-wide opt-in |
531
579
  | `onLoaded` gets a string, not the scene | it is called with the level id; load into your own container instead |
532
580
  | scene renders flat and unlit | no `Camera3d` — the 2D-camera path is CPU-projected and unlit (with a `Camera3d` on Canvas you get a black canvas instead) |
581
+ | scene renders in perspective but unlit | the asset carries no sun, point or spot light (or was loaded with `lights: false`), so its meshes are built unlit; author one in the asset and tune it at runtime by name |
533
582
 
534
583
  ## Related skills
535
584
 
536
585
  - `melonjs-3d` — conventions, `Camera3d`, meshes, `Light3d`, instancing, and
537
586
  `camera.setFog` (an outdoor scene almost always wants it; it is set on the
538
587
  camera, so a loaded scene needs no per-node work)
539
- - `melonjs-lighting` — how `Light3d` behaves once imported
588
+ - `melonjs-lighting` — the 2D `Light2d` system; imported lights are `Light3d`,
589
+ covered in `melonjs-3d`
540
590
  - `melonjs-loading-assets` — the loader, asset types and base URLs
541
591
  - `melonjs-plugins` — the Spine plugin, for skeletal characters
@@ -19,10 +19,13 @@ await loader.preload([
19
19
  ```
20
20
 
21
21
  1. **`audio.init(format)` must run before any audio asset is preloaded**, and
22
- lists the formats you actually ship. Preloading first throws
23
- `target audio extension(s) should be set through me.audio.init() before
24
- calling the preloader.` It returns `false` when no audio backend is
25
- available, so it is worth checking. The format list defaults to `"mp3"`.
22
+ lists the formats you actually ship. Preloading first fails the preload:
23
+ `await loader.preload(...)` rejects with the loader's `Failed loading
24
+ resource <src>`, which hides the real reason, `target audio extension(s)
25
+ should be set through me.audio.init() before calling the preloader.`
26
+ (thrown as it is by `audio.load()` called directly). It returns `false`
27
+ when no audio backend is available, so it is worth checking. The format
28
+ list defaults to `"mp3"`.
26
29
  2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus
27
30
  each configured extension and uses the first that loads — so
28
31
  `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename
@@ -50,13 +53,19 @@ audio.playTrack("theme"); // background music
50
53
  audio.stopTrack();
51
54
  ```
52
55
 
53
- Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always
56
+ Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` (or
57
+ `playTrack(name, { volume, sprite })`, to loop one region of the clip) always
54
58
  loops, and it records the name as the current track — which is what
55
59
  `app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via
56
60
  `state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,
57
61
  `resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there
58
62
  is only ever one current *track name*.
59
63
 
64
+ `playTrack` on a track that is paused restarts it. `pauseTrack` and
65
+ `resumeTrack` act on the track alone, so effects played from the same file (an
66
+ audio sprite holding both) keep going, and `resumeTrack` on a track that is
67
+ playing does nothing.
68
+
60
69
  **`playTrack` stops the track it replaces** (20.9.0 and later; before that it
61
70
  only assigned the slot, and the previous bed kept looping underneath where
62
71
  `stopTrack()` could no longer reach it). So a scene just states its own music
@@ -155,6 +164,11 @@ audio.unmuteAll(); // (audio.enable() is an alias)
155
164
  audio.muted(); // global mute state
156
165
  ```
157
166
 
167
+ `setVolume` is a master level over everything; the `volume` given to a `play`
168
+ or `playTrack` is that sound's own level, 1 when left out, and what is heard is
169
+ the product of the two. So a music slider fades the track's own level
170
+ (`audio.fade(name, v, v, 1)`) and a master slider calls `setVolume`.
171
+
158
172
  `audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`
159
173
  also clears the current-track pointer if it named that clip.
160
174
 
@@ -163,8 +177,10 @@ also clears the current-track pointer if it named that clip.
163
177
  When the window loses focus the engine mutes all audio, and unmutes it on
164
178
  focus. That covers sound effects, procedural `tone` / `noise` and the mixer
165
179
  effects, not just the music track that `state.pause()` pauses. Set
166
- `pauseOnBlur: false` in the application settings if a clip has to keep playing
167
- in the background.
180
+ `app.pauseOnBlur = false` (with `stopOnBlur` left at its `false` default) if a
181
+ clip has to keep playing in the background. It is a property of the
182
+ application, not a constructor setting: `new Application(w, h, { pauseOnBlur:
183
+ false })` does nothing.
168
184
 
169
185
  ## Spatial audio
170
186
 
@@ -197,10 +213,15 @@ audio.play("portal", { at: { x: 1200, y: 480 } });
197
213
 
198
214
  `follow` and `at` are mutually exclusive: passing both throws, rather than
199
215
  quietly picking one. `follow` re-reads the target's **absolute** position each
200
- frame, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you
201
- want for a sound that belongs to an entity: without it a looping clip on a
202
- destroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`
203
- detaches a sound and leaves it playing where it is.
216
+ frame, so a renderable nested in a moving container is placed correctly. That
217
+ position is where its `anchorPoint` sits: the center of a `Renderable` or a
218
+ `Sprite`, but the **top-left corner** of an `Entity` or an object loaded from
219
+ Tiled, whose anchor is `(0, 0)`, so a large one (a boss) sounds from up and to
220
+ the left of where it looks to be. `stopWithTarget` is what you want for a sound
221
+ that belongs to an entity: without it a looping clip on a destroyed renderable
222
+ keeps playing from wherever it last was. It also counts a pooled renderable
223
+ going back to its pool, which can lend it out again before the next frame.
224
+ `audio.unfollow(id)` detaches a sound and leaves it playing where it is.
204
225
 
205
226
  ### Move the listener, don't do the subtraction yourself
206
227
 
@@ -221,7 +242,12 @@ player's position.
221
242
  `setListener` places the ear at the target's absolute position after the world
222
243
  updates. A plain renderable contributes position only, so a side-scroller keeps
223
244
  the default "facing into the screen" and gets left-right panning plus distance
224
- falloff from the one call.
245
+ falloff from the one call. A `Camera2d` (`audio.setListener(app.viewport)` in a
246
+ 2D game) is heard from the **center of its view**, its `worldView`, zoom and
247
+ offset included, not from its `pos`, the view's top-left corner (before 20.12
248
+ it was the corner, and everything panned right). Following stops on its own
249
+ when the target is destroyed, which a stage that owns its camera does on every
250
+ state change.
225
251
 
226
252
  A `Camera3d` target also contributes its **orientation**, taken from its own
227
253
  basis, so turning the camera swings the stereo image with it. That is the whole
@@ -380,6 +406,14 @@ a gate screen — a splash, a "press any key", or a health/photosensitivity noti
380
406
  unconditionally. Note the unlock listens for `keydown` too, so a keyboard-only
381
407
  player is covered.
382
408
 
409
+ Where there is no DOM (nx.js on the Nintendo Switch, see `melonjs-deployment`)
410
+ there is no page to gesture at and no autoplay policy: audio unlocks as soon as
411
+ the engine has its context, and music can start on load. nx.js's Web Audio has
412
+ buffer sources, gain and stereo panning, but not the `PannerNode`,
413
+ `OscillatorNode` and `BiquadFilterNode` behind 3D `position()` / `panner()` and
414
+ `tone()` / `noise()`, nor the reverb, echo and compressor nodes: those throw
415
+ "Method not implemented" there.
416
+
383
417
  ### The context can stop again, long after it was unlocked
384
418
 
385
419
  Unlocking is only the first half. A context that has been running can be
@@ -463,10 +497,12 @@ the level starts. The loader calls the same function, so the same rules apply.
463
497
  Re-preloading a manifest that already contains a loaded clip is a no-op —
464
498
  `audio.unload(name)` first if you genuinely want to reload it.
465
499
 
466
- **Credentials use the same global switch as every other asset type** —
467
- `loader.setOptions({ withCredentials: true })`, not an audio-specific option.
468
- Buffered audio fetches through the shared loader path, so it honours it exactly
469
- as an image or a JSON file does. A streamed clip (`stream: true`, or
500
+ **Credentials work as for every other asset type.** Buffered audio on the
501
+ game's own origin carries its cookies and HTTP auth with no option; a clip on
502
+ another origin behind a login needs `loader.setOptions({ withCredentials: true })`,
503
+ not an audio-specific option, and that server must allow credentials. Buffered
504
+ audio fetches through the shared loader path, so it behaves exactly as an image
505
+ or a JSON file does. A streamed clip (`stream: true`, or
470
506
  `preload: false`) does not: it plays
471
507
  through an `<audio>` element, which needs a `crossorigin` attribute rather than
472
508
  fetch credentials. Preload a clip buffered if it sits behind a login.
@@ -488,8 +524,9 @@ either way — blank screen, empty console.
488
524
  | symptom | cause |
489
525
  |---|---|
490
526
  | audio assets 404 | `src` given as a full filename instead of a directory prefix |
491
- | `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |
527
+ | `preload()` rejects with `Failed loading resource data/sfx/` (or `audio.load()` throws `target audio extension(s) should be set...`) | `audio.init()` not called before preloading |
492
528
  | `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
529
+ | "Method not implemented" from `position()`, `panner()`, `tone()`, `noise()` or an effect, on nx.js | nx.js's Web Audio lacks the node it needs (panner, oscillator, filter, convolver...): use `stereo()` and recorded sounds there |
493
530
  | nothing plays until the user clicks | browser autoplay policy — expected, design around it |
494
531
  | `playing()` is false right after `play()` | playback starts asynchronously |
495
532
  | music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |
@@ -498,18 +535,24 @@ either way — blank screen, empty console.
498
535
  | `stereo` / `position` do nothing | the clip was loaded with `stream: true` or `preload: false` — spatial is WebAudio-only |
499
536
  | a `preload: false` clip never plays, and `audio.state()` stays `"unloaded"` | melonJS before 20.12.0 never fetched such a clip; preload it, or update |
500
537
  | one format works, another does not | that codec is not in the `audio.init()` list |
501
- | audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |
538
+ | audio on another origin behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses. Audio on the game's own origin needs nothing (before 20.13 it needed it too) |
502
539
  | a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |
503
540
  | `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |
504
541
  | `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |
505
542
  | a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |
506
543
  | a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |
507
544
  | a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |
508
- | a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |
545
+ | with `audio.setListener(app.viewport)`, a sound in the middle of the screen is heard off to the right (before 20.12) | the ear was at the camera's top-left corner: update, it is at the center of the view |
546
+ | a followed `Entity` (or Tiled object) sounds offset up and to the left | `follow` uses the anchor point, which is the top-left corner under their `(0, 0)` anchor |
547
+ | a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true`; with it, before 20.14 a pooled entity going back to its pool did not count either |
548
+ | new sounds are quieter than the volume slider says, or the music stays silent after the volume went to 0 and back (before 20.14) | the global volume was applied twice: update |
549
+ | "Retry" from a pause menu restarts the level with no music (before 20.14) | `playTrack` on a paused track stopped it: update |
550
+ | a clip's load-time `on: { end }` stops firing after `audio.stop(name)` (before 20.14) | `stop(name)` removed it: update |
509
551
  | a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |
510
552
  | a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |
511
553
  | a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see "A loop with no gap") |
512
554
  | sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |
555
+ | the game still pauses and goes quiet on blur although `pauseOnBlur: false` was passed to `new Application()` | it is not a setting: assign `app.pauseOnBlur = false` |
513
556
 
514
557
  ## Related skills
515
558
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-camera-and-drawing
3
- description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, shake, fadeIn, fadeOut, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, Rect, Ellipse, Polygon, Line, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
3
+ description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, zoom, roll, shake, fadeIn, fadeOut, CameraEffect, addCameraEffect, MaskEffect, PixelateTransitionEffect, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, setGlobalAlpha, fillArc, Rect, Ellipse, Polygon, Line, Path2D, SVG path, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -109,6 +109,13 @@ The fade pair is named from the *colour's* point of view, not the scene's:
109
109
  (screen starts at `color` and clears). Both default to a 1000 ms duration and
110
110
  take an optional completion callback.
111
111
 
112
+ `shake()` while a shake is still running is ignored, unless its fifth
113
+ argument, `force`, is `true`, which replaces the running one:
114
+
115
+ ```js
116
+ app.viewport.shake(8, 300, app.viewport.AXIS.BOTH, undefined, true);
117
+ ```
118
+
112
119
  **`fadeIn` does not HOLD the colour.** The effect completes, the camera's own
113
120
  update destroys it, and the screen snaps back — which is why the engine's
114
121
  idiom does the work in the completion callback and then calls `fadeOut`. To
@@ -136,6 +143,48 @@ non-identity, and drops it again after the frame. It is **not** shader-free —
136
143
  it is a `ColorMatrixEffect`, i.e. a `ShaderEffect`, so like every post effect it
137
144
  is inert on the Canvas renderer.
138
145
 
146
+ ### The camera effects, and your own
147
+
148
+ Each is a `CameraEffect`, added with `camera.addCameraEffect(effect)` (which
149
+ returns it), found with `getCameraEffect(Class)` and taken off with
150
+ `removeCameraEffect(effect)`, which destroys it:
151
+
152
+ | effect | options | what it does |
153
+ |---|---|---|
154
+ | `ShakeEffect` | `intensity`, `duration`, `axis`, `onComplete` | jitters `offset`; what `shake()` adds. Several can run at once this way |
155
+ | `FadeEffect` | `color`, `duration` (1000), `direction: "in" \| "out"`, `onComplete` | what `fadeIn()` / `fadeOut()` add |
156
+ | `MaskEffect` | `shape`, `color`, `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | an iris or a wipe through a unit-sized `Ellipse` or `Polygon` centred on the origin |
157
+ | `PixelateTransitionEffect` | `renderer`, `size` (32), `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | the view breaks into blocks up to `size` screen pixels, or comes back from them. A shader: on Canvas it keeps its timing and draws nothing |
158
+
159
+ `state.transition("fade" | "mask" | "pixelate", …)` drives the last three
160
+ for a state change (see `melonjs-scenes-and-state`). A `Trigger` that loads a
161
+ level transitions only with both a `color` and a `duration`, and takes only
162
+ `transition: "fade"` (the default) or `"mask"` with a `shape`: anything else,
163
+ `"pixelate"` included, is a fade.
164
+
165
+ For an effect of your own, extend `CameraEffect`: `update(dt)` runs every
166
+ frame (`dt` in ms), `draw(renderer, width, height)` draws over the camera's
167
+ view, and `destroy()` frees what it holds. Set `isComplete = true` and the
168
+ camera's update destroys it that frame, unless it is `isPersistent`.
169
+
170
+ ## Zoom and roll
171
+
172
+ ```js
173
+ app.viewport.zoom = 2; // above 1 zooms in, below 1 out
174
+ app.viewport.roll = Math.PI / 32; // a slight dutch angle, in radians
175
+ ```
176
+
177
+ A zoomed camera follows, keeps to its bounds and centres on `focusOn` in world
178
+ units, and `localToWorld` / `worldToLocal` count the zoom. A zoom of 0 or less
179
+ is taken as 1.
180
+
181
+ `roll` is absolute (assigning the same angle twice changes nothing), and the
182
+ pointer conversions undo it, so picking under a rolled camera needs nothing.
183
+ It OWNS the camera's `currentTransform`: each assignment rebuilds it, so a
184
+ `rotate()` or `scale()` called on the camera itself is lost at the next one.
185
+ `reset()` puts it back to 0. On a `Camera3d` the same `roll` banks the view
186
+ (see below).
187
+
139
188
  ## Coordinate conversion
140
189
 
141
190
  ```js
@@ -143,6 +192,12 @@ app.viewport.worldToLocal(x, y); // world → screen
143
192
  app.viewport.localToWorld(x, y); // screen → world
144
193
  ```
145
194
 
195
+ They agree with where the camera draws: a level smaller than the view is
196
+ centred by offsetting the world container (`world.pos`), which the default
197
+ camera draws and a minimap, corner or zoomed camera leaves out, and each
198
+ camera's conversions follow suit (20.13 and later; before, a minimap's were off
199
+ by `world.pos`).
200
+
146
201
  Both return a `Vector2d`; with no third argument that vector comes from the
147
202
  shared pool, so in a per-frame path pass your own `v` as the third argument, or
148
203
  release it with `getPool("vector2d").release(v)`.
@@ -205,6 +260,14 @@ and an overridden `postDraw`:
205
260
  this.cameras.set("minimap", new MinimapCamera());
206
261
  ```
207
262
 
263
+ A stage's cameras draw in the order they were added, each over the ones before
264
+ it, so a full-screen second camera is a layer: a `Camera3d` drawing a 3D
265
+ background first, the game's own camera over it, each kept to its part of the
266
+ world with `visibilityLayer` / `cullMask` (below). A camera's post effects
267
+ apply to what that camera draws: the first camera's pass starts the frame, and
268
+ a later camera's is blended over what is already there (20.13 and later;
269
+ before, it wiped out the cameras before it).
270
+
208
271
  View state is per camera, not per scene: a `Camera3d` carries its own distance
209
272
  fog (`setFog`), so a minimap or split-screen view fogs independently of the main
210
273
  one — and a `Camera2d` never fogs. See `melonjs-3d`.
@@ -228,6 +291,9 @@ minimap.cullMask = ~PARTICLES; // the minimap draws every layer but 2
228
291
  set it on an emitter and none of its particles are drawn. The world itself
229
292
  obeys it too, and so does what it contains: a child is drawn only if it and
230
293
  every container above it share a bit with the camera.
294
+ - Particles are on their emitter's layers, and follow a change to them on the
295
+ emitter's next update, so a camera that draws only the emitter's layer draws
296
+ its particles too.
231
297
  - A renderable on layer `0` is drawn by no camera; a `cullMask` of `0` draws
232
298
  nothing.
233
299
  - It decides drawing only: what a camera leaves out still updates, collides and
@@ -256,9 +322,56 @@ draw(renderer) {
256
322
  }
257
323
  ```
258
324
 
325
+ **The colour's alpha and the global alpha are two things**, as on a 2D
326
+ context. A fill or a stroke is drawn at the alpha of `setColor` times
327
+ `setGlobalAlpha` (which is where the renderable's opacity comes in), an image at
328
+ the global alpha (times the tint) alone, and `getGlobalAlpha()` never moves with
329
+ `setColor`. So to fade a translucent shape, lower the global alpha and keep the
330
+ colour; folding the colour's alpha into `setGlobalAlpha` as well counts it twice.
331
+
332
+ `lineWidth`, `lineJoin`, the colour and the global alpha are all part of what
333
+ `save()` / `restore()` keep, and every renderable's `draw()` runs between the
334
+ two, so set them freely there: the next renderable gets them back.
335
+
336
+ `fillArc` / `strokeArc` read their angles like `CanvasRenderingContext2D.arc`:
337
+ an end before the start wraps round (`end + 2π`), and a span of 2π or more in
338
+ the arc's own direction is the whole circle. A filled part-circle differs by
339
+ backend, though: Canvas closes it with a straight chord, the GPU renderers fill
340
+ a pie slice from the centre.
341
+
259
342
  Geometry available: `Rect`, `RoundRect`, `Ellipse`, `Polygon`, `Line`, `Point`
260
- (and a `Bounds`). `renderer.fill(shape)` and `renderer.stroke(shape)` take any
261
- of them and throw `Invalid geometry for fill/stroke` for anything else.
343
+ (and a `Bounds`), plus the 3D `Box3d` and `Sphere`, drawn as their footprint
344
+ and their circle on the screen plane. `renderer.fill(shape)` and
345
+ `renderer.stroke(shape)` take any of them and throw `Invalid geometry for
346
+ fill/stroke` for anything else.
347
+
348
+ ### Paths and SVG strings
349
+
350
+ For any other outline, build a path on the renderer's own `renderer.path2D`,
351
+ then call `fill()` / `stroke()` with no argument:
352
+
353
+ ```js
354
+ renderer.path2D.parseSVGPath("M 50 0 L 100 100 L 0 100 Z");
355
+ renderer.setColor("#4CAF50");
356
+ renderer.fill();
357
+ ```
358
+
359
+ `renderer.path2D` also takes the methods of the browser's `Path2D`
360
+ (`beginPath`, `moveTo`, `lineTo`, `arc`, `arcTo`, `ellipse`, `rect`,
361
+ `roundRect`, `quadraticCurveTo`, `bezierCurveTo`, `closePath`).
362
+
363
+ - **Every `moveTo`, every `M`, starts a sub-path, and filled, each one after
364
+ the first is a HOLE**: a donut, the counter of an "O". Wind a hole the
365
+ opposite way to the outline, as Canvas fills by the non-zero rule, where a
366
+ hole wound the same way fills in. Draw separate shapes with separate fills.
367
+ - **`parseSVGPath` reads absolute commands only** (`M L H V Q C A Z`). A
368
+ lowercase, relative command is read as its absolute twin, so a path
369
+ exported with relative commands comes out scrambled, and `H` / `V` take an
370
+ OFFSET from the pen, not the coordinate SVG means. Export absolute
371
+ coordinates.
372
+ - **The engine's `Path2D` class is not exported.** `new Path2D()` in a game is
373
+ the browser's own, which `fill()` / `stroke()` refuse as invalid geometry:
374
+ use `renderer.path2D`.
262
375
 
263
376
  `Gradient` gives linear and radial fills, built with
264
377
  `renderer.createLinearGradient(x0, y0, x1, y1)` /
@@ -319,10 +432,11 @@ drawable, and the engine's own bakes (the particle emitter's default texture)
319
432
  hand `.canvas` on for exactly that reason.
320
433
 
321
434
  **Create it once and keep it.** On a GPU backend, once drawn, its texture stays
322
- on the GPU for as long as the renderer lives: a stage change does not release
323
- it, and no public call does. A target made for every level, every restart or
324
- every spawned object leaks one texture each time, so make it once (at module
325
- level, say) and clear it for reuse. If you paint into it again after it has
435
+ on the GPU until `rt.destroy(renderer)` frees it: a stage change does not, and
436
+ `destroy()` without the renderer frees the canvas but not that copy. A target
437
+ made for every level, every restart or every spawned object and never
438
+ destroyed leaks one texture each time, so make it once (at module level, say)
439
+ and clear it for reuse. If you paint into it again after it has
326
440
  been drawn, call `rt.invalidate(renderer)` before the next draw so the GPU copy
327
441
  is refreshed.
328
442
 
@@ -383,6 +497,7 @@ const data = await rt.toImageData(0, 0, w, h);
383
497
 
384
498
  | symptom | cause |
385
499
  |---|---|
500
+ | a second camera's post effect wipes out what the first camera drew | before 20.13 a later camera's post-effect pass replaced the whole frame; update, or put the effects on a container instead of the camera |
386
501
  | `follow()` set but the view never moves | viewport bounds still at renderer size |
387
502
  | `invalid target for me.Camera2d.follow` | passed `player.pos` — pass the renderable, a `Vector2d` or a `Vector3d` |
388
503
  | camera drag jumps around | using world (`gameWorldX`) instead of screen (`gameScreenX`) coordinates |
@@ -390,12 +505,24 @@ const data = await rt.toImageData(0, 0, w, h);
390
505
  | custom drawing appears at the origin | `draw()` not reading `this.pos` |
391
506
  | custom drawing offset by half its size | centred `anchorPoint` not zeroed |
392
507
  | `setLineWidth` is not a function | it is the `lineWidth` property |
508
+ | after a translucent `setColor`, images, later colours and the next renderables come out faded (WebGL / WebGPU) | before 20.13 the colour's alpha leaked into the global alpha; update |
509
+ | a translucent colour comes out fainter than asked | its alpha folded into `setGlobalAlpha` as well, so it counts twice |
510
+ | a `lineWidth` set in one `draw()` thickens everything drawn after it (WebGL / WebGPU) | before 20.13 `restore()` did not put `lineWidth` back; update, or put it back yourself |
511
+ | `fillArc` with the end angle before the start fills the wrong part of the circle (WebGL / WebGPU) | before 20.13 the GPU renderers did not wrap the angles like Canvas; update |
512
+ | a minimap's `worldToLocal` / `localToWorld` are off by the level's centring offset | before 20.13 they applied `world.pos`, which a non-default camera does not draw; update |
393
513
  | clipping in the wrong place inside a container | container clips in local coordinates, after its own translate |
394
514
  | `container.clipping = true` does nothing | container has no explicit size, so `width`/`height` are `Infinity` |
395
515
  | baked target draws nothing / throws | passed the `CanvasRenderTarget`, not its `.canvas` |
396
516
  | `getImageData()` throws on one machine but not another | that one is on the WebGPU renderer, which cannot read back synchronously — `await toImageData()` instead |
397
517
  | frame rate drops with many static draws | bake into a `CanvasRenderTarget` instead |
398
- | GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it |
518
+ | GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it, or `destroy(renderer)` the old one |
519
+ | a second `shake()` does nothing | one is still running: pass `force` (the fifth argument) to replace it |
520
+ | a `Trigger` with `transition: "pixelate"` fades instead | a trigger takes only `"fade"` or `"mask"`; add a `PixelateTransitionEffect` yourself |
521
+ | a `Trigger` loads its level with no transition | it needs both `color` and `duration` |
522
+ | a `rotate()` or `scale()` on the camera is lost | `roll` owns the camera's `currentTransform` and rebuilds it; use `roll` and `zoom` |
523
+ | an SVG path from an editor comes out scrambled | `parseSVGPath` reads absolute commands only, and `H` / `V` as offsets |
524
+ | a hole in a path fills in, on Canvas only | it is wound the same way as the outline; wind it the opposite way |
525
+ | `fill(new Path2D(...))` throws `Invalid geometry for fill/stroke` | that is the browser's `Path2D`; build on `renderer.path2D` and call `fill()` |
399
526
  | a repainted `CanvasRenderTarget` still shows the old pixels | call `rt.invalidate(renderer)` after painting |
400
527
  | a 3D camera cannot be given an arbitrary up | `Camera3d#lookAt(target, up)` or `setBasis(right, up, forward)`; see `melonjs-3d` |
401
528
  | particles or effects clutter a minimap | every camera draws everything: put them on their own `visibilityLayer` and leave it out of the minimap's `cullMask` |