20 std::lock_guard<std::mutex> lock(
ttf_mutex);
29 std::lock_guard<std::mutex> lock(
ttf_mutex);
40 const auto begin = value.find_first_not_of(
" \t\r\n");
41 if (begin == std::string::npos) {
44 const auto end = value.find_last_not_of(
" \t\r\n");
45 return value.substr(begin, end - begin + 1);
50 const std::string trimmed =
trim_copy(value);
51 const char *begin = trimmed.data();
52 const char *end = trimmed.data() + trimmed.size();
53 const std::from_chars_result result = std::from_chars(begin, end, parsed, base);
54 if (result.ec != std::errc{} || result.ptr != end || parsed < 0 || parsed > 255) {
57 return static_cast<Uint8
>(parsed);
61 const std::string value =
trim_copy(spec);
66 if (value.front() ==
'#') {
67 if (value.size() != 7) {
76 return SDL_Color{*r, *g, *b, 255};
79 const std::size_t first = value.find(
',');
80 if (first == std::string::npos) {
83 const std::size_t second = value.find(
',', first + 1);
84 if (second == std::string::npos || value.find(
',', second + 1) != std::string::npos) {
94 return SDL_Color{*r, *g, *b, 255};
98 return [base_color](
int level) {
99 constexpr int max_level = 7;
100 const float t = std::clamp(
static_cast<float>(level) /
static_cast<float>(max_level), 0.0f, 1.0f);
101 const Uint8 r =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.r), std::pow(t, 1.0f)));
102 const Uint8 g =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.g), std::pow(t, 1.0f)));
103 const Uint8 b =
static_cast<Uint8
>(std::lerp(0.0f,
static_cast<float>(base_color.b), std::pow(t, 1.0f)));
104 const Uint8 a =
static_cast<Uint8
>(std::lerp(95.0f, 235.0f, t));
105 return SDL_Color{r, g, b, a};
110 return [base_color]() {
return SDL_Color{base_color.r, base_color.g, base_color.b, 255}; };
114 void Rain::SurfaceDeleter::operator()(SDL_Surface *
surface)
const {
120 void Rain::TtfDeleter::operator()(TTF_Font *font)
const {
121 if (font !=
nullptr) {
127 constexpr int max_level = 7;
128 const float t = std::clamp(
static_cast<float>(level) /
static_cast<float>(max_level), 0.0f, 1.0f);
129 const Uint8 r =
static_cast<Uint8
>(std::lerp(0.0f, 48.0f, std::pow(t, 2.9f)));
130 const Uint8 g =
static_cast<Uint8
>(std::lerp(42.0f, 205.0f, std::pow(t, 0.82f)));
131 const Uint8 b =
static_cast<Uint8
>(std::lerp(0.0f, 24.0f, std::pow(t, 3.1f)));
132 const Uint8 a =
static_cast<Uint8
>(std::lerp(95.0f, 235.0f, t));
133 return SDL_Color{r, g, b, a};
138 std::vector<Uint32>
Rain::full_symbol_set() {
return {0xFF66, 0xFF67, 0xFF68, 0xFF69, 0xFF6A, 0xFF6B, 0xFF6C, 0xFF6D, 0xFF6E, 0xFF6F, 0xFF71, 0xFF72, 0xFF73, 0xFF74, 0xFF75, 0xFF76, 0xFF77, 0xFF78, 0xFF79, 0xFF7A, 0xFF7B, 0xFF7C, 0xFF7D, 0xFF7E, 0xFF7F, 0xFF80, 0xFF81, 0xFF82, 0xFF83, 0xFF84, 0xFF85, 0xFF86, 0xFF87, 0xFF88, 0xFF89, 0xFF8A, 0xFF8B, 0xFF8C, 0xFF8D, 0xFF8E, 0xFF8F, 0xFF90, 0xFF91, 0xFF92, 0xFF93, 0xFF94, 0xFF95, 0xFF96, 0xFF97, 0xFF98, 0xFF99, 0xFF9A, 0xFF9B, 0xFF9C, 0xFF9D}; }
144 config.
font_path = asset_root +
"/data/NotoSansCJK-Bold.ttc";
155 Rain::Rain(
RainConfig rain_config, SurfaceMode mode) : config(std::move(rain_config)), rng(std::random_device{}()) {
157 throw mxvk::Exception(
"Matrix rain requires a font_path");
160 throw mxvk::Exception(
"Matrix rain requires at least one symbol");
163 config.level_color = &Rain::matrix_color;
166 config.head_color = &Rain::head_color;
168 if (!config.
color.empty()) {
169 const std::optional<SDL_Color> parsed_color = parse_color_spec(config.color);
171 throw mxvk::Exception(
"Invalid matrix rain color: " + config.color +
" (expected #RRGGBB or R,G,B)");
173 config.
level_color = make_tinted_level_color(*parsed_color);
174 config.
head_color = make_tinted_head_color(*parsed_color);
176 if (mode == SurfaceMode::explicit_default && config.surface_width <= 0 && config.surface_height <= 0) {
177 config.surface_width = default_surface_width;
178 config.surface_height = default_surface_height;
181 throw mxvk::Exception(
"Failed to initialize SDL_ttf: " + std::string(SDL_GetError()));
185 font.reset(TTF_OpenFont(config.font_path.c_str(), config.font_size));
188 ttf_acquired =
false;
189 throw mxvk::Exception(
"Failed to load matrix font: " + config.font_path +
": " + std::string(SDL_GetError()));
192 TTF_SetFontHinting(font.get(), TTF_HINTING_LIGHT);
194 if (config.surface_width > 0 && config.surface_height > 0) {
195 resize(config.surface_width, config.surface_height);
197 last_frame = std::chrono::steady_clock::now();
215 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_SPACE) {
228 canvas.reset(SDL_CreateSurface(
width,
height, SDL_PIXELFORMAT_RGBA32));
229 if (canvas ==
nullptr) {
230 throw mxvk::Exception(
"Failed to create matrix canvas: " + std::string(SDL_GetError()));
234 columns = std::max(1, (
width + cell_w - 1) / cell_w);
235 rows = std::max(1, (
height + cell_h - 1) / cell_h);
236 streams.assign(columns, {});
241 dt = std::clamp(dt, 0.0f, 1.0f / 15.0f);
242 if (canvas ==
nullptr || glyphs.empty()) {
249 if (rain_sprite ==
nullptr || canvas ==
nullptr) {
252 rain_sprite->updateTexture(canvas->pixels, canvas->w, canvas->h, canvas->pitch);
256 if (canvas ==
nullptr) {
259 render(canvas->w, canvas->h);
264 if (rain_sprite ==
nullptr || canvas ==
nullptr) {
276 last_frame = std::chrono::steady_clock::now();
280 const auto now = std::chrono::steady_clock::now();
281 float dt = std::chrono::duration<float>(now - last_frame).count();
289 const auto now = std::chrono::steady_clock::now();
290 float dt = std::chrono::duration<float>(now - last_frame).count();
294 render(render_width, render_height);
303 const void *
Rain::pixels()
const {
return canvas !=
nullptr ? canvas->pixels :
nullptr; }
305 int Rain::width()
const {
return canvas !=
nullptr ? canvas->w : 0; }
309 int Rain::pitch()
const {
return canvas !=
nullptr ? canvas->pitch : 0; }
311 void Rain::load_glyphs() {
312 const std::vector<Uint32> &symbols = config.
symbols;
316 glyphs.reserve(symbols.size());
317 for (
const Uint32 codepoint : symbols) {
319 glyph.codepoint = codepoint;
321 for (
int level = 0; level <= config.
glyph_levels; ++level) {
322 SDL_Surface *rendered = TTF_RenderGlyph_Blended(font.get(), codepoint, config.
level_color(level));
323 if (rendered ==
nullptr) {
326 SurfacePtr converted(SDL_ConvertSurface(rendered, SDL_PIXELFORMAT_RGBA32));
327 SDL_DestroySurface(rendered);
328 if (converted !=
nullptr) {
329 SDL_SetSurfaceBlendMode(converted.get(), SDL_BLENDMODE_BLEND);
330 max_glyph_w = std::max(max_glyph_w, converted->w);
331 max_glyph_h = std::max(max_glyph_h, converted->h);
332 glyph.levels.emplace_back(std::move(converted));
335 SDL_Surface *rendered_head = TTF_RenderGlyph_Blended(font.get(), codepoint, config.head_color());
336 if (rendered_head !=
nullptr) {
337 SurfacePtr converted(SDL_ConvertSurface(rendered_head, SDL_PIXELFORMAT_RGBA32));
338 SDL_DestroySurface(rendered_head);
339 if (converted !=
nullptr) {
340 SDL_SetSurfaceBlendMode(converted.get(), SDL_BLENDMODE_BLEND);
341 max_glyph_w = std::max(max_glyph_w, converted->w);
342 max_glyph_h = std::max(max_glyph_h, converted->h);
343 glyph.levels.emplace_back(std::move(converted));
346 if (glyph.levels.size() ==
static_cast<std::size_t
>(config.glyph_levels + 2)) {
347 glyphs.emplace_back(std::move(glyph));
351 if (glyphs.empty()) {
352 throw mxvk::Exception(
"Matrix demo could not render any glyphs from the bundled font");
355 cell_w = std::max(config.min_cell_width, max_glyph_w + config.cell_padding_width);
356 cell_h = std::max(config.min_cell_height, max_glyph_h + config.cell_padding_height);
359 void Rain::rebuild_for_extent(mxvk::VK_Window &window) {
361 const int window_width =
static_cast<int>(extent.width);
362 const int window_height =
static_cast<int>(extent.height);
363 if (window_width <= 0 || window_height <= 0) {
367 const int target_width = (config.surface_width > 0) ? config.surface_width : std::max(1, static_cast<int>(std::
lround(static_cast<float>(window_width) * config.surface_scale)));
368 const int target_height = (config.surface_height > 0) ? config.surface_height : std::max(1,
static_cast<int>(std::lround(
static_cast<float>(window_height) * config.surface_scale)));
370 const bool resized = (canvas ==
nullptr || canvas->w != target_width || canvas->h != target_height);
372 resize(target_width, target_height);
375 if (rain_sprite ==
nullptr) {
379 if (resized || streams.empty()) {
380 columns = std::max(1, (target_width + cell_w - 1) / cell_w);
381 rows = std::max(1, (target_height + cell_h - 1) / cell_h);
382 streams.assign(columns, {});
387 void Rain::randomize_streams() {
388 if (streams.empty() || glyphs.empty()) {
392 std::uniform_real_distribution<float> head_dist(-
static_cast<float>(rows) * config.initial_head_margin, 0.0f);
393 std::uniform_real_distribution<float> speed_dist(config.initial_speed_min, config.initial_speed_max);
394 std::uniform_int_distribution<int> length_dist(config.initial_length_min, config.initial_length_max);
395 std::uniform_int_distribution<int> glyph_dist(0,
static_cast<int>(glyphs.size() - 1));
396 std::uniform_int_distribution<int> shimmer_dist(0, 1200);
398 for (Stream &stream : streams) {
399 stream.head = head_dist(rng);
400 stream.speed = speed_dist(rng);
401 stream.length = std::min(std::max(config.initial_min_tail_rows, rows), length_dist(rng));
402 stream.glyphOffset = glyph_dist(rng);
403 stream.shimmer = shimmer_dist(rng);
407 void Rain::update_rain(
float dt) {
410 for (
int column = 0; column < columns; ++column) {
411 Stream &stream = streams[column];
412 stream.head += stream.speed * dt;
413 if (stream.head -
static_cast<float>(stream.length) >
static_cast<float>(rows + config.overscan_rows)) {
414 reset_stream(stream);
416 draw_stream(column, stream);
421 void Rain::reset_stream(Stream &stream) {
422 std::uniform_real_distribution<float> head_dist(-
static_cast<float>(rows) * config.reset_head_margin, -1.0f);
423 std::uniform_real_distribution<float> speed_dist(config.reset_speed_min, config.reset_speed_max);
424 std::uniform_int_distribution<int> length_dist(config.reset_length_min, config.reset_length_max);
425 std::uniform_int_distribution<int> glyph_dist(0,
static_cast<int>(glyphs.size() - 1));
426 std::uniform_int_distribution<int> shimmer_dist(0, 1200);
427 stream.head = head_dist(rng);
428 stream.speed = speed_dist(rng);
429 stream.length = std::min(std::max(config.reset_min_tail_rows, rows + rows / 4), length_dist(rng));
430 stream.glyphOffset = glyph_dist(rng);
431 stream.shimmer = shimmer_dist(rng);
434 void Rain::fade_canvas(
float dt) {
435 const float fade = std::pow(config.fade_base, dt * config.fade_exponent);
436 const float opacity_scale = opacity;
437 if (!SDL_LockSurface(canvas.get())) {
440 auto *
pixels =
static_cast<Uint8 *
>(canvas->pixels);
441 for (
int y = 0; y < canvas->h; ++y) {
442 Uint8 *row =
pixels + y * canvas->pitch;
443 for (
int x = 0; x < canvas->w; ++x) {
444 Uint8 *p = row + x * 4;
445 p[0] =
static_cast<Uint8
>(
static_cast<float>(p[0]) * fade * config.fade_red_scale);
446 p[1] =
static_cast<Uint8
>(
static_cast<float>(p[1]) * fade * config.fade_green_scale);
447 p[2] =
static_cast<Uint8
>(
static_cast<float>(p[2]) * fade * config.fade_blue_scale);
448 p[3] =
static_cast<Uint8
>(
static_cast<float>(p[3]) * fade * opacity_scale);
451 SDL_UnlockSurface(canvas.get());
454 void Rain::draw_stream(
int column,
const Stream &stream) {
455 const int head_row =
static_cast<int>(std::floor(stream.head));
456 const int x = column * cell_w;
457 for (
int tail = stream.length; tail >= 0; --tail) {
458 const int row = head_row - tail;
459 if (row < -1 || row >= rows) {
462 const float age =
static_cast<float>(tail) /
static_cast<float>(std::max(1, stream.length));
463 int level = config.glyph_levels -
static_cast<int>(std::round(age *
static_cast<float>(config.glyph_levels + 1)));
465 level = config.glyph_levels + 1;
466 }
else if (tail <= config.near_head_tail_threshold) {
467 level = config.glyph_levels;
469 level = std::clamp(level, 0, config.glyph_levels + 1);
470 const int glyph_index = (stream.glyphOffset + row * 17 + column * 11 + stream.shimmer + frame_counter / 3 + tail * 5) %
static_cast<int>(glyphs.size());
471 const Glyph &glyph = glyphs[glyph_index < 0 ? glyph_index + static_cast<int>(glyphs.size()) : glyph_index];
472 if (glyph.levels.empty()) {
475 const SDL_Rect clear_rect{x, row * cell_h, cell_w, cell_h};
476 SDL_FillSurfaceRect(canvas.get(), &clear_rect, SDL_MapSurfaceRGBA(canvas.get(), 0, 0, 0, 0));
477 SDL_Surface *
surface = glyph.levels[level].get();
478 Uint8 previous_alpha = 255;
479 SDL_GetSurfaceAlphaMod(
surface, &previous_alpha);
480 const Uint8 opacity_alpha =
static_cast<Uint8
>(std::lround(std::clamp(opacity, 0.0f, 1.0f) * 255.0f));
482 const int glyph_x = x + (cell_w - glyph.levels[level]->w) / 2;
483 const int glyph_y = row * cell_h;
484 const int glow_alpha =
static_cast<int>(std::lround(((tail == 0) ? config.head_glow_alpha : (tail <= config.near_head_tail_threshold ? config.near_head_glow_alpha : config.trail_glow_alpha)) * opacity));
486 SDL_SetSurfaceAlphaMod(
surface,
static_cast<Uint8
>((glow_alpha * opacity_alpha) / 255));
487 SDL_Rect glow_dst{glyph_x - 1, glyph_y, glyph.levels[level]->w, glyph.levels[level]->h};
488 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
489 glow_dst.x = glyph_x + 1;
490 glow_dst.y = glyph_y;
491 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
492 glow_dst.x = glyph_x;
493 glow_dst.y = glyph_y + 1;
494 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &glow_dst);
496 SDL_SetSurfaceAlphaMod(
surface,
static_cast<Uint8
>((
static_cast<int>(previous_alpha) * opacity_alpha) / 255));
497 SDL_Rect dst{glyph_x, glyph_y, glyph.levels[level]->w, glyph.levels[level]->h};
498 SDL_BlitSurface(
surface,
nullptr, canvas.get(), &dst);
499 SDL_SetSurfaceAlphaMod(
surface, previous_alpha);
503 void Rain::clear_canvas() {
504 if (canvas !=
nullptr) {
505 SDL_FillSurfaceRect(canvas.get(),
nullptr, SDL_MapSurfaceRGBA(canvas.get(), 0, 0, 0, 0));