tune(playfield): ona diagonal amb easing i cap brillant

This commit is contained in:
2026-05-21 22:06:02 +02:00
parent 07985228b2
commit 5c8a583e24
3 changed files with 76 additions and 42 deletions
+6 -2
View File
@@ -16,9 +16,13 @@ namespace Defaults::Playfield {
// Animació de creació amb timer intern del Playfield. // Animació de creació amb timer intern del Playfield.
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en // L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
// LINE_GROWTH_DURATION_S; l'spawn de les línies es distribueix perquè // LINE_GROWTH_DURATION_S; els spawns es distribueixen amb sweep des del
// l'última acabe just al final de TOTAL_ANIMATION_DURATION_S. // centre perquè verticals i horitzontals propaguen cap als extrems.
constexpr float LINE_GROWTH_DURATION_S = 0.4F; constexpr float LINE_GROWTH_DURATION_S = 0.4F;
constexpr float TOTAL_ANIMATION_DURATION_S = 3.0F; // = Defaults::Game::INIT_HUD_DURATION constexpr float TOTAL_ANIMATION_DURATION_S = 3.0F; // = Defaults::Game::INIT_HUD_DURATION
// Cap brillant de la línia mentre creix (extrem que avança).
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
} // namespace Defaults::Playfield } // namespace Defaults::Playfield
+64 -32
View File
@@ -4,24 +4,27 @@
#include "core/graphics/playfield.hpp" #include "core/graphics/playfield.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cstdlib>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
namespace Graphics { namespace Graphics {
namespace {
// Easing cubic-out: t → 1 - (1-t)^3. Decelera prop del final.
auto easeOutCubic(float t) -> float {
const float INV = 1.0F - t;
return 1.0F - (INV * INV * INV);
}
} // namespace
Playfield::Playfield(Rendering::Renderer* renderer) Playfield::Playfield(Rendering::Renderer* renderer)
: renderer_(renderer) { : renderer_(renderer) {
buildLines(); buildLines();
total_slots_ = static_cast<int>(lines_.size());
// Distribuïm els spawns de manera que la última línia acabe just a TOTAL_ANIMATION_DURATION_S.
// last_line_start = (N-1) * spawn_interval
// last_line_end = last_line_start + LINE_GROWTH_DURATION_S = TOTAL_ANIMATION_DURATION_S
if (total_slots_ > 1) {
spawn_interval_s_ =
(Defaults::Playfield::TOTAL_ANIMATION_DURATION_S - Defaults::Playfield::LINE_GROWTH_DURATION_S) / static_cast<float>(total_slots_ - 1);
}
} }
void Playfield::update(float delta_time) { void Playfield::update(float delta_time) {
@@ -40,8 +43,7 @@ namespace Graphics {
std::vector<Line> verticals; std::vector<Line> verticals;
std::vector<Line> horizontals; std::vector<Line> horizontals;
// Verticals: posicions i ∈ [1, SUB_VERTS-1]. Si i % SUBDIVISIONS == 0 → línia // Verticals: posicions i ∈ [1, SUB_VERTS-1].
// de la graella principal; si no, sub-graella.
for (int i = 1; i < SUB_VERTS; i++) { for (int i = 1; i < SUB_VERTS; i++) {
const float X = zona.x + (static_cast<float>(i) * SUB_W); const float X = zona.x + (static_cast<float>(i) * SUB_W);
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
@@ -52,10 +54,10 @@ namespace Graphics {
.start = {.x = X, .y = zona.y}, .start = {.x = X, .y = zona.y},
.end = {.x = X, .y = zona.y + zona.h}, .end = {.x = X, .y = zona.y + zona.h},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.slot = 0}); .spawn_time_s = 0.0F});
} }
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. Mateix criteri main/sub. // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
for (int j = 1; j < SUB_HORIZ; j++) { for (int j = 1; j < SUB_HORIZ; j++) {
const float Y = zona.y + (static_cast<float>(j) * SUB_H); const float Y = zona.y + (static_cast<float>(j) * SUB_H);
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
@@ -66,46 +68,76 @@ namespace Graphics {
.start = {.x = zona.x, .y = Y}, .start = {.x = zona.x, .y = Y},
.end = {.x = zona.x + zona.w, .y = Y}, .end = {.x = zona.x + zona.w, .y = Y},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.slot = 0}); .spawn_time_s = 0.0F});
} }
// Verticals ja venen ordenats per x ascendent (loop sobre i). Assignem slots 0..N-1. // Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
// Horitzontals ja venen ordenats per y ascendent. Assignem slots N..total-1. // propaguen cap a la dreta/inferior, en paral·lel. Verticals i
// horitzontals comparteixen la finestra temporal així el front arriba
// a la cantonada inferior-dreta alhora.
const float SPAWN_WINDOW =
Defaults::Playfield::TOTAL_ANIMATION_DURATION_S - Defaults::Playfield::LINE_GROWTH_DURATION_S;
const int NUM_V = static_cast<int>(verticals.size());
const int NUM_H = static_cast<int>(horizontals.size());
const float INTERVAL_V = (NUM_V > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_V - 1) : 0.0F;
const float INTERVAL_H = (NUM_H > 1) ? SPAWN_WINDOW / static_cast<float>(NUM_H - 1) : 0.0F;
lines_.clear(); lines_.clear();
lines_.reserve(verticals.size() + horizontals.size()); lines_.reserve(verticals.size() + horizontals.size());
int slot = 0; for (int i = 0; i < NUM_V; i++) {
for (auto& line : verticals) { verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
line.slot = slot++; lines_.push_back(verticals[i]);
lines_.push_back(line);
} }
for (auto& line : horizontals) { for (int i = 0; i < NUM_H; i++) {
line.slot = slot++; horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
lines_.push_back(line); lines_.push_back(horizontals[i]);
} }
} }
auto Playfield::computeLineProgress(int slot) const -> float { auto Playfield::computeLineProgress(const Line& line) const -> float {
const float LINE_START = static_cast<float>(slot) * spawn_interval_s_; const float LINE_ELAPSED = elapsed_s_ - line.spawn_time_s;
const float LINE_ELAPSED = elapsed_s_ - LINE_START;
return std::clamp(LINE_ELAPSED / Defaults::Playfield::LINE_GROWTH_DURATION_S, 0.0F, 1.0F); return std::clamp(LINE_ELAPSED / Defaults::Playfield::LINE_GROWTH_DURATION_S, 0.0F, 1.0F);
} }
void Playfield::draw() const { void Playfield::draw() const {
for (const auto& line : lines_) { for (const auto& line : lines_) {
const float P = computeLineProgress(line.slot); const float RAW_P = computeLineProgress(line);
if (P <= 0.0F) { if (RAW_P <= 0.0F) {
continue; continue;
} }
const float END_X = line.start.x + ((line.end.x - line.start.x) * P); const float P = easeOutCubic(RAW_P);
const float END_Y = line.start.y + ((line.end.y - line.start.y) * P);
const float DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y;
const float CURRENT_X = line.start.x + (DX * P);
const float CURRENT_Y = line.start.y + (DY * P);
// Tram base (brillo de la línia).
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(line.start.x), static_cast<int>(line.start.x),
static_cast<int>(line.start.y), static_cast<int>(line.start.y),
static_cast<int>(END_X), static_cast<int>(CURRENT_X),
static_cast<int>(END_Y), static_cast<int>(CURRENT_Y),
line.brightness); line.brightness);
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant.
if (P < 1.0F) {
const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
if (LENGTH > 0.0F) {
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH));
const float HEAD_X = line.start.x + (DX * HEAD_T);
const float HEAD_Y = line.start.y + (DY * HEAD_T);
Rendering::linea(
renderer_,
static_cast<int>(HEAD_X),
static_cast<int>(HEAD_Y),
static_cast<int>(CURRENT_X),
static_cast<int>(CURRENT_Y),
Defaults::Playfield::HEAD_BRIGHTNESS);
}
}
} }
} }
+6 -8
View File
@@ -30,19 +30,17 @@ namespace Graphics {
private: private:
struct Line { struct Line {
Vec2 start; // top (verticals) o left (horitzontals) Vec2 start; // top (verticals) o left (horitzontals)
Vec2 end; // bottom (verticals) o right (horitzontals) Vec2 end; // bottom (verticals) o right (horitzontals)
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
int slot; // posició a la timeline 0..total_slots-1 float spawn_time_s; // moment de naixement (verticals i horitzontals tenen ritmes independents)
}; };
void buildLines(); void buildLines();
[[nodiscard]] auto computeLineProgress(int slot) const -> float; [[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
Rendering::Renderer* renderer_; Rendering::Renderer* renderer_;
std::vector<Line> lines_; // verticals primer (ordenats per x), després horitzontals (ordenats per y) std::vector<Line> lines_;
int total_slots_{0};
float spawn_interval_s_{0.0F}; // calculat a buildLines() perquè la última línia acabi a TOTAL_ANIMATION_DURATION_S
float elapsed_s_{0.0F}; float elapsed_s_{0.0F};
}; };