melonjs 20.13.0 → 20.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +65 -22
  2. package/build/application/application.d.ts +6 -1
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/header.d.ts.map +1 -1
  5. package/build/application/resize.d.ts.map +1 -1
  6. package/build/application/settings.d.ts +13 -8
  7. package/build/application/settings.d.ts.map +1 -1
  8. package/build/audio/audio.d.ts +8 -4
  9. package/build/audio/audio.d.ts.map +1 -1
  10. package/build/audio/backend/engine.d.ts.map +1 -1
  11. package/build/audio/playback.d.ts +2 -2
  12. package/build/audio/playback.d.ts.map +1 -1
  13. package/build/audio/spatial.d.ts.map +1 -1
  14. package/build/audio/types.d.ts +15 -5
  15. package/build/audio/types.d.ts.map +1 -1
  16. package/build/camera/camera2d.d.ts +10 -9
  17. package/build/camera/camera2d.d.ts.map +1 -1
  18. package/build/camera/camera3d.d.ts +11 -8
  19. package/build/camera/camera3d.d.ts.map +1 -1
  20. package/build/geometries/path2d.d.ts +9 -8
  21. package/build/geometries/path2d.d.ts.map +1 -1
  22. package/build/index.d.ts +1 -1
  23. package/build/index.d.ts.map +1 -1
  24. package/build/index.js +40993 -39738
  25. package/build/index.js.map +4 -4
  26. package/build/input/keyboard.d.ts.map +1 -1
  27. package/build/input/pointerevent.d.ts.map +1 -1
  28. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  29. package/build/level/level.d.ts.map +1 -1
  30. package/build/level/tiled/TMXLayer.d.ts +2 -1
  31. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  32. package/build/level/tiled/TMXTileMap.d.ts +13 -3
  33. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  34. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  35. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  36. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  37. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -0
  38. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  39. package/build/level/tiled/renderer/TMXRenderer.d.ts +7 -3
  40. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  41. package/build/lighting/light3d.d.ts +55 -0
  42. package/build/lighting/light3d.d.ts.map +1 -1
  43. package/build/loader/cache.d.ts +146 -97
  44. package/build/loader/cache.d.ts.map +1 -1
  45. package/build/loader/internal-types.d.ts +69 -0
  46. package/build/loader/internal-types.d.ts.map +1 -0
  47. package/build/loader/loader.d.ts +584 -292
  48. package/build/loader/loader.d.ts.map +1 -1
  49. package/build/loader/parsers/audio.d.ts.map +1 -1
  50. package/build/loader/parsers/fontface.d.ts.map +1 -1
  51. package/build/loader/parsers/gltf.d.ts.map +1 -1
  52. package/build/loader/parsers/tmx.d.ts.map +1 -1
  53. package/build/math/color.d.ts +4 -0
  54. package/build/math/color.d.ts.map +1 -1
  55. package/build/math/noise.d.ts +16 -16
  56. package/build/math/noise.d.ts.map +1 -1
  57. package/build/physics/adapter.d.ts +64 -25
  58. package/build/physics/adapter.d.ts.map +1 -1
  59. package/build/physics/builtin/body.d.ts +9 -0
  60. package/build/physics/builtin/body.d.ts.map +1 -1
  61. package/build/physics/builtin/builtin-adapter.d.ts +25 -3
  62. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  63. package/build/physics/builtin/detector.d.ts.map +1 -1
  64. package/build/physics/builtin/raycast.d.ts.map +1 -1
  65. package/build/physics/world.d.ts +40 -10
  66. package/build/physics/world.d.ts.map +1 -1
  67. package/build/renderable/container.d.ts +1 -0
  68. package/build/renderable/container.d.ts.map +1 -1
  69. package/build/renderable/draggable.d.ts.map +1 -1
  70. package/build/renderable/frameAnimation.d.ts.map +1 -1
  71. package/build/renderable/instanced_mesh.d.ts +16 -14
  72. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  73. package/build/renderable/mesh.d.ts +62 -5
  74. package/build/renderable/mesh.d.ts.map +1 -1
  75. package/build/renderable/trigger.d.ts +5 -0
  76. package/build/renderable/trigger.d.ts.map +1 -1
  77. package/build/renderable/ui/uibaseelement.d.ts +8 -1
  78. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  79. package/build/renderable/ui/uicheckbox.d.ts.map +1 -1
  80. package/build/system/device.d.ts +11 -2
  81. package/build/system/device.d.ts.map +1 -1
  82. package/build/system/fullscreen.d.ts.map +1 -1
  83. package/build/system/pool.d.ts.map +1 -1
  84. package/build/utils/utils.d.ts.map +1 -1
  85. package/build/video/canvas/canvas_renderer.d.ts +16 -14
  86. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  87. package/build/video/canvas_factory.d.ts.map +1 -1
  88. package/build/video/effects/shadereffect.d.ts +127 -0
  89. package/build/video/effects/shadereffect.d.ts.map +1 -1
  90. package/build/video/renderer.d.ts.map +1 -1
  91. package/build/video/webgl/batchers/lit_mesh_batcher.d.ts.map +1 -1
  92. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  93. package/build/video/webgpu/batchers/lit_mesh_batcher.d.ts.map +1 -1
  94. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/skills/melonjs/SKILL.md +38 -14
  97. package/skills/melonjs-20-migration/SKILL.md +50 -7
  98. package/skills/melonjs-3d/SKILL.md +138 -38
  99. package/skills/melonjs-3d-assets/SKILL.md +83 -33
  100. package/skills/melonjs-audio/SKILL.md +55 -14
  101. package/skills/melonjs-camera-and-drawing/SKILL.md +95 -8
  102. package/skills/melonjs-deployment/SKILL.md +92 -14
  103. package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
  104. package/skills/melonjs-effects-and-shaders/SKILL.md +46 -15
  105. package/skills/melonjs-events/SKILL.md +3 -2
  106. package/skills/melonjs-getting-started/SKILL.md +5 -0
  107. package/skills/melonjs-input/SKILL.md +65 -15
  108. package/skills/melonjs-lighting/SKILL.md +13 -1
  109. package/skills/melonjs-loading-assets/SKILL.md +202 -27
  110. package/skills/melonjs-math/SKILL.md +12 -5
  111. package/skills/melonjs-particles-and-trails/SKILL.md +7 -3
  112. package/skills/melonjs-performance/SKILL.md +18 -8
  113. package/skills/melonjs-physics/SKILL.md +385 -127
  114. package/skills/melonjs-plugins/SKILL.md +45 -11
  115. package/skills/melonjs-renderables/SKILL.md +24 -7
  116. package/skills/melonjs-renderer-backends/SKILL.md +38 -17
  117. package/skills/melonjs-scenes-and-state/SKILL.md +30 -5
  118. package/skills/melonjs-sprites-and-animation/SKILL.md +23 -2
  119. package/skills/melonjs-tilemaps/SKILL.md +56 -18
  120. package/skills/melonjs-ui-and-text/SKILL.md +120 -25
@@ -19,10 +19,13 @@ await loader.preload([
19
19
  ```
20
20
 
21
21
  1. **`audio.init(format)` must run before any audio asset is preloaded**, and
22
- lists the formats you actually ship. Preloading first throws
23
- `target audio extension(s) should be set through me.audio.init() before
24
- calling the preloader.` It returns `false` when no audio backend is
25
- available, so it is worth checking. The format list defaults to `"mp3"`.
22
+ lists the formats you actually ship. Preloading first fails the preload:
23
+ `await loader.preload(...)` rejects with the loader's `Failed loading
24
+ resource <src>`, which hides the real reason, `target audio extension(s)
25
+ should be set through me.audio.init() before calling the preloader.`
26
+ (thrown as it is by `audio.load()` called directly). It returns `false`
27
+ when no audio backend is available, so it is worth checking. The format
28
+ list defaults to `"mp3"`.
26
29
  2. **`src` is a prefix, not a file.** The loader appends the asset `name` plus
27
30
  each configured extension and uses the first that loads — so
28
31
  `data/sfx/` + `cling` + `mp3` → `data/sfx/cling.mp3`. Passing a full filename
@@ -50,13 +53,19 @@ audio.playTrack("theme"); // background music
50
53
  audio.stopTrack();
51
54
  ```
52
55
 
53
- Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` always
56
+ Use **`playTrack`** for music, not `play`. `playTrack(name, volume?)` (or
57
+ `playTrack(name, { volume, sprite })`, to loop one region of the clip) always
54
58
  loops, and it records the name as the current track — which is what
55
59
  `app.pauseOnBlur` / `resumeOnFocus` / `stopOnBlur` act on, via
56
60
  `state.pause(true)` → `pauseTrack()`. `stopTrack()`, `pauseTrack()`,
57
61
  `resumeTrack()` and `getCurrentTrack()` all read that same single slot, so there
58
62
  is only ever one current *track name*.
59
63
 
64
+ `playTrack` on a track that is paused restarts it. `pauseTrack` and
65
+ `resumeTrack` act on the track alone, so effects played from the same file (an
66
+ audio sprite holding both) keep going, and `resumeTrack` on a track that is
67
+ playing does nothing.
68
+
60
69
  **`playTrack` stops the track it replaces** (20.9.0 and later; before that it
61
70
  only assigned the slot, and the previous bed kept looping underneath where
62
71
  `stopTrack()` could no longer reach it). So a scene just states its own music
@@ -155,6 +164,11 @@ audio.unmuteAll(); // (audio.enable() is an alias)
155
164
  audio.muted(); // global mute state
156
165
  ```
157
166
 
167
+ `setVolume` is a master level over everything; the `volume` given to a `play`
168
+ or `playTrack` is that sound's own level, 1 when left out, and what is heard is
169
+ the product of the two. So a music slider fades the track's own level
170
+ (`audio.fade(name, v, v, 1)`) and a master slider calls `setVolume`.
171
+
158
172
  `audio.unload(name)` / `audio.unloadAll()` free the decoded buffers; `unload`
159
173
  also clears the current-track pointer if it named that clip.
160
174
 
@@ -163,8 +177,10 @@ also clears the current-track pointer if it named that clip.
163
177
  When the window loses focus the engine mutes all audio, and unmutes it on
164
178
  focus. That covers sound effects, procedural `tone` / `noise` and the mixer
165
179
  effects, not just the music track that `state.pause()` pauses. Set
166
- `pauseOnBlur: false` in the application settings if a clip has to keep playing
167
- in the background.
180
+ `app.pauseOnBlur = false` (with `stopOnBlur` left at its `false` default) if a
181
+ clip has to keep playing in the background. It is a property of the
182
+ application, not a constructor setting: `new Application(w, h, { pauseOnBlur:
183
+ false })` does nothing.
168
184
 
169
185
  ## Spatial audio
170
186
 
@@ -197,10 +213,15 @@ audio.play("portal", { at: { x: 1200, y: 480 } });
197
213
 
198
214
  `follow` and `at` are mutually exclusive: passing both throws, rather than
199
215
  quietly picking one. `follow` re-reads the target's **absolute** position each
200
- frame, so a renderable nested in a moving container is placed correctly. `stopWithTarget` is what you
201
- want for a sound that belongs to an entity: without it a looping clip on a
202
- destroyed renderable keeps playing from wherever it last was. `audio.unfollow(id)`
203
- detaches a sound and leaves it playing where it is.
216
+ frame, so a renderable nested in a moving container is placed correctly. That
217
+ position is where its `anchorPoint` sits: the center of a `Renderable` or a
218
+ `Sprite`, but the **top-left corner** of an `Entity` or an object loaded from
219
+ Tiled, whose anchor is `(0, 0)`, so a large one (a boss) sounds from up and to
220
+ the left of where it looks to be. `stopWithTarget` is what you want for a sound
221
+ that belongs to an entity: without it a looping clip on a destroyed renderable
222
+ keeps playing from wherever it last was. It also counts a pooled renderable
223
+ going back to its pool, which can lend it out again before the next frame.
224
+ `audio.unfollow(id)` detaches a sound and leaves it playing where it is.
204
225
 
205
226
  ### Move the listener, don't do the subtraction yourself
206
227
 
@@ -221,7 +242,12 @@ player's position.
221
242
  `setListener` places the ear at the target's absolute position after the world
222
243
  updates. A plain renderable contributes position only, so a side-scroller keeps
223
244
  the default "facing into the screen" and gets left-right panning plus distance
224
- falloff from the one call.
245
+ falloff from the one call. A `Camera2d` (`audio.setListener(app.viewport)` in a
246
+ 2D game) is heard from the **center of its view**, its `worldView`, zoom and
247
+ offset included, not from its `pos`, the view's top-left corner (before 20.12
248
+ it was the corner, and everything panned right). Following stops on its own
249
+ when the target is destroyed, which a stage that owns its camera does on every
250
+ state change.
225
251
 
226
252
  A `Camera3d` target also contributes its **orientation**, taken from its own
227
253
  basis, so turning the camera swings the stereo image with it. That is the whole
@@ -380,6 +406,14 @@ a gate screen — a splash, a "press any key", or a health/photosensitivity noti
380
406
  unconditionally. Note the unlock listens for `keydown` too, so a keyboard-only
381
407
  player is covered.
382
408
 
409
+ Where there is no DOM (nx.js on the Nintendo Switch, see `melonjs-deployment`)
410
+ there is no page to gesture at and no autoplay policy: audio unlocks as soon as
411
+ the engine has its context, and music can start on load. nx.js's Web Audio has
412
+ buffer sources, gain and stereo panning, but not the `PannerNode`,
413
+ `OscillatorNode` and `BiquadFilterNode` behind 3D `position()` / `panner()` and
414
+ `tone()` / `noise()`, nor the reverb, echo and compressor nodes: those throw
415
+ "Method not implemented" there.
416
+
383
417
  ### The context can stop again, long after it was unlocked
384
418
 
385
419
  Unlocking is only the first half. A context that has been running can be
@@ -490,8 +524,9 @@ either way — blank screen, empty console.
490
524
  | symptom | cause |
491
525
  |---|---|
492
526
  | audio assets 404 | `src` given as a full filename instead of a directory prefix |
493
- | `target audio extension(s) should be set...` thrown at preload | `audio.init()` not called before preloading |
527
+ | `preload()` rejects with `Failed loading resource data/sfx/` (or `audio.load()` throws `target audio extension(s) should be set...`) | `audio.init()` not called before preloading |
494
528
  | `audio clip X does not exist` | the clip was never preloaded, or the name is mis-cased — every per-clip call throws this |
529
+ | "Method not implemented" from `position()`, `panner()`, `tone()`, `noise()` or an effect, on nx.js | nx.js's Web Audio lacks the node it needs (panner, oscillator, filter, convolver...): use `stereo()` and recorded sounds there |
495
530
  | nothing plays until the user clicks | browser autoplay policy — expected, design around it |
496
531
  | `playing()` is false right after `play()` | playback starts asynchronously |
497
532
  | music keeps playing after leaving a scene | used `play` instead of `playTrack`, or no `stopTrack` in teardown |
@@ -507,11 +542,17 @@ either way — blank screen, empty console.
507
542
  | a positioned sound is silent, or only audible right next to the player | placed by hand with `audio.position()`, which keeps WebAudio's metre defaults (`refDistance: 1`) — use `follow` / `at`, or set `refDistance` in pixels with `audio.panner()` |
508
543
  | a positioned sound does not follow the player | nothing is placing it — `audio.play(name, { follow: entity })`, and `audio.setListener(player)` once |
509
544
  | a followed sound pans to the wrong side as the player moves | the listener was moved **and** positions are still relative to the player — with a listener in the world, pass absolute world coordinates |
510
- | a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true` |
545
+ | with `audio.setListener(app.viewport)`, a sound in the middle of the screen is heard off to the right (before 20.12) | the ear was at the camera's top-left corner: update, it is at the center of the view |
546
+ | a followed `Entity` (or Tiled object) sounds offset up and to the left | `follow` uses the anchor point, which is the top-left corner under their `(0, 0)` anchor |
547
+ | a looping sound outlives the entity that owns it | `follow` without `stopWithTarget: true`; with it, before 20.14 a pooled entity going back to its pool did not count either |
548
+ | new sounds are quieter than the volume slider says, or the music stays silent after the volume went to 0 and back (before 20.14) | the global volume was applied twice: update |
549
+ | "Retry" from a pause menu restarts the level with no music (before 20.14) | `playTrack` on a paused track stopped it: update |
550
+ | a clip's load-time `on: { end }` stops firing after `audio.stop(name)` (before 20.14) | `stop(name)` removed it: update |
511
551
  | a voice panned with `stereo()` loses its 3D placement, or the reverse | one panner node per voice and last writer wins — pick one per voice and stay with it |
512
552
  | a reverb / compressor on the master gain does not affect the music | the music is `stream: true`, which bypasses the master gain — preload it buffered |
513
553
  | a looping MP3 drops out for a moment at every loop, in Safari only | Safari keeps the encoder's padding at the end of the file: loop a sprite region of the clip's exact length (see "A loop with no gap") |
514
554
  | sound effects keep playing after the window loses focus | `pauseOnBlur` and `stopOnBlur` are both off, so the engine is deliberately leaving the game running in the background |
555
+ | the game still pauses and goes quiet on blur although `pauseOnBlur: false` was passed to `new Application()` | it is not a setting: assign `app.pauseOnBlur = false` |
515
556
 
516
557
  ## Related skills
517
558
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-camera-and-drawing
3
- description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, shake, fadeIn, fadeOut, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, setGlobalAlpha, fillArc, Rect, Ellipse, Polygon, Line, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
3
+ description: "Use this skill for camera control and immediate-mode drawing in melonJS — following a target, viewport bounds, shake and fade, secondary cameras and what each one draws, and drawing shapes, lines and gradients inside a custom draw(). Covers world versus screen coordinate conversion, clipping and masking, baking with CanvasRenderTarget, and reading pixels back. Triggers on: Camera2d, viewport, follow, offset, look ahead, setBounds, minimap, second camera, split screen, cullMask, visibilityLayer, hide from a camera, zoom, roll, shake, fadeIn, fadeOut, CameraEffect, addCameraEffect, MaskEffect, PixelateTransitionEffect, worldToLocal, localToWorld, colorMatrix, renderer.fill, renderer.stroke, setGlobalAlpha, fillArc, Rect, Ellipse, Polygon, Line, Path2D, SVG path, Gradient, clipRect, mask, CanvasRenderTarget, NoiseTexture2d, lookAt, setBasis, toImageData, getImageData, toBlob, toDataURL, screenshot, read pixels."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -109,6 +109,13 @@ The fade pair is named from the *colour's* point of view, not the scene's:
109
109
  (screen starts at `color` and clears). Both default to a 1000 ms duration and
110
110
  take an optional completion callback.
111
111
 
112
+ `shake()` while a shake is still running is ignored, unless its fifth
113
+ argument, `force`, is `true`, which replaces the running one:
114
+
115
+ ```js
116
+ app.viewport.shake(8, 300, app.viewport.AXIS.BOTH, undefined, true);
117
+ ```
118
+
112
119
  **`fadeIn` does not HOLD the colour.** The effect completes, the camera's own
113
120
  update destroys it, and the screen snaps back — which is why the engine's
114
121
  idiom does the work in the completion callback and then calls `fadeOut`. To
@@ -136,6 +143,48 @@ non-identity, and drops it again after the frame. It is **not** shader-free —
136
143
  it is a `ColorMatrixEffect`, i.e. a `ShaderEffect`, so like every post effect it
137
144
  is inert on the Canvas renderer.
138
145
 
146
+ ### The camera effects, and your own
147
+
148
+ Each is a `CameraEffect`, added with `camera.addCameraEffect(effect)` (which
149
+ returns it), found with `getCameraEffect(Class)` and taken off with
150
+ `removeCameraEffect(effect)`, which destroys it:
151
+
152
+ | effect | options | what it does |
153
+ |---|---|---|
154
+ | `ShakeEffect` | `intensity`, `duration`, `axis`, `onComplete` | jitters `offset`; what `shake()` adds. Several can run at once this way |
155
+ | `FadeEffect` | `color`, `duration` (1000), `direction: "in" \| "out"`, `onComplete` | what `fadeIn()` / `fadeOut()` add |
156
+ | `MaskEffect` | `shape`, `color`, `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | an iris or a wipe through a unit-sized `Ellipse` or `Polygon` centred on the origin |
157
+ | `PixelateTransitionEffect` | `renderer`, `size` (32), `duration` (500), `direction: "hide" \| "reveal"`, `onComplete` | the view breaks into blocks up to `size` screen pixels, or comes back from them. A shader: on Canvas it keeps its timing and draws nothing |
158
+
159
+ `state.transition("fade" | "mask" | "pixelate", …)` drives the last three
160
+ for a state change (see `melonjs-scenes-and-state`). A `Trigger` that loads a
161
+ level transitions only with both a `color` and a `duration`, and takes only
162
+ `transition: "fade"` (the default) or `"mask"` with a `shape`: anything else,
163
+ `"pixelate"` included, is a fade.
164
+
165
+ For an effect of your own, extend `CameraEffect`: `update(dt)` runs every
166
+ frame (`dt` in ms), `draw(renderer, width, height)` draws over the camera's
167
+ view, and `destroy()` frees what it holds. Set `isComplete = true` and the
168
+ camera's update destroys it that frame, unless it is `isPersistent`.
169
+
170
+ ## Zoom and roll
171
+
172
+ ```js
173
+ app.viewport.zoom = 2; // above 1 zooms in, below 1 out
174
+ app.viewport.roll = Math.PI / 32; // a slight dutch angle, in radians
175
+ ```
176
+
177
+ A zoomed camera follows, keeps to its bounds and centres on `focusOn` in world
178
+ units, and `localToWorld` / `worldToLocal` count the zoom. A zoom of 0 or less
179
+ is taken as 1.
180
+
181
+ `roll` is absolute (assigning the same angle twice changes nothing), and the
182
+ pointer conversions undo it, so picking under a rolled camera needs nothing.
183
+ It OWNS the camera's `currentTransform`: each assignment rebuilds it, so a
184
+ `rotate()` or `scale()` called on the camera itself is lost at the next one.
185
+ `reset()` puts it back to 0. On a `Camera3d` the same `roll` banks the view
186
+ (see below).
187
+
139
188
  ## Coordinate conversion
140
189
 
141
190
  ```js
@@ -291,8 +340,38 @@ backend, though: Canvas closes it with a straight chord, the GPU renderers fill
291
340
  a pie slice from the centre.
292
341
 
293
342
  Geometry available: `Rect`, `RoundRect`, `Ellipse`, `Polygon`, `Line`, `Point`
294
- (and a `Bounds`). `renderer.fill(shape)` and `renderer.stroke(shape)` take any
295
- of them and throw `Invalid geometry for fill/stroke` for anything else.
343
+ (and a `Bounds`), plus the 3D `Box3d` and `Sphere`, drawn as their footprint
344
+ and their circle on the screen plane. `renderer.fill(shape)` and
345
+ `renderer.stroke(shape)` take any of them and throw `Invalid geometry for
346
+ fill/stroke` for anything else.
347
+
348
+ ### Paths and SVG strings
349
+
350
+ For any other outline, build a path on the renderer's own `renderer.path2D`,
351
+ then call `fill()` / `stroke()` with no argument:
352
+
353
+ ```js
354
+ renderer.path2D.parseSVGPath("M 50 0 L 100 100 L 0 100 Z");
355
+ renderer.setColor("#4CAF50");
356
+ renderer.fill();
357
+ ```
358
+
359
+ `renderer.path2D` also takes the methods of the browser's `Path2D`
360
+ (`beginPath`, `moveTo`, `lineTo`, `arc`, `arcTo`, `ellipse`, `rect`,
361
+ `roundRect`, `quadraticCurveTo`, `bezierCurveTo`, `closePath`).
362
+
363
+ - **Every `moveTo`, every `M`, starts a sub-path, and filled, each one after
364
+ the first is a HOLE**: a donut, the counter of an "O". Wind a hole the
365
+ opposite way to the outline, as Canvas fills by the non-zero rule, where a
366
+ hole wound the same way fills in. Draw separate shapes with separate fills.
367
+ - **`parseSVGPath` reads absolute commands only** (`M L H V Q C A Z`). A
368
+ lowercase, relative command is read as its absolute twin, so a path
369
+ exported with relative commands comes out scrambled, and `H` / `V` take an
370
+ OFFSET from the pen, not the coordinate SVG means. Export absolute
371
+ coordinates.
372
+ - **The engine's `Path2D` class is not exported.** `new Path2D()` in a game is
373
+ the browser's own, which `fill()` / `stroke()` refuse as invalid geometry:
374
+ use `renderer.path2D`.
296
375
 
297
376
  `Gradient` gives linear and radial fills, built with
298
377
  `renderer.createLinearGradient(x0, y0, x1, y1)` /
@@ -353,10 +432,11 @@ drawable, and the engine's own bakes (the particle emitter's default texture)
353
432
  hand `.canvas` on for exactly that reason.
354
433
 
355
434
  **Create it once and keep it.** On a GPU backend, once drawn, its texture stays
356
- on the GPU for as long as the renderer lives: a stage change does not release
357
- it, and no public call does. A target made for every level, every restart or
358
- every spawned object leaks one texture each time, so make it once (at module
359
- level, say) and clear it for reuse. If you paint into it again after it has
435
+ on the GPU until `rt.destroy(renderer)` frees it: a stage change does not, and
436
+ `destroy()` without the renderer frees the canvas but not that copy. A target
437
+ made for every level, every restart or every spawned object and never
438
+ destroyed leaks one texture each time, so make it once (at module level, say)
439
+ and clear it for reuse. If you paint into it again after it has
360
440
  been drawn, call `rt.invalidate(renderer)` before the next draw so the GPU copy
361
441
  is refreshed.
362
442
 
@@ -435,7 +515,14 @@ const data = await rt.toImageData(0, 0, w, h);
435
515
  | baked target draws nothing / throws | passed the `CanvasRenderTarget`, not its `.canvas` |
436
516
  | `getImageData()` throws on one machine but not another | that one is on the WebGPU renderer, which cannot read back synchronously — `await toImageData()` instead |
437
517
  | frame rate drops with many static draws | bake into a `CanvasRenderTarget` instead |
438
- | GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it |
518
+ | GPU memory grows with every level or restart | a `CanvasRenderTarget` created each time: make one and reuse it, or `destroy(renderer)` the old one |
519
+ | a second `shake()` does nothing | one is still running: pass `force` (the fifth argument) to replace it |
520
+ | a `Trigger` with `transition: "pixelate"` fades instead | a trigger takes only `"fade"` or `"mask"`; add a `PixelateTransitionEffect` yourself |
521
+ | a `Trigger` loads its level with no transition | it needs both `color` and `duration` |
522
+ | a `rotate()` or `scale()` on the camera is lost | `roll` owns the camera's `currentTransform` and rebuilds it; use `roll` and `zoom` |
523
+ | an SVG path from an editor comes out scrambled | `parseSVGPath` reads absolute commands only, and `H` / `V` as offsets |
524
+ | a hole in a path fills in, on Canvas only | it is wound the same way as the outline; wind it the opposite way |
525
+ | `fill(new Path2D(...))` throws `Invalid geometry for fill/stroke` | that is the browser's `Path2D`; build on `renderer.path2D` and call `fill()` |
439
526
  | a repainted `CanvasRenderTarget` still shows the old pixels | call `rt.invalidate(renderer)` after painting |
440
527
  | a 3D camera cannot be given an arbitrary up | `Camera3d#lookAt(target, up)` or `setBasis(right, up, forward)`; see `melonjs-3d` |
441
528
  | particles or effects clutter a minimap | every camera draws everything: put them on their own `visibilityLayer` and leave it out of the minimap's `cullMask` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-deployment
3
- description: "Use this skill when packaging, shipping or scaffolding a melonJS game — starting a project with create-melonjs, building for the web, wrapping as a native app with Capacitor or Cordova, publishing to WeChat Mini Games or Facebook Instant Games, browser support and transpiling. Triggers on: create-melonjs, scaffold, new project, build, deploy, publish, Capacitor, Cordova, native app, iOS, Android, WeChat, Instant Games, browser support, transpile, ES2022, bundle size, tree-shaking."
3
+ description: "Use this skill when packaging, shipping or scaffolding a melonJS game — starting a project with create-melonjs, building for the web, wrapping as a native app with Capacitor or Cordova, publishing to WeChat Mini Games or Facebook Instant Games, running with no DOM (nx.js on the Nintendo Switch, a Web Worker), browser support and transpiling. Triggers on: create-melonjs, scaffold, new project, build, deploy, publish, Capacitor, Cordova, native app, iOS, Android, WeChat, Instant Games, nx.js, Nintendo Switch, homebrew, no DOM, Web Worker, OffscreenCanvas, browser support, transpile, ES2022, bundle size, tree-shaking."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -15,9 +15,11 @@ npm create melonjs@latest my-game
15
15
  ```
16
16
 
17
17
  The project name is required — without it the command prints its usage and
18
- exits. It downloads the melonJS TypeScript boilerplate (Vite, TypeScript and the
19
- debug plugin already wired in), which saves you the top-level-await and
20
- asset-path issues that a hand-rolled config usually hits first.
18
+ exits — and it names a new directory: an existing one is refused. It downloads
19
+ the melonJS TypeScript boilerplate (Vite, TypeScript and the debug plugin
20
+ already wired in), which saves you the top-level-await and asset-path issues
21
+ that a hand-rolled config usually hits first. Then `npm install` and
22
+ `npm run dev` in that directory.
21
23
 
22
24
  ## Adding melonJS to an existing project
23
25
 
@@ -69,14 +71,22 @@ Two caveats worth knowing:
69
71
 
70
72
  ## Native apps
71
73
 
72
- **Capacitor** is the current recommendation for iOS and Android — see
73
- `@melonjs/capacitor-plugin`. **Cordova** also works and is documented, but is the
74
- older route.
75
-
76
- The thing that breaks first in both is asset loading: the app is served from
77
- `file://`, where `fetch()` cannot read local files. melonJS's loader falls back
78
- to XHR for that scheme, so engine-loaded assets work — but any `fetch()` in
79
- *your* game code will fail there.
74
+ **Capacitor** is the current recommendation for iOS and Android. A melonJS game
75
+ runs in its WebView unchanged; `@melonjs/capacitor-plugin` adds the native
76
+ lifecycle: the game pauses when the app goes to the background and resumes when
77
+ it comes back (but stays paused if the player had paused it, from 1.0.1), the
78
+ hardware back button reaches the current stage (`bindStageBack`), and
79
+ orientation lock and the splash screen are one call each. The boilerplate's
80
+ Vite build writes to `dist/`, Capacitor's default `webDir`. See
81
+ `melonjs-plugins` for its options. **Cordova** also works and is documented,
82
+ but is the older route.
83
+
84
+ The thing that breaks first under Cordova is asset loading: the app is served
85
+ from `file://`, where `fetch()` cannot read local files. melonJS's loader falls
86
+ back to XHR for that scheme, so engine-loaded assets work — but any `fetch()` in
87
+ *your* game code will fail there. Capacitor serves the app from a local web
88
+ server instead (`https://localhost` on Android, `capacitor://localhost` on
89
+ iOS), so `fetch()` works there.
80
90
 
81
91
  ## Mini-game platforms
82
92
 
@@ -85,7 +95,67 @@ wiki. Note that `device.platform.isWeixin` detects WeChat's **in-app browser**,
85
95
  which is a normal WebView. WeChat *Mini Games* are a different runtime with no
86
96
  DOM, and need platform-specific adaptation rather than just detection — the one
87
97
  hook the engine offers is that the renderer adopts a `globalThis.canvas` when
88
- the runtime's adapter provides one, instead of creating its own.
98
+ the runtime's adapter provides one, instead of creating its own. What the engine
99
+ does without a DOM is described in the next section.
100
+
101
+ ## Runtimes with no DOM: nx.js, Web Workers
102
+
103
+ melonJS (from 20.14) runs where there is no `document`: in a Web Worker, and in
104
+ runtimes that implement the web APIs a game uses but no page around it, such as
105
+ [nx.js](https://github.com/TooTallNate/nx.js), JavaScript homebrew for the
106
+ Nintendo Switch. There is no `<canvas>` to create there, so hand the
107
+ Application the canvas to draw to. On nx.js that is the global `screen`, the
108
+ 1280x720 display:
109
+
110
+ ```js
111
+ import { Application, video } from "melonjs";
112
+
113
+ const app = new Application(1280, 720, {
114
+ canvas: screen, // nx.js's display; an OffscreenCanvas in a worker
115
+ renderer: video.AUTO, // WebGL 2 where the runtime has it, else Canvas
116
+ scale: 1,
117
+ });
118
+ await app.init();
119
+ ```
120
+
121
+ What changes without a DOM:
122
+
123
+ - **Design at the canvas's size.** There is no page to fit, so
124
+ `scale: "auto"` measures nothing and the game is drawn at the scale it was
125
+ given. On the Switch that is 1280x720 (the console scales it when docked).
126
+ - `app.parentElement` is `undefined`: the canvas is the display and is not
127
+ attached to anything. Fullscreen and pointer lock do nothing.
128
+ - **Assets** load through `fetch` and `createImageBitmap` as in a browser; on
129
+ nx.js a relative path resolves inside the app's RomFS. Tiled maps must be
130
+ **JSON** (`.tmj` / `.tsj`): XML needs `DOMParser` (see `melonjs-tilemaps`).
131
+ The `video` and `script` asset types need a DOM and fail.
132
+ - **Input**: touch arrives as `TouchEvent`s on the canvas, and melonJS reports
133
+ them as pointer events as usual. There is no mouse. On nx.js the Joy-Con are
134
+ standard gamepads (`melonjs-input`), and keyboard events only come from a
135
+ USB keyboard.
136
+ - **Audio** needs no gesture to unlock. nx.js cannot say which formats it
137
+ decodes (its `Audio` has no `canPlayType`), so melonJS tries the first
138
+ extension given to `audio.init()`: list one FFmpeg decodes, such as
139
+ `audio.init("ogg,mp3")`. nx.js's Web Audio has no `PannerNode`,
140
+ `OscillatorNode`, `BiquadFilterNode` or effect nodes, so plain playback,
141
+ volume and `stereo()` work, but 3D `position()` / `panner()`, `tone()` /
142
+ `noise()` and the reverb / echo / compressor effects throw there (see
143
+ `melonjs-audio`).
144
+ - **Fonts**: nx.js's `FontFace` only takes the font's bytes, so a `fontface`
145
+ asset (a URL) fails there. Load the bytes yourself and add the face before
146
+ using it in a `Text`. The face is ready once made, and its `load()` throws
147
+ on nx.js, so do not call it:
148
+ `fonts.add(new FontFace("Pixel", await (await fetch("font.ttf")).arrayBuffer()))`.
149
+ - **Save data**: melonJS checks `localStorage` when it loads. On nx.js, if the
150
+ app declares user save data (`userAccountSaveDataSize` in its NACP), that
151
+ first read is when the Switch asks the player to pick a user profile. An app
152
+ that saves nothing should leave that size at 0, so no picker appears.
153
+ - A Web Worker has no `Image`, `Audio` or `AudioContext` of its own: draw
154
+ there, and play sound from the page.
155
+
156
+ The engine's own test, `tests/nxjs-compatibility.spec.js`, runs a game in a
157
+ Web Worker made to look like nx.js. It is the reference for what is expected
158
+ to work, not a test on Switch hardware.
89
159
 
90
160
  ## Size
91
161
 
@@ -103,11 +173,19 @@ are not decoded into memory.
103
173
  |---|---|
104
174
  | blank page, syntax error in the console | browser older than the ES2022 target |
105
175
  | transpiling changes nothing | build config excludes `node_modules` |
106
- | assets 404 in a packaged native app | `fetch()` in game code — `file://` needs XHR |
176
+ | assets 404 in a packaged Cordova app | `fetch()` in game code — `file://` needs XHR |
107
177
  | a built-in method is missing after transpiling | transpiling is syntax-only; polyfills are separate |
108
178
  | bundle much larger than expected | tree-shaking recovers little — subsystems are interconnected |
179
+ | a native app paused from its menu is running again after coming back from the background | capacitor-plugin < 1.0.1 resumed on every return; upgrade |
180
+ | `Cannot read properties of undefined (reading 'body')` from `new Application` in nx.js or a Web Worker | melonJS older than 20.14, which needed a DOM; update |
181
+ | WebGL 2 "could not be created" on nx.js | another context was taken on `screen` first: it holds one context kind, so let the Application be the first to call `getContext` on it |
182
+ | the game is drawn small, or cropped, on nx.js | its resolution is not the screen's 1280x720; with no page, `scale: "auto"` does not fit it |
183
+ | an XML Tiled map fails with "needs DOMParser" | no DOM to parse XML with: export the map and its tilesets as JSON (`.tmj` / `.tsj`) |
184
+ | a `fontface` asset fails on nx.js | nx.js's `FontFace` takes bytes, not a URL: fetch the file and add the face to `fonts` yourself |
185
+ | spatial sound, `tone()` or `noise()` throws "Method not implemented" on nx.js | the node it needs is missing from nx.js's Web Audio; use `stereo()` and recorded sounds |
109
186
 
110
187
  ## Related skills
111
188
 
112
189
  - `melonjs` — the companion packages, including the Capacitor plugin
190
+ - `melonjs-plugins` — registering the Capacitor plugin, and its helpers
113
191
  - `melonjs-getting-started` — the Application bootstrap the template produces
@@ -39,8 +39,11 @@ the usual arrangement: author once, scale to whatever you are given.
39
39
 
40
40
  ## `scale` and `scaleMethod`
41
41
 
42
- `scale: "auto"` turns on automatic scaling; a number is a fixed multiplier and
43
- `scaleMethod` is then ignored.
42
+ `scale: "auto"` turns on automatic scaling, and so does naming any
43
+ `scaleMethod` other than `"manual"`, even with no `scale` given:
44
+ `{ scaleMethod: "flex" }` alone scales to its container. A number is a fixed
45
+ multiplier, used only while `scaleMethod` is `"manual"` (the default); with
46
+ automatic scaling on, the number is ignored.
44
47
 
45
48
  `scaleMethod` decides how the available space is used:
46
49
 
@@ -54,7 +57,8 @@ the usual arrangement: author once, scale to whatever you are given.
54
57
  both axes.
55
58
  - **`"stretch"`** scales each axis independently, so the aspect ratio is not
56
59
  preserved and circles become ovals.
57
- - **`"manual"`** does nothing automatic.
60
+ - **`"manual"`**, the default, does nothing automatic with a numeric `scale`.
61
+ With `scale: "auto"` it scales as `"fit"` does.
58
62
 
59
63
  The distinction that matters: **`fit` scales a fixed frame; everything else
60
64
  changes how much of the world is visible.** A game whose layout assumes a
@@ -381,3 +385,4 @@ const app = new Application(1024, 576, {
381
385
  | `scaleRounding: "integer"` changes nothing | it applies to `scaleMethod: "fit"` only; the others resize the renderer |
382
386
  | text is soft but sprites are crisp | the glyphs were anti-aliased when `Text` baked them; use `BitmapText` (see below) |
383
387
  | the game shows more world on a bigger window | that is `fill-*` or `flex-*` doing their job; use `"fit"` to keep a fixed frame |
388
+ | a numeric `scale` (say `2`) is ignored | a `scaleMethod` other than `"manual"` turns automatic scaling on, which ignores the number |
@@ -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: 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."
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, OutlineEffect, BloomEffect, ToneMappingEffect, SwayEffect, SquishEffect, hdr, tone mapping, shader, GLSL, WGSL, uniform, setUniform, setTexture, setTime, getShader, prewarm, blendMode, colorMatrix, screen_texture, screen_uv, noise_uv, toFrameTexture, pixelate transition, post effect, filter."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -88,8 +88,9 @@ class Bar extends Renderable {
88
88
  }
89
89
  ```
90
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.
91
+ `ProgressBar`, `UIPanel` and `Trail` set it for you, and a sprite needs
92
+ nothing: the flag defaults to `false` and only ever changes the single-effect
93
+ case.
93
94
 
94
95
  ## Toggle with `enabled`, do not remove
95
96
 
@@ -229,7 +230,8 @@ squish.setBulge(1 - scaleY);
229
230
  ```
230
231
 
231
232
  Apply that from the sprite's own `update()`, not from a tween's `onUpdate`. A
232
- physics adapter that mirrors a body's angle (Matter does) rewrites
233
+ physics adapter that mirrors a body's angle (matter, planck and Rapier do, and so does
234
+ the built-in once a body has an angle or angular velocity) rewrites
233
235
  `currentTransform` on every step, before the renderables update, so a scale set
234
236
  anywhere else is gone by the time the sprite is drawn. Let the tween animate a
235
237
  plain `{ scaleY }` object and read it in `update()`.
@@ -457,8 +459,10 @@ renderer.setToneMapping("aces", { exposure: 0.4, white: 6 });
457
459
  renderer.setToneMapping("none"); // and off again
458
460
  ```
459
461
 
460
- `"none"` by default. The renderer keeps the curve LAST on the camera, so
461
- whatever you put in front of it is mapped along with everything else. Changing
462
+ `"none"` by default, and set at boot through the `toneMapping`,
463
+ `toneMappingExposure` and `toneMappingWhite` application settings. The
464
+ renderer keeps the curve LAST on the camera, so whatever you put in front of
465
+ it is mapped along with everything else. Changing
462
466
  the mode rebuilds one shader (the curve is baked); `exposure` and `white` are
463
467
  uniform writes, so a settings screen can drive them live. `white` is the value
464
468
  that comes out as display white — the knob a "brightness" or "peak" slider
@@ -483,6 +487,13 @@ It is still *implemented* as one: a non-identity matrix makes the camera append
483
487
  an internal `ColorMatrixEffect` to its post-effect chain for that frame, so it
484
488
  needs a GPU backend like everything else here.
485
489
 
490
+ The `"pixelate"` state transition (`state.transition("pixelate", …)`, or a
491
+ `PixelateTransitionEffect` added with `addCameraEffect`) is another such guest:
492
+ while it runs it puts a `PixelateEffect` FIRST in the camera's chain, so a
493
+ vignette or scanlines you added stay sharp over the blocks, and it takes the
494
+ effect off again when it is done. Being a shader, it does nothing on Canvas,
495
+ where `state.transition` changes the state directly instead.
496
+
486
497
  ## Custom effects are dual-language
487
498
 
488
499
  A custom `ShaderEffect` supplies a fragment **body**, not a whole program. To run
@@ -582,8 +593,8 @@ string in your own module:
582
593
 
583
594
  ```js
584
595
  // with the level's other assets, not in the stage that uses them.
585
- // `preload()` rather than `load()` — preload is what warms the shaders it
586
- // brought in, and it can be called again per level, not just at boot
596
+ // `preload()` or a bundle's `load(id)`, not `load(asset)`: those two warm
597
+ // the shaders they bring in, and run again per level, not just at boot
587
598
  await loader.preload([
588
599
  { name: "ripples", type: "shader", src: {
589
600
  glsl: "assets/level2/ripples.glsl",
@@ -596,6 +607,14 @@ await loader.preload([
596
607
  `src` takes the dual-language pair, so one asset carries both realizations and
597
608
  the two backends cannot drift apart. `data` takes inline source the same way.
598
609
 
610
+ A level's bundle (20.14: `loader.load("level2", [...])` declares and loads
611
+ one, warming like `preload` but with no loading screen) is also the shader's
612
+ lifetime. Unloading the last bundle that lists it, or `loader.unload()` of the
613
+ shader asset itself, destroys the shared instance whatever still draws with
614
+ it, and those renderables then draw without the effect. An effect that
615
+ outlives a level (a HUD's, a camera's) belongs in a bundle that outlives it
616
+ too; see `melonjs-loading-assets`.
617
+
599
618
  With `prewarm` on (the default since 20.6), `loader.preload()` warms on its way
600
619
  through — automatically, on every call — and compiles the GPU-side objects for
601
620
  every shader it holds — on WebGPU that is the
@@ -632,7 +651,9 @@ variants are covered by the loading-screen warm-up and by the per-renderable one
632
651
  For refraction, distortion and anything that samples what is already drawn:
633
652
 
634
653
  - **`screen_texture`** — a sampler kept filled with everything drawn so far
635
- (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`)
654
+ (GLSL: annotate the sampler `uniform sampler2D tex : screen_texture;`, or
655
+ `: screen_texture(repeat)` to sample it wrapping; `repeat-x` and
656
+ `repeat-y` work too)
636
657
  - **`screen_uv`** — this fragment's 0..1 position in that capture
637
658
  - **`noise_uv`** — 0..1 across the sprite regardless of its atlas frame, and
638
659
  across the renderable when it has several effects (it does not slide with
@@ -650,7 +671,11 @@ Two places where a straight port really does differ:
650
671
  into a `Texture2d` you hand to `setTexture` — the supported replacement for
651
672
  `readPixels` tricks. The GL capture is bottom-up (GLSL bodies sample it with
652
673
  `1.0 - uv.y`) and opaque RGB; the WebGPU capture is top-down (the WGSL twin
653
- must *not* flip) and preserves alpha.
674
+ must *not* flip) and preserves alpha. It holds what was drawn when it was
675
+ called, so call it right before the draw that samples it. By default it
676
+ refills one shared, renderer-owned texture; `toFrameTexture({ target: null })`
677
+ hands back a capture of your own, for two alive at once, which you
678
+ `destroy()` when done.
654
679
  - **Vertical UV offsets** — a drop shadow, a directional smear. The WebGL pooled
655
680
  multi-effect path composites through bottom-up FBOs, so declare a `uUVYDir`
656
681
  uniform (initialise it to `1.0`) and multiply vertical offsets by it; the
@@ -724,10 +749,14 @@ capture plus one composite **per draw**: `difference`, `overlay`, `hard-light`,
724
749
  accents; expensive for hundreds of objects. The Canvas renderer reaches all
725
750
  eight through `globalCompositeOperation` instead, at no extra cost.
726
751
 
727
- Anything else collapses to `"normal"`, and so do 3D meshes and `Gradient` fills
728
- whatever you ask for — the latter with a one-time console warning.
729
- `renderer.setBlendMode(mode)` returns the mode it actually applied, so comparing
730
- that against your request detects the fallback.
752
+ Anything else collapses to `"normal"`. `renderer.setBlendMode(mode)` returns
753
+ the mode it actually applied, so comparing that against your request detects
754
+ an unknown name, and `none` on Canvas.
755
+
756
+ Two kinds of draw cannot take the eight composited modes on the GPU backends:
757
+ a 3D mesh and a `Gradient` fill draw them as `"normal"`, with a one-time
758
+ console warning naming which. `setBlendMode` cannot see that one, since it
759
+ happens at the draw, and still reports the mode you asked for.
731
760
 
732
761
  ## The Canvas fallback
733
762
 
@@ -767,9 +796,11 @@ same question after construction.
767
796
  | ported shader renders upside down | a hand-bound `toFrameTexture()` capture — GL is bottom-up, WebGPU top-down |
768
797
  | shadow/smear offset flips on some draws | vertical UV offset not multiplied by `uUVYDir` |
769
798
  | frame rate collapses with many blended sprites | an *advanced* mode (overlay, darken, …) — each draw is a capture plus a composite |
799
+ | `overlay` (or another composited mode) does nothing on a mesh or a gradient fill, with a console warning | those two draws take the eight composited modes as `"normal"` on the GPU backends |
800
+ | an effect from `loader.getShader()` stops drawing after a level change | its asset, or the last bundle listing it, was unloaded, which destroys the shared instance |
770
801
  | many sprites with the same effect still cost a draw each | an effect instance per sprite, or a uniform set to a different value per sprite — share one instance and group sprites by value |
771
802
  | animated shader never moves | `setTime` fed milliseconds, or the shader declares no `uTime` |
772
- | squash and stretch only bulges, the height never changes, under Matter | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |
803
+ | squash and stretch only bulges, the height never changes, under matter, planck or Rapier (or a rotating built-in body) | the adapter rewrites `currentTransform` every step; set the scale from the sprite's `update()` |
773
804
  | `SquishEffect` squishes the whole screen instead of the sprite | it is chained with another effect, or on the camera; give the sprite it alone |
774
805
  | 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 |
775
806
 
@@ -55,7 +55,7 @@ game events in a `.ts` project.
55
55
  | event | when |
56
56
  |---|---|
57
57
  | `GAME_BEFORE_UPDATE` / `GAME_UPDATE` / `GAME_AFTER_UPDATE` | around the world update — `GAME_UPDATE` is **not** emitted while paused |
58
- | `GAME_BEFORE_DRAW` / `GAME_AFTER_DRAW` | around the draw pass |
58
+ | `GAME_BEFORE_DRAW` / `GAME_AFTER_DRAW` | around the draw pass, on frames that redraw: one where no update reported a change is not drawn |
59
59
  | `WORLD_STEP` | after the physics step |
60
60
  | `TICK` | the raw engine tick |
61
61
 
@@ -67,7 +67,7 @@ game events in a `.ts` project.
67
67
  | `STATE_CHANGE`, `STATE_PAUSE`, `STATE_RESUME`, `STATE_STOP` | state machine |
68
68
  | `STAGE_RESET` | a stage was reset |
69
69
  | `LEVEL_LOADED` | a Tiled level finished loading |
70
- | `LOADER_PROGRESS`, `LOADER_COMPLETE`, `LOADER_ERROR` | asset loading |
70
+ | `LOADER_PROGRESS`, `LOADER_COMPLETE`, `LOADER_ERROR` | asset loading: every `loader.preload()`, and a bundle loaded with `loader.load(id)` (20.14); not one asset loaded with `loader.load(descriptor)` |
71
71
  | `BLUR` / `FOCUS` | the page lost or regained focus |
72
72
  | `CANVAS_ONRESIZE`, `VIEWPORT_ONRESIZE` | relayout hooks |
73
73
  | `POINTERMOVE` | global pointer movement |
@@ -153,6 +153,7 @@ Use `removeChild()`, or flag the work and drain it from `Stage.update`.
153
153
  | crash during a stage switch | callback ran mid-teardown — guard with `state.isCurrent` |
154
154
  | crash after a collision | `removeChildNow()` inside a contact callback — use `removeChild()` |
155
155
  | progress callbacks never fire | assigned `loader.onProgress` (removed in 20.3) instead of subscribing to `LOADER_PROGRESS` |
156
+ | a `LOADER_COMPLETE` handler (a `state.change` to the first screen) runs again mid-game | a later `preload()`, or a level's bundle loaded with `loader.load(id)`, emits it too; subscribe with `event.once`, or use `preload()`'s promise or callback |
156
157
  | `onActivateEvent` never runs | the child was added to a container not yet attached to the world |
157
158
  | `onDeactivateEvent` runs twice when a group is removed (a count goes negative, a light or listener is released twice) | the group's `onDeactivateEvent` and its `destroy()` both reached the children, before 20.13.0 — now once per `onActivateEvent` |
158
159
  | `onDeactivateEvent` never runs on a child of a group built off-world and destroyed | it never had `onActivateEvent` either: the pair is only sent to children that entered the world |