melonjs 20.8.0 → 20.10.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 (206) hide show
  1. package/README.md +1 -0
  2. package/build/application/application.d.ts +66 -8
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/defaultApplicationSettings.d.ts +1 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/resize.d.ts +25 -0
  7. package/build/application/resize.d.ts.map +1 -1
  8. package/build/application/scaleMethods.d.ts +13 -3
  9. package/build/application/scaleMethods.d.ts.map +1 -1
  10. package/build/application/settings.d.ts +68 -13
  11. package/build/application/settings.d.ts.map +1 -1
  12. package/build/audio/audio.d.ts +29 -5
  13. package/build/audio/audio.d.ts.map +1 -1
  14. package/build/audio/autopause.d.ts +36 -0
  15. package/build/audio/autopause.d.ts.map +1 -0
  16. package/build/audio/backend/engine.d.ts +10 -0
  17. package/build/audio/backend/engine.d.ts.map +1 -1
  18. package/build/audio/backend/sound.d.ts.map +1 -1
  19. package/build/audio/backend/spatial.d.ts.map +1 -1
  20. package/build/audio/context.d.ts +35 -0
  21. package/build/audio/context.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts.map +1 -1
  23. package/build/audio/procedural.d.ts.map +1 -1
  24. package/build/audio/spatial.d.ts +87 -0
  25. package/build/audio/spatial.d.ts.map +1 -0
  26. package/build/audio/state.d.ts +1 -0
  27. package/build/audio/state.d.ts.map +1 -1
  28. package/build/audio/types.d.ts +46 -6
  29. package/build/audio/types.d.ts.map +1 -1
  30. package/build/camera/camera2d.d.ts.map +1 -1
  31. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  32. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  33. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  34. package/build/geometries/box3d.d.ts +1 -1
  35. package/build/geometries/box3d.d.ts.map +1 -1
  36. package/build/geometries/ellipse.d.ts +1 -1
  37. package/build/geometries/ellipse.d.ts.map +1 -1
  38. package/build/geometries/line.d.ts +1 -1
  39. package/build/geometries/line.d.ts.map +1 -1
  40. package/build/geometries/point.d.ts +1 -1
  41. package/build/geometries/point.d.ts.map +1 -1
  42. package/build/geometries/polygon.d.ts +1 -1
  43. package/build/geometries/polygon.d.ts.map +1 -1
  44. package/build/geometries/rectangle.d.ts +1 -1
  45. package/build/geometries/rectangle.d.ts.map +1 -1
  46. package/build/geometries/roundrect.d.ts +1 -1
  47. package/build/geometries/roundrect.d.ts.map +1 -1
  48. package/build/geometries/sphere.d.ts +1 -1
  49. package/build/geometries/sphere.d.ts.map +1 -1
  50. package/build/index.d.ts +9 -2
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +38737 -35039
  53. package/build/index.js.map +4 -4
  54. package/build/input/gamepad.d.ts +129 -3
  55. package/build/input/gamepad.d.ts.map +1 -1
  56. package/build/level/gltf/GLTFModel.d.ts +5 -4
  57. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  58. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  59. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  60. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  61. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  62. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -4
  63. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  64. package/build/loader/cache.d.ts +220 -0
  65. package/build/loader/cache.d.ts.map +1 -1
  66. package/build/loader/loader.d.ts +20 -349
  67. package/build/loader/loader.d.ts.map +1 -1
  68. package/build/loader/parsers/gltf.d.ts +115 -0
  69. package/build/loader/parsers/gltf.d.ts.map +1 -1
  70. package/build/math/color.d.ts +1 -1
  71. package/build/math/color.d.ts.map +1 -1
  72. package/build/math/matrix2d.d.ts +1 -1
  73. package/build/math/matrix2d.d.ts.map +1 -1
  74. package/build/math/matrix3d.d.ts +1 -1
  75. package/build/math/matrix3d.d.ts.map +1 -1
  76. package/build/math/observableVector2d.d.ts +14 -2
  77. package/build/math/observableVector2d.d.ts.map +1 -1
  78. package/build/math/observableVector3d.d.ts +15 -1
  79. package/build/math/observableVector3d.d.ts.map +1 -1
  80. package/build/math/vector2d.d.ts +31 -1
  81. package/build/math/vector2d.d.ts.map +1 -1
  82. package/build/math/vector3d.d.ts +38 -1
  83. package/build/math/vector3d.d.ts.map +1 -1
  84. package/build/math/vertex.d.ts.map +1 -1
  85. package/build/particles/emitter.d.ts.map +1 -1
  86. package/build/particles/particle.d.ts +7 -1
  87. package/build/particles/particle.d.ts.map +1 -1
  88. package/build/particles/settings.d.ts +35 -0
  89. package/build/particles/settings.d.ts.map +1 -1
  90. package/build/physics/bounds.d.ts +1 -1
  91. package/build/physics/bounds.d.ts.map +1 -1
  92. package/build/physics/broadphase/octree.d.ts +24 -0
  93. package/build/physics/broadphase/octree.d.ts.map +1 -1
  94. package/build/physics/broadphase/quadtree.d.ts +31 -0
  95. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  96. package/build/physics/builtin/body.d.ts +14 -0
  97. package/build/physics/builtin/body.d.ts.map +1 -1
  98. package/build/pool.d.ts +4 -0
  99. package/build/pool.d.ts.map +1 -1
  100. package/build/renderable/colorlayer.d.ts +13 -0
  101. package/build/renderable/colorlayer.d.ts.map +1 -1
  102. package/build/renderable/container.d.ts.map +1 -1
  103. package/build/renderable/groundshadow.d.ts.map +1 -1
  104. package/build/renderable/instanced_mesh.d.ts +56 -0
  105. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  106. package/build/renderable/mesh.d.ts +97 -11
  107. package/build/renderable/mesh.d.ts.map +1 -1
  108. package/build/renderable/renderable.d.ts +47 -3
  109. package/build/renderable/renderable.d.ts.map +1 -1
  110. package/build/renderable/sprite.d.ts +2 -1
  111. package/build/renderable/sprite.d.ts.map +1 -1
  112. package/build/renderable/sprite3d.d.ts +6 -2
  113. package/build/renderable/sprite3d.d.ts.map +1 -1
  114. package/build/renderable/text/bitmaptext.d.ts +63 -2
  115. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  116. package/build/renderable/text/bitmaptextdata.d.ts +1 -1
  117. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  118. package/build/renderable/text/glypheffect.d.ts +44 -0
  119. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  120. package/build/renderable/text/text.d.ts +17 -0
  121. package/build/renderable/text/text.d.ts.map +1 -1
  122. package/build/renderable/trail.d.ts.map +1 -1
  123. package/build/renderable/trail3d.d.ts +174 -0
  124. package/build/renderable/trail3d.d.ts.map +1 -0
  125. package/build/renderable/trigger.d.ts +2 -1
  126. package/build/renderable/trigger.d.ts.map +1 -1
  127. package/build/renderable/ui/panelpaint.d.ts +5 -0
  128. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  129. package/build/renderable/ui/progressbar.d.ts +2 -4
  130. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  131. package/build/renderable/ui/uipanel.d.ts +245 -0
  132. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  133. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  134. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  135. package/build/state/state.d.ts.map +1 -1
  136. package/build/system/bootstrap.d.ts.map +1 -1
  137. package/build/system/legacy_pool.d.ts +22 -2
  138. package/build/system/legacy_pool.d.ts.map +1 -1
  139. package/build/system/pool.d.ts +5 -8
  140. package/build/system/pool.d.ts.map +1 -1
  141. package/build/tweens/tween.d.ts +1 -1
  142. package/build/tweens/tween.d.ts.map +1 -1
  143. package/build/video/effects/shadereffect.d.ts.map +1 -1
  144. package/build/video/effects/squish.d.ts +74 -0
  145. package/build/video/effects/squish.d.ts.map +1 -0
  146. package/build/video/effects/tintPulse.d.ts +0 -5
  147. package/build/video/effects/tintPulse.d.ts.map +1 -1
  148. package/build/video/effects/wave.d.ts +0 -5
  149. package/build/video/effects/wave.d.ts.map +1 -1
  150. package/build/video/gpu/batcher.d.ts +8 -2
  151. package/build/video/gpu/batcher.d.ts.map +1 -1
  152. package/build/video/gradient.d.ts +1 -0
  153. package/build/video/gradient.d.ts.map +1 -1
  154. package/build/video/renderer.d.ts +16 -3
  155. package/build/video/renderer.d.ts.map +1 -1
  156. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  157. package/build/video/rendertarget/render_target_pool.d.ts.map +1 -1
  158. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  159. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  160. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  161. package/build/video/rendertarget/webgpurendertarget.d.ts +6 -12
  162. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  163. package/build/video/texture/atlas.d.ts +1 -1
  164. package/build/video/texture/atlas.d.ts.map +1 -1
  165. package/build/video/texture/texture2d.d.ts +6 -0
  166. package/build/video/texture/texture2d.d.ts.map +1 -1
  167. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -11
  168. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  169. package/build/video/webgl/batchers/material_batcher.d.ts +5 -0
  170. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  171. package/build/video/webgl/batchers/mesh_batcher.d.ts +21 -0
  172. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  173. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  174. package/build/video/webgl/buffer/deletable.d.ts +15 -0
  175. package/build/video/webgl/buffer/deletable.d.ts.map +1 -0
  176. package/build/video/webgl/buffer/instance_buffer.d.ts.map +1 -1
  177. package/build/video/webgl/buffer/retained_geometry.d.ts.map +1 -1
  178. package/build/video/webgl/buffer/uniformblock.d.ts.map +1 -1
  179. package/build/video/webgl/buffer/vertexstate.d.ts.map +1 -1
  180. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  181. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  182. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  183. package/build/video/webgpu/texture/compressed.d.ts.map +1 -1
  184. package/build/video/webgpu/texture/store.d.ts.map +1 -1
  185. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/skills/AGENTS.md +1 -1
  188. package/skills/melonjs/SKILL.md +2 -0
  189. package/skills/melonjs-3d/SKILL.md +192 -18
  190. package/skills/melonjs-3d-assets/SKILL.md +36 -2
  191. package/skills/melonjs-audio/SKILL.md +229 -8
  192. package/skills/melonjs-camera-and-drawing/SKILL.md +73 -3
  193. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  194. package/skills/melonjs-effects-and-shaders/SKILL.md +54 -10
  195. package/skills/melonjs-events/SKILL.md +8 -6
  196. package/skills/melonjs-input/SKILL.md +100 -4
  197. package/skills/melonjs-loading-assets/SKILL.md +22 -4
  198. package/skills/melonjs-math/SKILL.md +313 -0
  199. package/skills/melonjs-particles-and-trails/SKILL.md +88 -14
  200. package/skills/melonjs-performance/SKILL.md +57 -22
  201. package/skills/melonjs-physics/SKILL.md +2 -1
  202. package/skills/melonjs-renderables/SKILL.md +6 -1
  203. package/skills/melonjs-scenes-and-state/SKILL.md +14 -3
  204. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  205. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  206. package/skills/melonjs-ui-and-text/SKILL.md +382 -4
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-audio
3
- description: "Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, and procedural tone/noise generation. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, sprite audio, stereo, panner, audio.tone, audio.noise, getAudioContext, autoplay."
3
+ description: "Use this skill for sound and music in melonJS — loading audio, playing effects, background tracks, audio sprites, volume and muting, spatial audio, procedural tone/noise generation, and effects such as reverb or a compressor on the whole mix. Covers the mandatory audio.init format list, the directory-not-file src convention, playTrack versus play, the fixed listener and pixel-scale distances of positional sound, and browser autoplay unlocking. Triggers on: audio, sound, audio.init, audio.play, playTrack, stopTrack, audio.load, sfx, music, volume, mute, fade, rate, pitch, sprite audio, stereo, panner, position, listener, refDistance, reverb, echo, delay, compressor, distortion, filter, audio effect, audio.tone, audio.noise, getAudioContext, getMasterGain, hasAudio, hasFormat, autoplay, no sound, sound stopped working, context suspended, silent after idle, clips silent but tones play."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -29,6 +29,10 @@ await loader.preload([
29
29
  gives you a 404. A `data:audio/...` URL is the one exception: it is used
30
30
  verbatim, prefix and extension skipped.
31
31
 
32
+ `audio.hasAudio()` says whether the browser can play audio at all, and
33
+ `audio.hasFormat("ogg")` whether it can play one codec — useful for choosing
34
+ the `init` list or hiding a sound toggle on a device with no audio.
35
+
32
36
  Order matters — melonJS tries the listed formats left to right, so put the
33
37
  preferred one first. Two formats (`"webm,mp3"` or `"mp3,ogg"`) is the usual
34
38
  belt-and-braces for codec coverage. Accepted tokens: `mp3`, `mpeg`, `opus`,
@@ -51,7 +55,33 @@ loops, and it records the name as the current track — which is what
51
55
  `app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via
52
56
  `state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,
53
57
  `resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there
54
- is only ever one current track. Stop it in `onDestroyEvent`.
58
+ is only ever one current *track name*.
59
+
60
+ **`playTrack` stops the track it replaces** (20.9.0 and later; before that it
61
+ only assigned the slot, and the previous bed kept looping underneath where
62
+ `stopTrack()` could no longer reach it). So a scene just states its own music
63
+ and nothing cancels anything:
64
+
65
+ ```js
66
+ class PlayScreen extends Stage {
67
+ onResetEvent() { audio.playTrack("battle", 0.6); }
68
+ }
69
+ ```
70
+
71
+ Calling it with the track already playing **restarts** it, the same as `play()`
72
+ on any other clip. A scene that wants the bed to carry on across a screen
73
+ change should not call it again, rather than rely on it doing nothing.
74
+
75
+ `stopTrack()` is for where nothing follows: a scene whose successor has no
76
+ music of its own, or a game going quiet. `state.change` destroys the outgoing
77
+ stage **before** it resets the incoming one, so a `stopTrack()` in
78
+ `onDestroyEvent` and a `playTrack()` in the next `onResetEvent` are in that
79
+ order and never cancel each other.
80
+
81
+ To end a track on an event rather than on a screen change, say a run ending
82
+ while the player stays on the same screen, `audio.fade(name, from, 0, ms)`
83
+ takes it down gently. A fade to zero is silence, not a stop, so the voice is
84
+ still live until something stops it or replaces it.
55
85
 
56
86
  Every per-clip function throws `audio clip <name> does not exist` if the clip was
57
87
  never loaded, and `play` additionally throws when a named sprite region is
@@ -72,8 +102,9 @@ audio.play("sfx", { sprite: "jump" });
72
102
 
73
103
  The optional third element marks a region as looping. `play()`'s second argument
74
104
  takes either the original `loop` boolean or a `PlayOptions` object
75
- (`sprite`, `loop`, `onend`, `volume`) — both forms work. Object fields win over
76
- the positional `onend` / `volume` arguments when both are supplied.
105
+ (`sprite`, `loop`, `onend`, `volume`, plus the spatial `follow`, `at` and
106
+ `stopWithTarget`) — both forms work. Object fields win over the positional
107
+ `onend` / `volume` arguments when both are supplied.
77
108
 
78
109
  ## Clip state
79
110
 
@@ -88,7 +119,9 @@ audio.state("theme"); // "unloaded" | "loading" | "loaded"
88
119
  `stop()`, `pause()` and `resume()` take an optional instance `id` too; omit it
89
120
  and the whole group is affected. `audio.stop()` with no arguments at all stops
90
121
  every sound. `audio.seek(name)` reads the position, `audio.seek(name, s, id?)`
91
- writes it; `audio.rate` and `audio.fade(name, from, to, ms, id?)` round it out.
122
+ writes it; `audio.rate(name, r, id?)` changes speed and pitch together
123
+ (`0.5`..`4.0`, `1` is normal) and `audio.fade(name, from, to, ms, id?)` ramps
124
+ the volume (`0`..`1`, over milliseconds).
92
125
 
93
126
  ## Volume and muting
94
127
 
@@ -105,6 +138,14 @@ audio.muted(); // global mute state
105
138
  `audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`
106
139
  also clears the current-track pointer if it named that clip.
107
140
 
141
+ ### Losing focus mutes everything
142
+
143
+ When the window loses focus the engine mutes all audio, and unmutes it on
144
+ focus. That covers sound effects, procedural `tone` / `noise` and the mixer
145
+ effects, not just the music track that `state.pause()` pauses. Set
146
+ `pauseOnBlur: false` in the application settings if a clip has to keep playing
147
+ in the background.
148
+
108
149
  ## Spatial audio
109
150
 
110
151
  ```js
@@ -120,10 +161,113 @@ Called with just the clip name, each of the four returns the current value —
120
161
  `stereo` gives `0` and `position` gives `[0, 0, 0]` before they have ever been
121
162
  set.
122
163
 
164
+ ### Let the engine place the sound: `follow` and `at`
165
+
166
+ The shortest path to positional audio is a play option. Both take **world
167
+ pixels**, the same numbers as `pos.x` / `pos.y`, and the engine converts to
168
+ audio space for you:
169
+
170
+ ```js
171
+ // tracks the renderable every frame, and stops when it is destroyed
172
+ audio.play("ufo", { follow: this, stopWithTarget: true });
173
+
174
+ // pinned to a fixed world point
175
+ audio.play("portal", { at: { x: 1200, y: 480 } });
176
+ ```
177
+
178
+ `follow` and `at` are mutually exclusive: passing both throws, rather than
179
+ quietly picking one. `follow` re-reads the target's **absolute** position each
180
+ frame, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you
181
+ want for a sound that belongs to an entity: without it a looping clip on a
182
+ destroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`
183
+ detaches a sound and leaves it playing where it is.
184
+
185
+ ### Move the listener, don't do the subtraction yourself
186
+
187
+ ```js
188
+ audio.setListener(player); // tracked every frame
189
+ audio.listener(x, y, z); // or place it by hand
190
+ audio.listener(); // → [x, y, z] in world pixels
191
+ audio.listenerOrientation(fx, fy, fz, ux, uy, uz);
192
+ audio.setListener(null); // stop tracking
193
+ ```
194
+
195
+ With a listener in the world, source positions are **absolute world
196
+ coordinates**, not offsets. The older idiom — passing `source.pos.x -
197
+ player.pos.x` every frame with the ear stuck at the origin — still works, but
198
+ only if you leave the listener at `(0, 0, 0)`; mixing the two double-counts the
199
+ player's position.
200
+
201
+ `setListener` places the ear at the target's absolute position after the world
202
+ updates. A plain renderable contributes position only, so a side-scroller keeps
203
+ the default "facing into the screen" and gets left-right panning plus distance
204
+ falloff from the one call.
205
+
206
+ A `Camera3d` target also contributes its **orientation**, taken from its own
207
+ basis, so turning the camera swings the stereo image with it. That is the whole
208
+ of what a posed 3D scene needs:
209
+
210
+ ```js
211
+ audio.setListener(app.viewport); // in a 3D scene this is a Camera3d
212
+ audio.play("drone", { follow: saucer }); // scene coordinates, directly
213
+ ```
214
+
215
+ **Y is down.** Every one of these calls takes and returns melonJS world
216
+ coordinates, where y grows downward; the Y-up flip Web Audio wants happens
217
+ inside the engine. Pass `pos.y` as it is.
218
+
219
+ ### The defaults are tuned for pixels
220
+
221
+ `audio.setSpatialDefaults()` / `getSpatialDefaults()` set the panner attributes
222
+ a sound placed through `follow` or `at` starts from, and the shipped values
223
+ assume a 2D game measured in pixels:
224
+
225
+ | attribute | default |
226
+ |---|---|
227
+ | `refDistance` | `240` |
228
+ | `maxDistance` | `10000` |
229
+ | `panningModel` | `"equalpower"` |
230
+ | `distanceModel` | `"inverse"` |
231
+
232
+ With those, a placed sound is at full volume out to 240 px and then halves
233
+ each time the distance doubles: about half at 480, a quarter at 960, roughly
234
+ an eighth at 2000. `maxDistance` is where it stops getting quieter.
235
+
236
+ **They apply to placed sounds only.** A plain `audio.play()` followed by a
237
+ bare `audio.position()` keeps WebAudio's own defaults, where `refDistance` is
238
+ `1` METRE and a source 100 px out is already near silent. That path is
239
+ unchanged, which is what keeps old code working.
240
+
241
+ Widen or tighten the field once, globally:
242
+
243
+ ```js
244
+ audio.setSpatialDefaults({ refDistance: 400, maxDistance: 20000 });
245
+ ```
246
+
247
+ `audio.panner(name, attrs, id?)` still overrides per clip or per voice, and it
248
+ merges over the current values rather than replacing them, so writing one field
249
+ no longer resets the rest. Called without an `id` it writes the **group**
250
+ defaults, which is what a clip-wide tuning call looks like.
251
+
252
+ For a 3D scene where front-to-back matters, `panningModel: "HRTF"` is worth the
253
+ cost; for a 2D one it mostly muddies the stereo image.
254
+
255
+ ### `stereo` and `position`: last writer wins
256
+
257
+ Both drive the same per-instance panner node, and the engine swaps the node to
258
+ match whichever you called **last**: `stereo()` after `position()` gives you
259
+ plain left-right panning, and `position()` after `stereo()` gives you 3D
260
+ falloff. Neither silently does nothing any more, and the order you call them in
261
+ no longer matters.
262
+
263
+ Still pick one per voice and stay with it. A 2D game usually wants `stereo()`
264
+ alone; reach for `position()` (or `follow` / `at`) when distance falloff
265
+ matters.
266
+
123
267
  **Spatial audio is WebAudio-only.** On a clip loaded with `stream: true` (or
124
268
  `html5: true`) `stereo` / `position` / `orientation` return early and do
125
269
  nothing — which is exactly the case for the long music tracks people reach for
126
- first.
270
+ first. `follow` and `at` are equally inert on a streamed clip.
127
271
 
128
272
  ## Procedural sound
129
273
 
@@ -167,6 +311,42 @@ custom Web Audio work; both return `null` when audio is unavailable, so guard.
167
311
  Connect a custom graph to the master gain rather than `ctx.destination` if you
168
312
  want it to respect `setVolume` / `muteAll`.
169
313
 
314
+ ## Effects on all game audio: reverb, echo, compressor
315
+
316
+ There is no built-in reverb, delay, distortion or compressor, and no per-clip
317
+ filter on file playback. There does not need to be one for the common case:
318
+ every buffered clip and every `tone` / `noise` passes through the master gain
319
+ on its way to the speakers, so an effect inserted **after** it applies to the
320
+ whole mix.
321
+
322
+ ```js
323
+ const ctx = audio.getAudioContext(); // creates the context if needed
324
+ const master = audio.getMasterGain();
325
+ if (ctx && master) { // null when audio is unavailable
326
+ const comp = ctx.createDynamicsCompressor();
327
+ master.disconnect(); // master -> speakers becomes
328
+ master.connect(comp); // master -> compressor -> speakers
329
+ comp.connect(ctx.destination);
330
+ }
331
+ ```
332
+
333
+ Any Web Audio node or chain works the same way: a `ConvolverNode` with an
334
+ impulse response for reverb, a `DelayNode` with a feedback gain for echo, a
335
+ `BiquadFilterNode` for an underwater low-pass, a `WaveShaperNode` for
336
+ distortion. Do it once, at startup; the master gain lives as long as the
337
+ context.
338
+
339
+ - **`setVolume` / `muteAll` still work**, because they act on the master gain,
340
+ which is now upstream of the effect.
341
+ - **Streamed clips are not affected.** A `stream: true` (`html5: true`) clip
342
+ plays through an `<audio>` element and never reaches the master gain — which
343
+ is usually the music. Preload the music buffered if it must go through the
344
+ effect.
345
+ - **It is all or nothing.** The per-instance nodes are not public, so an effect
346
+ on one clip only (muffling one sound behind a wall) is not possible through
347
+ this route; `noise` has its own `filter` for generated sounds.
348
+ - To remove it, reverse the wiring: `master.disconnect(); master.connect(ctx.destination);`.
349
+
170
350
  ## Autoplay: sound is silent until the first gesture
171
351
 
172
352
  Browsers create the audio context in the `suspended` state and keep it there
@@ -174,8 +354,36 @@ until a user interaction. Nothing errors — audio simply does not play, which
174
354
  reads as a bug. melonJS installs one-shot `touchstart` / `touchend` / `click` /
175
355
  `keydown` unlock listeners on `document`, so the first real gesture anywhere
176
356
  unlocks it and fires the clip's `on.unlock` callback. Design for it: start music
177
- on the first click or key press, not on load. `tone` / `noise` additionally call
178
- `ctx.resume()` themselves, best-effort, on every invocation.
357
+ on the first click or key press, not on load. The usual way to guarantee one is
358
+ a gate screen — a splash, a "press any key", or a health/photosensitivity notice
359
+ — whose dismissal *is* the gesture, so the menu behind it can start its music
360
+ unconditionally. Note the unlock listens for `keydown` too, so a keyboard-only
361
+ player is covered.
362
+
363
+ ### The context can stop again, long after it was unlocked
364
+
365
+ Unlocking is only the first half. A context that has been running can be
366
+ stopped later: melonJS suspends it itself after thirty idle seconds to save
367
+ power, and browsers stop it for their own reasons, a backgrounded tab among
368
+ them. Recovery is automatic and you do not have to write it: a user gesture
369
+ or the tab coming back to the front revives it, and a deliberate idle
370
+ suspension is left alone so the power saving still works.
371
+
372
+ **The symptom worth memorising**, because it points straight at the cause:
373
+ if `tone` and `noise` are audible while loaded clips are silent, the context
374
+ is healthy and the engine's playback state is wrong. The generators only need
375
+ a live context, where a clip goes through the engine's state machine and can
376
+ be parked waiting for a resume. It is never "the context died".
377
+
378
+ Both routes ask the same owner to resume, so hearing a tone tells you the
379
+ hardware is back; it tells you nothing about whether the clips waiting on the
380
+ engine's state have been let go.
381
+
382
+ That asymmetry was a real Safari-only bug: thirty idle seconds before the
383
+ first click left the engine waiting on a suspend the browser never confirmed,
384
+ so every clip queued for the life of the page while generated sound played
385
+ normally. Fixed in 20.9, and the lesson generalises: a clip that will not play
386
+ is a state question, not a hardware one.
179
387
 
180
388
  Playback also starts **asynchronously**: `play()` returns an id while the
181
389
  instance is still starting, so `playing()` can legitimately be `false` on the
@@ -209,6 +417,10 @@ same switch under its backend name.) `on` accepts the full lifecycle set:
209
417
  `play`, `pause`, `stop`, `end`, `fade`, `seek`, `rate`, `volume`, `mute`,
210
418
  `unlock`.
211
419
 
420
+ Outside a preload, `audio.load(asset, onload?, onerror?)` takes the same
421
+ descriptor and loads one clip at runtime — for a level's sounds fetched when
422
+ the level starts. The loader calls the same function, so the same rules apply.
423
+
212
424
  Re-preloading a manifest that already contains a loaded clip is a no-op —
213
425
  `audio.unload(name)` first if you genuinely want to reload it.
214
426
 
@@ -241,12 +453,21 @@ either way — blank screen, empty console.
241
453
  | nothing plays until the user clicks | browser autoplay policy — expected, design around it |
242
454
  | `playing()` is false right after `play()` | playback starts asynchronously |
243
455
  | music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |
456
+ | two music tracks playing at once, and `stopTrack()` only silences one | melonJS before 20.9.0: `playTrack` did not stop the previous track, so the scene that started it had to `stopTrack()` on the way out |
457
+ | the music restarts when returning to a screen | `playTrack` with the track already playing restarts it; don't call it again if the bed should carry on |
244
458
  | `stereo` / `position` do nothing | the clip was loaded with `stream: true` — spatial is WebAudio-only |
245
459
  | one format works, another does not | that codec is not in the `audio.init()` list |
246
460
  | audio behind a login fails, or the preload hangs | `loader.setOptions({ withCredentials: true })` — the same switch every asset type uses |
247
461
  | a streamed clip ignores `withCredentials` | `stream: true` plays through an `<audio>` element; preload it buffered instead |
248
462
  | `audio.tone(440, {...})` does nothing / errors | `tone` takes a single options object with a required `duration` |
249
463
  | `tone` / `noise` are silent with no error | no WebAudio context — `getAudioContext()` returned `null` |
464
+ | 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()` |
465
+ | a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |
466
+ | 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 |
467
+ | a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |
468
+ | 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 |
469
+ | 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 |
470
+ | 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 |
250
471
 
251
472
  ## Related skills
252
473
 
@@ -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 drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, and baking with CanvasRenderTarget. Triggers on: Camera2d, viewport, follow, setBounds, shake, fadeIn, fadeOut, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, Rect, Ellipse, Polygon, Line, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis."
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 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, setBounds, 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."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -38,7 +38,26 @@ this.turret.angle = math.damp(this.turret.angle, wanted, 8, dt / 1000);
38
38
  ```
39
39
 
40
40
  `damp(current, target, lambda, dt)`. Higher `lambda` converges faster; it is
41
- the rate, not a per-frame fraction. Use it for camera pans you drive yourself,
41
+ the rate, not a per-frame fraction.
42
+
43
+ **For a DIRECTION rather than a scalar, the tool is `slerp`**, on `Vector2d`
44
+ and `Vector3d` alike. Two reasons, and the second is the one that bites:
45
+
46
+ ```js
47
+ // a turret, a ship, a camera: a facing that chases another facing
48
+ facing.slerp(toTarget, 1 - Math.exp((-6 * dt) / 1000));
49
+ ```
50
+
51
+ 1. Damping a facing component by component drags it through the inside of the
52
+ circle, so it slows at the ends, rushes the middle and comes out shorter
53
+ than it went in.
54
+ 2. **Damping a scalar heading goes the wrong way round the wrap.** From 170
55
+ degrees to -170 degrees is 20 degrees apart, and the numbers are 340 apart,
56
+ so the turret sweeps all the way round the back. `slerp` works on the
57
+ directions, takes the shorter arc by construction, and has no wrap to cross.
58
+
59
+ See `melonjs-3d` for the rest, including what happens between two exactly
60
+ opposite directions. Use it for camera pans you drive yourself,
42
61
  turret tracking, zoom, input smoothing and any value that should chase another
43
62
  one — it is the tool that is easy to miss and easy to hand-write slightly
44
63
  wrong.
@@ -72,6 +91,26 @@ The fade pair is named from the *colour's* point of view, not the scene's:
72
91
  (screen starts at `color` and clears). Both default to a 1000 ms duration and
73
92
  take an optional completion callback.
74
93
 
94
+ **`fadeIn` does not HOLD the colour.** The effect completes, the camera's own
95
+ update destroys it, and the screen snaps back — which is why the engine's
96
+ idiom does the work in the completion callback and then calls `fadeOut`. To
97
+ keep a filled screen across a state change, own the effect: a completed one
98
+ marked `isPersistent` is left alone, and taking it away again is yours to do.
99
+
100
+ ```js
101
+ const hide = camera.addCameraEffect(
102
+ new FadeEffect(camera, { color: "#000", duration: 400, direction: "in" }),
103
+ );
104
+ hide.isPersistent = true; // survives completion AND Camera2d#reset()
105
+ // ... later, once the new stage is up
106
+ camera.removeCameraEffect(hide); // which destroys it for you
107
+ ```
108
+
109
+ That is exactly what `state.transition()` is built on, so a hand-rolled
110
+ transition behaves the same way. Before 20.9 a persistent effect was destroyed
111
+ on completion like any other, which is why a transition used to flash the
112
+ scene it had just covered.
113
+
75
114
  `colorMatrix` (`brightness` / `contrast` / `saturate` / `hueRotate` / `sepia` /
76
115
  `invertColors`, all chainable) is the cheap way to grade a whole camera: the
77
116
  camera appends it as a final post-effect pass by itself whenever the matrix is
@@ -240,7 +279,7 @@ effect that paints from scratch:
240
279
  ```js
241
280
  const bar = new Sprite(0, 0, { image: Renderer.getWhitePixel() });
242
281
  bar.anchorPoint.set(0, 0);
243
- bar.tint.parseCSS("#39d0ff");
282
+ bar.setTint("#39d0ff");
244
283
  bar.scale(width, height); // a 1x1 texture, so scale IS the size in pixels
245
284
  ```
246
285
 
@@ -253,6 +292,36 @@ throws on a zero dimension, instead of `document.createElement("canvas")`.
253
292
  `renderer.toFrameTexture()` is the related tool for capturing the *current frame*
254
293
  into a texture for a shader to sample.
255
294
 
295
+ ## Reading pixels back
296
+
297
+ To grab what a render target holds, as pixels or as a file:
298
+
299
+ ```js
300
+ const pixels = await rt.toImageData(); // ImageData, any backend
301
+ const blob = await rt.toBlob(); // Blob, "image/png" by default
302
+ const url = await rt.toDataURL(); // data: URL string
303
+ const bitmap = await rt.toImageBitmap(); // ImageBitmap
304
+ ```
305
+
306
+ All four are promises, and that is not decoration. **WebGPU cannot read a
307
+ texture back synchronously** — it has to map a buffer, which only completes on
308
+ a later tick. So `toImageData()` is the portable readback and the other three
309
+ are built on it.
310
+
311
+ There is also a synchronous `getImageData()` on `CanvasRenderTarget` and
312
+ `WebGLRenderTarget`, because those two genuinely can. It is **not** part of the
313
+ `RenderTarget` contract and it **throws on a WebGPU target**. Reach for it only
314
+ in code that already knows which backend it is on; anything backend-agnostic
315
+ should await `toImageData()`.
316
+
317
+ ```js
318
+ // fine in canvas-only or WebGL-only code
319
+ const data = rt.getImageData(0, 0, w, h);
320
+
321
+ // the version that works wherever the game runs
322
+ const data = await rt.toImageData(0, 0, w, h);
323
+ ```
324
+
256
325
  ## Symptom → cause
257
326
 
258
327
  | symptom | cause |
@@ -267,6 +336,7 @@ into a texture for a shader to sample.
267
336
  | clipping in the wrong place inside a container | container clips in local coordinates, after its own translate |
268
337
  | `container.clipping = true` does nothing | container has no explicit size, so `width`/`height` are `Infinity` |
269
338
  | baked target draws nothing / throws | passed the `CanvasRenderTarget`, not its `.canvas` |
339
+ | `getImageData()` throws on one machine but not another | that one is on the WebGPU renderer, which cannot read back synchronously — `await toImageData()` instead |
270
340
  | frame rate drops with many static draws | bake into a `CanvasRenderTarget` instead |
271
341
  | a 3D camera cannot be given an arbitrary up | `Camera3d#lookAt(target, up)` or `setBasis(right, up, forward)`; see `melonjs-3d` |
272
342