P11.3: glossier candy gem & selection feel (sx / iOS)

Make the selection highlight read as glossy candy without new art and
without disturbing the idle-rest invariant. board_view's render_selection
layers a soft outward glow (two concentric stroked rings — the renderer has
no blur), a warm wash, a bright rim doubled by a thin inner highlight for a
glassy edge, and a wet sheen that rides the selected gem's live pose. Every
layer is a rect/overlay (issue 0002 forbids a draw-time gem-texture tint).

The gloss is selection-only: render_selection runs solely when a cell is
selected, so the resting board (no selection) is byte-identical to before
and the t==0 idle pose stays exactly the static sprite (locked by
tests/gem_pose). The selection-pop motion still comes from gem_anim; no
board / score / move state changes and input stays gated by BoardAnim.active.

Updated goldens/p6_select.png; README documents the P11.3 selection gloss
and its reproduce commands (reusing the P6.3 M3TE_SELECT + M3TE_ANIM_TIME
hooks).
This commit is contained in:
swipelab
2026-06-05 21:15:44 +03:00
parent a8629c378b
commit cd8667d170
3 changed files with 73 additions and 11 deletions

View File

@@ -172,3 +172,28 @@ env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=3.0 \
The combo emphasis is purely visual and self-pruning: it never gates input
(`BoardAnim.active` owns gating) and never touches board / score / move state.
### Glossier gem & selection feel (P11.3)
The selection highlight (`board_view.sx` `render_selection`) is a candy-glossier
overlay: two concentric stroked rings fake a soft outward glow, a warm wash tints
the cell, a bright rim is doubled by a thin inner highlight for a glassy edge, and
a wet sheen rides the selected gem's live pose. The engine can't tint a texture at
draw time (issue 0002), so every layer is a rect/overlay — never a gem-texture
tint. The selection-pop motion still comes from `gem_anim`, so the **t==0 idle
pose is byte-identical to the static sprite** (locked by `tests/gem_pose.sx`); the
gloss is selection-only, so the resting board (no selection) is unchanged.
Capture it with the same P6.3 hooks — no new env var:
```bash
# Glossy candy selection on cell (3,3), pinned mid-pop: goldens/p6_select.png
env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
# Same selection at exact rest (no pop) — isolates the overlay:
env SIMCTL_CHILD_M3TE_ANIM_TIME=0 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
```
The selection gloss is purely visual: it never gates input (`BoardAnim.active`
owns gating) and never touches board / score / move state.

View File

@@ -33,11 +33,18 @@ GEM_FILL_FRAC :f32: 0.84;
// actually sizes it; vertical centering inside the safe area is unchanged.
BOARD_INSET_X :f32: 16.0;
// Selection overlay: a translucent warm fill plus a bright opaque rim around the
// chosen cell. `add_stroked_rect` draws the rim in its FILL color (the renderer
// ignores the separate stroke color), so SELECT_RIM is passed as the fill.
SELECT_FILL :: Color.{ r = 255, g = 240, b = 120, a = 70 };
SELECT_RIM :: Color.{ r = 255, g = 228, b = 60, a = 255 };
// Selection overlay (P11.3): a soft candy "glow" halo, a warm wash over the cell,
// a bright rim topped by a glossy inner highlight, and a wet sheen on the chosen
// gem. `add_stroked_rect` paints the border band in its FILL colour (the shader
// ignores the separate stroke colour), so each ring colour is passed as the fill.
// The engine can't tint/fade a texture at draw time (issue 0002), so every layer
// here is a rect/overlay — never a gem-texture tint.
SELECT_GLOW_OUT :: Color.{ r = 255, g = 232, b = 140, a = 30 }; // wide faint outer bloom
SELECT_GLOW_IN :: Color.{ r = 255, g = 238, b = 150, a = 70 }; // brighter near-edge halo
SELECT_FILL :: Color.{ r = 255, g = 244, b = 150, a = 80 }; // warm wash over the gem
SELECT_RIM :: Color.{ r = 255, g = 234, b = 92, a = 255 }; // bright candy rim
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).
@@ -302,6 +309,39 @@ BoardView :: struct {
}
}
// Selection emphasis (P11.3): a glossier candy highlight on the chosen cell.
// Two concentric stroked rings fake a soft outward glow (the renderer has no
// blur), a warm wash tints the cell, a bright rim is doubled by a thin inner
// highlight for a glassy edge, and a wet sheen rides the selected gem's live
// pose. All rect/overlay layers (issue 0002 forbids a draw-time gem tint); the
// selection-pop motion still comes from gem_anim, so the t==0 idle pose is
// untouched.
render_selection :: (self: *BoardView, ctx: *RenderContext, dim: f32) {
cs := self.layout.cell_size;
cf := self.layout.cell_frame(self.sel.cell.col, self.sel.cell.row);
// Glow halo: rings just outside the cell edge, brighter nearer the rim, so
// the falloff reads as a soft bloom without tinting the gem interior.
ctx.add_stroked_rect(cf.expand(cs * 0.16), SELECT_GLOW_OUT, SELECT_GLOW_OUT, cs * 0.16, cs * 0.30);
ctx.add_stroked_rect(cf.expand(cs * 0.07), SELECT_GLOW_IN, SELECT_GLOW_IN, cs * 0.08, cs * 0.21);
// Warm wash + bright rim + a thin glossy highlight ring just inside the rim.
ctx.add_rounded_rect(cf, SELECT_FILL, cs * 0.14);
rim_w := max(2.0, cs * 0.06);
ctx.add_stroked_rect(cf, SELECT_RIM, SELECT_RIM, rim_w, cs * 0.14);
hi_w := max(1.0, cs * 0.022);
ctx.add_stroked_rect(cf.expand(0.0 - rim_w), SELECT_RIM_HI, SELECT_RIM_HI, hi_w, cs * 0.11);
// Wet sheen on the selected gem: a bright pill in its upper third, sized to
// the gem's live pose so it tracks the selection pop.
pose := self.gem_pose_at(self.sel.cell.col, self.sel.cell.row);
gf := self.gem_pose_frame(self.sel.cell.col, self.sel.cell.row, dim, pose);
gw := gf.size.width;
gh := gf.size.height;
gloss := Frame.make(gf.origin.x + gw * 0.22, gf.origin.y + gh * 0.13, gw * 0.40, gh * 0.22);
ctx.add_rounded_rect(gloss, SELECT_GLOSS, gh * 0.12);
}
// Play the active slice of the move timeline. Gem motion is clipped to the
// grid so refilled gems slide in from behind the top edge rather than
// overlapping the HUD band above the board.
@@ -582,13 +622,10 @@ impl View for BoardView {
}
}
// 3. Selection overlay on the chosen cell: a translucent fill under a
// bright rim, drawn over the whole grid so it reads as a highlight.
// 3. Selection emphasis on the chosen cell: a soft candy glow halo under a
// warm wash, a bright glossy rim, and a wet sheen on the popped gem.
if self.sel != null and self.sel.active {
cf := self.layout.cell_frame(self.sel.cell.col, self.sel.cell.row);
ctx.add_rect(cf, SELECT_FILL);
rim_w := max(2.0, self.layout.cell_size * 0.06);
ctx.add_stroked_rect(cf, SELECT_RIM, SELECT_RIM, rim_w, self.layout.cell_size * 0.14);
self.render_selection(ctx, gem_dim);
}
// 4. HUD card with score + remaining moves, in the band above the grid.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB