melonjs 20.12.0 → 20.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -22
- package/build/application/application.d.ts +6 -1
- package/build/application/application.d.ts.map +1 -1
- package/build/application/header.d.ts.map +1 -1
- package/build/application/resize.d.ts.map +1 -1
- package/build/application/settings.d.ts +13 -8
- package/build/application/settings.d.ts.map +1 -1
- package/build/audio/audio.d.ts +8 -4
- package/build/audio/audio.d.ts.map +1 -1
- package/build/audio/backend/engine.d.ts.map +1 -1
- package/build/audio/playback.d.ts +2 -2
- package/build/audio/playback.d.ts.map +1 -1
- package/build/audio/spatial.d.ts.map +1 -1
- package/build/audio/types.d.ts +15 -5
- package/build/audio/types.d.ts.map +1 -1
- package/build/camera/camera2d.d.ts +12 -9
- package/build/camera/camera2d.d.ts.map +1 -1
- package/build/camera/camera3d.d.ts +11 -8
- package/build/camera/camera3d.d.ts.map +1 -1
- package/build/camera/effects/fade_effect.d.ts.map +1 -1
- package/build/geometries/ellipse.d.ts +3 -0
- package/build/geometries/ellipse.d.ts.map +1 -1
- package/build/geometries/line.d.ts.map +1 -1
- package/build/geometries/path2d.d.ts +9 -8
- package/build/geometries/path2d.d.ts.map +1 -1
- package/build/geometries/polygon.d.ts +2 -1
- package/build/geometries/polygon.d.ts.map +1 -1
- package/build/index.d.ts +14 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +50215 -45051
- package/build/index.js.map +4 -4
- package/build/input/gamepad.d.ts.map +1 -1
- package/build/input/keyboard.d.ts.map +1 -1
- package/build/input/pointerevent.d.ts.map +1 -1
- package/build/level/gltf/GLTFModel.d.ts.map +1 -1
- package/build/level/level.d.ts.map +1 -1
- package/build/level/tiled/TMXLayer.d.ts +50 -15
- package/build/level/tiled/TMXLayer.d.ts.map +1 -1
- package/build/level/tiled/TMXTileMap.d.ts +14 -4
- 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 +1 -0
- 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 +7 -3
- package/build/level/tiled/renderer/TMXRenderer.d.ts.map +1 -1
- package/build/level/tiled/renderer/TMXStaggeredRenderer.d.ts.map +1 -1
- package/build/lighting/light3d.d.ts +55 -0
- package/build/lighting/light3d.d.ts.map +1 -1
- package/build/loader/cache.d.ts +146 -97
- package/build/loader/cache.d.ts.map +1 -1
- package/build/loader/internal-types.d.ts +69 -0
- package/build/loader/internal-types.d.ts.map +1 -0
- package/build/loader/loader.d.ts +589 -253
- package/build/loader/loader.d.ts.map +1 -1
- package/build/loader/parsers/audio.d.ts.map +1 -1
- package/build/loader/parsers/fontface.d.ts.map +1 -1
- package/build/loader/parsers/gltf.d.ts.map +1 -1
- package/build/loader/parsers/tmx.d.ts.map +1 -1
- package/build/math/color.d.ts +4 -0
- 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/noise.d.ts +16 -16
- package/build/math/noise.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/adapter.d.ts +64 -25
- package/build/physics/adapter.d.ts.map +1 -1
- package/build/physics/builtin/body.d.ts +23 -4
- package/build/physics/builtin/body.d.ts.map +1 -1
- package/build/physics/builtin/builtin-adapter.d.ts +25 -3
- 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 +40 -10
- package/build/physics/world.d.ts.map +1 -1
- package/build/renderable/container.d.ts +41 -6
- package/build/renderable/container.d.ts.map +1 -1
- package/build/renderable/draggable.d.ts.map +1 -1
- package/build/renderable/frameAnimation.d.ts.map +1 -1
- package/build/renderable/instanced_mesh.d.ts +16 -14
- package/build/renderable/instanced_mesh.d.ts.map +1 -1
- package/build/renderable/mesh.d.ts +62 -5
- package/build/renderable/mesh.d.ts.map +1 -1
- package/build/renderable/renderable.d.ts.map +1 -1
- package/build/renderable/sprite.d.ts +4 -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/trigger.d.ts +5 -0
- package/build/renderable/trigger.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 +443 -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.map +1 -1
- package/build/system/device.d.ts +11 -2
- package/build/system/device.d.ts.map +1 -1
- package/build/system/fullscreen.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/utils/utils.d.ts.map +1 -1
- package/build/video/canvas/canvas_renderer.d.ts +16 -19
- package/build/video/canvas/canvas_renderer.d.ts.map +1 -1
- package/build/video/canvas_factory.d.ts.map +1 -1
- package/build/video/effects/shadereffect.d.ts +127 -0
- package/build/video/effects/shadereffect.d.ts.map +1 -1
- package/build/video/renderer.d.ts +20 -4
- 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/utils/tessellation.d.ts +11 -0
- package/build/video/utils/tessellation.d.ts.map +1 -1
- package/build/video/webgl/batchers/lit_mesh_batcher.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/lit_mesh_batcher.d.ts.map +1 -1
- package/build/video/webgpu/batchers/primitive_batcher.d.ts.map +1 -1
- package/build/video/webgpu/webgpu_renderer.d.ts +3 -6
- package/build/video/webgpu/webgpu_renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/melonjs/SKILL.md +41 -17
- package/skills/melonjs-20-migration/SKILL.md +50 -7
- package/skills/melonjs-3d/SKILL.md +138 -38
- package/skills/melonjs-3d-assets/SKILL.md +83 -33
- package/skills/melonjs-audio/SKILL.md +62 -19
- package/skills/melonjs-camera-and-drawing/SKILL.md +135 -8
- package/skills/melonjs-deployment/SKILL.md +92 -14
- package/skills/melonjs-display-and-scaling/SKILL.md +8 -3
- package/skills/melonjs-effects-and-shaders/SKILL.md +47 -15
- package/skills/melonjs-events/SKILL.md +6 -3
- package/skills/melonjs-getting-started/SKILL.md +6 -2
- package/skills/melonjs-input/SKILL.md +102 -16
- package/skills/melonjs-lighting/SKILL.md +13 -1
- package/skills/melonjs-loading-assets/SKILL.md +255 -39
- package/skills/melonjs-math/SKILL.md +26 -8
- package/skills/melonjs-particles-and-trails/SKILL.md +77 -9
- package/skills/melonjs-performance/SKILL.md +18 -8
- package/skills/melonjs-physics/SKILL.md +435 -128
- package/skills/melonjs-plugins/SKILL.md +50 -11
- package/skills/melonjs-renderables/SKILL.md +31 -7
- package/skills/melonjs-renderer-backends/SKILL.md +38 -17
- package/skills/melonjs-scenes-and-state/SKILL.md +45 -5
- package/skills/melonjs-sprites-and-animation/SKILL.md +37 -2
- package/skills/melonjs-tilemaps/SKILL.md +149 -17
- package/skills/melonjs-ui-and-text/SKILL.md +674 -42
|
@@ -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,
|
|
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), keyboard and gamepad focus, 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, slider, volume slider, options menu, settings screen, scrolling list, UI skin, progress bar, health bar, menu, dialogue, moveToTop, onOver, onClick, bindKey, keyboard shortcut, Tween, animate a label, wavy text, glyphEffect, rainbow text, typewriter, visibleRatio, drag and drop, isDraggable, focus, fillStyle, fillGradient, gradient text, tint, font, fontface, wordWrapWidth, score display, fullscreen, requestFullscreen, fullscreen button."
|
|
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,58 @@ 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
|
+
|
|
207
|
+
### Typewriter reveal: `visibleRatio`
|
|
208
|
+
|
|
209
|
+
`visibleCharacters` (`-1`, the default, shows them all) and `visibleRatio`
|
|
210
|
+
(0 to 1) show a label a character at a time, on `Text` and `BitmapText` alike.
|
|
211
|
+
Tween the ratio: it takes the same time whatever the length of the string.
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
line.setText("The door is locked. The keeper has the key.");
|
|
215
|
+
line.visibleRatio = 0;
|
|
216
|
+
new Tween(line).to({ visibleRatio: 1 }, { duration: 1500 }).start();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Line breaks are not counted, and the wrapping and the bounds are those of the
|
|
220
|
+
whole string, so a word does not jump to the next line as it appears. The two
|
|
221
|
+
classes differ in two ways. A `BitmapText` keeps each glyph where the full line
|
|
222
|
+
puts it, while a centred or right-aligned `Text` aligns only the part shown so
|
|
223
|
+
far, so it slides sideways as it types: left-align a `Text` you reveal. And a
|
|
224
|
+
`Text` repaints and re-uploads its bake at every step, where a `BitmapText`
|
|
225
|
+
just draws fewer glyphs.
|
|
226
|
+
|
|
134
227
|
### `fillStyle` means two different things
|
|
135
228
|
|
|
136
229
|
The name is shared and the behaviour is not, which is the trap when moving a
|
|
@@ -142,7 +235,7 @@ label from one class to the other:
|
|
|
142
235
|
| white | white glyphs | the ABSENCE of a tint |
|
|
143
236
|
| gradient | yes, via `fillGradient` | no: a tint is one colour over a page image |
|
|
144
237
|
| 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 |
|
|
238
|
+
| stroke | `strokeStyle` + `lineWidth`, behind or over the glyphs with `paintOrder`, corners with `lineJoin` / `miterLimit` | none |
|
|
146
239
|
| `size` | pixels | a RATIO of the authored size |
|
|
147
240
|
|
|
148
241
|
### Per-glyph wave, shake and colour: `glyphEffect`
|
|
@@ -218,10 +311,13 @@ an amplitude around an eighth of it, a wavelength around ten times it, and a
|
|
|
218
311
|
little under 5 radians per second. For a shake, re-target each glyph roughly
|
|
219
312
|
20 to 40 times a second.
|
|
220
313
|
|
|
221
|
-
`ctx.time`
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
314
|
+
`ctx.time` runs only while an effect is set and nothing resets it, so it is 0
|
|
315
|
+
only when a label gets its **first** effect. A label that already had one, or
|
|
316
|
+
is reused across lives, carries the old clock, and a decay measured from
|
|
317
|
+
`ctx.time` itself is already spent there. Note `ctx.time` on the effect's first
|
|
318
|
+
call and measure from that, as the shake below does, and attach it at the
|
|
319
|
+
moment you want to be t=0: on the frame the hit lands, not when the label was
|
|
320
|
+
built.
|
|
225
321
|
|
|
226
322
|
Both, whole:
|
|
227
323
|
|
|
@@ -249,22 +345,28 @@ const angle = (i, tick) => {
|
|
|
249
345
|
const h = Math.sin(i * 127.1 + tick * 311.7) * 43758.5453;
|
|
250
346
|
return (h - Math.floor(h)) * Math.PI * 2;
|
|
251
347
|
};
|
|
252
|
-
const impact = (amplitude, durationMs, rate) =>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
348
|
+
const impact = (amplitude, durationMs, rate) => {
|
|
349
|
+
let start; // ctx.time on the first call
|
|
350
|
+
return (out, ctx) => {
|
|
351
|
+
start ??= ctx.time; // the label's clock never resets
|
|
352
|
+
const t = ctx.time - start;
|
|
353
|
+
const left = 1 - t / durationMs;
|
|
354
|
+
if (left <= 0) {
|
|
355
|
+
return; // settled; `out` is already zeroed
|
|
356
|
+
}
|
|
357
|
+
const amp = amplitude * left * left;
|
|
358
|
+
const tick = (t / 1000) * rate;
|
|
359
|
+
const held = Math.floor(tick);
|
|
360
|
+
const k = tick - held;
|
|
361
|
+
const a = angle(ctx.index, held);
|
|
362
|
+
const b = angle(ctx.index, held + 1);
|
|
363
|
+
out.offsetX = Math.round((Math.sin(a) + (Math.sin(b) - Math.sin(a)) * k) * amp);
|
|
364
|
+
out.offsetY = Math.round((Math.cos(a) + (Math.cos(b) - Math.cos(a)) * k) * amp);
|
|
365
|
+
};
|
|
265
366
|
};
|
|
266
367
|
|
|
267
|
-
//
|
|
368
|
+
// a new impact at the moment of each hit: it measures from its own first call,
|
|
369
|
+
// so it shakes again on a banner that has shown one before
|
|
268
370
|
banner.setText("CAPSIZED!");
|
|
269
371
|
banner.glyphEffect = impact(3, 650, 24);
|
|
270
372
|
```
|
|
@@ -290,6 +392,68 @@ There is no `UIContainer` class. Use `Container` for a bare grouping,
|
|
|
290
392
|
`UIBaseElement` when you want the pointer handling and nothing drawn — it is a
|
|
291
393
|
`Container` that already sets `floating = true` and `isKinematic = false`.
|
|
292
394
|
|
|
395
|
+
### Which widget for what
|
|
396
|
+
|
|
397
|
+
| you want | use | notes |
|
|
398
|
+
|---|---|---|
|
|
399
|
+
| a button with a label | `UITextButton` | drawn, or skinned from an atlas; `onClick` setting |
|
|
400
|
+
| an image-only button or icon | `UISpriteElement` | a `UIBaseElement` holding a `Sprite` (`sprite`); override `onClick` |
|
|
401
|
+
| an on/off option, or one of several | `UICheckbox` | a radio button with `group` |
|
|
402
|
+
| a value in a range: volume, sensitivity | `UISlider` | horizontal or vertical |
|
|
403
|
+
| health, shield, cooldown, loading | `ProgressBar` | not clickable; `value` redraws |
|
|
404
|
+
| a frame, dialog or menu card | `UIPanel` | skin, title, layout, scrolling, drag, shadow |
|
|
405
|
+
| your own clickable widget | extend `UIBaseElement` | pointer handlers, `enabled`, focus, bounce |
|
|
406
|
+
| a group with nothing drawn | `Container` | set `floating` for screen space |
|
|
407
|
+
|
|
408
|
+
### A whole options menu
|
|
409
|
+
|
|
410
|
+
Everything below in one place: a skinned panel that lays its widgets out
|
|
411
|
+
as a column, a checkbox, a slider, two buttons, a drop shadow, a little
|
|
412
|
+
bounce, and keyboard / gamepad play. Each piece has its own section after
|
|
413
|
+
this.
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
import {
|
|
417
|
+
UICheckbox, UIPanel, UISlider, UITextButton, audio, state,
|
|
418
|
+
} from "melonjs";
|
|
419
|
+
|
|
420
|
+
// one style object for every widget's text
|
|
421
|
+
const FONT = { font: "kenpixel", fontSize: 14, labelFillStyle: "black" };
|
|
422
|
+
|
|
423
|
+
const menu = new UIPanel(40, 40, {
|
|
424
|
+
width: 360, height: 100, // autoSize grows the height to fit
|
|
425
|
+
skin: { atlas, region: "grey_panel", insetx: 10, insety: 10 },
|
|
426
|
+
title: "OPTIONS", titleFont: "kenpixel", titleFontSize: 20,
|
|
427
|
+
layout: "column", padding: 16, gap: 12, align: "stretch", autoSize: true,
|
|
428
|
+
shadowColor: "#00000059", shadowOffsetY: 6, shadowBlur: 10, radius: 6,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const music = menu.addChild(new UICheckbox(0, 0, {
|
|
432
|
+
...FONT,
|
|
433
|
+
labelOn: "Music ON", labelOff: "Music OFF",
|
|
434
|
+
checked: settings.music,
|
|
435
|
+
onChange: (on) => { settings.music = on; }, // user input only
|
|
436
|
+
}));
|
|
437
|
+
|
|
438
|
+
menu.addChild(new UISlider(0, 0, {
|
|
439
|
+
...FONT,
|
|
440
|
+
label: "Volume", showValue: true,
|
|
441
|
+
valueFormat: (v, ratio) => `${Math.round(ratio * 100)}%`,
|
|
442
|
+
value: audio.getVolume(),
|
|
443
|
+
onChange: (v) => audio.setVolume(v), // while dragging
|
|
444
|
+
onChangeEnd: () => saveSettings(), // once, on release
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
menu.addChild(new UITextButton(0, 0, {
|
|
448
|
+
...FONT, text: "Back", height: 40,
|
|
449
|
+
hoverScale: 1.04, pressScale: 0.96, // the bounce
|
|
450
|
+
onClick: () => state.change(state.MENU), // a completed click
|
|
451
|
+
}));
|
|
452
|
+
|
|
453
|
+
app.world.addChild(menu, 10);
|
|
454
|
+
music.focus(); // arrows / D-pad and Enter / A now drive the menu
|
|
455
|
+
```
|
|
456
|
+
|
|
293
457
|
Three things matter here:
|
|
294
458
|
|
|
295
459
|
- **`floating = true`** opts the container out of camera transforms, so it stays
|
|
@@ -321,11 +485,19 @@ const hp = new ProgressBar(20, 20, {
|
|
|
321
485
|
hud.addChild(hp);
|
|
322
486
|
|
|
323
487
|
hp.value -= 15; // clamps; `ratio` reads back 0..1
|
|
488
|
+
hp.max = 50; // re-clamps the value into the new range
|
|
324
489
|
```
|
|
325
490
|
|
|
326
491
|
`direction` takes `"left-to-right"` (the default), `"right-to-left"`,
|
|
327
492
|
`"top-to-bottom"` or `"bottom-to-top"`, each growing from its own edge.
|
|
328
493
|
|
|
494
|
+
`showLabel: true` writes text centred over the bar, a percentage by default:
|
|
495
|
+
`` labelFormat: (value, ratio) => `${Math.round(value)} HP` `` changes what it says,
|
|
496
|
+
and `font` (a CSS family), `fontSize` and `labelFillStyle` how it looks.
|
|
497
|
+
`onChange(value, ratio, bar)` is called on every change of the value, from code
|
|
498
|
+
too, unlike a checkbox's or a slider's: a bar has no user input to tell apart.
|
|
499
|
+
Setting the value it already has calls nothing.
|
|
500
|
+
|
|
329
501
|
**The colour follows the value by mutating the `Color` you passed.** This is
|
|
330
502
|
the one non-obvious part, and it is deliberate: every game wants a different
|
|
331
503
|
rule, and most want the colour somewhere else too, next to a shader uniform or
|
|
@@ -441,6 +613,118 @@ One consequence worth knowing: `getBounds()` on a bobbing panel answers
|
|
|
441
613
|
differently each frame, so do not cache it or compare two children's bounds
|
|
442
614
|
while a bob is running.
|
|
443
615
|
|
|
616
|
+
### Panel art, title, dragging: `skin`, `title`, `draggable`
|
|
617
|
+
|
|
618
|
+
```js
|
|
619
|
+
const options = new UIPanel(20, 20, {
|
|
620
|
+
width: 380, height: 460,
|
|
621
|
+
// one atlas region, stretched as a nine-slice to the panel's size
|
|
622
|
+
skin: { atlas, region: "grey_panel", insetx: 10, insety: 10 },
|
|
623
|
+
title: "OPTIONS", titleFont: "kenpixel", titleFontSize: 20, titleFillStyle: "black",
|
|
624
|
+
draggable: true, // comes to the front when grabbed
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**A drop shadow**, here or on a `ProgressBar`, takes a colour whose alpha is
|
|
629
|
+
how dark it gets: `shadowColor: "#00000059"`, with `shadowOffsetX` (0),
|
|
630
|
+
`shadowOffsetY` (4) and `shadowBlur` (8 pixels of soft edge, `0` for sharp).
|
|
631
|
+
There is none until `shadowColor` is set.
|
|
632
|
+
|
|
633
|
+
```js
|
|
634
|
+
const card = new UIPanel(100, 80, {
|
|
635
|
+
width: 300, height: 180, fillColor: "#f0f0f0", radius: 8,
|
|
636
|
+
shadowColor: "#00000059", // about 35% black
|
|
637
|
+
shadowOffsetY: 6, // drops it below the card
|
|
638
|
+
shadowBlur: 10, // pixels of soft edge
|
|
639
|
+
});
|
|
640
|
+
const hp = new ProgressBar(20, 20, {
|
|
641
|
+
width: 200, height: 16, radius: 8,
|
|
642
|
+
shadowColor: "#00000080", shadowOffsetY: 2, shadowBlur: 4,
|
|
643
|
+
});
|
|
644
|
+
```
|
|
645
|
+
It follows `radius`, which a skinned panel can set to match its art's
|
|
646
|
+
corners, and it is drawn outside the bounds, so it never takes the pointer.
|
|
647
|
+
Under a see-through fill or track it shows through; give that a solid colour.
|
|
648
|
+
|
|
649
|
+
The title sits at the top and the content starts below it. `insetx` /
|
|
650
|
+
`insety` are the unscaled corners of the art, and also where the frame ends:
|
|
651
|
+
layout `padding` is measured from there.
|
|
652
|
+
|
|
653
|
+
### Laying a panel out: `layout`
|
|
654
|
+
|
|
655
|
+
Leave `layout` at `"none"` and children stay where you put them. Otherwise
|
|
656
|
+
the panel places them, below the title and inside the frame:
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
const menu = new UIPanel(x, y, {
|
|
660
|
+
width: 360, height: 100,
|
|
661
|
+
layout: "column", // "row", "grid" (with `columns`), or "none"
|
|
662
|
+
padding: 16, // from the frame to the content
|
|
663
|
+
gap: 12, // between children
|
|
664
|
+
align: "stretch", // "start", "center", "end", or "stretch"
|
|
665
|
+
autoSize: true, // grow the panel to fit
|
|
666
|
+
});
|
|
667
|
+
menu.addChild(new UITextButton(0, 0, { text: "Play", onClick: play }));
|
|
668
|
+
menu.addChild(new UITextButton(0, 0, { text: "Options", onClick: openOptions }));
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Children are laid out in the order they were added. It runs when a child is
|
|
672
|
+
added or removed and when the panel is resized, **not every frame**: a child
|
|
673
|
+
that changes size afterwards, a label whose text grew, needs
|
|
674
|
+
`menu.relayout()`. `"stretch"` resizes buttons and panels, which lay
|
|
675
|
+
themselves out again at a new size; anything else keeps its size.
|
|
676
|
+
|
|
677
|
+
A row inside a column is a second panel that draws nothing and lets clicks
|
|
678
|
+
through: `new UIPanel(0, 0, { width, height, layout: "row", blocking: false })`.
|
|
679
|
+
|
|
680
|
+
### Scrolling lists: `scroll`
|
|
681
|
+
|
|
682
|
+
```js
|
|
683
|
+
const levels = new UIPanel(420, 435, {
|
|
684
|
+
width: 340, height: 150,
|
|
685
|
+
layout: "grid", columns: 5, gap: 8, padding: 10,
|
|
686
|
+
scroll: "vertical", // "horizontal", "both", or "none"
|
|
687
|
+
onScroll: (x, y, panel) => { saved.levelScroll = y; },
|
|
688
|
+
});
|
|
689
|
+
const buttons = [];
|
|
690
|
+
for (let i = 1; i <= 30; i++) {
|
|
691
|
+
buttons[i] = levels.addChild(new UITextButton(0, 0, {
|
|
692
|
+
text: String(i), width: 56, height: 40,
|
|
693
|
+
enabled: i <= unlocked,
|
|
694
|
+
onClick: () => startLevel(i),
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
levels.scrollTo(0, saved.levelScroll); // restore where it was
|
|
698
|
+
levels.scrollToChild(buttons[unlocked]); // or show the latest
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
The content is cropped to the area inside the frame and below the title, and
|
|
702
|
+
scrolls with the wheel over the panel and by dragging, with momentum when
|
|
703
|
+
flung. A scrollbar shows while it does not fit. `scrollTo(x, y)`,
|
|
704
|
+
`scrollToChild(child)`, `scrollX` / `scrollY`, `maxScrollX` / `maxScrollY` and
|
|
705
|
+
an `onScroll(x, y, panel)` setting are there to drive it from code. Scrolling
|
|
706
|
+
from code stops a fling that is still coasting. `clearChildren()` empties the
|
|
707
|
+
list and scrolls back to the start, ready to be filled again, and `autoSize`
|
|
708
|
+
is ignored on a scrolling panel.
|
|
709
|
+
|
|
710
|
+
What to know:
|
|
711
|
+
|
|
712
|
+
- **Scrolling moves the children.** Their `pos` is where they are drawn, so
|
|
713
|
+
it changes as the list scrolls. A child added to a `"none"` layout while
|
|
714
|
+
scrolled is placed in content coordinates and moved into view.
|
|
715
|
+
- **A drag that starts on a button scrolls the list and cancels the press.**
|
|
716
|
+
The button gets `onCancel()` instead of `onRelease()`, and its `onClick`
|
|
717
|
+
SETTING is not called. The `onClick()` METHOD already ran on the press, so
|
|
718
|
+
a button in a list does its work in the setting: see the next section.
|
|
719
|
+
- **A slider in a scrolling panel is dragged, not the panel.** A widget of
|
|
720
|
+
your own that is dragged too, a knob or a colour picker, sets
|
|
721
|
+
`keepsDrag = true` to keep its drags.
|
|
722
|
+
- Children scrolled out of view do not get pointer events. A scrolling panel
|
|
723
|
+
inside another scrolls alone, and is cropped to both: a clip inside a clip
|
|
724
|
+
is the overlap of the two, on Canvas, WebGL and WebGPU alike.
|
|
725
|
+
- A draggable panel is moved by its title or its frame. A press on a widget
|
|
726
|
+
on it is the widget's.
|
|
727
|
+
|
|
444
728
|
### A pause overlay: `updateWhenPaused` cascades
|
|
445
729
|
|
|
446
730
|
`state.pause()` stops every world child stepping while rendering carries on, so
|
|
@@ -460,16 +744,18 @@ it, silently. A panel whose labels never appear while paused is this.
|
|
|
460
744
|
|
|
461
745
|
### Buttons: extend the handlers, do not bind listeners
|
|
462
746
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
`
|
|
747
|
+
Every UI element already registers itself for pointer events, so a button is
|
|
748
|
+
made by overriding methods rather than by wiring `registerPointerEvent`. For
|
|
749
|
+
an image-only button that is `UISpriteElement`: a `UIBaseElement` holding a
|
|
750
|
+
`Sprite`, placed like a sprite (`(x, y)` is where its anchor goes, the centre
|
|
751
|
+
by default):
|
|
466
752
|
|
|
467
753
|
| handler | fires | returns |
|
|
468
754
|
|---|---|---|
|
|
469
755
|
| `onClick(event)` | pressed | `false` to stop the event propagating |
|
|
470
|
-
| `onRelease(event)` |
|
|
756
|
+
| `onRelease(event)` | the press ends (up, left while held, or cancelled) | `false` to stop propagating |
|
|
471
757
|
| `onOver(event)` | pointer enters | `false` to stop propagating |
|
|
472
|
-
| `onOut(event)` | pointer leaves | — |
|
|
758
|
+
| `onOut(event)` | pointer leaves, or the element is removed while hovered (no event then) | — |
|
|
473
759
|
| `onHold()` | pressed and held | — |
|
|
474
760
|
|
|
475
761
|
```js
|
|
@@ -482,16 +768,174 @@ class MuteButton extends UISpriteElement {
|
|
|
482
768
|
onOut() { this.setOpacity(0.5); }
|
|
483
769
|
onClick() {
|
|
484
770
|
audio.muteAll();
|
|
485
|
-
|
|
771
|
+
this.setRegion(atlas.getRegion("speaker_off.png")); // or this.sprite.setRegion
|
|
772
|
+
return false; // consumed (the default)
|
|
486
773
|
}
|
|
487
774
|
}
|
|
488
775
|
```
|
|
489
776
|
|
|
777
|
+
Undo in `onOut` whatever `onOver` set up (a tooltip, a highlight, a cursor):
|
|
778
|
+
it also runs when the element leaves the world while the pointer is on it,
|
|
779
|
+
removed on its own, with the panel holding it, or with the scene, so nothing
|
|
780
|
+
is left behind. Keep effects that should only follow the pointer (a sound, a
|
|
781
|
+
tween) out of it, or check `event`, which is `undefined` for a removal.
|
|
782
|
+
|
|
783
|
+
The image is `button.sprite`: animations, tint and regions live there
|
|
784
|
+
(`setRegion`, `addAnimation`, `setCurrentAnimation` and `isCurrentAnimation`
|
|
785
|
+
are also on the element as shortcuts). As a `UIBaseElement` it has
|
|
786
|
+
`enabled` (drawn at half opacity when off), `cancelPress()` / `onCancel()`,
|
|
787
|
+
the bounce, and focus once `focusable` is set. `pos` is the element's top
|
|
788
|
+
left corner, and a second sprite of your own, such as a joystick's knob, is
|
|
789
|
+
a child added with `addChild`, not drawn by hand in `draw()`.
|
|
790
|
+
|
|
791
|
+
**A press stops at a UI element by default.** `onClick` and `onRelease`
|
|
792
|
+
return `false` unless you override them, on every `UIBaseElement` (buttons,
|
|
793
|
+
checkboxes, sliders, sprite buttons, your own widgets): a click on
|
|
794
|
+
a HUD button does not also click the world behind it. Return `true` from
|
|
795
|
+
them to let the press through as well. Hover is different: `onOver` lets it
|
|
796
|
+
through unless it returns `false`. A non-blocking `UIPanel` lets a press on
|
|
797
|
+
its background through, as a frame that only decorates should.
|
|
798
|
+
|
|
490
799
|
The pointer still has to reach it: a renderable with `isKinematic = true` — the
|
|
491
800
|
default on a plain `Renderable` — is skipped by the broadphase and receives
|
|
492
|
-
nothing.
|
|
801
|
+
nothing. Every `UIBaseElement` clears it for you; anything else
|
|
493
802
|
you make clickable has to clear it itself.
|
|
494
803
|
|
|
804
|
+
### A button that does one thing: `UITextButton`
|
|
805
|
+
|
|
806
|
+
No subclass needed for a single action:
|
|
807
|
+
|
|
808
|
+
```js
|
|
809
|
+
const play = new UITextButton(100, 200, {
|
|
810
|
+
text: "Play", // a CSS font by default ("sans-serif")
|
|
811
|
+
fontSize: 24, // pixels, with a CSS font or a bitmap font
|
|
812
|
+
onClick: () => state.change(state.PLAY),
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**`onClick` is called on a COMPLETED click**: pressed and released over the
|
|
817
|
+
button, or its `bindKey` pressed and released. A press dragged off the button,
|
|
818
|
+
cancelled, or turned into a scroll does not call it, and neither does a
|
|
819
|
+
disabled button. The `onClick()` method is different: it runs on the press,
|
|
820
|
+
as before, for subclasses.
|
|
821
|
+
|
|
822
|
+
`font` is a loaded bitmap font's name, drawn with `BitmapText` as before, or
|
|
823
|
+
any other name, drawn as a CSS font with `Text`. The label is
|
|
824
|
+
`button.label` (`bitmapText`, its old name, is deprecated since 20.13.0).
|
|
825
|
+
|
|
826
|
+
With no skin it is a rounded rectangle in `hoverOffColor`, `hoverOnColor`
|
|
827
|
+
under the pointer and `pressedColor` while held (`hoverOnColor` when left
|
|
828
|
+
out), outlined in `borderColor`.
|
|
829
|
+
|
|
830
|
+
The button takes the same names as the other widgets: `width` and `height`
|
|
831
|
+
for its size, `fontSize` (pixels) and `labelFillStyle` for its text,
|
|
832
|
+
`borderColor` for its outline. The older `borderWidth` / `borderHeight`
|
|
833
|
+
(its SIZE, not an outline thickness), `size` (a scale with a bitmap font)
|
|
834
|
+
and `fillStyle` still work. One style object can then go to every widget:
|
|
835
|
+
|
|
836
|
+
```js
|
|
837
|
+
const FONT = { font: "kenpixel", fontSize: 14, labelFillStyle: "black" };
|
|
838
|
+
new UITextButton(0, 0, { ...FONT, text: "OK", width: 160 });
|
|
839
|
+
new UICheckbox(0, 0, { ...FONT, label: "Music" });
|
|
840
|
+
new UISlider(0, 0, { ...FONT, label: "Volume" });
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**Skinned** from an atlas, drawn as a nine-slice so one image makes a button
|
|
844
|
+
of any width:
|
|
845
|
+
|
|
846
|
+
```js
|
|
847
|
+
new UITextButton(0, 0, {
|
|
848
|
+
font: "kenpixel", fontSize: 12, text: "Accept", width: 160,
|
|
849
|
+
skin: { atlas, normal: "green_button04", pressed: "green_button05" },
|
|
850
|
+
});
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
Pressed art is usually shorter than the normal art. The button draws it
|
|
854
|
+
lowered by the difference and moves the label down with it, so it looks
|
|
855
|
+
pushed in. `hover` and `disabled` regions are optional. `enabled`, shared by
|
|
856
|
+
every UI element, turns a widget off: `enabled = false`
|
|
857
|
+
draws the `disabled` region, or the button at half opacity, and ignores the
|
|
858
|
+
pointer and the key.
|
|
859
|
+
|
|
860
|
+
### A little bounce: `hoverScale` and `pressScale`
|
|
861
|
+
|
|
862
|
+
Every `UIBaseElement`, buttons, checkboxes, sliders, panels and your own,
|
|
863
|
+
can grow a little under the pointer or with the focus, and shrink while held
|
|
864
|
+
(`ProgressBar` is not a `UIBaseElement`, and does not):
|
|
865
|
+
|
|
866
|
+
```js
|
|
867
|
+
new UITextButton(0, 0, { text: "Play", hoverScale: 1.05, pressScale: 0.92 });
|
|
868
|
+
levelButton.hoverScale = 1.12; // or set it later
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Both default to 1, which is still. The scale eases there and back by itself.
|
|
872
|
+
Only the drawing scales, about the centre: the hit area and the layout stay
|
|
873
|
+
put, so nothing jitters and the widgets around it do not move.
|
|
874
|
+
|
|
875
|
+
### Keyboard and gamepad navigation: `focus()`
|
|
876
|
+
|
|
877
|
+
For a game played without a mouse or a touch screen, give a widget the focus:
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
playButton.focus(); // where keyboard or gamepad play starts
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
From then on the arrow keys or a pad's D-pad move the focus to the nearest
|
|
884
|
+
focusable widget that way. Enter, Space or the pad's A press the focused
|
|
885
|
+
widget as a click does: a button calls its `onClick` setting, a checkbox
|
|
886
|
+
toggles. Left and right step a slider (up and down on a vertical one). A ring
|
|
887
|
+
(`UIBaseElement.focusRingColor`) shows the focus, and hides while the mouse
|
|
888
|
+
is used. A focused widget in a scrolling panel is scrolled into view.
|
|
889
|
+
|
|
890
|
+
There is nothing to turn on. Until a widget has the focus the arrow keys and
|
|
891
|
+
the pad are left to the game, and `widget.blur()`, or the focused widget
|
|
892
|
+
leaving the scene, gives them back. `UIBaseElement.focusOwner` is the widget
|
|
893
|
+
with the focus. The pad is read directly, so the game's own pad bindings
|
|
894
|
+
are untouched.
|
|
895
|
+
|
|
896
|
+
Buttons, checkboxes and sliders are `focusable`. A widget of your own sets
|
|
897
|
+
`focusable = true`, and overrides `onNavigate(direction)` (return `true` for
|
|
898
|
+
what it handles) and `focusRelease()` for what Enter does:
|
|
899
|
+
|
|
900
|
+
```js
|
|
901
|
+
// a left / right picker: "< Normal >"; Enter confirms the choice
|
|
902
|
+
class Picker extends UIBaseElement {
|
|
903
|
+
constructor(x, y, options, onPick) {
|
|
904
|
+
super(x, y, 200, 32);
|
|
905
|
+
this.focusable = true;
|
|
906
|
+
this.options = options;
|
|
907
|
+
this.onPick = onPick;
|
|
908
|
+
this.index = 0;
|
|
909
|
+
// draw() left out: render this.options[this.index]
|
|
910
|
+
}
|
|
911
|
+
onNavigate(direction) {
|
|
912
|
+
if (direction === "left" || direction === "right") {
|
|
913
|
+
const step = direction === "left" ? -1 : 1;
|
|
914
|
+
this.index = (this.index + step + this.options.length) % this.options.length;
|
|
915
|
+
this.isDirty = true;
|
|
916
|
+
return true; // handled: the focus stays here
|
|
917
|
+
}
|
|
918
|
+
return false; // up and down move the focus on
|
|
919
|
+
}
|
|
920
|
+
focusRelease() {
|
|
921
|
+
super.focusRelease(); // the widget comes back up, as on a click
|
|
922
|
+
this.onPick(this.options[this.index]);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Moving the focus from code is `other.focus()`; `UIBaseElement.focusOwner?.blur()`
|
|
928
|
+
hands the keys back to the game, for example when gameplay resumes.
|
|
929
|
+
|
|
930
|
+
Three things to know. Enter and Space go to the focused widget, so another
|
|
931
|
+
button whose `bindKey` is Enter or Space waits while any other widget has the
|
|
932
|
+
focus, ring shown or hidden: one key press never clicks both. `blur()` the
|
|
933
|
+
focused widget to give Enter back to the shortcut (a disabled or hidden
|
|
934
|
+
focused widget does not keep it). Enter or A held while the window loses
|
|
935
|
+
focus is called off (`onCancel()`, no click), as a held `bindKey` is. And
|
|
936
|
+
accept calls a subclass's `onClick()` METHOD with no pointer event, as
|
|
937
|
+
`bindKey` does: guard `event` there, or act in the `onClick` setting.
|
|
938
|
+
|
|
495
939
|
### Keyboard shortcuts on a button: `bindKey`
|
|
496
940
|
|
|
497
941
|
A `UITextButton` can be activated from the keyboard as well as the pointer:
|
|
@@ -524,15 +968,131 @@ Three more rules, each with a reason you can feel in a game:
|
|
|
524
968
|
- **The first input to press owns the button.** A pointer cannot release a key
|
|
525
969
|
press and a key cannot release a pointer press, so a button cannot be left
|
|
526
970
|
half pressed by using both at once.
|
|
527
|
-
- **
|
|
528
|
-
|
|
529
|
-
`
|
|
530
|
-
|
|
531
|
-
|
|
971
|
+
- **A held key that is called off goes to `onCancel()`, not `onRelease()`.**
|
|
972
|
+
That happens when the window loses focus, the button is removed, or it is
|
|
973
|
+
disabled (`enabled = false`) while the key is down. If you use the pair as
|
|
974
|
+
press and depress, firing while held, stop in `onCancel()` too, or the gun
|
|
975
|
+
is still firing when the player alt-tabs back.
|
|
976
|
+
- **`isClickable` alone gates the press, not the release.** Clearing it while
|
|
977
|
+
the key is down still lets the key release the button, so it cannot stick.
|
|
532
978
|
|
|
533
979
|
Shortcuts live only while the button is activated, and never start dragging or
|
|
534
980
|
tap-and-hold: those stay pointer gestures.
|
|
535
981
|
|
|
982
|
+
### Checkboxes and radio buttons: `UICheckbox`
|
|
983
|
+
|
|
984
|
+
```js
|
|
985
|
+
const music = new UICheckbox(40, 60, {
|
|
986
|
+
labelOn: "Music ON", labelOff: "Music OFF", // or one `label`
|
|
987
|
+
checked: settings.music,
|
|
988
|
+
onChange: (on) => { settings.music = on; on ? audio.playTrack("theme") : audio.stopTrack(); },
|
|
989
|
+
});
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
The label is part of the widget: clicking it toggles the box, and removing
|
|
993
|
+
the checkbox takes the label with it. Drawn with primitives by default, or
|
|
994
|
+
`skin: { atlas, on, off, hoverOn?, hoverOff?, disabledOn?, disabledOff? }`.
|
|
995
|
+
`boxSize`, `boxColor`, `boxHoverColor` and `checkColor` style the drawn box,
|
|
996
|
+
and `labelOn` / `labelOff` can be changed later: the widget refits.
|
|
997
|
+
|
|
998
|
+
**Setting `checked` from code does NOT call `onChange`**, nor does
|
|
999
|
+
`toggle()`. Only the user does. Restoring saved settings into the widgets
|
|
1000
|
+
therefore does not re-run the game logic attached to them, so apply what
|
|
1001
|
+
they stand for yourself after restoring:
|
|
1002
|
+
|
|
1003
|
+
```js
|
|
1004
|
+
music.checked = saved.music; // the look only
|
|
1005
|
+
applyMusic(saved.music); // the effect, once
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
**Radio buttons** are checkboxes sharing a `group`: exactly one is checked,
|
|
1009
|
+
clicking the checked one leaves it checked (so does `toggle()`; set
|
|
1010
|
+
`checked = false` to clear a group on purpose), and they are drawn round. Only
|
|
1011
|
+
the one the user picked gets `onChange`, as in a web page; read the group with
|
|
1012
|
+
`UICheckbox.checkedIn("difficulty")`. A checkbox is in its group from when it is
|
|
1013
|
+
made until it is destroyed, so the group holds one choice, and `checkedIn`
|
|
1014
|
+
answers, before the panel is ever added to the world.
|
|
1015
|
+
|
|
1016
|
+
```js
|
|
1017
|
+
for (const level of ["Easy", "Normal", "Hard"]) {
|
|
1018
|
+
panel.addChild(new UICheckbox(0, 0, {
|
|
1019
|
+
label: level,
|
|
1020
|
+
group: "difficulty",
|
|
1021
|
+
checked: level === saved.difficulty,
|
|
1022
|
+
onChange: (on) => { if (on) { saved.difficulty = level; } },
|
|
1023
|
+
}));
|
|
1024
|
+
}
|
|
1025
|
+
// later: the checked UICheckbox of the group, or undefined
|
|
1026
|
+
const picked = UICheckbox.checkedIn("difficulty");
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
**Skinned** from an atlas (the region names are your art's; these are the
|
|
1030
|
+
UI example's):
|
|
1031
|
+
|
|
1032
|
+
```js
|
|
1033
|
+
new UICheckbox(0, 0, {
|
|
1034
|
+
label: "Sound FX",
|
|
1035
|
+
skin: {
|
|
1036
|
+
atlas,
|
|
1037
|
+
on: "green_boxCheckmark",
|
|
1038
|
+
off: "grey_boxCheckmark",
|
|
1039
|
+
// optional: hoverOn, hoverOff, disabledOn, disabledOff
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Sliders: `UISlider`
|
|
1045
|
+
|
|
1046
|
+
```js
|
|
1047
|
+
const volume = new UISlider(40, 120, {
|
|
1048
|
+
label: "Music", showValue: true,
|
|
1049
|
+
valueFormat: (v, ratio) => Math.round(ratio * 100) + "%",
|
|
1050
|
+
min: 0, max: 1, step: 0.01, value: audio.getVolume(),
|
|
1051
|
+
length: 300, // orientation: "vertical" too
|
|
1052
|
+
onChange: (v) => audio.setVolume(v), // every step of the drag
|
|
1053
|
+
onChangeEnd: () => saveSettings(), // once, on release
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
Drag the thumb, or click the track to jump there. The drag follows the
|
|
1058
|
+
pointer after it leaves the track, until it is released. As on any widget, the
|
|
1059
|
+
press (`onClick`) ends in exactly one of `onRelease` (let go, before
|
|
1060
|
+
`onChangeEnd`) or `onCancel` (called off by `cancelPress()`, `enabled = false`
|
|
1061
|
+
or removal, with no `onChangeEnd`). The mouse wheel over
|
|
1062
|
+
the slider moves it one `step`. `value` snaps to `step` and clamps to the
|
|
1063
|
+
range, and setting it does not call `onChange` or `onChangeEnd`: the same rule
|
|
1064
|
+
as the checkbox, and neither does changing `min`, `max` or `step`, which
|
|
1065
|
+
re-clamp it. `ratio` reads where it is from 0 to 1, and `valueFormat` gets
|
|
1066
|
+
both. `labelText` changes the caption later. The slider refits to its texts
|
|
1067
|
+
(the widest value it can show, at either end of the range), and on a
|
|
1068
|
+
horizontal one the value moves below the track when it would run into the
|
|
1069
|
+
caption; a `UIPanel` holding it needs `relayout()` if its size changes. `thumbColor`, `thumbHoverColor`, `thumbBorderColor` and
|
|
1070
|
+
`thumbBorderWidth` style the drawn thumb. `skin: { atlas, track, fill?, thumb, thumbHover? }` draws it
|
|
1071
|
+
from art: the track and fill as nine-slices along its length, the thumb at its
|
|
1072
|
+
own size.
|
|
1073
|
+
|
|
1074
|
+
Put work that should not run 60 times a second, such as saving or playing a
|
|
1075
|
+
test sound, in `onChangeEnd`, which runs once on release and after each wheel
|
|
1076
|
+
or key step.
|
|
1077
|
+
|
|
1078
|
+
```js
|
|
1079
|
+
// whole numbers: a step of 1 shows "7", not "7.00"
|
|
1080
|
+
new UISlider(0, 0, { label: "Difficulty", showValue: true, min: 1, max: 10, step: 1, value: 5 });
|
|
1081
|
+
|
|
1082
|
+
// vertical, 160 pixels tall: up raises it
|
|
1083
|
+
new UISlider(0, 0, { orientation: "vertical", length: 160, value: 0.5 });
|
|
1084
|
+
|
|
1085
|
+
// skinned: the track and fill stretch, the thumb keeps its size
|
|
1086
|
+
new UISlider(0, 0, {
|
|
1087
|
+
length: 240,
|
|
1088
|
+
// optional: fill (the part before the thumb), thumbHover
|
|
1089
|
+
skin: { atlas, track: "grey_sliderHorizontal", thumb: "blue_sliderDown" },
|
|
1090
|
+
});
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
With no `valueFormat`, the value text shows as many decimals as `step`
|
|
1094
|
+
has, and follows a later change of `step`.
|
|
1095
|
+
|
|
536
1096
|
### A fullscreen option
|
|
537
1097
|
|
|
538
1098
|
Three methods on the `Application`, not on `device` (they moved there in
|
|
@@ -592,6 +1152,47 @@ Fullscreen does not resize a fixed-resolution game: with
|
|
|
592
1152
|
unchanged and only the canvas scales, so a HUD built from
|
|
593
1153
|
`renderer.width`/`height` needs no repositioning.
|
|
594
1154
|
|
|
1155
|
+
### Dragging: `isDraggable`, or `Draggable` and `DropTarget`
|
|
1156
|
+
|
|
1157
|
+
Any UI element follows the pointer while held once `isDraggable` is set:
|
|
1158
|
+
`onClick` is the pick-up, `onMove(event)` runs after each move with `pos`
|
|
1159
|
+
already following, and `onRelease` is the drop. It works by touch as by
|
|
1160
|
+
mouse, on a floating element as on one in the world. (`UIPanel` has its own
|
|
1161
|
+
`draggable` setting, above.)
|
|
1162
|
+
|
|
1163
|
+
```js
|
|
1164
|
+
class Token extends UISpriteElement {
|
|
1165
|
+
constructor(x, y) {
|
|
1166
|
+
super(x, y, { image: atlas, region: "token" });
|
|
1167
|
+
this.isDraggable = true;
|
|
1168
|
+
}
|
|
1169
|
+
onRelease() {
|
|
1170
|
+
this.pos.x = Math.round(this.pos.x / 32) * 32; // snap to the grid
|
|
1171
|
+
this.pos.y = Math.round(this.pos.y / 32) * 32;
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
When something else has to notice the drop, `Draggable` and `DropTarget` are
|
|
1178
|
+
the pair. Both are plain `Renderable`s, in the world unless you make them
|
|
1179
|
+
`floating`, that draw nothing until you give them a `draw()`. A `Draggable`
|
|
1180
|
+
follows the pointer while held (`dragStart(e)` / `dragEnd(e)` to override,
|
|
1181
|
+
calling `super`), and when it is let go every `DropTarget` its bounds overlap
|
|
1182
|
+
gets `drop(draggable)`; after `setCheckMethod(target.CHECKMETHOD_CONTAINS)`
|
|
1183
|
+
only one it lies wholly inside does.
|
|
1184
|
+
|
|
1185
|
+
```js
|
|
1186
|
+
class Slot extends DropTarget {
|
|
1187
|
+
drop(card) {
|
|
1188
|
+
card.pos.set(this.pos.x, this.pos.y, card.pos.z); // settle it in
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
The drop is checked on the draggable's own pointer-up (`event.DRAGEND` comes
|
|
1194
|
+
from it), so the release has to land on the draggable itself.
|
|
1195
|
+
|
|
595
1196
|
### `z` is container-local
|
|
596
1197
|
|
|
597
1198
|
`addChild(child, z)` writes a depth that means something **only among that
|
|
@@ -619,8 +1220,9 @@ keeps the four corners at their own size, and stretches only the edges and the
|
|
|
619
1220
|
middle, so the border stays crisp at any size. Do not hand-roll it from nine
|
|
620
1221
|
sprites.
|
|
621
1222
|
|
|
622
|
-
|
|
623
|
-
|
|
1223
|
+
`UIPanel`'s `skin` and `UITextButton`'s `skin` already do this for you. By
|
|
1224
|
+
hand, from a texture atlas, the third argument of `createSpriteFromName` asks
|
|
1225
|
+
for the 9-slice version:
|
|
624
1226
|
|
|
625
1227
|
```js
|
|
626
1228
|
class Panel extends UIBaseElement {
|
|
@@ -665,14 +1267,16 @@ sprite.
|
|
|
665
1267
|
### An opaque panel has to say so
|
|
666
1268
|
|
|
667
1269
|
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
|
-
|
|
1270
|
+
consuming is what stops a widget underneath answering too. A press is consumed
|
|
1271
|
+
by default on a UI element, but hover is not, so a panel of your own that can
|
|
1272
|
+
overlap another one also stops hover (`UIPanel` does all of this with
|
|
1273
|
+
`blocking`):
|
|
670
1274
|
|
|
671
1275
|
```js
|
|
672
1276
|
class Panel extends UIBaseElement {
|
|
673
1277
|
onClick() {
|
|
674
1278
|
this.ancestor.moveToTop(this); // and bring it to the front
|
|
675
|
-
return false; // consumed
|
|
1279
|
+
return false; // consumed (the default)
|
|
676
1280
|
}
|
|
677
1281
|
onOver() {
|
|
678
1282
|
return false; // the frame the pointer crosses in
|
|
@@ -721,6 +1325,34 @@ of the level instead, with the scenery drawing over it. Both shipped idioms are
|
|
|
721
1325
|
the same rule: afterBurner's HUD sits at `-150`, and the glTF, Billboard, Night
|
|
722
1326
|
City and Instanced Forest examples park a floating sky at `-10000` or `100000`.
|
|
723
1327
|
|
|
1328
|
+
## When the UI misbehaves
|
|
1329
|
+
|
|
1330
|
+
| symptom | cause | fix |
|
|
1331
|
+
|---|---|---|
|
|
1332
|
+
| 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 |
|
|
1333
|
+
| `onChange` never fires when settings are restored | by design: only user input calls it | apply the restored value yourself |
|
|
1334
|
+
| 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 |
|
|
1335
|
+
| 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 |
|
|
1336
|
+
| 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` |
|
|
1337
|
+
| 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 |
|
|
1338
|
+
| a layout ignores a label that changed size | layout runs on add, remove and resize only | call `panel.relayout()` |
|
|
1339
|
+
| 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 |
|
|
1340
|
+
| 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 |
|
|
1341
|
+
| 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 |
|
|
1342
|
+
| a tooltip or highlight shown in `onOver` stays after its widget is removed (before 20.14) | the pointer's leave never reaches a removed widget | update: a widget removed while hovered now gets `onOut()`, with no event |
|
|
1343
|
+
| an `onOut` sound or tween plays when a menu closes or the scene changes (since 20.14) | a widget removed while hovered gets `onOut()` | keep pointer-only effects out of `onOut`, or skip them when its `event` is `undefined` |
|
|
1344
|
+
| 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()` |
|
|
1345
|
+
| 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 |
|
|
1346
|
+
| 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 |
|
|
1347
|
+
| 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 |
|
|
1348
|
+
| 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 |
|
|
1349
|
+
| `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 |
|
|
1350
|
+
| 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 |
|
|
1351
|
+
| `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 |
|
|
1352
|
+
| `UI skin: no region named "…" in the atlas` thrown where a widget is made | every region a `skin` names is looked up when the widget is built | fix the name, or re-export the atlas with that image |
|
|
1353
|
+
| a typewriter reveal on a centred `Text` slides sideways as it types | a `Text` aligns only the part shown so far | left-align it, or reveal a `BitmapText`, which keeps each glyph in its final place |
|
|
1354
|
+
| a `glyphEffect` decay or intro plays the first time and never again | `ctx.time` runs while any effect is set and never resets, so a later effect starts late | note `ctx.time` on the effect's first call and measure from that |
|
|
1355
|
+
|
|
724
1356
|
## Related skills
|
|
725
1357
|
|
|
726
1358
|
- `melonjs-scenes-and-state` — `Tween` and the easing families, for animating any of this
|