melonjs 20.11.0 → 20.13.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 (208) hide show
  1. package/README.md +3 -1
  2. package/build/application/application.d.ts.map +1 -1
  3. package/build/application/settings.d.ts +6 -2
  4. package/build/application/settings.d.ts.map +1 -1
  5. package/build/audio/backend/sound.d.ts.map +1 -1
  6. package/build/audio/playback.d.ts.map +1 -1
  7. package/build/audio/spatial.d.ts +13 -2
  8. package/build/audio/spatial.d.ts.map +1 -1
  9. package/build/audio/types.d.ts +25 -3
  10. package/build/audio/types.d.ts.map +1 -1
  11. package/build/camera/camera2d.d.ts +2 -0
  12. package/build/camera/camera2d.d.ts.map +1 -1
  13. package/build/camera/camera3d.d.ts.map +1 -1
  14. package/build/camera/effects/fade_effect.d.ts.map +1 -1
  15. package/build/camera/effects/pixelate_transition_effect.d.ts +67 -0
  16. package/build/camera/effects/pixelate_transition_effect.d.ts.map +1 -0
  17. package/build/geometries/ellipse.d.ts +11 -4
  18. package/build/geometries/ellipse.d.ts.map +1 -1
  19. package/build/geometries/line.d.ts +2 -2
  20. package/build/geometries/line.d.ts.map +1 -1
  21. package/build/geometries/polygon.d.ts +5 -4
  22. package/build/geometries/polygon.d.ts.map +1 -1
  23. package/build/geometries/rectangle.d.ts +2 -2
  24. package/build/geometries/rectangle.d.ts.map +1 -1
  25. package/build/geometries/roundrect.d.ts +2 -2
  26. package/build/geometries/roundrect.d.ts.map +1 -1
  27. package/build/index.d.ts +14 -2
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +12716 -7233
  30. package/build/index.js.map +4 -4
  31. package/build/input/gamepad.d.ts.map +1 -1
  32. package/build/input/pointerevent.d.ts.map +1 -1
  33. package/build/lang/console.d.ts +16 -1
  34. package/build/lang/console.d.ts.map +1 -1
  35. package/build/level/tiled/TMXLayer.d.ts +48 -14
  36. package/build/level/tiled/TMXLayer.d.ts.map +1 -1
  37. package/build/level/tiled/TMXTileMap.d.ts +1 -1
  38. package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
  39. package/build/level/tiled/TMXTileset.d.ts.map +1 -1
  40. package/build/level/tiled/TMXUtils.d.ts.map +1 -1
  41. package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
  42. package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
  43. package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
  44. package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
  45. package/build/level/tiled/renderer/TMXRenderer.d.ts +1 -1
  46. package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
  47. package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
  48. package/build/loader/loader.d.ts +55 -11
  49. package/build/loader/loader.d.ts.map +1 -1
  50. package/build/loader/loadingscreen.d.ts.map +1 -1
  51. package/build/math/color.d.ts.map +1 -1
  52. package/build/math/math.d.ts +6 -1
  53. package/build/math/math.d.ts.map +1 -1
  54. package/build/math/matrix2d.d.ts +3 -2
  55. package/build/math/matrix2d.d.ts.map +1 -1
  56. package/build/math/matrix3d.d.ts +4 -1
  57. package/build/math/matrix3d.d.ts.map +1 -1
  58. package/build/math/observableVector2d.d.ts +6 -6
  59. package/build/math/observableVector2d.d.ts.map +1 -1
  60. package/build/math/observableVector3d.d.ts +8 -8
  61. package/build/math/observableVector3d.d.ts.map +1 -1
  62. package/build/particles/emitter.d.ts +15 -3
  63. package/build/particles/emitter.d.ts.map +1 -1
  64. package/build/particles/particle.d.ts +6 -0
  65. package/build/particles/particle.d.ts.map +1 -1
  66. package/build/particles/settings.d.ts +22 -5
  67. package/build/particles/settings.d.ts.map +1 -1
  68. package/build/physics/bounds.d.ts +5 -5
  69. package/build/physics/bounds.d.ts.map +1 -1
  70. package/build/physics/builtin/body.d.ts +15 -4
  71. package/build/physics/builtin/body.d.ts.map +1 -1
  72. package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
  73. package/build/physics/builtin/detector.d.ts.map +1 -1
  74. package/build/physics/builtin/raycast.d.ts.map +1 -1
  75. package/build/physics/world.d.ts.map +1 -1
  76. package/build/renderable/animation.d.ts +6 -1
  77. package/build/renderable/animation.d.ts.map +1 -1
  78. package/build/renderable/container.d.ts +40 -6
  79. package/build/renderable/container.d.ts.map +1 -1
  80. package/build/renderable/entity/entity.d.ts.map +1 -1
  81. package/build/renderable/frameAnimation.d.ts.map +1 -1
  82. package/build/renderable/imagelayer.d.ts.map +1 -1
  83. package/build/renderable/instanced_mesh.d.ts.map +1 -1
  84. package/build/renderable/mesh.d.ts.map +1 -1
  85. package/build/renderable/renderable.d.ts.map +1 -1
  86. package/build/renderable/sprite.d.ts +8 -2
  87. package/build/renderable/sprite.d.ts.map +1 -1
  88. package/build/renderable/text/bitmaptext.d.ts +12 -3
  89. package/build/renderable/text/bitmaptext.d.ts.map +1 -1
  90. package/build/renderable/text/text.d.ts +147 -4
  91. package/build/renderable/text/text.d.ts.map +1 -1
  92. package/build/renderable/text/textmetrics.d.ts +10 -2
  93. package/build/renderable/text/textmetrics.d.ts.map +1 -1
  94. package/build/renderable/text/textstyle.d.ts.map +1 -1
  95. package/build/renderable/trail3d.d.ts.map +1 -1
  96. package/build/renderable/ui/panelpaint.d.ts +5 -1
  97. package/build/renderable/ui/panelpaint.d.ts.map +1 -1
  98. package/build/renderable/ui/progressbar.d.ts +267 -95
  99. package/build/renderable/ui/progressbar.d.ts.map +1 -1
  100. package/build/renderable/ui/uibaseelement.d.ts +436 -50
  101. package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
  102. package/build/renderable/ui/uicheckbox.d.ts +383 -0
  103. package/build/renderable/ui/uicheckbox.d.ts.map +1 -0
  104. package/build/renderable/ui/uifocus.d.ts +6 -0
  105. package/build/renderable/ui/uifocus.d.ts.map +1 -0
  106. package/build/renderable/ui/uilabel.d.ts +31 -0
  107. package/build/renderable/ui/uilabel.d.ts.map +1 -0
  108. package/build/renderable/ui/uipanel.d.ts +589 -116
  109. package/build/renderable/ui/uipanel.d.ts.map +1 -1
  110. package/build/renderable/ui/uiskin.d.ts +25 -0
  111. package/build/renderable/ui/uiskin.d.ts.map +1 -0
  112. package/build/renderable/ui/uislider.d.ts +518 -0
  113. package/build/renderable/ui/uislider.d.ts.map +1 -0
  114. package/build/renderable/ui/uispriteelement.d.ts +161 -85
  115. package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
  116. package/build/renderable/ui/uitextbutton.d.ts +369 -63
  117. package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
  118. package/build/state/stage.d.ts +3 -2
  119. package/build/state/stage.d.ts.map +1 -1
  120. package/build/state/state.d.ts +31 -6
  121. package/build/state/state.d.ts.map +1 -1
  122. package/build/system/legacy_pool.d.ts.map +1 -1
  123. package/build/system/pool.d.ts.map +1 -1
  124. package/build/system/save.d.ts +2 -1
  125. package/build/system/save.d.ts.map +1 -1
  126. package/build/tweens/tween.d.ts +5 -2
  127. package/build/tweens/tween.d.ts.map +1 -1
  128. package/build/utils/fetchdata.d.ts.map +1 -1
  129. package/build/video/canvas/canvas_renderer.d.ts +1 -5
  130. package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
  131. package/build/video/effects/bloom.d.ts +3 -2
  132. package/build/video/effects/bloom.d.ts.map +1 -1
  133. package/build/video/effects/blur.d.ts +1 -1
  134. package/build/video/effects/chromaticAberration.d.ts +1 -1
  135. package/build/video/effects/dissolve.d.ts.map +1 -1
  136. package/build/video/effects/dropShadow.d.ts +1 -1
  137. package/build/video/effects/glow.d.ts +1 -1
  138. package/build/video/effects/glsl_realization.d.ts.map +1 -1
  139. package/build/video/effects/hologram.d.ts.map +1 -1
  140. package/build/video/effects/local_space.d.ts +19 -0
  141. package/build/video/effects/local_space.d.ts.map +1 -0
  142. package/build/video/effects/mesh_splice.d.ts.map +1 -1
  143. package/build/video/effects/outline.d.ts +1 -1
  144. package/build/video/effects/pixelate.d.ts +1 -1
  145. package/build/video/effects/radialGradient.d.ts +4 -12
  146. package/build/video/effects/radialGradient.d.ts.map +1 -1
  147. package/build/video/effects/scanline.d.ts.map +1 -1
  148. package/build/video/effects/shadereffect.d.ts.map +1 -1
  149. package/build/video/effects/shine.d.ts +5 -0
  150. package/build/video/effects/shine.d.ts.map +1 -1
  151. package/build/video/effects/sway.d.ts +9 -4
  152. package/build/video/effects/sway.d.ts.map +1 -1
  153. package/build/video/effects/vignette.d.ts.map +1 -1
  154. package/build/video/effects/wave.d.ts +9 -7
  155. package/build/video/effects/wave.d.ts.map +1 -1
  156. package/build/video/effects/wgsl/parse.d.ts.map +1 -1
  157. package/build/video/effects/wgsl/scaffold.d.ts.map +1 -1
  158. package/build/video/effects/wgsl_realization.d.ts.map +1 -1
  159. package/build/video/renderer.d.ts +22 -5
  160. package/build/video/renderer.d.ts.map +1 -1
  161. package/build/video/renderstate.d.ts +26 -3
  162. package/build/video/renderstate.d.ts.map +1 -1
  163. package/build/video/rendertarget/webgpurendertarget.d.ts +1 -0
  164. package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
  165. package/build/video/texture/atlas.d.ts +31 -31
  166. package/build/video/texture/atlas.d.ts.map +1 -1
  167. package/build/video/utils/pixel_snap.d.ts +44 -0
  168. package/build/video/utils/pixel_snap.d.ts.map +1 -0
  169. package/build/video/utils/tessellation.d.ts +11 -0
  170. package/build/video/utils/tessellation.d.ts.map +1 -1
  171. package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
  172. package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
  173. package/build/video/webgl/batchers/quad_batcher.d.ts +2 -1
  174. package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
  175. package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts.map +1 -1
  176. package/build/video/webgl/webgl_renderer.d.ts +3 -6
  177. package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
  178. package/build/video/webgpu/batchers/mesh_batcher.d.ts +0 -1
  179. package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
  180. package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
  181. package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
  182. package/build/video/webgpu/effect_binding.d.ts.map +1 -1
  183. package/build/video/webgpu/renderers/tmxlayer/orthogonal.d.ts.map +1 -1
  184. package/build/video/webgpu/webgpu_renderer.d.ts +6 -10
  185. package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
  186. package/package.json +1 -1
  187. package/skills/AGENTS.md +2 -1
  188. package/skills/melonjs/SKILL.md +3 -2
  189. package/skills/melonjs-audio/SKILL.md +31 -8
  190. package/skills/melonjs-camera-and-drawing/SKILL.md +41 -1
  191. package/skills/melonjs-display-and-scaling/SKILL.md +19 -5
  192. package/skills/melonjs-effects-and-shaders/SKILL.md +53 -17
  193. package/skills/melonjs-events/SKILL.md +3 -1
  194. package/skills/melonjs-getting-started/SKILL.md +1 -2
  195. package/skills/melonjs-input/SKILL.md +61 -3
  196. package/skills/melonjs-lighting/SKILL.md +4 -3
  197. package/skills/melonjs-loading-assets/SKILL.md +57 -15
  198. package/skills/melonjs-math/SKILL.md +14 -3
  199. package/skills/melonjs-particles-and-trails/SKILL.md +72 -8
  200. package/skills/melonjs-performance/SKILL.md +3 -0
  201. package/skills/melonjs-physics/SKILL.md +53 -4
  202. package/skills/melonjs-plugins/SKILL.md +25 -1
  203. package/skills/melonjs-renderables/SKILL.md +11 -3
  204. package/skills/melonjs-renderer-backends/SKILL.md +2 -2
  205. package/skills/melonjs-scenes-and-state/SKILL.md +32 -2
  206. package/skills/melonjs-sprites-and-animation/SKILL.md +14 -0
  207. package/skills/melonjs-tilemaps/SKILL.md +96 -2
  208. package/skills/melonjs-ui-and-text/SKILL.md +560 -23
@@ -1,17 +1,32 @@
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, 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"
3
+ description: "Use this skill for HUDs, buttons, menus, options screens, dialogue panels, progress bars and on-screen text in melonJS. Covers UIBaseElement/UISpriteElement/UITextButton, UICheckbox, UISlider, ProgressBar, UIPanel (skins, layout, scrolling), 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, checkbox, radio button, UICheckbox, slider, UISlider, volume slider, options menu, settings screen, scrolling list, scroll panel, layout, column, grid, UI skin, 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
 
7
7
  # UI, HUD and text
8
8
 
9
- ## Outlined text: the stroke eats into the glyph
9
+ ## Outlined text: behind the glyphs, or over them
10
10
 
11
- `Text` draws `fillText` and then `strokeText`, so the outline lands **on top of
12
- the fill** and is centred on the glyph edge — half of it inward. On a small or
13
- chunky face a `lineWidth` of 3 leaves the letters solid black. Keep it to 1,
14
- and raise the font size rather than the stroke.
11
+ An outline is centred on the glyph's edge, so half of it lies inside the
12
+ glyph. `Text` paints it **behind** the glyphs by default (`paintOrder:
13
+ "stroke"`): the letters stay whole and the outline shows only around them, so
14
+ a thick outline works on any face. What shows is half of `lineWidth`: ask for
15
+ twice the width you want to see.
16
+
17
+ `paintOrder: "fill"` paints the glyphs first and the outline over them, as a
18
+ plain canvas does with `fillText` then `strokeText`, and as melonJS did before
19
+ 20.13. Its inner half then covers the letters: on a small or thin face a
20
+ `lineWidth` of 3 already leaves them solid outline colour.
21
+
22
+ ```js
23
+ const style = {
24
+ font: "sans-serif", size: 32,
25
+ fillStyle: "#ffd23f", strokeStyle: "#1b1b1b", lineWidth: 6,
26
+ };
27
+ new Text(x, y, { ...style, text: "BEHIND" }); // the default
28
+ new Text(x, y, { ...style, paintOrder: "fill", text: "OVER" }); // the old look
29
+ ```
15
30
 
16
31
  The stroke used to cost you the top row of pixels as well, and a display face
17
32
  whose glyphs overshoot the nominal ascent did the same on its own: the render
@@ -29,6 +44,36 @@ If you have a label carrying a leading `\n` to buy headroom, that workaround is
29
44
  now dead weight — drop it, and take the line height back off the position you
30
45
  shifted it by.
31
46
 
47
+ The outline's corners are set by `lineJoin`, the same property a canvas
48
+ context has, and `miterLimit`:
49
+
50
+ | setting | corners | use it for |
51
+ |---|---|---|
52
+ | `lineJoin: "round"` (default) | rounded: the outline is the same width all around | almost everything: HUDs, buttons, titles |
53
+ | `lineJoin: "bevel"` | cut flat | a harder edge with no spikes |
54
+ | `lineJoin: "miter"` | pointed, as a plain canvas strokes text | a sharp, angular look |
55
+ | `miterLimit: n` (default 10) | with `"miter"` only: a corner pointing further than `n` half line widths is cut flat instead | taming the spikes `"miter"` grows on "A", "V", "M" |
56
+
57
+ At the canvas default of 10, a thick `"miter"` outline grows spikes several
58
+ times its width from sharp glyphs; 2 to 4 keeps the corners pointed and the
59
+ spikes short:
60
+
61
+ ```js
62
+ const outline = {
63
+ font: "sans-serif", size: 48,
64
+ fillStyle: "#ffffff", strokeStyle: "#000000", lineWidth: 6,
65
+ };
66
+ new Text(x, y, { ...outline, text: "ROUND" }); // the default
67
+ new Text(x, y, { ...outline, lineJoin: "bevel", text: "BEVEL" });
68
+ new Text(x, y, { ...outline, lineJoin: "miter", text: "MITER" }); // long spikes
69
+ new Text(x, y, { ...outline, lineJoin: "miter", miterLimit: 3, text: "MITER 3" });
70
+ ```
71
+
72
+ Whichever you pick, the bake leaves room for it on all four sides, so no corner
73
+ is cut and the label's position and bounds do not move; pointed corners just
74
+ cost a bigger texture. Changed on a live label, they show from the next
75
+ `setText()`.
76
+
32
77
  ## Gradient text
33
78
 
34
79
  `fillStyle` takes a `Gradient` as well as a colour — the same object
@@ -127,10 +172,38 @@ including one it is not currently holding:
127
172
  const widest = hudScore.measureText("00:00").width;
128
173
  ```
129
174
 
175
+ Given a string, it returns a new `TextMetrics` and leaves the label as it was;
176
+ with no argument it returns the label's own metrics. Before 20.13 measuring
177
+ another string overwrote the label's own metrics, so a centred or
178
+ right-aligned label was drawn shifted until its next `setText()`.
179
+
130
180
  Advance widths scale linearly with the point size for one family, so measuring
131
181
  on an existing label and scaling the result is exact. For labels you genuinely
132
182
  spawn in bulk, `getPool("text")` recycles the bake.
133
183
 
184
+ ### Multi-line text: the baseline places the whole block
185
+
186
+ A `\n` in the string (or an array of strings) starts a new line, on `Text` and
187
+ `BitmapText` alike. `textBaseline` then places the block as one piece against
188
+ `pos.y`: its top for `"top"` / `"hanging"`, its middle for `"middle"`, its
189
+ bottom for `"alphabetic"`, `"ideographic"` and `"bottom"`. So a two-line
190
+ `"bottom"` label in a screen corner sits wholly above `pos.y`, and a centred
191
+ `"middle"` label centres every line, not the first. `getBounds()` is that same
192
+ box, which is what culling and the pointer use.
193
+
194
+ `wordWrapWidth` only ADDS line breaks: each `\n`-separated line is wrapped on
195
+ its own, so explicit breaks and blank lines survive. A line breaks between
196
+ words, never inside one, so a single word wider than `wordWrapWidth` gets a
197
+ line of its own and overflows it:
198
+
199
+ ```js
200
+ const help = new Text(8, 8, {
201
+ font: "sans-serif", size: 16, wordWrapWidth: 200,
202
+ text: "CONTROLS\nArrows move, Space jumps, Esc pauses the game.",
203
+ });
204
+ // "CONTROLS" on its own line, then the sentence wrapped under it
205
+ ```
206
+
134
207
  ### `fillStyle` means two different things
135
208
 
136
209
  The name is shared and the behaviour is not, which is the trap when moving a
@@ -142,7 +215,7 @@ label from one class to the other:
142
215
  | white | white glyphs | the ABSENCE of a tint |
143
216
  | gradient | yes, via `fillGradient` | no: a tint is one colour over a page image |
144
217
  | 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 |
145
- | stroke | `strokeStyle` + `lineWidth` | none |
218
+ | stroke | `strokeStyle` + `lineWidth`, behind or over the glyphs with `paintOrder`, corners with `lineJoin` / `miterLimit` | none |
146
219
  | `size` | pixels | a RATIO of the authored size |
147
220
 
148
221
  ### Per-glyph wave, shake and colour: `glyphEffect`
@@ -290,6 +363,68 @@ There is no `UIContainer` class. Use `Container` for a bare grouping,
290
363
  `UIBaseElement` when you want the pointer handling and nothing drawn — it is a
291
364
  `Container` that already sets `floating = true` and `isKinematic = false`.
292
365
 
366
+ ### Which widget for what
367
+
368
+ | you want | use | notes |
369
+ |---|---|---|
370
+ | a button with a label | `UITextButton` | drawn, or skinned from an atlas; `onClick` setting |
371
+ | an image-only button or icon | `UISpriteElement` | a `UIBaseElement` holding a `Sprite` (`sprite`); override `onClick` |
372
+ | an on/off option, or one of several | `UICheckbox` | a radio button with `group` |
373
+ | a value in a range: volume, sensitivity | `UISlider` | horizontal or vertical |
374
+ | health, shield, cooldown, loading | `ProgressBar` | not clickable; `value` redraws |
375
+ | a frame, dialog or menu card | `UIPanel` | skin, title, layout, scrolling, drag, shadow |
376
+ | your own clickable widget | extend `UIBaseElement` | pointer handlers, `enabled`, focus, bounce |
377
+ | a group with nothing drawn | `Container` | set `floating` for screen space |
378
+
379
+ ### A whole options menu
380
+
381
+ Everything below in one place: a skinned panel that lays its widgets out
382
+ as a column, a checkbox, a slider, two buttons, a drop shadow, a little
383
+ bounce, and keyboard / gamepad play. Each piece has its own section after
384
+ this.
385
+
386
+ ```js
387
+ import {
388
+ UICheckbox, UIPanel, UISlider, UITextButton, audio, state,
389
+ } from "melonjs";
390
+
391
+ // one style object for every widget's text
392
+ const FONT = { font: "kenpixel", fontSize: 14, labelFillStyle: "black" };
393
+
394
+ const menu = new UIPanel(40, 40, {
395
+ width: 360, height: 100, // autoSize grows the height to fit
396
+ skin: { atlas, region: "grey_panel", insetx: 10, insety: 10 },
397
+ title: "OPTIONS", titleFont: "kenpixel", titleFontSize: 20,
398
+ layout: "column", padding: 16, gap: 12, align: "stretch", autoSize: true,
399
+ shadowColor: "#00000059", shadowOffsetY: 6, shadowBlur: 10, radius: 6,
400
+ });
401
+
402
+ const music = menu.addChild(new UICheckbox(0, 0, {
403
+ ...FONT,
404
+ labelOn: "Music ON", labelOff: "Music OFF",
405
+ checked: settings.music,
406
+ onChange: (on) => { settings.music = on; }, // user input only
407
+ }));
408
+
409
+ menu.addChild(new UISlider(0, 0, {
410
+ ...FONT,
411
+ label: "Volume", showValue: true,
412
+ valueFormat: (v, ratio) => `${Math.round(ratio * 100)}%`,
413
+ value: audio.getVolume(),
414
+ onChange: (v) => audio.setVolume(v), // while dragging
415
+ onChangeEnd: () => saveSettings(), // once, on release
416
+ }));
417
+
418
+ menu.addChild(new UITextButton(0, 0, {
419
+ ...FONT, text: "Back", height: 40,
420
+ hoverScale: 1.04, pressScale: 0.96, // the bounce
421
+ onClick: () => state.change(state.MENU), // a completed click
422
+ }));
423
+
424
+ app.world.addChild(menu, 10);
425
+ music.focus(); // arrows / D-pad and Enter / A now drive the menu
426
+ ```
427
+
293
428
  Three things matter here:
294
429
 
295
430
  - **`floating = true`** opts the container out of camera transforms, so it stays
@@ -321,6 +456,7 @@ const hp = new ProgressBar(20, 20, {
321
456
  hud.addChild(hp);
322
457
 
323
458
  hp.value -= 15; // clamps; `ratio` reads back 0..1
459
+ hp.max = 50; // re-clamps the value into the new range
324
460
  ```
325
461
 
326
462
  `direction` takes `"left-to-right"` (the default), `"right-to-left"`,
@@ -441,6 +577,116 @@ One consequence worth knowing: `getBounds()` on a bobbing panel answers
441
577
  differently each frame, so do not cache it or compare two children's bounds
442
578
  while a bob is running.
443
579
 
580
+ ### Panel art, title, dragging: `skin`, `title`, `draggable`
581
+
582
+ ```js
583
+ const options = new UIPanel(20, 20, {
584
+ width: 380, height: 460,
585
+ // one atlas region, stretched as a nine-slice to the panel's size
586
+ skin: { atlas, region: "grey_panel", insetx: 10, insety: 10 },
587
+ title: "OPTIONS", titleFont: "kenpixel", titleFontSize: 20, titleFillStyle: "black",
588
+ draggable: true, // comes to the front when grabbed
589
+ });
590
+ ```
591
+
592
+ **A drop shadow**, here or on a `ProgressBar`, takes a colour whose alpha is
593
+ how dark it gets: `shadowColor: "#00000059"`, with `shadowOffsetX` (0),
594
+ `shadowOffsetY` (4) and `shadowBlur` (8 pixels of soft edge, `0` for sharp).
595
+ There is none until `shadowColor` is set.
596
+
597
+ ```js
598
+ const card = new UIPanel(100, 80, {
599
+ width: 300, height: 180, fillColor: "#f0f0f0", radius: 8,
600
+ shadowColor: "#00000059", // about 35% black
601
+ shadowOffsetY: 6, // drops it below the card
602
+ shadowBlur: 10, // pixels of soft edge
603
+ });
604
+ const hp = new ProgressBar(20, 20, {
605
+ width: 200, height: 16, radius: 8,
606
+ shadowColor: "#00000080", shadowOffsetY: 2, shadowBlur: 4,
607
+ });
608
+ ```
609
+ It follows `radius`, which a skinned panel can set to match its art's
610
+ corners, and it is drawn outside the bounds, so it never takes the pointer.
611
+ Under a see-through fill or track it shows through; give that a solid colour.
612
+
613
+ The title sits at the top and the content starts below it. `insetx` /
614
+ `insety` are the unscaled corners of the art, and also where the frame ends:
615
+ layout `padding` is measured from there.
616
+
617
+ ### Laying a panel out: `layout`
618
+
619
+ Leave `layout` at `"none"` and children stay where you put them. Otherwise
620
+ the panel places them, below the title and inside the frame:
621
+
622
+ ```js
623
+ const menu = new UIPanel(x, y, {
624
+ width: 360, height: 100,
625
+ layout: "column", // "row", "grid" (with `columns`), or "none"
626
+ padding: 16, // from the frame to the content
627
+ gap: 12, // between children
628
+ align: "stretch", // "start", "center", "end", or "stretch"
629
+ autoSize: true, // grow the panel to fit
630
+ });
631
+ menu.addChild(new UITextButton(0, 0, { text: "Play", onClick: play }));
632
+ menu.addChild(new UITextButton(0, 0, { text: "Options", onClick: openOptions }));
633
+ ```
634
+
635
+ Children are laid out in the order they were added. It runs when a child is
636
+ added or removed and when the panel is resized, **not every frame**: a child
637
+ that changes size afterwards, a label whose text grew, needs
638
+ `menu.relayout()`. `"stretch"` resizes buttons and panels, which lay
639
+ themselves out again at a new size; anything else keeps its size.
640
+
641
+ A row inside a column is a second panel that draws nothing and lets clicks
642
+ through: `new UIPanel(0, 0, { width, height, layout: "row", blocking: false })`.
643
+
644
+ ### Scrolling lists: `scroll`
645
+
646
+ ```js
647
+ const levels = new UIPanel(420, 435, {
648
+ width: 340, height: 150,
649
+ layout: "grid", columns: 5, gap: 8, padding: 10,
650
+ scroll: "vertical", // "horizontal", "both", or "none"
651
+ onScroll: (x, y, panel) => { saved.levelScroll = y; },
652
+ });
653
+ const buttons = [];
654
+ for (let i = 1; i <= 30; i++) {
655
+ buttons[i] = levels.addChild(new UITextButton(0, 0, {
656
+ text: String(i), width: 56, height: 40,
657
+ enabled: i <= unlocked,
658
+ onClick: () => startLevel(i),
659
+ }));
660
+ }
661
+ levels.scrollTo(0, saved.levelScroll); // restore where it was
662
+ levels.scrollToChild(buttons[unlocked]); // or show the latest
663
+ ```
664
+
665
+ The content is cropped to the area inside the frame and below the title, and
666
+ scrolls with the wheel over the panel and by dragging, with momentum when
667
+ flung. A scrollbar shows while it does not fit. `scrollTo(x, y)`,
668
+ `scrollToChild(child)`, `scrollX` / `scrollY`, `maxScrollX` / `maxScrollY` and
669
+ an `onScroll(x, y, panel)` setting are there to drive it from code. Scrolling
670
+ from code stops a fling that is still coasting.
671
+
672
+ What to know:
673
+
674
+ - **Scrolling moves the children.** Their `pos` is where they are drawn, so
675
+ it changes as the list scrolls. A child added to a `"none"` layout while
676
+ scrolled is placed in content coordinates and moved into view.
677
+ - **A drag that starts on a button scrolls the list and cancels the press.**
678
+ The button gets `onCancel()` instead of `onRelease()`, and its `onClick`
679
+ SETTING is not called. The `onClick()` METHOD already ran on the press, so
680
+ a button in a list does its work in the setting: see the next section.
681
+ - **A slider in a scrolling panel is dragged, not the panel.** A widget of
682
+ your own that is dragged too, a knob or a colour picker, sets
683
+ `keepsDrag = true` to keep its drags.
684
+ - Children scrolled out of view do not get pointer events. A scrolling panel
685
+ inside another scrolls alone, and is cropped to both: a clip inside a clip
686
+ is the overlap of the two, on Canvas, WebGL and WebGPU alike.
687
+ - A draggable panel is moved by its title or its frame. A press on a widget
688
+ on it is the widget's.
689
+
444
690
  ### A pause overlay: `updateWhenPaused` cascades
445
691
 
446
692
  `state.pause()` stops every world child stepping while rendering carries on, so
@@ -460,14 +706,16 @@ it, silently. A panel whose labels never appear while paused is this.
460
706
 
461
707
  ### Buttons: extend the handlers, do not bind listeners
462
708
 
463
- `UISpriteElement` is a `Sprite` that already registers itself for pointer
464
- events, so a button is made by overriding methods rather than by wiring
465
- `registerPointerEvent`:
709
+ Every UI element already registers itself for pointer events, so a button is
710
+ made by overriding methods rather than by wiring `registerPointerEvent`. For
711
+ an image-only button that is `UISpriteElement`: a `UIBaseElement` holding a
712
+ `Sprite`, placed like a sprite (`(x, y)` is where its anchor goes, the centre
713
+ by default):
466
714
 
467
715
  | handler | fires | returns |
468
716
  |---|---|---|
469
717
  | `onClick(event)` | pressed | `false` to stop the event propagating |
470
- | `onRelease(event)` | pressed and released | `false` to stop propagating |
718
+ | `onRelease(event)` | the press ends (up, left while held, or cancelled) | `false` to stop propagating |
471
719
  | `onOver(event)` | pointer enters | `false` to stop propagating |
472
720
  | `onOut(event)` | pointer leaves | — |
473
721
  | `onHold()` | pressed and held | — |
@@ -482,16 +730,164 @@ class MuteButton extends UISpriteElement {
482
730
  onOut() { this.setOpacity(0.5); }
483
731
  onClick() {
484
732
  audio.muteAll();
485
- return false; // consumed — do not fall through to the world
733
+ this.setRegion(atlas.getRegion("speaker_off.png")); // or this.sprite.setRegion
734
+ return false; // consumed (the default)
486
735
  }
487
736
  }
488
737
  ```
489
738
 
739
+ The image is `button.sprite`: animations, tint and regions live there
740
+ (`setRegion`, `addAnimation`, `setCurrentAnimation` and `isCurrentAnimation`
741
+ are also on the element as shortcuts). As a `UIBaseElement` it has
742
+ `enabled` (drawn at half opacity when off), `cancelPress()` / `onCancel()`,
743
+ the bounce, and focus once `focusable` is set. `pos` is the element's top
744
+ left corner, and a second sprite of your own, such as a joystick's knob, is
745
+ a child added with `addChild`, not drawn by hand in `draw()`.
746
+
747
+ **A press stops at a UI element by default.** `onClick` and `onRelease`
748
+ return `false` unless you override them, on every `UIBaseElement` (buttons,
749
+ checkboxes, sliders, sprite buttons, your own widgets): a click on
750
+ a HUD button does not also click the world behind it. Return `true` from
751
+ them to let the press through as well. Hover is different: `onOver` lets it
752
+ through unless it returns `false`. A non-blocking `UIPanel` lets a press on
753
+ its background through, as a frame that only decorates should.
754
+
490
755
  The pointer still has to reach it: a renderable with `isKinematic = true` — the
491
756
  default on a plain `Renderable` — is skipped by the broadphase and receives
492
- nothing. `UISpriteElement` and `UIBaseElement` clear it for you; anything else
757
+ nothing. Every `UIBaseElement` clears it for you; anything else
493
758
  you make clickable has to clear it itself.
494
759
 
760
+ ### A button that does one thing: `UITextButton`
761
+
762
+ No subclass needed for a single action:
763
+
764
+ ```js
765
+ const play = new UITextButton(100, 200, {
766
+ text: "Play", // a CSS font by default ("sans-serif")
767
+ fontSize: 24, // pixels, with a CSS font or a bitmap font
768
+ onClick: () => state.change(state.PLAY),
769
+ });
770
+ ```
771
+
772
+ **`onClick` is called on a COMPLETED click**: pressed and released over the
773
+ button, or its `bindKey` pressed and released. A press dragged off the button,
774
+ cancelled, or turned into a scroll does not call it, and neither does a
775
+ disabled button. The `onClick()` method is different: it runs on the press,
776
+ as before, for subclasses.
777
+
778
+ `font` is a loaded bitmap font's name, drawn with `BitmapText` as before, or
779
+ any other name, drawn as a CSS font with `Text`. The label is
780
+ `button.label` (`bitmapText` is the old name for it).
781
+
782
+ The button takes the same names as the other widgets: `width` and `height`
783
+ for its size, `fontSize` (pixels) and `labelFillStyle` for its text,
784
+ `borderColor` for its outline. The older `borderWidth` / `borderHeight`
785
+ (its SIZE, not an outline thickness), `size` (a scale with a bitmap font)
786
+ and `fillStyle` still work. One style object can then go to every widget:
787
+
788
+ ```js
789
+ const FONT = { font: "kenpixel", fontSize: 14, labelFillStyle: "black" };
790
+ new UITextButton(0, 0, { ...FONT, text: "OK", width: 160 });
791
+ new UICheckbox(0, 0, { ...FONT, label: "Music" });
792
+ new UISlider(0, 0, { ...FONT, label: "Volume" });
793
+ ```
794
+
795
+ **Skinned** from an atlas, drawn as a nine-slice so one image makes a button
796
+ of any width:
797
+
798
+ ```js
799
+ new UITextButton(0, 0, {
800
+ font: "kenpixel", fontSize: 12, text: "Accept", width: 160,
801
+ skin: { atlas, normal: "green_button04", pressed: "green_button05" },
802
+ });
803
+ ```
804
+
805
+ Pressed art is usually shorter than the normal art. The button draws it
806
+ lowered by the difference and moves the label down with it, so it looks
807
+ pushed in. `hover` and `disabled` regions are optional. `enabled`, shared by
808
+ every UI element, turns a widget off: `enabled = false`
809
+ draws the `disabled` region, or the button at half opacity, and ignores the
810
+ pointer and the key.
811
+
812
+ ### A little bounce: `hoverScale` and `pressScale`
813
+
814
+ Every `UIBaseElement`, buttons, checkboxes, sliders, panels and your own,
815
+ can grow a little under the pointer or with the focus, and shrink while held
816
+ (`ProgressBar` is not a `UIBaseElement`, and does not):
817
+
818
+ ```js
819
+ new UITextButton(0, 0, { text: "Play", hoverScale: 1.05, pressScale: 0.92 });
820
+ levelButton.hoverScale = 1.12; // or set it later
821
+ ```
822
+
823
+ Both default to 1, which is still. The scale eases there and back by itself.
824
+ Only the drawing scales, about the centre: the hit area and the layout stay
825
+ put, so nothing jitters and the widgets around it do not move.
826
+
827
+ ### Keyboard and gamepad navigation: `focus()`
828
+
829
+ For a game played without a mouse or a touch screen, give a widget the focus:
830
+
831
+ ```js
832
+ playButton.focus(); // where keyboard or gamepad play starts
833
+ ```
834
+
835
+ From then on the arrow keys or a pad's D-pad move the focus to the nearest
836
+ focusable widget that way. Enter, Space or the pad's A press the focused
837
+ widget as a click does: a button calls its `onClick` setting, a checkbox
838
+ toggles. Left and right step a slider (up and down on a vertical one). A ring
839
+ (`UIBaseElement.focusRingColor`) shows the focus, and hides while the mouse
840
+ is used. A focused widget in a scrolling panel is scrolled into view.
841
+
842
+ There is nothing to turn on. Until a widget has the focus the arrow keys and
843
+ the pad are left to the game, and `widget.blur()`, or the focused widget
844
+ leaving the scene, gives them back. `UIBaseElement.focusOwner` is the widget
845
+ with the focus. The pad is read directly, so the game's own pad bindings
846
+ are untouched.
847
+
848
+ Buttons, checkboxes and sliders are `focusable`. A widget of your own sets
849
+ `focusable = true`, and overrides `onNavigate(direction)` (return `true` for
850
+ what it handles) and `focusRelease()` for what Enter does:
851
+
852
+ ```js
853
+ // a left / right picker: "< Normal >"; Enter confirms the choice
854
+ class Picker extends UIBaseElement {
855
+ constructor(x, y, options, onPick) {
856
+ super(x, y, 200, 32);
857
+ this.focusable = true;
858
+ this.options = options;
859
+ this.onPick = onPick;
860
+ this.index = 0;
861
+ // draw() left out: render this.options[this.index]
862
+ }
863
+ onNavigate(direction) {
864
+ if (direction === "left" || direction === "right") {
865
+ const step = direction === "left" ? -1 : 1;
866
+ this.index = (this.index + step + this.options.length) % this.options.length;
867
+ this.isDirty = true;
868
+ return true; // handled: the focus stays here
869
+ }
870
+ return false; // up and down move the focus on
871
+ }
872
+ focusRelease() {
873
+ super.focusRelease(); // the widget comes back up, as on a click
874
+ this.onPick(this.options[this.index]);
875
+ }
876
+ }
877
+ ```
878
+
879
+ Moving the focus from code is `other.focus()`; `UIBaseElement.focusOwner?.blur()`
880
+ hands the keys back to the game, for example when gameplay resumes.
881
+
882
+ Three things to know. Enter and Space go to the focused widget, so another
883
+ button whose `bindKey` is Enter or Space waits while any other widget has the
884
+ focus, ring shown or hidden: one key press never clicks both. `blur()` the
885
+ focused widget to give Enter back to the shortcut (a disabled or hidden
886
+ focused widget does not keep it). Enter or A held while the window loses
887
+ focus is called off (`onCancel()`, no click), as a held `bindKey` is. And
888
+ accept calls a subclass's `onClick()` METHOD with no pointer event, as
889
+ `bindKey` does: guard `event` there, or act in the `onClick` setting.
890
+
495
891
  ### Keyboard shortcuts on a button: `bindKey`
496
892
 
497
893
  A `UITextButton` can be activated from the keyboard as well as the pointer:
@@ -524,15 +920,130 @@ Three more rules, each with a reason you can feel in a game:
524
920
  - **The first input to press owns the button.** A pointer cannot release a key
525
921
  press and a key cannot release a pointer press, so a button cannot be left
526
922
  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.
923
+ - **A held key that is called off goes to `onCancel()`, not `onRelease()`.**
924
+ That happens when the window loses focus, the button is removed, or it is
925
+ disabled (`enabled = false`) while the key is down. If you use the pair as
926
+ press and depress, firing while held, stop in `onCancel()` too, or the gun
927
+ is still firing when the player alt-tabs back.
928
+ - **`isClickable` alone gates the press, not the release.** Clearing it while
929
+ the key is down still lets the key release the button, so it cannot stick.
532
930
 
533
931
  Shortcuts live only while the button is activated, and never start dragging or
534
932
  tap-and-hold: those stay pointer gestures.
535
933
 
934
+ ### Checkboxes and radio buttons: `UICheckbox`
935
+
936
+ ```js
937
+ const music = new UICheckbox(40, 60, {
938
+ labelOn: "Music ON", labelOff: "Music OFF", // or one `label`
939
+ checked: settings.music,
940
+ onChange: (on) => { settings.music = on; on ? audio.playTrack("theme") : audio.stopTrack(); },
941
+ });
942
+ ```
943
+
944
+ The label is part of the widget: clicking it toggles the box, and removing
945
+ the checkbox takes the label with it. Drawn with primitives by default, or
946
+ `skin: { atlas, on, off, hoverOn?, hoverOff?, disabledOn?, disabledOff? }`.
947
+ `boxSize`, `boxColor`, `boxHoverColor` and `checkColor` style the drawn box,
948
+ and `labelOn` / `labelOff` can be changed later: the widget refits.
949
+
950
+ **Setting `checked` from code does NOT call `onChange`**, nor does
951
+ `toggle()`. Only the user does. Restoring saved settings into the widgets
952
+ therefore does not re-run the game logic attached to them, so apply what
953
+ they stand for yourself after restoring:
954
+
955
+ ```js
956
+ music.checked = saved.music; // the look only
957
+ applyMusic(saved.music); // the effect, once
958
+ ```
959
+
960
+ **Radio buttons** are checkboxes sharing a `group`: exactly one is checked,
961
+ clicking the checked one leaves it checked (so does `toggle()`; set
962
+ `checked = false` to clear a group on purpose), and they are drawn round. Only
963
+ the one the user picked gets `onChange`, as in a web page; read the group with
964
+ `UICheckbox.checkedIn("difficulty")`. A checkbox is in its group from when it is
965
+ made until it is destroyed, so the group holds one choice, and `checkedIn`
966
+ answers, before the panel is ever added to the world.
967
+
968
+ ```js
969
+ for (const level of ["Easy", "Normal", "Hard"]) {
970
+ panel.addChild(new UICheckbox(0, 0, {
971
+ label: level,
972
+ group: "difficulty",
973
+ checked: level === saved.difficulty,
974
+ onChange: (on) => { if (on) { saved.difficulty = level; } },
975
+ }));
976
+ }
977
+ // later: the checked UICheckbox of the group, or undefined
978
+ const picked = UICheckbox.checkedIn("difficulty");
979
+ ```
980
+
981
+ **Skinned** from an atlas (the region names are your art's; these are the
982
+ UI example's):
983
+
984
+ ```js
985
+ new UICheckbox(0, 0, {
986
+ label: "Sound FX",
987
+ skin: {
988
+ atlas,
989
+ on: "green_boxCheckmark",
990
+ off: "grey_boxCheckmark",
991
+ // optional: hoverOn, hoverOff, disabledOn, disabledOff
992
+ },
993
+ });
994
+ ```
995
+
996
+ ### Sliders: `UISlider`
997
+
998
+ ```js
999
+ const volume = new UISlider(40, 120, {
1000
+ label: "Music", showValue: true,
1001
+ valueFormat: (v, ratio) => Math.round(ratio * 100) + "%",
1002
+ min: 0, max: 1, step: 0.01, value: audio.getVolume(),
1003
+ length: 300, // orientation: "vertical" too
1004
+ onChange: (v) => audio.setVolume(v), // every step of the drag
1005
+ onChangeEnd: () => saveSettings(), // once, on release
1006
+ });
1007
+ ```
1008
+
1009
+ Drag the thumb, or click the track to jump there. The drag follows the
1010
+ pointer after it leaves the track, until it is released. As on any widget, the
1011
+ press (`onClick`) ends in exactly one of `onRelease` (let go, before
1012
+ `onChangeEnd`) or `onCancel` (called off by `cancelPress()`, `enabled = false`
1013
+ or removal, with no `onChangeEnd`). The mouse wheel over
1014
+ the slider moves it one `step`. `value` snaps to `step` and clamps to the
1015
+ range, and setting it does not call `onChange` or `onChangeEnd`: the same rule
1016
+ as the checkbox, and neither does changing `min`, `max` or `step`, which
1017
+ re-clamp it. `ratio` reads where it is from 0 to 1, and `valueFormat` gets
1018
+ both. `labelText` changes the caption later. The slider refits to its texts
1019
+ (the widest value it can show, at either end of the range), and on a
1020
+ horizontal one the value moves below the track when it would run into the
1021
+ caption; a `UIPanel` holding it needs `relayout()` if its size changes. `thumbColor`, `thumbHoverColor`, `thumbBorderColor` and
1022
+ `thumbBorderWidth` style the drawn thumb. `skin: { atlas, track, fill?, thumb, thumbHover? }` draws it
1023
+ from art: the track and fill as nine-slices along its length, the thumb at its
1024
+ own size.
1025
+
1026
+ Put work that should not run 60 times a second, such as saving or playing a
1027
+ test sound, in `onChangeEnd`.
1028
+
1029
+ ```js
1030
+ // whole numbers: a step of 1 shows "7", not "7.00"
1031
+ new UISlider(0, 0, { label: "Difficulty", showValue: true, min: 1, max: 10, step: 1, value: 5 });
1032
+
1033
+ // vertical, 160 pixels tall: up raises it
1034
+ new UISlider(0, 0, { orientation: "vertical", length: 160, value: 0.5 });
1035
+
1036
+ // skinned: the track and fill stretch, the thumb keeps its size
1037
+ new UISlider(0, 0, {
1038
+ length: 240,
1039
+ // optional: fill (the part before the thumb), thumbHover
1040
+ skin: { atlas, track: "grey_sliderHorizontal", thumb: "blue_sliderDown" },
1041
+ });
1042
+ ```
1043
+
1044
+ With no `valueFormat`, the value text shows as many decimals as `step`
1045
+ has, and follows a later change of `step`.
1046
+
536
1047
  ### A fullscreen option
537
1048
 
538
1049
  Three methods on the `Application`, not on `device` (they moved there in
@@ -619,8 +1130,9 @@ keeps the four corners at their own size, and stretches only the edges and the
619
1130
  middle, so the border stays crisp at any size. Do not hand-roll it from nine
620
1131
  sprites.
621
1132
 
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:
1133
+ `UIPanel`'s `skin` and `UITextButton`'s `skin` already do this for you. By
1134
+ hand, from a texture atlas, the third argument of `createSpriteFromName` asks
1135
+ for the 9-slice version:
624
1136
 
625
1137
  ```js
626
1138
  class Panel extends UIBaseElement {
@@ -665,14 +1177,16 @@ sprite.
665
1177
  ### An opaque panel has to say so
666
1178
 
667
1179
  The hit test asks the topmost renderable first and then keeps walking down, so
668
- consuming is what stops a widget underneath answering too. A panel that can
669
- overlap another one therefore wants both:
1180
+ consuming is what stops a widget underneath answering too. A press is consumed
1181
+ by default on a UI element, but hover is not, so a panel of your own that can
1182
+ overlap another one also stops hover (`UIPanel` does all of this with
1183
+ `blocking`):
670
1184
 
671
1185
  ```js
672
1186
  class Panel extends UIBaseElement {
673
1187
  onClick() {
674
1188
  this.ancestor.moveToTop(this); // and bring it to the front
675
- return false; // consumed
1189
+ return false; // consumed (the default)
676
1190
  }
677
1191
  onOver() {
678
1192
  return false; // the frame the pointer crosses in
@@ -721,6 +1235,29 @@ of the level instead, with the scenery drawing over it. Both shipped idioms are
721
1235
  the same rule: afterBurner's HUD sits at `-150`, and the glTF, Billboard, Night
722
1236
  City and Instanced Forest examples park a floating sky at `-10000` or `100000`.
723
1237
 
1238
+ ## When the UI misbehaves
1239
+
1240
+ | symptom | cause | fix |
1241
+ |---|---|---|
1242
+ | loading saved settings plays the music, saves again or fires game logic | the game called its own change handler while restoring | set `checkbox.checked` / `slider.value`, which never call `onChange`, then apply the setting once yourself |
1243
+ | `onChange` never fires when settings are restored | by design: only user input calls it | apply the restored value yourself |
1244
+ | a button in a scrolling list fires when the user only meant to scroll | the work is in the `onClick()` METHOD, which runs on the press | use the `onClick` SETTING, called on a completed click; a scroll cancels the press |
1245
+ | clicking a HUD button also clicks the world behind it (before 20.13) | a `UIBaseElement` let presses through by default | update: presses now stop at UI elements; a handler that returns `true` still lets them through |
1246
+ | a custom widget no longer lets clicks reach what is behind it (since 20.13) | presses stop at UI elements by default | return `true` from its `onClick` / `onRelease` |
1247
+ | a button scrolled out of a hand-made list still gets clicks | its container does not clip, so the pointer still finds it where it moved to | use `UIPanel`'s `scroll`, or set `clipping` on the container: since 20.13 the pointer skips children outside a clipping container |
1248
+ | a layout ignores a label that changed size | layout runs on add, remove and resize only | call `panel.relayout()` |
1249
+ | the arrow keys or the pad do nothing in a menu | no widget has the focus: nothing listens until one does | call `firstWidget.focus()` when the menu opens |
1250
+ | the arrow keys stop moving the player after a menu | a widget still has the focus | `UIBaseElement.focusOwner?.blur()`, or remove the menu: a focused widget leaving the scene gives the keys back |
1251
+ | a `bindKey: ENTER` button does not fire while another widget is focused | Enter and Space go to the focused widget, ring shown or not, so one press never clicks both | focus that button, `blur()` the focused one, or bind another key |
1252
+ | a widget removed while held gets `onCancel()` and no `onRelease()` | the pointer-up never reaches a removed widget, so the press is called off (and `hover` cleared), leaving it at rest when added back | stop what `onClick()` started in `onCancel()` as well as in `onRelease()` |
1253
+ | a `ProgressBar` fill runs past its track after `max` is lowered (before 20.13) | `min` and `max` did not re-clamp the value | update: they re-clamp it now (and call `onChange` if it moved); `ratio` stays within 0..1 |
1254
+ | a skinned panel's shadow has square corners | the shadow follows `radius`, and a skin does not set it | set `radius` to the art's corner size |
1255
+ | a wrapped label lost its `\n` line breaks, or starts with a blank line (before 20.13) | word wrap joined the lines with spaces, and closed an empty line before an over-wide first word | update: each line is now wrapped on its own and an over-wide word starts the first line |
1256
+ | the second line of a multi-line `"bottom"` or `"middle"` `Text` is drawn below where you put it, or outside its bounds (before 20.13) | `Text` placed only the first line by its baseline | update: the whole block is placed by the baseline, as `BitmapText` does |
1257
+ | `setText("1,000")` leaves a label showing `["1", "000"]` on two lines, or the other way round (before 20.13) | the change test compared `toString()`, and an array's joins with commas; update |
1258
+ | a centred or right-aligned label jumps sideways, or is clipped, after `label.measureText("…")` (before 20.13) | measuring another string overwrote the label's own metrics until its next `setText()`; update, or measure on a label that is not displayed |
1259
+ | `new UITextButton` with a font name draws in a plain font | the name is not a loaded bitmap font, so it is drawn as a CSS font | load the bitmap font (image and `.fnt`) under that name, or load the web font |
1260
+
724
1261
  ## Related skills
725
1262
 
726
1263
  - `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this