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
fire `onAction` and are otherwise inert; two slots are reserved. Dressed
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
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).",
"enabled": true,
"height": 92,
"height": 56,
"margin": { "left": 20, "right": 20, "bottom": 12 },
"padding": 16,
"button": {
"widthFactor": 0.72,
"maxWidth": 190,
"notch": 10,
"iconSize": 26,
"fontSize": 14,
"letterSpacing": 2.5
},
@ -39,7 +38,7 @@
"animation": {
"bootStagger": 70,
"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,
"burstOffset": 6,
"idleAlpha": 0.4,

View File

@ -6,17 +6,16 @@ import { canvasTexture } from '../utils/Textures.js';
import { CyberShape } from './CyberShape.js';
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:
* a cut-corner console panel with an energy rail, six evenly spaced slot
* buttons (drawn icons, RGB-split labels, per-slot accent colors), reserved
* "standby" slots, a rail comet that streaks past on a loop, periodic glitch
* buttons (label-only, per-slot accent rail on top, RGB-split labels),
* reserved "standby" slots, a rail comet that streaks past on a loop, periodic glitch
* bursts (slice bars + label jitter), and a boot flicker-in.
* 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
* 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
* (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),
reserved: toColor(ab.colors?.reserved, 0x3d4c74),
inkCss: toCss(ab.colors?.text ?? '#eaf6ff'),
iconY: -12,
labelY: 16,
iconSize: bc.iconSize ?? 26,
labelY: 0, // label-only buttons: the text is centred vertically
};
// The menu's CRT scanline tuning (data/theme.json) is the default, so
// 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 cyan = toCss('#00e5ff');
const magenta = toCss('#ff2d6f');
const iconSize = this.style.iconSize;
const iconY = this.style.iconY;
this.slots = buttons.map((b, i) => {
const live = typeof b.id === 'string' && b.id.length > 0;
@ -244,31 +239,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
_scaleTw: null,
};
s.panel = scene.add.graphics().setScrollFactor(0);
// RGB-split icon ghosts (the GlitchText technique on the menu title):
// 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);
}
slot.add(s.panel);
if (live) {
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('pointerdown', () => this.press(s));
} else {
// Reserved socket: a standby dot that breathes in update().
s.resDot = scene.add.circle(0, iconY, 1.6, this.style.reserved, 0.35).setScrollFactor(0);
// Reserved socket: a standby dot (centred) that breathes in update().
s.resDot = scene.add.circle(0, 0, 1.6, this.style.reserved, 0.35).setScrollFactor(0);
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
* 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) {
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 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
// idle, pulled apart + jittered during bursts; per-slot phase so the
// 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;
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.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
@ -835,85 +804,3 @@ function drawBarBody(ctx, W, H, P, c) {
corner(x - 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);
}
}
}