@umicat/three-sdk 0.9.1 → 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/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: 'rgba(255,255,255,0.14)',
337
- border: '2px solid rgba(255,255,255,0.35)',
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: 'rgba(255,255,255,0.14)',
348
- border: '2px solid rgba(255,255,255,0.35)', pointerEvents: 'auto',
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: 'rgba(255,255,255,0.55)',
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
- const makeButton = (label) => {
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: 'rgba(255,255,255,0.2)',
374
- border: '2px solid rgba(255,255,255,0.4)', pointerEvents: 'auto',
428
+ borderRadius: '50%', background: CONTROL.fill,
429
+ border: `2px solid ${CONTROL.rim}`, pointerEvents: 'auto',
375
430
  display: 'flex', alignItems: 'center', justifyContent: 'center',
376
- color: 'rgba(255,255,255,0.85)', font: '600 4vmin/1 system-ui, sans-serif',
431
+ color: CONTROL.ink, font: '600 4vmin/1 system-ui, sans-serif',
377
432
  });
378
- el.textContent = label;
379
- // Explicitly, not by inheritance. The label IS text -- a glyph in a div
380
- // -- and long-pressing the attack button is how the iOS Copy / Look Up /
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
- const btn = makeButton('▲');
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.9.1",
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",