P12.2: candy HUD & win/lose banner restyle (sx / iOS)

Restyle the code-drawn UI toward the candy look — colours, corner-rounding
and glossy feel only; no rect geometry moves.

- HUD: grape candy card with a glossy top sheen, a bright rounded rim and
  warm cream text on a soft purple shadow (was a flat dark translucent panel).
- Banner panel: grape candy fill under a sheen + bright rim, rounder corners.
- Titles: celebratory candy gold YOU WIN! / punchy coral OUT OF MOVES, each
  on a tinted drop shadow for pop.
- PLAY AGAIN: bubblegum candy button with a glossy sheen, bright rim and a
  darker bevel lip for a 3D candy edge.

BannerLayout rects (panel/title/button) and the restart hit-test are
untouched, so tests/banner_layout still passes. Refresh the p4_hud / p7_win /
p7_lose goldens.
This commit is contained in:
swipelab
2026-06-05 21:52:00 +03:00
parent 246dcfa224
commit 7d18ba7e4d
4 changed files with 81 additions and 30 deletions

View File

@@ -46,24 +46,42 @@ SELECT_RIM :: Color.{ r = 255, g = 234, b = 92, a = 255 }; // bright candy
SELECT_RIM_HI :: Color.{ r = 255, g = 255, b = 232, a = 220 }; // glossy inner highlight ring
SELECT_GLOSS :: Color.{ r = 255, g = 255, b = 255, a = 96 }; // wet sheen on the selected gem
// HUD: a translucent card with the score and remaining moves, in the loaded Lato
// font. Placed in the empty band above the centered grid (inside the safe area).
HUD_FONT :f32: 34.0;
HUD_PAD :f32: 14.0;
HUD_LINE_GAP :f32: 6.0;
HUD_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
HUD_PANEL :: Color.{ r = 12, g = 14, b = 22, a = 185 };
// HUD (P12.2): a glossy candy card with the score and remaining moves, in the
// loaded Lato font. Placed in the empty band above the centered grid (inside the
// safe area). The fill is a bright grape candy, lifted by a translucent top sheen
// and a bright rounded rim; cream text rides a soft purple shadow for punch.
HUD_FONT :f32: 34.0;
HUD_PAD :f32: 14.0;
HUD_LINE_GAP :f32: 6.0;
HUD_RADIUS :f32: 20.0;
HUD_TEXT :: Color.{ r = 255, g = 252, b = 245, a = 255 }; // warm cream text
HUD_TEXT_SH :: Color.{ r = 56, g = 18, b = 80, a = 150 }; // soft purple text shadow
HUD_PANEL :: Color.{ r = 92, g = 46, b = 150, a = 224 }; // bright grape candy fill
HUD_PANEL_HI :: Color.{ r = 196, g = 138, b = 240, a = 92 }; // glossy top sheen
HUD_PANEL_RIM:: Color.{ r = 236, g = 204, b = 255, a = 150 }; // bright candy rim
// Win/lose banner (P7.2): a dim over the board, an opaque panel, the win/lose
// headline, and a restart button. Built from text + rects only — the engine's
// image path can't tint/fade at draw time (issue 0002), but rects and text DO
// honour colour + alpha, so the whole overlay is drawn with them.
BANNER_DIM :: Color.{ r = 6, g = 8, b = 14, a = 188 };
BANNER_PANEL :: Color.{ r = 20, g = 24, b = 38, a = 240 };
BANNER_WIN_TEXT :: Color.{ r = 120, g = 240, b = 150, a = 255 };
BANNER_LOSE_TEXT :: Color.{ r = 255, g = 120, b = 110, a = 255 };
BANNER_BTN :: Color.{ r = 64, g = 132, b = 224, a = 255 };
BANNER_BTN_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
// Win/lose banner (P12.2): a warm dim over the board, a glossy candy panel, the
// win/lose headline, and a playful restart button. Built from text + rects only —
// the engine's image path can't tint/fade at draw time (issue 0002), but rects and
// text DO honour colour + alpha, so the whole overlay is drawn with them. Each
// candy surface is a fill + a top sheen + a bright rounded rim; titles and the
// button label ride a tinted drop shadow so they pop off the panel.
BANNER_DIM :: Color.{ r = 26, g = 10, b = 44, a = 184 }; // warm purple dim
BANNER_PANEL :: Color.{ r = 96, g = 50, b = 156, a = 244 }; // grape candy panel
BANNER_PANEL_HI :: Color.{ r = 198, g = 140, b = 242, a = 110 }; // glossy panel sheen
BANNER_PANEL_RIM :: Color.{ r = 240, g = 208, b = 255, a = 168 }; // bright panel rim
BANNER_WIN_TEXT :: Color.{ r = 255, g = 220, b = 96, a = 255 }; // celebratory candy gold
BANNER_WIN_SH :: Color.{ r = 120, g = 56, b = 8, a = 220 }; // warm amber shadow
BANNER_LOSE_TEXT :: Color.{ r = 255, g = 104, b = 104, a = 255 }; // punchy candy coral
BANNER_LOSE_SH :: Color.{ r = 92, g = 14, b = 32, a = 220 }; // deep berry shadow
BANNER_BTN :: Color.{ r = 255, g = 120, b = 178, a = 255 }; // bubblegum candy CTA
BANNER_BTN_HI :: Color.{ r = 255, g = 198, b = 222, a = 150 }; // glossy button sheen
BANNER_BTN_RIM :: Color.{ r = 255, g = 226, b = 240, a = 184 }; // bright button rim
BANNER_BTN_SHADE :: Color.{ r = 198, g = 52, b = 120, a = 210 }; // darker bevel lip (3D)
BANNER_BTN_TEXT :: Color.{ r = 255, g = 255, b = 255, a = 255 };
BANNER_BTN_TEXT_SH:: Color.{ r = 120, g = 20, b = 64, a = 200 }; // button label shadow
BANNER_PANEL_RADIUS :f32: 24.0;
BANNER_BTN_RADIUS :f32: 16.0;
BANNER_TITLE_FONT :f32: 52.0;
BANNER_BTN_FONT :f32: 30.0;
@@ -537,25 +555,38 @@ BoardView :: struct {
ctx.add_rect(self.layout.grid_frame(), BANNER_DIM);
bl := self.layout.banner();
ctx.add_rounded_rect(bl.panel, BANNER_PANEL, 18.0);
// Candy panel: grape fill under a glossy top sheen and a bright rounded rim.
// Geometry is the shared bl.panel — only colour / rounding / gloss change.
ctx.add_rounded_rect(bl.panel, BANNER_PANEL, BANNER_PANEL_RADIUS);
ctx.add_rounded_rect(top_sheen(bl.panel, 0.42, BANNER_PANEL_RADIUS * 0.6), BANNER_PANEL_HI, BANNER_PANEL_RADIUS * 0.8);
prim := max(2.0, BANNER_PANEL_RADIUS * 0.12);
ctx.add_stroked_rect(bl.panel, BANNER_PANEL_RIM, BANNER_PANEL_RIM, prim, BANNER_PANEL_RADIUS);
title := if status == .won then "YOU WIN!" else "OUT OF MOVES";
tcol := if status == .won then BANNER_WIN_TEXT else BANNER_LOSE_TEXT;
tsh := if status == .won then BANNER_WIN_SH else BANNER_LOSE_SH;
tfont := fit_font(title, BANNER_TITLE_FONT, bl.title.size.width);
tsz := measure_text(title, tfont);
ctx.add_text(
Frame.make(bl.title.mid_x() - tsz.width * 0.5, bl.title.mid_y() - tsz.height * 0.5, tsz.width, tsz.height),
title, tfont, tcol
);
tfr := Frame.make(bl.title.mid_x() - tsz.width * 0.5, bl.title.mid_y() - tsz.height * 0.5, tsz.width, tsz.height);
ctx.add_text(Frame.make(tfr.origin.x + 2.0, tfr.origin.y + 3.0, tfr.size.width, tfr.size.height), title, tfont, tsh);
ctx.add_text(tfr, title, tfont, tcol);
// Candy button: a darker bevel lip peeks under the bubblegum fill for a 3D
// candy edge, lifted by a glossy sheen and a bright rim. The fill / hit rect
// is the shared bl.button, so the restart hit-test is byte-for-byte unchanged.
ctx.add_rounded_rect(Frame.make(bl.button.origin.x, bl.button.origin.y + 3.0, bl.button.size.width, bl.button.size.height), BANNER_BTN_SHADE, BANNER_BTN_RADIUS);
ctx.add_rounded_rect(bl.button, BANNER_BTN, BANNER_BTN_RADIUS);
ctx.add_rounded_rect(top_sheen(bl.button, 0.46, BANNER_BTN_RADIUS * 0.5), BANNER_BTN_HI, BANNER_BTN_RADIUS * 0.8);
brim := max(2.0, BANNER_BTN_RADIUS * 0.14);
ctx.add_stroked_rect(bl.button, BANNER_BTN_RIM, BANNER_BTN_RIM, brim, BANNER_BTN_RADIUS);
ctx.add_rounded_rect(bl.button, BANNER_BTN, 12.0);
btxt := "PLAY AGAIN";
bfont := fit_font(btxt, BANNER_BTN_FONT, bl.button.size.width * 0.86);
bsz := measure_text(btxt, bfont);
ctx.add_text(
Frame.make(bl.button.mid_x() - bsz.width * 0.5, bl.button.mid_y() - bsz.height * 0.5, bsz.width, bsz.height),
btxt, bfont, BANNER_BTN_TEXT
);
bfr := Frame.make(bl.button.mid_x() - bsz.width * 0.5, bl.button.mid_y() - bsz.height * 0.5, bsz.width, bsz.height);
ctx.add_text(Frame.make(bfr.origin.x + 1.5, bfr.origin.y + 2.0, bfr.size.width, bfr.size.height), btxt, bfont, BANNER_BTN_TEXT_SH);
ctx.add_text(bfr, btxt, bfont, BANNER_BTN_TEXT);
}
// Restart action behind the banner's button: reseed the SAME starting level
@@ -583,6 +614,13 @@ fit_font :: (text: string, base: f32, max_w: f32) -> f32 {
base * max_w / sz.width
}
// A rounded rect covering the top `frac` of `f`, inset by `pad` on the sides and
// top — the glossy candy sheen sat over a panel/button fill. The renderer has no
// gradient, so a single brighter translucent cap fakes the gloss.
top_sheen :: (f: Frame, frac: f32, pad: f32) -> Frame {
Frame.make(f.origin.x + pad, f.origin.y + pad, f.size.width - pad * 2.0, f.size.height * frac)
}
impl View for BoardView {
size_that_fits :: (self: *BoardView, proposal: ProposedSize) -> Size {
Size.{ width = proposal.width ?? 0.0, height = proposal.height ?? 0.0 }
@@ -738,11 +776,24 @@ render_hud :: (ctx: *RenderContext, board: *Board, avail: Frame) {
panel_h := score_sz.height + HUD_LINE_GAP + moves_sz.height + HUD_PAD * 2.0;
panel_x := avail.origin.x + (avail.size.width - panel_w) * 0.5;
panel_y := avail.origin.y + HUD_PAD;
ctx.add_rounded_rect(Frame.make(panel_x, panel_y, panel_w, panel_h), HUD_PANEL, 12.0);
panel := Frame.make(panel_x, panel_y, panel_w, panel_h);
// Candy card: grape fill, a glossy top sheen, then a bright rounded rim.
ctx.add_rounded_rect(panel, HUD_PANEL, HUD_RADIUS);
ctx.add_rounded_rect(top_sheen(panel, 0.46, HUD_RADIUS * 0.5), HUD_PANEL_HI, HUD_RADIUS * 0.8);
rim := max(2.0, HUD_RADIUS * 0.12);
ctx.add_stroked_rect(panel, HUD_PANEL_RIM, HUD_PANEL_RIM, rim, HUD_RADIUS);
tx := panel_x + HUD_PAD;
ty := panel_y + HUD_PAD;
ctx.add_text(Frame.make(tx, ty, score_sz.width, score_sz.height), score_str, HUD_FONT, HUD_TEXT);
hud_line(ctx, Frame.make(tx, ty, score_sz.width, score_sz.height), score_str);
ty += score_sz.height + HUD_LINE_GAP;
ctx.add_text(Frame.make(tx, ty, moves_sz.width, moves_sz.height), moves_str, HUD_FONT, HUD_TEXT);
hud_line(ctx, Frame.make(tx, ty, moves_sz.width, moves_sz.height), moves_str);
}
// One HUD text row: a soft purple shadow under the warm cream text, so the line
// stays legible over the grape card. Geometry is the caller's row frame.
hud_line :: (ctx: *RenderContext, f: Frame, text: string) {
ctx.add_text(Frame.make(f.origin.x + 1.5, f.origin.y + 2.0, f.size.width, f.size.height), text, HUD_FONT, HUD_TEXT_SH);
ctx.add_text(f, text, HUD_FONT, HUD_TEXT);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB