Mechanical sweep of all .sx sources, plan docs, and tests/expected snapshots for the sx language rename (s8/s16/s32/s64 -> i8/i16/i32/i64). Verified: tools/run_tests.sh 23/23. Note: the ios-sim build has 2 pre-existing 'restart' dot-call errors from the sx opt-in UFCS change (sx a47ea14) — independent of this rename (present pre-sweep); migrated in the follow-up commit.
154 lines
7.0 KiB
Plaintext
154 lines
7.0 KiB
Plaintext
// Per-gem animation set (P6.3) — a PURELY VISUAL pose each gem sprite is drawn
|
|
// with: a calm always-on idle breath, a pop on selection, and a squash-bounce on
|
|
// landing. Everything here is a pure function of an animation clock and the cell;
|
|
// it never reads or writes the model, so a gem's idle bob/scale cannot change its
|
|
// logical cell or break hit-testing (which stays on the grid in board_layout.sx).
|
|
//
|
|
// Determinism: the idle is always-on, so a live screenshot would be time-
|
|
// dependent. `GemMotion.clock` is the single animation time; capture mode
|
|
// (M3TE_ANIM_TIME, read in main) freezes it at a chosen phase so the board can be
|
|
// screenshotted reproducibly. Every effect is built so that at clock t==0 the pose
|
|
// is exactly the resting sprite — so the pre-P6.3 goldens reproduce at t==0.
|
|
#import "modules/std.sx";
|
|
#import "modules/math";
|
|
#import "board.sx";
|
|
#import "board_anim.sx";
|
|
|
|
// A gem's draw transform about its cell centre. scale_x/scale_y scale the sprite
|
|
// (1.0 == the normal cell-fill size) and dx/dy nudge it in CELL units. The resting
|
|
// pose is all-ones / all-zeros, which draws identically to the static sprite.
|
|
GemPose :: struct {
|
|
scale_x: f32;
|
|
scale_y: f32;
|
|
dx: f32;
|
|
dy: f32;
|
|
}
|
|
|
|
gem_pose_rest :: () -> GemPose {
|
|
GemPose.{ scale_x = 1.0, scale_y = 1.0, dx = 0.0, dy = 0.0 }
|
|
}
|
|
|
|
// --- Idle breath -------------------------------------------------------------
|
|
// A gentle ~1s pulse + vertical bob, ramped in from rest so a freshly-shown board
|
|
// (and the t==0 capture) starts on the resting pose. A per-gem phase offset keeps
|
|
// the board from pulsing in lockstep without ever desyncing the t==0 rest.
|
|
IDLE_PERIOD :f32: 1.05; // seconds per breath
|
|
IDLE_SCALE_A :f32: 0.035; // +/- uniform scale amplitude
|
|
IDLE_BOB_A :f32: 0.024; // vertical bob amplitude (cell units)
|
|
IDLE_RAMP :f32: 0.45; // seconds to ease the idle up from full rest
|
|
|
|
// Smooth per-cell phase: a diagonal gradient wrapped into one breath period.
|
|
gem_idle_phase :: (col: i64, row: i64) -> f32 {
|
|
cast(f32) ((col * 2 + row * 3) % 8) / 8.0 * TAU
|
|
}
|
|
|
|
idle_pose :: (t: f32, col: i64, row: i64) -> GemPose {
|
|
ramp := clamp(t / IDLE_RAMP, 0.0, 1.0);
|
|
w := t / IDLE_PERIOD * TAU + gem_idle_phase(col, row);
|
|
s := IDLE_SCALE_A * sin(w) * ramp;
|
|
bob := IDLE_BOB_A * cos(w) * ramp;
|
|
GemPose.{ scale_x = 1.0 + s, scale_y = 1.0 + s, dx = 0.0, dy = bob }
|
|
}
|
|
|
|
// --- Selection pop -----------------------------------------------------------
|
|
// A quick scale-up that settles back: a single hump over the window so the tapped
|
|
// gem "pops" then relaxes (the highlight overlay still draws on top of this).
|
|
SELECT_DUR :f32: 0.34;
|
|
SELECT_POP_A :f32: 0.15;
|
|
|
|
select_pop_scale :: (ts: f32) -> f32 {
|
|
if ts <= 0.0 or ts >= SELECT_DUR { return 1.0; }
|
|
1.0 + SELECT_POP_A * sin(PI * ts / SELECT_DUR)
|
|
}
|
|
|
|
// --- Landing squash-bounce ---------------------------------------------------
|
|
// A damped wobble on settle: the gem flattens wide-and-short on impact, then a
|
|
// couple of decaying overshoots. 0 at tl==0 and again past the window (rest).
|
|
// The shape IS P15.1's `squash_envelope` (board_anim) over the normalized window
|
|
// tl/LAND_DUR, scaled to a tasteful amplitude — so the per-round fall bounce and
|
|
// this settle bounce are the EXACT same envelope (P17.3 drives both through here).
|
|
LAND_DUR :f32: 0.42;
|
|
LAND_SQUASH_A :f32: 0.18; // peak ~13% wide-and-short — reads on landing, still tasteful
|
|
|
|
land_squash :: (tl: f32) -> f32 {
|
|
if tl <= 0.0 or tl >= LAND_DUR { return 0.0; }
|
|
LAND_SQUASH_A * squash_envelope(tl / LAND_DUR)
|
|
}
|
|
|
|
// --- Clear pop ---------------------------------------------------------------
|
|
// 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_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 - ease_in_quad(u))
|
|
}
|
|
|
|
// Live per-gem animation state, heap-allocated (like BoardAnim/BoardFx) so it
|
|
// survives BoardView's per-frame rebuild. `clock` is the single animation time:
|
|
// the frame loop advances it by delta_time, or capture mode pins it. `land_at`
|
|
// records, per cell, the clock value when that cell last received a gem so only
|
|
// the cells that actually moved bounce.
|
|
GemMotion :: struct {
|
|
clock: f32;
|
|
pinned: bool;
|
|
land_at: [BOARD_CELLS]f32;
|
|
|
|
init :: (self: *GemMotion) {
|
|
self.clock = 0.0;
|
|
self.pinned = false;
|
|
self.reset_landings();
|
|
}
|
|
|
|
// Drop every landing stamp back to the never-landed sentinel so no cell
|
|
// carries a squash-bounce. `restart` calls this so a reseeded board starts at
|
|
// its resting pose instead of replaying the prior move's landing wobble; the
|
|
// idle clock keeps running, so the always-on idle simply resumes from rest.
|
|
reset_landings :: (self: *GemMotion) {
|
|
for 0..BOARD_CELLS (i) { self.land_at[i] = -1000.0; }
|
|
}
|
|
|
|
stamp_land :: (self: *GemMotion, i: i64) {
|
|
self.stamp_land_at(i, self.clock);
|
|
}
|
|
|
|
// Record cell `i`'s landing at an explicit clock value, so the settle hand-off
|
|
// can BACK-DATE the stamp to when the gem actually touched down mid-fall (each
|
|
// column lands at a staggered instant): land_squash then resumes the per-round
|
|
// bounce exactly where render_fall left it, with no double-pop at the seam.
|
|
stamp_land_at :: (self: *GemMotion, i: i64, at: f32) {
|
|
self.land_at[i] = at;
|
|
}
|
|
|
|
land_local :: (self: *GemMotion, i: i64) -> f32 {
|
|
self.clock - self.land_at[i]
|
|
}
|
|
}
|