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.
- package/README.md +3 -1
- package/build/application/application.d.ts.map +1 -1
- package/build/application/settings.d.ts +6 -2
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/backend/sound.d.ts.map +1 -1
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/spatial.d.ts +13 -2
- package/build/audio/spatial.d.ts.map +1 -1
- package/build/audio/types.d.ts +25 -3
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +2 -0
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/camera/effects/fade_effect.d.ts.map +1 -1
- package/build/camera/effects/pixelate_transition_effect.d.ts +67 -0
- package/build/camera/effects/pixelate_transition_effect.d.ts.map +1 -0
- package/build/geometries/ellipse.d.ts +11 -4
- package/build/geometries/ellipse.d.ts.map +1 -1
- package/build/geometries/line.d.ts +2 -2
- package/build/geometries/line.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +5 -4
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/geometries/rectangle.d.ts +2 -2
- package/build/geometries/rectangle.d.ts.map +1 -1
- package/build/geometries/roundrect.d.ts +2 -2
- package/build/geometries/roundrect.d.ts.map +1 -1
- package/build/index.d.ts +14 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +12716 -7233
- package/build/index.js.map +4 -4
- package/build/input/gamepad.d.ts.map +1 -1
- package/build/input/pointerevent.d.ts.map +1 -1
- package/build/lang/console.d.ts +16 -1
- package/build/lang/console.d.ts.map +1 -1
- package/build/level/tiled/TMXLayer.d.ts +48 -14
- package/build/level/tiled/TMXLayer.d.ts.map +1 -1
- package/build/level/tiled/TMXTileMap.d.ts +1 -1
- package/build/level/tiled/TMXTileMap.d.ts.map +1 -1
- package/build/level/tiled/TMXTileset.d.ts.map +1 -1
- package/build/level/tiled/TMXUtils.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXHexagonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXIsometricRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXObliqueRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXOrthogonalRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXRenderer.d.ts +1 -1
- package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
- package/build/loader/loader.d.ts +55 -11
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/loadingscreen.d.ts.map +1 -1
- package/build/math/color.d.ts.map +1 -1
- package/build/math/math.d.ts +6 -1
- package/build/math/math.d.ts.map +1 -1
- package/build/math/matrix2d.d.ts +3 -2
- package/build/math/matrix2d.d.ts.map +1 -1
- package/build/math/matrix3d.d.ts +4 -1
- package/build/math/matrix3d.d.ts.map +1 -1
- package/build/math/observableVector2d.d.ts +6 -6
- package/build/math/observableVector2d.d.ts.map +1 -1
- package/build/math/observableVector3d.d.ts +8 -8
- package/build/math/observableVector3d.d.ts.map +1 -1
- package/build/particles/emitter.d.ts +15 -3
- package/build/particles/emitter.d.ts.map +1 -1
- package/build/particles/particle.d.ts +6 -0
- package/build/particles/particle.d.ts.map +1 -1
- package/build/particles/settings.d.ts +22 -5
- package/build/particles/settings.d.ts.map +1 -1
- package/build/physics/bounds.d.ts +5 -5
- package/build/physics/bounds.d.ts.map +1 -1
- package/build/physics/builtin/body.d.ts +15 -4
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/builtin-adapter.d.ts.map +1 -1
- package/build/physics/builtin/detector.d.ts.map +1 -1
- package/build/physics/builtin/raycast.d.ts.map +1 -1
- package/build/physics/world.d.ts.map +1 -1
- package/build/renderable/animation.d.ts +6 -1
- package/build/renderable/animation.d.ts.map +1 -1
- package/build/renderable/container.d.ts +40 -6
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/entity/entity.d.ts.map +1 -1
- package/build/renderable/frameAnimation.d.ts.map +1 -1
- package/build/renderable/imagelayer.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +8 -2
- package/build/renderable/sprite.d.ts.map +1 -1
- package/build/renderable/text/bitmaptext.d.ts +12 -3
- package/build/renderable/text/bitmaptext.d.ts.map +1 -1
- package/build/renderable/text/text.d.ts +147 -4
- package/build/renderable/text/text.d.ts.map +1 -1
- package/build/renderable/text/textmetrics.d.ts +10 -2
- package/build/renderable/text/textmetrics.d.ts.map +1 -1
- package/build/renderable/text/textstyle.d.ts.map +1 -1
- package/build/renderable/trail3d.d.ts.map +1 -1
- package/build/renderable/ui/panelpaint.d.ts +5 -1
- package/build/renderable/ui/panelpaint.d.ts.map +1 -1
- package/build/renderable/ui/progressbar.d.ts +267 -95
- package/build/renderable/ui/progressbar.d.ts.map +1 -1
- package/build/renderable/ui/uibaseelement.d.ts +436 -50
- package/build/renderable/ui/uibaseelement.d.ts.map +1 -1
- package/build/renderable/ui/uicheckbox.d.ts +383 -0
- package/build/renderable/ui/uicheckbox.d.ts.map +1 -0
- package/build/renderable/ui/uifocus.d.ts +6 -0
- package/build/renderable/ui/uifocus.d.ts.map +1 -0
- package/build/renderable/ui/uilabel.d.ts +31 -0
- package/build/renderable/ui/uilabel.d.ts.map +1 -0
- package/build/renderable/ui/uipanel.d.ts +589 -116
- package/build/renderable/ui/uipanel.d.ts.map +1 -1
- package/build/renderable/ui/uiskin.d.ts +25 -0
- package/build/renderable/ui/uiskin.d.ts.map +1 -0
- package/build/renderable/ui/uislider.d.ts +518 -0
- package/build/renderable/ui/uislider.d.ts.map +1 -0
- package/build/renderable/ui/uispriteelement.d.ts +161 -85
- package/build/renderable/ui/uispriteelement.d.ts.map +1 -1
- package/build/renderable/ui/uitextbutton.d.ts +369 -63
- package/build/renderable/ui/uitextbutton.d.ts.map +1 -1
- package/build/state/stage.d.ts +3 -2
- package/build/state/stage.d.ts.map +1 -1
- package/build/state/state.d.ts +31 -6
- package/build/state/state.d.ts.map +1 -1
- package/build/system/legacy_pool.d.ts.map +1 -1
- package/build/system/pool.d.ts.map +1 -1
- package/build/system/save.d.ts +2 -1
- package/build/system/save.d.ts.map +1 -1
- package/build/tweens/tween.d.ts +5 -2
- package/build/tweens/tween.d.ts.map +1 -1
- package/build/utils/fetchdata.d.ts.map +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +1 -5
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/effects/bloom.d.ts +3 -2
- package/build/video/effects/bloom.d.ts.map +1 -1
- package/build/video/effects/blur.d.ts +1 -1
- package/build/video/effects/chromaticAberration.d.ts +1 -1
- package/build/video/effects/dissolve.d.ts.map +1 -1
- package/build/video/effects/dropShadow.d.ts +1 -1
- package/build/video/effects/glow.d.ts +1 -1
- package/build/video/effects/glsl_realization.d.ts.map +1 -1
- package/build/video/effects/hologram.d.ts.map +1 -1
- package/build/video/effects/local_space.d.ts +19 -0
- package/build/video/effects/local_space.d.ts.map +1 -0
- package/build/video/effects/mesh_splice.d.ts.map +1 -1
- package/build/video/effects/outline.d.ts +1 -1
- package/build/video/effects/pixelate.d.ts +1 -1
- package/build/video/effects/radialGradient.d.ts +4 -12
- package/build/video/effects/radialGradient.d.ts.map +1 -1
- package/build/video/effects/scanline.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/effects/shine.d.ts +5 -0
- package/build/video/effects/shine.d.ts.map +1 -1
- package/build/video/effects/sway.d.ts +9 -4
- package/build/video/effects/sway.d.ts.map +1 -1
- package/build/video/effects/vignette.d.ts.map +1 -1
- package/build/video/effects/wave.d.ts +9 -7
- package/build/video/effects/wave.d.ts.map +1 -1
- package/build/video/effects/wgsl/parse.d.ts.map +1 -1
- package/build/video/effects/wgsl/scaffold.d.ts.map +1 -1
- package/build/video/effects/wgsl_realization.d.ts.map +1 -1
- package/build/video/renderer.d.ts +22 -5
- package/build/video/renderer.d.ts.map +1 -1
- package/build/video/renderstate.d.ts +26 -3
- package/build/video/renderstate.d.ts.map +1 -1
- package/build/video/rendertarget/webgpurendertarget.d.ts +1 -0
- package/build/video/rendertarget/webgpurendertarget.d.ts.map +1 -1
- package/build/video/texture/atlas.d.ts +31 -31
- package/build/video/texture/atlas.d.ts.map +1 -1
- package/build/video/utils/pixel_snap.d.ts +44 -0
- package/build/video/utils/pixel_snap.d.ts.map +1 -0
- package/build/video/utils/tessellation.d.ts +11 -0
- package/build/video/utils/tessellation.d.ts.map +1 -1
- package/build/video/webgl/batchers/lit_quad_batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgl/batchers/quad_batcher.d.ts +2 -1
- package/build/video/webgl/batchers/quad_batcher.d.ts.map +1 -1
- package/build/video/webgl/renderers/tmxlayer/orthogonal.d.ts.map +1 -1
- package/build/video/webgl/webgl_renderer.d.ts +3 -6
- package/build/video/webgl/webgl_renderer.d.ts.map +1 -1
- package/build/video/webgpu/batchers/mesh_batcher.d.ts +0 -1
- package/build/video/webgpu/batchers/mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/quad_batcher.d.ts.map +1 -1
- package/build/video/webgpu/effect_binding.d.ts.map +1 -1
- package/build/video/webgpu/renderers/tmxlayer/orthogonal.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +6 -10
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/AGENTS.md +2 -1
- package/skills/melonjs/SKILL.md +3 -2
- package/skills/melonjs-audio/SKILL.md +31 -8
- package/skills/melonjs-camera-and-drawing/SKILL.md +41 -1
- package/skills/melonjs-display-and-scaling/SKILL.md +19 -5
- package/skills/melonjs-effects-and-shaders/SKILL.md +53 -17
- package/skills/melonjs-events/SKILL.md +3 -1
- package/skills/melonjs-getting-started/SKILL.md +1 -2
- package/skills/melonjs-input/SKILL.md +61 -3
- package/skills/melonjs-lighting/SKILL.md +4 -3
- package/skills/melonjs-loading-assets/SKILL.md +57 -15
- package/skills/melonjs-math/SKILL.md +14 -3
- package/skills/melonjs-particles-and-trails/SKILL.md +72 -8
- package/skills/melonjs-performance/SKILL.md +3 -0
- package/skills/melonjs-physics/SKILL.md +53 -4
- package/skills/melonjs-plugins/SKILL.md +25 -1
- package/skills/melonjs-renderables/SKILL.md +11 -3
- package/skills/melonjs-renderer-backends/SKILL.md +2 -2
- package/skills/melonjs-scenes-and-state/SKILL.md +32 -2
- package/skills/melonjs-sprites-and-animation/SKILL.md +14 -0
- package/skills/melonjs-tilemaps/SKILL.md +96 -2
- 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
|
|
9
|
+
## Outlined text: behind the glyphs, or over them
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
`
|
|
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)` |
|
|
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
|
-
|
|
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.
|
|
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
|
-
- **
|
|
528
|
-
|
|
529
|
-
`
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
623
|
-
|
|
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
|
|
669
|
-
|
|
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
|