@umicat/three-sdk 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GameAudio.d.ts +20 -4
- package/dist/GameAudio.js +58 -21
- package/dist/Input3D.d.ts +39 -1
- package/dist/Input3D.js +101 -15
- package/package.json +1 -1
package/dist/GameAudio.d.ts
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* **4. iOS suspends the context when the app goes away** and does not bring it
|
|
26
26
|
* back, so sound works until the first phone call and then never again.
|
|
27
27
|
*
|
|
28
|
+
* Music is the one exception to rule 1, and for the same reason rule 1 exists.
|
|
29
|
+
* A three-minute track decodes to something like seventy megabytes of PCM;
|
|
30
|
+
* forty short effects as elements was fatal, but ONE element playing one long
|
|
31
|
+
* thing is exactly what elements are for. So effects are buffers and music
|
|
32
|
+
* streams, routed through the same graph so ducking still works.
|
|
33
|
+
*
|
|
28
34
|
* The game brings its own clips; nothing here knows what a game sounds like.
|
|
29
35
|
*/
|
|
30
36
|
export interface AudioClipSpec {
|
|
@@ -42,8 +48,9 @@ export interface GameAudioOptions {
|
|
|
42
48
|
base?: string;
|
|
43
49
|
/** File extension, including the dot. */
|
|
44
50
|
extension?: string;
|
|
45
|
-
/** A
|
|
46
|
-
*
|
|
51
|
+
/** A track to loop as music. Streamed, not decoded — see the note above.
|
|
52
|
+
* A name containing a dot is used as-is, so `'theme.mp3'` works alongside
|
|
53
|
+
* `.ogg` effects. */
|
|
47
54
|
music?: string;
|
|
48
55
|
musicVolume?: number;
|
|
49
56
|
}
|
|
@@ -51,13 +58,13 @@ export declare class GameAudio {
|
|
|
51
58
|
private ctx;
|
|
52
59
|
private master;
|
|
53
60
|
private musicGain;
|
|
54
|
-
private
|
|
61
|
+
private musicEl;
|
|
62
|
+
private musicName;
|
|
55
63
|
private readonly buffers;
|
|
56
64
|
private readonly lastPlayed;
|
|
57
65
|
private readonly clips;
|
|
58
66
|
private readonly base;
|
|
59
67
|
private readonly ext;
|
|
60
|
-
private readonly music?;
|
|
61
68
|
private readonly musicVolume;
|
|
62
69
|
private muted;
|
|
63
70
|
private readonly cleanups;
|
|
@@ -66,8 +73,17 @@ export declare class GameAudio {
|
|
|
66
73
|
* a cached flag is exactly the bug described above. */
|
|
67
74
|
get ready(): boolean;
|
|
68
75
|
private start;
|
|
76
|
+
/** `'theme.mp3'` stays as it is; `'coin'` becomes `coin.ogg`. */
|
|
77
|
+
private urlFor;
|
|
69
78
|
private loadAll;
|
|
70
79
|
private startMusic;
|
|
80
|
+
/**
|
|
81
|
+
* Change the track. Pass `null` for silence.
|
|
82
|
+
*
|
|
83
|
+
* Each scene gets its own: a lobby that sounds like the fight is a lobby you
|
|
84
|
+
* do not linger in.
|
|
85
|
+
*/
|
|
86
|
+
setMusic(name: string | null): void;
|
|
71
87
|
play(name: string): void;
|
|
72
88
|
/** Duck the music for a moment — for an ending that should be heard over it. */
|
|
73
89
|
duck(seconds?: number): void;
|
package/dist/GameAudio.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* **4. iOS suspends the context when the app goes away** and does not bring it
|
|
26
26
|
* back, so sound works until the first phone call and then never again.
|
|
27
27
|
*
|
|
28
|
+
* Music is the one exception to rule 1, and for the same reason rule 1 exists.
|
|
29
|
+
* A three-minute track decodes to something like seventy megabytes of PCM;
|
|
30
|
+
* forty short effects as elements was fatal, but ONE element playing one long
|
|
31
|
+
* thing is exactly what elements are for. So effects are buffers and music
|
|
32
|
+
* streams, routed through the same graph so ducking still works.
|
|
33
|
+
*
|
|
28
34
|
* The game brings its own clips; nothing here knows what a game sounds like.
|
|
29
35
|
*/
|
|
30
36
|
export class GameAudio {
|
|
@@ -32,7 +38,8 @@ export class GameAudio {
|
|
|
32
38
|
this.ctx = null;
|
|
33
39
|
this.master = null;
|
|
34
40
|
this.musicGain = null;
|
|
35
|
-
this.
|
|
41
|
+
this.musicEl = null;
|
|
42
|
+
this.musicName = null;
|
|
36
43
|
this.buffers = new Map();
|
|
37
44
|
this.lastPlayed = new Map();
|
|
38
45
|
this.muted = false;
|
|
@@ -40,7 +47,7 @@ export class GameAudio {
|
|
|
40
47
|
this.clips = opts.clips;
|
|
41
48
|
this.base = opts.base ?? 'audio/';
|
|
42
49
|
this.ext = opts.extension ?? '.ogg';
|
|
43
|
-
this.
|
|
50
|
+
this.musicName = opts.music ?? null;
|
|
44
51
|
this.musicVolume = opts.musicVolume ?? 0.28;
|
|
45
52
|
const unlock = () => {
|
|
46
53
|
void this.start().then(() => {
|
|
@@ -92,42 +99,68 @@ export class GameAudio {
|
|
|
92
99
|
if (this.ready)
|
|
93
100
|
this.startMusic();
|
|
94
101
|
}
|
|
102
|
+
/** `'theme.mp3'` stays as it is; `'coin'` becomes `coin.ogg`. */
|
|
103
|
+
urlFor(name) {
|
|
104
|
+
return `${this.base}${name}${name.includes('.') ? '' : this.ext}`;
|
|
105
|
+
}
|
|
95
106
|
async loadAll() {
|
|
96
107
|
const load = async (name) => {
|
|
97
108
|
try {
|
|
98
|
-
const res = await fetch(
|
|
109
|
+
const res = await fetch(this.urlFor(name));
|
|
99
110
|
const bytes = await res.arrayBuffer();
|
|
100
111
|
const buf = await this.ctx.decodeAudioData(bytes);
|
|
101
112
|
// Tagged so a test can see WHICH clip played — a buffer has no name,
|
|
102
113
|
// and "some audio happened" is not a check.
|
|
103
114
|
buf.__name = name;
|
|
104
115
|
this.buffers.set(name, buf);
|
|
105
|
-
if (name === this.music && this.ready && !this.musicSource)
|
|
106
|
-
this.startMusic();
|
|
107
116
|
}
|
|
108
117
|
catch {
|
|
109
118
|
/* a clip that will not decode is not worth taking the game down for */
|
|
110
119
|
}
|
|
111
120
|
};
|
|
112
|
-
//
|
|
113
|
-
// behind every effect and left the first actions silent — caution about
|
|
121
|
+
// All at once. One at a time left the first actions silent — caution about
|
|
114
122
|
// decode cost, for work that is not on the main thread.
|
|
115
|
-
if (this.music)
|
|
116
|
-
await load(this.music);
|
|
117
123
|
await Promise.all(Object.keys(this.clips).map(load));
|
|
118
124
|
}
|
|
119
125
|
startMusic() {
|
|
120
|
-
if (!this.ctx || !this.musicGain || this.
|
|
126
|
+
if (!this.ctx || !this.musicGain || this.muted || !this.musicName)
|
|
121
127
|
return;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
if (!this.musicEl) {
|
|
129
|
+
const el = new Audio(this.urlFor(this.musicName));
|
|
130
|
+
el.loop = true;
|
|
131
|
+
el.preload = 'auto';
|
|
132
|
+
el.crossOrigin = 'anonymous';
|
|
133
|
+
// Routed through the graph, not played on its own, so `duck()` and the
|
|
134
|
+
// master gain reach it like everything else.
|
|
135
|
+
try {
|
|
136
|
+
this.ctx.createMediaElementSource(el).connect(this.musicGain);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Some engines refuse to route an element they consider tainted. Fall
|
|
140
|
+
// back to playing it directly rather than losing the music entirely.
|
|
141
|
+
el.volume = this.musicVolume;
|
|
142
|
+
}
|
|
143
|
+
this.musicEl = el;
|
|
144
|
+
}
|
|
145
|
+
void this.musicEl.play().catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Change the track. Pass `null` for silence.
|
|
149
|
+
*
|
|
150
|
+
* Each scene gets its own: a lobby that sounds like the fight is a lobby you
|
|
151
|
+
* do not linger in.
|
|
152
|
+
*/
|
|
153
|
+
setMusic(name) {
|
|
154
|
+
if (name === this.musicName)
|
|
155
|
+
return;
|
|
156
|
+
this.musicName = name;
|
|
157
|
+
if (this.musicEl) {
|
|
158
|
+
this.musicEl.pause();
|
|
159
|
+
this.musicEl.src = '';
|
|
160
|
+
this.musicEl = null; // a new element: the old one's graph node is spent
|
|
161
|
+
}
|
|
162
|
+
if (name && this.ready && !this.muted)
|
|
163
|
+
this.startMusic();
|
|
131
164
|
}
|
|
132
165
|
play(name) {
|
|
133
166
|
const ctx = this.ctx;
|
|
@@ -168,7 +201,11 @@ export class GameAudio {
|
|
|
168
201
|
if (this.master && this.ctx) {
|
|
169
202
|
this.master.gain.setTargetAtTime(on ? 0 : 1, this.ctx.currentTime, 0.02);
|
|
170
203
|
}
|
|
171
|
-
|
|
204
|
+
// Pause the stream as well as silencing it: a muted track still costs a
|
|
205
|
+
// decoder and a download for something nobody can hear.
|
|
206
|
+
if (on)
|
|
207
|
+
this.musicEl?.pause();
|
|
208
|
+
else if (this.ready)
|
|
172
209
|
this.startMusic();
|
|
173
210
|
}
|
|
174
211
|
get isMuted() { return this.muted; }
|
|
@@ -176,7 +213,7 @@ export class GameAudio {
|
|
|
176
213
|
for (const c of this.cleanups)
|
|
177
214
|
c();
|
|
178
215
|
this.cleanups.length = 0;
|
|
179
|
-
this.
|
|
216
|
+
this.musicEl?.pause();
|
|
180
217
|
void this.ctx?.close();
|
|
181
218
|
this.ctx = null;
|
|
182
219
|
}
|
package/dist/Input3D.d.ts
CHANGED
|
@@ -2,8 +2,24 @@
|
|
|
2
2
|
export interface Input3DAction {
|
|
3
3
|
/** What the game calls it: `input.held('attack')`, `input.consume('attack')`. */
|
|
4
4
|
id: string;
|
|
5
|
-
/** What the button shows. Keep it one glyph.
|
|
5
|
+
/** What the button shows, as text. Keep it one glyph. Ignored when `icon`
|
|
6
|
+
* is given. */
|
|
6
7
|
label?: string;
|
|
8
|
+
/**
|
|
9
|
+
* What the button shows, as a SHAPE: the URL of a single-colour SVG or PNG.
|
|
10
|
+
*
|
|
11
|
+
* Preferred over `label`, and the reason is not decoration. A label is text,
|
|
12
|
+
* and text on this layer has three problems that only show up on a device:
|
|
13
|
+
* an emoji is a different picture in every platform's font, text can be
|
|
14
|
+
* SELECTED (long-pressing a game's attack button is how the iOS Copy / Look
|
|
15
|
+
* Up / Translate callout comes up mid-fight), and it cannot take the
|
|
16
|
+
* button's colour, so a glyph can never match the controls drawn beside it.
|
|
17
|
+
*
|
|
18
|
+
* Drawn as a CSS mask, so the file's own colour is discarded and the shape
|
|
19
|
+
* is painted in the button's — which means one icon keeps matching if the
|
|
20
|
+
* control layer is ever restyled.
|
|
21
|
+
*/
|
|
22
|
+
icon?: string;
|
|
7
23
|
/** Keyboard equivalents, e.g. `['KeyJ']`. */
|
|
8
24
|
keys?: string[];
|
|
9
25
|
}
|
|
@@ -33,6 +49,9 @@ export interface Input3DOptions {
|
|
|
33
49
|
* platform places the controls, so the platform has to place ALL of them.
|
|
34
50
|
*/
|
|
35
51
|
actions?: Input3DAction[];
|
|
52
|
+
/** A shape for the JUMP button, in place of its `▲`. Same reasoning as
|
|
53
|
+
* `Input3DAction.icon`, for the one button the platform owns outright. */
|
|
54
|
+
jumpIcon?: string;
|
|
36
55
|
/**
|
|
37
56
|
* How the thumbstick behaves on touch.
|
|
38
57
|
*
|
|
@@ -91,6 +110,13 @@ export declare class Input3D {
|
|
|
91
110
|
private readonly stick;
|
|
92
111
|
private touchJump;
|
|
93
112
|
private actions;
|
|
113
|
+
private jumpIcon?;
|
|
114
|
+
/** The glyph inside each on-screen button, by action id (`'jump'` for the
|
|
115
|
+
* platform's own). Kept so an icon can be CHANGED while the game runs —
|
|
116
|
+
* which is the point of it for anything the player switches: an attack
|
|
117
|
+
* button that still shows a sword while you are holding a bow is a button
|
|
118
|
+
* that is lying about what it does. */
|
|
119
|
+
private glyphs;
|
|
94
120
|
private stickMode;
|
|
95
121
|
private wantLook;
|
|
96
122
|
private topInset;
|
|
@@ -137,6 +163,18 @@ export declare class Input3D {
|
|
|
137
163
|
/** Whether the jump control is held right now. Pass it to the controller —
|
|
138
164
|
* coyote time and buffering live there, not here. */
|
|
139
165
|
get jump(): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Change what a button SHOWS, without rebuilding the controls.
|
|
168
|
+
*
|
|
169
|
+
* `id` is an action's id, or `'jump'` for the platform's own button. Passing
|
|
170
|
+
* a url the button had no icon for at build time does nothing: the glyph is
|
|
171
|
+
* an element that only exists when one was given, and quietly growing one
|
|
172
|
+
* would move the label out from under a game that was relying on it.
|
|
173
|
+
*
|
|
174
|
+
* This is for buttons whose MEANING changes — an attack button under a bow
|
|
175
|
+
* and under a staff are two different actions wearing one control.
|
|
176
|
+
*/
|
|
177
|
+
setActionIcon(id: string, icon: string): void;
|
|
140
178
|
/** Whether an action is held right now — button or bound key, same answer. */
|
|
141
179
|
held(id: string): boolean;
|
|
142
180
|
/**
|
package/dist/Input3D.js
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control layer's palette.
|
|
3
|
+
*
|
|
4
|
+
* A DARK translucent fill with a light rim, not a light fill. White-on-white
|
|
5
|
+
* is what the first version was, and over a bright board — snow, or this
|
|
6
|
+
* engine's default grass — the buttons washed out into the scene exactly when
|
|
7
|
+
* a thumb was looking for them. A dark disc reads against anything: bright
|
|
8
|
+
* scenes darken it into view, dark scenes leave the rim and the glyph doing
|
|
9
|
+
* the work.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately neutral rather than themed. This layer belongs to the platform
|
|
12
|
+
* and sits over every game, so it cannot take a side about what colour a game
|
|
13
|
+
* is.
|
|
14
|
+
*/
|
|
15
|
+
const CONTROL = {
|
|
16
|
+
/** The disc under a thumb. */
|
|
17
|
+
fill: 'rgba(16,20,28,0.38)',
|
|
18
|
+
/** The rim, which is what actually makes it findable. */
|
|
19
|
+
rim: 'rgba(255,255,255,0.62)',
|
|
20
|
+
/** Glyphs and the stick's knob. */
|
|
21
|
+
ink: 'rgba(255,255,255,0.95)',
|
|
22
|
+
/** The stick's ring, a shade quieter than a button: it is a place to put a
|
|
23
|
+
* thumb rather than a thing to press. */
|
|
24
|
+
padFill: 'rgba(16,20,28,0.30)',
|
|
25
|
+
padRim: 'rgba(255,255,255,0.45)',
|
|
26
|
+
};
|
|
1
27
|
/** How long a press stays readable, however briefly it was actually made.
|
|
2
28
|
*
|
|
3
29
|
* A tap whose down and up both land between two frames is invisible to a
|
|
@@ -67,6 +93,12 @@ export class Input3D {
|
|
|
67
93
|
this.stick = { x: 0, z: 0 };
|
|
68
94
|
this.touchJump = false;
|
|
69
95
|
this.actions = [];
|
|
96
|
+
/** The glyph inside each on-screen button, by action id (`'jump'` for the
|
|
97
|
+
* platform's own). Kept so an icon can be CHANGED while the game runs —
|
|
98
|
+
* which is the point of it for anything the player switches: an attack
|
|
99
|
+
* button that still shows a sword while you are holding a bow is a button
|
|
100
|
+
* that is lying about what it does. */
|
|
101
|
+
this.glyphs = new Map();
|
|
70
102
|
this.stickMode = 'floating';
|
|
71
103
|
this.wantLook = true;
|
|
72
104
|
this.topInset = 'max(64px, 12%)';
|
|
@@ -87,6 +119,7 @@ export class Input3D {
|
|
|
87
119
|
// Losing focus mid-press would otherwise leave the character walking.
|
|
88
120
|
this.target.addEventListener('blur', this.onBlur);
|
|
89
121
|
this.actions = opts.actions ?? [];
|
|
122
|
+
this.jumpIcon = opts.jumpIcon;
|
|
90
123
|
this.stickMode = opts.stick ?? 'floating';
|
|
91
124
|
this.wantLook = opts.look ?? true;
|
|
92
125
|
this.topInset = opts.controlsTopInset ?? 'max(64px, 12%)';
|
|
@@ -175,6 +208,25 @@ export class Input3D {
|
|
|
175
208
|
get jump() {
|
|
176
209
|
return this.enabled && this.held_('jump', this.touchJump || this.isDown('Space'));
|
|
177
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Change what a button SHOWS, without rebuilding the controls.
|
|
213
|
+
*
|
|
214
|
+
* `id` is an action's id, or `'jump'` for the platform's own button. Passing
|
|
215
|
+
* a url the button had no icon for at build time does nothing: the glyph is
|
|
216
|
+
* an element that only exists when one was given, and quietly growing one
|
|
217
|
+
* would move the label out from under a game that was relying on it.
|
|
218
|
+
*
|
|
219
|
+
* This is for buttons whose MEANING changes — an attack button under a bow
|
|
220
|
+
* and under a staff are two different actions wearing one control.
|
|
221
|
+
*/
|
|
222
|
+
setActionIcon(id, icon) {
|
|
223
|
+
const g = this.glyphs.get(id);
|
|
224
|
+
if (!g)
|
|
225
|
+
return;
|
|
226
|
+
const u = `url("${icon}")`;
|
|
227
|
+
g.style.setProperty('mask-image', u);
|
|
228
|
+
g.style.setProperty('-webkit-mask-image', u);
|
|
229
|
+
}
|
|
178
230
|
/** Whether an action is held right now — button or bound key, same answer. */
|
|
179
231
|
held(id) {
|
|
180
232
|
if (!this.enabled)
|
|
@@ -333,8 +385,8 @@ export class Input3D {
|
|
|
333
385
|
Object.assign(pad.style, floating ? {
|
|
334
386
|
position: 'absolute', width: '30vmin', height: '30vmin',
|
|
335
387
|
maxWidth: '180px', maxHeight: '180px',
|
|
336
|
-
borderRadius: '50%', background:
|
|
337
|
-
border:
|
|
388
|
+
borderRadius: '50%', background: CONTROL.padFill,
|
|
389
|
+
border: `2px solid ${CONTROL.padRim}`,
|
|
338
390
|
// Never interactive when floating: the ZONE owns the pointer, and a pad
|
|
339
391
|
// that also captured it would steal the very first move event as the
|
|
340
392
|
// thumb crosses its edge.
|
|
@@ -344,13 +396,13 @@ export class Input3D {
|
|
|
344
396
|
} : {
|
|
345
397
|
position: 'absolute', left: '5vmin', bottom: '5vmin',
|
|
346
398
|
width: '30vmin', height: '30vmin', maxWidth: '180px', maxHeight: '180px',
|
|
347
|
-
borderRadius: '50%', background:
|
|
348
|
-
border:
|
|
399
|
+
borderRadius: '50%', background: CONTROL.padFill,
|
|
400
|
+
border: `2px solid ${CONTROL.padRim}`, pointerEvents: 'auto',
|
|
349
401
|
});
|
|
350
402
|
const knob = document.createElement('div');
|
|
351
403
|
Object.assign(knob.style, {
|
|
352
404
|
position: 'absolute', left: '50%', top: '50%', width: '40%', height: '40%',
|
|
353
|
-
borderRadius: '50%', background:
|
|
405
|
+
borderRadius: '50%', background: CONTROL.ink,
|
|
354
406
|
transform: 'translate(-50%, -50%)', pointerEvents: 'none',
|
|
355
407
|
});
|
|
356
408
|
pad.appendChild(knob);
|
|
@@ -365,28 +417,62 @@ export class Input3D {
|
|
|
365
417
|
flexWrap: 'wrap-reverse', justifyContent: 'flex-start',
|
|
366
418
|
gap: '3vmin', maxWidth: '52vmin', pointerEvents: 'none',
|
|
367
419
|
});
|
|
368
|
-
|
|
420
|
+
/** Returns the button; its glyph, if it has one, is left in `glyph`. */
|
|
421
|
+
let glyph = null;
|
|
422
|
+
const makeButton = (label, icon) => {
|
|
423
|
+
glyph = null;
|
|
369
424
|
const el = document.createElement('div');
|
|
370
425
|
Object.assign(el.style, {
|
|
371
426
|
width: '20vmin', height: '20vmin', maxWidth: '120px', maxHeight: '120px',
|
|
372
427
|
flex: '0 0 auto',
|
|
373
|
-
borderRadius: '50%', background:
|
|
374
|
-
border:
|
|
428
|
+
borderRadius: '50%', background: CONTROL.fill,
|
|
429
|
+
border: `2px solid ${CONTROL.rim}`, pointerEvents: 'auto',
|
|
375
430
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
376
|
-
color:
|
|
431
|
+
color: CONTROL.ink, font: '600 4vmin/1 system-ui, sans-serif',
|
|
377
432
|
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
433
|
+
if (icon) {
|
|
434
|
+
// A MASK, not an <img>. The file's own colour is thrown away and the
|
|
435
|
+
// shape is painted in the button's `color`, so an icon keeps matching
|
|
436
|
+
// the controls beside it if those are ever restyled — and one file can
|
|
437
|
+
// serve a light layout and a dark one.
|
|
438
|
+
const g = document.createElement('span');
|
|
439
|
+
const u = `url("${icon}")`;
|
|
440
|
+
Object.assign(g.style, {
|
|
441
|
+
width: '45%', height: '45%', display: 'block',
|
|
442
|
+
backgroundColor: 'currentColor',
|
|
443
|
+
webkitMask: `${u} center/contain no-repeat`,
|
|
444
|
+
mask: `${u} center/contain no-repeat`,
|
|
445
|
+
});
|
|
446
|
+
// Safari still wants the prefixed longhands; the shorthand above is not
|
|
447
|
+
// enough there, and a mask that does not apply is a SOLID SQUARE rather
|
|
448
|
+
// than nothing — the most visible possible failure.
|
|
449
|
+
g.style.setProperty('-webkit-mask-image', u);
|
|
450
|
+
g.style.setProperty('-webkit-mask-size', 'contain');
|
|
451
|
+
g.style.setProperty('-webkit-mask-repeat', 'no-repeat');
|
|
452
|
+
g.style.setProperty('-webkit-mask-position', 'center');
|
|
453
|
+
el.append(g);
|
|
454
|
+
glyph = g;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
el.textContent = label;
|
|
458
|
+
}
|
|
459
|
+
// Explicitly, not by inheritance. A label IS text -- a glyph in a div --
|
|
460
|
+
// and long-pressing the attack button is how the iOS Copy / Look Up /
|
|
381
461
|
// Translate callout came up mid-fight, with the selection handles
|
|
382
|
-
// clamped around the little crossed swords.
|
|
462
|
+
// clamped around the little crossed swords. Which is most of why `icon`
|
|
463
|
+
// exists.
|
|
383
464
|
Object.assign(el.style, NO_SELECTION);
|
|
384
465
|
return el;
|
|
385
466
|
};
|
|
386
|
-
|
|
467
|
+
this.glyphs.clear();
|
|
468
|
+
const btn = makeButton('▲', this.jumpIcon);
|
|
469
|
+
if (glyph)
|
|
470
|
+
this.glyphs.set('jump', glyph);
|
|
387
471
|
cluster.append(btn);
|
|
388
472
|
for (const a of this.actions) {
|
|
389
|
-
const el = makeButton(a.label ?? a.id.slice(0, 1).toUpperCase());
|
|
473
|
+
const el = makeButton(a.label ?? a.id.slice(0, 1).toUpperCase(), a.icon);
|
|
474
|
+
if (glyph)
|
|
475
|
+
this.glyphs.set(a.id, glyph);
|
|
390
476
|
cluster.append(el);
|
|
391
477
|
this.wireButton(el, a.id);
|
|
392
478
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|