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.
- package/README.md +1 -0
- package/build/application/application.d.ts +66 -8
- package/build/application/application.d.ts.map +1 -1
- package/build/application/defaultApplicationSettings.d.ts +1 -0
- package/build/application/defaultApplicationSettings.d.ts.map +1 -1
- package/build/application/resize.d.ts +25 -0
- package/build/application/resize.d.ts.map +1 -1
- package/build/application/scaleMethods.d.ts +13 -3
- package/build/application/scaleMethods.d.ts.map +1 -1
- package/build/application/settings.d.ts +68 -13
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +29 -5
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/autopause.d.ts +36 -0
- package/build/audio/autopause.d.ts.map +1 -0
- package/build/audio/backend/engine.d.ts +10 -0
- package/build/audio/backend/engine.d.ts.map +1 -1
- package/build/audio/backend/sound.d.ts.map +1 -1
- package/build/audio/backend/spatial.d.ts.map +1 -1
- package/build/audio/context.d.ts +35 -0
- package/build/audio/context.d.ts.map +1 -0
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/procedural.d.ts.map +1 -1
- package/build/audio/spatial.d.ts +87 -0
- package/build/audio/spatial.d.ts.map +1 -0
- package/build/audio/state.d.ts +1 -0
- package/build/audio/state.d.ts.map +1 -1
- package/build/audio/types.d.ts +46 -6
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/effects/fade_effect.d.ts.map +1 -1
- package/build/camera/effects/mask_effect.d.ts.map +1 -1
- package/build/camera/effects/shake_effect.d.ts.map +1 -1
- package/build/geometries/box3d.d.ts +1 -1
- package/build/geometries/box3d.d.ts.map +1 -1
- package/build/geometries/ellipse.d.ts +1 -1
- package/build/geometries/ellipse.d.ts.map +1 -1
- package/build/geometries/line.d.ts +1 -1
- package/build/geometries/line.d.ts.map +1 -1
- package/build/geometries/point.d.ts +1 -1
- package/build/geometries/point.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +1 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/geometries/rectangle.d.ts +1 -1
- package/build/geometries/rectangle.d.ts.map +1 -1
- package/build/geometries/roundrect.d.ts +1 -1
- package/build/geometries/roundrect.d.ts.map +1 -1
- package/build/geometries/sphere.d.ts +1 -1
- package/build/geometries/sphere.d.ts.map +1 -1
- package/build/index.d.ts +9 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +38737 -35039
- package/build/index.js.map +4 -4
- package/build/input/gamepad.d.ts +129 -3
- package/build/input/gamepad.d.ts.map +1 -1
- package/build/level/gltf/GLTFModel.d.ts +5 -4
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/level/gltf/GLTFScene.d.ts.map +1 -1
- package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
- package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
- package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -4
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
- package/build/loader/cache.d.ts +220 -0
- package/build/loader/cache.d.ts.map +1 -1
- package/build/loader/loader.d.ts +20 -349
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/gltf.d.ts +115 -0
- package/build/loader/parsers/gltf.d.ts.map +1 -1
- package/build/math/color.d.ts +1 -1
- package/build/math/color.d.ts.map +1 -1
- package/build/math/matrix2d.d.ts +1 -1
- package/build/math/matrix2d.d.ts.map +1 -1
- package/build/math/matrix3d.d.ts +1 -1
- package/build/math/matrix3d.d.ts.map +1 -1
- package/build/math/observableVector2d.d.ts +14 -2
- package/build/math/observableVector2d.d.ts.map +1 -1
- package/build/math/observableVector3d.d.ts +15 -1
- package/build/math/observableVector3d.d.ts.map +1 -1
- package/build/math/vector2d.d.ts +31 -1
- package/build/math/vector2d.d.ts.map +1 -1
- package/build/math/vector3d.d.ts +38 -1
- package/build/math/vector3d.d.ts.map +1 -1
- package/build/math/vertex.d.ts.map +1 -1
- package/build/particles/emitter.d.ts.map +1 -1
- package/build/particles/particle.d.ts +7 -1
- package/build/particles/particle.d.ts.map +1 -1
- package/build/particles/settings.d.ts +35 -0
- package/build/particles/settings.d.ts.map +1 -1
- package/build/physics/bounds.d.ts +1 -1
- package/build/physics/bounds.d.ts.map +1 -1
- package/build/physics/broadphase/octree.d.ts +24 -0
- package/build/physics/broadphase/octree.d.ts.map +1 -1
- package/build/physics/broadphase/quadtree.d.ts +31 -0
- package/build/physics/broadphase/quadtree.d.ts.map +1 -1
- package/build/physics/builtin/body.d.ts +14 -0
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/pool.d.ts +4 -0
- package/build/pool.d.ts.map +1 -1
- package/build/renderable/colorlayer.d.ts +13 -0
- package/build/renderable/colorlayer.d.ts.map +1 -1
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/groundshadow.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +56 -0
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +97 -11
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts +47 -3
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +2 -1
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/renderable/sprite3d.d.ts +6 -2
- package/build/renderable/sprite3d.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +63 -2
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/bitmaptextdata.d.ts +1 -1
- package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
- package/build/renderable/text/glypheffect.d.ts +44 -0
- package/build/renderable/text/glypheffect.d.ts.map +1 -0
- package/build/renderable/text/text.d.ts +17 -0
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/trail.d.ts.map +1 -1
- package/build/renderable/trail3d.d.ts +174 -0
- package/build/renderable/trail3d.d.ts.map +1 -0
- package/build/renderable/trigger.d.ts +2 -1
- package/build/renderable/trigger.d.ts.map +1 -1
- package/build/renderable/ui/panelpaint.d.ts +5 -0
- package/build/renderable/ui/panelpaint.d.ts.map +1 -0
- package/build/renderable/ui/progressbar.d.ts +2 -4
- package/build/renderable/ui/progressbar.d.ts.map +1 -1
- package/build/renderable/ui/uipanel.d.ts +245 -0
- package/build/renderable/ui/uipanel.d.ts.map +1 -0
- package/build/renderable/ui/uitextbutton.d.ts +30 -1
- package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
- package/build/state/state.d.ts.map +1 -1
- package/build/system/bootstrap.d.ts.map +1 -1
- package/build/system/legacy_pool.d.ts +22 -2
- package/build/system/legacy_pool.d.ts.map +1 -1
- package/build/system/pool.d.ts +5 -8
- package/build/system/pool.d.ts.map +1 -1
- package/build/tweens/tween.d.ts +1 -1
- package/build/tweens/tween.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/squish.d.ts +74 -0
- package/build/video/effects/squish.d.ts.map +1 -0
- package/build/video/effects/tintPulse.d.ts +0 -5
- package/build/video/effects/tintPulse.d.ts.map +1 -1
- package/build/video/effects/wave.d.ts +0 -5
- package/build/video/effects/wave.d.ts.map +1 -1
- package/build/video/gpu/batcher.d.ts +8 -2
- package/build/video/gpu/batcher.d.ts.map +1 -1
- package/build/video/gradient.d.ts +1 -0
- package/build/video/gradient.d.ts.map +1 -1
- package/build/video/renderer.d.ts +16 -3
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
- package/build/video/rendertarget/render_target_pool.d.ts.map +1 -1
- package/build/video/rendertarget/rendertarget.d.ts +12 -5
- package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
- package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
- package/build/video/rendertarget/webgpurendertarget.d.ts +6 -12
- package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
- package/build/video/texture/atlas.d.ts +1 -1
- package/build/video/texture/atlas.d.ts.map +1 -1
- package/build/video/texture/texture2d.d.ts +6 -0
- package/build/video/texture/texture2d.d.ts.map +1 -1
- package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -11
- package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/material_batcher.d.ts +5 -0
- package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/mesh_batcher.d.ts +21 -0
- package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
- package/build/video/webgl/buffer/deletable.d.ts +15 -0
- package/build/video/webgl/buffer/deletable.d.ts.map +1 -0
- package/build/video/webgl/buffer/instance_buffer.d.ts.map +1 -1
- package/build/video/webgl/buffer/retained_geometry.d.ts.map +1 -1
- package/build/video/webgl/buffer/uniformblock.d.ts.map +1 -1
- package/build/video/webgl/buffer/vertexstate.d.ts.map +1 -1
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
- package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
- package/build/video/webgpu/texture/compressed.d.ts.map +1 -1
- package/build/video/webgpu/texture/store.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +2 -2
- package/skills/AGENTS.md +1 -1
- package/skills/melonjs/SKILL.md +2 -0
- package/skills/melonjs-3d/SKILL.md +192 -18
- package/skills/melonjs-3d-assets/SKILL.md +36 -2
- package/skills/melonjs-audio/SKILL.md +229 -8
- package/skills/melonjs-camera-and-drawing/SKILL.md +73 -3
- package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
- package/skills/melonjs-effects-and-shaders/SKILL.md +54 -10
- package/skills/melonjs-events/SKILL.md +8 -6
- package/skills/melonjs-input/SKILL.md +100 -4
- package/skills/melonjs-loading-assets/SKILL.md +22 -4
- package/skills/melonjs-math/SKILL.md +313 -0
- package/skills/melonjs-particles-and-trails/SKILL.md +88 -14
- package/skills/melonjs-performance/SKILL.md +57 -22
- package/skills/melonjs-physics/SKILL.md +2 -1
- package/skills/melonjs-renderables/SKILL.md +6 -1
- package/skills/melonjs-scenes-and-state/SKILL.md +14 -3
- package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
- package/skills/melonjs-tilemaps/SKILL.md +40 -12
- 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,
|
|
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
|
|
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
|
|
76
|
-
|
|
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
|
|
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.
|
|
178
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|