melonjs 20.8.0 → 20.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +1 -0
  2. package/build/application/application.d.ts +66 -8
  3. package/build/application/application.d.ts.map +1 -1
  4. package/build/application/defaultApplicationSettings.d.ts +1 -0
  5. package/build/application/defaultApplicationSettings.d.ts.map +1 -1
  6. package/build/application/resize.d.ts +25 -0
  7. package/build/application/resize.d.ts.map +1 -1
  8. package/build/application/scaleMethods.d.ts +13 -3
  9. package/build/application/scaleMethods.d.ts.map +1 -1
  10. package/build/application/settings.d.ts +68 -13
  11. package/build/application/settings.d.ts.map +1 -1
  12. package/build/audio/audio.d.ts +29 -5
  13. package/build/audio/audio.d.ts.map +1 -1
  14. package/build/audio/autopause.d.ts +36 -0
  15. package/build/audio/autopause.d.ts.map +1 -0
  16. package/build/audio/backend/engine.d.ts +10 -0
  17. package/build/audio/backend/engine.d.ts.map +1 -1
  18. package/build/audio/backend/sound.d.ts.map +1 -1
  19. package/build/audio/backend/spatial.d.ts.map +1 -1
  20. package/build/audio/context.d.ts +35 -0
  21. package/build/audio/context.d.ts.map +1 -0
  22. package/build/audio/playback.d.ts.map +1 -1
  23. package/build/audio/procedural.d.ts.map +1 -1
  24. package/build/audio/spatial.d.ts +87 -0
  25. package/build/audio/spatial.d.ts.map +1 -0
  26. package/build/audio/state.d.ts +1 -0
  27. package/build/audio/state.d.ts.map +1 -1
  28. package/build/audio/types.d.ts +46 -6
  29. package/build/audio/types.d.ts.map +1 -1
  30. package/build/camera/camera2d.d.ts.map +1 -1
  31. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  32. package/build/camera/effects/mask_effect.d.ts.map +1 -1
  33. package/build/camera/effects/shake_effect.d.ts.map +1 -1
  34. package/build/geometries/box3d.d.ts +1 -1
  35. package/build/geometries/box3d.d.ts.map +1 -1
  36. package/build/geometries/ellipse.d.ts +1 -1
  37. package/build/geometries/ellipse.d.ts.map +1 -1
  38. package/build/geometries/line.d.ts +1 -1
  39. package/build/geometries/line.d.ts.map +1 -1
  40. package/build/geometries/point.d.ts +1 -1
  41. package/build/geometries/point.d.ts.map +1 -1
  42. package/build/geometries/polygon.d.ts +1 -1
  43. package/build/geometries/polygon.d.ts.map +1 -1
  44. package/build/geometries/rectangle.d.ts +1 -1
  45. package/build/geometries/rectangle.d.ts.map +1 -1
  46. package/build/geometries/roundrect.d.ts +1 -1
  47. package/build/geometries/roundrect.d.ts.map +1 -1
  48. package/build/geometries/sphere.d.ts +1 -1
  49. package/build/geometries/sphere.d.ts.map +1 -1
  50. package/build/index.d.ts +9 -2
  51. package/build/index.d.ts.map +1 -1
  52. package/build/index.js +38737 -35039
  53. package/build/index.js.map +4 -4
  54. package/build/input/gamepad.d.ts +129 -3
  55. package/build/input/gamepad.d.ts.map +1 -1
  56. package/build/level/gltf/GLTFModel.d.ts +5 -4
  57. package/build/level/gltf/GLTFModel.d.ts.map +1 -1
  58. package/build/level/gltf/GLTFScene.d.ts.map +1 -1
  59. package/build/level/tiled/TMXObjectFactory.d.ts +3 -2
  60. package/build/level/tiled/TMXObjectFactory.d.ts.map +1 -1
  61. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  62. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts +1 -4
  63. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  64. package/build/loader/cache.d.ts +220 -0
  65. package/build/loader/cache.d.ts.map +1 -1
  66. package/build/loader/loader.d.ts +20 -349
  67. package/build/loader/loader.d.ts.map +1 -1
  68. package/build/loader/parsers/gltf.d.ts +115 -0
  69. package/build/loader/parsers/gltf.d.ts.map +1 -1
  70. package/build/math/color.d.ts +1 -1
  71. package/build/math/color.d.ts.map +1 -1
  72. package/build/math/matrix2d.d.ts +1 -1
  73. package/build/math/matrix2d.d.ts.map +1 -1
  74. package/build/math/matrix3d.d.ts +1 -1
  75. package/build/math/matrix3d.d.ts.map +1 -1
  76. package/build/math/observableVector2d.d.ts +14 -2
  77. package/build/math/observableVector2d.d.ts.map +1 -1
  78. package/build/math/observableVector3d.d.ts +15 -1
  79. package/build/math/observableVector3d.d.ts.map +1 -1
  80. package/build/math/vector2d.d.ts +31 -1
  81. package/build/math/vector2d.d.ts.map +1 -1
  82. package/build/math/vector3d.d.ts +38 -1
  83. package/build/math/vector3d.d.ts.map +1 -1
  84. package/build/math/vertex.d.ts.map +1 -1
  85. package/build/particles/emitter.d.ts.map +1 -1
  86. package/build/particles/particle.d.ts +7 -1
  87. package/build/particles/particle.d.ts.map +1 -1
  88. package/build/particles/settings.d.ts +35 -0
  89. package/build/particles/settings.d.ts.map +1 -1
  90. package/build/physics/bounds.d.ts +1 -1
  91. package/build/physics/bounds.d.ts.map +1 -1
  92. package/build/physics/broadphase/octree.d.ts +24 -0
  93. package/build/physics/broadphase/octree.d.ts.map +1 -1
  94. package/build/physics/broadphase/quadtree.d.ts +31 -0
  95. package/build/physics/broadphase/quadtree.d.ts.map +1 -1
  96. package/build/physics/builtin/body.d.ts +14 -0
  97. package/build/physics/builtin/body.d.ts.map +1 -1
  98. package/build/pool.d.ts +4 -0
  99. package/build/pool.d.ts.map +1 -1
  100. package/build/renderable/colorlayer.d.ts +13 -0
  101. package/build/renderable/colorlayer.d.ts.map +1 -1
  102. package/build/renderable/container.d.ts.map +1 -1
  103. package/build/renderable/groundshadow.d.ts.map +1 -1
  104. package/build/renderable/instanced_mesh.d.ts +56 -0
  105. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  106. package/build/renderable/mesh.d.ts +97 -11
  107. package/build/renderable/mesh.d.ts.map +1 -1
  108. package/build/renderable/renderable.d.ts +47 -3
  109. package/build/renderable/renderable.d.ts.map +1 -1
  110. package/build/renderable/sprite.d.ts +2 -1
  111. package/build/renderable/sprite.d.ts.map +1 -1
  112. package/build/renderable/sprite3d.d.ts +6 -2
  113. package/build/renderable/sprite3d.d.ts.map +1 -1
  114. package/build/renderable/text/bitmaptext.d.ts +63 -2
  115. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  116. package/build/renderable/text/bitmaptextdata.d.ts +1 -1
  117. package/build/renderable/text/bitmaptextdata.d.ts.map +1 -1
  118. package/build/renderable/text/glypheffect.d.ts +44 -0
  119. package/build/renderable/text/glypheffect.d.ts.map +1 -0
  120. package/build/renderable/text/text.d.ts +17 -0
  121. package/build/renderable/text/text.d.ts.map +1 -1
  122. package/build/renderable/trail.d.ts.map +1 -1
  123. package/build/renderable/trail3d.d.ts +174 -0
  124. package/build/renderable/trail3d.d.ts.map +1 -0
  125. package/build/renderable/trigger.d.ts +2 -1
  126. package/build/renderable/trigger.d.ts.map +1 -1
  127. package/build/renderable/ui/panelpaint.d.ts +5 -0
  128. package/build/renderable/ui/panelpaint.d.ts.map +1 -0
  129. package/build/renderable/ui/progressbar.d.ts +2 -4
  130. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  131. package/build/renderable/ui/uipanel.d.ts +245 -0
  132. package/build/renderable/ui/uipanel.d.ts.map +1 -0
  133. package/build/renderable/ui/uitextbutton.d.ts +30 -1
  134. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  135. package/build/state/state.d.ts.map +1 -1
  136. package/build/system/bootstrap.d.ts.map +1 -1
  137. package/build/system/legacy_pool.d.ts +22 -2
  138. package/build/system/legacy_pool.d.ts.map +1 -1
  139. package/build/system/pool.d.ts +5 -8
  140. package/build/system/pool.d.ts.map +1 -1
  141. package/build/tweens/tween.d.ts +1 -1
  142. package/build/tweens/tween.d.ts.map +1 -1
  143. package/build/video/effects/shadereffect.d.ts.map +1 -1
  144. package/build/video/effects/squish.d.ts +74 -0
  145. package/build/video/effects/squish.d.ts.map +1 -0
  146. package/build/video/effects/tintPulse.d.ts +0 -5
  147. package/build/video/effects/tintPulse.d.ts.map +1 -1
  148. package/build/video/effects/wave.d.ts +0 -5
  149. package/build/video/effects/wave.d.ts.map +1 -1
  150. package/build/video/gpu/batcher.d.ts +8 -2
  151. package/build/video/gpu/batcher.d.ts.map +1 -1
  152. package/build/video/gradient.d.ts +1 -0
  153. package/build/video/gradient.d.ts.map +1 -1
  154. package/build/video/renderer.d.ts +16 -3
  155. package/build/video/renderer.d.ts.map +1 -1
  156. package/build/video/rendertarget/canvasrendertarget.d.ts.map +1 -1
  157. package/build/video/rendertarget/render_target_pool.d.ts.map +1 -1
  158. package/build/video/rendertarget/rendertarget.d.ts +12 -5
  159. package/build/video/rendertarget/rendertarget.d.ts.map +1 -1
  160. package/build/video/rendertarget/webglrendertarget.d.ts.map +1 -1
  161. package/build/video/rendertarget/webgpurendertarget.d.ts +6 -12
  162. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  163. package/build/video/texture/atlas.d.ts +1 -1
  164. package/build/video/texture/atlas.d.ts.map +1 -1
  165. package/build/video/texture/texture2d.d.ts +6 -0
  166. package/build/video/texture/texture2d.d.ts.map +1 -1
  167. package/build/video/webgl/batchers/lit_quad_batcher.d.ts +2 -11
  168. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  169. package/build/video/webgl/batchers/material_batcher.d.ts +5 -0
  170. package/build/video/webgl/batchers/material_batcher.d.ts.map +1 -1
  171. package/build/video/webgl/batchers/mesh_batcher.d.ts +21 -0
  172. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  173. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  174. package/build/video/webgl/buffer/deletable.d.ts +15 -0
  175. package/build/video/webgl/buffer/deletable.d.ts.map +1 -0
  176. package/build/video/webgl/buffer/instance_buffer.d.ts.map +1 -1
  177. package/build/video/webgl/buffer/retained_geometry.d.ts.map +1 -1
  178. package/build/video/webgl/buffer/uniformblock.d.ts.map +1 -1
  179. package/build/video/webgl/buffer/vertexstate.d.ts.map +1 -1
  180. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  181. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  182. package/build/video/webgpu/pipeline/cache.d.ts.map +1 -1
  183. package/build/video/webgpu/texture/compressed.d.ts.map +1 -1
  184. package/build/video/webgpu/texture/store.d.ts.map +1 -1
  185. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/skills/AGENTS.md +1 -1
  188. package/skills/melonjs/SKILL.md +2 -0
  189. package/skills/melonjs-3d/SKILL.md +192 -18
  190. package/skills/melonjs-3d-assets/SKILL.md +36 -2
  191. package/skills/melonjs-audio/SKILL.md +229 -8
  192. package/skills/melonjs-camera-and-drawing/SKILL.md +73 -3
  193. package/skills/melonjs-display-and-scaling/SKILL.md +273 -0
  194. package/skills/melonjs-effects-and-shaders/SKILL.md +54 -10
  195. package/skills/melonjs-events/SKILL.md +8 -6
  196. package/skills/melonjs-input/SKILL.md +100 -4
  197. package/skills/melonjs-loading-assets/SKILL.md +22 -4
  198. package/skills/melonjs-math/SKILL.md +313 -0
  199. package/skills/melonjs-particles-and-trails/SKILL.md +88 -14
  200. package/skills/melonjs-performance/SKILL.md +57 -22
  201. package/skills/melonjs-physics/SKILL.md +2 -1
  202. package/skills/melonjs-renderables/SKILL.md +6 -1
  203. package/skills/melonjs-scenes-and-state/SKILL.md +14 -3
  204. package/skills/melonjs-sprites-and-animation/SKILL.md +9 -4
  205. package/skills/melonjs-tilemaps/SKILL.md +40 -12
  206. package/skills/melonjs-ui-and-text/SKILL.md +382 -4
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-tilemaps
3
- description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, level.load async, await level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax."
3
+ description: "Use this skill for Tiled maps in melonJS — loading TMX/TSX levels, spawning entities from Tiled objects, collision shapes authored in Tiled, isometric and hexagonal maps, and image layers. Covers registerTiledObjectClass and the pool.register name contract, camera bounds, compressed maps needing the inflate plugin, and the level director API. Triggers on: Tiled, TMX, TSX, tilemap, level.load, level.load async, await level.load, tileset, ImageLayer, isometric, hexagonal, staggered, pool.register, Collectable, Trigger, object layer, collision layer, parallax, registerTiledObjectClass, registerTiledObjectFactory."
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -38,6 +38,10 @@ also skip `src` and pass the map inline via `data` (with `format: "json"` or
38
38
  `level <id> not found` for an unknown id — synchronously, in both forms, because
39
39
  that is a typo rather than a load failure.
40
40
 
41
+ Those `game.` defaults are the engine's own, and `game` is whichever
42
+ `Application` finished `init()` last. **Pass `container` explicitly** from a
43
+ `Stage`, which has the right one: `level.load("map", { container: this.app.world })`.
44
+
41
45
  **`level.load` is deferred while the game loop is running.** It calls
42
46
  `state.stop()` and finishes the load on a timer, after the current frame, so by
43
47
  default it returns `true` before anything is in the world. Two ways to sequence
@@ -72,19 +76,30 @@ Tiled **class**, then its **name**, then the structural fallbacks `"text"`,
72
76
  `"tile"` and `"shape"`.
73
77
 
74
78
  ```js
75
- pool.register("mainPlayer", PlayerEntity); // ← matches Tiled class OR name
79
+ import { registerTiledObjectClass, registerTiledObjectFactory } from "melonjs";
80
+
81
+ registerTiledObjectClass("Enemy", Enemy); // ← matches Tiled class OR name
82
+ registerTiledObjectFactory("Spine", (settings, map) => { /* → Renderable */ });
76
83
  level.load("map1");
77
84
  ```
78
85
 
79
- `pool.register(className, classObj, recycling)` also registers the class as a
80
- Tiled object factory (and again under an `me.`-prefixed alias), unless you set
81
- `pool.autoRegisterTiled = false`. The dedicated entry points are:
86
+ These are the entry points to use. The registry is Tiled's own, held by the
87
+ object factory, and has nothing to do with object pooling.
82
88
 
83
- ```js
84
- import { registerTiledObjectClass, registerTiledObjectFactory } from "melonjs";
89
+ `pool.register(className, classObj)` **also** works and registers a Tiled
90
+ factory as a side effect, under the name and again under an `me.`-prefixed
91
+ alias, unless you set `pool.autoRegisterTiled = false`. That is the older
92
+ spelling, kept working for maps and games written against it, and it now
93
+ prints a one-off deprecation notice: `me.pool` is deprecated since 18.0.0.
85
94
 
86
- registerTiledObjectClass("Enemy", Enemy); // new Enemy(x, y, settings)
87
- registerTiledObjectFactory("Spine", (settings, map) => { /* → Renderable */ });
95
+ Its third argument no longer buys anything here. It used to make Tiled
96
+ objects recyclable; they are constructed directly now, and the container
97
+ returns children to typed pools rather than to the name-keyed one. To pool a
98
+ class you spawn constantly, give it a `createPool` and register it for Tiled
99
+ separately:
100
+
101
+ ```js
102
+ registerTiledObjectClass("mainPlayer", PlayerEntity);
88
103
  ```
89
104
 
90
105
  Register **before** loading. With no match the object falls through to the
@@ -94,11 +109,24 @@ not error, it just has none of your behaviour.
94
109
  `registerTiledObjectClass` **throws** if you register a *different* constructor
95
110
  under a name already taken (re-registering the same one is a no-op).
96
111
  `registerTiledObjectFactory` overwrites and only `console.warn`s, so it is the
97
- one to use for overriding a built-in such as `"shape"`.
112
+ one to use for replacing something already registered.
113
+
114
+ **Overriding a built-in name always works.** The engine's own classes are
115
+ DEFAULTS: they fill a name nothing else has claimed, and yours takes it over
116
+ whenever you register, before or after a map has loaded.
117
+
118
+ ```js
119
+ registerTiledObjectClass("Trigger", MyTrigger); // whenever you like
120
+ ```
121
+
122
+ The throw is for a clash between two of *your* classes under one name. For
123
+ that case `registerTiledObjectFactory` overwrites and only warns.
98
124
 
99
125
  melonJS pre-registers `Renderable`, `Sprite`, `NineSliceSprite`, `Text`,
100
- `BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable` and `Trigger`
101
- as Tiled classes, so `Collectable` and `Trigger` work out of the box.
126
+ `BitmapText`, `ImageLayer`, `ColorLayer`, `Light2d`, `Collectable`, `Trigger`
127
+ and `Entity` as Tiled classes, so `Collectable` and `Trigger` work out of the
128
+ box. Each is registered under its `me.`-prefixed alias as well, so a map
129
+ authored against melonJS 1.x finds them where it looks.
102
130
 
103
131
  A `Trigger` forwards a **fixed list** of settings to `level.load()` — not
104
132
  whatever you pass it. Today that list is `container`, `onLoaded`, `flatten`,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: melonjs-ui-and-text
3
- description: "Use this skill for HUDs, buttons, menus, dialogue panels, progress bars and on-screen text in melonJS. Covers UIBaseElement/UISpriteElement/UITextButton, ProgressBar, Draggable and DropTarget, the floating screen-space container pattern, which of two overlapping panels gets the pointer, Text and BitmapText, web font loading, and NineSliceSprite panels. Triggers on: UI, HUD, button, UIBaseElement, UISpriteElement, UITextButton, ProgressBar, progress bar, health bar, gauge, Draggable, DropTarget, menu, dialogue, overlapping panels, moveToTop, onOver, onClick, Tween, easing, animate a label, Text, BitmapText, fillStyle, fillGradient, gradient text, tint, font, fontface, wordWrapWidth, NineSliceSprite, score display, floating."
3
+ description: "Use this skill for HUDs, buttons, menus, dialogue panels, progress bars and on-screen text in melonJS. Covers UIBaseElement/UISpriteElement/UITextButton, ProgressBar, UIPanel, Draggable and DropTarget, the floating screen-space container pattern, which of two overlapping panels gets the pointer, Text and BitmapText, web font loading, and NineSliceSprite panels. Triggers on: UI, HUD, button, UIBaseElement, UISpriteElement, UITextButton, ProgressBar, progress bar, health bar, gauge, Draggable, DropTarget, menu, dialogue, overlapping panels, moveToTop, onOver, onClick, bindKey, keyboard shortcut, button keyboard, Tween, easing, animate a label, wavy text, per-glyph effect, glyphEffect, shake text, rainbow text, Text, BitmapText, fillStyle, fillGradient, gradient text, tint, font, fontface, wordWrapWidth, NineSliceSprite, score display, floating., fullscreen, full screen, requestFullscreen, exitFullscreen, isFullscreen, fullscreen button, fullscreen option"
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -112,6 +112,25 @@ loader.preload([
112
112
  Reach for it over `Text` when the text is mostly static, has to stay crisp at
113
113
  integer scales, or wants recolouring without a re-bake.
114
114
 
115
+ ### Do not build a throwaway label to measure a string
116
+
117
+ A `Text` owns a canvas bake and a GPU texture, and only `destroy()` frees
118
+ them — which is `@internal` and absent from the published types, so a game
119
+ cannot free a label it owns outside a container. A label built purely to
120
+ measure something therefore strands both, and if it is built in a stage's
121
+ `onResetEvent` it strands another on every entry to that state.
122
+
123
+ `measureText(text?)` measures any string on a label that already exists,
124
+ including one it is not currently holding:
125
+
126
+ ```js
127
+ const widest = hudScore.measureText("00:00").width;
128
+ ```
129
+
130
+ Advance widths scale linearly with the point size for one family, so measuring
131
+ on an existing label and scaling the result is exact. For labels you genuinely
132
+ spawn in bulk, `getPool("text")` recycles the bake.
133
+
115
134
  ### `fillStyle` means two different things
116
135
 
117
136
  The name is shared and the behaviour is not, which is the trap when moving a
@@ -122,9 +141,134 @@ label from one class to the other:
122
141
  | what `fillStyle` holds | a real `Color`, the fill | `Renderable#tint`, under another name |
123
142
  | white | white glyphs | the ABSENCE of a tint |
124
143
  | gradient | yes, via `fillGradient` | no: a tint is one colour over a page image |
144
+ | recolour it later | `label.fillStyle.parseCSS("#ff4040")` — the field is a `Color` with no setter, and ASSIGNING a string replaces it, after which the glyphs stop filling entirely and nothing is logged | `label.fillStyle = "#ff4040"` — a real setter that parses the string |
125
145
  | stroke | `strokeStyle` + `lineWidth` | none |
126
146
  | `size` | pixels | a RATIO of the authored size |
127
147
 
148
+ ### Per-glyph wave, shake and colour: `glyphEffect`
149
+
150
+ Animating individual letters does **not** mean one renderable per character.
151
+ `BitmapText#glyphEffect` offsets and tints each glyph inside the batch the
152
+ label already draws, so an animated 100-character line stays one draw call and
153
+ costs no extra objects:
154
+
155
+ ```js
156
+ label.glyphEffect = (out, ctx) => {
157
+ // phase from the glyph's POSITION, not its index — see below
158
+ out.offsetY = Math.sin(ctx.time * 0.005 + ctx.x * 0.02) * 6; // wave
159
+ out.tint.setColor(255, 128, 128); // per glyph
160
+ };
161
+ label.glyphEffect = null; // back to the plain path
162
+ ```
163
+
164
+ `out` and `ctx` are **reused**: read `ctx` during the call and write `out`, but
165
+ never keep either. `out.offsetX` / `offsetY` start at zero and `out.tint` at
166
+ opaque white on every glyph, so an effect that only touches some glyphs leaves
167
+ the rest alone. `ctx` carries `index` (across lines, line breaks excluded),
168
+ `char`, `code`, `x`, `y` and `time`.
169
+
170
+ Four things that catch people:
171
+
172
+ - **`time` advances in `update`, not in `draw`.** A label drawn through two
173
+ cameras animates once, not twice. It is the milliseconds ACCUMULATED across
174
+ updates while an effect was set, and nothing resets it, so swapping the
175
+ callback continues the same clock rather than starting a new one.
176
+ - **Effects move pixels, not layout.** Metrics, wrapping, alignment,
177
+ `visibleCharacters` and the bounds all ignore the offsets, so the pen advance
178
+ and kerning are exactly as without one. The flip side is that **offsets do
179
+ not extend the culling bounds**: a big wave near the screen edge can clip, so
180
+ keep the measured text in view.
181
+ - **Canvas pays per distinct colour.** WebGL and WebGPU carry the tint in the
182
+ per-vertex colour, so batching survives. The Canvas renderer instead caches a
183
+ tinted copy of the whole font page per colour, and that cache is unbounded
184
+ for the renderer's life, so a colour driven by `ctx.time` allocates a
185
+ page-sized canvas every frame there. On Canvas, keep the palette finite or
186
+ animate only the offsets.
187
+ - **`tint` is multiplicative**, like `fillStyle` above: white leaves the glyph
188
+ alone, and `out.tint` multiplies whatever the label's own `fillStyle` is.
189
+
190
+ `Text` has no equivalent, since it rasterises the whole string into one texture.
191
+
192
+ ### Making it read as motion
193
+
194
+ Three things decide whether a wave looks alive or mechanical, and none of them
195
+ is the amplitude.
196
+
197
+ - **Take the phase from `ctx.x`, not `ctx.index`.** This is the big one.
198
+ Stepping per glyph advances the wave as far across a narrow `I` as across a
199
+ wide `M`, so the wavelength stretches and squashes with whatever the line
200
+ happens to spell, and the eye reads the unevenness as a mechanism. `ctx.x`
201
+ gives the wave one fixed length in pixels and lets it travel through the
202
+ text at a constant rate, whatever the letters are.
203
+ - **Round the offsets if the frame is upscaled.** They are added to the glyph
204
+ destination unchanged, so a fractional offset in a small design resolution
205
+ that is then magnified with nearest sampling makes the glyph's OWN pixels
206
+ come out uneven — and differently uneven each frame. That is a shimmer, not
207
+ movement. `Math.round` the offset and it steps cleanly, for the same reason
208
+ `subPixel` defaults to false. Whole pixels mean few steps, so the amplitude
209
+ has to be large enough to give you several.
210
+ - **A shake is not a sine.** A sine moves the whole line as one body, which is
211
+ a wave however fast you run it. A shake gives each glyph a direction
212
+ unrelated to its neighbours: pick a random angle per glyph, hold it for a
213
+ tick, interpolate to the next, and scale the whole thing by a decay if it is
214
+ an impact rather than a state.
215
+
216
+ Starting points that look right across sizes, as fractions of the font size:
217
+ an amplitude around an eighth of it, a wavelength around ten times it, and a
218
+ little under 5 radians per second. For a shake, re-target each glyph roughly
219
+ 20 to 40 times a second.
220
+
221
+ `ctx.time` starts when the effect is **assigned**, so attach it at the moment
222
+ you want to be t=0 — on the frame the hit lands, not when the label was built.
223
+ A label reused across lives carries the old clock, so a decay attached once
224
+ will already be spent the second time.
225
+
226
+ Both, whole:
227
+
228
+ ```js
229
+ import { BitmapText } from "melonjs";
230
+
231
+ const title = new BitmapText(app.viewport.width / 2, 60, {
232
+ font: "display",
233
+ textAlign: "center",
234
+ text: "JUNGLE RABBIT",
235
+ });
236
+ title.floating = true;
237
+ app.world.addChild(title);
238
+
239
+ // A wave: four pixels deep, a ~1.3s cycle, and a wavelength of roughly ten
240
+ // times the font size. Phase from ctx.x, offset rounded.
241
+ title.glyphEffect = (out, ctx) => {
242
+ out.offsetY = Math.round(Math.sin(ctx.time * 0.005 + ctx.x * 0.02) * 4);
243
+ };
244
+
245
+ // A shake: each glyph picks its own direction, holds it for a tick, slides to
246
+ // the next, and the whole thing decays. Deterministic, so the same hit shakes
247
+ // the same way every time.
248
+ const angle = (i, tick) => {
249
+ const h = Math.sin(i * 127.1 + tick * 311.7) * 43758.5453;
250
+ return (h - Math.floor(h)) * Math.PI * 2;
251
+ };
252
+ const impact = (amplitude, durationMs, rate) => (out, ctx) => {
253
+ const left = 1 - ctx.time / durationMs;
254
+ if (left <= 0) {
255
+ return; // settled; `out` is already zeroed
256
+ }
257
+ const amp = amplitude * left * left;
258
+ const tick = (ctx.time / 1000) * rate;
259
+ const held = Math.floor(tick);
260
+ const k = tick - held;
261
+ const a = angle(ctx.index, held);
262
+ const b = angle(ctx.index, held + 1);
263
+ out.offsetX = Math.round((Math.sin(a) + (Math.sin(b) - Math.sin(a)) * k) * amp);
264
+ out.offsetY = Math.round((Math.cos(a) + (Math.cos(b) - Math.cos(a)) * k) * amp);
265
+ };
266
+
267
+ // assigned at the moment of the hit, which is what makes ctx.time start here
268
+ banner.setText("CAPSIZED!");
269
+ banner.glyphEffect = impact(3, 650, 24);
270
+ ```
271
+
128
272
  ## The HUD pattern
129
273
 
130
274
  A HUD is a `floating` container at a high z, built once and re-added:
@@ -141,9 +285,10 @@ class HUD extends Container {
141
285
  app.world.addChild(new HUD(app), 100); // explicit z — not `.z = Infinity`
142
286
  ```
143
287
 
144
- There is no `UIContainer` class. Use `Container`, or `UIBaseElement` when the
145
- panel itself must react to the pointer — it is a `Container` that already sets
146
- `floating = true` and `isKinematic = false`.
288
+ There is no `UIContainer` class. Use `Container` for a bare grouping,
289
+ `UIPanel` for anything with a background or a border (see below), or
290
+ `UIBaseElement` when you want the pointer handling and nothing drawn — it is a
291
+ `Container` that already sets `floating = true` and `isKinematic = false`.
147
292
 
148
293
  Three things matter here:
149
294
 
@@ -231,6 +376,88 @@ Two more worth knowing:
231
376
  - **`trackColor: null` leaves the bar hollow**, which is what a HUD over a busy
232
377
  scene usually wants: the border is then the only chrome.
233
378
 
379
+ ### Panels and cards: `UIPanel`
380
+
381
+ A dialog, a menu card, a HUD frame: a rectangle with a fill, an optional
382
+ border and children on top. `UIPanel` is a `UIBaseElement`, so it is a
383
+ container that carries pointer events.
384
+
385
+ ```js
386
+ import { UIPanel, Text } from "melonjs";
387
+
388
+ const card = new UIPanel(cx - 200, cy - 120, {
389
+ width: 400, height: 240,
390
+ fillColor: "#0a1526", // null leaves it transparent
391
+ borderColor: "rgba(130, 200, 240, 0.55)", // null for none
392
+ borderWidth: 2,
393
+ radius: 14,
394
+ });
395
+ card.addChild(new Text(200, 24, { text: "PAUSED", textAlign: "center" }));
396
+ app.world.addChild(card, -150); // small depth: see the 3D section below
397
+ ```
398
+
399
+ **`fillColor`, not `backgroundColor`.** `Container` already has a
400
+ `backgroundColor` and it does something else entirely: it is handed to
401
+ `clearColor` and wipes the whole render target, so a panel setting it repaints
402
+ the screen. The names here match `ProgressBarSettings`.
403
+
404
+ **The border is drawn INSIDE the box.** `width` and `height` are the outer
405
+ size, border included, so a panel never paints past the rectangle you gave it
406
+ and its `getBounds()`, which is what the pointer dispatcher hit-tests, stays
407
+ that rectangle. One thing to know with a `radius`: keep `borderWidth` at 2 or
408
+ more. A rounded border has to be stroked, and a stroke at exactly 1 goes out as
409
+ plain lines, which drops a pixel where each arc meets its straight edge. A
410
+ square border is four fills and is fine at 1.
411
+
412
+ **`blocking` (default `true`)** makes the panel consume clicks and hovers that
413
+ miss its children, which is what a dialog wants — a click on the card, or on
414
+ the dimmed scene behind it, must not reach the game. Children are unaffected:
415
+ the dispatcher asks a child before the container holding it. Set it `false` for
416
+ a frame drawn over a playfield that still has to receive clicks.
417
+
418
+ A full-screen panel with a translucent `fillColor` and `borderColor: null` is
419
+ the veil behind a modal, and it blocks for free.
420
+
421
+ **`bobAmplitude` / `bobPeriod`** give the panel the slow float an idle
422
+ dialogue or pause panel usually has, so a screen waiting for input does not
423
+ look frozen:
424
+
425
+ ```js
426
+ card.bobAmplitude = 3; // pixels either side; 2 to 4 is the usual range
427
+ card.bobPeriod = 2500; // ms for a full up-and-down cycle
428
+ ```
429
+
430
+ Three things to know. **The panel is clickable where it is drawn**: the offset
431
+ goes into `getAbsolutePosition()`, which is what the bounds pipeline reads, so
432
+ the hit area of the panel and of every widget inside it follows the float at
433
+ any depth. `pos` is left alone, so it still reads and writes the resting place.
434
+ It is fed from `update(dt)`, so it stops with the rest of the game on
435
+ `state.pause()` unless the panel sets `updateWhenPaused`, which a pause menu
436
+ does anyway. And do not go below an amplitude of 2: the offset is rounded to whole
437
+ pixels, so an amplitude of 1 has three positions to move through and twitches
438
+ instead of drifting.
439
+
440
+ One consequence worth knowing: `getBounds()` on a bobbing panel answers
441
+ differently each frame, so do not cache it or compare two children's bounds
442
+ while a bob is running.
443
+
444
+ ### A pause overlay: `updateWhenPaused` cascades
445
+
446
+ `state.pause()` stops every world child stepping while rendering carries on, so
447
+ the overlay that reads the key to UNPAUSE has to opt out:
448
+
449
+ ```js
450
+ const overlay = new UIPanel(0, 0, { width: w, height: h, fillColor: "rgba(4,8,16,0.72)" });
451
+ overlay.updateWhenPaused = true; // the whole subtree, not just this one
452
+ ```
453
+
454
+ The flag covers the container's **entire subtree**, so nested labels, buttons
455
+ and the card inside the veil all keep stepping without being flagged one by
456
+ one. That matters for more than their `update`: `inViewport` is computed in the
457
+ same loop that honours the pause, so a skipped child keeps whatever it had —
458
+ `false` for one built while paused — and `Container.draw` then declines to draw
459
+ it, silently. A panel whose labels never appear while paused is this.
460
+
234
461
  ### Buttons: extend the handlers, do not bind listeners
235
462
 
236
463
  `UISpriteElement` is a `Sprite` that already registers itself for pointer
@@ -265,6 +492,106 @@ default on a plain `Renderable` — is skipped by the broadphase and receives
265
492
  nothing. `UISpriteElement` and `UIBaseElement` clear it for you; anything else
266
493
  you make clickable has to clear it itself.
267
494
 
495
+ ### Keyboard shortcuts on a button: `bindKey`
496
+
497
+ A `UITextButton` can be activated from the keyboard as well as the pointer:
498
+
499
+ ```js
500
+ const resume = new UITextButton(x, y, {
501
+ font: "PressStart2P", text: "RESUME",
502
+ bindKey: input.KEY.ENTER, // a keycode, or an action name you bound
503
+ });
504
+ ```
505
+
506
+ It takes either an `input.KEY` keycode or the NAME of an action the game has
507
+ already bound. Neither form touches the global bindings, so a keycode works
508
+ even when nothing has called `input.bindKey` for it.
509
+
510
+ **The handlers fire with no `Pointer`.** This is the one that breaks existing
511
+ code: a button whose `onClick(event)` reads `event.gameX` worked for years
512
+ because only a pointer could ever press it, and the first key press throws.
513
+ Guard the argument, or keep pointer-only work out of the handler:
514
+
515
+ ```js
516
+ onClick(event) {
517
+ if (event) { this.ripple(event.gameX, event.gameY); } // pointer only
518
+ this.resumeGame(); // either input
519
+ }
520
+ ```
521
+
522
+ Three more rules, each with a reason you can feel in a game:
523
+
524
+ - **The first input to press owns the button.** A pointer cannot release a key
525
+ press and a key cannot release a pointer press, so a button cannot be left
526
+ half pressed by using both at once.
527
+ - **Losing focus cancels a held key WITHOUT calling `onRelease()`.** If you
528
+ use the pair as press and depress, firing while held, also stop on
529
+ `event.BLUR` or the gun is still firing when the player alt-tabs back.
530
+ - **`isClickable` gates the press, not the release.** Disabling a button while
531
+ its key is down still releases it, so it cannot stick.
532
+
533
+ Shortcuts live only while the button is activated, and never start dragging or
534
+ tap-and-hold: those stay pointer gestures.
535
+
536
+ ### A fullscreen option
537
+
538
+ Three methods on the `Application`, not on `device` (they moved there in
539
+ 19.7.0):
540
+
541
+ ```js
542
+ app.isFullscreen(); // boolean
543
+ app.requestFullscreen(); // needs a USER GESTURE
544
+ app.exitFullscreen(); // needs none
545
+ ```
546
+
547
+ **`requestFullscreen` only works from a click or a key press.** Called at
548
+ startup, or from a timer, the browser rejects it and nothing happens. That is
549
+ why a fullscreen option lives on a menu entry or a key, never in a scene's
550
+ setup:
551
+
552
+ ```js
553
+ import { event, input } from "melonjs";
554
+
555
+ input.bindKey(input.KEY.F, "fullscreen");
556
+ event.on(event.KEYDOWN, (action, keyCode, edge) => {
557
+ // `edge` only: a held key auto-repeats, and a repeat toggles straight
558
+ // back out of what it just turned on
559
+ if (action !== "fullscreen" || !edge) {
560
+ return;
561
+ }
562
+ if (app.isFullscreen()) {
563
+ app.exitFullscreen();
564
+ } else {
565
+ app.requestFullscreen();
566
+ }
567
+ });
568
+ ```
569
+
570
+ One subscription on `KEYDOWN` covers every screen, where polling
571
+ `isKeyPressed` would need a copy in each menu that is up.
572
+
573
+ **It fullscreens the canvas's PARENT**, not the canvas, so a DOM HUD or
574
+ overlay inside that container goes with it. Pass an element to override.
575
+
576
+ **A menu entry showing `ON`/`OFF` must read the state back**, not cache it:
577
+ the player can leave fullscreen with Escape or the browser's own control
578
+ without your code hearing about it, and a remembered flag then lies.
579
+
580
+ ```js
581
+ entry.setLabel(`FULLSCREEN: ${app.isFullscreen() ? "ON" : "OFF"}`);
582
+ ```
583
+
584
+ **Nothing to branch on for support.** `requestFullscreen` checks
585
+ `device.hasFullscreenSupport` itself and no-ops without it, and
586
+ `exitFullscreen` no-ops when not fullscreen. Query
587
+ `device.hasFullscreenSupport` only to decide whether to *offer* the option at
588
+ all.
589
+
590
+ Fullscreen does not resize a fixed-resolution game: with
591
+ `new Application(w, h, { scale: "auto" })` the internal resolution is
592
+ unchanged and only the canvas scales, so a HUD built from
593
+ `renderer.width`/`height` needs no repositioning.
594
+
268
595
  ### `z` is container-local
269
596
 
270
597
  `addChild(child, z)` writes a depth that means something **only among that
@@ -284,6 +611,57 @@ Giving a child a huge `z` to "put it in front" lifts it only within its own
284
611
  panel. This is also why a widget's `z` never has to be coordinated with
285
612
  anything outside its own container.
286
613
 
614
+ ### Panels and boxes that stretch: `NineSliceSprite`
615
+
616
+ A panel, dialogue box or button background drawn from one small image wants
617
+ `NineSliceSprite`, not a `Sprite` scaled up. It cuts the image into a 3×3 grid,
618
+ keeps the four corners at their own size, and stretches only the edges and the
619
+ middle, so the border stays crisp at any size. Do not hand-roll it from nine
620
+ sprites.
621
+
622
+ From a texture atlas, the third argument of `createSpriteFromName` asks for the
623
+ 9-slice version. This is how the UI example's draggable panel draws itself:
624
+
625
+ ```js
626
+ class Panel extends UIBaseElement {
627
+ constructor(x, y, width, height) {
628
+ super(x, y, width, height);
629
+ // `true` returns a NineSliceSprite stretched to the panel's size
630
+ this.addChild(
631
+ texture.createSpriteFromName("grey_panel", { width, height }, true),
632
+ );
633
+ }
634
+ }
635
+ ```
636
+
637
+ From a loaded image, construct it directly (the Text example's dialogue box):
638
+
639
+ ```js
640
+ const box = new NineSliceSprite(48, 640, {
641
+ image: "panel",
642
+ width: 900,
643
+ height: 256,
644
+ insetx: 36, // the border thickness in the source image
645
+ insety: 36,
646
+ tint: "#3a3f58", // tint works as on any sprite
647
+ });
648
+ box.anchorPoint.set(0, 0); // place it by its top-left corner
649
+ ```
650
+
651
+ Three things trip people up:
652
+
653
+ - **`width` and `height` are mandatory.** They are the size to stretch *to*, not
654
+ the image's size, and the constructor throws without them.
655
+ - **Set `insetx` / `insety` to the art's border.** Left out, each defaults to a
656
+ quarter of the source image, which on most panel art cuts through the border:
657
+ the corners smear or the border thins out when the box grows.
658
+ - **The anchor is the centre**, as for any `Sprite`. Set it to `(0, 0)` to place
659
+ the box by its corner, as a layout usually wants.
660
+
661
+ It resizes live: set `width` / `height` and the next draw re-slices, so a box
662
+ that grows with its text, or a panel the player drags larger, needs no new
663
+ sprite.
664
+
287
665
  ### An opaque panel has to say so
288
666
 
289
667
  The hit test asks the topmost renderable first and then keeps walking down, so