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 { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
|
||||||
import { api } from '../services/api.js';
|
import { api } from '../services/api.js';
|
||||||
import { Button } from '../ui/Button.js';
|
import { Button } from '../ui/Button.js';
|
||||||
|
import { Plaque } from '../ui/Plaque.js';
|
||||||
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
||||||
import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.js';
|
import { playMenuMusic, stopMenuMusic } from '../ui/MenuMusic.js';
|
||||||
import { TutorialModal } from '../ui/TutorialModal.js';
|
import { TutorialModal } from '../ui/TutorialModal.js';
|
||||||
|
|
@ -38,8 +39,13 @@ export default class GameMenuScene extends Phaser.Scene {
|
||||||
fontSize: '64px',
|
fontSize: '64px',
|
||||||
color: COLORS.textHex,
|
color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(1);
|
}).setOrigin(0.5).setDepth(1);
|
||||||
|
titleText.setLetterSpacing(2);
|
||||||
|
titleText.setShadow(0, 4, 'rgba(0,0,0,0.9)', 8);
|
||||||
this._titleText = titleText;
|
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…', {
|
const loadingText = this.add.text(cx, 540, 'Loading game list…', {
|
||||||
fontSize: '24px', color: COLORS.mutedHex,
|
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._hintText) { this._hintText.destroy(); this._hintText = null; }
|
||||||
if (this._titleText) {
|
if (this._titleText) {
|
||||||
this._titleText.setText('Choose a game');
|
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)) {
|
for (const [k, btn] of Object.entries(this._tabs)) {
|
||||||
btn.setActive(k === key);
|
btn.setActive(k === key);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import * as Phaser from 'phaser';
|
||||||
import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
|
import { GAME_HEIGHT, GAME_WIDTH, COLORS } from '../config.js';
|
||||||
import { auth } from '../services/auth.js';
|
import { auth } from '../services/auth.js';
|
||||||
import { Button } from '../ui/Button.js';
|
import { Button } from '../ui/Button.js';
|
||||||
|
import { Plaque } from '../ui/Plaque.js';
|
||||||
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
import { addFullscreenButton } from '../ui/FullscreenButton.js';
|
||||||
import { playMenuMusic } from '../ui/MenuMusic.js';
|
import { playMenuMusic } from '../ui/MenuMusic.js';
|
||||||
|
|
||||||
|
|
@ -69,6 +70,7 @@ export default class LandingScene extends Phaser.Scene {
|
||||||
color: COLORS.accentHex,
|
color: COLORS.accentHex,
|
||||||
}).setOrigin(0.5).setDepth(1);
|
}).setOrigin(0.5).setDepth(1);
|
||||||
this._welcomeText = welcomeText;
|
this._welcomeText = welcomeText;
|
||||||
|
welcomeText.setShadow(0, 3, 'rgba(0,0,0,0.8)', 5);
|
||||||
|
|
||||||
const hasAvatar = !!user?.avatarPath;
|
const hasAvatar = !!user?.avatarPath;
|
||||||
const totalW = hasAvatar ? avatarR * 2 + avatarGap + welcomeText.width : welcomeText.width;
|
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);
|
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) {
|
if (hasAvatar) {
|
||||||
const avatarCx = groupLeft + avatarR;
|
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