melonjs 20.7.0 → 20.8.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 (243) hide show
  1. package/README.md +6 -3
  2. package/build/application/defaultApplicationSettings.d.ts +5 -0
  3. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/settings.d.ts +88 -0
  6. package/build/application/settings.d.ts.map +1 -1
  7. package/build/audio/backend/engine.d.ts +0 -1
  8. package/build/audio/backend/sound.d.ts +0 -1
  9. package/build/camera/camera2d.d.ts.map +1 -1
  10. package/build/camera/camera3d.d.ts +95 -8
  11. package/build/camera/camera3d.d.ts.map +1 -1
  12. package/build/geometries/box3d.d.ts +24 -6
  13. package/build/geometries/box3d.d.ts.map +1 -1
  14. package/build/geometries/ellipse.d.ts +14 -0
  15. package/build/geometries/ellipse.d.ts.map +1 -1
  16. package/build/geometries/polygon.d.ts +14 -0
  17. package/build/geometries/polygon.d.ts.map +1 -1
  18. package/build/geometries/sphere.d.ts +35 -6
  19. package/build/geometries/sphere.d.ts.map +1 -1
  20. package/build/index.d.ts +9 -2
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +2770 -538
  23. package/build/index.js.map +4 -4
  24. package/build/input/pointerevent.d.ts +8 -0
  25. package/build/input/pointerevent.d.ts.map +1 -1
  26. package/build/level/gltf/GLTFModel.d.ts +17 -31
  27. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  28. package/build/level/gltf/GLTFScene.d.ts +40 -1
  29. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  30. package/build/level/level.d.ts +35 -0
  31. package/build/level/level.d.ts.map +1 -1
  32. package/build/level/tiled/TMXLayer.d.ts +5 -2
  33. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  34. package/build/level/tiled/TMXObject.d.ts +0 -1
  35. package/build/level/tiled/TMXTile.d.ts +2 -1
  36. package/build/level/tiled/TMXTile.d.ts.map +1 -1
  37. package/build/level/tiled/TMXTileMap.d.ts +7 -4
  38. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  39. package/build/level/tiled/TMXTileset.d.ts +1 -1
  40. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  41. package/build/level/tiled/TMXTilesetGroup.d.ts +5 -0
  42. package/build/level/tiled/TMXTilesetGroup.d.ts.map +1 -1
  43. package/build/level/tiled/factories/shape.d.ts +0 -1
  44. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts +6 -5
  45. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  46. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts +5 -0
  47. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  48. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +8 -7
  49. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  50. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts +5 -0
  51. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  52. package/build/level/tiled/renderer/TMXRenderer.d.ts +19 -4
  53. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  54. package/build/level/tiled/renderer/autodetect.d.ts +0 -4
  55. package/build/level/tiled/renderer/autodetect.d.ts.map +1 -1
  56. package/build/loader/loader.d.ts +9 -0
  57. package/build/loader/loader.d.ts.map +1 -1
  58. package/build/loader/loadingscreen.d.ts +1 -2
  59. package/build/loader/loadingscreen.d.ts.map +1 -1
  60. package/build/loader/parsers/shader.d.ts +0 -2
  61. package/build/math/color.d.ts +27 -4
  62. package/build/math/color.d.ts.map +1 -1
  63. package/build/particles/emitter.d.ts.map +1 -1
  64. package/build/particles/particle.d.ts +22 -1
  65. package/build/particles/particle.d.ts.map +1 -1
  66. package/build/particles/settings.d.ts +69 -0
  67. package/build/particles/settings.d.ts.map +1 -1
  68. package/build/physics/adapter.d.ts +9 -1
  69. package/build/physics/adapter.d.ts.map +1 -1
  70. package/build/physics/builtin/body.d.ts +3 -2
  71. package/build/physics/builtin/body.d.ts.map +1 -1
  72. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  73. package/build/physics/builtin/detector.d.ts +0 -1
  74. package/build/physics/builtin/detector.d.ts.map +1 -1
  75. package/build/physics/builtin/sat.d.ts +0 -7
  76. package/build/physics/builtin/sat3d.d.ts +46 -0
  77. package/build/physics/builtin/sat3d.d.ts.map +1 -1
  78. package/build/physics/response.d.ts +45 -62
  79. package/build/physics/response.d.ts.map +1 -1
  80. package/build/renderable/collectable.d.ts +6 -1
  81. package/build/renderable/collectable.d.ts.map +1 -1
  82. package/build/renderable/colorlayer.d.ts +4 -4
  83. package/build/renderable/colorlayer.d.ts.map +1 -1
  84. package/build/renderable/container.d.ts +23 -17
  85. package/build/renderable/container.d.ts.map +1 -1
  86. package/build/renderable/entity/entity.d.ts +12 -16
  87. package/build/renderable/entity/entity.d.ts.map +1 -1
  88. package/build/renderable/frameAnimation.d.ts +1 -1
  89. package/build/renderable/groundshadow.d.ts +1 -0
  90. package/build/renderable/groundshadow.d.ts.map +1 -1
  91. package/build/renderable/imagelayer.d.ts +12 -5
  92. package/build/renderable/imagelayer.d.ts.map +1 -1
  93. package/build/renderable/instanced_mesh.d.ts +2 -9
  94. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  95. package/build/renderable/mesh.d.ts +283 -4
  96. package/build/renderable/mesh.d.ts.map +1 -1
  97. package/build/renderable/nineslicesprite.d.ts +3 -13
  98. package/build/renderable/nineslicesprite.d.ts.map +1 -1
  99. package/build/renderable/renderable.d.ts +90 -27
  100. package/build/renderable/renderable.d.ts.map +1 -1
  101. package/build/renderable/sprite.d.ts +5 -19
  102. package/build/renderable/sprite.d.ts.map +1 -1
  103. package/build/renderable/sprite3d.d.ts +27 -16
  104. package/build/renderable/sprite3d.d.ts.map +1 -1
  105. package/build/renderable/text/bitmaptext.d.ts +13 -3
  106. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  107. package/build/renderable/text/bitmaptextdata.d.ts +0 -4
  108. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  109. package/build/renderable/text/text.d.ts +5 -3
  110. package/build/renderable/text/text.d.ts.map +1 -1
  111. package/build/renderable/text/textmetrics.d.ts +2 -0
  112. package/build/renderable/text/textmetrics.d.ts.map +1 -1
  113. package/build/renderable/trail.d.ts +0 -1
  114. package/build/renderable/trail.d.ts.map +1 -1
  115. package/build/renderable/trigger.d.ts +27 -5
  116. package/build/renderable/trigger.d.ts.map +1 -1
  117. package/build/renderable/ui/progressbar.d.ts +194 -0
  118. package/build/renderable/ui/progressbar.d.ts.map +1 -0
  119. package/build/renderable/ui/uibaseelement.d.ts +48 -3
  120. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  121. package/build/renderable/ui/uispriteelement.d.ts +19 -3
  122. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  123. package/build/system/legacy_pool.d.ts +2 -2
  124. package/build/system/legacy_pool.d.ts.map +1 -1
  125. package/build/system/pool.d.ts +10 -0
  126. package/build/system/pool.d.ts.map +1 -1
  127. package/build/tweens/tween.d.ts +49 -2
  128. package/build/tweens/tween.d.ts.map +1 -1
  129. package/build/video/canvas/canvas_renderer.d.ts +34 -23
  130. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  131. package/build/video/effects/blendEffect.d.ts +0 -1
  132. package/build/video/effects/bloom.d.ts +72 -0
  133. package/build/video/effects/bloom.d.ts.map +1 -0
  134. package/build/video/effects/blur.d.ts +3 -2
  135. package/build/video/effects/blur.d.ts.map +1 -1
  136. package/build/video/effects/chromaticAberration.d.ts +3 -2
  137. package/build/video/effects/chromaticAberration.d.ts.map +1 -1
  138. package/build/video/effects/colorMatrix.d.ts +3 -2
  139. package/build/video/effects/colorMatrix.d.ts.map +1 -1
  140. package/build/video/effects/desaturate.d.ts +10 -2
  141. package/build/video/effects/desaturate.d.ts.map +1 -1
  142. package/build/video/effects/dissolve.d.ts +3 -2
  143. package/build/video/effects/dissolve.d.ts.map +1 -1
  144. package/build/video/effects/dropShadow.d.ts +3 -2
  145. package/build/video/effects/dropShadow.d.ts.map +1 -1
  146. package/build/video/effects/flash.d.ts +3 -2
  147. package/build/video/effects/flash.d.ts.map +1 -1
  148. package/build/video/effects/glow.d.ts +3 -2
  149. package/build/video/effects/glow.d.ts.map +1 -1
  150. package/build/video/effects/hologram.d.ts +3 -2
  151. package/build/video/effects/hologram.d.ts.map +1 -1
  152. package/build/video/effects/invert.d.ts +10 -2
  153. package/build/video/effects/invert.d.ts.map +1 -1
  154. package/build/video/effects/outline.d.ts +3 -2
  155. package/build/video/effects/outline.d.ts.map +1 -1
  156. package/build/video/effects/pixelate.d.ts +3 -2
  157. package/build/video/effects/pixelate.d.ts.map +1 -1
  158. package/build/video/effects/radialGradient.d.ts +8 -3
  159. package/build/video/effects/radialGradient.d.ts.map +1 -1
  160. package/build/video/effects/scanline.d.ts +3 -2
  161. package/build/video/effects/scanline.d.ts.map +1 -1
  162. package/build/video/effects/sepia.d.ts +10 -2
  163. package/build/video/effects/sepia.d.ts.map +1 -1
  164. package/build/video/effects/shadereffect.d.ts +10 -4
  165. package/build/video/effects/shadereffect.d.ts.map +1 -1
  166. package/build/video/effects/shine.d.ts +3 -2
  167. package/build/video/effects/shine.d.ts.map +1 -1
  168. package/build/video/effects/tintPulse.d.ts +3 -2
  169. package/build/video/effects/tintPulse.d.ts.map +1 -1
  170. package/build/video/effects/tonemap.d.ts +121 -0
  171. package/build/video/effects/tonemap.d.ts.map +1 -0
  172. package/build/video/effects/vignette.d.ts +3 -2
  173. package/build/video/effects/vignette.d.ts.map +1 -1
  174. package/build/video/effects/wave.d.ts +3 -2
  175. package/build/video/effects/wave.d.ts.map +1 -1
  176. package/build/video/gpu/vertexformat.d.ts +26 -0
  177. package/build/video/gpu/vertexformat.d.ts.map +1 -1
  178. package/build/video/gradient.d.ts +0 -1
  179. package/build/video/gradient.d.ts.map +1 -1
  180. package/build/video/renderer.d.ts +111 -7
  181. package/build/video/renderer.d.ts.map +1 -1
  182. package/build/video/renderstate.d.ts +2 -0
  183. package/build/video/renderstate.d.ts.map +1 -1
  184. package/build/video/rendertarget/canvasrendertarget.d.ts +3 -4
  185. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  186. package/build/video/rendertarget/render_target_pool.d.ts +0 -1
  187. package/build/video/rendertarget/webglrendertarget.d.ts +0 -1
  188. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  189. package/build/video/rendertarget/webgpurendertarget.d.ts +18 -2
  190. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  191. package/build/video/texture/atlas.d.ts +1 -0
  192. package/build/video/texture/atlas.d.ts.map +1 -1
  193. package/build/video/texture/cache.d.ts +1 -3
  194. package/build/video/texture/frametexture.d.ts +0 -1
  195. package/build/video/texture/frametexture.d.ts.map +1 -1
  196. package/build/video/texture/noise_texture2d.d.ts +1 -0
  197. package/build/video/texture/noise_texture2d.d.ts.map +1 -1
  198. package/build/video/utils/autodetect.d.ts +0 -2
  199. package/build/video/webgl/batchers/batcher.d.ts +0 -9
  200. package/build/video/webgl/batchers/batcher.d.ts.map +1 -1
  201. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +11 -0
  202. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  203. package/build/video/webgl/batchers/material_batcher.d.ts +4 -2
  204. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  205. package/build/video/webgl/batchers/mesh_batcher.d.ts +0 -3
  206. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  207. package/build/video/webgl/batchers/quad_batcher.d.ts +4 -0
  208. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  209. package/build/video/webgl/buffer/index.d.ts +0 -1
  210. package/build/video/webgl/buffer/retained_geometry.d.ts +0 -1
  211. package/build/video/webgl/glshader.d.ts +5 -3
  212. package/build/video/webgl/glshader.d.ts.map +1 -1
  213. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts +0 -4
  214. package/build/video/webgl/texture/store.d.ts +0 -1
  215. package/build/video/webgl/webgl_renderer.d.ts +63 -24
  216. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  217. package/build/video/webgpu/batchers/mesh_batcher.d.ts +0 -3
  218. package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
  219. package/build/video/webgpu/batchers/quad_batcher.d.ts +0 -2
  220. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  221. package/build/video/webgpu/batchers/webgpu_batcher.d.ts +0 -2
  222. package/build/video/webgpu/batchers/webgpu_batcher.d.ts.map +1 -1
  223. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  224. package/build/video/webgpu/renderers/tmxlayer/orthogonal.d.ts +0 -2
  225. package/build/video/webgpu/texture/frametexture.d.ts +0 -2
  226. package/build/video/webgpu/texture/frametexture.d.ts.map +1 -1
  227. package/build/video/webgpu/texture/store.d.ts +0 -1
  228. package/build/video/webgpu/webgpu_renderer.d.ts +7 -75
  229. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  230. package/package.json +1 -1
  231. package/skills/melonjs-3d/SKILL.md +277 -19
  232. package/skills/melonjs-3d-assets/SKILL.md +76 -9
  233. package/skills/melonjs-camera-and-drawing/SKILL.md +57 -1
  234. package/skills/melonjs-effects-and-shaders/SKILL.md +341 -29
  235. package/skills/melonjs-input/SKILL.md +48 -4
  236. package/skills/melonjs-particles-and-trails/SKILL.md +106 -4
  237. package/skills/melonjs-performance/SKILL.md +79 -2
  238. package/skills/melonjs-physics/SKILL.md +101 -27
  239. package/skills/melonjs-renderables/SKILL.md +2 -1
  240. package/skills/melonjs-scenes-and-state/SKILL.md +121 -1
  241. package/skills/melonjs-ui-and-text/SKILL.md +165 -4
  242. package/build/level/gltf/srgb.d.ts +0 -20
  243. package/build/level/gltf/srgb.d.ts.map +0 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-effects-and-shaders
3
- description: "Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: ShaderEffect, addPostEffect, removePostEffect, getPostEffect, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter."
3
+ description: "Use this skill for post-processing effects, custom shaders, blend modes and colour grading in melonJS. Covers the built-in ShaderEffect presets, addPostEffect on renderables and cameras, writing a custom dual-language GLSL/WGSL effect, the screen_texture builtins, and why effects silently do nothing on the Canvas fallback. Triggers on: Tween, easing, animate an effect, ShaderEffect, addPostEffect, removePostEffect, getPostEffect, postEffectNeedsCapture, VignetteEffect, GlowEffect, BlurEffect, PixelateEffect, ScanlineEffect, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, blendMode, colorMatrix, screen_texture, toFrameTexture, post effect, filter."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -41,7 +41,7 @@ quad.scale(viewW / anyImage.width, viewH / anyImage.height);
41
41
  quad.floating = true; // its own uv is now screen space
42
42
  quad.blendMode = "additive";
43
43
  quad.addPostEffect(myEffect);
44
- world.addChild(quad, HUD_Z - 10); // BELOW the labels, not above
44
+ world.addChild(quad, HUD_Z - 10); // 2D: below the labels. See the note.
45
45
  ```
46
46
 
47
47
  Two things follow from the quad filling the frame: its own `uv` is screen space
@@ -49,9 +49,47 @@ Two things follow from the quad filling the frame: its own `uv` is screen space
49
49
  and the incoming `color` is the quad's own texture, which a body that paints
50
50
  from scratch can ignore entirely.
51
51
 
52
- Depth here is easy to get backwards — a higher z draws **later**, i.e. on top.
53
- Verify it by returning a flat colour from the body for one frame: whatever it
54
- tints is what the pass covers.
52
+ **Depth here is easy to get backwards, and the rule FLIPS between the two sort
53
+ modes.** Under the default `sortOn = "z"` a higher z draws later, i.e. on top,
54
+ so `HUD_Z - 10` puts the pass under the labels as written above. Under
55
+ `sortOn = "depth"`, which every `Camera3d` scene sets, a floating child's sort
56
+ key is `pos.z * pos.z` and the walk paints far to near, so the SMALLER
57
+ magnitude is nearer and draws on top — `HUD_Z - 10` there puts the pass OVER
58
+ the labels, the opposite of the comment.
59
+
60
+ So in a 3D scene give the overlay a larger magnitude than the HUD, and give
61
+ the HUD a small one (`melonjs-3d` and `melonjs-ui-and-text` both say a HUD
62
+ wants a small depth, not the huge z that would put it on top in 2D).
63
+
64
+ Either way, verify it rather than reasoning about it: return a flat colour from
65
+ the body for one frame and see what it tints.
66
+
67
+ ## A renderable that draws with PRIMITIVES has to ask to be captured
68
+
69
+ One effect is applied the cheap way: the renderable is drawn with the effect's
70
+ own program instead of being captured offscreen and post-processed, which costs
71
+ no render target. That is equivalent only when everything the renderable draws
72
+ is a textured quad. `fillRect`, `strokeRect` and `renderer.fill(shape)` go
73
+ through a batcher that never reads that shader, so **one** effect on such a
74
+ renderable would silently do nothing, while **two** would work, because a chain
75
+ always captures.
76
+
77
+ So a renderable of your own that draws with primitives says so:
78
+
79
+ ```js
80
+ class Bar extends Renderable {
81
+ constructor(x, y, w, h) {
82
+ super(x, y, w, h);
83
+ this.postEffectNeedsCapture = true; // ← or one effect is a no-op
84
+ }
85
+ draw(renderer) {
86
+ renderer.fillRect(this.pos.x, this.pos.y, this.width, this.height);
87
+ }
88
+ }
89
+ ```
90
+
91
+ `ProgressBar` and `Trail` set it for you, and a sprite needs nothing: the flag
92
+ defaults to `false` and only ever changes the single-effect case.
55
93
 
56
94
  ## Toggle with `enabled`, do not remove
57
95
 
@@ -72,13 +110,257 @@ reused across renderables.
72
110
 
73
111
  ## Built-in effects
74
112
 
75
- Eighteen presets, every one constructed as `new XEffect(renderer, options)` and
113
+ Twenty-one presets, every one constructed as `new XEffect(renderer, options)` and
76
114
  every one shipping both a GLSL and a WGSL body, so they all run on either GPU
77
115
  backend: `VignetteEffect`, `ScanlineEffect` (optional CRT curvature),
78
- `GlowEffect`, `BlurEffect`, `PixelateEffect`, `ChromaticAberrationEffect`,
79
- `DropShadowEffect`, `FlashEffect`, `OutlineEffect`, `ColorMatrixEffect`,
80
- `DesaturateEffect`, `DissolveEffect`, `TintPulseEffect`, `WaveEffect`,
81
- `InvertEffect`, `SepiaEffect`, `HologramEffect`, `ShineEffect`.
116
+ `BloomEffect`, `GlowEffect`, `BlurEffect`, `PixelateEffect`,
117
+ `ChromaticAberrationEffect`, `DropShadowEffect`, `FlashEffect`,
118
+ `OutlineEffect`, `ColorMatrixEffect`, `DesaturateEffect`, `DissolveEffect`,
119
+ `TintPulseEffect`, `WaveEffect`, `InvertEffect`, `SepiaEffect`,
120
+ `HologramEffect`, `ShineEffect`, `RadialGradientEffect`, `ToneMappingEffect`.
121
+
122
+ `RadialGradientEffect` is a procedural soft spot: solid at the centre of its
123
+ host quad, fading linearly to transparent at the edge, and elliptical for free
124
+ on a non-square quad. It is the one to reach for instead of baking a disc into
125
+ a canvas, for a pickup highlight, a trigger marker or a damage flash. Its
126
+ falloff assumes the quad samples a FULL texture rect, so pair it with a
127
+ standalone image or `Renderer.getWhitePixel()` rather than a sprite that draws
128
+ a sub-region of an atlas, where the centre would land off-quad.
129
+
130
+ **`BloomEffect` and `GlowEffect` are not the same thing**, and reaching for the
131
+ wrong one is the usual disappointment here. `GlowEffect` draws a halo OUTSIDE a
132
+ sprite's silhouette: its body returns the colour untouched wherever alpha is
133
+ already non-zero, so on an opaque frame it returns early on every fragment and
134
+ a camera-wide glow does nothing at all. `BloomEffect` is the screen bloom —
135
+ bright pixels bleed into their neighbours — and it is the one to put on the
136
+ camera:
137
+
138
+ ```js
139
+ game.viewport.addPostEffect(new BloomEffect(app.renderer));
140
+ // only the brightest highlights, with a wider halo
141
+ game.viewport.addPostEffect(
142
+ new BloomEffect(app.renderer, { threshold: 0.85, radius: 4 }),
143
+ );
144
+ ```
145
+
146
+ Its tap offsets are in texels, so it takes its size from the renderer at
147
+ construction; call `setTextureSize(w, h)` from an `event.ONRESIZE` handler, or
148
+ when applying it to something smaller than the frame.
149
+
150
+ **`ToneMappingEffect` goes after it, and goes last.** Bloom spreads the bright
151
+ parts; the tone map is what stops the sum of the two clipping flat at white,
152
+ which is the difference between an effect that reads as light and one that
153
+ reads as a white sticker. `exposure` lifts the image into the curve, and the
154
+ curve (`aces`, `reinhard` or `exponential`) is baked at construction:
155
+
156
+ ```js
157
+ game.viewport.addPostEffect(new BloomEffect(app.renderer));
158
+ game.viewport.addPostEffect(
159
+ new ToneMappingEffect(app.renderer, { exposure: 1.6 }),
160
+ );
161
+ ```
162
+
163
+ It is a grade, not a colour pipeline. By default melonJS renders into `RGBA8`
164
+ everywhere, so the frame reaching it is already clamped and `exposure` past 1
165
+ is what gives the curve range to compress.
166
+
167
+ **`hdr: true` in the application settings changes that**: render targets
168
+ become half-float, so the chain carries values above 1 instead of clamping at
169
+ every write, and the bloom threshold starts discriminating genuinely
170
+ over-bright pixels rather than merely bright ones. Off by default: it costs
171
+ GPU memory on the camera's two render targets and depends on the driver.
172
+
173
+ On its own it is **not HDR output**: the frame is still presented in SDR, so
174
+ anything above 1 clamps on the final blit. What it buys is that nothing clamped
175
+ BEFORE then. Removing that last clamp is a second, separate opt-in —
176
+ `hdrOutput`, below — and it is WebGPU-only.
177
+
178
+ ```js
179
+ const app = new Application(1024, 576, { hdr: true, … });
180
+ // …and check what you actually got, since a driver can refuse it
181
+ if (app.renderer.supportsHDR) { /* the chain now carries values above 1 */ }
182
+ ```
183
+
184
+ Branch the GRADE on that flag, not just the feature. An exposure under 1 and a
185
+ bloom threshold at 1 make sense only where values above 1 exist; applied to a
186
+ chain that clamps, the same numbers tone map an already display-referred frame
187
+ and switch the bloom off, which reads as a dark, flat picture rather than as a
188
+ missing feature. Where there is no headroom, squeeze each emitter into range in
189
+ its own shader instead and put the bloom threshold back under 1.
190
+
191
+ **Nothing is added to your chain.** `hdr` is additive: the same frame, plus
192
+ additive content that no longer saturates per write and a threshold that means
193
+ something. Anything still above 1 at the end clamps on the blit, as it always
194
+ did.
195
+
196
+ A tone map is therefore a decision, not a consequence, and it is a bigger one
197
+ than it looks. Every curve maps 1 to less than 1, so a 2D look built ON the
198
+ clamp — clipped white specular, saturated neon — goes matte the moment you add
199
+ one, at any exposure. Add one when your art is authored in linear light and you
200
+ want the highlights rolled off; skip it when the clipping IS the look.
201
+
202
+ It applies to the CAMERA chain only. A sprite's chain keeps 8-bit targets on
203
+ purpose: its final blit composites with `ONE_MINUS_SRC_ALPHA`, and additive
204
+ blending accumulates alpha past 1 in a float target, so that factor would go
205
+ negative and subtract the backdrop.
206
+
207
+ ### Once it is on: putting values above 1 into the frame
208
+
209
+ `hdr: true` on its own changes nothing visible. It removes a ceiling; something
210
+ still has to reach for it. No single draw exceeds 1 on its own, so a game that
211
+ turns the setting on and changes nothing else renders very nearly the frame it
212
+ did before and looks like the feature is broken. The exception is worth knowing
213
+ because it is free: additive content that already overlapped was being clipped
214
+ at every write, and stops being.
215
+
216
+ What produces over-bright pixels:
217
+
218
+ ```js
219
+ // a built-in colour matrix, scaled past 1 — no custom shader needed
220
+ sprite.addPostEffect(new ColorMatrixEffect(app.renderer).brightness(4));
221
+
222
+ // in 3D, both of these were always unbounded; the 8-bit target was the only
223
+ // thing destroying the range. `emissive` is a 3-component Float32Array, set
224
+ // from the mesh settings or replaced wholesale — it is not a Color
225
+ const mesh = new Mesh(x, y, { emissive: [2.5, 1.8, 0.4], /* … */ });
226
+ light.intensity = 3;
227
+
228
+ // and additive blending, which accumulates past 1 once the target can hold it
229
+ sprite.blendMode = "additive";
230
+ ```
231
+
232
+ **One effect, not two.** A renderable carrying exactly ONE post effect takes
233
+ the renderer's `customShader` fast path: no offscreen target, and it draws
234
+ straight into the camera's half-float target with its range intact. A chain of
235
+ two or more is routed through a pooled intermediate that stays 8 bits per
236
+ channel on purpose, so the energy is clamped there, before the camera ever sees
237
+ it. Adding a second effect to an emitter is enough to silently undo its
238
+ headroom.
239
+
240
+ ### Everything inside the camera is graded, including your UI
241
+
242
+ The tone curve runs on the camera's whole frame. A `floating` HUD is still
243
+ inside it, so a label authored at display white arrives at the curve as 1 and
244
+ leaves at `1 - exp(-exposure)` — at exposure 0.45 that is 0.36, and the HUD
245
+ comes out at roughly a third of its brightness for no reason the author can
246
+ see.
247
+
248
+ UI in an HDR chain has to be authored for that chain, exactly as the scene is:
249
+
250
+ ```js
251
+ // drive the readout so the curve lands it back at display white. `shared`
252
+ // keeps one instance across every element: it opts the effect out of the
253
+ // per-renderable auto-destroy, so the first element torn down does not take
254
+ // the program the others are still using
255
+ const energy = new ColorMatrixEffect(app.renderer).brightness(2);
256
+ energy.shared = true;
257
+ for (const element of hudElements) element.addPostEffect(energy);
258
+ ```
259
+
260
+ Put it on each ELEMENT, not on their container: `beginPostEffect` clears
261
+ `customShader` for any renderable with no effects of its own, so a container's
262
+ shader is thrown away by its first child. The alternative is to keep the UI out
263
+ of the camera pass entirely, by overriding `Stage.draw` and drawing it after
264
+ `super.draw(...)` — at the cost of the bloom that makes additive UI glow.
265
+
266
+ The quad trick under *A camera effect covers the HUD too* does not help here.
267
+ It works by hosting a pass BELOW the HUD's z, and a tone map cannot live there:
268
+ it has to be the last thing before the blit to the 8-bit canvas, which is the
269
+ camera. Anything the camera draws goes through it.
270
+
271
+ Pick the multiplier by measuring against the no-headroom build, not by
272
+ inverting the curve. Inverting it over-estimates, because additive UI is inside
273
+ the BLOOM too and driving it harder also makes it spill wider into the same
274
+ pixels.
275
+
276
+ ### `hdrOutput`: actually presenting in HDR, and what it costs you
277
+
278
+ `hdr` gives the chain headroom and then throws it away at the very last step,
279
+ because the surface the browser presents into is 8 bits per channel. Values
280
+ above 1 clamp there, exactly as they did before the setting existed.
281
+ `hdrOutput` changes that surface, so they reach the compositor instead:
282
+
283
+ ```js
284
+ const app = new Application(1024, 576, { hdr: true, hdrOutput: true, … });
285
+ if (app.renderer.supportsHDROutput) { /* the display is getting values above 1 */ }
286
+
287
+ app.renderer.setHDROutput(false); // and at runtime, for a settings screen
288
+ ```
289
+
290
+ **It is WebGPU-only, and that is not a melonJS limitation.** The WebGPU
291
+ specification defines a tone mapping mode on the canvas configuration, and two
292
+ engines ship it. The WebGL2 equivalent needs a drawing-buffer tone mapping mode
293
+ that is still an unapproved specification change with no implementation in any
294
+ browser; the colour-space half of it exists in Chrome behind
295
+ `--enable-experimental-web-platform-features`, which is not something you can
296
+ ask a player to enable. So the WebGL2 backend reports `false`, warns once if
297
+ you asked, and presents in SDR as it always has.
298
+
299
+ That makes this the first setting where **the same machine answers differently
300
+ depending on which backend `video.AUTO` picked**. Branch on
301
+ `renderer.supportsHDROutput`, never on the setting.
302
+
303
+ **It requires `hdr`** — the last pass blits from the camera's target, so if
304
+ that target is 8 bits there is nothing above 1 left to present and the extra
305
+ cost would buy an identical picture. Turning `hdr` off turns this off with it.
306
+
307
+ **It changes how your game looks, on an HDR display.** Everything else here is
308
+ careful not to: `hdr` is additive, the tone curve defaults to none. This one
309
+ removes the final clamp, so anything you drove above 1 — an additive stack, an
310
+ emitter, a `brightness()` above 1 — gets genuinely brighter rather than
311
+ saturating at white. That is the point of it, and it is why it is a separate
312
+ opt-in rather than part of `hdr`. Two consequences worth planning for:
313
+
314
+ - a look built on clipped whites will not survive it unchanged, because the
315
+ clipping is what you were art-directing against
316
+ - a tone curve and `hdrOutput` pull in opposite directions. A curve exists to
317
+ fit values into `[0, 1]`; if you have removed the clamp, either drop the
318
+ curve or give it a white point high enough to leave the top of the range
319
+ alone
320
+ - **`"aces"` cannot produce HDR output.** Its expression clamps to `[0, 1]`
321
+ internally, so everything past the shoulder collapses to one value before
322
+ the white point scales it — different bright things come out equally bright.
323
+ Under `hdrOutput` use `"reinhard"` or `"exponential"`, which approach 1
324
+ asymptotically without clamping and so keep their ordering when the white
325
+ point lifts them past it, or use no curve at all
326
+
327
+ **What it is not.** It does not make an SDR display brighter, and there is no
328
+ way to ask melonJS how bright the panel is — `matchMedia("(dynamic-range:
329
+ high)")` is the closest the platform gets. Calibrating in nits, the way a
330
+ console settings screen does, is not reachable from the web platform today.
331
+
332
+ ### Exposure, and the runtime toggle
333
+
334
+ Under `hdr` the useful exposure is often **below** 1, which surprises people who
335
+ have only used the effect as an LDR grade. The curve has to work where it
336
+ separates: at exposure 1 the ACES shoulder maps 2, 4 and 8 to 0.86, 0.96 and
337
+ 0.99, so genuinely different energies all arrive at white and the headroom buys
338
+ nothing you can see. Lower exposure spreads them out. The right value is a
339
+ property of how bright your scene is authored, so measure it; there is no
340
+ default that is right for two different games.
341
+
342
+ A tone curve is a setting rather than an effect you wire up:
343
+
344
+ ```js
345
+ renderer.setToneMapping("aces", { exposure: 0.4, white: 6 });
346
+ renderer.setToneMapping("none"); // and off again
347
+ ```
348
+
349
+ `"none"` by default. The renderer keeps the curve LAST on the camera, so
350
+ whatever you put in front of it is mapped along with everything else. Changing
351
+ the mode rebuilds one shader (the curve is baked); `exposure` and `white` are
352
+ uniform writes, so a settings screen can drive them live. `white` is the value
353
+ that comes out as display white — the knob a "brightness" or "peak" slider
354
+ wants, since exposure alone drags the midtones up with the highlights.
355
+
356
+ `renderer.setHDR(enable)` moves the whole pipeline at runtime, the way
357
+ `setAntiAlias` does, for a settings toggle. It empties the render-target pool
358
+ when the format actually changes, so it is not something to drive per frame.
359
+ The boot header reports what happened either way:
360
+
361
+ ```
362
+ resolution: requested 1024x576 (HDR), got 1024x576 (SDR) ← the driver refused
363
+ ```
82
364
 
83
365
  For grading without writing a shader yourself, the camera has a colour matrix:
84
366
 
@@ -108,6 +390,23 @@ one `@group(3) @binding(0) var<uniform>` struct — so a single `setUniform` cal
108
390
  feeds whichever backend is live. `setTime` is a convenience for a `uTime`
109
391
  uniform and silently does nothing if the shader does not declare one.
110
392
 
393
+ **Never put a backtick in a shader body, including in its comments.** An inline
394
+ body is a JavaScript template literal, so a backtick ENDS it — and what follows
395
+ is parsed as code, which fails somewhere further down the file with an error
396
+ that says nothing about shaders:
397
+
398
+ ```js
399
+ const glsl = `
400
+ // x * x, not `pow(x, 2.0)`: the base goes negative here <-- ends the literal
401
+ vec4 apply(vec4 color, vec2 uv) { ... }
402
+ `;
403
+ ```
404
+
405
+ Markdown-quoting an identifier is a reflex when writing a comment, and this is
406
+ the one place it breaks the file. Write `pow(x, 2.0)` bare. A body loaded from
407
+ a `.glsl` / `.wgsl` file has no such problem, which is one more reason to move
408
+ a shader out once it stops being a sketch.
409
+
111
410
  ### One effect is one program — prefer preloading, and share
112
411
 
113
412
  A `ShaderEffect` compiles and links in its **constructor**, and the link is
@@ -201,28 +500,38 @@ Two places where a straight port really does differ:
201
500
  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the
202
501
  renderer feeds `-1` on that path and `+1` everywhere else.
203
502
 
204
- ## A ShaderEffect cannot shade a mesh on WebGPU
503
+ ## A ShaderEffect on a mesh is a colour hook, and drops some builtins
205
504
 
206
- The single-effect fast path swaps the renderable's program outright instead of
207
- rendering through an offscreen target. On WebGL that works for a `Mesh` too, so
208
- a custom mesh shader is a `ShaderEffect` away. **On WebGPU it does not** — the
209
- renderer says so in its own source, warns once, and then keeps the built-in
210
- mesh shading.
505
+ `mesh.addPostEffect(fx)` shades a `Mesh` on **both** backends since 20.5. The
506
+ body is spliced into the engine's own mesh shader as a colour hook, so the mesh
507
+ keeps its placement, alpha cutout, lighting and fog, and `setUniform` /
508
+ `setTime` drive it exactly as they do over a sprite. Uniform-only bodies are
509
+ the supported case and they are most of them.
211
510
 
212
- Nothing throws. The mesh simply draws with its plain texture, which looks like
213
- a broken shader, a bad binding, or a texture that failed to load — and since
214
- `video.AUTO` prefers WebGPU where it is available, this is the default outcome
215
- on most desktops. It cost several hours here: the symptom was a water surface
216
- rendering flat white, and alpha, bindings, transparency and lighting were all
217
- eliminated before the backend was.
511
+ Five things the quad tier gives an effect that the mesh tier does not, each
512
+ refused rather than half-working:
218
513
 
219
- 2D renderables are unaffected: `Sprite`, `Text` and the rest go through the
220
- quad path, which hosts effects on both backends. So a gradient fill for text,
221
- or a refraction over a sprite, is fine — it is specifically the mesh path.
222
-
223
- If you need a genuinely custom mesh shader that runs everywhere, supply a full
224
- dual-language `GLShader` rather than a fragment body, and declare the fog
225
- uniforms yourself (see `melonjs-3d`).
514
+ | refused | why |
515
+ |---|---|
516
+ | its own `setTexture` samplers | resolved by the quad tier's per-effect device state; refused on both backends |
517
+ | `screen_texture`, `screen_uv` | need the screen capture only the quad tier takes |
518
+ | `noise_uv` | derived from a quad's image and object size |
519
+ | `vColor`, on WGSL only | the WGSL mesh module has no module-scope equivalent; the GLSL one declares it, so GLSL is fine |
520
+ | `InstancedMesh` | the instanced WGSL builder drops everything between the two stages |
521
+
522
+ Hitting one warns **once** and falls back to the built-in shading. Nothing
523
+ throws, so the symptom is a mesh drawing with its plain texture, which reads as
524
+ a broken shader, a bad binding or a texture that failed to load. Check the
525
+ console for the warning before you start eliminating alpha, bindings and
526
+ transparency: it names which of the five it was.
527
+
528
+ 2D renderables never hit this. `Sprite`, `Text` and the rest go through the
529
+ quad path, which has the full feature set, so a gradient fill for text or a
530
+ refraction over a sprite is fine.
531
+
532
+ If you need one of the five on a mesh, supply a full dual-language `GLShader`
533
+ rather than a fragment body, and declare the fog uniforms yourself (see
534
+ `melonjs-3d`).
226
535
 
227
536
  ## Extra textures bind in setTexture order
228
537
 
@@ -294,13 +603,16 @@ same question after construction.
294
603
  | effect does nothing, warning in console | Canvas fallback — no programmable pipeline |
295
604
  | effect does nothing on some machines only | GLSL-only shader, `video.AUTO` chose WebGPU |
296
605
  | a white or solid box where the effect should be | carrier renderable drawn while the effect is disabled |
606
+ | ONE effect does nothing but two of them work | the renderable draws with primitives — set `postEffectNeedsCapture` |
297
607
  | effect cannot be re-enabled | `removePostEffect()` destroyed it — use `enabled` |
298
608
  | ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |
299
609
  | shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |
300
610
  | frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |
301
611
  | animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |
612
+ | syntax error in a file whose shader you just edited, pointing at a line of GLSL | a backtick in the shader body ended the template literal — most often one wrapped around an identifier in a comment |
302
613
 
303
614
  ## Related skills
615
+ - `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this
304
616
 
305
617
  - `melonjs-renderables` — where post effects attach, and the destroy trap
306
618
  - `melonjs-3d` — the GPU-backend requirement, and why a custom mesh shader is
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-input
3
- description: "Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, click, touch, drag."
3
+ description: "Use this skill for keyboard, pointer, mouse, touch and gamepad input in melonJS. Covers the bindKey action indirection, registerPointerEvent on regions, the isKinematic requirement that silently blocks pointer events, which of several overlapping objects receives an event and how to consume it, world vs screen pointer coordinates, and gamepad mapping onto the same actions. Triggers on: input, bindKey, isKeyPressed, KEY, registerPointerEvent, releasePointerEvent, pointerdown, pointerup, pointermove, POINTERMOVE, gamepad, bindGamepad, triggerKeyEvent, click, touch, drag, hit test, event propagation, stop propagating, overlapping, which object gets the click."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -64,9 +64,11 @@ subclasses with no body are the ones that catch people.
64
64
  Register in `onActivateEvent` and release in `onDeactivateEvent`, not in the
65
65
  constructor — pooled objects are reused and would otherwise accumulate handlers.
66
66
 
67
- `app.viewport` works as a whole-screen region when you want global clicks: the
68
- dispatcher appends it to the candidate list on every event, so it is reachable
69
- even when nothing else is under the pointer.
67
+ `app.viewport` works as a whole-screen region when you want global clicks. Note
68
+ it is asked **before** everything else, not as a fallback after nothing matched:
69
+ the dispatcher appends it to the candidate list and walks that list from the
70
+ end. So a viewport handler must not return `false` unless it really means to
71
+ swallow every pointer event in the game.
70
72
 
71
73
  The accepted event names are exactly `"pointerdown"`, `"pointerup"`,
72
74
  `"pointermove"`, `"pointercancel"`, `"pointerenter"`, `"pointerover"`,
@@ -76,6 +78,44 @@ throw `invalid event type`. melonJS maps the canonical name onto whichever
76
78
  mouse/touch events the device actually supports, so you always register the
77
79
  pointer name.
78
80
 
81
+ ## Which object gets the event
82
+
83
+ The dispatcher collects the regions near the pointer from the broadphase,
84
+ bounds-checks each one, orders them **the way they are drawn** and asks the
85
+ topmost first, walking down until something consumes.
86
+
87
+ A handler consumes by returning `false`. This is the DOM sense and it reads
88
+ backwards: `false` means handled, and anything else, `undefined` included, lets
89
+ the walk carry on to whatever is underneath.
90
+
91
+ ```js
92
+ input.registerPointerEvent("pointerdown", this, () => {
93
+ this.fire();
94
+ return false; // nothing under this gets the click
95
+ });
96
+ ```
97
+
98
+ Three things decide the order, and only the first is obvious:
99
+
100
+ - **`pos.z`, among siblings of one container.** Higher is on top.
101
+ - **Across containers, the common ancestor decides.** `pos.z` is
102
+ container-local, since `autoDepth` numbers each container's children from 1,
103
+ so a button at `z = 8` inside one panel does not outrank a second panel at
104
+ `z = 2`. The pair is resolved by walking both up to their shared parent and
105
+ comparing the two siblings there, which is exactly how drawing resolves it.
106
+ Raise a whole panel with `ancestor.moveToTop(panel)`, not by inflating a
107
+ child's `z`.
108
+ - **A child is asked before the container holding it**, since it draws over it.
109
+
110
+ Equal `z` between siblings falls back to child order, lower index on top. Two
111
+ callbacks registered on the **same** region for the same event run
112
+ last-registered-first.
113
+
114
+ Consuming a **move** also takes the pointer away from the regions below: each
115
+ one still holding it gets its `pointerleave`, because it is covered now and
116
+ being inside its own bounds no longer decides. Consuming a press, a release or a
117
+ wheel does not, since none of those says where the pointer is.
118
+
79
119
  ## World coordinates versus screen coordinates
80
120
 
81
121
  The pointer carries several, and picking the wrong one produces a feedback loop:
@@ -159,6 +199,10 @@ input.triggerKeyEvent(input.KEY.LEFT, false); // release
159
199
  | jump fires every frame while held | `bindKey` without its third `lock` argument |
160
200
  | `invalid event type` thrown at registration | non-pointer name (`"click"`, `"mousedown"`, …) passed to `registerPointerEvent` |
161
201
  | `no action defined for keycode N` | `bindGamepad` called before `bindKey` for that keycode |
202
+ | a widget under an overlapping panel still reacts | the panel consumes nothing — return `false` from its `onClick` and `onOver` |
203
+ | a widget stays highlighted after the pointer moves onto a panel over it | the panel consumes no `pointermove`, so nothing takes the pointer off the widget |
204
+ | a child's big `z` does not put it in front of another container | `pos.z` is container-local — use `ancestor.moveToTop()` on the container |
205
+ | every click in the game stops working | a handler on `app.viewport` returning `false`, which is asked first |
162
206
 
163
207
  ## Related skills
164
208
 
@@ -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, 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, Trail, explosion, smoke, sparks, 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: 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,101 @@ 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
+ ## Where particles are born
32
+
33
+ `width` and `height` are the spawn AREA, and both default to `1` — so by
34
+ default every particle launches from the same pixel. That is fine for a
35
+ fountain and wrong for an explosion: additive sprites piled on one point clip
36
+ all three channels there however well they are tinted, so a burst reads as one
37
+ bright flash rather than as debris.
38
+
39
+ Give it the size of whatever broke. The area is measured from the emitter's own
40
+ position, NOT centred on it, so offset by half or the whole burst sits down and
41
+ to the right of its source:
42
+
43
+ ```js
44
+ const area = radius * 2;
45
+ const emitter = new ParticleEmitter(x - area / 2, y - area / 2, {
46
+ width: area, height: area,
47
+ });
48
+ ```
49
+
50
+ ## Particles in 3D
51
+
52
+ By default a particle keeps the depth it was born at for its whole life: it
53
+ moves across the emitter's plane and never toward or away from the camera.
54
+ Under a `Camera3d` that reads as a flat sticker anywhere but dead ahead.
55
+
56
+ `elevation` lifts the launch out of that plane and `elevationVariation` spreads
57
+ it. `angle` is the azimuth within the plane, as it always was:
58
+
59
+ ```js
60
+ const emitter = new ParticleEmitter(x, y, {
61
+ angle: 0, angleVariation: Math.PI * 2,
62
+ elevation: 0, elevationVariation: Math.PI / 2, // a sphere of debris
63
+ speed: 9,
64
+ });
65
+ world.addChild(emitter, z); // the depth slice the burst belongs to
66
+ emitter.burstParticles();
67
+ ```
68
+
69
+ `speed` is the length of the whole 3D vector, so an "all directions" burst
70
+ covers the sphere evenly instead of bunching toward the poles.
71
+
72
+ Both default to `0`, and that is a gate rather than the 3D path with zeros in
73
+ it: an emitter that asks for no elevation runs the same two trig calls it
74
+ always did, writes no depth and starts no sort.
75
+
76
+ **The launch is in WORLD axes.** `vel.x` and `vel.y` add to the particle's
77
+ `pos.x`/`pos.y` and `velZ` adds to its depth, so `angle` and `elevation`
78
+ describe a direction in the world, not on the screen. Building them from a
79
+ camera basis works only while the camera's axes ARE the world axes, which stops
80
+ being true the moment you pose a `Camera3d`. For a direction `d` already in
81
+ world terms the pair is `angle: Math.atan2(-d.y, d.x)`,
82
+ `elevation: Math.asin(d.z)`.
83
+
84
+ ### Cones, rings and spheres: aiming around an axis
85
+
86
+ `angleVariation` and `elevationVariation` are sampled INDEPENDENTLY, spreading
87
+ particles over a rectangle of azimuth by elevation. That cannot describe a
88
+ shape defined against a direction. A ring of debris leaving a point on a sphere
89
+ is the set of directions at ninety degrees to that point's normal, and on a
90
+ rectangle the elevation that satisfies it is a function of the azimuth, so no
91
+ pair of variations gets there.
92
+
93
+ Set `maxSpread` and `angle`/`elevation` become an AXIS instead. Each particle
94
+ leaves at a polar angle between `minSpread` and `maxSpread` off that axis,
95
+ around a uniformly random azimuth:
96
+
97
+ | `minSpread` … `maxSpread` | shape |
98
+ |---|---|
99
+ | `0` … `0.5` | a tight cone along the axis |
100
+ | `0` … `Math.PI` | the whole sphere |
101
+ | either value at `Math.PI / 2` | a flat disc perpendicular to the axis |
102
+
103
+ An explosion on a planet's surface is a ring tangent to it: set the axis to the
104
+ surface normal and use `minSpread: Math.PI / 2 - 0.3, maxSpread: Math.PI / 2`.
105
+ Keeping the band at or inside ninety degrees is what stops debris tilting back
106
+ into the body it came off. `angleVariation` and `elevationVariation` are unused
107
+ while `maxSpread` is set.
108
+
109
+ ### Sorting, and why it is keyed off the blend mode
110
+
111
+ A 3D burst only needs depth sorting when its blend is order dependent, and the
112
+ emitter decides that for you. Additive blending is commutative, so an additive
113
+ burst looks the same however it is ordered and pays nothing; `"normal"` is not,
114
+ so a 3D burst drawn with it is sorted back to front.
115
+
116
+ The two differ in more than cost. Additive sprites overlapping near the centre
117
+ of a burst sum to white however they are tinted, so debris that has to keep its
118
+ colour wants `textureAdditive: false` and accepts the sort. A common shape is
119
+ two emitters: a solid, sorted body of debris with a sparser additive spray of
120
+ sparks over it.
121
+
122
+ The default particle sprite is a filled SQUARE, which additive blending hides
123
+ and normal blending does not. Pass your own soft `image` for non-additive
124
+ debris.
125
+
31
126
  For one-shot effects, `autoDestroyOnComplete: true` removes the emitter once its
32
127
  particles have finished. The `onComplete` callback fires at that same moment
33
128
  either way.
@@ -95,9 +190,10 @@ draws a flat ribbon that does not recede with perspective — so it is the wrong
95
190
  tool for a trail behind an object moving *into* the scene. For that:
96
191
 
97
192
  - **`ParticleEmitter` with `referenceSpace: "world"`** — particles stay where
98
- they were dropped while the emitter moves on, and the emitter propagates its
99
- depth to each particle so `Camera3d` projects them correctly. This is the
100
- built-in answer for exhaust, ski tracks and speed trails in 3D.
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.
101
197
  - **A procedural `Mesh`** built from the object's recent path, with
102
198
  `mesh.vertexColors` for a per-vertex gradient — more work, but a continuous
103
199
  ribbon rather than discrete puffs.
@@ -129,6 +225,12 @@ tool for a trail behind an object moving *into* the scene. For that:
129
225
  | particles do not travel far enough | settings tuned pre-20.2, when drawn travel was doubled |
130
226
  | one-shot emitters accumulate | missing `autoDestroyOnComplete: true` |
131
227
  | frame rate drops with many effects | an emitter per event instead of one re-aimed emitter |
228
+ | a 3D burst is invisible under a `Camera3d` | before 20.8 a particle was culled at twice its depth; update |
229
+ | the whole burst launches from one pixel | `width`/`height` are the spawn area and default to `1` |
230
+ | the burst sits down-right of its source | the spawn area starts at the emitter's position; offset by half |
231
+ | the middle of a burst goes white | additive sprites overlapping — `textureAdditive: false`, or spread them |
232
+ | debris chunks look like squares | the default sprite is a filled square; pass your own `image` |
233
+ | a ring tilts with the camera instead of staying on a surface | the launch axis is in WORLD axes, not the camera basis |
132
234
 
133
235
  ## Related skills
134
236