Simplify action bar slots to label-only buttons

- Remove the drawn slot icons (research/build/ship/menu/reserved) and their RGB ghost copies; keep the GlitchText split on labels and panel outline only.
- Center button text vertically and shrink the bar height from 92 to 56 px now that there is no icon row.
- Drop iconSize/iconY config and the drawIcon helper, and update README/comments to match the label-only design.
This commit is contained in:
Brian Fertig 2026-09-04 09:12:03 -06:00
parent 2b37be7494
commit adc0389c1e
3 changed files with 12 additions and 126 deletions

View File

@ -146,7 +146,7 @@ layers now, with the rules and panels to come next:
Six evenly spaced slots: **Research, Build, Ship, ·, ·, Menu**. The slots Six evenly spaced slots: **Research, Build, Ship, ·, ·, Menu**. The slots
fire `onAction` and are otherwise inert; two slots are reserved. Dressed fire `onAction` and are otherwise inert; two slots are reserved. Dressed
with the menu's CRT language — scanlines over the strip and the GlitchText with the menu's CRT language — scanlines over the strip and the GlitchText
RGB pull-apart on icons, labels, and the panel outline during bursts. All RGB pull-apart on labels and the panel outline during bursts. All
layout, palette, and motion (boot flicker, rail comet, glitch bursts, RGB layout, palette, and motion (boot flicker, rail comet, glitch bursts, RGB
split, scanlines) live in the config. split, scanlines) live in the config.

View File

@ -1,14 +1,13 @@
{ {
"_comment": "COMMAND DECK — the cyberpunk bar across the bottom of the screen (rendered by js/ui/ActionBar.js). Six evenly spaced slots: `buttons` is the ORDERED list — give a slot an `id` (the action it will fire) + `label` + `accent`, or leave both `null` for a reserved slot. Buttons currently fire `onAction` (see GameScene); the behavior behind them comes next. Everything visual is procedural (no image assets) and tunable here: layout in `height`/`margin`/`padding`/`button`, palette in `colors`, CRT scanlines in `scanline`, motion in `animation` (boot flicker, the rail comet, the glitch bursts, the RGB pull-apart).", "_comment": "COMMAND DECK — the cyberpunk bar across the bottom of the screen (rendered by js/ui/ActionBar.js). Six evenly spaced slots: `buttons` is the ORDERED list — give a slot an `id` (the action it will fire) + `label` + `accent`, or leave both `null` for a reserved slot. Buttons currently fire `onAction` (see GameScene); the behavior behind them comes next. Everything visual is procedural (no image assets) and tunable here: layout in `height`/`margin`/`padding`/`button`, palette in `colors`, CRT scanlines in `scanline`, motion in `animation` (boot flicker, the rail comet, the glitch bursts, the RGB pull-apart).",
"enabled": true, "enabled": true,
"height": 92, "height": 56,
"margin": { "left": 20, "right": 20, "bottom": 12 }, "margin": { "left": 20, "right": 20, "bottom": 12 },
"padding": 16, "padding": 16,
"button": { "button": {
"widthFactor": 0.72, "widthFactor": 0.72,
"maxWidth": 190, "maxWidth": 190,
"notch": 10, "notch": 10,
"iconSize": 26,
"fontSize": 14, "fontSize": 14,
"letterSpacing": 2.5 "letterSpacing": 2.5
}, },
@ -39,7 +38,7 @@
"animation": { "animation": {
"bootStagger": 70, "bootStagger": 70,
"rgb": { "rgb": {
"_comment": "RGB pull-apart on icons/labels/panel (the menu title's GlitchText technique): idleOffset px of constant chromatic fringe, burstOffset extra px at full burst, idleAlpha/burstAlpha ghost strength.", "_comment": "RGB pull-apart on labels/panel (the menu title's GlitchText technique): idleOffset px of constant chromatic fringe, burstOffset extra px at full burst, idleAlpha/burstAlpha ghost strength.",
"idleOffset": 1.8, "idleOffset": 1.8,
"burstOffset": 6, "burstOffset": 6,
"idleAlpha": 0.4, "idleAlpha": 0.4,

View File

@ -6,17 +6,16 @@ import { canvasTexture } from '../utils/Textures.js';
import { CyberShape } from './CyberShape.js'; import { CyberShape } from './CyberShape.js';
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
const DEG = Math.PI / 180;
/** /**
* COMMAND DECK the cyberpunk action bar across the bottom of the screen: * COMMAND DECK the cyberpunk action bar across the bottom of the screen:
* a cut-corner console panel with an energy rail, six evenly spaced slot * a cut-corner console panel with an energy rail, six evenly spaced slot
* buttons (drawn icons, RGB-split labels, per-slot accent colors), reserved * buttons (label-only, per-slot accent rail on top, RGB-split labels),
* "standby" slots, a rail comet that streaks past on a loop, periodic glitch * reserved "standby" slots, a rail comet that streaks past on a loop, periodic glitch
* bursts (slice bars + label jitter), and a boot flicker-in. * bursts (slice bars + label jitter), and a boot flicker-in.
* Dressed with the menu's CRT language: scanlines over the whole bar (the * Dressed with the menu's CRT language: scanlines over the whole bar (the
* same 1px-dark-row tile as CyberOverlay) and the GlitchText RGB pull-apart * same 1px-dark-row tile as CyberOverlay) and the GlitchText RGB pull-apart
* on everything icons, labels, and the panel outline itself on bursts. * on labels and the panel outline itself on bursts.
* *
* Everything is procedural no image assets and fully config-driven * Everything is procedural no image assets and fully config-driven
* (data/actionbar.json): slots, labels, accents, palette, and every * (data/actionbar.json): slots, labels, accents, palette, and every
@ -69,9 +68,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
slotBorder: toColor(ab.colors?.slotBorder, 0x22405f), slotBorder: toColor(ab.colors?.slotBorder, 0x22405f),
reserved: toColor(ab.colors?.reserved, 0x3d4c74), reserved: toColor(ab.colors?.reserved, 0x3d4c74),
inkCss: toCss(ab.colors?.text ?? '#eaf6ff'), inkCss: toCss(ab.colors?.text ?? '#eaf6ff'),
iconY: -12, labelY: 0, // label-only buttons: the text is centred vertically
labelY: 16,
iconSize: bc.iconSize ?? 26,
}; };
// The menu's CRT scanline tuning (data/theme.json) is the default, so // The menu's CRT scanline tuning (data/theme.json) is the default, so
// the bar matches the menu unless actionbar.json overrides it. // the bar matches the menu unless actionbar.json overrides it.
@ -212,8 +209,6 @@ export class ActionBar extends Phaser.GameObjects.Container {
const letterSpacing = bc.letterSpacing ?? 2.5; const letterSpacing = bc.letterSpacing ?? 2.5;
const cyan = toCss('#00e5ff'); const cyan = toCss('#00e5ff');
const magenta = toCss('#ff2d6f'); const magenta = toCss('#ff2d6f');
const iconSize = this.style.iconSize;
const iconY = this.style.iconY;
this.slots = buttons.map((b, i) => { this.slots = buttons.map((b, i) => {
const live = typeof b.id === 'string' && b.id.length > 0; const live = typeof b.id === 'string' && b.id.length > 0;
@ -244,31 +239,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
_scaleTw: null, _scaleTw: null,
}; };
s.panel = scene.add.graphics().setScrollFactor(0); s.panel = scene.add.graphics().setScrollFactor(0);
// RGB-split icon ghosts (the GlitchText technique on the menu title): slot.add(s.panel);
// additive magenta/cyan copies of the drawn icon, moved per frame.
s.iconGhostM = scene
.add.graphics()
.setPosition(0, iconY)
.setScrollFactor(0)
.setBlendMode(Phaser.BlendModes.ADD)
.setAlpha(0);
s.iconGhostC = scene
.add.graphics()
.setPosition(0, iconY)
.setScrollFactor(0)
.setBlendMode(Phaser.BlendModes.ADD)
.setAlpha(0);
s.icon = scene.add.graphics().setPosition(0, iconY).setScrollFactor(0);
slot.add([s.panel, s.iconGhostM, s.iconGhostC, s.icon]);
// Drawn icon (accent for live slots, dim for reserved sockets) —
// live slots also get their ghost copies in the split colors.
const iconKind = live ? b.id : 'reserved';
drawIcon(s.icon, iconKind, iconSize, live ? s.accent : this.style.reserved);
if (live) {
drawIcon(s.iconGhostM, iconKind, iconSize, 0xff2d6f);
drawIcon(s.iconGhostC, iconKind, iconSize, 0x00e5ff);
}
if (live) { if (live) {
const labelText = String(b.label ?? '').toUpperCase(); const labelText = String(b.label ?? '').toUpperCase();
@ -312,8 +283,8 @@ export class ActionBar extends Phaser.GameObjects.Container {
s.panel.on('pointerout', () => this.setHover(s, false)); s.panel.on('pointerout', () => this.setHover(s, false));
s.panel.on('pointerdown', () => this.press(s)); s.panel.on('pointerdown', () => this.press(s));
} else { } else {
// Reserved socket: a standby dot that breathes in update(). // Reserved socket: a standby dot (centred) that breathes in update().
s.resDot = scene.add.circle(0, iconY, 1.6, this.style.reserved, 0.35).setScrollFactor(0); s.resDot = scene.add.circle(0, 0, 1.6, this.style.reserved, 0.35).setScrollFactor(0);
slot.add(s.resDot); slot.add(s.resDot);
} }
@ -509,7 +480,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
/** /**
* Fire a glitch burst right now: slice bars across the deck, and the RGB * Fire a glitch burst right now: slice bars across the deck, and the RGB
* split (level 1, decays in update) on icons, labels, and the panel. * split (level 1, decays in update) on labels and the panel.
*/ */
triggerBurst(duration = 300, intensity = 1) { triggerBurst(duration = 300, intensity = 1) {
const now = this.lastTime ?? this.scene.time.now; const now = this.lastTime ?? this.scene.time.now;
@ -620,7 +591,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
const flick = 0.6 + 0.4 * Math.sin(t * 3.3); const flick = 0.6 + 0.4 * Math.sin(t * 3.3);
const baseA = Math.min(1, rgb.idleAlpha * flick + L * rgb.burstAlpha); const baseA = Math.min(1, rgb.idleAlpha * flick + L * rgb.burstAlpha);
// Slots: the menu's RGB pull-apart on icon + label — magenta left, // Slots: the menu's RGB pull-apart on the label — magenta left,
// cyan right (GlitchText's exact offsets), always a faint fringe at // cyan right (GlitchText's exact offsets), always a faint fringe at
// idle, pulled apart + jittered during bursts; per-slot phase so the // idle, pulled apart + jittered during bursts; per-slot phase so the
// six buttons don't shimmer in lockstep. // six buttons don't shimmer in lockstep.
@ -636,8 +607,6 @@ export class ActionBar extends Phaser.GameObjects.Container {
const a = s.hoverOn ? Math.min(1, baseA + 0.25) : baseA; const a = s.hoverOn ? Math.min(1, baseA + 0.25) : baseA;
s.ghostMagenta.setAlpha(a).setPosition(-off + jx, this.style.labelY + jy * 0.5); s.ghostMagenta.setAlpha(a).setPosition(-off + jx, this.style.labelY + jy * 0.5);
s.ghostCyan.setAlpha(a * 0.95).setPosition(off * 0.85, this.style.labelY - off * 0.3 + jy); s.ghostCyan.setAlpha(a * 0.95).setPosition(off * 0.85, this.style.labelY - off * 0.3 + jy);
s.iconGhostM.setAlpha(a * 0.8).setPosition(-off + jx, this.style.iconY + jy * 0.5);
s.iconGhostC.setAlpha(a * 0.75).setPosition(off * 0.85, this.style.iconY - off * 0.3 + jy);
} }
// The bar itself: the panel outline pulled apart in magenta/cyan while // The bar itself: the panel outline pulled apart in magenta/cyan while
@ -835,85 +804,3 @@ function drawBarBody(ctx, W, H, P, c) {
corner(x - 3, y + h + 3, 1, -1); corner(x - 3, y + h + 3, 1, -1);
corner(x + w + 3, y + h + 3, -1, -1); corner(x + w + 3, y + h + 3, -1, -1);
} }
/**
* The drawn slot icons small, geometric, in the console's language.
* Each icon is drawn centered on the icon Graphics' origin.
*/
function drawIcon(g, id, size, color) {
const r = size * 0.44;
const glow = (lw) => g.lineStyle(lw, color, 0.25);
switch (id) {
case 'research': {
// Orbit: a ring with a lit trail, a satellite riding it, a core.
glow(4);
g.strokeCircle(0, 0, r);
g.lineStyle(1.5, color, 0.95);
g.strokeCircle(0, 0, r);
g.lineStyle(2, color, 0.5); // trail behind the satellite
g.beginPath();
g.arc(0, 0, r, -110 * DEG, -25 * DEG);
g.stroke();
const sa = -35 * DEG;
g.fillStyle(color, 1);
g.fillCircle(Math.cos(sa) * r, Math.sin(sa) * r, 2.6);
g.fillStyle(color, 0.9);
g.fillCircle(0, 0, 1.7);
break;
}
case 'build': {
// Isometric cube — construction.
const v = [0, -30, 30, 90, 150, 210].map((deg) => ({
x: Math.cos(deg * DEG) * r,
y: Math.sin(deg * DEG) * r,
}));
glow(4);
g.strokePoints(v, true);
g.lineStyle(1.5, color, 0.95);
g.strokePoints(v, true);
g.lineStyle(1.5, color, 0.8); // inner edges of the cube
g.lineBetween(0, 0, v[0].x, v[0].y);
g.lineBetween(0, 0, v[2].x, v[2].y);
g.lineBetween(0, 0, v[4].x, v[4].y);
break;
}
case 'ship': {
// Chevron arrow, nose up.
const pts = [
{ x: 0, y: -r },
{ x: r * 0.78, y: r * 0.62 },
{ x: 0, y: r * 0.18 },
{ x: -r * 0.78, y: r * 0.62 },
];
glow(4);
g.strokePoints(pts, true);
g.lineStyle(1.5, color, 0.95);
g.strokePoints(pts, true);
break;
}
case 'menu': {
// Signal bars — slightly ragged widths, left-aligned.
g.fillStyle(color, 0.9);
g.fillRect(-r, -r * 0.75, r * 2, 2.4);
g.fillRect(-r, -1.2, r * 1.5, 2.4);
g.fillRect(-r, r * 0.75 - 2.4, r * 1.85, 2.4);
break;
}
default: {
// Reserved socket: a dim diamond — a slot waiting for a command.
const d = [
{ x: 0, y: -7 },
{ x: 7, y: 0 },
{ x: 0, y: 7 },
{ x: -7, y: 0 },
];
g.lineStyle(1.2, color, 0.45);
g.strokePoints(d, true);
}
}
}