Polish title plates with rounded brass-trimmed plaque and size animation
- Replace flat black rectangles behind the game-menu and landing titles with a new Plaque component that draws a rounded, accent-bordered plate with a soft drop shadow and warm top highlight. - Add letter-spacing and text shadow to the menu title so it reads as raised against the plate. - When the tab label swaps (e.g. "Loading…" → "Choose a game"), tween the plaque to its new width via a size proxy that redraws each frame, and pop the title text with a Back.easeOut scale so the change feels intentional.
This commit is contained in:
parent
cbec3b3a44
commit
74cd4df85b
|
|
@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
|
||||
import { api } from '../services/api.js';
|
||||
import { Button } from '../ui/Button.js';
|
||||
import { Plaque } from '../ui/Plaque.js';
|
||||
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
||||
import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.js';
|
||||
import { TutorialModal } from '../ui/TutorialModal.js';
|
||||
|
|
@ -38,8 +39,13 @@ export default class GameMenuScene extends Phaser.Scene {
|
|||
fontSize: '64px',
|
||||
color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(1);
|
||||
titleText.setLetterSpacing(2);
|
||||
titleText.setShadow(0, 4, 'rgba(0,0,0,0.9)', 8);
|
||||
this._titleText = titleText;
|
||||
this._titleBg = this.add.rectangle(cx, 60, titleText.width + 64, titleText.height + 28, 0x000000, 0.7);
|
||||
const plateW = titleText.width + 64;
|
||||
const plateH = titleText.height + 28;
|
||||
this._titleBg = new Plaque(this, plateW, plateH).setPosition(cx, 60).setDepth(0.5);
|
||||
this._titlePlateW = plateW;
|
||||
|
||||
const loadingText = this.add.text(cx, 540, 'Loading game list…', {
|
||||
fontSize: '24px', color: COLORS.mutedHex,
|
||||
|
|
@ -134,7 +140,15 @@ export default class GameMenuScene extends Phaser.Scene {
|
|||
if (this._hintText) { this._hintText.destroy(); this._hintText = null; }
|
||||
if (this._titleText) {
|
||||
this._titleText.setText('Choose a game');
|
||||
this._titleBg.setSize(this._titleText.width + 64, this._titleText.height + 28);
|
||||
const newW = this._titleText.width + 64;
|
||||
const newH = this._titleText.height + 28;
|
||||
if (this._titlePlateW && Math.abs(this._titlePlateW - newW) > 1) {
|
||||
this._titleBg.animateSize(newW, newH, 240, 'Quad.easeOut');
|
||||
this._titlePlateW = newW;
|
||||
}
|
||||
// Little pop so the label swap reads as intentional.
|
||||
this._titleText.setScale(0.9);
|
||||
this.tweens.add({ targets: this._titleText, scaleX: 1, scaleY: 1, duration: 240, ease: 'Back.easeOut' });
|
||||
}
|
||||
for (const [k, btn] of Object.entries(this._tabs)) {
|
||||
btn.setActive(k === key);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
|
|||
import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
|
||||
import { auth } from '../services/auth.js';
|
||||
import { Button } from '../ui/Button.js';
|
||||
import { Plaque } from '../ui/Plaque.js';
|
||||
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
||||
import { playMenuMusic } from '../ui/MenuMusic.js';
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ export default class LandingScene extends Phaser.Scene {
|
|||
color: COLORS.accentHex,
|
||||
}).setOrigin(0.5).setDepth(1);
|
||||
this._welcomeText = welcomeText;
|
||||
welcomeText.setShadow(0, 3, 'rgba(0,0,0,0.8)', 5);
|
||||
|
||||
const hasAvatar = !!user?.avatarPath;
|
||||
const totalW = hasAvatar ? avatarR * 2 + avatarGap + welcomeText.width : welcomeText.width;
|
||||
|
|
@ -76,7 +78,8 @@ export default class LandingScene extends Phaser.Scene {
|
|||
|
||||
welcomeText.setX(hasAvatar ? groupLeft + avatarR * 2 + avatarGap + welcomeText.width / 2 : cx);
|
||||
|
||||
this._welcomeBg = this.add.rectangle(cx, y, totalW + pad.x * 2, welcomeText.height + pad.y * 2, 0x000000, 0.45);
|
||||
this._welcomeBg = new Plaque(this, totalW + pad.x * 2, welcomeText.height + pad.y * 2, { radius: 16 })
|
||||
.setPosition(cx, y);
|
||||
|
||||
if (hasAvatar) {
|
||||
const avatarCx = groupLeft + avatarR;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { COLORS } from '../config.js';
|
||||
|
||||
// A rounded, brass-trimmed plaque in the same design language as the
|
||||
// category tabs. Use instead of a flat black rectangle behind title text.
|
||||
export class Plaque extends Phaser.GameObjects.Graphics {
|
||||
constructor(scene, width = 300, height = 90, options = {}) {
|
||||
super(scene, false);
|
||||
const {
|
||||
fill = 0x241c10,
|
||||
fillAlpha = 0.92,
|
||||
border = COLORS.accent,
|
||||
radius = 18,
|
||||
} = options;
|
||||
this._opts = { fill, fillAlpha, border, radius };
|
||||
this._w = width;
|
||||
this._h = height;
|
||||
this.postFX.addShadow(0, 3, 0.004, 1.5, 0x000000, 8, 0.65);
|
||||
this.redraw(width, height);
|
||||
scene.add.existing(this);
|
||||
}
|
||||
|
||||
redraw(w, h) {
|
||||
this._w = w;
|
||||
this._h = h;
|
||||
const { fill, fillAlpha, border, radius } = this._opts;
|
||||
const r = Math.max(4, Math.min(radius, w / 2, h / 2));
|
||||
this.clear();
|
||||
this.fillStyle(fill, fillAlpha);
|
||||
this.fillRoundedRect(-w / 2, -h / 2, w, h, r);
|
||||
// Faint warm highlight just inside the top edge reads as a raised plate.
|
||||
this.lineStyle(2, 0xfff6dd, 0.16);
|
||||
this.lineBetween(-w / 2 + r + 4, -h / 2 + 4, w / 2 - r - 4, -h / 2 + 4);
|
||||
this.lineStyle(2, border, 1);
|
||||
this.strokeRoundedRect(-w / 2, -h / 2, w, h, r);
|
||||
}
|
||||
|
||||
// Animate the plate to a new size. Graphics in this Phaser build exposes
|
||||
// no setScaleX/setScaleY (and scaling a redrawn shape is unreliable), so
|
||||
// we tween a size proxy and redraw every frame instead.
|
||||
animateSize(w, h, duration = 240, ease = 'Quad.easeOut', onComplete) {
|
||||
if (this._sizeTween) this._sizeTween.stop();
|
||||
const proxy = { w: this._w, h: this._h };
|
||||
this._sizeTween = this.scene.tweens.add({
|
||||
targets: proxy,
|
||||
w,
|
||||
h,
|
||||
duration,
|
||||
ease,
|
||||
onUpdate: () => {
|
||||
if (!this.scene) return; // destroyed mid-animation
|
||||
this.redraw(proxy.w, proxy.h);
|
||||
},
|
||||
onComplete: () => {
|
||||
this._sizeTween = null;
|
||||
if (!this.scene) return; // destroyed mid-animation
|
||||
this.redraw(w, h);
|
||||
if (onComplete) onComplete();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue