P18.1: organic combine — anticipation pop on match clear (sx, iOS sim)

Sharpen clear_pop_scale from a plain pop-then-shrink into a candy pop in
three beats over its local 0..1: a tiny anticipation squash dip (~8% below
rest), a snappy overshoot to ~1.40x via P15.1's ease_out_back, then an
accelerating collapse to nothing (ease_in_quad). Endpoints stay locked
(t=0 -> 1.0 rest, t=1 -> 0.0 gone), so the seam to the model board is clean
and M3TE_ANIM_TIME=0 still reproduces the rest board; the particle burst /
score popup compose on top unchanged. render_clear and CLEAR_ANIM_DUR are
untouched, so cascade-cue timing snapshots don't churn and the model is
unchanged.

tests/gem_pose.sx now pins the new envelope (locked rest endpoints, the
anticipation dip, the overshoot, the monotonic post-peak collapse) with its
expected snapshot updated.

goldens/p18_pop.png: new mid-clear pop golden at the overshoot peak
(M3TE_FX=3 M3TE_ANIM_TIME=0.21). goldens/p6_fx_match.png refreshed for the
new pop shape (same scene at its documented 0.22). README documents P18.1.
This commit is contained in:
swipelab
2026-06-06 13:01:37 +03:00
parent 0a90dfff19
commit 54a7eba432
6 changed files with 78 additions and 12 deletions

View File

@@ -76,22 +76,39 @@ land_squash :: (tl: f32) -> f32 {
}
// --- Clear pop ---------------------------------------------------------------
// The matched-gem clear: a snappy outward pop then a collapse to nothing over its
// local 0..1, so the clear reads as a satisfying candy pop rather than a plain
// shrink. A fast rise to a bigger overshoot makes the snap read; the soft
// particle burst / score popup (board_fx.sx) compose on top.
CLEAR_POP_A :f32: 0.34; // overshoot height above resting scale
CLEAR_POP_RISE :f32: 0.18; // fraction of the window spent rising to the peak
// The matched-gem clear, shaped as a candy pop in three beats over its local
// 0..1: a tiny anticipation squash dip (a "gather" just below rest), a snappy
// overshoot up to the peak via P15.1's ease_out_back, then an accelerating
// collapse to nothing. Endpoints are LOCKED — t==0 -> 1.0 (rest) and t==1 -> 0.0
// (gone) — so the seam to the model board stays clean; the soft particle burst /
// score popup (board_fx.sx) compose on top.
CLEAR_DIP_T :f32: 0.16; // fraction of the window spent on the anticipation dip
CLEAR_DIP_A :f32: 0.08; // how far the gem compresses below rest before popping
CLEAR_POP_RISE :f32: 0.52; // window fraction at which the overshoot peak is reached
CLEAR_POP_A :f32: 0.36; // overshoot height above resting scale
clear_pop_scale :: (t: f32) -> f32 {
if t <= 0.0 { return 1.0; }
if t >= 1.0 { return 0.0; }
if t < CLEAR_POP_RISE {
return 1.0 + CLEAR_POP_A * (t / CLEAR_POP_RISE);
if t < CLEAR_DIP_T {
// Anticipation gather: sin(PI*u) is 0 at both ends, so t==0 stays exactly
// at rest and the dip hands off to the rise at rest — a brief squash, not
// a step.
u := t / CLEAR_DIP_T;
return 1.0 - CLEAR_DIP_A * sin(PI * u);
}
if t < CLEAR_POP_RISE {
// Snap up to the peak: ease_out_back rises from rest, shoots a touch past
// 1+A, then eases back to exactly 1+A at the seam (its locked f(1)=1), so
// the maximum is a single clean overshoot with no second reversal.
u := (t - CLEAR_DIP_T) / (CLEAR_POP_RISE - CLEAR_DIP_T);
return 1.0 + CLEAR_POP_A * ease_out_back(u);
}
// Collapse to nothing: accelerate the shrink from the peak so the gem vanishes
// as the burst takes over. ease_in_quad pins the seam at the peak and t==1 at 0.
peak := 1.0 + CLEAR_POP_A;
u := (t - CLEAR_POP_RISE) / (1.0 - CLEAR_POP_RISE);
peak * (1.0 - u * u)
peak * (1.0 - ease_in_quad(u))
}
// Live per-gem animation state, heap-allocated (like BoardAnim/BoardFx) so it